knodin 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/server.js +108 -9
- package/dist/src/structural-fast-path.js +45 -10
- package/docs/releases/0.7.4.md +17 -0
- package/docs/releases/0.7.5.md +20 -0
- package/package.json +3 -1
package/dist/src/server.js
CHANGED
|
@@ -14,6 +14,49 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
14
14
|
import { recordDiagnosticFailure } from "./diagnostics.js";
|
|
15
15
|
import { getKnodinTools, handleKnodinTool } from "./tools/knodin-tools.js";
|
|
16
16
|
import { KNODIN_VERSION } from "./version.js";
|
|
17
|
+
const RETRY_SAFE_OPERATIONS = new Set([
|
|
18
|
+
"architecture_overview",
|
|
19
|
+
"context",
|
|
20
|
+
"docs",
|
|
21
|
+
"explain",
|
|
22
|
+
"map",
|
|
23
|
+
"query",
|
|
24
|
+
"review",
|
|
25
|
+
"search",
|
|
26
|
+
"status",
|
|
27
|
+
]);
|
|
28
|
+
class GatewayRequestQueue {
|
|
29
|
+
tail = Promise.resolve();
|
|
30
|
+
pending = 0;
|
|
31
|
+
async run(signal, task) {
|
|
32
|
+
const position = this.pending++;
|
|
33
|
+
const predecessor = this.tail;
|
|
34
|
+
let release;
|
|
35
|
+
this.tail = new Promise((resolve) => {
|
|
36
|
+
release = resolve;
|
|
37
|
+
});
|
|
38
|
+
try {
|
|
39
|
+
await predecessor;
|
|
40
|
+
if (signal.aborted)
|
|
41
|
+
throw Object.assign(new Error("MCP request cancelled before dispatch"), {
|
|
42
|
+
code: "KNODIN_REQUEST_CANCELLED",
|
|
43
|
+
});
|
|
44
|
+
return await task(position);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
this.pending--;
|
|
48
|
+
release();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function ignoreNotificationFailure() {
|
|
53
|
+
// Progress is advisory; a client that stops accepting it must not fail the tool call.
|
|
54
|
+
}
|
|
55
|
+
function startHeartbeat(notify, operation, startedAt) {
|
|
56
|
+
return setInterval(() => {
|
|
57
|
+
void notify(`${operation} still running (${Math.round((Date.now() - startedAt) / 1000)}s)`).catch(ignoreNotificationFailure);
|
|
58
|
+
}, 10_000);
|
|
59
|
+
}
|
|
17
60
|
export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cwd()) {
|
|
18
61
|
const effectiveRepo = typeof args?.repoPath === "string" ? args.repoPath : defaultRepo;
|
|
19
62
|
const diagnostic = recordDiagnosticFailure(effectiveRepo, {
|
|
@@ -26,7 +69,8 @@ export function recordMcpDiagnosticFailure(args, error, defaultRepo = process.cw
|
|
|
26
69
|
? new Error(`${error instanceof Error ? error.message : String(error)} [diagnostic ${diagnostic.correlationId}]`, { cause: error })
|
|
27
70
|
: error;
|
|
28
71
|
}
|
|
29
|
-
export function createServer() {
|
|
72
|
+
export function createServer(dispatch = handleKnodinTool) {
|
|
73
|
+
const queue = new GatewayRequestQueue();
|
|
30
74
|
const server = new Server({ name: "knodin", version: KNODIN_VERSION }, {
|
|
31
75
|
capabilities: { tools: {} },
|
|
32
76
|
instructions: "Use knodin first for cold or unfamiliar codebase work: context to orient, explain for source-evidenced symbol context, query impact before meaningful edits, and review before handoff. Prefer direct reads for exact literals, non-code files, or files just edited this turn. If status reports repair-needed, follow its typed repair steps: repair graph damage or init displaced lifecycle routing before relying on graph evidence.",
|
|
@@ -34,22 +78,77 @@ export function createServer() {
|
|
|
34
78
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
35
79
|
tools: getKnodinTools(),
|
|
36
80
|
}));
|
|
37
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
81
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
38
82
|
const { name, arguments: args } = request.params;
|
|
39
83
|
if (name !== "knodin") {
|
|
40
84
|
throw new Error(`unknown tool: ${name}`);
|
|
41
85
|
}
|
|
42
|
-
|
|
86
|
+
const values = args;
|
|
87
|
+
const operation = typeof values?.operation === "string" ? values.operation : "unknown";
|
|
88
|
+
const progressToken = extra._meta?.progressToken;
|
|
89
|
+
const requestId = String(extra.requestId);
|
|
90
|
+
const startedAt = Date.now();
|
|
91
|
+
let heartbeat;
|
|
43
92
|
try {
|
|
44
|
-
result = await
|
|
93
|
+
const result = await queue.run(extra.signal, async (position) => {
|
|
94
|
+
const notify = async (message) => {
|
|
95
|
+
if (progressToken === undefined)
|
|
96
|
+
return;
|
|
97
|
+
await extra.sendNotification({
|
|
98
|
+
method: "notifications/progress",
|
|
99
|
+
params: {
|
|
100
|
+
progressToken,
|
|
101
|
+
progress: Date.now() - startedAt,
|
|
102
|
+
message,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
await notify(position > 0
|
|
107
|
+
? `Starting ${operation} after waiting behind ${position} earlier request(s)`
|
|
108
|
+
: `Starting ${operation}`);
|
|
109
|
+
heartbeat = startHeartbeat(notify, operation, startedAt);
|
|
110
|
+
// Keep the production handler explicit: knodin's own source-evidence
|
|
111
|
+
// extractor maps this low-level MCP registration to its dispatcher.
|
|
112
|
+
return dispatch === handleKnodinTool ? await handleKnodinTool(args) : await dispatch(args);
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
116
|
+
};
|
|
45
117
|
}
|
|
46
118
|
catch (error) {
|
|
47
|
-
const
|
|
48
|
-
|
|
119
|
+
const recorded = recordMcpDiagnosticFailure(values, error);
|
|
120
|
+
const message = recorded instanceof Error ? recorded.message : String(recorded);
|
|
121
|
+
const traceId = /\[diagnostic ([a-f0-9]{16})\]/.exec(message)?.[1] ?? `mcp-${requestId}`;
|
|
122
|
+
const retrySafe = RETRY_SAFE_OPERATIONS.has(operation);
|
|
123
|
+
return {
|
|
124
|
+
isError: true,
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: JSON.stringify({
|
|
129
|
+
code: error?.code === "KNODIN_REQUEST_CANCELLED"
|
|
130
|
+
? "KNODIN_REQUEST_CANCELLED"
|
|
131
|
+
: "KNODIN_OPERATION_FAILED",
|
|
132
|
+
message,
|
|
133
|
+
traceId,
|
|
134
|
+
requestId,
|
|
135
|
+
operation,
|
|
136
|
+
repository: typeof values?.repoPath === "string" ? values.repoPath : process.cwd(),
|
|
137
|
+
phase: "dispatch",
|
|
138
|
+
retrySafe,
|
|
139
|
+
diagnosticLog: ".knodin/diagnostics/events.jsonl (only when opt-in diagnostics are enabled)",
|
|
140
|
+
recovery: retrySafe
|
|
141
|
+
? "Retry once. If the transport closes, restart the MCP client and run `knodin doctor`; include `knodin diagnostics export` when diagnostics are enabled."
|
|
142
|
+
: "Do not retry automatically. Run `knodin doctor`, inspect repository state, and include `knodin diagnostics export` when diagnostics are enabled.",
|
|
143
|
+
}, null, 2),
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
if (heartbeat)
|
|
150
|
+
clearInterval(heartbeat);
|
|
49
151
|
}
|
|
50
|
-
return {
|
|
51
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
52
|
-
};
|
|
53
152
|
});
|
|
54
153
|
return server;
|
|
55
154
|
}
|
|
@@ -54,6 +54,43 @@ function lexicalEndLine(lines, startIndex, python) {
|
|
|
54
54
|
}
|
|
55
55
|
return startIndex + 1;
|
|
56
56
|
}
|
|
57
|
+
function genericFunctionSymbol(line) {
|
|
58
|
+
const opening = line.indexOf("(");
|
|
59
|
+
if (opening <= 0)
|
|
60
|
+
return null;
|
|
61
|
+
let depth = 0;
|
|
62
|
+
let closing = -1;
|
|
63
|
+
for (let index = opening; index < line.length; index += 1) {
|
|
64
|
+
if (line[index] === "(")
|
|
65
|
+
depth += 1;
|
|
66
|
+
if (line[index] !== ")")
|
|
67
|
+
continue;
|
|
68
|
+
depth -= 1;
|
|
69
|
+
if (depth === 0) {
|
|
70
|
+
closing = index;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (closing < 0)
|
|
75
|
+
return null;
|
|
76
|
+
const suffix = line.slice(closing + 1).trimStart();
|
|
77
|
+
if (!suffix.startsWith("{") && !suffix.startsWith("=>"))
|
|
78
|
+
return null;
|
|
79
|
+
const prefix = line.slice(0, opening).trim();
|
|
80
|
+
if (/[=;{}()]/.test(prefix))
|
|
81
|
+
return null;
|
|
82
|
+
const tokens = prefix.split(/\s+/);
|
|
83
|
+
const symbol = tokens.at(-1) ?? "";
|
|
84
|
+
return tokens.length >= 2 && /^[A-Za-z_$][\w$]*$/.test(symbol) ? symbol : null;
|
|
85
|
+
}
|
|
86
|
+
function firstPatternSymbol(line, patterns) {
|
|
87
|
+
for (const { kind, expression } of patterns) {
|
|
88
|
+
const match = expression.exec(line);
|
|
89
|
+
if (match)
|
|
90
|
+
return { kind, symbol: match[1] };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
57
94
|
function directSymbols(filePath, content) {
|
|
58
95
|
const extension = path.extname(filePath).toLowerCase();
|
|
59
96
|
const python = extension === ".py";
|
|
@@ -73,21 +110,20 @@ function directSymbols(filePath, content) {
|
|
|
73
110
|
kind: "function",
|
|
74
111
|
expression: /^\s*(?:(?:export|public|private|protected|internal|async|static|final|pub(?:\([^)]*\))?)\s+)*(?:func|fn|function)\s+([A-Za-z_$][\w$]*)\s*\(/,
|
|
75
112
|
},
|
|
76
|
-
{
|
|
77
|
-
kind: "function",
|
|
78
|
-
expression: /^\s*(?:(?:export|public|private|protected|internal|async|static|final)\s+)*(?:[A-Za-z_$][\w$<>,.?[\]\s:*&]+\s+)+([A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?:\{|=>)/,
|
|
79
|
-
},
|
|
80
113
|
{
|
|
81
114
|
kind: "function",
|
|
82
115
|
expression: /^\s*(?:(?:export|const|let|var|public|private|protected|static|readonly)\s+)*([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/,
|
|
83
116
|
},
|
|
84
117
|
];
|
|
85
118
|
for (const [index, line] of lines.entries()) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
let found = firstPatternSymbol(line, patterns);
|
|
120
|
+
if (!found && !python) {
|
|
121
|
+
const symbol = genericFunctionSymbol(line);
|
|
122
|
+
if (symbol)
|
|
123
|
+
found = { kind: "function", symbol };
|
|
124
|
+
}
|
|
125
|
+
if (found) {
|
|
126
|
+
const { kind, symbol } = found;
|
|
91
127
|
const visibility = line.match(/\b(public|private|protected|internal)\b/)?.[1] ??
|
|
92
128
|
(python && symbol.startsWith("_") ? "private" : "default");
|
|
93
129
|
symbols.push({
|
|
@@ -104,7 +140,6 @@ function directSymbols(filePath, content) {
|
|
|
104
140
|
exported: /\bexport\b/.test(line) || /\bpub\b/.test(line),
|
|
105
141
|
evidenceQuality: "lexical-structural-fallback",
|
|
106
142
|
});
|
|
107
|
-
break;
|
|
108
143
|
}
|
|
109
144
|
}
|
|
110
145
|
return symbols;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# knodin 0.7.4
|
|
2
|
+
|
|
3
|
+
This patch release preserves the opt-in diagnostics and support-bundle workflow
|
|
4
|
+
introduced in 0.7.3 while hardening the structural cold-start parser.
|
|
5
|
+
|
|
6
|
+
- Replaces a potentially super-linear declaration-matching expression with a
|
|
7
|
+
bounded, linear parser for typed function declarations.
|
|
8
|
+
- Handles balanced parameter lists without mistaking calls or control-flow
|
|
9
|
+
statements for declarations.
|
|
10
|
+
- Adds regression coverage for typed declarations, nested calls, and negative
|
|
11
|
+
structural matches.
|
|
12
|
+
|
|
13
|
+
Diagnostics remain local and disabled until a user enables them. Knodin never
|
|
14
|
+
uploads a diagnostics bundle automatically; the user must inspect and
|
|
15
|
+
deliberately share it for troubleshooting.
|
|
16
|
+
|
|
17
|
+
This remains an ordinary 0.x release, not a dogfood-only build and not GA.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# knodin 0.7.5
|
|
2
|
+
|
|
3
|
+
This patch release makes the one-tool MCP gateway more resilient and corrects
|
|
4
|
+
the immutable-package handoff used by internal Artifactory publication.
|
|
5
|
+
|
|
6
|
+
- Serializes parallel gateway operations so expensive graph requests do not
|
|
7
|
+
compete for the same local engine and database resources.
|
|
8
|
+
- Emits MCP progress notifications for queued and long-running operations when
|
|
9
|
+
the client supplies a progress token.
|
|
10
|
+
- Returns stable, actionable tool-error details while keeping the MCP transport
|
|
11
|
+
available for subsequent requests.
|
|
12
|
+
- On release retries, downloads npm's immutable archive, verifies its registry
|
|
13
|
+
integrity, proves its unpacked content matches the release build, and uses
|
|
14
|
+
those exact bytes for attestations and downstream publication.
|
|
15
|
+
|
|
16
|
+
Diagnostics remain local and disabled until a user enables them. Knodin never
|
|
17
|
+
uploads a diagnostics bundle automatically; the user must inspect and
|
|
18
|
+
deliberately share it for troubleshooting.
|
|
19
|
+
|
|
20
|
+
This remains an ordinary 0.x release, not a dogfood-only build and not GA.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knodin",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "knodin — source-evidenced local code intelligence with known bounds. Stable identity, fresh evidence, truthful budgets, and recoverable bounded views.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"docs/releases/0.7.1.md",
|
|
50
50
|
"docs/releases/0.7.2.md",
|
|
51
51
|
"docs/releases/0.7.3.md",
|
|
52
|
+
"docs/releases/0.7.4.md",
|
|
53
|
+
"docs/releases/0.7.5.md",
|
|
52
54
|
"docs/assets/knodin-favicon.svg",
|
|
53
55
|
"docs/SYSTEMS-AND-RELATIONSHIPS.md",
|
|
54
56
|
"docs/TELEMETRY.md",
|