@agentproto/corpus-cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -21
- package/README.md +2 -0
- package/dist/cli.mjs +394 -46
- package/dist/cli.mjs.map +1 -1
- package/dist/specs/resources/aip-45/draft/AGENT-CLI.schema.json +67 -0
- package/package.json +5 -5
package/dist/cli.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import matter2 from 'gray-matter';
|
|
|
12
12
|
import { execFile } from 'child_process';
|
|
13
13
|
import { promisify } from 'util';
|
|
14
14
|
import { parseClaudeJsonOutput, spawnWithStdin } from '@agentproto/cli-exec';
|
|
15
|
-
import { stitchReport, buildPacks, reportConfigSchema } from '@agentproto/corpus/report';
|
|
15
|
+
import { stitchReport, buildPacks, lintReportConfig, reportConfigSchema } from '@agentproto/corpus/report';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* @agentproto/corpus-cli v0.1.0-alpha
|
|
@@ -104,9 +104,9 @@ function resolvePackageJson(packageName) {
|
|
|
104
104
|
}
|
|
105
105
|
return null;
|
|
106
106
|
}
|
|
107
|
-
async function readJsonIfExists(
|
|
107
|
+
async function readJsonIfExists(path6) {
|
|
108
108
|
try {
|
|
109
|
-
const raw = await readFile(
|
|
109
|
+
const raw = await readFile(path6, "utf8");
|
|
110
110
|
return JSON.parse(raw);
|
|
111
111
|
} catch (err) {
|
|
112
112
|
if (err.code === "ENOENT") return void 0;
|
|
@@ -115,9 +115,9 @@ async function readJsonIfExists(path5) {
|
|
|
115
115
|
}
|
|
116
116
|
async function readConfiguredPresetPackages() {
|
|
117
117
|
const base = process.env["AGENTPROTO_HOME"] ?? join(homedir(), ".agentproto");
|
|
118
|
-
const
|
|
118
|
+
const path6 = join(base, "config.json");
|
|
119
119
|
try {
|
|
120
|
-
const raw = await readFile(
|
|
120
|
+
const raw = await readFile(path6, "utf8");
|
|
121
121
|
const parsed = JSON.parse(raw);
|
|
122
122
|
const list = parsed.corpusPresetPackages;
|
|
123
123
|
if (Array.isArray(list) && list.length > 0) return list;
|
|
@@ -404,8 +404,8 @@ function resolveWorkspacePath(arg) {
|
|
|
404
404
|
if (!arg) return process.cwd();
|
|
405
405
|
return path.resolve(process.cwd(), arg);
|
|
406
406
|
}
|
|
407
|
-
function fail(
|
|
408
|
-
process.stderr.write(`corpus: ${
|
|
407
|
+
function fail(msg5, code = 1) {
|
|
408
|
+
process.stderr.write(`corpus: ${msg5}
|
|
409
409
|
`);
|
|
410
410
|
return code;
|
|
411
411
|
}
|
|
@@ -631,6 +631,160 @@ async function runLint(args) {
|
|
|
631
631
|
);
|
|
632
632
|
return report.errorCount > 0 ? 1 : 0;
|
|
633
633
|
}
|
|
634
|
+
var FLAG_PATTERNS = [
|
|
635
|
+
/does not match/i,
|
|
636
|
+
/content is about/i,
|
|
637
|
+
/no substantive content/i,
|
|
638
|
+
/content mismatch/i,
|
|
639
|
+
/appears to be a (?:newsletter|navigation|login|error) page/i,
|
|
640
|
+
/stated title/i,
|
|
641
|
+
/placeholder page/i,
|
|
642
|
+
/cookie consent/i,
|
|
643
|
+
/page could not be/i
|
|
644
|
+
];
|
|
645
|
+
function parse(args) {
|
|
646
|
+
const out = {
|
|
647
|
+
workspace: void 0,
|
|
648
|
+
facets: [],
|
|
649
|
+
thin: 8,
|
|
650
|
+
apply: false,
|
|
651
|
+
contaminated: false
|
|
652
|
+
};
|
|
653
|
+
for (let i = 0; i < args.length; i++) {
|
|
654
|
+
const a = args[i];
|
|
655
|
+
const next = () => args[++i];
|
|
656
|
+
switch (a) {
|
|
657
|
+
case "--facets": {
|
|
658
|
+
const v = next();
|
|
659
|
+
if (v) out.facets = v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case "--thin": {
|
|
663
|
+
const v = next();
|
|
664
|
+
if (v) out.thin = Number(v);
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
case "--apply":
|
|
668
|
+
out.apply = true;
|
|
669
|
+
break;
|
|
670
|
+
case "--contaminated":
|
|
671
|
+
out.contaminated = true;
|
|
672
|
+
break;
|
|
673
|
+
default:
|
|
674
|
+
if (!a.startsWith("-") && out.workspace === void 0) out.workspace = a;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return out;
|
|
678
|
+
}
|
|
679
|
+
function firstFlag(haystack) {
|
|
680
|
+
for (const re of FLAG_PATTERNS) {
|
|
681
|
+
const m = haystack.match(re);
|
|
682
|
+
if (m) return m[0];
|
|
683
|
+
}
|
|
684
|
+
return void 0;
|
|
685
|
+
}
|
|
686
|
+
function entryStatus(entry) {
|
|
687
|
+
const corpus = entry.frontmatter.metadata?.corpus;
|
|
688
|
+
return corpus?.status ?? "active";
|
|
689
|
+
}
|
|
690
|
+
function tagsOf(entry) {
|
|
691
|
+
const tags = entry.frontmatter.tags;
|
|
692
|
+
return Array.isArray(tags) ? tags.map(String) : [];
|
|
693
|
+
}
|
|
694
|
+
function sourcesOf(entry) {
|
|
695
|
+
const sources = entry.frontmatter.sources;
|
|
696
|
+
return Array.isArray(sources) ? sources.map(String) : [];
|
|
697
|
+
}
|
|
698
|
+
async function runVerify(args) {
|
|
699
|
+
const parsed = parse(args);
|
|
700
|
+
if (parsed.facets.length === 0) {
|
|
701
|
+
return fail(
|
|
702
|
+
"verify requires --facets a,b,c. Usage: corpus verify <workspace> --facets landscape,daemons,... [--thin 8] [--apply] [--contaminated]",
|
|
703
|
+
2
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
const target = resolveWorkspacePath(parsed.workspace);
|
|
707
|
+
const fs = new NodeFsAdapter({ root: target });
|
|
708
|
+
if (!await fs.exists("entries")) {
|
|
709
|
+
return fail(`verify: no entries/ under ${target}`, 1);
|
|
710
|
+
}
|
|
711
|
+
const snapshot = await new CorpusWorkspaceReader({ fs }).read("");
|
|
712
|
+
const entries = snapshot.entries.filter((e) => entryStatus(e) === "active");
|
|
713
|
+
const perFacet = /* @__PURE__ */ new Map();
|
|
714
|
+
for (const facet of parsed.facets) perFacet.set(facet, 0);
|
|
715
|
+
for (const entry of entries) {
|
|
716
|
+
for (const tag of tagsOf(entry)) {
|
|
717
|
+
if (perFacet.has(tag)) perFacet.set(tag, (perFacet.get(tag) ?? 0) + 1);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
process.stdout.write(`== coverage (${entries.length} entries) ==
|
|
721
|
+
`);
|
|
722
|
+
const thin = [];
|
|
723
|
+
for (const facet of parsed.facets) {
|
|
724
|
+
const n = perFacet.get(facet) ?? 0;
|
|
725
|
+
if (n < parsed.thin) thin.push(facet);
|
|
726
|
+
const mark = n < parsed.thin ? " THIN \u26A0" : "";
|
|
727
|
+
process.stdout.write(` ${facet.padEnd(16)} ${String(n).padStart(4)}${mark}
|
|
728
|
+
`);
|
|
729
|
+
}
|
|
730
|
+
const flagged = [];
|
|
731
|
+
for (const entry of entries) {
|
|
732
|
+
const title = String(entry.frontmatter.title ?? "");
|
|
733
|
+
const why = firstFlag(`${title}
|
|
734
|
+
${entry.body}`);
|
|
735
|
+
if (why) flagged.push({ entry, why });
|
|
736
|
+
}
|
|
737
|
+
process.stdout.write(`
|
|
738
|
+
== self-flagged entries (${flagged.length}) ==
|
|
739
|
+
`);
|
|
740
|
+
const poisonedSources = /* @__PURE__ */ new Set();
|
|
741
|
+
for (const { entry, why } of flagged) {
|
|
742
|
+
process.stdout.write(` ${entry.path} [${why}]
|
|
743
|
+
`);
|
|
744
|
+
for (const s of sourcesOf(entry)) poisonedSources.add(s);
|
|
745
|
+
}
|
|
746
|
+
const flaggedPaths = new Set(flagged.map((f) => f.entry.path));
|
|
747
|
+
const contaminated = [];
|
|
748
|
+
if (parsed.contaminated && poisonedSources.size > 0) {
|
|
749
|
+
for (const entry of entries) {
|
|
750
|
+
if (flaggedPaths.has(entry.path)) continue;
|
|
751
|
+
if (sourcesOf(entry).some((s) => poisonedSources.has(s))) {
|
|
752
|
+
contaminated.push(entry);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
process.stdout.write(
|
|
756
|
+
`
|
|
757
|
+
== contaminated siblings (${contaminated.length}) from ${poisonedSources.size} poisoned source(s) ==
|
|
758
|
+
`
|
|
759
|
+
);
|
|
760
|
+
for (const entry of contaminated) {
|
|
761
|
+
process.stdout.write(` ${entry.path}
|
|
762
|
+
`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (parsed.apply) {
|
|
766
|
+
const toMove = [...flagged.map((f) => f.entry), ...contaminated];
|
|
767
|
+
for (const entry of toMove) {
|
|
768
|
+
const rel = path.relative("entries", entry.path);
|
|
769
|
+
const dest = path.join(target, "demoted", rel);
|
|
770
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
771
|
+
await rename(path.join(target, entry.path), dest);
|
|
772
|
+
}
|
|
773
|
+
process.stdout.write(`
|
|
774
|
+
moved ${toMove.length} entries \u2192 demoted/
|
|
775
|
+
`);
|
|
776
|
+
} else if (flagged.length > 0) {
|
|
777
|
+
process.stdout.write(
|
|
778
|
+
"\n(re-run with --apply to quarantine; --contaminated to also demote siblings)\n"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
if (thin.length > 0) {
|
|
782
|
+
process.stdout.write(`
|
|
783
|
+
thin facets \u2192 loop back to \u2461 DISCOVER: ${thin.join(", ")}
|
|
784
|
+
`);
|
|
785
|
+
}
|
|
786
|
+
return 0;
|
|
787
|
+
}
|
|
634
788
|
var OsIdentityAdapter = class {
|
|
635
789
|
cached;
|
|
636
790
|
constructor(opts) {
|
|
@@ -1040,6 +1194,146 @@ function defaultYtDlpDownloader(bin, opts) {
|
|
|
1040
1194
|
}
|
|
1041
1195
|
};
|
|
1042
1196
|
}
|
|
1197
|
+
var execFileAsync2 = promisify(execFile);
|
|
1198
|
+
var YTDLP_INFO2 = z.object({ title: z.string().optional(), language: z.string().optional() }).loose();
|
|
1199
|
+
var YtDlpCaptionsFetcher = class {
|
|
1200
|
+
download;
|
|
1201
|
+
extraHosts;
|
|
1202
|
+
minChars;
|
|
1203
|
+
constructor(opts = {}) {
|
|
1204
|
+
this.minChars = opts.minChars ?? 400;
|
|
1205
|
+
this.download = opts.download ?? defaultYtDlpCaptionDownloader(opts.ytDlpBin ?? "yt-dlp", {
|
|
1206
|
+
...opts.subLangs ? { subLangs: opts.subLangs } : {},
|
|
1207
|
+
...opts.preferLang ? { preferLang: opts.preferLang } : {},
|
|
1208
|
+
...opts.maxDurationSec ? { maxDurationSec: opts.maxDurationSec } : {},
|
|
1209
|
+
...opts.cookiesFromBrowser ? { cookiesFromBrowser: opts.cookiesFromBrowser } : {},
|
|
1210
|
+
...opts.cookiesFile ? { cookiesFile: opts.cookiesFile } : {}
|
|
1211
|
+
});
|
|
1212
|
+
this.extraHosts = opts.extraVideoHosts ? new Set(opts.extraVideoHosts) : void 0;
|
|
1213
|
+
}
|
|
1214
|
+
async fetch(url) {
|
|
1215
|
+
if (!isVideoUrl(url, this.extraHosts)) return null;
|
|
1216
|
+
let dl;
|
|
1217
|
+
try {
|
|
1218
|
+
dl = await this.download(url);
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
process.stderr.write(
|
|
1221
|
+
`corpus: caption fetch failed for ${url} \u2014 falling back (${msg2(e)})
|
|
1222
|
+
`
|
|
1223
|
+
);
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
if (!dl.vttPath) return null;
|
|
1228
|
+
const text = parseVttToText(await readFile(dl.vttPath, "utf-8"));
|
|
1229
|
+
if (text.length < this.minChars) return null;
|
|
1230
|
+
return {
|
|
1231
|
+
title: dl.title || url,
|
|
1232
|
+
text,
|
|
1233
|
+
kind: "video",
|
|
1234
|
+
...dl.language ? { language: dl.language } : {},
|
|
1235
|
+
via: "captions"
|
|
1236
|
+
};
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
1239
|
+
} finally {
|
|
1240
|
+
await dl.cleanup().catch(() => {
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
function msg2(e) {
|
|
1246
|
+
return e instanceof Error ? e.message : String(e);
|
|
1247
|
+
}
|
|
1248
|
+
function parseVttToText(vtt) {
|
|
1249
|
+
const out = [];
|
|
1250
|
+
for (const raw of vtt.split(/\r?\n/)) {
|
|
1251
|
+
if (/^WEBVTT/.test(raw)) continue;
|
|
1252
|
+
if (/^(Kind|Language|NOTE|STYLE):/i.test(raw)) continue;
|
|
1253
|
+
if (raw.includes("-->")) continue;
|
|
1254
|
+
const line = raw.replace(/<[^>]+>/g, "").replace(/ /gi, " ").replace(/ /g, " ").trim();
|
|
1255
|
+
if (!line) continue;
|
|
1256
|
+
if (out.length && out[out.length - 1] === line) continue;
|
|
1257
|
+
out.push(line);
|
|
1258
|
+
}
|
|
1259
|
+
const cleaned = [];
|
|
1260
|
+
for (let i = 0; i < out.length; i++) {
|
|
1261
|
+
const next = out[i + 1];
|
|
1262
|
+
if (next && next.startsWith(out[i])) continue;
|
|
1263
|
+
cleaned.push(out[i]);
|
|
1264
|
+
}
|
|
1265
|
+
return cleaned.join(" ").replace(/\s+/g, " ").trim();
|
|
1266
|
+
}
|
|
1267
|
+
function buildYtDlpCaptionArgs(url, dir, opts) {
|
|
1268
|
+
const subLangs = opts?.subLangs ?? (opts?.preferLang ? `${opts.preferLang}.*` : "en.*,fr.*,es.*,de.*,it.*,pt.*,nl.*");
|
|
1269
|
+
const durationGuard = opts?.maxDurationSec && opts.maxDurationSec > 0 ? ["--match-filter", `duration <= ${Math.floor(opts.maxDurationSec)}`] : [];
|
|
1270
|
+
const cookieArgs = opts?.cookiesFromBrowser ? ["--cookies-from-browser", opts.cookiesFromBrowser] : opts?.cookiesFile ? ["--cookies", opts.cookiesFile] : [];
|
|
1271
|
+
return [
|
|
1272
|
+
"--skip-download",
|
|
1273
|
+
"--write-auto-subs",
|
|
1274
|
+
"--write-subs",
|
|
1275
|
+
"--sub-langs",
|
|
1276
|
+
subLangs,
|
|
1277
|
+
"--sub-format",
|
|
1278
|
+
"vtt",
|
|
1279
|
+
"--no-playlist",
|
|
1280
|
+
"--no-progress",
|
|
1281
|
+
"--sleep-requests",
|
|
1282
|
+
"1",
|
|
1283
|
+
...durationGuard,
|
|
1284
|
+
...cookieArgs,
|
|
1285
|
+
"--write-info-json",
|
|
1286
|
+
"-o",
|
|
1287
|
+
join(dir, "%(id)s.%(ext)s"),
|
|
1288
|
+
url
|
|
1289
|
+
];
|
|
1290
|
+
}
|
|
1291
|
+
function pickVtt(files, preferLang) {
|
|
1292
|
+
const vtts = files.filter((f) => f.endsWith(".vtt"));
|
|
1293
|
+
if (vtts.length === 0) return void 0;
|
|
1294
|
+
const bySuffix = (suffix) => vtts.find((f) => f.toLowerCase().endsWith(suffix.toLowerCase()));
|
|
1295
|
+
if (preferLang) {
|
|
1296
|
+
return bySuffix(`.${preferLang}-orig.vtt`) ?? bySuffix(`.${preferLang}.vtt`) ?? vtts.find((f) => new RegExp(`\\.${preferLang}[.-]`, "i").test(f)) ?? vtts.find((f) => /-orig\.vtt$/i.test(f)) ?? vtts[0];
|
|
1297
|
+
}
|
|
1298
|
+
return vtts.find((f) => /-orig\.vtt$/i.test(f)) ?? vtts[0];
|
|
1299
|
+
}
|
|
1300
|
+
function defaultYtDlpCaptionDownloader(bin, opts) {
|
|
1301
|
+
return async (url) => {
|
|
1302
|
+
const dir = await mkdtemp(join(tmpdir(), "corpus-captions-"));
|
|
1303
|
+
const cleanup = () => rm(dir, { recursive: true, force: true });
|
|
1304
|
+
try {
|
|
1305
|
+
await execFileAsync2(bin, buildYtDlpCaptionArgs(url, dir, opts), {
|
|
1306
|
+
maxBuffer: 16 * 1024 * 1024
|
|
1307
|
+
}).catch(() => {
|
|
1308
|
+
});
|
|
1309
|
+
const files = await readdir(dir);
|
|
1310
|
+
const vtt = pickVtt(files, opts?.preferLang);
|
|
1311
|
+
let title = url;
|
|
1312
|
+
let language;
|
|
1313
|
+
const infoName = files.find((f) => f.endsWith(".info.json"));
|
|
1314
|
+
if (infoName) {
|
|
1315
|
+
try {
|
|
1316
|
+
const info = YTDLP_INFO2.parse(
|
|
1317
|
+
JSON.parse(await readFile(join(dir, infoName), "utf-8"))
|
|
1318
|
+
);
|
|
1319
|
+
if (info.title) title = info.title;
|
|
1320
|
+
if (info.language) language = info.language;
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
vttPath: vtt ? join(dir, vtt) : null,
|
|
1326
|
+
title,
|
|
1327
|
+
...language ? { language } : {},
|
|
1328
|
+
cleanup
|
|
1329
|
+
};
|
|
1330
|
+
} catch (e) {
|
|
1331
|
+
await cleanup().catch(() => {
|
|
1332
|
+
});
|
|
1333
|
+
throw e;
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1043
1337
|
var WHISPER_RESPONSE = z.object({ text: z.string().optional(), language: z.string().optional() }).loose();
|
|
1044
1338
|
var WHISPER_MAX_BYTES = 25 * 1024 * 1024;
|
|
1045
1339
|
var OpenAiWhisperStt = class {
|
|
@@ -1157,18 +1451,18 @@ var AssemblyAiStt = class {
|
|
|
1157
1451
|
...job.language_code ? { language: job.language_code } : {}
|
|
1158
1452
|
};
|
|
1159
1453
|
}
|
|
1160
|
-
async post(
|
|
1161
|
-
const r = await fetch(`${this.baseUrl}${
|
|
1454
|
+
async post(path6, body) {
|
|
1455
|
+
const r = await fetch(`${this.baseUrl}${path6}`, {
|
|
1162
1456
|
method: "POST",
|
|
1163
1457
|
headers: { authorization: this.apiKey, "content-type": "application/json" },
|
|
1164
1458
|
body: JSON.stringify(body)
|
|
1165
1459
|
});
|
|
1166
|
-
if (!r.ok) throw new Error(`AssemblyAI POST ${
|
|
1460
|
+
if (!r.ok) throw new Error(`AssemblyAI POST ${path6} ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
1167
1461
|
return AAI_TRANSCRIPT.parse(await r.json());
|
|
1168
1462
|
}
|
|
1169
|
-
async get(
|
|
1170
|
-
const r = await fetch(`${this.baseUrl}${
|
|
1171
|
-
if (!r.ok) throw new Error(`AssemblyAI GET ${
|
|
1463
|
+
async get(path6) {
|
|
1464
|
+
const r = await fetch(`${this.baseUrl}${path6}`, { headers: { authorization: this.apiKey } });
|
|
1465
|
+
if (!r.ok) throw new Error(`AssemblyAI GET ${path6} ${r.status}: ${(await r.text()).slice(0, 200)}`);
|
|
1172
1466
|
return AAI_TRANSCRIPT.parse(await r.json());
|
|
1173
1467
|
}
|
|
1174
1468
|
};
|
|
@@ -1178,7 +1472,7 @@ function formatDiarized(job) {
|
|
|
1178
1472
|
}
|
|
1179
1473
|
return (job.text ?? "").trim();
|
|
1180
1474
|
}
|
|
1181
|
-
var
|
|
1475
|
+
var execFileAsync3 = promisify(execFile);
|
|
1182
1476
|
var DEFAULT_MAX_BYTES = 24 * 1024 * 1024;
|
|
1183
1477
|
var DEFAULT_SEGMENT_SECONDS = 1200;
|
|
1184
1478
|
var ChunkedStt = class {
|
|
@@ -1237,7 +1531,7 @@ var ChunkedStt = class {
|
|
|
1237
1531
|
function defaultFfmpegSplitter(bin) {
|
|
1238
1532
|
return async (audioPath, segmentSeconds, outDir) => {
|
|
1239
1533
|
const ext = extname(audioPath) || ".mp3";
|
|
1240
|
-
await
|
|
1534
|
+
await execFileAsync3(
|
|
1241
1535
|
bin,
|
|
1242
1536
|
[
|
|
1243
1537
|
"-hide_banner",
|
|
@@ -1453,7 +1747,7 @@ var SOURCE_URL_FRONTMATTER = z.object({
|
|
|
1453
1747
|
}).loose().optional().catch(void 0)
|
|
1454
1748
|
}).loose().optional().catch(void 0)
|
|
1455
1749
|
}).loose();
|
|
1456
|
-
function
|
|
1750
|
+
function parse2(args) {
|
|
1457
1751
|
const out = {
|
|
1458
1752
|
workspace: void 0,
|
|
1459
1753
|
urlsFile: void 0,
|
|
@@ -1468,6 +1762,7 @@ function parse(args) {
|
|
|
1468
1762
|
throttleMs: 2e3,
|
|
1469
1763
|
force: false,
|
|
1470
1764
|
diarize: false,
|
|
1765
|
+
noCaptions: false,
|
|
1471
1766
|
browserMcp: process.env.BROWSER_MCP_URL,
|
|
1472
1767
|
scrapeMcp: process.env.SCRAPE_MCP_URL,
|
|
1473
1768
|
importerId: "web",
|
|
@@ -1523,6 +1818,9 @@ function parse(args) {
|
|
|
1523
1818
|
case "--diarize":
|
|
1524
1819
|
out.diarize = true;
|
|
1525
1820
|
break;
|
|
1821
|
+
case "--no-captions":
|
|
1822
|
+
out.noCaptions = true;
|
|
1823
|
+
break;
|
|
1526
1824
|
case "--browser-mcp":
|
|
1527
1825
|
out.browserMcp = next();
|
|
1528
1826
|
break;
|
|
@@ -1579,7 +1877,7 @@ async function scanIngestedUrls(workspaceRoot) {
|
|
|
1579
1877
|
return seen;
|
|
1580
1878
|
}
|
|
1581
1879
|
async function runImportWeb(args) {
|
|
1582
|
-
const parsed =
|
|
1880
|
+
const parsed = parse2(args);
|
|
1583
1881
|
const target = resolveWorkspacePath(parsed.workspace);
|
|
1584
1882
|
const urls = await readUrls(parsed);
|
|
1585
1883
|
if (urls.length === 0) {
|
|
@@ -1589,10 +1887,11 @@ async function runImportWeb(args) {
|
|
|
1589
1887
|
const todo = urls.filter((u) => !done.has(u));
|
|
1590
1888
|
const batch = parsed.max !== void 0 ? todo.slice(0, parsed.max) : todo;
|
|
1591
1889
|
const remaining = todo.length - batch.length;
|
|
1890
|
+
const videoLabel = parsed.noCaptions ? parsed.diarize ? "AssemblyAI" : "Whisper" : `captions-first \u2192 ${parsed.diarize ? "AssemblyAI" : "Whisper"} fallback`;
|
|
1592
1891
|
const plan = ` workspace: ${target}
|
|
1593
1892
|
urls: ${urls.length} total \xB7 ${done.size && !parsed.force ? `${urls.length - todo.length} already ingested \xB7 ` : ""}${todo.length} to do
|
|
1594
1893
|
this run: ${batch.length}${parsed.max !== void 0 ? ` (--max ${parsed.max})` : ""} \xB7 ${remaining} remaining after
|
|
1595
|
-
video
|
|
1894
|
+
video: ${videoLabel}
|
|
1596
1895
|
throttle: ${parsed.throttleMs} ms between fetches
|
|
1597
1896
|
`;
|
|
1598
1897
|
if (batch.length === 0) {
|
|
@@ -1613,6 +1912,16 @@ ${plan}`);
|
|
|
1613
1912
|
process.stdout.write(`import-web
|
|
1614
1913
|
${plan}`);
|
|
1615
1914
|
const chain = [];
|
|
1915
|
+
if (!parsed.noCaptions) {
|
|
1916
|
+
chain.push(
|
|
1917
|
+
new YtDlpCaptionsFetcher({
|
|
1918
|
+
...parsed.lang ? { preferLang: parsed.lang } : {},
|
|
1919
|
+
...parsed.maxDurationSec ? { maxDurationSec: parsed.maxDurationSec } : {},
|
|
1920
|
+
...parsed.cookiesFromBrowser ? { cookiesFromBrowser: parsed.cookiesFromBrowser } : {},
|
|
1921
|
+
...parsed.cookiesFile ? { cookiesFile: parsed.cookiesFile } : {}
|
|
1922
|
+
})
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1616
1925
|
let stt;
|
|
1617
1926
|
if (parsed.diarize) {
|
|
1618
1927
|
const aaiKey = process.env.ASSEMBLYAI_API_KEY;
|
|
@@ -1627,7 +1936,7 @@ ${plan}`);
|
|
|
1627
1936
|
stt = new ChunkedStt({ base: new OpenAiWhisperStt({ apiKey: openaiKey }) });
|
|
1628
1937
|
else
|
|
1629
1938
|
process.stderr.write(
|
|
1630
|
-
"corpus: OPENAI_API_KEY not set \u2014 video URLs will be skipped (no transcription).\n"
|
|
1939
|
+
parsed.noCaptions ? "corpus: OPENAI_API_KEY not set \u2014 video URLs will be skipped (no transcription).\n" : "corpus: OPENAI_API_KEY not set \u2014 caption-less videos will be skipped (captioned videos still import).\n"
|
|
1631
1940
|
);
|
|
1632
1941
|
}
|
|
1633
1942
|
if (stt)
|
|
@@ -1645,7 +1954,7 @@ ${plan}`);
|
|
|
1645
1954
|
const client = await connectBrowserMcp({ endpoint: parsed.scrapeMcp });
|
|
1646
1955
|
chain.push(new ScrapeMcpFetcher({ client }));
|
|
1647
1956
|
} catch (e) {
|
|
1648
|
-
return fail(`could not connect to scrape MCP at ${parsed.scrapeMcp}: ${
|
|
1957
|
+
return fail(`could not connect to scrape MCP at ${parsed.scrapeMcp}: ${msg3(e)}`, 1);
|
|
1649
1958
|
}
|
|
1650
1959
|
}
|
|
1651
1960
|
if (parsed.browserMcp) {
|
|
@@ -1654,7 +1963,7 @@ ${plan}`);
|
|
|
1654
1963
|
browser = await connectBrowserMcp({ endpoint: parsed.browserMcp });
|
|
1655
1964
|
chain.push(new BrowserMcpFetcher({ browser }));
|
|
1656
1965
|
} catch (e) {
|
|
1657
|
-
return fail(`could not connect to browser MCP at ${parsed.browserMcp}: ${
|
|
1966
|
+
return fail(`could not connect to browser MCP at ${parsed.browserMcp}: ${msg3(e)}`, 1);
|
|
1658
1967
|
}
|
|
1659
1968
|
}
|
|
1660
1969
|
chain.push(new HttpReadabilityFetcher());
|
|
@@ -1685,7 +1994,7 @@ ${plan}`);
|
|
|
1685
1994
|
}
|
|
1686
1995
|
});
|
|
1687
1996
|
} catch (e) {
|
|
1688
|
-
return fail(`import failed: ${
|
|
1997
|
+
return fail(`import failed: ${msg3(e)}`, 1);
|
|
1689
1998
|
}
|
|
1690
1999
|
process.stdout.write(
|
|
1691
2000
|
`import-web \u2192 ${target}
|
|
@@ -1705,10 +2014,10 @@ knowledge engine (RAG) via the WriterPort. Import only stages the sources.
|
|
|
1705
2014
|
);
|
|
1706
2015
|
return 0;
|
|
1707
2016
|
}
|
|
1708
|
-
function
|
|
2017
|
+
function msg3(e) {
|
|
1709
2018
|
return e instanceof Error ? e.message : String(e);
|
|
1710
2019
|
}
|
|
1711
|
-
function
|
|
2020
|
+
function parse3(args) {
|
|
1712
2021
|
const out = {
|
|
1713
2022
|
topic: void 0,
|
|
1714
2023
|
outputPath: void 0,
|
|
@@ -1716,7 +2025,8 @@ function parse2(args) {
|
|
|
1716
2025
|
channels: /* @__PURE__ */ new Set(["web"]),
|
|
1717
2026
|
lang: void 0,
|
|
1718
2027
|
tags: [],
|
|
1719
|
-
doImport: false
|
|
2028
|
+
doImport: false,
|
|
2029
|
+
fresh: false
|
|
1720
2030
|
};
|
|
1721
2031
|
for (let i = 0; i < args.length; i++) {
|
|
1722
2032
|
const a = args[i];
|
|
@@ -1743,6 +2053,9 @@ function parse2(args) {
|
|
|
1743
2053
|
case "--import":
|
|
1744
2054
|
out.doImport = true;
|
|
1745
2055
|
break;
|
|
2056
|
+
case "--fresh":
|
|
2057
|
+
out.fresh = true;
|
|
2058
|
+
break;
|
|
1746
2059
|
default:
|
|
1747
2060
|
if (!a.startsWith("-")) {
|
|
1748
2061
|
if (out.topic === void 0) out.topic = a;
|
|
@@ -1870,10 +2183,18 @@ async function discoverSocial(_topic, _lang, _max) {
|
|
|
1870
2183
|
process.stdout.write(" social channel needs a Bureau session \u2014 skipped.\n");
|
|
1871
2184
|
return [];
|
|
1872
2185
|
}
|
|
2186
|
+
async function readExistingUrls(outFile) {
|
|
2187
|
+
try {
|
|
2188
|
+
const prior = await readFile(outFile, "utf-8");
|
|
2189
|
+
return prior.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2190
|
+
} catch {
|
|
2191
|
+
return [];
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
1873
2194
|
async function runDiscover(args) {
|
|
1874
|
-
const parsed =
|
|
2195
|
+
const parsed = parse3(args);
|
|
1875
2196
|
if (!parsed.topic) {
|
|
1876
|
-
return fail("discover requires a <topic> argument. Usage: corpus discover <topic> [path] [--max N] [--channels web,youtube,social] [--lang fr] [--tags t] [--import]", 2);
|
|
2197
|
+
return fail("discover requires a <topic> argument. Usage: corpus discover <topic> [path] [--max N] [--channels web,youtube,social] [--lang fr] [--tags t] [--import] [--fresh]", 2);
|
|
1877
2198
|
}
|
|
1878
2199
|
const outputPath = parsed.outputPath ? resolveWorkspacePath(parsed.outputPath) : process.cwd();
|
|
1879
2200
|
process.stdout.write(`discover "${parsed.topic}"
|
|
@@ -1916,10 +2237,22 @@ async function runDiscover(args) {
|
|
|
1916
2237
|
}
|
|
1917
2238
|
await mkdir(outputPath, { recursive: true });
|
|
1918
2239
|
const outFile = join(outputPath, "urls.discovered.txt");
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2240
|
+
const existing = parsed.fresh ? [] : await readExistingUrls(outFile);
|
|
2241
|
+
const merged = [...existing];
|
|
2242
|
+
const known = new Set(existing);
|
|
2243
|
+
for (const url of allUrls) {
|
|
2244
|
+
if (!known.has(url)) {
|
|
2245
|
+
known.add(url);
|
|
2246
|
+
merged.push(url);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
const added = merged.length - existing.length;
|
|
2250
|
+
await writeFile(outFile, merged.join("\n") + "\n", "utf-8");
|
|
2251
|
+
process.stdout.write(
|
|
2252
|
+
`
|
|
2253
|
+
written \u2192 ${outFile} (${merged.length} URLs` + (parsed.fresh ? ", --fresh overwrite" : `, ${added} new, ${existing.length} kept from previous runs`) + `)
|
|
2254
|
+
`
|
|
2255
|
+
);
|
|
1923
2256
|
if (parsed.doImport) {
|
|
1924
2257
|
process.stdout.write("\n --import: chaining import-web\u2026\n");
|
|
1925
2258
|
const importArgs = [
|
|
@@ -2290,7 +2623,7 @@ var DISTILLER_ENGINES = {
|
|
|
2290
2623
|
])
|
|
2291
2624
|
)
|
|
2292
2625
|
};
|
|
2293
|
-
function
|
|
2626
|
+
function parse4(args) {
|
|
2294
2627
|
const out = {
|
|
2295
2628
|
workspace: void 0,
|
|
2296
2629
|
sourceId: void 0,
|
|
@@ -2345,13 +2678,13 @@ async function readSources(root) {
|
|
|
2345
2678
|
const out = [];
|
|
2346
2679
|
for (const e of entries) {
|
|
2347
2680
|
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
2348
|
-
const
|
|
2681
|
+
const path6 = join(e.parentPath, e.name);
|
|
2349
2682
|
try {
|
|
2350
|
-
const parsed = matter2(await readFile(
|
|
2683
|
+
const parsed = matter2(await readFile(path6, "utf-8"));
|
|
2351
2684
|
const fm = SOURCE_FRONTMATTER.parse(parsed.data);
|
|
2352
2685
|
if (!fm.id || !parsed.content.trim()) continue;
|
|
2353
2686
|
out.push({
|
|
2354
|
-
path:
|
|
2687
|
+
path: path6,
|
|
2355
2688
|
id: fm.id,
|
|
2356
2689
|
title: fm.title ?? fm.id,
|
|
2357
2690
|
body: parsed.content.trim(),
|
|
@@ -2387,7 +2720,7 @@ async function scanDistilledSourceIds(root) {
|
|
|
2387
2720
|
}
|
|
2388
2721
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2389
2722
|
async function runDistill(args) {
|
|
2390
|
-
const parsed =
|
|
2723
|
+
const parsed = parse4(args);
|
|
2391
2724
|
const target = resolveWorkspacePath(parsed.workspace);
|
|
2392
2725
|
const engine = DISTILLER_ENGINES[parsed.engine];
|
|
2393
2726
|
if (!engine) {
|
|
@@ -2526,7 +2859,12 @@ async function runReport(args) {
|
|
|
2526
2859
|
}
|
|
2527
2860
|
function loadReportConfig(configPath) {
|
|
2528
2861
|
const raw = readFileSync(path.resolve(process.cwd(), configPath), "utf8");
|
|
2529
|
-
|
|
2862
|
+
const parsed = JSON.parse(raw);
|
|
2863
|
+
for (const warning of lintReportConfig(parsed)) {
|
|
2864
|
+
process.stderr.write(`corpus report: ${warning}
|
|
2865
|
+
`);
|
|
2866
|
+
}
|
|
2867
|
+
return reportConfigSchema.parse(parsed);
|
|
2530
2868
|
}
|
|
2531
2869
|
async function runReportPacks(args) {
|
|
2532
2870
|
let dataset;
|
|
@@ -2786,10 +3124,10 @@ var SseMcpClient = class {
|
|
|
2786
3124
|
try {
|
|
2787
3125
|
const parsed = RPC_MESSAGE.safeParse(JSON.parse(payload));
|
|
2788
3126
|
if (!parsed.success) return;
|
|
2789
|
-
const
|
|
2790
|
-
if (typeof
|
|
2791
|
-
this.pending.get(
|
|
2792
|
-
this.pending.delete(
|
|
3127
|
+
const msg5 = parsed.data;
|
|
3128
|
+
if (typeof msg5.id === "number" && this.pending.has(msg5.id)) {
|
|
3129
|
+
this.pending.get(msg5.id)(msg5);
|
|
3130
|
+
this.pending.delete(msg5.id);
|
|
2793
3131
|
}
|
|
2794
3132
|
} catch {
|
|
2795
3133
|
}
|
|
@@ -2858,7 +3196,7 @@ var McpSink = class {
|
|
|
2858
3196
|
try {
|
|
2859
3197
|
client = await this.ensure();
|
|
2860
3198
|
} catch (e) {
|
|
2861
|
-
return { uri: item.uri, ok: false, error: `connect: ${
|
|
3199
|
+
return { uri: item.uri, ok: false, error: `connect: ${msg4(e)}` };
|
|
2862
3200
|
}
|
|
2863
3201
|
const args = template(this.config.args, item);
|
|
2864
3202
|
const call = this.config.importedAlias ? { name: "mcp_imported_call", args: { alias: this.config.importedAlias, toolName: this.config.tool, args } } : { name: this.config.tool, args };
|
|
@@ -2867,7 +3205,7 @@ var McpSink = class {
|
|
|
2867
3205
|
if (res.isError) return { uri: item.uri, ok: false, error: "tool returned isError" };
|
|
2868
3206
|
return { uri: item.uri, ok: true };
|
|
2869
3207
|
} catch (e) {
|
|
2870
|
-
return { uri: item.uri, ok: false, error:
|
|
3208
|
+
return { uri: item.uri, ok: false, error: msg4(e) };
|
|
2871
3209
|
}
|
|
2872
3210
|
}
|
|
2873
3211
|
};
|
|
@@ -2911,7 +3249,7 @@ function stringify(v) {
|
|
|
2911
3249
|
if (Array.isArray(v)) return v.join(", ");
|
|
2912
3250
|
return v == null ? "" : String(v);
|
|
2913
3251
|
}
|
|
2914
|
-
function
|
|
3252
|
+
function msg4(e) {
|
|
2915
3253
|
return e instanceof Error ? e.message : String(e);
|
|
2916
3254
|
}
|
|
2917
3255
|
|
|
@@ -3007,12 +3345,20 @@ Commands:
|
|
|
3007
3345
|
(--preset seeds a full vertical; --list shows them)
|
|
3008
3346
|
validate [path] JSON Schema check across every AIP file
|
|
3009
3347
|
lint [path] Run lints declared in KNOWLEDGE.md
|
|
3348
|
+
verify [path] --facets a,b,... [--thin 8] [--apply] [--contaminated]
|
|
3349
|
+
SOP \u2464b: coverage per facet tag +
|
|
3350
|
+
quarantine of self-flagged bad scrapes.
|
|
3351
|
+
--apply moves flagged entries to
|
|
3352
|
+
demoted/ (reversible); --contaminated
|
|
3353
|
+
(with --apply) also demotes siblings
|
|
3354
|
+
sharing a poisoned source.
|
|
3010
3355
|
events:emit <kind> --payload <json> [path]
|
|
3011
3356
|
Append an event to _log.md
|
|
3012
3357
|
events:tail [path] Print _log.md
|
|
3013
|
-
discover <topic> [path] [--max N --channels web,youtube,social --lang l --tags t --import]
|
|
3358
|
+
discover <topic> [path] [--max N --channels web,youtube,social --lang l --tags t --import --fresh]
|
|
3014
3359
|
Fan out across channels (web search + YouTube +
|
|
3015
|
-
social), dedup,
|
|
3360
|
+
social), dedup, merge into urls.discovered.txt
|
|
3361
|
+
(union with previous runs; --fresh overwrites).
|
|
3016
3362
|
Web: auto-picks first available key (SERPER /
|
|
3017
3363
|
EXA / TAVILY / GOOGLE_SEARCH). YouTube: yt-dlp
|
|
3018
3364
|
flat-playlist. --import chains import-web.
|
|
@@ -3074,6 +3420,8 @@ async function main(argv2) {
|
|
|
3074
3420
|
return await runValidate(rest);
|
|
3075
3421
|
case "lint":
|
|
3076
3422
|
return await runLint(rest);
|
|
3423
|
+
case "verify":
|
|
3424
|
+
return await runVerify(rest);
|
|
3077
3425
|
case "events:emit":
|
|
3078
3426
|
return await runEventsEmit(rest);
|
|
3079
3427
|
case "events:tail":
|