@gmickel/gno 2.8.1 → 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 (61) hide show
  1. package/README.md +1 -1
  2. package/assets/skill/SKILL.md +4 -2
  3. package/browser-extension/artifacts/{gno-browser-clipper-v2.8.1.zip → gno-browser-clipper-v2.8.3.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v2.8.3.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +9 -1
  7. package/spec/cli.md +81 -8
  8. package/spec/mcp.md +5 -6
  9. package/src/cli/commands/daemon.ts +7 -2
  10. package/src/cli/commands/doctor.ts +42 -24
  11. package/src/cli/commands/index-cmd.ts +10 -2
  12. package/src/cli/commands/links.ts +5 -22
  13. package/src/cli/commands/shared.ts +26 -1
  14. package/src/cli/commands/status.ts +60 -0
  15. package/src/cli/commands/update.ts +6 -1
  16. package/src/cli/commands/vec.ts +42 -2
  17. package/src/cli/program.ts +2 -2
  18. package/src/config/types.ts +19 -0
  19. package/src/converters/adapters/markitdownTs/adapter.ts +1 -52
  20. package/src/converters/adapters/shared/ooxml-protection.ts +58 -0
  21. package/src/converters/adapters/xlsx/adapter.ts +132 -0
  22. package/src/converters/adapters/xlsx/sheet-markdown.ts +312 -0
  23. package/src/converters/native/markdown.ts +3 -1
  24. package/src/converters/registry.ts +62 -6
  25. package/src/converters/versions.ts +2 -0
  26. package/src/core/audit-freshness.ts +1 -1
  27. package/src/core/audit-links.ts +2 -1
  28. package/src/core/audit-outside-index.ts +91 -18
  29. package/src/core/change-diff.ts +18 -6
  30. package/src/core/link-destination-parse.ts +21 -4
  31. package/src/core/link-inventory-markdown.ts +23 -3
  32. package/src/core/link-inventory-opaque.ts +3 -1
  33. package/src/core/links.ts +74 -16
  34. package/src/core/network-boundary-inventory.ts +18 -0
  35. package/src/index.ts +10 -0
  36. package/src/ingestion/document-metadata.ts +256 -0
  37. package/src/ingestion/file-child-env.ts +9 -0
  38. package/src/ingestion/file-child.ts +106 -0
  39. package/src/ingestion/file-processor.ts +546 -0
  40. package/src/ingestion/frontmatter.ts +6 -2
  41. package/src/ingestion/index.ts +1 -0
  42. package/src/ingestion/prepare-convert.ts +32 -0
  43. package/src/ingestion/prepare-file.ts +272 -0
  44. package/src/ingestion/prepare-worker.ts +43 -0
  45. package/src/ingestion/record-container.ts +7 -2
  46. package/src/ingestion/strip.ts +149 -133
  47. package/src/ingestion/sync-options.ts +7 -1
  48. package/src/ingestion/sync.ts +208 -385
  49. package/src/ingestion/types.ts +18 -0
  50. package/src/llm/native-worker/embedding-identity.ts +15 -3
  51. package/src/mcp/http-transport.ts +11 -0
  52. package/src/mcp/tools/links.ts +10 -38
  53. package/src/serve/resident-request.ts +19 -0
  54. package/src/serve/resident-runtime.ts +84 -8
  55. package/src/serve/routes/links.ts +6 -24
  56. package/src/serve/server.ts +3 -2
  57. package/src/sessions/config-refresh.ts +3 -2
  58. package/src/store/sqlite/graph-similarity.ts +5 -13
  59. package/src/store/vector/partition-index.ts +100 -0
  60. package/src/store/vector/stored-vectors.ts +20 -15
  61. package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +0 -1
@@ -13,10 +13,16 @@ import {
13
13
  createVectorIndexPort,
14
14
  createVectorStatsPort,
15
15
  } from "../../store/vector";
16
+ import {
17
+ rebuildPartitionIndex,
18
+ syncPartitionIndex,
19
+ } from "../../store/vector/partition-index";
16
20
  import {
17
21
  dropVectorPartition,
22
+ storedVectorPartition,
18
23
  type VectorPartitionStatus,
19
24
  } from "../../store/vector/status";
25
+ import { loadSqliteVec } from "../../store/vector/variants";
20
26
 
21
27
  // ─────────────────────────────────────────────────────────────────────────────
22
28
  // Types
@@ -76,7 +82,9 @@ function inferDimensions(
76
82
  // ─────────────────────────────────────────────────────────────────────────────
77
83
 
78
84
  /**
79
- * Sync vec0 index with content_vectors (add missing, remove orphans).
85
+ * Sync the vec0 index with stored vectors (add missing, remove orphans): the
86
+ * active vector partition's variants, or legacy `content_vectors` on an
87
+ * index that never activated a partition.
80
88
  */
81
89
  export async function vecSync(
82
90
  options: VecOptions = {}
@@ -112,6 +120,21 @@ export async function vecSync(
112
120
  try {
113
121
  const db = store.getRawDb();
114
122
 
123
+ const partition = storedVectorPartition(db, modelUri);
124
+ if (partition) {
125
+ if (!(await loadSqliteVec(db))) {
126
+ return {
127
+ success: false,
128
+ error: "sqlite-vec not available. Cannot sync index.",
129
+ };
130
+ }
131
+ return {
132
+ success: true,
133
+ ...syncPartitionIndex(db, partition),
134
+ model: modelUri,
135
+ };
136
+ }
137
+
115
138
  // Infer dimensions from stored vectors for this model
116
139
  const dimensions = inferDimensions(db, modelUri);
117
140
  if (dimensions === null) {
@@ -154,7 +177,9 @@ export async function vecSync(
154
177
  }
155
178
 
156
179
  /**
157
- * Rebuild vec0 index from content_vectors (drop + recreate + repopulate).
180
+ * Rebuild the vec0 index (drop + recreate + repopulate) from the active
181
+ * vector partition's variants, or from legacy `content_vectors` on an index
182
+ * that never activated a partition.
158
183
  */
159
184
  export async function vecRebuild(
160
185
  options: VecOptions = {}
@@ -190,6 +215,21 @@ export async function vecRebuild(
190
215
  try {
191
216
  const db = store.getRawDb();
192
217
 
218
+ const partition = storedVectorPartition(db, modelUri);
219
+ if (partition) {
220
+ if (!(await loadSqliteVec(db))) {
221
+ return {
222
+ success: false,
223
+ error: "sqlite-vec not available. Cannot rebuild index.",
224
+ };
225
+ }
226
+ return {
227
+ success: true,
228
+ count: rebuildPartitionIndex(db, partition),
229
+ model: modelUri,
230
+ };
231
+ }
232
+
193
233
  // Infer dimensions from stored vectors for this model
194
234
  const dimensions = inferDimensions(db, modelUri);
195
235
  if (dimensions === null) {
@@ -3655,7 +3655,7 @@ function wireVecCommands(program: Command): void {
3655
3655
  addWriteLeaseFlags(
3656
3656
  vecCmd
3657
3657
  .command("sync")
3658
- .description("Sync vec0 index with content_vectors")
3658
+ .description("Sync the vec0 index with the active vector partition")
3659
3659
  .option("--json", "JSON output")
3660
3660
  ).action(async (cmdOpts: Record<string, unknown>) => {
3661
3661
  const format = getFormat(cmdOpts);
@@ -3728,7 +3728,7 @@ function wireVecCommands(program: Command): void {
3728
3728
  addWriteLeaseFlags(
3729
3729
  vecCmd
3730
3730
  .command("rebuild")
3731
- .description("Rebuild vec0 index from content_vectors")
3731
+ .description("Rebuild the vec0 index from the active vector partition")
3732
3732
  .option("--json", "JSON output")
3733
3733
  ).action(async (cmdOpts: Record<string, unknown>) => {
3734
3734
  const format = getFormat(cmdOpts);
@@ -429,6 +429,22 @@ export const DEFAULT_MODEL_PRESETS: ModelPreset[] = [
429
429
  },
430
430
  ];
431
431
 
432
+ /**
433
+ * Per-file conversion budget. A file that overruns either limit is recorded
434
+ * with TIMEOUT or MEMORY_LIMIT and retried by the next update.
435
+ */
436
+ export const ConversionConfigSchema = z
437
+ .object({
438
+ /** Wall-clock budget for converting one file, in ms (default 60000). */
439
+ timeoutMs: z.number().int().min(1000).max(86_400_000).optional(),
440
+ /**
441
+ * Resident-memory ceiling while a file converts, in MB (default: half
442
+ * of physical memory, at least 2048).
443
+ */
444
+ maxMemoryMb: z.number().int().min(256).max(1_048_576).optional(),
445
+ })
446
+ .strict();
447
+
432
448
  export const ModelConfigSchema = z.object({
433
449
  /** Active preset ID */
434
450
  activePreset: z.string().default("slim-tuned"),
@@ -608,6 +624,9 @@ export const ConfigSchema = z.object({
608
624
  /** Optional index-wide chunk size/overlap; omitted preserves legacy defaults. */
609
625
  chunking: ChunkingConfigSchema.optional(),
610
626
 
627
+ /** Per-file conversion budget; omitted fields use the runtime defaults. */
628
+ conversion: ConversionConfigSchema.optional(),
629
+
611
630
  /** Model configuration */
612
631
  models: ModelConfigSchema.optional(),
613
632
 
@@ -19,6 +19,7 @@ import {
19
19
  tooLargeError,
20
20
  } from "../../errors";
21
21
  import { ADAPTER_VERSIONS } from "../../versions";
22
+ import { hasPrefix, isPasswordProtectedXlsx } from "../shared/ooxml-protection";
22
23
 
23
24
  const CONVERTER_ID = "adapter/markitdown-ts" as const;
24
25
  const CONVERTER_VERSION = ADAPTER_VERSIONS["markitdown-ts"];
@@ -34,11 +35,6 @@ const SUPPORTED_MIMES = [
34
35
  ];
35
36
 
36
37
  const PDF_SIGNATURE = new Uint8Array([0x25, 0x50, 0x44, 0x46, 0x2d]);
37
- const CFB_SIGNATURE = new Uint8Array([
38
- 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
39
- ]);
40
- const ENCRYPTION_INFO = utf16le("EncryptionInfo");
41
- const ENCRYPTED_PACKAGE = utf16le("EncryptedPackage");
42
38
  const PDF_TRAILER_SCAN_BYTES = 2048;
43
39
  const MAX_MESSAGE_LENGTH = 200;
44
40
  const PASSWORD_ERROR_REGEX = /password(?:-protected)?|no password given/i;
@@ -51,45 +47,6 @@ function toBuffer(bytes: Uint8Array): Buffer {
51
47
  return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
52
48
  }
53
49
 
54
- function utf16le(value: string): Uint8Array {
55
- return new Uint8Array(Buffer.from(value, "utf16le"));
56
- }
57
-
58
- function hasPrefix(bytes: Uint8Array, prefix: Uint8Array): boolean {
59
- if (bytes.length < prefix.length) {
60
- return false;
61
- }
62
-
63
- for (let index = 0; index < prefix.length; index += 1) {
64
- if (bytes[index] !== prefix[index]) {
65
- return false;
66
- }
67
- }
68
-
69
- return true;
70
- }
71
-
72
- function includesBytes(bytes: Uint8Array, needle: Uint8Array): boolean {
73
- if (needle.length === 0 || bytes.length < needle.length) {
74
- return false;
75
- }
76
-
77
- outer: for (
78
- let index = 0;
79
- index <= bytes.length - needle.length;
80
- index += 1
81
- ) {
82
- for (let offset = 0; offset < needle.length; offset += 1) {
83
- if (bytes[index + offset] !== needle[offset]) {
84
- continue outer;
85
- }
86
- }
87
- return true;
88
- }
89
-
90
- return false;
91
- }
92
-
93
50
  function isPasswordProtectedPdf(bytes: Uint8Array): boolean {
94
51
  if (!hasPrefix(bytes, PDF_SIGNATURE)) {
95
52
  return false;
@@ -113,14 +70,6 @@ function hasCompletePdfTrailer(bytes: Uint8Array): boolean {
113
70
  return startXrefIndex >= 0 && eofIndex > startXrefIndex;
114
71
  }
115
72
 
116
- function isPasswordProtectedXlsx(bytes: Uint8Array): boolean {
117
- return (
118
- hasPrefix(bytes, CFB_SIGNATURE) &&
119
- includesBytes(bytes, ENCRYPTION_INFO) &&
120
- includesBytes(bytes, ENCRYPTED_PACKAGE)
121
- );
122
- }
123
-
124
73
  function isPasswordProtected(input: ConvertInput): boolean {
125
74
  if (input.ext === ".pdf") {
126
75
  return isPasswordProtectedPdf(input.bytes);
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Byte checks shared by document adapters: signature prefixes and the
3
+ * encrypted-OOXML (CFB container) test. Dependency-free so an adapter can
4
+ * use them without loading another adapter's parser stack.
5
+ *
6
+ * @module src/converters/adapters/shared/ooxml-protection
7
+ */
8
+
9
+ const CFB_SIGNATURE = new Uint8Array([
10
+ 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
11
+ ]);
12
+
13
+ export function utf16le(value: string): Uint8Array {
14
+ return new Uint8Array(Buffer.from(value, "utf16le"));
15
+ }
16
+
17
+ const ENCRYPTION_INFO = utf16le("EncryptionInfo");
18
+ const ENCRYPTED_PACKAGE = utf16le("EncryptedPackage");
19
+
20
+ export function hasPrefix(bytes: Uint8Array, prefix: Uint8Array): boolean {
21
+ if (bytes.length < prefix.length) {
22
+ return false;
23
+ }
24
+ for (let index = 0; index < prefix.length; index += 1) {
25
+ if (bytes[index] !== prefix[index]) {
26
+ return false;
27
+ }
28
+ }
29
+ return true;
30
+ }
31
+
32
+ export function includesBytes(bytes: Uint8Array, needle: Uint8Array): boolean {
33
+ if (needle.length === 0 || bytes.length < needle.length) {
34
+ return false;
35
+ }
36
+ outer: for (
37
+ let index = 0;
38
+ index <= bytes.length - needle.length;
39
+ index += 1
40
+ ) {
41
+ for (let offset = 0; offset < needle.length; offset += 1) {
42
+ if (bytes[index + offset] !== needle[offset]) {
43
+ continue outer;
44
+ }
45
+ }
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+
51
+ /** An encrypted (password-protected) OOXML workbook: a CFB container. */
52
+ export function isPasswordProtectedXlsx(bytes: Uint8Array): boolean {
53
+ return (
54
+ hasPrefix(bytes, CFB_SIGNATURE) &&
55
+ includesBytes(bytes, ENCRYPTION_INFO) &&
56
+ includesBytes(bytes, ENCRYPTED_PACKAGE)
57
+ );
58
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Excel (.xlsx) converter: SheetJS workbook -> Markdown tables, linear in
3
+ * cells (fn-198).
4
+ *
5
+ * Produces the Markdown markitdown-ts produced for workbooks (one `## sheet`
6
+ * section and GFM table per sheet with a range) but renders each table
7
+ * directly from the sheet data instead of through HTML, jsdom and turndown,
8
+ * whose cost grew with columns x rows^2 and about 20 KB per cell. Pivot
9
+ * cache parts are not user content and SheetJS does not read them, as
10
+ * before.
11
+ *
12
+ * @module src/converters/adapters/xlsx/adapter
13
+ */
14
+
15
+ import * as XLSX from "xlsx";
16
+
17
+ import type { Converter, ConvertInput, ConvertResult } from "../../types";
18
+
19
+ import { corruptError, permissionError, tooLargeError } from "../../errors";
20
+ import { ADAPTER_VERSIONS } from "../../versions";
21
+ import { isPasswordProtectedXlsx } from "../shared/ooxml-protection";
22
+ import { sheetToMarkdown } from "./sheet-markdown";
23
+
24
+ const CONVERTER_ID = "adapter/xlsx" as const;
25
+ const CONVERTER_VERSION = ADAPTER_VERSIONS.xlsx;
26
+ const XLSX_MIME =
27
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
28
+ const PASSWORD_ERROR_REGEX = /password(?:-protected)?|no password given/i;
29
+ /** Output under this length from a larger file is flagged as lossy. */
30
+ const SHORT_OUTPUT_CHARS = 10;
31
+ const SHORT_OUTPUT_MIN_BYTES = 1000;
32
+
33
+ /**
34
+ * Markdown for a parsed workbook: one section per sheet with a range,
35
+ * normalized as markitdown-ts normalizes every result (line endings, outer
36
+ * whitespace, at most one blank line).
37
+ */
38
+ export function workbookToMarkdown(workbook: XLSX.WorkBook): string {
39
+ let markdown = "";
40
+ for (const name of workbook.SheetNames) {
41
+ const sheet = workbook.Sheets[name];
42
+ if (sheet?.["!ref"]) {
43
+ markdown += `## ${name}\n${sheetToMarkdown(sheet)}\n\n`;
44
+ }
45
+ }
46
+ return markdown
47
+ .replace(/\r\n|\r|\n/g, "\n")
48
+ .trim()
49
+ .replace(/\n{3,}/g, "\n\n");
50
+ }
51
+
52
+ export const xlsxAdapter: Converter = {
53
+ id: CONVERTER_ID,
54
+ version: CONVERTER_VERSION,
55
+
56
+ canHandle(mime: string, ext: string): boolean {
57
+ return ext === ".xlsx" || mime === XLSX_MIME;
58
+ },
59
+
60
+ convert(input: ConvertInput): Promise<ConvertResult> {
61
+ return Promise.resolve(convertWorkbook(input));
62
+ },
63
+ };
64
+
65
+ function convertWorkbook(input: ConvertInput): ConvertResult {
66
+ if (input.bytes.length > input.limits.maxBytes) {
67
+ return { ok: false, error: tooLargeError(input, CONVERTER_ID) };
68
+ }
69
+ if (isPasswordProtectedXlsx(input.bytes)) {
70
+ return {
71
+ ok: false,
72
+ error: permissionError(
73
+ input,
74
+ CONVERTER_ID,
75
+ "File is password-protected",
76
+ undefined,
77
+ { protection: "xlsx" }
78
+ ),
79
+ };
80
+ }
81
+
82
+ let workbook: XLSX.WorkBook;
83
+ try {
84
+ // Dense sheets store rows as arrays: smaller than one object per cell.
85
+ workbook = XLSX.read(input.bytes, { type: "buffer", dense: true });
86
+ } catch (cause) {
87
+ const message = cause instanceof Error ? cause.message : String(cause);
88
+ return {
89
+ ok: false,
90
+ error: PASSWORD_ERROR_REGEX.test(message)
91
+ ? permissionError(
92
+ input,
93
+ CONVERTER_ID,
94
+ "File is password-protected",
95
+ cause,
96
+ { protection: "xlsx" }
97
+ )
98
+ : corruptError(
99
+ input,
100
+ CONVERTER_ID,
101
+ "Could not read the .xlsx workbook",
102
+ cause
103
+ ),
104
+ };
105
+ }
106
+
107
+ const markdown = workbookToMarkdown(workbook);
108
+ if (!markdown) {
109
+ return {
110
+ ok: false,
111
+ error: corruptError(input, CONVERTER_ID, "Empty conversion result"),
112
+ };
113
+ }
114
+ const lossy =
115
+ markdown.length < SHORT_OUTPUT_CHARS &&
116
+ input.bytes.length > SHORT_OUTPUT_MIN_BYTES;
117
+ return {
118
+ ok: true,
119
+ value: {
120
+ markdown,
121
+ title: workbook.Props?.Title || "Untitled",
122
+ meta: {
123
+ converterId: CONVERTER_ID,
124
+ converterVersion: CONVERTER_VERSION,
125
+ sourceMime: input.mime,
126
+ warnings: lossy
127
+ ? [{ code: "LOSSY", message: "Suspiciously short output" }]
128
+ : undefined,
129
+ },
130
+ },
131
+ };
132
+ }