simt-dev 0.2.1 → 0.3.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.
- package/CHANGELOG.md +239 -0
- package/bin/simt-dev.mjs +21 -2
- package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
- package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
- package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
- package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
- package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
- package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
- package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
- package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
- package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
- package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
- package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
- package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
- package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
- package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
- package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
- package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
- package/package.json +2 -1
- package/rules/core.md +1 -0
- package/rules/stacks/android.md +2 -0
- package/rules/stacks/cloudflare-workers.md +2 -0
- package/rules/stacks/connectiq.md +1 -0
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/dart.md +2 -0
- package/rules/stacks/deno.md +2 -0
- package/rules/stacks/dotnet.md +2 -0
- package/rules/stacks/elixir.md +2 -0
- package/rules/stacks/go.md +2 -0
- package/rules/stacks/jvm.md +2 -0
- package/rules/stacks/node.md +2 -0
- package/rules/stacks/python.md +2 -0
- package/rules/stacks/react-native.md +2 -0
- package/rules/stacks/react-web.md +2 -0
- package/rules/stacks/ruby.md +2 -0
- package/rules/stacks/rust.md +2 -0
- package/rules/stacks/swift.md +2 -0
- package/rules/stacks/typescript.md +2 -0
- package/rules/stacks/uv.md +2 -0
- package/src/agenthooks/decide.mjs +123 -0
- package/src/agenthooks/policy.mjs +78 -0
- package/src/agenthooks/render.mjs +240 -0
- package/src/argv.mjs +55 -1
- package/src/bench/ablate.mjs +630 -0
- package/src/bench/adapters.json +149 -0
- package/src/bench/grade.mjs +229 -0
- package/src/bench/mine.mjs +811 -0
- package/src/bench/probes.mjs +1350 -0
- package/src/bench/report.mjs +899 -0
- package/src/bench/run.mjs +1002 -0
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +359 -0
- package/src/commands/compare.mjs +15 -0
- package/src/commands/graph.mjs +45 -21
- package/src/commands/harness.mjs +301 -0
- package/src/commands/help.mjs +73 -2
- package/src/commands/index.mjs +67 -25
- package/src/commands/init.mjs +12 -8
- package/src/commands/mcp.mjs +20 -5
- package/src/commands/plan.mjs +99 -0
- package/src/commands/rules.mjs +42 -57
- package/src/commands/skills.mjs +67 -92
- package/src/commands/skillsinstall.mjs +17 -2
- package/src/commands/sync.mjs +62 -6
- package/src/commands/tokens.mjs +169 -0
- package/src/compare/row.mjs +73 -2
- package/src/decisions/index.mjs +23 -5
- package/src/graph/spec.mjs +227 -0
- package/src/graph/speccheck.mjs +68 -0
- package/src/hooks/dispatcher.mjs +35 -3
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/mcp/resources.mjs +157 -0
- package/src/mcp/schema.mjs +139 -0
- package/src/mcp/server.mjs +228 -20
- package/src/mcp/tools.mjs +372 -1
- package/src/measure/context.mjs +542 -0
- package/src/measure/loaderView.mjs +435 -0
- package/src/measure/session.mjs +267 -0
- package/src/monorepo.mjs +339 -0
- package/src/output/index.mjs +78 -2
- package/src/packs.mjs +182 -0
- package/src/plan/index.mjs +650 -0
- package/src/plan/template.mjs +331 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/manifest.mjs +114 -3
- package/src/projection/project.mjs +140 -9
- package/src/projection/rules.mjs +365 -22
- package/src/projection/surfaces.mjs +155 -23
- package/src/projection/targets.mjs +37 -0
- package/src/toolwords.mjs +69 -0
- package/templates/plan/tracker.json +36 -0
package/src/mcp/server.mjs
CHANGED
|
@@ -21,8 +21,26 @@
|
|
|
21
21
|
|
|
22
22
|
import { createInterface } from 'node:readline';
|
|
23
23
|
import { CLI } from '../name.mjs';
|
|
24
|
+
import { RESOURCE_TEMPLATES, listResources, readResource } from './resources.mjs';
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Every protocol revision this server speaks, newest first.
|
|
28
|
+
*
|
|
29
|
+
* The list is a claim about revisions that were read, not about ones that exist: each
|
|
30
|
+
* entry names a published revision and the behaviour this file implements for it, and
|
|
31
|
+
* ADR-068 records what changed. A revision not in this list is answered with
|
|
32
|
+
* `UnsupportedProtocolVersionError` rather than an optimistic guess, because the failure
|
|
33
|
+
* mode of guessing is a client that believes a feature is on and watches it misbehave.
|
|
34
|
+
*/
|
|
35
|
+
export const SUPPORTED_VERSIONS = ['2026-07-28', '2025-11-25', '2025-06-18'];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The revision this server answers with when the client has not asked for one.
|
|
39
|
+
*
|
|
40
|
+
* Named the way every caller has always named it, and it is the newest supported
|
|
41
|
+
* revision rather than the only one: a client that asks for an older one gets it.
|
|
42
|
+
*/
|
|
43
|
+
export const PROTOCOL_VERSION = SUPPORTED_VERSIONS[0];
|
|
26
44
|
|
|
27
45
|
/** JSON-RPC codes. -32700/-32600/-32601 are the spec's parse/request/method set. */
|
|
28
46
|
export const RPC = {
|
|
@@ -30,16 +48,60 @@ export const RPC = {
|
|
|
30
48
|
REQUEST: -32600,
|
|
31
49
|
METHOD: -32601,
|
|
32
50
|
INTERNAL: -32603,
|
|
51
|
+
/**
|
|
52
|
+
* `UnsupportedProtocolVersionError`, renumbered to -32022 in 2026-07-28. The spec
|
|
53
|
+
* reserves -32000..-32019 for implementations and -32020..-32099 for itself, so the
|
|
54
|
+
* older -32004 would now be an implementation-defined code in the wrong range.
|
|
55
|
+
*/
|
|
56
|
+
UNSUPPORTED_VERSION: -32022,
|
|
57
|
+
/** Resource not found: -32002 in every revision up to 2025-11-25, -32602 after. */
|
|
58
|
+
RESOURCE_NOT_FOUND_LEGACY: -32002,
|
|
59
|
+
RESOURCE_NOT_FOUND: -32602,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** The `_meta` keys 2026-07-28 puts on a request and in a result. */
|
|
63
|
+
export const META = {
|
|
64
|
+
PROTOCOL_VERSION: 'io.modelcontextprotocol/protocolVersion',
|
|
65
|
+
CLIENT_INFO: 'io.modelcontextprotocol/clientInfo',
|
|
66
|
+
CLIENT_CAPABILITIES: 'io.modelcontextprotocol/clientCapabilities',
|
|
67
|
+
SERVER_INFO: 'io.modelcontextprotocol/serverInfo',
|
|
33
68
|
};
|
|
34
69
|
|
|
70
|
+
/** Revisions that removed `ping`, per the 2026-07-28 changelog (SEP-2575). */
|
|
71
|
+
const PING_REMOVED_IN = '2026-07-28';
|
|
72
|
+
|
|
35
73
|
const SERVER_INFO = { name: 'simt-dev', title: 'SIMT developer and agent tooling' };
|
|
36
74
|
|
|
75
|
+
/**
|
|
76
|
+
* One hour, and the reason it is a number rather than an omission.
|
|
77
|
+
*
|
|
78
|
+
* `ttlMs` and `cacheScope` are required on list results from 2026-07-28, and the value
|
|
79
|
+
* is a fact about this server rather than a courtesy: the tool list changes when the
|
|
80
|
+
* release changes and not otherwise, so an hour is long enough to spare a client the
|
|
81
|
+
* round trip and short enough that a client started from a stale one re-reads it within
|
|
82
|
+
* a working day. `private`, because the list names the reader's own repository and no
|
|
83
|
+
* intermediary may hold it.
|
|
84
|
+
*/
|
|
85
|
+
const LIST_CACHE = { ttlMs: 3600000, cacheScope: 'private' };
|
|
86
|
+
|
|
37
87
|
/**
|
|
38
88
|
* Handle one parsed request. Pure with respect to the process: returns the response
|
|
39
89
|
* object or null (for a notification), and never writes anywhere. Testable without a
|
|
40
90
|
* pipe, which is the only way to test a line protocol honestly.
|
|
91
|
+
*
|
|
92
|
+
* `state` is the negotiated revision, and it is the only thing this function mutates.
|
|
93
|
+
* 2026-07-28 removed the handshake and made every request carry its own version, so a
|
|
94
|
+
* session is not part of the protocol any more - but a client on an older revision
|
|
95
|
+
* negotiates once and then sends bare requests, and answering those at the newest
|
|
96
|
+
* revision would hand it fields it did not ask for. So the version is read from the
|
|
97
|
+
* request when there is one and remembered only for the clients that need it.
|
|
98
|
+
*
|
|
99
|
+
* @param {object|null} request a parsed JSON-RPC message
|
|
100
|
+
* @param {object[]} tools
|
|
101
|
+
* @param {{packageVersion?: string, state?: {version: string|null}}} options
|
|
102
|
+
* @returns {Promise<object|null>}
|
|
41
103
|
*/
|
|
42
|
-
export async function handle(request, tools, {
|
|
104
|
+
export async function handle(request, tools, { packageVersion, state } = {}) {
|
|
43
105
|
if (request === null || typeof request !== 'object' || Array.isArray(request)) {
|
|
44
106
|
return error(null, RPC.REQUEST, 'request must be a JSON object');
|
|
45
107
|
}
|
|
@@ -54,25 +116,68 @@ export async function handle(request, tools, { version } = {}) {
|
|
|
54
116
|
}
|
|
55
117
|
if (isNotification) return null;
|
|
56
118
|
|
|
119
|
+
const asked = params?._meta?.[META.PROTOCOL_VERSION];
|
|
120
|
+
if (typeof asked === 'string' && !SUPPORTED_VERSIONS.includes(asked)) {
|
|
121
|
+
// The one place a client checks whether it got what it asked for, so this is where
|
|
122
|
+
// an unsupported version has to fail. The message names the revisions that do work,
|
|
123
|
+
// because "unsupported" alone leaves the client to guess between the two answers a
|
|
124
|
+
// server can give.
|
|
125
|
+
return error(
|
|
126
|
+
id,
|
|
127
|
+
RPC.UNSUPPORTED_VERSION,
|
|
128
|
+
`protocol version ${asked} is not supported; this server speaks ${SUPPORTED_VERSIONS.join(', ')}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
const negotiated =
|
|
132
|
+
typeof asked === 'string' ? asked : method === 'initialize' ? negotiate(params?.protocolVersion) : state?.version;
|
|
133
|
+
// A caller that passes no `state` gets one request with no memory: `serve` threads one
|
|
134
|
+
// through the stream, and a test that calls this directly usually wants exactly that.
|
|
135
|
+
const session = state ?? { version: null };
|
|
136
|
+
if (method === 'initialize') session.version = negotiated;
|
|
137
|
+
const v = negotiated ?? PROTOCOL_VERSION;
|
|
138
|
+
|
|
139
|
+
const reply = (value) => result(id, decorate(value, v, { packageVersion, tools }));
|
|
140
|
+
|
|
57
141
|
switch (method) {
|
|
58
142
|
case 'initialize':
|
|
59
|
-
return
|
|
60
|
-
protocolVersion:
|
|
61
|
-
capabilities:
|
|
62
|
-
serverInfo
|
|
63
|
-
|
|
64
|
-
|
|
143
|
+
return reply({
|
|
144
|
+
protocolVersion: v,
|
|
145
|
+
capabilities: capabilities(),
|
|
146
|
+
// `serverInfo` stays in the body here even on 2026-07-28: a client that has just
|
|
147
|
+
// called a method the newest revision removed is by definition an older client,
|
|
148
|
+
// and the field is where it looks.
|
|
149
|
+
serverInfo: serverInfo(packageVersion),
|
|
150
|
+
instructions: INSTRUCTIONS,
|
|
65
151
|
});
|
|
66
152
|
|
|
67
153
|
case 'notifications/initialized':
|
|
68
154
|
case 'initialized':
|
|
69
155
|
return null;
|
|
70
156
|
|
|
157
|
+
// MUST, from 2026-07-28 on. Cheap, stateless, and the stdio way for a client that
|
|
158
|
+
// supports both shapes to find out which one it is talking to before it commits.
|
|
159
|
+
case 'server/discover':
|
|
160
|
+
return reply({
|
|
161
|
+
supportedVersions: [...SUPPORTED_VERSIONS],
|
|
162
|
+
capabilities: capabilities(),
|
|
163
|
+
instructions: INSTRUCTIONS,
|
|
164
|
+
...(cacheable(v) ? LIST_CACHE : {}),
|
|
165
|
+
});
|
|
166
|
+
|
|
71
167
|
case 'ping':
|
|
72
|
-
|
|
168
|
+
// Removed in 2026-07-28, and kept before it: a client that speaks an older
|
|
169
|
+
// revision and probes liveness gets an answer, and one that speaks the newest
|
|
170
|
+
// gets told the method is gone rather than a success it would read as support.
|
|
171
|
+
if (atLeast(v, PING_REMOVED_IN)) {
|
|
172
|
+
return error(id, RPC.METHOD, `ping was removed in ${PING_REMOVED_IN}; this session negotiated ${v}`);
|
|
173
|
+
}
|
|
174
|
+
return reply({});
|
|
73
175
|
|
|
74
176
|
case 'tools/list':
|
|
75
|
-
return
|
|
177
|
+
return reply({
|
|
178
|
+
tools: tools.map((t) => describe(t, v)),
|
|
179
|
+
...(cacheable(v) ? LIST_CACHE : {}),
|
|
180
|
+
});
|
|
76
181
|
|
|
77
182
|
case 'tools/call': {
|
|
78
183
|
const name = params?.name;
|
|
@@ -85,20 +190,75 @@ export async function handle(request, tools, { version } = {}) {
|
|
|
85
190
|
const args = params?.arguments ?? {};
|
|
86
191
|
try {
|
|
87
192
|
const payload = await tool.run(args);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
193
|
+
// Both blocks, always. `structuredContent` is what a client validates against
|
|
194
|
+
// `outputSchema` and what it can index; the text block is what a client that
|
|
195
|
+
// predates structured output reads. Emitting one without the other leaves half
|
|
196
|
+
// the clients guessing, which is the state this release was filed for.
|
|
197
|
+
return reply({
|
|
198
|
+
content: [contentOf(payload)],
|
|
199
|
+
structuredContent: payload,
|
|
200
|
+
isError: false,
|
|
93
201
|
});
|
|
202
|
+
} catch (failure) {
|
|
203
|
+
// An error is a result too, and it carries the same two shapes: the remedy is
|
|
204
|
+
// the part a model needs to act on, and it has to survive in whichever block the
|
|
205
|
+
// client reads.
|
|
206
|
+
const payload = { error: failure.message, remedy: failure.remedy ?? null };
|
|
207
|
+
return reply({ content: [contentOf(payload)], structuredContent: payload, isError: true });
|
|
94
208
|
}
|
|
95
209
|
}
|
|
96
210
|
|
|
211
|
+
case 'resources/list':
|
|
212
|
+
return reply(
|
|
213
|
+
cacheable(v) ? { resources: await listResources(), ...LIST_CACHE } : { resources: await listResources() },
|
|
214
|
+
);
|
|
215
|
+
case 'resources/templates/list':
|
|
216
|
+
return reply(
|
|
217
|
+
cacheable(v)
|
|
218
|
+
? { resourceTemplates: RESOURCE_TEMPLATES, ...LIST_CACHE }
|
|
219
|
+
: { resourceTemplates: RESOURCE_TEMPLATES },
|
|
220
|
+
);
|
|
221
|
+
case 'resources/read': {
|
|
222
|
+
// The code for a resource that is not there CHANGED, and a server that answers
|
|
223
|
+
// with one code to both revisions is a server that is wrong to whichever half of
|
|
224
|
+
// the client's world it is not. The newest revision forbids an empty `contents`
|
|
225
|
+
// array as the answer, so "absent" can only be an error.
|
|
226
|
+
const code = atLeast(v, '2026-07-28') ? RPC.RESOURCE_NOT_FOUND : RPC.RESOURCE_NOT_FOUND_LEGACY;
|
|
227
|
+
const uri = request.params?.uri;
|
|
228
|
+
const found = await readResource(uri);
|
|
229
|
+
if (found === null) return error(id, code, `no such resource: ${String(uri)}`);
|
|
230
|
+
return reply(
|
|
231
|
+
cacheable(v) ? { contents: [found], ...LIST_CACHE } : { contents: [found] },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
97
234
|
default:
|
|
98
235
|
return isNotification ? null : error(id, RPC.METHOD, `unknown method "${method}"`);
|
|
99
236
|
}
|
|
100
237
|
}
|
|
101
238
|
|
|
239
|
+
const INSTRUCTIONS =
|
|
240
|
+
`Read-only. Every answer is JSON. \`${CLI} skills brief\` is the one to call first: it returns the pre-assembled set for a task, plus what it deliberately left out and why.`;
|
|
241
|
+
|
|
242
|
+
function serverInfo(packageVersion) {
|
|
243
|
+
return { ...SERVER_INFO, version: packageVersion ?? '0.0.0' };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function capabilities() {
|
|
247
|
+
return {
|
|
248
|
+
tools: { listChanged: false },
|
|
249
|
+
// Unconditional, and that is the point: every rule and every decision record ships
|
|
250
|
+
// with the package, so there is always something to read. The empty object is the
|
|
251
|
+
// documented way to say "resources, but no subscriptions and no change
|
|
252
|
+
// notifications" — both of which this server has no way to honour, and a client
|
|
253
|
+
// told otherwise would subscribe to a stream that never arrives.
|
|
254
|
+
//
|
|
255
|
+
// It used to be conditional on a property hung on the tools array, which nothing
|
|
256
|
+
// ever set: a capability gated on a flag nobody flips is the same promise a client
|
|
257
|
+
// waits on forever, only quieter.
|
|
258
|
+
resources: {},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
102
262
|
/**
|
|
103
263
|
* Echo the client's protocol version when it is one this server implements.
|
|
104
264
|
*
|
|
@@ -107,16 +267,58 @@ export async function handle(request, tools, { version } = {}) {
|
|
|
107
267
|
* fails loudly at the one place the client checks.
|
|
108
268
|
*/
|
|
109
269
|
function negotiate(requested) {
|
|
110
|
-
|
|
111
|
-
|
|
270
|
+
return typeof requested === 'string' && SUPPORTED_VERSIONS.includes(requested)
|
|
271
|
+
? requested
|
|
272
|
+
: PROTOCOL_VERSION;
|
|
112
273
|
}
|
|
113
274
|
|
|
114
|
-
|
|
275
|
+
/** Revision ordering. These are dates, so lexicographic is chronological - stated here
|
|
276
|
+
* because "sort a version string" is the kind of helper that gets rewritten by someone
|
|
277
|
+
* who thinks it is a number, and this one is right. */
|
|
278
|
+
function atLeast(version, floor) {
|
|
279
|
+
return version >= floor;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** 2026-07-28 requires `ttlMs` and `cacheScope` on list results; earlier ones ignore them. */
|
|
283
|
+
function cacheable(version) {
|
|
284
|
+
return atLeast(version, '2026-07-28');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* What the newest revision adds to every result.
|
|
289
|
+
*
|
|
290
|
+
* `resultType` is required from 2026-07-28 and is absent before it — clients on older
|
|
291
|
+
* revisions treat it as an unknown field, and clients MUST read a missing one as
|
|
292
|
+
* `complete`, so emitting it unconditionally is the compatible half and omitting it on
|
|
293
|
+
* the new revision is not. `_meta.serverInfo` is a SHOULD, and it is how a stateless
|
|
294
|
+
* client learns who it is talking to now that `initialize` is gone.
|
|
295
|
+
*/
|
|
296
|
+
function decorate(value, version, { packageVersion }) {
|
|
297
|
+
if (!atLeast(version, '2026-07-28')) return value;
|
|
115
298
|
return {
|
|
299
|
+
resultType: 'complete',
|
|
300
|
+
...value,
|
|
301
|
+
_meta: { [META.SERVER_INFO]: serverInfo(packageVersion) },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function describe(tool, version) {
|
|
306
|
+
const entry = {
|
|
116
307
|
name: tool.name,
|
|
117
308
|
description: tool.description,
|
|
118
309
|
inputSchema: tool.inputSchema,
|
|
310
|
+
// Every tool here is a reader: none writes, and none writes twice. Saying so lets a
|
|
311
|
+
// client cache the answer and, where it offers, let the model call without a
|
|
312
|
+
// confirmation step. The declaration is per tool rather than global because the next
|
|
313
|
+
// tool this file gains may not be one.
|
|
314
|
+
annotations: { readOnlyHint: true, idempotentHint: true, ...(tool.annotations ?? {}) },
|
|
119
315
|
};
|
|
316
|
+
// `outputSchema` exists in all three revisions, and a tool that declares its input
|
|
317
|
+
// shape but not its output shape leaves the client guessing what a successful answer
|
|
318
|
+
// looks like - which is the same defect as a returned value nothing can be checked
|
|
319
|
+
// against.
|
|
320
|
+
if (tool.outputSchema) entry.outputSchema = tool.outputSchema;
|
|
321
|
+
return entry;
|
|
120
322
|
}
|
|
121
323
|
|
|
122
324
|
/** Content is one text block. Bounded, and truncation is announced in the text itself -
|
|
@@ -146,15 +348,21 @@ function error(id, code, message) {
|
|
|
146
348
|
* Lines are processed strictly in order. A server that interleaves responses would let a
|
|
147
349
|
* client match the wrong answer to the wrong question, and the tools here are already
|
|
148
350
|
* sequential and cheap - parallelising them would buy nothing and cost correctness.
|
|
351
|
+
*
|
|
352
|
+
* One `state` for the whole stream, holding the revision a client negotiated through
|
|
353
|
+
* `initialize`. That is the residue of a handshake the 2026-07-28 revision removed, kept
|
|
354
|
+
* only because a client on an older revision sends bare requests afterwards and expects
|
|
355
|
+
* the answers it agreed to.
|
|
149
356
|
*/
|
|
150
|
-
export async function serve(tools, { input = process.stdin, output = process.stdout,
|
|
357
|
+
export async function serve(tools, { input = process.stdin, output = process.stdout, packageVersion, onError } = {}) {
|
|
151
358
|
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
359
|
+
const state = { version: null };
|
|
152
360
|
for await (const line of rl) {
|
|
153
361
|
if (line.trim() === '') continue;
|
|
154
362
|
|
|
155
363
|
let response;
|
|
156
364
|
try {
|
|
157
|
-
response = await handle(JSON.parse(line), tools, {
|
|
365
|
+
response = await handle(JSON.parse(line), tools, { packageVersion, state });
|
|
158
366
|
} catch (parseFailure) {
|
|
159
367
|
response = error(null, RPC.PARSE, `invalid JSON: ${parseFailure.message}`);
|
|
160
368
|
}
|