@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.
Files changed (80) hide show
  1. package/README.md +63 -22
  2. package/dist/authoring.js +2 -2
  3. package/dist/{authority-06eqhqam.js → authority-35cbvp1x.js} +2 -2
  4. package/dist/benchmark.js +3 -3
  5. package/dist/capture.js +6 -6
  6. package/dist/cli.js +26 -24
  7. package/dist/clip/acquire.js +3 -3
  8. package/dist/clip/args.js +1 -1
  9. package/dist/clip/cli.js +6 -6
  10. package/dist/clip/doctor.js +4 -4
  11. package/dist/evaluation-builder.js +10 -10
  12. package/dist/evaluation-kb.js +10 -10
  13. package/dist/graph-authority.js +4 -4
  14. package/dist/graph-percolation.js +6 -6
  15. package/dist/graph.js +5 -1
  16. package/dist/{index-bcknqxrq.js → index-0eacgvpv.js} +9 -1
  17. package/dist/{index-xv80vzfp.js → index-1er88ckw.js} +4 -4
  18. package/dist/{index-233z9wmn.js → index-1s8mc4hz.js} +13 -5
  19. package/dist/{index-de2w8crk.js → index-2fmnttca.js} +82 -15
  20. package/dist/{index-vnybgywh.js → index-ahyhryb8.js} +1 -1
  21. package/dist/{index-ecqpgr5y.js → index-b0khj0h5.js} +164 -9
  22. package/dist/{index-jk1sgx36.js → index-bdwcjvr4.js} +1 -1
  23. package/dist/{index-dfag79p7.js → index-byz4kzww.js} +3 -0
  24. package/dist/{index-hgve9rh2.js → index-cd75vky9.js} +96 -1
  25. package/dist/{index-rr9kzq4n.js → index-fp732bgg.js} +2 -2
  26. package/dist/{index-qbssx940.js → index-jvb7w0gg.js} +227 -9
  27. package/dist/{index-f984hw45.js → index-k86wepd8.js} +1 -1
  28. package/dist/{index-1k595z6v.js → index-mnq2wy34.js} +2 -2
  29. package/dist/{index-n386evxj.js → index-nn67kzwa.js} +1 -1
  30. package/dist/{index-fc4dr114.js → index-nwrehke7.js} +1 -1
  31. package/dist/{index-76dbn6pe.js → index-p70t8j13.js} +1 -1
  32. package/dist/{index-tf4wzjew.js → index-pgtm2nhf.js} +1 -1
  33. package/dist/{index-054mb7d3.js → index-qfqjfxaa.js} +10 -9
  34. package/dist/{index-pnt9wr59.js → index-r0m5taz2.js} +2 -2
  35. package/dist/{index-bqtqeak3.js → index-r4qs6vq4.js} +4 -4
  36. package/dist/{index-djrcc1yf.js → index-tcaq1c7f.js} +4 -4
  37. package/dist/{index-q7tbg07z.js → index-tvd2sqrx.js} +7 -7
  38. package/dist/{index-9h7zh0wt.js → index-vtwbapjr.js} +3814 -162
  39. package/dist/{index-g5vsqmdy.js → index-xwxy71ew.js} +190 -4
  40. package/dist/index.js +22 -14
  41. package/dist/pdf.js +3 -3
  42. package/dist/percolate.js +2 -2
  43. package/dist/portfolio.js +10 -10
  44. package/dist/publish.js +3 -3
  45. package/dist/sdk.js +9 -9
  46. package/dist/search.js +2 -2
  47. package/dist/semantic.js +4 -4
  48. package/dist/{support-m3t917ev.js → support-c8hy7qfa.js} +25 -23
  49. package/dist/url-intelligence.js +1 -1
  50. package/dist/workflows/decision-context.js +10 -10
  51. package/dist/workflows/index.js +10 -10
  52. package/package.json +12 -4
  53. package/skills/wordcell/AGENTS.md +1 -0
  54. package/skills/wordcell/SKILL.md +5 -3
  55. package/skills/wordcell/references/plan-structure.md +1 -1
  56. package/skills/wordcell/references/query.md +35 -1
  57. package/skills/wordcell/references/session-memory.md +207 -0
  58. package/skills/wordcell/references/url-platforms.md +1 -1
  59. package/src/authoring-import.ts +32 -0
  60. package/src/authoring-model.ts +185 -2
  61. package/src/authoring-program.ts +19 -7
  62. package/src/cli-help.ts +893 -0
  63. package/src/cli-intro.ts +1 -1
  64. package/src/cli-program.ts +575 -82
  65. package/src/cli-style.ts +132 -0
  66. package/src/clip/acquire.ts +20 -0
  67. package/src/clip/args.ts +3 -0
  68. package/src/clip/cli.ts +82 -5
  69. package/src/clip/cookie-permission.ts +241 -0
  70. package/src/clip/url-metadata-cli.ts +6 -5
  71. package/src/clip/url-metadata.ts +14 -0
  72. package/src/graph.ts +88 -13
  73. package/src/import-supermemory.ts +1629 -0
  74. package/src/mcp-server.ts +415 -0
  75. package/src/mcp-tools.ts +1122 -0
  76. package/src/mention-index.ts +194 -0
  77. package/src/pdf/cli.ts +5 -4
  78. package/src/support-profile.ts +1 -1
  79. package/src/support.ts +3 -0
  80. package/src/vault.ts +26 -2
@@ -0,0 +1,194 @@
1
+ /** Finite UTF-16 multi-pattern matching for complete vault analysis. */
2
+ export type MentionIndexLimits = {
3
+ readonly maxNodes?: number;
4
+ readonly maxWork?: number;
5
+ readonly maxMatches?: number;
6
+ readonly maxInputCodeUnits?: number;
7
+ };
8
+
9
+ export const MENTION_INDEX_LIMITS = {
10
+ maxNodes: 262_144,
11
+ maxWork: 64 * 1_024 * 1_024,
12
+ maxMatches: 1_000_000,
13
+ maxInputCodeUnits: 256 * 1_024 * 1_024,
14
+ } as const;
15
+
16
+ export type MentionIndexBudgetKind =
17
+ | "mention-index-nodes"
18
+ | "mention-index-work"
19
+ | "mention-matches"
20
+ | "mention-input-code-units";
21
+
22
+ export class MentionIndexBudgetError extends RangeError {
23
+ readonly kind: MentionIndexBudgetKind;
24
+ readonly limit: number;
25
+
26
+ constructor(kind: MentionIndexBudgetKind, limit: number) {
27
+ super(`Vault analysis exceeds the ${limit} ${kind} limit.`);
28
+ this.kind = kind;
29
+ this.limit = limit;
30
+ }
31
+ }
32
+
33
+ function checkedLimits(options: MentionIndexLimits): Record<keyof MentionIndexLimits, number> {
34
+ const limits: Record<keyof MentionIndexLimits, number> = { ...MENTION_INDEX_LIMITS };
35
+ for (const name of Object.keys(MENTION_INDEX_LIMITS) as (keyof MentionIndexLimits)[]) {
36
+ const value = options[name] ?? MENTION_INDEX_LIMITS[name];
37
+ if (!Number.isSafeInteger(value) || value < 0 || value > MENTION_INDEX_LIMITS[name]) {
38
+ throw new RangeError(`${name} must be a safe integer from 0 through ${MENTION_INDEX_LIMITS[name]}.`);
39
+ }
40
+ limits[name] = value;
41
+ }
42
+ return limits;
43
+ }
44
+
45
+ /** Admit original strings before trimming, normalizing, or indexing any phrase. */
46
+ export function preflightMentionInput(inputs: Iterable<string>, options: MentionIndexLimits): { rawInputCodeUnits: number; work: number } {
47
+ const limits = checkedLimits(options);
48
+ let rawInputCodeUnits = 0, work = 0;
49
+ for (const input of inputs) {
50
+ rawInputCodeUnits += input.length;
51
+ if (rawInputCodeUnits > limits.maxInputCodeUnits) throw new MentionIndexBudgetError("mention-input-code-units", limits.maxInputCodeUnits);
52
+ work += input.length + 1;
53
+ if (work > limits.maxWork) throw new MentionIndexBudgetError("mention-index-work", limits.maxWork);
54
+ }
55
+ return { rawInputCodeUnits, work };
56
+ }
57
+
58
+ export type MentionPattern = {
59
+ readonly targetId: string;
60
+ readonly phrase: string;
61
+ readonly lowerPhrase: string;
62
+ readonly rank: number;
63
+ };
64
+ export type IndexedMention = { readonly pattern: MentionPattern; readonly offset: number };
65
+ type TrieNode = {
66
+ readonly next: Map<number, number>;
67
+ failure: number;
68
+ output: number;
69
+ pattern: MentionPattern | undefined;
70
+ };
71
+
72
+ /** The legacy matcher uses ASCII word boundaries, even for Unicode phrases. */
73
+ export function cleanMentionBoundary(text: string, phrase: string, offset: number): boolean {
74
+ const word = (value: string): boolean => /[A-Za-z0-9]/.test(value);
75
+ return (!word(phrase[0] ?? "") || !word(text[offset - 1] ?? ""))
76
+ && (!word(phrase.at(-1) ?? "") || !word(text[offset + phrase.length] ?? ""));
77
+ }
78
+
79
+ export class MentionMatcher {
80
+ private readonly limits: Record<keyof MentionIndexLimits, number>;
81
+ private readonly nodes: TrieNode[] = [];
82
+ private work = 0;
83
+ private matches = 0;
84
+ private input = 0;
85
+
86
+ constructor(patterns: readonly MentionPattern[], options: MentionIndexLimits = {}, rawInputCodeUnits = 0, preflightWork = 0) {
87
+ this.limits = checkedLimits(options);
88
+ if (!Number.isSafeInteger(preflightWork) || preflightWork < 0) throw new RangeError("Invalid mention preflight work.");
89
+ if (preflightWork > this.limits.maxWork) this.fail("mention-index-work", this.limits.maxWork);
90
+ this.work = preflightWork;
91
+ if (!Number.isSafeInteger(rawInputCodeUnits) || rawInputCodeUnits < 0) throw new RangeError("Invalid mention input length.");
92
+ if (rawInputCodeUnits > this.limits.maxInputCodeUnits) this.fail("mention-input-code-units", this.limits.maxInputCodeUnits);
93
+ this.node();
94
+ for (const pattern of patterns) {
95
+ this.inputUnits(pattern.lowerPhrase.length);
96
+ let state = 0;
97
+ for (let index = 0; index < pattern.lowerPhrase.length; index += 1) {
98
+ this.step();
99
+ const character = pattern.lowerPhrase.charCodeAt(index);
100
+ let next = this.nodes[state]!.next.get(character);
101
+ if (next === undefined) {
102
+ next = this.node();
103
+ this.nodes[state]!.next.set(character, next);
104
+ }
105
+ state = next;
106
+ }
107
+ if (pattern.lowerPhrase.length === 0 || this.nodes[state]!.pattern !== undefined) {
108
+ throw new TypeError("Mention index requires nonempty, uniquely owned normalized phrases.");
109
+ }
110
+ this.nodes[state]!.pattern = pattern;
111
+ }
112
+ const queue = [...this.nodes[0]!.next.values()];
113
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
114
+ const state = queue[cursor]!;
115
+ for (const [character, child] of this.nodes[state]!.next) {
116
+ this.step();
117
+ let fallback = this.nodes[state]!.failure;
118
+ while (fallback !== 0 && !this.nodes[fallback]!.next.has(character)) {
119
+ this.step();
120
+ fallback = this.nodes[fallback]!.failure;
121
+ }
122
+ const failure = this.nodes[fallback]!.next.get(character) ?? 0;
123
+ this.nodes[child]!.failure = failure;
124
+ this.nodes[child]!.output = this.nodes[failure]!.pattern === undefined
125
+ ? this.nodes[failure]!.output : failure;
126
+ queue.push(child);
127
+ }
128
+ }
129
+ }
130
+
131
+ private fail(kind: MentionIndexBudgetKind, limit: number): never { throw new MentionIndexBudgetError(kind, limit); }
132
+ private step(): void {
133
+ if (this.work >= this.limits.maxWork) this.fail("mention-index-work", this.limits.maxWork);
134
+ this.work += 1;
135
+ }
136
+ private node(): number {
137
+ if (this.nodes.length >= this.limits.maxNodes) this.fail("mention-index-nodes", this.limits.maxNodes);
138
+ this.nodes.push({ next: new Map(), failure: 0, output: -1, pattern: undefined });
139
+ return this.nodes.length - 1;
140
+ }
141
+ private inputUnits(count: number): void {
142
+ this.input += count;
143
+ if (this.input > this.limits.maxInputCodeUnits) this.fail("mention-input-code-units", this.limits.maxInputCodeUnits);
144
+ }
145
+
146
+ newlineOffsets(originalText: string): readonly number[] {
147
+ const offsets: number[] = [];
148
+ for (let index = 0; index < originalText.length; index += 1) {
149
+ this.step();
150
+ if (originalText.charCodeAt(index) === 10) offsets.push(index);
151
+ }
152
+ return offsets;
153
+ }
154
+
155
+ lineAt(offsets: readonly number[], offset: number): number {
156
+ let low = 0, high = offsets.length;
157
+ while (low < high) {
158
+ this.step();
159
+ const middle = low + Math.floor((high - low) / 2);
160
+ if (offsets[middle]! < offset) low = middle + 1;
161
+ else high = middle;
162
+ }
163
+ return low + 1;
164
+ }
165
+
166
+ scan(lowerText: string): ReadonlyMap<string, IndexedMention> {
167
+ this.inputUnits(lowerText.length);
168
+ const result = new Map<string, IndexedMention>();
169
+ let state = 0;
170
+ for (let index = 0; index < lowerText.length; index += 1) {
171
+ this.step();
172
+ const character = lowerText.charCodeAt(index);
173
+ while (state !== 0 && !this.nodes[state]!.next.has(character)) {
174
+ this.step();
175
+ state = this.nodes[state]!.failure;
176
+ }
177
+ state = this.nodes[state]!.next.get(character) ?? 0;
178
+ let output = this.nodes[state]!.pattern === undefined ? this.nodes[state]!.output : state;
179
+ while (output !== -1) {
180
+ if (this.matches >= this.limits.maxMatches) this.fail("mention-matches", this.limits.maxMatches);
181
+ this.matches += 1; // Count overlapping/repeated outputs before deduplication or boundary filtering.
182
+ const pattern = this.nodes[output]!.pattern!;
183
+ const offset = index + 1 - pattern.lowerPhrase.length;
184
+ const existing = result.get(pattern.targetId);
185
+ if (cleanMentionBoundary(lowerText, pattern.lowerPhrase, offset)
186
+ && (existing === undefined || pattern.rank < existing.pattern.rank)) {
187
+ result.set(pattern.targetId, { pattern, offset });
188
+ }
189
+ output = this.nodes[output]!.output;
190
+ }
191
+ }
192
+ return result;
193
+ }
194
+ }
package/src/pdf/cli.ts CHANGED
@@ -2,6 +2,7 @@ import { lstatSync, readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
 
4
4
  import { redactSensitiveText } from "../clip/persist.js";
5
+ import { renderFailure, sentence, stderrStyle, terminalOutput } from "../cli-style.js";
5
6
  import { sanitizeTerminalLine, sanitizeTerminalText } from "../clip/terminal.js";
6
7
  import { parsePdfArguments, pdfUsage } from "./args.js";
7
8
  import { runPdfCapture } from "./capture.js";
@@ -19,10 +20,10 @@ export type PdfCliOutput = {
19
20
  readonly stderr: (value: string) => void;
20
21
  };
21
22
 
22
- const defaultOutput: PdfCliOutput = {
23
+ const defaultOutput: PdfCliOutput = terminalOutput({
23
24
  stdout: (value) => process.stdout.write(value),
24
25
  stderr: (value) => process.stderr.write(value),
25
- };
26
+ });
26
27
 
27
28
  export type PdfCliDependencies = {
28
29
  readonly runPdfCapture?: typeof runPdfCapture;
@@ -89,7 +90,7 @@ export async function main(
89
90
  ): Promise<number> {
90
91
  const parsed = parsePdfArguments(rawArguments, environment);
91
92
  if (!parsed.ok) {
92
- output.stderr(`error: ${safe(parsed.message)}\n\n${sanitizeTerminalText(pdfUsage)}`);
93
+ output.stderr(renderFailure(sentence(safe(parsed.message)), "wordcell pdf --help", stderrStyle(environment, output)));
93
94
  return 2;
94
95
  }
95
96
  const arguments_ = parsed.value;
@@ -149,7 +150,7 @@ export async function main(
149
150
  } catch (error) {
150
151
  const message = safe(error instanceof Error ? error.message : String(error));
151
152
  if (arguments_.json) output.stdout(terminalSafeJson({ ok: false, error: message }));
152
- else output.stderr(`error: ${message}\n`);
153
+ else output.stderr(renderFailure(message, "wordcell doctor", stderrStyle(environment, output)));
153
154
  return 1;
154
155
  } finally {
155
156
  preparedSource?.dispose();
@@ -2,6 +2,6 @@
2
2
  export const supportProfile = {
3
3
  id: "kb",
4
4
  name: "Wordcell",
5
- valueProposition: "Support ongoing development of inspectable Markdown memory.",
5
+ valueProposition: "Support ongoing development of Wordcell.",
6
6
  updates: false,
7
7
  } as const;
package/src/support.ts CHANGED
@@ -52,6 +52,9 @@ export function isUsefulSupportResult(args: readonly string[], environment: Read
52
52
  const metadata = parseUrlMetadataArguments(command.arguments, environment);
53
53
  return metadata.ok && metadata.value.kind === "backfill";
54
54
  }
55
+ case "mcp":
56
+ // stdout belongs to JSON-RPC frames, so a long-running server never invites.
57
+ return false;
55
58
  default:
56
59
  return false;
57
60
  }
package/src/vault.ts CHANGED
@@ -13,6 +13,7 @@ import { basename, dirname, join, relative, resolve, sep } from "node:path";
13
13
 
14
14
  import {
15
15
  analyzeVault,
16
+ analyzeVaultComplete,
16
17
  isCanonicalNoteId,
17
18
  lookupNote,
18
19
  MAX_ANALYZED_NOTES,
@@ -21,6 +22,7 @@ import {
21
22
  renderCatalog,
22
23
  replaceCatalog,
23
24
  type AnalyzeVaultOptions,
25
+ type CompleteAnalyzeVaultOptions,
24
26
  type Note,
25
27
  type VaultAnalysis,
26
28
  } from "./graph.js";
@@ -90,6 +92,8 @@ export type ScanVaultOptions = Omit<AnalyzeVaultOptions, "mentionScope"> & {
90
92
  readonly mentionScope?: string | false;
91
93
  };
92
94
 
95
+ export type CompleteScanVaultOptions = ScanVaultOptions & Pick<CompleteAnalyzeVaultOptions, "mentionIndexLimits">;
96
+
93
97
  export async function markdownFiles(
94
98
  directory: string,
95
99
  ignoredDirectories: ReadonlySet<string> = defaultIgnoredDirectories,
@@ -520,8 +524,9 @@ async function atomicReplace(
520
524
 
521
525
  async function snapshot(
522
526
  rootInput: string,
523
- options: ScanVaultOptions,
527
+ options: CompleteScanVaultOptions,
524
528
  writeIndex: boolean,
529
+ completeMentions = false,
525
530
  ): Promise<VaultSnapshot> {
526
531
  const requestedRoot = resolve(rootInput);
527
532
  const root = await realpath(requestedRoot);
@@ -613,7 +618,7 @@ async function snapshot(
613
618
  catalogMode,
614
619
  index,
615
620
  notes,
616
- analysis: analyzeVault(notes, {
621
+ analysis: (completeMentions ? analyzeVaultComplete : analyzeVault)(notes, {
617
622
  catalogNoteId,
618
623
  ...(options.includeInSuggestions === undefined
619
624
  ? {}
@@ -631,6 +636,9 @@ async function snapshot(
631
636
  ...(options.maxMentions === undefined
632
637
  ? {}
633
638
  : { maxMentions: options.maxMentions }),
639
+ ...(!completeMentions || options.mentionIndexLimits === undefined
640
+ ? {}
641
+ : { mentionIndexLimits: options.mentionIndexLimits }),
634
642
  }),
635
643
  };
636
644
  }
@@ -648,3 +656,19 @@ export async function refreshVault(
648
656
  ): Promise<VaultSnapshot> {
649
657
  return snapshot(root, options, true);
650
658
  }
659
+
660
+ /** Read every admitted note and compute the full graph through the phrase index. */
661
+ export async function scanVaultComplete(
662
+ root = ".",
663
+ options: CompleteScanVaultOptions = {},
664
+ ): Promise<VaultSnapshot> {
665
+ return snapshot(root, options, false, true);
666
+ }
667
+
668
+ /** Refresh the configured catalog while retaining complete indexed analysis. */
669
+ export async function refreshVaultComplete(
670
+ root = ".",
671
+ options: CompleteScanVaultOptions = {},
672
+ ): Promise<VaultSnapshot> {
673
+ return snapshot(root, options, true, true);
674
+ }