@agentproto/corpus-cli 0.1.1 → 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 +482 -50
- package/dist/cli.mjs.map +1 -1
- package/dist/ports/index.d.ts +34 -6
- package/dist/ports/index.mjs +53 -24
- package/dist/ports/index.mjs.map +1 -1
- package/dist/specs/resources/aip-14/draft/TOOL.schema.json +1 -1
- package/dist/specs/resources/aip-45/draft/AGENT-CLI.schema.json +62 -0
- package/dist/specs/resources/aip-52/draft/HARNESS.schema.json +254 -0
- package/package.json +3 -3
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(
|
|
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
|
}
|
|
@@ -926,7 +926,8 @@ var YtDlpWhisperFetcher = class {
|
|
|
926
926
|
this.download = opts.download ?? defaultYtDlpDownloader(opts.ytDlpBin ?? "yt-dlp", {
|
|
927
927
|
maxDurationSec: opts.maxDurationSec,
|
|
928
928
|
cookiesFromBrowser: opts.cookiesFromBrowser,
|
|
929
|
-
cookiesFile: opts.cookiesFile
|
|
929
|
+
cookiesFile: opts.cookiesFile,
|
|
930
|
+
ffmpegLocation: opts.ffmpegLocation
|
|
930
931
|
});
|
|
931
932
|
this.extraHosts = opts.extraVideoHosts ? new Set(opts.extraVideoHosts) : void 0;
|
|
932
933
|
}
|
|
@@ -970,32 +971,44 @@ function isAuthError(e) {
|
|
|
970
971
|
const m = msg(e).toLowerCase();
|
|
971
972
|
return m.includes(" 401") || m.includes(" 403") || m.includes("unauthorized") || m.includes("forbidden") || m.includes("api key") || m.includes("api_key");
|
|
972
973
|
}
|
|
974
|
+
function buildYtDlpArgs(url, dir, opts) {
|
|
975
|
+
const durationGuard = opts?.maxDurationSec && opts.maxDurationSec > 0 ? ["--match-filter", `duration <= ${Math.floor(opts.maxDurationSec)}`] : [];
|
|
976
|
+
const cookieArgs = opts?.cookiesFromBrowser ? ["--cookies-from-browser", opts.cookiesFromBrowser] : opts?.cookiesFile ? ["--cookies", opts.cookiesFile] : [];
|
|
977
|
+
const ffmpegArgs = opts?.ffmpegLocation ? ["--ffmpeg-location", opts.ffmpegLocation] : [];
|
|
978
|
+
const client = cookieArgs.length === 0 ? "android" : "web_creator";
|
|
979
|
+
const extractorArgs = ["--extractor-args", `youtube:player_client=${client}`];
|
|
980
|
+
return [
|
|
981
|
+
"-f",
|
|
982
|
+
"bestaudio/best",
|
|
983
|
+
// /best fallback: many videos lack a standalone bestaudio
|
|
984
|
+
"-x",
|
|
985
|
+
"--audio-format",
|
|
986
|
+
"mp3",
|
|
987
|
+
"--audio-quality",
|
|
988
|
+
"64K",
|
|
989
|
+
"--no-playlist",
|
|
990
|
+
"--no-progress",
|
|
991
|
+
"--remote-components",
|
|
992
|
+
"ejs:github",
|
|
993
|
+
// solves YouTube nsig "n challenge"
|
|
994
|
+
...extractorArgs,
|
|
995
|
+
...durationGuard,
|
|
996
|
+
...cookieArgs,
|
|
997
|
+
...ffmpegArgs,
|
|
998
|
+
"--write-info-json",
|
|
999
|
+
"-o",
|
|
1000
|
+
join(dir, "track.%(ext)s"),
|
|
1001
|
+
url
|
|
1002
|
+
];
|
|
1003
|
+
}
|
|
973
1004
|
function defaultYtDlpDownloader(bin, opts) {
|
|
974
1005
|
return async (url) => {
|
|
975
1006
|
const dir = await mkdtemp(join(tmpdir(), "corpus-ytdlp-"));
|
|
976
1007
|
const cleanup = () => rm(dir, { recursive: true, force: true });
|
|
977
1008
|
try {
|
|
978
|
-
const durationGuard = opts?.maxDurationSec && opts.maxDurationSec > 0 ? ["--match-filter", `duration <= ${Math.floor(opts.maxDurationSec)}`] : [];
|
|
979
|
-
const cookieArgs = opts?.cookiesFromBrowser ? ["--cookies-from-browser", opts.cookiesFromBrowser] : opts?.cookiesFile ? ["--cookies", opts.cookiesFile] : [];
|
|
980
1009
|
await execFileAsync(
|
|
981
1010
|
bin,
|
|
982
|
-
|
|
983
|
-
"-f",
|
|
984
|
-
"bestaudio",
|
|
985
|
-
"-x",
|
|
986
|
-
"--audio-format",
|
|
987
|
-
"mp3",
|
|
988
|
-
"--audio-quality",
|
|
989
|
-
"64K",
|
|
990
|
-
"--no-playlist",
|
|
991
|
-
"--no-progress",
|
|
992
|
-
...durationGuard,
|
|
993
|
-
...cookieArgs,
|
|
994
|
-
"--write-info-json",
|
|
995
|
-
"-o",
|
|
996
|
-
join(dir, "track.%(ext)s"),
|
|
997
|
-
url
|
|
998
|
-
],
|
|
1011
|
+
buildYtDlpArgs(url, dir, opts),
|
|
999
1012
|
{ maxBuffer: 16 * 1024 * 1024 }
|
|
1000
1013
|
);
|
|
1001
1014
|
const files = await readdir(dir);
|
|
@@ -1027,6 +1040,146 @@ function defaultYtDlpDownloader(bin, opts) {
|
|
|
1027
1040
|
}
|
|
1028
1041
|
};
|
|
1029
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(/ /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
|
+
}
|
|
1030
1183
|
var WHISPER_RESPONSE = z.object({ text: z.string().optional(), language: z.string().optional() }).loose();
|
|
1031
1184
|
var WHISPER_MAX_BYTES = 25 * 1024 * 1024;
|
|
1032
1185
|
var OpenAiWhisperStt = class {
|
|
@@ -1165,7 +1318,7 @@ function formatDiarized(job) {
|
|
|
1165
1318
|
}
|
|
1166
1319
|
return (job.text ?? "").trim();
|
|
1167
1320
|
}
|
|
1168
|
-
var
|
|
1321
|
+
var execFileAsync3 = promisify(execFile);
|
|
1169
1322
|
var DEFAULT_MAX_BYTES = 24 * 1024 * 1024;
|
|
1170
1323
|
var DEFAULT_SEGMENT_SECONDS = 1200;
|
|
1171
1324
|
var ChunkedStt = class {
|
|
@@ -1224,7 +1377,7 @@ var ChunkedStt = class {
|
|
|
1224
1377
|
function defaultFfmpegSplitter(bin) {
|
|
1225
1378
|
return async (audioPath, segmentSeconds, outDir) => {
|
|
1226
1379
|
const ext = extname(audioPath) || ".mp3";
|
|
1227
|
-
await
|
|
1380
|
+
await execFileAsync3(
|
|
1228
1381
|
bin,
|
|
1229
1382
|
[
|
|
1230
1383
|
"-hide_banner",
|
|
@@ -1451,9 +1604,11 @@ function parse(args) {
|
|
|
1451
1604
|
maxDurationSec: void 0,
|
|
1452
1605
|
cookiesFromBrowser: void 0,
|
|
1453
1606
|
cookiesFile: void 0,
|
|
1607
|
+
ffmpegLocation: void 0,
|
|
1454
1608
|
throttleMs: 2e3,
|
|
1455
1609
|
force: false,
|
|
1456
1610
|
diarize: false,
|
|
1611
|
+
noCaptions: false,
|
|
1457
1612
|
browserMcp: process.env.BROWSER_MCP_URL,
|
|
1458
1613
|
scrapeMcp: process.env.SCRAPE_MCP_URL,
|
|
1459
1614
|
importerId: "web",
|
|
@@ -1495,6 +1650,9 @@ function parse(args) {
|
|
|
1495
1650
|
case "--cookies":
|
|
1496
1651
|
out.cookiesFile = next();
|
|
1497
1652
|
break;
|
|
1653
|
+
case "--ffmpeg-location":
|
|
1654
|
+
out.ffmpegLocation = next();
|
|
1655
|
+
break;
|
|
1498
1656
|
case "--throttle": {
|
|
1499
1657
|
const v = next();
|
|
1500
1658
|
if (v) out.throttleMs = Number(v);
|
|
@@ -1506,6 +1664,9 @@ function parse(args) {
|
|
|
1506
1664
|
case "--diarize":
|
|
1507
1665
|
out.diarize = true;
|
|
1508
1666
|
break;
|
|
1667
|
+
case "--no-captions":
|
|
1668
|
+
out.noCaptions = true;
|
|
1669
|
+
break;
|
|
1509
1670
|
case "--browser-mcp":
|
|
1510
1671
|
out.browserMcp = next();
|
|
1511
1672
|
break;
|
|
@@ -1572,10 +1733,11 @@ async function runImportWeb(args) {
|
|
|
1572
1733
|
const todo = urls.filter((u) => !done.has(u));
|
|
1573
1734
|
const batch = parsed.max !== void 0 ? todo.slice(0, parsed.max) : todo;
|
|
1574
1735
|
const remaining = todo.length - batch.length;
|
|
1736
|
+
const videoLabel = parsed.noCaptions ? parsed.diarize ? "AssemblyAI" : "Whisper" : `captions-first \u2192 ${parsed.diarize ? "AssemblyAI" : "Whisper"} fallback`;
|
|
1575
1737
|
const plan = ` workspace: ${target}
|
|
1576
1738
|
urls: ${urls.length} total \xB7 ${done.size && !parsed.force ? `${urls.length - todo.length} already ingested \xB7 ` : ""}${todo.length} to do
|
|
1577
1739
|
this run: ${batch.length}${parsed.max !== void 0 ? ` (--max ${parsed.max})` : ""} \xB7 ${remaining} remaining after
|
|
1578
|
-
video
|
|
1740
|
+
video: ${videoLabel}
|
|
1579
1741
|
throttle: ${parsed.throttleMs} ms between fetches
|
|
1580
1742
|
`;
|
|
1581
1743
|
if (batch.length === 0) {
|
|
@@ -1596,6 +1758,16 @@ ${plan}`);
|
|
|
1596
1758
|
process.stdout.write(`import-web
|
|
1597
1759
|
${plan}`);
|
|
1598
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
|
+
}
|
|
1599
1771
|
let stt;
|
|
1600
1772
|
if (parsed.diarize) {
|
|
1601
1773
|
const aaiKey = process.env.ASSEMBLYAI_API_KEY;
|
|
@@ -1610,7 +1782,7 @@ ${plan}`);
|
|
|
1610
1782
|
stt = new ChunkedStt({ base: new OpenAiWhisperStt({ apiKey: openaiKey }) });
|
|
1611
1783
|
else
|
|
1612
1784
|
process.stderr.write(
|
|
1613
|
-
"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"
|
|
1614
1786
|
);
|
|
1615
1787
|
}
|
|
1616
1788
|
if (stt)
|
|
@@ -1619,7 +1791,8 @@ ${plan}`);
|
|
|
1619
1791
|
stt,
|
|
1620
1792
|
...parsed.maxDurationSec ? { maxDurationSec: parsed.maxDurationSec } : {},
|
|
1621
1793
|
...parsed.cookiesFromBrowser ? { cookiesFromBrowser: parsed.cookiesFromBrowser } : {},
|
|
1622
|
-
...parsed.cookiesFile ? { cookiesFile: parsed.cookiesFile } : {}
|
|
1794
|
+
...parsed.cookiesFile ? { cookiesFile: parsed.cookiesFile } : {},
|
|
1795
|
+
...parsed.ffmpegLocation ? { ffmpegLocation: parsed.ffmpegLocation } : {}
|
|
1623
1796
|
})
|
|
1624
1797
|
);
|
|
1625
1798
|
if (parsed.scrapeMcp) {
|
|
@@ -1627,7 +1800,7 @@ ${plan}`);
|
|
|
1627
1800
|
const client = await connectBrowserMcp({ endpoint: parsed.scrapeMcp });
|
|
1628
1801
|
chain.push(new ScrapeMcpFetcher({ client }));
|
|
1629
1802
|
} catch (e) {
|
|
1630
|
-
return fail(`could not connect to scrape MCP at ${parsed.scrapeMcp}: ${
|
|
1803
|
+
return fail(`could not connect to scrape MCP at ${parsed.scrapeMcp}: ${msg3(e)}`, 1);
|
|
1631
1804
|
}
|
|
1632
1805
|
}
|
|
1633
1806
|
if (parsed.browserMcp) {
|
|
@@ -1636,7 +1809,7 @@ ${plan}`);
|
|
|
1636
1809
|
browser = await connectBrowserMcp({ endpoint: parsed.browserMcp });
|
|
1637
1810
|
chain.push(new BrowserMcpFetcher({ browser }));
|
|
1638
1811
|
} catch (e) {
|
|
1639
|
-
return fail(`could not connect to browser MCP at ${parsed.browserMcp}: ${
|
|
1812
|
+
return fail(`could not connect to browser MCP at ${parsed.browserMcp}: ${msg3(e)}`, 1);
|
|
1640
1813
|
}
|
|
1641
1814
|
}
|
|
1642
1815
|
chain.push(new HttpReadabilityFetcher());
|
|
@@ -1667,7 +1840,7 @@ ${plan}`);
|
|
|
1667
1840
|
}
|
|
1668
1841
|
});
|
|
1669
1842
|
} catch (e) {
|
|
1670
|
-
return fail(`import failed: ${
|
|
1843
|
+
return fail(`import failed: ${msg3(e)}`, 1);
|
|
1671
1844
|
}
|
|
1672
1845
|
process.stdout.write(
|
|
1673
1846
|
`import-web \u2192 ${target}
|
|
@@ -1687,9 +1860,234 @@ knowledge engine (RAG) via the WriterPort. Import only stages the sources.
|
|
|
1687
1860
|
);
|
|
1688
1861
|
return 0;
|
|
1689
1862
|
}
|
|
1690
|
-
function
|
|
1863
|
+
function msg3(e) {
|
|
1691
1864
|
return e instanceof Error ? e.message : String(e);
|
|
1692
1865
|
}
|
|
1866
|
+
function parse2(args) {
|
|
1867
|
+
const out = {
|
|
1868
|
+
topic: void 0,
|
|
1869
|
+
outputPath: void 0,
|
|
1870
|
+
max: 20,
|
|
1871
|
+
channels: /* @__PURE__ */ new Set(["web"]),
|
|
1872
|
+
lang: void 0,
|
|
1873
|
+
tags: [],
|
|
1874
|
+
doImport: false
|
|
1875
|
+
};
|
|
1876
|
+
for (let i = 0; i < args.length; i++) {
|
|
1877
|
+
const a = args[i];
|
|
1878
|
+
const next = () => args[++i];
|
|
1879
|
+
switch (a) {
|
|
1880
|
+
case "--max": {
|
|
1881
|
+
const v = next();
|
|
1882
|
+
if (v) out.max = Number(v);
|
|
1883
|
+
break;
|
|
1884
|
+
}
|
|
1885
|
+
case "--channels": {
|
|
1886
|
+
const v = next();
|
|
1887
|
+
if (v) out.channels = new Set(v.split(",").map((s) => s.trim()).filter(Boolean));
|
|
1888
|
+
break;
|
|
1889
|
+
}
|
|
1890
|
+
case "--lang":
|
|
1891
|
+
out.lang = next();
|
|
1892
|
+
break;
|
|
1893
|
+
case "--tags": {
|
|
1894
|
+
const v = next();
|
|
1895
|
+
if (v) out.tags.push(...v.split(",").map((s) => s.trim()).filter(Boolean));
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
case "--import":
|
|
1899
|
+
out.doImport = true;
|
|
1900
|
+
break;
|
|
1901
|
+
default:
|
|
1902
|
+
if (!a.startsWith("-")) {
|
|
1903
|
+
if (out.topic === void 0) out.topic = a;
|
|
1904
|
+
else if (out.outputPath === void 0) out.outputPath = a;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return out;
|
|
1909
|
+
}
|
|
1910
|
+
async function searchSerper(query, lang, max) {
|
|
1911
|
+
const apiKey = process.env.SERPER_API_KEY;
|
|
1912
|
+
const gl = lang === "fr" ? "fr" : lang ?? "us";
|
|
1913
|
+
const hl = lang ?? "en";
|
|
1914
|
+
const body = JSON.stringify({ q: query, gl, hl, num: max });
|
|
1915
|
+
const res = await fetch("https://google.serper.dev/search", {
|
|
1916
|
+
method: "POST",
|
|
1917
|
+
headers: { "X-API-KEY": apiKey, "Content-Type": "application/json" },
|
|
1918
|
+
body
|
|
1919
|
+
});
|
|
1920
|
+
if (!res.ok) throw new Error(`Serper ${res.status}: ${await res.text()}`);
|
|
1921
|
+
const json = await res.json();
|
|
1922
|
+
return (json.organic ?? []).flatMap((r) => r.link ? [{ url: r.link, title: r.title }] : []);
|
|
1923
|
+
}
|
|
1924
|
+
async function searchExa(query, _lang, max) {
|
|
1925
|
+
const apiKey = process.env.EXA_API_KEY;
|
|
1926
|
+
const body = JSON.stringify({ query, numResults: max, type: "neural", contents: { text: false } });
|
|
1927
|
+
const res = await fetch("https://api.exa.ai/search", {
|
|
1928
|
+
method: "POST",
|
|
1929
|
+
headers: { "x-api-key": apiKey, "Content-Type": "application/json" },
|
|
1930
|
+
body
|
|
1931
|
+
});
|
|
1932
|
+
if (!res.ok) throw new Error(`Exa ${res.status}: ${await res.text()}`);
|
|
1933
|
+
const json = await res.json();
|
|
1934
|
+
return (json.results ?? []).flatMap((r) => r.url ? [{ url: r.url, title: r.title }] : []);
|
|
1935
|
+
}
|
|
1936
|
+
async function searchTavily(query, _lang, max) {
|
|
1937
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
1938
|
+
const body = JSON.stringify({ api_key: apiKey, query, max_results: max });
|
|
1939
|
+
const res = await fetch("https://api.tavily.com/search", {
|
|
1940
|
+
method: "POST",
|
|
1941
|
+
headers: { "Content-Type": "application/json" },
|
|
1942
|
+
body
|
|
1943
|
+
});
|
|
1944
|
+
if (!res.ok) throw new Error(`Tavily ${res.status}: ${await res.text()}`);
|
|
1945
|
+
const json = await res.json();
|
|
1946
|
+
return (json.results ?? []).flatMap((r) => r.url ? [{ url: r.url, title: r.title }] : []);
|
|
1947
|
+
}
|
|
1948
|
+
async function searchGoogle(query, lang, max) {
|
|
1949
|
+
const apiKey = process.env.GOOGLE_SEARCH_API_KEY;
|
|
1950
|
+
const cx = process.env.GOOGLE_SEARCH_CX ?? "";
|
|
1951
|
+
const params = new URLSearchParams({
|
|
1952
|
+
key: apiKey,
|
|
1953
|
+
q: query,
|
|
1954
|
+
num: String(Math.min(max, 10)),
|
|
1955
|
+
...cx ? { cx } : {},
|
|
1956
|
+
...lang ? { lr: `lang_${lang}`, hl: lang } : {}
|
|
1957
|
+
});
|
|
1958
|
+
const res = await fetch(`https://www.googleapis.com/customsearch/v1?${params}`);
|
|
1959
|
+
if (!res.ok) throw new Error(`Google ${res.status}: ${await res.text()}`);
|
|
1960
|
+
const json = await res.json();
|
|
1961
|
+
return (json.items ?? []).flatMap((r) => r.link ? [{ url: r.link, title: r.title }] : []);
|
|
1962
|
+
}
|
|
1963
|
+
function resolveWebProvider() {
|
|
1964
|
+
if (process.env.SERPER_API_KEY) return { name: "serper", fn: searchSerper };
|
|
1965
|
+
if (process.env.EXA_API_KEY) return { name: "exa", fn: searchExa };
|
|
1966
|
+
if (process.env.TAVILY_API_KEY) return { name: "tavily", fn: searchTavily };
|
|
1967
|
+
if (process.env.GOOGLE_SEARCH_API_KEY) return { name: "google", fn: searchGoogle };
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
function buildWebQueries(topic, lang) {
|
|
1971
|
+
const langSuffix = lang && lang !== "en" ? ` ${lang}` : "";
|
|
1972
|
+
const q1 = topic;
|
|
1973
|
+
const q2 = `${topic} guide tutorial${langSuffix}`;
|
|
1974
|
+
const q3 = lang && lang !== "en" ? `${topic} ressources ${lang}` : `${topic} best practices resources`;
|
|
1975
|
+
return [q1, q2, q3];
|
|
1976
|
+
}
|
|
1977
|
+
async function discoverWeb(topic, lang, max) {
|
|
1978
|
+
const provider = resolveWebProvider();
|
|
1979
|
+
if (!provider) {
|
|
1980
|
+
process.stderr.write("corpus discover: no web search API key found (SERPER_API_KEY / EXA_API_KEY / TAVILY_API_KEY / GOOGLE_SEARCH_API_KEY) \u2014 web channel skipped.\n");
|
|
1981
|
+
return { urls: [], provider: "none" };
|
|
1982
|
+
}
|
|
1983
|
+
const queries = buildWebQueries(topic, lang);
|
|
1984
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1985
|
+
const urls = [];
|
|
1986
|
+
for (const q of queries) {
|
|
1987
|
+
if (urls.length >= max) break;
|
|
1988
|
+
try {
|
|
1989
|
+
const results = await provider.fn(q, lang, max);
|
|
1990
|
+
for (const r of results) {
|
|
1991
|
+
if (urls.length >= max) break;
|
|
1992
|
+
if (!seen.has(r.url)) {
|
|
1993
|
+
seen.add(r.url);
|
|
1994
|
+
urls.push(r.url);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
} catch (e) {
|
|
1998
|
+
process.stderr.write(`corpus discover: web search query "${q}" failed \u2014 ${e instanceof Error ? e.message : String(e)}
|
|
1999
|
+
`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return { urls, provider: provider.name };
|
|
2003
|
+
}
|
|
2004
|
+
async function discoverYoutube(topic, max) {
|
|
2005
|
+
return new Promise((resolve2) => {
|
|
2006
|
+
execFile(
|
|
2007
|
+
"yt-dlp",
|
|
2008
|
+
[`ytsearch${max}:${topic}`, "--flat-playlist", "--print", "%(url)s", "--no-warnings"],
|
|
2009
|
+
{ timeout: 6e4 },
|
|
2010
|
+
(err, stdout, _stderr) => {
|
|
2011
|
+
if (err) {
|
|
2012
|
+
process.stderr.write(`corpus discover: youtube channel failed \u2014 ${err.message}
|
|
2013
|
+
`);
|
|
2014
|
+
resolve2([]);
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
resolve2(
|
|
2018
|
+
stdout.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("http"))
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
);
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
async function discoverSocial(_topic, _lang, _max) {
|
|
2025
|
+
process.stdout.write(" social channel needs a Bureau session \u2014 skipped.\n");
|
|
2026
|
+
return [];
|
|
2027
|
+
}
|
|
2028
|
+
async function runDiscover(args) {
|
|
2029
|
+
const parsed = parse2(args);
|
|
2030
|
+
if (!parsed.topic) {
|
|
2031
|
+
return fail("discover requires a <topic> argument. Usage: corpus discover <topic> [path] [--max N] [--channels web,youtube,social] [--lang fr] [--tags t] [--import]", 2);
|
|
2032
|
+
}
|
|
2033
|
+
const outputPath = parsed.outputPath ? resolveWorkspacePath(parsed.outputPath) : process.cwd();
|
|
2034
|
+
process.stdout.write(`discover "${parsed.topic}"
|
|
2035
|
+
`);
|
|
2036
|
+
process.stdout.write(` channels: ${[...parsed.channels].join(", ")} \xB7 max: ${parsed.max}${parsed.lang ? ` \xB7 lang: ${parsed.lang}` : ""}
|
|
2037
|
+
`);
|
|
2038
|
+
const webUrls = [];
|
|
2039
|
+
const ytUrls = [];
|
|
2040
|
+
const socialUrls = [];
|
|
2041
|
+
if (parsed.channels.has("web")) {
|
|
2042
|
+
const { urls, provider } = await discoverWeb(parsed.topic, parsed.lang, parsed.max);
|
|
2043
|
+
webUrls.push(...urls);
|
|
2044
|
+
if (urls.length > 0) process.stdout.write(` web (${provider}): ${urls.length} URLs
|
|
2045
|
+
`);
|
|
2046
|
+
}
|
|
2047
|
+
if (parsed.channels.has("youtube")) {
|
|
2048
|
+
const urls = await discoverYoutube(parsed.topic, parsed.max);
|
|
2049
|
+
ytUrls.push(...urls);
|
|
2050
|
+
if (urls.length > 0) process.stdout.write(` youtube: ${urls.length} URLs
|
|
2051
|
+
`);
|
|
2052
|
+
}
|
|
2053
|
+
if (parsed.channels.has("social")) {
|
|
2054
|
+
const urls = await discoverSocial();
|
|
2055
|
+
socialUrls.push(...urls);
|
|
2056
|
+
}
|
|
2057
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2058
|
+
const allUrls = [];
|
|
2059
|
+
for (const url of [...webUrls, ...ytUrls, ...socialUrls]) {
|
|
2060
|
+
if (!seen.has(url)) {
|
|
2061
|
+
seen.add(url);
|
|
2062
|
+
allUrls.push(url);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
process.stdout.write(`
|
|
2066
|
+
web: ${webUrls.length} \xB7 youtube: ${ytUrls.length} \xB7 social: ${socialUrls.length} \xB7 total: ${allUrls.length} unique
|
|
2067
|
+
`);
|
|
2068
|
+
if (allUrls.length === 0) {
|
|
2069
|
+
process.stderr.write("corpus discover: no URLs found across any channel.\n");
|
|
2070
|
+
return 1;
|
|
2071
|
+
}
|
|
2072
|
+
await mkdir(outputPath, { recursive: true });
|
|
2073
|
+
const outFile = join(outputPath, "urls.discovered.txt");
|
|
2074
|
+
await writeFile(outFile, allUrls.join("\n") + "\n", "utf-8");
|
|
2075
|
+
process.stdout.write(`
|
|
2076
|
+
written \u2192 ${outFile}
|
|
2077
|
+
`);
|
|
2078
|
+
if (parsed.doImport) {
|
|
2079
|
+
process.stdout.write("\n --import: chaining import-web\u2026\n");
|
|
2080
|
+
const importArgs = [
|
|
2081
|
+
outputPath,
|
|
2082
|
+
"--urls-file",
|
|
2083
|
+
outFile,
|
|
2084
|
+
...parsed.tags.flatMap((t) => ["--tags", t]),
|
|
2085
|
+
...parsed.lang ? ["--lang", parsed.lang] : []
|
|
2086
|
+
];
|
|
2087
|
+
return runImportWeb(importArgs);
|
|
2088
|
+
}
|
|
2089
|
+
return 0;
|
|
2090
|
+
}
|
|
1693
2091
|
|
|
1694
2092
|
// src/ports/anthropic-distiller.ts
|
|
1695
2093
|
var ANTHROPIC_RESPONSE = z.object({
|
|
@@ -1703,15 +2101,17 @@ var AnthropicDistiller = class {
|
|
|
1703
2101
|
baseUrl;
|
|
1704
2102
|
maxItems;
|
|
1705
2103
|
onUsage;
|
|
2104
|
+
lang;
|
|
1706
2105
|
constructor(opts) {
|
|
1707
2106
|
this.apiKey = opts.apiKey;
|
|
1708
2107
|
this.model = opts.model ?? "claude-sonnet-4-6";
|
|
1709
2108
|
this.baseUrl = (opts.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/+$/, "");
|
|
1710
2109
|
this.maxItems = opts.maxItems ?? 8;
|
|
1711
2110
|
this.onUsage = opts.onUsage;
|
|
2111
|
+
this.lang = opts.lang;
|
|
1712
2112
|
}
|
|
1713
2113
|
async distill(input) {
|
|
1714
|
-
const prompt = buildDistillPrompt(input, this.maxItems);
|
|
2114
|
+
const prompt = buildDistillPrompt(input, this.maxItems, { lang: this.lang });
|
|
1715
2115
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1716
2116
|
const res = await fetch(`${this.baseUrl}/messages`, {
|
|
1717
2117
|
method: "POST",
|
|
@@ -1750,18 +2150,20 @@ var CliAgentDistiller = class {
|
|
|
1750
2150
|
maxItems;
|
|
1751
2151
|
timeoutMs;
|
|
1752
2152
|
cwd;
|
|
2153
|
+
lang;
|
|
1753
2154
|
constructor(opts) {
|
|
1754
2155
|
this.engine = opts.engine;
|
|
1755
2156
|
this.model = opts.model;
|
|
1756
2157
|
this.maxItems = opts.maxItems ?? 8;
|
|
1757
2158
|
this.timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1758
2159
|
this.cwd = opts.cwd ?? tmpdir();
|
|
2160
|
+
this.lang = opts.lang;
|
|
1759
2161
|
}
|
|
1760
2162
|
async distill(input) {
|
|
1761
|
-
const prompt = buildDistillPrompt(input, this.maxItems);
|
|
2163
|
+
const prompt = buildDistillPrompt(input, this.maxItems, { lang: this.lang });
|
|
1762
2164
|
const stdout = await spawnWithStdin({
|
|
1763
2165
|
command: this.engine.command,
|
|
1764
|
-
args: this.engine.buildArgs({ ...this.model ? { model: this.model } : {} }),
|
|
2166
|
+
args: this.engine.buildArgs({ ...this.model ? { model: this.model } : {}, prompt }),
|
|
1765
2167
|
stdin: prompt,
|
|
1766
2168
|
cwd: this.cwd,
|
|
1767
2169
|
timeoutMs: this.timeoutMs
|
|
@@ -1836,11 +2238,23 @@ var OPENCODE = {
|
|
|
1836
2238
|
buildArgs: ({ model }) => ["run", ...model ? ["-m", model] : []],
|
|
1837
2239
|
parseOutput: plainTextOutput
|
|
1838
2240
|
};
|
|
2241
|
+
var HERMES = {
|
|
2242
|
+
id: "hermes",
|
|
2243
|
+
subscriptionBilled: true,
|
|
2244
|
+
command: "hermes",
|
|
2245
|
+
buildArgs: ({ model, prompt }) => [
|
|
2246
|
+
"--oneshot",
|
|
2247
|
+
prompt ?? "",
|
|
2248
|
+
...model ? ["-m", model] : []
|
|
2249
|
+
],
|
|
2250
|
+
parseOutput: plainTextOutput
|
|
2251
|
+
};
|
|
1839
2252
|
var CLI_ENGINES = {
|
|
1840
2253
|
[CLAUDE_CODE.id]: CLAUDE_CODE,
|
|
1841
2254
|
[GEMINI.id]: GEMINI,
|
|
1842
2255
|
[CODEX.id]: CODEX,
|
|
1843
|
-
[OPENCODE.id]: OPENCODE
|
|
2256
|
+
[OPENCODE.id]: OPENCODE,
|
|
2257
|
+
[HERMES.id]: HERMES
|
|
1844
2258
|
};
|
|
1845
2259
|
var PRICING = [
|
|
1846
2260
|
{ match: "opus", inPerM: 15, outPerM: 75 },
|
|
@@ -2009,10 +2423,11 @@ var DISTILLER_ENGINES = {
|
|
|
2009
2423
|
[DEFAULT_ENGINE]: {
|
|
2010
2424
|
id: DEFAULT_ENGINE,
|
|
2011
2425
|
needsApiKey: true,
|
|
2012
|
-
create: ({ apiKey, model, onUsage }) => new AnthropicDistiller({
|
|
2426
|
+
create: ({ apiKey, model, onUsage, lang }) => new AnthropicDistiller({
|
|
2013
2427
|
apiKey,
|
|
2014
2428
|
...model ? { model } : {},
|
|
2015
|
-
...onUsage ? { onUsage } : {}
|
|
2429
|
+
...onUsage ? { onUsage } : {},
|
|
2430
|
+
...lang ? { lang } : {}
|
|
2016
2431
|
})
|
|
2017
2432
|
},
|
|
2018
2433
|
...Object.fromEntries(
|
|
@@ -2021,19 +2436,24 @@ var DISTILLER_ENGINES = {
|
|
|
2021
2436
|
{
|
|
2022
2437
|
id: engine.id,
|
|
2023
2438
|
needsApiKey: false,
|
|
2024
|
-
create: ({ model }) => new CliAgentDistiller({
|
|
2439
|
+
create: ({ model, lang }) => new CliAgentDistiller({
|
|
2440
|
+
engine,
|
|
2441
|
+
...model ? { model } : {},
|
|
2442
|
+
...lang ? { lang } : {}
|
|
2443
|
+
})
|
|
2025
2444
|
}
|
|
2026
2445
|
])
|
|
2027
2446
|
)
|
|
2028
2447
|
};
|
|
2029
|
-
function
|
|
2448
|
+
function parse3(args) {
|
|
2030
2449
|
const out = {
|
|
2031
2450
|
workspace: void 0,
|
|
2032
2451
|
sourceId: void 0,
|
|
2033
2452
|
max: void 0,
|
|
2034
2453
|
throttleMs: 1e3,
|
|
2035
2454
|
model: void 0,
|
|
2036
|
-
engine: DEFAULT_ENGINE
|
|
2455
|
+
engine: DEFAULT_ENGINE,
|
|
2456
|
+
lang: void 0
|
|
2037
2457
|
};
|
|
2038
2458
|
for (let i = 0; i < args.length; i++) {
|
|
2039
2459
|
const a = args[i];
|
|
@@ -2060,6 +2480,9 @@ function parse2(args) {
|
|
|
2060
2480
|
if (v) out.engine = v;
|
|
2061
2481
|
break;
|
|
2062
2482
|
}
|
|
2483
|
+
case "--lang":
|
|
2484
|
+
out.lang = next();
|
|
2485
|
+
break;
|
|
2063
2486
|
default:
|
|
2064
2487
|
if (!a.startsWith("-") && out.workspace === void 0) out.workspace = a;
|
|
2065
2488
|
}
|
|
@@ -2119,7 +2542,7 @@ async function scanDistilledSourceIds(root) {
|
|
|
2119
2542
|
}
|
|
2120
2543
|
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
2121
2544
|
async function runDistill(args) {
|
|
2122
|
-
const parsed =
|
|
2545
|
+
const parsed = parse3(args);
|
|
2123
2546
|
const target = resolveWorkspacePath(parsed.workspace);
|
|
2124
2547
|
const engine = DISTILLER_ENGINES[parsed.engine];
|
|
2125
2548
|
if (!engine) {
|
|
@@ -2157,7 +2580,8 @@ async function runDistill(args) {
|
|
|
2157
2580
|
distiller: engine.create({
|
|
2158
2581
|
...apiKey ? { apiKey } : {},
|
|
2159
2582
|
...parsed.model ? { model: parsed.model } : {},
|
|
2160
|
-
onUsage: usage.record
|
|
2583
|
+
onUsage: usage.record,
|
|
2584
|
+
...parsed.lang ? { lang: parsed.lang } : {}
|
|
2161
2585
|
})
|
|
2162
2586
|
});
|
|
2163
2587
|
let totalEntries = 0;
|
|
@@ -2517,10 +2941,10 @@ var SseMcpClient = class {
|
|
|
2517
2941
|
try {
|
|
2518
2942
|
const parsed = RPC_MESSAGE.safeParse(JSON.parse(payload));
|
|
2519
2943
|
if (!parsed.success) return;
|
|
2520
|
-
const
|
|
2521
|
-
if (typeof
|
|
2522
|
-
this.pending.get(
|
|
2523
|
-
this.pending.delete(
|
|
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);
|
|
2524
2948
|
}
|
|
2525
2949
|
} catch {
|
|
2526
2950
|
}
|
|
@@ -2589,7 +3013,7 @@ var McpSink = class {
|
|
|
2589
3013
|
try {
|
|
2590
3014
|
client = await this.ensure();
|
|
2591
3015
|
} catch (e) {
|
|
2592
|
-
return { uri: item.uri, ok: false, error: `connect: ${
|
|
3016
|
+
return { uri: item.uri, ok: false, error: `connect: ${msg4(e)}` };
|
|
2593
3017
|
}
|
|
2594
3018
|
const args = template(this.config.args, item);
|
|
2595
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 };
|
|
@@ -2598,7 +3022,7 @@ var McpSink = class {
|
|
|
2598
3022
|
if (res.isError) return { uri: item.uri, ok: false, error: "tool returned isError" };
|
|
2599
3023
|
return { uri: item.uri, ok: true };
|
|
2600
3024
|
} catch (e) {
|
|
2601
|
-
return { uri: item.uri, ok: false, error:
|
|
3025
|
+
return { uri: item.uri, ok: false, error: msg4(e) };
|
|
2602
3026
|
}
|
|
2603
3027
|
}
|
|
2604
3028
|
};
|
|
@@ -2642,7 +3066,7 @@ function stringify(v) {
|
|
|
2642
3066
|
if (Array.isArray(v)) return v.join(", ");
|
|
2643
3067
|
return v == null ? "" : String(v);
|
|
2644
3068
|
}
|
|
2645
|
-
function
|
|
3069
|
+
function msg4(e) {
|
|
2646
3070
|
return e instanceof Error ? e.message : String(e);
|
|
2647
3071
|
}
|
|
2648
3072
|
|
|
@@ -2741,6 +3165,12 @@ Commands:
|
|
|
2741
3165
|
events:emit <kind> --payload <json> [path]
|
|
2742
3166
|
Append an event to _log.md
|
|
2743
3167
|
events:tail [path] Print _log.md
|
|
3168
|
+
discover <topic> [path] [--max N --channels web,youtube,social --lang l --tags t --import]
|
|
3169
|
+
Fan out across channels (web search + YouTube +
|
|
3170
|
+
social), dedup, write urls.discovered.txt.
|
|
3171
|
+
Web: auto-picks first available key (SERPER /
|
|
3172
|
+
EXA / TAVILY / GOOGLE_SEARCH). YouTube: yt-dlp
|
|
3173
|
+
flat-playlist. --import chains import-web.
|
|
2744
3174
|
import-web [path] --urls-file <f> [--max n --max-duration s --throttle ms --tags t --lang l --force --diarize]
|
|
2745
3175
|
Import URLs as sources (video\u2192Whisper, article\u2192
|
|
2746
3176
|
readability). Resumable: skips already-ingested
|
|
@@ -2755,7 +3185,7 @@ Commands:
|
|
|
2755
3185
|
scrape MCP server (stealth + clean Markdown) for
|
|
2756
3186
|
walled/JS pages, ahead of plain readability.
|
|
2757
3187
|
--diarize: AssemblyAI speaker labels (interviews).
|
|
2758
|
-
distill [path] [--source id --max n --throttle ms --model m]
|
|
3188
|
+
distill [path] [--source id --max n --throttle ms --model m --lang l --engine e]
|
|
2759
3189
|
Distill raw sources \u2192 refined entries
|
|
2760
3190
|
(principle/pattern/\u2026) with sources:[id] provenance.
|
|
2761
3191
|
Resumable: skips already-distilled sources.
|
|
@@ -2805,6 +3235,8 @@ async function main(argv2) {
|
|
|
2805
3235
|
return await runEventsTail(rest);
|
|
2806
3236
|
case "import-web":
|
|
2807
3237
|
return await runImportWeb(rest);
|
|
3238
|
+
case "discover":
|
|
3239
|
+
return await runDiscover(rest);
|
|
2808
3240
|
case "distill":
|
|
2809
3241
|
return await runDistill(rest);
|
|
2810
3242
|
case "knowledge":
|