kly 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs ADDED
@@ -0,0 +1,699 @@
1
+ #!/usr/bin/env node
2
+ import { C as openDatabase, E as saveState, F as isGitRepo, J as loadConfig, K as initKlyDir, M as getFileHistory, S as loadState, a as searchFilesWithRerank, c as buildDependencyGraph, d as renderGraphAscii, f as renderGraphSvg, i as searchFiles, l as generateMermaid, q as isInitialized, s as buildIndex, t as enrichErrorStack, w as removeBranchDb, x as listBranchDbs } from "./enrich-BHaZ2daX.mjs";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { execSync } from "node:child_process";
6
+ import { getModel } from "@mariozechner/pi-ai";
7
+ import { Command } from "commander";
8
+ import * as p from "@clack/prompts";
9
+ //#region src/commands/output.ts
10
+ /** Output data as JSON (default) or human-readable (--pretty). */
11
+ function output(data, opts, prettyFormatter) {
12
+ if (opts.pretty && prettyFormatter) process.stdout.write(prettyFormatter(data) + "\n");
13
+ else process.stdout.write(JSON.stringify(data, null, 2) + "\n");
14
+ }
15
+ /** Output error to stderr and exit. Optionally show a hint with correct usage. */
16
+ function error(message, hint) {
17
+ let msg = `Error: ${message}`;
18
+ if (hint) msg += `\n ${hint}`;
19
+ process.stderr.write(msg + "\n");
20
+ process.exit(1);
21
+ }
22
+ /** Output warning to stderr. */
23
+ function warn(message) {
24
+ process.stderr.write(`Warning: ${message}\n`);
25
+ }
26
+ /** Output info to stderr (for progress/status messages that shouldn't pollute stdout). */
27
+ function info(message) {
28
+ process.stderr.write(`${message}\n`);
29
+ }
30
+ //#endregion
31
+ //#region src/commands/shared.ts
32
+ function ensureInitialized(root) {
33
+ if (!isInitialized(root)) error("Not initialized.", "kly init --provider <provider> --api-key <key>");
34
+ }
35
+ //#endregion
36
+ //#region src/commands/build.ts
37
+ async function runBuild(root, options = {}) {
38
+ ensureInitialized(root);
39
+ if (!options.quiet) info("building index...");
40
+ try {
41
+ const result = await buildIndex(root, {
42
+ full: options.full,
43
+ quiet: options.quiet,
44
+ onProgress: (progress) => {
45
+ if (!options.quiet) {
46
+ let msg = `indexing [${progress.total > 0 ? Math.round(progress.completed / progress.total * 100) : 100}%] ${progress.current || "..."}`;
47
+ if (progress.skipped > 0) msg += ` (${progress.skipped} unchanged)`;
48
+ process.stderr.write(`\r${msg}`);
49
+ }
50
+ }
51
+ });
52
+ if (!options.quiet) {
53
+ process.stderr.write("\r\x1B[K");
54
+ const parts = [];
55
+ if (result.newFiles > 0) parts.push(`${result.newFiles} new`);
56
+ if (result.updatedFiles > 0) parts.push(`${result.updatedFiles} updated`);
57
+ if (result.deletedFiles > 0) parts.push(`${result.deletedFiles} deleted`);
58
+ if (result.unchangedFiles > 0) parts.push(`${result.unchangedFiles} unchanged`);
59
+ const summary = parts.length > 0 ? parts.join(", ") : "no changes";
60
+ const duration = (result.durationMs / 1e3).toFixed(1);
61
+ const lines = [`indexed ${result.totalFiles} files (${summary})`, `branch: ${result.branch}`];
62
+ if (result.commit) lines.push(`commit: ${result.commit.slice(0, 7)}`);
63
+ lines.push(`duration: ${duration}s`);
64
+ process.stdout.write(lines.join("\n") + "\n");
65
+ }
66
+ } catch (err) {
67
+ process.stderr.write(`\r\x1b[K`);
68
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
69
+ process.exit(1);
70
+ }
71
+ }
72
+ //#endregion
73
+ //#region src/commands/dependents.ts
74
+ function formatDependents(data) {
75
+ const result = data;
76
+ if (result.dependents.length === 0) return `${result.file} is not imported by any indexed file`;
77
+ const lines = [`${result.file} is imported by ${result.dependents.length} file(s)`, ""];
78
+ for (const dep of result.dependents) lines.push(` ${dep}`);
79
+ return lines.join("\n");
80
+ }
81
+ function runDependents(root, filePath, options = {}) {
82
+ ensureInitialized(root);
83
+ const db = openDatabase(root);
84
+ try {
85
+ if (!db.getFile(filePath)) error(`File not found in index: ${filePath}`, `kly query "${filePath.split("/").pop()}"`);
86
+ output({
87
+ file: filePath,
88
+ dependents: db.getDependents(filePath)
89
+ }, options, formatDependents);
90
+ } finally {
91
+ db.close();
92
+ }
93
+ }
94
+ //#endregion
95
+ //#region src/commands/enrich.ts
96
+ function readStdin() {
97
+ return new Promise((resolve, reject) => {
98
+ const chunks = [];
99
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
100
+ process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
101
+ process.stdin.on("error", reject);
102
+ if (process.stdin.isTTY) reject(/* @__PURE__ */ new Error("no input"));
103
+ });
104
+ }
105
+ function parseFrames(input) {
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(input);
109
+ } catch {
110
+ error("Invalid JSON input.", "echo '[{\"file\":\"src/foo.ts\",\"line\":42}]' | kly enrich");
111
+ }
112
+ if (!Array.isArray(parsed)) error("Input must be a JSON array of ErrorFrame objects.", "kly enrich --frames '[{\"file\":\"src/foo.ts\",\"line\":42}]'");
113
+ for (const [i, frame] of parsed.entries()) {
114
+ if (!frame.file || typeof frame.file !== "string") error(`Frame ${i}: missing or invalid "file" field.`);
115
+ if (typeof frame.line !== "number") error(`Frame ${i}: missing or invalid "line" field.`);
116
+ }
117
+ return parsed;
118
+ }
119
+ async function runEnrich(root, options = {}) {
120
+ ensureInitialized(root);
121
+ let input;
122
+ if (options.frames) input = options.frames;
123
+ else try {
124
+ input = await readStdin();
125
+ } catch {
126
+ error("No input provided. Pass frames via --frames or stdin.", "echo '[{\"file\":\"src/foo.ts\",\"line\":42}]' | kly enrich\n kly enrich --frames '[{\"file\":\"src/foo.ts\",\"line\":42}]'");
127
+ }
128
+ const frames = parseFrames(input.trim());
129
+ if (frames.length === 0) error("Empty frames array.");
130
+ const db = openDatabase(root);
131
+ try {
132
+ const result = enrichErrorStack(db, root, frames);
133
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
134
+ } finally {
135
+ db.close();
136
+ }
137
+ }
138
+ //#endregion
139
+ //#region src/commands/gc.ts
140
+ function runGc(root) {
141
+ ensureInitialized(root);
142
+ if (!isGitRepo(root)) {
143
+ warn("not a git repository, nothing to clean");
144
+ return;
145
+ }
146
+ const branchOutput = execSync("git branch --format='%(refname:short)'", {
147
+ cwd: root,
148
+ encoding: "utf-8"
149
+ }).trim();
150
+ const activeBranches = new Set(branchOutput.split("\n").filter(Boolean).map((b) => b.replace(/'/g, "").replace(/\//g, "--")));
151
+ activeBranches.add("default");
152
+ const dbNames = listBranchDbs(root);
153
+ const state = loadState(root);
154
+ let cleaned = 0;
155
+ for (const dbName of dbNames) {
156
+ if (activeBranches.has(dbName) || dbName.startsWith("_detached--")) continue;
157
+ removeBranchDb(root, dbName);
158
+ delete state.branches[dbName];
159
+ cleaned++;
160
+ info(`removed: ${dbName}.db`);
161
+ }
162
+ if (cleaned > 0) {
163
+ saveState(root, state);
164
+ info(`cleaned ${cleaned} stale database(s)`);
165
+ } else info("no stale databases found");
166
+ }
167
+ //#endregion
168
+ //#region src/commands/graph.ts
169
+ function formatGraph(data) {
170
+ const graph = data;
171
+ if (graph.nodes.length === 0) return "no files indexed yet. run `kly build` first.";
172
+ if (graph.edges.length === 0) return "no dependencies found between indexed files.";
173
+ const forward = /* @__PURE__ */ new Map();
174
+ const reverse = /* @__PURE__ */ new Map();
175
+ for (const edge of graph.edges) {
176
+ if (!forward.has(edge.from)) forward.set(edge.from, []);
177
+ forward.get(edge.from).push(edge.to);
178
+ if (!reverse.has(edge.to)) reverse.set(edge.to, []);
179
+ reverse.get(edge.to).push(edge.from);
180
+ }
181
+ const lines = [`${graph.nodes.length} node(s), ${graph.edges.length} edge(s)`, ""];
182
+ for (const node of graph.nodes) {
183
+ lines.push(node.path);
184
+ const deps = forward.get(node.path) || [];
185
+ const dependents = reverse.get(node.path) || [];
186
+ for (const d of deps) lines.push(` -> ${d}`);
187
+ for (const d of dependents) lines.push(` <- ${d}`);
188
+ if (deps.length === 0 && dependents.length === 0) lines.push(" (no connections)");
189
+ lines.push("");
190
+ }
191
+ return lines.join("\n").trimEnd();
192
+ }
193
+ function runGraph(root, options = {}) {
194
+ ensureInitialized(root);
195
+ const depth = options.depth ?? 2;
196
+ const format = options.format ?? (options.pretty ? "ascii" : "mermaid");
197
+ const db = openDatabase(root);
198
+ try {
199
+ const graph = buildDependencyGraph(db, {
200
+ focus: options.focus,
201
+ depth
202
+ });
203
+ if (options.focus && !graph.nodes.has(options.focus)) error(`File not in index: ${options.focus}`, `kly query "${options.focus.split("/").pop()}"`);
204
+ if (format === "mermaid" || format === "ascii" || format === "svg") {
205
+ const mermaid = generateMermaid(graph);
206
+ if (graph.nodes.size === 0) {
207
+ console.log("no files indexed yet. run `kly build` first.");
208
+ return;
209
+ }
210
+ if (graph.edges.length === 0) {
211
+ console.log("no dependencies found between indexed files.");
212
+ return;
213
+ }
214
+ switch (format) {
215
+ case "mermaid":
216
+ console.log(mermaid);
217
+ break;
218
+ case "ascii":
219
+ console.log(renderGraphAscii(mermaid));
220
+ break;
221
+ case "svg":
222
+ console.log(renderGraphSvg(mermaid));
223
+ break;
224
+ }
225
+ return;
226
+ }
227
+ output({
228
+ nodes: Array.from(graph.nodes.values()),
229
+ edges: graph.edges
230
+ }, options, formatGraph);
231
+ } finally {
232
+ db.close();
233
+ }
234
+ }
235
+ //#endregion
236
+ //#region src/commands/history.ts
237
+ function formatHistory(data) {
238
+ const result = data;
239
+ if (result.commits.length === 0) return `no git history found for ${result.file}`;
240
+ const lines = [];
241
+ for (const c of result.commits) {
242
+ const date = (/* @__PURE__ */ new Date(c.date * 1e3)).toISOString().slice(0, 10);
243
+ lines.push(`${c.hash.slice(0, 7)} @${c.author} ${date} ${c.message}`);
244
+ }
245
+ return lines.join("\n");
246
+ }
247
+ function runHistory(root, filePath, options = {}) {
248
+ ensureInitialized(root);
249
+ output({
250
+ file: filePath,
251
+ commits: getFileHistory(root, filePath, options.limit ?? 5)
252
+ }, options, formatHistory);
253
+ }
254
+ //#endregion
255
+ //#region src/commands/hook.ts
256
+ const HOOK_BEGIN = "# BEGIN kly";
257
+ const HOOK_END = "# END kly";
258
+ const HOOK_CONTENT = `${HOOK_BEGIN}
259
+ kly build --quiet 2>/dev/null || true
260
+ ${HOOK_END}`;
261
+ function runHook(root, action) {
262
+ if (action !== "install" && action !== "uninstall") error("Invalid action.", "kly hook install\n kly hook uninstall");
263
+ const gitDir = path.join(root, ".git");
264
+ if (!fs.existsSync(gitDir)) error("Not a git repository.");
265
+ const hooksDir = path.join(gitDir, "hooks");
266
+ const hookPath = path.join(hooksDir, "post-commit");
267
+ if (action === "install") installHook(hooksDir, hookPath);
268
+ else uninstallHook(hookPath);
269
+ }
270
+ function installHook(hooksDir, hookPath) {
271
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
272
+ if (fs.existsSync(hookPath)) {
273
+ if (fs.readFileSync(hookPath, "utf-8").includes(HOOK_BEGIN)) {
274
+ info("kly hook already installed (no-op)");
275
+ return;
276
+ }
277
+ fs.appendFileSync(hookPath, `\n${HOOK_CONTENT}\n`);
278
+ } else fs.writeFileSync(hookPath, `#!/bin/sh\n${HOOK_CONTENT}\n`, { mode: 493 });
279
+ info("installed post-commit hook");
280
+ }
281
+ function uninstallHook(hookPath) {
282
+ if (!fs.existsSync(hookPath)) {
283
+ warn("no post-commit hook found (no-op)");
284
+ return;
285
+ }
286
+ const content = fs.readFileSync(hookPath, "utf-8");
287
+ if (!content.includes(HOOK_BEGIN)) {
288
+ warn("kly hook not found in post-commit (no-op)");
289
+ return;
290
+ }
291
+ const regex = new RegExp(`\\n?${HOOK_BEGIN}[\\s\\S]*?${HOOK_END}\\n?`, "g");
292
+ const updated = content.replace(regex, "\n").trim();
293
+ if (updated === "#!/bin/sh" || updated === "") fs.unlinkSync(hookPath);
294
+ else fs.writeFileSync(hookPath, updated + "\n", { mode: 493 });
295
+ info("uninstalled post-commit hook");
296
+ }
297
+ //#endregion
298
+ //#region src/commands/init.ts
299
+ const VALID_PROVIDERS = [
300
+ "openrouter",
301
+ "anthropic",
302
+ "openai",
303
+ "google",
304
+ "mistral",
305
+ "groq"
306
+ ];
307
+ const DEFAULT_MODELS = {
308
+ openrouter: "anthropic/claude-haiku-4.5",
309
+ anthropic: "claude-haiku-4.5",
310
+ openai: "gpt-4o-mini",
311
+ google: "gemini-2.0-flash",
312
+ mistral: "mistral-small-latest",
313
+ groq: "llama-3.1-8b-instant"
314
+ };
315
+ const DEFAULT_INCLUDE = [
316
+ "**/*.ts",
317
+ "**/*.tsx",
318
+ "**/*.js",
319
+ "**/*.jsx",
320
+ "**/*.swift"
321
+ ];
322
+ const DEFAULT_EXCLUDE = [
323
+ "**/node_modules/**",
324
+ "**/dist/**",
325
+ "**/build/**",
326
+ "**/.git/**",
327
+ "**/.kly/**",
328
+ "**/vendor/**",
329
+ "**/*.d.ts",
330
+ "**/*.test.*",
331
+ "**/*.spec.*",
332
+ "**/__tests__/**"
333
+ ];
334
+ async function runInit(root, options = {}) {
335
+ if (options.provider && options.apiKey) {
336
+ runNonInteractive(root, options);
337
+ return;
338
+ }
339
+ await runInteractive(root, options);
340
+ }
341
+ function runNonInteractive(root, options) {
342
+ if (!VALID_PROVIDERS.includes(options.provider)) {
343
+ process.stderr.write(`Error: Invalid provider "${options.provider}".\n Valid providers: ${VALID_PROVIDERS.join(", ")}\n kly init --provider openrouter --api-key <key>\n`);
344
+ process.exit(1);
345
+ }
346
+ initKlyDir(root, {
347
+ llm: {
348
+ provider: options.provider,
349
+ model: options.model || DEFAULT_MODELS[options.provider] || "",
350
+ apiKey: options.apiKey
351
+ },
352
+ include: options.include?.length ? options.include : DEFAULT_INCLUDE,
353
+ exclude: options.exclude?.length ? options.exclude : DEFAULT_EXCLUDE
354
+ });
355
+ info("initialized .kly/ directory");
356
+ if (options.hook) if (fs.existsSync(path.join(root, ".git"))) {
357
+ runHook(root, "install");
358
+ info("installed post-commit hook");
359
+ } else process.stderr.write("Warning: Not a git repo, skipping hook install.\n");
360
+ }
361
+ async function runInteractive(root, _options) {
362
+ p.intro("kly init");
363
+ if (isInitialized(root)) p.log.warn(".kly/ directory already exists, reconfiguring...");
364
+ const provider = await p.select({
365
+ message: "Select LLM provider",
366
+ options: [
367
+ {
368
+ value: "openrouter",
369
+ label: "OpenRouter",
370
+ hint: "recommended, supports all models"
371
+ },
372
+ {
373
+ value: "anthropic",
374
+ label: "Anthropic"
375
+ },
376
+ {
377
+ value: "openai",
378
+ label: "OpenAI"
379
+ },
380
+ {
381
+ value: "google",
382
+ label: "Google"
383
+ },
384
+ {
385
+ value: "mistral",
386
+ label: "Mistral"
387
+ },
388
+ {
389
+ value: "groq",
390
+ label: "Groq"
391
+ }
392
+ ]
393
+ });
394
+ if (p.isCancel(provider)) {
395
+ p.cancel("Init cancelled.");
396
+ process.exit(0);
397
+ }
398
+ const apiKey = await p.password({
399
+ message: `Enter your ${provider} API key`,
400
+ validate: (value) => {
401
+ if (!value || value.trim().length === 0) return "API key is required";
402
+ }
403
+ });
404
+ if (p.isCancel(apiKey)) {
405
+ p.cancel("Init cancelled.");
406
+ process.exit(0);
407
+ }
408
+ const model = await p.text({
409
+ message: "Model name",
410
+ initialValue: DEFAULT_MODELS[provider] || "",
411
+ validate: (value) => {
412
+ if (!value || value.trim().length === 0) return "Model name is required";
413
+ }
414
+ });
415
+ if (p.isCancel(model)) {
416
+ p.cancel("Init cancelled.");
417
+ process.exit(0);
418
+ }
419
+ initKlyDir(root, {
420
+ llm: {
421
+ provider,
422
+ model,
423
+ apiKey
424
+ },
425
+ include: DEFAULT_INCLUDE,
426
+ exclude: DEFAULT_EXCLUDE
427
+ });
428
+ p.log.success("Initialized .kly/ directory");
429
+ p.log.info("Edit .kly/config.yaml to customize settings");
430
+ if (fs.existsSync(path.join(root, ".git"))) {
431
+ const installHook = await p.confirm({
432
+ message: "Install post-commit hook for automatic indexing?",
433
+ initialValue: true
434
+ });
435
+ if (!p.isCancel(installHook) && installHook) runHook(root, "install");
436
+ }
437
+ p.outro("Done!");
438
+ }
439
+ //#endregion
440
+ //#region src/commands/overview.ts
441
+ function formatOverview(data) {
442
+ const overview = data;
443
+ if (overview.totalFiles === 0) return "no files indexed yet. run `kly build` first.";
444
+ const lines = [
445
+ `total_files: ${overview.totalFiles}`,
446
+ "",
447
+ "languages:"
448
+ ];
449
+ const sorted = Object.entries(overview.languages).sort(([, a], [, b]) => b - a);
450
+ for (const [lang, count] of sorted) {
451
+ const pct = (count / overview.totalFiles * 100).toFixed(1);
452
+ lines.push(` ${lang}: ${count} (${pct}%)`);
453
+ }
454
+ lines.push("", "files:");
455
+ for (const file of overview.files) {
456
+ lines.push(` ${file.path}`);
457
+ if (file.description) lines.push(` ${file.description}`);
458
+ }
459
+ return lines.join("\n");
460
+ }
461
+ function runOverview(root, options = {}) {
462
+ ensureInitialized(root);
463
+ const db = openDatabase(root);
464
+ try {
465
+ output({
466
+ totalFiles: db.getFileCount(),
467
+ languages: db.getLanguageStats(),
468
+ files: db.getAllFiles().map((f) => ({
469
+ path: f.path,
470
+ name: f.name,
471
+ description: f.description
472
+ }))
473
+ }, options, formatOverview);
474
+ } finally {
475
+ db.close();
476
+ }
477
+ }
478
+ //#endregion
479
+ //#region src/commands/query.ts
480
+ function formatResults(data) {
481
+ const results = data;
482
+ if (results.length === 0) return "no matching files found";
483
+ const lines = [`found ${results.length} file(s)`, ""];
484
+ for (const r of results) {
485
+ lines.push(r.path);
486
+ lines.push(` name: ${r.name}`);
487
+ lines.push(` description: ${r.description}`);
488
+ lines.push(` score: ${r.score.toFixed(2)}`);
489
+ if (r.summary) {
490
+ const summary = r.summary.replace(/\s+/g, " ").trim();
491
+ lines.push(` summary: ${summary.length > 120 ? summary.slice(0, 117) + "..." : summary}`);
492
+ }
493
+ if (r.symbols.length > 0) {
494
+ const shown = r.symbols.slice(0, 5);
495
+ const more = r.symbols.length - shown.length;
496
+ lines.push(` symbols: ${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`);
497
+ }
498
+ lines.push("");
499
+ }
500
+ return lines.join("\n").trimEnd();
501
+ }
502
+ function toOutputData(results) {
503
+ return results.map((r) => ({
504
+ path: r.file.path,
505
+ name: r.file.name,
506
+ description: r.file.description,
507
+ score: r.score,
508
+ summary: r.file.summary,
509
+ language: r.file.language,
510
+ symbols: r.file.symbols.map((s) => s.name)
511
+ }));
512
+ }
513
+ async function runQuery(root, description, options = {}) {
514
+ ensureInitialized(root);
515
+ const limit = options.limit ?? 10;
516
+ const db = openDatabase(root);
517
+ try {
518
+ let results;
519
+ if (options.rerank) {
520
+ const config = loadConfig(root);
521
+ const model = getModel(config.llm.provider, config.llm.model);
522
+ const envKey = {
523
+ openrouter: "OPENROUTER_API_KEY",
524
+ anthropic: "ANTHROPIC_API_KEY",
525
+ openai: "OPENAI_API_KEY",
526
+ google: "GOOGLE_API_KEY",
527
+ mistral: "MISTRAL_API_KEY",
528
+ groq: "GROQ_API_KEY",
529
+ xai: "XAI_API_KEY"
530
+ }[config.llm.provider] || `${config.llm.provider.toUpperCase()}_API_KEY`;
531
+ if (config.llm.apiKey && !process.env[envKey]) process.env[envKey] = config.llm.apiKey;
532
+ if (options.pretty) info("reranking results with LLM...");
533
+ results = await searchFilesWithRerank(db, model, description, limit);
534
+ } else results = searchFiles(db, description, limit);
535
+ output(toOutputData(results), options, formatResults);
536
+ } finally {
537
+ db.close();
538
+ }
539
+ }
540
+ //#endregion
541
+ //#region src/commands/show.ts
542
+ function formatFile(data) {
543
+ const file = data;
544
+ const lines = [
545
+ `path: ${file.path}`,
546
+ `name: ${file.name}`,
547
+ `language: ${file.language}`,
548
+ `description: ${file.description}`
549
+ ];
550
+ if (file.summary) lines.push(`summary: ${file.summary}`);
551
+ if (file.imports.length > 0) {
552
+ lines.push("", `imports (${file.imports.length}):`);
553
+ for (const imp of file.imports) lines.push(` ${imp}`);
554
+ }
555
+ if (file.exports.length > 0) {
556
+ lines.push("", `exports (${file.exports.length}):`);
557
+ for (const exp of file.exports) lines.push(` ${exp}`);
558
+ }
559
+ if (file.symbols.length > 0) {
560
+ lines.push("", `symbols (${file.symbols.length}):`);
561
+ for (const s of file.symbols) {
562
+ lines.push(` ${s.kind} ${s.name}`);
563
+ if (s.description) lines.push(` ${s.description}`);
564
+ }
565
+ }
566
+ lines.push("", `hash: ${file.hash}`, `indexed_at: ${file.indexedAt}`);
567
+ return lines.join("\n");
568
+ }
569
+ function runShow(root, filePath, options = {}) {
570
+ ensureInitialized(root);
571
+ const db = openDatabase(root);
572
+ try {
573
+ const fileIndex = db.getFile(filePath);
574
+ if (!fileIndex) error(`File not found in index: ${filePath}`, `kly query "${filePath.split("/").pop()}"`);
575
+ output({
576
+ path: fileIndex.path,
577
+ name: fileIndex.name,
578
+ description: fileIndex.description,
579
+ language: fileIndex.language,
580
+ summary: fileIndex.summary,
581
+ imports: fileIndex.imports,
582
+ exports: fileIndex.exports,
583
+ symbols: fileIndex.symbols,
584
+ hash: fileIndex.hash,
585
+ indexedAt: new Date(fileIndex.indexedAt).toISOString()
586
+ }, options, formatFile);
587
+ } finally {
588
+ db.close();
589
+ }
590
+ }
591
+ //#endregion
592
+ //#region src/cli.ts
593
+ const program = new Command();
594
+ program.name("kly").description("Code repository file-level indexing tool").version("0.2.0").showHelpAfterError().showSuggestionAfterError();
595
+ program.command("init").description("Initialize kly in the current repository").option("--provider <name>", "LLM provider (openrouter, anthropic, openai, google, mistral, groq)").option("--model <name>", "Model name (default: provider-specific)").option("--api-key <key>", "API key (or set via env variable)").option("--hook", "Also install post-commit hook").option("--include <glob...>", "File include patterns").option("--exclude <glob...>", "File exclude patterns").addHelpText("after", `
596
+ Examples:
597
+ kly init --provider openrouter --api-key sk-or-xxx
598
+ kly init --provider anthropic --model claude-haiku-4.5 --api-key sk-ant-xxx --hook
599
+ kly init # interactive mode (no flags)
600
+ `).action(async (options) => {
601
+ await runInit(process.cwd(), options);
602
+ });
603
+ program.command("build").description("Build or update the repository index").option("--full", "Force full rebuild").option("--quiet", "Suppress output (for git hooks)").addHelpText("after", `
604
+ Examples:
605
+ kly build # incremental build
606
+ kly build --full # full rebuild
607
+ kly build --full --quiet # CI / git hook mode
608
+ `).action(async (options) => {
609
+ await runBuild(process.cwd(), options);
610
+ });
611
+ program.command("query <text>").description("Search indexed files by natural language description").option("--rerank", "Use LLM to rerank results for better relevance").option("--limit <n>", "Maximum results", "10").option("--pretty", "Human-readable output").addHelpText("after", `
612
+ Examples:
613
+ kly query "authentication middleware"
614
+ kly query "error handling" --limit 5
615
+ kly query "database migration" --rerank
616
+ kly query "auth" --pretty
617
+ `).action(async (text, options) => {
618
+ await runQuery(process.cwd(), text, {
619
+ rerank: options.rerank,
620
+ limit: options.limit ? parseInt(options.limit, 10) : void 0,
621
+ pretty: options.pretty
622
+ });
623
+ });
624
+ program.command("show <path>").description("Show indexed metadata for a file").option("--pretty", "Human-readable output").addHelpText("after", `
625
+ Examples:
626
+ kly show src/auth.ts
627
+ kly show src/auth.ts --pretty
628
+ `).action((filePath, options) => {
629
+ runShow(process.cwd(), filePath, options);
630
+ });
631
+ program.command("overview").description("Show repository index summary with language breakdown").option("--pretty", "Human-readable output").addHelpText("after", `
632
+ Examples:
633
+ kly overview
634
+ kly overview --pretty
635
+ `).action((options) => {
636
+ runOverview(process.cwd(), options);
637
+ });
638
+ program.command("graph").description("Show file dependency graph").option("--focus <path>", "Focus on a specific file").option("--depth <n>", "Traversal depth", "2").option("--format <type>", "Output format: mermaid, json, ascii, svg (default: mermaid, or ascii with --pretty)").option("--pretty", "Human-readable output").addHelpText("after", `
639
+ Examples:
640
+ kly graph
641
+ kly graph --format ascii
642
+ kly graph --format svg --focus src/auth.ts --depth 3
643
+ kly graph --format mermaid
644
+ kly graph --pretty
645
+ `).action((options) => {
646
+ runGraph(process.cwd(), {
647
+ focus: options.focus,
648
+ depth: options.depth ? parseInt(options.depth, 10) : void 0,
649
+ format: options.format,
650
+ pretty: options.pretty
651
+ });
652
+ });
653
+ program.command("dependents <path>").description("Show files that import the given file (reverse dependencies)").option("--pretty", "Human-readable output").addHelpText("after", `
654
+ Examples:
655
+ kly dependents src/database.ts
656
+ kly dependents src/types.ts --pretty
657
+ `).action((filePath, options) => {
658
+ runDependents(process.cwd(), filePath, options);
659
+ });
660
+ program.command("history <path>").description("Show recent git modification history for a file").option("--limit <n>", "Number of commits", "5").option("--pretty", "Human-readable output").addHelpText("after", `
661
+ Examples:
662
+ kly history src/auth.ts
663
+ kly history src/auth.ts --limit 10
664
+ kly history src/auth.ts --pretty
665
+ `).action((filePath, options) => {
666
+ runHistory(process.cwd(), filePath, {
667
+ limit: options.limit ? parseInt(options.limit, 10) : void 0,
668
+ pretty: options.pretty
669
+ });
670
+ });
671
+ program.command("enrich").description("Enrich error stack frames with file descriptions, dependencies, and git history").option("--frames <json>", "Error frames as JSON string").addHelpText("after", `
672
+ Input: JSON array of ErrorFrame objects via --frames or stdin.
673
+ Each frame: { "file": "path", "line": number, "column?": number, "function?": "name" }
674
+
675
+ Examples:
676
+ echo '[{"file":"src/auth.ts","line":42}]' | kly enrich
677
+ kly enrich --frames '[{"file":"src/auth.ts","line":42,"function":"validate"}]'
678
+ cat error-frames.json | kly enrich
679
+ `).action(async (options) => {
680
+ await runEnrich(process.cwd(), options);
681
+ });
682
+ program.command("hook <action>").description("Install or uninstall the post-commit hook").addHelpText("after", `
683
+ Examples:
684
+ kly hook install
685
+ kly hook uninstall
686
+ `).action((action) => {
687
+ runHook(process.cwd(), action);
688
+ });
689
+ program.command("gc").description("Remove databases for deleted git branches").addHelpText("after", `
690
+ Examples:
691
+ kly gc
692
+ `).action(() => {
693
+ runGc(process.cwd());
694
+ });
695
+ await program.parseAsync();
696
+ //#endregion
697
+ export {};
698
+
699
+ //# sourceMappingURL=cli.mjs.map