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