@agentproto/corpus-cli 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/cli.mjs +2607 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +172 -0
- package/dist/index.mjs.map +1 -0
- package/dist/os-identity.adapter-CLBmnRoD.d.ts +83 -0
- package/dist/ports/index.d.ts +531 -0
- package/dist/ports/index.mjs +1129 -0
- package/dist/ports/index.mjs.map +1 -0
- package/package.json +76 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,2607 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, stat, mkdir, writeFile, rename, appendFile, readdir, rm, mkdtemp } from 'fs/promises';
|
|
3
|
+
import os, { tmpdir, homedir } from 'os';
|
|
4
|
+
import { createRequire } from 'module';
|
|
5
|
+
import path, { basename, join, extname, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { randomBytes, randomUUID } from 'crypto';
|
|
9
|
+
import { readFileSync, existsSync } from 'fs';
|
|
10
|
+
import { buildDistillPrompt, parseItems, isRefinedKind, SyncRunner, resolveKnowledge, DistillRunner, systemClock, WebImporter, ImporterRunner, CorpusEventEmitter, CorpusWorkspaceReader, CorpusLinter, CorpusValidator, normalizeLanguageTag } from '@agentproto/corpus';
|
|
11
|
+
import matter2 from 'gray-matter';
|
|
12
|
+
import { execFile } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
import { parseClaudeJsonOutput, spawnWithStdin } from '@agentproto/cli-exec';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @agentproto/corpus-cli v0.1.0-alpha
|
|
18
|
+
* The `corpus` binary — local-topology host for AIP-10 corpora.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
var CORPUS_PRESET_MANIFEST_SCHEMA = "agentproto/corpus-preset/v1";
|
|
22
|
+
var PresetEntrySchema = z.object({
|
|
23
|
+
/** The slug `corpus init <slug>` matches. */
|
|
24
|
+
slug: z.string().regex(/^[a-z][a-z0-9-]*$/),
|
|
25
|
+
/** Path to the entry module, relative to the preset package root. */
|
|
26
|
+
entry: z.string().min(1),
|
|
27
|
+
/** Named export inside `entry` — the CorpusPreset constant. */
|
|
28
|
+
export: z.string().min(1),
|
|
29
|
+
/** Free-form description; shown when listing available slugs. */
|
|
30
|
+
description: z.string().optional()
|
|
31
|
+
}).loose();
|
|
32
|
+
var CorpusPresetManifestSchema = z.object({
|
|
33
|
+
schema: z.literal(CORPUS_PRESET_MANIFEST_SCHEMA),
|
|
34
|
+
presets: z.array(PresetEntrySchema).default([])
|
|
35
|
+
}).loose();
|
|
36
|
+
|
|
37
|
+
// src/registry/preset-loader.ts
|
|
38
|
+
var DEFAULT_PRESET_PACKAGES = ["@agentproto/corpus-presets"];
|
|
39
|
+
async function discoverPresets() {
|
|
40
|
+
const packages = await readConfiguredPresetPackages();
|
|
41
|
+
const out = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const pkg of packages) {
|
|
43
|
+
const result = await readPresetManifest(pkg);
|
|
44
|
+
if (!result) continue;
|
|
45
|
+
const { manifest, packageRoot } = result;
|
|
46
|
+
for (const entry of manifest.presets) {
|
|
47
|
+
out.set(entry.slug, {
|
|
48
|
+
...entry,
|
|
49
|
+
packageName: pkg,
|
|
50
|
+
packageRoot
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
async function loadPreset(slug) {
|
|
57
|
+
const presets = await discoverPresets();
|
|
58
|
+
const entry = presets.get(slug);
|
|
59
|
+
if (!entry) return null;
|
|
60
|
+
const abs = entry.entry.startsWith(".") ? join(entry.packageRoot, entry.entry) : entry.entry;
|
|
61
|
+
const url = entry.entry.startsWith(".") ? pathToFileURL(abs).href : entry.entry;
|
|
62
|
+
const mod = await import(url);
|
|
63
|
+
const exported = mod[entry.export];
|
|
64
|
+
if (!exported || typeof exported !== "object") {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`corpus-presets: '${entry.packageName}' entry '${entry.entry}' has no object export '${entry.export}' (got ${typeof exported}).`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return exported;
|
|
70
|
+
}
|
|
71
|
+
async function readPresetManifest(packageName) {
|
|
72
|
+
const packageJsonPath = resolvePackageJson(packageName);
|
|
73
|
+
if (!packageJsonPath) return null;
|
|
74
|
+
const packageRoot = dirname(packageJsonPath);
|
|
75
|
+
const standalonePath = join(packageRoot, "agentproto-corpus-preset.json");
|
|
76
|
+
const standalone = await readJsonIfExists(standalonePath);
|
|
77
|
+
if (standalone !== void 0) {
|
|
78
|
+
return {
|
|
79
|
+
manifest: CorpusPresetManifestSchema.parse(standalone),
|
|
80
|
+
packageRoot
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const pkgJson = await readJsonIfExists(packageJsonPath);
|
|
84
|
+
const block = pkgJson?.["agentproto-corpus-preset"];
|
|
85
|
+
if (block && typeof block === "object") {
|
|
86
|
+
return {
|
|
87
|
+
manifest: CorpusPresetManifestSchema.parse(block),
|
|
88
|
+
packageRoot
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function resolvePackageJson(packageName) {
|
|
94
|
+
const candidates = [
|
|
95
|
+
import.meta.url,
|
|
96
|
+
pathToFileURL(join(process.cwd(), "package.json")).href
|
|
97
|
+
];
|
|
98
|
+
for (const root of candidates) {
|
|
99
|
+
try {
|
|
100
|
+
return createRequire(root).resolve(`${packageName}/package.json`);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
async function readJsonIfExists(path4) {
|
|
107
|
+
try {
|
|
108
|
+
const raw = await readFile(path4, "utf8");
|
|
109
|
+
return JSON.parse(raw);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err.code === "ENOENT") return void 0;
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function readConfiguredPresetPackages() {
|
|
116
|
+
const base = process.env["AGENTPROTO_HOME"] ?? join(homedir(), ".agentproto");
|
|
117
|
+
const path4 = join(base, "config.json");
|
|
118
|
+
try {
|
|
119
|
+
const raw = await readFile(path4, "utf8");
|
|
120
|
+
const parsed = JSON.parse(raw);
|
|
121
|
+
const list = parsed.corpusPresetPackages;
|
|
122
|
+
if (Array.isArray(list) && list.length > 0) return list;
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
return DEFAULT_PRESET_PACKAGES;
|
|
126
|
+
}
|
|
127
|
+
var NodeFsAdapter = class {
|
|
128
|
+
root;
|
|
129
|
+
constructor(opts) {
|
|
130
|
+
this.root = opts.root;
|
|
131
|
+
}
|
|
132
|
+
async exists(p) {
|
|
133
|
+
try {
|
|
134
|
+
await stat(this.resolve(p));
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async readFile(p) {
|
|
141
|
+
return await readFile(this.resolve(p), "utf8");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Atomic write: write to `${target}.tmp-{nonce}`, fsync, rename.
|
|
145
|
+
* Rename is atomic on the same filesystem. Creates parent dirs as
|
|
146
|
+
* needed (most callers expect mkdir -p semantics).
|
|
147
|
+
*/
|
|
148
|
+
async writeFile(p, content) {
|
|
149
|
+
const target = this.resolve(p);
|
|
150
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
151
|
+
const tmp = `${target}.tmp-${randomBytes(8).toString("hex")}`;
|
|
152
|
+
await writeFile(tmp, content, "utf8");
|
|
153
|
+
await rename(tmp, target);
|
|
154
|
+
}
|
|
155
|
+
async appendFile(p, content) {
|
|
156
|
+
const target = this.resolve(p);
|
|
157
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
158
|
+
await appendFile(target, content, "utf8");
|
|
159
|
+
}
|
|
160
|
+
async readdir(p) {
|
|
161
|
+
const target = this.resolve(p);
|
|
162
|
+
const entries = await readdir(target);
|
|
163
|
+
return entries;
|
|
164
|
+
}
|
|
165
|
+
async walk(p) {
|
|
166
|
+
const out = [];
|
|
167
|
+
const start = this.resolve(p);
|
|
168
|
+
const rootForRelative = start;
|
|
169
|
+
const visit = async (dir) => {
|
|
170
|
+
let entries = [];
|
|
171
|
+
try {
|
|
172
|
+
entries = await readdir(dir);
|
|
173
|
+
} catch {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
for (const ent of entries) {
|
|
177
|
+
if (ent.startsWith(".")) continue;
|
|
178
|
+
const full = path.join(dir, ent);
|
|
179
|
+
const s = await stat(full);
|
|
180
|
+
if (s.isDirectory()) {
|
|
181
|
+
await visit(full);
|
|
182
|
+
} else if (s.isFile()) {
|
|
183
|
+
const rel = path.relative(rootForRelative, full).split(path.sep).join("/");
|
|
184
|
+
out.push(rel);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
await visit(start);
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
async stat(p) {
|
|
192
|
+
try {
|
|
193
|
+
const s = await stat(this.resolve(p));
|
|
194
|
+
return {
|
|
195
|
+
kind: s.isDirectory() ? "directory" : "file",
|
|
196
|
+
bytes: s.isFile() ? s.size : void 0,
|
|
197
|
+
modifiedAt: s.mtime
|
|
198
|
+
};
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Advisory lock via a lockfile. Spins waiting for the lockfile to
|
|
205
|
+
* disappear, then creates it with exclusive flag. Released by
|
|
206
|
+
* deleting it. Single-process safe; multi-process semi-safe (the
|
|
207
|
+
* write is best-effort exclusive — adequate for the corpus's
|
|
208
|
+
* promote-entry transaction which is rare and short-lived).
|
|
209
|
+
*/
|
|
210
|
+
async lock(p) {
|
|
211
|
+
const target = this.resolve(p);
|
|
212
|
+
const lockDir = path.join(path.dirname(target), ".corpus-locks");
|
|
213
|
+
await mkdir(lockDir, { recursive: true });
|
|
214
|
+
const lockFile = path.join(lockDir, path.basename(target) + ".lock");
|
|
215
|
+
const myToken = `${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
216
|
+
while (true) {
|
|
217
|
+
if (!existsSync(lockFile)) {
|
|
218
|
+
try {
|
|
219
|
+
await writeFile(lockFile, myToken, { encoding: "utf8", flag: "wx" });
|
|
220
|
+
const written = await readFile(lockFile, "utf8").catch(() => "");
|
|
221
|
+
if (written === myToken) break;
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
release: async () => {
|
|
229
|
+
try {
|
|
230
|
+
await rm(lockFile, { force: true });
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
237
|
+
/**
|
|
238
|
+
* Resolve a workspace-relative path to an absolute host path.
|
|
239
|
+
* Refuses to climb out of the workspace root (defense against
|
|
240
|
+
* `../../etc/passwd`-style abuse) — the corpus kit's paths must
|
|
241
|
+
* stay inside the workspace.
|
|
242
|
+
*/
|
|
243
|
+
resolve(p) {
|
|
244
|
+
const normalized = path.posix.normalize("/" + p.replace(/^\/+/, ""));
|
|
245
|
+
const rel = normalized.slice(1);
|
|
246
|
+
if (rel.startsWith("..")) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`NodeFsAdapter: path "${p}" escapes the workspace root "${this.root}"`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return path.join(this.root, rel);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// src/scaffold/bare.ts
|
|
256
|
+
function titleCase(name) {
|
|
257
|
+
return name.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
|
|
258
|
+
}
|
|
259
|
+
var ENTRY_KINDS = [
|
|
260
|
+
"principles",
|
|
261
|
+
"patterns",
|
|
262
|
+
"critiques",
|
|
263
|
+
"examples",
|
|
264
|
+
"summaries",
|
|
265
|
+
"timelines"
|
|
266
|
+
];
|
|
267
|
+
function bareManifest(name) {
|
|
268
|
+
const title = titleCase(name);
|
|
269
|
+
return `---
|
|
270
|
+
schema: knowledge.workspace/v1
|
|
271
|
+
name: ${name}
|
|
272
|
+
title: ${title}
|
|
273
|
+
description: >-
|
|
274
|
+
${title} knowledge corpus (AIP-10). Sources are scraped + content-hashed and
|
|
275
|
+
immutable; entries are distilled, curated, mutable claims that cite them.
|
|
276
|
+
This manifest is intentionally domain-agnostic \u2014 tune entityTypes, lints, and
|
|
277
|
+
metadata.corpus to fit the domain.
|
|
278
|
+
version: 1.0.0
|
|
279
|
+
entityTypes:
|
|
280
|
+
- { name: Principle, description: "Durable expert rule" }
|
|
281
|
+
- { name: Example, description: "Concrete source-derived example" }
|
|
282
|
+
- { name: Pattern, description: "Reusable transferable mechanic" }
|
|
283
|
+
- { name: Critique, description: "Negative example or failure mode" }
|
|
284
|
+
- { name: Summary, description: "Compressed source synthesis" }
|
|
285
|
+
- { name: Timeline, description: "Time-sensitive evolution" }
|
|
286
|
+
sources:
|
|
287
|
+
retention: forever
|
|
288
|
+
signing: optional
|
|
289
|
+
hashAlgo: sha256
|
|
290
|
+
authorityDefault: secondary
|
|
291
|
+
lints:
|
|
292
|
+
- { id: require-source-on-examples, kind: require-source, appliesTo: Example, severity: error }
|
|
293
|
+
- { id: broken-ref-all, kind: broken-ref, appliesTo: "*", severity: error }
|
|
294
|
+
- { id: orphan-all, kind: orphan, appliesTo: "*", severity: info }
|
|
295
|
+
curation:
|
|
296
|
+
tone: "neutral, expert, source-backed"
|
|
297
|
+
depth: medium
|
|
298
|
+
autoLink: byName
|
|
299
|
+
conflictResolution: authority
|
|
300
|
+
queryHints:
|
|
301
|
+
preferRecent: true
|
|
302
|
+
preferAuthoritative: true
|
|
303
|
+
metadata:
|
|
304
|
+
corpus:
|
|
305
|
+
# "flat" \u2192 entries/<kind>/<slug>.md (matches the scaffolded dirs).
|
|
306
|
+
# Set "dated" for entries/<kind>/<year>/<slug>.md on high-volume corpora.
|
|
307
|
+
entryLayout: flat
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
# ${title}
|
|
311
|
+
|
|
312
|
+
Neutral AIP-10 knowledge workspace.
|
|
313
|
+
|
|
314
|
+
- \`sources/\` \u2014 scraped source snapshots (immutable provenance: \`captured_at\` + \`content_hash\`)
|
|
315
|
+
- \`entries/<kind>/\` \u2014 distilled, curated claims that cite sources
|
|
316
|
+
- \`collections/\` \u2014 AIP-18 curation collections (candidates, etc.)
|
|
317
|
+
|
|
318
|
+
Populate with \`corpus import-web\` \u2192 \`corpus distill\` \u2192 \`corpus knowledge\`.
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
var STARTER_SURFACES = {
|
|
322
|
+
operators: { dir: "operators", aip: "AIP-9 (OPERATOR.md per operator)" },
|
|
323
|
+
playbooks: { dir: "playbooks", aip: "AIP-12 (PLAYBOOK.md per playbook)" },
|
|
324
|
+
workflows: { dir: "workflows", aip: "AIP-15 (WORKFLOW.md per workflow)" },
|
|
325
|
+
routines: { dir: "routines", aip: "AIP-41 (ROUTINE.md per routine)" }
|
|
326
|
+
};
|
|
327
|
+
function buildBareWorkspace(name, withStarters = []) {
|
|
328
|
+
const files = {
|
|
329
|
+
"KNOWLEDGE.md": bareManifest(name),
|
|
330
|
+
"sources/.gitkeep": "",
|
|
331
|
+
"collections/.gitkeep": ""
|
|
332
|
+
};
|
|
333
|
+
for (const kind of ENTRY_KINDS) files[`entries/${kind}/.gitkeep`] = "";
|
|
334
|
+
for (const s of withStarters) {
|
|
335
|
+
const surface = STARTER_SURFACES[s];
|
|
336
|
+
if (!surface) continue;
|
|
337
|
+
files[`${surface.dir}/.gitkeep`] = "";
|
|
338
|
+
files[`${surface.dir}/README.md`] = `# ${titleCase(s)}
|
|
339
|
+
|
|
340
|
+
Opt-in starter surface. Add one file per item: ${surface.aip}.
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
return files;
|
|
344
|
+
}
|
|
345
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
346
|
+
var __dirname$1 = path.dirname(__filename$1);
|
|
347
|
+
function findSpecsRoot() {
|
|
348
|
+
const candidates = [
|
|
349
|
+
// source-tree (vendored): packages/corpus-cli/src/commands → ts repo's own specs/resources
|
|
350
|
+
path.resolve(__dirname$1, "../../../../specs/resources"),
|
|
351
|
+
// dist-tree (vendored): packages/corpus-cli/dist → ts repo's own specs/resources
|
|
352
|
+
path.resolve(__dirname$1, "../../../specs/resources"),
|
|
353
|
+
// source-tree (sibling agentproto repo): up to projects/agentproto/agentproto/specs/resources
|
|
354
|
+
path.resolve(__dirname$1, "../../../../../agentproto/specs/resources"),
|
|
355
|
+
// dist-tree (sibling agentproto repo): up to projects/agentproto/agentproto/specs/resources
|
|
356
|
+
path.resolve(__dirname$1, "../../../../agentproto/specs/resources"),
|
|
357
|
+
// global install: share/agentproto-specs/resources next to the bin
|
|
358
|
+
path.resolve(__dirname$1, "../share/agentproto-specs/resources")
|
|
359
|
+
];
|
|
360
|
+
for (const c of candidates) {
|
|
361
|
+
try {
|
|
362
|
+
readFileSync(path.join(c, "aip-10", "draft", "KNOWLEDGE.schema.json"));
|
|
363
|
+
return c;
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
throw new Error(
|
|
368
|
+
"corpus-cli: could not locate AgentProto spec schemas. Re-install or set CORPUS_SPECS_ROOT."
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
var SPECS_ROOT = process.env.CORPUS_SPECS_ROOT || findSpecsRoot();
|
|
372
|
+
function loadSchema(aip, doctype) {
|
|
373
|
+
return JSON.parse(
|
|
374
|
+
readFileSync(
|
|
375
|
+
path.join(SPECS_ROOT, `aip-${aip}`, "draft", `${doctype}.schema.json`),
|
|
376
|
+
"utf8"
|
|
377
|
+
)
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
function loadAipSchemaBundle() {
|
|
381
|
+
return {
|
|
382
|
+
schemas: {
|
|
383
|
+
knowledge: loadSchema(10, "KNOWLEDGE"),
|
|
384
|
+
collection: loadSchema(18, "COLLECTION"),
|
|
385
|
+
playbook: loadSchema(12, "PLAYBOOK"),
|
|
386
|
+
operator: loadSchema(9, "OPERATOR"),
|
|
387
|
+
workflow: loadSchema(15, "WORKFLOW"),
|
|
388
|
+
routine: loadSchema(41, "ROUTINE")
|
|
389
|
+
},
|
|
390
|
+
externals: [loadSchema(16, "IO"), loadSchema(17, "RUNNER")]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function resolveWorkspacePath(arg) {
|
|
394
|
+
if (!arg) return process.cwd();
|
|
395
|
+
return path.resolve(process.cwd(), arg);
|
|
396
|
+
}
|
|
397
|
+
function fail(msg4, code = 1) {
|
|
398
|
+
process.stderr.write(`corpus: ${msg4}
|
|
399
|
+
`);
|
|
400
|
+
return code;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/commands/init.ts
|
|
404
|
+
async function runInit(args) {
|
|
405
|
+
const positionals = [];
|
|
406
|
+
let listOnly = false;
|
|
407
|
+
let presetSlug;
|
|
408
|
+
const withStarters = [];
|
|
409
|
+
for (let i = 0; i < args.length; i++) {
|
|
410
|
+
const a = args[i];
|
|
411
|
+
if (a === void 0) continue;
|
|
412
|
+
if (a === "--list" || a === "-l") {
|
|
413
|
+
listOnly = true;
|
|
414
|
+
} else if (a === "--preset" || a === "-p") {
|
|
415
|
+
presetSlug = args[++i];
|
|
416
|
+
} else if (a.startsWith("--preset=")) {
|
|
417
|
+
presetSlug = a.slice("--preset=".length);
|
|
418
|
+
} else if (a === "--with" || a === "-w") {
|
|
419
|
+
withStarters.push(...splitList(args[++i]));
|
|
420
|
+
} else if (a.startsWith("--with=")) {
|
|
421
|
+
withStarters.push(...splitList(a.slice("--with=".length)));
|
|
422
|
+
} else {
|
|
423
|
+
positionals.push(a);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (listOnly) {
|
|
427
|
+
return runList();
|
|
428
|
+
}
|
|
429
|
+
const [name, pathArg] = positionals;
|
|
430
|
+
if (!name) {
|
|
431
|
+
return fail(
|
|
432
|
+
`init requires a <name> argument.
|
|
433
|
+
corpus init <name> [path] # bare AIP-10 scaffold (default)
|
|
434
|
+
corpus init <name> [path] --with operators,playbooks
|
|
435
|
+
corpus init <name> [path] --preset <slug> # seed a full preset
|
|
436
|
+
Run \`corpus init --list\` for available presets.`,
|
|
437
|
+
2
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const unknown = withStarters.filter((s) => !(s in STARTER_SURFACES));
|
|
441
|
+
if (unknown.length) {
|
|
442
|
+
return fail(
|
|
443
|
+
`init: unknown --with surface(s): ${unknown.join(", ")}. Known: ${Object.keys(STARTER_SURFACES).join(", ")}.`,
|
|
444
|
+
2
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const target = resolveWorkspacePath(pathArg);
|
|
448
|
+
const fs = new NodeFsAdapter({ root: target });
|
|
449
|
+
if (await fs.exists("KNOWLEDGE.md")) {
|
|
450
|
+
return fail(
|
|
451
|
+
`init: refusing to overwrite an existing workspace at ${target}. Delete KNOWLEDGE.md first if you really want to re-init.`,
|
|
452
|
+
1
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
if (presetSlug) {
|
|
456
|
+
const preset = await loadPreset(presetSlug);
|
|
457
|
+
if (!preset) {
|
|
458
|
+
const available = await formatAvailable();
|
|
459
|
+
return fail(
|
|
460
|
+
`init: preset "${presetSlug}" not found in any configured package.
|
|
461
|
+
${available}`,
|
|
462
|
+
2
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
let count2 = 0;
|
|
466
|
+
for (const [rel, content] of Object.entries(preset.files)) {
|
|
467
|
+
await fs.writeFile(rel, content);
|
|
468
|
+
count2++;
|
|
469
|
+
}
|
|
470
|
+
if (preset.bootstrap) {
|
|
471
|
+
await preset.bootstrap({
|
|
472
|
+
workspacePath: "",
|
|
473
|
+
write: (rel, content) => fs.writeFile(rel, content)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
process.stdout.write(
|
|
477
|
+
`corpus: initialized "${preset.slug}" preset (${preset.title}) at ${target}
|
|
478
|
+
${count2} files written
|
|
479
|
+
Try: corpus validate ${pathArg ?? "."}
|
|
480
|
+
corpus lint ${pathArg ?? "."}
|
|
481
|
+
`
|
|
482
|
+
);
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
const files = buildBareWorkspace(name, withStarters);
|
|
486
|
+
let count = 0;
|
|
487
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
488
|
+
await fs.writeFile(rel, content);
|
|
489
|
+
count++;
|
|
490
|
+
}
|
|
491
|
+
const withNote = withStarters.length ? ` (+ starters: ${withStarters.join(", ")})` : "";
|
|
492
|
+
process.stdout.write(
|
|
493
|
+
`corpus: initialized bare corpus "${name}"${withNote} at ${target}
|
|
494
|
+
${count} files written \u2014 neutral KNOWLEDGE.md + AIP-10 structure
|
|
495
|
+
Try: corpus import-web ${pathArg ?? "."} --urls-file urls.txt
|
|
496
|
+
corpus validate ${pathArg ?? "."}
|
|
497
|
+
`
|
|
498
|
+
);
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
function splitList(v) {
|
|
502
|
+
return (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
503
|
+
}
|
|
504
|
+
async function runList() {
|
|
505
|
+
const presets = await discoverPresets();
|
|
506
|
+
if (presets.size === 0) {
|
|
507
|
+
process.stdout.write(
|
|
508
|
+
"corpus init: no presets discovered. Configure corpusPresetPackages[] in ~/.agentproto/config.json or install @agentproto/corpus-presets.\n"
|
|
509
|
+
);
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
for (const entry of presets.values()) {
|
|
513
|
+
process.stdout.write(`\u2022 ${entry.slug} (${entry.packageName})
|
|
514
|
+
`);
|
|
515
|
+
if (entry.description) {
|
|
516
|
+
process.stdout.write(` ${entry.description}
|
|
517
|
+
`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
async function formatAvailable() {
|
|
523
|
+
const presets = await discoverPresets();
|
|
524
|
+
if (presets.size === 0) {
|
|
525
|
+
return "No presets discovered. Configure corpusPresetPackages[] in ~/.agentproto/config.json or install @agentproto/corpus-presets.";
|
|
526
|
+
}
|
|
527
|
+
const slugs = [...presets.keys()].sort();
|
|
528
|
+
return `Available slugs: ${slugs.join(", ")} (run \`corpus init --list\` for details).`;
|
|
529
|
+
}
|
|
530
|
+
async function runValidate(args) {
|
|
531
|
+
const target = resolveWorkspacePath(args[0]);
|
|
532
|
+
const fs = new NodeFsAdapter({ root: target });
|
|
533
|
+
if (!await fs.exists("KNOWLEDGE.md")) {
|
|
534
|
+
return fail(
|
|
535
|
+
`validate: no KNOWLEDGE.md at ${target}. Run \`corpus init marketing\` to scaffold one.`,
|
|
536
|
+
1
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
const snapshot = await new CorpusWorkspaceReader({ fs }).read("");
|
|
540
|
+
const validator = new CorpusValidator({ bundle: loadAipSchemaBundle() });
|
|
541
|
+
const result = validator.validateWorkspace(snapshot);
|
|
542
|
+
const counts = countBucket(snapshot);
|
|
543
|
+
process.stdout.write(
|
|
544
|
+
`corpus: scanned ${counts.total} files (${counts.summary})
|
|
545
|
+
`
|
|
546
|
+
);
|
|
547
|
+
if (result.issues.length === 0) {
|
|
548
|
+
process.stdout.write("corpus: all files conform to AIP schemas \u2713\n");
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
const errors = result.issues.filter((i) => i.severity === "error");
|
|
552
|
+
const warns = result.issues.filter((i) => i.severity === "warn");
|
|
553
|
+
const infos = result.issues.filter((i) => i.severity === "info");
|
|
554
|
+
for (const i of result.issues) {
|
|
555
|
+
const tag = i.severity === "error" ? "ERROR" : i.severity === "warn" ? "WARN " : "INFO ";
|
|
556
|
+
process.stdout.write(` ${tag} ${i.path}${i.instancePath}: ${i.message}
|
|
557
|
+
`);
|
|
558
|
+
}
|
|
559
|
+
process.stdout.write(
|
|
560
|
+
`corpus: ${errors.length} error${errors.length === 1 ? "" : "s"}, ${warns.length} warning${warns.length === 1 ? "" : "s"}, ${infos.length} info
|
|
561
|
+
`
|
|
562
|
+
);
|
|
563
|
+
return result.valid ? 0 : 1;
|
|
564
|
+
}
|
|
565
|
+
function countBucket(s) {
|
|
566
|
+
const parts = [];
|
|
567
|
+
if (s.workspace) parts.push("1 workspace");
|
|
568
|
+
if (s.sources.length) parts.push(`${s.sources.length} sources`);
|
|
569
|
+
if (s.entries.length) parts.push(`${s.entries.length} entries`);
|
|
570
|
+
if (s.collections.length) parts.push(`${s.collections.length} collections`);
|
|
571
|
+
if (s.collectionItems.length)
|
|
572
|
+
parts.push(`${s.collectionItems.length} items`);
|
|
573
|
+
if (s.playbooks.length) parts.push(`${s.playbooks.length} playbooks`);
|
|
574
|
+
if (s.operators.length) parts.push(`${s.operators.length} operators`);
|
|
575
|
+
if (s.workflows.length) parts.push(`${s.workflows.length} workflows`);
|
|
576
|
+
if (s.routines.length) parts.push(`${s.routines.length} routines`);
|
|
577
|
+
if (s.unknown.length) parts.push(`${s.unknown.length} unknown`);
|
|
578
|
+
const total = (s.workspace ? 1 : 0) + s.sources.length + s.entries.length + s.collections.length + s.collectionItems.length + s.playbooks.length + s.operators.length + s.workflows.length + s.routines.length + s.unknown.length;
|
|
579
|
+
return { total, summary: parts.join(", ") };
|
|
580
|
+
}
|
|
581
|
+
async function runLint(args) {
|
|
582
|
+
const target = resolveWorkspacePath(args[0]);
|
|
583
|
+
const fs = new NodeFsAdapter({ root: target });
|
|
584
|
+
if (!await fs.exists("KNOWLEDGE.md")) {
|
|
585
|
+
return fail(
|
|
586
|
+
`lint: no KNOWLEDGE.md at ${target}. Run \`corpus init marketing\` to scaffold one.`,
|
|
587
|
+
1
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
const snapshot = await new CorpusWorkspaceReader({ fs }).read("");
|
|
591
|
+
const report = new CorpusLinter({ clock: systemClock }).lint(snapshot);
|
|
592
|
+
if (report.issues.length === 0) {
|
|
593
|
+
process.stdout.write("corpus: lint clean \u2713\n");
|
|
594
|
+
return 0;
|
|
595
|
+
}
|
|
596
|
+
for (const i of report.issues) {
|
|
597
|
+
const tag = i.severity === "error" ? "ERROR" : i.severity === "warn" ? "WARN " : "INFO ";
|
|
598
|
+
process.stdout.write(` ${tag} [${i.lintId}] ${i.path}: ${i.message}
|
|
599
|
+
`);
|
|
600
|
+
}
|
|
601
|
+
process.stdout.write(
|
|
602
|
+
`corpus: ${report.errorCount} error${report.errorCount === 1 ? "" : "s"}, ${report.warnCount} warning${report.warnCount === 1 ? "" : "s"}, ${report.infoCount} info
|
|
603
|
+
`
|
|
604
|
+
);
|
|
605
|
+
return report.errorCount > 0 ? 1 : 0;
|
|
606
|
+
}
|
|
607
|
+
var OsIdentityAdapter = class {
|
|
608
|
+
cached;
|
|
609
|
+
constructor(opts) {
|
|
610
|
+
const user = opts.userOverride ?? process.env.USER ?? process.env.USERNAME ?? safeGetUser() ?? "anonymous";
|
|
611
|
+
const wsName = path.basename(opts.workspaceRoot) || "default";
|
|
612
|
+
this.cached = Object.freeze({
|
|
613
|
+
principal: `ws://users/${slugify(user)}`,
|
|
614
|
+
identityTree: Object.freeze([
|
|
615
|
+
`ws://users/${slugify(user)}`,
|
|
616
|
+
`ws://workspaces/${slugify(wsName)}`
|
|
617
|
+
]),
|
|
618
|
+
displayName: user
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
async resolve() {
|
|
622
|
+
return this.cached;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
function safeGetUser() {
|
|
626
|
+
try {
|
|
627
|
+
return os.userInfo().username;
|
|
628
|
+
} catch {
|
|
629
|
+
return void 0;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function slugify(s) {
|
|
633
|
+
return s.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 64) || "anonymous";
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/commands/events.ts
|
|
637
|
+
var KNOWN_EVENT_KINDS = /* @__PURE__ */ new Set([
|
|
638
|
+
"corpus.candidate.discovered",
|
|
639
|
+
"corpus.candidate.analyzed",
|
|
640
|
+
"corpus.candidate.approved",
|
|
641
|
+
"corpus.candidate.rejected",
|
|
642
|
+
"corpus.entry.promoted",
|
|
643
|
+
"corpus.entry.deprecated",
|
|
644
|
+
"corpus.entry.archived",
|
|
645
|
+
"corpus.gap.opened",
|
|
646
|
+
"corpus.gap.resolved",
|
|
647
|
+
"playbook.shadow.registered",
|
|
648
|
+
"playbook.activated",
|
|
649
|
+
"playbook.archived"
|
|
650
|
+
]);
|
|
651
|
+
async function runEventsEmit(args) {
|
|
652
|
+
const positionals = [];
|
|
653
|
+
let payloadStr;
|
|
654
|
+
for (let i = 0; i < args.length; i++) {
|
|
655
|
+
const a = args[i];
|
|
656
|
+
if (a === "--payload") {
|
|
657
|
+
payloadStr = args[++i];
|
|
658
|
+
} else if (a.startsWith("--payload=")) {
|
|
659
|
+
payloadStr = a.slice("--payload=".length);
|
|
660
|
+
} else if (a.startsWith("--")) {
|
|
661
|
+
return fail(`events:emit: unknown flag ${a}`, 2);
|
|
662
|
+
} else {
|
|
663
|
+
positionals.push(a);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const [kind, pathArg] = positionals;
|
|
667
|
+
if (!kind) {
|
|
668
|
+
return fail(
|
|
669
|
+
'events:emit requires a <kind> argument. Try `corpus events:emit corpus.entry.promoted --payload \'{"slug":"foo"}\'`.',
|
|
670
|
+
2
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
if (!KNOWN_EVENT_KINDS.has(kind)) {
|
|
674
|
+
process.stderr.write(
|
|
675
|
+
`corpus: warning \u2014 "${kind}" is not in the standard event taxonomy. Emitting anyway.
|
|
676
|
+
`
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (!payloadStr) {
|
|
680
|
+
return fail(
|
|
681
|
+
"events:emit requires --payload <json>. Pass at least `--payload '{}'`.",
|
|
682
|
+
2
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
let payload;
|
|
686
|
+
try {
|
|
687
|
+
payload = JSON.parse(payloadStr);
|
|
688
|
+
} catch (e) {
|
|
689
|
+
return fail(
|
|
690
|
+
`events:emit: --payload must be valid JSON (${e instanceof Error ? e.message : String(e)})`,
|
|
691
|
+
2
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
695
|
+
return fail("events:emit: --payload must be a JSON object", 2);
|
|
696
|
+
}
|
|
697
|
+
const target = resolveWorkspacePath(pathArg);
|
|
698
|
+
const fs = new NodeFsAdapter({ root: target });
|
|
699
|
+
if (!await fs.exists("KNOWLEDGE.md")) {
|
|
700
|
+
return fail(
|
|
701
|
+
`events:emit: no KNOWLEDGE.md at ${target}. Run \`corpus init marketing\` first.`,
|
|
702
|
+
1
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
const emitter = new CorpusEventEmitter({
|
|
706
|
+
fs,
|
|
707
|
+
clock: systemClock,
|
|
708
|
+
identity: new OsIdentityAdapter({ workspaceRoot: target }),
|
|
709
|
+
workspaceRoot: ""
|
|
710
|
+
});
|
|
711
|
+
const event = await emitter.emit(kind, payload);
|
|
712
|
+
process.stdout.write(
|
|
713
|
+
`corpus: emitted ${event.kind} at ${event.at} by ${event.actor}
|
|
714
|
+
`
|
|
715
|
+
);
|
|
716
|
+
return 0;
|
|
717
|
+
}
|
|
718
|
+
async function runEventsTail(args) {
|
|
719
|
+
const target = resolveWorkspacePath(args[0]);
|
|
720
|
+
const fs = new NodeFsAdapter({ root: target });
|
|
721
|
+
if (!await fs.exists("_log.md")) {
|
|
722
|
+
process.stdout.write(
|
|
723
|
+
`corpus: _log.md does not exist yet at ${target}. Emit an event to create it.
|
|
724
|
+
`
|
|
725
|
+
);
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
728
|
+
const content = await fs.readFile("_log.md");
|
|
729
|
+
process.stdout.write(content);
|
|
730
|
+
if (!content.endsWith("\n")) process.stdout.write("\n");
|
|
731
|
+
return 0;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/ports/video-hosts.ts
|
|
735
|
+
var VIDEO_HOSTS = /* @__PURE__ */ new Set([
|
|
736
|
+
"youtube.com",
|
|
737
|
+
"www.youtube.com",
|
|
738
|
+
"m.youtube.com",
|
|
739
|
+
"music.youtube.com",
|
|
740
|
+
"youtu.be",
|
|
741
|
+
"vimeo.com"
|
|
742
|
+
]);
|
|
743
|
+
function isVideoUrl(url, extraHosts) {
|
|
744
|
+
let host;
|
|
745
|
+
try {
|
|
746
|
+
host = new URL(url).hostname;
|
|
747
|
+
} catch {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
const bare = host.replace(/^www\./, "");
|
|
751
|
+
return VIDEO_HOSTS.has(host) || VIDEO_HOSTS.has(bare) || (extraHosts?.has(host) ?? false) || (extraHosts?.has(bare) ?? false);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/ports/browser-fetcher.adapter.ts
|
|
755
|
+
var FETCHED_PAYLOAD = z.object({
|
|
756
|
+
title: z.string(),
|
|
757
|
+
text: z.string(),
|
|
758
|
+
kind: z.enum(["video", "article", "page", "unknown"]).catch("unknown"),
|
|
759
|
+
language: z.string().optional().catch(void 0),
|
|
760
|
+
via: z.string().optional().catch(void 0)
|
|
761
|
+
}).loose();
|
|
762
|
+
var MCP_TEXT_BLOCK = z.object({ type: z.literal("text"), text: z.string() }).loose();
|
|
763
|
+
var READABILITY_FN = `() => {
|
|
764
|
+
const title = document.title || "";
|
|
765
|
+
const article = document.querySelector("article") || document.querySelector("main");
|
|
766
|
+
const root = article || document.body;
|
|
767
|
+
const text = (root?.innerText || "").replace(/\\n{3,}/g, "\\n\\n").trim().slice(0, 200000);
|
|
768
|
+
const lang = document.documentElement.getAttribute("lang") || undefined;
|
|
769
|
+
return { title, text, kind: "article", language: lang, via: "readability" };
|
|
770
|
+
}`;
|
|
771
|
+
var BrowserMcpFetcher = class {
|
|
772
|
+
browser;
|
|
773
|
+
navigateTool;
|
|
774
|
+
evaluateTool;
|
|
775
|
+
constructor(opts) {
|
|
776
|
+
this.browser = opts.browser;
|
|
777
|
+
this.navigateTool = opts.navigateToolName ?? "navigate_page";
|
|
778
|
+
this.evaluateTool = opts.evaluateToolName ?? "evaluate_script";
|
|
779
|
+
}
|
|
780
|
+
async fetch(url) {
|
|
781
|
+
if (isVideoUrl(url)) return null;
|
|
782
|
+
await this.browser.callTool({
|
|
783
|
+
name: this.navigateTool,
|
|
784
|
+
arguments: { url }
|
|
785
|
+
});
|
|
786
|
+
const result = await this.browser.callTool({
|
|
787
|
+
name: this.evaluateTool,
|
|
788
|
+
arguments: { function: READABILITY_FN }
|
|
789
|
+
});
|
|
790
|
+
const extracted = parseEvaluateResult(result);
|
|
791
|
+
if (!extracted || !extracted.text.trim()) return null;
|
|
792
|
+
return extracted;
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
function parseEvaluateResult(result) {
|
|
796
|
+
return coerceFetched(result.structuredContent) ?? coerceFetched(extractText(result.content));
|
|
797
|
+
}
|
|
798
|
+
function coerceFetched(value) {
|
|
799
|
+
let obj = value;
|
|
800
|
+
if (typeof obj === "string") {
|
|
801
|
+
try {
|
|
802
|
+
obj = JSON.parse(obj);
|
|
803
|
+
} catch {
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const parsed = FETCHED_PAYLOAD.safeParse(obj);
|
|
808
|
+
if (!parsed.success) return null;
|
|
809
|
+
const { title, text, kind, language, via } = parsed.data;
|
|
810
|
+
return {
|
|
811
|
+
title,
|
|
812
|
+
text,
|
|
813
|
+
kind,
|
|
814
|
+
...language ? { language } : {},
|
|
815
|
+
...via ? { via } : {}
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
function extractText(content) {
|
|
819
|
+
if (typeof content === "string") return content;
|
|
820
|
+
if (Array.isArray(content)) {
|
|
821
|
+
for (const block of content) {
|
|
822
|
+
const parsed = MCP_TEXT_BLOCK.safeParse(block);
|
|
823
|
+
if (parsed.success) return parsed.data.text;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
var SCRAPE_PAYLOAD = z.object({
|
|
829
|
+
title: z.string().optional().catch(void 0),
|
|
830
|
+
markdown: z.string().optional().catch(void 0),
|
|
831
|
+
html: z.string().optional().catch(void 0),
|
|
832
|
+
language: z.string().optional().catch(void 0),
|
|
833
|
+
tierUsed: z.union([z.string(), z.number()]).optional().catch(void 0)
|
|
834
|
+
}).loose();
|
|
835
|
+
var MCP_TEXT_BLOCK2 = z.object({ type: z.literal("text"), text: z.string() }).loose();
|
|
836
|
+
var ScrapeMcpFetcher = class {
|
|
837
|
+
client;
|
|
838
|
+
toolName;
|
|
839
|
+
constructor(opts) {
|
|
840
|
+
this.client = opts.client;
|
|
841
|
+
this.toolName = opts.scrapeToolName ?? "scrape";
|
|
842
|
+
}
|
|
843
|
+
async fetch(url) {
|
|
844
|
+
if (isVideoUrl(url)) return null;
|
|
845
|
+
let res;
|
|
846
|
+
try {
|
|
847
|
+
res = await this.client.callTool({
|
|
848
|
+
name: this.toolName,
|
|
849
|
+
arguments: { url }
|
|
850
|
+
});
|
|
851
|
+
} catch {
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
if (res.isError) return null;
|
|
855
|
+
const payload = extractPayload(res);
|
|
856
|
+
if (!payload) return null;
|
|
857
|
+
const text = (payload.markdown?.trim() || stripTags(payload.html)).trim();
|
|
858
|
+
if (!text) return null;
|
|
859
|
+
return {
|
|
860
|
+
title: payload.title?.trim() || url,
|
|
861
|
+
text,
|
|
862
|
+
kind: "article",
|
|
863
|
+
...payload.language ? { language: payload.language } : {},
|
|
864
|
+
via: payload.tierUsed ? `scrape:${payload.tierUsed}` : "scrape"
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
function extractPayload(res) {
|
|
869
|
+
if (res.structuredContent && typeof res.structuredContent === "object") {
|
|
870
|
+
const parsed = SCRAPE_PAYLOAD.safeParse(res.structuredContent);
|
|
871
|
+
if (parsed.success) return parsed.data;
|
|
872
|
+
}
|
|
873
|
+
if (Array.isArray(res.content)) {
|
|
874
|
+
for (const block of res.content) {
|
|
875
|
+
const text = MCP_TEXT_BLOCK2.safeParse(block);
|
|
876
|
+
if (!text.success) continue;
|
|
877
|
+
try {
|
|
878
|
+
const parsed = SCRAPE_PAYLOAD.safeParse(JSON.parse(text.data.text));
|
|
879
|
+
if (parsed.success) return parsed.data;
|
|
880
|
+
} catch {
|
|
881
|
+
return { markdown: text.data.text };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
function stripTags(html) {
|
|
888
|
+
if (!html) return "";
|
|
889
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
890
|
+
}
|
|
891
|
+
var execFileAsync = promisify(execFile);
|
|
892
|
+
var YTDLP_INFO = z.object({ title: z.string().optional(), language: z.string().optional() }).loose();
|
|
893
|
+
var YtDlpWhisperFetcher = class {
|
|
894
|
+
stt;
|
|
895
|
+
download;
|
|
896
|
+
extraHosts;
|
|
897
|
+
constructor(opts) {
|
|
898
|
+
this.stt = opts.stt;
|
|
899
|
+
this.download = opts.download ?? defaultYtDlpDownloader(opts.ytDlpBin ?? "yt-dlp", {
|
|
900
|
+
maxDurationSec: opts.maxDurationSec,
|
|
901
|
+
cookiesFromBrowser: opts.cookiesFromBrowser,
|
|
902
|
+
cookiesFile: opts.cookiesFile
|
|
903
|
+
});
|
|
904
|
+
this.extraHosts = opts.extraVideoHosts ? new Set(opts.extraVideoHosts) : void 0;
|
|
905
|
+
}
|
|
906
|
+
async fetch(url) {
|
|
907
|
+
if (!isVideoUrl(url, this.extraHosts)) return null;
|
|
908
|
+
let dl;
|
|
909
|
+
try {
|
|
910
|
+
dl = await this.download(url);
|
|
911
|
+
} catch (e) {
|
|
912
|
+
process.stderr.write(`corpus: download failed for ${url} \u2014 skipped (${msg(e)})
|
|
913
|
+
`);
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const t = await this.stt.transcribe(dl.audioPath);
|
|
918
|
+
if (!t.text.trim()) return null;
|
|
919
|
+
return {
|
|
920
|
+
title: dl.title || url,
|
|
921
|
+
text: t.text,
|
|
922
|
+
kind: "video",
|
|
923
|
+
...t.language ?? dl.language ? { language: t.language ?? dl.language } : {},
|
|
924
|
+
via: "transcription"
|
|
925
|
+
};
|
|
926
|
+
} catch (e) {
|
|
927
|
+
if (isAuthError(e)) throw e;
|
|
928
|
+
process.stderr.write(
|
|
929
|
+
`corpus: transcription failed for ${url} \u2014 skipped (${msg(e)})
|
|
930
|
+
`
|
|
931
|
+
);
|
|
932
|
+
return null;
|
|
933
|
+
} finally {
|
|
934
|
+
await dl.cleanup().catch(() => {
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
function msg(e) {
|
|
940
|
+
return e instanceof Error ? e.message : String(e);
|
|
941
|
+
}
|
|
942
|
+
function isAuthError(e) {
|
|
943
|
+
const m = msg(e).toLowerCase();
|
|
944
|
+
return m.includes(" 401") || m.includes(" 403") || m.includes("unauthorized") || m.includes("forbidden") || m.includes("api key") || m.includes("api_key");
|
|
945
|
+
}
|
|
946
|
+
function defaultYtDlpDownloader(bin, opts) {
|
|
947
|
+
return async (url) => {
|
|
948
|
+
const dir = await mkdtemp(join(tmpdir(), "corpus-ytdlp-"));
|
|
949
|
+
const cleanup = () => rm(dir, { recursive: true, force: true });
|
|
950
|
+
try {
|
|
951
|
+
const durationGuard = opts?.maxDurationSec && opts.maxDurationSec > 0 ? ["--match-filter", `duration <= ${Math.floor(opts.maxDurationSec)}`] : [];
|
|
952
|
+
const cookieArgs = opts?.cookiesFromBrowser ? ["--cookies-from-browser", opts.cookiesFromBrowser] : opts?.cookiesFile ? ["--cookies", opts.cookiesFile] : [];
|
|
953
|
+
await execFileAsync(
|
|
954
|
+
bin,
|
|
955
|
+
[
|
|
956
|
+
"-f",
|
|
957
|
+
"bestaudio",
|
|
958
|
+
"-x",
|
|
959
|
+
"--audio-format",
|
|
960
|
+
"mp3",
|
|
961
|
+
"--audio-quality",
|
|
962
|
+
"64K",
|
|
963
|
+
"--no-playlist",
|
|
964
|
+
"--no-progress",
|
|
965
|
+
...durationGuard,
|
|
966
|
+
...cookieArgs,
|
|
967
|
+
"--write-info-json",
|
|
968
|
+
"-o",
|
|
969
|
+
join(dir, "track.%(ext)s"),
|
|
970
|
+
url
|
|
971
|
+
],
|
|
972
|
+
{ maxBuffer: 16 * 1024 * 1024 }
|
|
973
|
+
);
|
|
974
|
+
const files = await readdir(dir);
|
|
975
|
+
const audio = files.find((f) => f === "track.mp3") ?? files.find((f) => /\.(mp3|m4a|opus|webm|ogg|wav)$/i.test(f));
|
|
976
|
+
if (!audio) throw new Error("yt-dlp produced no audio file");
|
|
977
|
+
let title = url;
|
|
978
|
+
let language;
|
|
979
|
+
const infoName = files.find((f) => f.endsWith(".info.json"));
|
|
980
|
+
if (infoName) {
|
|
981
|
+
try {
|
|
982
|
+
const info = YTDLP_INFO.parse(
|
|
983
|
+
JSON.parse(await readFile(join(dir, infoName), "utf-8"))
|
|
984
|
+
);
|
|
985
|
+
if (info.title) title = info.title;
|
|
986
|
+
if (info.language) language = info.language;
|
|
987
|
+
} catch {
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
audioPath: join(dir, audio),
|
|
992
|
+
title,
|
|
993
|
+
...language ? { language } : {},
|
|
994
|
+
cleanup
|
|
995
|
+
};
|
|
996
|
+
} catch (e) {
|
|
997
|
+
await cleanup().catch(() => {
|
|
998
|
+
});
|
|
999
|
+
throw e;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
var WHISPER_RESPONSE = z.object({ text: z.string().optional(), language: z.string().optional() }).loose();
|
|
1004
|
+
var WHISPER_MAX_BYTES = 25 * 1024 * 1024;
|
|
1005
|
+
var OpenAiWhisperStt = class {
|
|
1006
|
+
apiKey;
|
|
1007
|
+
model;
|
|
1008
|
+
baseUrl;
|
|
1009
|
+
constructor(opts) {
|
|
1010
|
+
this.apiKey = opts.apiKey;
|
|
1011
|
+
this.model = opts.model ?? "whisper-1";
|
|
1012
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
1013
|
+
}
|
|
1014
|
+
async transcribe(audioPath) {
|
|
1015
|
+
const bytes = await readFile(audioPath);
|
|
1016
|
+
if (bytes.byteLength > WHISPER_MAX_BYTES) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
`audio ${basename(audioPath)} is ${(bytes.byteLength / 1024 / 1024).toFixed(1)} MB \u2014 over Whisper's 25 MB cap. Lower the bitrate or chunk the media.`
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
const form = new FormData();
|
|
1022
|
+
form.append("file", new Blob([bytes]), basename(audioPath));
|
|
1023
|
+
form.append("model", this.model);
|
|
1024
|
+
form.append("response_format", "verbose_json");
|
|
1025
|
+
const res = await this.postWithRetry(form);
|
|
1026
|
+
if (!res.ok) {
|
|
1027
|
+
const body = await res.text().catch(() => "");
|
|
1028
|
+
throw new Error(`Whisper STT ${res.status} ${res.statusText}: ${body.slice(0, 200)}`);
|
|
1029
|
+
}
|
|
1030
|
+
const data = WHISPER_RESPONSE.parse(await res.json());
|
|
1031
|
+
const language = normalizeLanguageTag(data.language);
|
|
1032
|
+
return {
|
|
1033
|
+
text: (data.text ?? "").trim(),
|
|
1034
|
+
...language ? { language } : {}
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* POST the audio with bounded retry. Transcribing a long talk fans out
|
|
1039
|
+
* into many segment uploads (see ChunkedStt), so a single transient
|
|
1040
|
+
* network blip ("fetch failed") or a 429 / 5xx must not abort the whole
|
|
1041
|
+
* batch. Network errors and retryable statuses back off and retry;
|
|
1042
|
+
* client errors (401 auth, 400 bad request) return immediately so a real
|
|
1043
|
+
* misconfiguration still surfaces loudly.
|
|
1044
|
+
*/
|
|
1045
|
+
async postWithRetry(form, attempts = 4) {
|
|
1046
|
+
const url = `${this.baseUrl}/audio/transcriptions`;
|
|
1047
|
+
let lastErr;
|
|
1048
|
+
for (let i = 0; i < attempts; i++) {
|
|
1049
|
+
try {
|
|
1050
|
+
const res = await fetch(url, {
|
|
1051
|
+
method: "POST",
|
|
1052
|
+
headers: { authorization: `Bearer ${this.apiKey}` },
|
|
1053
|
+
body: form
|
|
1054
|
+
});
|
|
1055
|
+
if (res.status !== 429 && res.status < 500) return res;
|
|
1056
|
+
lastErr = new Error(`Whisper STT ${res.status} ${res.statusText}`);
|
|
1057
|
+
} catch (e) {
|
|
1058
|
+
lastErr = e;
|
|
1059
|
+
}
|
|
1060
|
+
if (i < attempts - 1) await delay(500 * 2 ** i);
|
|
1061
|
+
}
|
|
1062
|
+
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
function delay(ms) {
|
|
1066
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1067
|
+
}
|
|
1068
|
+
var UPLOAD_RESPONSE = z.object({ upload_url: z.string() });
|
|
1069
|
+
var AAI_TRANSCRIPT = z.object({
|
|
1070
|
+
id: z.string(),
|
|
1071
|
+
status: z.enum(["queued", "processing", "completed", "error"]),
|
|
1072
|
+
text: z.string().optional(),
|
|
1073
|
+
language_code: z.string().optional(),
|
|
1074
|
+
error: z.string().optional(),
|
|
1075
|
+
utterances: z.array(z.object({ speaker: z.string(), text: z.string() })).nullish()
|
|
1076
|
+
}).loose();
|
|
1077
|
+
var AssemblyAiStt = class {
|
|
1078
|
+
apiKey;
|
|
1079
|
+
baseUrl;
|
|
1080
|
+
pollIntervalMs;
|
|
1081
|
+
maxWaitMs;
|
|
1082
|
+
sleep;
|
|
1083
|
+
constructor(opts) {
|
|
1084
|
+
this.apiKey = opts.apiKey;
|
|
1085
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.assemblyai.com/v2").replace(/\/+$/, "");
|
|
1086
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? 3e3;
|
|
1087
|
+
this.maxWaitMs = opts.maxWaitMs ?? 15 * 60 * 1e3;
|
|
1088
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1089
|
+
}
|
|
1090
|
+
async transcribe(audioPath) {
|
|
1091
|
+
const bytes = await readFile(audioPath);
|
|
1092
|
+
const up = await fetch(`${this.baseUrl}/upload`, {
|
|
1093
|
+
method: "POST",
|
|
1094
|
+
headers: { authorization: this.apiKey, "content-type": "application/octet-stream" },
|
|
1095
|
+
body: bytes
|
|
1096
|
+
});
|
|
1097
|
+
if (!up.ok) throw new Error(`AssemblyAI upload ${up.status}: ${(await up.text()).slice(0, 200)}`);
|
|
1098
|
+
const { upload_url } = UPLOAD_RESPONSE.parse(await up.json());
|
|
1099
|
+
const created = await this.post("/transcript", {
|
|
1100
|
+
audio_url: upload_url,
|
|
1101
|
+
speaker_labels: true,
|
|
1102
|
+
language_detection: true
|
|
1103
|
+
});
|
|
1104
|
+
const id = created.id;
|
|
1105
|
+
const deadline = this.maxWaitMs;
|
|
1106
|
+
let waited = 0;
|
|
1107
|
+
let job = created;
|
|
1108
|
+
while (job.status !== "completed" && job.status !== "error") {
|
|
1109
|
+
if (waited >= deadline) throw new Error(`AssemblyAI transcript ${id} timed out after ${deadline} ms`);
|
|
1110
|
+
await this.sleep(this.pollIntervalMs);
|
|
1111
|
+
waited += this.pollIntervalMs;
|
|
1112
|
+
job = await this.get(`/transcript/${id}`);
|
|
1113
|
+
}
|
|
1114
|
+
if (job.status === "error") throw new Error(`AssemblyAI transcript failed: ${job.error ?? "unknown"}`);
|
|
1115
|
+
return {
|
|
1116
|
+
text: formatDiarized(job),
|
|
1117
|
+
...job.language_code ? { language: job.language_code } : {}
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
async post(path4, body) {
|
|
1121
|
+
const r = await fetch(`${this.baseUrl}${path4}`, {
|
|
1122
|
+
method: "POST",
|
|
1123
|
+
headers: { authorization: this.apiKey, "content-type": "application/json" },
|
|
1124
|
+
body: JSON.stringify(body)
|
|
1125
|
+
});
|
|
1126
|
+
if (!r.ok) throw new Error(`AssemblyAI POST ${path4} ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
1127
|
+
return AAI_TRANSCRIPT.parse(await r.json());
|
|
1128
|
+
}
|
|
1129
|
+
async get(path4) {
|
|
1130
|
+
const r = await fetch(`${this.baseUrl}${path4}`, { headers: { authorization: this.apiKey } });
|
|
1131
|
+
if (!r.ok) throw new Error(`AssemblyAI GET ${path4} ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
1132
|
+
return AAI_TRANSCRIPT.parse(await r.json());
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
function formatDiarized(job) {
|
|
1136
|
+
if (job.utterances && job.utterances.length > 0) {
|
|
1137
|
+
return job.utterances.map((u) => `Speaker ${u.speaker}: ${u.text.trim()}`).join("\n\n").trim();
|
|
1138
|
+
}
|
|
1139
|
+
return (job.text ?? "").trim();
|
|
1140
|
+
}
|
|
1141
|
+
var execFileAsync2 = promisify(execFile);
|
|
1142
|
+
var DEFAULT_MAX_BYTES = 24 * 1024 * 1024;
|
|
1143
|
+
var DEFAULT_SEGMENT_SECONDS = 1200;
|
|
1144
|
+
var ChunkedStt = class {
|
|
1145
|
+
base;
|
|
1146
|
+
maxBytes;
|
|
1147
|
+
segmentSeconds;
|
|
1148
|
+
split;
|
|
1149
|
+
constructor(opts) {
|
|
1150
|
+
this.base = opts.base;
|
|
1151
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
1152
|
+
this.segmentSeconds = opts.segmentSeconds ?? DEFAULT_SEGMENT_SECONDS;
|
|
1153
|
+
this.split = opts.split ?? defaultFfmpegSplitter(opts.ffmpegBin ?? "ffmpeg");
|
|
1154
|
+
}
|
|
1155
|
+
async transcribe(audioPath) {
|
|
1156
|
+
const { size } = await stat(audioPath);
|
|
1157
|
+
if (size <= this.maxBytes) return this.base.transcribe(audioPath);
|
|
1158
|
+
const dir = await mkdtemp(join(tmpdir(), "corpus-stt-chunk-"));
|
|
1159
|
+
try {
|
|
1160
|
+
const parts = await this.split(audioPath, this.segmentSeconds, dir);
|
|
1161
|
+
if (parts.length === 0) {
|
|
1162
|
+
return this.base.transcribe(audioPath);
|
|
1163
|
+
}
|
|
1164
|
+
const texts = [];
|
|
1165
|
+
let language;
|
|
1166
|
+
let failed = 0;
|
|
1167
|
+
let firstErr;
|
|
1168
|
+
for (const part of parts) {
|
|
1169
|
+
try {
|
|
1170
|
+
const t = await this.base.transcribe(part);
|
|
1171
|
+
if (t.text.trim()) texts.push(t.text.trim());
|
|
1172
|
+
language ??= t.language;
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
failed++;
|
|
1175
|
+
firstErr ??= e;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (texts.length === 0) {
|
|
1179
|
+
throw firstErr ?? new Error("all audio segments failed transcription");
|
|
1180
|
+
}
|
|
1181
|
+
if (failed > 0) {
|
|
1182
|
+
process.stderr.write(
|
|
1183
|
+
`corpus: ${failed}/${parts.length} audio segments failed \u2014 partial transcript
|
|
1184
|
+
`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
text: texts.join("\n\n"),
|
|
1189
|
+
...language ? { language } : {}
|
|
1190
|
+
};
|
|
1191
|
+
} finally {
|
|
1192
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
function defaultFfmpegSplitter(bin) {
|
|
1198
|
+
return async (audioPath, segmentSeconds, outDir) => {
|
|
1199
|
+
const ext = extname(audioPath) || ".mp3";
|
|
1200
|
+
await execFileAsync2(
|
|
1201
|
+
bin,
|
|
1202
|
+
[
|
|
1203
|
+
"-hide_banner",
|
|
1204
|
+
"-loglevel",
|
|
1205
|
+
"error",
|
|
1206
|
+
"-i",
|
|
1207
|
+
audioPath,
|
|
1208
|
+
"-f",
|
|
1209
|
+
"segment",
|
|
1210
|
+
"-segment_time",
|
|
1211
|
+
String(segmentSeconds),
|
|
1212
|
+
"-c",
|
|
1213
|
+
"copy",
|
|
1214
|
+
join(outDir, `part%03d${ext}`)
|
|
1215
|
+
],
|
|
1216
|
+
{ maxBuffer: 16 * 1024 * 1024 }
|
|
1217
|
+
);
|
|
1218
|
+
const files = (await readdir(outDir)).filter((f) => f.startsWith("part") && f.endsWith(ext)).sort();
|
|
1219
|
+
return files.map((f) => join(outDir, f));
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/ports/http-readability-fetcher.adapter.ts
|
|
1224
|
+
var DEFAULT_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0 Safari/537.36";
|
|
1225
|
+
var HttpReadabilityFetcher = class {
|
|
1226
|
+
ua;
|
|
1227
|
+
maxChars;
|
|
1228
|
+
constructor(opts = {}) {
|
|
1229
|
+
this.ua = opts.userAgent ?? DEFAULT_UA;
|
|
1230
|
+
this.maxChars = opts.maxChars ?? 2e5;
|
|
1231
|
+
}
|
|
1232
|
+
async fetch(url) {
|
|
1233
|
+
if (isVideoUrl(url)) return null;
|
|
1234
|
+
let res;
|
|
1235
|
+
try {
|
|
1236
|
+
res = await fetch(url, {
|
|
1237
|
+
headers: { "user-agent": this.ua, "accept-language": "en,fr;q=0.8" },
|
|
1238
|
+
redirect: "follow"
|
|
1239
|
+
});
|
|
1240
|
+
} catch {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
if (!res.ok) return null;
|
|
1244
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
1245
|
+
if (!ct.includes("text/html") && !ct.includes("application/xhtml")) return null;
|
|
1246
|
+
const html = await res.text();
|
|
1247
|
+
const title = decodeEntities(
|
|
1248
|
+
(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "").trim()
|
|
1249
|
+
);
|
|
1250
|
+
const lang = html.match(/<html[^>]*\blang=["']?([a-zA-Z-]+)/i)?.[1];
|
|
1251
|
+
const text = extractReadable(html).slice(0, this.maxChars);
|
|
1252
|
+
if (!text.trim()) return null;
|
|
1253
|
+
return {
|
|
1254
|
+
title: title || url,
|
|
1255
|
+
text,
|
|
1256
|
+
kind: "article",
|
|
1257
|
+
...lang ? { language: lang } : {},
|
|
1258
|
+
via: "readability"
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
function extractReadable(html) {
|
|
1263
|
+
const region = html.match(/<article[\s\S]*?<\/article>/i)?.[0] ?? html.match(/<main[\s\S]*?<\/main>/i)?.[0] ?? html.match(/<body[\s\S]*?<\/body>/i)?.[0] ?? html;
|
|
1264
|
+
return decodeEntities(
|
|
1265
|
+
region.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<(nav|header|footer|aside|form)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]+>/g, " ").replace(/[ \t]+/g, " ").replace(/\n{3,}/g, "\n\n")
|
|
1266
|
+
).trim();
|
|
1267
|
+
}
|
|
1268
|
+
function decodeEntities(s) {
|
|
1269
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, " ").replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/ports/composite-fetcher.ts
|
|
1273
|
+
var CompositeFetcher = class {
|
|
1274
|
+
children;
|
|
1275
|
+
constructor(children) {
|
|
1276
|
+
this.children = children;
|
|
1277
|
+
}
|
|
1278
|
+
async fetch(url) {
|
|
1279
|
+
for (const child of this.children) {
|
|
1280
|
+
const result = await child.fetch(url);
|
|
1281
|
+
if (result) return result;
|
|
1282
|
+
}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// src/ports/throttle-fetcher.adapter.ts
|
|
1288
|
+
var ThrottleFetcher = class {
|
|
1289
|
+
inner;
|
|
1290
|
+
minIntervalMs;
|
|
1291
|
+
now;
|
|
1292
|
+
sleep;
|
|
1293
|
+
lastStart = 0;
|
|
1294
|
+
started = false;
|
|
1295
|
+
constructor(inner, opts) {
|
|
1296
|
+
this.inner = inner;
|
|
1297
|
+
this.minIntervalMs = Math.max(0, opts.minIntervalMs);
|
|
1298
|
+
this.now = opts.now ?? (() => Date.now());
|
|
1299
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
1300
|
+
}
|
|
1301
|
+
async fetch(url) {
|
|
1302
|
+
if (this.minIntervalMs > 0 && this.started) {
|
|
1303
|
+
const wait = this.minIntervalMs - (this.now() - this.lastStart);
|
|
1304
|
+
if (wait > 0) await this.sleep(wait);
|
|
1305
|
+
}
|
|
1306
|
+
this.started = true;
|
|
1307
|
+
this.lastStart = this.now();
|
|
1308
|
+
return this.inner.fetch(url);
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
var RPC_RESPONSE = z.object({ result: z.unknown().optional(), error: z.unknown().optional() }).loose();
|
|
1312
|
+
var CALL_RESULT = z.object({
|
|
1313
|
+
content: z.unknown().optional(),
|
|
1314
|
+
structuredContent: z.unknown().optional(),
|
|
1315
|
+
isError: z.boolean().optional()
|
|
1316
|
+
}).loose();
|
|
1317
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
1318
|
+
async function connectBrowserMcp(opts) {
|
|
1319
|
+
const client = new StreamableHttpMcpClient(opts);
|
|
1320
|
+
await client.initialize();
|
|
1321
|
+
return client;
|
|
1322
|
+
}
|
|
1323
|
+
var StreamableHttpMcpClient = class {
|
|
1324
|
+
id = 0;
|
|
1325
|
+
sessionId;
|
|
1326
|
+
endpoint;
|
|
1327
|
+
headers;
|
|
1328
|
+
protocolVersion;
|
|
1329
|
+
constructor(opts) {
|
|
1330
|
+
this.endpoint = opts.endpoint;
|
|
1331
|
+
this.headers = opts.headers ?? {};
|
|
1332
|
+
this.protocolVersion = opts.protocolVersion ?? PROTOCOL_VERSION;
|
|
1333
|
+
}
|
|
1334
|
+
async initialize() {
|
|
1335
|
+
const res = await this.post({
|
|
1336
|
+
jsonrpc: "2.0",
|
|
1337
|
+
id: ++this.id,
|
|
1338
|
+
method: "initialize",
|
|
1339
|
+
params: {
|
|
1340
|
+
protocolVersion: this.protocolVersion,
|
|
1341
|
+
capabilities: {},
|
|
1342
|
+
clientInfo: { name: "corpus-cli", version: "0.1.0" }
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
await this.post(
|
|
1346
|
+
{ jsonrpc: "2.0", method: "notifications/initialized", params: {} },
|
|
1347
|
+
{ expectResponse: false }
|
|
1348
|
+
);
|
|
1349
|
+
if (res?.error) {
|
|
1350
|
+
throw new Error(`initialize failed: ${JSON.stringify(res.error)}`);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
async callTool(args) {
|
|
1354
|
+
const res = await this.post({
|
|
1355
|
+
jsonrpc: "2.0",
|
|
1356
|
+
id: ++this.id,
|
|
1357
|
+
method: "tools/call",
|
|
1358
|
+
params: { name: args.name, arguments: args.arguments }
|
|
1359
|
+
});
|
|
1360
|
+
if (res?.error) {
|
|
1361
|
+
throw new Error(`tools/call ${args.name} failed: ${JSON.stringify(res.error)}`);
|
|
1362
|
+
}
|
|
1363
|
+
const parsed = CALL_RESULT.safeParse(res?.result ?? {});
|
|
1364
|
+
return parsed.success ? parsed.data : {};
|
|
1365
|
+
}
|
|
1366
|
+
async post(body, opts = {}) {
|
|
1367
|
+
const res = await fetch(this.endpoint, {
|
|
1368
|
+
method: "POST",
|
|
1369
|
+
headers: {
|
|
1370
|
+
"content-type": "application/json",
|
|
1371
|
+
accept: "application/json, text/event-stream",
|
|
1372
|
+
...this.sessionId ? { "mcp-session-id": this.sessionId } : {},
|
|
1373
|
+
...this.headers
|
|
1374
|
+
},
|
|
1375
|
+
body: JSON.stringify(body)
|
|
1376
|
+
});
|
|
1377
|
+
const sid = res.headers.get("mcp-session-id");
|
|
1378
|
+
if (sid) this.sessionId = sid;
|
|
1379
|
+
if (opts.expectResponse === false) return null;
|
|
1380
|
+
if (!res.ok) {
|
|
1381
|
+
throw new Error(`MCP HTTP ${res.status} ${res.statusText}`);
|
|
1382
|
+
}
|
|
1383
|
+
return parseRpcResponse(await res.text(), res.headers.get("content-type"));
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
function parseRpcResponse(text, contentType) {
|
|
1387
|
+
if (!text.trim()) return null;
|
|
1388
|
+
const isSse = (contentType ?? "").includes("text/event-stream");
|
|
1389
|
+
if (!isSse) {
|
|
1390
|
+
const parsed = RPC_RESPONSE.safeParse(JSON.parse(text));
|
|
1391
|
+
return parsed.success ? parsed.data : null;
|
|
1392
|
+
}
|
|
1393
|
+
let last = null;
|
|
1394
|
+
for (const line of text.split("\n")) {
|
|
1395
|
+
const trimmed = line.trim();
|
|
1396
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
1397
|
+
const payload = trimmed.slice("data:".length).trim();
|
|
1398
|
+
if (!payload || payload === "[DONE]") continue;
|
|
1399
|
+
try {
|
|
1400
|
+
last = JSON.parse(payload);
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return last;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/commands/import-web.ts
|
|
1408
|
+
var SOURCE_URL_FRONTMATTER = z.object({
|
|
1409
|
+
metadata: z.object({
|
|
1410
|
+
corpus: z.object({
|
|
1411
|
+
originalUrl: z.string().optional().catch(void 0),
|
|
1412
|
+
importerSourceUrl: z.string().optional().catch(void 0)
|
|
1413
|
+
}).loose().optional().catch(void 0)
|
|
1414
|
+
}).loose().optional().catch(void 0)
|
|
1415
|
+
}).loose();
|
|
1416
|
+
function parse(args) {
|
|
1417
|
+
const out = {
|
|
1418
|
+
workspace: void 0,
|
|
1419
|
+
urlsFile: void 0,
|
|
1420
|
+
urls: [],
|
|
1421
|
+
tags: [],
|
|
1422
|
+
lang: void 0,
|
|
1423
|
+
max: void 0,
|
|
1424
|
+
maxDurationSec: void 0,
|
|
1425
|
+
cookiesFromBrowser: void 0,
|
|
1426
|
+
cookiesFile: void 0,
|
|
1427
|
+
throttleMs: 2e3,
|
|
1428
|
+
force: false,
|
|
1429
|
+
diarize: false,
|
|
1430
|
+
browserMcp: process.env.BROWSER_MCP_URL,
|
|
1431
|
+
scrapeMcp: process.env.SCRAPE_MCP_URL,
|
|
1432
|
+
importerId: "web",
|
|
1433
|
+
dryRun: false
|
|
1434
|
+
};
|
|
1435
|
+
for (let i = 0; i < args.length; i++) {
|
|
1436
|
+
const a = args[i];
|
|
1437
|
+
const next = () => args[++i];
|
|
1438
|
+
switch (a) {
|
|
1439
|
+
case "--urls-file":
|
|
1440
|
+
out.urlsFile = next();
|
|
1441
|
+
break;
|
|
1442
|
+
case "--url": {
|
|
1443
|
+
const v = next();
|
|
1444
|
+
if (v) out.urls.push(v);
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
1447
|
+
case "--tags": {
|
|
1448
|
+
const v = next();
|
|
1449
|
+
if (v) out.tags.push(...v.split(",").map((s) => s.trim()).filter(Boolean));
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
case "--lang":
|
|
1453
|
+
out.lang = next();
|
|
1454
|
+
break;
|
|
1455
|
+
case "--max": {
|
|
1456
|
+
const v = next();
|
|
1457
|
+
if (v) out.max = Number(v);
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
case "--max-duration": {
|
|
1461
|
+
const v = next();
|
|
1462
|
+
if (v) out.maxDurationSec = Number(v);
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
case "--cookies-from-browser":
|
|
1466
|
+
out.cookiesFromBrowser = next();
|
|
1467
|
+
break;
|
|
1468
|
+
case "--cookies":
|
|
1469
|
+
out.cookiesFile = next();
|
|
1470
|
+
break;
|
|
1471
|
+
case "--throttle": {
|
|
1472
|
+
const v = next();
|
|
1473
|
+
if (v) out.throttleMs = Number(v);
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
case "--force":
|
|
1477
|
+
out.force = true;
|
|
1478
|
+
break;
|
|
1479
|
+
case "--diarize":
|
|
1480
|
+
out.diarize = true;
|
|
1481
|
+
break;
|
|
1482
|
+
case "--browser-mcp":
|
|
1483
|
+
out.browserMcp = next();
|
|
1484
|
+
break;
|
|
1485
|
+
case "--scrape-mcp":
|
|
1486
|
+
out.scrapeMcp = next();
|
|
1487
|
+
break;
|
|
1488
|
+
case "--importer-id":
|
|
1489
|
+
out.importerId = next() ?? "web";
|
|
1490
|
+
break;
|
|
1491
|
+
case "--dry-run":
|
|
1492
|
+
out.dryRun = true;
|
|
1493
|
+
break;
|
|
1494
|
+
default:
|
|
1495
|
+
if (!a.startsWith("-") && out.workspace === void 0) out.workspace = a;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return out;
|
|
1499
|
+
}
|
|
1500
|
+
async function readUrls(parsed) {
|
|
1501
|
+
const fromFile = [];
|
|
1502
|
+
if (parsed.urlsFile) {
|
|
1503
|
+
const raw = await readFile(parsed.urlsFile, "utf-8");
|
|
1504
|
+
for (const line of raw.split("\n")) {
|
|
1505
|
+
const url = line.trim();
|
|
1506
|
+
const m = url.match(/https?:\/\/\S+/);
|
|
1507
|
+
if (m) fromFile.push(m[0]);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1511
|
+
return [...fromFile, ...parsed.urls].filter((u) => seen.has(u) ? false : seen.add(u));
|
|
1512
|
+
}
|
|
1513
|
+
async function scanIngestedUrls(workspaceRoot) {
|
|
1514
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1515
|
+
const sourcesDir = join(workspaceRoot, "sources");
|
|
1516
|
+
let entries;
|
|
1517
|
+
try {
|
|
1518
|
+
entries = await readdir(sourcesDir, {
|
|
1519
|
+
recursive: true,
|
|
1520
|
+
withFileTypes: true
|
|
1521
|
+
});
|
|
1522
|
+
} catch {
|
|
1523
|
+
return seen;
|
|
1524
|
+
}
|
|
1525
|
+
for (const e of entries) {
|
|
1526
|
+
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
1527
|
+
try {
|
|
1528
|
+
const raw = await readFile(join(e.parentPath, e.name), "utf-8");
|
|
1529
|
+
const fm = SOURCE_URL_FRONTMATTER.parse(matter2(raw).data);
|
|
1530
|
+
const url = fm.metadata?.corpus?.originalUrl ?? fm.metadata?.corpus?.importerSourceUrl;
|
|
1531
|
+
if (url) seen.add(url);
|
|
1532
|
+
} catch {
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return seen;
|
|
1536
|
+
}
|
|
1537
|
+
async function runImportWeb(args) {
|
|
1538
|
+
const parsed = parse(args);
|
|
1539
|
+
const target = resolveWorkspacePath(parsed.workspace);
|
|
1540
|
+
const urls = await readUrls(parsed);
|
|
1541
|
+
if (urls.length === 0) {
|
|
1542
|
+
return fail("import-web requires --urls-file <path> and/or one or more --url <u>.", 2);
|
|
1543
|
+
}
|
|
1544
|
+
const done = parsed.force ? /* @__PURE__ */ new Set() : await scanIngestedUrls(target);
|
|
1545
|
+
const todo = urls.filter((u) => !done.has(u));
|
|
1546
|
+
const batch = parsed.max !== void 0 ? todo.slice(0, parsed.max) : todo;
|
|
1547
|
+
const remaining = todo.length - batch.length;
|
|
1548
|
+
const plan = ` workspace: ${target}
|
|
1549
|
+
urls: ${urls.length} total \xB7 ${done.size && !parsed.force ? `${urls.length - todo.length} already ingested \xB7 ` : ""}${todo.length} to do
|
|
1550
|
+
this run: ${batch.length}${parsed.max !== void 0 ? ` (--max ${parsed.max})` : ""} \xB7 ${remaining} remaining after
|
|
1551
|
+
video stt: ${parsed.diarize ? "AssemblyAI (speaker-labelled)" : "Whisper"}
|
|
1552
|
+
throttle: ${parsed.throttleMs} ms between fetches
|
|
1553
|
+
`;
|
|
1554
|
+
if (batch.length === 0) {
|
|
1555
|
+
process.stdout.write(`import-web \u2192 ${target}
|
|
1556
|
+
nothing to do \u2014 all ${urls.length} URLs already ingested.
|
|
1557
|
+
`);
|
|
1558
|
+
return 0;
|
|
1559
|
+
}
|
|
1560
|
+
if (parsed.dryRun) {
|
|
1561
|
+
process.stdout.write(`import-web (dry run)
|
|
1562
|
+
${plan}`);
|
|
1563
|
+
for (const u of batch.slice(0, 20)) process.stdout.write(` - ${u}
|
|
1564
|
+
`);
|
|
1565
|
+
if (batch.length > 20) process.stdout.write(` \u2026 +${batch.length - 20} more
|
|
1566
|
+
`);
|
|
1567
|
+
return 0;
|
|
1568
|
+
}
|
|
1569
|
+
process.stdout.write(`import-web
|
|
1570
|
+
${plan}`);
|
|
1571
|
+
const chain = [];
|
|
1572
|
+
let stt;
|
|
1573
|
+
if (parsed.diarize) {
|
|
1574
|
+
const aaiKey = process.env.ASSEMBLYAI_API_KEY;
|
|
1575
|
+
if (aaiKey) stt = new AssemblyAiStt({ apiKey: aaiKey });
|
|
1576
|
+
else
|
|
1577
|
+
process.stderr.write(
|
|
1578
|
+
"corpus: --diarize needs ASSEMBLYAI_API_KEY \u2014 video URLs will be skipped.\n"
|
|
1579
|
+
);
|
|
1580
|
+
} else {
|
|
1581
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1582
|
+
if (openaiKey)
|
|
1583
|
+
stt = new ChunkedStt({ base: new OpenAiWhisperStt({ apiKey: openaiKey }) });
|
|
1584
|
+
else
|
|
1585
|
+
process.stderr.write(
|
|
1586
|
+
"corpus: OPENAI_API_KEY not set \u2014 video URLs will be skipped (no transcription).\n"
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
if (stt)
|
|
1590
|
+
chain.push(
|
|
1591
|
+
new YtDlpWhisperFetcher({
|
|
1592
|
+
stt,
|
|
1593
|
+
...parsed.maxDurationSec ? { maxDurationSec: parsed.maxDurationSec } : {},
|
|
1594
|
+
...parsed.cookiesFromBrowser ? { cookiesFromBrowser: parsed.cookiesFromBrowser } : {},
|
|
1595
|
+
...parsed.cookiesFile ? { cookiesFile: parsed.cookiesFile } : {}
|
|
1596
|
+
})
|
|
1597
|
+
);
|
|
1598
|
+
if (parsed.scrapeMcp) {
|
|
1599
|
+
try {
|
|
1600
|
+
const client = await connectBrowserMcp({ endpoint: parsed.scrapeMcp });
|
|
1601
|
+
chain.push(new ScrapeMcpFetcher({ client }));
|
|
1602
|
+
} catch (e) {
|
|
1603
|
+
return fail(`could not connect to scrape MCP at ${parsed.scrapeMcp}: ${msg2(e)}`, 1);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
if (parsed.browserMcp) {
|
|
1607
|
+
let browser;
|
|
1608
|
+
try {
|
|
1609
|
+
browser = await connectBrowserMcp({ endpoint: parsed.browserMcp });
|
|
1610
|
+
chain.push(new BrowserMcpFetcher({ browser }));
|
|
1611
|
+
} catch (e) {
|
|
1612
|
+
return fail(`could not connect to browser MCP at ${parsed.browserMcp}: ${msg2(e)}`, 1);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
chain.push(new HttpReadabilityFetcher());
|
|
1616
|
+
if (chain.length === 0) {
|
|
1617
|
+
return fail("no fetchers available \u2014 set OPENAI_API_KEY (videos) and/or --browser-mcp.", 2);
|
|
1618
|
+
}
|
|
1619
|
+
const fetcher = new ThrottleFetcher(new CompositeFetcher(chain), {
|
|
1620
|
+
minIntervalMs: parsed.throttleMs
|
|
1621
|
+
});
|
|
1622
|
+
const importer = new WebImporter({ fetcher });
|
|
1623
|
+
const runner = new ImporterRunner({
|
|
1624
|
+
// The fs is rooted AT the workspace, so corpus paths are relative to
|
|
1625
|
+
// it — workspacePath is "" (root), NOT the absolute target (which
|
|
1626
|
+
// would double-nest under the fs root).
|
|
1627
|
+
fs: new NodeFsAdapter({ root: target }),
|
|
1628
|
+
clock: systemClock,
|
|
1629
|
+
identity: new OsIdentityAdapter({ workspaceRoot: target }),
|
|
1630
|
+
workspacePath: ""
|
|
1631
|
+
});
|
|
1632
|
+
let report;
|
|
1633
|
+
try {
|
|
1634
|
+
report = await runner.run(importer, {
|
|
1635
|
+
importerId: parsed.importerId,
|
|
1636
|
+
config: {
|
|
1637
|
+
urls: batch,
|
|
1638
|
+
...parsed.tags.length ? { tags: parsed.tags } : {},
|
|
1639
|
+
...parsed.lang ? { language: parsed.lang } : {}
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
} catch (e) {
|
|
1643
|
+
return fail(`import failed: ${msg2(e)}`, 1);
|
|
1644
|
+
}
|
|
1645
|
+
process.stdout.write(
|
|
1646
|
+
`import-web \u2192 ${target}
|
|
1647
|
+
batch: ${report.batchId}
|
|
1648
|
+
archived: ${report.archivedSlugs.length}
|
|
1649
|
+
duplicates: ${report.duplicateSlugs.length}
|
|
1650
|
+
candidates: ${report.candidateIds.length}
|
|
1651
|
+
`
|
|
1652
|
+
);
|
|
1653
|
+
for (const w of report.warnings.slice(0, 20)) process.stdout.write(` ! ${w}
|
|
1654
|
+
`);
|
|
1655
|
+
process.stdout.write(
|
|
1656
|
+
`
|
|
1657
|
+
Next: review candidates, then promote them \u2014 promotion pushes chunks into the
|
|
1658
|
+
knowledge engine (RAG) via the WriterPort. Import only stages the sources.
|
|
1659
|
+
`
|
|
1660
|
+
);
|
|
1661
|
+
return 0;
|
|
1662
|
+
}
|
|
1663
|
+
function msg2(e) {
|
|
1664
|
+
return e instanceof Error ? e.message : String(e);
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/ports/anthropic-distiller.ts
|
|
1668
|
+
var ANTHROPIC_RESPONSE = z.object({
|
|
1669
|
+
model: z.string().optional(),
|
|
1670
|
+
content: z.array(z.object({ type: z.string(), text: z.string().optional() }).loose()).optional(),
|
|
1671
|
+
usage: z.object({ input_tokens: z.number(), output_tokens: z.number() }).loose().optional()
|
|
1672
|
+
}).loose();
|
|
1673
|
+
var AnthropicDistiller = class {
|
|
1674
|
+
apiKey;
|
|
1675
|
+
model;
|
|
1676
|
+
baseUrl;
|
|
1677
|
+
maxItems;
|
|
1678
|
+
onUsage;
|
|
1679
|
+
constructor(opts) {
|
|
1680
|
+
this.apiKey = opts.apiKey;
|
|
1681
|
+
this.model = opts.model ?? "claude-sonnet-4-6";
|
|
1682
|
+
this.baseUrl = (opts.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/+$/, "");
|
|
1683
|
+
this.maxItems = opts.maxItems ?? 8;
|
|
1684
|
+
this.onUsage = opts.onUsage;
|
|
1685
|
+
}
|
|
1686
|
+
async distill(input) {
|
|
1687
|
+
const prompt = buildDistillPrompt(input, this.maxItems);
|
|
1688
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1689
|
+
const res = await fetch(`${this.baseUrl}/messages`, {
|
|
1690
|
+
method: "POST",
|
|
1691
|
+
headers: {
|
|
1692
|
+
"x-api-key": this.apiKey,
|
|
1693
|
+
"anthropic-version": "2023-06-01",
|
|
1694
|
+
"content-type": "application/json"
|
|
1695
|
+
},
|
|
1696
|
+
body: JSON.stringify({
|
|
1697
|
+
model: this.model,
|
|
1698
|
+
max_tokens: 4096,
|
|
1699
|
+
messages: [{ role: "user", content: prompt }]
|
|
1700
|
+
})
|
|
1701
|
+
});
|
|
1702
|
+
if (!res.ok) {
|
|
1703
|
+
throw new Error(`Anthropic distill ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
1704
|
+
}
|
|
1705
|
+
const parsed = ANTHROPIC_RESPONSE.safeParse(await res.json());
|
|
1706
|
+
const text = parsed.success ? (parsed.data.content ?? []).find((c) => c.type === "text")?.text ?? "" : "";
|
|
1707
|
+
if (parsed.success && parsed.data.usage && this.onUsage) {
|
|
1708
|
+
this.onUsage({
|
|
1709
|
+
model: parsed.data.model ?? this.model,
|
|
1710
|
+
inputTokens: parsed.data.usage.input_tokens,
|
|
1711
|
+
outputTokens: parsed.data.usage.output_tokens,
|
|
1712
|
+
label: input.title,
|
|
1713
|
+
startedAt,
|
|
1714
|
+
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
return parseItems(text);
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
var CliAgentDistiller = class {
|
|
1721
|
+
engine;
|
|
1722
|
+
model;
|
|
1723
|
+
maxItems;
|
|
1724
|
+
timeoutMs;
|
|
1725
|
+
cwd;
|
|
1726
|
+
constructor(opts) {
|
|
1727
|
+
this.engine = opts.engine;
|
|
1728
|
+
this.model = opts.model;
|
|
1729
|
+
this.maxItems = opts.maxItems ?? 8;
|
|
1730
|
+
this.timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1731
|
+
this.cwd = opts.cwd ?? tmpdir();
|
|
1732
|
+
}
|
|
1733
|
+
async distill(input) {
|
|
1734
|
+
const prompt = buildDistillPrompt(input, this.maxItems);
|
|
1735
|
+
const stdout = await spawnWithStdin({
|
|
1736
|
+
command: this.engine.command,
|
|
1737
|
+
args: this.engine.buildArgs({ ...this.model ? { model: this.model } : {} }),
|
|
1738
|
+
stdin: prompt,
|
|
1739
|
+
cwd: this.cwd,
|
|
1740
|
+
timeoutMs: this.timeoutMs
|
|
1741
|
+
});
|
|
1742
|
+
const text = this.engine.parseOutput(stdout) ?? stdout;
|
|
1743
|
+
return parseItems(text);
|
|
1744
|
+
}
|
|
1745
|
+
};
|
|
1746
|
+
var CLAUDE_CODE = {
|
|
1747
|
+
id: "claude-code",
|
|
1748
|
+
subscriptionBilled: true,
|
|
1749
|
+
command: "claude",
|
|
1750
|
+
buildArgs: ({ model }) => [
|
|
1751
|
+
"-p",
|
|
1752
|
+
"--output-format",
|
|
1753
|
+
"json",
|
|
1754
|
+
"--strict-mcp-config",
|
|
1755
|
+
...model ? ["--model", model] : []
|
|
1756
|
+
],
|
|
1757
|
+
parseOutput: parseClaudeJsonOutput
|
|
1758
|
+
};
|
|
1759
|
+
var CLI_ENGINES = {
|
|
1760
|
+
[CLAUDE_CODE.id]: CLAUDE_CODE
|
|
1761
|
+
};
|
|
1762
|
+
var PRICING = [
|
|
1763
|
+
{ match: "opus", inPerM: 15, outPerM: 75 },
|
|
1764
|
+
{ match: "haiku", inPerM: 1, outPerM: 5 },
|
|
1765
|
+
{ match: "sonnet", inPerM: 3, outPerM: 15 }
|
|
1766
|
+
];
|
|
1767
|
+
function priceFor(model) {
|
|
1768
|
+
const m = model.toLowerCase();
|
|
1769
|
+
return PRICING.find((p) => m.includes(p.match)) ?? { inPerM: 3, outPerM: 15 };
|
|
1770
|
+
}
|
|
1771
|
+
function costUsd(u) {
|
|
1772
|
+
const { inPerM, outPerM } = priceFor(u.model);
|
|
1773
|
+
const input = u.inputTokens / 1e6 * inPerM;
|
|
1774
|
+
const output = u.outputTokens / 1e6 * outPerM;
|
|
1775
|
+
return { input, output, total: input + output };
|
|
1776
|
+
}
|
|
1777
|
+
function readLangfuseConfig() {
|
|
1778
|
+
const publicKey = process.env.LANGFUSE_PUBLIC_KEY;
|
|
1779
|
+
const secretKey = process.env.LANGFUSE_SECRET_KEY;
|
|
1780
|
+
if (!publicKey || !secretKey) return void 0;
|
|
1781
|
+
return {
|
|
1782
|
+
baseUrl: (process.env.LANGFUSE_BASE_URL ?? "https://cloud.langfuse.com").replace(/\/+$/, ""),
|
|
1783
|
+
publicKey,
|
|
1784
|
+
secretKey,
|
|
1785
|
+
environment: process.env.LANGFUSE_TRACING_ENVIRONMENT
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
var fmt = (n) => n.toLocaleString("en-US");
|
|
1789
|
+
var usd = (n) => `$${n.toFixed(n < 1 ? 4 : 2)}`;
|
|
1790
|
+
function createUsageSink(opts) {
|
|
1791
|
+
const records = [];
|
|
1792
|
+
const lf = readLangfuseConfig();
|
|
1793
|
+
async function shipToLangfuse(cfg, traceId) {
|
|
1794
|
+
const auth = Buffer.from(`${cfg.publicKey}:${cfg.secretKey}`).toString("base64");
|
|
1795
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1796
|
+
const batch = [
|
|
1797
|
+
{
|
|
1798
|
+
id: randomUUID(),
|
|
1799
|
+
type: "trace-create",
|
|
1800
|
+
timestamp: now,
|
|
1801
|
+
body: {
|
|
1802
|
+
id: traceId,
|
|
1803
|
+
name: `corpus-distill: ${opts.runName}`,
|
|
1804
|
+
timestamp: now,
|
|
1805
|
+
...cfg.environment ? { environment: cfg.environment } : {},
|
|
1806
|
+
metadata: { tool: "corpus-cli", phase: "distill", calls: records.length }
|
|
1807
|
+
}
|
|
1808
|
+
},
|
|
1809
|
+
...records.map((u) => {
|
|
1810
|
+
const c = costUsd(u);
|
|
1811
|
+
return {
|
|
1812
|
+
id: randomUUID(),
|
|
1813
|
+
type: "generation-create",
|
|
1814
|
+
timestamp: u.endedAt,
|
|
1815
|
+
body: {
|
|
1816
|
+
id: randomUUID(),
|
|
1817
|
+
traceId,
|
|
1818
|
+
type: "GENERATION",
|
|
1819
|
+
name: u.label,
|
|
1820
|
+
model: u.model,
|
|
1821
|
+
startTime: u.startedAt,
|
|
1822
|
+
endTime: u.endedAt,
|
|
1823
|
+
...cfg.environment ? { environment: cfg.environment } : {},
|
|
1824
|
+
usage: {
|
|
1825
|
+
input: u.inputTokens,
|
|
1826
|
+
output: u.outputTokens,
|
|
1827
|
+
total: u.inputTokens + u.outputTokens,
|
|
1828
|
+
unit: "TOKENS",
|
|
1829
|
+
inputCost: c.input,
|
|
1830
|
+
outputCost: c.output,
|
|
1831
|
+
totalCost: c.total
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
})
|
|
1836
|
+
];
|
|
1837
|
+
const res = await fetch(`${cfg.baseUrl}/api/public/ingestion`, {
|
|
1838
|
+
method: "POST",
|
|
1839
|
+
headers: { authorization: `Basic ${auth}`, "content-type": "application/json" },
|
|
1840
|
+
body: JSON.stringify({ batch })
|
|
1841
|
+
});
|
|
1842
|
+
if (!res.ok) {
|
|
1843
|
+
throw new Error(`${res.status}: ${(await res.text()).slice(0, 160)}`);
|
|
1844
|
+
}
|
|
1845
|
+
return records.length;
|
|
1846
|
+
}
|
|
1847
|
+
return {
|
|
1848
|
+
record(u) {
|
|
1849
|
+
records.push(u);
|
|
1850
|
+
},
|
|
1851
|
+
async flush() {
|
|
1852
|
+
if (records.length === 0) return;
|
|
1853
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
1854
|
+
let grandTotal = 0;
|
|
1855
|
+
for (const u of records) {
|
|
1856
|
+
const c = costUsd(u);
|
|
1857
|
+
grandTotal += c.total;
|
|
1858
|
+
const m = byModel.get(u.model) ?? { input: 0, output: 0, cost: 0, calls: 0 };
|
|
1859
|
+
m.input += u.inputTokens;
|
|
1860
|
+
m.output += u.outputTokens;
|
|
1861
|
+
m.cost += c.total;
|
|
1862
|
+
m.calls += 1;
|
|
1863
|
+
byModel.set(u.model, m);
|
|
1864
|
+
}
|
|
1865
|
+
process.stderr.write(`
|
|
1866
|
+
usage \u2014 ${records.length} metered call(s):
|
|
1867
|
+
`);
|
|
1868
|
+
for (const [model, m] of byModel) {
|
|
1869
|
+
process.stderr.write(
|
|
1870
|
+
` ${model} ${m.calls} call(s) \xB7 in ${fmt(m.input)} \xB7 out ${fmt(m.output)} \xB7 ~${usd(m.cost)} est.
|
|
1871
|
+
`
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
process.stderr.write(` total ~${usd(grandTotal)} est.
|
|
1875
|
+
`);
|
|
1876
|
+
if (!lf) {
|
|
1877
|
+
process.stderr.write(
|
|
1878
|
+
" langfuse: not configured (set LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY to export traces).\n"
|
|
1879
|
+
);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const traceId = randomUUID();
|
|
1883
|
+
try {
|
|
1884
|
+
const n = await shipToLangfuse(lf, traceId);
|
|
1885
|
+
process.stderr.write(
|
|
1886
|
+
` langfuse: shipped ${n} generation(s) \u2192 trace ${traceId}${lf.environment ? ` (env: ${lf.environment})` : ""}
|
|
1887
|
+
`
|
|
1888
|
+
);
|
|
1889
|
+
} catch (e) {
|
|
1890
|
+
process.stderr.write(
|
|
1891
|
+
` langfuse: export failed \u2014 ${e instanceof Error ? e.message : String(e)}
|
|
1892
|
+
`
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// src/commands/distill.ts
|
|
1900
|
+
var SOURCE_FRONTMATTER = z.object({
|
|
1901
|
+
id: z.string().optional().catch(void 0),
|
|
1902
|
+
title: z.string().optional().catch(void 0),
|
|
1903
|
+
tags: z.array(z.string()).optional().catch(void 0),
|
|
1904
|
+
language: z.string().optional().catch(void 0),
|
|
1905
|
+
metadata: z.object({
|
|
1906
|
+
corpus: z.object({
|
|
1907
|
+
access: z.string().optional().catch(void 0),
|
|
1908
|
+
domain: z.string().optional().catch(void 0)
|
|
1909
|
+
}).loose().optional().catch(void 0)
|
|
1910
|
+
}).loose().optional().catch(void 0)
|
|
1911
|
+
}).loose();
|
|
1912
|
+
var ENTRY_SOURCES_FRONTMATTER = z.object({ sources: z.array(z.string()).optional().catch(void 0) }).loose();
|
|
1913
|
+
var DEFAULT_ENGINE = "anthropic-api";
|
|
1914
|
+
async function readEntryLayout(target) {
|
|
1915
|
+
try {
|
|
1916
|
+
const raw = await readFile(join(target, "KNOWLEDGE.md"), "utf8");
|
|
1917
|
+
const layout = matter2(raw).data?.metadata;
|
|
1918
|
+
const corpus = layout?.["corpus"];
|
|
1919
|
+
const v = corpus?.["entryLayout"];
|
|
1920
|
+
return v === "flat" || v === "dated" ? v : void 0;
|
|
1921
|
+
} catch {
|
|
1922
|
+
return void 0;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
var DISTILLER_ENGINES = {
|
|
1926
|
+
[DEFAULT_ENGINE]: {
|
|
1927
|
+
id: DEFAULT_ENGINE,
|
|
1928
|
+
needsApiKey: true,
|
|
1929
|
+
create: ({ apiKey, model, onUsage }) => new AnthropicDistiller({
|
|
1930
|
+
apiKey,
|
|
1931
|
+
...model ? { model } : {},
|
|
1932
|
+
...onUsage ? { onUsage } : {}
|
|
1933
|
+
})
|
|
1934
|
+
},
|
|
1935
|
+
...Object.fromEntries(
|
|
1936
|
+
Object.values(CLI_ENGINES).map((engine) => [
|
|
1937
|
+
engine.id,
|
|
1938
|
+
{
|
|
1939
|
+
id: engine.id,
|
|
1940
|
+
needsApiKey: false,
|
|
1941
|
+
create: ({ model }) => new CliAgentDistiller({ engine, ...model ? { model } : {} })
|
|
1942
|
+
}
|
|
1943
|
+
])
|
|
1944
|
+
)
|
|
1945
|
+
};
|
|
1946
|
+
function parse2(args) {
|
|
1947
|
+
const out = {
|
|
1948
|
+
workspace: void 0,
|
|
1949
|
+
sourceId: void 0,
|
|
1950
|
+
max: void 0,
|
|
1951
|
+
throttleMs: 1e3,
|
|
1952
|
+
model: void 0,
|
|
1953
|
+
engine: DEFAULT_ENGINE
|
|
1954
|
+
};
|
|
1955
|
+
for (let i = 0; i < args.length; i++) {
|
|
1956
|
+
const a = args[i];
|
|
1957
|
+
const next = () => args[++i];
|
|
1958
|
+
switch (a) {
|
|
1959
|
+
case "--source":
|
|
1960
|
+
out.sourceId = next();
|
|
1961
|
+
break;
|
|
1962
|
+
case "--max": {
|
|
1963
|
+
const v = next();
|
|
1964
|
+
if (v) out.max = Number(v);
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
case "--throttle": {
|
|
1968
|
+
const v = next();
|
|
1969
|
+
if (v) out.throttleMs = Number(v);
|
|
1970
|
+
break;
|
|
1971
|
+
}
|
|
1972
|
+
case "--model":
|
|
1973
|
+
out.model = next();
|
|
1974
|
+
break;
|
|
1975
|
+
case "--engine": {
|
|
1976
|
+
const v = next();
|
|
1977
|
+
if (v) out.engine = v;
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
default:
|
|
1981
|
+
if (!a.startsWith("-") && out.workspace === void 0) out.workspace = a;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
return out;
|
|
1985
|
+
}
|
|
1986
|
+
async function readSources(root) {
|
|
1987
|
+
const dir = join(root, "sources");
|
|
1988
|
+
let entries;
|
|
1989
|
+
try {
|
|
1990
|
+
entries = await readdir(dir, { recursive: true, withFileTypes: true });
|
|
1991
|
+
} catch {
|
|
1992
|
+
return [];
|
|
1993
|
+
}
|
|
1994
|
+
const out = [];
|
|
1995
|
+
for (const e of entries) {
|
|
1996
|
+
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
1997
|
+
const path4 = join(e.parentPath, e.name);
|
|
1998
|
+
try {
|
|
1999
|
+
const parsed = matter2(await readFile(path4, "utf-8"));
|
|
2000
|
+
const fm = SOURCE_FRONTMATTER.parse(parsed.data);
|
|
2001
|
+
if (!fm.id || !parsed.content.trim()) continue;
|
|
2002
|
+
out.push({
|
|
2003
|
+
path: path4,
|
|
2004
|
+
id: fm.id,
|
|
2005
|
+
title: fm.title ?? fm.id,
|
|
2006
|
+
body: parsed.content.trim(),
|
|
2007
|
+
...fm.tags ? { tags: fm.tags } : {},
|
|
2008
|
+
...fm.metadata?.corpus?.access ? { access: fm.metadata.corpus.access } : {},
|
|
2009
|
+
...fm.metadata?.corpus?.domain ? { domain: fm.metadata.corpus.domain } : {}
|
|
2010
|
+
});
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return out;
|
|
2015
|
+
}
|
|
2016
|
+
async function scanDistilledSourceIds(root) {
|
|
2017
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2018
|
+
const dir = join(root, "entries");
|
|
2019
|
+
let entries;
|
|
2020
|
+
try {
|
|
2021
|
+
entries = await readdir(dir, { recursive: true, withFileTypes: true });
|
|
2022
|
+
} catch {
|
|
2023
|
+
return seen;
|
|
2024
|
+
}
|
|
2025
|
+
for (const e of entries) {
|
|
2026
|
+
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
2027
|
+
try {
|
|
2028
|
+
const fm = ENTRY_SOURCES_FRONTMATTER.parse(
|
|
2029
|
+
matter2(await readFile(join(e.parentPath, e.name), "utf-8")).data
|
|
2030
|
+
);
|
|
2031
|
+
if (fm.sources) for (const s of fm.sources) seen.add(s);
|
|
2032
|
+
} catch {
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return seen;
|
|
2036
|
+
}
|
|
2037
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2038
|
+
async function runDistill(args) {
|
|
2039
|
+
const parsed = parse2(args);
|
|
2040
|
+
const target = resolveWorkspacePath(parsed.workspace);
|
|
2041
|
+
const engine = DISTILLER_ENGINES[parsed.engine];
|
|
2042
|
+
if (!engine) {
|
|
2043
|
+
return fail(
|
|
2044
|
+
`unknown --engine "${parsed.engine}". Valid: ${Object.keys(DISTILLER_ENGINES).join(", ")}.`,
|
|
2045
|
+
2
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
2049
|
+
if (engine.needsApiKey && !apiKey) {
|
|
2050
|
+
return fail(`distill engine "${engine.id}" needs ANTHROPIC_API_KEY in the environment.`, 2);
|
|
2051
|
+
}
|
|
2052
|
+
const all = await readSources(target);
|
|
2053
|
+
if (all.length === 0) return fail("no sources found under sources/ \u2014 run import-web first.", 2);
|
|
2054
|
+
const done = await scanDistilledSourceIds(target);
|
|
2055
|
+
const pool = parsed.sourceId ? all.filter((s) => s.id === parsed.sourceId) : all.filter((s) => !done.has(s.id));
|
|
2056
|
+
const batch = parsed.max !== void 0 ? pool.slice(0, parsed.max) : pool;
|
|
2057
|
+
process.stdout.write(
|
|
2058
|
+
`distill \u2192 ${target}
|
|
2059
|
+
engine: ${engine.id}
|
|
2060
|
+
sources: ${all.length} total \xB7 ${all.length - pool.length} already distilled \xB7 ${pool.length} to do
|
|
2061
|
+
this run: ${batch.length}${parsed.max !== void 0 ? ` (--max ${parsed.max})` : ""}
|
|
2062
|
+
`
|
|
2063
|
+
);
|
|
2064
|
+
if (batch.length === 0) {
|
|
2065
|
+
process.stdout.write(" nothing to do.\n");
|
|
2066
|
+
return 0;
|
|
2067
|
+
}
|
|
2068
|
+
const usage = createUsageSink({ runName: basename(target) });
|
|
2069
|
+
const layout = await readEntryLayout(target);
|
|
2070
|
+
const runner = new DistillRunner({
|
|
2071
|
+
fs: new NodeFsAdapter({ root: target }),
|
|
2072
|
+
clock: systemClock,
|
|
2073
|
+
...layout ? { layout } : {},
|
|
2074
|
+
distiller: engine.create({
|
|
2075
|
+
...apiKey ? { apiKey } : {},
|
|
2076
|
+
...parsed.model ? { model: parsed.model } : {},
|
|
2077
|
+
onUsage: usage.record
|
|
2078
|
+
})
|
|
2079
|
+
});
|
|
2080
|
+
let totalEntries = 0;
|
|
2081
|
+
for (let i = 0; i < batch.length; i++) {
|
|
2082
|
+
const src = batch[i];
|
|
2083
|
+
if (i > 0 && parsed.throttleMs > 0) await sleep(parsed.throttleMs);
|
|
2084
|
+
try {
|
|
2085
|
+
const report = await runner.run(src);
|
|
2086
|
+
totalEntries += report.entryPaths.length;
|
|
2087
|
+
process.stdout.write(` \u2713 ${src.id} \u2192 ${report.entryPaths.length} entries
|
|
2088
|
+
`);
|
|
2089
|
+
} catch (e) {
|
|
2090
|
+
process.stdout.write(` ! ${src.id} \u2014 ${e instanceof Error ? e.message : String(e)}
|
|
2091
|
+
`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
process.stdout.write(`
|
|
2095
|
+
${totalEntries} refined entries written (each with sources:[<id>] provenance).
|
|
2096
|
+
`);
|
|
2097
|
+
await usage.flush();
|
|
2098
|
+
return 0;
|
|
2099
|
+
}
|
|
2100
|
+
async function runKnowledge(args) {
|
|
2101
|
+
let workspace;
|
|
2102
|
+
const tags = [];
|
|
2103
|
+
const kinds = [];
|
|
2104
|
+
let access;
|
|
2105
|
+
let max;
|
|
2106
|
+
for (let i = 0; i < args.length; i++) {
|
|
2107
|
+
const a = args[i];
|
|
2108
|
+
const next = () => args[++i];
|
|
2109
|
+
switch (a) {
|
|
2110
|
+
case "--tags": {
|
|
2111
|
+
const v = next();
|
|
2112
|
+
if (v) tags.push(...v.split(",").map((s) => s.trim()).filter(Boolean));
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
2115
|
+
case "--kind": {
|
|
2116
|
+
const v = next();
|
|
2117
|
+
if (v && isRefinedKind(v)) kinds.push(v);
|
|
2118
|
+
break;
|
|
2119
|
+
}
|
|
2120
|
+
case "--access":
|
|
2121
|
+
access = next();
|
|
2122
|
+
break;
|
|
2123
|
+
case "--max": {
|
|
2124
|
+
const v = next();
|
|
2125
|
+
if (v) max = Number(v);
|
|
2126
|
+
break;
|
|
2127
|
+
}
|
|
2128
|
+
default:
|
|
2129
|
+
if (!a.startsWith("-") && workspace === void 0) workspace = a;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
const target = resolveWorkspacePath(workspace);
|
|
2133
|
+
if (tags.length === 0 && kinds.length === 0) {
|
|
2134
|
+
return fail("knowledge needs --tags a,b and/or --kind <k>.", 2);
|
|
2135
|
+
}
|
|
2136
|
+
const query = {
|
|
2137
|
+
...tags.length ? { tags } : {},
|
|
2138
|
+
...kinds.length ? { kinds } : {},
|
|
2139
|
+
...max !== void 0 ? { maxResults: max } : {}
|
|
2140
|
+
};
|
|
2141
|
+
const hits = await resolveKnowledge({
|
|
2142
|
+
fs: new NodeFsAdapter({ root: target }),
|
|
2143
|
+
query,
|
|
2144
|
+
...access ? { allowedAccess: /* @__PURE__ */ new Set([access, "public"]) } : {}
|
|
2145
|
+
});
|
|
2146
|
+
process.stdout.write(
|
|
2147
|
+
`knowledge ${tags.length ? `tags=[${tags.join(",")}] ` : ""}${kinds.length ? `kinds=[${kinds.join(",")}] ` : ""}\u2192 ${hits.length} refined entries
|
|
2148
|
+
|
|
2149
|
+
`
|
|
2150
|
+
);
|
|
2151
|
+
for (const h of hits) {
|
|
2152
|
+
process.stdout.write(
|
|
2153
|
+
` \u2022 [${h.kind}] ${h.title} (conf ${h.confidence})
|
|
2154
|
+
\u21B3 derivedFrom: ${h.sources.join(", ") || "\u2014"}${h.access ? ` \xB7 access: ${h.access}` : ""}
|
|
2155
|
+
${h.body.slice(0, 120).replace(/\s+/g, " ")}\u2026
|
|
2156
|
+
|
|
2157
|
+
`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
return 0;
|
|
2161
|
+
}
|
|
2162
|
+
var RPC_RESPONSE2 = z.object({ result: z.unknown().optional(), error: z.unknown().optional() }).loose();
|
|
2163
|
+
var RPC_MESSAGE = z.object({
|
|
2164
|
+
id: z.number().optional(),
|
|
2165
|
+
result: z.unknown().optional(),
|
|
2166
|
+
error: z.unknown().optional()
|
|
2167
|
+
}).loose();
|
|
2168
|
+
var CALL_RESULT2 = z.object({
|
|
2169
|
+
content: z.unknown().optional(),
|
|
2170
|
+
structuredContent: z.unknown().optional(),
|
|
2171
|
+
isError: z.boolean().optional()
|
|
2172
|
+
}).loose();
|
|
2173
|
+
async function connectMcpHttp(opts) {
|
|
2174
|
+
if (opts.transport === "sse") {
|
|
2175
|
+
const sse = new SseMcpClient(opts);
|
|
2176
|
+
await sse.connect();
|
|
2177
|
+
return sse;
|
|
2178
|
+
}
|
|
2179
|
+
const client = new StreamableHttpMcpClient2(opts);
|
|
2180
|
+
await client.initialize();
|
|
2181
|
+
return client;
|
|
2182
|
+
}
|
|
2183
|
+
var StreamableHttpMcpClient2 = class {
|
|
2184
|
+
id = 0;
|
|
2185
|
+
sessionId;
|
|
2186
|
+
endpoint;
|
|
2187
|
+
headers;
|
|
2188
|
+
protocolVersion;
|
|
2189
|
+
constructor(opts) {
|
|
2190
|
+
this.endpoint = opts.endpoint;
|
|
2191
|
+
this.headers = opts.headers ?? {};
|
|
2192
|
+
this.protocolVersion = opts.protocolVersion ?? "2025-06-18";
|
|
2193
|
+
}
|
|
2194
|
+
async initialize() {
|
|
2195
|
+
const res = await this.rpc({
|
|
2196
|
+
jsonrpc: "2.0",
|
|
2197
|
+
id: ++this.id,
|
|
2198
|
+
method: "initialize",
|
|
2199
|
+
params: {
|
|
2200
|
+
protocolVersion: this.protocolVersion,
|
|
2201
|
+
capabilities: {},
|
|
2202
|
+
clientInfo: { name: "corpus-cli", version: "0.1.0" }
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
await this.rpc({ jsonrpc: "2.0", method: "notifications/initialized", params: {} }, false);
|
|
2206
|
+
if (res?.error) throw new Error(`initialize failed: ${JSON.stringify(res.error)}`);
|
|
2207
|
+
}
|
|
2208
|
+
async callTool(name, args) {
|
|
2209
|
+
const res = await this.rpc({
|
|
2210
|
+
jsonrpc: "2.0",
|
|
2211
|
+
id: ++this.id,
|
|
2212
|
+
method: "tools/call",
|
|
2213
|
+
params: { name, arguments: args }
|
|
2214
|
+
});
|
|
2215
|
+
if (res?.error) throw new Error(`tools/call ${name}: ${JSON.stringify(res.error)}`);
|
|
2216
|
+
const parsed = CALL_RESULT2.safeParse(res?.result ?? {});
|
|
2217
|
+
return parsed.success ? parsed.data : {};
|
|
2218
|
+
}
|
|
2219
|
+
async rpc(body, expectResponse = true) {
|
|
2220
|
+
const res = await fetch(this.endpoint, {
|
|
2221
|
+
method: "POST",
|
|
2222
|
+
headers: {
|
|
2223
|
+
"content-type": "application/json",
|
|
2224
|
+
accept: "application/json, text/event-stream",
|
|
2225
|
+
...this.sessionId ? { "mcp-session-id": this.sessionId } : {},
|
|
2226
|
+
...this.headers
|
|
2227
|
+
},
|
|
2228
|
+
body: JSON.stringify(body)
|
|
2229
|
+
});
|
|
2230
|
+
const sid = res.headers.get("mcp-session-id");
|
|
2231
|
+
if (sid) this.sessionId = sid;
|
|
2232
|
+
if (!expectResponse) return null;
|
|
2233
|
+
if (!res.ok) throw new Error(`MCP HTTP ${res.status} ${res.statusText}`);
|
|
2234
|
+
return parseRpc(await res.text(), res.headers.get("content-type"));
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
var SseMcpClient = class {
|
|
2238
|
+
constructor(opts) {
|
|
2239
|
+
this.opts = opts;
|
|
2240
|
+
this.base = new URL(opts.endpoint);
|
|
2241
|
+
}
|
|
2242
|
+
id = 0;
|
|
2243
|
+
messagesUrl;
|
|
2244
|
+
pending = /* @__PURE__ */ new Map();
|
|
2245
|
+
readyResolve;
|
|
2246
|
+
ready = new Promise((r) => this.readyResolve = r);
|
|
2247
|
+
base;
|
|
2248
|
+
async connect() {
|
|
2249
|
+
const res = await fetch(this.opts.endpoint, {
|
|
2250
|
+
headers: { accept: "text/event-stream", ...this.opts.headers ?? {} }
|
|
2251
|
+
});
|
|
2252
|
+
if (!res.ok || !res.body) throw new Error(`SSE connect ${res.status} ${res.statusText}`);
|
|
2253
|
+
void this.readLoop(res.body.getReader());
|
|
2254
|
+
await withTimeout(this.ready, 1e4, "SSE endpoint event");
|
|
2255
|
+
const init = await this.send("initialize", {
|
|
2256
|
+
protocolVersion: this.opts.protocolVersion ?? "2025-06-18",
|
|
2257
|
+
capabilities: {},
|
|
2258
|
+
clientInfo: { name: "corpus-cli", version: "0.1.0" }
|
|
2259
|
+
});
|
|
2260
|
+
if (init?.error) throw new Error(`initialize failed: ${JSON.stringify(init.error)}`);
|
|
2261
|
+
await this.post({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
|
|
2262
|
+
}
|
|
2263
|
+
async callTool(name, args) {
|
|
2264
|
+
const res = await this.send("tools/call", { name, arguments: args });
|
|
2265
|
+
if (res?.error) throw new Error(`tools/call ${name}: ${JSON.stringify(res.error)}`);
|
|
2266
|
+
const parsed = CALL_RESULT2.safeParse(res?.result ?? {});
|
|
2267
|
+
return parsed.success ? parsed.data : {};
|
|
2268
|
+
}
|
|
2269
|
+
async send(method, params) {
|
|
2270
|
+
const id = ++this.id;
|
|
2271
|
+
const wait = new Promise((resolve) => this.pending.set(id, resolve));
|
|
2272
|
+
await this.post({ jsonrpc: "2.0", id, method, params });
|
|
2273
|
+
return withTimeout(wait, 6e4, `${method} response`);
|
|
2274
|
+
}
|
|
2275
|
+
async post(body) {
|
|
2276
|
+
if (!this.messagesUrl) throw new Error("SSE messages endpoint not ready");
|
|
2277
|
+
const res = await fetch(this.messagesUrl, {
|
|
2278
|
+
method: "POST",
|
|
2279
|
+
headers: { "content-type": "application/json", ...this.opts.headers ?? {} },
|
|
2280
|
+
body: JSON.stringify(body)
|
|
2281
|
+
});
|
|
2282
|
+
if (!res.ok && res.status !== 202) throw new Error(`SSE POST ${res.status}`);
|
|
2283
|
+
}
|
|
2284
|
+
async readLoop(reader) {
|
|
2285
|
+
const dec = new TextDecoder();
|
|
2286
|
+
let buf = "";
|
|
2287
|
+
for (; ; ) {
|
|
2288
|
+
const { done, value } = await reader.read();
|
|
2289
|
+
if (done) break;
|
|
2290
|
+
buf += dec.decode(value, { stream: true });
|
|
2291
|
+
let nl;
|
|
2292
|
+
while ((nl = buf.indexOf("\n\n")) >= 0) {
|
|
2293
|
+
const raw = buf.slice(0, nl);
|
|
2294
|
+
buf = buf.slice(nl + 2);
|
|
2295
|
+
this.handleEvent(raw);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
handleEvent(raw) {
|
|
2300
|
+
let event = "message";
|
|
2301
|
+
const data = [];
|
|
2302
|
+
for (const line of raw.split("\n")) {
|
|
2303
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
2304
|
+
else if (line.startsWith("data:")) data.push(line.slice(5).trim());
|
|
2305
|
+
}
|
|
2306
|
+
const payload = data.join("\n");
|
|
2307
|
+
if (event === "endpoint") {
|
|
2308
|
+
this.messagesUrl = new URL(payload, this.base).toString();
|
|
2309
|
+
this.readyResolve();
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
if (!payload) return;
|
|
2313
|
+
try {
|
|
2314
|
+
const parsed = RPC_MESSAGE.safeParse(JSON.parse(payload));
|
|
2315
|
+
if (!parsed.success) return;
|
|
2316
|
+
const msg4 = parsed.data;
|
|
2317
|
+
if (typeof msg4.id === "number" && this.pending.has(msg4.id)) {
|
|
2318
|
+
this.pending.get(msg4.id)(msg4);
|
|
2319
|
+
this.pending.delete(msg4.id);
|
|
2320
|
+
}
|
|
2321
|
+
} catch {
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
function withTimeout(p, ms, what) {
|
|
2326
|
+
return Promise.race([
|
|
2327
|
+
p,
|
|
2328
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error(`timeout waiting for ${what}`)), ms))
|
|
2329
|
+
]);
|
|
2330
|
+
}
|
|
2331
|
+
function parseRpc(text, contentType) {
|
|
2332
|
+
if (!text.trim()) return null;
|
|
2333
|
+
if (!(contentType ?? "").includes("text/event-stream")) {
|
|
2334
|
+
const parsed = RPC_RESPONSE2.safeParse(JSON.parse(text));
|
|
2335
|
+
return parsed.success ? parsed.data : null;
|
|
2336
|
+
}
|
|
2337
|
+
let last = null;
|
|
2338
|
+
for (const line of text.split("\n")) {
|
|
2339
|
+
const t = line.trim();
|
|
2340
|
+
if (!t.startsWith("data:")) continue;
|
|
2341
|
+
const p = t.slice(5).trim();
|
|
2342
|
+
if (!p || p === "[DONE]") continue;
|
|
2343
|
+
try {
|
|
2344
|
+
last = JSON.parse(p);
|
|
2345
|
+
} catch {
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return last;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// src/ports/mcp-sink.adapter.ts
|
|
2352
|
+
var SINK_CONFIG_SCHEMA = z.object({
|
|
2353
|
+
endpoint: z.string().min(1),
|
|
2354
|
+
tool: z.string().min(1),
|
|
2355
|
+
/** Arg template — substituted per entry. */
|
|
2356
|
+
args: z.record(z.string(), z.unknown()),
|
|
2357
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
2358
|
+
/** MCP transport: "streamable-http" (default) or "sse". */
|
|
2359
|
+
transport: z.enum(["streamable-http", "sse"]).optional(),
|
|
2360
|
+
/**
|
|
2361
|
+
* If set, calls go through `mcp_imported_call({ alias, toolName, args })`
|
|
2362
|
+
* (the agentproto daemon's imported-MCP proxy) instead of the tool directly.
|
|
2363
|
+
*/
|
|
2364
|
+
importedAlias: z.string().optional()
|
|
2365
|
+
});
|
|
2366
|
+
var McpSink = class {
|
|
2367
|
+
constructor(config, client) {
|
|
2368
|
+
this.config = config;
|
|
2369
|
+
this.client = client;
|
|
2370
|
+
}
|
|
2371
|
+
client;
|
|
2372
|
+
async ensure() {
|
|
2373
|
+
if (!this.client) {
|
|
2374
|
+
this.client = await connectMcpHttp({
|
|
2375
|
+
endpoint: this.config.endpoint,
|
|
2376
|
+
...this.config.headers ? { headers: this.config.headers } : {},
|
|
2377
|
+
...this.config.transport ? { transport: this.config.transport } : {}
|
|
2378
|
+
});
|
|
2379
|
+
}
|
|
2380
|
+
return this.client;
|
|
2381
|
+
}
|
|
2382
|
+
async push(item) {
|
|
2383
|
+
let client;
|
|
2384
|
+
try {
|
|
2385
|
+
client = await this.ensure();
|
|
2386
|
+
} catch (e) {
|
|
2387
|
+
return { uri: item.uri, ok: false, error: `connect: ${msg3(e)}` };
|
|
2388
|
+
}
|
|
2389
|
+
const args = template(this.config.args, item);
|
|
2390
|
+
const call = this.config.importedAlias ? { name: "mcp_imported_call", args: { alias: this.config.importedAlias, toolName: this.config.tool, args } } : { name: this.config.tool, args };
|
|
2391
|
+
try {
|
|
2392
|
+
const res = await client.callTool(call.name, call.args);
|
|
2393
|
+
if (res.isError) return { uri: item.uri, ok: false, error: "tool returned isError" };
|
|
2394
|
+
return { uri: item.uri, ok: true };
|
|
2395
|
+
} catch (e) {
|
|
2396
|
+
return { uri: item.uri, ok: false, error: msg3(e) };
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
var PLACEHOLDER_RE = /\$\{(\w+)\}/g;
|
|
2401
|
+
function template(args, item) {
|
|
2402
|
+
const fields = {
|
|
2403
|
+
slug: item.slug,
|
|
2404
|
+
title: item.title,
|
|
2405
|
+
body: item.body,
|
|
2406
|
+
sources: item.sources,
|
|
2407
|
+
tags: item.tags,
|
|
2408
|
+
access: item.access ?? "",
|
|
2409
|
+
uri: item.uri,
|
|
2410
|
+
kind: item.kind,
|
|
2411
|
+
confidence: item.confidence
|
|
2412
|
+
};
|
|
2413
|
+
const out = {};
|
|
2414
|
+
for (const [key, value] of Object.entries(args)) {
|
|
2415
|
+
out[key] = substitute(value, fields);
|
|
2416
|
+
}
|
|
2417
|
+
return out;
|
|
2418
|
+
}
|
|
2419
|
+
function substitute(value, fields) {
|
|
2420
|
+
if (typeof value === "string") {
|
|
2421
|
+
const whole = value.match(/^\$\{(\w+)\}$/);
|
|
2422
|
+
if (whole && whole[1] in fields) return fields[whole[1]];
|
|
2423
|
+
return value.replace(
|
|
2424
|
+
PLACEHOLDER_RE,
|
|
2425
|
+
(_, k) => k in fields ? stringify(fields[k]) : `\${${k}}`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
if (Array.isArray(value)) return value.map((v) => substitute(v, fields));
|
|
2429
|
+
if (value && typeof value === "object") {
|
|
2430
|
+
const out = {};
|
|
2431
|
+
for (const [k, v] of Object.entries(value)) out[k] = substitute(v, fields);
|
|
2432
|
+
return out;
|
|
2433
|
+
}
|
|
2434
|
+
return value;
|
|
2435
|
+
}
|
|
2436
|
+
function stringify(v) {
|
|
2437
|
+
if (Array.isArray(v)) return v.join(", ");
|
|
2438
|
+
return v == null ? "" : String(v);
|
|
2439
|
+
}
|
|
2440
|
+
function msg3(e) {
|
|
2441
|
+
return e instanceof Error ? e.message : String(e);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
// src/commands/sync.ts
|
|
2445
|
+
async function runSync(args) {
|
|
2446
|
+
let workspace;
|
|
2447
|
+
let configPath;
|
|
2448
|
+
const tags = [];
|
|
2449
|
+
const kinds = [];
|
|
2450
|
+
let throttleMs = 500;
|
|
2451
|
+
for (let i = 0; i < args.length; i++) {
|
|
2452
|
+
const a = args[i];
|
|
2453
|
+
const next = () => args[++i];
|
|
2454
|
+
switch (a) {
|
|
2455
|
+
case "--config":
|
|
2456
|
+
configPath = next();
|
|
2457
|
+
break;
|
|
2458
|
+
case "--tags": {
|
|
2459
|
+
const v = next();
|
|
2460
|
+
if (v) tags.push(...v.split(",").map((s) => s.trim()).filter(Boolean));
|
|
2461
|
+
break;
|
|
2462
|
+
}
|
|
2463
|
+
case "--kind": {
|
|
2464
|
+
const v = next();
|
|
2465
|
+
if (v && isRefinedKind(v)) kinds.push(v);
|
|
2466
|
+
break;
|
|
2467
|
+
}
|
|
2468
|
+
case "--throttle": {
|
|
2469
|
+
const v = next();
|
|
2470
|
+
if (v) throttleMs = Number(v);
|
|
2471
|
+
break;
|
|
2472
|
+
}
|
|
2473
|
+
default:
|
|
2474
|
+
if (!a.startsWith("-") && workspace === void 0) workspace = a;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
const target = resolveWorkspacePath(workspace);
|
|
2478
|
+
if (!configPath) return fail("sync needs --config <sink-manifest.json>.", 2);
|
|
2479
|
+
let raw;
|
|
2480
|
+
try {
|
|
2481
|
+
raw = JSON.parse(await readFile(configPath, "utf-8"));
|
|
2482
|
+
} catch (e) {
|
|
2483
|
+
return fail(`could not read sink config: ${e instanceof Error ? e.message : String(e)}`, 2);
|
|
2484
|
+
}
|
|
2485
|
+
const parsedConfig = SINK_CONFIG_SCHEMA.safeParse(raw);
|
|
2486
|
+
if (!parsedConfig.success) {
|
|
2487
|
+
return fail(
|
|
2488
|
+
`invalid sink config (needs endpoint, tool, args): ${parsedConfig.error.issues[0]?.message ?? "schema mismatch"}`,
|
|
2489
|
+
2
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
const config = parsedConfig.data;
|
|
2493
|
+
const select = {
|
|
2494
|
+
...tags.length ? { tags } : {},
|
|
2495
|
+
...kinds.length ? { kinds } : {}
|
|
2496
|
+
};
|
|
2497
|
+
const report = await new SyncRunner({
|
|
2498
|
+
fs: new NodeFsAdapter({ root: target }),
|
|
2499
|
+
sink: new McpSink(config),
|
|
2500
|
+
select,
|
|
2501
|
+
throttleMs
|
|
2502
|
+
}).run();
|
|
2503
|
+
process.stdout.write(
|
|
2504
|
+
`sync \u2192 ${config.endpoint} (tool: ${config.tool})
|
|
2505
|
+
pushed: ${report.pushed} \xB7 skipped: ${report.skipped} \xB7 failed: ${report.failed}
|
|
2506
|
+
`
|
|
2507
|
+
);
|
|
2508
|
+
for (const r of report.results.filter((r2) => !r2.ok).slice(0, 10)) {
|
|
2509
|
+
process.stdout.write(` ! ${r.uri} \u2014 ${r.error}
|
|
2510
|
+
`);
|
|
2511
|
+
}
|
|
2512
|
+
return report.failed > 0 && report.pushed === 0 ? 1 : 0;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/version.ts
|
|
2516
|
+
var VERSION = "0.1.0-alpha.0";
|
|
2517
|
+
|
|
2518
|
+
// src/cli.ts
|
|
2519
|
+
var HELP = `corpus \u2014 AIP-10 corpus workspace operator (v${VERSION})
|
|
2520
|
+
|
|
2521
|
+
Commands:
|
|
2522
|
+
init <name> [path] [--with a,b] [--preset <slug>]
|
|
2523
|
+
Scaffold a corpus \u2014 bare AIP-10 by default
|
|
2524
|
+
(--preset seeds a full vertical; --list shows them)
|
|
2525
|
+
validate [path] JSON Schema check across every AIP file
|
|
2526
|
+
lint [path] Run lints declared in KNOWLEDGE.md
|
|
2527
|
+
events:emit <kind> --payload <json> [path]
|
|
2528
|
+
Append an event to _log.md
|
|
2529
|
+
events:tail [path] Print _log.md
|
|
2530
|
+
import-web [path] --urls-file <f> [--max n --max-duration s --throttle ms --tags t --lang l --force --diarize]
|
|
2531
|
+
Import URLs as sources (video\u2192Whisper, article\u2192
|
|
2532
|
+
readability). Resumable: skips already-ingested
|
|
2533
|
+
URLs, so re-run with --max N to batch through.
|
|
2534
|
+
--max-duration: skip videos longer than s seconds
|
|
2535
|
+
(no download); omit for no cap (long media is
|
|
2536
|
+
segmented under Whisper's 25 MB limit).
|
|
2537
|
+
--cookies-from-browser <b>: auth yt-dlp from a
|
|
2538
|
+
local browser (chrome/firefox) to dodge YouTube's
|
|
2539
|
+
bot-check; --cookies <file> for a cookies.txt.
|
|
2540
|
+
--scrape-mcp <url>: delegate article fetching to a
|
|
2541
|
+
scrape MCP server (stealth + clean Markdown) for
|
|
2542
|
+
walled/JS pages, ahead of plain readability.
|
|
2543
|
+
--diarize: AssemblyAI speaker labels (interviews).
|
|
2544
|
+
distill [path] [--source id --max n --throttle ms --model m]
|
|
2545
|
+
Distill raw sources \u2192 refined entries
|
|
2546
|
+
(principle/pattern/\u2026) with sources:[id] provenance.
|
|
2547
|
+
Resumable: skips already-distilled sources.
|
|
2548
|
+
knowledge [path] --tags a,b [--kind k --access scope --max n]
|
|
2549
|
+
Preview what a skill's knowledge: binding resolves to
|
|
2550
|
+
\u2014 refined entries + their provenance (filesystem).
|
|
2551
|
+
sync [path] --config <sink.json> [--tags a,b --kind k --throttle ms]
|
|
2552
|
+
Push refined entries to an external store via a
|
|
2553
|
+
config-driven MCP sink (host-agnostic).
|
|
2554
|
+
-h, --help Show this help
|
|
2555
|
+
-v, --version Show version
|
|
2556
|
+
|
|
2557
|
+
Conventions:
|
|
2558
|
+
[path] defaults to the current working directory.
|
|
2559
|
+
All commands target the workspace whose root contains KNOWLEDGE.md
|
|
2560
|
+
(except 'init' which creates one).
|
|
2561
|
+
`;
|
|
2562
|
+
async function main(argv2) {
|
|
2563
|
+
const [cmd, ...rest] = argv2;
|
|
2564
|
+
if (!cmd || cmd === "-h" || cmd === "--help") {
|
|
2565
|
+
process.stdout.write(HELP);
|
|
2566
|
+
return 0;
|
|
2567
|
+
}
|
|
2568
|
+
if (cmd === "-v" || cmd === "--version") {
|
|
2569
|
+
process.stdout.write(`corpus v${VERSION}
|
|
2570
|
+
`);
|
|
2571
|
+
return 0;
|
|
2572
|
+
}
|
|
2573
|
+
switch (cmd) {
|
|
2574
|
+
case "init":
|
|
2575
|
+
return await runInit(rest);
|
|
2576
|
+
case "validate":
|
|
2577
|
+
return await runValidate(rest);
|
|
2578
|
+
case "lint":
|
|
2579
|
+
return await runLint(rest);
|
|
2580
|
+
case "events:emit":
|
|
2581
|
+
return await runEventsEmit(rest);
|
|
2582
|
+
case "events:tail":
|
|
2583
|
+
return await runEventsTail(rest);
|
|
2584
|
+
case "import-web":
|
|
2585
|
+
return await runImportWeb(rest);
|
|
2586
|
+
case "distill":
|
|
2587
|
+
return await runDistill(rest);
|
|
2588
|
+
case "knowledge":
|
|
2589
|
+
return await runKnowledge(rest);
|
|
2590
|
+
case "sync":
|
|
2591
|
+
return await runSync(rest);
|
|
2592
|
+
default:
|
|
2593
|
+
process.stderr.write(`corpus: unknown command "${cmd}". Try --help.
|
|
2594
|
+
`);
|
|
2595
|
+
return 2;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
var argv = process.argv.slice(2);
|
|
2599
|
+
main(argv).then((code) => process.exit(code)).catch((err) => {
|
|
2600
|
+
process.stderr.write(
|
|
2601
|
+
`corpus: unhandled error \u2014 ${err instanceof Error ? err.message : String(err)}
|
|
2602
|
+
`
|
|
2603
|
+
);
|
|
2604
|
+
process.exit(1);
|
|
2605
|
+
});
|
|
2606
|
+
//# sourceMappingURL=cli.mjs.map
|
|
2607
|
+
//# sourceMappingURL=cli.mjs.map
|