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/README.md +150 -0
- package/README.zh-CN.md +224 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +699 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/enrich-BHaZ2daX.mjs +3000 -0
- package/dist/enrich-BHaZ2daX.mjs.map +1 -0
- package/dist/index.d.mts +307 -0
- package/dist/index.mjs +2 -0
- package/package.json +82 -0
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
|