knodin 0.5.0

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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/dist/bin/cli.js +1704 -0
  4. package/dist/src/agent-integration.js +250 -0
  5. package/dist/src/artifact-refresh.js +81 -0
  6. package/dist/src/cli-args.js +267 -0
  7. package/dist/src/cli-model.js +324 -0
  8. package/dist/src/compact-structural.js +96 -0
  9. package/dist/src/competitive-constraints.js +20 -0
  10. package/dist/src/competitive-manifest.js +330 -0
  11. package/dist/src/competitive-measurement.js +183 -0
  12. package/dist/src/competitive-runner.js +453 -0
  13. package/dist/src/competitive-sandbox.js +108 -0
  14. package/dist/src/context-export.js +422 -0
  15. package/dist/src/context.js +102 -0
  16. package/dist/src/docs-sections.js +141 -0
  17. package/dist/src/doctor.js +380 -0
  18. package/dist/src/engine/ann-hnsw.js +271 -0
  19. package/dist/src/engine/embeddings.js +193 -0
  20. package/dist/src/engine/file-walker.js +43 -0
  21. package/dist/src/engine/index.js +13030 -0
  22. package/dist/src/engine/perf.js +115 -0
  23. package/dist/src/engine/prune.js +112 -0
  24. package/dist/src/engine/source-policy.js +69 -0
  25. package/dist/src/engine/sqlite.js +71 -0
  26. package/dist/src/engine/symbol-delete.js +58 -0
  27. package/dist/src/failure-diagnosis.js +590 -0
  28. package/dist/src/fleet.js +7 -0
  29. package/dist/src/git-executable.js +31 -0
  30. package/dist/src/graph-query-health.js +115 -0
  31. package/dist/src/index-activity.js +125 -0
  32. package/dist/src/init-progress-worker.js +107 -0
  33. package/dist/src/init-progress.js +155 -0
  34. package/dist/src/init.js +985 -0
  35. package/dist/src/lifecycle-health.js +213 -0
  36. package/dist/src/lsp-readonly.js +217 -0
  37. package/dist/src/output-compression.js +629 -0
  38. package/dist/src/output-telemetry.js +359 -0
  39. package/dist/src/pr-triage.js +638 -0
  40. package/dist/src/relationship-adapters.js +370 -0
  41. package/dist/src/release-attestation.js +533 -0
  42. package/dist/src/repair-progress-worker.js +121 -0
  43. package/dist/src/repair-progress.js +262 -0
  44. package/dist/src/repository-init-process.js +173 -0
  45. package/dist/src/repository-management.js +1089 -0
  46. package/dist/src/response-budget.js +184 -0
  47. package/dist/src/server.js +53 -0
  48. package/dist/src/system-config.js +615 -0
  49. package/dist/src/terminal-help.js +83 -0
  50. package/dist/src/tools/knodin-tools.js +1438 -0
  51. package/dist/src/tools/reckon-tools.js +5 -0
  52. package/dist/src/update-policy.js +944 -0
  53. package/dist/src/update-trust.js +503 -0
  54. package/dist/src/version.js +13 -0
  55. package/dist/src/visualization.js +162 -0
  56. package/dist/src/wait-for-fresh.js +98 -0
  57. package/dist/src/worktree-lifecycle.js +231 -0
  58. package/docs/CLI.md +39 -0
  59. package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
  60. package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
  61. package/docs/DOCTOR-AND-UPDATES.md +84 -0
  62. package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
  63. package/docs/INSTALLATION.md +208 -0
  64. package/docs/MCP.md +100 -0
  65. package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
  66. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  67. package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
  68. package/docs/SIGNED-UPDATES.md +146 -0
  69. package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
  70. package/docs/TELEMETRY.md +42 -0
  71. package/docs/releases/0.3.0.md +46 -0
  72. package/docs/releases/0.4.0.md +68 -0
  73. package/docs/releases/0.4.1.md +28 -0
  74. package/docs/releases/0.4.2.md +27 -0
  75. package/docs/releases/0.4.3.md +23 -0
  76. package/docs/releases/0.5.0.md +29 -0
  77. package/package.json +110 -0
  78. package/schemas/release-attestation-v1.schema.json +210 -0
  79. package/tree-sitter-prisma.wasm +0 -0
  80. package/tree-sitter-sql.wasm +0 -0
  81. package/tree-sitter-xml.wasm +0 -0
@@ -0,0 +1,1704 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `knodin` CLI — the daily driver.
4
+ *
5
+ * knodin explain <symbol> [minimal] edit-ready source + direct call paths + blast radius
6
+ * knodin review [base] risk-scored context with explicit git diff scopes
7
+ * knodin map subsystems + confidence-tagged edges
8
+ * knodin wiki [--force] write .reckon/wiki/ (index.md + per-community pages)
9
+ * knodin serve run the MCP server on stdio
10
+ *
11
+ * The same engine backs both the CLI and the MCP server, so any assistant and
12
+ * a human at a terminal see identical results.
13
+ */
14
+ import { spawn, spawnSync } from "node:child_process";
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import readline from "node:readline/promises";
18
+ import { fileURLToPath } from "node:url";
19
+ import { detectSupportedAgents, parseInitScope, } from "../src/agent-integration.js";
20
+ import { refreshExternalGraphArtifacts, writeArtifactRefreshRecord, } from "../src/artifact-refresh.js";
21
+ import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, planIndex, resolveCliRuntimeCommand, resolveRepo, } from "../src/cli-args.js";
22
+ import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
23
+ import { buildKnodinContext } from "../src/context.js";
24
+ import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/context-export.js";
25
+ import { getDocSection, listDocTopics } from "../src/docs-sections.js";
26
+ import { diagnoseInstallation } from "../src/doctor.js";
27
+ import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
28
+ import { diagnoseFailure, } from "../src/failure-diagnosis.js";
29
+ import { gitExecutable } from "../src/git-executable.js";
30
+ import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
31
+ import { createIndexActivityReporter } from "../src/index-activity.js";
32
+ import { detectTrackedTeamIntegration, InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
33
+ import { createInitProgressRenderer } from "../src/init-progress.js";
34
+ import { attachLifecycleHealth } from "../src/lifecycle-health.js";
35
+ import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
36
+ import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
37
+ import { auditPullRequests } from "../src/pr-triage.js";
38
+ import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, resolveRepairProgressMode, serializeRepairJsonlRecord, } from "../src/repair-progress.js";
39
+ import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
40
+ import { discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, searchRepositories, } from "../src/repository-management.js";
41
+ import { applyResponseBudget } from "../src/response-budget.js";
42
+ import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
43
+ import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
44
+ import { KNODIN_VERSION } from "../src/version.js";
45
+ import { writeVisualization } from "../src/visualization.js";
46
+ import { waitForFresh } from "../src/wait-for-fresh.js";
47
+ import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../src/worktree-lifecycle.js";
48
+ function explicitScope(args) {
49
+ const index = args.indexOf("--scope");
50
+ if (index >= 0) {
51
+ const value = args[index + 1];
52
+ if (!value)
53
+ throw new Error("knodin: --scope requires a value");
54
+ return parseInitScope(value);
55
+ }
56
+ const equals = args.find((argument) => argument.startsWith("--scope="));
57
+ return equals ? parseInitScope(equals.slice("--scope=".length)) : null;
58
+ }
59
+ async function chooseInitScope(args, repo) {
60
+ const selected = explicitScope(args);
61
+ if (selected)
62
+ return selected;
63
+ if (detectTrackedTeamIntegration(repo)) {
64
+ process.stderr.write("[init:scope] Detected tracked knodin team integration; preserving team scope\n");
65
+ return "team";
66
+ }
67
+ if (!process.stdin.isTTY || !process.stderr.isTTY)
68
+ return "personal";
69
+ const prompt = readline.createInterface({
70
+ input: process.stdin,
71
+ output: process.stderr,
72
+ });
73
+ try {
74
+ const answer = await prompt.question([
75
+ "How should knodin integrate with coding agents?",
76
+ " 1. Personal (recommended) — all detected agents; Git stays clean",
77
+ " 2. Team — create commit-ready shared configuration",
78
+ " 3. CLI-only — agents will not discover or invoke knodin automatically",
79
+ "Select [1]: ",
80
+ ].join("\n"));
81
+ if (!answer.trim() || answer.trim() === "1")
82
+ return "personal";
83
+ if (answer.trim() === "2")
84
+ return "team";
85
+ if (answer.trim() === "3") {
86
+ const confirmation = await prompt.question("CLI-only requires manual knodin commands. Continue? [y/N] ");
87
+ if (!/^y(?:es)?$/i.test(confirmation.trim()))
88
+ throw new Error("knodin init: CLI-only selection cancelled");
89
+ return "cli-only";
90
+ }
91
+ return parseInitScope(answer.trim());
92
+ }
93
+ finally {
94
+ prompt.close();
95
+ }
96
+ }
97
+ function integrationAgents(repo) {
98
+ const previous = readRepositoryIntegrationConfig(repo)?.agents ?? [];
99
+ return [...new Set([...detectSupportedAgents(), ...previous])];
100
+ }
101
+ function formatInitHuman(result) {
102
+ const agents = result.paths.scope === "cli-only"
103
+ ? "CLI-only — AI agents are not configured to discover knodin"
104
+ : result.paths.agentIntegration.configured.length > 0
105
+ ? `${result.paths.scope} — ${result.paths.agentIntegration.configured.join(", ")}`
106
+ : `${result.paths.scope} — no supported coding agents detected`;
107
+ const failures = result.paths.agentIntegration.failed
108
+ .map(({ agent, message }) => `\nAgent warning (${agent}): ${message}`)
109
+ .join("");
110
+ const refresh = result.paths.lifecycleRefresh.state === "fresh"
111
+ ? "fresh"
112
+ : `still running (${result.paths.lifecycleRefresh.queuedEvents} queued event(s)); run \`knodin wait --fresh\``;
113
+ return `${result.message}\nGraph: ${result.paths.database}\nGit refresh: ${result.paths.gitHooks.length} lifecycle hooks installed; ${refresh}\nAgent integration: ${agents}${failures}\nBackground indexer: ${result.paths.backgroundIndexer}\n`;
114
+ }
115
+ function formatConfigureStatusHuman(result) {
116
+ if (result.scope === "unconfigured")
117
+ return "Agent integration: unconfigured.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal`.\n";
118
+ if (result.scope === "cli-only")
119
+ return "Agent integration: CLI-only.\nWarning: AI agents will not discover or invoke knodin automatically. Run `knodin configure --scope personal` or `--scope team` to enable them.\n";
120
+ return `Agent integration: ${result.scope}${result.agents.length > 0 ? ` (${result.agents.join(", ")})` : ""}.\n`;
121
+ }
122
+ function formatConfigureHuman(result) {
123
+ let configured = "none detected";
124
+ if (result.paths.agentIntegration.configured.length > 0) {
125
+ configured = result.paths.agentIntegration.configured.join(", ");
126
+ }
127
+ else if (result.paths.scope === "cli-only") {
128
+ configured = "none (CLI-only)";
129
+ }
130
+ const failures = result.paths.agentIntegration.failed
131
+ .map(({ agent, message }) => `\nAgent warning (${agent}): ${message}`)
132
+ .join("");
133
+ const refresh = result.paths.lifecycleRefresh.state === "fresh"
134
+ ? "fresh"
135
+ : `still running (${result.paths.lifecycleRefresh.queuedEvents} queued event(s))`;
136
+ return `${result.message}\nAgent integration: ${result.paths.scope} — ${configured}${failures}\nGraph initialization: unchanged\nLifecycle refresh: ${refresh}\nNext: ${result.nextAction}\n`;
137
+ }
138
+ function formatRepairHuman(result) {
139
+ const coverage = result.after.coverage;
140
+ if (result.cancelled) {
141
+ return `Repair paused: ${result.remaining ?? 0} file(s) remaining. Run \`knodin repair\` again to finish.\n`;
142
+ }
143
+ if (result.verified) {
144
+ return `Repair verified: graph is healthy (${coverage.indexedFiles} indexed files, ${coverage.filesWithSymbols} files with symbols).\n`;
145
+ }
146
+ return `Repair finished with remaining issues. Run \`knodin status --deep\` for details.\n`;
147
+ }
148
+ function formatIndexHuman(result) {
149
+ if (result.indexed.length === 0 && result.unchanged.length > 0) {
150
+ const noun = result.unchanged.length === 1 ? "file" : "files";
151
+ return `Graph already current: ${result.unchanged.length.toLocaleString()} requested ${noun} needed no work; health verified.\n`;
152
+ }
153
+ const unchanged = result.unchanged.length > 0
154
+ ? `; ${result.unchanged.length.toLocaleString()} already current`
155
+ : "";
156
+ const noun = result.indexed.length === 1 ? "file" : "files";
157
+ return `Index complete: ${result.indexed.length.toLocaleString()} ${noun} indexed${unchanged}; graph health verified.\n`;
158
+ }
159
+ function formatIndexVerificationError(result) {
160
+ const firstIssue = result.verification.missing.files[0] ?? result.verification.missing.records[0];
161
+ const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
162
+ return `knodin index: requested work completed, but ${result.verification.issueCount.toLocaleString()} graph issue(s) remain.${detail} Run \`knodin repair\`.\n`;
163
+ }
164
+ function formatStatusHuman(result) {
165
+ const coverage = `${result.coverage.sourceFiles} source files, ${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols`;
166
+ if (result.status === "indexing" && result.activity) {
167
+ const count = result.activity.phaseTotal === undefined
168
+ ? ""
169
+ : ` ${result.activity.phaseCompleted}/${result.activity.phaseTotal}`;
170
+ const elapsed = Math.max(0, Math.floor((Date.now() - Date.parse(result.activity.startedAt)) / 1_000));
171
+ return `Graph update in progress: ${result.activity.phase}${count} — ${result.activity.message} (${elapsed}s elapsed; ${coverage}).\n`;
172
+ }
173
+ const integration = result.integration;
174
+ const integrationLine = integration
175
+ ? `Agent integration: ${integration.scope}${integration.agents.length > 0 ? ` (${integration.agents.join(", ")})` : ""}.\n`
176
+ : "Agent integration: unconfigured. AI agents will not discover knodin automatically; run `knodin configure --scope personal`.\n";
177
+ const lifecycleLine = result.lifecycle
178
+ ? result.lifecycle.status === "healthy"
179
+ ? "Hooks: installed and executable.\n"
180
+ : `Lifecycle refresh: ${result.lifecycle.status}; ${result.lifecycle.issues[0] ?? "refresh capability is not verified"}. Run \`knodin init\`.\n`
181
+ : "";
182
+ const head = (value) => value?.slice(0, 12) ?? "unknown";
183
+ const distance = result.freshness.commitDistance === null
184
+ ? ""
185
+ : ` by ${result.freshness.commitDistance.toLocaleString()} commit(s)`;
186
+ const freshnessLine = `Freshness: ${result.freshness.state}; indexed ${head(result.freshness.indexedHead)}, ` +
187
+ `current ${head(result.freshness.currentHead)} (${result.freshness.commitRelation}${distance}); ` +
188
+ `${result.freshness.workingTree.pendingPaths ?? "unknown"} pending path(s).\n` +
189
+ `Last successful refresh: ${result.freshness.lastSuccessfulRefresh ?? "never"}.\n`;
190
+ if (result.status === "healthy")
191
+ return `Graph content is healthy: ${coverage} (knodin ${result.version}; ${result.verification.mode}).\n${freshnessLine}${lifecycleLine}${integrationLine}`;
192
+ if (result.status === "stale")
193
+ return `Graph content is intact but evidence is stale (${coverage}).\n${freshnessLine}${lifecycleLine}${integrationLine}Run \`knodin wait --fresh\` or issue a graph query to reconcile bounded drift.\n`;
194
+ const outstanding = result.missing.files.length + result.missing.records.length;
195
+ const firstIssue = result.missing.files[0] ?? result.missing.records[0];
196
+ const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
197
+ const repairCommand = result.lifecycle?.status === "degraded" &&
198
+ result.missing.records.every((record) => result.lifecycle?.issues.includes(record))
199
+ ? "Run `knodin init`."
200
+ : "Run `knodin repair`.";
201
+ return `Graph or lifecycle needs repair: ${outstanding} issue(s) found (${coverage}).${detail} ${repairCommand}\n${lifecycleLine}${integrationLine}`;
202
+ }
203
+ function humanLabel(key) {
204
+ return key.replace(/([a-z])([A-Z])/g, "$1 $2");
205
+ }
206
+ /** Render bounded CLI data for a terminal without turning it back into JSON. */
207
+ function formatHumanValue(value, indent = "", label) {
208
+ if (value === null || typeof value !== "object") {
209
+ return [`${indent}${label ? `${humanLabel(label)}: ` : ""}${String(value)}`];
210
+ }
211
+ if (Array.isArray(value)) {
212
+ if (value.length === 0)
213
+ return label ? [] : [`${indent}(none)`];
214
+ const lines = label ? [`${indent}${humanLabel(label)}:`] : [];
215
+ for (const item of value) {
216
+ if (item === null || typeof item !== "object")
217
+ lines.push(`${indent} - ${String(item)}`);
218
+ else
219
+ lines.push(...formatHumanValue(item, `${indent} - `));
220
+ }
221
+ return lines;
222
+ }
223
+ const entries = Object.entries(value).filter(([key]) => key !== "responseBudget");
224
+ const lines = label ? [`${indent}${humanLabel(label)}:`] : [];
225
+ for (const [key, child] of entries) {
226
+ if (child === null || typeof child !== "object") {
227
+ lines.push(`${indent}${label ? " " : ""}${humanLabel(key)}: ${String(child)}`);
228
+ }
229
+ else
230
+ lines.push(...formatHumanValue(child, `${indent}${label ? " " : ""}`, key));
231
+ }
232
+ return lines;
233
+ }
234
+ function formatGenericHuman(cmd, result) {
235
+ return `${[`${humanLabel(cmd)}:`, ...formatHumanValue(result, " ")].join("\n")}\n`;
236
+ }
237
+ function formatCompressionHuman(result) {
238
+ const omitted = result.omittedRanges.reduce((total, range) => total + range.lineCount, 0);
239
+ let fidelity = `Compressed output; ${omitted} line(s) omitted.`;
240
+ if (result.status === "insufficient-budget")
241
+ fidelity = `INSUFFICIENT BUDGET: ${result.fidelity.unpreservedSignals.length} detected signal line(s) are available only through retained drill-down.`;
242
+ else if (result.complete)
243
+ fidelity = "Complete output; nothing omitted.";
244
+ const artifact = result.artifact.retained
245
+ ? ` Retained artifact: ${result.artifact.id} (${result.artifact.path}).`
246
+ : " Raw retention disabled; omitted regions cannot be retrieved.";
247
+ const content = result.content ? `${result.content}\n` : "";
248
+ return (`${content}---\n${fidelity} ` +
249
+ `${result.output.lines}/${result.input.lines} lines, ${result.output.bytes}/${result.input.bytes} bytes; ` +
250
+ `exit=${result.exit.code ?? "unknown"}, signal=${result.exit.signal ?? "none"}.${artifact}\n`);
251
+ }
252
+ function formatCompressionReadHuman(result) {
253
+ const content = result.content ? `${result.content}\n` : "";
254
+ const redactionStatus = result.raw
255
+ ? "UNREDACTED raw view"
256
+ : `${result.secretRedactions} secret(s) redacted`;
257
+ return (`${content}---\nArtifact ${result.artifactId}, lines ${result.range.startLine}-${result.range.endLine}` +
258
+ ` of ${result.range.totalLines}; ${result.bytes}/${result.byteBudget} bytes; ` +
259
+ `${redactionStatus}.\n`);
260
+ }
261
+ function formatFailureRelations(label, relations) {
262
+ if (relations.length === 0)
263
+ return `${label}: none`;
264
+ const formatted = relations.map(({ symbol, file, line }) => {
265
+ const location = file ? ` (${file}:${line ?? "?"})` : "";
266
+ return `${symbol}${location}`;
267
+ });
268
+ return `${label}: ${formatted.join(", ")}`;
269
+ }
270
+ function formatFailureDiagnosisHuman(result) {
271
+ const lines = [
272
+ `Failure diagnosis: ${result.status}; ${result.diagnostics.length} resolved, ${result.unresolved.length} unresolved.`,
273
+ `Freshness: ${result.freshness.state}; indexed=${result.freshness.indexedHead ?? "unknown"}; current=${result.freshness.currentHead ?? "unknown"}.`,
274
+ ];
275
+ for (const diagnostic of result.diagnostics) {
276
+ const location = `${diagnostic.file}:${diagnostic.reference.line ?? "?"}`;
277
+ const identity = diagnostic.owner?.identity ? ` [${diagnostic.owner.identity}]` : "";
278
+ const owner = `${diagnostic.owner?.symbol ?? "no owning symbol"}${identity}`;
279
+ const packageDescription = diagnostic.package
280
+ ? `${diagnostic.package.name ?? "unnamed"} (${diagnostic.package.kind}, ${diagnostic.package.manifest})`
281
+ : "none";
282
+ lines.push("", `${location} -> ${owner}`, `Package: ${packageDescription}`, formatFailureRelations("Tests", diagnostic.tests), formatFailureRelations("Upstream", diagnostic.upstream), formatFailureRelations("Downstream", diagnostic.downstream));
283
+ const snippet = result.contextBundle.snippets.find(({ file }) => file === diagnostic.file);
284
+ if (snippet)
285
+ lines.push(snippet.content);
286
+ }
287
+ for (const unresolved of result.unresolved) {
288
+ const candidates = unresolved.candidates ? ` (${unresolved.candidates.join(", ")})` : "";
289
+ lines.push("", `Unresolved ${unresolved.reference.path}: ${unresolved.reason}${candidates}`);
290
+ }
291
+ lines.push("", ...result.limitations.map((limitation) => `Limitation: ${limitation}`));
292
+ return `${lines.join("\n")}\n`;
293
+ }
294
+ async function readBoundedStdin(maxBytes) {
295
+ const chunks = [];
296
+ let bytes = 0;
297
+ for await (const chunk of process.stdin) {
298
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
299
+ bytes += buffer.byteLength;
300
+ if (bytes > maxBytes)
301
+ throw new Error(`knodin compress: input exceeded bounded retention limit of ${maxBytes} bytes`);
302
+ chunks.push(buffer);
303
+ }
304
+ return Buffer.concat(chunks).toString("utf-8");
305
+ }
306
+ const CLEAR_TERMINAL_LINE = "\r\x1b[2K";
307
+ const PROGRESS_WORKER_CLOSE_TIMEOUT_MS = 2_000;
308
+ function createProgressWorkerRenderer(workerName, startMessage) {
309
+ const extension = fileURLToPath(import.meta.url).endsWith(".ts") ? ".ts" : ".js";
310
+ const workerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), `../src/${workerName}${extension}`);
311
+ const workerArgs = extension === ".ts" && path.basename(process.execPath).startsWith("node")
312
+ ? [...process.execArgv, workerPath]
313
+ : [workerPath];
314
+ const worker = spawn(process.execPath, workerArgs, {
315
+ stdio: ["pipe", "ignore", "inherit"],
316
+ env: { ...process.env, RECKON_FORCE_PROGRESS_TTY: "1" },
317
+ });
318
+ let failed = false;
319
+ const closed = new Promise((resolve) => {
320
+ worker.once("close", () => resolve());
321
+ worker.once("error", () => resolve());
322
+ });
323
+ worker.once("error", () => {
324
+ failed = true;
325
+ });
326
+ worker.stdin.once("error", () => {
327
+ failed = true;
328
+ });
329
+ const send = (message) => {
330
+ if (failed || worker.stdin.destroyed)
331
+ return;
332
+ try {
333
+ worker.stdin.write(`${JSON.stringify(message)}\n`);
334
+ }
335
+ catch {
336
+ failed = true;
337
+ }
338
+ };
339
+ return {
340
+ start: () => send(startMessage),
341
+ onProgress: (event) => send({ type: "progress", event }),
342
+ stop: async () => {
343
+ send({ type: "stop" });
344
+ worker.stdin.end();
345
+ const waitForClose = (timeoutMs) => new Promise((resolve) => {
346
+ let settled = false;
347
+ const finish = (closedCleanly) => {
348
+ if (settled)
349
+ return;
350
+ settled = true;
351
+ clearTimeout(timer);
352
+ resolve(closedCleanly);
353
+ };
354
+ const timer = setTimeout(() => finish(false), timeoutMs);
355
+ timer.unref();
356
+ void closed.then(() => finish(true));
357
+ });
358
+ if (!(await waitForClose(PROGRESS_WORKER_CLOSE_TIMEOUT_MS))) {
359
+ // Rendering is observational. A wedged terminal renderer must never
360
+ // hold graph work or its completion summary hostage.
361
+ worker.kill("SIGKILL");
362
+ if (!(await waitForClose(500)))
363
+ worker.unref();
364
+ }
365
+ process.stderr.write(CLEAR_TERMINAL_LINE);
366
+ },
367
+ };
368
+ }
369
+ function createInitRenderer(operation = "init") {
370
+ if (process.stderr.isTTY !== true) {
371
+ const renderer = createInitProgressRenderer({
372
+ stderr: process.stderr,
373
+ operation,
374
+ });
375
+ return {
376
+ start: () => renderer.start(),
377
+ onProgress: (event) => renderer.onProgress(event),
378
+ stop: async () => renderer.stop(),
379
+ };
380
+ }
381
+ return createProgressWorkerRenderer("init-progress-worker", {
382
+ type: "start",
383
+ operation,
384
+ });
385
+ }
386
+ async function main() {
387
+ const argv = process.argv.slice(2);
388
+ if (argv[0] === "__repository-init-worker") {
389
+ if (argv.length !== 2 || !path.isAbsolute(argv[1]))
390
+ throw new Error("knodin internal repository init worker requires one absolute path");
391
+ const repository = await fs.promises.realpath(argv[1]);
392
+ const interval = setInterval(() => {
393
+ process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
394
+ }, 50);
395
+ interval.unref();
396
+ process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
397
+ const engine = createEngine();
398
+ try {
399
+ const systemConfig = loadSystemConfiguration(repository);
400
+ const summary = await initializeRepositories([repository], {
401
+ // A portfolio worker owns exactly one already-discovered worktree.
402
+ // Do not recursively initialize nested repositories inside the same
403
+ // process, which would evade the per-repository memory boundary.
404
+ depth: 0,
405
+ include: [repository],
406
+ command: resolveCliRuntimeCommand(process),
407
+ index: (target) => engine.index(target),
408
+ status: (target) => engine.status(target),
409
+ agents: detectSupportedAgents(),
410
+ indexMode: (target) => indexModeForPath(systemConfig, target),
411
+ });
412
+ const result = summary.results.find(({ repository: target }) => target === repository);
413
+ if (!result)
414
+ throw new Error("repository init worker produced no repository result");
415
+ process.send?.({ type: "rss", rssBytes: process.memoryUsage().rss });
416
+ process.send?.({ type: "result", result });
417
+ if (result.status === "failed")
418
+ process.exitCode = 1;
419
+ }
420
+ finally {
421
+ clearInterval(interval);
422
+ await engine.close();
423
+ }
424
+ return;
425
+ }
426
+ // Keep a normalized argv for compatibility dispatch while Commander remains
427
+ // the authoritative grammar, option parser, validator, and help source.
428
+ const { rest: cleaned } = extractRepoFlag(argv);
429
+ const [cmd, ...rawRest] = cleaned;
430
+ if (!cmd || cmd === "-h" || cmd === "--help") {
431
+ process.stdout.write(renderCliHelp([], process.stdout.isTTY ? process.stdout.columns : undefined));
432
+ return;
433
+ }
434
+ if (cmd === "-v" || cmd === "--version" || cmd === "version") {
435
+ process.stdout.write(`${KNODIN_VERSION}\n`);
436
+ return;
437
+ }
438
+ if (argv.some((argument) => argument === "-h" || argument === "--help")) {
439
+ process.stdout.write(renderCliHelp(helpCommandPath(argv), process.stdout.isTTY ? process.stdout.columns : undefined));
440
+ return;
441
+ }
442
+ const invocation = parseCliInvocation(argv);
443
+ const optionKey = (flag) => flag.slice(2).replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase());
444
+ const selectorValue = (flag) => {
445
+ const value = invocation.options[optionKey(flag)];
446
+ return typeof value === "string" || typeof value === "number" ? String(value) : undefined;
447
+ };
448
+ const repoFlag = selectorValue("--repo");
449
+ const runtimeCommand = resolveCliRuntimeCommand(process);
450
+ // hook-refresh is an internal machine-to-machine command. Keep it JSON even
451
+ // when an older installed hook predates the explicit --json argument.
452
+ const jsonOutput = invocation.options.json === true || cmd === "hook-refresh";
453
+ // `--json` is a shared output flag. Repair owns its richer --json/--jsonl
454
+ // parser; all other commands receive their original arguments minus it.
455
+ const rest = cmd === "repair" ? rawRest : rawRest.filter((argument) => argument !== "--json");
456
+ const selector = {
457
+ identity: selectorValue("--identity"),
458
+ file: selectorValue("--file"),
459
+ kind: selectorValue("--kind"),
460
+ toIdentity: selectorValue("--to-identity"),
461
+ toFile: selectorValue("--to-file"),
462
+ toKind: selectorValue("--to-kind"),
463
+ };
464
+ const responseBudget = {
465
+ bytes: selectorValue("--bytes") ? Number(selectorValue("--bytes")) : undefined,
466
+ tokens: selectorValue("--tokens") ? Number(selectorValue("--tokens")) : undefined,
467
+ items: selectorValue("--items") ? Number(selectorValue("--items")) : undefined,
468
+ };
469
+ for (const [flag, value, minimum] of [
470
+ ["--bytes", responseBudget.bytes, 256],
471
+ ["--tokens", responseBudget.tokens, 64],
472
+ ["--items", responseBudget.items, 1],
473
+ ]) {
474
+ if (value !== undefined && (!Number.isInteger(value) || value < minimum))
475
+ throw new Error(`knodin: ${flag} must be an integer >= ${minimum}`);
476
+ }
477
+ if (cmd === "init") {
478
+ explicitScope(rawRest);
479
+ const unsupported = rawRest.filter((argument, index) => {
480
+ if (argument === "--json" || argument.startsWith("--scope="))
481
+ return false;
482
+ if (argument === "--scope" || rawRest[index - 1] === "--scope")
483
+ return false;
484
+ return true;
485
+ });
486
+ if (unsupported.length > 0)
487
+ throw new Error(`knodin init: unknown option: ${unsupported[0]}`);
488
+ }
489
+ if (cmd === "configure") {
490
+ const status = rawRest.includes("--status");
491
+ const scope = explicitScope(rawRest);
492
+ const unsupported = rawRest.filter((argument, index) => {
493
+ if (argument === "--json" || argument === "--status" || argument.startsWith("--scope="))
494
+ return false;
495
+ if (argument === "--scope" || rawRest[index - 1] === "--scope")
496
+ return false;
497
+ return true;
498
+ });
499
+ if (unsupported.length > 0)
500
+ throw new Error(`knodin configure: unknown option: ${unsupported[0]}`);
501
+ if (status && scope)
502
+ throw new Error("knodin configure: --status and --scope are mutually exclusive");
503
+ if (!status && !scope)
504
+ throw new Error("knodin configure requires --scope personal|team|cli-only or --status");
505
+ }
506
+ if (cmd === "serve") {
507
+ const { startServer } = await import("../src/server.js");
508
+ await startServer();
509
+ return;
510
+ }
511
+ if (cmd === "fleet") {
512
+ if (repoFlag !== undefined) {
513
+ throw new Error("knodin fleet init accepts discovery roots, not --repo");
514
+ }
515
+ const plan = parseFleetInitArgs(rawRest, process.cwd());
516
+ if (!plan.json) {
517
+ process.stderr.write("warning: `knodin fleet init` is deprecated; use `knodin repos init --linked-worktrees=skip|include` (alias retained for two minor releases)\n");
518
+ }
519
+ const engine = createEngine();
520
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
521
+ const summary = await initializeRepositories(plan.roots, {
522
+ command: runtimeCommand,
523
+ depth: plan.depth,
524
+ dryRun: plan.dryRun,
525
+ worktrees: plan.worktrees,
526
+ index: (target) => engine.index(target),
527
+ status: (target) => engine.status(target),
528
+ agents: detectSupportedAgents(),
529
+ indexMode: (target) => indexModeForPath(systemConfig, target),
530
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command: runtimeCommand }),
531
+ });
532
+ await engine.close();
533
+ process.stdout.write(plan.json ? `${JSON.stringify(summary)}\n` : formatRepositoryHuman(summary));
534
+ process.exitCode = summary.exitCode;
535
+ return;
536
+ }
537
+ if (cmd === "repos") {
538
+ if (repoFlag !== undefined) {
539
+ throw new Error("knodin repos accepts discovery roots, not --repo");
540
+ }
541
+ const plan = parseRepositoryCommandArgs(rawRest, process.cwd());
542
+ if (plan.command === "discover") {
543
+ const discovery = await discoverRepositories(plan.roots, {
544
+ depth: plan.depth,
545
+ linkedWorktrees: plan.linkedWorktrees,
546
+ });
547
+ const engine = createEngine();
548
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
549
+ const repositories = [];
550
+ for (const record of discovery.repositories) {
551
+ repositories.push(await inventoryRepository(record, {
552
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
553
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
554
+ }));
555
+ }
556
+ await engine.close();
557
+ const output = {
558
+ schemaVersion: 1,
559
+ command: "discover",
560
+ linkedWorktrees: plan.linkedWorktrees,
561
+ ...discovery,
562
+ repositories,
563
+ };
564
+ process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
565
+ process.exitCode = discovery.issues.length > 0 ? 1 : 0;
566
+ return;
567
+ }
568
+ if (plan.command === "init") {
569
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
570
+ if (plan.dryRun) {
571
+ const summary = await initializeRepositories(plan.roots, {
572
+ command: runtimeCommand,
573
+ depth: plan.depth,
574
+ dryRun: true,
575
+ worktrees: plan.linkedWorktrees,
576
+ manifestPath: plan.manifestPath,
577
+ include: plan.include,
578
+ exclude: plan.exclude,
579
+ index: async () => undefined,
580
+ status: async () => {
581
+ throw new Error("dry-run must not inspect graph databases");
582
+ },
583
+ agents: detectSupportedAgents(),
584
+ indexMode: (target) => indexModeForPath(systemConfig, target),
585
+ });
586
+ const planned = summary.results.reduce((counts, result) => {
587
+ const key = result.plannedStatus ?? result.status;
588
+ counts[key] = (counts[key] ?? 0) + 1;
589
+ return counts;
590
+ }, {});
591
+ const output = {
592
+ schemaVersion: 1,
593
+ command: "init",
594
+ dryRun: true,
595
+ graphDatabasesOpened: 0,
596
+ modelsLoaded: 0,
597
+ selectedRepositories: summary.results.filter(({ message }) => message.startsWith("dry-run:")).length,
598
+ planned,
599
+ ...summary,
600
+ repositories: [],
601
+ };
602
+ process.stdout.write(plan.json ? `${JSON.stringify(output)}\n` : formatRepositoryHuman(summary));
603
+ process.exitCode = summary.exitCode;
604
+ return;
605
+ }
606
+ const engine = createEngine();
607
+ const summary = await initializeRepositories(plan.roots, {
608
+ command: runtimeCommand,
609
+ depth: plan.depth,
610
+ dryRun: false,
611
+ worktrees: plan.linkedWorktrees,
612
+ manifestPath: plan.manifestPath,
613
+ include: plan.include,
614
+ exclude: plan.exclude,
615
+ index: (target) => engine.index(target),
616
+ status: (target) => engine.status(target),
617
+ agents: detectSupportedAgents(),
618
+ indexMode: (target) => indexModeForPath(systemConfig, target),
619
+ isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command: runtimeCommand }),
620
+ });
621
+ const discovery = await discoverRepositories(plan.roots, {
622
+ depth: plan.depth,
623
+ linkedWorktrees: plan.linkedWorktrees,
624
+ });
625
+ const repositories = [];
626
+ const selectedPaths = new Set(summary.results.map(({ repository }) => repository));
627
+ for (const record of discovery.repositories.filter(({ path }) => selectedPaths.has(path))) {
628
+ repositories.push(await inventoryRepository(record, {
629
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
630
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
631
+ }));
632
+ }
633
+ await engine.close();
634
+ const output = { schemaVersion: 1, command: "init", ...summary, repositories };
635
+ process.stdout.write(plan.json ? `${JSON.stringify(output)}\n` : formatRepositoryHuman(summary));
636
+ process.exitCode = summary.exitCode;
637
+ return;
638
+ }
639
+ if (plan.command === "search") {
640
+ const engine = createEngine();
641
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
642
+ const output = await searchRepositories(plan.roots, plan.query ?? "", {
643
+ depth: plan.depth,
644
+ linkedWorktrees: plan.linkedWorktrees,
645
+ include: plan.include,
646
+ exclude: plan.exclude,
647
+ allowPartial: plan.allowPartial,
648
+ itemBudget: plan.itemBudget,
649
+ byteBudget: plan.byteBudget,
650
+ tokenBudget: plan.tokenBudget,
651
+ cursor: plan.cursor,
652
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
653
+ search: (query, target, limit, offset) => engine.search(query, target, limit, {
654
+ offset,
655
+ includeSource: true,
656
+ federate: false,
657
+ }),
658
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
659
+ });
660
+ await engine.close();
661
+ process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
662
+ process.exitCode = output.status === "unavailable" ? 1 : 0;
663
+ return;
664
+ }
665
+ const discovery = await discoverRepositories(plan.roots, {
666
+ depth: plan.depth,
667
+ linkedWorktrees: plan.linkedWorktrees,
668
+ });
669
+ const engine = createEngine();
670
+ const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
671
+ const repositories = [];
672
+ for (const repository of discovery.repositories) {
673
+ try {
674
+ if (plan.command === "doctor") {
675
+ const graph = attachLifecycleHealth(repository.path, await engine.status(repository.path, { audit: "deep" }));
676
+ const inventory = await inventoryRepository(repository, {
677
+ status: async () => graph,
678
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
679
+ });
680
+ repositories.push({
681
+ ...inventory,
682
+ diagnosis: await diagnoseInstallation(repository.path, {
683
+ currentVersion: KNODIN_VERSION,
684
+ runtimeCommand,
685
+ graph,
686
+ }),
687
+ });
688
+ }
689
+ else {
690
+ repositories.push(await inventoryRepository(repository, {
691
+ status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
692
+ systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
693
+ }));
694
+ }
695
+ }
696
+ catch (error) {
697
+ repositories.push({
698
+ path: repository.path,
699
+ status: "unknown",
700
+ error: error instanceof Error ? error.message : String(error),
701
+ });
702
+ }
703
+ }
704
+ await engine.close();
705
+ const output = {
706
+ schemaVersion: 1,
707
+ command: plan.command,
708
+ linkedWorktrees: plan.linkedWorktrees,
709
+ repositories,
710
+ issues: discovery.issues,
711
+ emptyRoots: discovery.emptyRoots,
712
+ staleLinkedWorktrees: discovery.staleLinkedWorktrees,
713
+ };
714
+ process.stdout.write(`${JSON.stringify(output, null, plan.json ? 0 : 2)}\n`);
715
+ process.exitCode =
716
+ discovery.issues.length > 0 ||
717
+ repositories.some((entry) => ("health" in entry && entry.health !== "healthy") ||
718
+ ("status" in entry && entry.status === "unknown"))
719
+ ? 1
720
+ : 0;
721
+ return;
722
+ }
723
+ if (cmd === "docs") {
724
+ if (repoFlag !== undefined)
725
+ throw new Error("knodin docs does not accept --repo");
726
+ const [topic, ...unsupported] = rawRest;
727
+ if (unsupported.length > 0)
728
+ throw new Error(`knodin docs: unknown argument ${unsupported[0]}`);
729
+ if (!topic || topic === "list") {
730
+ process.stdout.write(`${listDocTopics().join("\n")}\n`);
731
+ return;
732
+ }
733
+ const content = getDocSection(topic);
734
+ if (!content)
735
+ throw new Error(`knodin docs: unknown topic ${topic}`);
736
+ process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
737
+ return;
738
+ }
739
+ if (cmd === "update") {
740
+ if (repoFlag !== undefined)
741
+ throw new Error("knodin update does not accept --repo");
742
+ const [action, ...unsupported] = rest;
743
+ if (!action || !["status", "check", "explain", "apply", "rollback"].includes(action)) {
744
+ throw new Error("knodin update requires status, check, explain, apply, or rollback");
745
+ }
746
+ if (unsupported.length > 0)
747
+ throw new Error(`knodin update ${action}: unknown argument ${unsupported[0]}`);
748
+ const method = detectUpdateInstallMethod(runtimeCommand);
749
+ const runManager = async (argv) => {
750
+ const [executable, ...arguments_] = argv;
751
+ if (!executable)
752
+ return { status: 1, stdout: "", stderr: "missing manager command" };
753
+ const outcome = spawnSync(executable, arguments_, {
754
+ encoding: "utf-8",
755
+ timeout: 120_000,
756
+ maxBuffer: 4 * 1_024 * 1_024,
757
+ stdio: ["ignore", "pipe", "pipe"],
758
+ });
759
+ return {
760
+ status: outcome.status,
761
+ stdout: outcome.stdout ?? "",
762
+ stderr: outcome.stderr ?? outcome.error?.message ?? "",
763
+ };
764
+ };
765
+ const healthCheck = async () => {
766
+ const [executable, ...arguments_] = runtimeCommand;
767
+ if (!executable)
768
+ return false;
769
+ const outcome = spawnSync(executable, [...arguments_, "--version"], {
770
+ encoding: "utf-8",
771
+ timeout: 10_000,
772
+ stdio: ["ignore", "pipe", "pipe"],
773
+ });
774
+ return outcome.status === 0 && /^\d+\.\d+\.\d+/.test(outcome.stdout.trim());
775
+ };
776
+ const options = {
777
+ currentVersion: KNODIN_VERSION,
778
+ installMethod: method,
779
+ env: process.env,
780
+ runManager,
781
+ healthCheck,
782
+ };
783
+ let output;
784
+ if (action === "status")
785
+ output = trustedUpdateStatus(options);
786
+ else if (action === "check") {
787
+ try {
788
+ output = await checkTrustedUpdate(options);
789
+ }
790
+ finally {
791
+ if (process.env.RECKON_UPDATE_BACKGROUND === "1")
792
+ releaseScheduledUpdateCheck();
793
+ }
794
+ }
795
+ else if (action === "explain")
796
+ output = explainTrustedUpdate(options);
797
+ else if (action === "apply")
798
+ output = await applyTrustedUpdate(options);
799
+ else
800
+ output = await rollbackTrustedUpdate(options);
801
+ process.stdout.write(jsonOutput ? `${JSON.stringify(output)}\n` : formatGenericHuman("update", output));
802
+ return;
803
+ }
804
+ if (process.stdout.isTTY &&
805
+ claimScheduledUpdateCheck({
806
+ currentVersion: KNODIN_VERSION,
807
+ installMethod: detectUpdateInstallMethod(runtimeCommand),
808
+ env: process.env,
809
+ })) {
810
+ const [executable, ...arguments_] = runtimeCommand;
811
+ if (executable) {
812
+ const background = spawn(executable, [...arguments_, "update", "check", "--json"], {
813
+ detached: true,
814
+ stdio: "ignore",
815
+ env: { ...process.env, RECKON_UPDATE_BACKGROUND: "1" },
816
+ });
817
+ background.unref();
818
+ }
819
+ }
820
+ const resolved = resolveRepo(repoFlag, process.cwd());
821
+ if (!resolved.ok) {
822
+ process.stderr.write(`${resolved.error}\n`);
823
+ process.exit(1);
824
+ }
825
+ const repo = resolved.repo;
826
+ if (cmd === "doctor") {
827
+ const gitProbe = spawnSync(gitExecutable(), ["rev-parse", "--is-inside-work-tree"], {
828
+ cwd: repo,
829
+ encoding: "utf-8",
830
+ stdio: ["ignore", "pipe", "ignore"],
831
+ });
832
+ if (gitProbe.status !== 0 || gitProbe.stdout.trim() !== "true") {
833
+ const portfolio = {
834
+ schemaVersion: 1,
835
+ status: "portfolio-root",
836
+ path: repo,
837
+ graph: "not-inspected",
838
+ warning: "Target is not a Git worktree; treating a portfolio parent as one repository would be misleading.",
839
+ remediation: [`knodin repos doctor ${JSON.stringify(repo)}`],
840
+ };
841
+ process.stdout.write(jsonOutput ? `${JSON.stringify(portfolio)}\n` : formatGenericHuman("doctor", portfolio));
842
+ process.exitCode = 1;
843
+ return;
844
+ }
845
+ }
846
+ if (cmd === "compress") {
847
+ const valueFlags = new Set([
848
+ "--strategy",
849
+ "--adapter",
850
+ "--lines",
851
+ "--max-output-bytes",
852
+ "--context",
853
+ "--exit-code",
854
+ "--signal",
855
+ "--max-input-bytes",
856
+ "--start",
857
+ "--end",
858
+ "--limit",
859
+ ]);
860
+ const positionals = rest.filter((argument, index) => !argument.startsWith("--") && !valueFlags.has(rest[index - 1]));
861
+ const integer = (flag, fallback, minimum, maximum) => {
862
+ const raw = selectorValue(flag);
863
+ const value = raw === undefined ? fallback : Number(raw);
864
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
865
+ throw new Error(`knodin compress: ${flag} must be an integer from ${minimum} to ${maximum}`);
866
+ return value;
867
+ };
868
+ const action = positionals[0] === "read" || positionals[0] === "diagnose" || positionals[0] === "delete"
869
+ ? positionals[0]
870
+ : "create";
871
+ const artifactId = action === "create" ? undefined : positionals[1];
872
+ if (action !== "create" && !artifactId)
873
+ throw new Error(`knodin compress ${action} requires an artifact id`);
874
+ let compressionResult;
875
+ let diagnosisUnavailable = false;
876
+ if (action === "diagnose") {
877
+ const diagnosisEngine = createEngine();
878
+ try {
879
+ const health = await inspectGraphQueryHealth(repo, (target) => diagnosisEngine.status(target, { audit: "cached" }));
880
+ if (health.available) {
881
+ compressionResult = await diagnoseFailure(diagnosisEngine, repo, {
882
+ artifactId: artifactId ?? "",
883
+ maxDiagnostics: integer("--limit", 10, 1, 50),
884
+ contextLines: integer("--context", 2, 0, 10),
885
+ contextByteBudget: integer("--max-output-bytes", 16_384, 256, 128 * 1024),
886
+ });
887
+ }
888
+ else {
889
+ compressionResult = health;
890
+ diagnosisUnavailable = true;
891
+ process.exitCode = 1;
892
+ }
893
+ }
894
+ finally {
895
+ await diagnosisEngine.close();
896
+ }
897
+ }
898
+ else if (action === "read") {
899
+ compressionResult = readOutputArtifact(repo, artifactId ?? "", {
900
+ startLine: integer("--start", 1, 1, Number.MAX_SAFE_INTEGER),
901
+ endLine: selectorValue("--end")
902
+ ? integer("--end", 200, 1, Number.MAX_SAFE_INTEGER)
903
+ : undefined,
904
+ byteBudget: integer("--max-output-bytes", 16_384, 256, 4 * 1024 * 1024),
905
+ raw: rest.includes("--raw"),
906
+ });
907
+ }
908
+ else if (action === "delete") {
909
+ compressionResult = deleteOutputArtifact(repo, artifactId ?? "");
910
+ }
911
+ else {
912
+ const strategy = selectorValue("--strategy") ?? "smart";
913
+ if (!["smart", "head-tail", "errors-only"].includes(strategy))
914
+ throw new Error("knodin compress: invalid --strategy");
915
+ const adapter = selectorValue("--adapter") ?? "auto";
916
+ if (![
917
+ "auto",
918
+ "generic",
919
+ "vitest",
920
+ "jest",
921
+ "pytest",
922
+ "go-test",
923
+ "maven",
924
+ "gradle",
925
+ "dotnet",
926
+ "cargo",
927
+ ].includes(adapter))
928
+ throw new Error("knodin compress: invalid --adapter");
929
+ const maxInputBytes = integer("--max-input-bytes", 16 * 1024 * 1024, 1, 64 * 1024 * 1024);
930
+ const request = {
931
+ exitCode: selectorValue("--exit-code") ? integer("--exit-code", 0, 0, 255) : undefined,
932
+ signal: selectorValue("--signal"),
933
+ strategy: strategy,
934
+ adapter: adapter,
935
+ lineBudget: integer("--lines", 200, 1, 10_000),
936
+ byteBudget: integer("--max-output-bytes", 16_384, 256, 4 * 1024 * 1024),
937
+ contextLines: integer("--context", 1, 0, 10),
938
+ maxInputBytes,
939
+ retain: !rest.includes("--no-retain"),
940
+ redactSecrets: !rest.includes("--no-redact"),
941
+ };
942
+ const input = positionals[0] === "create" ? (positionals[1] ?? "-") : (positionals[0] ?? "-");
943
+ compressionResult =
944
+ input === "-"
945
+ ? compressOutput(repo, { ...request, text: await readBoundedStdin(maxInputBytes) })
946
+ : compressOutputFile(repo, input, request);
947
+ }
948
+ if (jsonOutput)
949
+ process.stdout.write(`${JSON.stringify(compressionResult)}\n`);
950
+ else if (action === "diagnose" && !diagnosisUnavailable)
951
+ process.stdout.write(formatFailureDiagnosisHuman(compressionResult));
952
+ else if (action === "diagnose")
953
+ process.stdout.write(formatGenericHuman("compress diagnose", compressionResult));
954
+ else if (action === "read")
955
+ process.stdout.write(formatCompressionReadHuman(compressionResult));
956
+ else if (action === "create")
957
+ process.stdout.write(formatCompressionHuman(compressionResult));
958
+ else
959
+ process.stdout.write(formatGenericHuman("compress delete", compressionResult));
960
+ return;
961
+ }
962
+ const engine = createEngine();
963
+ let result;
964
+ let repairOutput;
965
+ let repairWasPlan = false;
966
+ let repairExitCode = 0;
967
+ let statusWasWatched = false;
968
+ const graphRead = async (run) => {
969
+ const health = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
970
+ if (!health.available) {
971
+ process.exitCode = 1;
972
+ return health;
973
+ }
974
+ const value = await run();
975
+ const verified = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
976
+ if (!verified.available) {
977
+ process.exitCode = 1;
978
+ return verified;
979
+ }
980
+ return decorateGraphQueryResult(value, verified.state, verified.graph.freshness);
981
+ };
982
+ switch (cmd) {
983
+ case "doctor": {
984
+ const client = selectorValue("--client");
985
+ if (client !== undefined && !["claude", "codex", "gemini", "antigravity"].includes(client))
986
+ throw new Error("knodin doctor: --client must be claude, codex, gemini, or antigravity");
987
+ const unsupported = rest.filter((argument, index) => argument !== "--client" && rest[index - 1] !== "--client");
988
+ if (unsupported.length > 0)
989
+ throw new Error(`knodin doctor: unknown option ${unsupported[0]}`);
990
+ const diagnosis = await diagnoseInstallation(repo, {
991
+ currentVersion: KNODIN_VERSION,
992
+ runtimeCommand: [...runtimeCommand, "serve"],
993
+ graph: await engine.status(repo, { audit: "deep" }),
994
+ client,
995
+ });
996
+ const manager = diagnosis.manager.name;
997
+ diagnosis.update = await checkTrustedUpdate({
998
+ currentVersion: KNODIN_VERSION,
999
+ installMethod: ["npm", "mise", "volta", "nvm", "fnm", "asdf", "homebrew"].includes(manager ?? "")
1000
+ ? manager
1001
+ : "unknown",
1002
+ env: process.env,
1003
+ });
1004
+ result = diagnosis;
1005
+ break;
1006
+ }
1007
+ case "system": {
1008
+ const allowPartial = rest.includes("--allow-partial");
1009
+ const [action, systemId, ...unsupported] = rest.filter((argument) => argument !== "--allow-partial");
1010
+ if (!action || !["list", "show", "validate", "query"].includes(action)) {
1011
+ throw new Error("knodin system requires list, show, validate, or query");
1012
+ }
1013
+ if (allowPartial && action !== "query")
1014
+ throw new Error(`knodin system ${action}: --allow-partial applies only to query`);
1015
+ if (unsupported.length > 0)
1016
+ throw new Error(`knodin system ${action}: unknown argument ${unsupported[0]}`);
1017
+ const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
1018
+ if (action === "list") {
1019
+ if (systemId)
1020
+ throw new Error("knodin system list accepts no system id");
1021
+ result = {
1022
+ schemaVersion: config.schemaVersion,
1023
+ systems: config.systems.map(({ id, components }) => ({
1024
+ id,
1025
+ componentCount: components.length,
1026
+ })),
1027
+ };
1028
+ break;
1029
+ }
1030
+ if (!systemId)
1031
+ throw new Error(`knodin system ${action} requires <system-id>`);
1032
+ const system = config.systems.find(({ id }) => id === systemId);
1033
+ if (!system) {
1034
+ result = {
1035
+ status: "not-found",
1036
+ systemId,
1037
+ available: config.systems.map(({ id }) => id),
1038
+ };
1039
+ process.exitCode = 1;
1040
+ break;
1041
+ }
1042
+ if (action === "show") {
1043
+ result = { status: "ok", system, repositories: config.repositories };
1044
+ }
1045
+ else if (action === "validate") {
1046
+ const validation = await validateSystemHealth(config, systemId, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })), repo);
1047
+ result = { systemId, ...validation };
1048
+ if (!validation.valid)
1049
+ process.exitCode = 1;
1050
+ }
1051
+ else {
1052
+ const validation = await validateSystemHealth(config, systemId, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })), repo);
1053
+ result = queryConfiguredSystem(config, systemId, allowPartial, validation);
1054
+ if (result.status === "unavailable")
1055
+ process.exitCode = 1;
1056
+ }
1057
+ break;
1058
+ }
1059
+ case "hook-refresh": {
1060
+ const [kind, first, second] = rest;
1061
+ let event;
1062
+ if (kind === "commit")
1063
+ event = { kind };
1064
+ else if (kind === "checkout" && first && second) {
1065
+ event = { kind, before: first, after: second };
1066
+ }
1067
+ else if (kind === "merge" && first && second) {
1068
+ event = { kind, before: first, after: second };
1069
+ }
1070
+ else if (kind === "rewrite" && first) {
1071
+ event = { kind, inputPath: first };
1072
+ }
1073
+ else {
1074
+ throw new Error("knodin hook-refresh: invalid lifecycle event");
1075
+ }
1076
+ const indexed = await refreshFromGitEvent(repo, event, (target, files) => engine.index(target, files));
1077
+ result = { indexed };
1078
+ break;
1079
+ }
1080
+ case "init": {
1081
+ const scope = await chooseInitScope(rawRest, repo);
1082
+ const agents = scope === "team" ? [] : integrationAgents(repo);
1083
+ const renderer = createInitRenderer();
1084
+ const activity = createIndexActivityReporter(repo);
1085
+ activity.start();
1086
+ renderer.start();
1087
+ let paths;
1088
+ try {
1089
+ try {
1090
+ paths = await initializeRepository(repo, {
1091
+ command: runtimeCommand,
1092
+ index: (target, options) => engine.index(target, undefined, false, options),
1093
+ scope,
1094
+ agents,
1095
+ onProgress: (event) => {
1096
+ activity.update(event);
1097
+ renderer.onProgress(event);
1098
+ },
1099
+ });
1100
+ }
1101
+ finally {
1102
+ await renderer.stop();
1103
+ activity.stop();
1104
+ }
1105
+ }
1106
+ catch (error) {
1107
+ if (!(error instanceof InitializationHealthError))
1108
+ throw error;
1109
+ process.stderr.write(`${error.message}\n`);
1110
+ await engine.close();
1111
+ process.exitCode = 1;
1112
+ return;
1113
+ }
1114
+ result = {
1115
+ status: "success",
1116
+ message: "knodin initialized successfully. Git lifecycle hooks configured.",
1117
+ paths,
1118
+ };
1119
+ break;
1120
+ }
1121
+ case "configure": {
1122
+ if (rawRest.includes("--status")) {
1123
+ result = readRepositoryIntegrationConfig(repo) ?? {
1124
+ scope: "unconfigured",
1125
+ agents: [],
1126
+ warning: "AI agents are not configured by knodin. Run `knodin configure --scope personal`.",
1127
+ };
1128
+ break;
1129
+ }
1130
+ const scope = explicitScope(rawRest);
1131
+ if (!scope)
1132
+ throw new Error("knodin configure: missing --scope");
1133
+ if (!fs.existsSync(path.join(repo, ".reckon", "db.sqlite"))) {
1134
+ throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");
1135
+ }
1136
+ const agents = scope === "team" ? [] : integrationAgents(repo);
1137
+ const paths = await initializeRepository(repo, {
1138
+ command: runtimeCommand,
1139
+ index: async () => undefined,
1140
+ scope,
1141
+ agents,
1142
+ allowTrackedTransition: true,
1143
+ });
1144
+ result = {
1145
+ status: "success",
1146
+ message: `knodin agent integration changed to ${scope}.`,
1147
+ graphInitialization: "unchanged",
1148
+ nextAction: "run `knodin status` and reload the configured client",
1149
+ paths,
1150
+ };
1151
+ break;
1152
+ }
1153
+ case "index": {
1154
+ // Target the repo unambiguously and never report success for a no-op:
1155
+ // a lone directory positional means "index this repo", file positionals
1156
+ // must resolve inside the repo, and a run touching zero files fails loud.
1157
+ const positionals = extractPositionals(rest);
1158
+ const plan = planIndex(repoFlag, positionals, process.cwd());
1159
+ if (!plan.ok) {
1160
+ process.stderr.write(`${plan.error}\n`);
1161
+ await engine.close();
1162
+ process.exit(1);
1163
+ }
1164
+ const clean = rest.includes("--clean") || rest.includes("--force");
1165
+ const renderer = createInitRenderer("index");
1166
+ const activity = createIndexActivityReporter(plan.repo);
1167
+ activity.start();
1168
+ renderer.start();
1169
+ let indexResult;
1170
+ try {
1171
+ indexResult = await engine.index(plan.repo, plan.files, clean, {
1172
+ onProgress: (event) => {
1173
+ activity.update(event);
1174
+ renderer.onProgress(event);
1175
+ },
1176
+ });
1177
+ }
1178
+ finally {
1179
+ await renderer.stop();
1180
+ activity.stop();
1181
+ }
1182
+ result = indexResult;
1183
+ const indexedCheck = checkIndexed([...indexResult.indexed, ...indexResult.unchanged], plan.repo);
1184
+ if (!indexedCheck.ok) {
1185
+ process.stderr.write(`${indexedCheck.error}\n`);
1186
+ await engine.close();
1187
+ process.exit(1);
1188
+ }
1189
+ if (indexResult.verification.status !== "healthy") {
1190
+ process.stderr.write(formatIndexVerificationError(indexResult));
1191
+ await engine.close();
1192
+ process.exit(1);
1193
+ }
1194
+ break;
1195
+ }
1196
+ case "status": {
1197
+ const snapshot = async () => ({
1198
+ ...attachLifecycleHealth(repo, await engine.status(repo, {
1199
+ audit: rest.includes("--deep") ? "deep" : "cached",
1200
+ })),
1201
+ integration: inspectRepositoryIntegrationStatus(repo),
1202
+ update: trustedUpdateStatus({
1203
+ currentVersion: KNODIN_VERSION,
1204
+ installMethod: detectUpdateInstallMethod(runtimeCommand),
1205
+ env: process.env,
1206
+ }),
1207
+ });
1208
+ if (rest.includes("--watch")) {
1209
+ const intervalRaw = selectorValue("--interval");
1210
+ const intervalSeconds = intervalRaw === undefined ? 1 : Number(intervalRaw);
1211
+ if (!Number.isFinite(intervalSeconds) || intervalSeconds < 0.1 || intervalSeconds > 60)
1212
+ throw new Error("knodin status: --interval must be between 0.1 and 60 seconds");
1213
+ let watching = true;
1214
+ const stop = () => {
1215
+ watching = false;
1216
+ };
1217
+ process.once("SIGINT", stop);
1218
+ process.once("SIGTERM", stop);
1219
+ while (watching) {
1220
+ const observed = await snapshot();
1221
+ process.stdout.write(`${JSON.stringify({ observedAt: new Date().toISOString(), ...observed })}\n`);
1222
+ await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1_000));
1223
+ }
1224
+ process.removeListener("SIGINT", stop);
1225
+ process.removeListener("SIGTERM", stop);
1226
+ statusWasWatched = true;
1227
+ result = null;
1228
+ }
1229
+ else
1230
+ result = await snapshot();
1231
+ break;
1232
+ }
1233
+ case "wait": {
1234
+ if (!rest.includes("--fresh"))
1235
+ throw new Error("knodin wait requires --fresh");
1236
+ const timeoutRaw = selectorValue("--timeout");
1237
+ const timeoutSeconds = timeoutRaw === undefined ? 30 : Number(timeoutRaw);
1238
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 0 || timeoutSeconds > 300)
1239
+ throw new Error("knodin wait: --timeout must be between 0 and 300 seconds");
1240
+ result = await waitForFresh(engine, repo, Math.round(timeoutSeconds * 1_000));
1241
+ if (result.status !== "fresh")
1242
+ process.exitCode = 1;
1243
+ break;
1244
+ }
1245
+ case "repair": {
1246
+ const options = parseRepairCliArgs(rest);
1247
+ if (options.plan) {
1248
+ result = createRepairPlan(await engine.status(repo, { audit: "deep" }));
1249
+ repairOutput = options.output;
1250
+ repairWasPlan = true;
1251
+ break;
1252
+ }
1253
+ const progressMode = resolveRepairProgressMode(options, process.env, process.stderr.isTTY);
1254
+ const renderer = progressMode === "tty"
1255
+ ? createProgressWorkerRenderer("repair-progress-worker", { type: "start" })
1256
+ : (() => {
1257
+ const direct = createRepairProgressRenderer({
1258
+ mode: progressMode,
1259
+ intervalMs: options.progressIntervalMs,
1260
+ stdout: process.stdout,
1261
+ stderr: process.stderr,
1262
+ });
1263
+ return {
1264
+ start: () => direct.start(),
1265
+ onProgress: (event) => direct.onProgress(event),
1266
+ stop: async () => direct.stop(),
1267
+ };
1268
+ })();
1269
+ const controller = new AbortController();
1270
+ const abortRepair = () => controller.abort();
1271
+ process.once("SIGINT", abortRepair);
1272
+ renderer.start();
1273
+ try {
1274
+ result = await engine.repair(repo, {
1275
+ signal: controller.signal,
1276
+ onProgress: (event) => renderer.onProgress(event),
1277
+ });
1278
+ }
1279
+ finally {
1280
+ process.removeListener("SIGINT", abortRepair);
1281
+ await renderer.stop();
1282
+ }
1283
+ repairOutput = options.output;
1284
+ if (result.cancelled)
1285
+ repairExitCode = 130;
1286
+ break;
1287
+ }
1288
+ case "refresh-artifacts": {
1289
+ const event = rest[0] ?? "code-change";
1290
+ if (event !== "checkout" && event !== "merge" && event !== "code-change") {
1291
+ process.stderr.write("knodin refresh-artifacts accepts checkout, merge, or code-change\n");
1292
+ process.exit(1);
1293
+ }
1294
+ const refreshResult = refreshExternalGraphArtifacts(repo, event);
1295
+ writeArtifactRefreshRecord(repo, refreshResult);
1296
+ result = refreshResult;
1297
+ break;
1298
+ }
1299
+ case "explain": {
1300
+ const symbol = rest[0];
1301
+ if (!symbol) {
1302
+ process.stderr.write("knodin explain requires a <symbol>\n");
1303
+ process.exit(1);
1304
+ }
1305
+ result = await graphRead(async () => {
1306
+ const explained = await engine.explain(symbol, repo, rest[1] === "minimal" ? "minimal" : "standard", selector);
1307
+ return rest[1] === "source" && !explained.ambiguity
1308
+ ? {
1309
+ mode: "source",
1310
+ identity: explained.identity,
1311
+ symbol: explained.symbol,
1312
+ source: explained.source,
1313
+ staleness: explained.staleness,
1314
+ }
1315
+ : explained;
1316
+ });
1317
+ break;
1318
+ }
1319
+ case "review": {
1320
+ const plan = parseReviewArgs(rest);
1321
+ result = await graphRead(() => engine.review(plan.base, repo, plan.detailLevel, plan.options));
1322
+ break;
1323
+ }
1324
+ case "map":
1325
+ result = await graphRead(() => engine.map(repo, rest.includes("--standard") ? "standard" : "minimal", {
1326
+ topN: selectorValue("--top") ? Number(selectorValue("--top")) : undefined,
1327
+ sort: selectorValue("--sort"),
1328
+ relationKinds: selectorValue("--relations")?.split(",").filter(Boolean),
1329
+ }));
1330
+ break;
1331
+ case "wiki": {
1332
+ const health = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1333
+ if (!health.available) {
1334
+ result = health;
1335
+ process.exitCode = 1;
1336
+ break;
1337
+ }
1338
+ const force = rest.includes("--force");
1339
+ const summary = await engine.wiki(repo, force);
1340
+ await engine.close();
1341
+ process.stdout.write(`wrote ${summary.written.length} pages, skipped ${summary.skipped.length}\n`);
1342
+ process.exitCode = 0;
1343
+ return;
1344
+ }
1345
+ case "visualize": {
1346
+ const entry = rest.find((value, index) => !value.startsWith("--") && !rest[index - 1]?.startsWith("--"));
1347
+ const outputPath = selectorValue("--output");
1348
+ if (!entry)
1349
+ throw new Error("knodin visualize requires an <entry> selector");
1350
+ if (!outputPath)
1351
+ throw new Error("knodin visualize requires --output <path.html>");
1352
+ result = await graphRead(() => writeVisualization(engine, repo, {
1353
+ entry,
1354
+ outputPath,
1355
+ depth: selectorValue("--depth") ? Number(selectorValue("--depth")) : undefined,
1356
+ byteBudget: selectorValue("--max-bytes")
1357
+ ? Number(selectorValue("--max-bytes"))
1358
+ : undefined,
1359
+ selector: {
1360
+ identity: selector.identity,
1361
+ file: selector.file,
1362
+ kind: selector.kind,
1363
+ },
1364
+ }));
1365
+ break;
1366
+ }
1367
+ case "search": {
1368
+ const query = rest[0];
1369
+ if (!query) {
1370
+ process.stderr.write("knodin search requires a <query>\n");
1371
+ process.exit(1);
1372
+ }
1373
+ const positionalLimit = rest[1] && !rest[1].startsWith("--") ? rest[1] : undefined;
1374
+ const limit = Number(selectorValue("--limit") ?? positionalLimit ?? 5);
1375
+ if (!Number.isInteger(limit) || limit < 1)
1376
+ throw new Error("knodin search: limit must be a positive integer");
1377
+ result = await graphRead(() => engine.search(query, repo, limit, {
1378
+ languages: selectorValue("--languages")?.split(",").filter(Boolean),
1379
+ extensions: selectorValue("--extensions")?.split(",").filter(Boolean),
1380
+ kinds: selectorValue("--kinds")?.split(",").filter(Boolean),
1381
+ path: selectorValue("--path"),
1382
+ testScope: rest.includes("--tests-only")
1383
+ ? "test"
1384
+ : rest.includes("--production-only")
1385
+ ? "production"
1386
+ : "all",
1387
+ includeSource: !rest.includes("--no-source"),
1388
+ offset: selectorValue("--offset") ? Number(selectorValue("--offset")) : 0,
1389
+ }));
1390
+ break;
1391
+ }
1392
+ case "pack": {
1393
+ const action = rest[0];
1394
+ const diffScope = selectorValue("--diff-scope");
1395
+ if (diffScope && !["unstaged", "staged", "all", "compare"].includes(diffScope))
1396
+ throw new Error("knodin pack: invalid --diff-scope");
1397
+ if (action === "read") {
1398
+ if (!rest[1])
1399
+ throw new Error("knodin pack read requires an artifact path");
1400
+ result = readPackedArtifact(repo, rest[1], Number(selectorValue("--start") ?? 1), Number(selectorValue("--end") ?? 200), Number(selectorValue("--bytes") ?? 16_384));
1401
+ }
1402
+ else if (action === "grep") {
1403
+ if (!rest[1] || !rest[2])
1404
+ throw new Error("knodin pack grep requires an artifact path and regex");
1405
+ result = grepPackedArtifact(repo, rest[1], rest[2], selectorValue("--flags") ?? "", Number(selectorValue("--limit") ?? 100));
1406
+ }
1407
+ else {
1408
+ const policies = {};
1409
+ for (const assignment of selectorValue("--policy")?.split(",") ?? []) {
1410
+ const [glob, policy] = assignment.split("=");
1411
+ if (!glob || !["full", "summary", "structure-only"].includes(policy))
1412
+ throw new Error("knodin pack: invalid --policy assignment");
1413
+ policies[glob] = policy;
1414
+ }
1415
+ result = exportContext(repo, {
1416
+ format: selectorValue("--format") ?? "markdown",
1417
+ include: selectorValue("--include")?.split(",").filter(Boolean),
1418
+ exclude: selectorValue("--exclude")?.split(",").filter(Boolean),
1419
+ policies,
1420
+ alreadyPresent: selectorValue("--already-present")?.split(",").filter(Boolean),
1421
+ chatFiles: selectorValue("--chat-files")?.split(",").filter(Boolean),
1422
+ lineNumbers: rest.includes("--line-numbers"),
1423
+ includeTree: rest.includes("--tree"),
1424
+ byteBudget: Number(selectorValue("--bytes") ?? 65_536),
1425
+ tokenBudget: Number(selectorValue("--tokens") ?? 16_384),
1426
+ outputPath: selectorValue("--output"),
1427
+ git: diffScope || selectorValue("--log")
1428
+ ? {
1429
+ diffScope: diffScope,
1430
+ from: selectorValue("--from"),
1431
+ to: selectorValue("--to"),
1432
+ log: selectorValue("--log") ? Number(selectorValue("--log")) : undefined,
1433
+ }
1434
+ : undefined,
1435
+ });
1436
+ }
1437
+ break;
1438
+ }
1439
+ case "query": {
1440
+ const pattern = rest[0];
1441
+ const repoWide = REPO_WIDE_QUERY_PATTERNS.includes(pattern);
1442
+ // architecture_overview's only positional is the detail level (minimal|
1443
+ // standard), not a target — keep its target empty so it's not misreported.
1444
+ const target = pattern === "architecture_overview" ||
1445
+ pattern === "import_cycles" ||
1446
+ rest[1]?.startsWith("--")
1447
+ ? ""
1448
+ : (rest[1] ?? "");
1449
+ if (!pattern) {
1450
+ process.stderr.write("knodin query requires a <pattern> (lsp_diagnostics|lsp_definitions|lsp_declarations|lsp_implementations|callers_of|callees_of|imports_of|importers_of|import_cycles|file_summary|batch_outline|project_overview|shortest_path|inheritors_of|structural_implementations_of|tests_for|impact|dead_code|large_functions|large_files|rename_preview|flows|flow_of|stats|traverse|feature_path|flow_analysis|knowledge_gaps|surprising_connections|suggested_questions|architecture_overview|community|triggers_of|publishers_of|listeners_of|handlers_of|endpoints_for|consumers_of|children_of|federated_repos|mcp_tools|api_contract_mismatches)\n");
1451
+ process.exit(1);
1452
+ }
1453
+ const directionValue = selectorValue("--direction");
1454
+ if (directionValue && !["upstream", "downstream", "both"].includes(directionValue)) {
1455
+ process.stderr.write("knodin query: --direction must be upstream, downstream, or both\n");
1456
+ process.exit(1);
1457
+ }
1458
+ const facetsValue = selectorValue("--facets")?.split(",").filter(Boolean);
1459
+ if (facetsValue?.some((facet) => !["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"].includes(facet))) {
1460
+ process.stderr.write("knodin query: --facets contains an unknown architecture facet\n");
1461
+ process.exit(1);
1462
+ }
1463
+ if (!target && !repoWide) {
1464
+ process.stderr.write(`knodin query ${pattern} requires a <target>\n`);
1465
+ process.exit(1);
1466
+ }
1467
+ // shortest_path: `knodin query shortest_path <from> <to>`
1468
+ // rename_preview: `knodin query rename_preview <old> <new>`
1469
+ const to = pattern === "shortest_path" || pattern === "rename_preview" ? rest[2] : undefined;
1470
+ if (pattern === "rename_preview" && !to) {
1471
+ process.stderr.write("knodin query rename_preview requires <old> and <new>\n");
1472
+ process.exit(1);
1473
+ }
1474
+ // traverse: `knodin query traverse <symbol> [--depth n | <n>]`. Depth is
1475
+ // clamped (1-6) in the engine, so a raw value passes straight through.
1476
+ let depth;
1477
+ if (pattern === "traverse" || pattern === "feature_path" || pattern === "impact") {
1478
+ const flagIdx = rest.indexOf("--depth");
1479
+ const raw = flagIdx >= 0 ? rest[flagIdx + 1] : rest[2];
1480
+ if (raw !== undefined && raw !== "" && Number.isFinite(Number(raw)))
1481
+ depth = Number(raw);
1482
+ }
1483
+ // architecture_overview: `knodin query architecture_overview [minimal|standard]`.
1484
+ // Reuses the same detail knob as explain/review; minimal is the default.
1485
+ let detailLevel;
1486
+ if (pattern === "architecture_overview" && rest.includes("standard")) {
1487
+ detailLevel = "standard";
1488
+ }
1489
+ const queryLimitRaw = selectorValue("--limit");
1490
+ if (queryLimitRaw !== undefined &&
1491
+ (!Number.isInteger(Number(queryLimitRaw)) || Number(queryLimitRaw) < 1)) {
1492
+ process.stderr.write("knodin query: --limit must be a positive integer\n");
1493
+ process.exit(1);
1494
+ }
1495
+ const queryHealth = pattern.startsWith("lsp_")
1496
+ ? null
1497
+ : await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1498
+ if (queryHealth && !queryHealth.available) {
1499
+ result = queryHealth;
1500
+ process.exitCode = 1;
1501
+ break;
1502
+ }
1503
+ result = await engine.query(pattern, target, repo, to, queryLimitRaw ? Number(queryLimitRaw) : undefined, depth, detailLevel, selector, pattern === "impact"
1504
+ ? {
1505
+ mode: selectorValue("--impact-mode") === "file" ? "file" : "symbol",
1506
+ direction: ["upstream", "downstream", "both"].includes(selectorValue("--direction") ?? "")
1507
+ ? selectorValue("--direction")
1508
+ : undefined,
1509
+ relationKinds: selectorValue("--relations")
1510
+ ?.split(",")
1511
+ .map((kind) => kind.trim())
1512
+ .filter(Boolean),
1513
+ minConfidence: selectorValue("--min-confidence")
1514
+ ? Number(selectorValue("--min-confidence"))
1515
+ : undefined,
1516
+ includeTests: !rest.includes("--exclude-tests"),
1517
+ includeDataFlow: rest.includes("--data-flow"),
1518
+ }
1519
+ : undefined, {
1520
+ minLines: selectorValue("--min-lines") ? Number(selectorValue("--min-lines")) : undefined,
1521
+ minComplexity: selectorValue("--min-complexity")
1522
+ ? Number(selectorValue("--min-complexity"))
1523
+ : undefined,
1524
+ kinds: selectorValue("--kinds")?.split(",").filter(Boolean),
1525
+ path: selectorValue("--path"),
1526
+ direction: pattern === "traverse" &&
1527
+ ["upstream", "downstream", "both"].includes(selectorValue("--direction") ?? "")
1528
+ ? selectorValue("--direction")
1529
+ : undefined,
1530
+ includeDataFlow: pattern === "traverse" ? rest.includes("--data-flow") : undefined,
1531
+ flowVariable: pattern === "flow_analysis" ? selectorValue("--variable") : undefined,
1532
+ architectureFacets: facetsValue,
1533
+ topN: selectorValue("--top") ? Number(selectorValue("--top")) : undefined,
1534
+ sort: selectorValue("--sort"),
1535
+ relationKinds: pattern === "impact"
1536
+ ? undefined
1537
+ : selectorValue("--relations")?.split(",").filter(Boolean),
1538
+ detailLevel,
1539
+ });
1540
+ if (pattern === "impact" || pattern === "dead_code") {
1541
+ const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
1542
+ result = incorporateSystemQueryEvidence(config, repo, pattern, target, result);
1543
+ }
1544
+ if (queryHealth?.available) {
1545
+ const verifiedQueryHealth = await inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
1546
+ if (!verifiedQueryHealth.available) {
1547
+ result = verifiedQueryHealth;
1548
+ process.exitCode = 1;
1549
+ break;
1550
+ }
1551
+ result = decorateGraphQueryResult(result, verifiedQueryHealth.state, verifiedQueryHealth.graph.freshness);
1552
+ }
1553
+ break;
1554
+ }
1555
+ case "rename": {
1556
+ // knodin rename <old> <new> [--apply] [--no-verify]
1557
+ const positional = rest.filter((a) => !a.startsWith("--"));
1558
+ const oldName = positional[0];
1559
+ const newName = positional[1];
1560
+ const apply = rest.includes("--apply");
1561
+ // Post-apply typecheck is on by default; --no-verify opts out.
1562
+ const verify = !rest.includes("--no-verify");
1563
+ if (!oldName || !newName) {
1564
+ process.stderr.write("knodin rename requires <old> and <new>\n");
1565
+ process.exit(1);
1566
+ }
1567
+ result = await graphRead(() => engine.rename(oldName, newName, repo, apply, verify, selector));
1568
+ break;
1569
+ }
1570
+ case "prs": {
1571
+ try {
1572
+ const args = rest[0] === "audit" ? rest.slice(1) : rest;
1573
+ const flag = (name) => {
1574
+ const index = args.indexOf(name);
1575
+ return index >= 0 ? args[index + 1] : undefined;
1576
+ };
1577
+ const state = flag("--state");
1578
+ const limitValue = flag("--limit");
1579
+ const limit = limitValue === undefined ? 50 : Number(limitValue);
1580
+ if (!Number.isInteger(limit) || limit < 1)
1581
+ throw new Error("knodin prs: --limit must be a positive integer");
1582
+ result = await auditPullRequests(repo, engine, {
1583
+ state,
1584
+ limit,
1585
+ branches: flag("--branches"),
1586
+ range: flag("--range"),
1587
+ base: flag("--base"),
1588
+ head: flag("--head"),
1589
+ expectedLogin: flag("--expected-login"),
1590
+ });
1591
+ }
1592
+ catch (err) {
1593
+ await engine.close();
1594
+ process.stderr.write(`${err.message}\n`);
1595
+ process.exit(1);
1596
+ }
1597
+ break;
1598
+ }
1599
+ case "worktrees": {
1600
+ const action = rest[0] ?? "status";
1601
+ if (action === "status") {
1602
+ result = await inspectWorktrees(repo, (worktree) => engine.status(worktree, { audit: "cached" }));
1603
+ }
1604
+ else if (action === "reconcile") {
1605
+ result = reconcileWorktrees(repo);
1606
+ }
1607
+ else if (action === "remove") {
1608
+ const target = rest[1];
1609
+ if (!target)
1610
+ throw new Error("knodin worktrees remove requires <path>");
1611
+ result = removeManagedWorktree(repo, target, rest.includes("--dry-run"));
1612
+ }
1613
+ else {
1614
+ throw new Error(`knodin worktrees: unknown action ${action}`);
1615
+ }
1616
+ break;
1617
+ }
1618
+ case "telemetry": {
1619
+ const action = rest[0];
1620
+ const rawRetention = selectorValue("--retention-days");
1621
+ const retentionDays = rawRetention === undefined ? 30 : Number(rawRetention);
1622
+ if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 3650)
1623
+ throw new Error("knodin telemetry: --retention-days must be an integer from 1 to 3650");
1624
+ const input = selectorValue("--input");
1625
+ if (action === "status")
1626
+ result = telemetryStatus(repo, input, retentionDays);
1627
+ else if (action === "report")
1628
+ result = writeTelemetryReport(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"));
1629
+ else if (action === "export")
1630
+ result = exportTelemetry(repo, readTelemetryRecords(repo, input, retentionDays), selectorValue("--output"));
1631
+ else if (action === "clear")
1632
+ result = clearTelemetry(repo, input);
1633
+ else
1634
+ throw new Error("knodin telemetry requires status, report, export, or clear");
1635
+ break;
1636
+ }
1637
+ case "context": {
1638
+ // knodin context "<task>" [base]
1639
+ const task = rest[0];
1640
+ if (!task) {
1641
+ process.stderr.write('knodin context requires a "<task>" description\n');
1642
+ process.exit(1);
1643
+ }
1644
+ result = await graphRead(() => buildKnodinContext(engine, task, repo, rest[1]));
1645
+ break;
1646
+ }
1647
+ default:
1648
+ process.stderr.write(`unknown command: ${cmd}\n\n${renderCliHelp([], process.stderr.isTTY ? process.stderr.columns : undefined)}`);
1649
+ process.exit(1);
1650
+ }
1651
+ await engine.close();
1652
+ if (statusWasWatched)
1653
+ return;
1654
+ const boundedResult = applyResponseBudget(result, cmd, responseBudget, {
1655
+ bytes: 65_536,
1656
+ tokens: 16_384,
1657
+ items: 100,
1658
+ });
1659
+ const finalExitCode = Math.max(Number(process.exitCode ?? 0), repairExitCode);
1660
+ if (cmd === "init" && !jsonOutput) {
1661
+ process.stdout.write(formatInitHuman(boundedResult));
1662
+ process.exitCode = finalExitCode;
1663
+ return;
1664
+ }
1665
+ if (cmd === "configure" && !jsonOutput && !rawRest.includes("--status")) {
1666
+ process.stdout.write(formatConfigureHuman(boundedResult));
1667
+ return;
1668
+ }
1669
+ if (cmd === "configure" && !jsonOutput && rawRest.includes("--status")) {
1670
+ process.stdout.write(formatConfigureStatusHuman(boundedResult));
1671
+ return;
1672
+ }
1673
+ if (cmd === "repair" && repairOutput === "human" && !repairWasPlan) {
1674
+ process.stdout.write(formatRepairHuman(boundedResult));
1675
+ process.exitCode = finalExitCode;
1676
+ return;
1677
+ }
1678
+ if (cmd === "index" && !jsonOutput) {
1679
+ // Human counts must describe the actual operation. The response budget is
1680
+ // a JSON transport constraint and may truncate large `indexed` arrays.
1681
+ process.stdout.write(formatIndexHuman(result));
1682
+ process.exitCode = finalExitCode;
1683
+ return;
1684
+ }
1685
+ if (cmd === "status" && !jsonOutput) {
1686
+ process.stdout.write(formatStatusHuman(boundedResult));
1687
+ process.exitCode = finalExitCode;
1688
+ return;
1689
+ }
1690
+ if (!jsonOutput && repairOutput !== "jsonl") {
1691
+ process.stdout.write(formatGenericHuman(cmd, boundedResult));
1692
+ process.exitCode = finalExitCode;
1693
+ return;
1694
+ }
1695
+ // Budget accounting is over the exact compact serialization written here.
1696
+ process.stdout.write(repairOutput === "jsonl"
1697
+ ? serializeRepairJsonlRecord("result", boundedResult)
1698
+ : `${JSON.stringify(boundedResult)}\n`);
1699
+ process.exitCode = finalExitCode;
1700
+ }
1701
+ main().catch((err) => {
1702
+ console.error(err instanceof Error ? err.message : String(err));
1703
+ process.exit(1);
1704
+ });