@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.
- package/README.md +1 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v2.8.2.zip → gno-browser-clipper-v2.8.3.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.3.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +9 -1
- package/spec/cli.md +66 -4
- package/spec/mcp.md +5 -6
- package/src/cli/commands/daemon.ts +7 -2
- package/src/cli/commands/doctor.ts +42 -24
- package/src/cli/commands/index-cmd.ts +10 -2
- package/src/cli/commands/links.ts +5 -22
- package/src/cli/commands/shared.ts +26 -1
- package/src/cli/commands/status.ts +60 -0
- package/src/cli/commands/update.ts +6 -1
- package/src/cli/commands/vec.ts +42 -2
- package/src/cli/program.ts +2 -2
- package/src/config/types.ts +19 -0
- package/src/converters/adapters/markitdownTs/adapter.ts +1 -52
- package/src/converters/adapters/shared/ooxml-protection.ts +58 -0
- package/src/converters/adapters/xlsx/adapter.ts +132 -0
- package/src/converters/adapters/xlsx/sheet-markdown.ts +312 -0
- package/src/converters/registry.ts +62 -6
- package/src/converters/versions.ts +2 -0
- package/src/core/audit-freshness.ts +1 -1
- package/src/core/audit-outside-index.ts +48 -3
- package/src/core/change-diff.ts +16 -4
- package/src/core/link-destination-parse.ts +21 -4
- package/src/core/link-inventory-markdown.ts +23 -3
- package/src/core/links.ts +74 -16
- package/src/core/network-boundary-inventory.ts +18 -0
- package/src/index.ts +10 -0
- package/src/ingestion/document-metadata.ts +256 -0
- package/src/ingestion/file-child-env.ts +9 -0
- package/src/ingestion/file-child.ts +106 -0
- package/src/ingestion/file-processor.ts +546 -0
- package/src/ingestion/frontmatter.ts +6 -2
- package/src/ingestion/index.ts +1 -0
- package/src/ingestion/prepare-convert.ts +32 -0
- package/src/ingestion/prepare-file.ts +272 -0
- package/src/ingestion/prepare-worker.ts +43 -0
- package/src/ingestion/record-container.ts +7 -2
- package/src/ingestion/strip.ts +71 -8
- package/src/ingestion/sync-options.ts +7 -1
- package/src/ingestion/sync.ts +207 -400
- package/src/ingestion/types.ts +18 -0
- package/src/llm/native-worker/embedding-identity.ts +15 -3
- package/src/mcp/http-transport.ts +11 -0
- package/src/mcp/tools/links.ts +10 -38
- package/src/serve/resident-request.ts +19 -0
- package/src/serve/resident-runtime.ts +84 -8
- package/src/serve/routes/links.ts +6 -24
- package/src/serve/server.ts +3 -2
- package/src/sessions/config-refresh.ts +3 -2
- package/src/store/sqlite/graph-similarity.ts +5 -13
- package/src/store/vector/partition-index.ts +100 -0
- package/src/store/vector/stored-vectors.ts +20 -15
- 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/
|
|
80
|
-
* 4. adapter/
|
|
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 {
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
?
|
|
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 {
|
|
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
|
-
?
|
|
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 };
|
package/src/core/change-diff.ts
CHANGED
|
@@ -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:
|
|
117
|
+
{ markdownSource, excludedRanges }: DocumentStructureOptions
|
|
112
118
|
): DocumentStructureSnapshot => {
|
|
113
|
-
const links =
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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 (
|
|
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](
|
|
89
|
-
* Captures: 1=text
|
|
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 =
|
|
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
|
|
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 =
|
|
484
|
+
const { url } = destination;
|
|
421
485
|
if (!url) continue;
|
|
422
486
|
|
|
423
487
|
// Logseq alias syntax: [Display]([[Target]])
|
|
424
|
-
if (
|
|
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
|
|
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))
|