@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
@@ -1,5 +1,18 @@
1
1
  import { terminalIntro } from "./cli-intro.js";
2
- import { open } from "node:fs/promises";
2
+ import {
3
+ advancedHelp,
4
+ closestMatch,
5
+ commandHelpText,
6
+ commandWordCount,
7
+ knownCommandWords,
8
+ optionNames,
9
+ resolveCommandId,
10
+ rootHelp,
11
+ startHelp,
12
+ valueOptions,
13
+ } from "./cli-help.js";
14
+ import { detectAudience, renderFailure, sentence, symbol, terminalOutput, terminalStyle, type TerminalEnvironment } from "./cli-style.js";
15
+ import { open, realpath, stat } from "node:fs/promises";
3
16
  import { cpus, release, totalmem } from "node:os";
4
17
  import { relative, resolve } from "node:path";
5
18
  import { format } from "node:util";
@@ -104,6 +117,10 @@ import {
104
117
  type PublishSelectionInput,
105
118
  } from "./publish.js";
106
119
  import { serveSite } from "./serve.js";
120
+ import { resolveVault } from "./authoring-platform.js";
121
+ import { guardStdout, runMcpServer, serverVersion } from "./mcp-server.js";
122
+ import { importSupermemory, MAX_IMPORT_FILES, normalizePrefix, type ImportReport } from "./import-supermemory.js";
123
+ import { createToolCatalog, serverInstructions, type ContextBuilder } from "./mcp-tools.js";
107
124
  import {
108
125
  MAX_AUTHORIZED_VAULTS,
109
126
  loadPortfolioRegistry,
@@ -142,6 +159,7 @@ import {
142
159
  qmdIndexerVersion,
143
160
  recommendedEmbeddingModel,
144
161
  recommendedEmbeddingModelSha256,
162
+ semanticDatabasePath,
145
163
  sha256EmbeddingModelFile,
146
164
  type SemanticIndexResult,
147
165
  } from "./semantic.js";
@@ -180,10 +198,46 @@ type Output = {
180
198
  readonly stderr: (value: string) => void;
181
199
  };
182
200
 
183
- const defaultOutput: Output = {
201
+ const defaultOutput: Output = terminalOutput({
184
202
  stdout: (value) => process.stdout.write(value),
185
203
  stderr: (value) => process.stderr.write(value),
186
- };
204
+ });
205
+
206
+ /** Read standard input as fatal UTF-8, stopping as soon as it passes `maximumBytes`. */
207
+ async function readBoundedStdinUtf8(
208
+ source: StdinSource,
209
+ maximumBytes: number,
210
+ label: string,
211
+ ): Promise<string> {
212
+ if (source.isTTY) {
213
+ throw new Error(
214
+ "--body-file - reads the note body from standard input, but standard input is a terminal; pipe the body in or name a file",
215
+ );
216
+ }
217
+ const chunks: Uint8Array[] = [];
218
+ let total = 0;
219
+ for await (const chunk of source.chunks) {
220
+ total += chunk.byteLength;
221
+ if (total > maximumBytes) {
222
+ throw new Error(`${label} exceeds the ${maximumBytes}-byte limit`);
223
+ }
224
+ chunks.push(chunk);
225
+ }
226
+ const bytes = new Uint8Array(total);
227
+ let offset = 0;
228
+ for (const chunk of chunks) {
229
+ bytes.set(chunk, offset);
230
+ offset += chunk.byteLength;
231
+ }
232
+ let text: string;
233
+ try {
234
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
235
+ } catch (error) {
236
+ throw new Error(`${label} is not valid UTF-8`, { cause: error });
237
+ }
238
+ if (text.trim() === "") throw new Error(`${label} from standard input is empty`);
239
+ return text;
240
+ }
187
241
 
188
242
  async function readBoundedUtf8(
189
243
  path: string,
@@ -232,73 +286,15 @@ async function loadSearchRulesFile(path: string): Promise<SearchRulesV1> {
232
286
  return parseSearchRules(input);
233
287
  }
234
288
 
235
- export const usage = `wordcell — a local Markdown knowledge base with superpowers
236
-
237
- Start here (no account or model needed):
238
- wordcell init kb
239
- wordcell note create notes/decision --title "A decision" --body "Keep retries bounded." --root kb
240
- wordcell search "retries" --root kb --mode exact
241
-
242
- Already have Markdown? Search it with --root <your-notes-directory>.
243
- Use --mode exact for model-free search; optional hybrid search needs a local index.
244
- Quick start and examples: https://wordcell.io/docs
245
-
246
- Usage:
247
- wordcell init [directory] [--json]
248
- wordcell clip <url|current> [capture options]
249
- wordcell capture show <bundle> [--verify-assets] [--include-source-html] [--json]
250
- wordcell capture verify <bundle> [--verify-assets] [--json]
251
- wordcell capture diff <bundle> [--repo <repository>] [--ref <ref>] [--json]
252
- wordcell url-metadata tool <build|check>
253
- wordcell url-metadata backfill [metadata options]
254
- wordcell inspect <url> [capture options]
255
- wordcell pdf <file-or-url> [PDF options]
256
- wordcell refresh [--root <directory>] [--index <path>] [--json]
257
- wordcell check [--root <directory>] [--index <path>] [--no-catalog] [--repo <repository>] [--json]
258
- wordcell catalog [--root <directory>] [--index <path>] [--json]
259
- wordcell graph [--root <directory>] [--index <path>] [--json]
260
- wordcell graph rebuild [--fresh] [--root <directory>] [--index <path>] [--json]
261
- wordcell graph verify [--root <directory>] [--index <path>] [--json]
262
- wordcell graph query --program <backlinks|reachability|scope-route|relation-closure|shared-tags|shared-concepts> [--note <id> | --scope <path>] [--predicate <predicate>] [--depth <count>] [--limit <count>] [--persisted] [--root <directory>] [--index <path>] [--json]
263
- wordcell backlinks <note> [--root <directory>] [--index <path>] [--json]
264
- wordcell links <note> [--root <directory>] [--direction <in|out|both>] [--depth <count>] [--limit <count>] [--json]
265
- wordcell note create <id> --title <title> [--type <type>] [--tag <tag>] [--body <markdown> | --body-file <path>] [--root <directory>] [--json]
266
- wordcell relation add <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
267
- wordcell relation remove <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
268
- wordcell relation list <note> [--root <directory>] [--json]
269
- wordcell percolate [note] [--proofs] [--root <directory>] [--min-support <count>] [--limit <count>] [--json]
270
- wordcell list [--root <directory>] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--sort <field>] [--order <asc|desc>] [--limit <count>] [--json]
271
- wordcell index [--root <directory>] [--database <path>] [--force] [--json]
272
- wordcell search <query> [--root <directory>] [--repo <repository>] [--database <path>] [--mode <hybrid|exact|keyword|semantic>] [--rules <file>] [--priority] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--related <note>] [--graph-depth <1|2>] [--no-graph] [--history | --no-history | --require-history] [--limit <count>] [--candidate-limit <count>] [--min-score <score>] [--rerank <typesafe>] [--rerank-limit <2..25>] [--json]
273
- wordcell history <note> [--root <directory>] [--repo <repository>] [--limit <count>] [--cochanged-limit <count>] [--json]
274
- wordcell history search <query-or-path> [--root <directory>] [--repo <repository>] [--limit <count>] [--commit-limit <count>] [--cochanged-limit <count>] [--json]
275
- wordcell evaluate <manifest.json> [--root <directory>] [--repo <repository>] [--database <path>] [--retriever <id>] [--split <development|test|all>] [--limit <count>] [--cutoff <count>] [--timeout <milliseconds>] [--baseline <id>] [--model-file <path>] [--cache-state <cold|mixed|warm>] [--json]
276
- wordcell portfolio search <query> --registry <file> --workspace <directory> (--shared | --vault <owner/id>...) [--mode <hybrid|exact|keyword|semantic>] [--rules <file>] [--priority] [--limit <count>] [--require-all] [--json]
277
- wordcell portfolio audit --registry <file> --workspace <directory> (--all | --shared | --vault <owner/id>...) [--strict] [--json]
278
- wordcell publish --out <directory> [--root <directory>] [--index <path>] [--include <path>]... [--exclude <path>]... [--include-glob <pattern>]... [--exclude-glob <pattern>]... [--where <path=value>]... [--has <path>]... [--tag <tag>]... [--scope <repository-path>]... [--from <note> [--depth <count>] [--direction <in|out|both>]] [--title <title>] [--description <text>] [--base-path <path>] [--base-url <url>] [--noindex] [--no-index-content] [--deterministic] [--dry-run] [--list-limit <0-1000>] [--force] [--json]
279
- wordcell serve --root <directory> [--host <host>] [--port <port>] [--json]
280
- wordcell inbox [--root <directory>] [--source-prefix <directory>] [--limit <count>] [--json]
281
- wordcell context <repository-path> [--root <vault>] [--repo <repository>] [--kind <auto|file|directory>] [--json]
282
- wordcell agents identity <repository-scope> [--json]
283
- wordcell agents check [--root <vault>] [--repo <repository>] [--json]
284
- wordcell agents audit [--root <vault>] [--repo <repository>] [--json]
285
- wordcell doctor [--json]
286
- wordcell adapters [--json]
287
- wordcell support [--json | protocol --json | offer --json | shown <id> | release <id> | dismiss | snooze | enable | status --json]
288
-
289
- Optional support uses separate local preferences. HRANESS_SUPPORT=off or
290
- HRANESS_SUPPORT_AUDIENCE=off keeps invitations quiet; explicit support remains available.
291
- Agents receive discovery on stderr after useful standalone work. Human mode requires
292
- HRANESS_SUPPORT_AUDIENCE=human and an interactive stderr. No command signs up or pays.
293
-
294
- Run \`wordcell clip --help\` for web capture options or \`wordcell pdf --help\` for PDF conversion options.
295
- `;
289
+ /** Grouped root help printed by `wordcell --help`. */
290
+ export const usage = rootHelp();
296
291
 
297
292
  type VaultCommand = "refresh" | "check" | "graph" | "backlinks" | "links";
298
293
 
299
294
  type ParsedCommand =
300
295
  | GraphCliCommand
301
- | { readonly kind: "help" }
296
+ | { readonly kind: "help"; readonly topic?: string }
297
+ | { readonly kind: "version"; readonly json: boolean }
302
298
  | { readonly kind: "clip"; readonly arguments: readonly string[] }
303
299
  | {
304
300
  readonly kind: "capture-bundle";
@@ -460,6 +456,15 @@ type ParsedCommand =
460
456
  readonly bodyFile?: string;
461
457
  readonly json: boolean;
462
458
  }
459
+ | {
460
+ readonly kind: "import-supermemory";
461
+ readonly root: string;
462
+ /** Export file paths as given, read relative to the working directory. */
463
+ readonly files: readonly string[];
464
+ readonly prefix?: string;
465
+ readonly dryRun: boolean;
466
+ readonly json: boolean;
467
+ }
463
468
  | {
464
469
  readonly kind: "relation";
465
470
  readonly action: "add" | "remove" | "list";
@@ -503,13 +508,19 @@ type ParsedCommand =
503
508
  readonly host: string;
504
509
  readonly port: number;
505
510
  readonly json: boolean;
511
+ }
512
+ | {
513
+ readonly kind: "mcp";
514
+ readonly root: string;
515
+ readonly repository?: string;
516
+ readonly readOnly: boolean;
506
517
  };
507
518
 
508
519
  type ParseResult =
509
520
  | { readonly ok: true; readonly value: ParsedCommand }
510
521
  | { readonly ok: false; readonly message: string };
511
522
 
512
- type CliDependencies = GraphCliDependencies & {
523
+ export type CliDependencies = GraphCliDependencies & {
513
524
  readonly runClipCommand?: typeof runClipCommand;
514
525
  readonly readCaptureBundle?: typeof readCaptureBundle;
515
526
  readonly verifyCaptureBundle?: typeof verifyCaptureBundle;
@@ -524,6 +535,8 @@ type CliDependencies = GraphCliDependencies & {
524
535
  readonly refreshVault?: typeof refreshVault;
525
536
  readonly indexSemanticVault?: typeof indexSemanticVault;
526
537
  readonly openKnowledgeBase?: typeof openKnowledgeBase;
538
+ /** Whether a local search index exists for a vault root; injected in tests. */
539
+ readonly semanticIndexExists?: (root: string) => Promise<boolean>;
527
540
  readonly openKnowledgeBaseEvaluation?: typeof openKnowledgeBaseEvaluation;
528
541
  /** Test seam; production wiring builds the TypeSafe reranker lazily per flag. */
529
542
  readonly rerankers?: readonly SearchReranker[];
@@ -541,6 +554,31 @@ type CliDependencies = GraphCliDependencies & {
541
554
  readonly validateMarkdownAttachments?: typeof validateMarkdownAttachments;
542
555
  readonly publishVault?: typeof publishVault;
543
556
  readonly serveSite?: typeof serveSite;
557
+ /** Test seam: serve MCP frames over these streams instead of guarding the process stdout. */
558
+ readonly mcp?: McpStreams;
559
+ /** Test seam: the standard input that `note create --body-file -` reads. */
560
+ readonly stdin?: StdinSource;
561
+ readonly importSupermemory?: typeof importSupermemory;
562
+ /** Test seam: the clock for the provenance line of `import supermemory`. */
563
+ readonly importNow?: () => Date;
564
+ /** Test seam: terminal facts for symbols, color, and the help intro. */
565
+ readonly terminal?: CliTerminal;
566
+ /** Test seam: the version `--version` and bare invocation print. */
567
+ readonly packageVersion?: () => Promise<string>;
568
+ };
569
+
570
+ export type StdinSource = {
571
+ readonly isTTY: boolean;
572
+ readonly chunks: AsyncIterable<Uint8Array>;
573
+ };
574
+
575
+ function processStdin(): StdinSource {
576
+ return { isTTY: process.stdin.isTTY === true, chunks: process.stdin };
577
+ }
578
+
579
+ type McpStreams = {
580
+ readonly input: AsyncIterable<Uint8Array>;
581
+ readonly writeFrame: (line: string) => Promise<void>;
544
582
  };
545
583
 
546
584
  function safe(value: string): string {
@@ -1126,6 +1164,38 @@ function parseServeCommand(arguments_: readonly string[]): ParseResult {
1126
1164
  return { ok: true, value: { kind: "serve", root, host, port, json } };
1127
1165
  }
1128
1166
 
1167
+ /** No `--json`: stdout carries only protocol frames. */
1168
+ function parseMcpCommand(arguments_: readonly string[]): ParseResult {
1169
+ let root: string | undefined;
1170
+ let repository: string | undefined;
1171
+ let readOnly = false;
1172
+ for (let cursor = 0; cursor < arguments_.length; cursor += 1) {
1173
+ const argument = arguments_[cursor];
1174
+ if (argument === undefined) continue;
1175
+ if (argument === "--read-only") { readOnly = true; continue; }
1176
+ if (argument === "--root" || argument === "--repo") {
1177
+ const value = readValue(arguments_, cursor);
1178
+ // An empty directory would resolve to the working directory of whatever launched the server.
1179
+ if (value === null || value === "") return { ok: false, message: `${argument} requires a value` };
1180
+ if (argument === "--root") root = value;
1181
+ else repository = value;
1182
+ cursor += 1;
1183
+ continue;
1184
+ }
1185
+ return {
1186
+ ok: false,
1187
+ message: argument.startsWith("--")
1188
+ ? "unknown mcp option"
1189
+ : "mcp does not accept positional arguments",
1190
+ };
1191
+ }
1192
+ if (root === undefined) return { ok: false, message: "mcp requires --root <vault>" };
1193
+ return {
1194
+ ok: true,
1195
+ value: { kind: "mcp", root, ...(repository === undefined ? {} : { repository }), readOnly },
1196
+ };
1197
+ }
1198
+
1129
1199
  function parseInboxCommand(arguments_: readonly string[]): ParseResult {
1130
1200
  let root = ".";
1131
1201
  let index: string | undefined;
@@ -1893,6 +1963,53 @@ function parseNoteCommand(arguments_: readonly string[]): ParseResult {
1893
1963
  };
1894
1964
  }
1895
1965
 
1966
+ function parseImportCommand(arguments_: readonly string[]): ParseResult {
1967
+ if (arguments_[0] !== "supermemory") return { ok: false, message: "import requires supermemory" };
1968
+ let root = ".";
1969
+ let prefix: string | undefined;
1970
+ let dryRun = false;
1971
+ let json = false;
1972
+ const files: string[] = [];
1973
+ for (let cursor = 1; cursor < arguments_.length; cursor += 1) {
1974
+ const argument = arguments_[cursor];
1975
+ if (argument === undefined) continue;
1976
+ if (argument === "--json") { json = true; continue; }
1977
+ if (argument === "--dry-run") { dryRun = true; continue; }
1978
+ if (argument === "--root" || argument === "--prefix") {
1979
+ const value = readValue(arguments_, cursor);
1980
+ if (value === null || value === "") return { ok: false, message: `${argument} requires a value` };
1981
+ if (argument === "--root") root = value;
1982
+ else prefix = value;
1983
+ cursor += 1;
1984
+ continue;
1985
+ }
1986
+ if (argument.startsWith("--")) return { ok: false, message: "unknown import supermemory option" };
1987
+ files.push(argument);
1988
+ }
1989
+ if (files.length === 0) return { ok: false, message: "import supermemory requires at least one export file" };
1990
+ if (files.length > MAX_IMPORT_FILES) {
1991
+ return { ok: false, message: `import supermemory accepts at most ${MAX_IMPORT_FILES} export files` };
1992
+ }
1993
+ if (prefix !== undefined) {
1994
+ try {
1995
+ prefix = normalizePrefix(prefix);
1996
+ } catch (error) {
1997
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
1998
+ }
1999
+ }
2000
+ return {
2001
+ ok: true,
2002
+ value: {
2003
+ kind: "import-supermemory",
2004
+ root,
2005
+ files,
2006
+ ...(prefix === undefined ? {} : { prefix }),
2007
+ dryRun,
2008
+ json,
2009
+ },
2010
+ };
2011
+ }
2012
+
1896
2013
  function isNoteRevision(value: string): value is `sha256:${string}` {
1897
2014
  return /^sha256:[0-9a-f]{64}$/u.test(value);
1898
2015
  }
@@ -2156,11 +2273,72 @@ function parsePortfolioCommand(arguments_: readonly string[]): ParseResult {
2156
2273
  };
2157
2274
  }
2158
2275
 
2276
+ function isHelpFlag(argument: string | undefined): boolean {
2277
+ return argument === "--help" || argument === "-h";
2278
+ }
2279
+
2280
+ /** True for the help request a delegated parser answers itself. */
2281
+ function isDelegatedHelp(command: ParsedCommand): boolean {
2282
+ if (command.kind === "clip") return command.arguments.length === 1 && command.arguments[0] === "help";
2283
+ if (command.kind === "pdf" || command.kind === "url-metadata") {
2284
+ return command.arguments.length === 1 && command.arguments[0] === "--help";
2285
+ }
2286
+ return false;
2287
+ }
2288
+
2289
+ /** Help for delegated commands comes from their own parsers. */
2290
+ function delegatedHelp(first: string): ParsedCommand | undefined {
2291
+ if (first === "clip" || first === "inspect") return { kind: "clip", arguments: ["help"] };
2292
+ if (first === "pdf") return { kind: "pdf", arguments: ["--help"] };
2293
+ if (first === "url-metadata") return { kind: "url-metadata", arguments: ["--help"] };
2294
+ return undefined;
2295
+ }
2296
+
2297
+ /** `help`, `help advanced`, and `help <command...>`. */
2298
+ function parseHelpTopic(words: readonly string[]): ParseResult {
2299
+ const topic = words.filter((word) => !isHelpFlag(word) && word !== "--json");
2300
+ if (topic.length === 0) return { ok: true, value: { kind: "help" } };
2301
+ if (topic.length === 1 && topic[0] === "advanced") return { ok: true, value: { kind: "help", topic: "advanced" } };
2302
+ const delegated = delegatedHelp(topic[0] ?? "");
2303
+ if (delegated !== undefined) return { ok: true, value: delegated };
2304
+ const id = resolveCommandId(topic);
2305
+ if (id === undefined) return { ok: false, message: "unknown help topic" };
2306
+ return { ok: true, value: { kind: "help", topic: id } };
2307
+ }
2308
+
2309
+ /**
2310
+ * `<command> --help` anywhere a person would type it: right after the command
2311
+ * words, or as the last argument when it is not the value of an option.
2312
+ */
2313
+ function embeddedHelp(arguments_: readonly string[]): ParseResult | undefined {
2314
+ const separator = arguments_.indexOf("--");
2315
+ const end = separator === -1 ? arguments_.length : separator;
2316
+ const index = arguments_.slice(0, end).findIndex(isHelpFlag);
2317
+ if (index <= 0) return undefined;
2318
+ const delegated = delegatedHelp(arguments_[0] ?? "");
2319
+ if (delegated !== undefined) {
2320
+ return index === 1 || index === end - 1 ? { ok: true, value: delegated } : undefined;
2321
+ }
2322
+ const id = resolveCommandId(arguments_.slice(0, index).filter((word) => !word.startsWith("-")));
2323
+ if (id === undefined) return undefined;
2324
+ const words = commandWordCount(id);
2325
+ const previous = arguments_[index - 1] ?? "";
2326
+ // `--help` is never an option value; `-h` could be one, as in `--body -h`.
2327
+ const flag = arguments_[index];
2328
+ const lastArgument = index === end - 1 && (flag === "--help" || !valueOptions(id).has(previous));
2329
+ if (index !== words && !lastArgument) return undefined;
2330
+ return { ok: true, value: { kind: "help", topic: id } };
2331
+ }
2332
+
2159
2333
  export function parseArguments(arguments_: readonly string[]): ParseResult {
2160
2334
  const command = arguments_[0];
2161
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
2162
- return { ok: true, value: { kind: "help" } };
2335
+ if (command === undefined) return { ok: true, value: { kind: "help", topic: "start" } };
2336
+ if (command === "help" || isHelpFlag(command)) return parseHelpTopic(arguments_.slice(1));
2337
+ if (command === "--version" || command === "-V" || command === "-v" || command === "version") {
2338
+ return { ok: true, value: { kind: "version", json: arguments_.includes("--json") } };
2163
2339
  }
2340
+ const help = embeddedHelp(arguments_);
2341
+ if (help !== undefined) return help;
2164
2342
  if (command === "capture" && arguments_[1] === "diff") {
2165
2343
  return parseCaptureDiffCommand(arguments_.slice(2));
2166
2344
  }
@@ -2216,10 +2394,12 @@ export function parseArguments(arguments_: readonly string[]): ParseResult {
2216
2394
  if (command === "context") return parseContextCommand(arguments_.slice(1));
2217
2395
  if (command === "agents") return parseAgentsCommand(arguments_.slice(1));
2218
2396
  if (command === "note") return parseNoteCommand(arguments_.slice(1));
2397
+ if (command === "import") return parseImportCommand(arguments_.slice(1));
2219
2398
  if (command === "relation") return parseRelationCommand(arguments_.slice(1));
2220
2399
  if (command === "percolate") return parsePercolateCommand(arguments_.slice(1));
2221
2400
  if (command === "publish") return parsePublishCommand(arguments_.slice(1));
2222
2401
  if (command === "serve") return parseServeCommand(arguments_.slice(1));
2402
+ if (command === "mcp") return parseMcpCommand(arguments_.slice(1));
2223
2403
  return { ok: false, message: "unknown command" };
2224
2404
  }
2225
2405
 
@@ -2287,7 +2467,14 @@ async function runSemantic(
2287
2467
  output: Output,
2288
2468
  dependencies: CliDependencies,
2289
2469
  ): Promise<number> {
2470
+ const terminal = dependencies.terminal ?? processTerminal();
2471
+ const person = !command.json && speaksToPerson(terminal);
2290
2472
  if (command.kind === "index") {
2473
+ if (person) {
2474
+ const style = terminalStyle(terminal.env, terminal.stderrIsTTY);
2475
+ output.stderr(`${symbol("progress", style)} Indexing ${safe(command.root)}. `
2476
+ + "The first run downloads a search model of about 300 MB and can take a few minutes.\n");
2477
+ }
2291
2478
  const result = await (dependencies.indexSemanticVault ?? indexSemanticVault)({
2292
2479
  root: command.root,
2293
2480
  ...(command.database === undefined ? {} : { database: command.database }),
@@ -2296,6 +2483,17 @@ async function runSemantic(
2296
2483
  output.stdout(command.json ? terminalSafeJson(result) : sanitizeTerminalText(renderSemanticIndex(result)));
2297
2484
  return 0;
2298
2485
  }
2486
+ // With no mode and no index, meaning-based search has nothing to rank, so
2487
+ // search the words instead and say how to turn on the rest. This applies to
2488
+ // the real store only; an injected knowledge base keeps its default.
2489
+ const indexExists = dependencies.semanticIndexExists
2490
+ ?? (dependencies.openKnowledgeBase === undefined ? defaultSemanticIndexExists : undefined);
2491
+ // Score thresholds and reranking only apply to ranked modes, so a search
2492
+ // that asks for them keeps the default mode and its own error.
2493
+ const withoutIndex = command.mode === undefined && command.database === undefined
2494
+ && command.minScore === undefined && command.rerank === undefined
2495
+ && indexExists !== undefined && !await indexExists(command.root);
2496
+ const mode = withoutIndex ? "exact" as const : command.mode;
2299
2497
  const searchRules = command.rulesPath === undefined
2300
2498
  ? undefined
2301
2499
  : await loadSearchRulesFile(command.rulesPath);
@@ -2313,7 +2511,7 @@ async function runSemantic(
2313
2511
  try {
2314
2512
  const result = await kb.search({
2315
2513
  query: command.query,
2316
- ...(command.mode === undefined ? {} : { mode: command.mode }),
2514
+ ...(mode === undefined ? {} : { mode }),
2317
2515
  ordering: command.ordering,
2318
2516
  filters: command.filters,
2319
2517
  tags: command.tags,
@@ -2335,6 +2533,11 @@ async function runSemantic(
2335
2533
  output.stdout(command.json
2336
2534
  ? terminalSafeJson(result)
2337
2535
  : sanitizeTerminalText(renderKnowledgeBaseSearch(result)));
2536
+ if (withoutIndex && person) {
2537
+ output.stderr("Searched without an index, so only exact words matched. "
2538
+ + `For meaning-based search, run wordcell index --root ${safe(shellWord(command.root))} `
2539
+ + "(downloads about 300 MB once).\n");
2540
+ }
2338
2541
  return 0;
2339
2542
  } finally {
2340
2543
  await kb.close();
@@ -2967,7 +3170,9 @@ async function runNoteCreate(
2967
3170
  const body = command.body ?? (
2968
3171
  command.bodyFile === undefined
2969
3172
  ? undefined
2970
- : await readBoundedUtf8(command.bodyFile, 16 * 1024 * 1024, "note body")
3173
+ : command.bodyFile === "-"
3174
+ ? await readBoundedStdinUtf8(dependencies.stdin ?? processStdin(), 16 * 1024 * 1024, "note body")
3175
+ : await readBoundedUtf8(command.bodyFile, 16 * 1024 * 1024, "note body")
2971
3176
  );
2972
3177
  const result = await (dependencies.createNote ?? createNote)(
2973
3178
  command.root,
@@ -2982,6 +3187,67 @@ async function runNoteCreate(
2982
3187
  return 0;
2983
3188
  }
2984
3189
 
3190
+ function countOf(count: number, noun: string): string {
3191
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
3192
+ }
3193
+
3194
+ function shellWord(value: string): string {
3195
+ return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
3196
+ }
3197
+
3198
+ function renderImportReport(report: ImportReport): string {
3199
+ const { counts } = report;
3200
+ const tally = `created ${counts.created}, updated ${counts.updated}, skipped ${counts.skipped}, `
3201
+ + `conflicts ${counts.conflicts}, rejected ${counts.rejected}`;
3202
+ const scope = `${countOf(report.items.length, "item")} from ${countOf(report.files, "file")}`;
3203
+ const lines = [report.dryRun
3204
+ ? `Dry run of ${scope}: ${tally}. Nothing was written.`
3205
+ : `Imported ${scope}: ${tally}.`];
3206
+ const location = (item: ImportReport["items"][number]) => item.note === undefined
3207
+ ? `${item.file}#${item.pointer}`
3208
+ : `${item.note} from ${item.file}#${item.pointer}`;
3209
+ for (const item of report.items) {
3210
+ if (item.outcome !== "conflict" && item.outcome !== "rejected") continue;
3211
+ const external = item.externalId === undefined ? "" : ` (${item.externalId})`;
3212
+ lines.push(`${item.outcome}: ${safe(location(item))}${safe(external)}: ${safe(item.reason ?? "no reason given")}`);
3213
+ }
3214
+ const added = report.relations.filter((relation) => relation.outcome === "added").length;
3215
+ if (added > 0) {
3216
+ lines.push(`${report.dryRun ? "Would add" : "Added"} ${countOf(added, "supersedes relation")}.`);
3217
+ }
3218
+ for (const relation of report.relations) {
3219
+ if (relation.outcome !== "failed" && relation.outcome !== "omitted") continue;
3220
+ lines.push(`relation ${relation.outcome}: ${safe(relation.source)} supersedes ${safe(relation.target)}: ${safe(relation.reason ?? "no reason given")}`);
3221
+ }
3222
+ for (const diagnostic of report.diagnostics) lines.push(`diagnostic: ${safe(diagnostic)}`);
3223
+ for (const item of report.items) {
3224
+ for (const diagnostic of item.diagnostics ?? []) {
3225
+ lines.push(`diagnostic: ${safe(item.note ?? `${item.file}#${item.pointer}`)}: ${safe(diagnostic)}`);
3226
+ }
3227
+ }
3228
+ if (!report.dryRun && counts.created + counts.updated + added > 0) {
3229
+ const root = safe(shellWord(report.root));
3230
+ lines.push(`Next: wordcell refresh --root ${root}, then wordcell check --root ${root}.`);
3231
+ }
3232
+ return `${lines.join("\n")}\n`;
3233
+ }
3234
+
3235
+ async function runImportSupermemory(
3236
+ command: Extract<ParsedCommand, { readonly kind: "import-supermemory" }>,
3237
+ output: Output,
3238
+ dependencies: CliDependencies,
3239
+ ): Promise<number> {
3240
+ const report = await (dependencies.importSupermemory ?? importSupermemory)({
3241
+ root: command.root,
3242
+ files: command.files,
3243
+ ...(command.prefix === undefined ? {} : { prefix: command.prefix }),
3244
+ dryRun: command.dryRun,
3245
+ ...(dependencies.importNow === undefined ? {} : { now: dependencies.importNow() }),
3246
+ });
3247
+ output.stdout(command.json ? terminalSafeJson(report) : sanitizeTerminalText(renderImportReport(report)));
3248
+ return report.ok ? 0 : 1;
3249
+ }
3250
+
2985
3251
  async function runRelation(
2986
3252
  command: Extract<ParsedCommand, { readonly kind: "relation" }>,
2987
3253
  output: Output,
@@ -3248,6 +3514,73 @@ async function runServe(
3248
3514
  return 0;
3249
3515
  }
3250
3516
 
3517
+ /** The `context` tool's payload, identical to `wordcell context --json`. */
3518
+ function mcpContextBuilder(dependencies: CliDependencies): ContextBuilder {
3519
+ return async (snapshot, request) => {
3520
+ const inspection = await (
3521
+ dependencies.inspectAgentContextRepository ?? inspectAgentContextRepository
3522
+ )(snapshot.notes, request);
3523
+ const memory = await (
3524
+ dependencies.buildRepositoryMemoryContext ?? buildRepositoryMemoryContext
3525
+ )(snapshot.notes, { repositoryRoot: request.repositoryRoot, target: inspection.target });
3526
+ return contextPayload(inspection, snapshot, memory);
3527
+ };
3528
+ }
3529
+
3530
+ async function resolveMcpDirectories(
3531
+ command: Extract<ParsedCommand, { readonly kind: "mcp" }>,
3532
+ ): Promise<{ readonly root: string; readonly repository?: string }> {
3533
+ const { root } = await resolveVault(command.root);
3534
+ if (command.repository === undefined) return { root };
3535
+ const repository = await realpath(resolve(command.repository));
3536
+ if (!(await stat(repository)).isDirectory()) throw new Error(`--repo is not a directory: ${repository}`);
3537
+ return { root, repository };
3538
+ }
3539
+
3540
+ async function runMcp(
3541
+ command: Extract<ParsedCommand, { readonly kind: "mcp" }>,
3542
+ output: Output,
3543
+ dependencies: CliDependencies,
3544
+ ): Promise<number> {
3545
+ let directories: Awaited<ReturnType<typeof resolveMcpDirectories>>;
3546
+ try {
3547
+ directories = await resolveMcpDirectories(command);
3548
+ } catch (error) {
3549
+ const message = error instanceof Error ? error.message : String(error);
3550
+ const terminal = dependencies.terminal ?? processTerminal();
3551
+ output.stderr(renderFailure(safe(message), "wordcell mcp --help", terminalStyle(terminal.env, terminal.stderrIsTTY)));
3552
+ return 2;
3553
+ }
3554
+ const version = await serverVersion();
3555
+ const catalog = createToolCatalog({
3556
+ ...directories,
3557
+ readOnly: command.readOnly,
3558
+ context: mcpContextBuilder(dependencies),
3559
+ warn: (message) => output.stderr(`${safe(message)}\n`),
3560
+ });
3561
+ const instructions = serverInstructions({ root: directories.root, readOnly: command.readOnly });
3562
+ const streams = mcpStreams(dependencies);
3563
+ try {
3564
+ return await runMcpServer({
3565
+ catalog,
3566
+ instructions,
3567
+ version,
3568
+ input: streams.input,
3569
+ writeFrame: streams.writeFrame,
3570
+ stderr: (text) => output.stderr(text),
3571
+ });
3572
+ } finally {
3573
+ streams.restore();
3574
+ }
3575
+ }
3576
+
3577
+ /** Process stdio with stdout held for frames, unless a test supplies its own streams. */
3578
+ function mcpStreams(dependencies: CliDependencies): McpStreams & { readonly restore: () => void } {
3579
+ if (dependencies.mcp !== undefined) return { ...dependencies.mcp, restore: () => undefined };
3580
+ const guard = guardStdout();
3581
+ return { input: process.stdin, writeFrame: guard.writeFrame, restore: guard.restore };
3582
+ }
3583
+
3251
3584
  async function runCatalog(
3252
3585
  command: Extract<ParsedCommand, { readonly kind: "catalog" }>,
3253
3586
  output: Output,
@@ -3288,20 +3621,37 @@ async function runInbox(
3288
3621
  return 0;
3289
3622
  }
3290
3623
 
3624
+ function speaksToPerson(terminal: CliTerminal): boolean {
3625
+ return detectAudience(terminal.env, terminal.stderrIsTTY) === "human";
3626
+ }
3627
+
3291
3628
  async function runInit(
3292
3629
  command: Extract<ParsedCommand, { readonly kind: "init" }>,
3293
3630
  output: Output,
3294
3631
  initialize: typeof initVault,
3632
+ terminal: CliTerminal,
3295
3633
  ): Promise<number> {
3296
3634
  const result: InitVaultResult = await initialize(command.directory);
3297
3635
  if (command.json) output.stdout(terminalSafeJson(result));
3298
3636
  else {
3299
3637
  const relativeRoot = relative(process.cwd(), result.root) || ".";
3300
3638
  output.stdout(`Initialized ${safe(relativeRoot)} with ${result.files.length} files.\n`);
3639
+ if (speaksToPerson(terminal)) {
3640
+ output.stderr(`Next: wordcell search "your question" --root ${safe(shellWord(relativeRoot))}\n`);
3641
+ }
3301
3642
  }
3302
3643
  return 0;
3303
3644
  }
3304
3645
 
3646
+ async function defaultSemanticIndexExists(root: string): Promise<boolean> {
3647
+ try {
3648
+ await stat(semanticDatabasePath(await realpath(root)));
3649
+ return true;
3650
+ } catch {
3651
+ return false;
3652
+ }
3653
+ }
3654
+
3305
3655
  async function runCaptureBundle(
3306
3656
  command: Extract<ParsedCommand, { readonly kind: "capture-bundle" }>,
3307
3657
  output: Output,
@@ -3950,31 +4300,166 @@ async function runVault(
3950
4300
  return 0;
3951
4301
  }
3952
4302
 
4303
+ /** `mcp` owns stdout for protocol frames, so it never takes a `--json` path and its failures go to stderr. */
4304
+ function machineJsonRequested(rawArguments: readonly string[]): boolean {
4305
+ return rawArguments[0] !== "mcp" && rawArguments.includes("--json");
4306
+ }
4307
+
4308
+ /** Terminal facts the human renderer needs; injectable for golden tests. */
4309
+ export type CliTerminal = {
4310
+ readonly env: TerminalEnvironment;
4311
+ readonly stdoutIsTTY: boolean;
4312
+ readonly stderrIsTTY: boolean;
4313
+ readonly columns: number | undefined;
4314
+ };
4315
+
4316
+ function processTerminal(): CliTerminal {
4317
+ return {
4318
+ env: process.env,
4319
+ stdoutIsTTY: process.stdout.isTTY === true,
4320
+ stderrIsTTY: process.stderr.isTTY === true,
4321
+ columns: process.stdout.columns,
4322
+ };
4323
+ }
4324
+
4325
+ const SAFE_WORD = /^[A-Za-z][A-Za-z0-9-]{0,39}$/u;
4326
+ const SAFE_OPTION = /^--?[A-Za-z][A-Za-z0-9-]{0,39}$/u;
4327
+
4328
+ /**
4329
+ * The first option the parser rejected, found by parsing growing prefixes of
4330
+ * the arguments. Only the option name is returned, never an `=value` part.
4331
+ */
4332
+ function rejectedOption(rawArguments: readonly string[], message: string): string | undefined {
4333
+ for (let index = 1; index < rawArguments.length; index += 1) {
4334
+ const token = rawArguments[index] ?? "";
4335
+ if (token === "--") return undefined;
4336
+ if (!token.startsWith("-")) continue;
4337
+ const through = parseArguments(rawArguments.slice(0, index + 1));
4338
+ if (through.ok || through.message !== message) continue;
4339
+ const before = parseArguments(rawArguments.slice(0, index));
4340
+ if (!before.ok && before.message === message) return undefined;
4341
+ const name = token.split("=")[0] ?? "";
4342
+ return SAFE_OPTION.test(name) ? name : undefined;
4343
+ }
4344
+ return undefined;
4345
+ }
4346
+
4347
+ type UsageFailure = { readonly text: string; readonly next: string };
4348
+
4349
+ /** One sentence saying what was wrong with the command line, and one next command. */
4350
+ export function describeUsageError(rawArguments: readonly string[], message: string): UsageFailure {
4351
+ const first = rawArguments[0] ?? "";
4352
+ if (message === "unknown command") {
4353
+ const known = knownCommandWords();
4354
+ const suggestion = SAFE_WORD.test(first) ? closestMatch(first, known) : undefined;
4355
+ const text = SAFE_WORD.test(first)
4356
+ ? `Unknown command "${first}".${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`
4357
+ : "Unknown command.";
4358
+ return { text, next: "wordcell --help" };
4359
+ }
4360
+ if (message === "unknown help topic") {
4361
+ const suggestion = SAFE_WORD.test(rawArguments[1] ?? "") ? closestMatch(rawArguments[1] ?? "", knownCommandWords()) : undefined;
4362
+ return {
4363
+ text: `There is no help for that command.${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`,
4364
+ next: "wordcell --help",
4365
+ };
4366
+ }
4367
+ const id = resolveCommandId(rawArguments.filter((word) => !word.startsWith("-")));
4368
+ const next = id === undefined ? "wordcell --help" : `wordcell ${id} --help`;
4369
+ if (/^unknown .*option/u.test(message) && id !== undefined) {
4370
+ const option = rejectedOption(rawArguments, message);
4371
+ if (option !== undefined) {
4372
+ const suggestion = closestMatch(option, optionNames(id));
4373
+ return {
4374
+ text: `Unknown option "${option}" for ${id}.${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`,
4375
+ next,
4376
+ };
4377
+ }
4378
+ }
4379
+ return { text: sentence(safe(message)), next };
4380
+ }
4381
+
4382
+ function runtimeNext(rawArguments: readonly string[]): string {
4383
+ const id = resolveCommandId(rawArguments.filter((word) => !word.startsWith("-")));
4384
+ return id === undefined ? "wordcell --help" : `wordcell ${id} --help`;
4385
+ }
4386
+
4387
+ async function renderHelp(
4388
+ command: Extract<ParsedCommand, { readonly kind: "help" }>,
4389
+ output: Output,
4390
+ terminal: CliTerminal,
4391
+ readVersion: () => Promise<string>,
4392
+ showIntro: boolean,
4393
+ ): Promise<number> {
4394
+ const intro = showIntro
4395
+ ? terminalIntro({ isTTY: terminal.stdoutIsTTY, columns: terminal.columns, term: terminal.env.TERM })
4396
+ : "";
4397
+ if (command.topic === undefined) {
4398
+ output.stdout(intro + sanitizeTerminalText(usage));
4399
+ return 0;
4400
+ }
4401
+ if (command.topic === "start") {
4402
+ const version = await readVersion().catch(() => undefined);
4403
+ output.stdout(intro + sanitizeTerminalText(startHelp(version)));
4404
+ return 0;
4405
+ }
4406
+ if (command.topic === "advanced") {
4407
+ output.stdout(sanitizeTerminalText(advancedHelp()));
4408
+ return 0;
4409
+ }
4410
+ output.stdout(sanitizeTerminalText(commandHelpText(command.topic) ?? usage));
4411
+ return 0;
4412
+ }
4413
+
3953
4414
  /** Stable CLI entry point with injectable filesystem and capture boundaries. */
3954
4415
  export async function main(
3955
4416
  rawArguments: readonly string[] = process.argv.slice(2),
3956
4417
  output: Output = defaultOutput,
3957
4418
  dependencies: CliDependencies = {},
3958
4419
  ): Promise<number> {
3959
- const jsonRequested = rawArguments.includes("--json");
4420
+ const jsonRequested = machineJsonRequested(rawArguments);
4421
+ const terminal = dependencies.terminal ?? processTerminal();
4422
+ const errorStyle = terminalStyle(terminal.env, terminal.stderrIsTTY);
4423
+ const readVersion = dependencies.packageVersion ?? (() => serverVersion());
3960
4424
  const parsed = parseArguments(rawArguments);
3961
4425
  if (!parsed.ok) {
4426
+ const failure = describeUsageError(rawArguments, parsed.message);
3962
4427
  if (jsonRequested) {
3963
4428
  output.stdout(terminalSafeJson({
3964
4429
  ok: false,
3965
- error: { kind: "parse", message: parsed.message },
4430
+ error: { kind: "parse", code: "usage", message: parsed.message, next: failure.next },
3966
4431
  }));
3967
4432
  } else {
3968
- output.stderr(`error: ${safe(parsed.message)}\n\n${sanitizeTerminalText(usage)}`);
4433
+ output.stderr(renderFailure(failure.text, failure.next, errorStyle));
3969
4434
  }
3970
4435
  return 2;
3971
4436
  }
3972
4437
  const command = parsed.value;
3973
- if (command.kind === "help") {
3974
- if (output === defaultOutput && !jsonRequested) output.stdout(terminalIntro({ isTTY: process.stdout.isTTY, columns: process.stdout.columns, term: process.env.TERM }));
3975
- output.stdout(sanitizeTerminalText(usage));
4438
+ if (command.kind === "version") {
4439
+ const version = await readVersion();
4440
+ output.stdout(command.json || jsonRequested
4441
+ ? terminalSafeJson({ name: "wordcell", version })
4442
+ : `wordcell ${version}\n`);
3976
4443
  return 0;
3977
4444
  }
4445
+ if (command.kind === "help") {
4446
+ if (jsonRequested) {
4447
+ const text: string[] = [];
4448
+ await renderHelp(command, { stdout: (value) => text.push(value), stderr: output.stderr }, terminal, readVersion, false);
4449
+ output.stdout(terminalSafeJson({ kind: "help", text: text.join("") }));
4450
+ return 0;
4451
+ }
4452
+ return renderHelp(command, output, terminal, readVersion, output === defaultOutput);
4453
+ }
4454
+ if (jsonRequested && isDelegatedHelp(command)) {
4455
+ const text: string[] = [];
4456
+ const code = await main([command.kind === "clip" ? "clip" : command.kind, "--help"], {
4457
+ stdout: (value) => text.push(value),
4458
+ stderr: output.stderr,
4459
+ }, dependencies);
4460
+ output.stdout(terminalSafeJson({ kind: "help", text: text.join("") }));
4461
+ return code;
4462
+ }
3978
4463
  try {
3979
4464
  if (command.kind === "clip") {
3980
4465
  return await (dependencies.runClipCommand ?? runClipCommand)(command.arguments, process.env, output);
@@ -3990,7 +4475,7 @@ export async function main(
3990
4475
  return await (dependencies.runPdfCommand ?? runPdfCommand)(command.arguments, process.env, output);
3991
4476
  }
3992
4477
  if (command.kind === "init") {
3993
- return await runInit(command, output, dependencies.initVault ?? initVault);
4478
+ return await runInit(command, output, dependencies.initVault ?? initVault, dependencies.terminal ?? processTerminal());
3994
4479
  }
3995
4480
  if (command.kind === "index" || command.kind === "search") {
3996
4481
  return await runSemantic(command, output, dependencies);
@@ -4001,6 +4486,7 @@ export async function main(
4001
4486
  if (command.kind === "agent-identity") return runAgentIdentity(command, output);
4002
4487
  if (command.kind === "agents") return await runAgents(command, output, dependencies);
4003
4488
  if (command.kind === "note-create") return await runNoteCreate(command, output, dependencies);
4489
+ if (command.kind === "import-supermemory") return await runImportSupermemory(command, output, dependencies);
4004
4490
  if (command.kind === "relation") return await runRelation(command, output, dependencies);
4005
4491
  if (command.kind === "graph-rebuild" || command.kind === "graph-verify" || command.kind === "graph-query") {
4006
4492
  const result = await executeGraphCommand(command, dependencies);
@@ -4011,18 +4497,23 @@ export async function main(
4011
4497
  if (command.kind === "list") return await runList(command, output, dependencies);
4012
4498
  if (command.kind === "publish") return await runPublish(command, output, dependencies);
4013
4499
  if (command.kind === "serve") return await runServe(command, output, dependencies);
4500
+ if (command.kind === "mcp") return await runMcp(command, output, dependencies);
4014
4501
  if (command.kind === "inbox") return await runInbox(command, output, dependencies);
4015
4502
  if (command.kind === "catalog") return await runCatalog(command, output, dependencies);
4016
4503
  return await runVault(command, output, dependencies);
4017
4504
  } catch (error) {
4018
4505
  const message = error instanceof Error ? error.message : String(error);
4506
+ const next = runtimeNext(rawArguments);
4019
4507
  if (jsonRequested) {
4020
4508
  output.stdout(terminalSafeJson({
4021
4509
  ok: false,
4022
- error: { kind: "runtime", message },
4510
+ error: { kind: "runtime", message, next },
4023
4511
  }));
4024
4512
  } else {
4025
- output.stderr(`error: ${safe(message)}\n`);
4513
+ output.stderr(renderFailure(safe(message), next, errorStyle));
4514
+ if (terminal.env.HRANESS_DEBUG === "1" && error instanceof Error && error.stack !== undefined) {
4515
+ output.stderr(`${sanitizeTerminalText(redactSensitiveText(error.stack))}\n`);
4516
+ }
4026
4517
  }
4027
4518
  return 1;
4028
4519
  }
@@ -4061,7 +4552,7 @@ export async function runExecutable(
4061
4552
  rawArguments: readonly string[] = process.argv.slice(2),
4062
4553
  dependencies: CliDependencies = {},
4063
4554
  ): Promise<number> {
4064
- if (!rawArguments.includes("--json")) {
4555
+ if (!machineJsonRequested(rawArguments)) {
4065
4556
  return main(rawArguments, defaultOutput, dependencies);
4066
4557
  }
4067
4558
  return serializeStrictJson(async () => {