@gmickel/gno 2.8.2 → 2.8.3

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 (57) hide show
  1. package/README.md +1 -1
  2. package/browser-extension/artifacts/{gno-browser-clipper-v2.8.2.zip → gno-browser-clipper-v2.8.3.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v2.8.3.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +9 -1
  6. package/spec/cli.md +66 -4
  7. package/spec/mcp.md +5 -6
  8. package/src/cli/commands/daemon.ts +7 -2
  9. package/src/cli/commands/doctor.ts +42 -24
  10. package/src/cli/commands/index-cmd.ts +10 -2
  11. package/src/cli/commands/links.ts +5 -22
  12. package/src/cli/commands/shared.ts +26 -1
  13. package/src/cli/commands/status.ts +60 -0
  14. package/src/cli/commands/update.ts +6 -1
  15. package/src/cli/commands/vec.ts +42 -2
  16. package/src/cli/program.ts +2 -2
  17. package/src/config/types.ts +19 -0
  18. package/src/converters/adapters/markitdownTs/adapter.ts +1 -52
  19. package/src/converters/adapters/shared/ooxml-protection.ts +58 -0
  20. package/src/converters/adapters/xlsx/adapter.ts +132 -0
  21. package/src/converters/adapters/xlsx/sheet-markdown.ts +312 -0
  22. package/src/converters/registry.ts +62 -6
  23. package/src/converters/versions.ts +2 -0
  24. package/src/core/audit-freshness.ts +1 -1
  25. package/src/core/audit-outside-index.ts +48 -3
  26. package/src/core/change-diff.ts +16 -4
  27. package/src/core/link-destination-parse.ts +21 -4
  28. package/src/core/link-inventory-markdown.ts +23 -3
  29. package/src/core/links.ts +74 -16
  30. package/src/core/network-boundary-inventory.ts +18 -0
  31. package/src/index.ts +10 -0
  32. package/src/ingestion/document-metadata.ts +256 -0
  33. package/src/ingestion/file-child-env.ts +9 -0
  34. package/src/ingestion/file-child.ts +106 -0
  35. package/src/ingestion/file-processor.ts +546 -0
  36. package/src/ingestion/frontmatter.ts +6 -2
  37. package/src/ingestion/index.ts +1 -0
  38. package/src/ingestion/prepare-convert.ts +32 -0
  39. package/src/ingestion/prepare-file.ts +272 -0
  40. package/src/ingestion/prepare-worker.ts +43 -0
  41. package/src/ingestion/record-container.ts +7 -2
  42. package/src/ingestion/strip.ts +71 -8
  43. package/src/ingestion/sync-options.ts +7 -1
  44. package/src/ingestion/sync.ts +207 -400
  45. package/src/ingestion/types.ts +18 -0
  46. package/src/llm/native-worker/embedding-identity.ts +15 -3
  47. package/src/mcp/http-transport.ts +11 -0
  48. package/src/mcp/tools/links.ts +10 -38
  49. package/src/serve/resident-request.ts +19 -0
  50. package/src/serve/resident-runtime.ts +84 -8
  51. package/src/serve/routes/links.ts +6 -24
  52. package/src/serve/server.ts +3 -2
  53. package/src/sessions/config-refresh.ts +3 -2
  54. package/src/store/sqlite/graph-similarity.ts +5 -13
  55. package/src/store/vector/partition-index.ts +100 -0
  56. package/src/store/vector/stored-vectors.ts +20 -15
  57. package/browser-extension/artifacts/gno-browser-clipper-v2.8.2.zip.sha256 +0 -1
@@ -10,7 +10,7 @@ import type {
10
10
  RecordAdapter,
11
11
  } from "./types";
12
12
 
13
- import { unsupportedError } from "./errors";
13
+ import { adapterError, unsupportedError } from "./errors";
14
14
 
15
15
  export class ConverterRegistry {
16
16
  private readonly converters: Converter[] = [];
@@ -71,13 +71,48 @@ export class ConverterRegistry {
71
71
  }
72
72
  }
73
73
 
74
+ /**
75
+ * Load an adapter module; if it cannot load in this runtime, stand in a
76
+ * converter that fails its file types with ADAPTER_FAILURE and the reason.
77
+ */
78
+ export async function loadAdapter(
79
+ load: () => Promise<Converter>,
80
+ id: string,
81
+ extensions: readonly string[],
82
+ mimes: readonly string[]
83
+ ): Promise<Converter> {
84
+ try {
85
+ return await load();
86
+ } catch (cause) {
87
+ const reason = cause instanceof Error ? cause.message : String(cause);
88
+ return {
89
+ id,
90
+ version: "unavailable",
91
+ // Same matching as the real adapter, so a sniffed MIME without an
92
+ // extension still reports ADAPTER_FAILURE, not UNSUPPORTED.
93
+ canHandle: (mime, ext) =>
94
+ extensions.includes(ext) || mimes.includes(mime),
95
+ convert: (input) =>
96
+ Promise.resolve({
97
+ ok: false,
98
+ error: adapterError(
99
+ input,
100
+ id,
101
+ `${id} is unavailable in this build: ${reason}`
102
+ ),
103
+ }),
104
+ };
105
+ }
106
+ }
107
+
74
108
  /**
75
109
  * Create the default registry with all MVP converters.
76
110
  * Priority order per PRD §8.6:
77
111
  * 1. native/markdown - handles .md
78
112
  * 2. native/plaintext - handles .txt
79
- * 3. adapter/markitdown-ts - handles .pdf, .docx, .xlsx
80
- * 4. adapter/officeparser - handles .pptx
113
+ * 3. adapter/xlsx - handles .xlsx (linear SheetJS -> Markdown tables)
114
+ * 4. adapter/markitdown-ts - handles .pdf, .docx
115
+ * 5. adapter/officeparser - handles .pptx
81
116
  */
82
117
  export async function createDefaultRegistry(): Promise<ConverterRegistry> {
83
118
  const registry = new ConverterRegistry();
@@ -85,9 +120,29 @@ export async function createDefaultRegistry(): Promise<ConverterRegistry> {
85
120
  // Import converters dynamically to avoid circular deps
86
121
  const { markdownConverter } = await import("./native/markdown");
87
122
  const { plaintextConverter } = await import("./native/plaintext");
88
- const { markitdownAdapter } = await import("./adapters/markitdownTs/adapter");
89
- const { officeparserAdapter } =
90
- await import("./adapters/officeparser/adapter");
123
+ const { xlsxAdapter } = await import("./adapters/xlsx/adapter");
124
+ // The PDF/Office adapters load pdf.js, which cannot initialize where its
125
+ // native canvas binding is missing (a standalone compiled executable). A
126
+ // failed adapter only fails its own file types.
127
+ const markitdownAdapter = await loadAdapter(
128
+ async () =>
129
+ (await import("./adapters/markitdownTs/adapter")).markitdownAdapter,
130
+ "adapter/markitdown-ts",
131
+ [".pdf", ".docx"],
132
+ [
133
+ "application/pdf",
134
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
135
+ ]
136
+ );
137
+ const officeparserAdapter = await loadAdapter(
138
+ async () =>
139
+ (await import("./adapters/officeparser/adapter")).officeparserAdapter,
140
+ "adapter/officeparser",
141
+ [".pptx"],
142
+ [
143
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
144
+ ]
145
+ );
91
146
  const { jsonlAdapter } = await import("./adapters/jsonl/adapter");
92
147
  const { transcriptAdapter } = await import("./adapters/transcript/adapter");
93
148
  const { emailRecordAdapter } = await import("./adapters/email/adapter");
@@ -98,6 +153,7 @@ export async function createDefaultRegistry(): Promise<ConverterRegistry> {
98
153
  // Register in priority order
99
154
  registry.register(markdownConverter);
100
155
  registry.register(plaintextConverter);
156
+ registry.register(xlsxAdapter);
101
157
  registry.register(markitdownAdapter);
102
158
  registry.register(officeparserAdapter);
103
159
  registry.registerRecordAdapter(jsonlAdapter);
@@ -18,5 +18,7 @@ export const NATIVE_VERSIONS = {
18
18
  */
19
19
  export const ADAPTER_VERSIONS = {
20
20
  "markitdown-ts": "0.0.10+xlsx.0.20.3",
21
+ /** Direct SheetJS -> Markdown tables (fn-198); output matches markitdown. */
22
+ xlsx: "sheetjs-0.20.3+md.1",
21
23
  officeparser: "7.8.0+pdfjs.6.3.289",
22
24
  } as const;
@@ -144,7 +144,7 @@ export const evaluateFreshnessAudit = (
144
144
  document,
145
145
  kind: "stale-indexed-revision",
146
146
  message: document.lastErrorCode
147
- ? "The latest indexing attempt recorded an error"
147
+ ? `The latest indexing attempt recorded an error (${document.lastErrorCode})`
148
148
  : "The document has no indexed-at revision evidence",
149
149
  detail: {
150
150
  indexedAt: document.indexedAt,
@@ -19,7 +19,10 @@ import { readdir, realpath, stat } from "node:fs/promises";
19
19
  // node:path join: platform path algebra; no Bun equivalent.
20
20
  import { join } from "node:path";
21
21
 
22
- import type { AuditLinkSnapshot } from "../store/sqlite/graph-link-resolver";
22
+ import type {
23
+ AuditLinkSnapshot,
24
+ AuditLinkSnapshotLink,
25
+ } from "../store/sqlite/graph-link-resolver";
23
26
 
24
27
  import {
25
28
  createWorkspaceFileMatcher,
@@ -30,6 +33,27 @@ import { pathContains, placeDocument } from "./link-workspace";
30
33
  /** Path identity for exact-path matching: NFC, case-insensitive. */
31
34
  const pathKey = (path: string): string => path.normalize("NFC").toLowerCase();
32
35
 
36
+ /**
37
+ * A Markdown link names the file's exact path; as with wiki links, `.md` is
38
+ * optional, so `[x](Note)` matches an unindexed `Note.md`. An indexed
39
+ * `Note.md` is not outside the index, so that link keeps its status. Indexed
40
+ * paths are compared under the same key as listed files (NFC, Unicode
41
+ * lowercase), never SQLite's ASCII-only NOCASE.
42
+ */
43
+ const markdownTargetExists = (
44
+ link: AuditLinkSnapshotLink,
45
+ targetPath: string,
46
+ paths: ReadonlySet<string> | undefined,
47
+ indexedKeys: (collection: string) => ReadonlySet<string>
48
+ ): boolean => {
49
+ const key = pathKey(targetPath);
50
+ if (paths?.has(key) === true) return true;
51
+ if (key.endsWith(".md") || paths?.has(`${key}.md`) !== true) return false;
52
+ return !indexedKeys(link.targetCollection).has(
53
+ pathKey(`${link.targetRefNorm}.md`)
54
+ );
55
+ };
56
+
33
57
  /** Upper bound of files listed per workspace; beyond it the listing is partial. */
34
58
  export const WORKSPACE_FILE_LISTING_MAX_FILES = 200_000;
35
59
 
@@ -152,7 +176,7 @@ export const listWorkspaceFiles = async (
152
176
  * Mark unresolved plain wiki links and relative Markdown links of audited
153
177
  * documents inside a link workspace whose target exists as a workspace file.
154
178
  * A wiki link matches under workspace resolution rules; a Markdown link must
155
- * name the file's exact path (NFC, case-insensitive). Each involved workspace
179
+ * name the file's exact path (NFC, case-insensitive), `.md` optional. Each involved workspace
156
180
  * is listed once per call. A failed or partial listing never marks a link
157
181
  * that is not in it; the snapshot then carries one diagnostic.
158
182
  */
@@ -220,12 +244,33 @@ export const markOutsideIndexLinks = async (
220
244
  paths: new Set(listing.files.map(pathKey)),
221
245
  });
222
246
  }
247
+ const indexedByCollection = new Map<string, Set<string>>();
248
+ const indexedKeys = (collection: string): ReadonlySet<string> => {
249
+ let keys = indexedByCollection.get(collection);
250
+ if (!keys) {
251
+ keys = new Set(
252
+ db
253
+ .query<{ relPath: string }, [string]>(
254
+ "SELECT rel_path AS relPath FROM documents WHERE active = 1 AND collection = ?"
255
+ )
256
+ .all(collection)
257
+ .map(({ relPath }) => pathKey(relPath))
258
+ );
259
+ indexedByCollection.set(collection, keys);
260
+ }
261
+ return keys;
262
+ };
223
263
  const links = [...snapshot.links];
224
264
  for (const entry of placed) {
225
265
  const listing = listings.get(entry.key);
226
266
  const exists =
227
267
  "targetPath" in entry
228
- ? listing?.paths.has(pathKey(entry.targetPath)) === true
268
+ ? markdownTargetExists(
269
+ entry.link,
270
+ entry.targetPath,
271
+ listing?.paths,
272
+ indexedKeys
273
+ )
229
274
  : listing?.matches(entry.link.targetRefNorm, entry.sourcePath) === true;
230
275
  if (exists) {
231
276
  links[entry.index] = { ...entry.link, outsideIndex: true };
@@ -104,15 +104,27 @@ const extractTypedEdges = (markdown: string): string[] => {
104
104
  return [...new Set(values)].sort();
105
105
  };
106
106
 
107
+ export interface DocumentStructureOptions {
108
+ /** Only Markdown sources carry links (matches link extraction at sync). */
109
+ markdownSource: boolean;
110
+ excludedRanges?: ExcludedRange[];
111
+ }
112
+
107
113
  export const extractDocumentStructure = (
108
114
  markdown: string,
109
115
  relPath: string,
110
116
  dateFields: Readonly<Record<string, string>> | null | undefined,
111
- excludedRanges: ExcludedRange[] = getExcludedRanges(markdown)
117
+ { markdownSource, excludedRanges }: DocumentStructureOptions
112
118
  ): DocumentStructureSnapshot => {
113
- const links = parseLinks(markdown, buildLineOffsets(markdown), excludedRanges)
114
- .map((link) => linkSummary(link, relPath))
115
- .filter((value): value is string => value !== null);
119
+ const links = markdownSource
120
+ ? parseLinks(
121
+ markdown,
122
+ buildLineOffsets(markdown),
123
+ excludedRanges ?? getExcludedRanges(markdown)
124
+ )
125
+ .map((link) => linkSummary(link, relPath))
126
+ .filter((value): value is string => value !== null)
127
+ : [];
116
128
 
117
129
  return {
118
130
  headings: withOccurrences(
@@ -42,6 +42,19 @@ const COMMONMARK_ESCAPABLE = new Set([
42
42
  " ",
43
43
  ]);
44
44
 
45
+ /**
46
+ * True when the character at `index` is backslash-escaped: preceded by an odd
47
+ * number of backslashes. An escaped `[` is literal text in CommonMark and
48
+ * cannot open a link.
49
+ */
50
+ export function isBackslashEscaped(text: string, index: number): boolean {
51
+ let backslashes = 0;
52
+ for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
53
+ backslashes += 1;
54
+ }
55
+ return backslashes % 2 === 1;
56
+ }
57
+
45
58
  export interface LinkEncodingStyle {
46
59
  spaces: "percent" | "backslash" | "raw";
47
60
  parens: "percent" | "backslash" | "raw";
@@ -220,10 +233,10 @@ export function parseParenthesizedDestination(
220
233
  i += 1;
221
234
  continue;
222
235
  }
223
- if (
224
- depth === 0 &&
225
- (ch === " " || ch === "\t" || ch === "\n" || ch === "\r")
226
- ) {
236
+ if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
237
+ // CommonMark: a bare destination has no unescaped whitespace, inside
238
+ // nested parentheses too (`(foo(bar baz).md)` is not a link).
239
+ if (depth > 0) return null;
227
240
  destEnd = i;
228
241
  break;
229
242
  }
@@ -262,6 +275,10 @@ export function parseParenthesizedDestination(
262
275
  close = j;
263
276
  }
264
277
 
278
+ // The destination and optional title must end at a real `)`:
279
+ // `[x](<note.md>` or `[x](<note.md>.` is not a link.
280
+ if (markdown[close] !== ")") return null;
281
+
265
282
  const destinationRaw = markdown.slice(destStart, destEnd);
266
283
  if (!destinationRaw) return null;
267
284
  return {
@@ -11,6 +11,7 @@ import { offsetToPosition } from "../ingestion/position";
11
11
  import { rangeIntersectsExcluded } from "../ingestion/strip";
12
12
  import {
13
13
  detectEncodingStyle,
14
+ isBackslashEscaped,
14
15
  parseParenthesizedDestination,
15
16
  splitDestinationPath,
16
17
  stripAngleBracketDestination,
@@ -298,12 +299,31 @@ export function inventoryInlineMarkdown(
298
299
  while (searchFrom < markdown.length) {
299
300
  const labelOpen = markdown.indexOf("[", searchFrom);
300
301
  if (labelOpen < 0) break;
301
- if (labelOpen > 0 && markdown[labelOpen - 1] === "!") {
302
+ if (
303
+ (labelOpen > 0 && markdown[labelOpen - 1] === "!") ||
304
+ isBackslashEscaped(markdown, labelOpen)
305
+ ) {
306
+ searchFrom = labelOpen + 1;
307
+ continue;
308
+ }
309
+ // Match link extraction: escaped brackets are literal label text, and
310
+ // nested brackets must balance (`[see [1]](note.md)` is one link).
311
+ let labelClose = -1;
312
+ let depth = 0;
313
+ for (let i = labelOpen + 1; i < markdown.length; i += 1) {
314
+ const ch = markdown[i];
315
+ if ((ch !== "]" && ch !== "[") || isBackslashEscaped(markdown, i))
316
+ continue;
317
+ if (ch === "[") depth += 1;
318
+ else if (depth === 0) {
319
+ labelClose = i;
320
+ break;
321
+ } else depth -= 1;
322
+ }
323
+ if (labelClose < 0) {
302
324
  searchFrom = labelOpen + 1;
303
325
  continue;
304
326
  }
305
- const labelClose = markdown.indexOf("]", labelOpen + 1);
306
- if (labelClose < 0) break;
307
327
  if (markdown[labelClose + 1] !== "(") {
308
328
  searchFrom = labelOpen + 1;
309
329
  continue;
package/src/core/links.ts CHANGED
@@ -14,6 +14,12 @@ import type { ExcludedRange } from "../ingestion/strip";
14
14
 
15
15
  import { buildLineOffsets, offsetToPosition } from "../ingestion/position";
16
16
  import { rangeIntersectsExcluded } from "../ingestion/strip";
17
+ import {
18
+ isBackslashEscaped,
19
+ parseParenthesizedDestination,
20
+ stripAngleBracketDestination,
21
+ unescapeCommonMarkDestination,
22
+ } from "./link-destination-parse";
17
23
 
18
24
  // ─────────────────────────────────────────────────────────────────────────────
19
25
  // Types
@@ -85,19 +91,24 @@ const LOGSEQ_EMBED_REGEX =
85
91
  /\{\{\s*embed\s+(\[\[[^\]]+\]\]|\(\([^)]+\)\))\s*\}\}/gi;
86
92
 
87
93
  /**
88
- * Markdown inline link: [text](url)
89
- * Captures: 1=text, 2=url (path and optional anchor)
94
+ * Markdown inline link opener: `[text](`, up to and including the `(`.
95
+ * Captures: 1=text. The destination after `(` is read by
96
+ * parseParenthesizedDestination, as CommonMark reads it.
90
97
  * Negative lookbehind to avoid image links ![]()
91
98
  * Link text may contain balanced square brackets one level deep
92
- * (`[see [1]](note.md)`), as CommonMark allows.
99
+ * (`[see [1]](note.md)`), as CommonMark allows; a backslash-escaped bracket
100
+ * (`\]`, `\[`) in the text is literal and never opens or closes it.
93
101
  *
94
102
  * SCOPE LIMITATIONS:
95
103
  * - Only matches simple inline links [text](url)
96
104
  * - Does NOT match reference-style links [text][ref] or [text]
97
105
  * - Does NOT match autolinks <url> or bare URLs
98
- * - Parens in URLs not supported (use %28 %29 encoding)
99
106
  */
100
- const MARKDOWN_LINK_REGEX = /(?<!!)\[((?:[^[\]]|\[[^[\]]*\])*)\]\(([^)]+)\)/g;
107
+ const MARKDOWN_LINK_REGEX =
108
+ /(?<!!)\[((?:\\[\s\S]|[^[\]\\]|\[(?:\\[\s\S]|[^[\]\\])*\])*)\]\(/g;
109
+
110
+ /** Logseq alias destination at the `(`: `([[Target]])`. */
111
+ const LOGSEQ_ALIAS_DESTINATION_REGEX = /\((\[\[[^)]*\]\])\)/y;
101
112
 
102
113
  /** Square brackets in a destination mean the text was split, not a path. */
103
114
  const BRACKET_IN_DESTINATION_REGEX = /[[\]]/;
@@ -287,6 +298,40 @@ export function parseTargetParts(target: string): TargetParts {
287
298
  // Link Parsing
288
299
  // ─────────────────────────────────────────────────────────────────────────────
289
300
 
301
+ /**
302
+ * Read an inline link destination at the `(` after `[text]`, the way
303
+ * CommonMark reads it: optional title, balanced parentheses, and no
304
+ * unescaped whitespace unless wrapped in `<...>`. Returns the unescaped
305
+ * destination (angle brackets removed) and the offset after the closing `)`,
306
+ * or null when the text is not a link. A Logseq alias `([[Target]])` is
307
+ * returned verbatim.
308
+ */
309
+ function readInlineDestination(
310
+ markdown: string,
311
+ parenOffset: number
312
+ ): { url: string; endOffset: number; logseqAlias: boolean } | null {
313
+ LOGSEQ_ALIAS_DESTINATION_REGEX.lastIndex = parenOffset;
314
+ const logseq = LOGSEQ_ALIAS_DESTINATION_REGEX.exec(markdown);
315
+ if (logseq) {
316
+ return {
317
+ url: logseq[1] ?? "",
318
+ endOffset: parenOffset + logseq[0].length,
319
+ logseqAlias: true,
320
+ };
321
+ }
322
+ const parsed = parseParenthesizedDestination(markdown, parenOffset);
323
+ if (!parsed || parsed.destinationRaw.includes("\n")) {
324
+ return null;
325
+ }
326
+ return {
327
+ url: unescapeCommonMarkDestination(
328
+ stripAngleBracketDestination(parsed.destinationRaw).path
329
+ ),
330
+ endOffset: parsed.closeParenOffset + 1,
331
+ logseqAlias: false,
332
+ };
333
+ }
334
+
290
335
  /**
291
336
  * Parse all links from markdown content.
292
337
  * Skips links inside excluded ranges (code blocks, frontmatter, etc.).
@@ -409,32 +454,45 @@ export function parseLinks(
409
454
 
410
455
  while ((match = MARKDOWN_LINK_REGEX.exec(markdown)) !== null) {
411
456
  const startOffset = match.index;
412
- const endOffset = startOffset + match[0].length;
457
+ const parenOffset = startOffset + match[0].length - 1;
458
+
459
+ // `\[` is literal text in CommonMark: it cannot open a link. A link
460
+ // may still start at a later `[` inside the text.
461
+ if (isBackslashEscaped(markdown, startOffset)) {
462
+ MARKDOWN_LINK_REGEX.lastIndex = startOffset + 1;
463
+ continue;
464
+ }
465
+
466
+ const destination = readInlineDestination(markdown, parenOffset);
467
+ // Not a link: a destination with an unescaped space outside `<...>`
468
+ // (`[x](my note.md)`) or no closing parenthesis. It is plain text; a link
469
+ // may still start at a later `[` inside the text.
470
+ if (!destination) {
471
+ MARKDOWN_LINK_REGEX.lastIndex = startOffset + 1;
472
+ continue;
473
+ }
474
+ const endOffset = destination.endOffset;
475
+ MARKDOWN_LINK_REGEX.lastIndex = endOffset;
413
476
 
414
477
  // Skip if inside excluded range
415
478
  if (rangeIntersectsExcluded(startOffset, endOffset, excludedRanges)) {
416
479
  continue;
417
480
  }
418
481
 
482
+ const raw = markdown.slice(startOffset, endOffset);
419
483
  const linkText = match[1] ?? "";
420
- const url = match[2];
484
+ const { url } = destination;
421
485
  if (!url) continue;
422
486
 
423
487
  // Logseq alias syntax: [Display]([[Target]])
424
- if (url.startsWith("[[") && url.endsWith("]]")) {
488
+ if (destination.logseqAlias) {
425
489
  const innerTarget = url.slice(2, -2).trim();
426
490
  if (innerTarget.length > 0) {
427
491
  const displayText =
428
492
  linkText && linkText !== innerTarget
429
493
  ? truncateText(linkText, MAX_DISPLAY_TEXT_GRAPHEMES)
430
494
  : undefined;
431
- pushWikiLink(
432
- match[0],
433
- innerTarget,
434
- startOffset,
435
- endOffset,
436
- displayText
437
- );
495
+ pushWikiLink(raw, innerTarget, startOffset, endOffset, displayText);
438
496
  }
439
497
  continue;
440
498
  }
@@ -491,7 +549,7 @@ export function parseLinks(
491
549
 
492
550
  links.push({
493
551
  kind: "markdown",
494
- raw: match[0],
552
+ raw,
495
553
  targetRef: parts.ref,
496
554
  targetAnchor: anchor ?? parts.anchor,
497
555
  targetCollection: parts.collection,
@@ -305,6 +305,24 @@ export const NETWORK_BOUNDARY_INVENTORY = [
305
305
  action: null,
306
306
  enforcement: "local_process_only",
307
307
  },
308
+ {
309
+ // `ps` for the file processor child's resident memory (non-Linux Unix).
310
+ id: "file-processor-rss-probe",
311
+ key: "src/ingestion/file-processor.ts::child_process#1",
312
+ path: "src/ingestion/file-processor.ts",
313
+ primitive: "child_process",
314
+ action: null,
315
+ enforcement: "local_process_only",
316
+ },
317
+ {
318
+ // The same executable re-run as the file processor child (compiled builds).
319
+ id: "file-processor-child",
320
+ key: "src/ingestion/file-processor.ts::child_process#2",
321
+ path: "src/ingestion/file-processor.ts",
322
+ primitive: "child_process",
323
+ action: null,
324
+ enforcement: "local_process_only",
325
+ },
308
326
  {
309
327
  id: "http-mcp-tools",
310
328
  key: "logical::http-mcp-tools",
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { runCli } from "./cli/run";
10
+ import { FILE_PROCESSOR_CHILD_ENV } from "./ingestion/file-child-env";
10
11
  import { resetModelManager } from "./llm/nodeLlamaCpp/lifecycle";
11
12
  import { IMPORT_CHILD_ENV } from "./sessions/import-child-env";
12
13
 
@@ -78,6 +79,15 @@ if (process.env[IMPORT_CHILD_ENV] === "1") {
78
79
  await cleanupAndExit(0);
79
80
  }
80
81
 
82
+ // A compiled executable re-run as the file processor child (see
83
+ // src/ingestion/file-processor.ts) prepares files for its parent until the
84
+ // parent disconnects.
85
+ if (process.env[FILE_PROCESSOR_CHILD_ENV] === "1") {
86
+ const { runFileProcessorChild } = await import("./ingestion/file-child");
87
+ await runFileProcessorChild();
88
+ await cleanupAndExit(0);
89
+ }
90
+
81
91
  // Await module completion so pending piped stdin keeps Windows Bun alive.
82
92
  await runCli(process.argv)
83
93
  .then((code) => cleanupAndExit(interruptExitCode || code))