@hraness/wordcell 0.22.5 → 0.24.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 (78) hide show
  1. package/README.md +67 -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-n9kwenpt.js → index-1w0sp1q7.js} +1 -1
  20. package/dist/{index-de2w8crk.js → index-2fmnttca.js} +82 -15
  21. package/dist/{index-8bgnhjgq.js → index-44kw9tr8.js} +3867 -162
  22. package/dist/{index-42tb8qqp.js → index-5d1rd5v9.js} +444 -9
  23. package/dist/{index-aer0jdrq.js → index-ahyhryb8.js} +1 -1
  24. package/dist/{index-f7fnww7a.js → index-b0khj0h5.js} +164 -9
  25. package/dist/{index-qdb8f8va.js → index-bdwcjvr4.js} +1 -1
  26. package/dist/{index-dfag79p7.js → index-byz4kzww.js} +3 -0
  27. package/dist/{index-hgve9rh2.js → index-cd75vky9.js} +96 -1
  28. package/dist/{index-h2qn3y3f.js → index-f184qyh1.js} +4 -4
  29. package/dist/{index-n57tewfr.js → index-fp732bgg.js} +2 -2
  30. package/dist/{index-zy7an84p.js → index-jvb7w0gg.js} +5 -2
  31. package/dist/{index-f984hw45.js → index-k86wepd8.js} +1 -1
  32. package/dist/{index-1r5rnkfr.js → index-mvs6f7ne.js} +2 -2
  33. package/dist/{index-p96kb6t2.js → index-nn67kzwa.js} +1 -1
  34. package/dist/{index-fc4dr114.js → index-nwrehke7.js} +1 -1
  35. package/dist/{index-pz2b2x0y.js → index-pgtm2nhf.js} +1 -1
  36. package/dist/{index-054mb7d3.js → index-qfqjfxaa.js} +10 -9
  37. package/dist/{index-xzvcw9ga.js → index-r0m5taz2.js} +2 -2
  38. package/dist/{index-gtwqye5a.js → index-tcaq1c7f.js} +4 -4
  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-1yhv6vm6.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 +12 -4
  53. package/skills/wordcell/AGENTS.md +1 -0
  54. package/skills/wordcell/SKILL.md +5 -3
  55. package/skills/wordcell/references/query.md +52 -4
  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 +894 -0
  61. package/src/cli-intro.ts +1 -1
  62. package/src/cli-program.ts +612 -83
  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 +1138 -0
  74. package/src/pdf/cli.ts +5 -4
  75. package/src/sdk.ts +149 -1
  76. package/src/selected-passage.ts +409 -0
  77. package/src/support-profile.ts +1 -1
  78. package/src/support.ts +3 -0
@@ -1,29 +1,41 @@
1
1
  // @bun
2
2
  import {
3
3
  backfillSavedUrlMetadata,
4
- createRustMetadataSearchProvider
5
- } from "./index-bcknqxrq.js";
4
+ createRustMetadataSearchProvider,
5
+ savedSourceProblem
6
+ } from "./index-0eacgvpv.js";
6
7
  import {
7
8
  main as main2
8
- } from "./index-054mb7d3.js";
9
+ } from "./index-qfqjfxaa.js";
9
10
  import {
10
11
  MAX_AUTHORIZED_VAULTS,
11
12
  auditKnowledgePortfolio,
12
13
  loadPortfolioRegistry,
13
14
  openKnowledgePortfolio,
14
15
  snapshotPortfolioRegistry
15
- } from "./index-h2qn3y3f.js";
16
+ } from "./index-f184qyh1.js";
16
17
  import {
17
18
  diffCaptureBundle
18
19
  } from "./index-j4zgmzjr.js";
19
20
  import {
20
21
  main
21
- } from "./index-de2w8crk.js";
22
+ } from "./index-2fmnttca.js";
23
+ import {
24
+ detectAudience,
25
+ renderFailure,
26
+ sentence,
27
+ slugify,
28
+ stderrStyle,
29
+ symbol,
30
+ terminalOutput,
31
+ terminalStyle
32
+ } from "./index-cd75vky9.js";
22
33
  import {
23
34
  verifyCaptureBundle
24
35
  } from "./index-npg9z1a4.js";
25
36
  import {
26
- redactSensitiveText
37
+ redactSensitiveText,
38
+ sanitizeArtifactUrl
27
39
  } from "./index-mxxxytys.js";
28
40
  import {
29
41
  initVault
@@ -39,7 +51,7 @@ import {
39
51
  import {
40
52
  knowledgeBaseEvaluationRetrieverIds,
41
53
  openKnowledgeBaseEvaluation
42
- } from "./index-1r5rnkfr.js";
54
+ } from "./index-mvs6f7ne.js";
43
55
  import {
44
56
  DEFAULT_SEARCH_RESULTS,
45
57
  MAX_SEARCH_CANDIDATES,
@@ -48,7 +60,7 @@ import {
48
60
  MAX_SEARCH_RESULTS,
49
61
  openKnowledgeBase,
50
62
  searchEvidenceRank
51
- } from "./index-42tb8qqp.js";
63
+ } from "./index-5d1rd5v9.js";
52
64
  import {
53
65
  MAX_SEARCH_RULE_CONFIG_BYTES,
54
66
  parseSearchRules
@@ -58,8 +70,9 @@ import {
58
70
  qmdIndexerVersion,
59
71
  recommendedEmbeddingModel,
60
72
  recommendedEmbeddingModelSha256,
73
+ semanticDatabasePath,
61
74
  sha256EmbeddingModelFile
62
- } from "./index-n57tewfr.js";
75
+ } from "./index-fp732bgg.js";
63
76
  import {
64
77
  MAX_RERANK_CANDIDATES
65
78
  } from "./index-j70m75wd.js";
@@ -72,25 +85,25 @@ import {
72
85
  } from "./index-b88v3vtm.js";
73
86
  import {
74
87
  percolateWithGraph
75
- } from "./index-gtwqye5a.js";
88
+ } from "./index-tcaq1c7f.js";
76
89
  import {
77
90
  MAX_PERCOLATION_MENTIONS,
78
91
  MAX_PERCOLATION_MENTION_PAIRS,
79
92
  MAX_PERCOLATION_NOTES,
80
93
  MAX_SCOPED_PERCOLATION_MENTION_PAIRS,
81
94
  percolateVault
82
- } from "./index-qdb8f8va.js";
95
+ } from "./index-bdwcjvr4.js";
83
96
  import {
84
97
  queryGraph,
85
98
  rebuildGraph,
86
99
  verifyGraph
87
- } from "./index-f75zmmdr.js";
100
+ } from "./index-1er88ckw.js";
88
101
  import {
89
102
  validateGraphQueryRequest
90
- } from "./index-pz2b2x0y.js";
103
+ } from "./index-pgtm2nhf.js";
91
104
  import {
92
105
  validateSearchQuery
93
- } from "./index-aer0jdrq.js";
106
+ } from "./index-ahyhryb8.js";
94
107
  import {
95
108
  sanitizeTerminalLine,
96
109
  sanitizeTerminalText
@@ -100,18 +113,23 @@ import {
100
113
  MAX_PUBLISH_LIST_LIMIT,
101
114
  publishVault,
102
115
  renderPublishReportText
103
- } from "./index-xzvcw9ga.js";
116
+ } from "./index-r0m5taz2.js";
104
117
  import {
118
+ MAX_SCANNED_NOTES,
119
+ MAX_VAULT_UTF8_BYTES,
120
+ markdownFiles,
105
121
  refreshVaultComplete,
106
122
  scanVault,
107
123
  scanVaultComplete
108
- } from "./index-8v6k9h4r.js";
124
+ } from "./index-1s8mc4hz.js";
109
125
  import {
110
126
  navigateLinks
111
127
  } from "./index-d13v9ckt.js";
112
128
  import {
113
129
  MAX_QUERY_FILTERS,
130
+ MAX_QUERY_METADATA_PATH_UTF8_BYTES,
114
131
  MAX_QUERY_TAGS,
132
+ MAX_QUERY_TEXT_UTF8_BYTES,
115
133
  queryVault,
116
134
  validateQueryOptions
117
135
  } from "./index-48pz4jpc.js";
@@ -141,15 +159,38 @@ import {
141
159
  validateMarkdownAttachments
142
160
  } from "./index-x3fthpsc.js";
143
161
  import {
162
+ InvalidCanonicalNoteIdError,
163
+ MAX_NOTE_BYTES,
164
+ NOTE_REVISION_PATTERN,
165
+ NoteAlreadyExistsError,
166
+ NoteRevisionConflictError,
144
167
  addNoteRelation,
168
+ assertExactDirectoryEntry,
169
+ assertSafeParent,
170
+ canonicalNoteId,
145
171
  createNote,
146
- removeNoteRelation
147
- } from "./index-f7fnww7a.js";
172
+ createNoteProgram,
173
+ frontmatter,
174
+ isErrno,
175
+ nativeAuthoringPlatform,
176
+ noteRevision,
177
+ relationsFromParts,
178
+ removeNoteRelation,
179
+ renderCreatedNote,
180
+ renderUpdatedNoteBodyAndFields,
181
+ resolveVault,
182
+ revisionFor,
183
+ runAuthoring,
184
+ sha256,
185
+ updateNoteBody,
186
+ updateNoteBodyProgram
187
+ } from "./index-b0khj0h5.js";
148
188
  import {
189
+ isMetadataNumber,
149
190
  lookupNote,
150
191
  parseVaultKey,
151
192
  renderCatalog
152
- } from "./index-zy7an84p.js";
193
+ } from "./index-jvb7w0gg.js";
153
194
 
154
195
  // src/clip/url-metadata-cli.ts
155
196
  import { resolve as resolve2 } from "path";
@@ -630,10 +671,10 @@ Build or validate the immutable metadata-search-engine-rs helper directly from
630
671
  an installed @hraness/wordcell package:
631
672
  wordcell url-metadata tool build
632
673
  `;
633
- var defaultOutput = {
674
+ var defaultOutput = terminalOutput({
634
675
  stdout: (value) => process.stdout.write(value),
635
676
  stderr: (value) => process.stderr.write(value)
636
- };
677
+ });
637
678
  function integer(value, minimum, maximum) {
638
679
  if (!/^(?:0|[1-9]\d*)$/u.test(value))
639
680
  return null;
@@ -759,9 +800,7 @@ async function main3(rawArguments = process.argv.slice(2), environment = process
759
800
  if (parsed.json)
760
801
  output.stdout(terminalJson({ ok: false, error: parsed.message }));
761
802
  else
762
- output.stderr(`error: ${sanitizeTerminalText(parsed.message)}
763
-
764
- ${urlMetadataUsage}`);
803
+ output.stderr(renderFailure(sentence(sanitizeTerminalLine(parsed.message)), "wordcell url-metadata --help", stderrStyle(environment, output)));
765
804
  return 2;
766
805
  }
767
806
  if (parsed.value.kind === "help") {
@@ -795,8 +834,7 @@ ${urlMetadataUsage}`);
795
834
  if (parsed.value.kind === "backfill" && parsed.value.json) {
796
835
  output.stdout(terminalJson({ ok: false, error: message }));
797
836
  } else
798
- output.stderr(`error: ${sanitizeTerminalText(message)}
799
- `);
837
+ output.stderr(renderFailure(sanitizeTerminalLine(message), "wordcell url-metadata tool check", stderrStyle(environment, output)));
800
838
  return 1;
801
839
  }
802
840
  }
@@ -809,17 +847,844 @@ function terminalIntro(terminal) {
809
847
  return "";
810
848
  return ` .----. /
811
849
  | ===| / wordcell
812
- | ===|/ Markdown memory for your agents.
850
+ | ===|/ The decisions behind your code.
813
851
  |____/
814
852
 
815
853
  `;
816
854
  }
817
855
 
856
+ // src/cli-help.ts
857
+ var ROOT = ["--root <directory>", "Knowledge base folder (default: current directory)"];
858
+ var INDEX = ["--index <path>", "Catalog note to use (default: index.md)"];
859
+ var JSON_OPTION = ["--json", "Print machine-readable output"];
860
+ var REPO = ["--repo <repository>", "Git repository the notes describe"];
861
+ var WHERE = ["--where <path=value>", "Keep notes whose metadata field has this value"];
862
+ var HAS = ["--has <path>", "Keep notes that set this metadata field"];
863
+ var TAG = ["--tag <tag>", "Keep notes with this tag"];
864
+ var SCOPE = ["--scope <repository-path>", "Keep notes about this code path"];
865
+ var DATABASE = ["--database <path>", "Use this search index file"];
866
+ var MODE = [
867
+ "--mode <hybrid|exact|keyword|semantic>",
868
+ "Match exact words, the keyword index, meaning, or keywords and meaning together"
869
+ ];
870
+ var RULES = ["--rules <file>", "Apply reviewed search aliases from a JSON file"];
871
+ var PRIORITY = ["--priority", "Order results by the rules file first"];
872
+ var DEPTH = ["--depth <count>", "How many links to follow (1 to 10)"];
873
+ var DIRECTION = ["--direction <in|out|both>", "Follow incoming links, outgoing links, or both"];
874
+ var commandHelp = [
875
+ {
876
+ id: "init",
877
+ label: "init [directory]",
878
+ usage: "wordcell init [directory] [--json]",
879
+ summary: "Create a knowledge base folder (default: kb). It never merges into an existing folder.",
880
+ group: "start",
881
+ options: [JSON_OPTION],
882
+ examples: ["wordcell init kb"]
883
+ },
884
+ {
885
+ id: "note create",
886
+ label: "note create <id>",
887
+ usage: "wordcell note create <id> --title <title> [options]",
888
+ summary: "Write one new Markdown note.",
889
+ group: "start",
890
+ options: [
891
+ ["--title <title>", "Note title (required)"],
892
+ ["--type <type>", "Note type, such as decision or concept"],
893
+ ["--tag <tag>", "Add a tag (repeatable)"],
894
+ ["--body <markdown>", "Note body"],
895
+ ["--body-file <path|->", "Read the body from a file, or from piped input with -"],
896
+ ROOT,
897
+ JSON_OPTION
898
+ ],
899
+ examples: [
900
+ 'wordcell note create notes/first --title "First note" --body "Keep retries bounded." --root kb'
901
+ ]
902
+ },
903
+ {
904
+ id: "search",
905
+ label: "search <query>",
906
+ usage: "wordcell search <query> [options]",
907
+ summary: "Search notes by words or meaning.",
908
+ group: "start",
909
+ notes: [
910
+ "Exact mode needs no index or model. Keyword, semantic, and hybrid modes",
911
+ "use the local index from `wordcell index`."
912
+ ],
913
+ options: [
914
+ ROOT,
915
+ MODE,
916
+ ["--limit <count>", "Show at most this many results"],
917
+ WHERE,
918
+ HAS,
919
+ TAG,
920
+ SCOPE,
921
+ ["--related <note>", "Also show notes linked to this note"],
922
+ ["--graph-depth <1|2>", "How far to follow links for context"],
923
+ ["--no-graph", "Skip link context"],
924
+ ["--history", "Add recent Git history when available"],
925
+ ["--no-history", "Skip Git history (default)"],
926
+ ["--require-history", "Fail when Git history is unavailable"],
927
+ REPO,
928
+ DATABASE,
929
+ RULES,
930
+ PRIORITY,
931
+ ["--candidate-limit <count>", "How many candidates to rank before trimming"],
932
+ ["--min-score <score>", "Drop results scoring below this (0 to 1)"],
933
+ ["--rerank <typesafe>", "Rerank results with the hosted TypeSafe service"],
934
+ ["--rerank-limit <2..25>", "How many results the reranker sees"],
935
+ ["--selected-passage", "Add a local source excerpt to each result"],
936
+ JSON_OPTION
937
+ ],
938
+ examples: ['wordcell search "retries" --root kb --mode exact', 'wordcell search "retry policy" --root kb --limit 5']
939
+ },
940
+ {
941
+ id: "check",
942
+ label: "check",
943
+ usage: "wordcell check [options]",
944
+ summary: "Check links, metadata, and attachments. It changes no files.",
945
+ group: "start",
946
+ options: [
947
+ ROOT,
948
+ INDEX,
949
+ ["--no-catalog", "Skip the catalog check"],
950
+ ["--repo <repository>", "Also report notes whose code paths no longer exist"],
951
+ JSON_OPTION
952
+ ],
953
+ examples: ["wordcell check --root kb", "wordcell check --root kb --repo ."]
954
+ },
955
+ {
956
+ id: "list",
957
+ label: "list",
958
+ usage: "wordcell list [options]",
959
+ summary: "List notes by metadata or tag. Also: wordcell notes.",
960
+ group: "notes",
961
+ options: [
962
+ ROOT,
963
+ INDEX,
964
+ WHERE,
965
+ HAS,
966
+ TAG,
967
+ SCOPE,
968
+ ["--sort <field>", "Sort by a metadata field, title, path, or link counts"],
969
+ ["--order <asc|desc>", "Sort direction"],
970
+ ["--limit <count>", "Show at most this many notes"],
971
+ JSON_OPTION
972
+ ],
973
+ examples: ["wordcell list --root kb --tag decision", "wordcell list --root kb --where status=active"]
974
+ },
975
+ {
976
+ id: "links",
977
+ label: "links <note>",
978
+ usage: "wordcell links <note> [options]",
979
+ summary: "Follow links into and out of a note.",
980
+ group: "notes",
981
+ options: [ROOT, INDEX, DIRECTION, DEPTH, ["--limit <count>", "Show at most this many notes"], JSON_OPTION],
982
+ examples: ["wordcell links notes/first --root kb --direction both --depth 2"]
983
+ },
984
+ {
985
+ id: "backlinks",
986
+ label: "backlinks <note>",
987
+ usage: "wordcell backlinks <note> [options]",
988
+ summary: "Show the notes that link to a note.",
989
+ group: "notes",
990
+ options: [ROOT, INDEX, JSON_OPTION],
991
+ examples: ["wordcell backlinks notes/first --root kb"]
992
+ },
993
+ {
994
+ id: "relation",
995
+ label: "relation add|remove|list",
996
+ usage: "wordcell relation <add|remove|list> ...",
997
+ summary: "Edit a note's typed links. They live in its frontmatter.",
998
+ group: "notes",
999
+ family: true,
1000
+ options: [],
1001
+ examples: ["wordcell relation add notes/first supersedes notes/old --root kb"],
1002
+ notes: ["Run `wordcell relation add --help` for each action."]
1003
+ },
1004
+ {
1005
+ id: "relation add",
1006
+ label: "relation add <source> <predicate> <target>",
1007
+ usage: "wordcell relation add <source> <predicate> <target> [options]",
1008
+ summary: "Add a typed link from one note to another. Running it twice changes nothing.",
1009
+ group: "notes",
1010
+ unlisted: true,
1011
+ options: [ROOT, ["--expected-revision <sha256:...>", "Only write if the note is still at this revision"], JSON_OPTION],
1012
+ examples: ["wordcell relation add notes/first supersedes notes/old --root kb"]
1013
+ },
1014
+ {
1015
+ id: "relation remove",
1016
+ label: "relation remove <source> <predicate> <target>",
1017
+ usage: "wordcell relation remove <source> <predicate> <target> [options]",
1018
+ summary: "Remove a typed link from a note.",
1019
+ group: "notes",
1020
+ unlisted: true,
1021
+ options: [ROOT, ["--expected-revision <sha256:...>", "Only write if the note is still at this revision"], JSON_OPTION],
1022
+ examples: ["wordcell relation remove notes/first supersedes notes/old --root kb"]
1023
+ },
1024
+ {
1025
+ id: "relation list",
1026
+ label: "relation list <note>",
1027
+ usage: "wordcell relation list <note> [options]",
1028
+ summary: "List a note's typed links in both directions.",
1029
+ group: "notes",
1030
+ unlisted: true,
1031
+ options: [ROOT, JSON_OPTION],
1032
+ examples: ["wordcell relation list notes/first --root kb"]
1033
+ },
1034
+ {
1035
+ id: "refresh",
1036
+ label: "refresh",
1037
+ usage: "wordcell refresh [options]",
1038
+ summary: "Update the catalog in index.md. It also reports link problems.",
1039
+ group: "notes",
1040
+ options: [ROOT, INDEX, JSON_OPTION],
1041
+ examples: ["wordcell refresh --root kb"]
1042
+ },
1043
+ {
1044
+ id: "catalog",
1045
+ label: "catalog",
1046
+ usage: "wordcell catalog [options]",
1047
+ summary: "Print a catalog of every note. It writes nothing.",
1048
+ group: "notes",
1049
+ options: [ROOT, INDEX, JSON_OPTION],
1050
+ examples: ["wordcell catalog --root kb"]
1051
+ },
1052
+ {
1053
+ id: "import supermemory",
1054
+ label: "import supermemory <file>",
1055
+ usage: "wordcell import supermemory <export.json>... [options]",
1056
+ summary: "Import a Supermemory export as notes.",
1057
+ group: "notes",
1058
+ options: [
1059
+ ROOT,
1060
+ ["--prefix <directory>", "Put every imported note in this folder"],
1061
+ ["--dry-run", "Show what would be written without writing"],
1062
+ JSON_OPTION
1063
+ ],
1064
+ examples: ["wordcell import supermemory export.json --root kb --dry-run"]
1065
+ },
1066
+ {
1067
+ id: "index",
1068
+ label: "index",
1069
+ usage: "wordcell index [options]",
1070
+ summary: "Build the local index for meaning-based search.",
1071
+ group: "search",
1072
+ notes: ["The first run downloads a local embedding model (about 300 MB) once."],
1073
+ options: [ROOT, DATABASE, ["--force", "Rebuild the index from scratch"], JSON_OPTION],
1074
+ examples: ["wordcell index --root kb"]
1075
+ },
1076
+ {
1077
+ id: "context",
1078
+ label: "context <path>",
1079
+ usage: "wordcell context <repository-path> [options]",
1080
+ summary: "Find notes and AGENTS.md files for a code path.",
1081
+ group: "search",
1082
+ options: [
1083
+ ["--root <vault>", "Knowledge base folder (default: current directory)"],
1084
+ REPO,
1085
+ ["--kind <auto|file|directory>", "Treat the path as a file or a folder"],
1086
+ JSON_OPTION
1087
+ ],
1088
+ examples: ["wordcell context src/index.ts --root kb --repo ."]
1089
+ },
1090
+ {
1091
+ id: "history",
1092
+ label: "history <note>",
1093
+ usage: "wordcell history <note> [options]",
1094
+ summary: "Show the Git commits that changed a note. Also: wordcell history search <query>.",
1095
+ group: "search",
1096
+ options: [
1097
+ ROOT,
1098
+ REPO,
1099
+ ["--limit <count>", "Show at most this many commits"],
1100
+ ["--cochanged-limit <count>", "Show at most this many files changed alongside"],
1101
+ JSON_OPTION
1102
+ ],
1103
+ examples: ["wordcell history notes/first --root kb --repo ."]
1104
+ },
1105
+ {
1106
+ id: "history search",
1107
+ label: "history search <query>",
1108
+ usage: "wordcell history search <query-or-path> [options]",
1109
+ summary: "Search commit messages and changed paths in the repository.",
1110
+ group: "search",
1111
+ unlisted: true,
1112
+ options: [
1113
+ ROOT,
1114
+ REPO,
1115
+ ["--limit <count>", "Show at most this many results"],
1116
+ ["--commit-limit <count>", "Read at most this many commits"],
1117
+ ["--cochanged-limit <count>", "Show at most this many files changed alongside"],
1118
+ JSON_OPTION
1119
+ ],
1120
+ examples: ['wordcell history search "retry" --root kb --repo .']
1121
+ },
1122
+ {
1123
+ id: "inbox",
1124
+ label: "inbox",
1125
+ usage: "wordcell inbox [options]",
1126
+ summary: "List saved sources no note links to yet. Also: wordcell source-inbox.",
1127
+ group: "search",
1128
+ options: [
1129
+ ROOT,
1130
+ INDEX,
1131
+ ["--source-prefix <directory>", "Look for sources in this folder (repeatable)"],
1132
+ ["--limit <count>", "Show at most this many sources"],
1133
+ JSON_OPTION
1134
+ ],
1135
+ examples: ["wordcell inbox --root kb"]
1136
+ },
1137
+ {
1138
+ id: "clip",
1139
+ label: "clip <url|current>",
1140
+ usage: "wordcell clip <url|current> [options]",
1141
+ summary: "Save a web page as Markdown. Threads, videos, and images are kept too.",
1142
+ group: "capture",
1143
+ options: [],
1144
+ examples: ["wordcell clip https://example.com/article --output kb/articles"],
1145
+ notes: ["Run `wordcell clip --help` for every capture option."]
1146
+ },
1147
+ {
1148
+ id: "inspect",
1149
+ label: "inspect <url>",
1150
+ usage: "wordcell inspect <url> [options]",
1151
+ summary: "Preview a capture. It writes no files.",
1152
+ group: "capture",
1153
+ options: [],
1154
+ examples: ["wordcell inspect https://example.com/article"]
1155
+ },
1156
+ {
1157
+ id: "pdf",
1158
+ label: "pdf <file-or-url>",
1159
+ usage: "wordcell pdf <file-or-url> [options]",
1160
+ summary: "Save a PDF as Markdown. The original file and its images are kept.",
1161
+ group: "capture",
1162
+ options: [],
1163
+ examples: ["wordcell pdf paper.pdf --output kb/articles"]
1164
+ },
1165
+ {
1166
+ id: "capture",
1167
+ label: "capture show|verify|diff",
1168
+ usage: "wordcell capture <show|verify|diff> <bundle> [options]",
1169
+ summary: "Read, check, or compare a saved capture.",
1170
+ group: "capture",
1171
+ family: true,
1172
+ options: [],
1173
+ examples: ["wordcell capture verify kb/articles/example"],
1174
+ notes: ["Run `wordcell capture show --help` for each action. `wordcell capture <url>` is the same as clip."]
1175
+ },
1176
+ {
1177
+ id: "capture show",
1178
+ label: "capture show <bundle>",
1179
+ usage: "wordcell capture show <bundle> [options]",
1180
+ summary: "Print a saved capture as plain text.",
1181
+ group: "capture",
1182
+ unlisted: true,
1183
+ options: [
1184
+ ["--verify-assets", "Also check saved images and files"],
1185
+ ["--include-source-html", "Include the original page HTML"],
1186
+ JSON_OPTION
1187
+ ],
1188
+ examples: ["wordcell capture show kb/articles/example"]
1189
+ },
1190
+ {
1191
+ id: "capture verify",
1192
+ label: "capture verify <bundle>",
1193
+ usage: "wordcell capture verify <bundle> [options]",
1194
+ summary: "Check that a saved capture still matches its recorded hashes.",
1195
+ group: "capture",
1196
+ unlisted: true,
1197
+ options: [["--verify-assets", "Also check saved images and files"], JSON_OPTION],
1198
+ examples: ["wordcell capture verify kb/articles/example --verify-assets"]
1199
+ },
1200
+ {
1201
+ id: "capture diff",
1202
+ label: "capture diff <bundle>",
1203
+ usage: "wordcell capture diff <bundle> [options]",
1204
+ summary: "Compare a saved capture with an earlier Git version.",
1205
+ group: "capture",
1206
+ unlisted: true,
1207
+ options: [REPO, ["--ref <ref>", "Git version to compare with (default: HEAD)"], JSON_OPTION],
1208
+ examples: ["wordcell capture diff kb/articles/example --repo . --ref main"]
1209
+ },
1210
+ {
1211
+ id: "publish",
1212
+ label: "publish --out <directory>",
1213
+ usage: "wordcell publish --out <directory> [options]",
1214
+ summary: "Build a static website from your notes.",
1215
+ group: "publish",
1216
+ options: [
1217
+ ["--out <directory>", "Folder to write the site to (required)"],
1218
+ ROOT,
1219
+ INDEX,
1220
+ ["--include <path>", "Publish only these notes or folders (repeatable)"],
1221
+ ["--exclude <path>", "Leave out these notes or folders (repeatable)"],
1222
+ ["--include-glob <pattern>", "Publish notes matching a pattern (repeatable)"],
1223
+ ["--exclude-glob <pattern>", "Leave out notes matching a pattern (repeatable)"],
1224
+ WHERE,
1225
+ HAS,
1226
+ TAG,
1227
+ SCOPE,
1228
+ ["--from <note>", "Publish the notes linked from this note"],
1229
+ DEPTH,
1230
+ DIRECTION,
1231
+ ["--title <title>", "Site title"],
1232
+ ["--description <text>", "Site description"],
1233
+ ["--base-path <path>", "Serve the site under this path"],
1234
+ ["--base-url <url>", "Public address of the site"],
1235
+ ["--noindex", "Ask search engines not to index the site"],
1236
+ ["--no-index-content", "Leave note text out of the site search"],
1237
+ ["--deterministic", "Omit the build time so builds match byte for byte"],
1238
+ ["--dry-run", "Show what would be published without writing"],
1239
+ ["--list-limit <0-1000>", "How many paths to list in the report"],
1240
+ ["--force", "Replace an existing output folder"],
1241
+ JSON_OPTION
1242
+ ],
1243
+ examples: ["wordcell publish --root kb --out site", "wordcell publish --root kb --out site --tag public"]
1244
+ },
1245
+ {
1246
+ id: "serve",
1247
+ label: "serve --root <directory>",
1248
+ usage: "wordcell serve --root <directory> [options]",
1249
+ summary: "Preview a published site on this computer.",
1250
+ group: "publish",
1251
+ options: [
1252
+ ["--root <directory>", "Published site folder (required)"],
1253
+ ["--host <host>", "Address to listen on (default: 127.0.0.1)"],
1254
+ ["--port <port>", "Port to listen on"],
1255
+ JSON_OPTION
1256
+ ],
1257
+ examples: ["wordcell serve --root site --port 8080"]
1258
+ },
1259
+ {
1260
+ id: "mcp",
1261
+ label: "mcp --root <vault>",
1262
+ usage: "wordcell mcp --root <vault> [options]",
1263
+ summary: "Serve your notes to AI apps over MCP.",
1264
+ group: "publish",
1265
+ notes: ["MCP (Model Context Protocol) is how AI apps such as Claude call local tools."],
1266
+ options: [
1267
+ ["--root <vault>", "Knowledge base folder (required)"],
1268
+ REPO,
1269
+ ["--read-only", "Offer only the read tools"]
1270
+ ],
1271
+ examples: ["wordcell mcp --root /absolute/path/to/kb --read-only"]
1272
+ },
1273
+ {
1274
+ id: "agents",
1275
+ label: "agents check|audit",
1276
+ usage: "wordcell agents <check|audit|identity> ...",
1277
+ summary: "Check the notes that give coding agents context for each code folder.",
1278
+ group: "agents",
1279
+ family: true,
1280
+ unlisted: true,
1281
+ options: [],
1282
+ examples: ["wordcell agents check --root kb --repo ."],
1283
+ notes: ["Run `wordcell agents check --help` for each action."]
1284
+ },
1285
+ {
1286
+ id: "agents check",
1287
+ label: "agents check",
1288
+ usage: "wordcell agents check [options]",
1289
+ summary: "Check AGENTS.md context notes against the code.",
1290
+ group: "agents",
1291
+ options: [["--root <vault>", "Knowledge base folder"], REPO, JSON_OPTION],
1292
+ examples: ["wordcell agents check --root kb --repo ."]
1293
+ },
1294
+ {
1295
+ id: "agents audit",
1296
+ label: "agents audit",
1297
+ usage: "wordcell agents audit [options]",
1298
+ summary: "Find long or repeated AGENTS.md rules. It runs agents check first.",
1299
+ group: "agents",
1300
+ options: [["--root <vault>", "Knowledge base folder"], REPO, JSON_OPTION],
1301
+ examples: ["wordcell agents audit --root kb --repo ."]
1302
+ },
1303
+ {
1304
+ id: "doctor",
1305
+ label: "doctor",
1306
+ usage: "wordcell doctor [--json]",
1307
+ summary: "Check optional tools for capture and search. It changes nothing.",
1308
+ group: "diagnostics",
1309
+ options: [JSON_OPTION],
1310
+ examples: ["wordcell doctor"]
1311
+ },
1312
+ {
1313
+ id: "adapters",
1314
+ label: "adapters",
1315
+ usage: "wordcell adapters [--json]",
1316
+ summary: "List the sites and platforms clip supports.",
1317
+ group: "diagnostics",
1318
+ options: [JSON_OPTION],
1319
+ examples: ["wordcell adapters"]
1320
+ },
1321
+ {
1322
+ id: "graph",
1323
+ label: "graph",
1324
+ usage: "wordcell graph [options]",
1325
+ summary: "Show the link graph and broken links. Unlinked notes are listed too.",
1326
+ group: "diagnostics",
1327
+ notes: ["See `wordcell help advanced` for graph rebuild, verify, and query."],
1328
+ options: [ROOT, INDEX, JSON_OPTION],
1329
+ examples: ["wordcell graph --root kb"]
1330
+ },
1331
+ {
1332
+ id: "agents identity",
1333
+ label: "agents identity <scope>",
1334
+ usage: "wordcell agents identity <repository-scope> [--json]",
1335
+ summary: "Print the context note name and marker for a code folder, without writing.",
1336
+ group: "advanced",
1337
+ options: [JSON_OPTION],
1338
+ examples: ["wordcell agents identity packages/parser"]
1339
+ },
1340
+ {
1341
+ id: "graph rebuild",
1342
+ label: "graph rebuild",
1343
+ usage: "wordcell graph rebuild [options]",
1344
+ summary: "Rebuild the local graph cache from the Markdown files.",
1345
+ group: "advanced",
1346
+ options: [["--fresh", "Discard the old cache first"], ROOT, INDEX, JSON_OPTION],
1347
+ examples: ["wordcell graph rebuild --root kb"]
1348
+ },
1349
+ {
1350
+ id: "graph verify",
1351
+ label: "graph verify",
1352
+ usage: "wordcell graph verify [options]",
1353
+ summary: "Check that the local graph cache matches the Markdown files.",
1354
+ group: "advanced",
1355
+ options: [ROOT, INDEX, JSON_OPTION],
1356
+ examples: ["wordcell graph verify --root kb"]
1357
+ },
1358
+ {
1359
+ id: "graph query",
1360
+ label: "graph query --program <name>",
1361
+ usage: "wordcell graph query --program <name> [options]",
1362
+ summary: "Run a built-in graph query, with proof of each answer.",
1363
+ group: "advanced",
1364
+ options: [
1365
+ ["--program <name>", "backlinks, reachability, scope-route, relation-closure, shared-tags, or shared-concepts"],
1366
+ ["--note <id>", "Start from this note"],
1367
+ ["--scope <path>", "Start from this code path"],
1368
+ ["--predicate <predicate>", "Follow only this typed link"],
1369
+ DEPTH,
1370
+ ["--limit <count>", "Show at most this many results"],
1371
+ ["--persisted", "Read the saved graph cache"],
1372
+ ROOT,
1373
+ INDEX,
1374
+ JSON_OPTION
1375
+ ],
1376
+ examples: ["wordcell graph query --program backlinks --note notes/first --root kb"]
1377
+ },
1378
+ {
1379
+ id: "percolate",
1380
+ label: "percolate [note]",
1381
+ usage: "wordcell percolate [note] [options]",
1382
+ summary: "Suggest recurring concepts and missing links, with evidence. Writes nothing.",
1383
+ group: "advanced",
1384
+ options: [
1385
+ ["--proofs", "Show the evidence for one note"],
1386
+ ["--min-support <count>", "Require this many supporting notes"],
1387
+ ["--limit <count>", "Show at most this many suggestions"],
1388
+ ROOT,
1389
+ JSON_OPTION
1390
+ ],
1391
+ examples: ["wordcell percolate --root kb"]
1392
+ },
1393
+ {
1394
+ id: "portfolio search",
1395
+ label: "portfolio search <query>",
1396
+ usage: "wordcell portfolio search <query> --registry <file> --workspace <directory> [options]",
1397
+ summary: "Search several knowledge bases you list in a registry file.",
1398
+ group: "advanced",
1399
+ options: [
1400
+ ["--registry <file>", "Registry of knowledge bases (required)"],
1401
+ ["--workspace <directory>", "Folder that holds their checkouts (required)"],
1402
+ ["--shared", "Search every shared entry"],
1403
+ ["--vault <owner/id>", "Search this entry (repeatable)"],
1404
+ MODE,
1405
+ RULES,
1406
+ PRIORITY,
1407
+ ["--limit <count>", "Show at most this many results"],
1408
+ ["--require-all", "Fail when any selected knowledge base is missing"],
1409
+ JSON_OPTION
1410
+ ],
1411
+ examples: ['wordcell portfolio search "retries" --registry kb-portfolio.json --workspace .. --shared']
1412
+ },
1413
+ {
1414
+ id: "portfolio audit",
1415
+ label: "portfolio audit",
1416
+ usage: "wordcell portfolio audit --registry <file> --workspace <directory> [options]",
1417
+ summary: "Check several knowledge bases for broken links, duplicates, and missing files.",
1418
+ group: "advanced",
1419
+ options: [
1420
+ ["--registry <file>", "Registry of knowledge bases (required)"],
1421
+ ["--workspace <directory>", "Folder that holds their checkouts (required)"],
1422
+ ["--all", "Audit every entry"],
1423
+ ["--shared", "Audit every shared entry"],
1424
+ ["--vault <owner/id>", "Audit this entry (repeatable)"],
1425
+ ["--strict", "Fail on warnings"],
1426
+ JSON_OPTION
1427
+ ],
1428
+ examples: ["wordcell portfolio audit --registry kb-portfolio.json --workspace .. --all"]
1429
+ },
1430
+ {
1431
+ id: "evaluate",
1432
+ label: "evaluate <manifest.json>",
1433
+ usage: "wordcell evaluate <manifest.json> [options]",
1434
+ summary: "Measure how well each search method finds the right notes in a frozen test set.",
1435
+ group: "advanced",
1436
+ options: [
1437
+ ROOT,
1438
+ REPO,
1439
+ DATABASE,
1440
+ ["--retriever <id>", "Run only this search method (repeatable)"],
1441
+ ["--split <development|test|all>", "Which questions to run"],
1442
+ ["--limit <count>", "Results to keep per question"],
1443
+ ["--cutoff <count>", "Rank cutoff for the scores"],
1444
+ ["--timeout <milliseconds>", "Time limit per question"],
1445
+ ["--baseline <id>", "Compare every method with this one"],
1446
+ ["--model-file <path>", "Record the embedding model file used"],
1447
+ ["--cache-state <cold|mixed|warm>", "Record how warm the caches were"],
1448
+ JSON_OPTION
1449
+ ],
1450
+ examples: ["wordcell evaluate eval/manifest.json --root kb --repo ."]
1451
+ },
1452
+ {
1453
+ id: "url-metadata tool",
1454
+ label: "url-metadata tool build|check",
1455
+ usage: "wordcell url-metadata tool <build|check>",
1456
+ summary: "Build or check the helper that looks up metadata for saved links. Needs Rust.",
1457
+ group: "advanced",
1458
+ options: [],
1459
+ examples: ["wordcell url-metadata tool build"]
1460
+ },
1461
+ {
1462
+ id: "url-metadata backfill",
1463
+ label: "url-metadata backfill",
1464
+ usage: "wordcell url-metadata backfill [options]",
1465
+ summary: "Add title and archive metadata files next to saved links.",
1466
+ group: "advanced",
1467
+ options: [],
1468
+ examples: ["wordcell url-metadata backfill --root kb"],
1469
+ notes: ["Run `wordcell url-metadata --help` for every option."]
1470
+ },
1471
+ {
1472
+ id: "support",
1473
+ label: "support",
1474
+ usage: "wordcell support [status | enable | dismiss | snooze] [--json]",
1475
+ summary: "Optional support settings. No command signs you up or charges you.",
1476
+ group: "advanced",
1477
+ notes: ["Set HRANESS_SUPPORT=off to turn off support invitations."],
1478
+ options: [JSON_OPTION],
1479
+ examples: ["wordcell support status"]
1480
+ }
1481
+ ];
1482
+ var COMMAND_ALIASES = {
1483
+ notes: "list",
1484
+ "source-inbox": "inbox"
1485
+ };
1486
+ var byId = new Map(commandHelp.map((entry) => [entry.id, entry]));
1487
+ var GROUP_TITLES = [
1488
+ ["start", "Start here"],
1489
+ ["notes", "Notes and links"],
1490
+ ["search", "Search"],
1491
+ ["capture", "Capture"],
1492
+ ["publish", "Publish and serve"],
1493
+ ["agents", "Agents"],
1494
+ ["diagnostics", "Diagnostics"]
1495
+ ];
1496
+ var DESCRIPTION = [
1497
+ "Wordcell keeps decisions, plans, and sources as Markdown beside your code,",
1498
+ "so coding agents can find them from the file they are about to change."
1499
+ ];
1500
+ var LABEL_WIDTH = 28;
1501
+ var MAX_COLUMNS = 80;
1502
+ function wrapWords(text, width) {
1503
+ const lines = [];
1504
+ let current = "";
1505
+ for (const word of text.split(" ")) {
1506
+ if (current !== "" && current.length + 1 + word.length > width) {
1507
+ lines.push(current);
1508
+ current = word;
1509
+ } else {
1510
+ current = current === "" ? word : `${current} ${word}`;
1511
+ }
1512
+ }
1513
+ if (current !== "")
1514
+ lines.push(current);
1515
+ return lines;
1516
+ }
1517
+ function row(label, summary, indent = " ", width = LABEL_WIDTH) {
1518
+ const column = indent.length + width;
1519
+ const padded = `${indent}${label}`;
1520
+ const summaryLines = wrapWords(summary, Math.max(20, MAX_COLUMNS - column));
1521
+ const continuation = summaryLines.slice(1).map((line) => `${" ".repeat(column)}${line}`);
1522
+ if (label === "")
1523
+ return [`${" ".repeat(column)}${summaryLines[0] ?? ""}`, ...continuation].join(`
1524
+ `);
1525
+ const head = padded.length < column ? [`${padded.padEnd(column)}${summaryLines[0] ?? ""}`] : [padded, `${" ".repeat(column)}${summaryLines[0] ?? ""}`];
1526
+ return [...head, ...continuation].join(`
1527
+ `);
1528
+ }
1529
+ function firstSentence(summary) {
1530
+ const end = summary.search(/\.(\s|$)/u);
1531
+ return end === -1 ? summary : summary.slice(0, end);
1532
+ }
1533
+ function listGroup(group) {
1534
+ return commandHelp.filter((entry) => entry.group === group && entry.unlisted !== true).map((entry) => row(entry.label, firstSentence(entry.summary)));
1535
+ }
1536
+ function startHelp(version) {
1537
+ const start = (command, summary) => row(command, summary, " ", 40);
1538
+ const lines = [
1539
+ ...DESCRIPTION,
1540
+ "",
1541
+ "Start here (no account or model needed)",
1542
+ start("wordcell init kb", "Create a knowledge base in ./kb"),
1543
+ start('wordcell note create notes/first --title "First note" --root kb', "Write your first note"),
1544
+ start('wordcell search "first" --root kb', "Search your notes"),
1545
+ start("wordcell check --root kb", "Check links and metadata"),
1546
+ "",
1547
+ "Everyday",
1548
+ start("wordcell clip <url> --output kb/articles", "Save a web page as Markdown"),
1549
+ start("wordcell context <path> --root kb", "Find notes about a code path"),
1550
+ start("wordcell index --root kb", "Turn on meaning-based search"),
1551
+ "",
1552
+ "All commands: wordcell --help \xB7 Command help: wordcell help <command>"
1553
+ ];
1554
+ if (version !== undefined)
1555
+ lines.push(`wordcell ${version}`);
1556
+ return `${lines.join(`
1557
+ `)}
1558
+ `;
1559
+ }
1560
+ function rootHelp() {
1561
+ const lines = ["Usage: wordcell <command> [options]", "", ...DESCRIPTION];
1562
+ for (const [group, title] of GROUP_TITLES) {
1563
+ lines.push("", title, ...listGroup(group));
1564
+ }
1565
+ lines.push("", "Options", row("-h, --help", "Show help. Also: wordcell help <command>", " ", 22), row("-V, --version", "Print the version", " ", 22), row("--root <directory>", "Knowledge base folder (default: current directory)", " ", 22), row("--json", "Print machine-readable output", " ", 22), "", "More commands: wordcell help advanced \xB7 Docs: https://wordcell.io/docs", "Optional support: wordcell support \xB7 Turn off: HRANESS_SUPPORT=off");
1566
+ return `${lines.join(`
1567
+ `)}
1568
+ `;
1569
+ }
1570
+ function advancedHelp() {
1571
+ const lines = [
1572
+ "Usage: wordcell <command> [options]",
1573
+ "",
1574
+ "Advanced commands for maintainers, evaluation, and several knowledge bases.",
1575
+ "",
1576
+ ...listGroup("advanced"),
1577
+ "",
1578
+ "Command help: wordcell help <command> \xB7 Everyday commands: wordcell --help"
1579
+ ];
1580
+ return `${lines.join(`
1581
+ `)}
1582
+ `;
1583
+ }
1584
+ function commandHelpText(id) {
1585
+ const entry = byId.get(id);
1586
+ if (entry === undefined)
1587
+ return;
1588
+ const lines = [`Usage: ${entry.usage}`, "", ...wrapWords(entry.summary, MAX_COLUMNS)];
1589
+ if (entry.notes !== undefined)
1590
+ lines.push(...entry.notes);
1591
+ const family = commandHelp.filter((candidate) => candidate.id.startsWith(`${entry.id} `));
1592
+ if (family.length > 0) {
1593
+ lines.push("", "Commands", ...family.map((candidate) => row(`${candidate.label}`, firstSentence(candidate.summary))));
1594
+ }
1595
+ if (entry.options.length > 0) {
1596
+ const width = Math.min(30, Math.max(...entry.options.map(([spelling]) => spelling.length)) + 2);
1597
+ lines.push("", "Options", ...entry.options.map(([spelling, description]) => row(spelling, description, " ", width)));
1598
+ }
1599
+ if (entry.examples.length > 0) {
1600
+ lines.push("", entry.examples.length === 1 ? "Example" : "Examples", ...entry.examples.map((example) => ` ${example}`));
1601
+ }
1602
+ return `${lines.join(`
1603
+ `)}
1604
+ `;
1605
+ }
1606
+ function canonicalFirstWord(word) {
1607
+ return COMMAND_ALIASES[word] ?? word;
1608
+ }
1609
+ function resolveCommandId(words) {
1610
+ const first = words[0];
1611
+ if (first === undefined)
1612
+ return;
1613
+ const head = canonicalFirstWord(first);
1614
+ const second = words[1];
1615
+ if (second !== undefined && byId.has(`${head} ${second}`))
1616
+ return `${head} ${second}`;
1617
+ if (byId.has(head))
1618
+ return head;
1619
+ if (head === "note")
1620
+ return "note create";
1621
+ if (head === "import")
1622
+ return "import supermemory";
1623
+ if (head === "portfolio")
1624
+ return "portfolio search";
1625
+ if (head === "url-metadata")
1626
+ return "url-metadata backfill";
1627
+ return;
1628
+ }
1629
+ function commandWordCount(id) {
1630
+ return id.split(" ").length;
1631
+ }
1632
+ function knownCommandWords() {
1633
+ const words = new Set;
1634
+ for (const entry of commandHelp)
1635
+ words.add(entry.id.split(" ")[0] ?? entry.id);
1636
+ for (const alias of Object.keys(COMMAND_ALIASES))
1637
+ words.add(alias);
1638
+ for (const word of ["help", "version", "note", "import", "portfolio", "url-metadata"])
1639
+ words.add(word);
1640
+ return [...words].toSorted();
1641
+ }
1642
+ function valueOptions(id) {
1643
+ const entry = byId.get(id);
1644
+ const names = new Set;
1645
+ for (const [spelling] of entry?.options ?? []) {
1646
+ if (spelling.includes("<"))
1647
+ names.add(spelling.split(" ")[0] ?? spelling);
1648
+ }
1649
+ return names;
1650
+ }
1651
+ function optionNames(id) {
1652
+ const entry = byId.get(id);
1653
+ return (entry?.options ?? []).map(([spelling]) => spelling.split(" ")[0] ?? spelling);
1654
+ }
1655
+ function editDistance(left, right) {
1656
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
1657
+ for (let row_ = 1;row_ <= left.length; row_ += 1) {
1658
+ let diagonal = previous[0] ?? 0;
1659
+ previous[0] = row_;
1660
+ for (let column = 1;column <= right.length; column += 1) {
1661
+ const above = previous[column] ?? 0;
1662
+ const cost = left[row_ - 1] === right[column - 1] ? 0 : 1;
1663
+ previous[column] = Math.min(above + 1, (previous[column - 1] ?? 0) + 1, diagonal + cost);
1664
+ diagonal = above;
1665
+ }
1666
+ }
1667
+ return previous[right.length] ?? Number.MAX_SAFE_INTEGER;
1668
+ }
1669
+ function closestMatch(input, candidates) {
1670
+ const letters = input.replace(/^-+/u, "").length;
1671
+ let best;
1672
+ let bestDistance = Math.max(1, Math.floor(letters / 3)) + 1;
1673
+ for (const candidate of candidates) {
1674
+ const distance = editDistance(input, candidate);
1675
+ if (distance < bestDistance) {
1676
+ best = candidate;
1677
+ bestDistance = distance;
1678
+ }
1679
+ }
1680
+ return best;
1681
+ }
1682
+
818
1683
  // src/cli-program.ts
819
- import { open as open2 } from "fs/promises";
1684
+ import { open as open3, realpath as realpath2, stat as stat2 } from "fs/promises";
820
1685
  import { cpus, release, totalmem } from "os";
821
1686
  import { relative, resolve as resolve3 } from "path";
822
- import { format } from "util";
1687
+ import { format as format2 } from "util";
823
1688
 
824
1689
  // src/graph-cli.ts
825
1690
  function parseGraphCommand(arguments_) {
@@ -902,7 +1767,7 @@ async function executeGraphCommand(command, dependencies = {}) {
902
1767
  `Snapshot: ${value2.revision}`,
903
1768
  "Proofs and exact source record digests are included in --json output.",
904
1769
  value2.columns.join("\t"),
905
- ...value2.rows.map((row) => row.values.map((atom) => JSON.stringify(atom)).join("\t"))
1770
+ ...value2.rows.map((row2) => row2.values.map((atom) => JSON.stringify(atom)).join("\t"))
906
1771
  ];
907
1772
  if (value2.truncated || value2.proofsTruncated) {
908
1773
  lines.push(`Partial result: ${value2.truncationReasons.join(", ") || "proof evidence was truncated"}.`);
@@ -1031,45 +1896,2465 @@ async function createServeHandler(root) {
1031
1896
  "Content-Length": String(body.size),
1032
1897
  "Content-Type": "text/html; charset=utf-8"
1033
1898
  }
1034
- });
1899
+ });
1900
+ }
1901
+ return new Response(head ? null : "Not found", {
1902
+ status: 404,
1903
+ headers: { "Cache-Control": "no-store" }
1904
+ });
1905
+ };
1906
+ }
1907
+ async function serveSite(options, io = {}) {
1908
+ const host = options.host ?? "127.0.0.1";
1909
+ const port = options.port ?? 8080;
1910
+ if (!Number.isSafeInteger(port) || port < 0 || port > 65535) {
1911
+ throw new Error("Port must be an integer from 0 through 65535");
1912
+ }
1913
+ const rootReal = await realpath(options.root);
1914
+ if (!(await stat(rootReal)).isDirectory()) {
1915
+ throw new Error(`Site root is not a directory: ${options.root}`);
1916
+ }
1917
+ const listen = io.listen ?? ((init) => Bun.serve(init));
1918
+ const server = listen({
1919
+ hostname: host,
1920
+ port,
1921
+ fetch: await createServeHandler(rootReal)
1922
+ });
1923
+ const boundPort = server.port ?? port;
1924
+ return Object.freeze({
1925
+ root: rootReal,
1926
+ host,
1927
+ port: boundPort,
1928
+ url: `http://${host}:${String(boundPort)}/`,
1929
+ close: () => server.stop()
1930
+ });
1931
+ }
1932
+
1933
+ // src/mcp-server.ts
1934
+ import { readFile } from "fs/promises";
1935
+ import { format } from "util";
1936
+ var LATEST_PROTOCOL_VERSION = "2025-11-25";
1937
+ var SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18"];
1938
+ var MAX_FRAME_BYTES = 1048576;
1939
+ var SERVER_NAME = "hraness-wordcell";
1940
+ var PARSE_ERROR = -32700;
1941
+ var INVALID_REQUEST = -32600;
1942
+ var METHOD_NOT_FOUND = -32601;
1943
+ var INVALID_PARAMS = -32602;
1944
+ var INTERNAL_ERROR = -32603;
1945
+ function negotiateProtocolVersion(requested) {
1946
+ const match = SUPPORTED_PROTOCOL_VERSIONS.find((version) => version === requested);
1947
+ return match ?? LATEST_PROTOCOL_VERSION;
1948
+ }
1949
+ function isRequestId(value) {
1950
+ return typeof value === "string" || typeof value === "number" && Number.isSafeInteger(value);
1951
+ }
1952
+ function isRecord(value) {
1953
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1954
+ }
1955
+ function invalid(id, message) {
1956
+ return { kind: "invalid", id, code: INVALID_REQUEST, message };
1957
+ }
1958
+ function parseFrame(text) {
1959
+ let value;
1960
+ try {
1961
+ value = JSON.parse(text);
1962
+ } catch {
1963
+ return { kind: "invalid", id: null, code: PARSE_ERROR, message: "Frame must be valid JSON." };
1964
+ }
1965
+ if (Array.isArray(value))
1966
+ return invalid(null, "Batch requests are not supported.");
1967
+ if (!isRecord(value))
1968
+ return invalid(null, "Expected a JSON-RPC object.");
1969
+ const hasMethod = Object.hasOwn(value, "method");
1970
+ if (!hasMethod && (Object.hasOwn(value, "result") || Object.hasOwn(value, "error"))) {
1971
+ return { kind: "response" };
1972
+ }
1973
+ const hasId = Object.hasOwn(value, "id");
1974
+ const id = hasId && isRequestId(value.id) ? value.id : null;
1975
+ if (value.jsonrpc !== "2.0")
1976
+ return invalid(id, 'Expected jsonrpc "2.0".');
1977
+ if (hasId && id === null)
1978
+ return invalid(null, "Request id must be a string or a safe integer.");
1979
+ if (!hasMethod)
1980
+ return invalid(id, "Missing method.");
1981
+ if (typeof value.method !== "string")
1982
+ return invalid(id, "Method must be a string.");
1983
+ const params = value.params;
1984
+ if (params !== undefined && !isRecord(params))
1985
+ return invalid(id, "Params must be an object.");
1986
+ if (id === null)
1987
+ return { kind: "notification", method: value.method, params };
1988
+ return { kind: "request", id, method: value.method, params };
1989
+ }
1990
+ function frameFromLine(event, maxFrameBytes = MAX_FRAME_BYTES) {
1991
+ if (event.kind === "line")
1992
+ return parseFrame(event.text);
1993
+ if (event.kind === "oversized")
1994
+ return invalid(null, `Frame exceeds ${maxFrameBytes} bytes.`);
1995
+ return { kind: "invalid", id: null, code: PARSE_ERROR, message: "Frame must be valid UTF-8." };
1996
+ }
1997
+ var NEWLINE = 10;
1998
+ var CARRIAGE_RETURN = 13;
1999
+ async function readFrames(input, onFrame, options = {}) {
2000
+ const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
2001
+ if (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes < 1) {
2002
+ throw new RangeError("maxFrameBytes must be a positive safe integer.");
2003
+ }
2004
+ let pieces = [];
2005
+ let length = 0;
2006
+ let oversized = false;
2007
+ const append = (piece) => {
2008
+ if (oversized || piece.length === 0)
2009
+ return;
2010
+ length += piece.length;
2011
+ if (length > maxFrameBytes) {
2012
+ oversized = true;
2013
+ pieces = [];
2014
+ return;
2015
+ }
2016
+ pieces.push(piece);
2017
+ };
2018
+ const finish = async () => {
2019
+ const wasOversized = oversized;
2020
+ const line = concat(pieces, length);
2021
+ pieces = [];
2022
+ length = 0;
2023
+ oversized = false;
2024
+ if (wasOversized)
2025
+ return onFrame({ kind: "oversized" });
2026
+ const end = line.length > 0 && line[line.length - 1] === CARRIAGE_RETURN ? line.length - 1 : line.length;
2027
+ let text;
2028
+ try {
2029
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(line.subarray(0, end));
2030
+ } catch {
2031
+ return onFrame({ kind: "unparseable" });
2032
+ }
2033
+ if (text.trim() === "")
2034
+ return;
2035
+ return onFrame({ kind: "line", text });
2036
+ };
2037
+ for await (const chunk of input) {
2038
+ let start = 0;
2039
+ for (let index = chunk.indexOf(NEWLINE);index !== -1; index = chunk.indexOf(NEWLINE, start)) {
2040
+ append(chunk.subarray(start, index));
2041
+ await finish();
2042
+ start = index + 1;
2043
+ }
2044
+ append(chunk.slice(start));
2045
+ }
2046
+ if (length > 0 || oversized)
2047
+ await finish();
2048
+ }
2049
+ function concat(pieces, length) {
2050
+ if (pieces.length === 1)
2051
+ return pieces[0];
2052
+ const out = new Uint8Array(length);
2053
+ let offset = 0;
2054
+ for (const piece of pieces) {
2055
+ out.set(piece, offset);
2056
+ offset += piece.length;
2057
+ }
2058
+ return out;
2059
+ }
2060
+ function errorResponse(id, code, message) {
2061
+ return { jsonrpc: "2.0", id, error: { code, message } };
2062
+ }
2063
+ function errorMessage(error) {
2064
+ return error instanceof Error ? error.message : String(error);
2065
+ }
2066
+ function createDispatcher(options) {
2067
+ const { catalog, instructions, version } = options;
2068
+ const listed = new Set(catalog.tools.map((tool) => tool.name));
2069
+ const request = async (id, method, params) => {
2070
+ switch (method) {
2071
+ case "initialize":
2072
+ return {
2073
+ jsonrpc: "2.0",
2074
+ id,
2075
+ result: {
2076
+ protocolVersion: negotiateProtocolVersion(params?.protocolVersion),
2077
+ capabilities: { tools: { listChanged: false } },
2078
+ serverInfo: { name: SERVER_NAME, version },
2079
+ instructions
2080
+ }
2081
+ };
2082
+ case "ping":
2083
+ return { jsonrpc: "2.0", id, result: {} };
2084
+ case "tools/list":
2085
+ return { jsonrpc: "2.0", id, result: { tools: catalog.tools } };
2086
+ case "tools/call": {
2087
+ const name = params?.name;
2088
+ const args = params !== undefined && Object.hasOwn(params, "arguments") ? params.arguments : {};
2089
+ if (typeof name !== "string" || !isRecord(args)) {
2090
+ return errorResponse(id, INVALID_PARAMS, "Invalid tools/call params.");
2091
+ }
2092
+ if (!listed.has(name))
2093
+ return errorResponse(id, INVALID_PARAMS, `Unknown tool: ${name}`);
2094
+ return { jsonrpc: "2.0", id, result: await catalog.call(name, args) };
2095
+ }
2096
+ default:
2097
+ return errorResponse(id, METHOD_NOT_FOUND, `Method ${method} is not supported.`);
2098
+ }
2099
+ };
2100
+ return {
2101
+ async handle(frame) {
2102
+ if (frame.kind === "invalid")
2103
+ return errorResponse(frame.id, frame.code, frame.message);
2104
+ if (frame.kind !== "request")
2105
+ return null;
2106
+ try {
2107
+ return await request(frame.id, frame.method, frame.params);
2108
+ } catch (error) {
2109
+ return errorResponse(frame.id, INTERNAL_ERROR, `Internal error: ${errorMessage(error)}`);
2110
+ }
2111
+ }
2112
+ };
2113
+ }
2114
+ function encodeResponse(response) {
2115
+ try {
2116
+ const text = JSON.stringify(response);
2117
+ if (typeof text === "string")
2118
+ return `${text}
2119
+ `;
2120
+ throw new TypeError("response is not serializable");
2121
+ } catch (error) {
2122
+ return `${JSON.stringify(errorResponse(response.id, INTERNAL_ERROR, `Internal error: ${errorMessage(error)}`))}
2123
+ `;
2124
+ }
2125
+ }
2126
+ var STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
2127
+ function parseStableVersion(value) {
2128
+ const match = typeof value === "string" ? STABLE_VERSION.exec(value) : null;
2129
+ if (match === null || match.slice(1).some((part) => !Number.isSafeInteger(Number(part)))) {
2130
+ throw new TypeError("package.json version must be a stable x.y.z version.");
2131
+ }
2132
+ return match[0];
2133
+ }
2134
+ async function serverVersion(packageJson = new URL("../package.json", import.meta.url)) {
2135
+ const manifest = JSON.parse(await readFile(packageJson, "utf8"));
2136
+ return parseStableVersion(isRecord(manifest) ? manifest.version : undefined);
2137
+ }
2138
+ function guardStdout() {
2139
+ const stdout = process.stdout;
2140
+ const ownWrite = Object.getOwnPropertyDescriptor(stdout, "write");
2141
+ const rawStdoutWrite = stdout.write.bind(stdout);
2142
+ const rawStderrWrite = process.stderr.write.bind(process.stderr);
2143
+ const originalConsole = { log: console.log, info: console.info, debug: console.debug };
2144
+ let streamError;
2145
+ const onError = (error) => {
2146
+ streamError ??= error;
2147
+ };
2148
+ const restore = () => {
2149
+ if (ownWrite === undefined)
2150
+ Reflect.deleteProperty(stdout, "write");
2151
+ else
2152
+ Object.defineProperty(stdout, "write", ownWrite);
2153
+ console.log = originalConsole.log;
2154
+ console.info = originalConsole.info;
2155
+ console.debug = originalConsole.debug;
2156
+ stdout.off("error", onError);
2157
+ };
2158
+ const redirectedWrite = (...arguments_) => {
2159
+ Reflect.apply(rawStderrWrite, process.stderr, arguments_);
2160
+ return true;
2161
+ };
2162
+ const redirectedConsole = (...arguments_) => {
2163
+ rawStderrWrite(`${format(...arguments_)}
2164
+ `);
2165
+ };
2166
+ try {
2167
+ stdout.on("error", onError);
2168
+ Object.defineProperty(stdout, "write", { configurable: true, writable: true, value: redirectedWrite });
2169
+ console.log = redirectedConsole;
2170
+ console.info = redirectedConsole;
2171
+ console.debug = redirectedConsole;
2172
+ } catch (error) {
2173
+ restore();
2174
+ throw error;
2175
+ }
2176
+ return {
2177
+ writeFrame: (line) => new Promise((resolve3, reject) => {
2178
+ if (streamError !== undefined)
2179
+ return reject(streamError);
2180
+ rawStdoutWrite(line, (error) => {
2181
+ const failure = error ?? streamError;
2182
+ if (failure)
2183
+ reject(failure);
2184
+ else
2185
+ resolve3();
2186
+ });
2187
+ }),
2188
+ restore
2189
+ };
2190
+ }
2191
+ async function runMcpServer(options) {
2192
+ const dispatcher = createDispatcher(options);
2193
+ const maxFrameBytes = options.maxFrameBytes ?? MAX_FRAME_BYTES;
2194
+ let code = 0;
2195
+ try {
2196
+ await readFrames(options.input, async (event) => {
2197
+ const response = await dispatcher.handle(frameFromLine(event, maxFrameBytes));
2198
+ if (response !== null)
2199
+ await options.writeFrame(encodeResponse(response));
2200
+ }, { maxFrameBytes });
2201
+ } catch (error) {
2202
+ options.stderr(`error: MCP transport failed: ${errorMessage(error).replaceAll(`
2203
+ `, " ")}
2204
+ `);
2205
+ code = 1;
2206
+ }
2207
+ try {
2208
+ await options.catalog.close();
2209
+ } catch (error) {
2210
+ options.stderr(`error: MCP session close failed: ${errorMessage(error).replaceAll(`
2211
+ `, " ")}
2212
+ `);
2213
+ code = 1;
2214
+ }
2215
+ return code;
2216
+ }
2217
+
2218
+ // src/import-supermemory.ts
2219
+ import { lstat, mkdir, open, readdir, readFile as readFile2 } from "fs/promises";
2220
+ import { basename as basename2, join as join3 } from "path";
2221
+
2222
+ // src/authoring-import.ts
2223
+ async function createNoteWithFields(root, input, options, fields) {
2224
+ return runAuthoring(createNoteProgram(nativeAuthoringPlatform, root, input, options, fields));
2225
+ }
2226
+ async function updateNoteBodyWithFields(root, id, body, options, fields) {
2227
+ return runAuthoring(updateNoteBodyProgram(nativeAuthoringPlatform, root, id, body, options, fields));
2228
+ }
2229
+
2230
+ // src/import-supermemory.ts
2231
+ var MAX_IMPORT_FILE_BYTES = 128 * 1024 * 1024;
2232
+ var MAX_IMPORT_FILES = 1000;
2233
+ var MAX_IMPORT_ITEMS = MAX_SCANNED_NOTES;
2234
+ var MAX_METADATA_KEYS = 64;
2235
+ var MAX_METADATA_VALUE_CHARS = 1024;
2236
+ var MAX_TITLE_CHARS = 200;
2237
+ var MAX_JSON_DEPTH = 32;
2238
+ var MAX_CONTAINER_TAGS = 64;
2239
+ var MAX_SOURCE_DOCUMENT_IDS = 256;
2240
+ var MAX_CUSTOM_ID_CHARS = 255;
2241
+ var MAX_FIELD_CHARS = 2048;
2242
+ var MAX_TITLE_SOURCE_CHARS = 65536;
2243
+ var READ_CHUNK_BYTES = 1024 * 1024;
2244
+ var MAX_SLUG_ATTEMPTS = 25000;
2245
+ var EXTERNAL_ID = /^[A-Za-z0-9_-]{1,128}$/u;
2246
+ var CONTAINER_TAG = /^[a-zA-Z0-9_:-]{1,100}$/u;
2247
+ var ENUM_VALUE = /^[a-z][a-z0-9_]{0,63}$/u;
2248
+ var METADATA_KEY = /^[A-Za-z0-9_.:-]{1,64}$/u;
2249
+ var LONE_SURROGATE = /\p{Cs}/u;
2250
+ var CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/u;
2251
+ var STRICT_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/u;
2252
+ var SUPERMEMORY_DOCUMENT_TYPES = [
2253
+ "text",
2254
+ "pdf",
2255
+ "tweet",
2256
+ "google_doc",
2257
+ "google_slide",
2258
+ "google_sheet",
2259
+ "image",
2260
+ "video",
2261
+ "audio",
2262
+ "notion_doc",
2263
+ "webpage",
2264
+ "onedrive",
2265
+ "github_markdown",
2266
+ "granola"
2267
+ ];
2268
+ var SUPERMEMORY_STATUSES = [
2269
+ "unknown",
2270
+ "queued",
2271
+ "extracting",
2272
+ "chunking",
2273
+ "embedding",
2274
+ "indexing",
2275
+ "done",
2276
+ "failed"
2277
+ ];
2278
+
2279
+ class ImportFileError extends Error {
2280
+ file;
2281
+ pointer;
2282
+ constructor(file, pointer, reason, options) {
2283
+ super(pointer === "" ? `${file}: ${reason}` : `${file}#${pointer}: ${reason}`, options);
2284
+ this.name = "ImportFileError";
2285
+ this.file = file;
2286
+ this.pointer = pointer;
2287
+ }
2288
+ }
2289
+ function isRecord2(value) {
2290
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2291
+ }
2292
+ function pointerSegment(segment) {
2293
+ return `/${String(segment).replaceAll("~", "~0").replaceAll("/", "~1")}`;
2294
+ }
2295
+ function errorMessage2(error) {
2296
+ return error instanceof Error ? error.message : String(error);
2297
+ }
2298
+ async function readExportFile(path, file, maximumBytes = MAX_IMPORT_FILE_BYTES) {
2299
+ let handle;
2300
+ try {
2301
+ handle = await open(path, "r");
2302
+ } catch (error) {
2303
+ throw new ImportFileError(file, "", `cannot read export file (${errorMessage2(error)})`, { cause: error });
2304
+ }
2305
+ try {
2306
+ const chunks = [];
2307
+ let total = 0;
2308
+ for (;; ) {
2309
+ const chunk = new Uint8Array(Math.min(READ_CHUNK_BYTES, maximumBytes + 1 - total));
2310
+ let bytesRead;
2311
+ try {
2312
+ ({ bytesRead } = await handle.read(chunk, 0, chunk.byteLength, null));
2313
+ } catch (error) {
2314
+ throw new ImportFileError(file, "", `cannot read export file (${errorMessage2(error)})`, { cause: error });
2315
+ }
2316
+ if (bytesRead === 0)
2317
+ break;
2318
+ total += bytesRead;
2319
+ if (total > maximumBytes) {
2320
+ throw new ImportFileError(file, "", `export file exceeds the ${maximumBytes}-byte limit`);
2321
+ }
2322
+ chunks.push(chunk.subarray(0, bytesRead));
2323
+ }
2324
+ try {
2325
+ return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total));
2326
+ } catch (error) {
2327
+ throw new ImportFileError(file, "", "export file is not valid UTF-8", { cause: error });
2328
+ }
2329
+ } finally {
2330
+ await handle.close();
2331
+ }
2332
+ }
2333
+ function assertJsonDepth(text, file) {
2334
+ let depth = 0;
2335
+ let inString = false;
2336
+ let escaped = false;
2337
+ for (let index = 0;index < text.length; index += 1) {
2338
+ const code = text.charCodeAt(index);
2339
+ if (inString) {
2340
+ if (escaped)
2341
+ escaped = false;
2342
+ else if (code === 92)
2343
+ escaped = true;
2344
+ else if (code === 34)
2345
+ inString = false;
2346
+ continue;
2347
+ }
2348
+ if (code === 34)
2349
+ inString = true;
2350
+ else if (code === 91 || code === 123) {
2351
+ depth += 1;
2352
+ if (depth > MAX_JSON_DEPTH) {
2353
+ throw new ImportFileError(file, "", `JSON nesting exceeds ${MAX_JSON_DEPTH} levels`);
2354
+ }
2355
+ } else if (code === 93 || code === 125)
2356
+ depth -= 1;
2357
+ }
2358
+ }
2359
+ var PAGE_KEYS = ["memories", "memoryEntries", "documents"];
2360
+ function isPage(value) {
2361
+ return isRecord2(value) && PAGE_KEYS.some((key) => Array.isArray(value[key]));
2362
+ }
2363
+ function bareItemKind(value) {
2364
+ return isRecord2(value) && typeof value.memory === "string" && typeof value.version === "number" ? "memory" : "document";
2365
+ }
2366
+ function parseExportText(text, file) {
2367
+ const source = text.startsWith("\uFEFF") ? text.slice(1) : text;
2368
+ assertJsonDepth(source, file);
2369
+ let root;
2370
+ try {
2371
+ root = JSON.parse(source);
2372
+ } catch (error) {
2373
+ throw new ImportFileError(file, "", `not valid JSON (${errorMessage2(error)})`, { cause: error });
2374
+ }
2375
+ const items = [];
2376
+ const push = (pointer, kind, value) => {
2377
+ if (items.length >= MAX_IMPORT_ITEMS) {
2378
+ throw new ImportFileError(file, pointer, `export holds more than ${MAX_IMPORT_ITEMS} items`);
2379
+ }
2380
+ items.push({ file, pointer, kind, value });
2381
+ };
2382
+ const collectPage = (page, base) => {
2383
+ for (const key of PAGE_KEYS) {
2384
+ const rows = page[key];
2385
+ if (rows === undefined)
2386
+ continue;
2387
+ if (!Array.isArray(rows)) {
2388
+ throw new ImportFileError(file, `${base}${pointerSegment(key)}`, `${key} must be an array`);
2389
+ }
2390
+ rows.forEach((value, index) => {
2391
+ const pointer = `${base}${pointerSegment(key)}${pointerSegment(index)}`;
2392
+ const kind = key === "memories" ? "document" : key === "memoryEntries" ? "memory" : bareItemKind(value);
2393
+ push(pointer, kind, value);
2394
+ });
2395
+ }
2396
+ const pagination = page.pagination;
2397
+ if (pagination !== undefined && !isRecord2(pagination)) {
2398
+ throw new ImportFileError(file, `${base}/pagination`, "pagination must be an object");
2399
+ }
2400
+ };
2401
+ if (Array.isArray(root)) {
2402
+ if (root.length > 0 && root.every(isPage)) {
2403
+ root.forEach((page, index) => collectPage(page, pointerSegment(index)));
2404
+ } else {
2405
+ root.forEach((value, index) => push(pointerSegment(index), bareItemKind(value), value));
2406
+ }
2407
+ } else if (isPage(root)) {
2408
+ collectPage(root, "");
2409
+ } else {
2410
+ throw new ImportFileError(file, "", "unknown export shape: expected {memories}, {memoryEntries}, {documents}, or an array");
2411
+ }
2412
+ return items;
2413
+ }
2414
+ function daysInMonth(year, month) {
2415
+ if (month === 2)
2416
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
2417
+ return [4, 6, 9, 11].includes(month) ? 30 : 31;
2418
+ }
2419
+ function parseStrictTimestamp(value) {
2420
+ if (typeof value !== "string")
2421
+ return;
2422
+ const match = STRICT_TIMESTAMP.exec(value);
2423
+ if (match === null)
2424
+ return;
2425
+ const [year, month, day, hour, minute, second] = match.slice(1, 7).map(Number);
2426
+ const fraction = match[7] ?? "";
2427
+ const zone = match[8] ?? "Z";
2428
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month))
2429
+ return;
2430
+ if (hour > 23 || minute > 59 || second > 59)
2431
+ return;
2432
+ let offsetMinutes = 0;
2433
+ if (zone !== "Z") {
2434
+ const offsetHour = Number(zone.slice(1, 3));
2435
+ const offsetMinute = Number(zone.slice(4, 6));
2436
+ if (offsetHour > 23 || offsetMinute > 59)
2437
+ return;
2438
+ offsetMinutes = (zone.startsWith("-") ? -1 : 1) * (offsetHour * 60 + offsetMinute);
2439
+ }
2440
+ const milliseconds = Number(fraction.padEnd(3, "0").slice(0, 3));
2441
+ const instant = new Date(0);
2442
+ instant.setUTCFullYear(year, month - 1, day);
2443
+ instant.setUTCHours(hour, minute, second, milliseconds);
2444
+ const time = instant.getTime() - offsetMinutes * 60000;
2445
+ if (!Number.isFinite(time))
2446
+ return;
2447
+ const result = new Date(time);
2448
+ const resultYear = result.getUTCFullYear();
2449
+ if (resultYear < 0 || resultYear > 9999)
2450
+ return;
2451
+ return result.toISOString();
2452
+ }
2453
+ function stringProblem(value, maximum, singleLine) {
2454
+ if (LONE_SURROGATE.test(value))
2455
+ return "is not well-formed Unicode";
2456
+ if (value.includes("\x00"))
2457
+ return "contains a NUL character";
2458
+ if (singleLine && CONTROL_CHARACTER.test(value))
2459
+ return "contains control characters";
2460
+ if (value.length > maximum)
2461
+ return `is longer than ${maximum} characters`;
2462
+ return;
2463
+ }
2464
+ function flatMetadata(value, label = "metadata") {
2465
+ if (value === undefined || value === null)
2466
+ return { diagnostics: [] };
2467
+ if (!isRecord2(value))
2468
+ return { diagnostics: [`${label}: expected an object; dropped`] };
2469
+ const diagnostics = [];
2470
+ const kept = [];
2471
+ for (const key of Object.keys(value).sort()) {
2472
+ const entry = value[key];
2473
+ if (entry === null || entry === undefined)
2474
+ continue;
2475
+ const name = JSON.stringify(key.length > 64 ? `${key.slice(0, 64)}\u2026` : key);
2476
+ if (!METADATA_KEY.test(key) || key === "__proto__") {
2477
+ diagnostics.push(`${label} key ${name} is not a safe key; dropped`);
2478
+ continue;
2479
+ }
2480
+ if (typeof entry === "string") {
2481
+ const problem = stringProblem(entry, MAX_METADATA_VALUE_CHARS, true);
2482
+ if (problem !== undefined) {
2483
+ diagnostics.push(`${label}.${key} ${problem}; dropped`);
2484
+ continue;
2485
+ }
2486
+ } else if (typeof entry === "number") {
2487
+ if (!Number.isFinite(entry)) {
2488
+ diagnostics.push(`${label}.${key} is not a finite number; dropped`);
2489
+ continue;
2490
+ }
2491
+ if (!isMetadataNumber(entry)) {
2492
+ diagnostics.push(`${label}.${key} is an integer outside the safe range; dropped`);
2493
+ continue;
2494
+ }
2495
+ } else if (typeof entry !== "boolean") {
2496
+ diagnostics.push(`${label}.${key} is a nested value; dropped`);
2497
+ continue;
2498
+ }
2499
+ if (kept.length >= MAX_METADATA_KEYS) {
2500
+ diagnostics.push(`${label}.${key} is past the ${MAX_METADATA_KEYS}-key limit; dropped`);
2501
+ continue;
2502
+ }
2503
+ kept.push([key, entry]);
2504
+ }
2505
+ return kept.length === 0 ? { diagnostics } : { value: Object.fromEntries(kept), diagnostics };
2506
+ }
2507
+ function normalizeText(value) {
2508
+ return value.replace(/\r\n?/gu, `
2509
+ `).replace(/^(?:[ \t]*\n)+/u, "").trimEnd();
2510
+ }
2511
+ function cleanTitle(value) {
2512
+ if (value === undefined)
2513
+ return "";
2514
+ const single = value.replace(/[\s\p{Cc}]+/gu, " ").trim();
2515
+ const characters = Array.from(single);
2516
+ if (characters.length <= MAX_TITLE_CHARS)
2517
+ return single;
2518
+ return `${characters.slice(0, MAX_TITLE_CHARS - 1).join("").trimEnd()}\u2026`;
2519
+ }
2520
+ function titleFor(candidates) {
2521
+ for (const candidate of candidates) {
2522
+ const title = cleanTitle(candidate);
2523
+ if (title !== "")
2524
+ return title;
2525
+ }
2526
+ return "Untitled";
2527
+ }
2528
+ function slugFor(candidates) {
2529
+ for (const candidate of candidates) {
2530
+ if (candidate === undefined)
2531
+ continue;
2532
+ const slug = slugify(candidate);
2533
+ if (slug !== "")
2534
+ return slug;
2535
+ }
2536
+ return "untitled";
2537
+ }
2538
+
2539
+ class FieldReader {
2540
+ reasons = [];
2541
+ diagnostics = [];
2542
+ record;
2543
+ constructor(record) {
2544
+ this.record = record;
2545
+ }
2546
+ id(key, required) {
2547
+ const value = this.record[key];
2548
+ if (value === undefined || value === null) {
2549
+ if (required)
2550
+ this.reasons.push(`${key}: missing`);
2551
+ return;
2552
+ }
2553
+ if (typeof value !== "string" || !EXTERNAL_ID.test(value)) {
2554
+ this.reasons.push(`${key}: expected an ID of 1 to 128 letters, digits, "_" or "-"`);
2555
+ return;
2556
+ }
2557
+ return value;
2558
+ }
2559
+ timestamp(key) {
2560
+ const value = this.record[key];
2561
+ if (value === undefined || value === null) {
2562
+ this.reasons.push(`${key}: missing`);
2563
+ return;
2564
+ }
2565
+ const parsed = parseStrictTimestamp(value);
2566
+ if (parsed === undefined)
2567
+ this.reasons.push(`${key}: expected an RFC 3339 timestamp with an offset`);
2568
+ return parsed;
2569
+ }
2570
+ optionalString(key, maximum = MAX_FIELD_CHARS, singleLine = true) {
2571
+ const value = this.record[key];
2572
+ if (value === undefined || value === null || value === "")
2573
+ return;
2574
+ if (typeof value !== "string") {
2575
+ this.reasons.push(`${key}: expected a string or null`);
2576
+ return;
2577
+ }
2578
+ const problem = stringProblem(value, maximum, singleLine);
2579
+ if (problem !== undefined) {
2580
+ this.reasons.push(`${key}: ${problem}`);
2581
+ return;
2582
+ }
2583
+ return value;
2584
+ }
2585
+ optionalEnum(key, known) {
2586
+ const value = this.optionalString(key, 64);
2587
+ if (value === undefined)
2588
+ return;
2589
+ if (!ENUM_VALUE.test(value)) {
2590
+ this.reasons.push(`${key}: expected a lower-case identifier`);
2591
+ return;
2592
+ }
2593
+ if (!known.includes(value))
2594
+ this.diagnostics.push(`${key} "${value}" is not a documented value`);
2595
+ return value;
2596
+ }
2597
+ boolean(key, fallback) {
2598
+ const value = this.record[key];
2599
+ if (value === undefined || value === null)
2600
+ return fallback;
2601
+ if (typeof value !== "boolean") {
2602
+ this.reasons.push(`${key}: expected a boolean`);
2603
+ return fallback;
2604
+ }
2605
+ return value;
2606
+ }
2607
+ body(keys) {
2608
+ for (const key of keys) {
2609
+ const value = this.record[key];
2610
+ if (value === undefined || value === null)
2611
+ continue;
2612
+ if (typeof value !== "string") {
2613
+ this.reasons.push(`${key}: expected a string or null`);
2614
+ return;
2615
+ }
2616
+ const text = normalizeText(value);
2617
+ if (text.trim() === "")
2618
+ continue;
2619
+ const problem = stringProblem(text, Number.POSITIVE_INFINITY, false);
2620
+ if (problem !== undefined) {
2621
+ this.reasons.push(`${key}: ${problem}`);
2622
+ return;
2623
+ }
2624
+ if (Buffer.byteLength(text, "utf8") > MAX_NOTE_BYTES) {
2625
+ this.reasons.push(`${key}: body exceeds the ${MAX_NOTE_BYTES}-byte note limit`);
2626
+ return;
2627
+ }
2628
+ return text;
2629
+ }
2630
+ this.reasons.push(`empty body: no non-blank ${keys.join(" or ")}`);
2631
+ return;
2632
+ }
2633
+ idList(key, maximum) {
2634
+ const value = this.record[key];
2635
+ if (value === undefined || value === null)
2636
+ return [];
2637
+ if (!Array.isArray(value)) {
2638
+ this.diagnostics.push(`${key}: expected an array; dropped`);
2639
+ return [];
2640
+ }
2641
+ const kept = [];
2642
+ value.forEach((entry, index) => {
2643
+ if (typeof entry !== "string" || !EXTERNAL_ID.test(entry)) {
2644
+ this.diagnostics.push(`${key}/${index}: not a valid ID; dropped`);
2645
+ } else if (kept.length >= maximum) {
2646
+ this.diagnostics.push(`${key}/${index}: past the ${maximum}-entry limit; dropped`);
2647
+ } else if (!kept.includes(entry))
2648
+ kept.push(entry);
2649
+ });
2650
+ return kept;
2651
+ }
2652
+ }
2653
+ function rejection(raw, reasons, externalId) {
2654
+ return {
2655
+ ok: false,
2656
+ rejection: {
2657
+ file: raw.file,
2658
+ pointer: raw.pointer,
2659
+ ...externalId === undefined ? {} : { externalId },
2660
+ reasons
2661
+ }
2662
+ };
2663
+ }
2664
+ function externalIdOf(value) {
2665
+ return isRecord2(value) && typeof value.id === "string" && EXTERNAL_ID.test(value.id) ? value.id : undefined;
2666
+ }
2667
+ function validateDocument(raw) {
2668
+ if (!isRecord2(raw.value))
2669
+ return rejection(raw, ["expected an object"]);
2670
+ const fields = new FieldReader(raw.value);
2671
+ const externalId = fields.id("id", true);
2672
+ const customId = fields.optionalString("customId", MAX_CUSTOM_ID_CHARS);
2673
+ const rawTitle = fields.optionalString("title", MAX_TITLE_SOURCE_CHARS, false);
2674
+ const supermemoryType = fields.optionalEnum("type", SUPERMEMORY_DOCUMENT_TYPES);
2675
+ const status = fields.optionalEnum("status", SUPERMEMORY_STATUSES);
2676
+ const url = fields.optionalString("url");
2677
+ const connectionId = fields.optionalString("connectionId", 128);
2678
+ const filepath = fields.optionalString("filepath");
2679
+ const created = fields.timestamp("createdAt");
2680
+ const updated = fields.timestamp("updatedAt");
2681
+ const text = fields.body(["content", "summary"]);
2682
+ const containerTags = [];
2683
+ const tags = raw.value.containerTags;
2684
+ if (Array.isArray(tags)) {
2685
+ tags.forEach((tag, index) => {
2686
+ if (typeof tag !== "string" || !CONTAINER_TAG.test(tag)) {
2687
+ fields.diagnostics.push(`containerTags/${index}: not a valid container tag; dropped`);
2688
+ } else if (containerTags.length >= MAX_CONTAINER_TAGS) {
2689
+ fields.diagnostics.push(`containerTags/${index}: past the ${MAX_CONTAINER_TAGS}-tag limit; dropped`);
2690
+ } else if (!containerTags.includes(tag))
2691
+ containerTags.push(tag);
2692
+ });
2693
+ } else if (tags !== undefined && tags !== null) {
2694
+ fields.diagnostics.push("containerTags: expected an array; dropped");
2695
+ }
2696
+ const metadata = flatMetadata(raw.value.metadata);
2697
+ if (fields.reasons.length > 0 || externalId === undefined || created === undefined || updated === undefined || text === undefined) {
2698
+ return rejection(raw, fields.reasons, externalIdOf(raw.value));
2699
+ }
2700
+ return {
2701
+ ok: true,
2702
+ item: {
2703
+ kind: "document",
2704
+ file: raw.file,
2705
+ pointer: raw.pointer,
2706
+ externalId,
2707
+ ...customId === undefined ? {} : { customId },
2708
+ title: titleFor([rawTitle, customId, externalId]),
2709
+ slug: slugFor([customId, rawTitle, externalId]),
2710
+ ...supermemoryType === undefined ? {} : { supermemoryType },
2711
+ ...status === undefined ? {} : { status },
2712
+ containerTags,
2713
+ ...url === undefined ? {} : { url },
2714
+ ...connectionId === undefined ? {} : { connectionId },
2715
+ ...filepath === undefined ? {} : { filepath },
2716
+ created,
2717
+ updated,
2718
+ ...metadata.value === undefined ? {} : { metadata: metadata.value },
2719
+ text,
2720
+ diagnostics: [...fields.diagnostics, ...metadata.diagnostics]
2721
+ }
2722
+ };
2723
+ }
2724
+ function validateMemoryVersion(raw, value, pointer, history) {
2725
+ const at = { ...raw, pointer, value };
2726
+ if (!isRecord2(value))
2727
+ return rejection(at, ["expected an object"]);
2728
+ const fields = new FieldReader(value);
2729
+ const externalId = fields.id("id", true);
2730
+ const parentId = fields.id("parentMemoryId", false);
2731
+ const rootId = fields.id("rootMemoryId", false);
2732
+ const version = value.version;
2733
+ if (typeof version !== "number" || !Number.isFinite(version)) {
2734
+ fields.reasons.push("version: expected a finite number");
2735
+ } else if (!isMetadataNumber(version)) {
2736
+ fields.reasons.push("version: integer outside the safe range");
2737
+ }
2738
+ const created = fields.timestamp("createdAt");
2739
+ const updated = fields.timestamp("updatedAt");
2740
+ const isLatest = fields.boolean("isLatest", !history);
2741
+ const forgotten = fields.boolean("isForgotten", false);
2742
+ const isStatic = history ? false : fields.boolean("isStatic", false);
2743
+ const isInference = history ? false : fields.boolean("isInference", false);
2744
+ const text = fields.body(["memory"]);
2745
+ let forgetAfter;
2746
+ if (!history && value.forgetAfter !== undefined && value.forgetAfter !== null) {
2747
+ forgetAfter = parseStrictTimestamp(value.forgetAfter);
2748
+ if (forgetAfter === undefined)
2749
+ fields.diagnostics.push("forgetAfter: not a valid timestamp; dropped");
2750
+ }
2751
+ const sourceDocumentIds = history ? [] : fields.idList("documentIds", MAX_SOURCE_DOCUMENT_IDS);
2752
+ const metadata = history ? { diagnostics: [] } : flatMetadata(value.metadata);
2753
+ const relations = value.memoryRelations;
2754
+ const hasMemoryRelations = !history && isRecord2(relations) && Object.keys(relations).length > 0;
2755
+ if (fields.reasons.length > 0 || externalId === undefined || typeof version !== "number" || created === undefined || updated === undefined || text === undefined) {
2756
+ return rejection(at, fields.reasons, externalIdOf(value));
2757
+ }
2758
+ const title = titleFor([text.split(`
2759
+ `).find((line) => line.trim() !== ""), externalId]);
2760
+ return {
2761
+ ok: true,
2762
+ item: {
2763
+ kind: "memory",
2764
+ file: raw.file,
2765
+ pointer,
2766
+ externalId,
2767
+ title,
2768
+ slug: slugFor([title, externalId]),
2769
+ version,
2770
+ isLatest,
2771
+ forgotten,
2772
+ ...parentId === undefined ? {} : { parentId },
2773
+ ...rootId === undefined ? {} : { rootId },
2774
+ isStatic,
2775
+ isInference,
2776
+ ...forgetAfter === undefined ? {} : { forgetAfter },
2777
+ sourceDocumentIds,
2778
+ created,
2779
+ updated,
2780
+ ...metadata.value === undefined ? {} : { metadata: metadata.value },
2781
+ hasMemoryRelations,
2782
+ text,
2783
+ diagnostics: [...fields.diagnostics, ...metadata.diagnostics]
2784
+ }
2785
+ };
2786
+ }
2787
+ function validateMemoryEntry(raw) {
2788
+ const results = [validateMemoryVersion(raw, raw.value, raw.pointer, false)];
2789
+ if (!isRecord2(raw.value))
2790
+ return results;
2791
+ const history = raw.value.history;
2792
+ if (history === undefined || history === null)
2793
+ return results;
2794
+ if (!Array.isArray(history)) {
2795
+ const latest = results[0];
2796
+ if (latest?.ok === true) {
2797
+ results[0] = {
2798
+ ok: true,
2799
+ item: { ...latest.item, diagnostics: [...latest.item.diagnostics, "history: expected an array; dropped"] }
2800
+ };
2801
+ }
2802
+ return results;
2803
+ }
2804
+ history.forEach((entry, index) => {
2805
+ results.push(validateMemoryVersion(raw, entry, `${raw.pointer}/history/${index}`, true));
2806
+ });
2807
+ return results;
2808
+ }
2809
+ function validateRawItem(raw) {
2810
+ return raw.kind === "memory" ? validateMemoryEntry(raw) : [validateDocument(raw)];
2811
+ }
2812
+ var ARTICLES_DIRECTORY = "articles";
2813
+ var IMPORTED_NOTES_DIRECTORY = "notes/imported";
2814
+ var IMPORTED_MEMORIES_DIRECTORY = "notes/imported/memories";
2815
+ var DOCUMENT_KEYS = [
2816
+ "imported_from",
2817
+ "external_id",
2818
+ "custom_id",
2819
+ "supermemory_type",
2820
+ "status",
2821
+ "container_tag",
2822
+ "container_tags",
2823
+ "source",
2824
+ "url",
2825
+ "connection_id",
2826
+ "filepath",
2827
+ "clipped",
2828
+ "created",
2829
+ "updated",
2830
+ "metadata",
2831
+ "import_digest"
2832
+ ];
2833
+ var MEMORY_KEYS = [
2834
+ "imported_from",
2835
+ "external_id",
2836
+ "version",
2837
+ "parent_id",
2838
+ "root_id",
2839
+ "is_static",
2840
+ "is_inference",
2841
+ "forget_after",
2842
+ "source_document_ids",
2843
+ "created",
2844
+ "updated",
2845
+ "metadata",
2846
+ "import_digest"
2847
+ ];
2848
+ var PROVENANCE_LINE = /\n\nImported from supermemory export `[^`\n]*` on \d{4}-\d{2}-\d{2}\.$/u;
2849
+ function occupancyKey(value) {
2850
+ return value.normalize("NFC").toLowerCase();
2851
+ }
2852
+ function normalizePrefix(prefix) {
2853
+ const trimmed = prefix.replace(/\/+$/u, "");
2854
+ let canonical;
2855
+ try {
2856
+ canonical = canonicalNoteId(trimmed);
2857
+ } catch {
2858
+ canonical = undefined;
2859
+ }
2860
+ if (canonical !== trimmed || trimmed === "") {
2861
+ throw new TypeError("--prefix must be a vault-relative directory such as notes/imported");
2862
+ }
2863
+ const key = occupancyKey(trimmed);
2864
+ if (key === ARTICLES_DIRECTORY || key.startsWith(`${ARTICLES_DIRECTORY}/`)) {
2865
+ throw new TypeError("--prefix must be outside articles/, where each directory holds one captured source; omit --prefix to place web documents there");
2866
+ }
2867
+ return trimmed;
2868
+ }
2869
+ function credentialShaped(url) {
2870
+ let parsed;
2871
+ try {
2872
+ parsed = new URL(url);
2873
+ } catch {
2874
+ return false;
2875
+ }
2876
+ if (parsed.username !== "" || parsed.password !== "")
2877
+ return true;
2878
+ const probe = parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : `https://supermemory.invalid/${parsed.pathname.replace(/^\/+/u, "")}${parsed.search}${parsed.hash}`;
2879
+ try {
2880
+ return sanitizeArtifactUrl(probe) !== new URL(probe).href;
2881
+ } catch {
2882
+ return true;
2883
+ }
2884
+ }
2885
+ function urlPlacement(url) {
2886
+ if (credentialShaped(url)) {
2887
+ return { kind: "dropped", diagnostic: "url carries credential-shaped data; dropped" };
2888
+ }
2889
+ if (!/^https?:/iu.test(url))
2890
+ return { kind: "url", value: url };
2891
+ const problem = savedSourceProblem(url);
2892
+ return problem === undefined ? { kind: "source", value: url } : { kind: "url", value: url, diagnostic: `url is kept as url, not source: ${problem}` };
2893
+ }
2894
+ function noteType(item) {
2895
+ if (item.kind === "memory")
2896
+ return "memory";
2897
+ const url = item.url;
2898
+ return url !== undefined && urlPlacement(url).kind === "source" || item.supermemoryType !== undefined && item.supermemoryType !== "text" ? "article" : "note";
2899
+ }
2900
+ function placement(item, slug, prefix) {
2901
+ if (prefix !== undefined) {
2902
+ const id2 = `${prefix}/${slug}`;
2903
+ return { id: id2, keys: [occupancyKey(id2)] };
2904
+ }
2905
+ if (item.kind === "memory") {
2906
+ const id2 = `${IMPORTED_MEMORIES_DIRECTORY}/${slug}`;
2907
+ return { id: id2, keys: [occupancyKey(id2)] };
2908
+ }
2909
+ if (noteType(item) !== "article") {
2910
+ const id2 = `${IMPORTED_NOTES_DIRECTORY}/${slug}`;
2911
+ return { id: id2, keys: [occupancyKey(id2)] };
2912
+ }
2913
+ const directory = `${ARTICLES_DIRECTORY}/${slug}`;
2914
+ const id = `${directory}/${slug}`;
2915
+ return { id, keys: [occupancyKey(id), occupancyKey(directory)] };
2916
+ }
2917
+ function sortedJson(value) {
2918
+ if (Array.isArray(value))
2919
+ return value.map(sortedJson);
2920
+ if (isRecord2(value)) {
2921
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortedJson(value[key])]));
2922
+ }
2923
+ return value;
2924
+ }
2925
+ function importDigest(title, fields, text) {
2926
+ const owned = Object.fromEntries(Object.entries(fields).filter(([key]) => key !== "import_digest"));
2927
+ return `sha256:${sha256(JSON.stringify(sortedJson({ title, fields: owned, text })))}`;
2928
+ }
2929
+ function provenanceLine(file, now) {
2930
+ const name = basename2(file).replace(/[`\u0000-\u001f\u007f]/gu, "_");
2931
+ return `Imported from supermemory export \`${name}\` on ${localCalendarDate(now)}.`;
2932
+ }
2933
+ function localCalendarDate(now) {
2934
+ const pad = (value) => String(value).padStart(2, "0");
2935
+ return `${String(now.getFullYear()).padStart(4, "0")}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
2936
+ }
2937
+ function localImportedText(body) {
2938
+ const text = normalizeText(body);
2939
+ const match = PROVENANCE_LINE.exec(text);
2940
+ return (match === null ? text : text.slice(0, match.index)).trimEnd();
2941
+ }
2942
+ function ownedFields(item, inArticles) {
2943
+ const fields = { imported_from: "supermemory", external_id: item.externalId };
2944
+ if (item.kind === "document") {
2945
+ if (item.customId !== undefined)
2946
+ fields.custom_id = item.customId;
2947
+ if (item.supermemoryType !== undefined)
2948
+ fields.supermemory_type = item.supermemoryType;
2949
+ if (item.status !== undefined && item.status !== "done")
2950
+ fields.status = item.status;
2951
+ const [firstTag] = item.containerTags;
2952
+ if (firstTag !== undefined)
2953
+ fields.container_tag = firstTag;
2954
+ if (item.containerTags.length > 1)
2955
+ fields.container_tags = item.containerTags;
2956
+ if (item.url !== undefined) {
2957
+ const url = urlPlacement(item.url);
2958
+ if (url.kind === "source")
2959
+ fields.source = url.value;
2960
+ else if (url.kind === "url")
2961
+ fields.url = url.value;
2962
+ }
2963
+ if (item.connectionId !== undefined)
2964
+ fields.connection_id = item.connectionId;
2965
+ if (item.filepath !== undefined)
2966
+ fields.filepath = item.filepath;
2967
+ if (inArticles)
2968
+ fields.clipped = item.created.slice(0, 10);
2969
+ } else {
2970
+ fields.version = item.version;
2971
+ if (item.parentId !== undefined)
2972
+ fields.parent_id = item.parentId;
2973
+ if (item.rootId !== undefined)
2974
+ fields.root_id = item.rootId;
2975
+ if (item.isStatic)
2976
+ fields.is_static = true;
2977
+ if (item.isInference)
2978
+ fields.is_inference = true;
2979
+ if (item.forgetAfter !== undefined)
2980
+ fields.forget_after = item.forgetAfter;
2981
+ if (item.sourceDocumentIds.length > 0)
2982
+ fields.source_document_ids = item.sourceDocumentIds;
2983
+ }
2984
+ fields.created = item.created;
2985
+ fields.updated = item.updated;
2986
+ if (item.metadata !== undefined)
2987
+ fields.metadata = item.metadata;
2988
+ return fields;
2989
+ }
2990
+ function isInArticles(id) {
2991
+ return id.startsWith(`${ARTICLES_DIRECTORY}/`);
2992
+ }
2993
+ function renderedBody(item, now) {
2994
+ return `${item.text}
2995
+
2996
+ ${provenanceLine(item.file, now)}
2997
+ `;
2998
+ }
2999
+ function itemDiagnostics(item) {
3000
+ if (item.kind !== "document")
3001
+ return item.diagnostics;
3002
+ const diagnostics = [...item.diagnostics];
3003
+ const url = item.url === undefined ? undefined : urlPlacement(item.url).diagnostic;
3004
+ if (url !== undefined)
3005
+ diagnostics.push(url);
3006
+ if (item.status !== undefined && item.status !== "done") {
3007
+ diagnostics.push(`status is "${item.status}"; the imported text may be incomplete`);
3008
+ }
3009
+ return diagnostics;
3010
+ }
3011
+ function localState(note, keys) {
3012
+ const revision = note.revision ?? revisionFor(new TextEncoder().encode(note.content));
3013
+ let parts;
3014
+ try {
3015
+ parts = frontmatter(note.content, note.path);
3016
+ } catch (error) {
3017
+ return { revision, error: `cannot read frontmatter (${errorMessage2(error)})` };
3018
+ }
3019
+ const data = parts.document.toJS();
3020
+ if (!isRecord2(data))
3021
+ return { revision };
3022
+ const recorded = typeof data.import_digest === "string" ? data.import_digest : undefined;
3023
+ const fields = Object.fromEntries(keys.filter((key) => (key in data)).map((key) => [key, data[key]]));
3024
+ const title = typeof data.title === "string" ? data.title : "";
3025
+ return {
3026
+ digest: importDigest(title, fields, localImportedText(parts.bodySuffix)),
3027
+ ...recorded === undefined ? {} : { recorded },
3028
+ revision
3029
+ };
3030
+ }
3031
+ function laterItem(left, right) {
3032
+ return Date.parse(right.updated) > Date.parse(left.updated);
3033
+ }
3034
+ function planImport(input) {
3035
+ const prefix = input.prefix === undefined ? undefined : normalizePrefix(input.prefix);
3036
+ const items = new Array(input.items.length);
3037
+ const winners = new Map;
3038
+ input.items.forEach((result, index) => {
3039
+ if (!result.ok) {
3040
+ const { rejection: rejection2 } = result;
3041
+ items[index] = {
3042
+ outcome: "rejected",
3043
+ file: rejection2.file,
3044
+ pointer: rejection2.pointer,
3045
+ ...rejection2.externalId === undefined ? {} : { externalId: rejection2.externalId },
3046
+ reason: rejection2.reasons.join("; "),
3047
+ diagnostics: []
3048
+ };
3049
+ return;
3050
+ }
3051
+ const previous = winners.get(result.item.externalId);
3052
+ const previousResult = previous === undefined ? undefined : input.items[previous];
3053
+ if (previousResult?.ok !== true || laterItem(previousResult.item, result.item)) {
3054
+ winners.set(result.item.externalId, index);
3055
+ }
3056
+ });
3057
+ const accepted = [];
3058
+ input.items.forEach((result, index) => {
3059
+ if (!result.ok)
3060
+ return;
3061
+ const { item } = result;
3062
+ const base = { file: item.file, pointer: item.pointer, externalId: item.externalId, diagnostics: itemDiagnostics(item) };
3063
+ if (winners.get(item.externalId) !== index) {
3064
+ items[index] = { ...base, outcome: "skipped", reason: "duplicate in export" };
3065
+ } else if (item.kind === "memory" && item.forgotten) {
3066
+ items[index] = { ...base, outcome: "skipped", reason: "forgotten" };
3067
+ } else {
3068
+ accepted.push({ index, item });
3069
+ }
3070
+ });
3071
+ const existing = new Map;
3072
+ for (const note of input.existing) {
3073
+ if (note.metadata.imported_from !== "supermemory")
3074
+ continue;
3075
+ const externalId = note.metadata.external_id;
3076
+ if (typeof externalId !== "string")
3077
+ continue;
3078
+ existing.set(externalId, [...existing.get(externalId) ?? [], note]);
3079
+ }
3080
+ const fresh = accepted.filter(({ item }) => !existing.has(item.externalId)).map((entry) => ({ ...entry, baseId: placement(entry.item, entry.item.slug, prefix).id })).sort((left, right) => left.baseId < right.baseId ? -1 : left.baseId > right.baseId ? 1 : left.item.externalId < right.item.externalId ? -1 : left.item.externalId > right.item.externalId ? 1 : 0);
3081
+ const taken = new Set(input.occupied);
3082
+ const allocated = new Map;
3083
+ const nextSuffix = new Map;
3084
+ for (const { index, item, baseId } of fresh) {
3085
+ const counter = occupancyKey(baseId);
3086
+ const first = nextSuffix.get(counter) ?? 1;
3087
+ let suffix = first;
3088
+ for (;suffix < first + MAX_SLUG_ATTEMPTS; suffix += 1) {
3089
+ const candidate = placement(item, suffix === 1 ? item.slug : `${item.slug}-${suffix}`, prefix);
3090
+ if (candidate.keys.some((key) => taken.has(key)))
3091
+ continue;
3092
+ for (const key of candidate.keys)
3093
+ taken.add(key);
3094
+ allocated.set(index, candidate.id);
3095
+ break;
3096
+ }
3097
+ nextSuffix.set(counter, suffix + 1);
3098
+ }
3099
+ let growthNotes = 0;
3100
+ let growthBytes = 0;
3101
+ for (const { index, item } of accepted) {
3102
+ const base = { file: item.file, pointer: item.pointer, externalId: item.externalId, diagnostics: itemDiagnostics(item) };
3103
+ const keys = item.kind === "document" ? DOCUMENT_KEYS : MEMORY_KEYS;
3104
+ const body = renderedBody(item, input.now);
3105
+ const matches = existing.get(item.externalId);
3106
+ if (matches === undefined) {
3107
+ const id = allocated.get(index);
3108
+ if (id === undefined) {
3109
+ items[index] = {
3110
+ ...base,
3111
+ outcome: "rejected",
3112
+ reason: `no free note ID for slug "${item.slug}" after ${MAX_SLUG_ATTEMPTS} attempts`
3113
+ };
3114
+ continue;
3115
+ }
3116
+ const fields2 = ownedFields(item, isInArticles(id));
3117
+ const write = {
3118
+ kind: "create",
3119
+ input: { id, title: item.title, type: noteType(item), body },
3120
+ fields: { ...fields2, import_digest: importDigest(item.title, fields2, item.text) }
3121
+ };
3122
+ const rendered = renderedSize(() => renderCreatedNote(write.input, MEASURE_DOCUMENT_ID, write.fields));
3123
+ if (typeof rendered === "string") {
3124
+ items[index] = { ...base, outcome: "rejected", reason: rendered };
3125
+ continue;
3126
+ }
3127
+ growthNotes += 1;
3128
+ growthBytes += rendered;
3129
+ items[index] = { ...base, outcome: "created", note: id, write };
3130
+ continue;
3131
+ }
3132
+ if (matches.length > 1) {
3133
+ const paths = matches.map((note2) => note2.path).sort().join(", ");
3134
+ items[index] = { ...base, outcome: "conflict", reason: `more than one note imports this ID: ${paths}` };
3135
+ continue;
3136
+ }
3137
+ const note = matches[0];
3138
+ const local = localState(note, keys);
3139
+ const fields = ownedFields(item, isInArticles(note.id));
3140
+ const digest = importDigest(item.title, fields, item.text);
3141
+ const matched = { ...base, note: note.id };
3142
+ if (local.error !== undefined) {
3143
+ items[index] = { ...matched, outcome: "conflict", reason: local.error };
3144
+ } else if (local.recorded === undefined) {
3145
+ items[index] = { ...matched, outcome: "conflict", reason: "missing import_digest" };
3146
+ } else if (local.digest !== local.recorded) {
3147
+ items[index] = { ...matched, outcome: "conflict", reason: "the note was edited after the last import" };
3148
+ } else if (digest === local.recorded) {
3149
+ items[index] = { ...matched, outcome: "skipped", reason: "unchanged" };
3150
+ } else {
3151
+ const updates = { title: item.title };
3152
+ for (const key of keys)
3153
+ updates[key] = key === "import_digest" ? digest : fields[key] ?? null;
3154
+ const rendered = renderedSize(() => renderUpdatedNoteBodyAndFields({ content: note.content, relativePath: note.path }, frontmatter(note.content, note.path), body, updates));
3155
+ if (typeof rendered === "string") {
3156
+ items[index] = { ...matched, outcome: "rejected", reason: rendered };
3157
+ continue;
3158
+ }
3159
+ growthBytes += rendered - Buffer.byteLength(note.content, "utf8");
3160
+ items[index] = {
3161
+ ...matched,
3162
+ outcome: "updated",
3163
+ write: { kind: "update", body, expectedRevision: local.revision, fields: updates }
3164
+ };
3165
+ }
3166
+ }
3167
+ const noteByExternalId = new Map;
3168
+ for (const [externalId, notes] of existing) {
3169
+ const [only] = notes;
3170
+ if (notes.length === 1 && only !== undefined)
3171
+ noteByExternalId.set(externalId, only.id);
3172
+ }
3173
+ const outcomeByNote = new Map;
3174
+ for (const planned of items) {
3175
+ if (planned?.note !== undefined && planned.externalId !== undefined) {
3176
+ noteByExternalId.set(planned.externalId, planned.note);
3177
+ outcomeByNote.set(planned.note, planned.outcome);
3178
+ }
3179
+ }
3180
+ const existingById = new Map(input.existing.map((note) => [note.id, note]));
3181
+ const candidates = [];
3182
+ let sawMemoryRelations = false;
3183
+ for (const { index, item } of accepted) {
3184
+ if (item.kind !== "memory")
3185
+ continue;
3186
+ sawMemoryRelations ||= item.hasMemoryRelations;
3187
+ const planned = items[index];
3188
+ if (item.parentId === undefined || planned?.note === undefined)
3189
+ continue;
3190
+ if (planned.outcome === "conflict" || planned.outcome === "rejected")
3191
+ continue;
3192
+ const target = noteByExternalId.get(item.parentId);
3193
+ if (target === undefined) {
3194
+ items[index] = {
3195
+ ...planned,
3196
+ diagnostics: [
3197
+ ...planned.diagnostics,
3198
+ `parent memory ${item.parentId} was not imported; supersedes relation omitted`
3199
+ ]
3200
+ };
3201
+ continue;
3202
+ }
3203
+ if (target === planned.note)
3204
+ continue;
3205
+ const targetOutcome = outcomeByNote.get(target);
3206
+ if (targetOutcome === "conflict" || targetOutcome === "rejected")
3207
+ continue;
3208
+ const written = planned.outcome === "created" || planned.outcome === "updated" || targetOutcome === "created";
3209
+ if (!written)
3210
+ continue;
3211
+ const source = existingById.get(planned.note);
3212
+ if (source !== undefined && supersedesTargets(source).includes(target))
3213
+ continue;
3214
+ candidates.push({ index, relation: { source: planned.note, predicate: "supersedes", target } });
3215
+ }
3216
+ candidates.sort((left, right) => compareRelations(left.relation, right.relation));
3217
+ const graph = new Map;
3218
+ const addEdge = (source, target) => {
3219
+ const targets = graph.get(source);
3220
+ if (targets === undefined)
3221
+ graph.set(source, [target]);
3222
+ else
3223
+ targets.push(target);
3224
+ };
3225
+ for (const note of input.existing) {
3226
+ for (const target of supersedesTargets(note))
3227
+ addEdge(note.id, target);
3228
+ }
3229
+ const relations = [];
3230
+ for (const { index, relation } of candidates) {
3231
+ if (reaches(graph, relation.target, relation.source)) {
3232
+ const planned = items[index];
3233
+ items[index] = {
3234
+ ...planned,
3235
+ diagnostics: [
3236
+ ...planned.diagnostics,
3237
+ `supersedes relation to ${relation.target} omitted because it would close a cycle`
3238
+ ]
3239
+ };
3240
+ continue;
3241
+ }
3242
+ addEdge(relation.source, relation.target);
3243
+ relations.push(relation);
3244
+ }
3245
+ return {
3246
+ items: items.filter((item) => item !== undefined),
3247
+ relations,
3248
+ diagnostics: sawMemoryRelations ? ["memoryRelations are the service's inferred links and were not imported"] : [],
3249
+ growth: { notes: growthNotes, bytes: growthBytes }
3250
+ };
3251
+ }
3252
+ var MEASURE_DOCUMENT_ID = "00000000-0000-4000-8000-000000000000";
3253
+ function renderedSize(render) {
3254
+ let content;
3255
+ try {
3256
+ content = render();
3257
+ } catch (error) {
3258
+ return error instanceof RangeError ? `the rendered note is larger than the ${MAX_NOTE_BYTES / (1024 * 1024)} MiB note limit` : errorMessage2(error);
3259
+ }
3260
+ const bytes = Buffer.byteLength(content, "utf8");
3261
+ return bytes > MAX_NOTE_BYTES ? `the rendered note is larger than the ${MAX_NOTE_BYTES / (1024 * 1024)} MiB note limit` : bytes;
3262
+ }
3263
+ function compareRelations(left, right) {
3264
+ return left.source < right.source ? -1 : left.source > right.source ? 1 : left.target < right.target ? -1 : left.target > right.target ? 1 : 0;
3265
+ }
3266
+ function reaches(graph, from, to) {
3267
+ const seen = new Set([from]);
3268
+ const stack = [from];
3269
+ for (let current = stack.pop();current !== undefined; current = stack.pop()) {
3270
+ if (current === to)
3271
+ return true;
3272
+ for (const next of graph.get(current) ?? []) {
3273
+ if (seen.has(next))
3274
+ continue;
3275
+ seen.add(next);
3276
+ stack.push(next);
3277
+ }
3278
+ }
3279
+ return false;
3280
+ }
3281
+ function supersedesTargets(note) {
3282
+ if (note.supersedes !== undefined)
3283
+ return note.supersedes;
3284
+ try {
3285
+ return relationsFromParts(frontmatter(note.content, note.path), note.path).filter((relation) => relation.predicate === "supersedes").map((relation) => relation.target);
3286
+ } catch {
3287
+ return [];
3288
+ }
3289
+ }
3290
+ function reportItem(item) {
3291
+ return {
3292
+ outcome: item.outcome,
3293
+ file: item.file,
3294
+ pointer: item.pointer,
3295
+ ...item.externalId === undefined ? {} : { externalId: item.externalId },
3296
+ ...item.note === undefined ? {} : { note: item.note, path: `${item.note}.md` },
3297
+ ...item.reason === undefined ? {} : { reason: item.reason },
3298
+ ...item.diagnostics.length === 0 ? {} : { diagnostics: item.diagnostics }
3299
+ };
3300
+ }
3301
+ async function lstatOrNull(path) {
3302
+ try {
3303
+ return await lstat(path);
3304
+ } catch (error) {
3305
+ if (isErrno(error, "ENOENT"))
3306
+ return null;
3307
+ throw error;
3308
+ }
3309
+ }
3310
+ async function ensureNoteParent(vault, noteId) {
3311
+ const id = canonicalNoteId(noteId);
3312
+ let current = vault.root;
3313
+ for (const segment of id.split("/").slice(0, -1)) {
3314
+ const next = join3(current, segment);
3315
+ let metadata = await lstatOrNull(next);
3316
+ if (metadata === null) {
3317
+ try {
3318
+ await mkdir(next);
3319
+ } catch (error) {
3320
+ if (!isErrno(error, "EEXIST"))
3321
+ throw error;
3322
+ }
3323
+ metadata = await lstat(next);
3324
+ }
3325
+ await assertExactDirectoryEntry(current, segment);
3326
+ if (metadata.isSymbolicLink())
3327
+ throw new Error("the note path must not traverse a symbolic link");
3328
+ if (!metadata.isDirectory())
3329
+ throw new Error("every note parent must be a directory");
3330
+ current = next;
3331
+ }
3332
+ await assertSafeParent(vault, join3(vault.root, `${id}.md`));
3333
+ }
3334
+ async function applyItem(vault, item, options) {
3335
+ const reported = reportItem(item);
3336
+ const { write, note } = item;
3337
+ if (write === undefined || note === undefined)
3338
+ return reported;
3339
+ const authoring = options.dependencies === undefined ? {} : { dependencies: options.dependencies };
3340
+ try {
3341
+ if (write.kind === "create") {
3342
+ await ensureNoteParent(vault, note);
3343
+ const result = await createNoteWithFields(vault.root, write.input, authoring, write.fields);
3344
+ return result.changed ? reported : { ...reported, outcome: "skipped", reason: "unchanged" };
3345
+ }
3346
+ await updateNoteBodyWithFields(vault.root, note, write.body, { ...authoring, expectedRevision: write.expectedRevision }, write.fields);
3347
+ return reported;
3348
+ } catch (error) {
3349
+ if (error instanceof NoteRevisionConflictError) {
3350
+ return { ...reported, outcome: "conflict", reason: "the note changed during import" };
3351
+ }
3352
+ if (error instanceof NoteAlreadyExistsError) {
3353
+ return { ...reported, outcome: "conflict", reason: error.message };
3354
+ }
3355
+ return { ...reported, outcome: "rejected", reason: errorMessage2(error) };
3356
+ }
3357
+ }
3358
+ async function applyImportPlan(root, plan, options = {}) {
3359
+ const vault = await resolveVault(root);
3360
+ const items = [];
3361
+ for (const item of plan.items)
3362
+ items.push(await applyItem(vault, item, options));
3363
+ const failed = new Set(items.flatMap((item) => item.note !== undefined && (item.outcome === "conflict" || item.outcome === "rejected") ? [item.note] : []));
3364
+ const authoring = options.dependencies === undefined ? {} : { dependencies: options.dependencies };
3365
+ const relations = [];
3366
+ for (const relation of plan.relations) {
3367
+ const unwritten = [relation.source, relation.target].find((note) => failed.has(note));
3368
+ if (unwritten !== undefined) {
3369
+ relations.push({ ...relation, outcome: "omitted", reason: `${unwritten} was not written` });
3370
+ continue;
3371
+ }
3372
+ try {
3373
+ const result = await addNoteRelation(vault.root, relation.source, relation.predicate, relation.target, authoring);
3374
+ relations.push({ ...relation, outcome: result.changed ? "added" : "unchanged" });
3375
+ } catch (error) {
3376
+ relations.push({ ...relation, outcome: "failed", reason: errorMessage2(error) });
3377
+ }
3378
+ }
3379
+ return { items, relations };
3380
+ }
3381
+ async function occupiedIds(vault, ids, prefix) {
3382
+ const occupied = new Set(ids.map(occupancyKey));
3383
+ const parents = prefix === undefined ? [ARTICLES_DIRECTORY, IMPORTED_NOTES_DIRECTORY, IMPORTED_MEMORIES_DIRECTORY] : [prefix];
3384
+ for (const parent of parents) {
3385
+ const directory = join3(vault.root, ...parent.split("/"));
3386
+ const metadata = await lstatOrNull(directory).catch((error) => {
3387
+ if (isErrno(error, "ENOTDIR"))
3388
+ return null;
3389
+ throw error;
3390
+ });
3391
+ if (metadata === null || !metadata.isDirectory())
3392
+ continue;
3393
+ for (const name of await readdir(directory)) {
3394
+ occupied.add(occupancyKey(`${parent}/${name}`));
3395
+ if (/\.md$/iu.test(name))
3396
+ occupied.add(occupancyKey(`${parent}/${name.slice(0, -3)}`));
3397
+ }
3398
+ }
3399
+ return occupied;
3400
+ }
3401
+ async function diskRevision(root, path, content) {
3402
+ let bytes;
3403
+ try {
3404
+ bytes = await readFile2(join3(root, ...path.split("/")));
3405
+ } catch {
3406
+ return;
3407
+ }
3408
+ return new TextDecoder().decode(bytes) === content ? revisionFor(bytes) : undefined;
3409
+ }
3410
+ function assertWithinVaultCaps(notes, plan, limits) {
3411
+ const maximumNotes = limits?.notes ?? MAX_SCANNED_NOTES;
3412
+ const maximumBytes = limits?.bytes ?? MAX_VAULT_UTF8_BYTES;
3413
+ const noteCount = notes.length + plan.growth.notes;
3414
+ if (noteCount > maximumNotes) {
3415
+ throw new Error(`the import would leave ${noteCount} notes, more than the vault's ${maximumNotes}-note limit; nothing was written`);
3416
+ }
3417
+ let bytes = plan.growth.bytes;
3418
+ for (const note of notes)
3419
+ bytes += Buffer.byteLength(note.content, "utf8");
3420
+ if (bytes > maximumBytes) {
3421
+ throw new Error(`the import would leave ${bytes} bytes of notes, more than the vault's ${maximumBytes}-byte limit; nothing was written`);
3422
+ }
3423
+ }
3424
+ function countOutcomes(items) {
3425
+ const count = (outcome) => items.filter((item) => item.outcome === outcome).length;
3426
+ return {
3427
+ created: count("created"),
3428
+ updated: count("updated"),
3429
+ skipped: count("skipped"),
3430
+ conflicts: count("conflict"),
3431
+ rejected: count("rejected")
3432
+ };
3433
+ }
3434
+ async function importSupermemory(options) {
3435
+ if (options.files.length === 0)
3436
+ throw new TypeError("import supermemory requires at least one export file");
3437
+ if (options.files.length > MAX_IMPORT_FILES) {
3438
+ throw new TypeError(`import supermemory accepts at most ${MAX_IMPORT_FILES} export files`);
3439
+ }
3440
+ const prefix = options.prefix === undefined ? undefined : normalizePrefix(options.prefix);
3441
+ const vault = await resolveVault(options.root);
3442
+ const now = options.now ?? new Date;
3443
+ const raw = [];
3444
+ for (const file of options.files) {
3445
+ for (const item of parseExportText(await readExportFile(file, file), file))
3446
+ raw.push(item);
3447
+ if (raw.length > MAX_IMPORT_ITEMS)
3448
+ throw new Error(`the exports hold more than ${MAX_IMPORT_ITEMS} items`);
3449
+ }
3450
+ const validated = raw.flatMap((item) => validateRawItem(item));
3451
+ if (validated.length > MAX_IMPORT_ITEMS) {
3452
+ throw new Error(`the exports hold more than ${MAX_IMPORT_ITEMS} items after memory history versions`);
3453
+ }
3454
+ const snapshot = await scanVault(vault.root, { mentionScope: false });
3455
+ const wanted = new Set(validated.flatMap((result) => result.ok ? [result.item.externalId] : []));
3456
+ const existing = [];
3457
+ for (const note of snapshot.notes) {
3458
+ const supersedes = (note.relationDeclarations ?? []).filter((relation) => relation.predicate === "supersedes").map((relation) => relation.target.replace(/\.md$/iu, ""));
3459
+ const base = { id: note.id, path: note.path, content: note.content, metadata: note.metadata, supersedes };
3460
+ const externalId = note.metadata.external_id;
3461
+ const matched = note.metadata.imported_from === "supermemory" && typeof externalId === "string" && wanted.has(externalId);
3462
+ const revision = matched ? await diskRevision(vault.root, note.path, note.content) : undefined;
3463
+ existing.push(revision === undefined ? base : { ...base, revision });
3464
+ }
3465
+ const occupied = await occupiedIds(vault, snapshot.notes.map((note) => note.id), prefix);
3466
+ const plan = planImport({ items: validated, existing, occupied, ...prefix === undefined ? {} : { prefix }, now });
3467
+ assertWithinVaultCaps(snapshot.notes, plan, options.limits);
3468
+ const dryRun = options.dryRun === true;
3469
+ const applied = dryRun ? {
3470
+ items: plan.items.map(reportItem),
3471
+ relations: plan.relations.map((relation) => ({ ...relation, outcome: "added" }))
3472
+ } : await applyImportPlan(vault.root, plan, options);
3473
+ const counts = countOutcomes(applied.items);
3474
+ return {
3475
+ ok: applied.items.length === 0 || counts.rejected < applied.items.length,
3476
+ dryRun,
3477
+ root: vault.root,
3478
+ files: options.files.length,
3479
+ counts,
3480
+ items: applied.items,
3481
+ relations: applied.relations,
3482
+ diagnostics: plan.diagnostics
3483
+ };
3484
+ }
3485
+
3486
+ // src/mcp-tools.ts
3487
+ import { createHash } from "crypto";
3488
+ import { lstat as lstat2 } from "fs/promises";
3489
+ var MAX_TOOL_RESULT_BYTES = 65536;
3490
+ var MAX_NOTE_READ_BYTES = 65536;
3491
+ var MAX_NOTE_ID_UTF8_BYTES = 4096;
3492
+ var DEFAULT_LIST_LIMIT = 100;
3493
+ var MAX_LIST_LIMIT = 1000;
3494
+ var DEFAULT_LINK_LIMIT = 50;
3495
+ var MAX_LINK_LIMIT = 1000;
3496
+ var MAX_LINK_DEPTH = 10;
3497
+ var SEARCH_MODES = ["exact", "keyword", "semantic", "hybrid"];
3498
+ var LINK_DIRECTIONS = ["in", "out", "both"];
3499
+ var SORT_FIELDS = ["title", "path", "inbound", "outbound"];
3500
+ var ORDERS = ["asc", "desc"];
3501
+ var ID_HINT = "IDs are vault-relative paths without .md, for example notes/decision.";
3502
+
3503
+ class ToolArgumentError extends Error {
3504
+ name = "ToolArgumentError";
3505
+ }
3506
+
3507
+ class ResultTooLargeError extends Error {
3508
+ name = "ResultTooLargeError";
3509
+ }
3510
+ function utf8Length(text) {
3511
+ return Buffer.byteLength(text, "utf8");
3512
+ }
3513
+ function jsonLength(value) {
3514
+ return utf8Length(JSON.stringify(value));
3515
+ }
3516
+ function errorMessage3(error) {
3517
+ return error instanceof Error ? error.message : String(error);
3518
+ }
3519
+ function toolSuccess(value) {
3520
+ return { content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: value };
3521
+ }
3522
+ function toolFailure(message) {
3523
+ return { content: [{ type: "text", text: message }], isError: true };
3524
+ }
3525
+ function isMissingPath(error) {
3526
+ return errorMessage3(error).startsWith("vault path component is not exact");
3527
+ }
3528
+ function noteNotFound(id, suggestion) {
3529
+ return toolFailure(suggestion === undefined || suggestion === id ? `note ${id} was not found` : `note ${id} was not found; did you mean ${suggestion}?`);
3530
+ }
3531
+ function failureFor(error) {
3532
+ if (error instanceof NoteRevisionConflictError) {
3533
+ const recovery = error.recoveryPath === null ? "" : `; displaced bytes remain at ${error.recoveryPath}`;
3534
+ return toolFailure(`revision conflict: expected ${error.expected ?? "none"}, current ${error.actual ?? "none"}${recovery}; ` + "call get_note and retry");
3535
+ }
3536
+ const message = errorMessage3(error);
3537
+ return toolFailure(error instanceof InvalidCanonicalNoteIdError ? `${message} ${ID_HINT}` : message);
3538
+ }
3539
+ function fitResult(value, steps, maxBytes = MAX_TOOL_RESULT_BYTES) {
3540
+ if (jsonLength(value) <= maxBytes)
3541
+ return value;
3542
+ let current = value;
3543
+ let omitted = 0;
3544
+ const marked = (candidate, dropped) => ({
3545
+ ...candidate,
3546
+ truncated: true,
3547
+ omitted: omitted + dropped
3548
+ });
3549
+ for (const step of steps) {
3550
+ const field = current[step.field];
3551
+ if (step.kind === "clear") {
3552
+ if (field === step.empty || field === undefined)
3553
+ continue;
3554
+ current = marked({ ...current, [step.field]: step.empty }, 0);
3555
+ if (jsonLength(current) <= maxBytes)
3556
+ return current;
3557
+ continue;
3558
+ }
3559
+ if (!Array.isArray(field) || field.length === 0)
3560
+ continue;
3561
+ const list = field;
3562
+ const empty = marked({ ...current, [step.field]: [] }, list.length);
3563
+ let size = jsonLength(empty);
3564
+ if (size > maxBytes) {
3565
+ current = empty;
3566
+ omitted += list.length;
3567
+ continue;
3568
+ }
3569
+ const kept = [];
3570
+ for (const item of list) {
3571
+ const cost = utf8Length(JSON.stringify(item) ?? "null") + (kept.length === 0 ? 0 : 1);
3572
+ if (size + cost > maxBytes)
3573
+ continue;
3574
+ kept.push(item);
3575
+ size += cost;
3576
+ }
3577
+ return marked({ ...current, [step.field]: kept }, list.length - kept.length);
3578
+ }
3579
+ if (jsonLength(current) <= maxBytes)
3580
+ return current;
3581
+ throw new ResultTooLargeError(`The result exceeds ${maxBytes} bytes even after trimming.`);
3582
+ }
3583
+ function fitBody(value, maxBytes = MAX_TOOL_RESULT_BYTES) {
3584
+ if (jsonLength(value) <= maxBytes)
3585
+ return value;
3586
+ const points = Array.from(value.body);
3587
+ const withBody = (count) => ({ ...value, body: points.slice(0, count).join(""), truncated: true });
3588
+ if (points.length === 0 || jsonLength(withBody(0)) > maxBytes) {
3589
+ throw new ResultTooLargeError(`The result exceeds ${maxBytes} bytes even without the note body.`);
3590
+ }
3591
+ let low = 0;
3592
+ let high = points.length - 1;
3593
+ while (low < high) {
3594
+ const middle = Math.ceil((low + high) / 2);
3595
+ if (jsonLength(withBody(middle)) <= maxBytes)
3596
+ low = middle;
3597
+ else
3598
+ high = middle - 1;
3599
+ }
3600
+ return withBody(low);
3601
+ }
3602
+ function has(arguments_, key) {
3603
+ return Object.hasOwn(arguments_, key) && arguments_[key] !== undefined;
3604
+ }
3605
+ function allowOnly(arguments_, allowed) {
3606
+ for (const key of Object.keys(arguments_)) {
3607
+ if (!allowed.includes(key))
3608
+ throw new ToolArgumentError(`Unknown argument "${key}".`);
3609
+ }
3610
+ }
3611
+ function checkedText(value, label, maxBytes) {
3612
+ if (typeof value !== "string" || value.length === 0) {
3613
+ throw new ToolArgumentError(`Invalid argument "${label}": expected a non-empty string.`);
3614
+ }
3615
+ if (utf8Length(value) > maxBytes) {
3616
+ throw new ToolArgumentError(`Invalid argument "${label}": expected at most ${maxBytes} UTF-8 bytes.`);
3617
+ }
3618
+ return value;
3619
+ }
3620
+ function requiredText(arguments_, key, maxBytes) {
3621
+ if (!has(arguments_, key))
3622
+ throw new ToolArgumentError(`Missing argument "${key}".`);
3623
+ return checkedText(arguments_[key], key, maxBytes);
3624
+ }
3625
+ function optionalText(arguments_, key, maxBytes) {
3626
+ return has(arguments_, key) ? checkedText(arguments_[key], key, maxBytes) : undefined;
3627
+ }
3628
+ function optionalBoolean(arguments_, key) {
3629
+ if (!has(arguments_, key))
3630
+ return;
3631
+ const value = arguments_[key];
3632
+ if (typeof value !== "boolean") {
3633
+ throw new ToolArgumentError(`Invalid argument "${key}": expected a boolean.`);
3634
+ }
3635
+ return value;
3636
+ }
3637
+ function optionalInteger(arguments_, key, minimum, maximum) {
3638
+ if (!has(arguments_, key))
3639
+ return;
3640
+ const value = arguments_[key];
3641
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
3642
+ throw new ToolArgumentError(`Invalid argument "${key}": expected an integer from ${minimum} through ${maximum}.`);
3643
+ }
3644
+ return value;
3645
+ }
3646
+ function optionalChoice(arguments_, key, choices) {
3647
+ if (!has(arguments_, key))
3648
+ return;
3649
+ const value = arguments_[key];
3650
+ const choice = choices.find((candidate) => candidate === value);
3651
+ if (choice === undefined) {
3652
+ throw new ToolArgumentError(`Invalid argument "${key}": expected one of ${choices.join(", ")}.`);
3653
+ }
3654
+ return choice;
3655
+ }
3656
+ function optionalTextList(arguments_, key, maxItems, maxBytes) {
3657
+ if (!has(arguments_, key))
3658
+ return;
3659
+ const value = arguments_[key];
3660
+ if (!Array.isArray(value) || value.length > maxItems) {
3661
+ throw new ToolArgumentError(`Invalid argument "${key}": expected an array of at most ${maxItems} strings.`);
3662
+ }
3663
+ const items = value;
3664
+ return items.map((item, index) => checkedText(item, `${key}[${index}]`, maxBytes));
3665
+ }
3666
+ function isPlainObject(value) {
3667
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3668
+ }
3669
+ function metadataScalar(value, label) {
3670
+ if (value === null || typeof value === "boolean")
3671
+ return value;
3672
+ if (typeof value === "number" && Number.isFinite(value))
3673
+ return value;
3674
+ if (typeof value === "string" && utf8Length(value) <= MAX_QUERY_TEXT_UTF8_BYTES)
3675
+ return value;
3676
+ throw new ToolArgumentError(`Invalid argument "${label}": expected a string, finite number, boolean, or null.`);
3677
+ }
3678
+ function metadataFilters(arguments_) {
3679
+ const filters = [];
3680
+ if (has(arguments_, "where")) {
3681
+ const where = arguments_["where"];
3682
+ if (!Array.isArray(where) || where.length > MAX_QUERY_FILTERS) {
3683
+ throw new ToolArgumentError(`Invalid argument "where": expected an array of at most ${MAX_QUERY_FILTERS} {path, value} objects.`);
3684
+ }
3685
+ const entries = where;
3686
+ entries.forEach((entry, index) => {
3687
+ const label = `where[${index}]`;
3688
+ if (!isPlainObject(entry)) {
3689
+ throw new ToolArgumentError(`Invalid argument "${label}": expected a {path, value} object.`);
3690
+ }
3691
+ allowOnlyIn(entry, ["path", "value"], label);
3692
+ if (!Object.hasOwn(entry, "value")) {
3693
+ throw new ToolArgumentError(`Invalid argument "${label}": missing value.`);
3694
+ }
3695
+ filters.push({
3696
+ kind: "equals",
3697
+ path: checkedText(entry["path"], `${label}.path`, MAX_QUERY_METADATA_PATH_UTF8_BYTES),
3698
+ value: metadataScalar(entry["value"], `${label}.value`)
3699
+ });
3700
+ });
3701
+ }
3702
+ for (const path of optionalTextList(arguments_, "has", MAX_QUERY_FILTERS, MAX_QUERY_METADATA_PATH_UTF8_BYTES) ?? []) {
3703
+ filters.push({ kind: "exists", path });
3704
+ }
3705
+ if (filters.length > MAX_QUERY_FILTERS) {
3706
+ throw new ToolArgumentError(`Invalid arguments "where" and "has": at most ${MAX_QUERY_FILTERS} filters in total.`);
3707
+ }
3708
+ return filters;
3709
+ }
3710
+ function allowOnlyIn(value, allowed, label) {
3711
+ for (const key of Object.keys(value)) {
3712
+ if (!allowed.includes(key))
3713
+ throw new ToolArgumentError(`Invalid argument "${label}": unknown field "${key}".`);
3714
+ }
3715
+ }
3716
+ function bodyArgument(arguments_, key, required) {
3717
+ if (!has(arguments_, key)) {
3718
+ if (required)
3719
+ throw new ToolArgumentError(`Missing argument "${key}".`);
3720
+ return;
3721
+ }
3722
+ const value = arguments_[key];
3723
+ if (typeof value !== "string" || utf8Length(value) > MAX_FRAME_BYTES) {
3724
+ throw new ToolArgumentError(`Invalid argument "${key}": expected a string of at most ${MAX_FRAME_BYTES} UTF-8 bytes.`);
3725
+ }
3726
+ return value;
3727
+ }
3728
+ function revisionArgument(arguments_, key, required) {
3729
+ if (!has(arguments_, key)) {
3730
+ if (required)
3731
+ throw new ToolArgumentError(`Missing argument "${key}".`);
3732
+ return;
3733
+ }
3734
+ const value = arguments_[key];
3735
+ if (typeof value !== "string" || !NOTE_REVISION_PATTERN.test(value)) {
3736
+ throw new ToolArgumentError(`Invalid argument "${key}": expected sha256: followed by 64 lowercase hex digits.`);
3737
+ }
3738
+ return value;
3739
+ }
3740
+ function noteIdArgument(arguments_, key) {
3741
+ return canonicalNoteId(requiredText(arguments_, key, MAX_NOTE_ID_UTF8_BYTES));
3742
+ }
3743
+ function querySort(raw) {
3744
+ const builtin = SORT_FIELDS.find((field) => field === raw);
3745
+ if (builtin !== undefined)
3746
+ return { kind: "builtin", field: builtin };
3747
+ const path = raw.startsWith("metadata.") ? raw.slice("metadata.".length) : "";
3748
+ if (path === "") {
3749
+ throw new ToolArgumentError(`Invalid argument "sort": expected ${SORT_FIELDS.join(", ")}, or metadata.<path>.`);
3750
+ }
3751
+ return { kind: "metadata", path };
3752
+ }
3753
+ async function vaultFingerprint(root) {
3754
+ const files = await markdownFiles(root);
3755
+ const stats = await Promise.all(files.map((file) => lstat2(file, { bigint: true }).catch(() => null)));
3756
+ const hash = createHash("sha256");
3757
+ files.forEach((file, index) => {
3758
+ const stat2 = stats[index];
3759
+ hash.update(stat2 === null || stat2 === undefined ? `${file}\x00missing
3760
+ ` : `${file}\x00${stat2.ino}\x00${stat2.size}\x00${stat2.mtimeNs}\x00${stat2.ctimeNs}
3761
+ `);
3762
+ });
3763
+ return hash.digest("hex");
3764
+ }
3765
+ function createVaultSessions(options) {
3766
+ let open2 = null;
3767
+ const warn = options.warn ?? ((message) => {
3768
+ process.stderr.write(`${message}
3769
+ `);
3770
+ });
3771
+ const close = async () => {
3772
+ const current = open2;
3773
+ open2 = null;
3774
+ if (current !== null)
3775
+ await current.entry.kb.close();
3776
+ };
3777
+ const discard = async () => {
3778
+ try {
3779
+ await close();
3780
+ } catch (error) {
3781
+ warn(`warning: closing the cached vault session failed: ${errorMessage3(error).replaceAll(`
3782
+ `, " ")}`);
3783
+ }
3784
+ };
3785
+ return {
3786
+ async get() {
3787
+ const fingerprint = await vaultFingerprint(options.root);
3788
+ if (open2 !== null && open2.fingerprint === fingerprint)
3789
+ return open2.entry;
3790
+ await discard();
3791
+ const scan = options.dependencies?.scanVault ?? scanVault;
3792
+ const captured = {};
3793
+ const kb = await openKnowledgeBase({ root: options.root, ...options.repository === undefined ? {} : { repository: options.repository } }, {
3794
+ ...options.dependencies,
3795
+ scanVault: async (root, scanOptions) => {
3796
+ const snapshot = await scan(root, scanOptions);
3797
+ captured.snapshot = snapshot;
3798
+ return snapshot;
3799
+ }
3800
+ });
3801
+ if (captured.snapshot === undefined) {
3802
+ await kb.close();
3803
+ throw new Error("The vault scan did not run.");
3804
+ }
3805
+ open2 = { fingerprint, entry: { kb, snapshot: captured.snapshot } };
3806
+ return open2.entry;
3807
+ },
3808
+ invalidate: discard,
3809
+ close
3810
+ };
3811
+ }
3812
+ var noteIdSchema = {
3813
+ type: "string",
3814
+ minLength: 1,
3815
+ maxLength: MAX_NOTE_ID_UTF8_BYTES,
3816
+ description: ID_HINT
3817
+ };
3818
+ var textListSchema = (maxItems, maxLength, description) => ({
3819
+ type: "array",
3820
+ maxItems,
3821
+ items: { type: "string", minLength: 1, maxLength },
3822
+ description
3823
+ });
3824
+ var filterProperties = {
3825
+ where: {
3826
+ type: "array",
3827
+ maxItems: MAX_QUERY_FILTERS,
3828
+ items: {
3829
+ type: "object",
3830
+ additionalProperties: false,
3831
+ required: ["path", "value"],
3832
+ properties: {
3833
+ path: { type: "string", minLength: 1, maxLength: MAX_QUERY_METADATA_PATH_UTF8_BYTES },
3834
+ value: { type: ["string", "number", "boolean", "null"] }
3835
+ }
3836
+ },
3837
+ description: "Frontmatter fields that must equal the given values. Filters combine with AND."
3838
+ },
3839
+ has: textListSchema(MAX_QUERY_FILTERS, MAX_QUERY_METADATA_PATH_UTF8_BYTES, "Frontmatter fields that must exist."),
3840
+ tags: textListSchema(MAX_QUERY_TAGS, MAX_QUERY_TEXT_UTF8_BYTES, "Tags that must all be present."),
3841
+ scope: textListSchema(MAX_REPOSITORY_SCOPES, MAX_QUERY_TEXT_UTF8_BYTES, "Repository scopes; a note matches when it declares any of them.")
3842
+ };
3843
+ var linkProperties = {
3844
+ id: noteIdSchema,
3845
+ depth: { type: "integer", minimum: 1, maximum: MAX_LINK_DEPTH, default: 1 },
3846
+ limit: {
3847
+ type: "integer",
3848
+ minimum: 1,
3849
+ maximum: MAX_LINK_LIMIT,
3850
+ default: DEFAULT_LINK_LIMIT,
3851
+ description: "Maximum returned notes, including the starting note."
3852
+ }
3853
+ };
3854
+ var revisionSchema = {
3855
+ type: "string",
3856
+ pattern: NOTE_REVISION_PATTERN.source,
3857
+ description: "The revision from get_note or from an earlier write."
3858
+ };
3859
+ var READ_ONLY = {
3860
+ readOnlyHint: true,
3861
+ destructiveHint: false,
3862
+ idempotentHint: true,
3863
+ openWorldHint: false
3864
+ };
3865
+ function listRow(row2) {
3866
+ return {
3867
+ id: row2.id,
3868
+ path: row2.path,
3869
+ title: row2.title,
3870
+ aliases: row2.aliases,
3871
+ tags: row2.tags,
3872
+ properties: row2.properties,
3873
+ metadata: row2.metadata,
3874
+ summary: row2.summary,
3875
+ inboundContextualCount: row2.inboundContextualCount,
3876
+ outboundContextualCount: row2.outboundContextualCount
3877
+ };
3878
+ }
3879
+ function bodyAfterFrontmatter(suffix) {
3880
+ let body = suffix;
3881
+ for (let count = 0;count < 2; count += 1) {
3882
+ if (body.startsWith(`\r
3883
+ `))
3884
+ body = body.slice(2);
3885
+ else if (body.startsWith(`
3886
+ `))
3887
+ body = body.slice(1);
3888
+ else
3889
+ break;
3890
+ }
3891
+ return body;
3892
+ }
3893
+ function noteView(read, revision) {
3894
+ let parts;
3895
+ try {
3896
+ parts = frontmatter(read.content, read.path);
3897
+ } catch (error) {
3898
+ const reason = read.truncated ? `the note is larger than ${MAX_NOTE_READ_BYTES} bytes and its frontmatter did not fit` : errorMessage3(error);
3899
+ throw new Error(`Cannot split note ${read.id} into frontmatter and body: ${reason}.`);
3900
+ }
3901
+ const document = parts.document.toJSON();
3902
+ return {
3903
+ id: read.id,
3904
+ path: read.path,
3905
+ title: read.title,
3906
+ frontmatter: document ?? {},
3907
+ body: parts.hadFrontmatter ? bodyAfterFrontmatter(parts.bodySuffix) : parts.bodySuffix,
3908
+ revision,
3909
+ truncated: read.truncated
3910
+ };
3911
+ }
3912
+ var UTF8_BOM = new Uint8Array([239, 187, 191]);
3913
+ function contentMatchesRevision(content, revision) {
3914
+ const bytes = new TextEncoder().encode(content);
3915
+ if (revisionFor(bytes) === revision)
3916
+ return true;
3917
+ const withBom = new Uint8Array(UTF8_BOM.length + bytes.length);
3918
+ withBom.set(UTF8_BOM);
3919
+ withBom.set(bytes, UTF8_BOM.length);
3920
+ return revisionFor(withBom) === revision;
3921
+ }
3922
+ function readTools(root, sessions) {
3923
+ return [
3924
+ {
3925
+ definition: {
3926
+ name: "search",
3927
+ title: "Search notes",
3928
+ description: "Search the vault. Hybrid mode joins exact matches with local semantic search (QMD); each hit keeps " + "its own evidence. When QMD is unavailable the result is partial and diagnostics.lanes says why.",
3929
+ inputSchema: {
3930
+ type: "object",
3931
+ additionalProperties: false,
3932
+ required: ["query"],
3933
+ properties: {
3934
+ query: { type: "string", minLength: 1, maxLength: MAX_QUERY_TEXT_UTF8_BYTES },
3935
+ mode: { type: "string", enum: SEARCH_MODES, default: "hybrid" },
3936
+ selectedPassage: {
3937
+ type: "boolean",
3938
+ default: false,
3939
+ description: "Include a local source excerpt with line references and section headings. Ranking is unchanged."
3940
+ },
3941
+ limit: { type: "integer", minimum: 1, maximum: MAX_SEARCH_RESULTS, default: DEFAULT_SEARCH_RESULTS },
3942
+ ...filterProperties
3943
+ }
3944
+ },
3945
+ annotations: { title: "Search notes", ...READ_ONLY }
3946
+ },
3947
+ handler: async (arguments_) => {
3948
+ allowOnly(arguments_, ["query", "mode", "limit", "tags", "where", "has", "scope", "selectedPassage"]);
3949
+ const query = requiredText(arguments_, "query", MAX_QUERY_TEXT_UTF8_BYTES);
3950
+ const mode = optionalChoice(arguments_, "mode", SEARCH_MODES) ?? "hybrid";
3951
+ const limit = optionalInteger(arguments_, "limit", 1, MAX_SEARCH_RESULTS) ?? DEFAULT_SEARCH_RESULTS;
3952
+ const selectedPassage = optionalBoolean(arguments_, "selectedPassage");
3953
+ const filters = metadataFilters(arguments_);
3954
+ const tags = optionalTextList(arguments_, "tags", MAX_QUERY_TAGS, MAX_QUERY_TEXT_UTF8_BYTES);
3955
+ const scopes = optionalTextList(arguments_, "scope", MAX_REPOSITORY_SCOPES, MAX_QUERY_TEXT_UTF8_BYTES);
3956
+ const { kb } = await sessions.get();
3957
+ const result = await kb.search({
3958
+ query,
3959
+ mode,
3960
+ limit,
3961
+ ...selectedPassage === undefined ? {} : { selectedPassage },
3962
+ ...filters.length === 0 ? {} : { filters },
3963
+ ...tags === undefined ? {} : { tags },
3964
+ ...scopes === undefined ? {} : { repositoryScopes: scopes }
3965
+ });
3966
+ return toolSuccess(fitResult({ ...result, truncated: false }, [
3967
+ { kind: "list", field: "results" },
3968
+ { kind: "clear", field: "graph", empty: null }
3969
+ ]));
3970
+ }
3971
+ },
3972
+ {
3973
+ definition: {
3974
+ name: "list_notes",
3975
+ title: "List notes",
3976
+ description: "List notes with their frontmatter, filtered by frontmatter fields, tags, and repository scopes. " + "total counts every match before the limit.",
3977
+ inputSchema: {
3978
+ type: "object",
3979
+ additionalProperties: false,
3980
+ properties: {
3981
+ ...filterProperties,
3982
+ sort: {
3983
+ type: "string",
3984
+ minLength: 1,
3985
+ description: "title, path, inbound, outbound, or metadata.<path>. Defaults to path."
3986
+ },
3987
+ order: { type: "string", enum: ORDERS, default: "asc" },
3988
+ limit: { type: "integer", minimum: 1, maximum: MAX_LIST_LIMIT, default: DEFAULT_LIST_LIMIT }
3989
+ }
3990
+ },
3991
+ annotations: { title: "List notes", ...READ_ONLY }
3992
+ },
3993
+ handler: async (arguments_) => {
3994
+ allowOnly(arguments_, ["where", "has", "tags", "scope", "sort", "order", "limit"]);
3995
+ const filters = metadataFilters(arguments_);
3996
+ const tags = optionalTextList(arguments_, "tags", MAX_QUERY_TAGS, MAX_QUERY_TEXT_UTF8_BYTES);
3997
+ const scopes = optionalTextList(arguments_, "scope", MAX_REPOSITORY_SCOPES, MAX_QUERY_TEXT_UTF8_BYTES);
3998
+ const sortText = optionalText(arguments_, "sort", MAX_QUERY_METADATA_PATH_UTF8_BYTES + "metadata.".length);
3999
+ const sort = sortText === undefined ? { kind: "builtin", field: "path" } : querySort(sortText);
4000
+ const direction = optionalChoice(arguments_, "order", ORDERS) ?? "asc";
4001
+ const limit = optionalInteger(arguments_, "limit", 1, MAX_LIST_LIMIT) ?? DEFAULT_LIST_LIMIT;
4002
+ const { kb } = await sessions.get();
4003
+ const rows = kb.list({
4004
+ sort,
4005
+ direction,
4006
+ ...filters.length === 0 ? {} : { filters },
4007
+ ...tags === undefined ? {} : { tags },
4008
+ ...scopes === undefined ? {} : { repositoryScopes: scopes }
4009
+ });
4010
+ const notes = rows.slice(0, limit).map(listRow);
4011
+ return toolSuccess(fitResult({ notes, total: rows.length, truncated: rows.length > notes.length }, [{ kind: "list", field: "notes" }]));
4012
+ }
4013
+ },
4014
+ {
4015
+ definition: {
4016
+ name: "get_note",
4017
+ title: "Read a note",
4018
+ description: "Read one note as frontmatter, body, and revision. Pass revision as expected_revision to " + "update_note_body. When truncated is true the body is incomplete: do not send it back.",
4019
+ inputSchema: {
4020
+ type: "object",
4021
+ additionalProperties: false,
4022
+ required: ["id"],
4023
+ properties: { id: noteIdSchema }
4024
+ },
4025
+ annotations: { title: "Read a note", ...READ_ONLY }
4026
+ },
4027
+ handler: async (arguments_) => {
4028
+ allowOnly(arguments_, ["id"]);
4029
+ const id = noteIdArgument(arguments_, "id");
4030
+ for (let attempt = 0;attempt < 2; attempt += 1) {
4031
+ let before;
4032
+ try {
4033
+ before = await noteRevision(root, id);
4034
+ } catch (error) {
4035
+ const resolved = (await sessions.get()).kb.read(id, { maxBytes: 1 }).id;
4036
+ if (isMissingPath(error))
4037
+ return noteNotFound(id, resolved);
4038
+ throw error;
4039
+ }
4040
+ const { kb } = await sessions.get();
4041
+ const read = kb.read(id, { maxBytes: MAX_NOTE_READ_BYTES });
4042
+ const after = await noteRevision(root, id);
4043
+ if (read.id === id && before === after && (read.truncated || contentMatchesRevision(read.content, after))) {
4044
+ return toolSuccess(fitBody(noteView(read, after)));
4045
+ }
4046
+ await sessions.invalidate();
4047
+ }
4048
+ return toolFailure(`note ${id} changed while it was read; call get_note again`);
4049
+ }
4050
+ },
4051
+ {
4052
+ definition: {
4053
+ name: "backlinks",
4054
+ title: "Find backlinks",
4055
+ description: "Notes that link to this note, with the linking edges and authored relations.",
4056
+ inputSchema: {
4057
+ type: "object",
4058
+ additionalProperties: false,
4059
+ required: ["id"],
4060
+ properties: linkProperties
4061
+ },
4062
+ annotations: { title: "Find backlinks", ...READ_ONLY }
4063
+ },
4064
+ handler: async (arguments_) => {
4065
+ allowOnly(arguments_, ["id", "depth", "limit"]);
4066
+ const id = noteIdArgument(arguments_, "id");
4067
+ const depth = optionalInteger(arguments_, "depth", 1, MAX_LINK_DEPTH) ?? 1;
4068
+ const limit = optionalInteger(arguments_, "limit", 1, MAX_LINK_LIMIT) ?? DEFAULT_LINK_LIMIT;
4069
+ const { kb } = await sessions.get();
4070
+ return toolSuccess(fitNeighborhood(kb.backlinks(id, { depth, limit })));
4071
+ }
4072
+ },
4073
+ {
4074
+ definition: {
4075
+ name: "links",
4076
+ title: "Follow links",
4077
+ description: "Notes linked from or to this note, with the edges and authored relations between them.",
4078
+ inputSchema: {
4079
+ type: "object",
4080
+ additionalProperties: false,
4081
+ required: ["id"],
4082
+ properties: {
4083
+ ...linkProperties,
4084
+ direction: { type: "string", enum: LINK_DIRECTIONS, default: "both" }
4085
+ }
4086
+ },
4087
+ annotations: { title: "Follow links", ...READ_ONLY }
4088
+ },
4089
+ handler: async (arguments_) => {
4090
+ allowOnly(arguments_, ["id", "direction", "depth", "limit"]);
4091
+ const id = noteIdArgument(arguments_, "id");
4092
+ const direction = optionalChoice(arguments_, "direction", LINK_DIRECTIONS) ?? "both";
4093
+ const depth = optionalInteger(arguments_, "depth", 1, MAX_LINK_DEPTH) ?? 1;
4094
+ const limit = optionalInteger(arguments_, "limit", 1, MAX_LINK_LIMIT) ?? DEFAULT_LINK_LIMIT;
4095
+ const { kb } = await sessions.get();
4096
+ return toolSuccess(fitNeighborhood(kb.links(id, { direction, depth, limit })));
4097
+ }
4098
+ }
4099
+ ];
4100
+ }
4101
+ function fitNeighborhood(neighborhood) {
4102
+ return fitResult({ ...neighborhood }, [
4103
+ { kind: "list", field: "nodes" },
4104
+ { kind: "list", field: "edges" },
4105
+ { kind: "list", field: "relations" }
4106
+ ]);
4107
+ }
4108
+ var CONTEXT_KINDS = ["auto", "file", "directory"];
4109
+ function contextTool(repository, sessions, build) {
4110
+ return {
4111
+ needsRepository: true,
4112
+ definition: {
4113
+ name: "context",
4114
+ title: "Repository context",
4115
+ description: "Agent guides, context notes, and repository memory records that apply to a file or directory " + "in the configured repository. Issues are reported in the result.",
4116
+ inputSchema: {
4117
+ type: "object",
4118
+ additionalProperties: false,
4119
+ required: ["path"],
4120
+ properties: {
4121
+ path: {
4122
+ type: "string",
4123
+ minLength: 1,
4124
+ maxLength: MAX_NOTE_ID_UTF8_BYTES,
4125
+ description: "Repository-relative path, for example src/index.ts or . for the root."
4126
+ },
4127
+ kind: { type: "string", enum: CONTEXT_KINDS, default: "auto" }
4128
+ }
4129
+ },
4130
+ annotations: { title: "Repository context", ...READ_ONLY }
4131
+ },
4132
+ handler: async (arguments_) => {
4133
+ allowOnly(arguments_, ["path", "kind"]);
4134
+ const target = requiredText(arguments_, "path", MAX_NOTE_ID_UTF8_BYTES);
4135
+ const targetKind = optionalChoice(arguments_, "kind", CONTEXT_KINDS) ?? "auto";
4136
+ const { snapshot } = await sessions.get();
4137
+ const payload = await build(snapshot, { repositoryRoot: repository, target, targetKind });
4138
+ return toolSuccess(fitResult({ ...payload, truncated: false }, [
4139
+ { kind: "list", field: "contexts" },
4140
+ { kind: "list", field: "guides" },
4141
+ { kind: "list", field: "issues" },
4142
+ { kind: "clear", field: "records", empty: null }
4143
+ ]));
1035
4144
  }
1036
- return new Response(head ? null : "Not found", {
1037
- status: 404,
1038
- headers: { "Cache-Control": "no-store" }
1039
- });
1040
4145
  };
1041
4146
  }
1042
- async function serveSite(options, io = {}) {
1043
- const host = options.host ?? "127.0.0.1";
1044
- const port = options.port ?? 8080;
1045
- if (!Number.isSafeInteger(port) || port < 0 || port > 65535) {
1046
- throw new Error("Port must be an integer from 0 through 65535");
1047
- }
1048
- const rootReal = await realpath(options.root);
1049
- if (!(await stat(rootReal)).isDirectory()) {
1050
- throw new Error(`Site root is not a directory: ${options.root}`);
4147
+ function alreadyExists(id, path) {
4148
+ return toolFailure(`note ${id} already exists at ${path}; read it with get_note and change it with update_note_body`);
4149
+ }
4150
+ function authoringSuccess(result) {
4151
+ return toolSuccess({ ...result });
4152
+ }
4153
+ async function writing(sessions, write) {
4154
+ try {
4155
+ return await write();
4156
+ } finally {
4157
+ await sessions.invalidate();
1051
4158
  }
1052
- const listen = io.listen ?? ((init) => Bun.serve(init));
1053
- const server = listen({
1054
- hostname: host,
1055
- port,
1056
- fetch: await createServeHandler(rootReal)
1057
- });
1058
- const boundPort = server.port ?? port;
1059
- return Object.freeze({
1060
- root: rootReal,
1061
- host,
1062
- port: boundPort,
1063
- url: `http://${host}:${String(boundPort)}/`,
1064
- close: () => server.stop()
4159
+ }
4160
+ function writeTools(root, sessions) {
4161
+ return [
4162
+ {
4163
+ writes: true,
4164
+ definition: {
4165
+ name: "create_note",
4166
+ title: "Create a note",
4167
+ description: "Create a new Markdown note with frontmatter. Never overwrites: an existing note is an error. " + "The parent directory must already exist. Search first to avoid duplicates.",
4168
+ inputSchema: {
4169
+ type: "object",
4170
+ additionalProperties: false,
4171
+ required: ["id", "title"],
4172
+ properties: {
4173
+ id: noteIdSchema,
4174
+ title: { type: "string", minLength: 1, maxLength: MAX_QUERY_TEXT_UTF8_BYTES },
4175
+ type: { type: "string", minLength: 1, maxLength: MAX_QUERY_METADATA_PATH_UTF8_BYTES, default: "note" },
4176
+ tags: textListSchema(MAX_QUERY_TAGS, MAX_QUERY_TEXT_UTF8_BYTES, "Tags for the new note."),
4177
+ body: { type: "string", description: "Markdown after the frontmatter. Defaults to a heading with the title." }
4178
+ }
4179
+ },
4180
+ annotations: {
4181
+ title: "Create a note",
4182
+ readOnlyHint: false,
4183
+ destructiveHint: false,
4184
+ idempotentHint: false,
4185
+ openWorldHint: false
4186
+ }
4187
+ },
4188
+ handler: async (arguments_) => {
4189
+ allowOnly(arguments_, ["id", "title", "type", "tags", "body"]);
4190
+ const id = noteIdArgument(arguments_, "id");
4191
+ const title = requiredText(arguments_, "title", MAX_QUERY_TEXT_UTF8_BYTES);
4192
+ const type = optionalText(arguments_, "type", MAX_QUERY_METADATA_PATH_UTF8_BYTES) ?? "note";
4193
+ const tags = optionalTextList(arguments_, "tags", MAX_QUERY_TAGS, MAX_QUERY_TEXT_UTF8_BYTES);
4194
+ const body = bodyArgument(arguments_, "body", false);
4195
+ return writing(sessions, async () => {
4196
+ let result;
4197
+ try {
4198
+ result = await createNote(root, {
4199
+ id,
4200
+ title,
4201
+ type,
4202
+ ...tags === undefined ? {} : { tags },
4203
+ ...body === undefined ? {} : { body }
4204
+ });
4205
+ } catch (error) {
4206
+ if (error instanceof NoteAlreadyExistsError)
4207
+ return alreadyExists(id, error.path);
4208
+ if (isMissingPath(error)) {
4209
+ return toolFailure(`${errorMessage3(error)}; create_note does not create directories`);
4210
+ }
4211
+ throw error;
4212
+ }
4213
+ if (!result.changed)
4214
+ return alreadyExists(id, result.path);
4215
+ return authoringSuccess(result);
4216
+ });
4217
+ }
4218
+ },
4219
+ {
4220
+ writes: true,
4221
+ definition: {
4222
+ name: "update_note_body",
4223
+ title: "Replace a note body",
4224
+ description: "Replace the Markdown after a note's frontmatter. expected_revision must be the note's current " + "revision; a stale one is a conflict and nothing is written.",
4225
+ inputSchema: {
4226
+ type: "object",
4227
+ additionalProperties: false,
4228
+ required: ["id", "body", "expected_revision"],
4229
+ properties: {
4230
+ id: noteIdSchema,
4231
+ body: { type: "string" },
4232
+ expected_revision: revisionSchema
4233
+ }
4234
+ },
4235
+ annotations: {
4236
+ title: "Replace a note body",
4237
+ readOnlyHint: false,
4238
+ destructiveHint: true,
4239
+ idempotentHint: false,
4240
+ openWorldHint: false
4241
+ }
4242
+ },
4243
+ handler: async (arguments_) => {
4244
+ allowOnly(arguments_, ["id", "body", "expected_revision"]);
4245
+ const id = noteIdArgument(arguments_, "id");
4246
+ const body = bodyArgument(arguments_, "body", true) ?? "";
4247
+ const expectedRevision = revisionArgument(arguments_, "expected_revision", true);
4248
+ if (expectedRevision === undefined)
4249
+ throw new ToolArgumentError(`Missing argument "expected_revision".`);
4250
+ return writing(sessions, async () => {
4251
+ try {
4252
+ return authoringSuccess(await updateNoteBody(root, id, body, { expectedRevision }));
4253
+ } catch (error) {
4254
+ if (isMissingPath(error))
4255
+ return noteNotFound(id);
4256
+ throw error;
4257
+ }
4258
+ });
4259
+ }
4260
+ },
4261
+ {
4262
+ writes: true,
4263
+ definition: {
4264
+ name: "add_relation",
4265
+ title: "Add a relation",
4266
+ description: "Add a typed relation from one note to another in the source note's frontmatter. " + "Adding an existing relation changes nothing.",
4267
+ inputSchema: {
4268
+ type: "object",
4269
+ additionalProperties: false,
4270
+ required: ["source", "predicate", "target"],
4271
+ properties: {
4272
+ source: noteIdSchema,
4273
+ predicate: {
4274
+ type: "string",
4275
+ minLength: 1,
4276
+ maxLength: MAX_QUERY_METADATA_PATH_UTF8_BYTES,
4277
+ description: "Relation name, for example supports or depends_on."
4278
+ },
4279
+ target: noteIdSchema,
4280
+ expected_revision: revisionSchema
4281
+ }
4282
+ },
4283
+ annotations: {
4284
+ title: "Add a relation",
4285
+ readOnlyHint: false,
4286
+ destructiveHint: false,
4287
+ idempotentHint: true,
4288
+ openWorldHint: false
4289
+ }
4290
+ },
4291
+ handler: async (arguments_) => {
4292
+ allowOnly(arguments_, ["source", "predicate", "target", "expected_revision"]);
4293
+ const source = noteIdArgument(arguments_, "source");
4294
+ const predicate = requiredText(arguments_, "predicate", MAX_QUERY_METADATA_PATH_UTF8_BYTES);
4295
+ const target = requiredText(arguments_, "target", MAX_NOTE_ID_UTF8_BYTES);
4296
+ const expectedRevision = revisionArgument(arguments_, "expected_revision", false);
4297
+ return writing(sessions, async () => {
4298
+ try {
4299
+ return authoringSuccess(await addNoteRelation(root, source, predicate, target, expectedRevision === undefined ? {} : { expectedRevision }));
4300
+ } catch (error) {
4301
+ if (!isMissingPath(error))
4302
+ throw error;
4303
+ const sourceMissing = await noteRevision(root, source).then(() => false, isMissingPath);
4304
+ return noteNotFound(sourceMissing ? source : target);
4305
+ }
4306
+ });
4307
+ }
4308
+ }
4309
+ ];
4310
+ }
4311
+ function catalogEntries(options, sessions) {
4312
+ const [search, ...reads] = readTools(options.root, sessions);
4313
+ const context = options.repository === undefined || options.context === undefined ? [] : [contextTool(options.repository, sessions, options.context)];
4314
+ return [
4315
+ ...search === undefined ? [] : [search],
4316
+ ...context,
4317
+ ...reads,
4318
+ ...writeTools(options.root, sessions)
4319
+ ];
4320
+ }
4321
+ function visibleEntries(entries, options) {
4322
+ return entries.filter((entry) => !(options.readOnly && entry.writes === true) && !(options.repository === undefined && entry.needsRepository === true));
4323
+ }
4324
+ function createToolCatalog(options) {
4325
+ const sessions = createVaultSessions({
4326
+ root: options.root,
4327
+ ...options.repository === undefined ? {} : { repository: options.repository },
4328
+ ...options.dependencies === undefined ? {} : { dependencies: options.dependencies },
4329
+ ...options.warn === undefined ? {} : { warn: options.warn }
1065
4330
  });
4331
+ const entries = visibleEntries(catalogEntries(options, sessions), options);
4332
+ const handlers = new Map(entries.map((entry) => [entry.definition.name, entry.handler]));
4333
+ return {
4334
+ tools: entries.map((entry) => entry.definition),
4335
+ async call(name, arguments_) {
4336
+ const handler = handlers.get(name);
4337
+ if (handler === undefined)
4338
+ return toolFailure(`Unknown tool: ${name}`);
4339
+ try {
4340
+ return await handler(arguments_);
4341
+ } catch (error) {
4342
+ return failureFor(error);
4343
+ }
4344
+ },
4345
+ close: () => sessions.close()
4346
+ };
4347
+ }
4348
+ function serverInstructions(options) {
4349
+ const writes = options.readOnly ? "This server is read-only: the write tools are off." : "Call search before create_note to avoid duplicates, and pass get_note's revision as " + "expected_revision to update_note_body.";
4350
+ return `Wordcell vault at ${options.root}. Notes are Markdown files addressed by vault-relative IDs ` + `without .md, for example notes/decision. ${writes}`;
1066
4351
  }
1067
4352
 
1068
4353
  // src/rerank-credentials.ts
1069
4354
  import { constants as constants2 } from "fs";
1070
- import { open } from "fs/promises";
4355
+ import { open as open2 } from "fs/promises";
1071
4356
  import { homedir } from "os";
1072
- import { isAbsolute as isAbsolute2, join as join3 } from "path";
4357
+ import { isAbsolute as isAbsolute2, join as join4 } from "path";
1073
4358
  function unavailable(message) {
1074
4359
  return { id: "typesafe", rerank: async () => ({ status: "unavailable", message }) };
1075
4360
  }
@@ -1082,10 +4367,10 @@ async function createCliTypeSafeReranker(environment = process.env, homeDirector
1082
4367
  if (explicitFile !== undefined && !isAbsolute2(explicitFile) || configDirectory !== undefined && configDirectory !== "" && !isAbsolute2(configDirectory)) {
1083
4368
  return unavailable("TypeSafe credential file configuration requires absolute paths.");
1084
4369
  }
1085
- const path = explicitFile ?? join3(configDirectory || join3(homeDirectory, ".config"), "wordcell", "typesafe-api-key");
4370
+ const path = explicitFile ?? join4(configDirectory || join4(homeDirectory, ".config"), "wordcell", "typesafe-api-key");
1086
4371
  let handle;
1087
4372
  try {
1088
- handle = await open(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
4373
+ handle = await open2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
1089
4374
  const metadata = await handle.stat();
1090
4375
  const uid = process.getuid?.();
1091
4376
  if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 1 || metadata.size > 514 || process.platform !== "win32" && ((metadata.mode & 63) !== 0 || metadata.uid !== uid)) {
@@ -1106,12 +4391,41 @@ async function createCliTypeSafeReranker(environment = process.env, homeDirector
1106
4391
  }
1107
4392
 
1108
4393
  // src/cli-program.ts
1109
- var defaultOutput2 = {
4394
+ var defaultOutput2 = terminalOutput({
1110
4395
  stdout: (value) => process.stdout.write(value),
1111
4396
  stderr: (value) => process.stderr.write(value)
1112
- };
4397
+ });
4398
+ async function readBoundedStdinUtf8(source, maximumBytes, label) {
4399
+ if (source.isTTY) {
4400
+ throw new Error("--body-file - reads the note body from standard input, but standard input is a terminal; pipe the body in or name a file");
4401
+ }
4402
+ const chunks = [];
4403
+ let total = 0;
4404
+ for await (const chunk of source.chunks) {
4405
+ total += chunk.byteLength;
4406
+ if (total > maximumBytes) {
4407
+ throw new Error(`${label} exceeds the ${maximumBytes}-byte limit`);
4408
+ }
4409
+ chunks.push(chunk);
4410
+ }
4411
+ const bytes = new Uint8Array(total);
4412
+ let offset = 0;
4413
+ for (const chunk of chunks) {
4414
+ bytes.set(chunk, offset);
4415
+ offset += chunk.byteLength;
4416
+ }
4417
+ let text;
4418
+ try {
4419
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4420
+ } catch (error) {
4421
+ throw new Error(`${label} is not valid UTF-8`, { cause: error });
4422
+ }
4423
+ if (text.trim() === "")
4424
+ throw new Error(`${label} from standard input is empty`);
4425
+ return text;
4426
+ }
1113
4427
  async function readBoundedUtf8(path, maximumBytes, label) {
1114
- const handle = await open2(path, "r");
4428
+ const handle = await open3(path, "r");
1115
4429
  try {
1116
4430
  const bytes = new Uint8Array(maximumBytes + 1);
1117
4431
  let offset = 0;
@@ -1143,67 +4457,10 @@ async function loadSearchRulesFile(path) {
1143
4457
  }
1144
4458
  return parseSearchRules(input);
1145
4459
  }
1146
- var usage = `wordcell \u2014 a local Markdown knowledge base with superpowers
1147
-
1148
- Start here (no account or model needed):
1149
- wordcell init kb
1150
- wordcell note create notes/decision --title "A decision" --body "Keep retries bounded." --root kb
1151
- wordcell search "retries" --root kb --mode exact
1152
-
1153
- Already have Markdown? Search it with --root <your-notes-directory>.
1154
- Use --mode exact for model-free search; optional hybrid search needs a local index.
1155
- Quick start and examples: https://wordcell.io/docs
1156
-
1157
- Usage:
1158
- wordcell init [directory] [--json]
1159
- wordcell clip <url|current> [capture options]
1160
- wordcell capture show <bundle> [--verify-assets] [--include-source-html] [--json]
1161
- wordcell capture verify <bundle> [--verify-assets] [--json]
1162
- wordcell capture diff <bundle> [--repo <repository>] [--ref <ref>] [--json]
1163
- wordcell url-metadata tool <build|check>
1164
- wordcell url-metadata backfill [metadata options]
1165
- wordcell inspect <url> [capture options]
1166
- wordcell pdf <file-or-url> [PDF options]
1167
- wordcell refresh [--root <directory>] [--index <path>] [--json]
1168
- wordcell check [--root <directory>] [--index <path>] [--no-catalog] [--repo <repository>] [--json]
1169
- wordcell catalog [--root <directory>] [--index <path>] [--json]
1170
- wordcell graph [--root <directory>] [--index <path>] [--json]
1171
- wordcell graph rebuild [--fresh] [--root <directory>] [--index <path>] [--json]
1172
- wordcell graph verify [--root <directory>] [--index <path>] [--json]
1173
- 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]
1174
- wordcell backlinks <note> [--root <directory>] [--index <path>] [--json]
1175
- wordcell links <note> [--root <directory>] [--direction <in|out|both>] [--depth <count>] [--limit <count>] [--json]
1176
- wordcell note create <id> --title <title> [--type <type>] [--tag <tag>] [--body <markdown> | --body-file <path>] [--root <directory>] [--json]
1177
- wordcell relation add <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
1178
- wordcell relation remove <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
1179
- wordcell relation list <note> [--root <directory>] [--json]
1180
- wordcell percolate [note] [--proofs] [--root <directory>] [--min-support <count>] [--limit <count>] [--json]
1181
- wordcell list [--root <directory>] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--sort <field>] [--order <asc|desc>] [--limit <count>] [--json]
1182
- wordcell index [--root <directory>] [--database <path>] [--force] [--json]
1183
- 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]
1184
- wordcell history <note> [--root <directory>] [--repo <repository>] [--limit <count>] [--cochanged-limit <count>] [--json]
1185
- wordcell history search <query-or-path> [--root <directory>] [--repo <repository>] [--limit <count>] [--commit-limit <count>] [--cochanged-limit <count>] [--json]
1186
- 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]
1187
- 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]
1188
- wordcell portfolio audit --registry <file> --workspace <directory> (--all | --shared | --vault <owner/id>...) [--strict] [--json]
1189
- 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]
1190
- wordcell serve --root <directory> [--host <host>] [--port <port>] [--json]
1191
- wordcell inbox [--root <directory>] [--source-prefix <directory>] [--limit <count>] [--json]
1192
- wordcell context <repository-path> [--root <vault>] [--repo <repository>] [--kind <auto|file|directory>] [--json]
1193
- wordcell agents identity <repository-scope> [--json]
1194
- wordcell agents check [--root <vault>] [--repo <repository>] [--json]
1195
- wordcell agents audit [--root <vault>] [--repo <repository>] [--json]
1196
- wordcell doctor [--json]
1197
- wordcell adapters [--json]
1198
- wordcell support [--json | protocol --json | offer --json | shown <id> | release <id> | dismiss | snooze | enable | status --json]
1199
-
1200
- Optional support uses separate local preferences. HRANESS_SUPPORT=off or
1201
- HRANESS_SUPPORT_AUDIENCE=off keeps invitations quiet; explicit support remains available.
1202
- Agents receive discovery on stderr after useful standalone work. Human mode requires
1203
- HRANESS_SUPPORT_AUDIENCE=human and an interactive stderr. No command signs up or pays.
1204
-
1205
- Run \`wordcell clip --help\` for web capture options or \`wordcell pdf --help\` for PDF conversion options.
1206
- `;
4460
+ var usage = rootHelp();
4461
+ function processStdin() {
4462
+ return { isTTY: process.stdin.isTTY === true, chunks: process.stdin };
4463
+ }
1207
4464
  function safe(value) {
1208
4465
  return sanitizeTerminalLine(redactSensitiveText(value));
1209
4466
  }
@@ -1424,7 +4681,7 @@ function parseCatalogCommand(arguments_) {
1424
4681
  }
1425
4682
  };
1426
4683
  }
1427
- function metadataScalar(raw) {
4684
+ function metadataScalar2(raw) {
1428
4685
  const value = raw.trim();
1429
4686
  if (value.startsWith('"') || value.endsWith('"')) {
1430
4687
  try {
@@ -1457,7 +4714,7 @@ function metadataScalar(raw) {
1457
4714
  }
1458
4715
  return { ok: true, value };
1459
4716
  }
1460
- function querySort(raw) {
4717
+ function querySort2(raw) {
1461
4718
  const value = raw.trim();
1462
4719
  if (value === "title" || value === "path" || value === "inbound" || value === "outbound") {
1463
4720
  return { kind: "builtin", field: value };
@@ -1522,7 +4779,7 @@ function parseListCommand(arguments_) {
1522
4779
  const path = equals === -1 ? "" : value.slice(0, equals).trim();
1523
4780
  if (path === "")
1524
4781
  return { ok: false, message: "--where requires path=value" };
1525
- const scalar = metadataScalar(value.slice(equals + 1));
4782
+ const scalar = metadataScalar2(value.slice(equals + 1));
1526
4783
  if (!scalar.ok)
1527
4784
  return scalar;
1528
4785
  if (filters.length >= MAX_QUERY_FILTERS) {
@@ -1533,7 +4790,7 @@ function parseListCommand(arguments_) {
1533
4790
  }
1534
4791
  filters.push({ kind: "equals", path, value: scalar.value });
1535
4792
  } else if (argument === "--sort") {
1536
- const parsed = querySort(value);
4793
+ const parsed = querySort2(value);
1537
4794
  if (parsed === null)
1538
4795
  return { ok: false, message: "--sort requires a field" };
1539
4796
  sort = parsed;
@@ -1707,7 +4964,7 @@ function parsePublishCommand(arguments_) {
1707
4964
  const path = equals === -1 ? "" : value.slice(0, equals).trim();
1708
4965
  if (path === "")
1709
4966
  return { ok: false, message: "--where requires path=value" };
1710
- const scalar = metadataScalar(value.slice(equals + 1));
4967
+ const scalar = metadataScalar2(value.slice(equals + 1));
1711
4968
  if (!scalar.ok)
1712
4969
  return scalar;
1713
4970
  if (filters.length >= MAX_QUERY_FILTERS) {
@@ -1802,6 +5059,41 @@ function parseServeCommand(arguments_) {
1802
5059
  return { ok: false, message: "serve requires --root <directory>" };
1803
5060
  return { ok: true, value: { kind: "serve", root, host, port, json } };
1804
5061
  }
5062
+ function parseMcpCommand(arguments_) {
5063
+ let root;
5064
+ let repository;
5065
+ let readOnly = false;
5066
+ for (let cursor = 0;cursor < arguments_.length; cursor += 1) {
5067
+ const argument = arguments_[cursor];
5068
+ if (argument === undefined)
5069
+ continue;
5070
+ if (argument === "--read-only") {
5071
+ readOnly = true;
5072
+ continue;
5073
+ }
5074
+ if (argument === "--root" || argument === "--repo") {
5075
+ const value = readValue(arguments_, cursor);
5076
+ if (value === null || value === "")
5077
+ return { ok: false, message: `${argument} requires a value` };
5078
+ if (argument === "--root")
5079
+ root = value;
5080
+ else
5081
+ repository = value;
5082
+ cursor += 1;
5083
+ continue;
5084
+ }
5085
+ return {
5086
+ ok: false,
5087
+ message: argument.startsWith("--") ? "unknown mcp option" : "mcp does not accept positional arguments"
5088
+ };
5089
+ }
5090
+ if (root === undefined)
5091
+ return { ok: false, message: "mcp requires --root <vault>" };
5092
+ return {
5093
+ ok: true,
5094
+ value: { kind: "mcp", root, ...repository === undefined ? {} : { repository }, readOnly }
5095
+ };
5096
+ }
1805
5097
  function parseInboxCommand(arguments_) {
1806
5098
  let root = ".";
1807
5099
  let index;
@@ -1876,6 +5168,7 @@ function parseSemanticCommand(command, arguments_) {
1876
5168
  let minScore;
1877
5169
  let rerank;
1878
5170
  let rerankLimit;
5171
+ let selectedPassage = false;
1879
5172
  let graphDepth;
1880
5173
  let noGraph = false;
1881
5174
  let history = false;
@@ -1902,6 +5195,10 @@ function parseSemanticCommand(command, arguments_) {
1902
5195
  noGraph = true;
1903
5196
  continue;
1904
5197
  }
5198
+ if (argument === "--selected-passage" && command === "search") {
5199
+ selectedPassage = true;
5200
+ continue;
5201
+ }
1905
5202
  if (argument === "--no-history" && command === "search") {
1906
5203
  noHistory = true;
1907
5204
  continue;
@@ -1958,7 +5255,7 @@ function parseSemanticCommand(command, arguments_) {
1958
5255
  const path = equals === -1 ? "" : value.slice(0, equals).trim();
1959
5256
  if (path === "")
1960
5257
  return { ok: false, message: "--where requires path=value" };
1961
- const scalar = metadataScalar(value.slice(equals + 1));
5258
+ const scalar = metadataScalar2(value.slice(equals + 1));
1962
5259
  if (!scalar.ok)
1963
5260
  return scalar;
1964
5261
  if (filters.length >= MAX_QUERY_FILTERS) {
@@ -2127,6 +5424,7 @@ function parseSemanticCommand(command, arguments_) {
2127
5424
  ...minScore === undefined ? {} : { minScore },
2128
5425
  ...rerank === undefined ? {} : { rerank },
2129
5426
  ...rerankLimit === undefined ? {} : { rerankLimit },
5427
+ ...selectedPassage ? { selectedPassage: true } : {},
2130
5428
  query,
2131
5429
  json
2132
5430
  }
@@ -2532,6 +5830,65 @@ function parseNoteCommand(arguments_) {
2532
5830
  }
2533
5831
  };
2534
5832
  }
5833
+ function parseImportCommand(arguments_) {
5834
+ if (arguments_[0] !== "supermemory")
5835
+ return { ok: false, message: "import requires supermemory" };
5836
+ let root = ".";
5837
+ let prefix;
5838
+ let dryRun = false;
5839
+ let json = false;
5840
+ const files = [];
5841
+ for (let cursor = 1;cursor < arguments_.length; cursor += 1) {
5842
+ const argument = arguments_[cursor];
5843
+ if (argument === undefined)
5844
+ continue;
5845
+ if (argument === "--json") {
5846
+ json = true;
5847
+ continue;
5848
+ }
5849
+ if (argument === "--dry-run") {
5850
+ dryRun = true;
5851
+ continue;
5852
+ }
5853
+ if (argument === "--root" || argument === "--prefix") {
5854
+ const value = readValue(arguments_, cursor);
5855
+ if (value === null || value === "")
5856
+ return { ok: false, message: `${argument} requires a value` };
5857
+ if (argument === "--root")
5858
+ root = value;
5859
+ else
5860
+ prefix = value;
5861
+ cursor += 1;
5862
+ continue;
5863
+ }
5864
+ if (argument.startsWith("--"))
5865
+ return { ok: false, message: "unknown import supermemory option" };
5866
+ files.push(argument);
5867
+ }
5868
+ if (files.length === 0)
5869
+ return { ok: false, message: "import supermemory requires at least one export file" };
5870
+ if (files.length > MAX_IMPORT_FILES) {
5871
+ return { ok: false, message: `import supermemory accepts at most ${MAX_IMPORT_FILES} export files` };
5872
+ }
5873
+ if (prefix !== undefined) {
5874
+ try {
5875
+ prefix = normalizePrefix(prefix);
5876
+ } catch (error) {
5877
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
5878
+ }
5879
+ }
5880
+ return {
5881
+ ok: true,
5882
+ value: {
5883
+ kind: "import-supermemory",
5884
+ root,
5885
+ files,
5886
+ ...prefix === undefined ? {} : { prefix },
5887
+ dryRun,
5888
+ json
5889
+ }
5890
+ };
5891
+ }
2535
5892
  function isNoteRevision(value) {
2536
5893
  return /^sha256:[0-9a-f]{64}$/u.test(value);
2537
5894
  }
@@ -2796,11 +6153,73 @@ function parsePortfolioCommand(arguments_) {
2796
6153
  }
2797
6154
  };
2798
6155
  }
2799
- function parseArguments(arguments_) {
2800
- const command = arguments_[0];
2801
- if (command === undefined || command === "help" || command === "--help" || command === "-h") {
6156
+ function isHelpFlag(argument) {
6157
+ return argument === "--help" || argument === "-h";
6158
+ }
6159
+ function isDelegatedHelp(command) {
6160
+ if (command.kind === "clip")
6161
+ return command.arguments.length === 1 && command.arguments[0] === "help";
6162
+ if (command.kind === "pdf" || command.kind === "url-metadata") {
6163
+ return command.arguments.length === 1 && command.arguments[0] === "--help";
6164
+ }
6165
+ return false;
6166
+ }
6167
+ function delegatedHelp(first) {
6168
+ if (first === "clip" || first === "inspect")
6169
+ return { kind: "clip", arguments: ["help"] };
6170
+ if (first === "pdf")
6171
+ return { kind: "pdf", arguments: ["--help"] };
6172
+ if (first === "url-metadata")
6173
+ return { kind: "url-metadata", arguments: ["--help"] };
6174
+ return;
6175
+ }
6176
+ function parseHelpTopic(words) {
6177
+ const topic = words.filter((word) => !isHelpFlag(word) && word !== "--json");
6178
+ if (topic.length === 0)
2802
6179
  return { ok: true, value: { kind: "help" } };
6180
+ if (topic.length === 1 && topic[0] === "advanced")
6181
+ return { ok: true, value: { kind: "help", topic: "advanced" } };
6182
+ const delegated = delegatedHelp(topic[0] ?? "");
6183
+ if (delegated !== undefined)
6184
+ return { ok: true, value: delegated };
6185
+ const id = resolveCommandId(topic);
6186
+ if (id === undefined)
6187
+ return { ok: false, message: "unknown help topic" };
6188
+ return { ok: true, value: { kind: "help", topic: id } };
6189
+ }
6190
+ function embeddedHelp(arguments_) {
6191
+ const separator = arguments_.indexOf("--");
6192
+ const end = separator === -1 ? arguments_.length : separator;
6193
+ const index = arguments_.slice(0, end).findIndex(isHelpFlag);
6194
+ if (index <= 0)
6195
+ return;
6196
+ const delegated = delegatedHelp(arguments_[0] ?? "");
6197
+ if (delegated !== undefined) {
6198
+ return index === 1 || index === end - 1 ? { ok: true, value: delegated } : undefined;
2803
6199
  }
6200
+ const id = resolveCommandId(arguments_.slice(0, index).filter((word) => !word.startsWith("-")));
6201
+ if (id === undefined)
6202
+ return;
6203
+ const words = commandWordCount(id);
6204
+ const previous = arguments_[index - 1] ?? "";
6205
+ const flag = arguments_[index];
6206
+ const lastArgument = index === end - 1 && (flag === "--help" || !valueOptions(id).has(previous));
6207
+ if (index !== words && !lastArgument)
6208
+ return;
6209
+ return { ok: true, value: { kind: "help", topic: id } };
6210
+ }
6211
+ function parseArguments(arguments_) {
6212
+ const command = arguments_[0];
6213
+ if (command === undefined)
6214
+ return { ok: true, value: { kind: "help", topic: "start" } };
6215
+ if (command === "help" || isHelpFlag(command))
6216
+ return parseHelpTopic(arguments_.slice(1));
6217
+ if (command === "--version" || command === "-V" || command === "-v" || command === "version") {
6218
+ return { ok: true, value: { kind: "version", json: arguments_.includes("--json") } };
6219
+ }
6220
+ const help = embeddedHelp(arguments_);
6221
+ if (help !== undefined)
6222
+ return help;
2804
6223
  if (command === "capture" && arguments_[1] === "diff") {
2805
6224
  return parseCaptureDiffCommand(arguments_.slice(2));
2806
6225
  }
@@ -2869,6 +6288,8 @@ function parseArguments(arguments_) {
2869
6288
  return parseAgentsCommand(arguments_.slice(1));
2870
6289
  if (command === "note")
2871
6290
  return parseNoteCommand(arguments_.slice(1));
6291
+ if (command === "import")
6292
+ return parseImportCommand(arguments_.slice(1));
2872
6293
  if (command === "relation")
2873
6294
  return parseRelationCommand(arguments_.slice(1));
2874
6295
  if (command === "percolate")
@@ -2877,6 +6298,8 @@ function parseArguments(arguments_) {
2877
6298
  return parsePublishCommand(arguments_.slice(1));
2878
6299
  if (command === "serve")
2879
6300
  return parseServeCommand(arguments_.slice(1));
6301
+ if (command === "mcp")
6302
+ return parseMcpCommand(arguments_.slice(1));
2880
6303
  return { ok: false, message: "unknown command" };
2881
6304
  }
2882
6305
  function embeddingCount(result) {
@@ -2893,6 +6316,22 @@ function renderSemanticIndex(result) {
2893
6316
  ].join(`
2894
6317
  `);
2895
6318
  }
6319
+ function searchForDisplay(result) {
6320
+ return {
6321
+ ...result,
6322
+ results: result.results.map((hit) => {
6323
+ const passage = hit.selectedPassage;
6324
+ if (passage?.status !== "selected")
6325
+ return hit;
6326
+ const texts = [passage.text, ...passage.headings.map(({ text }) => text)];
6327
+ if (texts.every((text) => sanitizeTerminalText(redactSensitiveText(text)) === text.replaceAll(`\r
6328
+ `, `
6329
+ `).replaceAll("\t", " ")))
6330
+ return hit;
6331
+ return { ...hit, selectedPassage: { status: "unavailable", reason: "display-sanitized" } };
6332
+ })
6333
+ };
6334
+ }
2896
6335
  function renderKnowledgeBaseSearch(result) {
2897
6336
  const lines = [
2898
6337
  `${result.mode[0]?.toLocaleUpperCase("en-US") ?? ""}${result.mode.slice(1)} results for \u201C${safe(result.query)}\u201D (${result.results.length})${result.partial ? " [partial]" : ""}`
@@ -2915,8 +6354,21 @@ function renderKnowledgeBaseSearch(result) {
2915
6354
  const location = `${safe(hit.path)}${hit.line === undefined ? "" : `:${hit.line}`}`;
2916
6355
  const evidence = hit.evidence.map((item) => `${item.kind}#${searchEvidenceRank(item)}`).join(", ");
2917
6356
  lines.push(` ${hit.rank}. ${hit.score.toFixed(3)} ${location} \u2014 ${safe(hit.title)} [${safe(evidence)}]`);
2918
- if (hit.snippet !== "")
2919
- lines.push(` ${safe(hit.snippet)}`);
6357
+ const passage = hit.selectedPassage;
6358
+ if (passage?.status === "selected") {
6359
+ lines.push(` Selected passage: ${safe(hit.path)}:${passage.startLine}-${passage.endLine}` + (passage.clippedStart || passage.clippedEnd ? " (clipped excerpt)" : ""));
6360
+ for (const heading of passage.headings) {
6361
+ lines.push(` Section: ${safe(heading.text)}${heading.truncated ? " (clipped)" : ""}`);
6362
+ }
6363
+ for (const line of passage.text.split(/\r?\n/u))
6364
+ lines.push(` ${safe(line)}`);
6365
+ } else {
6366
+ if (hit.snippet !== "")
6367
+ lines.push(` ${safe(hit.snippet)}`);
6368
+ if (passage !== undefined) {
6369
+ lines.push(` Selected passage: ${passage.status} (${safe(passage.reason)}); showing the search snippet.`);
6370
+ }
6371
+ }
2920
6372
  }
2921
6373
  if ((result.graph?.related.length ?? 0) > 0) {
2922
6374
  lines.push(` Related graph context: ${result.graph?.related.length ?? 0}`);
@@ -2930,7 +6382,14 @@ function renderKnowledgeBaseSearch(result) {
2930
6382
  `;
2931
6383
  }
2932
6384
  async function runSemantic(command, output, dependencies) {
6385
+ const terminal = dependencies.terminal ?? processTerminal();
6386
+ const person = !command.json && speaksToPerson(terminal);
2933
6387
  if (command.kind === "index") {
6388
+ if (person) {
6389
+ const style = terminalStyle(terminal.env, terminal.stderrIsTTY);
6390
+ output.stderr(`${symbol("progress", style)} Indexing ${safe(command.root)}. ` + `The first run downloads a search model of about 300 MB and can take a few minutes.
6391
+ `);
6392
+ }
2934
6393
  const result = await (dependencies.indexSemanticVault ?? indexSemanticVault)({
2935
6394
  root: command.root,
2936
6395
  ...command.database === undefined ? {} : { database: command.database },
@@ -2939,6 +6398,9 @@ async function runSemantic(command, output, dependencies) {
2939
6398
  output.stdout(command.json ? terminalSafeJson(result) : sanitizeTerminalText(renderSemanticIndex(result)));
2940
6399
  return 0;
2941
6400
  }
6401
+ const indexExists = dependencies.semanticIndexExists ?? (dependencies.openKnowledgeBase === undefined ? defaultSemanticIndexExists : undefined);
6402
+ const withoutIndex = command.mode === undefined && command.database === undefined && command.minScore === undefined && command.rerank === undefined && indexExists !== undefined && !await indexExists(command.root);
6403
+ const mode = withoutIndex ? "exact" : command.mode;
2942
6404
  const searchRules = command.rulesPath === undefined ? undefined : await loadSearchRulesFile(command.rulesPath);
2943
6405
  const kb = await (dependencies.openKnowledgeBase ?? openKnowledgeBase)({
2944
6406
  root: command.root,
@@ -2949,7 +6411,7 @@ async function runSemantic(command, output, dependencies) {
2949
6411
  try {
2950
6412
  const result = await kb.search({
2951
6413
  query: command.query,
2952
- ...command.mode === undefined ? {} : { mode: command.mode },
6414
+ ...mode === undefined ? {} : { mode },
2953
6415
  ordering: command.ordering,
2954
6416
  filters: command.filters,
2955
6417
  tags: command.tags,
@@ -2959,6 +6421,7 @@ async function runSemantic(command, output, dependencies) {
2959
6421
  ...command.limit === undefined ? {} : { limit: command.limit },
2960
6422
  ...command.candidateLimit === undefined ? {} : { candidateLimit: command.candidateLimit },
2961
6423
  ...command.minScore === undefined ? {} : { minScore: command.minScore },
6424
+ ...command.selectedPassage === undefined ? {} : { selectedPassage: command.selectedPassage },
2962
6425
  ...command.rerank === undefined ? {} : {
2963
6426
  rerank: {
2964
6427
  engine: command.rerank,
@@ -2966,7 +6429,12 @@ async function runSemantic(command, output, dependencies) {
2966
6429
  }
2967
6430
  }
2968
6431
  });
2969
- output.stdout(command.json ? terminalSafeJson(result) : sanitizeTerminalText(renderKnowledgeBaseSearch(result)));
6432
+ const display = searchForDisplay(result);
6433
+ output.stdout(command.json ? terminalSafeJson(display) : sanitizeTerminalText(renderKnowledgeBaseSearch(display)));
6434
+ if (withoutIndex && person) {
6435
+ output.stderr("Searched without an index, so only exact words matched. " + `For meaning-based search, run wordcell index --root ${safe(shellWord(command.root))} ` + `(downloads about 300 MB once).
6436
+ `);
6437
+ }
2970
6438
  return 0;
2971
6439
  } finally {
2972
6440
  await kb.close();
@@ -3114,9 +6582,9 @@ async function runEvaluation(command, output, dependencies) {
3114
6582
  ...embeddingModelFile === undefined ? {} : { embeddingModelFile }
3115
6583
  });
3116
6584
  try {
3117
- const byId = new Map(evaluation.retrievers.map((retriever) => [retriever.id, retriever]));
6585
+ const byId2 = new Map(evaluation.retrievers.map((retriever) => [retriever.id, retriever]));
3118
6586
  const retrievers = command.retrievers.map((id) => {
3119
- const retriever = byId.get(id);
6587
+ const retriever = byId2.get(id);
3120
6588
  if (retriever === undefined)
3121
6589
  throw new Error(`Evaluation adapter ${id} is unavailable.`);
3122
6590
  return retriever;
@@ -3406,9 +6874,9 @@ function renderList(rows) {
3406
6874
  const lines = [`Notes (${rows.length})`];
3407
6875
  if (rows.length === 0)
3408
6876
  lines.push(" None.");
3409
- for (const row of rows) {
3410
- const tags = row.tags.length === 0 ? "" : ` #${row.tags.map(safe).join(" #")}`;
3411
- lines.push(` ${safe(row.path)} \u2014 ${safe(row.title)} \u2190 ${row.inboundContextualCount} \u2192 ${row.outboundContextualCount}${tags}`);
6877
+ for (const row2 of rows) {
6878
+ const tags = row2.tags.length === 0 ? "" : ` #${row2.tags.map(safe).join(" #")}`;
6879
+ lines.push(` ${safe(row2.path)} \u2014 ${safe(row2.title)} \u2190 ${row2.inboundContextualCount} \u2192 ${row2.outboundContextualCount}${tags}`);
3412
6880
  }
3413
6881
  return `${lines.join(`
3414
6882
  `)}
@@ -3441,7 +6909,7 @@ function renderAuthoringResult(verb, result) {
3441
6909
  `);
3442
6910
  }
3443
6911
  async function runNoteCreate(command, output, dependencies) {
3444
- const body = command.body ?? (command.bodyFile === undefined ? undefined : await readBoundedUtf8(command.bodyFile, 16 * 1024 * 1024, "note body"));
6912
+ const body = command.body ?? (command.bodyFile === undefined ? undefined : command.bodyFile === "-" ? await readBoundedStdinUtf8(dependencies.stdin ?? processStdin(), 16 * 1024 * 1024, "note body") : await readBoundedUtf8(command.bodyFile, 16 * 1024 * 1024, "note body"));
3445
6913
  const result = await (dependencies.createNote ?? createNote)(command.root, {
3446
6914
  ...command.input,
3447
6915
  ...body === undefined ? {} : { body }
@@ -3449,6 +6917,59 @@ async function runNoteCreate(command, output, dependencies) {
3449
6917
  output.stdout(command.json ? terminalSafeJson(result) : sanitizeTerminalText(renderAuthoringResult("Created", result)));
3450
6918
  return 0;
3451
6919
  }
6920
+ function countOf(count, noun) {
6921
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
6922
+ }
6923
+ function shellWord(value) {
6924
+ return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
6925
+ }
6926
+ function renderImportReport(report) {
6927
+ const { counts } = report;
6928
+ const tally = `created ${counts.created}, updated ${counts.updated}, skipped ${counts.skipped}, ` + `conflicts ${counts.conflicts}, rejected ${counts.rejected}`;
6929
+ const scope = `${countOf(report.items.length, "item")} from ${countOf(report.files, "file")}`;
6930
+ const lines = [report.dryRun ? `Dry run of ${scope}: ${tally}. Nothing was written.` : `Imported ${scope}: ${tally}.`];
6931
+ const location = (item) => item.note === undefined ? `${item.file}#${item.pointer}` : `${item.note} from ${item.file}#${item.pointer}`;
6932
+ for (const item of report.items) {
6933
+ if (item.outcome !== "conflict" && item.outcome !== "rejected")
6934
+ continue;
6935
+ const external = item.externalId === undefined ? "" : ` (${item.externalId})`;
6936
+ lines.push(`${item.outcome}: ${safe(location(item))}${safe(external)}: ${safe(item.reason ?? "no reason given")}`);
6937
+ }
6938
+ const added = report.relations.filter((relation) => relation.outcome === "added").length;
6939
+ if (added > 0) {
6940
+ lines.push(`${report.dryRun ? "Would add" : "Added"} ${countOf(added, "supersedes relation")}.`);
6941
+ }
6942
+ for (const relation of report.relations) {
6943
+ if (relation.outcome !== "failed" && relation.outcome !== "omitted")
6944
+ continue;
6945
+ lines.push(`relation ${relation.outcome}: ${safe(relation.source)} supersedes ${safe(relation.target)}: ${safe(relation.reason ?? "no reason given")}`);
6946
+ }
6947
+ for (const diagnostic of report.diagnostics)
6948
+ lines.push(`diagnostic: ${safe(diagnostic)}`);
6949
+ for (const item of report.items) {
6950
+ for (const diagnostic of item.diagnostics ?? []) {
6951
+ lines.push(`diagnostic: ${safe(item.note ?? `${item.file}#${item.pointer}`)}: ${safe(diagnostic)}`);
6952
+ }
6953
+ }
6954
+ if (!report.dryRun && counts.created + counts.updated + added > 0) {
6955
+ const root = safe(shellWord(report.root));
6956
+ lines.push(`Next: wordcell refresh --root ${root}, then wordcell check --root ${root}.`);
6957
+ }
6958
+ return `${lines.join(`
6959
+ `)}
6960
+ `;
6961
+ }
6962
+ async function runImportSupermemory(command, output, dependencies) {
6963
+ const report = await (dependencies.importSupermemory ?? importSupermemory)({
6964
+ root: command.root,
6965
+ files: command.files,
6966
+ ...command.prefix === undefined ? {} : { prefix: command.prefix },
6967
+ dryRun: command.dryRun,
6968
+ ...dependencies.importNow === undefined ? {} : { now: dependencies.importNow() }
6969
+ });
6970
+ output.stdout(command.json ? terminalSafeJson(report) : sanitizeTerminalText(renderImportReport(report)));
6971
+ return report.ok ? 0 : 1;
6972
+ }
3452
6973
  async function runRelation(command, output, dependencies) {
3453
6974
  if (command.action === "list") {
3454
6975
  const snapshot = await (dependencies.scanVault ?? scanVault)(command.root, { mentionScope: false });
@@ -3644,6 +7165,63 @@ async function runServe(command, output, dependencies) {
3644
7165
  `);
3645
7166
  return 0;
3646
7167
  }
7168
+ function mcpContextBuilder(dependencies) {
7169
+ return async (snapshot, request) => {
7170
+ const inspection = await (dependencies.inspectAgentContextRepository ?? inspectAgentContextRepository)(snapshot.notes, request);
7171
+ const memory = await (dependencies.buildRepositoryMemoryContext ?? buildRepositoryMemoryContext)(snapshot.notes, { repositoryRoot: request.repositoryRoot, target: inspection.target });
7172
+ return contextPayload(inspection, snapshot, memory);
7173
+ };
7174
+ }
7175
+ async function resolveMcpDirectories(command) {
7176
+ const { root } = await resolveVault(command.root);
7177
+ if (command.repository === undefined)
7178
+ return { root };
7179
+ const repository = await realpath2(resolve3(command.repository));
7180
+ if (!(await stat2(repository)).isDirectory())
7181
+ throw new Error(`--repo is not a directory: ${repository}`);
7182
+ return { root, repository };
7183
+ }
7184
+ async function runMcp(command, output, dependencies) {
7185
+ let directories;
7186
+ try {
7187
+ directories = await resolveMcpDirectories(command);
7188
+ } catch (error) {
7189
+ const message = error instanceof Error ? error.message : String(error);
7190
+ const terminal = dependencies.terminal ?? processTerminal();
7191
+ output.stderr(renderFailure(safe(message), "wordcell mcp --help", terminalStyle(terminal.env, terminal.stderrIsTTY)));
7192
+ return 2;
7193
+ }
7194
+ const version = await serverVersion();
7195
+ const catalog = createToolCatalog({
7196
+ ...directories,
7197
+ readOnly: command.readOnly,
7198
+ context: mcpContextBuilder(dependencies),
7199
+ warn: (message) => output.stderr(`${safe(message)}
7200
+ `)
7201
+ });
7202
+ const instructions = serverInstructions({ root: directories.root, readOnly: command.readOnly });
7203
+ const streams = mcpStreams(dependencies);
7204
+ try {
7205
+ return await runMcpServer({
7206
+ catalog,
7207
+ instructions,
7208
+ version,
7209
+ input: streams.input,
7210
+ writeFrame: streams.writeFrame,
7211
+ stderr: (text) => output.stderr(text)
7212
+ });
7213
+ } finally {
7214
+ streams.restore();
7215
+ }
7216
+ }
7217
+ function mcpStreams(dependencies) {
7218
+ if (dependencies.mcp !== undefined)
7219
+ return { ...dependencies.mcp, restore: () => {
7220
+ return;
7221
+ } };
7222
+ const guard = guardStdout();
7223
+ return { input: process.stdin, writeFrame: guard.writeFrame, restore: guard.restore };
7224
+ }
3647
7225
  async function runCatalog(command, output, dependencies) {
3648
7226
  const snapshot = await (dependencies.scanVault ?? scanVault)(command.root, command.options);
3649
7227
  const relativeIndex = relative(snapshot.root, snapshot.indexPath).split("\\").join("/");
@@ -3667,7 +7245,10 @@ async function runInbox(command, output, dependencies) {
3667
7245
  output.stdout(command.json ? terminalSafeJson({ root: snapshot.root, ...report }) : sanitizeTerminalText(renderSourceInbox(report)));
3668
7246
  return 0;
3669
7247
  }
3670
- async function runInit(command, output, initialize) {
7248
+ function speaksToPerson(terminal) {
7249
+ return detectAudience(terminal.env, terminal.stderrIsTTY) === "human";
7250
+ }
7251
+ async function runInit(command, output, initialize, terminal) {
3671
7252
  const result = await initialize(command.directory);
3672
7253
  if (command.json)
3673
7254
  output.stdout(terminalSafeJson(result));
@@ -3675,9 +7256,21 @@ async function runInit(command, output, initialize) {
3675
7256
  const relativeRoot = relative(process.cwd(), result.root) || ".";
3676
7257
  output.stdout(`Initialized ${safe(relativeRoot)} with ${result.files.length} files.
3677
7258
  `);
7259
+ if (speaksToPerson(terminal)) {
7260
+ output.stderr(`Next: wordcell search "your question" --root ${safe(shellWord(relativeRoot))}
7261
+ `);
7262
+ }
3678
7263
  }
3679
7264
  return 0;
3680
7265
  }
7266
+ async function defaultSemanticIndexExists(root) {
7267
+ try {
7268
+ await stat2(semanticDatabasePath(await realpath2(root)));
7269
+ return true;
7270
+ } catch {
7271
+ return false;
7272
+ }
7273
+ }
3681
7274
  async function runCaptureBundle(command, output, dependencies) {
3682
7275
  const verification = await (dependencies.verifyCaptureBundle ?? verifyCaptureBundle)(command.path, command.options);
3683
7276
  const inspection = verification.inspection;
@@ -3918,8 +7511,8 @@ function renderContext(inspection, snapshot, memory) {
3918
7511
  }
3919
7512
  if (memory.invalidRecords.total > 0) {
3920
7513
  lines.push(`Repository-memory errors (${memory.invalidRecords.returned}/${memory.invalidRecords.total}):`);
3921
- for (const invalid of memory.invalidRecords.details) {
3922
- lines.push(` ${safe(invalid.path)}: ${invalid.issues.map(safe).join(" ")}`);
7514
+ for (const invalid2 of memory.invalidRecords.details) {
7515
+ lines.push(` ${safe(invalid2.path)}: ${invalid2.issues.map(safe).join(" ")}`);
3923
7516
  }
3924
7517
  }
3925
7518
  if (memory.advisories.total > 0) {
@@ -4151,29 +7744,133 @@ async function runVault(command, output, dependencies) {
4151
7744
  output.stdout(command.json ? terminalSafeJson(backlinkPayload(lookup.note.path, backlinks, relationBacklinks)) : sanitizeTerminalText(renderBacklinks(lookup.note.path, backlinks, relationBacklinks)));
4152
7745
  return 0;
4153
7746
  }
7747
+ function machineJsonRequested(rawArguments) {
7748
+ return rawArguments[0] !== "mcp" && rawArguments.includes("--json");
7749
+ }
7750
+ function processTerminal() {
7751
+ return {
7752
+ env: process.env,
7753
+ stdoutIsTTY: process.stdout.isTTY === true,
7754
+ stderrIsTTY: process.stderr.isTTY === true,
7755
+ columns: process.stdout.columns
7756
+ };
7757
+ }
7758
+ var SAFE_WORD = /^[A-Za-z][A-Za-z0-9-]{0,39}$/u;
7759
+ var SAFE_OPTION = /^--?[A-Za-z][A-Za-z0-9-]{0,39}$/u;
7760
+ function rejectedOption(rawArguments, message) {
7761
+ for (let index = 1;index < rawArguments.length; index += 1) {
7762
+ const token = rawArguments[index] ?? "";
7763
+ if (token === "--")
7764
+ return;
7765
+ if (!token.startsWith("-"))
7766
+ continue;
7767
+ const through = parseArguments(rawArguments.slice(0, index + 1));
7768
+ if (through.ok || through.message !== message)
7769
+ continue;
7770
+ const before = parseArguments(rawArguments.slice(0, index));
7771
+ if (!before.ok && before.message === message)
7772
+ return;
7773
+ const name = token.split("=")[0] ?? "";
7774
+ return SAFE_OPTION.test(name) ? name : undefined;
7775
+ }
7776
+ return;
7777
+ }
7778
+ function describeUsageError(rawArguments, message) {
7779
+ const first = rawArguments[0] ?? "";
7780
+ if (message === "unknown command") {
7781
+ const known = knownCommandWords();
7782
+ const suggestion = SAFE_WORD.test(first) ? closestMatch(first, known) : undefined;
7783
+ const text = SAFE_WORD.test(first) ? `Unknown command "${first}".${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}` : "Unknown command.";
7784
+ return { text, next: "wordcell --help" };
7785
+ }
7786
+ if (message === "unknown help topic") {
7787
+ const suggestion = SAFE_WORD.test(rawArguments[1] ?? "") ? closestMatch(rawArguments[1] ?? "", knownCommandWords()) : undefined;
7788
+ return {
7789
+ text: `There is no help for that command.${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`,
7790
+ next: "wordcell --help"
7791
+ };
7792
+ }
7793
+ const id = resolveCommandId(rawArguments.filter((word) => !word.startsWith("-")));
7794
+ const next = id === undefined ? "wordcell --help" : `wordcell ${id} --help`;
7795
+ if (/^unknown .*option/u.test(message) && id !== undefined) {
7796
+ const option = rejectedOption(rawArguments, message);
7797
+ if (option !== undefined) {
7798
+ const suggestion = closestMatch(option, optionNames(id));
7799
+ return {
7800
+ text: `Unknown option "${option}" for ${id}.${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`,
7801
+ next
7802
+ };
7803
+ }
7804
+ }
7805
+ return { text: sentence(safe(message)), next };
7806
+ }
7807
+ function runtimeNext(rawArguments) {
7808
+ const id = resolveCommandId(rawArguments.filter((word) => !word.startsWith("-")));
7809
+ return id === undefined ? "wordcell --help" : `wordcell ${id} --help`;
7810
+ }
7811
+ async function renderHelp(command, output, terminal, readVersion, showIntro) {
7812
+ const intro = showIntro ? terminalIntro({ isTTY: terminal.stdoutIsTTY, columns: terminal.columns, term: terminal.env.TERM }) : "";
7813
+ if (command.topic === undefined) {
7814
+ output.stdout(intro + sanitizeTerminalText(usage));
7815
+ return 0;
7816
+ }
7817
+ if (command.topic === "start") {
7818
+ const version = await readVersion().catch(() => {
7819
+ return;
7820
+ });
7821
+ output.stdout(intro + sanitizeTerminalText(startHelp(version)));
7822
+ return 0;
7823
+ }
7824
+ if (command.topic === "advanced") {
7825
+ output.stdout(sanitizeTerminalText(advancedHelp()));
7826
+ return 0;
7827
+ }
7828
+ output.stdout(sanitizeTerminalText(commandHelpText(command.topic) ?? usage));
7829
+ return 0;
7830
+ }
4154
7831
  async function main4(rawArguments = process.argv.slice(2), output = defaultOutput2, dependencies = {}) {
4155
- const jsonRequested = rawArguments.includes("--json");
7832
+ const jsonRequested = machineJsonRequested(rawArguments);
7833
+ const terminal = dependencies.terminal ?? processTerminal();
7834
+ const errorStyle = terminalStyle(terminal.env, terminal.stderrIsTTY);
7835
+ const readVersion = dependencies.packageVersion ?? (() => serverVersion());
4156
7836
  const parsed = parseArguments(rawArguments);
4157
7837
  if (!parsed.ok) {
7838
+ const failure = describeUsageError(rawArguments, parsed.message);
4158
7839
  if (jsonRequested) {
4159
7840
  output.stdout(terminalSafeJson({
4160
7841
  ok: false,
4161
- error: { kind: "parse", message: parsed.message }
7842
+ error: { kind: "parse", code: "usage", message: parsed.message, next: failure.next }
4162
7843
  }));
4163
7844
  } else {
4164
- output.stderr(`error: ${safe(parsed.message)}
4165
-
4166
- ${sanitizeTerminalText(usage)}`);
7845
+ output.stderr(renderFailure(failure.text, failure.next, errorStyle));
4167
7846
  }
4168
7847
  return 2;
4169
7848
  }
4170
7849
  const command = parsed.value;
4171
- if (command.kind === "help") {
4172
- if (output === defaultOutput2 && !jsonRequested)
4173
- output.stdout(terminalIntro({ isTTY: process.stdout.isTTY, columns: process.stdout.columns, term: process.env.TERM }));
4174
- output.stdout(sanitizeTerminalText(usage));
7850
+ if (command.kind === "version") {
7851
+ const version = await readVersion();
7852
+ output.stdout(command.json || jsonRequested ? terminalSafeJson({ name: "wordcell", version }) : `wordcell ${version}
7853
+ `);
4175
7854
  return 0;
4176
7855
  }
7856
+ if (command.kind === "help") {
7857
+ if (jsonRequested) {
7858
+ const text = [];
7859
+ await renderHelp(command, { stdout: (value) => text.push(value), stderr: output.stderr }, terminal, readVersion, false);
7860
+ output.stdout(terminalSafeJson({ kind: "help", text: text.join("") }));
7861
+ return 0;
7862
+ }
7863
+ return renderHelp(command, output, terminal, readVersion, output === defaultOutput2);
7864
+ }
7865
+ if (jsonRequested && isDelegatedHelp(command)) {
7866
+ const text = [];
7867
+ const code = await main4([command.kind === "clip" ? "clip" : command.kind, "--help"], {
7868
+ stdout: (value) => text.push(value),
7869
+ stderr: output.stderr
7870
+ }, dependencies);
7871
+ output.stdout(terminalSafeJson({ kind: "help", text: text.join("") }));
7872
+ return code;
7873
+ }
4177
7874
  try {
4178
7875
  if (command.kind === "clip") {
4179
7876
  return await (dependencies.runClipCommand ?? main)(command.arguments, process.env, output);
@@ -4193,7 +7890,7 @@ ${sanitizeTerminalText(usage)}`);
4193
7890
  return await (dependencies.runPdfCommand ?? main2)(command.arguments, process.env, output);
4194
7891
  }
4195
7892
  if (command.kind === "init") {
4196
- return await runInit(command, output, dependencies.initVault ?? initVault);
7893
+ return await runInit(command, output, dependencies.initVault ?? initVault, dependencies.terminal ?? processTerminal());
4197
7894
  }
4198
7895
  if (command.kind === "index" || command.kind === "search") {
4199
7896
  return await runSemantic(command, output, dependencies);
@@ -4210,6 +7907,8 @@ ${sanitizeTerminalText(usage)}`);
4210
7907
  return await runAgents(command, output, dependencies);
4211
7908
  if (command.kind === "note-create")
4212
7909
  return await runNoteCreate(command, output, dependencies);
7910
+ if (command.kind === "import-supermemory")
7911
+ return await runImportSupermemory(command, output, dependencies);
4213
7912
  if (command.kind === "relation")
4214
7913
  return await runRelation(command, output, dependencies);
4215
7914
  if (command.kind === "graph-rebuild" || command.kind === "graph-verify" || command.kind === "graph-query") {
@@ -4225,6 +7924,8 @@ ${sanitizeTerminalText(usage)}`);
4225
7924
  return await runPublish(command, output, dependencies);
4226
7925
  if (command.kind === "serve")
4227
7926
  return await runServe(command, output, dependencies);
7927
+ if (command.kind === "mcp")
7928
+ return await runMcp(command, output, dependencies);
4228
7929
  if (command.kind === "inbox")
4229
7930
  return await runInbox(command, output, dependencies);
4230
7931
  if (command.kind === "catalog")
@@ -4232,14 +7933,18 @@ ${sanitizeTerminalText(usage)}`);
4232
7933
  return await runVault(command, output, dependencies);
4233
7934
  } catch (error) {
4234
7935
  const message = error instanceof Error ? error.message : String(error);
7936
+ const next = runtimeNext(rawArguments);
4235
7937
  if (jsonRequested) {
4236
7938
  output.stdout(terminalSafeJson({
4237
7939
  ok: false,
4238
- error: { kind: "runtime", message }
7940
+ error: { kind: "runtime", message, next }
4239
7941
  }));
4240
7942
  } else {
4241
- output.stderr(`error: ${safe(message)}
7943
+ output.stderr(renderFailure(safe(message), next, errorStyle));
7944
+ if (terminal.env.HRANESS_DEBUG === "1" && error instanceof Error && error.stack !== undefined) {
7945
+ output.stderr(`${sanitizeTerminalText(redactSensitiveText(error.stack))}
4242
7946
  `);
7947
+ }
4243
7948
  }
4244
7949
  return 1;
4245
7950
  }
@@ -4268,7 +7973,7 @@ function strictProtocolObject(value) {
4268
7973
  return parsed;
4269
7974
  }
4270
7975
  async function runExecutable(rawArguments = process.argv.slice(2), dependencies = {}) {
4271
- if (!rawArguments.includes("--json")) {
7976
+ if (!machineJsonRequested(rawArguments)) {
4272
7977
  return main4(rawArguments, defaultOutput2, dependencies);
4273
7978
  }
4274
7979
  return serializeStrictJson(async () => {
@@ -4293,7 +7998,7 @@ async function runExecutable(rawArguments = process.argv.slice(2), dependencies
4293
7998
  return true;
4294
7999
  };
4295
8000
  const redirectedConsole = (...arguments_) => {
4296
- rawStderrWrite(`${format(...arguments_)}
8001
+ rawStderrWrite(`${format2(...arguments_)}
4297
8002
  `);
4298
8003
  };
4299
8004
  try {
@@ -4352,4 +8057,4 @@ async function runExecutable(rawArguments = process.argv.slice(2), dependencies
4352
8057
  });
4353
8058
  }
4354
8059
 
4355
- export { parseUrlMetadataArguments, usage, parseArguments, main4 as main, runExecutable };
8060
+ export { parseUrlMetadataArguments, usage, parseArguments, describeUsageError, main4 as main, runExecutable };