@gmickel/gno 1.27.1 → 1.28.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 (83) hide show
  1. package/README.md +2 -2
  2. package/assets/skill/SKILL.md +26 -1
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.27.1.zip → gno-browser-clipper-v1.28.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +2 -2
  7. package/spec/cli.md +41 -4
  8. package/spec/db/schema.sql +12 -0
  9. package/spec/mcp.md +5 -0
  10. package/spec/output-schemas/ask.schema.json +125 -0
  11. package/spec/output-schemas/context-capsule-v1.schema.json +94 -1
  12. package/spec/output-schemas/get.schema.json +94 -0
  13. package/spec/output-schemas/mcp-job-status.schema.json +28 -0
  14. package/spec/output-schemas/multi-get.schema.json +128 -0
  15. package/spec/output-schemas/record-import.schema.json +193 -0
  16. package/spec/output-schemas/search-result.schema.json +94 -0
  17. package/spec/output-schemas/search-results.schema.json +125 -0
  18. package/spec/project-profile.schema.json +66 -0
  19. package/src/app/context-format.ts +1 -1
  20. package/src/cli/commands/get.ts +12 -2
  21. package/src/cli/commands/index-cmd.ts +13 -0
  22. package/src/cli/commands/multi-get.ts +9 -2
  23. package/src/cli/commands/shared.ts +28 -0
  24. package/src/cli/commands/update.ts +5 -0
  25. package/src/cli/program.ts +4 -0
  26. package/src/config/project-profile.ts +2 -0
  27. package/src/config/types.ts +16 -0
  28. package/src/converters/adapters/browser-export/adapter.ts +199 -0
  29. package/src/converters/adapters/browser-export/formats.ts +358 -0
  30. package/src/converters/adapters/email/adapter.ts +429 -0
  31. package/src/converters/adapters/email/html.ts +162 -0
  32. package/src/converters/adapters/email/mime.ts +454 -0
  33. package/src/converters/adapters/email/parameters.ts +77 -0
  34. package/src/converters/adapters/ical/adapter.ts +475 -0
  35. package/src/converters/adapters/ical/recurrence.ts +186 -0
  36. package/src/converters/adapters/jsonl/adapter.ts +238 -0
  37. package/src/converters/adapters/jsonl/config.ts +105 -0
  38. package/src/converters/adapters/shared/html-text.ts +165 -0
  39. package/src/converters/adapters/shared/record-utils.ts +79 -0
  40. package/src/converters/adapters/shared/utf8-lines.ts +141 -0
  41. package/src/converters/adapters/transcript/adapter.ts +295 -0
  42. package/src/converters/adapters/transcript/json.ts +184 -0
  43. package/src/converters/adapters/transcript/model.ts +171 -0
  44. package/src/converters/adapters/transcript/text.ts +58 -0
  45. package/src/converters/adapters/transcript/timed.ts +152 -0
  46. package/src/converters/index.ts +11 -1
  47. package/src/converters/mime.ts +8 -0
  48. package/src/converters/pipeline.ts +15 -1
  49. package/src/converters/registry.ts +37 -1
  50. package/src/converters/types.ts +136 -0
  51. package/src/core/context-capsule-schema.ts +87 -0
  52. package/src/core/context-capsule.ts +20 -0
  53. package/src/core/context-evidence.ts +7 -1
  54. package/src/core/document-capabilities.ts +12 -0
  55. package/src/core/project-profile-apply-state.ts +5 -0
  56. package/src/core/project-profile.ts +4 -0
  57. package/src/core/record-metadata.ts +49 -0
  58. package/src/ingestion/record-adapter-canonical.ts +433 -0
  59. package/src/ingestion/record-adapter.ts +437 -0
  60. package/src/ingestion/record-container.ts +688 -0
  61. package/src/ingestion/record-path.ts +20 -0
  62. package/src/ingestion/record-sync.ts +70 -0
  63. package/src/ingestion/sync.ts +228 -36
  64. package/src/ingestion/types.ts +69 -1
  65. package/src/ingestion/walker.ts +31 -11
  66. package/src/mcp/tools/get.ts +10 -2
  67. package/src/mcp/tools/multi-get.ts +9 -2
  68. package/src/mcp/tools/workspace-write.ts +2 -0
  69. package/src/pipeline/filters.ts +1 -1
  70. package/src/pipeline/graph-retrieval.ts +7 -4
  71. package/src/pipeline/hybrid.ts +7 -4
  72. package/src/pipeline/result-context.ts +12 -4
  73. package/src/pipeline/search.ts +11 -4
  74. package/src/pipeline/types.ts +3 -0
  75. package/src/pipeline/vsearch.ts +26 -8
  76. package/src/sdk/documents.ts +13 -5
  77. package/src/serve/browse-tree.ts +4 -2
  78. package/src/serve/routes/api.ts +63 -65
  79. package/src/store/migrations/022-record-export-lineage.ts +42 -0
  80. package/src/store/migrations/index.ts +2 -0
  81. package/src/store/sqlite/adapter.ts +114 -7
  82. package/src/store/types.ts +40 -0
  83. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
@@ -21,6 +21,10 @@ import {
21
21
  getDocumentCapabilities,
22
22
  type DocumentCapabilities,
23
23
  } from "../../core/document-capabilities";
24
+ import {
25
+ projectRecordEvidenceMetadata,
26
+ type RecordEvidenceMetadata,
27
+ } from "../../core/record-metadata";
24
28
  import {
25
29
  evidenceFromExactDocument,
26
30
  RetrievalTraceSession as TraceSession,
@@ -85,6 +89,7 @@ export interface GetResponse {
85
89
  converterVersion?: string;
86
90
  mirrorHash?: string;
87
91
  };
92
+ record?: RecordEvidenceMetadata;
88
93
  capabilities: DocumentCapabilities;
89
94
  }
90
95
 
@@ -332,6 +337,7 @@ function buildResponse(ctx: BuildResponseContext): GetResult {
332
337
  language: doc.languageHint ?? undefined,
333
338
  source: buildSourceMeta(doc, config),
334
339
  conversion: buildConversionMeta(doc),
340
+ record: projectRecordEvidenceMetadata(doc),
335
341
  capabilities: buildCapabilities(doc),
336
342
  },
337
343
  };
@@ -349,6 +355,7 @@ interface DocRow {
349
355
  sourceSize: number;
350
356
  sourceMtime?: string;
351
357
  sourceHash: string;
358
+ recordSourcePath?: string | null;
352
359
  }
353
360
 
354
361
  function buildSourceMeta(
@@ -356,11 +363,12 @@ function buildSourceMeta(
356
363
  config: ConfigLike
357
364
  ): GetResponse["source"] {
358
365
  const coll = config.collections.find((c) => c.name === doc.collection);
359
- const absPath = coll ? `${coll.path}/${doc.relPath}` : undefined;
366
+ const relPath = doc.recordSourcePath ?? doc.relPath;
367
+ const absPath = coll ? `${coll.path}/${relPath}` : undefined;
360
368
 
361
369
  return {
362
370
  absPath,
363
- relPath: doc.relPath,
371
+ relPath,
364
372
  mime: doc.sourceMime,
365
373
  ext: doc.sourceExt,
366
374
  modifiedAt: doc.sourceMtime ?? undefined,
@@ -373,11 +381,13 @@ function buildCapabilities(doc: {
373
381
  sourceExt: string;
374
382
  sourceMime: string;
375
383
  mirrorHash?: string | null;
384
+ recordKey?: string | null;
376
385
  }): DocumentCapabilities {
377
386
  return getDocumentCapabilities({
378
387
  sourceExt: doc.sourceExt,
379
388
  sourceMime: doc.sourceMime,
380
389
  contentAvailable: doc.mirrorHash !== null,
390
+ recordKey: doc.recordKey,
381
391
  });
382
392
  }
383
393
 
@@ -32,6 +32,8 @@ export interface IndexOptions {
32
32
  yes?: boolean;
33
33
  /** Verbose output */
34
34
  verbose?: boolean;
35
+ /** Emit the complete structured index receipt. */
36
+ json?: boolean;
35
37
  }
36
38
 
37
39
  /**
@@ -124,6 +126,17 @@ export function formatIndex(
124
126
  return `Error: ${result.error}`;
125
127
  }
126
128
 
129
+ if (options.json) {
130
+ return JSON.stringify(
131
+ {
132
+ syncResult: result.syncResult,
133
+ embedSkipped: result.embedSkipped,
134
+ ...(result.embedResult ? { embedResult: result.embedResult } : {}),
135
+ },
136
+ null,
137
+ 2
138
+ );
139
+ }
127
140
  const { syncResult, embedSkipped } = result;
128
141
  const lines: string[] = ["Indexing complete.", ""];
129
142
 
@@ -17,6 +17,10 @@ import {
17
17
  indexNamesMatch,
18
18
  isValidIndexName,
19
19
  } from "../../app/index-name";
20
+ import {
21
+ projectRecordEvidenceMetadata,
22
+ type RecordEvidenceMetadata,
23
+ } from "../../core/record-metadata";
20
24
  import { isGlobPattern, parseRef, splitRefs } from "./ref-parser";
21
25
  import { initStore } from "./shared";
22
26
 
@@ -53,6 +57,7 @@ export interface MultiGetDocument {
53
57
  truncated?: boolean;
54
58
  totalLines?: number;
55
59
  source: { absPath?: string; relPath: string; mime: string; ext: string };
60
+ record?: RecordEvidenceMetadata;
56
61
  }
57
62
 
58
63
  export interface SkippedDoc {
@@ -230,6 +235,7 @@ async function fetchSingleDocument(
230
235
  );
231
236
  const coll = ctx.config.collections.find((c) => c.name === doc.collection);
232
237
 
238
+ const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
233
239
  ctx.documents.push({
234
240
  docid: doc.docid,
235
241
  uri: decorateUriForIndex(doc.uri, ctx.indexName),
@@ -238,11 +244,12 @@ async function fetchSingleDocument(
238
244
  truncated: truncated || undefined,
239
245
  totalLines: content.split("\n").length,
240
246
  source: {
241
- absPath: coll ? `${coll.path}/${doc.relPath}` : undefined,
242
- relPath: doc.relPath,
247
+ absPath: coll ? `${coll.path}/${sourceRelPath}` : undefined,
248
+ relPath: sourceRelPath,
243
249
  mime: doc.sourceMime,
244
250
  ext: doc.sourceExt,
245
251
  },
252
+ record: projectRecordEvidenceMetadata(doc),
246
253
  });
247
254
  }
248
255
 
@@ -168,6 +168,34 @@ export function formatSyncResultLines(
168
168
  lines.push(` ${c.filesMarkedInactive} marked inactive`);
169
169
  }
170
170
 
171
+ for (const file of c.files ?? []) {
172
+ const receipt = file.recordImport;
173
+ if (
174
+ !receipt ||
175
+ (receipt.failures.length === 0 && receipt.warnings.length === 0)
176
+ )
177
+ continue;
178
+ const warningCount = receipt.failures.length + receipt.warnings.length;
179
+ lines.push(
180
+ ` ${file.relPath}: ${warningCount} record warning${warningCount === 1 ? "" : "s"} (${receipt.snapshotState} snapshot)`
181
+ );
182
+ if (options.verbose) {
183
+ for (const warning of receipt.warnings) {
184
+ lines.push(
185
+ ` [${warning.code}]: ${warning.message} (retryable=${warning.retryable ? "yes" : "no"})`
186
+ );
187
+ }
188
+ for (const failure of receipt.failures) {
189
+ const locator = failure.sourceLocator
190
+ ? ` at ${failure.sourceLocator}`
191
+ : "";
192
+ lines.push(
193
+ ` [${failure.code}]${locator}: ${failure.message} (retryable=${failure.retryable ? "yes" : "no"})`
194
+ );
195
+ }
196
+ }
197
+ }
198
+
171
199
  if (options.verbose && c.errors.length > 0) {
172
200
  for (const err of c.errors) {
173
201
  lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
@@ -24,6 +24,8 @@ export interface UpdateOptions {
24
24
  gitPull?: boolean;
25
25
  /** Verbose output */
26
26
  verbose?: boolean;
27
+ /** Emit the complete structured sync receipt. */
28
+ json?: boolean;
27
29
  }
28
30
 
29
31
  /**
@@ -80,5 +82,8 @@ export function formatUpdate(
80
82
  return `Error: ${result.error}`;
81
83
  }
82
84
 
85
+ if (options.json) {
86
+ return JSON.stringify(result.result, null, 2);
87
+ }
83
88
  return formatSyncResultLines(result.result, options).join("\n");
84
89
  }
@@ -1509,6 +1509,7 @@ function wireOnboardingCommands(program: Command): void {
1509
1509
  .option("--no-embed", "skip embedding after sync")
1510
1510
  .option("--git-pull", "run git pull in git repositories")
1511
1511
  .option("--models-pull", "download models if missing")
1512
+ .option("--json", "JSON output")
1512
1513
  .action(
1513
1514
  async (
1514
1515
  collection: string | undefined,
@@ -1525,6 +1526,7 @@ function wireOnboardingCommands(program: Command): void {
1525
1526
  modelsPull: Boolean(cmdOpts.modelsPull),
1526
1527
  yes: globals.yes,
1527
1528
  verbose: globals.verbose,
1529
+ json: getFormat(cmdOpts) === "json",
1528
1530
  };
1529
1531
  const result = await index(opts);
1530
1532
 
@@ -2515,6 +2517,7 @@ function wireManagementCommands(program: Command): void {
2515
2517
  .command("update")
2516
2518
  .description("Sync files from disk into the index")
2517
2519
  .option("--git-pull", "run git pull in git repositories")
2520
+ .option("--json", "JSON output")
2518
2521
  .action(async (cmdOpts: Record<string, unknown>) => {
2519
2522
  const globals = getGlobals();
2520
2523
  const { update, formatUpdate } = await import("./commands/update");
@@ -2523,6 +2526,7 @@ function wireManagementCommands(program: Command): void {
2523
2526
  indexName: globals.index,
2524
2527
  gitPull: Boolean(cmdOpts.gitPull),
2525
2528
  verbose: globals.verbose,
2529
+ json: getFormat(cmdOpts) === "json",
2526
2530
  };
2527
2531
  const result = await update(opts);
2528
2532
 
@@ -5,6 +5,7 @@ import { hasLikelySecretPath } from "../core/path-rules";
5
5
  import {
6
6
  CONTENT_TYPE_SEARCH_BOOST_MAX,
7
7
  CONTENT_TYPE_SEARCH_BOOST_MIN,
8
+ CollectionSchema,
8
9
  isValidLanguageHint,
9
10
  PROJECT_AFFINITY_MAX_CONTRIBUTION,
10
11
  } from "./types";
@@ -218,6 +219,7 @@ export const ProjectProfileCollectionSchema = z
218
219
  .refine(isValidLanguageHint, "Invalid BCP-47 language hint")
219
220
  .optional(),
220
221
  modelPreset: z.string().regex(REFERENCE_PATTERN).optional(),
222
+ recordAdapters: CollectionSchema.shape.recordAdapters,
221
223
  })
222
224
  .strict();
223
225
 
@@ -10,6 +10,7 @@ import { isAbsolute } from "node:path";
10
10
  import { z } from "zod";
11
11
 
12
12
  import { URI_PREFIX } from "../app/constants";
13
+ import { JsonlFieldMappingSchema } from "../converters/adapters/jsonl/config";
13
14
  import { RetrievalTraceConfigSchema } from "./retrieval-traces";
14
15
 
15
16
  // ─────────────────────────────────────────────────────────────────────────────
@@ -112,6 +113,21 @@ export const CollectionSchema = z.object({
112
113
  gen: z.string().min(1).optional(),
113
114
  })
114
115
  .optional(),
116
+
117
+ /** Optional declarative overrides for ambiguous export formats. */
118
+ recordAdapters: z
119
+ .object({
120
+ jsonl: z
121
+ .object({ fieldMapping: JsonlFieldMappingSchema.optional() })
122
+ .strict()
123
+ .optional(),
124
+ transcript: z
125
+ .object({ format: z.enum(["json", "srt", "text", "vtt"]) })
126
+ .strict()
127
+ .optional(),
128
+ })
129
+ .strict()
130
+ .optional(),
115
131
  });
116
132
 
117
133
  export type Collection = z.infer<typeof CollectionSchema>;
@@ -0,0 +1,199 @@
1
+ import type {
2
+ RecordAdapter,
3
+ RecordAdapterEvent,
4
+ RecordAdapterInput,
5
+ } from "../../types";
6
+
7
+ import {
8
+ type BrowserExportRecord,
9
+ parseBrowserJson,
10
+ parseNetscapeBookmarks,
11
+ } from "./formats";
12
+
13
+ const LIVE_PROFILE_MARKERS = [
14
+ "/library/safari/",
15
+ "/chrome/user data/",
16
+ "/google/chrome/",
17
+ "/chromium/user data/",
18
+ "/.config/google-chrome/",
19
+ "/.config/chromium/",
20
+ "/appdata/local/google/chrome/user data/",
21
+ "/firefox/profiles/",
22
+ "/.mozilla/firefox/",
23
+ "/mozilla/firefox/",
24
+ "/appdata/roaming/mozilla/firefox/profiles/",
25
+ ];
26
+
27
+ const safeMarkdown = (value: string): string =>
28
+ value
29
+ .normalize("NFC")
30
+ .replaceAll("&", "&amp;")
31
+ .replaceAll("<", "&lt;")
32
+ .replaceAll(">", "&gt;")
33
+ .replace(/([\\`*_[\]#])/g, "\\$1")
34
+ .trim();
35
+
36
+ const liveProfileReason = (input: RecordAdapterInput): string | undefined => {
37
+ const normalized = input.sourcePath.replaceAll("\\", "/").toLowerCase();
38
+ if (LIVE_PROFILE_MARKERS.some((marker) => normalized.includes(marker))) {
39
+ return "live-profile";
40
+ }
41
+ const basename = normalized.split("/").at(-1);
42
+ if (
43
+ basename === "cookies" ||
44
+ basename === "history" ||
45
+ basename?.endsWith(".sqlite") ||
46
+ basename?.endsWith(".db")
47
+ ) {
48
+ return "live-database";
49
+ }
50
+ return undefined;
51
+ };
52
+
53
+ const normalizeUrl = (value: string): string | undefined => {
54
+ try {
55
+ const url = new URL(value.trim());
56
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
57
+ return undefined;
58
+ }
59
+ if (url.username || url.password) return undefined;
60
+ url.hash = "";
61
+ return url.toString();
62
+ } catch {
63
+ return undefined;
64
+ }
65
+ };
66
+
67
+ const readSource = async (input: RecordAdapterInput): Promise<string> => {
68
+ const decoder = new TextDecoder("utf-8", { fatal: true });
69
+ let source = "";
70
+ for await (const chunk of input.open()) {
71
+ source += decoder.decode(chunk, { stream: true });
72
+ }
73
+ return source + decoder.decode();
74
+ };
75
+
76
+ const recordEvent = (
77
+ record: BrowserExportRecord
78
+ ): RecordAdapterEvent | undefined => {
79
+ const url = normalizeUrl(record.url);
80
+ if (!url) return undefined;
81
+ const title = safeMarkdown(record.title || url);
82
+ const folder = record.folder ? safeMarkdown(record.folder) : undefined;
83
+ const tags = record.tags?.map(safeMarkdown).filter(Boolean);
84
+ const lines = [`# ${title}`, "", url];
85
+ if (folder) lines.push("", `Folder: ${folder}`);
86
+ if (tags && tags.length > 0) lines.push(`Tags: ${tags.join(", ")}`);
87
+ for (const [name, value] of Object.entries(record.dates ?? {}).sort(
88
+ ([left], [right]) => left.localeCompare(right)
89
+ )) {
90
+ lines.push(`${safeMarkdown(name)}: ${safeMarkdown(value)}`);
91
+ }
92
+ const identity = record.externalId
93
+ ? `${record.kind}:id:${record.externalId}`
94
+ : `${record.kind}:url:${url}`;
95
+ return {
96
+ type: "record",
97
+ record: {
98
+ stableId: `browser:${identity}`,
99
+ sourceLocator: record.sourceLocator,
100
+ markdown: lines.join("\n"),
101
+ title,
102
+ metadata: {
103
+ categories: ["browser-export", record.kind, ...(tags ?? [])],
104
+ dateFields: record.dates,
105
+ },
106
+ anchors: [{ kind: "record", value: record.sourceLocator }],
107
+ },
108
+ };
109
+ };
110
+
111
+ async function* parseBrowserExport(
112
+ input: RecordAdapterInput
113
+ ): AsyncGenerator<RecordAdapterEvent> {
114
+ const denied = liveProfileReason(input);
115
+ if (denied) {
116
+ yield {
117
+ type: "failure",
118
+ failure: {
119
+ code: "ADAPTER_FAILURE",
120
+ message: "Live browser profile access is not allowed.",
121
+ retryable: false,
122
+ sourceLocator: denied,
123
+ },
124
+ };
125
+ yield { type: "snapshot", state: "partial" };
126
+ return;
127
+ }
128
+
129
+ let source: string;
130
+ try {
131
+ source = await readSource(input);
132
+ } catch {
133
+ yield {
134
+ type: "failure",
135
+ failure: {
136
+ code: "MALFORMED_RECORD",
137
+ message: "Browser export could not be decoded.",
138
+ retryable: true,
139
+ sourceLocator: "export",
140
+ },
141
+ };
142
+ yield { type: "snapshot", state: "partial" };
143
+ return;
144
+ }
145
+ const trimmed = source.trimStart();
146
+ const parsed = trimmed.startsWith("<")
147
+ ? parseNetscapeBookmarks(
148
+ source,
149
+ input.limits.maxRecords,
150
+ input.limits.maxFailures
151
+ )
152
+ : parseBrowserJson(
153
+ source,
154
+ input.limits.maxRecords,
155
+ input.limits.maxFailures
156
+ );
157
+ let failures = parsed.failures.length;
158
+ for (const candidate of parsed.records) {
159
+ const event = recordEvent(candidate);
160
+ if (event) yield event;
161
+ else {
162
+ failures += 1;
163
+ yield {
164
+ type: "failure",
165
+ failure: {
166
+ code: "MALFORMED_RECORD",
167
+ message: "Browser export contained an invalid or unsafe URL.",
168
+ retryable: false,
169
+ sourceLocator: candidate.sourceLocator,
170
+ },
171
+ };
172
+ }
173
+ }
174
+ for (const locator of parsed.failures) {
175
+ yield {
176
+ type: "failure",
177
+ failure: {
178
+ code: "MALFORMED_RECORD",
179
+ message: "Browser export record could not be parsed.",
180
+ retryable: false,
181
+ sourceLocator: locator,
182
+ },
183
+ };
184
+ }
185
+ yield {
186
+ type: "snapshot",
187
+ state: failures === 0 ? "complete" : "partial",
188
+ };
189
+ }
190
+
191
+ export const browserExportAdapter: RecordAdapter = {
192
+ id: "adapter/browser-export",
193
+ version: "1.0.0",
194
+ canHandle: (mime, ext) =>
195
+ mime === "application/x-gno-browser-export+json" ||
196
+ mime === "text/x-gno-browser-bookmarks+html" ||
197
+ ext === ".browser-export",
198
+ records: parseBrowserExport,
199
+ };