knodin 0.7.6 → 0.8.2

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 (130) hide show
  1. package/README.md +19 -7
  2. package/benchmarks/competitors/SYNTHESIS.md +66 -0
  3. package/dist/bin/cli.js +2164 -108
  4. package/dist/bin/launcher.js +25 -3
  5. package/dist/src/agent-integration.js +304 -0
  6. package/dist/src/artifact-refresh.js +82 -0
  7. package/dist/src/cli-args.js +292 -0
  8. package/dist/src/cli-model.js +384 -0
  9. package/dist/src/codeflow-replay.js +81 -0
  10. package/dist/src/compact-structural.js +96 -0
  11. package/dist/src/compare.js +39 -0
  12. package/dist/src/competitive-cold-mcp.js +40 -0
  13. package/dist/src/competitive-constraints.js +21 -0
  14. package/dist/src/competitive-manifest.js +411 -0
  15. package/dist/src/competitive-measurement.js +183 -0
  16. package/dist/src/competitive-runner.js +487 -0
  17. package/dist/src/competitive-sandbox.js +108 -0
  18. package/dist/src/context-export.js +423 -0
  19. package/dist/src/context.js +102 -0
  20. package/dist/src/deterministic-random.js +34 -0
  21. package/dist/src/diagnostics-write-helper.js +473 -0
  22. package/dist/src/diagnostics.js +1476 -0
  23. package/dist/src/docs-sections.js +141 -0
  24. package/dist/src/doctor.js +382 -0
  25. package/dist/src/engine/ann-hnsw.js +261 -0
  26. package/dist/src/engine/embeddings.js +193 -0
  27. package/dist/src/engine/file-walker.js +49 -0
  28. package/dist/src/engine/git-history.js +289 -0
  29. package/dist/src/engine/index.js +14238 -0
  30. package/dist/src/engine/perf.js +115 -0
  31. package/dist/src/engine/prune.js +112 -0
  32. package/dist/src/engine/sarif-import.js +341 -0
  33. package/dist/src/engine/scip-import.js +423 -0
  34. package/dist/src/engine/source-policy.js +85 -0
  35. package/dist/src/engine/sqlite.js +71 -0
  36. package/dist/src/engine/state-paths.js +175 -0
  37. package/dist/src/engine/symbol-delete.js +58 -0
  38. package/dist/src/execution-profile.js +208 -0
  39. package/dist/src/failure-diagnosis.js +655 -0
  40. package/dist/src/fleet.js +7 -0
  41. package/dist/src/git-executable.js +31 -0
  42. package/dist/src/graph-layout.js +173 -0
  43. package/dist/src/graph-query-health.js +115 -0
  44. package/dist/src/hook-manager-integration.js +156 -0
  45. package/dist/src/index-activity.js +126 -0
  46. package/dist/src/init-progress-worker.js +106 -2
  47. package/dist/src/init-progress.js +155 -0
  48. package/dist/src/init.js +1295 -0
  49. package/dist/src/lifecycle-health.js +282 -0
  50. package/dist/src/lsp-readonly.js +217 -0
  51. package/dist/src/mcp-graph-worker.js +69 -0
  52. package/dist/src/mcp-reliability.js +154 -0
  53. package/dist/src/mcp-worker-supervisor.js +350 -0
  54. package/dist/src/mirror.js +290 -0
  55. package/dist/src/node-runtime.js +157 -0
  56. package/dist/src/output-compression.js +630 -0
  57. package/dist/src/output-telemetry.js +368 -0
  58. package/dist/src/pr-triage.js +638 -0
  59. package/dist/src/progressive-evidence.js +477 -0
  60. package/dist/src/pure-compression-cli.js +102 -0
  61. package/dist/src/relationship-adapters.js +377 -0
  62. package/dist/src/release-attestation.js +533 -0
  63. package/dist/src/release-preflight.js +513 -0
  64. package/dist/src/repair-lease.js +85 -0
  65. package/dist/src/repair-progress-worker.js +120 -2
  66. package/dist/src/repair-progress.js +262 -0
  67. package/dist/src/repository-init-process.js +177 -0
  68. package/dist/src/repository-management.js +1261 -0
  69. package/dist/src/response-budget.js +196 -0
  70. package/dist/src/server.js +217 -0
  71. package/dist/src/structural-fast-path.js +344 -0
  72. package/dist/src/structural-snapshot.js +37 -0
  73. package/dist/src/system-config.js +638 -0
  74. package/dist/src/terminal-help.js +83 -0
  75. package/dist/src/tools/knodin-tools.js +1640 -0
  76. package/dist/src/update-ceremony.js +162 -0
  77. package/dist/src/update-policy.js +944 -0
  78. package/dist/src/update-trust.js +504 -0
  79. package/dist/src/version.js +13 -0
  80. package/dist/src/visualization.js +515 -0
  81. package/dist/src/wait-for-fresh.js +98 -0
  82. package/dist/src/worktree-lifecycle.js +234 -0
  83. package/docs/BEHAVIORAL-CONTRACT.md +72 -0
  84. package/docs/CLI.md +20 -1
  85. package/docs/COMPARISON.md +403 -0
  86. package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
  87. package/docs/CONTAINED-EXECUTION.md +77 -0
  88. package/docs/DIAGNOSTICS.md +80 -0
  89. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  90. package/docs/HANDOFF.md +180 -0
  91. package/docs/INSTALLATION.md +21 -18
  92. package/docs/MCP.md +59 -8
  93. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  94. package/docs/PT-ACCESS-RECOMMENDATION.md +89 -0
  95. package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
  96. package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
  97. package/docs/SCIP-IMPORT.md +62 -0
  98. package/docs/SIGNED-UPDATES.md +151 -0
  99. package/docs/TELEMETRY.md +46 -0
  100. package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
  101. package/docs/assets/knodin-favicon.svg +4 -0
  102. package/docs/releases/0.3.0.md +46 -0
  103. package/docs/releases/0.4.0.md +68 -0
  104. package/docs/releases/0.4.1.md +28 -0
  105. package/docs/releases/0.4.2.md +27 -0
  106. package/docs/releases/0.4.3.md +23 -0
  107. package/docs/releases/0.5.0.md +29 -0
  108. package/docs/releases/0.5.1.md +17 -0
  109. package/docs/releases/0.6.0.md +18 -0
  110. package/docs/releases/0.7.0.md +24 -0
  111. package/docs/releases/0.7.1.md +21 -0
  112. package/docs/releases/0.7.2.md +21 -0
  113. package/docs/releases/0.7.3.md +23 -0
  114. package/docs/releases/0.7.4.md +17 -0
  115. package/docs/releases/0.7.5.md +20 -0
  116. package/docs/releases/0.8.0.md +74 -0
  117. package/docs/releases/0.8.2.md +34 -0
  118. package/package.json +127 -4
  119. package/roadmap/competitive-roadmap.md +3801 -0
  120. package/schemas/release-attestation-v1.schema.json +210 -0
  121. package/schemas/support-bundle-v2.schema.json +212 -0
  122. package/dist/chunks/chunk-DMQAGX77.js +0 -654
  123. package/dist/chunks/chunk-F4Z3Z766.js +0 -4
  124. package/dist/chunks/chunk-SIJAQVSX.js +0 -3
  125. package/dist/chunks/chunk-X6M4HUUE.js +0 -2
  126. package/dist/chunks/chunk-YPRMY2LP.js +0 -8
  127. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +0 -5
  128. package/dist/chunks/server-7EDF4CBY.js +0 -14
  129. package/dist/chunks/structural-fast-path-KD5KQSPX.js +0 -4
  130. package/docs/releases/0.7.6.md +0 -25
@@ -0,0 +1,141 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export const DOCS_SECTIONS = {
5
+ quickstart: `# Quickstart Guide
6
+
7
+ knodin — source-evidenced local code intelligence with known bounds. It builds a semantic and syntactic knowledge graph of your repository and exposes five primary operations through its MCP tool gateway or CLI:
8
+
9
+ 1. **context**: Orient on a repository. Returns high-level statistics, key subsystems/hubs/flows, a heuristic next-operation recommendation, and a diff-based risk score if changes are detected.
10
+ 2. **explain**: Understand a specific symbol (function, class, or file). Returns edit-ready source code, inbound/outbound call paths, and blast radius (transitive callers).
11
+ 3. **review**: Analyze local modifications. Displays risk-scored changes, affected execution flows, and missing test coverage before you commit or create a PR.
12
+ 4. **map**: Visualize subsystem boundaries. Partitions the code into cohesive modular communities using Louvain community detection.
13
+ 5. **search**: Fuzzy semantic and keyword lookup to find symbols when you do not know their exact name.
14
+
15
+ ### Command Usage (CLI)
16
+ \`\`\`bash
17
+ knodin context "explore auth"
18
+ knodin explain login
19
+ knodin map
20
+ \`\`\``,
21
+ "query-patterns": `# Structured Query Patterns
22
+
23
+ The \`query\` operation lets you ask precise, structured questions about your code using a variety of built-in graph patterns:
24
+
25
+ - **callers_of <target>**: Who invokes this symbol (transitive/direct).
26
+ - **callees_of <target>**: What other symbols does this symbol invoke.
27
+ - **tests_for <target>**: Find test files that cover this symbol.
28
+ - **file_summary <target>**: Lists all symbols defined in a file.
29
+ - **shortest_path <from> <to>**: Find the call-graph chain connecting two symbols.
30
+ - **impact <symbol>**: Directional, depth-bounded symbol reach with stable selectors, relation/confidence/test filters, edge evidence, and heuristic summaries. Use \`impactMode: "file"\` explicitly for a distinctly labeled changed-file blast radius.
31
+ - **dead_code**: Surface exported or unexported symbols with zero references.
32
+ - **large_functions / large_files**: Spot complex "god" functions and files exceeding size thresholds.
33
+ - **traverse <symbol>**: Perform a bounded BFS neighborhood walk. Use \`direction\` (\`upstream|downstream|both\`), \`relationKinds\`, and optional \`includeDataFlow\`; every discovered hop carries its exact kind, direction, confidence, provenance, source line, and files. Call arguments are labeled heuristic source evidence, never runtime proof.
34
+ - **knowledge_gaps**: Report repo-health weaknesses like thin communities (<3 symbols) and untested hubs/bridges.
35
+ - **surprising_connections**: Score and rank coupling that crosses community, language, or test boundaries.
36
+ - **suggested_questions**: prioritizes human-readable review prompts based on untested hotspots and high-surprise edges.
37
+ - **architecture_overview**: Formats community cohesion and coupling plus independently selectable \`architectureFacets\`: packages, layers, boundaries, hotspots, entryPoints, and languages. \`path\` scopes every facet consistently.`,
38
+ federation: `# Cross-Repository Composition
39
+
40
+ Federation is knodin's internal query-composition mechanism, not the primary
41
+ user-facing model. Use \`knodin repos\` for operational checkout management and
42
+ \`knodin system\` with commit-ready \`knodin.yaml\` stable identities for
43
+ cooperating components. Sibling directory placement and shared search matches
44
+ never establish system membership.
45
+
46
+ Legacy \`.knodin/federation.json\` remains a compatibility input and is not
47
+ silently discarded. Migrate it to \`knodin.yaml\` plus personal XDG checkout
48
+ paths. The \`federated_repos\` query remains available for inspecting the
49
+ engine's currently composed repository paths; it does not prove those
50
+ repositories form one system.`,
51
+ "rename-safety": `# Rename Safety and Refactoring
52
+
53
+ knodin provides an AST-backed, type-safe refactoring pipeline for renaming symbols.
54
+
55
+ ### Two-Step Pipeline
56
+ 1. **Dry-Run (default)**: Returns every edit site (definitions, references, imports) and generates a unified diff. Refuses to make changes if there's an ambiguity or conflict.
57
+ 2. **Apply (\`apply: true\` or \`--apply\`)**: Atomically writes line-scoped word-boundary edits to disk and triggers an automatic reindexing of touched files.
58
+
59
+ ### Safety Guards
60
+ The engine refuses to write to disk and rolls back any changes if:
61
+ - **Ambiguity**: The old symbol name resolves to more than one definition file.
62
+ - **Collision**: The new name already exists as a symbol in any of the edit-site files.
63
+ - **Out of Scope**: The edit targets files outside the repository.
64
+ - **Type Errors**: If verification is enabled (default), it verifies the workspace compiles successfully using \`tsc\` (or equivalent) after applying the edit, and automatically rolls back if compilation fails.`,
65
+ "language-support": `# Language and Metadata Support
66
+
67
+ knodin is multi-language, not language-universal.
68
+
69
+ ### Native syntax
70
+ TypeScript (\`.ts/.tsx/.mts/.cts\`), JavaScript (\`.js/.jsx/.mjs/.cjs\`), Python, Java, C#, Salesforce Apex, SQL/PLSQL, Prisma, and XML-backed formats.
71
+
72
+ ### Domain-specific structure
73
+ Salesforce LWC, Aura, Visualforce, Experience Cloud, and selected Salesforce DX metadata/automations; Terraform/HCL; Dockerfiles; dbt manifests; and Workday Studio XML.
74
+
75
+ ### Imported graph
76
+ LSIF can import symbols and relationships produced by a compatible language server. This is not native parsing.
77
+
78
+ Go, Rust, PHP, Ruby, Kotlin, Swift, Perl, PowerShell, Bash, and MuleSoft/RAML are known native-parser gaps. Coverage differs by language, and static dead-code candidates must be corroborated when runtime or platform configuration can invoke code dynamically. Salesforce candidates should be checked against deployed-org and platform dependency data before deletion.`,
79
+ troubleshooting: `# Troubleshooting and Recovery
80
+
81
+ ### Index health and surgical repair
82
+
83
+ Run \`knodin status\` (or MCP \`operation: "status"\`) to inspect the local schema/model/version, file and symbol coverage, orphaned or missing records, and the last successful reconciliation. Its repair steps are actionable. Run \`knodin repair\` (or MCP \`operation: "repair"\`) to rebuild only missing/damaged rows and verify health; healthy indexed files are not deleted or rebuilt.
84
+
85
+ The MCP \`telemetry\` operation reads process-local metadata-only measurements: actual response bytes and \`gpt-tokenizer@3.4.0:o200k_base\` tokens, executed baselines where available, negative or positive savings, latency, RSS, schema cost, truncation, and detail mode. It never contains source or file paths and does not change existing operation response shapes. Telemetry is not persisted by default. Set MCP \`persistTelemetry: true\` to append metadata-only JSONL to the repo-root \`.knodin-telemetry.jsonl\`; knodin never sends it over the network. Use \`telemetryAction: "report"\` or \`knodin telemetry report\` for a static repository-local HTML report.
86
+
87
+ Common issues and how to resolve them when using knodin:
88
+
89
+ ### Corrupt Embedding Row
90
+ If you see warnings like \`Skipping corrupt embedding... dimension mismatch\`, your index database contains malformed embeddings.
91
+ - **Fix**: Force a clean re-index of the repository to rebuild the vector store.
92
+ \`\`\`bash
93
+ # Prefer surgical local repair; it preserves healthy state
94
+ knodin repair
95
+ \`\`\`
96
+
97
+ ### Missing 'gh' CLI Dependency
98
+ The \`prs\` triage operation requires the official GitHub CLI (\`gh\`) to be installed, in your PATH, and authenticated.
99
+ - **Fix**: Run \`gh auth login\` to authenticate locally.
100
+
101
+ ### Legacy federation configuration
102
+ If legacy federation configuration fails, validate
103
+ \`.knodin/federation.json\`, then migrate stable identities to \`knodin.yaml\`
104
+ and local paths to XDG configuration. Do not infer membership from siblings.`,
105
+ };
106
+ const DOC_TOPIC_FILES = {
107
+ installation: "INSTALLATION.md",
108
+ mcp: "MCP.md",
109
+ repositories: "REPOSITORIES-AND-WORKTREES.md",
110
+ systems: "SYSTEMS-AND-RELATIONSHIPS.md",
111
+ provenance: "INDEXING-POLICY-AND-PROVENANCE.md",
112
+ "dead-code": "DEAD-CODE-AND-IMPACT.md",
113
+ doctor: "DOCTOR-AND-UPDATES.md",
114
+ compression: "COMMAND-OUTPUT-COMPRESSION.md",
115
+ };
116
+ function packageRoot() {
117
+ let current = path.dirname(fileURLToPath(import.meta.url));
118
+ while (!fs.existsSync(path.join(current, "package.json")) &&
119
+ current !== path.parse(current).root) {
120
+ current = path.dirname(current);
121
+ }
122
+ return current;
123
+ }
124
+ export function listDocTopics() {
125
+ return [...new Set([...Object.keys(DOCS_SECTIONS), ...Object.keys(DOC_TOPIC_FILES)])].sort((left, right) => left.localeCompare(right));
126
+ }
127
+ /** Read canonical long-form guides directly so CLI and MCP cannot drift from Markdown. */
128
+ export function getDocSection(topic) {
129
+ const file = DOC_TOPIC_FILES[topic];
130
+ if (file) {
131
+ try {
132
+ return fs.readFileSync(path.join(packageRoot(), "docs", file), "utf-8");
133
+ }
134
+ catch (error) {
135
+ if (error.code === "ENOENT")
136
+ return undefined;
137
+ throw error;
138
+ }
139
+ }
140
+ return DOCS_SECTIONS[topic];
141
+ }
@@ -0,0 +1,382 @@
1
+ import child_process from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { detectSupportedAgents } from "./agent-integration.js";
6
+ import { inspectLifecycleHealth } from "./lifecycle-health.js";
7
+ import { trustedUpdateStatus } from "./update-policy.js";
8
+ function defaultRun(command, args, options = {}) {
9
+ const result = child_process.spawnSync(command, args, {
10
+ encoding: "utf-8",
11
+ input: options.input,
12
+ timeout: 5_000,
13
+ stdio: ["pipe", "pipe", "pipe"],
14
+ });
15
+ return {
16
+ status: result.status,
17
+ stdout: result.stdout ?? "",
18
+ stderr: result.stderr ?? result.error?.message ?? "",
19
+ };
20
+ }
21
+ function managerEvidence(executable, env) {
22
+ const explicit = [
23
+ ["mise", env.MISE_DATA_DIR ?? env.MISE_ENV, "MISE_DATA_DIR or MISE_ENV is set"],
24
+ ["volta", env.VOLTA_HOME, "VOLTA_HOME is set"],
25
+ ["nvm", env.NVM_BIN ?? env.NVM_DIR, "NVM_BIN or NVM_DIR is set"],
26
+ ["fnm", env.FNM_DIR ?? env.FNM_MULTISHELL_PATH, "FNM_DIR or FNM_MULTISHELL_PATH is set"],
27
+ ["asdf", env.ASDF_DIR ?? env.ASDF_DATA_DIR, "ASDF_DIR or ASDF_DATA_DIR is set"],
28
+ ["homebrew", env.HOMEBREW_PREFIX, "HOMEBREW_PREFIX is set"],
29
+ ];
30
+ for (const [name, value, evidence] of explicit) {
31
+ if (value)
32
+ return { name, confidence: "high", evidence: [evidence] };
33
+ }
34
+ const lower = executable.toLowerCase();
35
+ for (const [needle, name] of [
36
+ ["/.local/share/mise/", "mise"],
37
+ ["/.volta/", "volta"],
38
+ ["/.nvm/", "nvm"],
39
+ ["/fnm", "fnm"],
40
+ ["/.asdf/", "asdf"],
41
+ ["/homebrew/", "homebrew"],
42
+ ]) {
43
+ if (lower.includes(needle)) {
44
+ return {
45
+ name,
46
+ confidence: "medium",
47
+ evidence: [`resolved executable path contains ${needle}; path heuristic only`],
48
+ };
49
+ }
50
+ }
51
+ return {
52
+ name: "unknown",
53
+ confidence: "low",
54
+ evidence: ["no manager-owned environment evidence; executable path alone is inconclusive"],
55
+ };
56
+ }
57
+ function symlinkChain(executable) {
58
+ const chain = [path.resolve(executable)];
59
+ for (let count = 0; count < 16; count++) {
60
+ const current = chain.at(-1);
61
+ if (!current)
62
+ break;
63
+ try {
64
+ if (!fs.lstatSync(current).isSymbolicLink()) {
65
+ const canonical = fs.realpathSync(current);
66
+ if (canonical !== current)
67
+ chain.push(canonical);
68
+ break;
69
+ }
70
+ const target = fs.readlinkSync(current);
71
+ const resolved = path.resolve(path.dirname(current), target);
72
+ if (chain.includes(resolved))
73
+ break;
74
+ chain.push(resolved);
75
+ }
76
+ catch {
77
+ break;
78
+ }
79
+ }
80
+ return chain;
81
+ }
82
+ function executableCandidates(env) {
83
+ const candidates = [];
84
+ for (const directory of (env.PATH ?? "").split(path.delimiter)) {
85
+ if (!directory)
86
+ continue;
87
+ const candidate = path.join(directory, process.platform === "win32" ? "knodin.cmd" : "knodin");
88
+ try {
89
+ fs.accessSync(candidate, fs.constants.X_OK);
90
+ const resolved = fs.realpathSync(candidate);
91
+ if (!candidates.includes(resolved))
92
+ candidates.push(resolved);
93
+ }
94
+ catch {
95
+ // Not an executable candidate.
96
+ }
97
+ }
98
+ return candidates;
99
+ }
100
+ function runtimeTarget(command) {
101
+ const [executable, ...arguments_] = command;
102
+ if (!executable)
103
+ return "";
104
+ const basename = path.basename(executable).toLowerCase();
105
+ if (basename === "node" ||
106
+ basename === "node.exe" ||
107
+ basename === "bun" ||
108
+ basename === "tsx" ||
109
+ basename === "tsx.cmd") {
110
+ const valueFlags = new Set([
111
+ "--import",
112
+ "--loader",
113
+ "--experimental-loader",
114
+ "--require",
115
+ "-r",
116
+ ]);
117
+ for (let index = 0; index < arguments_.length; index++) {
118
+ const argument = arguments_[index];
119
+ if (valueFlags.has(argument)) {
120
+ index++;
121
+ continue;
122
+ }
123
+ if (argument.startsWith("-"))
124
+ continue;
125
+ return argument;
126
+ }
127
+ }
128
+ return executable;
129
+ }
130
+ function sanitizeRegistry(raw) {
131
+ const value = raw.trim();
132
+ try {
133
+ const parsed = new URL(value);
134
+ parsed.username = "";
135
+ parsed.password = "";
136
+ parsed.search = "";
137
+ parsed.hash = "";
138
+ return parsed.toString();
139
+ }
140
+ catch {
141
+ return "unknown";
142
+ }
143
+ }
144
+ function readAgentConfig(agent, file) {
145
+ try {
146
+ const content = fs.readFileSync(file, "utf-8");
147
+ if (agent === "claude" && path.basename(file) === ".claude.json") {
148
+ const present = content.includes('"knodin"');
149
+ return { agent, file, present, command: present ? "knodin" : null, args: ["serve"] };
150
+ }
151
+ if (file.endsWith(".toml")) {
152
+ const sectionStart = content.indexOf('[mcp_servers."knodin"]');
153
+ const sectionTail = sectionStart >= 0 ? content.slice(sectionStart) : "";
154
+ const nextSection = sectionTail.indexOf("\n[", 1);
155
+ const block = nextSection >= 0 ? sectionTail.slice(0, nextSection) : sectionTail;
156
+ const command = /^[ \t]*command[ \t]*=[ \t]*"([^"\r\n]+)"/m.exec(block)?.[1];
157
+ const args = /^[ \t]*args[ \t]*=[ \t]*\[([^\]]*)\]/m
158
+ .exec(block)?.[1]
159
+ ?.split(",")
160
+ .map((value) => value.trim().replace(/^"|"$/g, ""))
161
+ .filter(Boolean);
162
+ return {
163
+ agent,
164
+ file,
165
+ present: sectionStart >= 0,
166
+ command: command ?? null,
167
+ args: args ?? [],
168
+ };
169
+ }
170
+ const document = JSON.parse(content);
171
+ const registration = document.mcpServers?.knodin ?? document.servers?.knodin;
172
+ return {
173
+ agent,
174
+ file,
175
+ present: registration !== undefined,
176
+ command: typeof registration?.command === "string" ? registration.command : null,
177
+ args: Array.isArray(registration?.args)
178
+ ? registration.args.filter((value) => typeof value === "string")
179
+ : [],
180
+ };
181
+ }
182
+ catch {
183
+ return { agent, file, present: false, command: null, args: [] };
184
+ }
185
+ }
186
+ function configuredAgentCommands(repo) {
187
+ const candidates = [
188
+ { agent: "claude", file: ".mcp.json" },
189
+ { agent: "codex", file: ".codex/config.toml" },
190
+ { agent: "gemini", file: ".gemini/settings.json" },
191
+ { agent: "copilot", file: ".vscode/mcp.json" },
192
+ { agent: "antigravity", file: ".agents/mcp_config.json" },
193
+ ];
194
+ return candidates.map(({ agent, file }) => readAgentConfig(agent, path.join(repo, file)));
195
+ }
196
+ function personalAgentCommands(homeDir) {
197
+ return [
198
+ readAgentConfig("claude", path.join(homeDir, ".claude.json")),
199
+ readAgentConfig("codex", path.join(homeDir, ".codex", "config.toml")),
200
+ readAgentConfig("gemini", path.join(homeDir, ".gemini", "settings.json")),
201
+ readAgentConfig("copilot", path.join(homeDir, "Library", "Application Support", "Code", "User", "mcp.json")),
202
+ readAgentConfig("antigravity", path.join(homeDir, ".agents", "mcp_config.json")),
203
+ ];
204
+ }
205
+ function installedPackageVersion(executable) {
206
+ let directory = path.dirname(executable);
207
+ for (let depth = 0; depth < 6; depth++) {
208
+ try {
209
+ const manifest = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf-8"));
210
+ if (manifest.name === "knodin" && typeof manifest.version === "string")
211
+ return manifest.version;
212
+ }
213
+ catch {
214
+ // Continue toward the package root.
215
+ }
216
+ const parent = path.dirname(directory);
217
+ if (parent === directory)
218
+ break;
219
+ directory = parent;
220
+ }
221
+ return "unknown";
222
+ }
223
+ function managerRemediation(manager, version) {
224
+ if (manager === "mise")
225
+ return `mise use --global npm:knodin@${version}`;
226
+ if (manager === "volta")
227
+ return `volta install knodin@${version}`;
228
+ if (manager === "homebrew")
229
+ return "brew update && brew upgrade knodin";
230
+ if (manager === "nvm")
231
+ return `nvm use 24 && npm install --global --ignore-scripts knodin@${version}`;
232
+ if (manager === "fnm")
233
+ return `fnm use 24 && npm install --global --ignore-scripts knodin@${version}`;
234
+ if (manager === "asdf")
235
+ return `asdf set nodejs 24 && npm install --global --ignore-scripts knodin@${version}`;
236
+ return `npm install --global --ignore-scripts knodin@${version}`;
237
+ }
238
+ function parseMcp(output) {
239
+ const messages = output
240
+ .split(/\r?\n/)
241
+ .filter(Boolean)
242
+ .flatMap((line) => {
243
+ try {
244
+ return [JSON.parse(line)];
245
+ }
246
+ catch {
247
+ return [];
248
+ }
249
+ });
250
+ const initialize = messages.find(({ id }) => id === 1);
251
+ const tools = messages.find(({ id }) => id === 2);
252
+ const initializeResult = initialize?.result;
253
+ const toolsResult = tools?.result;
254
+ const names = (toolsResult?.tools ?? []).flatMap(({ name }) => typeof name === "string" ? [name] : []);
255
+ return {
256
+ initialize: initializeResult?.serverInfo ? "ok" : "failed",
257
+ toolsList: names.length > 0 ? "ok" : "failed",
258
+ serverVersion: typeof initializeResult?.serverInfo?.version === "string"
259
+ ? initializeResult.serverInfo.version
260
+ : undefined,
261
+ toolNames: names,
262
+ };
263
+ }
264
+ function mcpProbe(command, run) {
265
+ const input = [
266
+ JSON.stringify({
267
+ jsonrpc: "2.0",
268
+ id: 1,
269
+ method: "initialize",
270
+ params: {
271
+ protocolVersion: "2025-11-25",
272
+ capabilities: {},
273
+ clientInfo: { name: "knodin-doctor", version: "1" },
274
+ },
275
+ }),
276
+ JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }),
277
+ JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }),
278
+ "",
279
+ ].join("\n");
280
+ const [executable, ...args] = command;
281
+ if (!executable)
282
+ return { initialize: "failed", toolsList: "failed", toolNames: [], stderr: "no command" };
283
+ const result = run(executable, args, { input });
284
+ return { ...parseMcp(result.stdout), stderr: result.status === 0 ? undefined : result.stderr };
285
+ }
286
+ export async function diagnoseInstallation(repoPath, options) {
287
+ const repo = path.resolve(repoPath);
288
+ const env = options.env ?? process.env;
289
+ const run = options.run ?? defaultRun;
290
+ const executable = runtimeTarget(options.runtimeCommand);
291
+ const chain = symlinkChain(executable);
292
+ const candidates = executableCandidates(env);
293
+ const npmPrefix = run("npm", ["config", "get", "prefix"]);
294
+ const npmRegistry = run("npm", ["config", "get", "registry"]);
295
+ const mcp = mcpProbe(options.runtimeCommand, run);
296
+ const runtimeVersion = mcp.serverVersion;
297
+ const lifecycle = inspectLifecycleHealth(repo);
298
+ const detectedAgents = detectSupportedAgents();
299
+ const repositoryCommands = configuredAgentCommands(repo);
300
+ const personalCommands = personalAgentCommands(options.homeDir ?? os.homedir());
301
+ const clients = repositoryCommands
302
+ .filter(({ agent }) => options.client === undefined || options.client === agent)
303
+ .map((repository) => {
304
+ const personal = personalCommands.find(({ agent }) => agent === repository.agent);
305
+ const configured = repository.present || personal?.present === true;
306
+ return {
307
+ client: repository.agent,
308
+ detected: detectedAgents.includes(repository.agent),
309
+ repository,
310
+ personal,
311
+ server: {
312
+ initialize: mcp.initialize,
313
+ toolsList: mcp.toolsList,
314
+ toolNames: mcp.toolNames,
315
+ },
316
+ activeSessionExposure: "unknown",
317
+ note: repository.agent === "claude"
318
+ ? ".mcp.json is Claude project configuration, not a universal MCP registration."
319
+ : "Configuration presence does not prove that the active client session loaded it.",
320
+ remediation: configured
321
+ ? "Restart or reload the client, then verify its active tools list."
322
+ : `Run \`knodin configure --scope personal\` or \`knodin configure --scope team\` for ${repository.agent}.`,
323
+ };
324
+ });
325
+ const manager = managerEvidence(chain.at(-1) ?? executable, env);
326
+ const packageVersion = installedPackageVersion(chain.at(-1) ?? executable);
327
+ return {
328
+ schemaVersion: 1,
329
+ status: options.graph.status === "healthy" &&
330
+ mcp.initialize === "ok" &&
331
+ mcp.toolsList === "ok" &&
332
+ candidates.length <= 1
333
+ ? "healthy"
334
+ : "attention-required",
335
+ versions: {
336
+ package: packageVersion,
337
+ cli: runtimeVersion ?? "unknown",
338
+ source: options.currentVersion,
339
+ agree: runtimeVersion === options.currentVersion &&
340
+ (packageVersion === "unknown" || packageVersion === options.currentVersion),
341
+ },
342
+ runtime: {
343
+ node: process.version,
344
+ platform: process.platform,
345
+ arch: process.arch,
346
+ },
347
+ npm: {
348
+ prefix: npmPrefix.status === 0 ? npmPrefix.stdout.trim() : "unknown",
349
+ registry: npmRegistry.status === 0 ? sanitizeRegistry(npmRegistry.stdout) : "unknown",
350
+ },
351
+ manager,
352
+ executable: {
353
+ resolved: chain.at(-1) ?? executable,
354
+ chain,
355
+ candidates,
356
+ conflicts: candidates.filter((candidate) => candidate !== chain.at(-1)),
357
+ },
358
+ agents: {
359
+ detected: detectedAgents,
360
+ configuredCommands: repositoryCommands,
361
+ clients,
362
+ },
363
+ mcp,
364
+ lifecycle,
365
+ graph: options.graph,
366
+ update: trustedUpdateStatus({
367
+ currentVersion: options.currentVersion,
368
+ stateHome: options.cacheHome,
369
+ installMethod: manager.name,
370
+ env,
371
+ }),
372
+ remediation: {
373
+ manager: managerRemediation(manager.name, options.currentVersion),
374
+ duplicates: candidates.length > 1
375
+ ? "Remove only the unintended manager-owned installation, then rerun `knodin doctor`."
376
+ : null,
377
+ graph: options.graph.status === "healthy"
378
+ ? null
379
+ : "Run `knodin repair`, then `knodin status --deep`.",
380
+ },
381
+ };
382
+ }