open-agents-ai 0.184.69 → 0.184.71

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.
Files changed (2) hide show
  1. package/dist/index.js +235 -22
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11949,7 +11949,7 @@ function formatTime(seconds) {
11949
11949
  const s = Math.floor(seconds % 60);
11950
11950
  return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
11951
11951
  }
11952
- var AUDIO_EXTS, VIDEO_EXTS, _tcModule, _tcChecked, TranscribeFileTool, TranscribeUrlTool;
11952
+ var AUDIO_EXTS, VIDEO_EXTS, _tcModule, _tcChecked, TranscribeFileTool, TranscribeUrlTool, YouTubeDownloadTool;
11953
11953
  var init_transcribe_tool = __esm({
11954
11954
  "packages/execution/dist/tools/transcribe-tool.js"() {
11955
11955
  "use strict";
@@ -12267,6 +12267,90 @@ ${result.output}`,
12267
12267
  }
12268
12268
  }
12269
12269
  };
12270
+ YouTubeDownloadTool = class {
12271
+ name = "youtube_download";
12272
+ description = "Download video or audio from YouTube. Saves mp4 (video) or mp3 (audio) to the working directory. Uses yt-dlp (auto-installed). Supports youtube.com/watch, youtu.be, shorts, live URLs.";
12273
+ parameters = {
12274
+ type: "object",
12275
+ properties: {
12276
+ url: {
12277
+ type: "string",
12278
+ description: "YouTube URL (youtube.com/watch?v=..., youtu.be/..., etc.)"
12279
+ },
12280
+ format: {
12281
+ type: "string",
12282
+ enum: ["mp3", "mp4"],
12283
+ description: "Output format: 'mp3' for audio only, 'mp4' for video (default: mp3)"
12284
+ },
12285
+ output_dir: {
12286
+ type: "string",
12287
+ description: "Directory to save the file (default: current working directory)"
12288
+ }
12289
+ },
12290
+ required: ["url"]
12291
+ };
12292
+ workingDir;
12293
+ constructor(workingDir) {
12294
+ this.workingDir = workingDir;
12295
+ }
12296
+ async execute(args) {
12297
+ const start = Date.now();
12298
+ const url = String(args.url ?? "").trim();
12299
+ const format = String(args.format ?? "mp3").toLowerCase();
12300
+ const outputDir = String(args.output_dir ?? this.workingDir);
12301
+ if (!url) {
12302
+ return { success: false, output: "", error: "URL is required", durationMs: Date.now() - start };
12303
+ }
12304
+ if (!isYouTubeUrl(url)) {
12305
+ return { success: false, output: "", error: "Not a recognized YouTube URL. Supported: youtube.com/watch, youtu.be, shorts, live, embed", durationMs: Date.now() - start };
12306
+ }
12307
+ if (!ensureYtDlp()) {
12308
+ return { success: false, output: "", error: "yt-dlp not available and auto-install failed. Install manually: pip install yt-dlp", durationMs: Date.now() - start };
12309
+ }
12310
+ mkdirSync7(outputDir, { recursive: true });
12311
+ try {
12312
+ let title = "download";
12313
+ try {
12314
+ title = execSync10(`yt-dlp --get-title "${url}"`, { timeout: 15e3, stdio: "pipe" }).toString().trim().replace(/[<>:"/\\|?*]/g, "_").slice(0, 100);
12315
+ } catch {
12316
+ }
12317
+ if (format === "mp4") {
12318
+ const outPath = join19(outputDir, `${title}.mp4`);
12319
+ const outTemplate = join19(outputDir, `${title}.%(ext)s`);
12320
+ execSync10(`yt-dlp -f "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best" --merge-output-format mp4 -o "${outTemplate}" "${url}"`, { timeout: 6e5, stdio: "pipe", cwd: outputDir });
12321
+ const actualPath = existsSync16(outPath) ? outPath : outTemplate.replace("%(ext)s", "mp4");
12322
+ return {
12323
+ success: true,
12324
+ output: `Downloaded video: ${actualPath}
12325
+ Title: ${title}
12326
+ Format: mp4`,
12327
+ durationMs: Date.now() - start
12328
+ };
12329
+ } else {
12330
+ const outPath = join19(outputDir, `${title}.mp3`);
12331
+ const outTemplate = join19(outputDir, `${title}.%(ext)s`);
12332
+ execSync10(`yt-dlp -x --audio-format mp3 --audio-quality 0 -o "${outTemplate}" "${url}"`, { timeout: 6e5, stdio: "pipe", cwd: outputDir });
12333
+ const actualPath = existsSync16(outPath) ? outPath : outTemplate.replace("%(ext)s", "mp3");
12334
+ return {
12335
+ success: true,
12336
+ output: `Downloaded audio: ${actualPath}
12337
+ Title: ${title}
12338
+ Format: mp3`,
12339
+ durationMs: Date.now() - start
12340
+ };
12341
+ }
12342
+ } catch (err) {
12343
+ const msg = err instanceof Error ? err.message : String(err);
12344
+ const stderr = err?.stderr?.toString?.()?.slice(0, 500) ?? "";
12345
+ return {
12346
+ success: false,
12347
+ output: "",
12348
+ error: `YouTube download failed: ${msg}${stderr ? "\n" + stderr : ""}`,
12349
+ durationMs: Date.now() - start
12350
+ };
12351
+ }
12352
+ }
12353
+ };
12270
12354
  }
12271
12355
  });
12272
12356
 
@@ -22603,6 +22687,7 @@ __export(dist_exports, {
22603
22687
  WebFetchTool: () => WebFetchTool,
22604
22688
  WebSearchTool: () => WebSearchTool,
22605
22689
  WorkingNotesTool: () => WorkingNotesTool,
22690
+ YouTubeDownloadTool: () => YouTubeDownloadTool,
22606
22691
  applyPatch: () => applyPatch,
22607
22692
  buildCompactDiff: () => buildCompactDiff,
22608
22693
  buildCustomTools: () => buildCustomTools,
@@ -28399,21 +28484,35 @@ ${transcript}`
28399
28484
  thinking;
28400
28485
  /** Abort signal — set by the runner so /stop can cancel in-flight requests */
28401
28486
  _abortSignal = null;
28487
+ /** Multi-key pool — round-robin rotation per request for load distribution */
28488
+ _keyPool = [];
28489
+ _keyIndex = 0;
28402
28490
  constructor(baseUrl, model, apiKey, thinking) {
28403
28491
  this.baseUrl = normalizeBaseUrl(baseUrl);
28404
28492
  this.model = model;
28405
28493
  this.apiKey = apiKey ?? "";
28406
28494
  this.thinking = thinking ?? true;
28407
28495
  }
28496
+ /** Set multiple API keys for round-robin rotation per request */
28497
+ setKeyPool(keys) {
28498
+ this._keyPool = keys.filter((k) => k.length > 0);
28499
+ this._keyIndex = 0;
28500
+ }
28408
28501
  /** Set the abort signal from the runner (called at run start) */
28409
28502
  setAbortSignal(signal) {
28410
28503
  this._abortSignal = signal;
28411
28504
  }
28412
- /** Build auth headers — all providers use standard Bearer token auth. */
28505
+ /** Build auth headers — all providers use standard Bearer token auth.
28506
+ * When a key pool is set, round-robins through keys per request. */
28413
28507
  authHeaders() {
28414
28508
  const headers = { "Content-Type": "application/json" };
28415
- if (this.apiKey) {
28416
- headers["Authorization"] = `Bearer ${this.apiKey}`;
28509
+ let key = this.apiKey;
28510
+ if (this._keyPool.length > 0) {
28511
+ key = this._keyPool[this._keyIndex % this._keyPool.length];
28512
+ this._keyIndex++;
28513
+ }
28514
+ if (key) {
28515
+ headers["Authorization"] = `Bearer ${key}`;
28417
28516
  }
28418
28517
  return headers;
28419
28518
  }
@@ -45945,26 +46044,45 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
45945
46044
  const isArm = process.arch === "arm64" || process.arch === "arm";
45946
46045
  if (isArm) {
45947
46046
  renderInfo(" ARM device detected \u2014 installing build prerequisites then deps individually.");
46047
+ const isMac = process.platform === "darwin";
45948
46048
  try {
45949
46049
  const { spawnSync } = await import("node:child_process");
45950
- renderInfo(" System build tools needed (llvm, gcc). Requesting sudo access...");
45951
- const sudoCheck = spawnSync("sudo", ["-v"], { stdio: "inherit", timeout: 6e4 });
45952
- if (sudoCheck.status === 0) {
45953
- renderInfo(" Installing system build dependencies (this may take a minute)...");
45954
- spawnSync("sudo", [
45955
- "apt-get",
45956
- "install",
45957
- "-y",
45958
- "--no-install-recommends",
45959
- "llvm-dev",
45960
- "gcc",
45961
- "g++",
45962
- "gfortran",
45963
- "libopenblas-dev",
45964
- "libsndfile1-dev"
45965
- ], { stdio: "inherit", timeout: 12e4 });
46050
+ if (isMac) {
46051
+ renderInfo(" macOS ARM detected \u2014 installing build deps via Homebrew...");
46052
+ const brewCheck = spawnSync("which", ["brew"], { stdio: "pipe", timeout: 5e3 });
46053
+ if (brewCheck.status === 0) {
46054
+ spawnSync("brew", ["install", "llvm", "gcc", "openblas", "libsndfile"], {
46055
+ stdio: "inherit",
46056
+ timeout: 3e5
46057
+ });
46058
+ const llvmPrefix = spawnSync("brew", ["--prefix", "llvm"], { stdio: "pipe", timeout: 5e3 });
46059
+ if (llvmPrefix.stdout) {
46060
+ process.env.LLVM_CONFIG = `${llvmPrefix.stdout.toString().trim()}/bin/llvm-config`;
46061
+ }
46062
+ } else {
46063
+ renderWarning(" Homebrew not found. Install it: https://brew.sh");
46064
+ renderWarning(" Then run: brew install llvm gcc openblas libsndfile");
46065
+ }
45966
46066
  } else {
45967
- renderWarning(" sudo not available \u2014 skipping system build deps. librosa/lhotse may fail to compile.");
46067
+ renderInfo(" System build tools needed (llvm, gcc). Requesting sudo access...");
46068
+ const sudoCheck = spawnSync("sudo", ["-v"], { stdio: "inherit", timeout: 6e4 });
46069
+ if (sudoCheck.status === 0) {
46070
+ renderInfo(" Installing system build dependencies (this may take a minute)...");
46071
+ spawnSync("sudo", [
46072
+ "apt-get",
46073
+ "install",
46074
+ "-y",
46075
+ "--no-install-recommends",
46076
+ "llvm-dev",
46077
+ "gcc",
46078
+ "g++",
46079
+ "gfortran",
46080
+ "libopenblas-dev",
46081
+ "libsndfile1-dev"
46082
+ ], { stdio: "inherit", timeout: 12e4 });
46083
+ } else {
46084
+ renderWarning(" sudo not available \u2014 skipping system build deps. librosa/lhotse may fail to compile.");
46085
+ }
45968
46086
  }
45969
46087
  } catch (err) {
45970
46088
  renderWarning(` Could not install system build deps: ${err instanceof Error ? err.message : String(err)}`);
@@ -49969,6 +50087,85 @@ async function handleEndpoint(arg, ctx, local = false) {
49969
50087
  await handleSponsoredEndpoint(ctx, local);
49970
50088
  return;
49971
50089
  }
50090
+ if (arg.startsWith("add ")) {
50091
+ const addArg = arg.slice(4).replace(/\u2014/g, "--").replace(/\u2013/g, "--");
50092
+ const addParts = addArg.split(/\s+/);
50093
+ const addUrl = addParts[0];
50094
+ const addAuthIdx = addParts.indexOf("--auth") !== -1 ? addParts.indexOf("--auth") : addParts.indexOf("-auth");
50095
+ const addKey = addAuthIdx !== -1 && addParts[addAuthIdx + 1] ? addParts[addAuthIdx + 1] : "";
50096
+ if (!addKey) {
50097
+ renderError("Usage: /endpoint add <url> --auth <key>");
50098
+ return;
50099
+ }
50100
+ const normalizedAddUrl = normalizeBaseUrl(addUrl);
50101
+ const suffix = addKey.slice(-4);
50102
+ const provider2 = detectProvider(addUrl);
50103
+ const settings = resolveSettings(ctx.repoRoot);
50104
+ const pool = settings.endpointKeys ?? [];
50105
+ if (pool.some((k) => k.suffix === suffix)) {
50106
+ renderWarning(`Key ending ...${suffix} is already in the pool.`);
50107
+ return;
50108
+ }
50109
+ pool.push({ key: addKey, suffix });
50110
+ const backendType2 = provider2.id === "ollama" ? "ollama" : "vllm";
50111
+ ctx.setEndpoint(normalizedAddUrl, backendType2, addKey);
50112
+ setConfigValue("backendUrl", normalizedAddUrl);
50113
+ setConfigValue("backendType", backendType2);
50114
+ setConfigValue("apiKey", addKey);
50115
+ ctx.saveSettings({ backendUrl: normalizedAddUrl, backendType: backendType2, apiKey: addKey, endpointKeys: pool });
50116
+ saveGlobalSettings({ endpointKeys: pool });
50117
+ if (ctx.setKeyPool)
50118
+ ctx.setKeyPool(pool.map((k) => k.key));
50119
+ recordUsage("endpoint", normalizedAddUrl, {
50120
+ meta: { provider: provider2.label, backendType: backendType2, authHint: addKey.slice(0, 4) + "...", authKey: addKey }
50121
+ });
50122
+ renderInfo(`Added key ...${suffix} to pool (${pool.length} key${pool.length > 1 ? "s" : ""} total)`);
50123
+ for (const k of pool) {
50124
+ process.stdout.write(` ${c2.dim("\u25CF")} ${provider2.label}-${k.suffix} ${c2.dim(k.key.slice(0, 4) + "..." + k.key.slice(-4))}
50125
+ `);
50126
+ }
50127
+ process.stdout.write("\n");
50128
+ return;
50129
+ }
50130
+ if (arg === "keys" || arg === "pool") {
50131
+ const settings = resolveSettings(ctx.repoRoot);
50132
+ const pool = settings.endpointKeys ?? [];
50133
+ if (pool.length === 0) {
50134
+ renderInfo("No key pool configured. Use /endpoint add <url> --auth <key> to add keys.");
50135
+ return;
50136
+ }
50137
+ const provider2 = detectProvider(ctx.config.backendUrl);
50138
+ renderInfo(`Key pool for ${c2.bold(ctx.config.backendUrl)} (${pool.length} key${pool.length > 1 ? "s" : ""}):`);
50139
+ for (const k of pool) {
50140
+ process.stdout.write(` ${c2.green("\u25CF")} ${provider2.label}-${k.suffix} ${c2.dim(k.key.slice(0, 4) + "..." + k.key.slice(-4))}
50141
+ `);
50142
+ }
50143
+ process.stdout.write(`
50144
+ ${c2.dim("Remove: /endpoint remove <suffix>")}
50145
+
50146
+ `);
50147
+ return;
50148
+ }
50149
+ if (arg.startsWith("remove ") || arg.startsWith("rm ")) {
50150
+ const suffix = arg.split(/\s+/)[1];
50151
+ const settings = resolveSettings(ctx.repoRoot);
50152
+ const pool = settings.endpointKeys ?? [];
50153
+ const idx = pool.findIndex((k) => k.suffix === suffix);
50154
+ if (idx === -1) {
50155
+ renderError(`No key with suffix "${suffix}" in the pool.`);
50156
+ return;
50157
+ }
50158
+ pool.splice(idx, 1);
50159
+ ctx.saveSettings({ endpointKeys: pool });
50160
+ saveGlobalSettings({ endpointKeys: pool });
50161
+ if (ctx.setKeyPool)
50162
+ ctx.setKeyPool(pool.map((k) => k.key));
50163
+ renderInfo(`Removed key ...${suffix} from pool (${pool.length} remaining).`);
50164
+ if (pool.length === 0) {
50165
+ renderInfo("Pool empty \u2014 using single API key from config.");
50166
+ }
50167
+ return;
50168
+ }
49972
50169
  const normalizedArg = arg.replace(/\u2014/g, "--").replace(/\u2013/g, "--");
49973
50170
  const parts = normalizedArg.split(/\s+/);
49974
50171
  const url = parts[0];
@@ -63656,9 +63853,10 @@ function buildTools(repoRoot, config, contextWindowSize) {
63656
63853
  // Skill system (AIWG skills — discovery and execution)
63657
63854
  new SkillListTool(repoRoot),
63658
63855
  new SkillExecuteTool(repoRoot),
63659
- // Transcription tools (transcribe-cli / faster-whisper)
63856
+ // Transcription + media download tools (transcribe-cli / faster-whisper / yt-dlp)
63660
63857
  new TranscribeFileTool(repoRoot),
63661
63858
  new TranscribeUrlTool(repoRoot),
63859
+ new YouTubeDownloadTool(repoRoot),
63662
63860
  // Structured file generation (CSV, JSON, Markdown, Excel-compatible)
63663
63861
  new StructuredFileTool(repoRoot),
63664
63862
  // Code sandbox (isolated code execution)
@@ -64049,6 +64247,10 @@ ${lines.join("\n")}
64049
64247
  backend = new NexusAgenticBackend(sendFn, config.model, targetPeer, config.apiKey, thinkingEnabled);
64050
64248
  } else {
64051
64249
  backend = new OllamaAgenticBackend(config.backendUrl, config.model, config.apiKey, thinkingEnabled);
64250
+ const poolKeys = config._keyPool;
64251
+ if (poolKeys && poolKeys.length > 0 && backend instanceof OllamaAgenticBackend) {
64252
+ backend.setKeyPool(poolKeys);
64253
+ }
64052
64254
  }
64053
64255
  try {
64054
64256
  const endpointHistory = loadUsageHistory("endpoint", repoRoot);
@@ -64934,6 +65136,9 @@ async function startInteractive(config, repoPath) {
64934
65136
  }
64935
65137
  if (savedSettings.apiKey)
64936
65138
  config = { ...config, apiKey: savedSettings.apiKey };
65139
+ if (savedSettings.endpointKeys && savedSettings.endpointKeys.length > 0) {
65140
+ config._keyPool = savedSettings.endpointKeys.map((k) => k.key);
65141
+ }
64937
65142
  if (savedSettings.verbose !== void 0)
64938
65143
  config = { ...config, verbose: savedSettings.verbose };
64939
65144
  if (savedSettings.maxRetries !== void 0)
@@ -65982,6 +66187,14 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
65982
66187
  statusBar.stopRemoteMetricsPolling();
65983
66188
  }
65984
66189
  },
66190
+ setKeyPool(keys) {
66191
+ if (activeTask) {
66192
+ const backend = activeTask.runner._backend;
66193
+ if (backend?.setKeyPool)
66194
+ backend.setKeyPool(keys);
66195
+ }
66196
+ currentConfig._keyPool = keys;
66197
+ },
65985
66198
  clearScreen() {
65986
66199
  if (isNeovimActive()) {
65987
66200
  writeToNeovimOutput("[screen cleared]\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.184.69",
3
+ "version": "0.184.71",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",