@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { isolatedAgentBrowserEnvironment } from "./acquire.js";
|
|
6
|
+
import { BoundedByteBuffer } from "./bounded-byte-buffer.js";
|
|
7
|
+
import { findKbPackageRoot } from "./package-root.js";
|
|
8
|
+
import type { Platform } from "./platforms.js";
|
|
9
|
+
|
|
10
|
+
export const expectedBunVersion = "1.3.14" as const;
|
|
11
|
+
export const expectedQmdVersion = "2.5.3" as const;
|
|
12
|
+
export const expectedSqliteVecVersion = "0.1.9" as const;
|
|
13
|
+
export const expectedNodeLlamaCppVersion = "3.18.1" as const;
|
|
14
|
+
|
|
15
|
+
const expectedBetterSqliteVersion = "12.10.0" as const;
|
|
16
|
+
const homebrewSqliteLibraryPaths = [
|
|
17
|
+
"/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib",
|
|
18
|
+
"/usr/local/opt/sqlite/lib/libsqlite3.dylib",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type CapabilityStatus = "ready" | "partial" | "unavailable";
|
|
22
|
+
|
|
23
|
+
export type DependencyReport = {
|
|
24
|
+
readonly name: "defuddle" | "agent-browser" | "@steipete/sweet-cookie" | "@tobilu/qmd";
|
|
25
|
+
readonly expectedVersion: string;
|
|
26
|
+
readonly declaredVersion: string | null;
|
|
27
|
+
readonly installedVersion: string | null;
|
|
28
|
+
readonly status: CapabilityStatus;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SearchReadinessReport = {
|
|
32
|
+
readonly runtime: "bun" | "node";
|
|
33
|
+
readonly platform: NodeJS.Platform;
|
|
34
|
+
readonly architecture: NodeJS.Architecture;
|
|
35
|
+
/** Keyword-only QMD search does not initialize or require an embedding model. */
|
|
36
|
+
readonly keywordOnly: {
|
|
37
|
+
readonly status: CapabilityStatus;
|
|
38
|
+
readonly modelRequired: false;
|
|
39
|
+
};
|
|
40
|
+
/** Static prerequisites only. Doctor never loads sqlite-vec or an embedding model. */
|
|
41
|
+
readonly semanticPrerequisites: {
|
|
42
|
+
readonly status: CapabilityStatus;
|
|
43
|
+
readonly sqlite: {
|
|
44
|
+
readonly provider: "bun-built-in" | "homebrew" | "better-sqlite3";
|
|
45
|
+
readonly expectedVersion: string | null;
|
|
46
|
+
readonly installedVersion: string | null;
|
|
47
|
+
readonly searchedLibraryPaths: readonly string[];
|
|
48
|
+
readonly selectedLibraryPath: string | null;
|
|
49
|
+
readonly nativeBindingPresent: boolean | null;
|
|
50
|
+
readonly status: CapabilityStatus;
|
|
51
|
+
};
|
|
52
|
+
readonly sqliteVec: {
|
|
53
|
+
readonly expectedVersion: typeof expectedSqliteVecVersion;
|
|
54
|
+
readonly installedVersion: string | null;
|
|
55
|
+
readonly nativePackageName: string | null;
|
|
56
|
+
readonly nativeInstalledVersion: string | null;
|
|
57
|
+
readonly nativeBinaryPresent: boolean | null;
|
|
58
|
+
readonly status: CapabilityStatus;
|
|
59
|
+
};
|
|
60
|
+
readonly embeddingRuntime: {
|
|
61
|
+
readonly expectedVersion: typeof expectedNodeLlamaCppVersion;
|
|
62
|
+
readonly installedVersion: string | null;
|
|
63
|
+
readonly nativePackageName: string | null;
|
|
64
|
+
readonly nativeInstalledVersion: string | null;
|
|
65
|
+
readonly nativeBinaryPresent: boolean | null;
|
|
66
|
+
readonly status: CapabilityStatus;
|
|
67
|
+
};
|
|
68
|
+
readonly embeddingModel: {
|
|
69
|
+
readonly cacheStatus: "not-inspected";
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type BrowserReport = {
|
|
75
|
+
readonly name: "Google Chrome" | "Chromium" | "Microsoft Edge" | "Arc";
|
|
76
|
+
readonly paths: readonly string[];
|
|
77
|
+
readonly status: CapabilityStatus;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type ToolReport = {
|
|
81
|
+
readonly name: "yt-dlp" | "ffmpeg" | "pdfinfo" | "pdftohtml" | "tesseract";
|
|
82
|
+
readonly path: string | null;
|
|
83
|
+
readonly version: string | null;
|
|
84
|
+
readonly status: CapabilityStatus;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export type DoctorReport = {
|
|
88
|
+
/** Schema 2 adds `search`; all schema-1 fields retain their names and meanings. */
|
|
89
|
+
readonly schemaVersion: 2;
|
|
90
|
+
readonly generatedAt: string;
|
|
91
|
+
readonly bun: {
|
|
92
|
+
readonly expectedVersion: typeof expectedBunVersion;
|
|
93
|
+
readonly currentVersion: string;
|
|
94
|
+
readonly status: CapabilityStatus;
|
|
95
|
+
};
|
|
96
|
+
readonly dependencies: readonly DependencyReport[];
|
|
97
|
+
readonly deriveClient: {
|
|
98
|
+
readonly available: boolean;
|
|
99
|
+
readonly status: CapabilityStatus;
|
|
100
|
+
};
|
|
101
|
+
readonly browsers: readonly BrowserReport[];
|
|
102
|
+
/** Display names only. Cookie stores and keychains are never inspected by doctor. */
|
|
103
|
+
readonly chromeProfileNames: readonly string[];
|
|
104
|
+
readonly tools: readonly ToolReport[];
|
|
105
|
+
readonly search: SearchReadinessReport;
|
|
106
|
+
readonly warnings: readonly string[];
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type DiagnosticCommand = {
|
|
110
|
+
readonly command: readonly string[];
|
|
111
|
+
readonly timeoutMs: number;
|
|
112
|
+
readonly maxOutputBytes: number;
|
|
113
|
+
readonly cwd?: string;
|
|
114
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type DiagnosticCommandResult = {
|
|
118
|
+
readonly stdout: string;
|
|
119
|
+
readonly stderr: string;
|
|
120
|
+
readonly exitCode: number;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type DiagnosticCommandRunner = (specification: DiagnosticCommand) => Promise<DiagnosticCommandResult>;
|
|
124
|
+
|
|
125
|
+
export type DoctorOptions = {
|
|
126
|
+
readonly packageRoot?: string;
|
|
127
|
+
readonly homeDirectory?: string;
|
|
128
|
+
readonly platform?: NodeJS.Platform;
|
|
129
|
+
readonly architecture?: NodeJS.Architecture;
|
|
130
|
+
readonly runtime?: "bun" | "node";
|
|
131
|
+
readonly currentBunVersion?: string;
|
|
132
|
+
readonly now?: () => Date;
|
|
133
|
+
readonly exists?: (path: string) => boolean;
|
|
134
|
+
readonly readText?: (path: string) => string;
|
|
135
|
+
readonly realpath?: (path: string) => string;
|
|
136
|
+
readonly which?: (executable: string) => string | null;
|
|
137
|
+
readonly run?: DiagnosticCommandRunner;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
141
|
+
|
|
142
|
+
const dependencyVersions = {
|
|
143
|
+
"defuddle": "0.19.1",
|
|
144
|
+
"agent-browser": "0.32.3",
|
|
145
|
+
"@steipete/sweet-cookie": "0.4.2",
|
|
146
|
+
"@tobilu/qmd": expectedQmdVersion,
|
|
147
|
+
} as const satisfies Readonly<Record<DependencyReport["name"], string>>;
|
|
148
|
+
const dependencyNames: readonly DependencyReport["name"][] = [
|
|
149
|
+
"defuddle",
|
|
150
|
+
"agent-browser",
|
|
151
|
+
"@steipete/sweet-cookie",
|
|
152
|
+
"@tobilu/qmd",
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const isRecord = (value: unknown): value is JsonRecord =>
|
|
156
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
157
|
+
|
|
158
|
+
async function readBoundedStream(stream: ReadableStream<Uint8Array>, maxBytes: number): Promise<string> {
|
|
159
|
+
const reader = stream.getReader();
|
|
160
|
+
const bytes = new BoundedByteBuffer(maxBytes);
|
|
161
|
+
try {
|
|
162
|
+
for (;;) {
|
|
163
|
+
const result = await reader.read();
|
|
164
|
+
if (result.done) break;
|
|
165
|
+
if (!bytes.append(result.value)) throw new Error(`diagnostic output exceeded ${maxBytes} bytes`);
|
|
166
|
+
}
|
|
167
|
+
} finally {
|
|
168
|
+
reader.releaseLock();
|
|
169
|
+
}
|
|
170
|
+
return new TextDecoder().decode(bytes.toUint8Array());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const runDiagnosticCommand: DiagnosticCommandRunner = async (specification) => {
|
|
174
|
+
const child = Bun.spawn([...specification.command], {
|
|
175
|
+
stdin: "ignore",
|
|
176
|
+
stdout: "pipe",
|
|
177
|
+
stderr: "pipe",
|
|
178
|
+
...(specification.cwd === undefined ? {} : { cwd: specification.cwd }),
|
|
179
|
+
...(specification.environment === undefined ? {} : { env: specification.environment }),
|
|
180
|
+
});
|
|
181
|
+
let timedOut = false;
|
|
182
|
+
let forceKillTimer: ReturnType<typeof setTimeout> | null = null;
|
|
183
|
+
const timer = setTimeout(() => {
|
|
184
|
+
timedOut = true;
|
|
185
|
+
child.kill("SIGTERM");
|
|
186
|
+
forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 1_000);
|
|
187
|
+
}, specification.timeoutMs);
|
|
188
|
+
try {
|
|
189
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
190
|
+
readBoundedStream(child.stdout, specification.maxOutputBytes),
|
|
191
|
+
readBoundedStream(child.stderr, specification.maxOutputBytes),
|
|
192
|
+
child.exited,
|
|
193
|
+
]);
|
|
194
|
+
if (timedOut) throw new Error(`diagnostic command timed out after ${specification.timeoutMs}ms`);
|
|
195
|
+
return { stdout, stderr, exitCode };
|
|
196
|
+
} catch (error) {
|
|
197
|
+
child.kill("SIGKILL");
|
|
198
|
+
await child.exited;
|
|
199
|
+
throw error;
|
|
200
|
+
} finally {
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
if (forceKillTimer !== null) clearTimeout(forceKillTimer);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
function parseJson(value: string): unknown {
|
|
207
|
+
const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
208
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
209
|
+
const line = lines[index];
|
|
210
|
+
if (line === undefined || (line[0] !== "{" && line[0] !== "[")) continue;
|
|
211
|
+
try {
|
|
212
|
+
return JSON.parse(line) as unknown;
|
|
213
|
+
} catch {
|
|
214
|
+
// Some CLIs emit a diagnostic line before their final JSON payload.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readJsonRecord(path: string, readText: (path: string) => string): JsonRecord | null {
|
|
221
|
+
try {
|
|
222
|
+
const parsed: unknown = JSON.parse(readText(path));
|
|
223
|
+
return isRecord(parsed) ? parsed : null;
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function dependencyVersion(manifest: JsonRecord | null, name: string): string | null {
|
|
230
|
+
if (manifest === null) return null;
|
|
231
|
+
for (const key of ["dependencies", "devDependencies"] as const) {
|
|
232
|
+
const section = manifest[key];
|
|
233
|
+
if (!isRecord(section)) continue;
|
|
234
|
+
const value = section[name];
|
|
235
|
+
if (typeof value === "string") return value;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function packageDirectory(name: string): readonly string[] {
|
|
241
|
+
return name.startsWith("@") ? name.split("/") : [name];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function installedDependencyDirectory(
|
|
245
|
+
name: string,
|
|
246
|
+
packageRoot: string,
|
|
247
|
+
readText: (path: string) => string,
|
|
248
|
+
): { readonly directory: string; readonly manifest: JsonRecord } | null {
|
|
249
|
+
const segments = packageDirectory(name);
|
|
250
|
+
let directory = resolve(packageRoot);
|
|
251
|
+
for (let depth = 0; depth < 12; depth += 1) {
|
|
252
|
+
const candidate = join(directory, "node_modules", ...segments);
|
|
253
|
+
const manifest = readJsonRecord(join(candidate, "package.json"), readText);
|
|
254
|
+
if (manifest !== null) return { directory: candidate, manifest };
|
|
255
|
+
const parent = dirname(directory);
|
|
256
|
+
if (parent === directory) break;
|
|
257
|
+
directory = parent;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function installedPackageVersion(
|
|
263
|
+
name: string,
|
|
264
|
+
fromDirectory: string,
|
|
265
|
+
readText: (path: string) => string,
|
|
266
|
+
): { readonly directory: string; readonly version: string | null } | null {
|
|
267
|
+
const installed = installedDependencyDirectory(name, fromDirectory, readText);
|
|
268
|
+
if (installed === null) return null;
|
|
269
|
+
return {
|
|
270
|
+
directory: installed.directory,
|
|
271
|
+
version: typeof installed.manifest.version === "string" ? installed.manifest.version : null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function capabilityStatus(statuses: readonly CapabilityStatus[]): CapabilityStatus {
|
|
276
|
+
if (statuses.some((status) => status === "unavailable")) return "unavailable";
|
|
277
|
+
return statuses.every((status) => status === "ready") ? "ready" : "partial";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function canonicalDirectory(path: string, realpath: (path: string) => string): string {
|
|
281
|
+
try {
|
|
282
|
+
return realpath(path);
|
|
283
|
+
} catch {
|
|
284
|
+
return path;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function nativeSqliteVecPackage(
|
|
289
|
+
platform: NodeJS.Platform,
|
|
290
|
+
architecture: NodeJS.Architecture,
|
|
291
|
+
): { readonly name: string; readonly binary: string } | null {
|
|
292
|
+
if (platform === "darwin" && (architecture === "arm64" || architecture === "x64")) {
|
|
293
|
+
return { name: `sqlite-vec-darwin-${architecture}`, binary: "vec0.dylib" };
|
|
294
|
+
}
|
|
295
|
+
if (platform === "linux" && (architecture === "arm64" || architecture === "x64")) {
|
|
296
|
+
return { name: `sqlite-vec-linux-${architecture}`, binary: "vec0.so" };
|
|
297
|
+
}
|
|
298
|
+
if (platform === "win32" && architecture === "x64") {
|
|
299
|
+
return { name: "sqlite-vec-windows-x64", binary: "vec0.dll" };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function nativeNodeLlamaCppPackage(
|
|
305
|
+
platform: NodeJS.Platform,
|
|
306
|
+
architecture: NodeJS.Architecture,
|
|
307
|
+
): { readonly name: string; readonly binary: string } | null {
|
|
308
|
+
if (platform === "darwin" && architecture === "arm64") {
|
|
309
|
+
return {
|
|
310
|
+
name: "@node-llama-cpp/mac-arm64-metal",
|
|
311
|
+
binary: join("bins", "mac-arm64-metal", "llama-addon.node"),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (platform === "darwin" && architecture === "x64") {
|
|
315
|
+
return {
|
|
316
|
+
name: "@node-llama-cpp/mac-x64",
|
|
317
|
+
binary: join("bins", "mac-x64", "llama-addon.node"),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
if (platform === "linux" && architecture === "arm64") {
|
|
321
|
+
return {
|
|
322
|
+
name: "@node-llama-cpp/linux-arm64",
|
|
323
|
+
binary: join("bins", "linux-arm64", "llama-addon.node"),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (platform === "linux" && architecture === "arm") {
|
|
327
|
+
return {
|
|
328
|
+
name: "@node-llama-cpp/linux-armv7l",
|
|
329
|
+
binary: join("bins", "linux-armv7l", "llama-addon.node"),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
if (platform === "linux" && architecture === "x64") {
|
|
333
|
+
return {
|
|
334
|
+
name: "@node-llama-cpp/linux-x64",
|
|
335
|
+
binary: join("bins", "linux-x64", "llama-addon.node"),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (platform === "win32" && architecture === "arm64") {
|
|
339
|
+
return {
|
|
340
|
+
name: "@node-llama-cpp/win-arm64",
|
|
341
|
+
binary: join("bins", "win-arm64", "llama-addon.node"),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (platform === "win32" && architecture === "x64") {
|
|
345
|
+
return {
|
|
346
|
+
name: "@node-llama-cpp/win-x64",
|
|
347
|
+
binary: join("bins", "win-x64", "llama-addon.node"),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function inspectSearchReadiness(
|
|
354
|
+
dependencies: readonly DependencyReport[],
|
|
355
|
+
packageRoot: string,
|
|
356
|
+
runtime: SearchReadinessReport["runtime"],
|
|
357
|
+
platform: NodeJS.Platform,
|
|
358
|
+
architecture: NodeJS.Architecture,
|
|
359
|
+
currentBunVersion: string,
|
|
360
|
+
exists: (path: string) => boolean,
|
|
361
|
+
readText: (path: string) => string,
|
|
362
|
+
realpath: (path: string) => string,
|
|
363
|
+
): SearchReadinessReport {
|
|
364
|
+
const qmd = dependencies.find(({ name }) => name === "@tobilu/qmd");
|
|
365
|
+
const qmdStatus = qmd?.status ?? "unavailable";
|
|
366
|
+
const runtimeStatus: CapabilityStatus = runtime === "bun" && currentBunVersion !== expectedBunVersion
|
|
367
|
+
? "partial"
|
|
368
|
+
: "ready";
|
|
369
|
+
const installedQmdDirectory = installedDependencyDirectory("@tobilu/qmd", packageRoot, readText)?.directory;
|
|
370
|
+
const qmdDirectory = installedQmdDirectory === undefined
|
|
371
|
+
? packageRoot
|
|
372
|
+
: canonicalDirectory(installedQmdDirectory, realpath);
|
|
373
|
+
|
|
374
|
+
let sqlite: SearchReadinessReport["semanticPrerequisites"]["sqlite"];
|
|
375
|
+
if (runtime === "bun" && platform === "darwin") {
|
|
376
|
+
const selectedLibraryPath = homebrewSqliteLibraryPaths.find((path) => exists(path)) ?? null;
|
|
377
|
+
sqlite = {
|
|
378
|
+
provider: "homebrew",
|
|
379
|
+
expectedVersion: null,
|
|
380
|
+
installedVersion: null,
|
|
381
|
+
searchedLibraryPaths: homebrewSqliteLibraryPaths,
|
|
382
|
+
selectedLibraryPath,
|
|
383
|
+
nativeBindingPresent: null,
|
|
384
|
+
status: selectedLibraryPath === null ? "unavailable" : "ready",
|
|
385
|
+
};
|
|
386
|
+
} else if (runtime === "bun") {
|
|
387
|
+
sqlite = {
|
|
388
|
+
provider: "bun-built-in",
|
|
389
|
+
expectedVersion: null,
|
|
390
|
+
installedVersion: null,
|
|
391
|
+
searchedLibraryPaths: [],
|
|
392
|
+
selectedLibraryPath: null,
|
|
393
|
+
nativeBindingPresent: null,
|
|
394
|
+
status: "ready",
|
|
395
|
+
};
|
|
396
|
+
} else {
|
|
397
|
+
const installed = installedPackageVersion("better-sqlite3", qmdDirectory, readText);
|
|
398
|
+
const nativeBindingPresent = installed === null
|
|
399
|
+
? false
|
|
400
|
+
: exists(join(installed.directory, "build", "Release", "better_sqlite3.node"));
|
|
401
|
+
sqlite = {
|
|
402
|
+
provider: "better-sqlite3",
|
|
403
|
+
expectedVersion: expectedBetterSqliteVersion,
|
|
404
|
+
installedVersion: installed?.version ?? null,
|
|
405
|
+
searchedLibraryPaths: [],
|
|
406
|
+
selectedLibraryPath: null,
|
|
407
|
+
nativeBindingPresent,
|
|
408
|
+
status: installed === null
|
|
409
|
+
? "unavailable"
|
|
410
|
+
: installed.version === expectedBetterSqliteVersion && nativeBindingPresent
|
|
411
|
+
? "ready"
|
|
412
|
+
: "partial",
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const sqliteVecPackage = installedPackageVersion("sqlite-vec", qmdDirectory, readText);
|
|
417
|
+
const nativeDefinition = nativeSqliteVecPackage(platform, architecture);
|
|
418
|
+
const nativePackage = nativeDefinition === null
|
|
419
|
+
? null
|
|
420
|
+
: installedPackageVersion(nativeDefinition.name, qmdDirectory, readText);
|
|
421
|
+
const nativeBinaryPresent = nativeDefinition === null || nativePackage === null
|
|
422
|
+
? null
|
|
423
|
+
: exists(join(nativePackage.directory, nativeDefinition.binary));
|
|
424
|
+
const sqliteVecStatus: CapabilityStatus = nativeDefinition === null || sqliteVecPackage === null || nativePackage === null
|
|
425
|
+
? "unavailable"
|
|
426
|
+
: sqliteVecPackage.version === expectedSqliteVecVersion
|
|
427
|
+
&& nativePackage.version === expectedSqliteVecVersion
|
|
428
|
+
&& nativeBinaryPresent === true
|
|
429
|
+
? "ready"
|
|
430
|
+
: "partial";
|
|
431
|
+
const nodeLlamaCpp = installedPackageVersion("node-llama-cpp", qmdDirectory, readText);
|
|
432
|
+
const canonicalNodeLlamaCppDirectory = nodeLlamaCpp === null
|
|
433
|
+
? qmdDirectory
|
|
434
|
+
: canonicalDirectory(nodeLlamaCpp.directory, realpath);
|
|
435
|
+
const nativeEmbeddingDefinition = nativeNodeLlamaCppPackage(platform, architecture);
|
|
436
|
+
const nativeEmbeddingPackage = nativeEmbeddingDefinition === null
|
|
437
|
+
? null
|
|
438
|
+
: installedPackageVersion(nativeEmbeddingDefinition.name, canonicalNodeLlamaCppDirectory, readText);
|
|
439
|
+
const nativeEmbeddingBinaryPresent = nativeEmbeddingDefinition === null || nativeEmbeddingPackage === null
|
|
440
|
+
? null
|
|
441
|
+
: exists(join(nativeEmbeddingPackage.directory, nativeEmbeddingDefinition.binary));
|
|
442
|
+
const embeddingRuntimeStatus: CapabilityStatus = nodeLlamaCpp === null
|
|
443
|
+
? "unavailable"
|
|
444
|
+
: nativeEmbeddingDefinition === null || nativeEmbeddingPackage === null
|
|
445
|
+
? "partial"
|
|
446
|
+
: nodeLlamaCpp.version === expectedNodeLlamaCppVersion
|
|
447
|
+
&& nativeEmbeddingPackage.version === expectedNodeLlamaCppVersion
|
|
448
|
+
&& nativeEmbeddingBinaryPresent === true
|
|
449
|
+
? "ready"
|
|
450
|
+
: "partial";
|
|
451
|
+
const keywordStatus = capabilityStatus([
|
|
452
|
+
qmdStatus,
|
|
453
|
+
runtimeStatus,
|
|
454
|
+
...(runtime === "node" ? [sqlite.status] : []),
|
|
455
|
+
]);
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
runtime,
|
|
459
|
+
platform,
|
|
460
|
+
architecture,
|
|
461
|
+
keywordOnly: {
|
|
462
|
+
status: keywordStatus,
|
|
463
|
+
modelRequired: false,
|
|
464
|
+
},
|
|
465
|
+
semanticPrerequisites: {
|
|
466
|
+
status: capabilityStatus([keywordStatus, sqlite.status, sqliteVecStatus, embeddingRuntimeStatus]),
|
|
467
|
+
sqlite,
|
|
468
|
+
sqliteVec: {
|
|
469
|
+
expectedVersion: expectedSqliteVecVersion,
|
|
470
|
+
installedVersion: sqliteVecPackage?.version ?? null,
|
|
471
|
+
nativePackageName: nativeDefinition?.name ?? null,
|
|
472
|
+
nativeInstalledVersion: nativePackage?.version ?? null,
|
|
473
|
+
nativeBinaryPresent,
|
|
474
|
+
status: sqliteVecStatus,
|
|
475
|
+
},
|
|
476
|
+
embeddingRuntime: {
|
|
477
|
+
expectedVersion: expectedNodeLlamaCppVersion,
|
|
478
|
+
installedVersion: nodeLlamaCpp?.version ?? null,
|
|
479
|
+
nativePackageName: nativeEmbeddingDefinition?.name ?? null,
|
|
480
|
+
nativeInstalledVersion: nativeEmbeddingPackage?.version ?? null,
|
|
481
|
+
nativeBinaryPresent: nativeEmbeddingBinaryPresent,
|
|
482
|
+
status: embeddingRuntimeStatus,
|
|
483
|
+
},
|
|
484
|
+
embeddingModel: {
|
|
485
|
+
cacheStatus: "not-inspected",
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function reportDependency(
|
|
492
|
+
name: DependencyReport["name"],
|
|
493
|
+
packageRoot: string,
|
|
494
|
+
rootManifest: JsonRecord | null,
|
|
495
|
+
readText: (path: string) => string,
|
|
496
|
+
): DependencyReport {
|
|
497
|
+
const expectedVersion = dependencyVersions[name];
|
|
498
|
+
const declaredVersion = dependencyVersion(rootManifest, name);
|
|
499
|
+
const installed = installedDependencyDirectory(name, packageRoot, readText);
|
|
500
|
+
const installedVersion = typeof installed?.manifest.version === "string" ? installed.manifest.version : null;
|
|
501
|
+
const status: CapabilityStatus = installedVersion === expectedVersion && declaredVersion !== null
|
|
502
|
+
? "ready"
|
|
503
|
+
: installedVersion === null
|
|
504
|
+
? "unavailable"
|
|
505
|
+
: "partial";
|
|
506
|
+
return { name, expectedVersion, declaredVersion, installedVersion, status };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
type BrowserDefinition = {
|
|
510
|
+
readonly name: BrowserReport["name"];
|
|
511
|
+
readonly macApplications: readonly string[];
|
|
512
|
+
readonly executableNames: Readonly<Partial<Record<NodeJS.Platform, readonly string[]>>>;
|
|
513
|
+
readonly linuxPaths: readonly string[];
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const browserDefinitions: readonly BrowserDefinition[] = [
|
|
517
|
+
{
|
|
518
|
+
name: "Google Chrome",
|
|
519
|
+
macApplications: ["Google Chrome"],
|
|
520
|
+
executableNames: {
|
|
521
|
+
darwin: ["google-chrome"],
|
|
522
|
+
linux: ["google-chrome", "google-chrome-stable"],
|
|
523
|
+
win32: ["chrome"],
|
|
524
|
+
},
|
|
525
|
+
linuxPaths: [
|
|
526
|
+
"/usr/bin/google-chrome",
|
|
527
|
+
"/usr/bin/google-chrome-stable",
|
|
528
|
+
"/usr/local/bin/google-chrome",
|
|
529
|
+
"/opt/google/chrome/google-chrome",
|
|
530
|
+
],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: "Chromium",
|
|
534
|
+
macApplications: ["Chromium"],
|
|
535
|
+
executableNames: {
|
|
536
|
+
darwin: ["chromium"],
|
|
537
|
+
linux: ["chromium", "chromium-browser"],
|
|
538
|
+
win32: ["chromium"],
|
|
539
|
+
},
|
|
540
|
+
linuxPaths: [
|
|
541
|
+
"/usr/bin/chromium",
|
|
542
|
+
"/usr/bin/chromium-browser",
|
|
543
|
+
"/usr/local/bin/chromium",
|
|
544
|
+
"/snap/bin/chromium",
|
|
545
|
+
],
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "Microsoft Edge",
|
|
549
|
+
macApplications: ["Microsoft Edge"],
|
|
550
|
+
executableNames: {
|
|
551
|
+
darwin: ["microsoft-edge"],
|
|
552
|
+
linux: ["microsoft-edge", "microsoft-edge-stable"],
|
|
553
|
+
win32: ["msedge"],
|
|
554
|
+
},
|
|
555
|
+
linuxPaths: [
|
|
556
|
+
"/usr/bin/microsoft-edge",
|
|
557
|
+
"/usr/bin/microsoft-edge-stable",
|
|
558
|
+
"/opt/microsoft/msedge/msedge",
|
|
559
|
+
],
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
name: "Arc",
|
|
563
|
+
macApplications: ["Arc"],
|
|
564
|
+
executableNames: {},
|
|
565
|
+
linuxPaths: [],
|
|
566
|
+
},
|
|
567
|
+
];
|
|
568
|
+
|
|
569
|
+
function browserPaths(
|
|
570
|
+
definition: BrowserDefinition,
|
|
571
|
+
platform: NodeJS.Platform,
|
|
572
|
+
homeDirectory: string,
|
|
573
|
+
which: (name: string) => string | null,
|
|
574
|
+
exists: (path: string) => boolean,
|
|
575
|
+
): readonly string[] {
|
|
576
|
+
const candidates: string[] = [];
|
|
577
|
+
if (platform === "darwin") {
|
|
578
|
+
for (const applicationName of definition.macApplications) {
|
|
579
|
+
candidates.push(
|
|
580
|
+
`/Applications/${applicationName}.app`,
|
|
581
|
+
join(homeDirectory, "Applications", `${applicationName}.app`),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (platform === "linux") candidates.push(...definition.linuxPaths);
|
|
586
|
+
for (const executable of definition.executableNames[platform] ?? []) {
|
|
587
|
+
const path = which(executable);
|
|
588
|
+
if (path !== null) candidates.push(path);
|
|
589
|
+
}
|
|
590
|
+
return [...new Set(candidates.filter((path) => exists(path)))];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function findExecutable(
|
|
594
|
+
name: string,
|
|
595
|
+
commonPaths: readonly string[],
|
|
596
|
+
which: (name: string) => string | null,
|
|
597
|
+
exists: (path: string) => boolean,
|
|
598
|
+
): string | null {
|
|
599
|
+
const fromPath = which(name);
|
|
600
|
+
if (fromPath !== null && exists(fromPath)) return fromPath;
|
|
601
|
+
return commonPaths.find((path) => exists(path)) ?? null;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function commandVersion(
|
|
605
|
+
path: string | null,
|
|
606
|
+
arguments_: readonly string[],
|
|
607
|
+
run: DiagnosticCommandRunner,
|
|
608
|
+
): Promise<string | null> {
|
|
609
|
+
if (path === null) return null;
|
|
610
|
+
try {
|
|
611
|
+
const result = await run({ command: [path, ...arguments_], timeoutMs: 30_000, maxOutputBytes: 128 * 1024 });
|
|
612
|
+
if (result.exitCode !== 0) return null;
|
|
613
|
+
const firstLine = (result.stdout.trim() || result.stderr.trim()).split(/\r?\n/, 1)[0] ?? "";
|
|
614
|
+
return firstLine === "" ? null : firstLine;
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function inspectAgentBrowser(
|
|
621
|
+
agentBrowserDirectory: string | null,
|
|
622
|
+
exists: (path: string) => boolean,
|
|
623
|
+
run: DiagnosticCommandRunner,
|
|
624
|
+
): Promise<{ readonly deriveClient: boolean; readonly profiles: readonly string[] }> {
|
|
625
|
+
if (agentBrowserDirectory === null) return { deriveClient: false, profiles: [] };
|
|
626
|
+
const executable = join(agentBrowserDirectory, "bin", "agent-browser.js");
|
|
627
|
+
if (!exists(executable)) return { deriveClient: false, profiles: [] };
|
|
628
|
+
const directory = mkdtempSync(join(tmpdir(), "hraness-kb-doctor-"));
|
|
629
|
+
const socketRoot = process.platform === "win32" ? tmpdir() : "/tmp";
|
|
630
|
+
const socketDirectory = mkdtempSync(join(socketRoot, "jc-ab-doctor-"));
|
|
631
|
+
chmodSync(directory, 0o700);
|
|
632
|
+
chmodSync(socketDirectory, 0o700);
|
|
633
|
+
const configPath = join(directory, "agent-browser.config.json");
|
|
634
|
+
writeFileSync(configPath, "{}\n", { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
635
|
+
chmodSync(configPath, 0o600);
|
|
636
|
+
const base = [process.execPath, executable, "--config", configPath];
|
|
637
|
+
const isolatedCommand = {
|
|
638
|
+
cwd: directory,
|
|
639
|
+
environment: isolatedAgentBrowserEnvironment(process.env, socketDirectory),
|
|
640
|
+
timeoutMs: 20_000,
|
|
641
|
+
maxOutputBytes: 2 * 1024 * 1024,
|
|
642
|
+
} as const;
|
|
643
|
+
let skillsResult: DiagnosticCommandResult | null;
|
|
644
|
+
let profilesResult: DiagnosticCommandResult | null;
|
|
645
|
+
try {
|
|
646
|
+
[skillsResult, profilesResult] = await Promise.all([
|
|
647
|
+
run({ ...isolatedCommand, command: [...base, "skills", "list", "--json"] }).catch(() => null),
|
|
648
|
+
run({ ...isolatedCommand, command: [...base, "profiles", "--json"] }).catch(() => null),
|
|
649
|
+
]);
|
|
650
|
+
} finally {
|
|
651
|
+
rmSync(socketDirectory, { recursive: true, force: true });
|
|
652
|
+
rmSync(directory, { recursive: true, force: true });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let deriveClient = false;
|
|
656
|
+
if (skillsResult?.exitCode === 0) {
|
|
657
|
+
const parsed = parseJson(skillsResult.stdout);
|
|
658
|
+
if (isRecord(parsed) && parsed.success === true && Array.isArray(parsed.data)) {
|
|
659
|
+
deriveClient = parsed.data.some((entry) => isRecord(entry) && entry.name === "derive-client");
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const profiles = new Set<string>();
|
|
664
|
+
if (profilesResult?.exitCode === 0) {
|
|
665
|
+
const parsed = parseJson(profilesResult.stdout);
|
|
666
|
+
if (isRecord(parsed) && parsed.success === true && Array.isArray(parsed.data)) {
|
|
667
|
+
for (const entry of parsed.data) {
|
|
668
|
+
if (!isRecord(entry) || typeof entry.name !== "string") continue;
|
|
669
|
+
const name = entry.name.trim();
|
|
670
|
+
if (name !== "") profiles.add(name);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { deriveClient, profiles: [...profiles].sort((left, right) => left.localeCompare(right)) };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/** Inspect local clipping capabilities without reading cookies, browser databases, or the system keychain. */
|
|
678
|
+
export async function inspectClipEnvironment(options: DoctorOptions = {}): Promise<DoctorReport> {
|
|
679
|
+
const packageRoot = resolve(options.packageRoot ?? findKbPackageRoot());
|
|
680
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
681
|
+
const platform = options.platform ?? process.platform;
|
|
682
|
+
const architecture = options.architecture ?? process.arch;
|
|
683
|
+
const runtime = options.runtime ?? (process.versions.bun === undefined ? "node" : "bun");
|
|
684
|
+
const exists = options.exists ?? existsSync;
|
|
685
|
+
const readText = options.readText ?? ((path: string) => readFileSync(path, "utf8"));
|
|
686
|
+
const realpath = options.realpath ?? realpathSync;
|
|
687
|
+
const which = options.which ?? ((name: string) => typeof Bun === "undefined" ? null : Bun.which(name));
|
|
688
|
+
const run = options.run ?? runDiagnosticCommand;
|
|
689
|
+
const rootManifest = readJsonRecord(join(packageRoot, "package.json"), readText);
|
|
690
|
+
const dependencies = dependencyNames.map((name) => reportDependency(name, packageRoot, rootManifest, readText));
|
|
691
|
+
const agentBrowserDirectory = installedDependencyDirectory(
|
|
692
|
+
"agent-browser",
|
|
693
|
+
packageRoot,
|
|
694
|
+
readText,
|
|
695
|
+
)?.directory ?? null;
|
|
696
|
+
|
|
697
|
+
const browsers: readonly BrowserReport[] = browserDefinitions.map((definition) => {
|
|
698
|
+
const paths = browserPaths(definition, platform, homeDirectory, which, exists);
|
|
699
|
+
return { name: definition.name, paths, status: paths.length === 0 ? "unavailable" : "ready" };
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const ytDlpPath = findExecutable("yt-dlp", [
|
|
703
|
+
join(homeDirectory, ".local", "bin", "yt-dlp"),
|
|
704
|
+
"/opt/homebrew/bin/yt-dlp",
|
|
705
|
+
"/usr/local/bin/yt-dlp",
|
|
706
|
+
], which, exists);
|
|
707
|
+
const ffmpegPath = findExecutable("ffmpeg", [
|
|
708
|
+
"/opt/homebrew/bin/ffmpeg",
|
|
709
|
+
"/usr/local/bin/ffmpeg",
|
|
710
|
+
], which, exists);
|
|
711
|
+
const pdfinfoPath = findExecutable("pdfinfo", [
|
|
712
|
+
"/opt/homebrew/bin/pdfinfo",
|
|
713
|
+
"/usr/local/bin/pdfinfo",
|
|
714
|
+
"/usr/bin/pdfinfo",
|
|
715
|
+
], which, exists);
|
|
716
|
+
const pdftohtmlPath = findExecutable("pdftohtml", [
|
|
717
|
+
"/opt/homebrew/bin/pdftohtml",
|
|
718
|
+
"/usr/local/bin/pdftohtml",
|
|
719
|
+
"/usr/bin/pdftohtml",
|
|
720
|
+
], which, exists);
|
|
721
|
+
const tesseractPath = findExecutable("tesseract", [
|
|
722
|
+
join(homeDirectory, ".local", "bin", "tesseract"),
|
|
723
|
+
"/opt/homebrew/bin/tesseract",
|
|
724
|
+
"/usr/local/bin/tesseract",
|
|
725
|
+
"/usr/bin/tesseract",
|
|
726
|
+
], which, exists);
|
|
727
|
+
|
|
728
|
+
const [
|
|
729
|
+
agentBrowser,
|
|
730
|
+
ytDlpVersion,
|
|
731
|
+
ffmpegVersion,
|
|
732
|
+
pdfinfoVersion,
|
|
733
|
+
pdftohtmlVersion,
|
|
734
|
+
tesseractVersion,
|
|
735
|
+
] = await Promise.all([
|
|
736
|
+
inspectAgentBrowser(agentBrowserDirectory, exists, run),
|
|
737
|
+
commandVersion(ytDlpPath, ["--version"], run),
|
|
738
|
+
commandVersion(ffmpegPath, ["-version"], run),
|
|
739
|
+
commandVersion(pdfinfoPath, ["-v"], run),
|
|
740
|
+
commandVersion(pdftohtmlPath, ["-v"], run),
|
|
741
|
+
commandVersion(tesseractPath, ["--version"], run),
|
|
742
|
+
]);
|
|
743
|
+
const tools: readonly ToolReport[] = [
|
|
744
|
+
{
|
|
745
|
+
name: "yt-dlp",
|
|
746
|
+
path: ytDlpPath,
|
|
747
|
+
version: ytDlpVersion,
|
|
748
|
+
status: ytDlpPath === null ? "unavailable" : ytDlpVersion === null ? "partial" : "ready",
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
name: "ffmpeg",
|
|
752
|
+
path: ffmpegPath,
|
|
753
|
+
version: ffmpegVersion,
|
|
754
|
+
status: ffmpegPath === null ? "unavailable" : ffmpegVersion === null ? "partial" : "ready",
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
name: "pdfinfo",
|
|
758
|
+
path: pdfinfoPath,
|
|
759
|
+
version: pdfinfoVersion,
|
|
760
|
+
status: pdfinfoPath === null ? "unavailable" : pdfinfoVersion === null ? "partial" : "ready",
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: "pdftohtml",
|
|
764
|
+
path: pdftohtmlPath,
|
|
765
|
+
version: pdftohtmlVersion,
|
|
766
|
+
status: pdftohtmlPath === null ? "unavailable" : pdftohtmlVersion === null ? "partial" : "ready",
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
name: "tesseract",
|
|
770
|
+
path: tesseractPath,
|
|
771
|
+
version: tesseractVersion,
|
|
772
|
+
status: tesseractPath === null ? "unavailable" : tesseractVersion === null ? "partial" : "ready",
|
|
773
|
+
},
|
|
774
|
+
];
|
|
775
|
+
|
|
776
|
+
const warnings: string[] = [];
|
|
777
|
+
const currentBunVersion = options.currentBunVersion ?? (typeof Bun === "undefined" ? "not running under Bun" : Bun.version);
|
|
778
|
+
const search = inspectSearchReadiness(
|
|
779
|
+
dependencies,
|
|
780
|
+
packageRoot,
|
|
781
|
+
runtime,
|
|
782
|
+
platform,
|
|
783
|
+
architecture,
|
|
784
|
+
currentBunVersion,
|
|
785
|
+
exists,
|
|
786
|
+
readText,
|
|
787
|
+
realpath,
|
|
788
|
+
);
|
|
789
|
+
if (currentBunVersion !== expectedBunVersion) {
|
|
790
|
+
warnings.push(`Use Bun ${expectedBunVersion}; current runtime is ${currentBunVersion}.`);
|
|
791
|
+
}
|
|
792
|
+
for (const dependency of dependencies) {
|
|
793
|
+
if (dependency.status === "unavailable") {
|
|
794
|
+
warnings.push(`${dependency.name} ${dependency.expectedVersion} is not installed; reinstall @hraness/kb with Bun.`);
|
|
795
|
+
} else if (dependency.status === "partial") {
|
|
796
|
+
warnings.push(`${dependency.name} must resolve to ${dependency.expectedVersion} for this kb release.`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (!agentBrowser.deriveClient) {
|
|
800
|
+
warnings.push("agent-browser does not expose the derive-client skill; reinstall the pinned dependency before HAR-based client work.");
|
|
801
|
+
}
|
|
802
|
+
const renderedBrowserAvailable = browsers.some(({ name, status }) =>
|
|
803
|
+
(name === "Google Chrome" || name === "Chromium") && status === "ready"
|
|
804
|
+
);
|
|
805
|
+
if (!renderedBrowserAvailable) {
|
|
806
|
+
warnings.push("Install Google Chrome or Chromium for rendered capture; Microsoft Edge can also be used through an explicitly selected CDP session, while Arc remains an explicit cookie or CDP source.");
|
|
807
|
+
}
|
|
808
|
+
if (agentBrowser.profiles.length === 0) {
|
|
809
|
+
warnings.push("No discoverable Chrome profile names were reported; use --browser-live, --cdp, or an explicit profile path when needed.");
|
|
810
|
+
}
|
|
811
|
+
if (ytDlpPath === null) {
|
|
812
|
+
warnings.push("Install yt-dlp to capture default YouTube metadata, thumbnails, and transcripts, and to enable optional --media all downloads.");
|
|
813
|
+
}
|
|
814
|
+
if (ffmpegPath === null) warnings.push("Install ffmpeg for yt-dlp formats that require audio/video merging or remuxing.");
|
|
815
|
+
if (pdfinfoPath === null || pdftohtmlPath === null) {
|
|
816
|
+
const missing = [
|
|
817
|
+
...(pdfinfoPath === null ? ["pdfinfo"] : []),
|
|
818
|
+
...(pdftohtmlPath === null ? ["pdftohtml"] : []),
|
|
819
|
+
];
|
|
820
|
+
warnings.push(`Install Poppler's ${missing.join(" and ")} command${missing.length === 1 ? "" : "s"}; kb pdf requires both pdfinfo and pdftohtml.`);
|
|
821
|
+
} else if (pdfinfoVersion === null || pdftohtmlVersion === null) {
|
|
822
|
+
warnings.push("Poppler was found, but a PDF tool version could not be verified; kb pdf can attempt ingestion with a degraded tool report.");
|
|
823
|
+
}
|
|
824
|
+
if (tesseractPath === null) {
|
|
825
|
+
warnings.push("Install Tesseract to transcribe text in scans and screenshots; kb pdf still preserves native text and images without OCR.");
|
|
826
|
+
} else if (tesseractVersion === null) {
|
|
827
|
+
warnings.push("Tesseract was found, but its version could not be verified; kb pdf can attempt OCR with a degraded tool report.");
|
|
828
|
+
}
|
|
829
|
+
if (search.keywordOnly.status !== "ready") {
|
|
830
|
+
warnings.push("QMD keyword-only search is not ready; exact Markdown search remains available.");
|
|
831
|
+
}
|
|
832
|
+
const keywordFallback = search.keywordOnly.status === "ready"
|
|
833
|
+
? " Keyword-only QMD search remains available."
|
|
834
|
+
: "";
|
|
835
|
+
if (
|
|
836
|
+
runtime === "bun"
|
|
837
|
+
&& platform === "darwin"
|
|
838
|
+
&& search.semanticPrerequisites.sqlite.status !== "ready"
|
|
839
|
+
) {
|
|
840
|
+
warnings.push(`Install Homebrew SQLite with \`brew install sqlite\`; semantic and hybrid vector retrieval are unavailable.${keywordFallback}`);
|
|
841
|
+
} else if (runtime === "node" && search.semanticPrerequisites.sqlite.status !== "ready") {
|
|
842
|
+
warnings.push(`Reinstall @hraness/kb with Node so better-sqlite3 ${expectedBetterSqliteVersion} and its native binding are available.${keywordFallback}`);
|
|
843
|
+
}
|
|
844
|
+
if (search.semanticPrerequisites.sqliteVec.status !== "ready") {
|
|
845
|
+
const nativePackage = search.semanticPrerequisites.sqliteVec.nativePackageName;
|
|
846
|
+
warnings.push(nativePackage === null
|
|
847
|
+
? `sqlite-vec ${expectedSqliteVecVersion} has no native package for ${platform}-${architecture}; semantic and hybrid vector retrieval are unavailable.${keywordFallback}`
|
|
848
|
+
: `Reinstall @hraness/kb so sqlite-vec ${expectedSqliteVecVersion} and ${nativePackage} ${expectedSqliteVecVersion} are installed; semantic and hybrid vector retrieval are unavailable.${keywordFallback}`);
|
|
849
|
+
}
|
|
850
|
+
if (search.semanticPrerequisites.embeddingRuntime.status !== "ready") {
|
|
851
|
+
const nativePackage = search.semanticPrerequisites.embeddingRuntime.nativePackageName;
|
|
852
|
+
warnings.push(nativePackage === null
|
|
853
|
+
? `node-llama-cpp ${expectedNodeLlamaCppVersion} has no prebuilt package for ${platform}-${architecture}; semantic and hybrid vector retrieval require a compatible local embedding runtime.${keywordFallback}`
|
|
854
|
+
: `Reinstall @hraness/kb with Bun so node-llama-cpp ${expectedNodeLlamaCppVersion} and ${nativePackage} ${expectedNodeLlamaCppVersion} with its native binary are installed; semantic and hybrid vector retrieval are not ready.${keywordFallback}`);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
schemaVersion: 2,
|
|
859
|
+
generatedAt: (options.now ?? (() => new Date()))().toISOString(),
|
|
860
|
+
bun: {
|
|
861
|
+
expectedVersion: expectedBunVersion,
|
|
862
|
+
currentVersion: currentBunVersion,
|
|
863
|
+
status: currentBunVersion === expectedBunVersion ? "ready" : "partial",
|
|
864
|
+
},
|
|
865
|
+
dependencies,
|
|
866
|
+
deriveClient: {
|
|
867
|
+
available: agentBrowser.deriveClient,
|
|
868
|
+
status: agentBrowser.deriveClient ? "ready" : "unavailable",
|
|
869
|
+
},
|
|
870
|
+
browsers,
|
|
871
|
+
chromeProfileNames: agentBrowser.profiles,
|
|
872
|
+
tools,
|
|
873
|
+
search,
|
|
874
|
+
warnings,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function versionSummary(report: DependencyReport): string {
|
|
879
|
+
const declared = report.declaredVersion ?? "not declared";
|
|
880
|
+
const installed = report.installedVersion ?? "not installed";
|
|
881
|
+
return `${report.name}: ${report.status} (declared ${declared}; installed ${installed}; expected ${report.expectedVersion})`;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Render a stable, secret-free report suitable for a terminal. */
|
|
885
|
+
export function renderDoctorReport(report: DoctorReport): string {
|
|
886
|
+
const sqlite = report.search.semanticPrerequisites.sqlite;
|
|
887
|
+
const sqliteDetail = sqlite.provider === "homebrew"
|
|
888
|
+
? sqlite.selectedLibraryPath === null
|
|
889
|
+
? `not found (checked ${sqlite.searchedLibraryPaths.join(", ")})`
|
|
890
|
+
: sqlite.selectedLibraryPath
|
|
891
|
+
: sqlite.provider === "better-sqlite3"
|
|
892
|
+
? `${sqlite.installedVersion ?? "not installed"}; native binding ${sqlite.nativeBindingPresent === true ? "present" : "not found"}`
|
|
893
|
+
: "provided by Bun";
|
|
894
|
+
const sqliteVec = report.search.semanticPrerequisites.sqliteVec;
|
|
895
|
+
const nativePackage = sqliteVec.nativePackageName === null
|
|
896
|
+
? `unsupported for ${report.search.platform}-${report.search.architecture}`
|
|
897
|
+
: `${sqliteVec.nativePackageName} ${sqliteVec.nativeInstalledVersion ?? "not installed"}; native binary ${sqliteVec.nativeBinaryPresent === true ? "present" : "not found"}`;
|
|
898
|
+
const embeddingRuntime = report.search.semanticPrerequisites.embeddingRuntime;
|
|
899
|
+
const nativeEmbeddingRuntime = embeddingRuntime.nativePackageName === null
|
|
900
|
+
? `unsupported for ${report.search.platform}-${report.search.architecture}`
|
|
901
|
+
: `${embeddingRuntime.nativePackageName} ${embeddingRuntime.nativeInstalledVersion ?? "not installed"}; native binary ${embeddingRuntime.nativeBinaryPresent === true ? "present" : "not found"}`;
|
|
902
|
+
const lines = [
|
|
903
|
+
"KB environment",
|
|
904
|
+
`Bun: ${report.bun.status} (${report.bun.currentVersion}; expected ${report.bun.expectedVersion})`,
|
|
905
|
+
...report.dependencies.map(versionSummary),
|
|
906
|
+
`agent-browser derive-client: ${report.deriveClient.status}`,
|
|
907
|
+
...report.browsers.map((browser) => `${browser.name}: ${browser.status}${browser.paths.length === 0 ? "" : ` (${browser.paths.join(", ")})`}`),
|
|
908
|
+
`Chrome profiles: ${report.chromeProfileNames.length === 0 ? "none discovered" : report.chromeProfileNames.join(", ")}`,
|
|
909
|
+
...report.tools.map((tool) => `${tool.name}: ${tool.status}${tool.version === null ? "" : ` (${tool.version})`}${tool.path === null ? "" : ` at ${tool.path}`}`),
|
|
910
|
+
`QMD keyword-only search: ${report.search.keywordOnly.status} (embedding model not required)`,
|
|
911
|
+
`Semantic prerequisites: ${report.search.semanticPrerequisites.status}`,
|
|
912
|
+
`SQLite extension support: ${sqlite.status} (${sqlite.provider}: ${sqliteDetail})`,
|
|
913
|
+
`sqlite-vec: ${sqliteVec.status} (installed ${sqliteVec.installedVersion ?? "not installed"}; expected ${sqliteVec.expectedVersion}; ${nativePackage})`,
|
|
914
|
+
`Embedding runtime: ${embeddingRuntime.status} (node-llama-cpp ${embeddingRuntime.installedVersion ?? "not installed"}; expected ${embeddingRuntime.expectedVersion}; ${nativeEmbeddingRuntime})`,
|
|
915
|
+
"Embedding model cache: not inspected; first semantic use may download the configured local model",
|
|
916
|
+
"Cookie/keychain probe: not performed",
|
|
917
|
+
];
|
|
918
|
+
if (report.warnings.length > 0) {
|
|
919
|
+
lines.push("", "Warnings:", ...report.warnings.map((warning) => `- ${warning}`));
|
|
920
|
+
}
|
|
921
|
+
return `${lines.join("\n")}\n`;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
export type AdapterCompleteness = "complete" | "bounded" | "best-effort" | "access-dependent" | "unsupported";
|
|
925
|
+
|
|
926
|
+
export type AdapterCapability = {
|
|
927
|
+
readonly id: Platform | null;
|
|
928
|
+
readonly platform: string;
|
|
929
|
+
readonly preferredModes: readonly string[];
|
|
930
|
+
readonly page: AdapterCompleteness;
|
|
931
|
+
readonly conversations: AdapterCompleteness;
|
|
932
|
+
readonly media: AdapterCompleteness;
|
|
933
|
+
readonly limitations: readonly string[];
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
/** Honest capability matrix: statuses describe capture guarantees, not marketing claims. */
|
|
937
|
+
export const adapterCapabilities: readonly AdapterCapability[] = [
|
|
938
|
+
{
|
|
939
|
+
id: "generic",
|
|
940
|
+
platform: "Generic web",
|
|
941
|
+
preferredModes: ["HTTP + Defuddle", "rendered browser fallback", "saved HTML"],
|
|
942
|
+
page: "best-effort",
|
|
943
|
+
conversations: "best-effort",
|
|
944
|
+
media: "best-effort",
|
|
945
|
+
limitations: [
|
|
946
|
+
"JavaScript-only regions require a browser",
|
|
947
|
+
"visible conversation prose can be retained, but site-specific item trees are not inferred generically",
|
|
948
|
+
],
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
id: "x",
|
|
952
|
+
platform: "X",
|
|
953
|
+
preferredModes: ["rendered Chrome profile/live session", "Defuddle X extractor"],
|
|
954
|
+
page: "best-effort",
|
|
955
|
+
conversations: "best-effort",
|
|
956
|
+
media: "best-effort",
|
|
957
|
+
limitations: ["Only posts and replies loaded into the rendered page are captured", "virtualized or unloaded replies remain partial", "private GraphQL clients are not invoked automatically"],
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
id: "substack",
|
|
961
|
+
platform: "Substack",
|
|
962
|
+
preferredModes: ["HTTP + Defuddle", "authorized rendered session for subscriber pages"],
|
|
963
|
+
page: "access-dependent",
|
|
964
|
+
conversations: "best-effort",
|
|
965
|
+
media: "best-effort",
|
|
966
|
+
limitations: ["Subscriber-only text is captured only when the selected session can already view it", "visible comments are retained as unstructured rendered context with conservative counts", "email/app-only or virtualized comments can be absent"],
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
id: "instagram",
|
|
970
|
+
platform: "Instagram",
|
|
971
|
+
preferredModes: ["authorized rendered session", "yt-dlp for accessible media"],
|
|
972
|
+
page: "best-effort",
|
|
973
|
+
conversations: "best-effort",
|
|
974
|
+
media: "best-effort",
|
|
975
|
+
limitations: ["Generic rendered context; no dedicated item adapter", "Login walls, virtualization, and lazy-loaded comments limit completeness", "private accounts require the user's authorized session"],
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
id: "linkedin",
|
|
979
|
+
platform: "LinkedIn",
|
|
980
|
+
preferredModes: ["authorized rendered session", "saved rendered HTML"],
|
|
981
|
+
page: "best-effort",
|
|
982
|
+
conversations: "best-effort",
|
|
983
|
+
media: "best-effort",
|
|
984
|
+
limitations: ["Generic rendered fallback; no dedicated adapter", "UI changes and collapsed comment branches can reduce completeness", "no automated private API derivation"],
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
id: null,
|
|
988
|
+
platform: "Signed-in pages",
|
|
989
|
+
preferredModes: ["current browser tab", "temporary browser-profile copy", "origin-filtered cookie HTTP fallback"],
|
|
990
|
+
page: "access-dependent",
|
|
991
|
+
conversations: "access-dependent",
|
|
992
|
+
media: "access-dependent",
|
|
993
|
+
limitations: ["Captures what the selected session renders", "virtualized or unloaded regions can remain partial"],
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
id: "hacker-news",
|
|
997
|
+
platform: "Hacker News",
|
|
998
|
+
preferredModes: ["official Firebase API", "rendered/Defuddle fallback"],
|
|
999
|
+
page: "complete",
|
|
1000
|
+
conversations: "bounded",
|
|
1001
|
+
media: "unsupported",
|
|
1002
|
+
limitations: ["Comment count and depth obey CLI bounds", "deleted and dead items remain represented only as exposed by the service"],
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
id: "reddit",
|
|
1006
|
+
platform: "Reddit",
|
|
1007
|
+
preferredModes: ["best-effort public listing JSON", "rendered session", "Defuddle Reddit extractor"],
|
|
1008
|
+
page: "best-effort",
|
|
1009
|
+
conversations: "bounded",
|
|
1010
|
+
media: "best-effort",
|
|
1011
|
+
limitations: ["The unofficial JSON surface can be denied or changed and falls back automatically", "collapsed/deleted branches and configured item/depth bounds remain explicit"],
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
id: "facebook",
|
|
1015
|
+
platform: "Facebook",
|
|
1016
|
+
preferredModes: ["authorized rendered session", "yt-dlp for accessible media"],
|
|
1017
|
+
page: "best-effort",
|
|
1018
|
+
conversations: "best-effort",
|
|
1019
|
+
media: "best-effort",
|
|
1020
|
+
limitations: ["Generic rendered context; no dedicated item adapter", "Only visible, loaded content is captured", "private audiences require the user's authorized session"],
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
id: "tiktok",
|
|
1024
|
+
platform: "TikTok",
|
|
1025
|
+
preferredModes: ["rendered session", "yt-dlp for accessible media"],
|
|
1026
|
+
page: "best-effort",
|
|
1027
|
+
conversations: "best-effort",
|
|
1028
|
+
media: "best-effort",
|
|
1029
|
+
limitations: ["Generic rendered context; no dedicated thread adapter", "Regional/login gates, virtualization, and lazy-loaded comments can reduce completeness", "DRM and access controls are never bypassed"],
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
id: "bluesky",
|
|
1033
|
+
platform: "Bluesky",
|
|
1034
|
+
preferredModes: ["public AT Protocol thread API", "rendered/Defuddle fallback"],
|
|
1035
|
+
page: "complete",
|
|
1036
|
+
conversations: "bounded",
|
|
1037
|
+
media: "best-effort",
|
|
1038
|
+
limitations: ["Thread count and depth obey CLI bounds", "moderation labels and unavailable records are preserved only as exposed"],
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
id: "threads",
|
|
1042
|
+
platform: "Threads",
|
|
1043
|
+
preferredModes: ["current browser tab", "rendered browser profile", "saved rendered HTML"],
|
|
1044
|
+
page: "best-effort",
|
|
1045
|
+
conversations: "best-effort",
|
|
1046
|
+
media: "best-effort",
|
|
1047
|
+
limitations: ["Only rendered posts and replies are retained", "virtualized or unloaded replies remain partial"],
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
id: "whatsapp",
|
|
1051
|
+
platform: "WhatsApp Web",
|
|
1052
|
+
preferredModes: ["current signed-in browser tab", "rendered browser profile"],
|
|
1053
|
+
page: "access-dependent",
|
|
1054
|
+
conversations: "best-effort",
|
|
1055
|
+
media: "best-effort",
|
|
1056
|
+
limitations: ["Only the open, rendered conversation is retained", "older virtualized messages can remain partial"],
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
id: "youtube",
|
|
1060
|
+
platform: "YouTube",
|
|
1061
|
+
preferredModes: ["yt-dlp metadata + thumbnail + transcript", "HTTP + Defuddle", "rendered browser"],
|
|
1062
|
+
page: "best-effort",
|
|
1063
|
+
conversations: "best-effort",
|
|
1064
|
+
media: "best-effort",
|
|
1065
|
+
limitations: [
|
|
1066
|
+
"Transcript capture depends on an available exact-language caption track",
|
|
1067
|
+
"Only loaded comments are retained",
|
|
1068
|
+
"Full audio/video download remains opt-in with --media all",
|
|
1069
|
+
],
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
id: "github",
|
|
1073
|
+
platform: "GitHub issues, pull requests, and discussions",
|
|
1074
|
+
preferredModes: ["Defuddle GitHub extractor", "current signed-in browser tab for private repositories"],
|
|
1075
|
+
page: "best-effort",
|
|
1076
|
+
conversations: "best-effort",
|
|
1077
|
+
media: "best-effort",
|
|
1078
|
+
limitations: ["Only loaded timeline and review items are retained", "collapsed or paginated history can remain partial"],
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
id: "discourse",
|
|
1082
|
+
platform: "Discourse",
|
|
1083
|
+
preferredModes: ["Defuddle Discourse extractor", "rendered browser fallback"],
|
|
1084
|
+
page: "best-effort",
|
|
1085
|
+
conversations: "best-effort",
|
|
1086
|
+
media: "best-effort",
|
|
1087
|
+
limitations: ["Only loaded topic posts are retained", "long or login-gated topics can remain partial"],
|
|
1088
|
+
},
|
|
1089
|
+
];
|
|
1090
|
+
|
|
1091
|
+
export function renderAdapterCapabilities(capabilities: readonly AdapterCapability[] = adapterCapabilities): string {
|
|
1092
|
+
const lines = [
|
|
1093
|
+
"Clip adapters",
|
|
1094
|
+
"Statuses: complete, bounded, best-effort, access-dependent, unsupported.",
|
|
1095
|
+
"",
|
|
1096
|
+
];
|
|
1097
|
+
for (const capability of capabilities) {
|
|
1098
|
+
lines.push(
|
|
1099
|
+
`${capability.platform}: page=${capability.page}, conversations=${capability.conversations}, media=${capability.media}`,
|
|
1100
|
+
` modes: ${capability.preferredModes.join("; ")}`,
|
|
1101
|
+
...capability.limitations.map((limitation) => ` - ${limitation}`),
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
lines.push(
|
|
1105
|
+
"",
|
|
1106
|
+
"Use the current browser tab or a temporary copy of a signed-in browser profile for pages you already have open.",
|
|
1107
|
+
"Capture is ingestion-only: it reads rendered content and does not post, like, follow, send, or submit.",
|
|
1108
|
+
);
|
|
1109
|
+
return `${lines.join("\n")}\n`;
|
|
1110
|
+
}
|