@hraness/wordcell 0.22.5 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -18
- package/dist/authoring.js +2 -2
- package/dist/{authority-3ga47syb.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 +3 -1
- package/dist/{index-bcknqxrq.js → index-0eacgvpv.js} +9 -1
- package/dist/{index-f75zmmdr.js → index-1er88ckw.js} +4 -4
- package/dist/{index-8v6k9h4r.js → index-1s8mc4hz.js} +1 -1
- package/dist/{index-n9kwenpt.js → index-1w0sp1q7.js} +1 -1
- package/dist/{index-de2w8crk.js → index-2fmnttca.js} +82 -15
- package/dist/{index-8bgnhjgq.js → index-44kw9tr8.js} +3867 -162
- package/dist/{index-42tb8qqp.js → index-5d1rd5v9.js} +444 -9
- package/dist/{index-aer0jdrq.js → index-ahyhryb8.js} +1 -1
- package/dist/{index-f7fnww7a.js → index-b0khj0h5.js} +164 -9
- package/dist/{index-qdb8f8va.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-h2qn3y3f.js → index-f184qyh1.js} +4 -4
- package/dist/{index-n57tewfr.js → index-fp732bgg.js} +2 -2
- package/dist/{index-zy7an84p.js → index-jvb7w0gg.js} +5 -2
- package/dist/{index-f984hw45.js → index-k86wepd8.js} +1 -1
- package/dist/{index-1r5rnkfr.js → index-mvs6f7ne.js} +2 -2
- package/dist/{index-p96kb6t2.js → index-nn67kzwa.js} +1 -1
- package/dist/{index-fc4dr114.js → index-nwrehke7.js} +1 -1
- package/dist/{index-pz2b2x0y.js → index-pgtm2nhf.js} +1 -1
- package/dist/{index-054mb7d3.js → index-qfqjfxaa.js} +10 -9
- package/dist/{index-xzvcw9ga.js → index-r0m5taz2.js} +2 -2
- package/dist/{index-gtwqye5a.js → index-tcaq1c7f.js} +4 -4
- package/dist/{index-g5vsqmdy.js → index-xwxy71ew.js} +190 -4
- package/dist/index.js +15 -13
- 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-jf9v10j7.js → support-1yhv6vm6.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/query.md +52 -4
- package/skills/wordcell/references/session-memory.md +207 -0
- 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 +894 -0
- package/src/cli-intro.ts +1 -1
- package/src/cli-program.ts +612 -83
- 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 +9 -4
- package/src/import-supermemory.ts +1629 -0
- package/src/mcp-server.ts +415 -0
- package/src/mcp-tools.ts +1138 -0
- package/src/pdf/cli.ts +5 -4
- package/src/sdk.ts +149 -1
- package/src/selected-passage.ts +409 -0
- package/src/support-profile.ts +1 -1
- package/src/support.ts +3 -0
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";
|
|
@@ -180,10 +198,46 @@ type Output = {
|
|
|
180
198
|
readonly stderr: (value: string) => void;
|
|
181
199
|
};
|
|
182
200
|
|
|
183
|
-
const defaultOutput: Output = {
|
|
201
|
+
const defaultOutput: Output = terminalOutput({
|
|
184
202
|
stdout: (value) => process.stdout.write(value),
|
|
185
203
|
stderr: (value) => process.stderr.write(value),
|
|
186
|
-
};
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
/** Read standard input as fatal UTF-8, stopping as soon as it passes `maximumBytes`. */
|
|
207
|
+
async function readBoundedStdinUtf8(
|
|
208
|
+
source: StdinSource,
|
|
209
|
+
maximumBytes: number,
|
|
210
|
+
label: string,
|
|
211
|
+
): Promise<string> {
|
|
212
|
+
if (source.isTTY) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"--body-file - reads the note body from standard input, but standard input is a terminal; pipe the body in or name a file",
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const chunks: Uint8Array[] = [];
|
|
218
|
+
let total = 0;
|
|
219
|
+
for await (const chunk of source.chunks) {
|
|
220
|
+
total += chunk.byteLength;
|
|
221
|
+
if (total > maximumBytes) {
|
|
222
|
+
throw new Error(`${label} exceeds the ${maximumBytes}-byte limit`);
|
|
223
|
+
}
|
|
224
|
+
chunks.push(chunk);
|
|
225
|
+
}
|
|
226
|
+
const bytes = new Uint8Array(total);
|
|
227
|
+
let offset = 0;
|
|
228
|
+
for (const chunk of chunks) {
|
|
229
|
+
bytes.set(chunk, offset);
|
|
230
|
+
offset += chunk.byteLength;
|
|
231
|
+
}
|
|
232
|
+
let text: string;
|
|
233
|
+
try {
|
|
234
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
throw new Error(`${label} is not valid UTF-8`, { cause: error });
|
|
237
|
+
}
|
|
238
|
+
if (text.trim() === "") throw new Error(`${label} from standard input is empty`);
|
|
239
|
+
return text;
|
|
240
|
+
}
|
|
187
241
|
|
|
188
242
|
async function readBoundedUtf8(
|
|
189
243
|
path: string,
|
|
@@ -232,73 +286,15 @@ async function loadSearchRulesFile(path: string): Promise<SearchRulesV1> {
|
|
|
232
286
|
return parseSearchRules(input);
|
|
233
287
|
}
|
|
234
288
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
Start here (no account or model needed):
|
|
238
|
-
wordcell init kb
|
|
239
|
-
wordcell note create notes/decision --title "A decision" --body "Keep retries bounded." --root kb
|
|
240
|
-
wordcell search "retries" --root kb --mode exact
|
|
241
|
-
|
|
242
|
-
Already have Markdown? Search it with --root <your-notes-directory>.
|
|
243
|
-
Use --mode exact for model-free search; optional hybrid search needs a local index.
|
|
244
|
-
Quick start and examples: https://wordcell.io/docs
|
|
245
|
-
|
|
246
|
-
Usage:
|
|
247
|
-
wordcell init [directory] [--json]
|
|
248
|
-
wordcell clip <url|current> [capture options]
|
|
249
|
-
wordcell capture show <bundle> [--verify-assets] [--include-source-html] [--json]
|
|
250
|
-
wordcell capture verify <bundle> [--verify-assets] [--json]
|
|
251
|
-
wordcell capture diff <bundle> [--repo <repository>] [--ref <ref>] [--json]
|
|
252
|
-
wordcell url-metadata tool <build|check>
|
|
253
|
-
wordcell url-metadata backfill [metadata options]
|
|
254
|
-
wordcell inspect <url> [capture options]
|
|
255
|
-
wordcell pdf <file-or-url> [PDF options]
|
|
256
|
-
wordcell refresh [--root <directory>] [--index <path>] [--json]
|
|
257
|
-
wordcell check [--root <directory>] [--index <path>] [--no-catalog] [--repo <repository>] [--json]
|
|
258
|
-
wordcell catalog [--root <directory>] [--index <path>] [--json]
|
|
259
|
-
wordcell graph [--root <directory>] [--index <path>] [--json]
|
|
260
|
-
wordcell graph rebuild [--fresh] [--root <directory>] [--index <path>] [--json]
|
|
261
|
-
wordcell graph verify [--root <directory>] [--index <path>] [--json]
|
|
262
|
-
wordcell graph query --program <backlinks|reachability|scope-route|relation-closure|shared-tags|shared-concepts> [--note <id> | --scope <path>] [--predicate <predicate>] [--depth <count>] [--limit <count>] [--persisted] [--root <directory>] [--index <path>] [--json]
|
|
263
|
-
wordcell backlinks <note> [--root <directory>] [--index <path>] [--json]
|
|
264
|
-
wordcell links <note> [--root <directory>] [--direction <in|out|both>] [--depth <count>] [--limit <count>] [--json]
|
|
265
|
-
wordcell note create <id> --title <title> [--type <type>] [--tag <tag>] [--body <markdown> | --body-file <path>] [--root <directory>] [--json]
|
|
266
|
-
wordcell relation add <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
|
|
267
|
-
wordcell relation remove <source> <predicate> <target> [--root <directory>] [--expected-revision <sha256:...>] [--json]
|
|
268
|
-
wordcell relation list <note> [--root <directory>] [--json]
|
|
269
|
-
wordcell percolate [note] [--proofs] [--root <directory>] [--min-support <count>] [--limit <count>] [--json]
|
|
270
|
-
wordcell list [--root <directory>] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--sort <field>] [--order <asc|desc>] [--limit <count>] [--json]
|
|
271
|
-
wordcell index [--root <directory>] [--database <path>] [--force] [--json]
|
|
272
|
-
wordcell search <query> [--root <directory>] [--repo <repository>] [--database <path>] [--mode <hybrid|exact|keyword|semantic>] [--rules <file>] [--priority] [--where <path=value>] [--has <path>] [--tag <tag>] [--scope <repository-path>] [--related <note>] [--graph-depth <1|2>] [--no-graph] [--history | --no-history | --require-history] [--limit <count>] [--candidate-limit <count>] [--min-score <score>] [--rerank <typesafe>] [--rerank-limit <2..25>] [--json]
|
|
273
|
-
wordcell history <note> [--root <directory>] [--repo <repository>] [--limit <count>] [--cochanged-limit <count>] [--json]
|
|
274
|
-
wordcell history search <query-or-path> [--root <directory>] [--repo <repository>] [--limit <count>] [--commit-limit <count>] [--cochanged-limit <count>] [--json]
|
|
275
|
-
wordcell evaluate <manifest.json> [--root <directory>] [--repo <repository>] [--database <path>] [--retriever <id>] [--split <development|test|all>] [--limit <count>] [--cutoff <count>] [--timeout <milliseconds>] [--baseline <id>] [--model-file <path>] [--cache-state <cold|mixed|warm>] [--json]
|
|
276
|
-
wordcell portfolio search <query> --registry <file> --workspace <directory> (--shared | --vault <owner/id>...) [--mode <hybrid|exact|keyword|semantic>] [--rules <file>] [--priority] [--limit <count>] [--require-all] [--json]
|
|
277
|
-
wordcell portfolio audit --registry <file> --workspace <directory> (--all | --shared | --vault <owner/id>...) [--strict] [--json]
|
|
278
|
-
wordcell publish --out <directory> [--root <directory>] [--index <path>] [--include <path>]... [--exclude <path>]... [--include-glob <pattern>]... [--exclude-glob <pattern>]... [--where <path=value>]... [--has <path>]... [--tag <tag>]... [--scope <repository-path>]... [--from <note> [--depth <count>] [--direction <in|out|both>]] [--title <title>] [--description <text>] [--base-path <path>] [--base-url <url>] [--noindex] [--no-index-content] [--deterministic] [--dry-run] [--list-limit <0-1000>] [--force] [--json]
|
|
279
|
-
wordcell serve --root <directory> [--host <host>] [--port <port>] [--json]
|
|
280
|
-
wordcell inbox [--root <directory>] [--source-prefix <directory>] [--limit <count>] [--json]
|
|
281
|
-
wordcell context <repository-path> [--root <vault>] [--repo <repository>] [--kind <auto|file|directory>] [--json]
|
|
282
|
-
wordcell agents identity <repository-scope> [--json]
|
|
283
|
-
wordcell agents check [--root <vault>] [--repo <repository>] [--json]
|
|
284
|
-
wordcell agents audit [--root <vault>] [--repo <repository>] [--json]
|
|
285
|
-
wordcell doctor [--json]
|
|
286
|
-
wordcell adapters [--json]
|
|
287
|
-
wordcell support [--json | protocol --json | offer --json | shown <id> | release <id> | dismiss | snooze | enable | status --json]
|
|
288
|
-
|
|
289
|
-
Optional support uses separate local preferences. HRANESS_SUPPORT=off or
|
|
290
|
-
HRANESS_SUPPORT_AUDIENCE=off keeps invitations quiet; explicit support remains available.
|
|
291
|
-
Agents receive discovery on stderr after useful standalone work. Human mode requires
|
|
292
|
-
HRANESS_SUPPORT_AUDIENCE=human and an interactive stderr. No command signs up or pays.
|
|
293
|
-
|
|
294
|
-
Run \`wordcell clip --help\` for web capture options or \`wordcell pdf --help\` for PDF conversion options.
|
|
295
|
-
`;
|
|
289
|
+
/** Grouped root help printed by `wordcell --help`. */
|
|
290
|
+
export const usage = rootHelp();
|
|
296
291
|
|
|
297
292
|
type VaultCommand = "refresh" | "check" | "graph" | "backlinks" | "links";
|
|
298
293
|
|
|
299
294
|
type ParsedCommand =
|
|
300
295
|
| GraphCliCommand
|
|
301
|
-
| { readonly kind: "help" }
|
|
296
|
+
| { readonly kind: "help"; readonly topic?: string }
|
|
297
|
+
| { readonly kind: "version"; readonly json: boolean }
|
|
302
298
|
| { readonly kind: "clip"; readonly arguments: readonly string[] }
|
|
303
299
|
| {
|
|
304
300
|
readonly kind: "capture-bundle";
|
|
@@ -389,6 +385,7 @@ type ParsedCommand =
|
|
|
389
385
|
readonly minScore?: number;
|
|
390
386
|
readonly rerank?: "typesafe";
|
|
391
387
|
readonly rerankLimit?: number;
|
|
388
|
+
readonly selectedPassage?: boolean;
|
|
392
389
|
readonly query: string;
|
|
393
390
|
readonly json: boolean;
|
|
394
391
|
}
|
|
@@ -460,6 +457,15 @@ type ParsedCommand =
|
|
|
460
457
|
readonly bodyFile?: string;
|
|
461
458
|
readonly json: boolean;
|
|
462
459
|
}
|
|
460
|
+
| {
|
|
461
|
+
readonly kind: "import-supermemory";
|
|
462
|
+
readonly root: string;
|
|
463
|
+
/** Export file paths as given, read relative to the working directory. */
|
|
464
|
+
readonly files: readonly string[];
|
|
465
|
+
readonly prefix?: string;
|
|
466
|
+
readonly dryRun: boolean;
|
|
467
|
+
readonly json: boolean;
|
|
468
|
+
}
|
|
463
469
|
| {
|
|
464
470
|
readonly kind: "relation";
|
|
465
471
|
readonly action: "add" | "remove" | "list";
|
|
@@ -503,13 +509,19 @@ type ParsedCommand =
|
|
|
503
509
|
readonly host: string;
|
|
504
510
|
readonly port: number;
|
|
505
511
|
readonly json: boolean;
|
|
512
|
+
}
|
|
513
|
+
| {
|
|
514
|
+
readonly kind: "mcp";
|
|
515
|
+
readonly root: string;
|
|
516
|
+
readonly repository?: string;
|
|
517
|
+
readonly readOnly: boolean;
|
|
506
518
|
};
|
|
507
519
|
|
|
508
520
|
type ParseResult =
|
|
509
521
|
| { readonly ok: true; readonly value: ParsedCommand }
|
|
510
522
|
| { readonly ok: false; readonly message: string };
|
|
511
523
|
|
|
512
|
-
type CliDependencies = GraphCliDependencies & {
|
|
524
|
+
export type CliDependencies = GraphCliDependencies & {
|
|
513
525
|
readonly runClipCommand?: typeof runClipCommand;
|
|
514
526
|
readonly readCaptureBundle?: typeof readCaptureBundle;
|
|
515
527
|
readonly verifyCaptureBundle?: typeof verifyCaptureBundle;
|
|
@@ -524,6 +536,8 @@ type CliDependencies = GraphCliDependencies & {
|
|
|
524
536
|
readonly refreshVault?: typeof refreshVault;
|
|
525
537
|
readonly indexSemanticVault?: typeof indexSemanticVault;
|
|
526
538
|
readonly openKnowledgeBase?: typeof openKnowledgeBase;
|
|
539
|
+
/** Whether a local search index exists for a vault root; injected in tests. */
|
|
540
|
+
readonly semanticIndexExists?: (root: string) => Promise<boolean>;
|
|
527
541
|
readonly openKnowledgeBaseEvaluation?: typeof openKnowledgeBaseEvaluation;
|
|
528
542
|
/** Test seam; production wiring builds the TypeSafe reranker lazily per flag. */
|
|
529
543
|
readonly rerankers?: readonly SearchReranker[];
|
|
@@ -541,6 +555,31 @@ type CliDependencies = GraphCliDependencies & {
|
|
|
541
555
|
readonly validateMarkdownAttachments?: typeof validateMarkdownAttachments;
|
|
542
556
|
readonly publishVault?: typeof publishVault;
|
|
543
557
|
readonly serveSite?: typeof serveSite;
|
|
558
|
+
/** Test seam: serve MCP frames over these streams instead of guarding the process stdout. */
|
|
559
|
+
readonly mcp?: McpStreams;
|
|
560
|
+
/** Test seam: the standard input that `note create --body-file -` reads. */
|
|
561
|
+
readonly stdin?: StdinSource;
|
|
562
|
+
readonly importSupermemory?: typeof importSupermemory;
|
|
563
|
+
/** Test seam: the clock for the provenance line of `import supermemory`. */
|
|
564
|
+
readonly importNow?: () => Date;
|
|
565
|
+
/** Test seam: terminal facts for symbols, color, and the help intro. */
|
|
566
|
+
readonly terminal?: CliTerminal;
|
|
567
|
+
/** Test seam: the version `--version` and bare invocation print. */
|
|
568
|
+
readonly packageVersion?: () => Promise<string>;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
export type StdinSource = {
|
|
572
|
+
readonly isTTY: boolean;
|
|
573
|
+
readonly chunks: AsyncIterable<Uint8Array>;
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
function processStdin(): StdinSource {
|
|
577
|
+
return { isTTY: process.stdin.isTTY === true, chunks: process.stdin };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
type McpStreams = {
|
|
581
|
+
readonly input: AsyncIterable<Uint8Array>;
|
|
582
|
+
readonly writeFrame: (line: string) => Promise<void>;
|
|
544
583
|
};
|
|
545
584
|
|
|
546
585
|
function safe(value: string): string {
|
|
@@ -1126,6 +1165,38 @@ function parseServeCommand(arguments_: readonly string[]): ParseResult {
|
|
|
1126
1165
|
return { ok: true, value: { kind: "serve", root, host, port, json } };
|
|
1127
1166
|
}
|
|
1128
1167
|
|
|
1168
|
+
/** No `--json`: stdout carries only protocol frames. */
|
|
1169
|
+
function parseMcpCommand(arguments_: readonly string[]): ParseResult {
|
|
1170
|
+
let root: string | undefined;
|
|
1171
|
+
let repository: string | undefined;
|
|
1172
|
+
let readOnly = false;
|
|
1173
|
+
for (let cursor = 0; cursor < arguments_.length; cursor += 1) {
|
|
1174
|
+
const argument = arguments_[cursor];
|
|
1175
|
+
if (argument === undefined) continue;
|
|
1176
|
+
if (argument === "--read-only") { readOnly = true; continue; }
|
|
1177
|
+
if (argument === "--root" || argument === "--repo") {
|
|
1178
|
+
const value = readValue(arguments_, cursor);
|
|
1179
|
+
// An empty directory would resolve to the working directory of whatever launched the server.
|
|
1180
|
+
if (value === null || value === "") return { ok: false, message: `${argument} requires a value` };
|
|
1181
|
+
if (argument === "--root") root = value;
|
|
1182
|
+
else repository = value;
|
|
1183
|
+
cursor += 1;
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
ok: false,
|
|
1188
|
+
message: argument.startsWith("--")
|
|
1189
|
+
? "unknown mcp option"
|
|
1190
|
+
: "mcp does not accept positional arguments",
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
if (root === undefined) return { ok: false, message: "mcp requires --root <vault>" };
|
|
1194
|
+
return {
|
|
1195
|
+
ok: true,
|
|
1196
|
+
value: { kind: "mcp", root, ...(repository === undefined ? {} : { repository }), readOnly },
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1129
1200
|
function parseInboxCommand(arguments_: readonly string[]): ParseResult {
|
|
1130
1201
|
let root = ".";
|
|
1131
1202
|
let index: string | undefined;
|
|
@@ -1204,6 +1275,7 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
|
|
|
1204
1275
|
let minScore: number | undefined;
|
|
1205
1276
|
let rerank: "typesafe" | undefined;
|
|
1206
1277
|
let rerankLimit: number | undefined;
|
|
1278
|
+
let selectedPassage = false;
|
|
1207
1279
|
let graphDepth: number | undefined;
|
|
1208
1280
|
let noGraph = false;
|
|
1209
1281
|
let history = false;
|
|
@@ -1230,6 +1302,10 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
|
|
|
1230
1302
|
noGraph = true;
|
|
1231
1303
|
continue;
|
|
1232
1304
|
}
|
|
1305
|
+
if (argument === "--selected-passage" && command === "search") {
|
|
1306
|
+
selectedPassage = true;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1233
1309
|
if (argument === "--no-history" && command === "search") {
|
|
1234
1310
|
noHistory = true;
|
|
1235
1311
|
continue;
|
|
@@ -1465,6 +1541,7 @@ function parseSemanticCommand(command: "index" | "search", arguments_: readonly
|
|
|
1465
1541
|
...(minScore === undefined ? {} : { minScore }),
|
|
1466
1542
|
...(rerank === undefined ? {} : { rerank }),
|
|
1467
1543
|
...(rerankLimit === undefined ? {} : { rerankLimit }),
|
|
1544
|
+
...(selectedPassage ? { selectedPassage: true } : {}),
|
|
1468
1545
|
query,
|
|
1469
1546
|
json,
|
|
1470
1547
|
},
|
|
@@ -1893,6 +1970,53 @@ function parseNoteCommand(arguments_: readonly string[]): ParseResult {
|
|
|
1893
1970
|
};
|
|
1894
1971
|
}
|
|
1895
1972
|
|
|
1973
|
+
function parseImportCommand(arguments_: readonly string[]): ParseResult {
|
|
1974
|
+
if (arguments_[0] !== "supermemory") return { ok: false, message: "import requires supermemory" };
|
|
1975
|
+
let root = ".";
|
|
1976
|
+
let prefix: string | undefined;
|
|
1977
|
+
let dryRun = false;
|
|
1978
|
+
let json = false;
|
|
1979
|
+
const files: string[] = [];
|
|
1980
|
+
for (let cursor = 1; cursor < arguments_.length; cursor += 1) {
|
|
1981
|
+
const argument = arguments_[cursor];
|
|
1982
|
+
if (argument === undefined) continue;
|
|
1983
|
+
if (argument === "--json") { json = true; continue; }
|
|
1984
|
+
if (argument === "--dry-run") { dryRun = true; continue; }
|
|
1985
|
+
if (argument === "--root" || argument === "--prefix") {
|
|
1986
|
+
const value = readValue(arguments_, cursor);
|
|
1987
|
+
if (value === null || value === "") return { ok: false, message: `${argument} requires a value` };
|
|
1988
|
+
if (argument === "--root") root = value;
|
|
1989
|
+
else prefix = value;
|
|
1990
|
+
cursor += 1;
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
if (argument.startsWith("--")) return { ok: false, message: "unknown import supermemory option" };
|
|
1994
|
+
files.push(argument);
|
|
1995
|
+
}
|
|
1996
|
+
if (files.length === 0) return { ok: false, message: "import supermemory requires at least one export file" };
|
|
1997
|
+
if (files.length > MAX_IMPORT_FILES) {
|
|
1998
|
+
return { ok: false, message: `import supermemory accepts at most ${MAX_IMPORT_FILES} export files` };
|
|
1999
|
+
}
|
|
2000
|
+
if (prefix !== undefined) {
|
|
2001
|
+
try {
|
|
2002
|
+
prefix = normalizePrefix(prefix);
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return {
|
|
2008
|
+
ok: true,
|
|
2009
|
+
value: {
|
|
2010
|
+
kind: "import-supermemory",
|
|
2011
|
+
root,
|
|
2012
|
+
files,
|
|
2013
|
+
...(prefix === undefined ? {} : { prefix }),
|
|
2014
|
+
dryRun,
|
|
2015
|
+
json,
|
|
2016
|
+
},
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
|
|
1896
2020
|
function isNoteRevision(value: string): value is `sha256:${string}` {
|
|
1897
2021
|
return /^sha256:[0-9a-f]{64}$/u.test(value);
|
|
1898
2022
|
}
|
|
@@ -2156,11 +2280,72 @@ function parsePortfolioCommand(arguments_: readonly string[]): ParseResult {
|
|
|
2156
2280
|
};
|
|
2157
2281
|
}
|
|
2158
2282
|
|
|
2283
|
+
function isHelpFlag(argument: string | undefined): boolean {
|
|
2284
|
+
return argument === "--help" || argument === "-h";
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
/** True for the help request a delegated parser answers itself. */
|
|
2288
|
+
function isDelegatedHelp(command: ParsedCommand): boolean {
|
|
2289
|
+
if (command.kind === "clip") return command.arguments.length === 1 && command.arguments[0] === "help";
|
|
2290
|
+
if (command.kind === "pdf" || command.kind === "url-metadata") {
|
|
2291
|
+
return command.arguments.length === 1 && command.arguments[0] === "--help";
|
|
2292
|
+
}
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/** Help for delegated commands comes from their own parsers. */
|
|
2297
|
+
function delegatedHelp(first: string): ParsedCommand | undefined {
|
|
2298
|
+
if (first === "clip" || first === "inspect") return { kind: "clip", arguments: ["help"] };
|
|
2299
|
+
if (first === "pdf") return { kind: "pdf", arguments: ["--help"] };
|
|
2300
|
+
if (first === "url-metadata") return { kind: "url-metadata", arguments: ["--help"] };
|
|
2301
|
+
return undefined;
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/** `help`, `help advanced`, and `help <command...>`. */
|
|
2305
|
+
function parseHelpTopic(words: readonly string[]): ParseResult {
|
|
2306
|
+
const topic = words.filter((word) => !isHelpFlag(word) && word !== "--json");
|
|
2307
|
+
if (topic.length === 0) return { ok: true, value: { kind: "help" } };
|
|
2308
|
+
if (topic.length === 1 && topic[0] === "advanced") return { ok: true, value: { kind: "help", topic: "advanced" } };
|
|
2309
|
+
const delegated = delegatedHelp(topic[0] ?? "");
|
|
2310
|
+
if (delegated !== undefined) return { ok: true, value: delegated };
|
|
2311
|
+
const id = resolveCommandId(topic);
|
|
2312
|
+
if (id === undefined) return { ok: false, message: "unknown help topic" };
|
|
2313
|
+
return { ok: true, value: { kind: "help", topic: id } };
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
/**
|
|
2317
|
+
* `<command> --help` anywhere a person would type it: right after the command
|
|
2318
|
+
* words, or as the last argument when it is not the value of an option.
|
|
2319
|
+
*/
|
|
2320
|
+
function embeddedHelp(arguments_: readonly string[]): ParseResult | undefined {
|
|
2321
|
+
const separator = arguments_.indexOf("--");
|
|
2322
|
+
const end = separator === -1 ? arguments_.length : separator;
|
|
2323
|
+
const index = arguments_.slice(0, end).findIndex(isHelpFlag);
|
|
2324
|
+
if (index <= 0) return undefined;
|
|
2325
|
+
const delegated = delegatedHelp(arguments_[0] ?? "");
|
|
2326
|
+
if (delegated !== undefined) {
|
|
2327
|
+
return index === 1 || index === end - 1 ? { ok: true, value: delegated } : undefined;
|
|
2328
|
+
}
|
|
2329
|
+
const id = resolveCommandId(arguments_.slice(0, index).filter((word) => !word.startsWith("-")));
|
|
2330
|
+
if (id === undefined) return undefined;
|
|
2331
|
+
const words = commandWordCount(id);
|
|
2332
|
+
const previous = arguments_[index - 1] ?? "";
|
|
2333
|
+
// `--help` is never an option value; `-h` could be one, as in `--body -h`.
|
|
2334
|
+
const flag = arguments_[index];
|
|
2335
|
+
const lastArgument = index === end - 1 && (flag === "--help" || !valueOptions(id).has(previous));
|
|
2336
|
+
if (index !== words && !lastArgument) return undefined;
|
|
2337
|
+
return { ok: true, value: { kind: "help", topic: id } };
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2159
2340
|
export function parseArguments(arguments_: readonly string[]): ParseResult {
|
|
2160
2341
|
const command = arguments_[0];
|
|
2161
|
-
if (command === undefined
|
|
2162
|
-
|
|
2342
|
+
if (command === undefined) return { ok: true, value: { kind: "help", topic: "start" } };
|
|
2343
|
+
if (command === "help" || isHelpFlag(command)) return parseHelpTopic(arguments_.slice(1));
|
|
2344
|
+
if (command === "--version" || command === "-V" || command === "-v" || command === "version") {
|
|
2345
|
+
return { ok: true, value: { kind: "version", json: arguments_.includes("--json") } };
|
|
2163
2346
|
}
|
|
2347
|
+
const help = embeddedHelp(arguments_);
|
|
2348
|
+
if (help !== undefined) return help;
|
|
2164
2349
|
if (command === "capture" && arguments_[1] === "diff") {
|
|
2165
2350
|
return parseCaptureDiffCommand(arguments_.slice(2));
|
|
2166
2351
|
}
|
|
@@ -2216,10 +2401,12 @@ export function parseArguments(arguments_: readonly string[]): ParseResult {
|
|
|
2216
2401
|
if (command === "context") return parseContextCommand(arguments_.slice(1));
|
|
2217
2402
|
if (command === "agents") return parseAgentsCommand(arguments_.slice(1));
|
|
2218
2403
|
if (command === "note") return parseNoteCommand(arguments_.slice(1));
|
|
2404
|
+
if (command === "import") return parseImportCommand(arguments_.slice(1));
|
|
2219
2405
|
if (command === "relation") return parseRelationCommand(arguments_.slice(1));
|
|
2220
2406
|
if (command === "percolate") return parsePercolateCommand(arguments_.slice(1));
|
|
2221
2407
|
if (command === "publish") return parsePublishCommand(arguments_.slice(1));
|
|
2222
2408
|
if (command === "serve") return parseServeCommand(arguments_.slice(1));
|
|
2409
|
+
if (command === "mcp") return parseMcpCommand(arguments_.slice(1));
|
|
2223
2410
|
return { ok: false, message: "unknown command" };
|
|
2224
2411
|
}
|
|
2225
2412
|
|
|
@@ -2238,6 +2425,22 @@ function renderSemanticIndex(result: SemanticIndexResult): string {
|
|
|
2238
2425
|
].join("\n");
|
|
2239
2426
|
}
|
|
2240
2427
|
|
|
2428
|
+
function searchForDisplay(result: KnowledgeBaseSearchResult): KnowledgeBaseSearchResult {
|
|
2429
|
+
return {
|
|
2430
|
+
...result,
|
|
2431
|
+
results: result.results.map((hit) => {
|
|
2432
|
+
const passage = hit.selectedPassage;
|
|
2433
|
+
if (passage?.status !== "selected") return hit;
|
|
2434
|
+
const texts = [passage.text, ...passage.headings.map(({ text }) => text)];
|
|
2435
|
+
// Terminal output always renders CRLF as LF and tabs as four spaces; only other changes lose content.
|
|
2436
|
+
if (texts.every((text) => sanitizeTerminalText(redactSensitiveText(text))
|
|
2437
|
+
=== text.replaceAll("\r\n", "\n").replaceAll("\t", " "))) return hit;
|
|
2438
|
+
// Redacted display text cannot retain a claim of exact source bytes.
|
|
2439
|
+
return { ...hit, selectedPassage: { status: "unavailable", reason: "display-sanitized" } as const };
|
|
2440
|
+
}),
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2241
2444
|
function renderKnowledgeBaseSearch(result: KnowledgeBaseSearchResult): string {
|
|
2242
2445
|
const lines = [
|
|
2243
2446
|
`${result.mode[0]?.toLocaleUpperCase("en-US") ?? ""}${result.mode.slice(1)} results for “${safe(result.query)}” (${result.results.length})${result.partial ? " [partial]" : ""}`,
|
|
@@ -2265,7 +2468,20 @@ function renderKnowledgeBaseSearch(result: KnowledgeBaseSearchResult): string {
|
|
|
2265
2468
|
.map((item) => `${item.kind}#${searchEvidenceRank(item)}`)
|
|
2266
2469
|
.join(", ");
|
|
2267
2470
|
lines.push(` ${hit.rank}. ${hit.score.toFixed(3)} ${location} — ${safe(hit.title)} [${safe(evidence)}]`);
|
|
2268
|
-
|
|
2471
|
+
const passage = hit.selectedPassage;
|
|
2472
|
+
if (passage?.status === "selected") {
|
|
2473
|
+
lines.push(` Selected passage: ${safe(hit.path)}:${passage.startLine}-${passage.endLine}`
|
|
2474
|
+
+ (passage.clippedStart || passage.clippedEnd ? " (clipped excerpt)" : ""));
|
|
2475
|
+
for (const heading of passage.headings) {
|
|
2476
|
+
lines.push(` Section: ${safe(heading.text)}${heading.truncated ? " (clipped)" : ""}`);
|
|
2477
|
+
}
|
|
2478
|
+
for (const line of passage.text.split(/\r?\n/u)) lines.push(` ${safe(line)}`);
|
|
2479
|
+
} else {
|
|
2480
|
+
if (hit.snippet !== "") lines.push(` ${safe(hit.snippet)}`);
|
|
2481
|
+
if (passage !== undefined) {
|
|
2482
|
+
lines.push(` Selected passage: ${passage.status} (${safe(passage.reason)}); showing the search snippet.`);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2269
2485
|
}
|
|
2270
2486
|
if ((result.graph?.related.length ?? 0) > 0) {
|
|
2271
2487
|
lines.push(` Related graph context: ${result.graph?.related.length ?? 0}`);
|
|
@@ -2287,7 +2503,14 @@ async function runSemantic(
|
|
|
2287
2503
|
output: Output,
|
|
2288
2504
|
dependencies: CliDependencies,
|
|
2289
2505
|
): Promise<number> {
|
|
2506
|
+
const terminal = dependencies.terminal ?? processTerminal();
|
|
2507
|
+
const person = !command.json && speaksToPerson(terminal);
|
|
2290
2508
|
if (command.kind === "index") {
|
|
2509
|
+
if (person) {
|
|
2510
|
+
const style = terminalStyle(terminal.env, terminal.stderrIsTTY);
|
|
2511
|
+
output.stderr(`${symbol("progress", style)} Indexing ${safe(command.root)}. `
|
|
2512
|
+
+ "The first run downloads a search model of about 300 MB and can take a few minutes.\n");
|
|
2513
|
+
}
|
|
2291
2514
|
const result = await (dependencies.indexSemanticVault ?? indexSemanticVault)({
|
|
2292
2515
|
root: command.root,
|
|
2293
2516
|
...(command.database === undefined ? {} : { database: command.database }),
|
|
@@ -2296,6 +2519,17 @@ async function runSemantic(
|
|
|
2296
2519
|
output.stdout(command.json ? terminalSafeJson(result) : sanitizeTerminalText(renderSemanticIndex(result)));
|
|
2297
2520
|
return 0;
|
|
2298
2521
|
}
|
|
2522
|
+
// With no mode and no index, meaning-based search has nothing to rank, so
|
|
2523
|
+
// search the words instead and say how to turn on the rest. This applies to
|
|
2524
|
+
// the real store only; an injected knowledge base keeps its default.
|
|
2525
|
+
const indexExists = dependencies.semanticIndexExists
|
|
2526
|
+
?? (dependencies.openKnowledgeBase === undefined ? defaultSemanticIndexExists : undefined);
|
|
2527
|
+
// Score thresholds and reranking only apply to ranked modes, so a search
|
|
2528
|
+
// that asks for them keeps the default mode and its own error.
|
|
2529
|
+
const withoutIndex = command.mode === undefined && command.database === undefined
|
|
2530
|
+
&& command.minScore === undefined && command.rerank === undefined
|
|
2531
|
+
&& indexExists !== undefined && !await indexExists(command.root);
|
|
2532
|
+
const mode = withoutIndex ? "exact" as const : command.mode;
|
|
2299
2533
|
const searchRules = command.rulesPath === undefined
|
|
2300
2534
|
? undefined
|
|
2301
2535
|
: await loadSearchRulesFile(command.rulesPath);
|
|
@@ -2313,7 +2547,7 @@ async function runSemantic(
|
|
|
2313
2547
|
try {
|
|
2314
2548
|
const result = await kb.search({
|
|
2315
2549
|
query: command.query,
|
|
2316
|
-
...(
|
|
2550
|
+
...(mode === undefined ? {} : { mode }),
|
|
2317
2551
|
ordering: command.ordering,
|
|
2318
2552
|
filters: command.filters,
|
|
2319
2553
|
tags: command.tags,
|
|
@@ -2325,6 +2559,7 @@ async function runSemantic(
|
|
|
2325
2559
|
? {}
|
|
2326
2560
|
: { candidateLimit: command.candidateLimit }),
|
|
2327
2561
|
...(command.minScore === undefined ? {} : { minScore: command.minScore }),
|
|
2562
|
+
...(command.selectedPassage === undefined ? {} : { selectedPassage: command.selectedPassage }),
|
|
2328
2563
|
...(command.rerank === undefined ? {} : {
|
|
2329
2564
|
rerank: {
|
|
2330
2565
|
engine: command.rerank,
|
|
@@ -2332,9 +2567,15 @@ async function runSemantic(
|
|
|
2332
2567
|
},
|
|
2333
2568
|
}),
|
|
2334
2569
|
});
|
|
2570
|
+
const display = searchForDisplay(result);
|
|
2335
2571
|
output.stdout(command.json
|
|
2336
|
-
? terminalSafeJson(
|
|
2337
|
-
: sanitizeTerminalText(renderKnowledgeBaseSearch(
|
|
2572
|
+
? terminalSafeJson(display)
|
|
2573
|
+
: sanitizeTerminalText(renderKnowledgeBaseSearch(display)));
|
|
2574
|
+
if (withoutIndex && person) {
|
|
2575
|
+
output.stderr("Searched without an index, so only exact words matched. "
|
|
2576
|
+
+ `For meaning-based search, run wordcell index --root ${safe(shellWord(command.root))} `
|
|
2577
|
+
+ "(downloads about 300 MB once).\n");
|
|
2578
|
+
}
|
|
2338
2579
|
return 0;
|
|
2339
2580
|
} finally {
|
|
2340
2581
|
await kb.close();
|
|
@@ -2967,7 +3208,9 @@ async function runNoteCreate(
|
|
|
2967
3208
|
const body = command.body ?? (
|
|
2968
3209
|
command.bodyFile === undefined
|
|
2969
3210
|
? undefined
|
|
2970
|
-
:
|
|
3211
|
+
: command.bodyFile === "-"
|
|
3212
|
+
? await readBoundedStdinUtf8(dependencies.stdin ?? processStdin(), 16 * 1024 * 1024, "note body")
|
|
3213
|
+
: await readBoundedUtf8(command.bodyFile, 16 * 1024 * 1024, "note body")
|
|
2971
3214
|
);
|
|
2972
3215
|
const result = await (dependencies.createNote ?? createNote)(
|
|
2973
3216
|
command.root,
|
|
@@ -2982,6 +3225,67 @@ async function runNoteCreate(
|
|
|
2982
3225
|
return 0;
|
|
2983
3226
|
}
|
|
2984
3227
|
|
|
3228
|
+
function countOf(count: number, noun: string): string {
|
|
3229
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
function shellWord(value: string): string {
|
|
3233
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
function renderImportReport(report: ImportReport): string {
|
|
3237
|
+
const { counts } = report;
|
|
3238
|
+
const tally = `created ${counts.created}, updated ${counts.updated}, skipped ${counts.skipped}, `
|
|
3239
|
+
+ `conflicts ${counts.conflicts}, rejected ${counts.rejected}`;
|
|
3240
|
+
const scope = `${countOf(report.items.length, "item")} from ${countOf(report.files, "file")}`;
|
|
3241
|
+
const lines = [report.dryRun
|
|
3242
|
+
? `Dry run of ${scope}: ${tally}. Nothing was written.`
|
|
3243
|
+
: `Imported ${scope}: ${tally}.`];
|
|
3244
|
+
const location = (item: ImportReport["items"][number]) => item.note === undefined
|
|
3245
|
+
? `${item.file}#${item.pointer}`
|
|
3246
|
+
: `${item.note} from ${item.file}#${item.pointer}`;
|
|
3247
|
+
for (const item of report.items) {
|
|
3248
|
+
if (item.outcome !== "conflict" && item.outcome !== "rejected") continue;
|
|
3249
|
+
const external = item.externalId === undefined ? "" : ` (${item.externalId})`;
|
|
3250
|
+
lines.push(`${item.outcome}: ${safe(location(item))}${safe(external)}: ${safe(item.reason ?? "no reason given")}`);
|
|
3251
|
+
}
|
|
3252
|
+
const added = report.relations.filter((relation) => relation.outcome === "added").length;
|
|
3253
|
+
if (added > 0) {
|
|
3254
|
+
lines.push(`${report.dryRun ? "Would add" : "Added"} ${countOf(added, "supersedes relation")}.`);
|
|
3255
|
+
}
|
|
3256
|
+
for (const relation of report.relations) {
|
|
3257
|
+
if (relation.outcome !== "failed" && relation.outcome !== "omitted") continue;
|
|
3258
|
+
lines.push(`relation ${relation.outcome}: ${safe(relation.source)} supersedes ${safe(relation.target)}: ${safe(relation.reason ?? "no reason given")}`);
|
|
3259
|
+
}
|
|
3260
|
+
for (const diagnostic of report.diagnostics) lines.push(`diagnostic: ${safe(diagnostic)}`);
|
|
3261
|
+
for (const item of report.items) {
|
|
3262
|
+
for (const diagnostic of item.diagnostics ?? []) {
|
|
3263
|
+
lines.push(`diagnostic: ${safe(item.note ?? `${item.file}#${item.pointer}`)}: ${safe(diagnostic)}`);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
if (!report.dryRun && counts.created + counts.updated + added > 0) {
|
|
3267
|
+
const root = safe(shellWord(report.root));
|
|
3268
|
+
lines.push(`Next: wordcell refresh --root ${root}, then wordcell check --root ${root}.`);
|
|
3269
|
+
}
|
|
3270
|
+
return `${lines.join("\n")}\n`;
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
async function runImportSupermemory(
|
|
3274
|
+
command: Extract<ParsedCommand, { readonly kind: "import-supermemory" }>,
|
|
3275
|
+
output: Output,
|
|
3276
|
+
dependencies: CliDependencies,
|
|
3277
|
+
): Promise<number> {
|
|
3278
|
+
const report = await (dependencies.importSupermemory ?? importSupermemory)({
|
|
3279
|
+
root: command.root,
|
|
3280
|
+
files: command.files,
|
|
3281
|
+
...(command.prefix === undefined ? {} : { prefix: command.prefix }),
|
|
3282
|
+
dryRun: command.dryRun,
|
|
3283
|
+
...(dependencies.importNow === undefined ? {} : { now: dependencies.importNow() }),
|
|
3284
|
+
});
|
|
3285
|
+
output.stdout(command.json ? terminalSafeJson(report) : sanitizeTerminalText(renderImportReport(report)));
|
|
3286
|
+
return report.ok ? 0 : 1;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
2985
3289
|
async function runRelation(
|
|
2986
3290
|
command: Extract<ParsedCommand, { readonly kind: "relation" }>,
|
|
2987
3291
|
output: Output,
|
|
@@ -3248,6 +3552,73 @@ async function runServe(
|
|
|
3248
3552
|
return 0;
|
|
3249
3553
|
}
|
|
3250
3554
|
|
|
3555
|
+
/** The `context` tool's payload, identical to `wordcell context --json`. */
|
|
3556
|
+
function mcpContextBuilder(dependencies: CliDependencies): ContextBuilder {
|
|
3557
|
+
return async (snapshot, request) => {
|
|
3558
|
+
const inspection = await (
|
|
3559
|
+
dependencies.inspectAgentContextRepository ?? inspectAgentContextRepository
|
|
3560
|
+
)(snapshot.notes, request);
|
|
3561
|
+
const memory = await (
|
|
3562
|
+
dependencies.buildRepositoryMemoryContext ?? buildRepositoryMemoryContext
|
|
3563
|
+
)(snapshot.notes, { repositoryRoot: request.repositoryRoot, target: inspection.target });
|
|
3564
|
+
return contextPayload(inspection, snapshot, memory);
|
|
3565
|
+
};
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
async function resolveMcpDirectories(
|
|
3569
|
+
command: Extract<ParsedCommand, { readonly kind: "mcp" }>,
|
|
3570
|
+
): Promise<{ readonly root: string; readonly repository?: string }> {
|
|
3571
|
+
const { root } = await resolveVault(command.root);
|
|
3572
|
+
if (command.repository === undefined) return { root };
|
|
3573
|
+
const repository = await realpath(resolve(command.repository));
|
|
3574
|
+
if (!(await stat(repository)).isDirectory()) throw new Error(`--repo is not a directory: ${repository}`);
|
|
3575
|
+
return { root, repository };
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
async function runMcp(
|
|
3579
|
+
command: Extract<ParsedCommand, { readonly kind: "mcp" }>,
|
|
3580
|
+
output: Output,
|
|
3581
|
+
dependencies: CliDependencies,
|
|
3582
|
+
): Promise<number> {
|
|
3583
|
+
let directories: Awaited<ReturnType<typeof resolveMcpDirectories>>;
|
|
3584
|
+
try {
|
|
3585
|
+
directories = await resolveMcpDirectories(command);
|
|
3586
|
+
} catch (error) {
|
|
3587
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3588
|
+
const terminal = dependencies.terminal ?? processTerminal();
|
|
3589
|
+
output.stderr(renderFailure(safe(message), "wordcell mcp --help", terminalStyle(terminal.env, terminal.stderrIsTTY)));
|
|
3590
|
+
return 2;
|
|
3591
|
+
}
|
|
3592
|
+
const version = await serverVersion();
|
|
3593
|
+
const catalog = createToolCatalog({
|
|
3594
|
+
...directories,
|
|
3595
|
+
readOnly: command.readOnly,
|
|
3596
|
+
context: mcpContextBuilder(dependencies),
|
|
3597
|
+
warn: (message) => output.stderr(`${safe(message)}\n`),
|
|
3598
|
+
});
|
|
3599
|
+
const instructions = serverInstructions({ root: directories.root, readOnly: command.readOnly });
|
|
3600
|
+
const streams = mcpStreams(dependencies);
|
|
3601
|
+
try {
|
|
3602
|
+
return await runMcpServer({
|
|
3603
|
+
catalog,
|
|
3604
|
+
instructions,
|
|
3605
|
+
version,
|
|
3606
|
+
input: streams.input,
|
|
3607
|
+
writeFrame: streams.writeFrame,
|
|
3608
|
+
stderr: (text) => output.stderr(text),
|
|
3609
|
+
});
|
|
3610
|
+
} finally {
|
|
3611
|
+
streams.restore();
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
/** Process stdio with stdout held for frames, unless a test supplies its own streams. */
|
|
3616
|
+
function mcpStreams(dependencies: CliDependencies): McpStreams & { readonly restore: () => void } {
|
|
3617
|
+
if (dependencies.mcp !== undefined) return { ...dependencies.mcp, restore: () => undefined };
|
|
3618
|
+
const guard = guardStdout();
|
|
3619
|
+
return { input: process.stdin, writeFrame: guard.writeFrame, restore: guard.restore };
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3251
3622
|
async function runCatalog(
|
|
3252
3623
|
command: Extract<ParsedCommand, { readonly kind: "catalog" }>,
|
|
3253
3624
|
output: Output,
|
|
@@ -3288,20 +3659,37 @@ async function runInbox(
|
|
|
3288
3659
|
return 0;
|
|
3289
3660
|
}
|
|
3290
3661
|
|
|
3662
|
+
function speaksToPerson(terminal: CliTerminal): boolean {
|
|
3663
|
+
return detectAudience(terminal.env, terminal.stderrIsTTY) === "human";
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3291
3666
|
async function runInit(
|
|
3292
3667
|
command: Extract<ParsedCommand, { readonly kind: "init" }>,
|
|
3293
3668
|
output: Output,
|
|
3294
3669
|
initialize: typeof initVault,
|
|
3670
|
+
terminal: CliTerminal,
|
|
3295
3671
|
): Promise<number> {
|
|
3296
3672
|
const result: InitVaultResult = await initialize(command.directory);
|
|
3297
3673
|
if (command.json) output.stdout(terminalSafeJson(result));
|
|
3298
3674
|
else {
|
|
3299
3675
|
const relativeRoot = relative(process.cwd(), result.root) || ".";
|
|
3300
3676
|
output.stdout(`Initialized ${safe(relativeRoot)} with ${result.files.length} files.\n`);
|
|
3677
|
+
if (speaksToPerson(terminal)) {
|
|
3678
|
+
output.stderr(`Next: wordcell search "your question" --root ${safe(shellWord(relativeRoot))}\n`);
|
|
3679
|
+
}
|
|
3301
3680
|
}
|
|
3302
3681
|
return 0;
|
|
3303
3682
|
}
|
|
3304
3683
|
|
|
3684
|
+
async function defaultSemanticIndexExists(root: string): Promise<boolean> {
|
|
3685
|
+
try {
|
|
3686
|
+
await stat(semanticDatabasePath(await realpath(root)));
|
|
3687
|
+
return true;
|
|
3688
|
+
} catch {
|
|
3689
|
+
return false;
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3305
3693
|
async function runCaptureBundle(
|
|
3306
3694
|
command: Extract<ParsedCommand, { readonly kind: "capture-bundle" }>,
|
|
3307
3695
|
output: Output,
|
|
@@ -3950,31 +4338,166 @@ async function runVault(
|
|
|
3950
4338
|
return 0;
|
|
3951
4339
|
}
|
|
3952
4340
|
|
|
4341
|
+
/** `mcp` owns stdout for protocol frames, so it never takes a `--json` path and its failures go to stderr. */
|
|
4342
|
+
function machineJsonRequested(rawArguments: readonly string[]): boolean {
|
|
4343
|
+
return rawArguments[0] !== "mcp" && rawArguments.includes("--json");
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
/** Terminal facts the human renderer needs; injectable for golden tests. */
|
|
4347
|
+
export type CliTerminal = {
|
|
4348
|
+
readonly env: TerminalEnvironment;
|
|
4349
|
+
readonly stdoutIsTTY: boolean;
|
|
4350
|
+
readonly stderrIsTTY: boolean;
|
|
4351
|
+
readonly columns: number | undefined;
|
|
4352
|
+
};
|
|
4353
|
+
|
|
4354
|
+
function processTerminal(): CliTerminal {
|
|
4355
|
+
return {
|
|
4356
|
+
env: process.env,
|
|
4357
|
+
stdoutIsTTY: process.stdout.isTTY === true,
|
|
4358
|
+
stderrIsTTY: process.stderr.isTTY === true,
|
|
4359
|
+
columns: process.stdout.columns,
|
|
4360
|
+
};
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
const SAFE_WORD = /^[A-Za-z][A-Za-z0-9-]{0,39}$/u;
|
|
4364
|
+
const SAFE_OPTION = /^--?[A-Za-z][A-Za-z0-9-]{0,39}$/u;
|
|
4365
|
+
|
|
4366
|
+
/**
|
|
4367
|
+
* The first option the parser rejected, found by parsing growing prefixes of
|
|
4368
|
+
* the arguments. Only the option name is returned, never an `=value` part.
|
|
4369
|
+
*/
|
|
4370
|
+
function rejectedOption(rawArguments: readonly string[], message: string): string | undefined {
|
|
4371
|
+
for (let index = 1; index < rawArguments.length; index += 1) {
|
|
4372
|
+
const token = rawArguments[index] ?? "";
|
|
4373
|
+
if (token === "--") return undefined;
|
|
4374
|
+
if (!token.startsWith("-")) continue;
|
|
4375
|
+
const through = parseArguments(rawArguments.slice(0, index + 1));
|
|
4376
|
+
if (through.ok || through.message !== message) continue;
|
|
4377
|
+
const before = parseArguments(rawArguments.slice(0, index));
|
|
4378
|
+
if (!before.ok && before.message === message) return undefined;
|
|
4379
|
+
const name = token.split("=")[0] ?? "";
|
|
4380
|
+
return SAFE_OPTION.test(name) ? name : undefined;
|
|
4381
|
+
}
|
|
4382
|
+
return undefined;
|
|
4383
|
+
}
|
|
4384
|
+
|
|
4385
|
+
type UsageFailure = { readonly text: string; readonly next: string };
|
|
4386
|
+
|
|
4387
|
+
/** One sentence saying what was wrong with the command line, and one next command. */
|
|
4388
|
+
export function describeUsageError(rawArguments: readonly string[], message: string): UsageFailure {
|
|
4389
|
+
const first = rawArguments[0] ?? "";
|
|
4390
|
+
if (message === "unknown command") {
|
|
4391
|
+
const known = knownCommandWords();
|
|
4392
|
+
const suggestion = SAFE_WORD.test(first) ? closestMatch(first, known) : undefined;
|
|
4393
|
+
const text = SAFE_WORD.test(first)
|
|
4394
|
+
? `Unknown command "${first}".${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`
|
|
4395
|
+
: "Unknown command.";
|
|
4396
|
+
return { text, next: "wordcell --help" };
|
|
4397
|
+
}
|
|
4398
|
+
if (message === "unknown help topic") {
|
|
4399
|
+
const suggestion = SAFE_WORD.test(rawArguments[1] ?? "") ? closestMatch(rawArguments[1] ?? "", knownCommandWords()) : undefined;
|
|
4400
|
+
return {
|
|
4401
|
+
text: `There is no help for that command.${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`,
|
|
4402
|
+
next: "wordcell --help",
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
const id = resolveCommandId(rawArguments.filter((word) => !word.startsWith("-")));
|
|
4406
|
+
const next = id === undefined ? "wordcell --help" : `wordcell ${id} --help`;
|
|
4407
|
+
if (/^unknown .*option/u.test(message) && id !== undefined) {
|
|
4408
|
+
const option = rejectedOption(rawArguments, message);
|
|
4409
|
+
if (option !== undefined) {
|
|
4410
|
+
const suggestion = closestMatch(option, optionNames(id));
|
|
4411
|
+
return {
|
|
4412
|
+
text: `Unknown option "${option}" for ${id}.${suggestion === undefined ? "" : ` Did you mean "${suggestion}"?`}`,
|
|
4413
|
+
next,
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
return { text: sentence(safe(message)), next };
|
|
4418
|
+
}
|
|
4419
|
+
|
|
4420
|
+
function runtimeNext(rawArguments: readonly string[]): string {
|
|
4421
|
+
const id = resolveCommandId(rawArguments.filter((word) => !word.startsWith("-")));
|
|
4422
|
+
return id === undefined ? "wordcell --help" : `wordcell ${id} --help`;
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
async function renderHelp(
|
|
4426
|
+
command: Extract<ParsedCommand, { readonly kind: "help" }>,
|
|
4427
|
+
output: Output,
|
|
4428
|
+
terminal: CliTerminal,
|
|
4429
|
+
readVersion: () => Promise<string>,
|
|
4430
|
+
showIntro: boolean,
|
|
4431
|
+
): Promise<number> {
|
|
4432
|
+
const intro = showIntro
|
|
4433
|
+
? terminalIntro({ isTTY: terminal.stdoutIsTTY, columns: terminal.columns, term: terminal.env.TERM })
|
|
4434
|
+
: "";
|
|
4435
|
+
if (command.topic === undefined) {
|
|
4436
|
+
output.stdout(intro + sanitizeTerminalText(usage));
|
|
4437
|
+
return 0;
|
|
4438
|
+
}
|
|
4439
|
+
if (command.topic === "start") {
|
|
4440
|
+
const version = await readVersion().catch(() => undefined);
|
|
4441
|
+
output.stdout(intro + sanitizeTerminalText(startHelp(version)));
|
|
4442
|
+
return 0;
|
|
4443
|
+
}
|
|
4444
|
+
if (command.topic === "advanced") {
|
|
4445
|
+
output.stdout(sanitizeTerminalText(advancedHelp()));
|
|
4446
|
+
return 0;
|
|
4447
|
+
}
|
|
4448
|
+
output.stdout(sanitizeTerminalText(commandHelpText(command.topic) ?? usage));
|
|
4449
|
+
return 0;
|
|
4450
|
+
}
|
|
4451
|
+
|
|
3953
4452
|
/** Stable CLI entry point with injectable filesystem and capture boundaries. */
|
|
3954
4453
|
export async function main(
|
|
3955
4454
|
rawArguments: readonly string[] = process.argv.slice(2),
|
|
3956
4455
|
output: Output = defaultOutput,
|
|
3957
4456
|
dependencies: CliDependencies = {},
|
|
3958
4457
|
): Promise<number> {
|
|
3959
|
-
const jsonRequested = rawArguments
|
|
4458
|
+
const jsonRequested = machineJsonRequested(rawArguments);
|
|
4459
|
+
const terminal = dependencies.terminal ?? processTerminal();
|
|
4460
|
+
const errorStyle = terminalStyle(terminal.env, terminal.stderrIsTTY);
|
|
4461
|
+
const readVersion = dependencies.packageVersion ?? (() => serverVersion());
|
|
3960
4462
|
const parsed = parseArguments(rawArguments);
|
|
3961
4463
|
if (!parsed.ok) {
|
|
4464
|
+
const failure = describeUsageError(rawArguments, parsed.message);
|
|
3962
4465
|
if (jsonRequested) {
|
|
3963
4466
|
output.stdout(terminalSafeJson({
|
|
3964
4467
|
ok: false,
|
|
3965
|
-
error: { kind: "parse", message: parsed.message },
|
|
4468
|
+
error: { kind: "parse", code: "usage", message: parsed.message, next: failure.next },
|
|
3966
4469
|
}));
|
|
3967
4470
|
} else {
|
|
3968
|
-
output.stderr(
|
|
4471
|
+
output.stderr(renderFailure(failure.text, failure.next, errorStyle));
|
|
3969
4472
|
}
|
|
3970
4473
|
return 2;
|
|
3971
4474
|
}
|
|
3972
4475
|
const command = parsed.value;
|
|
3973
|
-
if (command.kind === "
|
|
3974
|
-
|
|
3975
|
-
output.stdout(
|
|
4476
|
+
if (command.kind === "version") {
|
|
4477
|
+
const version = await readVersion();
|
|
4478
|
+
output.stdout(command.json || jsonRequested
|
|
4479
|
+
? terminalSafeJson({ name: "wordcell", version })
|
|
4480
|
+
: `wordcell ${version}\n`);
|
|
3976
4481
|
return 0;
|
|
3977
4482
|
}
|
|
4483
|
+
if (command.kind === "help") {
|
|
4484
|
+
if (jsonRequested) {
|
|
4485
|
+
const text: string[] = [];
|
|
4486
|
+
await renderHelp(command, { stdout: (value) => text.push(value), stderr: output.stderr }, terminal, readVersion, false);
|
|
4487
|
+
output.stdout(terminalSafeJson({ kind: "help", text: text.join("") }));
|
|
4488
|
+
return 0;
|
|
4489
|
+
}
|
|
4490
|
+
return renderHelp(command, output, terminal, readVersion, output === defaultOutput);
|
|
4491
|
+
}
|
|
4492
|
+
if (jsonRequested && isDelegatedHelp(command)) {
|
|
4493
|
+
const text: string[] = [];
|
|
4494
|
+
const code = await main([command.kind === "clip" ? "clip" : command.kind, "--help"], {
|
|
4495
|
+
stdout: (value) => text.push(value),
|
|
4496
|
+
stderr: output.stderr,
|
|
4497
|
+
}, dependencies);
|
|
4498
|
+
output.stdout(terminalSafeJson({ kind: "help", text: text.join("") }));
|
|
4499
|
+
return code;
|
|
4500
|
+
}
|
|
3978
4501
|
try {
|
|
3979
4502
|
if (command.kind === "clip") {
|
|
3980
4503
|
return await (dependencies.runClipCommand ?? runClipCommand)(command.arguments, process.env, output);
|
|
@@ -3990,7 +4513,7 @@ export async function main(
|
|
|
3990
4513
|
return await (dependencies.runPdfCommand ?? runPdfCommand)(command.arguments, process.env, output);
|
|
3991
4514
|
}
|
|
3992
4515
|
if (command.kind === "init") {
|
|
3993
|
-
return await runInit(command, output, dependencies.initVault ?? initVault);
|
|
4516
|
+
return await runInit(command, output, dependencies.initVault ?? initVault, dependencies.terminal ?? processTerminal());
|
|
3994
4517
|
}
|
|
3995
4518
|
if (command.kind === "index" || command.kind === "search") {
|
|
3996
4519
|
return await runSemantic(command, output, dependencies);
|
|
@@ -4001,6 +4524,7 @@ export async function main(
|
|
|
4001
4524
|
if (command.kind === "agent-identity") return runAgentIdentity(command, output);
|
|
4002
4525
|
if (command.kind === "agents") return await runAgents(command, output, dependencies);
|
|
4003
4526
|
if (command.kind === "note-create") return await runNoteCreate(command, output, dependencies);
|
|
4527
|
+
if (command.kind === "import-supermemory") return await runImportSupermemory(command, output, dependencies);
|
|
4004
4528
|
if (command.kind === "relation") return await runRelation(command, output, dependencies);
|
|
4005
4529
|
if (command.kind === "graph-rebuild" || command.kind === "graph-verify" || command.kind === "graph-query") {
|
|
4006
4530
|
const result = await executeGraphCommand(command, dependencies);
|
|
@@ -4011,18 +4535,23 @@ export async function main(
|
|
|
4011
4535
|
if (command.kind === "list") return await runList(command, output, dependencies);
|
|
4012
4536
|
if (command.kind === "publish") return await runPublish(command, output, dependencies);
|
|
4013
4537
|
if (command.kind === "serve") return await runServe(command, output, dependencies);
|
|
4538
|
+
if (command.kind === "mcp") return await runMcp(command, output, dependencies);
|
|
4014
4539
|
if (command.kind === "inbox") return await runInbox(command, output, dependencies);
|
|
4015
4540
|
if (command.kind === "catalog") return await runCatalog(command, output, dependencies);
|
|
4016
4541
|
return await runVault(command, output, dependencies);
|
|
4017
4542
|
} catch (error) {
|
|
4018
4543
|
const message = error instanceof Error ? error.message : String(error);
|
|
4544
|
+
const next = runtimeNext(rawArguments);
|
|
4019
4545
|
if (jsonRequested) {
|
|
4020
4546
|
output.stdout(terminalSafeJson({
|
|
4021
4547
|
ok: false,
|
|
4022
|
-
error: { kind: "runtime", message },
|
|
4548
|
+
error: { kind: "runtime", message, next },
|
|
4023
4549
|
}));
|
|
4024
4550
|
} else {
|
|
4025
|
-
output.stderr(
|
|
4551
|
+
output.stderr(renderFailure(safe(message), next, errorStyle));
|
|
4552
|
+
if (terminal.env.HRANESS_DEBUG === "1" && error instanceof Error && error.stack !== undefined) {
|
|
4553
|
+
output.stderr(`${sanitizeTerminalText(redactSensitiveText(error.stack))}\n`);
|
|
4554
|
+
}
|
|
4026
4555
|
}
|
|
4027
4556
|
return 1;
|
|
4028
4557
|
}
|
|
@@ -4061,7 +4590,7 @@ export async function runExecutable(
|
|
|
4061
4590
|
rawArguments: readonly string[] = process.argv.slice(2),
|
|
4062
4591
|
dependencies: CliDependencies = {},
|
|
4063
4592
|
): Promise<number> {
|
|
4064
|
-
if (!rawArguments
|
|
4593
|
+
if (!machineJsonRequested(rawArguments)) {
|
|
4065
4594
|
return main(rawArguments, defaultOutput, dependencies);
|
|
4066
4595
|
}
|
|
4067
4596
|
return serializeStrictJson(async () => {
|