Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 62 additions & 7 deletions ck-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ struct Cli {
#[arg(long = "status-verbose", help = "Show detailed index statistics")]
status_verbose: bool,

#[arg(long = "status-json", help = "Output index status as JSON")]
status_json: bool,

#[arg(
long = "inspect",
help = "Show detailed metadata for a specific file (chunks, embeddings, tree-sitter parsing info)"
Expand Down Expand Up @@ -1136,21 +1139,73 @@ async fn run_cli_mode(cli: Cli) -> Result<()> {
return Ok(());
}

if cli.status || cli.status_verbose {
// Handle --status and --status-verbose flags
if cli.status || cli.status_verbose || cli.status_json {
// Handle --status, --status-verbose, and --status-json flags
let status_path = cli
.files
.first()
.cloned()
.unwrap_or_else(|| PathBuf::from("."));
let verbose = cli.status_verbose;

status.section_header("Index Status");
let check_spinner = status.create_spinner("Reading index...");
let stats = ck_index::get_index_stats(&status_path)?;
status.finish_progress(check_spinner, "Status retrieved");
let stats = if cli.status_json {
// For JSON output, skip spinner and human-readable messages
ck_index::get_index_stats(&status_path)?
} else {
status.section_header("Index Status");
let check_spinner = status.create_spinner("Reading index...");
let stats = ck_index::get_index_stats(&status_path)?;
status.finish_progress(check_spinner, "Status retrieved");
stats
};

if cli.status_json {
// Output JSON format
let mut json_output = serde_json::json!({
"path": status_path.to_string_lossy(),
"index_exists": stats.total_files > 0,
"total_files": stats.total_files,
"total_chunks": stats.total_chunks,
"embedded_chunks": stats.embedded_chunks,
"total_size_bytes": stats.total_size_bytes,
"index_size_bytes": stats.index_size_bytes,
"index_created": stats.index_created,
"index_updated": stats.index_updated,
});

// Add model information if available
let manifest_path = status_path.join(".ck").join("manifest.json");
if let Ok(data) = std::fs::read(&manifest_path)
&& let Ok(manifest) = serde_json::from_slice::<ck_index::IndexManifest>(&data)
&& let Some(model_name) = manifest.embedding_model
{
let registry = ck_models::ModelRegistry::default();
let alias = registry
.models
.iter()
.find(|(_, config)| config.name == model_name)
.map(|(alias, _)| alias.clone())
.unwrap_or_else(|| model_name.clone());
let dims = manifest
.embedding_dimensions
.or_else(|| {
registry
.models
.iter()
.find(|(_, config)| config.name == model_name)
.map(|(_, config)| config.dimensions)
})
.unwrap_or(0);

json_output["model"] = serde_json::json!({
"name": model_name,
"alias": alias,
"dimensions": dims,
});
}

if stats.total_files == 0 {
println!("{}", serde_json::to_string_pretty(&json_output)?);
} else if stats.total_files == 0 {
status.warn(&format!("No index found at {}", status_path.display()));
status.info("Run 'ck --index .' to create an index");
} else {
Expand Down
33 changes: 33 additions & 0 deletions ck-cli/src/mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1690,8 +1690,41 @@ impl CkMcpServer {
} else if let Ok(index_stats) = ck_index::get_index_stats(&path_buf) {
index_info["total_files"] = json!(index_stats.total_files);
index_info["total_chunks"] = json!(index_stats.total_chunks);
index_info["embedded_chunks"] = json!(index_stats.embedded_chunks);
index_info["total_size_bytes"] = json!(index_stats.total_size_bytes);
index_info["cache_hit"] = json!(false);

// Add model information if available
let manifest_path = path_buf.join(".ck").join("manifest.json");
if let Ok(data) = std::fs::read(&manifest_path)
&& let Ok(manifest) = serde_json::from_slice::<ck_index::IndexManifest>(&data)
&& let Some(model_name) = manifest.embedding_model
{
let registry = ck_models::ModelRegistry::default();
let alias = registry
.models
.iter()
.find(|(_, config)| config.name == model_name)
.map(|(alias, _)| alias.clone())
.unwrap_or_else(|| model_name.clone());
let dims = manifest
.embedding_dimensions
.or_else(|| {
registry
.models
.iter()
.find(|(_, config)| config.name == model_name)
.map(|(_, config)| config.dimensions)
})
.unwrap_or(0);

index_info["model"] = json!({
"name": model_name,
"alias": alias,
"dimensions": dims,
});
}

// Update cache with fresh stats
let cache_stats = crate::mcp::cache::IndexStats {
file_count: index_stats.total_files,
Expand Down
80 changes: 39 additions & 41 deletions ck-vscode/src/cliAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class CkCliAdapter {
*/
async getIndexStatus(path: string): Promise<IndexStatus> {
return new Promise((resolve, reject) => {
const child = spawn(this.cliPath, ['--status', path], {
const child = spawn(this.cliPath, ['--status-json', path], {
shell: false
});

Expand All @@ -117,27 +117,30 @@ export class CkCliAdapter {
});

child.on('close', (code) => {
// Parse status output
// This is a simplified version - actual parsing depends on ck's status output format
const exists = !stdout.includes('No index found');
if (code !== 0) {
reject(new Error(`ck exited with code ${code}: ${stderr}`));
return;
}

const indexDir = pathModule.join(path, '.ck');
let lastModified: number | undefined;
try {
const stats = fs.statSync(indexDir);
lastModified = Math.floor(stats.mtimeMs / 1000);
} catch {
// ignore missing directory or access issues
}
// Parse JSON output from --status-json
const json = JSON.parse(stdout);

resolve({
exists,
path,
totalFiles: this.extractNumber(stdout, /(\d+)\s+files/),
totalChunks: this.extractNumber(stdout, /(\d+)\s+chunks/),
indexPath: indexDir,
lastModified
});
resolve({
exists: Boolean(json.index_exists),
path: json.path || path,
totalFiles: json.total_files,
totalChunks: json.total_chunks,
indexPath: pathModule.join(path, '.ck'),
lastModified: json.index_updated || json.index_created,
embeddedChunks: json.embedded_chunks,
indexSizeBytes: json.index_size_bytes,
totalSizeBytes: json.total_size_bytes,
model: json.model
});
} catch (err) {
reject(new Error(`Failed to parse status JSON: ${err instanceof Error ? err.message : String(err)}`));
}
});

child.on('error', (err) => {
Expand Down Expand Up @@ -286,20 +289,7 @@ export class CkCliAdapter {
args.push('--no-default-excludes');
}

// Query
args.push(options.query);

// File targets (include patterns)
if (options.includePatterns && options.includePatterns.length > 0) {
options.includePatterns
.filter(pattern => pattern.trim().length > 0)
.forEach(pattern => args.push(pattern.trim()));
} else {
// Default to searching the current workspace directory
args.push('.');
}

// Optional parameters
// Optional parameters (must come before positional args)
if (options.topK !== undefined) {
args.push('--topk', options.topK.toString());
}
Expand Down Expand Up @@ -327,6 +317,22 @@ export class CkCliAdapter {
});
}

// Add -- separator to prevent query from being parsed as a flag
args.push('--');

// Query (after -- separator to handle dash-prefixed queries)
args.push(options.query);

// File targets (include patterns)
if (options.includePatterns && options.includePatterns.length > 0) {
options.includePatterns
.filter(pattern => pattern.trim().length > 0)
.forEach(pattern => args.push(pattern.trim()));
} else {
// Default to searching the current workspace directory
args.push('.');
}

return args;
}

Expand All @@ -352,14 +358,6 @@ export class CkCliAdapter {
};
}

/**
* Extract a number from text using regex
*/
private extractNumber(text: string, regex: RegExp): number | undefined {
const match = text.match(regex);
return match ? parseInt(match[1], 10) : undefined;
}

dispose(): void {
// CLI adapter does not maintain persistent resources
this.progressEmitter.dispose();
Expand Down
9 changes: 8 additions & 1 deletion ck-vscode/src/mcpAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,18 @@ export class CkMcpAdapter {
path: status.path ?? pathToCheck,
totalFiles: status.total_files,
totalChunks: status.total_chunks,
embeddedChunks: status.embedded_chunks,
totalSizeBytes: status.total_size_bytes,
lastModified: status.last_modified,
indexPath: status.index_path,
indexSizeBytes: status.index_size_bytes,
estimatedFileCount: status.estimated_file_count,
cacheHit: status.cache_hit
cacheHit: status.cache_hit,
model: status.model ? {
name: status.model.name,
alias: status.model.alias,
dimensions: status.model.dimensions
} : undefined
};
}

Expand Down
89 changes: 25 additions & 64 deletions ck-vscode/src/searchPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,71 +26,19 @@ export class CkSearchPanel implements vscode.WebviewViewProvider {
private currentIndexRoot: string | undefined;
private adapterDisposables: vscode.Disposable[] = [];

// Fallback copy of ck-core defaults; extension fetches live content when possible.
// Minimal fallback used only when backend fetch fails
private static readonly FALLBACK_CKIGNORE_CONTENT = `# .ckignore - Default patterns for ck semantic search
# Created automatically during first index
#
# ERROR: Could not fetch default patterns from ck backend.
# This may indicate a problem with your ck installation.
#
# To get the default patterns, run:
# ck --print-default-ckignore
#
# Or reinstall/update the ck extension.
#
# Syntax: same as .gitignore (glob patterns, ! for negation)

# Images
*.png
*.jpg
*.jpeg
*.gif
*.bmp
*.svg
*.ico
*.webp
*.tiff

# Video
*.mp4
*.avi
*.mov
*.mkv
*.wmv
*.flv
*.webm

# Audio
*.mp3
*.wav
*.flac
*.aac
*.ogg
*.m4a

# Binary/Compiled
*.exe
*.dll
*.so
*.dylib
*.a
*.lib
*.obj
*.o

# Archives
*.zip
*.tar
*.tar.gz
*.tgz
*.rar
*.7z
*.bz2
*.gz

# Data files
*.db
*.sqlite
*.sqlite3
*.parquet
*.arrow

# Config formats (issue #27)
*.json
*.yaml
*.yml

# Add your custom patterns below this line
`;

Expand Down Expand Up @@ -527,15 +475,21 @@ export class CkSearchPanel implements vscode.WebviewViewProvider {
const indexRoot = this.resolveIndexRoot(workspaceFolder);
const adapter = this.getAdapter(indexRoot);
let defaultContent = CkSearchPanel.FALLBACK_CKIGNORE_CONTENT;
let usedFallback = true;

if (adapter.getDefaultCkignoreContent) {
try {
const fetched = await adapter.getDefaultCkignoreContent(indexRoot);
if (typeof fetched === 'string' && fetched.trim().length > 0) {
defaultContent = fetched;
usedFallback = false;
}
} catch (error) {
console.warn('Failed to fetch default .ckignore content from ck backend:', error);
console.error('Failed to fetch default .ckignore content from ck backend:', error);
vscode.window.showWarningMessage(
'Could not fetch default .ckignore patterns from ck. Using minimal template. ' +
'Please check your ck installation.'
);
}
}

Expand All @@ -547,7 +501,14 @@ export class CkSearchPanel implements vscode.WebviewViewProvider {
ckignoreUri,
Buffer.from(defaultContent, 'utf8')
);
vscode.window.showInformationMessage('.ckignore created with default ck patterns');

if (usedFallback) {
vscode.window.showWarningMessage(
'.ckignore created with minimal template. Run "ck --print-default-ckignore" to get the default patterns.'
);
} else {
vscode.window.showInformationMessage('.ckignore created with default ck patterns');
}
}

const document = await vscode.workspace.openTextDocument(ckignoreUri);
Expand Down
Loading