@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,893 @@
1
+ /**
2
+ * Command help for the `wordcell` executable: the short bare-invocation
3
+ * screen, grouped root help, `help advanced`, and one help page per command.
4
+ * The command table is the single public command inventory; the package
5
+ * contract test and the parse-error renderer both read it.
6
+ */
7
+
8
+ export type HelpGroup =
9
+ | "start"
10
+ | "notes"
11
+ | "search"
12
+ | "capture"
13
+ | "publish"
14
+ | "agents"
15
+ | "diagnostics"
16
+ | "advanced";
17
+
18
+ export type HelpOption = readonly [spelling: string, description: string];
19
+
20
+ export type CommandHelp = {
21
+ /** Command words after `wordcell`, such as `note create`. */
22
+ readonly id: string;
23
+ /** Short form for the grouped list, such as `note create <id>`. */
24
+ readonly label: string;
25
+ readonly usage: string;
26
+ readonly summary: string;
27
+ readonly group: HelpGroup;
28
+ readonly options: readonly HelpOption[];
29
+ readonly examples: readonly string[];
30
+ /** Extra plain-language lines shown after the summary. */
31
+ readonly notes?: readonly string[];
32
+ /** Hide from the grouped root list (still listed in its family help). */
33
+ readonly unlisted?: true;
34
+ /** A help page for a command family, such as `relation`, not a command. */
35
+ readonly family?: true;
36
+ };
37
+
38
+ const ROOT: HelpOption = ["--root <directory>", "Knowledge base folder (default: current directory)"];
39
+ const INDEX: HelpOption = ["--index <path>", "Catalog note to use (default: index.md)"];
40
+ const JSON_OPTION: HelpOption = ["--json", "Print machine-readable output"];
41
+ const REPO: HelpOption = ["--repo <repository>", "Git repository the notes describe"];
42
+ const WHERE: HelpOption = ["--where <path=value>", "Keep notes whose metadata field has this value"];
43
+ const HAS: HelpOption = ["--has <path>", "Keep notes that set this metadata field"];
44
+ const TAG: HelpOption = ["--tag <tag>", "Keep notes with this tag"];
45
+ const SCOPE: HelpOption = ["--scope <repository-path>", "Keep notes about this code path"];
46
+ const DATABASE: HelpOption = ["--database <path>", "Use this search index file"];
47
+ const MODE: HelpOption = [
48
+ "--mode <hybrid|exact|keyword|semantic>",
49
+ "Match exact words, the keyword index, meaning, or keywords and meaning together",
50
+ ];
51
+ const RULES: HelpOption = ["--rules <file>", "Apply reviewed search aliases from a JSON file"];
52
+ const PRIORITY: HelpOption = ["--priority", "Order results by the rules file first"];
53
+ const DEPTH: HelpOption = ["--depth <count>", "How many links to follow (1 to 10)"];
54
+ const DIRECTION: HelpOption = ["--direction <in|out|both>", "Follow incoming links, outgoing links, or both"];
55
+
56
+ export const commandHelp: readonly CommandHelp[] = [
57
+ {
58
+ id: "init",
59
+ label: "init [directory]",
60
+ usage: "wordcell init [directory] [--json]",
61
+ summary: "Create a knowledge base folder (default: kb). It never merges into an existing folder.",
62
+ group: "start",
63
+ options: [JSON_OPTION],
64
+ examples: ["wordcell init kb"],
65
+ },
66
+ {
67
+ id: "note create",
68
+ label: "note create <id>",
69
+ usage: "wordcell note create <id> --title <title> [options]",
70
+ summary: "Write one new Markdown note.",
71
+ group: "start",
72
+ options: [
73
+ ["--title <title>", "Note title (required)"],
74
+ ["--type <type>", "Note type, such as decision or concept"],
75
+ ["--tag <tag>", "Add a tag (repeatable)"],
76
+ ["--body <markdown>", "Note body"],
77
+ ["--body-file <path|->", "Read the body from a file, or from piped input with -"],
78
+ ROOT,
79
+ JSON_OPTION,
80
+ ],
81
+ examples: [
82
+ 'wordcell note create notes/first --title "First note" --body "Keep retries bounded." --root kb',
83
+ ],
84
+ },
85
+ {
86
+ id: "search",
87
+ label: "search <query>",
88
+ usage: "wordcell search <query> [options]",
89
+ summary: "Search notes by words or meaning.",
90
+ group: "start",
91
+ notes: [
92
+ "Exact mode needs no index or model. Keyword, semantic, and hybrid modes",
93
+ "use the local index from `wordcell index`.",
94
+ ],
95
+ options: [
96
+ ROOT,
97
+ MODE,
98
+ ["--limit <count>", "Show at most this many results"],
99
+ WHERE,
100
+ HAS,
101
+ TAG,
102
+ SCOPE,
103
+ ["--related <note>", "Also show notes linked to this note"],
104
+ ["--graph-depth <1|2>", "How far to follow links for context"],
105
+ ["--no-graph", "Skip link context"],
106
+ ["--history", "Add recent Git history when available"],
107
+ ["--no-history", "Skip Git history (default)"],
108
+ ["--require-history", "Fail when Git history is unavailable"],
109
+ REPO,
110
+ DATABASE,
111
+ RULES,
112
+ PRIORITY,
113
+ ["--candidate-limit <count>", "How many candidates to rank before trimming"],
114
+ ["--min-score <score>", "Drop results scoring below this (0 to 1)"],
115
+ ["--rerank <typesafe>", "Rerank results with the hosted TypeSafe service"],
116
+ ["--rerank-limit <2..25>", "How many results the reranker sees"],
117
+ JSON_OPTION,
118
+ ],
119
+ examples: ['wordcell search "retries" --root kb --mode exact', 'wordcell search "retry policy" --root kb --limit 5'],
120
+ },
121
+ {
122
+ id: "check",
123
+ label: "check",
124
+ usage: "wordcell check [options]",
125
+ summary: "Check links, metadata, and attachments. It changes no files.",
126
+ group: "start",
127
+ options: [
128
+ ROOT,
129
+ INDEX,
130
+ ["--no-catalog", "Skip the catalog check"],
131
+ ["--repo <repository>", "Also report notes whose code paths no longer exist"],
132
+ JSON_OPTION,
133
+ ],
134
+ examples: ["wordcell check --root kb", "wordcell check --root kb --repo ."],
135
+ },
136
+ {
137
+ id: "list",
138
+ label: "list",
139
+ usage: "wordcell list [options]",
140
+ summary: "List notes by metadata or tag. Also: wordcell notes.",
141
+ group: "notes",
142
+ options: [
143
+ ROOT,
144
+ INDEX,
145
+ WHERE,
146
+ HAS,
147
+ TAG,
148
+ SCOPE,
149
+ ["--sort <field>", "Sort by a metadata field, title, path, or link counts"],
150
+ ["--order <asc|desc>", "Sort direction"],
151
+ ["--limit <count>", "Show at most this many notes"],
152
+ JSON_OPTION,
153
+ ],
154
+ examples: ["wordcell list --root kb --tag decision", "wordcell list --root kb --where status=active"],
155
+ },
156
+ {
157
+ id: "links",
158
+ label: "links <note>",
159
+ usage: "wordcell links <note> [options]",
160
+ summary: "Follow links into and out of a note.",
161
+ group: "notes",
162
+ options: [ROOT, INDEX, DIRECTION, DEPTH, ["--limit <count>", "Show at most this many notes"], JSON_OPTION],
163
+ examples: ["wordcell links notes/first --root kb --direction both --depth 2"],
164
+ },
165
+ {
166
+ id: "backlinks",
167
+ label: "backlinks <note>",
168
+ usage: "wordcell backlinks <note> [options]",
169
+ summary: "Show the notes that link to a note.",
170
+ group: "notes",
171
+ options: [ROOT, INDEX, JSON_OPTION],
172
+ examples: ["wordcell backlinks notes/first --root kb"],
173
+ },
174
+ {
175
+ id: "relation",
176
+ label: "relation add|remove|list",
177
+ usage: "wordcell relation <add|remove|list> ...",
178
+ summary: "Edit a note's typed links. They live in its frontmatter.",
179
+ group: "notes",
180
+ family: true,
181
+ options: [],
182
+ examples: ["wordcell relation add notes/first supersedes notes/old --root kb"],
183
+ notes: ["Run `wordcell relation add --help` for each action."],
184
+ },
185
+ {
186
+ id: "relation add",
187
+ label: "relation add <source> <predicate> <target>",
188
+ usage: "wordcell relation add <source> <predicate> <target> [options]",
189
+ summary: "Add a typed link from one note to another. Running it twice changes nothing.",
190
+ group: "notes",
191
+ unlisted: true,
192
+ options: [ROOT, ["--expected-revision <sha256:...>", "Only write if the note is still at this revision"], JSON_OPTION],
193
+ examples: ["wordcell relation add notes/first supersedes notes/old --root kb"],
194
+ },
195
+ {
196
+ id: "relation remove",
197
+ label: "relation remove <source> <predicate> <target>",
198
+ usage: "wordcell relation remove <source> <predicate> <target> [options]",
199
+ summary: "Remove a typed link from a note.",
200
+ group: "notes",
201
+ unlisted: true,
202
+ options: [ROOT, ["--expected-revision <sha256:...>", "Only write if the note is still at this revision"], JSON_OPTION],
203
+ examples: ["wordcell relation remove notes/first supersedes notes/old --root kb"],
204
+ },
205
+ {
206
+ id: "relation list",
207
+ label: "relation list <note>",
208
+ usage: "wordcell relation list <note> [options]",
209
+ summary: "List a note's typed links in both directions.",
210
+ group: "notes",
211
+ unlisted: true,
212
+ options: [ROOT, JSON_OPTION],
213
+ examples: ["wordcell relation list notes/first --root kb"],
214
+ },
215
+ {
216
+ id: "refresh",
217
+ label: "refresh",
218
+ usage: "wordcell refresh [options]",
219
+ summary: "Update the catalog in index.md. It also reports link problems.",
220
+ group: "notes",
221
+ options: [ROOT, INDEX, JSON_OPTION],
222
+ examples: ["wordcell refresh --root kb"],
223
+ },
224
+ {
225
+ id: "catalog",
226
+ label: "catalog",
227
+ usage: "wordcell catalog [options]",
228
+ summary: "Print a catalog of every note. It writes nothing.",
229
+ group: "notes",
230
+ options: [ROOT, INDEX, JSON_OPTION],
231
+ examples: ["wordcell catalog --root kb"],
232
+ },
233
+ {
234
+ id: "import supermemory",
235
+ label: "import supermemory <file>",
236
+ usage: "wordcell import supermemory <export.json>... [options]",
237
+ summary: "Import a Supermemory export as notes.",
238
+ group: "notes",
239
+ options: [
240
+ ROOT,
241
+ ["--prefix <directory>", "Put every imported note in this folder"],
242
+ ["--dry-run", "Show what would be written without writing"],
243
+ JSON_OPTION,
244
+ ],
245
+ examples: ["wordcell import supermemory export.json --root kb --dry-run"],
246
+ },
247
+ {
248
+ id: "index",
249
+ label: "index",
250
+ usage: "wordcell index [options]",
251
+ summary: "Build the local index for meaning-based search.",
252
+ group: "search",
253
+ notes: ["The first run downloads a local embedding model (about 300 MB) once."],
254
+ options: [ROOT, DATABASE, ["--force", "Rebuild the index from scratch"], JSON_OPTION],
255
+ examples: ["wordcell index --root kb"],
256
+ },
257
+ {
258
+ id: "context",
259
+ label: "context <path>",
260
+ usage: "wordcell context <repository-path> [options]",
261
+ summary: "Find notes and AGENTS.md files for a code path.",
262
+ group: "search",
263
+ options: [
264
+ ["--root <vault>", "Knowledge base folder (default: current directory)"],
265
+ REPO,
266
+ ["--kind <auto|file|directory>", "Treat the path as a file or a folder"],
267
+ JSON_OPTION,
268
+ ],
269
+ examples: ["wordcell context src/index.ts --root kb --repo ."],
270
+ },
271
+ {
272
+ id: "history",
273
+ label: "history <note>",
274
+ usage: "wordcell history <note> [options]",
275
+ summary: "Show the Git commits that changed a note. Also: wordcell history search <query>.",
276
+ group: "search",
277
+ options: [
278
+ ROOT,
279
+ REPO,
280
+ ["--limit <count>", "Show at most this many commits"],
281
+ ["--cochanged-limit <count>", "Show at most this many files changed alongside"],
282
+ JSON_OPTION,
283
+ ],
284
+ examples: ["wordcell history notes/first --root kb --repo ."],
285
+ },
286
+ {
287
+ id: "history search",
288
+ label: "history search <query>",
289
+ usage: "wordcell history search <query-or-path> [options]",
290
+ summary: "Search commit messages and changed paths in the repository.",
291
+ group: "search",
292
+ unlisted: true,
293
+ options: [
294
+ ROOT,
295
+ REPO,
296
+ ["--limit <count>", "Show at most this many results"],
297
+ ["--commit-limit <count>", "Read at most this many commits"],
298
+ ["--cochanged-limit <count>", "Show at most this many files changed alongside"],
299
+ JSON_OPTION,
300
+ ],
301
+ examples: ['wordcell history search "retry" --root kb --repo .'],
302
+ },
303
+ {
304
+ id: "inbox",
305
+ label: "inbox",
306
+ usage: "wordcell inbox [options]",
307
+ summary: "List saved sources no note links to yet. Also: wordcell source-inbox.",
308
+ group: "search",
309
+ options: [
310
+ ROOT,
311
+ INDEX,
312
+ ["--source-prefix <directory>", "Look for sources in this folder (repeatable)"],
313
+ ["--limit <count>", "Show at most this many sources"],
314
+ JSON_OPTION,
315
+ ],
316
+ examples: ["wordcell inbox --root kb"],
317
+ },
318
+ {
319
+ id: "clip",
320
+ label: "clip <url|current>",
321
+ usage: "wordcell clip <url|current> [options]",
322
+ summary: "Save a web page as Markdown. Threads, videos, and images are kept too.",
323
+ group: "capture",
324
+ options: [],
325
+ examples: ["wordcell clip https://example.com/article --output kb/articles"],
326
+ notes: ["Run `wordcell clip --help` for every capture option."],
327
+ },
328
+ {
329
+ id: "inspect",
330
+ label: "inspect <url>",
331
+ usage: "wordcell inspect <url> [options]",
332
+ summary: "Preview a capture. It writes no files.",
333
+ group: "capture",
334
+ options: [],
335
+ examples: ["wordcell inspect https://example.com/article"],
336
+ },
337
+ {
338
+ id: "pdf",
339
+ label: "pdf <file-or-url>",
340
+ usage: "wordcell pdf <file-or-url> [options]",
341
+ summary: "Save a PDF as Markdown. The original file and its images are kept.",
342
+ group: "capture",
343
+ options: [],
344
+ examples: ["wordcell pdf paper.pdf --output kb/articles"],
345
+ },
346
+ {
347
+ id: "capture",
348
+ label: "capture show|verify|diff",
349
+ usage: "wordcell capture <show|verify|diff> <bundle> [options]",
350
+ summary: "Read, check, or compare a saved capture.",
351
+ group: "capture",
352
+ family: true,
353
+ options: [],
354
+ examples: ["wordcell capture verify kb/articles/example"],
355
+ notes: ["Run `wordcell capture show --help` for each action. `wordcell capture <url>` is the same as clip."],
356
+ },
357
+ {
358
+ id: "capture show",
359
+ label: "capture show <bundle>",
360
+ usage: "wordcell capture show <bundle> [options]",
361
+ summary: "Print a saved capture as plain text.",
362
+ group: "capture",
363
+ unlisted: true,
364
+ options: [
365
+ ["--verify-assets", "Also check saved images and files"],
366
+ ["--include-source-html", "Include the original page HTML"],
367
+ JSON_OPTION,
368
+ ],
369
+ examples: ["wordcell capture show kb/articles/example"],
370
+ },
371
+ {
372
+ id: "capture verify",
373
+ label: "capture verify <bundle>",
374
+ usage: "wordcell capture verify <bundle> [options]",
375
+ summary: "Check that a saved capture still matches its recorded hashes.",
376
+ group: "capture",
377
+ unlisted: true,
378
+ options: [["--verify-assets", "Also check saved images and files"], JSON_OPTION],
379
+ examples: ["wordcell capture verify kb/articles/example --verify-assets"],
380
+ },
381
+ {
382
+ id: "capture diff",
383
+ label: "capture diff <bundle>",
384
+ usage: "wordcell capture diff <bundle> [options]",
385
+ summary: "Compare a saved capture with an earlier Git version.",
386
+ group: "capture",
387
+ unlisted: true,
388
+ options: [REPO, ["--ref <ref>", "Git version to compare with (default: HEAD)"], JSON_OPTION],
389
+ examples: ["wordcell capture diff kb/articles/example --repo . --ref main"],
390
+ },
391
+ {
392
+ id: "publish",
393
+ label: "publish --out <directory>",
394
+ usage: "wordcell publish --out <directory> [options]",
395
+ summary: "Build a static website from your notes.",
396
+ group: "publish",
397
+ options: [
398
+ ["--out <directory>", "Folder to write the site to (required)"],
399
+ ROOT,
400
+ INDEX,
401
+ ["--include <path>", "Publish only these notes or folders (repeatable)"],
402
+ ["--exclude <path>", "Leave out these notes or folders (repeatable)"],
403
+ ["--include-glob <pattern>", "Publish notes matching a pattern (repeatable)"],
404
+ ["--exclude-glob <pattern>", "Leave out notes matching a pattern (repeatable)"],
405
+ WHERE,
406
+ HAS,
407
+ TAG,
408
+ SCOPE,
409
+ ["--from <note>", "Publish the notes linked from this note"],
410
+ DEPTH,
411
+ DIRECTION,
412
+ ["--title <title>", "Site title"],
413
+ ["--description <text>", "Site description"],
414
+ ["--base-path <path>", "Serve the site under this path"],
415
+ ["--base-url <url>", "Public address of the site"],
416
+ ["--noindex", "Ask search engines not to index the site"],
417
+ ["--no-index-content", "Leave note text out of the site search"],
418
+ ["--deterministic", "Omit the build time so builds match byte for byte"],
419
+ ["--dry-run", "Show what would be published without writing"],
420
+ ["--list-limit <0-1000>", "How many paths to list in the report"],
421
+ ["--force", "Replace an existing output folder"],
422
+ JSON_OPTION,
423
+ ],
424
+ examples: ["wordcell publish --root kb --out site", "wordcell publish --root kb --out site --tag public"],
425
+ },
426
+ {
427
+ id: "serve",
428
+ label: "serve --root <directory>",
429
+ usage: "wordcell serve --root <directory> [options]",
430
+ summary: "Preview a published site on this computer.",
431
+ group: "publish",
432
+ options: [
433
+ ["--root <directory>", "Published site folder (required)"],
434
+ ["--host <host>", "Address to listen on (default: 127.0.0.1)"],
435
+ ["--port <port>", "Port to listen on"],
436
+ JSON_OPTION,
437
+ ],
438
+ examples: ["wordcell serve --root site --port 8080"],
439
+ },
440
+ {
441
+ id: "mcp",
442
+ label: "mcp --root <vault>",
443
+ usage: "wordcell mcp --root <vault> [options]",
444
+ summary: "Serve your notes to AI apps over MCP.",
445
+ group: "publish",
446
+ notes: ["MCP (Model Context Protocol) is how AI apps such as Claude call local tools."],
447
+ options: [
448
+ ["--root <vault>", "Knowledge base folder (required)"],
449
+ REPO,
450
+ ["--read-only", "Offer only the read tools"],
451
+ ],
452
+ examples: ["wordcell mcp --root /absolute/path/to/kb --read-only"],
453
+ },
454
+ {
455
+ id: "agents",
456
+ label: "agents check|audit",
457
+ usage: "wordcell agents <check|audit|identity> ...",
458
+ summary: "Check the notes that give coding agents context for each code folder.",
459
+ group: "agents",
460
+ family: true,
461
+ unlisted: true,
462
+ options: [],
463
+ examples: ["wordcell agents check --root kb --repo ."],
464
+ notes: ["Run `wordcell agents check --help` for each action."],
465
+ },
466
+ {
467
+ id: "agents check",
468
+ label: "agents check",
469
+ usage: "wordcell agents check [options]",
470
+ summary: "Check AGENTS.md context notes against the code.",
471
+ group: "agents",
472
+ options: [["--root <vault>", "Knowledge base folder"], REPO, JSON_OPTION],
473
+ examples: ["wordcell agents check --root kb --repo ."],
474
+ },
475
+ {
476
+ id: "agents audit",
477
+ label: "agents audit",
478
+ usage: "wordcell agents audit [options]",
479
+ summary: "Find long or repeated AGENTS.md rules. It runs agents check first.",
480
+ group: "agents",
481
+ options: [["--root <vault>", "Knowledge base folder"], REPO, JSON_OPTION],
482
+ examples: ["wordcell agents audit --root kb --repo ."],
483
+ },
484
+ {
485
+ id: "doctor",
486
+ label: "doctor",
487
+ usage: "wordcell doctor [--json]",
488
+ summary: "Check optional tools for capture and search. It changes nothing.",
489
+ group: "diagnostics",
490
+ options: [JSON_OPTION],
491
+ examples: ["wordcell doctor"],
492
+ },
493
+ {
494
+ id: "adapters",
495
+ label: "adapters",
496
+ usage: "wordcell adapters [--json]",
497
+ summary: "List the sites and platforms clip supports.",
498
+ group: "diagnostics",
499
+ options: [JSON_OPTION],
500
+ examples: ["wordcell adapters"],
501
+ },
502
+ {
503
+ id: "graph",
504
+ label: "graph",
505
+ usage: "wordcell graph [options]",
506
+ summary: "Show the link graph and broken links. Unlinked notes are listed too.",
507
+ group: "diagnostics",
508
+ notes: ["See `wordcell help advanced` for graph rebuild, verify, and query."],
509
+ options: [ROOT, INDEX, JSON_OPTION],
510
+ examples: ["wordcell graph --root kb"],
511
+ },
512
+ {
513
+ id: "agents identity",
514
+ label: "agents identity <scope>",
515
+ usage: "wordcell agents identity <repository-scope> [--json]",
516
+ summary: "Print the context note name and marker for a code folder, without writing.",
517
+ group: "advanced",
518
+ options: [JSON_OPTION],
519
+ examples: ["wordcell agents identity packages/parser"],
520
+ },
521
+ {
522
+ id: "graph rebuild",
523
+ label: "graph rebuild",
524
+ usage: "wordcell graph rebuild [options]",
525
+ summary: "Rebuild the local graph cache from the Markdown files.",
526
+ group: "advanced",
527
+ options: [["--fresh", "Discard the old cache first"], ROOT, INDEX, JSON_OPTION],
528
+ examples: ["wordcell graph rebuild --root kb"],
529
+ },
530
+ {
531
+ id: "graph verify",
532
+ label: "graph verify",
533
+ usage: "wordcell graph verify [options]",
534
+ summary: "Check that the local graph cache matches the Markdown files.",
535
+ group: "advanced",
536
+ options: [ROOT, INDEX, JSON_OPTION],
537
+ examples: ["wordcell graph verify --root kb"],
538
+ },
539
+ {
540
+ id: "graph query",
541
+ label: "graph query --program <name>",
542
+ usage: "wordcell graph query --program <name> [options]",
543
+ summary: "Run a built-in graph query, with proof of each answer.",
544
+ group: "advanced",
545
+ options: [
546
+ ["--program <name>", "backlinks, reachability, scope-route, relation-closure, shared-tags, or shared-concepts"],
547
+ ["--note <id>", "Start from this note"],
548
+ ["--scope <path>", "Start from this code path"],
549
+ ["--predicate <predicate>", "Follow only this typed link"],
550
+ DEPTH,
551
+ ["--limit <count>", "Show at most this many results"],
552
+ ["--persisted", "Read the saved graph cache"],
553
+ ROOT,
554
+ INDEX,
555
+ JSON_OPTION,
556
+ ],
557
+ examples: ["wordcell graph query --program backlinks --note notes/first --root kb"],
558
+ },
559
+ {
560
+ id: "percolate",
561
+ label: "percolate [note]",
562
+ usage: "wordcell percolate [note] [options]",
563
+ summary: "Suggest recurring concepts and missing links, with evidence. Writes nothing.",
564
+ group: "advanced",
565
+ options: [
566
+ ["--proofs", "Show the evidence for one note"],
567
+ ["--min-support <count>", "Require this many supporting notes"],
568
+ ["--limit <count>", "Show at most this many suggestions"],
569
+ ROOT,
570
+ JSON_OPTION,
571
+ ],
572
+ examples: ["wordcell percolate --root kb"],
573
+ },
574
+ {
575
+ id: "portfolio search",
576
+ label: "portfolio search <query>",
577
+ usage: "wordcell portfolio search <query> --registry <file> --workspace <directory> [options]",
578
+ summary: "Search several knowledge bases you list in a registry file.",
579
+ group: "advanced",
580
+ options: [
581
+ ["--registry <file>", "Registry of knowledge bases (required)"],
582
+ ["--workspace <directory>", "Folder that holds their checkouts (required)"],
583
+ ["--shared", "Search every shared entry"],
584
+ ["--vault <owner/id>", "Search this entry (repeatable)"],
585
+ MODE,
586
+ RULES,
587
+ PRIORITY,
588
+ ["--limit <count>", "Show at most this many results"],
589
+ ["--require-all", "Fail when any selected knowledge base is missing"],
590
+ JSON_OPTION,
591
+ ],
592
+ examples: ['wordcell portfolio search "retries" --registry kb-portfolio.json --workspace .. --shared'],
593
+ },
594
+ {
595
+ id: "portfolio audit",
596
+ label: "portfolio audit",
597
+ usage: "wordcell portfolio audit --registry <file> --workspace <directory> [options]",
598
+ summary: "Check several knowledge bases for broken links, duplicates, and missing files.",
599
+ group: "advanced",
600
+ options: [
601
+ ["--registry <file>", "Registry of knowledge bases (required)"],
602
+ ["--workspace <directory>", "Folder that holds their checkouts (required)"],
603
+ ["--all", "Audit every entry"],
604
+ ["--shared", "Audit every shared entry"],
605
+ ["--vault <owner/id>", "Audit this entry (repeatable)"],
606
+ ["--strict", "Fail on warnings"],
607
+ JSON_OPTION,
608
+ ],
609
+ examples: ["wordcell portfolio audit --registry kb-portfolio.json --workspace .. --all"],
610
+ },
611
+ {
612
+ id: "evaluate",
613
+ label: "evaluate <manifest.json>",
614
+ usage: "wordcell evaluate <manifest.json> [options]",
615
+ summary: "Measure how well each search method finds the right notes in a frozen test set.",
616
+ group: "advanced",
617
+ options: [
618
+ ROOT,
619
+ REPO,
620
+ DATABASE,
621
+ ["--retriever <id>", "Run only this search method (repeatable)"],
622
+ ["--split <development|test|all>", "Which questions to run"],
623
+ ["--limit <count>", "Results to keep per question"],
624
+ ["--cutoff <count>", "Rank cutoff for the scores"],
625
+ ["--timeout <milliseconds>", "Time limit per question"],
626
+ ["--baseline <id>", "Compare every method with this one"],
627
+ ["--model-file <path>", "Record the embedding model file used"],
628
+ ["--cache-state <cold|mixed|warm>", "Record how warm the caches were"],
629
+ JSON_OPTION,
630
+ ],
631
+ examples: ["wordcell evaluate eval/manifest.json --root kb --repo ."],
632
+ },
633
+ {
634
+ id: "url-metadata tool",
635
+ label: "url-metadata tool build|check",
636
+ usage: "wordcell url-metadata tool <build|check>",
637
+ summary: "Build or check the helper that looks up metadata for saved links. Needs Rust.",
638
+ group: "advanced",
639
+ options: [],
640
+ examples: ["wordcell url-metadata tool build"],
641
+ },
642
+ {
643
+ id: "url-metadata backfill",
644
+ label: "url-metadata backfill",
645
+ usage: "wordcell url-metadata backfill [options]",
646
+ summary: "Add title and archive metadata files next to saved links.",
647
+ group: "advanced",
648
+ options: [],
649
+ examples: ["wordcell url-metadata backfill --root kb"],
650
+ notes: ["Run `wordcell url-metadata --help` for every option."],
651
+ },
652
+ {
653
+ id: "support",
654
+ label: "support",
655
+ usage: "wordcell support [status | enable | dismiss | snooze] [--json]",
656
+ summary: "Optional support settings. No command signs you up or charges you.",
657
+ group: "advanced",
658
+ notes: ["Set HRANESS_SUPPORT=off to turn off support invitations."],
659
+ options: [JSON_OPTION],
660
+ examples: ["wordcell support status"],
661
+ },
662
+ ];
663
+
664
+ /** Alternative first words that reach the same help page. */
665
+ const COMMAND_ALIASES: Readonly<Record<string, string>> = {
666
+ notes: "list",
667
+ "source-inbox": "inbox",
668
+ };
669
+
670
+
671
+
672
+ const byId = new Map(commandHelp.map((entry) => [entry.id, entry]));
673
+
674
+ const GROUP_TITLES: readonly (readonly [HelpGroup, string])[] = [
675
+ ["start", "Start here"],
676
+ ["notes", "Notes and links"],
677
+ ["search", "Search"],
678
+ ["capture", "Capture"],
679
+ ["publish", "Publish and serve"],
680
+ ["agents", "Agents"],
681
+ ["diagnostics", "Diagnostics"],
682
+ ];
683
+
684
+ export const DESCRIPTION = [
685
+ "Wordcell keeps decisions, plans, and sources as Markdown beside your code,",
686
+ "so coding agents can find them from the file they are about to change.",
687
+ ];
688
+
689
+ const LABEL_WIDTH = 28;
690
+
691
+ const MAX_COLUMNS = 80;
692
+
693
+ function wrapWords(text: string, width: number): string[] {
694
+ const lines: string[] = [];
695
+ let current = "";
696
+ for (const word of text.split(" ")) {
697
+ if (current !== "" && current.length + 1 + word.length > width) {
698
+ lines.push(current);
699
+ current = word;
700
+ } else {
701
+ current = current === "" ? word : `${current} ${word}`;
702
+ }
703
+ }
704
+ if (current !== "") lines.push(current);
705
+ return lines;
706
+ }
707
+
708
+ /** A two-column row that wraps the summary so no line passes 80 columns. */
709
+ function row(label: string, summary: string, indent = " ", width = LABEL_WIDTH): string {
710
+ const column = indent.length + width;
711
+ const padded = `${indent}${label}`;
712
+ const summaryLines = wrapWords(summary, Math.max(20, MAX_COLUMNS - column));
713
+ const continuation = summaryLines.slice(1).map((line) => `${" ".repeat(column)}${line}`);
714
+ if (label === "") return [`${" ".repeat(column)}${summaryLines[0] ?? ""}`, ...continuation].join("\n");
715
+ const head = padded.length < column
716
+ ? [`${padded.padEnd(column)}${summaryLines[0] ?? ""}`]
717
+ : [padded, `${" ".repeat(column)}${summaryLines[0] ?? ""}`];
718
+ return [...head, ...continuation].join("\n");
719
+ }
720
+
721
+ function firstSentence(summary: string): string {
722
+ const end = summary.search(/\.(\s|$)/u);
723
+ return end === -1 ? summary : summary.slice(0, end);
724
+ }
725
+
726
+ function listGroup(group: HelpGroup): string[] {
727
+ return commandHelp
728
+ .filter((entry) => entry.group === group && entry.unlisted !== true)
729
+ .map((entry) => row(entry.label, firstSentence(entry.summary)));
730
+ }
731
+
732
+ /** Bare `wordcell`: what it is and the first commands to run, in at most 25 lines. */
733
+ export function startHelp(version: string | undefined): string {
734
+ const start = (command: string, summary: string): string => row(command, summary, " ", 40);
735
+ const lines = [
736
+ ...DESCRIPTION,
737
+ "",
738
+ "Start here (no account or model needed)",
739
+ start("wordcell init kb", "Create a knowledge base in ./kb"),
740
+ start('wordcell note create notes/first --title "First note" --root kb', "Write your first note"),
741
+ start('wordcell search "first" --root kb', "Search your notes"),
742
+ start("wordcell check --root kb", "Check links and metadata"),
743
+ "",
744
+ "Everyday",
745
+ start("wordcell clip <url> --output kb/articles", "Save a web page as Markdown"),
746
+ start("wordcell context <path> --root kb", "Find notes about a code path"),
747
+ start("wordcell index --root kb", "Turn on meaning-based search"),
748
+ "",
749
+ "All commands: wordcell --help · Command help: wordcell help <command>",
750
+ ];
751
+ if (version !== undefined) lines.push(`wordcell ${version}`);
752
+ return `${lines.join("\n")}\n`;
753
+ }
754
+
755
+ /** `wordcell --help`: every everyday command, grouped, in at most 60 lines. */
756
+ export function rootHelp(): string {
757
+ const lines = ["Usage: wordcell <command> [options]", "", ...DESCRIPTION];
758
+ for (const [group, title] of GROUP_TITLES) {
759
+ lines.push("", title, ...listGroup(group));
760
+ }
761
+ lines.push(
762
+ "",
763
+ "Options",
764
+ row("-h, --help", "Show help. Also: wordcell help <command>", " ", 22),
765
+ row("-V, --version", "Print the version", " ", 22),
766
+ row("--root <directory>", "Knowledge base folder (default: current directory)", " ", 22),
767
+ row("--json", "Print machine-readable output", " ", 22),
768
+ "",
769
+ "More commands: wordcell help advanced · Docs: https://wordcell.io/docs",
770
+ "Optional support: wordcell support · Turn off: HRANESS_SUPPORT=off",
771
+ );
772
+ return `${lines.join("\n")}\n`;
773
+ }
774
+
775
+ /** `wordcell help advanced`: maintainer, evaluation, and multi-vault commands. */
776
+ export function advancedHelp(): string {
777
+ const lines = [
778
+ "Usage: wordcell <command> [options]",
779
+ "",
780
+ "Advanced commands for maintainers, evaluation, and several knowledge bases.",
781
+ "",
782
+ ...listGroup("advanced"),
783
+ "",
784
+ "Command help: wordcell help <command> · Everyday commands: wordcell --help",
785
+ ];
786
+ return `${lines.join("\n")}\n`;
787
+ }
788
+
789
+ /** Per-command help page, or undefined for an unknown command. */
790
+ export function commandHelpText(id: string): string | undefined {
791
+ const entry = byId.get(id);
792
+ if (entry === undefined) return undefined;
793
+ const lines = [`Usage: ${entry.usage}`, "", ...wrapWords(entry.summary, MAX_COLUMNS)];
794
+ if (entry.notes !== undefined) lines.push(...entry.notes);
795
+ const family = commandHelp.filter((candidate) => candidate.id.startsWith(`${entry.id} `));
796
+ if (family.length > 0) {
797
+ lines.push("", "Commands", ...family.map((candidate) => row(`${candidate.label}`, firstSentence(candidate.summary))));
798
+ }
799
+ if (entry.options.length > 0) {
800
+ const width = Math.min(30, Math.max(...entry.options.map(([spelling]) => spelling.length)) + 2);
801
+ lines.push("", "Options", ...entry.options.map(([spelling, description]) => row(spelling, description, " ", width)));
802
+ }
803
+ if (entry.examples.length > 0) {
804
+ lines.push("", entry.examples.length === 1 ? "Example" : "Examples", ...entry.examples.map((example) => ` ${example}`));
805
+ }
806
+ return `${lines.join("\n")}\n`;
807
+ }
808
+
809
+ function canonicalFirstWord(word: string): string {
810
+ return COMMAND_ALIASES[word] ?? word;
811
+ }
812
+
813
+ /**
814
+ * The help id for command words, preferring the two-word form
815
+ * (`note create`) over the family (`note`). Returns undefined when the words
816
+ * name no command.
817
+ */
818
+ export function resolveCommandId(words: readonly string[]): string | undefined {
819
+ const first = words[0];
820
+ if (first === undefined) return undefined;
821
+ const head = canonicalFirstWord(first);
822
+ const second = words[1];
823
+ if (second !== undefined && byId.has(`${head} ${second}`)) return `${head} ${second}`;
824
+ if (byId.has(head)) return head;
825
+ if (head === "note") return "note create";
826
+ if (head === "import") return "import supermemory";
827
+ if (head === "portfolio") return "portfolio search";
828
+ if (head === "url-metadata") return "url-metadata backfill";
829
+ return undefined;
830
+ }
831
+
832
+ /** Number of command words `resolveCommandId` consumed. */
833
+ export function commandWordCount(id: string): number {
834
+ return id.split(" ").length;
835
+ }
836
+
837
+ export function knownCommandWords(): readonly string[] {
838
+ const words = new Set<string>();
839
+ for (const entry of commandHelp) words.add(entry.id.split(" ")[0] ?? entry.id);
840
+ for (const alias of Object.keys(COMMAND_ALIASES)) words.add(alias);
841
+ for (const word of ["help", "version", "note", "import", "portfolio", "url-metadata"]) words.add(word);
842
+ return [...words].toSorted();
843
+ }
844
+
845
+ /** Options with a `<value>` in their spelling take the next argument. */
846
+ export function valueOptions(id: string): ReadonlySet<string> {
847
+ const entry = byId.get(id);
848
+ const names = new Set<string>();
849
+ for (const [spelling] of entry?.options ?? []) {
850
+ if (spelling.includes("<")) names.add(spelling.split(" ")[0] ?? spelling);
851
+ }
852
+ return names;
853
+ }
854
+
855
+ export function optionNames(id: string): readonly string[] {
856
+ const entry = byId.get(id);
857
+ return (entry?.options ?? []).map(([spelling]) => spelling.split(" ")[0] ?? spelling);
858
+ }
859
+
860
+ function editDistance(left: string, right: string): number {
861
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
862
+ for (let row_ = 1; row_ <= left.length; row_ += 1) {
863
+ let diagonal = previous[0] ?? 0;
864
+ previous[0] = row_;
865
+ for (let column = 1; column <= right.length; column += 1) {
866
+ const above = previous[column] ?? 0;
867
+ const cost = left[row_ - 1] === right[column - 1] ? 0 : 1;
868
+ previous[column] = Math.min(above + 1, (previous[column - 1] ?? 0) + 1, diagonal + cost);
869
+ diagonal = above;
870
+ }
871
+ }
872
+ return previous[right.length] ?? Number.MAX_SAFE_INTEGER;
873
+ }
874
+
875
+ /** Closest candidate close enough to be a likely typo, for "Did you mean" hints. */
876
+ export function closestMatch(input: string, candidates: readonly string[]): string | undefined {
877
+ const letters = input.replace(/^-+/u, "").length;
878
+ let best: string | undefined;
879
+ let bestDistance = Math.max(1, Math.floor(letters / 3)) + 1;
880
+ for (const candidate of candidates) {
881
+ const distance = editDistance(input, candidate);
882
+ if (distance < bestDistance) {
883
+ best = candidate;
884
+ bestDistance = distance;
885
+ }
886
+ }
887
+ return best;
888
+ }
889
+
890
+ /** Every command id, for inventory checks. */
891
+ export function commandIds(): readonly string[] {
892
+ return commandHelp.filter((entry) => entry.family !== true).map(({ id }) => id).toSorted();
893
+ }