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,282 @@
1
+ import childProcess from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { compareBytes } from "./compare.js";
5
+ import { isMirror, resolveDbPath } from "./engine/state-paths.js";
6
+ import { inspectLefthookIntegration, isActiveLefthookHook } from "./hook-manager-integration.js";
7
+ const MANAGED_MARKER = "KNODIN MANAGED HOOK";
8
+ const HOOK_NAMES = ["post-commit", "post-checkout", "post-merge", "post-rewrite"];
9
+ const HOOK_FAILURE_FILE = "last-refresh-failure";
10
+ function readEvent(target) {
11
+ try {
12
+ const [kind = "", legacyBefore = "", legacyAfter = "", ...rest] = fs
13
+ .readFileSync(target, "utf-8")
14
+ .split(/\r?\n/);
15
+ if (!kind)
16
+ return null;
17
+ const metadata = new Map([legacyBefore, legacyAfter, ...rest]
18
+ .map((line) => {
19
+ const separator = line.indexOf("=");
20
+ return separator < 0 ? null : [line.slice(0, separator), line.slice(separator + 1)];
21
+ })
22
+ .filter((entry) => entry !== null));
23
+ return {
24
+ sequence: metadata.get("sequence") || null,
25
+ kind,
26
+ before: metadata.get("before") ||
27
+ (kind === "commit" || kind === "checkout" || kind === "merge" ? legacyBefore : "") ||
28
+ null,
29
+ after: metadata.get("after") ||
30
+ (kind === "commit" || kind === "checkout" || kind === "merge" ? legacyAfter : "") ||
31
+ null,
32
+ worktree: metadata.get("worktree") || null,
33
+ at: metadata.get("at") || fs.statSync(target).mtime.toISOString(),
34
+ };
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function readLastEvent(repo) {
41
+ return readEvent(path.join(repo, ".knodin", "hooks", "last-refresh-event"));
42
+ }
43
+ function readPendingEvent(repo) {
44
+ try {
45
+ const directory = path.join(repo, ".knodin", "hooks", "events");
46
+ const next = fs
47
+ .readdirSync(directory)
48
+ .filter((entry) => entry.endsWith(".event"))
49
+ // Byte order: `[0]` picks which queued event is reported as pending.
50
+ .sort(compareBytes)[0];
51
+ return next ? readEvent(path.join(directory, next)) : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ function processorState(repo) {
58
+ const pidPath = path.join(repo, ".knodin", "hooks", "refresh.lock", "pid");
59
+ if (!fs.existsSync(path.dirname(pidPath)))
60
+ return { state: "idle", pid: null };
61
+ let pid = null;
62
+ try {
63
+ const candidate = Number(fs.readFileSync(pidPath, "utf-8"));
64
+ if (Number.isInteger(candidate) && candidate > 0)
65
+ pid = candidate;
66
+ }
67
+ catch {
68
+ return { state: "stale-lock", pid: null };
69
+ }
70
+ if (pid === null)
71
+ return { state: "stale-lock", pid: null };
72
+ try {
73
+ process.kill(pid, 0);
74
+ return { state: "running", pid };
75
+ }
76
+ catch (error) {
77
+ if (error.code === "EPERM")
78
+ return { state: "running", pid };
79
+ return { state: "stale-lock", pid };
80
+ }
81
+ }
82
+ function queuedEventCount(repo) {
83
+ try {
84
+ return fs
85
+ .readdirSync(path.join(repo, ".knodin", "hooks", "events"))
86
+ .filter((entry) => entry.endsWith(".event")).length;
87
+ }
88
+ catch {
89
+ return 0;
90
+ }
91
+ }
92
+ function activeHooksDirectory(repo) {
93
+ const output = childProcess.execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-path", "hooks"], { cwd: repo, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
94
+ return output.trim();
95
+ }
96
+ /**
97
+ * Classifies how lifecycle hooks reach knodin. Both routes present reads as
98
+ * "mixed" rather than picking a winner, because that combination is what a
99
+ * partially-migrated repository looks like and the caller must be able to say so.
100
+ */
101
+ function hookRouting(wrappers, managerNative) {
102
+ if (wrappers > 0 && managerNative > 0)
103
+ return "mixed";
104
+ if (wrappers > 0)
105
+ return "managed-wrapper";
106
+ return managerNative > 0 ? "manager-native" : "unavailable";
107
+ }
108
+ function inspectManagedHooks(repo, hooksDirectory) {
109
+ const activeHooks = [];
110
+ const missingHooks = [];
111
+ let wrappers = 0;
112
+ let managerNative = 0;
113
+ const lefthook = inspectLefthookIntegration(repo);
114
+ for (const hookName of HOOK_NAMES) {
115
+ try {
116
+ const hookPath = path.join(hooksDirectory, hookName);
117
+ const content = fs.readFileSync(hookPath, "utf-8");
118
+ const mode = fs.statSync(hookPath).mode;
119
+ const wrapper = content.startsWith(`#!/bin/sh\n# ${MANAGED_MARKER}: ${hookName}\n`);
120
+ const native = lefthook.configured && isActiveLefthookHook(content);
121
+ const active = (mode & 0o111) !== 0 && (wrapper || native);
122
+ if (active && wrapper)
123
+ wrappers++;
124
+ else if (active && native)
125
+ managerNative++;
126
+ (active ? activeHooks : missingHooks).push(hookName);
127
+ }
128
+ catch {
129
+ missingHooks.push(hookName);
130
+ }
131
+ }
132
+ return {
133
+ activeHooks,
134
+ missingHooks,
135
+ hookManager: lefthook.configured ? "lefthook" : null,
136
+ routing: hookRouting(wrappers, managerNative),
137
+ };
138
+ }
139
+ function readRefreshFailure(repo) {
140
+ const failurePath = path.join(repo, ".knodin", "hooks", HOOK_FAILURE_FILE);
141
+ if (!fs.existsSync(failurePath))
142
+ return null;
143
+ try {
144
+ return (fs.readFileSync(failurePath, "utf-8").split(/\r?\n/, 1)[0]?.trim() ||
145
+ "last background refresh failed");
146
+ }
147
+ catch {
148
+ return "last background refresh failed";
149
+ }
150
+ }
151
+ /** Read-only proof that committed Git activity can still reach knodin's indexer. */
152
+ export function inspectLifecycleHealth(repoPath) {
153
+ const repo = path.resolve(repoPath);
154
+ // A mirror deliberately has no lifecycle hooks: its working tree is replaced
155
+ // wholesale on refresh and nothing of ours may live inside it. Judging it
156
+ // against hook health would report "degraded" and demand `knodin init` for
157
+ // every mirror, forever — a permanent false alarm that teaches callers to
158
+ // ignore genuine repair prompts. Its freshness comes from the recorded
159
+ // snapshot, not from Git events.
160
+ if (isMirror(repo)) {
161
+ return {
162
+ status: "healthy",
163
+ refreshCapable: false,
164
+ hooksDirectory: null,
165
+ activeHooks: [],
166
+ missingHooks: [],
167
+ issues: [],
168
+ queuedEvents: 0,
169
+ processor: { state: "idle", pid: null },
170
+ pendingEvent: null,
171
+ lastEvent: null,
172
+ lastError: null,
173
+ hookManager: null,
174
+ routing: "unavailable",
175
+ };
176
+ }
177
+ if (!fs.existsSync(resolveDbPath(repo))) {
178
+ return {
179
+ status: "unconfigured",
180
+ refreshCapable: false,
181
+ hooksDirectory: null,
182
+ activeHooks: [],
183
+ missingHooks: [...HOOK_NAMES],
184
+ issues: ["local graph database is not initialized"],
185
+ queuedEvents: 0,
186
+ processor: { state: "idle", pid: null },
187
+ pendingEvent: null,
188
+ lastEvent: null,
189
+ lastError: null,
190
+ hookManager: null,
191
+ routing: "unavailable",
192
+ };
193
+ }
194
+ let hooksDirectory;
195
+ try {
196
+ hooksDirectory = activeHooksDirectory(repo);
197
+ }
198
+ catch {
199
+ return {
200
+ status: "degraded",
201
+ refreshCapable: false,
202
+ hooksDirectory: null,
203
+ activeHooks: [],
204
+ missingHooks: [...HOOK_NAMES],
205
+ issues: ["active Git hooks directory could not be resolved"],
206
+ queuedEvents: queuedEventCount(repo),
207
+ processor: processorState(repo),
208
+ pendingEvent: readPendingEvent(repo),
209
+ lastEvent: readLastEvent(repo),
210
+ lastError: null,
211
+ hookManager: null,
212
+ routing: "unavailable",
213
+ };
214
+ }
215
+ const { activeHooks, missingHooks, hookManager, routing } = inspectManagedHooks(repo, hooksDirectory);
216
+ const background = path.join(repo, ".knodin", "hooks", "background-index.sh");
217
+ const backgroundReady = fs.existsSync(background) && (fs.statSync(background).mode & 0o111) !== 0;
218
+ const issues = missingHooks.map((hook) => `${hook} no longer routes through knodin's active Git hook path`);
219
+ if (!backgroundReady)
220
+ issues.push("background indexer is missing or not executable");
221
+ const lastError = readRefreshFailure(repo);
222
+ if (lastError)
223
+ issues.push(`${lastError} See .knodin/indexer.log for details`);
224
+ const refreshCapable = missingHooks.length === 0 && backgroundReady && issues.length === 0;
225
+ return {
226
+ status: refreshCapable ? "healthy" : "degraded",
227
+ refreshCapable,
228
+ hooksDirectory,
229
+ activeHooks,
230
+ missingHooks,
231
+ issues,
232
+ queuedEvents: queuedEventCount(repo),
233
+ processor: processorState(repo),
234
+ pendingEvent: readPendingEvent(repo),
235
+ lastEvent: readLastEvent(repo),
236
+ lastError,
237
+ hookManager,
238
+ routing,
239
+ };
240
+ }
241
+ /** Merge graph and refresh-path health without reporting absent evidence as healthy. */
242
+ export function attachLifecycleHealth(repo, graph) {
243
+ const lifecycle = inspectLifecycleHealth(repo);
244
+ if (lifecycle.queuedEvents > 0 &&
245
+ graph.status !== "repair-needed" &&
246
+ graph.status !== "indexing" &&
247
+ graph.freshness) {
248
+ return {
249
+ ...graph,
250
+ status: "stale",
251
+ freshness: { ...graph.freshness, state: "queued" },
252
+ lifecycle,
253
+ };
254
+ }
255
+ if (graph.status !== "healthy" || lifecycle.status !== "degraded")
256
+ return { ...graph, lifecycle };
257
+ return {
258
+ ...graph,
259
+ status: "repair-needed",
260
+ missing: {
261
+ files: [...graph.missing.files],
262
+ records: [...graph.missing.records, ...lifecycle.issues],
263
+ },
264
+ repairSteps: [
265
+ ...graph.repairSteps,
266
+ "Run `knodin init` to restore managed lifecycle hooks.",
267
+ "Run `knodin status` again to verify graph and refresh-path health.",
268
+ ],
269
+ lifecycle,
270
+ };
271
+ }
272
+ /** Keep graph-repair success distinct from lifecycle integration health. */
273
+ export function attachRepairLifecycle(repo, result) {
274
+ const lifecycle = inspectLifecycleHealth(repo);
275
+ return {
276
+ ...result,
277
+ lifecycle,
278
+ nextSteps: lifecycle.status === "degraded"
279
+ ? ["Run `knodin init` to restore lifecycle routing, then `knodin status`."]
280
+ : [],
281
+ };
282
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Read-only local language-service adapters.
3
+ *
4
+ * This deliberately embeds no daemon and never invokes workspace commands. The
5
+ * first adapter is TypeScript's compiler language service, which implements the
6
+ * same source-position operations exposed by an LSP server. Other languages are
7
+ * explicitly unavailable until a similarly local, read-only adapter is added.
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ const TYPESCRIPT_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"]);
12
+ function unavailable(pattern, target, reason) {
13
+ return {
14
+ pattern,
15
+ target,
16
+ count: 0,
17
+ results: [],
18
+ adapter: "none",
19
+ available: false,
20
+ readOnly: true,
21
+ reason,
22
+ };
23
+ }
24
+ function parseTarget(repoPath, target) {
25
+ const match = target.match(/^(.*?)(?::(\d+):(\d+))?$/);
26
+ if (!match?.[1])
27
+ return undefined;
28
+ const file = match[1].replaceAll("\\", "/");
29
+ const absoluteFile = path.resolve(repoPath, file);
30
+ const relative = path.relative(repoPath, absoluteFile);
31
+ if (relative.startsWith("..") || path.isAbsolute(relative))
32
+ return undefined;
33
+ if ((match[2] === undefined) !== (match[3] === undefined))
34
+ return undefined;
35
+ return {
36
+ file: relative.replaceAll(path.sep, "/"),
37
+ absoluteFile,
38
+ ...(match[2] ? { line: Number(match[2]), column: Number(match[3]) } : {}),
39
+ };
40
+ }
41
+ function filesForService(repoPath, ts) {
42
+ const isInsideRepo = (file) => {
43
+ try {
44
+ const relativePath = path.relative(repoPath, fs.realpathSync(file));
45
+ return !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ };
51
+ const config = ts.findConfigFile(repoPath, ts.sys.fileExists, "tsconfig.json");
52
+ if (config) {
53
+ const loaded = ts.readConfigFile(config, ts.sys.readFile);
54
+ if (!loaded.error) {
55
+ const parsed = ts.parseJsonConfigFileContent(loaded.config, ts.sys, path.dirname(config));
56
+ return parsed.fileNames.filter((file) => TYPESCRIPT_EXTENSIONS.has(path.extname(file)) && isInsideRepo(file));
57
+ }
58
+ }
59
+ const files = [];
60
+ const visit = (directory) => {
61
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
62
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".knodin")
63
+ continue;
64
+ const candidate = path.join(directory, entry.name);
65
+ if (entry.isDirectory())
66
+ visit(candidate);
67
+ else if (entry.isFile() && TYPESCRIPT_EXTENSIONS.has(path.extname(entry.name)))
68
+ files.push(candidate);
69
+ }
70
+ };
71
+ visit(repoPath);
72
+ return files;
73
+ }
74
+ function createService(repoPath, files, ts) {
75
+ const versions = new Map(files.map((file) => [file, "0"]));
76
+ const options = {
77
+ target: ts.ScriptTarget.ES2022,
78
+ module: ts.ModuleKind.NodeNext,
79
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
80
+ strict: true,
81
+ skipLibCheck: true,
82
+ };
83
+ return ts.createLanguageService({
84
+ getCompilationSettings: () => options,
85
+ getScriptFileNames: () => files,
86
+ getScriptVersion: (file) => versions.get(file) ?? "0",
87
+ getScriptSnapshot: (file) => {
88
+ try {
89
+ return ts.ScriptSnapshot.fromString(fs.readFileSync(file, "utf-8"));
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ },
95
+ getCurrentDirectory: () => repoPath,
96
+ getDefaultLibFileName: (settings) => ts.getDefaultLibFilePath(settings),
97
+ fileExists: ts.sys.fileExists,
98
+ readFile: ts.sys.readFile,
99
+ readDirectory: ts.sys.readDirectory,
100
+ directoryExists: ts.sys.directoryExists,
101
+ getDirectories: ts.sys.getDirectories,
102
+ });
103
+ }
104
+ function relative(repoPath, file) {
105
+ return path.relative(repoPath, file).replaceAll(path.sep, "/");
106
+ }
107
+ function positionFor(source, line, column) {
108
+ if (!Number.isInteger(line) || !Number.isInteger(column) || line < 1 || column < 1)
109
+ return undefined;
110
+ const lines = source.split("\n");
111
+ if (line > lines.length || column > lines[line - 1].length + 1)
112
+ return undefined;
113
+ return (lines.slice(0, line - 1).reduce((offset, current) => offset + current.length + 1, 0) +
114
+ column -
115
+ 1);
116
+ }
117
+ function diagnosticRow(repoPath, diagnostic, ts) {
118
+ const file = diagnostic.file;
119
+ const location = file && diagnostic.start !== undefined
120
+ ? file.getLineAndCharacterOfPosition(diagnostic.start)
121
+ : undefined;
122
+ return {
123
+ file: file ? relative(repoPath, file.fileName) : undefined,
124
+ line: location ? location.line + 1 : undefined,
125
+ column: location ? location.character + 1 : undefined,
126
+ kind: ts.DiagnosticCategory[diagnostic.category].toLowerCase(),
127
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
128
+ code: diagnostic.code,
129
+ };
130
+ }
131
+ function navigationRows(repoPath, entries, ts) {
132
+ return (entries ?? []).map((entry) => {
133
+ const source = fs.readFileSync(entry.fileName, "utf-8");
134
+ const location = ts
135
+ .createSourceFile(entry.fileName, source, ts.ScriptTarget.Latest)
136
+ .getLineAndCharacterOfPosition(entry.textSpan.start);
137
+ return {
138
+ file: relative(repoPath, entry.fileName),
139
+ line: location.line + 1,
140
+ column: location.character + 1,
141
+ kind: entry.kind,
142
+ symbol: "name" in entry ? entry.name : undefined,
143
+ };
144
+ });
145
+ }
146
+ /** Executes a bounded, local compiler-language-service query without writes. */
147
+ export async function runReadOnlyLspQuery(pattern, target, repoPath) {
148
+ let ts;
149
+ try {
150
+ ts = await import("typescript");
151
+ }
152
+ catch {
153
+ return unavailable(pattern, target, "No local TypeScript language-service adapter is installed; install TypeScript to enable this read-only query.");
154
+ }
155
+ let canonicalRepoPath;
156
+ try {
157
+ canonicalRepoPath = fs.realpathSync(repoPath);
158
+ }
159
+ catch {
160
+ return unavailable(pattern, target, "Repository path must be an accessible directory.");
161
+ }
162
+ const parsed = parseTarget(repoPath, target);
163
+ if (!parsed || !fs.existsSync(parsed.absoluteFile))
164
+ return unavailable(pattern, target, "Target must be an existing repo-relative file, optionally with :line:column.");
165
+ if (!TYPESCRIPT_EXTENSIONS.has(path.extname(parsed.absoluteFile)))
166
+ return unavailable(pattern, target, `No local LSP adapter is installed for ${path.extname(parsed.absoluteFile) || "this file type"}.`);
167
+ let realTarget;
168
+ try {
169
+ realTarget = fs.realpathSync(parsed.absoluteFile);
170
+ const relativeTarget = path.relative(canonicalRepoPath, realTarget);
171
+ if (relativeTarget.startsWith("..") || path.isAbsolute(relativeTarget))
172
+ return unavailable(pattern, target, "Target must resolve inside the repository.");
173
+ }
174
+ catch {
175
+ return unavailable(pattern, target, "Target must be a readable regular file.");
176
+ }
177
+ if (pattern !== "lsp_diagnostics" && (parsed.line === undefined || parsed.column === undefined))
178
+ return unavailable(pattern, target, "Navigation queries require target syntax file:line:column.");
179
+ const files = filesForService(canonicalRepoPath, ts);
180
+ if (!files.includes(realTarget))
181
+ files.push(realTarget);
182
+ const service = createService(canonicalRepoPath, files, ts);
183
+ try {
184
+ let rows;
185
+ if (pattern === "lsp_diagnostics") {
186
+ rows = [
187
+ ...service.getSyntacticDiagnostics(realTarget),
188
+ ...service.getSemanticDiagnostics(realTarget),
189
+ ].map((diagnostic) => diagnosticRow(canonicalRepoPath, diagnostic, ts));
190
+ }
191
+ else {
192
+ const line = parsed.line;
193
+ const column = parsed.column;
194
+ if (line === undefined || column === undefined)
195
+ return unavailable(pattern, target, "Navigation queries require target syntax file:line:column.");
196
+ const position = positionFor(fs.readFileSync(realTarget, "utf-8"), line, column);
197
+ if (position === undefined)
198
+ return unavailable(pattern, target, "Target line and column must identify a position inside the source file.");
199
+ rows = navigationRows(canonicalRepoPath, pattern === "lsp_implementations"
200
+ ? service.getImplementationAtPosition(realTarget, position)
201
+ : service.getDefinitionAtPosition(realTarget, position), ts);
202
+ }
203
+ return {
204
+ pattern,
205
+ target,
206
+ count: rows.length,
207
+ results: rows.slice(0, 100),
208
+ hasMore: rows.length > 100 || undefined,
209
+ adapter: "typescript-language-service",
210
+ available: true,
211
+ readOnly: true,
212
+ };
213
+ }
214
+ finally {
215
+ service.dispose();
216
+ }
217
+ }
@@ -0,0 +1,69 @@
1
+ import readline from "node:readline";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { resolveDbPath } from "./engine/state-paths.js";
4
+ import { attachMcpRequestSignal, handleKnodinTool } from "./tools/knodin-tools.js";
5
+ const IPC_VERSION = 1;
6
+ const active = new Map();
7
+ function send(value) {
8
+ process.stdout.write(`${JSON.stringify(value)}\n`);
9
+ }
10
+ async function receive(message) {
11
+ if (message.version !== IPC_VERSION)
12
+ return;
13
+ if (message.type === "cancel") {
14
+ active.get(message.id)?.abort("gateway-cancelled");
15
+ return;
16
+ }
17
+ const controller = new AbortController();
18
+ active.set(message.id, controller);
19
+ send({ version: IPC_VERSION, type: "progress", id: message.id, sequence: 1, phase: "dispatch" });
20
+ try {
21
+ const fault = message.args?.__c89Fault;
22
+ if (process.env.KNODIN_MCP_FAULT_FIXTURES === "1") {
23
+ if (fault === "crash")
24
+ process.exit(91);
25
+ if (fault === "hang")
26
+ await new Promise(() => undefined);
27
+ if (fault === "lock") {
28
+ const repo = message.args.repoPath ?? process.cwd();
29
+ const database = new DatabaseSync(resolveDbPath(repo));
30
+ database.exec("INSERT INTO c89_lock_fixture(value) VALUES ('worker')");
31
+ database.close();
32
+ }
33
+ }
34
+ attachMcpRequestSignal(message.args, controller.signal);
35
+ const result = await handleKnodinTool(message.args);
36
+ send({ version: IPC_VERSION, type: "result", id: message.id, result });
37
+ }
38
+ catch (error) {
39
+ send({
40
+ version: IPC_VERSION,
41
+ type: "error",
42
+ id: message.id,
43
+ error: {
44
+ name: error instanceof Error ? error.name : "Error",
45
+ message: error instanceof Error ? error.message : String(error),
46
+ code: typeof error?.code === "string"
47
+ ? error.code
48
+ : null,
49
+ },
50
+ });
51
+ }
52
+ finally {
53
+ active.delete(message.id);
54
+ }
55
+ }
56
+ const lines = readline.createInterface({
57
+ input: process.stdin,
58
+ crlfDelay: Number.POSITIVE_INFINITY,
59
+ });
60
+ lines.on("line", (line) => {
61
+ try {
62
+ void receive(JSON.parse(line));
63
+ }
64
+ catch {
65
+ send({ version: IPC_VERSION, type: "protocol-error" });
66
+ }
67
+ });
68
+ lines.on("close", () => process.exit(0));
69
+ send({ version: IPC_VERSION, type: "ready", pid: process.pid });