@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.
- package/README.md +2 -2
- package/assets/skill/SKILL.md +26 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.27.1.zip → gno-browser-clipper-v1.28.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/spec/cli.md +41 -4
- package/spec/db/schema.sql +12 -0
- package/spec/mcp.md +5 -0
- package/spec/output-schemas/ask.schema.json +125 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +94 -1
- package/spec/output-schemas/get.schema.json +94 -0
- package/spec/output-schemas/mcp-job-status.schema.json +28 -0
- package/spec/output-schemas/multi-get.schema.json +128 -0
- package/spec/output-schemas/record-import.schema.json +193 -0
- package/spec/output-schemas/search-result.schema.json +94 -0
- package/spec/output-schemas/search-results.schema.json +125 -0
- package/spec/project-profile.schema.json +66 -0
- package/src/app/context-format.ts +1 -1
- package/src/cli/commands/get.ts +12 -2
- package/src/cli/commands/index-cmd.ts +13 -0
- package/src/cli/commands/multi-get.ts +9 -2
- package/src/cli/commands/shared.ts +28 -0
- package/src/cli/commands/update.ts +5 -0
- package/src/cli/program.ts +4 -0
- package/src/config/project-profile.ts +2 -0
- package/src/config/types.ts +16 -0
- package/src/converters/adapters/browser-export/adapter.ts +199 -0
- package/src/converters/adapters/browser-export/formats.ts +358 -0
- package/src/converters/adapters/email/adapter.ts +429 -0
- package/src/converters/adapters/email/html.ts +162 -0
- package/src/converters/adapters/email/mime.ts +454 -0
- package/src/converters/adapters/email/parameters.ts +77 -0
- package/src/converters/adapters/ical/adapter.ts +475 -0
- package/src/converters/adapters/ical/recurrence.ts +186 -0
- package/src/converters/adapters/jsonl/adapter.ts +238 -0
- package/src/converters/adapters/jsonl/config.ts +105 -0
- package/src/converters/adapters/shared/html-text.ts +165 -0
- package/src/converters/adapters/shared/record-utils.ts +79 -0
- package/src/converters/adapters/shared/utf8-lines.ts +141 -0
- package/src/converters/adapters/transcript/adapter.ts +295 -0
- package/src/converters/adapters/transcript/json.ts +184 -0
- package/src/converters/adapters/transcript/model.ts +171 -0
- package/src/converters/adapters/transcript/text.ts +58 -0
- package/src/converters/adapters/transcript/timed.ts +152 -0
- package/src/converters/index.ts +11 -1
- package/src/converters/mime.ts +8 -0
- package/src/converters/pipeline.ts +15 -1
- package/src/converters/registry.ts +37 -1
- package/src/converters/types.ts +136 -0
- package/src/core/context-capsule-schema.ts +87 -0
- package/src/core/context-capsule.ts +20 -0
- package/src/core/context-evidence.ts +7 -1
- package/src/core/document-capabilities.ts +12 -0
- package/src/core/project-profile-apply-state.ts +5 -0
- package/src/core/project-profile.ts +4 -0
- package/src/core/record-metadata.ts +49 -0
- package/src/ingestion/record-adapter-canonical.ts +433 -0
- package/src/ingestion/record-adapter.ts +437 -0
- package/src/ingestion/record-container.ts +688 -0
- package/src/ingestion/record-path.ts +20 -0
- package/src/ingestion/record-sync.ts +70 -0
- package/src/ingestion/sync.ts +228 -36
- package/src/ingestion/types.ts +69 -1
- package/src/ingestion/walker.ts +31 -11
- package/src/mcp/tools/get.ts +10 -2
- package/src/mcp/tools/multi-get.ts +9 -2
- package/src/mcp/tools/workspace-write.ts +2 -0
- package/src/pipeline/filters.ts +1 -1
- package/src/pipeline/graph-retrieval.ts +7 -4
- package/src/pipeline/hybrid.ts +7 -4
- package/src/pipeline/result-context.ts +12 -4
- package/src/pipeline/search.ts +11 -4
- package/src/pipeline/types.ts +3 -0
- package/src/pipeline/vsearch.ts +26 -8
- package/src/sdk/documents.ts +13 -5
- package/src/serve/browse-tree.ts +4 -2
- package/src/serve/routes/api.ts +63 -65
- package/src/store/migrations/022-record-export-lineage.ts +42 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +114 -7
- package/src/store/types.ts +40 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
package/src/cli/commands/get.ts
CHANGED
|
@@ -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
|
|
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
|
|
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}/${
|
|
242
|
-
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
|
}
|
package/src/cli/program.ts
CHANGED
|
@@ -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
|
|
package/src/config/types.ts
CHANGED
|
@@ -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("&", "&")
|
|
31
|
+
.replaceAll("<", "<")
|
|
32
|
+
.replaceAll(">", ">")
|
|
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
|
+
};
|