@agentproto/corpus-cli 0.2.0 → 0.3.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/dist/cli.mjs CHANGED
@@ -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(msg4, code = 1) {
408
- process.stderr.write(`corpus: ${msg4}
407
+ function fail(msg5, code = 1) {
408
+ process.stderr.write(`corpus: ${msg5}
409
409
  `);
410
410
  return code;
411
411
  }
@@ -1040,6 +1040,146 @@ function defaultYtDlpDownloader(bin, opts) {
1040
1040
  }
1041
1041
  };
1042
1042
  }
1043
+ var execFileAsync2 = promisify(execFile);
1044
+ var YTDLP_INFO2 = z.object({ title: z.string().optional(), language: z.string().optional() }).loose();
1045
+ var YtDlpCaptionsFetcher = class {
1046
+ download;
1047
+ extraHosts;
1048
+ minChars;
1049
+ constructor(opts = {}) {
1050
+ this.minChars = opts.minChars ?? 400;
1051
+ this.download = opts.download ?? defaultYtDlpCaptionDownloader(opts.ytDlpBin ?? "yt-dlp", {
1052
+ ...opts.subLangs ? { subLangs: opts.subLangs } : {},
1053
+ ...opts.preferLang ? { preferLang: opts.preferLang } : {},
1054
+ ...opts.maxDurationSec ? { maxDurationSec: opts.maxDurationSec } : {},
1055
+ ...opts.cookiesFromBrowser ? { cookiesFromBrowser: opts.cookiesFromBrowser } : {},
1056
+ ...opts.cookiesFile ? { cookiesFile: opts.cookiesFile } : {}
1057
+ });
1058
+ this.extraHosts = opts.extraVideoHosts ? new Set(opts.extraVideoHosts) : void 0;
1059
+ }
1060
+ async fetch(url) {
1061
+ if (!isVideoUrl(url, this.extraHosts)) return null;
1062
+ let dl;
1063
+ try {
1064
+ dl = await this.download(url);
1065
+ } catch (e) {
1066
+ process.stderr.write(
1067
+ `corpus: caption fetch failed for ${url} \u2014 falling back (${msg2(e)})
1068
+ `
1069
+ );
1070
+ return null;
1071
+ }
1072
+ try {
1073
+ if (!dl.vttPath) return null;
1074
+ const text = parseVttToText(await readFile(dl.vttPath, "utf-8"));
1075
+ if (text.length < this.minChars) return null;
1076
+ return {
1077
+ title: dl.title || url,
1078
+ text,
1079
+ kind: "video",
1080
+ ...dl.language ? { language: dl.language } : {},
1081
+ via: "captions"
1082
+ };
1083
+ } catch {
1084
+ return null;
1085
+ } finally {
1086
+ await dl.cleanup().catch(() => {
1087
+ });
1088
+ }
1089
+ }
1090
+ };
1091
+ function msg2(e) {
1092
+ return e instanceof Error ? e.message : String(e);
1093
+ }
1094
+ function parseVttToText(vtt) {
1095
+ const out = [];
1096
+ for (const raw of vtt.split(/\r?\n/)) {
1097
+ if (/^WEBVTT/.test(raw)) continue;
1098
+ if (/^(Kind|Language|NOTE|STYLE):/i.test(raw)) continue;
1099
+ if (raw.includes("-->")) continue;
1100
+ const line = raw.replace(/<[^>]+>/g, "").replace(/&nbsp;/gi, " ").replace(/ /g, " ").trim();
1101
+ if (!line) continue;
1102
+ if (out.length && out[out.length - 1] === line) continue;
1103
+ out.push(line);
1104
+ }
1105
+ const cleaned = [];
1106
+ for (let i = 0; i < out.length; i++) {
1107
+ const next = out[i + 1];
1108
+ if (next && next.startsWith(out[i])) continue;
1109
+ cleaned.push(out[i]);
1110
+ }
1111
+ return cleaned.join(" ").replace(/\s+/g, " ").trim();
1112
+ }
1113
+ function buildYtDlpCaptionArgs(url, dir, opts) {
1114
+ const subLangs = opts?.subLangs ?? (opts?.preferLang ? `${opts.preferLang}.*` : "en.*,fr.*,es.*,de.*,it.*,pt.*,nl.*");
1115
+ const durationGuard = opts?.maxDurationSec && opts.maxDurationSec > 0 ? ["--match-filter", `duration <= ${Math.floor(opts.maxDurationSec)}`] : [];
1116
+ const cookieArgs = opts?.cookiesFromBrowser ? ["--cookies-from-browser", opts.cookiesFromBrowser] : opts?.cookiesFile ? ["--cookies", opts.cookiesFile] : [];
1117
+ return [
1118
+ "--skip-download",
1119
+ "--write-auto-subs",
1120
+ "--write-subs",
1121
+ "--sub-langs",
1122
+ subLangs,
1123
+ "--sub-format",
1124
+ "vtt",
1125
+ "--no-playlist",
1126
+ "--no-progress",
1127
+ "--sleep-requests",
1128
+ "1",
1129
+ ...durationGuard,
1130
+ ...cookieArgs,
1131
+ "--write-info-json",
1132
+ "-o",
1133
+ join(dir, "%(id)s.%(ext)s"),
1134
+ url
1135
+ ];
1136
+ }
1137
+ function pickVtt(files, preferLang) {
1138
+ const vtts = files.filter((f) => f.endsWith(".vtt"));
1139
+ if (vtts.length === 0) return void 0;
1140
+ const bySuffix = (suffix) => vtts.find((f) => f.toLowerCase().endsWith(suffix.toLowerCase()));
1141
+ if (preferLang) {
1142
+ 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];
1143
+ }
1144
+ return vtts.find((f) => /-orig\.vtt$/i.test(f)) ?? vtts[0];
1145
+ }
1146
+ function defaultYtDlpCaptionDownloader(bin, opts) {
1147
+ return async (url) => {
1148
+ const dir = await mkdtemp(join(tmpdir(), "corpus-captions-"));
1149
+ const cleanup = () => rm(dir, { recursive: true, force: true });
1150
+ try {
1151
+ await execFileAsync2(bin, buildYtDlpCaptionArgs(url, dir, opts), {
1152
+ maxBuffer: 16 * 1024 * 1024
1153
+ }).catch(() => {
1154
+ });
1155
+ const files = await readdir(dir);
1156
+ const vtt = pickVtt(files, opts?.preferLang);
1157
+ let title = url;
1158
+ let language;
1159
+ const infoName = files.find((f) => f.endsWith(".info.json"));
1160
+ if (infoName) {
1161
+ try {
1162
+ const info = YTDLP_INFO2.parse(
1163
+ JSON.parse(await readFile(join(dir, infoName), "utf-8"))
1164
+ );
1165
+ if (info.title) title = info.title;
1166
+ if (info.language) language = info.language;
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ return {
1171
+ vttPath: vtt ? join(dir, vtt) : null,
1172
+ title,
1173
+ ...language ? { language } : {},
1174
+ cleanup
1175
+ };
1176
+ } catch (e) {
1177
+ await cleanup().catch(() => {
1178
+ });
1179
+ throw e;
1180
+ }
1181
+ };
1182
+ }
1043
1183
  var WHISPER_RESPONSE = z.object({ text: z.string().optional(), language: z.string().optional() }).loose();
1044
1184
  var WHISPER_MAX_BYTES = 25 * 1024 * 1024;
1045
1185
  var OpenAiWhisperStt = class {
@@ -1178,7 +1318,7 @@ function formatDiarized(job) {
1178
1318
  }
1179
1319
  return (job.text ?? "").trim();
1180
1320
  }
1181
- var execFileAsync2 = promisify(execFile);
1321
+ var execFileAsync3 = promisify(execFile);
1182
1322
  var DEFAULT_MAX_BYTES = 24 * 1024 * 1024;
1183
1323
  var DEFAULT_SEGMENT_SECONDS = 1200;
1184
1324
  var ChunkedStt = class {
@@ -1237,7 +1377,7 @@ var ChunkedStt = class {
1237
1377
  function defaultFfmpegSplitter(bin) {
1238
1378
  return async (audioPath, segmentSeconds, outDir) => {
1239
1379
  const ext = extname(audioPath) || ".mp3";
1240
- await execFileAsync2(
1380
+ await execFileAsync3(
1241
1381
  bin,
1242
1382
  [
1243
1383
  "-hide_banner",
@@ -1468,6 +1608,7 @@ function parse(args) {
1468
1608
  throttleMs: 2e3,
1469
1609
  force: false,
1470
1610
  diarize: false,
1611
+ noCaptions: false,
1471
1612
  browserMcp: process.env.BROWSER_MCP_URL,
1472
1613
  scrapeMcp: process.env.SCRAPE_MCP_URL,
1473
1614
  importerId: "web",
@@ -1523,6 +1664,9 @@ function parse(args) {
1523
1664
  case "--diarize":
1524
1665
  out.diarize = true;
1525
1666
  break;
1667
+ case "--no-captions":
1668
+ out.noCaptions = true;
1669
+ break;
1526
1670
  case "--browser-mcp":
1527
1671
  out.browserMcp = next();
1528
1672
  break;
@@ -1589,10 +1733,11 @@ async function runImportWeb(args) {
1589
1733
  const todo = urls.filter((u) => !done.has(u));
1590
1734
  const batch = parsed.max !== void 0 ? todo.slice(0, parsed.max) : todo;
1591
1735
  const remaining = todo.length - batch.length;
1736
+ const videoLabel = parsed.noCaptions ? parsed.diarize ? "AssemblyAI" : "Whisper" : `captions-first \u2192 ${parsed.diarize ? "AssemblyAI" : "Whisper"} fallback`;
1592
1737
  const plan = ` workspace: ${target}
1593
1738
  urls: ${urls.length} total \xB7 ${done.size && !parsed.force ? `${urls.length - todo.length} already ingested \xB7 ` : ""}${todo.length} to do
1594
1739
  this run: ${batch.length}${parsed.max !== void 0 ? ` (--max ${parsed.max})` : ""} \xB7 ${remaining} remaining after
1595
- video stt: ${parsed.diarize ? "AssemblyAI (speaker-labelled)" : "Whisper"}
1740
+ video: ${videoLabel}
1596
1741
  throttle: ${parsed.throttleMs} ms between fetches
1597
1742
  `;
1598
1743
  if (batch.length === 0) {
@@ -1613,6 +1758,16 @@ ${plan}`);
1613
1758
  process.stdout.write(`import-web
1614
1759
  ${plan}`);
1615
1760
  const chain = [];
1761
+ if (!parsed.noCaptions) {
1762
+ chain.push(
1763
+ new YtDlpCaptionsFetcher({
1764
+ ...parsed.lang ? { preferLang: parsed.lang } : {},
1765
+ ...parsed.maxDurationSec ? { maxDurationSec: parsed.maxDurationSec } : {},
1766
+ ...parsed.cookiesFromBrowser ? { cookiesFromBrowser: parsed.cookiesFromBrowser } : {},
1767
+ ...parsed.cookiesFile ? { cookiesFile: parsed.cookiesFile } : {}
1768
+ })
1769
+ );
1770
+ }
1616
1771
  let stt;
1617
1772
  if (parsed.diarize) {
1618
1773
  const aaiKey = process.env.ASSEMBLYAI_API_KEY;
@@ -1627,7 +1782,7 @@ ${plan}`);
1627
1782
  stt = new ChunkedStt({ base: new OpenAiWhisperStt({ apiKey: openaiKey }) });
1628
1783
  else
1629
1784
  process.stderr.write(
1630
- "corpus: OPENAI_API_KEY not set \u2014 video URLs will be skipped (no transcription).\n"
1785
+ 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
1786
  );
1632
1787
  }
1633
1788
  if (stt)
@@ -1645,7 +1800,7 @@ ${plan}`);
1645
1800
  const client = await connectBrowserMcp({ endpoint: parsed.scrapeMcp });
1646
1801
  chain.push(new ScrapeMcpFetcher({ client }));
1647
1802
  } catch (e) {
1648
- return fail(`could not connect to scrape MCP at ${parsed.scrapeMcp}: ${msg2(e)}`, 1);
1803
+ return fail(`could not connect to scrape MCP at ${parsed.scrapeMcp}: ${msg3(e)}`, 1);
1649
1804
  }
1650
1805
  }
1651
1806
  if (parsed.browserMcp) {
@@ -1654,7 +1809,7 @@ ${plan}`);
1654
1809
  browser = await connectBrowserMcp({ endpoint: parsed.browserMcp });
1655
1810
  chain.push(new BrowserMcpFetcher({ browser }));
1656
1811
  } catch (e) {
1657
- return fail(`could not connect to browser MCP at ${parsed.browserMcp}: ${msg2(e)}`, 1);
1812
+ return fail(`could not connect to browser MCP at ${parsed.browserMcp}: ${msg3(e)}`, 1);
1658
1813
  }
1659
1814
  }
1660
1815
  chain.push(new HttpReadabilityFetcher());
@@ -1685,7 +1840,7 @@ ${plan}`);
1685
1840
  }
1686
1841
  });
1687
1842
  } catch (e) {
1688
- return fail(`import failed: ${msg2(e)}`, 1);
1843
+ return fail(`import failed: ${msg3(e)}`, 1);
1689
1844
  }
1690
1845
  process.stdout.write(
1691
1846
  `import-web \u2192 ${target}
@@ -1705,7 +1860,7 @@ knowledge engine (RAG) via the WriterPort. Import only stages the sources.
1705
1860
  );
1706
1861
  return 0;
1707
1862
  }
1708
- function msg2(e) {
1863
+ function msg3(e) {
1709
1864
  return e instanceof Error ? e.message : String(e);
1710
1865
  }
1711
1866
  function parse2(args) {
@@ -2786,10 +2941,10 @@ var SseMcpClient = class {
2786
2941
  try {
2787
2942
  const parsed = RPC_MESSAGE.safeParse(JSON.parse(payload));
2788
2943
  if (!parsed.success) return;
2789
- const msg4 = parsed.data;
2790
- if (typeof msg4.id === "number" && this.pending.has(msg4.id)) {
2791
- this.pending.get(msg4.id)(msg4);
2792
- this.pending.delete(msg4.id);
2944
+ const msg5 = parsed.data;
2945
+ if (typeof msg5.id === "number" && this.pending.has(msg5.id)) {
2946
+ this.pending.get(msg5.id)(msg5);
2947
+ this.pending.delete(msg5.id);
2793
2948
  }
2794
2949
  } catch {
2795
2950
  }
@@ -2858,7 +3013,7 @@ var McpSink = class {
2858
3013
  try {
2859
3014
  client = await this.ensure();
2860
3015
  } catch (e) {
2861
- return { uri: item.uri, ok: false, error: `connect: ${msg3(e)}` };
3016
+ return { uri: item.uri, ok: false, error: `connect: ${msg4(e)}` };
2862
3017
  }
2863
3018
  const args = template(this.config.args, item);
2864
3019
  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 +3022,7 @@ var McpSink = class {
2867
3022
  if (res.isError) return { uri: item.uri, ok: false, error: "tool returned isError" };
2868
3023
  return { uri: item.uri, ok: true };
2869
3024
  } catch (e) {
2870
- return { uri: item.uri, ok: false, error: msg3(e) };
3025
+ return { uri: item.uri, ok: false, error: msg4(e) };
2871
3026
  }
2872
3027
  }
2873
3028
  };
@@ -2911,7 +3066,7 @@ function stringify(v) {
2911
3066
  if (Array.isArray(v)) return v.join(", ");
2912
3067
  return v == null ? "" : String(v);
2913
3068
  }
2914
- function msg3(e) {
3069
+ function msg4(e) {
2915
3070
  return e instanceof Error ? e.message : String(e);
2916
3071
  }
2917
3072