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