@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document metadata and tag extraction from canonical Markdown.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions shared by sync and the per-file worker (fn-198), kept out
|
|
5
|
+
* of sync.ts so the worker does not load the store.
|
|
6
|
+
*
|
|
7
|
+
* @module src/ingestion/document-metadata
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { NormalizedContentTypeRule } from "../config";
|
|
11
|
+
import type { TypedMetadata } from "../core/typed-metadata";
|
|
12
|
+
import type { ContentTypeSource } from "./types";
|
|
13
|
+
|
|
14
|
+
import { resolveContentTypeRule } from "../config";
|
|
15
|
+
import { normalizeTag, validateTag } from "../core/tags";
|
|
16
|
+
import {
|
|
17
|
+
extractHashtags,
|
|
18
|
+
parseFrontmatter,
|
|
19
|
+
stripFrontmatter,
|
|
20
|
+
} from "./frontmatter";
|
|
21
|
+
import { extractTypedMetadata } from "./typed-metadata";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract tags from markdown content.
|
|
25
|
+
* Combines frontmatter tags and inline hashtags, normalized and validated.
|
|
26
|
+
*/
|
|
27
|
+
export function extractTags(markdown: string): string[] {
|
|
28
|
+
const tags = new Set<string>();
|
|
29
|
+
|
|
30
|
+
// 1. Extract from frontmatter
|
|
31
|
+
const frontmatter = parseFrontmatter(markdown);
|
|
32
|
+
for (const tag of frontmatter.tags) {
|
|
33
|
+
const normalized = normalizeTag(tag);
|
|
34
|
+
if (validateTag(normalized)) {
|
|
35
|
+
tags.add(normalized);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Extract hashtags from body (after stripping frontmatter)
|
|
40
|
+
const body = stripFrontmatter(markdown);
|
|
41
|
+
const hashtags = extractHashtags(body);
|
|
42
|
+
for (const tag of hashtags) {
|
|
43
|
+
const normalized = normalizeTag(tag);
|
|
44
|
+
if (validateTag(normalized)) {
|
|
45
|
+
tags.add(normalized);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [...tags];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DocumentMetadata {
|
|
53
|
+
typedMetadata?: TypedMetadata;
|
|
54
|
+
metadataError?: string;
|
|
55
|
+
contentType?: string;
|
|
56
|
+
contentTypeSource: ContentTypeSource;
|
|
57
|
+
categories?: string[];
|
|
58
|
+
author?: string;
|
|
59
|
+
frontmatterDate?: string;
|
|
60
|
+
dateFields?: Record<string, string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const CODE_EXTENSIONS = new Set([
|
|
64
|
+
".c",
|
|
65
|
+
".cc",
|
|
66
|
+
".cpp",
|
|
67
|
+
".cs",
|
|
68
|
+
".go",
|
|
69
|
+
".java",
|
|
70
|
+
".js",
|
|
71
|
+
".jsx",
|
|
72
|
+
".m",
|
|
73
|
+
".mm",
|
|
74
|
+
".php",
|
|
75
|
+
".py",
|
|
76
|
+
".rb",
|
|
77
|
+
".rs",
|
|
78
|
+
".swift",
|
|
79
|
+
".ts",
|
|
80
|
+
".tsx",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const AUTHOR_KEYS = ["author", "by", "owner", "creator"] as const;
|
|
84
|
+
const DATE_KEYS = [
|
|
85
|
+
"date",
|
|
86
|
+
"published",
|
|
87
|
+
"published_at",
|
|
88
|
+
"created",
|
|
89
|
+
"created_at",
|
|
90
|
+
"updated",
|
|
91
|
+
"updated_at",
|
|
92
|
+
] as const;
|
|
93
|
+
const DATE_FIELD_KEY_REGEX =
|
|
94
|
+
/(^|_)(date|time|created|updated|published|modified|deadline|expires|expiry|start|end)(_|$)/;
|
|
95
|
+
|
|
96
|
+
function normalizeMetadataKey(rawKey: string): string {
|
|
97
|
+
return rawKey
|
|
98
|
+
.trim()
|
|
99
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
102
|
+
.replace(/^_+|_+$/g, "")
|
|
103
|
+
.replace(/_+/g, "_");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeDate(value: unknown): string | undefined {
|
|
107
|
+
if (value instanceof Date) {
|
|
108
|
+
return Number.isNaN(value.getTime()) ? undefined : value.toISOString();
|
|
109
|
+
}
|
|
110
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
const normalizedValue =
|
|
114
|
+
typeof value === "string"
|
|
115
|
+
? value.trim().replace(/^["'](.*)["']$/, "$1")
|
|
116
|
+
: value;
|
|
117
|
+
const parsed = new Date(normalizedValue);
|
|
118
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return parsed.toISOString();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function inferPathContentType(
|
|
125
|
+
relPath: string,
|
|
126
|
+
ext: string
|
|
127
|
+
): {
|
|
128
|
+
contentType: string;
|
|
129
|
+
source: ContentTypeSource;
|
|
130
|
+
} {
|
|
131
|
+
const lowerPath = relPath.toLowerCase();
|
|
132
|
+
if (CODE_EXTENSIONS.has(ext.toLowerCase())) {
|
|
133
|
+
return { contentType: "code", source: "path-ext" };
|
|
134
|
+
}
|
|
135
|
+
if (/(meeting|standup|retro|minutes)/.test(lowerPath)) {
|
|
136
|
+
return { contentType: "meeting", source: "path-ext" };
|
|
137
|
+
}
|
|
138
|
+
if (/(spec|rfc|adr|design)/.test(lowerPath)) {
|
|
139
|
+
return { contentType: "spec", source: "path-ext" };
|
|
140
|
+
}
|
|
141
|
+
if (/(notes|journal|log)/.test(lowerPath)) {
|
|
142
|
+
return { contentType: "notes", source: "path-ext" };
|
|
143
|
+
}
|
|
144
|
+
return { contentType: "prose", source: "fallback" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeFrontmatterScalar(value: string): string {
|
|
148
|
+
const trimmed = value.trim();
|
|
149
|
+
if (trimmed.length < 2) {
|
|
150
|
+
return trimmed;
|
|
151
|
+
}
|
|
152
|
+
const first = trimmed[0];
|
|
153
|
+
const last = trimmed.at(-1);
|
|
154
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
155
|
+
return trimmed.slice(1, -1).trim();
|
|
156
|
+
}
|
|
157
|
+
return trimmed;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseCategories(input: unknown): string[] {
|
|
161
|
+
if (Array.isArray(input)) {
|
|
162
|
+
return input
|
|
163
|
+
.filter((v): v is string => typeof v === "string")
|
|
164
|
+
.map((v) => normalizeFrontmatterScalar(v).toLowerCase())
|
|
165
|
+
.filter((v) => v.length > 0);
|
|
166
|
+
}
|
|
167
|
+
if (typeof input === "string") {
|
|
168
|
+
return input
|
|
169
|
+
.split(",")
|
|
170
|
+
.map((v) => normalizeFrontmatterScalar(v).toLowerCase())
|
|
171
|
+
.filter((v) => v.length > 0);
|
|
172
|
+
}
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function extractDocumentMetadata(
|
|
177
|
+
markdown: string,
|
|
178
|
+
relPath: string,
|
|
179
|
+
ext: string,
|
|
180
|
+
contentTypeRules: NormalizedContentTypeRule[] = []
|
|
181
|
+
): DocumentMetadata {
|
|
182
|
+
const parsed = parseFrontmatter(markdown);
|
|
183
|
+
const metadata = parsed.metadata;
|
|
184
|
+
const rawFrontmatterType =
|
|
185
|
+
typeof metadata.type === "string"
|
|
186
|
+
? normalizeFrontmatterScalar(metadata.type)
|
|
187
|
+
: "";
|
|
188
|
+
const configuredRule = resolveContentTypeRule(
|
|
189
|
+
rawFrontmatterType,
|
|
190
|
+
relPath,
|
|
191
|
+
contentTypeRules
|
|
192
|
+
);
|
|
193
|
+
const inferred = inferPathContentType(relPath, ext);
|
|
194
|
+
const contentType = configuredRule?.rule.id ?? inferred.contentType;
|
|
195
|
+
const contentTypeSource: ContentTypeSource =
|
|
196
|
+
configuredRule?.source === "configured-id"
|
|
197
|
+
? "frontmatter-type"
|
|
198
|
+
: configuredRule?.source === "prefix"
|
|
199
|
+
? "prefix"
|
|
200
|
+
: inferred.source;
|
|
201
|
+
const categories = new Set<string>([contentType]);
|
|
202
|
+
|
|
203
|
+
const fmCategories = parseCategories(
|
|
204
|
+
metadata.category ?? metadata.categories ?? metadata.type
|
|
205
|
+
);
|
|
206
|
+
for (const category of fmCategories) {
|
|
207
|
+
categories.add(category);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let author: string | undefined;
|
|
211
|
+
for (const key of AUTHOR_KEYS) {
|
|
212
|
+
const value = metadata[key];
|
|
213
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
214
|
+
author = value.trim();
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const normalizedMetadata = new Map<string, unknown>();
|
|
220
|
+
for (const [rawKey, value] of Object.entries(metadata)) {
|
|
221
|
+
const key = normalizeMetadataKey(rawKey);
|
|
222
|
+
if (key.length > 0 && !normalizedMetadata.has(key)) {
|
|
223
|
+
normalizedMetadata.set(key, value);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let frontmatterDate: string | undefined;
|
|
228
|
+
for (const key of DATE_KEYS) {
|
|
229
|
+
const normalized = normalizeDate(normalizedMetadata.get(key));
|
|
230
|
+
if (normalized) {
|
|
231
|
+
frontmatterDate = normalized;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const dateFields: Record<string, string> = {};
|
|
237
|
+
for (const [key, value] of normalizedMetadata.entries()) {
|
|
238
|
+
if (!DATE_FIELD_KEY_REGEX.test(key)) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const normalized = normalizeDate(value);
|
|
242
|
+
if (normalized) {
|
|
243
|
+
dateFields[key] = normalized;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
...extractTypedMetadata(markdown),
|
|
249
|
+
contentType,
|
|
250
|
+
contentTypeSource,
|
|
251
|
+
categories: [...categories],
|
|
252
|
+
author,
|
|
253
|
+
frontmatterDate,
|
|
254
|
+
dateFields: Object.keys(dateFields).length > 0 ? dateFields : undefined,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment flag that makes a compiled GNO executable serve as the file
|
|
3
|
+
* processor child instead of the CLI (fn-198). Kept in its own module so the
|
|
4
|
+
* CLI entry can check it without loading the ingestion stack.
|
|
5
|
+
*
|
|
6
|
+
* @module src/ingestion/file-child-env
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const FILE_PROCESSOR_CHILD_ENV = "GNO_INTERNAL_FILE_PROCESSOR_CHILD";
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Child-process backend of the file processor (fn-198).
|
|
3
|
+
*
|
|
4
|
+
* Where a TypeScript worker cannot start (a standalone compiled executable),
|
|
5
|
+
* sync prepares each file in a child process instead: the same executable
|
|
6
|
+
* re-run with `FILE_PROCESSOR_CHILD_ENV` set (see src/index.ts), or this
|
|
7
|
+
* module under a source runtime. It serves `prepareFile` requests over IPC,
|
|
8
|
+
* one at a time, until its parent disconnects; the parent kills it when a
|
|
9
|
+
* file overruns its budget, exactly as it terminates the worker, and on
|
|
10
|
+
* every shutdown path it controls.
|
|
11
|
+
*
|
|
12
|
+
* A child busy in a synchronous step cannot notice its parent dying. On
|
|
13
|
+
* Linux it asks the kernel to SIGKILL it when the parent dies
|
|
14
|
+
* (PR_SET_PDEATHSIG, through Bun's built-in FFI), which holds even if the
|
|
15
|
+
* parent is SIGKILLed. Elsewhere it checks between steps that its parent is
|
|
16
|
+
* still there and exits if not; a parent killed with SIGKILL on macOS or
|
|
17
|
+
* Windows can leave the child running until its current step ends.
|
|
18
|
+
*
|
|
19
|
+
* @module src/ingestion/file-child
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type {
|
|
23
|
+
FileChildMessage,
|
|
24
|
+
FileWorkerRequest,
|
|
25
|
+
ReadyMessage,
|
|
26
|
+
} from "./file-processor";
|
|
27
|
+
|
|
28
|
+
import { defaultChunker } from "./chunker";
|
|
29
|
+
import { FILE_PROCESSOR_CHILD_ENV } from "./file-child-env";
|
|
30
|
+
import { convertForPreparation } from "./prepare-convert";
|
|
31
|
+
import { prepareFile } from "./prepare-file";
|
|
32
|
+
|
|
33
|
+
const send = (message: FileChildMessage | ReadyMessage): void => {
|
|
34
|
+
process.send?.(message);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const PR_SET_PDEATHSIG = 1;
|
|
38
|
+
const SIGKILL = 9;
|
|
39
|
+
const LINUX_LIBC_NAMES = ["libc.so.6", "libc.so"];
|
|
40
|
+
|
|
41
|
+
/** Linux: have the kernel SIGKILL this process when its parent dies. */
|
|
42
|
+
async function killWithParent(): Promise<void> {
|
|
43
|
+
if (process.platform !== "linux") return;
|
|
44
|
+
try {
|
|
45
|
+
// bun:ffi is built into Bun (no native dependency); loaded only here.
|
|
46
|
+
const { dlopen, FFIType } = await import("bun:ffi");
|
|
47
|
+
for (const name of LINUX_LIBC_NAMES) {
|
|
48
|
+
try {
|
|
49
|
+
const libc = dlopen(name, {
|
|
50
|
+
prctl: {
|
|
51
|
+
args: [
|
|
52
|
+
FFIType.i32,
|
|
53
|
+
FFIType.u64,
|
|
54
|
+
FFIType.u64,
|
|
55
|
+
FFIType.u64,
|
|
56
|
+
FFIType.u64,
|
|
57
|
+
],
|
|
58
|
+
returns: FFIType.i32,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
libc.symbols.prctl(PR_SET_PDEATHSIG, BigInt(SIGKILL), 0n, 0n, 0n);
|
|
62
|
+
libc.close();
|
|
63
|
+
return;
|
|
64
|
+
} catch {
|
|
65
|
+
// Try the next libc name; the between-step check still applies.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// No FFI in this runtime: the between-step check still applies.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Serve prepare requests from the parent until it disconnects. */
|
|
74
|
+
export async function runFileProcessorChild(): Promise<void> {
|
|
75
|
+
// Nothing this child starts may re-enter child mode.
|
|
76
|
+
delete process.env[FILE_PROCESSOR_CHILD_ENV];
|
|
77
|
+
const parentPid = process.ppid;
|
|
78
|
+
await killWithParent();
|
|
79
|
+
// The parent may have died before the death signal was armed.
|
|
80
|
+
const parentGone = (): boolean => process.ppid !== parentPid;
|
|
81
|
+
if (parentGone()) process.exit(1);
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
process.on("message", async (job: Pick<FileWorkerRequest, "request">) => {
|
|
84
|
+
const outcome = await prepareFile(job.request, {
|
|
85
|
+
convert: convertForPreparation,
|
|
86
|
+
chunker: defaultChunker,
|
|
87
|
+
onPrevious: (structure) => send({ type: "previous", structure }),
|
|
88
|
+
onPhase: (phase) => send({ type: "phase", phase }),
|
|
89
|
+
// The parent enforces the budget and kills this process; between
|
|
90
|
+
// steps the child only checks that its parent is still alive.
|
|
91
|
+
check: () => {
|
|
92
|
+
if (parentGone()) process.exit(1);
|
|
93
|
+
return null;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
send({ type: "done", outcome });
|
|
97
|
+
});
|
|
98
|
+
process.on("disconnect", () => resolve());
|
|
99
|
+
send({ type: "ready" });
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (import.meta.main) {
|
|
104
|
+
await runFileProcessorChild();
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|