@agentproto/corpus-cli 0.1.1 → 0.2.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.
@@ -133,12 +133,22 @@ declare class OpenAiWhisperStt implements SttPort {
133
133
  * transcribes it with an injected SttPort (Whisper). Captions-independent
134
134
  * and robust: works whether or not the video has subtitles.
135
135
  *
136
- * video URL → yt-dlp -f bestaudio -x (mp3) → SttPort.transcribe → text
136
+ * video URL → yt-dlp -f bestaudio/best -x (mp3) → SttPort.transcribe → text
137
137
  *
138
- * yt-dlp handles the pot token / SABR negotiation / signature itself, so
139
- * no browser or cookies are needed for public videos. Non-video URLs
140
- * resolve to `null` so this fetcher composes ahead of a readability
141
- * fetcher (see CompositeFetcher).
138
+ * Modern YouTube requires three workarounds beyond a plain yt-dlp call:
139
+ * `-f bestaudio/best` many videos no longer expose a standalone
140
+ * bestaudio format; the /best fallback avoids "format not available".
141
+ * `--remote-components ejs:github` — solves the nsig "n challenge"
142
+ * that otherwise yields "Only images are available".
143
+ * • `--extractor-args youtube:player_client=<client>` — both the android
144
+ * client (unauthenticated) and the web_creator client (authenticated)
145
+ * fall back to progressive format 18, which requires no PO token and
146
+ * avoids the HTTP 403 that DASH segment requests trigger. The client is
147
+ * chosen based on whether cookies are provided: android does not support
148
+ * cookie auth (yt-dlp skips it when cookies are present), so web_creator
149
+ * is used instead for authenticated sessions.
150
+ * Cookies (--cookies-from-browser or --cookies) are still recommended to
151
+ * avoid bot-check rate-limits on burst downloads.
142
152
  *
143
153
  * The yt-dlp invocation is injected as `download` so the adapter is unit-
144
154
  * testable with a fake downloader + fake SttPort — no binary, no network.
@@ -178,6 +188,13 @@ interface YtDlpWhisperFetcherOptions {
178
188
  readonly cookiesFromBrowser?: string;
179
189
  /** Path to a Netscape cookies.txt — passed to yt-dlp as `--cookies`. */
180
190
  readonly cookiesFile?: string;
191
+ /**
192
+ * Directory that contains the `ffmpeg` / `ffprobe` binaries (e.g.
193
+ * `/opt/homebrew/bin`). Passed as `--ffmpeg-location` to yt-dlp so the
194
+ * postprocessor can find them even when PATH is not inherited by the
195
+ * subprocess. Omit to rely on yt-dlp's default PATH search.
196
+ */
197
+ readonly ffmpegLocation?: string;
181
198
  }
182
199
  declare class YtDlpWhisperFetcher implements FetcherPort {
183
200
  private readonly stt;
@@ -386,6 +403,8 @@ interface AnthropicDistillerOptions {
386
403
  readonly maxItems?: number;
387
404
  /** Optional sink for per-call token usage (cost + Langfuse export). */
388
405
  readonly onUsage?: (usage: DistillUsage) => void;
406
+ /** Output language code (e.g. "fr"). Absent → English (default). */
407
+ readonly lang?: string;
389
408
  }
390
409
  declare class AnthropicDistiller implements DistillPort {
391
410
  private readonly apiKey;
@@ -393,6 +412,7 @@ declare class AnthropicDistiller implements DistillPort {
393
412
  private readonly baseUrl;
394
413
  private readonly maxItems;
395
414
  private readonly onUsage;
415
+ private readonly lang;
396
416
  constructor(opts: AnthropicDistillerOptions);
397
417
  distill(input: DistillInput): Promise<readonly DistilledItem[]>;
398
418
  }
@@ -435,9 +455,14 @@ interface CliEngine {
435
455
  readonly subscriptionBilled: boolean;
436
456
  /** Executable to invoke — must be on PATH and logged in. */
437
457
  readonly command: string;
438
- /** Build the argv for one completion. The prompt is fed over stdin, not argv. */
458
+ /**
459
+ * Build the argv for one completion.
460
+ * Most engines receive the prompt over stdin and ignore `opts.prompt`.
461
+ * Arg-based engines (hermes `-z`) embed the prompt in argv instead.
462
+ */
439
463
  buildArgs(opts: {
440
464
  readonly model?: string;
465
+ readonly prompt?: string;
441
466
  }): string[];
442
467
  /** Extract the model's text from captured stdout; null → fall back to raw stdout. */
443
468
  parseOutput(stdout: string): string | null;
@@ -473,6 +498,8 @@ interface CliAgentDistillerOptions {
473
498
  readonly timeoutMs?: number;
474
499
  /** Working dir. Default os.tmpdir() — neutral, no project CLAUDE.md / repo. */
475
500
  readonly cwd?: string;
501
+ /** Output language code (e.g. "fr"). Absent → English (default). */
502
+ readonly lang?: string;
476
503
  }
477
504
  declare class CliAgentDistiller implements DistillPort {
478
505
  private readonly engine;
@@ -480,6 +507,7 @@ declare class CliAgentDistiller implements DistillPort {
480
507
  private readonly maxItems;
481
508
  private readonly timeoutMs;
482
509
  private readonly cwd;
510
+ private readonly lang;
483
511
  constructor(opts: CliAgentDistillerOptions);
484
512
  distill(input: DistillInput): Promise<readonly DistilledItem[]>;
485
513
  }
@@ -339,7 +339,8 @@ var YtDlpWhisperFetcher = class {
339
339
  this.download = opts.download ?? defaultYtDlpDownloader(opts.ytDlpBin ?? "yt-dlp", {
340
340
  maxDurationSec: opts.maxDurationSec,
341
341
  cookiesFromBrowser: opts.cookiesFromBrowser,
342
- cookiesFile: opts.cookiesFile
342
+ cookiesFile: opts.cookiesFile,
343
+ ffmpegLocation: opts.ffmpegLocation
343
344
  });
344
345
  this.extraHosts = opts.extraVideoHosts ? new Set(opts.extraVideoHosts) : void 0;
345
346
  }
@@ -383,32 +384,44 @@ function isAuthError(e) {
383
384
  const m = msg(e).toLowerCase();
384
385
  return m.includes(" 401") || m.includes(" 403") || m.includes("unauthorized") || m.includes("forbidden") || m.includes("api key") || m.includes("api_key");
385
386
  }
387
+ function buildYtDlpArgs(url, dir, opts) {
388
+ const durationGuard = opts?.maxDurationSec && opts.maxDurationSec > 0 ? ["--match-filter", `duration <= ${Math.floor(opts.maxDurationSec)}`] : [];
389
+ const cookieArgs = opts?.cookiesFromBrowser ? ["--cookies-from-browser", opts.cookiesFromBrowser] : opts?.cookiesFile ? ["--cookies", opts.cookiesFile] : [];
390
+ const ffmpegArgs = opts?.ffmpegLocation ? ["--ffmpeg-location", opts.ffmpegLocation] : [];
391
+ const client = cookieArgs.length === 0 ? "android" : "web_creator";
392
+ const extractorArgs = ["--extractor-args", `youtube:player_client=${client}`];
393
+ return [
394
+ "-f",
395
+ "bestaudio/best",
396
+ // /best fallback: many videos lack a standalone bestaudio
397
+ "-x",
398
+ "--audio-format",
399
+ "mp3",
400
+ "--audio-quality",
401
+ "64K",
402
+ "--no-playlist",
403
+ "--no-progress",
404
+ "--remote-components",
405
+ "ejs:github",
406
+ // solves YouTube nsig "n challenge"
407
+ ...extractorArgs,
408
+ ...durationGuard,
409
+ ...cookieArgs,
410
+ ...ffmpegArgs,
411
+ "--write-info-json",
412
+ "-o",
413
+ join(dir, "track.%(ext)s"),
414
+ url
415
+ ];
416
+ }
386
417
  function defaultYtDlpDownloader(bin, opts) {
387
418
  return async (url) => {
388
419
  const dir = await mkdtemp(join(tmpdir(), "corpus-ytdlp-"));
389
420
  const cleanup = () => rm(dir, { recursive: true, force: true });
390
421
  try {
391
- const durationGuard = opts?.maxDurationSec && opts.maxDurationSec > 0 ? ["--match-filter", `duration <= ${Math.floor(opts.maxDurationSec)}`] : [];
392
- const cookieArgs = opts?.cookiesFromBrowser ? ["--cookies-from-browser", opts.cookiesFromBrowser] : opts?.cookiesFile ? ["--cookies", opts.cookiesFile] : [];
393
422
  await execFileAsync(
394
423
  bin,
395
- [
396
- "-f",
397
- "bestaudio",
398
- "-x",
399
- "--audio-format",
400
- "mp3",
401
- "--audio-quality",
402
- "64K",
403
- "--no-playlist",
404
- "--no-progress",
405
- ...durationGuard,
406
- ...cookieArgs,
407
- "--write-info-json",
408
- "-o",
409
- join(dir, "track.%(ext)s"),
410
- url
411
- ],
424
+ buildYtDlpArgs(url, dir, opts),
412
425
  { maxBuffer: 16 * 1024 * 1024 }
413
426
  );
414
427
  const files = await readdir(dir);
@@ -761,15 +774,17 @@ var AnthropicDistiller = class {
761
774
  baseUrl;
762
775
  maxItems;
763
776
  onUsage;
777
+ lang;
764
778
  constructor(opts) {
765
779
  this.apiKey = opts.apiKey;
766
780
  this.model = opts.model ?? "claude-sonnet-4-6";
767
781
  this.baseUrl = (opts.baseUrl ?? "https://api.anthropic.com/v1").replace(/\/+$/, "");
768
782
  this.maxItems = opts.maxItems ?? 8;
769
783
  this.onUsage = opts.onUsage;
784
+ this.lang = opts.lang;
770
785
  }
771
786
  async distill(input) {
772
- const prompt = buildDistillPrompt(input, this.maxItems);
787
+ const prompt = buildDistillPrompt(input, this.maxItems, { lang: this.lang });
773
788
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
774
789
  const res = await fetch(`${this.baseUrl}/messages`, {
775
790
  method: "POST",
@@ -808,18 +823,20 @@ var CliAgentDistiller = class {
808
823
  maxItems;
809
824
  timeoutMs;
810
825
  cwd;
826
+ lang;
811
827
  constructor(opts) {
812
828
  this.engine = opts.engine;
813
829
  this.model = opts.model;
814
830
  this.maxItems = opts.maxItems ?? 8;
815
831
  this.timeoutMs = opts.timeoutMs ?? 12e4;
816
832
  this.cwd = opts.cwd ?? tmpdir();
833
+ this.lang = opts.lang;
817
834
  }
818
835
  async distill(input) {
819
- const prompt = buildDistillPrompt(input, this.maxItems);
836
+ const prompt = buildDistillPrompt(input, this.maxItems, { lang: this.lang });
820
837
  const stdout = await spawnWithStdin({
821
838
  command: this.engine.command,
822
- args: this.engine.buildArgs({ ...this.model ? { model: this.model } : {} }),
839
+ args: this.engine.buildArgs({ ...this.model ? { model: this.model } : {}, prompt }),
823
840
  stdin: prompt,
824
841
  cwd: this.cwd,
825
842
  timeoutMs: this.timeoutMs
@@ -894,11 +911,23 @@ var OPENCODE = {
894
911
  buildArgs: ({ model }) => ["run", ...model ? ["-m", model] : []],
895
912
  parseOutput: plainTextOutput
896
913
  };
914
+ var HERMES = {
915
+ id: "hermes",
916
+ subscriptionBilled: true,
917
+ command: "hermes",
918
+ buildArgs: ({ model, prompt }) => [
919
+ "--oneshot",
920
+ prompt ?? "",
921
+ ...model ? ["-m", model] : []
922
+ ],
923
+ parseOutput: plainTextOutput
924
+ };
897
925
  var CLI_ENGINES = {
898
926
  [CLAUDE_CODE.id]: CLAUDE_CODE,
899
927
  [GEMINI.id]: GEMINI,
900
928
  [CODEX.id]: CODEX,
901
- [OPENCODE.id]: OPENCODE
929
+ [OPENCODE.id]: OPENCODE,
930
+ [HERMES.id]: HERMES
902
931
  };
903
932
  var RPC_RESPONSE = z.object({ result: z.unknown().optional(), error: z.unknown().optional() }).loose();
904
933
  var RPC_MESSAGE = z.object({