@hraness/wordcell 0.22.5 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +56 -18
  2. package/dist/authoring.js +2 -2
  3. package/dist/{authority-3ga47syb.js → authority-35cbvp1x.js} +2 -2
  4. package/dist/benchmark.js +3 -3
  5. package/dist/capture.js +6 -6
  6. package/dist/cli.js +26 -24
  7. package/dist/clip/acquire.js +3 -3
  8. package/dist/clip/args.js +1 -1
  9. package/dist/clip/cli.js +6 -6
  10. package/dist/clip/doctor.js +4 -4
  11. package/dist/evaluation-builder.js +10 -10
  12. package/dist/evaluation-kb.js +10 -10
  13. package/dist/graph-authority.js +4 -4
  14. package/dist/graph-percolation.js +6 -6
  15. package/dist/graph.js +3 -1
  16. package/dist/{index-bcknqxrq.js → index-0eacgvpv.js} +9 -1
  17. package/dist/{index-f75zmmdr.js → index-1er88ckw.js} +4 -4
  18. package/dist/{index-8v6k9h4r.js → index-1s8mc4hz.js} +1 -1
  19. package/dist/{index-de2w8crk.js → index-2fmnttca.js} +82 -15
  20. package/dist/{index-aer0jdrq.js → index-ahyhryb8.js} +1 -1
  21. package/dist/{index-f7fnww7a.js → index-b0khj0h5.js} +164 -9
  22. package/dist/{index-qdb8f8va.js → index-bdwcjvr4.js} +1 -1
  23. package/dist/{index-dfag79p7.js → index-byz4kzww.js} +3 -0
  24. package/dist/{index-hgve9rh2.js → index-cd75vky9.js} +96 -1
  25. package/dist/{index-n57tewfr.js → index-fp732bgg.js} +2 -2
  26. package/dist/{index-zy7an84p.js → index-jvb7w0gg.js} +5 -2
  27. package/dist/{index-f984hw45.js → index-k86wepd8.js} +1 -1
  28. package/dist/{index-1r5rnkfr.js → index-mnq2wy34.js} +2 -2
  29. package/dist/{index-p96kb6t2.js → index-nn67kzwa.js} +1 -1
  30. package/dist/{index-fc4dr114.js → index-nwrehke7.js} +1 -1
  31. package/dist/{index-n9kwenpt.js → index-p70t8j13.js} +1 -1
  32. package/dist/{index-pz2b2x0y.js → index-pgtm2nhf.js} +1 -1
  33. package/dist/{index-054mb7d3.js → index-qfqjfxaa.js} +10 -9
  34. package/dist/{index-xzvcw9ga.js → index-r0m5taz2.js} +2 -2
  35. package/dist/{index-h2qn3y3f.js → index-r4qs6vq4.js} +4 -4
  36. package/dist/{index-gtwqye5a.js → index-tcaq1c7f.js} +4 -4
  37. package/dist/{index-42tb8qqp.js → index-tvd2sqrx.js} +7 -7
  38. package/dist/{index-8bgnhjgq.js → index-vtwbapjr.js} +3810 -159
  39. package/dist/{index-g5vsqmdy.js → index-xwxy71ew.js} +190 -4
  40. package/dist/index.js +15 -13
  41. package/dist/pdf.js +3 -3
  42. package/dist/percolate.js +2 -2
  43. package/dist/portfolio.js +10 -10
  44. package/dist/publish.js +3 -3
  45. package/dist/sdk.js +9 -9
  46. package/dist/search.js +2 -2
  47. package/dist/semantic.js +4 -4
  48. package/dist/{support-jf9v10j7.js → support-c8hy7qfa.js} +25 -23
  49. package/dist/url-intelligence.js +1 -1
  50. package/dist/workflows/decision-context.js +10 -10
  51. package/dist/workflows/index.js +10 -10
  52. package/package.json +11 -4
  53. package/skills/wordcell/AGENTS.md +1 -0
  54. package/skills/wordcell/SKILL.md +5 -3
  55. package/skills/wordcell/references/query.md +35 -1
  56. package/skills/wordcell/references/session-memory.md +207 -0
  57. package/src/authoring-import.ts +32 -0
  58. package/src/authoring-model.ts +185 -2
  59. package/src/authoring-program.ts +19 -7
  60. package/src/cli-help.ts +893 -0
  61. package/src/cli-intro.ts +1 -1
  62. package/src/cli-program.ts +571 -80
  63. package/src/cli-style.ts +132 -0
  64. package/src/clip/acquire.ts +20 -0
  65. package/src/clip/args.ts +3 -0
  66. package/src/clip/cli.ts +82 -5
  67. package/src/clip/cookie-permission.ts +241 -0
  68. package/src/clip/url-metadata-cli.ts +6 -5
  69. package/src/clip/url-metadata.ts +14 -0
  70. package/src/graph.ts +9 -4
  71. package/src/import-supermemory.ts +1629 -0
  72. package/src/mcp-server.ts +415 -0
  73. package/src/mcp-tools.ts +1122 -0
  74. package/src/pdf/cli.ts +5 -4
  75. package/src/support-profile.ts +1 -1
  76. package/src/support.ts +3 -0
@@ -0,0 +1,415 @@
1
+ /**
2
+ * Local Model Context Protocol server over stdio.
3
+ *
4
+ * This module owns framing, JSON-RPC parsing, and dispatch. It holds no vault
5
+ * logic; tools arrive from `mcp-tools.ts` through the dispatcher options.
6
+ */
7
+
8
+ import { readFile } from "node:fs/promises";
9
+ import { format } from "node:util";
10
+
11
+ /** Newest handshake-era revision; the hosted `/api/v1/mcp` route speaks the same one. */
12
+ export const LATEST_PROTOCOL_VERSION = "2025-11-25";
13
+
14
+ /**
15
+ * Revisions this server answers with, newest first. 2025-03-26 is excluded because it
16
+ * requires servers to accept JSON-RPC batches, and 2024-11-05 lacks tool annotations
17
+ * and structured tool output.
18
+ */
19
+ export const SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"] as const;
20
+
21
+ export type SupportedProtocolVersion = (typeof SUPPORTED_PROTOCOL_VERSIONS)[number];
22
+
23
+ /** Largest accepted frame, counted in bytes before the delimiting newline. */
24
+ export const MAX_FRAME_BYTES = 1_048_576;
25
+
26
+ export const SERVER_NAME = "hraness-wordcell";
27
+
28
+ export const PARSE_ERROR = -32700;
29
+ export const INVALID_REQUEST = -32600;
30
+ export const METHOD_NOT_FOUND = -32601;
31
+ export const INVALID_PARAMS = -32602;
32
+ export const INTERNAL_ERROR = -32603;
33
+
34
+ export type RequestId = string | number;
35
+
36
+ export type Frame =
37
+ | { readonly kind: "request"; readonly id: RequestId; readonly method: string; readonly params: unknown }
38
+ | { readonly kind: "notification"; readonly method: string; readonly params: unknown }
39
+ | { readonly kind: "response" }
40
+ | {
41
+ readonly kind: "invalid";
42
+ readonly id: RequestId | null;
43
+ readonly code: typeof PARSE_ERROR | typeof INVALID_REQUEST;
44
+ readonly message: string;
45
+ };
46
+
47
+ /** One newline-delimited unit read from the input stream. */
48
+ export type LineEvent =
49
+ | { readonly kind: "line"; readonly text: string }
50
+ | { readonly kind: "oversized" }
51
+ | { readonly kind: "unparseable" };
52
+
53
+ /**
54
+ * Answers the requested revision when this server supports it, else the newest one it
55
+ * supports (2025-11-25 lifecycle: MUST echo a supported request, otherwise SHOULD offer
56
+ * the latest).
57
+ */
58
+ export function negotiateProtocolVersion(requested: unknown): SupportedProtocolVersion {
59
+ const match = SUPPORTED_PROTOCOL_VERSIONS.find((version) => version === requested);
60
+ return match ?? LATEST_PROTOCOL_VERSION;
61
+ }
62
+
63
+ function isRequestId(value: unknown): value is RequestId {
64
+ return typeof value === "string" || (typeof value === "number" && Number.isSafeInteger(value));
65
+ }
66
+
67
+ function isRecord(value: unknown): value is Record<string, unknown> {
68
+ return typeof value === "object" && value !== null && !Array.isArray(value);
69
+ }
70
+
71
+ function invalid(id: RequestId | null, message: string): Frame {
72
+ return { kind: "invalid", id, code: INVALID_REQUEST, message };
73
+ }
74
+
75
+ /** Parses one frame's text as a single JSON-RPC 2.0 message. Batches are refused. */
76
+ export function parseFrame(text: string): Frame {
77
+ let value: unknown;
78
+ try {
79
+ value = JSON.parse(text);
80
+ } catch {
81
+ return { kind: "invalid", id: null, code: PARSE_ERROR, message: "Frame must be valid JSON." };
82
+ }
83
+ if (Array.isArray(value)) return invalid(null, "Batch requests are not supported.");
84
+ if (!isRecord(value)) return invalid(null, "Expected a JSON-RPC object.");
85
+ const hasMethod = Object.hasOwn(value, "method");
86
+ if (!hasMethod && (Object.hasOwn(value, "result") || Object.hasOwn(value, "error"))) {
87
+ return { kind: "response" };
88
+ }
89
+ const hasId = Object.hasOwn(value, "id");
90
+ const id = hasId && isRequestId(value.id) ? value.id : null;
91
+ if (value.jsonrpc !== "2.0") return invalid(id, "Expected jsonrpc \"2.0\".");
92
+ if (hasId && id === null) return invalid(null, "Request id must be a string or a safe integer.");
93
+ if (!hasMethod) return invalid(id, "Missing method.");
94
+ if (typeof value.method !== "string") return invalid(id, "Method must be a string.");
95
+ const params = value.params;
96
+ if (params !== undefined && !isRecord(params)) return invalid(id, "Params must be an object.");
97
+ if (id === null) return { kind: "notification", method: value.method, params };
98
+ return { kind: "request", id, method: value.method, params };
99
+ }
100
+
101
+ /** Maps a line event to a frame. Oversized and non-UTF-8 lines have no recoverable id. */
102
+ export function frameFromLine(event: LineEvent, maxFrameBytes = MAX_FRAME_BYTES): Frame {
103
+ if (event.kind === "line") return parseFrame(event.text);
104
+ if (event.kind === "oversized") return invalid(null, `Frame exceeds ${maxFrameBytes} bytes.`);
105
+ return { kind: "invalid", id: null, code: PARSE_ERROR, message: "Frame must be valid UTF-8." };
106
+ }
107
+
108
+ export type ReadFramesOptions = { readonly maxFrameBytes?: number };
109
+
110
+ const NEWLINE = 0x0a;
111
+ const CARRIAGE_RETURN = 0x0d;
112
+
113
+ /**
114
+ * Splits a byte stream on `\n`, drops one trailing `\r`, and skips blank lines. Each
115
+ * event is awaited before the next, so frames are handled strictly in order. A line
116
+ * longer than `maxFrameBytes` is discarded through its newline and reported once as
117
+ * `oversized`; a line that is not valid UTF-8 is reported as `unparseable`. A final
118
+ * line without a newline is still delivered when the stream ends.
119
+ */
120
+ export async function readFrames(
121
+ input: AsyncIterable<Uint8Array>,
122
+ onFrame: (event: LineEvent) => Promise<void>,
123
+ options: ReadFramesOptions = {},
124
+ ): Promise<void> {
125
+ const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
126
+ if (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes < 1) {
127
+ throw new RangeError("maxFrameBytes must be a positive safe integer.");
128
+ }
129
+ let pieces: Uint8Array[] = [];
130
+ let length = 0;
131
+ let oversized = false;
132
+
133
+ const append = (piece: Uint8Array) => {
134
+ if (oversized || piece.length === 0) return;
135
+ length += piece.length;
136
+ if (length > maxFrameBytes) {
137
+ oversized = true;
138
+ pieces = [];
139
+ return;
140
+ }
141
+ pieces.push(piece);
142
+ };
143
+
144
+ const finish = async () => {
145
+ const wasOversized = oversized;
146
+ const line = concat(pieces, length);
147
+ pieces = [];
148
+ length = 0;
149
+ oversized = false;
150
+ if (wasOversized) return onFrame({ kind: "oversized" });
151
+ const end = line.length > 0 && line[line.length - 1] === CARRIAGE_RETURN ? line.length - 1 : line.length;
152
+ let text: string;
153
+ try {
154
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(line.subarray(0, end));
155
+ } catch {
156
+ return onFrame({ kind: "unparseable" });
157
+ }
158
+ if (text.trim() === "") return;
159
+ return onFrame({ kind: "line", text });
160
+ };
161
+
162
+ for await (const chunk of input) {
163
+ let start = 0;
164
+ for (let index = chunk.indexOf(NEWLINE); index !== -1; index = chunk.indexOf(NEWLINE, start)) {
165
+ append(chunk.subarray(start, index));
166
+ await finish();
167
+ start = index + 1;
168
+ }
169
+ // Copy the tail: some streams reuse their chunk buffers after yielding them.
170
+ append(chunk.slice(start));
171
+ }
172
+ if (length > 0 || oversized) await finish();
173
+ }
174
+
175
+ function concat(pieces: readonly Uint8Array[], length: number): Uint8Array {
176
+ if (pieces.length === 1) return pieces[0]!;
177
+ const out = new Uint8Array(length);
178
+ let offset = 0;
179
+ for (const piece of pieces) {
180
+ out.set(piece, offset);
181
+ offset += piece.length;
182
+ }
183
+ return out;
184
+ }
185
+
186
+ export type ToolAnnotations = {
187
+ readonly title?: string;
188
+ readonly readOnlyHint: boolean;
189
+ readonly destructiveHint: boolean;
190
+ readonly idempotentHint: boolean;
191
+ readonly openWorldHint: boolean;
192
+ };
193
+
194
+ export type ToolDefinition = {
195
+ readonly name: string;
196
+ readonly title?: string;
197
+ readonly description: string;
198
+ readonly inputSchema: Readonly<Record<string, unknown>>;
199
+ readonly outputSchema?: Readonly<Record<string, unknown>>;
200
+ readonly annotations: ToolAnnotations;
201
+ };
202
+
203
+ export type CallToolResult = {
204
+ readonly content: readonly { readonly type: "text"; readonly text: string }[];
205
+ readonly structuredContent?: Readonly<Record<string, unknown>>;
206
+ readonly isError?: boolean;
207
+ };
208
+
209
+ /** The tool surface a server exposes. Tools absent from `tools` are unknown to callers. */
210
+ export type ToolCatalog = {
211
+ readonly tools: readonly ToolDefinition[];
212
+ call(name: string, args: Readonly<Record<string, unknown>>): Promise<CallToolResult>;
213
+ close(): Promise<void>;
214
+ };
215
+
216
+ export type DispatcherOptions = {
217
+ readonly catalog: ToolCatalog;
218
+ readonly instructions: string;
219
+ readonly version: string;
220
+ };
221
+
222
+ type JsonRpcResponse =
223
+ | { readonly jsonrpc: "2.0"; readonly id: RequestId; readonly result: unknown }
224
+ | { readonly jsonrpc: "2.0"; readonly id: RequestId | null; readonly error: { readonly code: number; readonly message: string } };
225
+
226
+ export type Dispatcher = {
227
+ /** Answers one frame. Notifications and client responses yield `null`. */
228
+ handle(frame: Frame): Promise<JsonRpcResponse | null>;
229
+ };
230
+
231
+ function errorResponse(id: RequestId | null, code: number, message: string): JsonRpcResponse {
232
+ return { jsonrpc: "2.0", id, error: { code, message } };
233
+ }
234
+
235
+ function errorMessage(error: unknown): string {
236
+ return error instanceof Error ? error.message : String(error);
237
+ }
238
+
239
+ /**
240
+ * Stateless like the hosted route: every method works without a prior `initialize`.
241
+ * `server/discover` and other unsupported methods get -32601, which dual-era clients
242
+ * treat as a cue to fall back to the `initialize` handshake.
243
+ */
244
+ export function createDispatcher(options: DispatcherOptions): Dispatcher {
245
+ const { catalog, instructions, version } = options;
246
+ const listed = new Set(catalog.tools.map((tool) => tool.name));
247
+
248
+ const request = async (id: RequestId, method: string, params: Readonly<Record<string, unknown>> | undefined) => {
249
+ switch (method) {
250
+ case "initialize":
251
+ return {
252
+ jsonrpc: "2.0",
253
+ id,
254
+ result: {
255
+ protocolVersion: negotiateProtocolVersion(params?.protocolVersion),
256
+ capabilities: { tools: { listChanged: false } },
257
+ serverInfo: { name: SERVER_NAME, version },
258
+ instructions,
259
+ },
260
+ } as const;
261
+ case "ping":
262
+ return { jsonrpc: "2.0", id, result: {} } as const;
263
+ case "tools/list":
264
+ return { jsonrpc: "2.0", id, result: { tools: catalog.tools } } as const;
265
+ case "tools/call": {
266
+ const name = params?.name;
267
+ const args = params !== undefined && Object.hasOwn(params, "arguments") ? params.arguments : {};
268
+ if (typeof name !== "string" || !isRecord(args)) {
269
+ return errorResponse(id, INVALID_PARAMS, "Invalid tools/call params.");
270
+ }
271
+ if (!listed.has(name)) return errorResponse(id, INVALID_PARAMS, `Unknown tool: ${name}`);
272
+ return { jsonrpc: "2.0", id, result: await catalog.call(name, args) } as const;
273
+ }
274
+ default:
275
+ return errorResponse(id, METHOD_NOT_FOUND, `Method ${method} is not supported.`);
276
+ }
277
+ };
278
+
279
+ return {
280
+ async handle(frame) {
281
+ if (frame.kind === "invalid") return errorResponse(frame.id, frame.code, frame.message);
282
+ if (frame.kind !== "request") return null;
283
+ try {
284
+ return await request(frame.id, frame.method, frame.params as Readonly<Record<string, unknown>> | undefined);
285
+ } catch (error: unknown) {
286
+ return errorResponse(frame.id, INTERNAL_ERROR, `Internal error: ${errorMessage(error)}`);
287
+ }
288
+ },
289
+ };
290
+ }
291
+
292
+ /** Serializes a response as one frame line; an unserializable result becomes -32603 for its id. */
293
+ export function encodeResponse(response: JsonRpcResponse): string {
294
+ try {
295
+ const text = JSON.stringify(response);
296
+ if (typeof text === "string") return `${text}\n`;
297
+ throw new TypeError("response is not serializable");
298
+ } catch (error: unknown) {
299
+ return `${JSON.stringify(errorResponse(response.id, INTERNAL_ERROR, `Internal error: ${errorMessage(error)}`))}\n`;
300
+ }
301
+ }
302
+
303
+ const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
304
+
305
+ /** Parses a stable package version: three canonical decimal components, each a safe integer. */
306
+ export function parseStableVersion(value: unknown): string {
307
+ const match = typeof value === "string" ? STABLE_VERSION.exec(value) : null;
308
+ if (match === null || match.slice(1).some((part) => !Number.isSafeInteger(Number(part)))) {
309
+ throw new TypeError("package.json version must be a stable x.y.z version.");
310
+ }
311
+ return match[0];
312
+ }
313
+
314
+ /**
315
+ * Reads this package's version for `serverInfo`. The relative URL reaches the package
316
+ * root from both `src/` and the flat `dist/` chunks.
317
+ */
318
+ export async function serverVersion(packageJson: URL = new URL("../package.json", import.meta.url)): Promise<string> {
319
+ const manifest: unknown = JSON.parse(await readFile(packageJson, "utf8"));
320
+ return parseStableVersion(isRecord(manifest) ? manifest.version : undefined);
321
+ }
322
+
323
+ export type StdoutGuard = {
324
+ /** Writes one complete frame line to the real stdout and resolves once it is flushed. */
325
+ writeFrame(line: string): Promise<void>;
326
+ restore(): void;
327
+ };
328
+
329
+ /**
330
+ * Holds stdout for protocol frames for the server's lifetime. Stray `process.stdout.write`
331
+ * and `console.log/info/debug` calls from dependencies go to stderr instead, the same
332
+ * boundary `runExecutable` applies to `--json` output.
333
+ */
334
+ export function guardStdout(): StdoutGuard {
335
+ const stdout = process.stdout;
336
+ const ownWrite = Object.getOwnPropertyDescriptor(stdout, "write");
337
+ const rawStdoutWrite = stdout.write.bind(stdout);
338
+ const rawStderrWrite = process.stderr.write.bind(process.stderr);
339
+ const originalConsole = { log: console.log, info: console.info, debug: console.debug };
340
+ let streamError: Error | undefined;
341
+ const onError = (error: Error) => {
342
+ streamError ??= error;
343
+ };
344
+ const restore = () => {
345
+ if (ownWrite === undefined) Reflect.deleteProperty(stdout, "write");
346
+ else Object.defineProperty(stdout, "write", ownWrite);
347
+ console.log = originalConsole.log;
348
+ console.info = originalConsole.info;
349
+ console.debug = originalConsole.debug;
350
+ stdout.off("error", onError);
351
+ };
352
+ const redirectedWrite = ((...arguments_: readonly unknown[]): boolean => {
353
+ Reflect.apply(rawStderrWrite, process.stderr, arguments_);
354
+ return true;
355
+ }) as typeof process.stdout.write;
356
+ const redirectedConsole = (...arguments_: readonly unknown[]): void => {
357
+ rawStderrWrite(`${format(...arguments_)}\n`);
358
+ };
359
+ try {
360
+ stdout.on("error", onError);
361
+ Object.defineProperty(stdout, "write", { configurable: true, writable: true, value: redirectedWrite });
362
+ console.log = redirectedConsole;
363
+ console.info = redirectedConsole;
364
+ console.debug = redirectedConsole;
365
+ } catch (error: unknown) {
366
+ restore();
367
+ throw error;
368
+ }
369
+ return {
370
+ writeFrame: (line) => new Promise<void>((resolve, reject) => {
371
+ if (streamError !== undefined) return reject(streamError);
372
+ rawStdoutWrite(line, (error?: Error | null) => {
373
+ const failure = error ?? streamError;
374
+ if (failure) reject(failure);
375
+ else resolve();
376
+ });
377
+ }),
378
+ restore,
379
+ };
380
+ }
381
+
382
+ export type RunMcpServerOptions = DispatcherOptions & {
383
+ readonly input: AsyncIterable<Uint8Array>;
384
+ /** Receives each complete frame line, including its trailing newline. */
385
+ readonly writeFrame: (line: string) => Promise<void>;
386
+ readonly stderr: (text: string) => void;
387
+ readonly maxFrameBytes?: number;
388
+ };
389
+
390
+ /**
391
+ * Serves frames from `input` in order until it ends. Returns 0 after the last response
392
+ * is flushed, or 1 with one stderr line when reading or writing fails. The catalog is
393
+ * closed either way.
394
+ */
395
+ export async function runMcpServer(options: RunMcpServerOptions): Promise<0 | 1> {
396
+ const dispatcher = createDispatcher(options);
397
+ const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
398
+ let code: 0 | 1 = 0;
399
+ try {
400
+ await readFrames(options.input, async (event) => {
401
+ const response = await dispatcher.handle(frameFromLine(event, maxFrameBytes));
402
+ if (response !== null) await options.writeFrame(encodeResponse(response));
403
+ }, { maxFrameBytes });
404
+ } catch (error: unknown) {
405
+ options.stderr(`error: MCP transport failed: ${errorMessage(error).replaceAll("\n", " ")}\n`);
406
+ code = 1;
407
+ }
408
+ try {
409
+ await options.catalog.close();
410
+ } catch (error: unknown) {
411
+ options.stderr(`error: MCP session close failed: ${errorMessage(error).replaceAll("\n", " ")}\n`);
412
+ code = 1;
413
+ }
414
+ return code;
415
+ }