open-agents-ai 0.103.8 → 0.103.10

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/index.js CHANGED
@@ -12839,7 +12839,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12839
12839
  ];
12840
12840
  NexusTool = class {
12841
12841
  name = "nexus";
12842
- description = "Decentralized agent-to-agent communication via open-agents-nexus v1.5.0. Spawns a background Node.js process with real network sockets for libp2p + NATS P2P mesh. Simple flow: connect \u2192 join room \u2192 send messages. Auto-installs/updates nexus if needed. EXPOSE: expose action queries Ollama models, fetches OpenRouter market rates, registers metered inference capabilities so peers can invoke your models. pricing_menu shows rates. v1.5.0: register_capability, block_peer/unblock_peer, metering_status, room_members. Also supports direct peer invoke, IPFS storage, DMs, discovery, x402, inference proofs. WALLET: wallet_create generates EVM wallet (secp256k1, Base mainnet USDC). wallet_status shows address, USDC balance, and ledger summary. PAYMENTS: ledger_status tracks earnings/spending. budget_status/budget_set configure spending limits. spend: sign EIP-3009 USDC transfer (target_address + amount_usdc). x402 native payment via daemon. REMOTE INFERENCE: remote_infer routes a prompt to a remote peer's model on the mesh. Auto-discovers peers with the requested model capability, budget-checks, invokes, and returns the result. Use: nexus(action='remote_infer', model='qwen3.5:70b', prompt='...'). Optional: target_peer, temperature, max_tokens.";
12842
+ description = "P2P agent mesh networking. IMPORTANT: You MUST call action='connect' FIRST \u2014 it spawns the daemon. Nothing works without it. After connect: join_room, send_message, discover_peers, expose (share models), remote_infer (use remote models). Quick start: connect \u2192 join_room \u2192 send_message. Also: wallet_create, wallet_status, ledger_status, budget_set, spend, invoke_capability, store_content.";
12843
12843
  parameters = {
12844
12844
  type: "object",
12845
12845
  properties: {
@@ -12878,7 +12878,7 @@ process.on('SIGINT', () => process.emit('SIGTERM'));
12878
12878
  "spend",
12879
12879
  "remote_infer"
12880
12880
  ],
12881
- description: "The nexus action to perform"
12881
+ description: "The nexus action. MUST call 'connect' first (spawns daemon). Then: join_room, send_message, discover_peers, expose, status, etc."
12882
12882
  },
12883
12883
  room_id: {
12884
12884
  type: "string",
@@ -25086,6 +25086,13 @@ function renderSlashHelp() {
25086
25086
  ["/nexus", "Show nexus P2P network status (or connect/disconnect/rooms)"],
25087
25087
  ["/nexus connect", "Connect to the agent mesh network"],
25088
25088
  ["/nexus wallet", "Show wallet address and x402 payment status"],
25089
+ ["/expose <backend>", "Share local inference via cloudflared tunnel"],
25090
+ ["/expose <backend> --key", "Share with auto-generated auth key"],
25091
+ ["/expose status", "Show gateway stats (requests, models, errors)"],
25092
+ ["/expose stop", "Stop the expose gateway"],
25093
+ ["/p2p start", "Join the P2P agent mesh network"],
25094
+ ["/p2p status", "Show mesh peers and rooms"],
25095
+ ["/p2p stop", "Leave the mesh network"],
25089
25096
  ["/style", "Show current response style"],
25090
25097
  ["/style <preset>", "Set style: concise, balanced, verbose, pedagogical"],
25091
25098
  ["/commands auto", "Allow agent to invoke slash commands as tools"],
@@ -25148,17 +25155,6 @@ function renderModelSwitch(oldModel, newModel) {
25148
25155
 
25149
25156
  `);
25150
25157
  }
25151
- function renderConfig(config) {
25152
- process.stdout.write(`
25153
- ${c2.bold("Configuration:")}
25154
-
25155
- `);
25156
- for (const [key, value] of Object.entries(config)) {
25157
- process.stdout.write(` ${c2.cyan(key.padEnd(20))} ${value}
25158
- `);
25159
- }
25160
- process.stdout.write("\n");
25161
- }
25162
25158
  function formatToolArgs(toolName, args, verbose) {
25163
25159
  const maxArg = verbose ? 1e4 : Math.max(40, getTermWidth() - 20);
25164
25160
  switch (toolName) {
@@ -26198,10 +26194,57 @@ var init_voice_session = __esm({
26198
26194
 
26199
26195
  // packages/cli/dist/tui/expose.js
26200
26196
  import { createServer as createServer2, request as httpRequest } from "node:http";
26201
- import { spawn as spawn14 } from "node:child_process";
26197
+ import { spawn as spawn14, execSync as execSync25 } from "node:child_process";
26202
26198
  import { EventEmitter as EventEmitter3 } from "node:events";
26203
26199
  import { randomBytes as randomBytes7 } from "node:crypto";
26204
26200
  import { URL as URL2 } from "node:url";
26201
+ import { loadavg, cpus, totalmem, freemem } from "node:os";
26202
+ function collectSystemMetrics() {
26203
+ const [l1, l5, l15] = loadavg();
26204
+ const cores = cpus().length;
26205
+ const totalMem = totalmem();
26206
+ const freeMem = freemem();
26207
+ const usedMem = totalMem - freeMem;
26208
+ const gpu = {
26209
+ available: false,
26210
+ name: "",
26211
+ utilization: 0,
26212
+ vramUsedMB: 0,
26213
+ vramTotalMB: 0,
26214
+ vramUtilization: 0
26215
+ };
26216
+ try {
26217
+ const smi = execSync25("nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total,name --format=csv,noheader,nounits 2>/dev/null", { encoding: "utf8", timeout: 3e3 });
26218
+ const line = smi.trim().split("\n")[0];
26219
+ if (line) {
26220
+ const parts = line.split(",").map((s) => s.trim());
26221
+ gpu.available = true;
26222
+ gpu.utilization = parseInt(parts[0] ?? "0", 10) || 0;
26223
+ gpu.vramUsedMB = parseInt(parts[1] ?? "0", 10) || 0;
26224
+ gpu.vramTotalMB = parseInt(parts[2] ?? "0", 10) || 0;
26225
+ gpu.name = parts[3] ?? "";
26226
+ gpu.vramUtilization = gpu.vramTotalMB > 0 ? Math.round(gpu.vramUsedMB / gpu.vramTotalMB * 100) : 0;
26227
+ }
26228
+ } catch {
26229
+ }
26230
+ return {
26231
+ cpu: {
26232
+ loadAvg1m: Math.round(l1 * 100) / 100,
26233
+ loadAvg5m: Math.round(l5 * 100) / 100,
26234
+ loadAvg15m: Math.round(l15 * 100) / 100,
26235
+ cores,
26236
+ utilization: Math.min(100, Math.round(l1 / cores * 100))
26237
+ },
26238
+ memory: {
26239
+ totalGB: Math.round(totalMem / 1024 ** 3 * 10) / 10,
26240
+ freeGB: Math.round(freeMem / 1024 ** 3 * 10) / 10,
26241
+ usedGB: Math.round(usedMem / 1024 ** 3 * 10) / 10,
26242
+ utilization: Math.round(usedMem / totalMem * 100)
26243
+ },
26244
+ gpu,
26245
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
26246
+ };
26247
+ }
26205
26248
  var DEFAULT_TARGETS, ExposeGateway;
26206
26249
  var init_expose = __esm({
26207
26250
  "packages/cli/dist/tui/expose.js"() {
@@ -26288,85 +26331,102 @@ var init_expose = __esm({
26288
26331
  // ── Proxy server ────────────────────────────────────────────────────────
26289
26332
  createProxyServer(localPort) {
26290
26333
  const target = new URL2(this._targetUrl);
26291
- return createServer2((req, res) => {
26334
+ const server = createServer2((req, res) => {
26335
+ res.on("error", () => {
26336
+ });
26292
26337
  const authHeader = req.headers.authorization;
26293
26338
  const url = new URL2(req.url ?? "/", `http://127.0.0.1:${localPort}`);
26294
26339
  const queryKey = url.searchParams.get("key");
26295
26340
  const providedKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : queryKey;
26296
26341
  if (providedKey !== this._authKey) {
26297
- this._stats.errors++;
26298
26342
  res.writeHead(401, { "Content-Type": "application/json" });
26299
26343
  res.end(JSON.stringify({ error: "Unauthorized \u2014 provide Bearer token or ?key= parameter" }));
26300
- this.emitStats();
26344
+ return;
26345
+ }
26346
+ if (url.pathname === "/v1/system/metrics" && req.method === "GET") {
26347
+ const metrics = collectSystemMetrics();
26348
+ const gatewayStats = {
26349
+ totalRequests: this._stats.totalRequests,
26350
+ activeConnections: this._stats.activeConnections,
26351
+ errors: this._stats.errors,
26352
+ modelUsage: Object.fromEntries(this._stats.modelUsage)
26353
+ };
26354
+ res.writeHead(200, { "Content-Type": "application/json" });
26355
+ res.end(JSON.stringify({ ...metrics, gateway: gatewayStats }));
26301
26356
  return;
26302
26357
  }
26303
26358
  this._stats.totalRequests++;
26304
26359
  this._stats.activeConnections++;
26305
26360
  this.emitStats();
26306
- this.extractModelFromRequest(req);
26307
26361
  url.searchParams.delete("key");
26308
26362
  const forwardPath = url.pathname + url.search;
26309
- const forwardHeaders = { ...req.headers, host: target.host };
26310
- delete forwardHeaders.authorization;
26311
- const proxyReq = httpRequest({
26312
- hostname: target.hostname,
26313
- port: target.port || (target.protocol === "https:" ? 443 : 80),
26314
- path: forwardPath,
26315
- method: req.method,
26316
- headers: forwardHeaders
26317
- }, (proxyRes) => {
26318
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
26319
- proxyRes.pipe(res);
26320
- proxyRes.on("end", () => {
26363
+ const bodyChunks = [];
26364
+ req.on("data", (chunk) => bodyChunks.push(chunk));
26365
+ req.on("end", () => {
26366
+ const body = Buffer.concat(bodyChunks);
26367
+ if (req.method === "POST" && body.length > 0) {
26368
+ try {
26369
+ const text = body.toString("utf8", 0, Math.min(body.length, 1024));
26370
+ const modelMatch = text.match(/"model"\s*:\s*"([^"]+)"/);
26371
+ if (modelMatch) {
26372
+ const model = modelMatch[1];
26373
+ const count = this._stats.modelUsage.get(model) ?? 0;
26374
+ this._stats.modelUsage.set(model, count + 1);
26375
+ }
26376
+ } catch {
26377
+ }
26378
+ }
26379
+ const forwardHeaders = { ...req.headers, host: target.host };
26380
+ delete forwardHeaders.authorization;
26381
+ if (body.length > 0) {
26382
+ forwardHeaders["content-length"] = String(body.length);
26383
+ }
26384
+ const proxyReq = httpRequest({
26385
+ hostname: target.hostname,
26386
+ port: target.port || (target.protocol === "https:" ? 443 : 80),
26387
+ path: forwardPath,
26388
+ method: req.method,
26389
+ headers: forwardHeaders
26390
+ }, (proxyRes) => {
26391
+ proxyRes.on("error", () => {
26392
+ this._stats.activeConnections = Math.max(0, this._stats.activeConnections - 1);
26393
+ this.emitStats();
26394
+ });
26395
+ res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
26396
+ proxyRes.pipe(res);
26397
+ proxyRes.on("end", () => {
26398
+ this._stats.activeConnections = Math.max(0, this._stats.activeConnections - 1);
26399
+ this.emitStats();
26400
+ });
26401
+ });
26402
+ proxyReq.on("error", (err) => {
26403
+ this._stats.errors++;
26321
26404
  this._stats.activeConnections = Math.max(0, this._stats.activeConnections - 1);
26405
+ if (!res.headersSent) {
26406
+ res.writeHead(502, { "Content-Type": "application/json" });
26407
+ res.end(JSON.stringify({ error: `Backend unreachable: ${err.message}` }));
26408
+ } else {
26409
+ try {
26410
+ res.end();
26411
+ } catch {
26412
+ }
26413
+ }
26322
26414
  this.emitStats();
26323
26415
  });
26416
+ if (body.length > 0) {
26417
+ proxyReq.write(body);
26418
+ }
26419
+ proxyReq.end();
26324
26420
  });
26325
- proxyReq.on("error", (err) => {
26326
- this._stats.errors++;
26421
+ req.on("error", () => {
26327
26422
  this._stats.activeConnections = Math.max(0, this._stats.activeConnections - 1);
26328
- if (!res.headersSent) {
26329
- res.writeHead(502, { "Content-Type": "application/json" });
26330
- res.end(JSON.stringify({ error: `Backend unreachable: ${err.message}` }));
26331
- }
26332
- this._stats.status = "error";
26333
26423
  this.emitStats();
26334
- setTimeout(() => {
26335
- if (this.server && this._stats.status === "error") {
26336
- this._stats.status = "active";
26337
- this.emitStats();
26338
- }
26339
- }, 5e3);
26340
26424
  });
26341
- req.pipe(proxyReq);
26342
26425
  });
26343
- }
26344
- /** Best-effort model name extraction from JSON body for usage tracking */
26345
- extractModelFromRequest(req) {
26346
- if (req.method !== "POST")
26347
- return;
26348
- const chunks = [];
26349
- const origEmit = req.emit.bind(req);
26350
- let peeked = false;
26351
- const stats = this._stats;
26352
- req.emit = function(event, ...args) {
26353
- if (event === "data" && !peeked) {
26354
- peeked = true;
26355
- try {
26356
- const chunk = args[0];
26357
- chunks.push(chunk);
26358
- const text = Buffer.concat(chunks).toString();
26359
- const modelMatch = text.match(/"model"\s*:\s*"([^"]+)"/);
26360
- if (modelMatch) {
26361
- const model = modelMatch[1];
26362
- const count = stats.modelUsage.get(model) ?? 0;
26363
- stats.modelUsage.set(model, count + 1);
26364
- }
26365
- } catch {
26366
- }
26367
- }
26368
- return origEmit(event, ...args);
26369
- };
26426
+ server.on("error", (err) => {
26427
+ this.options.onError?.(`Proxy server error: ${err.message}`);
26428
+ });
26429
+ return server;
26370
26430
  }
26371
26431
  // ── Cloudflared ─────────────────────────────────────────────────────────
26372
26432
  startCloudflared(port) {
@@ -28563,7 +28623,7 @@ var init_oa_directory = __esm({
28563
28623
 
28564
28624
  // packages/cli/dist/tui/setup.js
28565
28625
  import * as readline from "node:readline";
28566
- import { execSync as execSync25, spawn as spawn15 } from "node:child_process";
28626
+ import { execSync as execSync26, spawn as spawn15 } from "node:child_process";
28567
28627
  import { existsSync as existsSync29, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10 } from "node:fs";
28568
28628
  import { join as join39 } from "node:path";
28569
28629
  import { homedir as homedir10, platform } from "node:os";
@@ -28573,7 +28633,7 @@ function detectSystemSpecs() {
28573
28633
  let gpuVramGB = 0;
28574
28634
  let gpuName = "";
28575
28635
  try {
28576
- const memInfo = execSync25("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", {
28636
+ const memInfo = execSync26("free -b 2>/dev/null || sysctl -n hw.memsize 2>/dev/null", {
28577
28637
  encoding: "utf8",
28578
28638
  timeout: 5e3
28579
28639
  });
@@ -28593,7 +28653,7 @@ function detectSystemSpecs() {
28593
28653
  } catch {
28594
28654
  }
28595
28655
  try {
28596
- const nvidiaSmi = execSync25("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { encoding: "utf8", timeout: 5e3 });
28656
+ const nvidiaSmi = execSync26("nvidia-smi --query-gpu=memory.total,name --format=csv,noheader,nounits 2>/dev/null", { encoding: "utf8", timeout: 5e3 });
28597
28657
  const lines = nvidiaSmi.trim().split("\n");
28598
28658
  if (lines.length > 0) {
28599
28659
  for (const line of lines) {
@@ -28718,7 +28778,7 @@ function ensureCurl() {
28718
28778
  for (const s of strategies) {
28719
28779
  if (hasCmd(s.check)) {
28720
28780
  try {
28721
- execSync25(s.install, { stdio: "inherit", timeout: 12e4 });
28781
+ execSync26(s.install, { stdio: "inherit", timeout: 12e4 });
28722
28782
  if (hasCmd("curl")) {
28723
28783
  process.stdout.write(` ${c2.green("\u2714")} curl installed via ${s.label}.
28724
28784
  `);
@@ -28732,7 +28792,7 @@ function ensureCurl() {
28732
28792
  }
28733
28793
  if (plat === "darwin") {
28734
28794
  try {
28735
- execSync25("xcode-select --install", { stdio: "inherit", timeout: 3e5 });
28795
+ execSync26("xcode-select --install", { stdio: "inherit", timeout: 3e5 });
28736
28796
  if (hasCmd("curl"))
28737
28797
  return true;
28738
28798
  } catch {
@@ -28767,7 +28827,7 @@ function installOllamaLinux() {
28767
28827
 
28768
28828
  `);
28769
28829
  try {
28770
- execSync25("curl -fsSL https://ollama.com/install.sh | sh", {
28830
+ execSync26("curl -fsSL https://ollama.com/install.sh | sh", {
28771
28831
  stdio: "inherit",
28772
28832
  timeout: 3e5
28773
28833
  });
@@ -28795,7 +28855,7 @@ async function installOllamaMac(_rl) {
28795
28855
 
28796
28856
  `);
28797
28857
  try {
28798
- execSync25('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', { stdio: "inherit", timeout: 6e5 });
28858
+ execSync26('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', { stdio: "inherit", timeout: 6e5 });
28799
28859
  if (!hasCmd("brew")) {
28800
28860
  try {
28801
28861
  const brewPrefix = existsSync29("/opt/homebrew/bin/brew") ? "/opt/homebrew" : "/usr/local";
@@ -28828,7 +28888,7 @@ async function installOllamaMac(_rl) {
28828
28888
 
28829
28889
  `);
28830
28890
  try {
28831
- execSync25("brew install ollama", {
28891
+ execSync26("brew install ollama", {
28832
28892
  stdio: "inherit",
28833
28893
  timeout: 3e5
28834
28894
  });
@@ -28855,7 +28915,7 @@ function installOllamaWindows() {
28855
28915
 
28856
28916
  `);
28857
28917
  try {
28858
- execSync25('powershell -Command "irm https://ollama.com/install.ps1 | iex"', {
28918
+ execSync26('powershell -Command "irm https://ollama.com/install.ps1 | iex"', {
28859
28919
  stdio: "inherit",
28860
28920
  timeout: 3e5
28861
28921
  });
@@ -28936,7 +28996,7 @@ async function ensureOllamaRunning(backendUrl, rl) {
28936
28996
  }
28937
28997
  function pullModelWithAutoUpdate(tag) {
28938
28998
  try {
28939
- execSync25(`ollama pull ${tag}`, {
28999
+ execSync26(`ollama pull ${tag}`, {
28940
29000
  stdio: "inherit",
28941
29001
  timeout: 36e5
28942
29002
  // 1 hour max
@@ -28956,7 +29016,7 @@ function pullModelWithAutoUpdate(tag) {
28956
29016
 
28957
29017
  `);
28958
29018
  try {
28959
- execSync25("curl -fsSL https://ollama.com/install.sh | sh", {
29019
+ execSync26("curl -fsSL https://ollama.com/install.sh | sh", {
28960
29020
  stdio: "inherit",
28961
29021
  timeout: 3e5
28962
29022
  // 5 min max for install
@@ -28967,7 +29027,7 @@ function pullModelWithAutoUpdate(tag) {
28967
29027
  process.stdout.write(` ${c2.cyan("\u25CF")} Retrying pull of ${c2.bold(tag)}...
28968
29028
 
28969
29029
  `);
28970
- execSync25(`ollama pull ${tag}`, {
29030
+ execSync26(`ollama pull ${tag}`, {
28971
29031
  stdio: "inherit",
28972
29032
  timeout: 36e5
28973
29033
  });
@@ -29069,7 +29129,7 @@ function ensurePython3() {
29069
29129
  if (plat === "darwin") {
29070
29130
  if (hasCmd("brew")) {
29071
29131
  try {
29072
- execSync25("brew install python3", { stdio: "inherit", timeout: 3e5 });
29132
+ execSync26("brew install python3", { stdio: "inherit", timeout: 3e5 });
29073
29133
  if (hasCmd("python3")) {
29074
29134
  process.stdout.write(` ${c2.green("\u2714")} Python3 installed via Homebrew.
29075
29135
  `);
@@ -29082,7 +29142,7 @@ function ensurePython3() {
29082
29142
  for (const s of strategies) {
29083
29143
  if (hasCmd(s.check)) {
29084
29144
  try {
29085
- execSync25(s.install, { stdio: "inherit", timeout: 12e4 });
29145
+ execSync26(s.install, { stdio: "inherit", timeout: 12e4 });
29086
29146
  if (hasCmd("python3") || hasCmd("python")) {
29087
29147
  process.stdout.write(` ${c2.green("\u2714")} Python3 installed via ${s.label}.
29088
29148
  `);
@@ -29098,11 +29158,11 @@ function ensurePython3() {
29098
29158
  }
29099
29159
  function checkPythonVenv() {
29100
29160
  try {
29101
- execSync25("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
29161
+ execSync26("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
29102
29162
  return true;
29103
29163
  } catch {
29104
29164
  try {
29105
- execSync25("python -m venv --help", { stdio: "pipe", timeout: 5e3 });
29165
+ execSync26("python -m venv --help", { stdio: "pipe", timeout: 5e3 });
29106
29166
  return true;
29107
29167
  } catch {
29108
29168
  return false;
@@ -29121,7 +29181,7 @@ function ensurePythonVenv() {
29121
29181
  for (const s of strategies) {
29122
29182
  if (hasCmd(s.check)) {
29123
29183
  try {
29124
- execSync25(s.install, { stdio: "inherit", timeout: 12e4 });
29184
+ execSync26(s.install, { stdio: "inherit", timeout: 12e4 });
29125
29185
  if (checkPythonVenv()) {
29126
29186
  process.stdout.write(` ${c2.green("\u2714")} python3-venv installed via ${s.label}.
29127
29187
  `);
@@ -29557,7 +29617,7 @@ async function doSetup(config, rl) {
29557
29617
  const modelfilePath = join39(modelDir2, `Modelfile.${customName}`);
29558
29618
  writeFileSync10(modelfilePath, modelfileContent + "\n", "utf8");
29559
29619
  process.stdout.write(` ${c2.dim("Creating model...")} `);
29560
- execSync25(`ollama create ${customName} -f ${modelfilePath}`, {
29620
+ execSync26(`ollama create ${customName} -f ${modelfilePath}`, {
29561
29621
  stdio: "pipe",
29562
29622
  timeout: 12e4
29563
29623
  });
@@ -29608,7 +29668,7 @@ function isFirstRun() {
29608
29668
  function hasCmd(cmd) {
29609
29669
  try {
29610
29670
  const whichCmd = process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`;
29611
- execSync25(whichCmd, { stdio: "pipe", timeout: 3e3 });
29671
+ execSync26(whichCmd, { stdio: "pipe", timeout: 3e3 });
29612
29672
  return true;
29613
29673
  } catch {
29614
29674
  return false;
@@ -29641,7 +29701,7 @@ function getVenvDir() {
29641
29701
  }
29642
29702
  function hasVenvModule() {
29643
29703
  try {
29644
- execSync25("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
29704
+ execSync26("python3 -m venv --help", { stdio: "pipe", timeout: 5e3 });
29645
29705
  return true;
29646
29706
  } catch {
29647
29707
  return false;
@@ -29663,8 +29723,8 @@ function ensureVenv(log) {
29663
29723
  }
29664
29724
  try {
29665
29725
  mkdirSync10(join39(homedir10(), ".open-agents"), { recursive: true });
29666
- execSync25(`python3 -m venv "${venvDir}"`, { stdio: "pipe", timeout: 3e4 });
29667
- execSync25(`"${join39(venvDir, "bin", "pip")}" install --upgrade pip`, {
29726
+ execSync26(`python3 -m venv "${venvDir}"`, { stdio: "pipe", timeout: 3e4 });
29727
+ execSync26(`"${join39(venvDir, "bin", "pip")}" install --upgrade pip`, {
29668
29728
  stdio: "pipe",
29669
29729
  timeout: 6e4
29670
29730
  });
@@ -29677,7 +29737,7 @@ function ensureVenv(log) {
29677
29737
  }
29678
29738
  function trySudoPasswordless(cmd, timeoutMs = 12e4) {
29679
29739
  try {
29680
- execSync25(`sudo -n ${cmd}`, {
29740
+ execSync26(`sudo -n ${cmd}`, {
29681
29741
  stdio: "pipe",
29682
29742
  timeout: timeoutMs,
29683
29743
  env: { ...process.env, DEBIAN_FRONTEND: "noninteractive" }
@@ -29689,7 +29749,8 @@ function trySudoPasswordless(cmd, timeoutMs = 12e4) {
29689
29749
  }
29690
29750
  function runWithSudo(cmd, password, timeoutMs = 12e4) {
29691
29751
  try {
29692
- execSync25(`sudo -S ${cmd}`, {
29752
+ const escaped = cmd.replace(/'/g, "'\\''");
29753
+ execSync26(`sudo -S bash -c '${escaped}'`, {
29693
29754
  input: password + "\n",
29694
29755
  stdio: ["pipe", "pipe", "pipe"],
29695
29756
  timeout: timeoutMs,
@@ -29796,7 +29857,7 @@ async function ensureVisionDeps(onInfo, getSudoPassword) {
29796
29857
  ok = await sudoInstall(batchCmd, getPassword, log, cachedPasswordRef, 18e4);
29797
29858
  } else {
29798
29859
  try {
29799
- execSync25(batchCmd, { stdio: "pipe", timeout: 18e4 });
29860
+ execSync26(batchCmd, { stdio: "pipe", timeout: 18e4 });
29800
29861
  ok = true;
29801
29862
  } catch {
29802
29863
  ok = false;
@@ -29833,7 +29894,7 @@ async function ensureVisionDeps(onInfo, getSudoPassword) {
29833
29894
  const venvCmds = {
29834
29895
  apt: () => {
29835
29896
  try {
29836
- const pyVer = execSync25(`python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 }).trim();
29897
+ const pyVer = execSync26(`python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 }).trim();
29837
29898
  return `apt-get install -y python3-venv python${pyVer}-venv`;
29838
29899
  } catch {
29839
29900
  return "apt-get install -y python3-venv";
@@ -29861,12 +29922,12 @@ async function ensureVisionDeps(onInfo, getSudoPassword) {
29861
29922
  const venvPip = join39(venvBin, "pip");
29862
29923
  log("Installing moondream-station in ~/.open-agents/venv...");
29863
29924
  try {
29864
- execSync25(`"${venvPip}" install moondream-station`, { stdio: "pipe", timeout: 3e5 });
29925
+ execSync26(`"${venvPip}" install moondream-station`, { stdio: "pipe", timeout: 3e5 });
29865
29926
  if (existsSync29(venvMoondream)) {
29866
29927
  log("moondream-station installed successfully.");
29867
29928
  } else {
29868
29929
  try {
29869
- const check = execSync25(`"${venvPip}" show moondream-station`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 });
29930
+ const check = execSync26(`"${venvPip}" show moondream-station`, { encoding: "utf8", stdio: "pipe", timeout: 5e3 });
29870
29931
  if (check.includes("moondream")) {
29871
29932
  log("moondream-station package installed.");
29872
29933
  }
@@ -29883,7 +29944,7 @@ async function ensureVisionDeps(onInfo, getSudoPassword) {
29883
29944
  const venvPip2 = join39(venvBin, "pip");
29884
29945
  let ocrStackInstalled = false;
29885
29946
  try {
29886
- execSync25(`"${venvPython}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
29947
+ execSync26(`"${venvPython}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
29887
29948
  ocrStackInstalled = true;
29888
29949
  } catch {
29889
29950
  }
@@ -29891,9 +29952,9 @@ async function ensureVisionDeps(onInfo, getSudoPassword) {
29891
29952
  const ocrPackages = "pytesseract Pillow opencv-python-headless numpy";
29892
29953
  log("Installing OCR Python stack (pytesseract, OpenCV, Pillow, numpy)...");
29893
29954
  try {
29894
- execSync25(`"${venvPip2}" install ${ocrPackages}`, { stdio: "pipe", timeout: 3e5 });
29955
+ execSync26(`"${venvPip2}" install ${ocrPackages}`, { stdio: "pipe", timeout: 3e5 });
29895
29956
  try {
29896
- execSync25(`"${venvPython}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
29957
+ execSync26(`"${venvPython}" -c "import cv2, pytesseract, numpy, PIL"`, { stdio: "pipe", timeout: 1e4 });
29897
29958
  log("OCR Python stack installed successfully.");
29898
29959
  } catch {
29899
29960
  log("OCR Python stack install completed but import verification failed.");
@@ -29927,7 +29988,7 @@ function ensureCloudflaredBackground(onInfo) {
29927
29988
  const archMap = { x64: "amd64", arm64: "arm64", arm: "arm" };
29928
29989
  const cfArch = archMap[arch] ?? "amd64";
29929
29990
  try {
29930
- execSync25(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && mkdir -p "${homedir10()}/.local/bin" && mv /tmp/cloudflared "${homedir10()}/.local/bin/cloudflared"`, { stdio: "pipe", timeout: 6e4 });
29991
+ execSync26(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && mkdir -p "${homedir10()}/.local/bin" && mv /tmp/cloudflared "${homedir10()}/.local/bin/cloudflared"`, { stdio: "pipe", timeout: 6e4 });
29931
29992
  if (!process.env.PATH?.includes(`${homedir10()}/.local/bin`)) {
29932
29993
  process.env.PATH = `${homedir10()}/.local/bin:${process.env.PATH}`;
29933
29994
  }
@@ -29938,7 +29999,7 @@ function ensureCloudflaredBackground(onInfo) {
29938
29999
  } catch {
29939
30000
  }
29940
30001
  try {
29941
- execSync25(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && sudo mv /tmp/cloudflared /usr/local/bin/cloudflared 2>/dev/null`, { stdio: "pipe", timeout: 6e4 });
30002
+ execSync26(`curl -fsSL "https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}" -o /tmp/cloudflared && chmod +x /tmp/cloudflared && sudo mv /tmp/cloudflared /usr/local/bin/cloudflared 2>/dev/null`, { stdio: "pipe", timeout: 6e4 });
29942
30003
  if (hasCmd("cloudflared")) {
29943
30004
  log("cloudflared installed.");
29944
30005
  return true;
@@ -29947,7 +30008,7 @@ function ensureCloudflaredBackground(onInfo) {
29947
30008
  }
29948
30009
  } else if (os === "darwin") {
29949
30010
  try {
29950
- execSync25("brew install cloudflared", { stdio: "pipe", timeout: 12e4 });
30011
+ execSync26("brew install cloudflared", { stdio: "pipe", timeout: 12e4 });
29951
30012
  if (hasCmd("cloudflared")) {
29952
30013
  log("cloudflared installed via Homebrew.");
29953
30014
  return true;
@@ -29997,7 +30058,7 @@ function createExpandedVariant(baseModel, specs, sizeGB) {
29997
30058
  mkdirSync10(modelDir2, { recursive: true });
29998
30059
  const modelfilePath = join39(modelDir2, `Modelfile.${customName}`);
29999
30060
  writeFileSync10(modelfilePath, modelfileContent + "\n", "utf8");
30000
- execSync25(`ollama create ${customName} -f ${modelfilePath}`, {
30061
+ execSync26(`ollama create ${customName} -f ${modelfilePath}`, {
30001
30062
  stdio: "pipe",
30002
30063
  timeout: 12e4
30003
30064
  });
@@ -30250,7 +30311,8 @@ function tuiSelect(opts) {
30250
30311
  lines.push(` ${selectColors.dim(` \u25BC ${remaining} more`)}`);
30251
30312
  }
30252
30313
  lines.push("");
30253
- lines.push(` ${selectColors.dim("\u2191/\u2193 navigate Enter select Esc " + (filter ? "clear filter" : "cancel") + " Type to filter")}`);
30314
+ const actionHint = opts.onAction ? " \u2190/\u2192/Space toggle" : "";
30315
+ lines.push(` ${selectColors.dim("\u2191/\u2193 navigate Enter select" + actionHint + " Esc " + (filter ? "clear filter" : "cancel") + " Type to filter")}`);
30254
30316
  lines.push("");
30255
30317
  const output = lines.join("\n");
30256
30318
  process.stdout.write(output);
@@ -30320,6 +30382,21 @@ function tuiSelect(opts) {
30320
30382
  cursor = last;
30321
30383
  render();
30322
30384
  }
30385
+ } else if (seq === "\x1B[D" && opts.onAction) {
30386
+ if (!isSkippable(cursor) && matchSet.has(cursor)) {
30387
+ if (opts.onAction(items[cursor], "left"))
30388
+ render();
30389
+ }
30390
+ } else if (seq === "\x1B[C" && opts.onAction) {
30391
+ if (!isSkippable(cursor) && matchSet.has(cursor)) {
30392
+ if (opts.onAction(items[cursor], "right"))
30393
+ render();
30394
+ }
30395
+ } else if (seq === " " && opts.onAction) {
30396
+ if (!isSkippable(cursor) && matchSet.has(cursor)) {
30397
+ if (opts.onAction(items[cursor], "space"))
30398
+ render();
30399
+ }
30323
30400
  } else if (seq === "\r" || seq === "\n") {
30324
30401
  if (!isSkippable(cursor) && matchSet.has(cursor)) {
30325
30402
  cleanup();
@@ -30426,38 +30503,7 @@ async function handleSlashCommand(input, ctx) {
30426
30503
  return "handled";
30427
30504
  case "config":
30428
30505
  case "cfg": {
30429
- const resolved = loadProjectSettings(ctx.repoRoot);
30430
- const globalS = loadGlobalSettings();
30431
- const merged = { ...globalS, ...resolved };
30432
- renderConfig({
30433
- // -- Core inference --
30434
- model: ctx.config.model,
30435
- backendType: ctx.config.backendType,
30436
- backendUrl: ctx.config.backendUrl,
30437
- apiKey: ctx.config.apiKey ? "[set]" : "[not set]",
30438
- maxRetries: String(ctx.config.maxRetries),
30439
- timeoutMs: String(ctx.config.timeoutMs),
30440
- dbPath: ctx.config.dbPath,
30441
- // -- Behaviour --
30442
- dryRun: String(ctx.config.dryRun),
30443
- verbose: String(ctx.config.verbose),
30444
- stream: String(merged.stream ?? false),
30445
- bruteforce: String(merged.bruteforce ?? false),
30446
- deepContext: String(merged.deepContext ?? false),
30447
- // -- Presentation --
30448
- emojis: String(ctx.getEmojis?.() ?? merged.emojis ?? true),
30449
- colors: String(ctx.getColors?.() ?? merged.colors ?? true),
30450
- style: String(ctx.getStyle?.() ?? merged.style ?? "balanced"),
30451
- // -- Voice --
30452
- voice: String(merged.voice ?? false),
30453
- voiceModel: String(merged.voiceModel ?? "glados"),
30454
- // -- Autonomy --
30455
- commandsMode: String(ctx.getCommandsMode?.() ?? merged.commandsMode ?? "manual"),
30456
- updateMode: String(merged.updateMode ?? "auto"),
30457
- // -- Integrations --
30458
- telegramKey: merged.telegramKey ? "[set]" : "[not set]",
30459
- telegramAdmin: String(merged.telegramAdmin ?? "[not set]")
30460
- });
30506
+ await showConfigEditor(ctx);
30461
30507
  return "handled";
30462
30508
  }
30463
30509
  case "cost":
@@ -30871,25 +30917,26 @@ async function handleSlashCommand(input, ctx) {
30871
30917
  }
30872
30918
  case "commands":
30873
30919
  case "cmds": {
30874
- if (!ctx.getCommandsMode || !ctx.setCommandsMode) {
30875
- renderWarning("Commands mode not available in this context.");
30876
- return "handled";
30877
- }
30878
30920
  if (arg === "auto") {
30921
+ if (!ctx.setCommandsMode) {
30922
+ renderWarning("Commands mode not available.");
30923
+ return "handled";
30924
+ }
30879
30925
  ctx.setCommandsMode("auto");
30880
30926
  const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
30881
30927
  save({ commandsMode: "auto" });
30882
30928
  renderInfo("Commands mode: auto \u2014 the agent can now invoke slash commands as tools.");
30883
30929
  } else if (arg === "manual") {
30930
+ if (!ctx.setCommandsMode) {
30931
+ renderWarning("Commands mode not available.");
30932
+ return "handled";
30933
+ }
30884
30934
  ctx.setCommandsMode("manual");
30885
30935
  const save = hasLocal ? ctx.saveLocalSettings.bind(ctx) : ctx.saveSettings.bind(ctx);
30886
30936
  save({ commandsMode: "manual" });
30887
30937
  renderInfo("Commands mode: manual \u2014 slash commands are user-only.");
30888
30938
  } else {
30889
- const current = ctx.getCommandsMode();
30890
- renderInfo(`Commands mode: ${c2.bold(current)}`);
30891
- renderInfo(" /commands auto \u2014 agent can invoke slash commands as tools");
30892
- renderInfo(" /commands manual \u2014 slash commands are user-only (default)");
30939
+ renderSlashHelp();
30893
30940
  }
30894
30941
  return "handled";
30895
30942
  }
@@ -31496,6 +31543,162 @@ async function handleSlashCommand(input, ctx) {
31496
31543
  }
31497
31544
  }
31498
31545
  }
31546
+ async function showConfigEditor(ctx) {
31547
+ const merged = { ...loadGlobalSettings(), ...loadProjectSettings(ctx.repoRoot) };
31548
+ const pendingChanges = {};
31549
+ let hasChanges = false;
31550
+ const entries = [
31551
+ // -- Headers & categories --
31552
+ { key: "__h_core__", label: c2.dim("\u2500\u2500 Core Inference \u2500\u2500"), detail: "", kind: "header", value: "" },
31553
+ { key: "model", label: "model", kind: "readonly", value: ctx.config.model, detail: ctx.config.model },
31554
+ { key: "backendUrl", label: "backendUrl", kind: "readonly", value: ctx.config.backendUrl, detail: ctx.config.backendUrl },
31555
+ { key: "backendType", label: "backendType", kind: "readonly", value: ctx.config.backendType, detail: ctx.config.backendType },
31556
+ { key: "apiKey", label: "apiKey", kind: "readonly", value: ctx.config.apiKey ? "[set]" : "[not set]", detail: ctx.config.apiKey ? "[set]" : "[not set]" },
31557
+ { key: "maxRetries", label: "maxRetries", kind: "string", value: String(ctx.config.maxRetries), detail: String(ctx.config.maxRetries), settingsKey: "maxRetries" },
31558
+ { key: "timeoutMs", label: "timeoutMs", kind: "string", value: String(ctx.config.timeoutMs), detail: String(ctx.config.timeoutMs), settingsKey: "timeoutMs" },
31559
+ { key: "__h_behav__", label: c2.dim("\u2500\u2500 Behaviour \u2500\u2500"), detail: "", kind: "header", value: "" },
31560
+ { key: "verbose", label: "verbose", kind: "boolean", value: String(ctx.config.verbose), detail: String(ctx.config.verbose), settingsKey: "verbose" },
31561
+ { key: "stream", label: "stream", kind: "boolean", value: String(merged.stream ?? false), detail: String(merged.stream ?? false), settingsKey: "stream" },
31562
+ { key: "bruteforce", label: "bruteforce", kind: "boolean", value: String(merged.bruteforce ?? false), detail: String(merged.bruteforce ?? false), settingsKey: "bruteforce" },
31563
+ { key: "deepContext", label: "deepContext", kind: "boolean", value: String(merged.deepContext ?? false), detail: String(merged.deepContext ?? false), settingsKey: "deepContext" },
31564
+ { key: "dryRun", label: "dryRun", kind: "boolean", value: String(ctx.config.dryRun), detail: String(ctx.config.dryRun), settingsKey: "dryRun" },
31565
+ { key: "__h_pres__", label: c2.dim("\u2500\u2500 Presentation \u2500\u2500"), detail: "", kind: "header", value: "" },
31566
+ { key: "emojis", label: "emojis", kind: "boolean", value: String(ctx.getEmojis?.() ?? merged.emojis ?? true), detail: String(ctx.getEmojis?.() ?? merged.emojis ?? true), settingsKey: "emojis" },
31567
+ { key: "colors", label: "colors", kind: "boolean", value: String(ctx.getColors?.() ?? merged.colors ?? true), detail: String(ctx.getColors?.() ?? merged.colors ?? true), settingsKey: "colors" },
31568
+ { key: "style", label: "style", kind: "enum", value: String(ctx.getStyle?.() ?? merged.style ?? "balanced"), detail: String(ctx.getStyle?.() ?? merged.style ?? "balanced"), options: [...PRESET_NAMES], settingsKey: "style" },
31569
+ { key: "__h_voice__", label: c2.dim("\u2500\u2500 Voice \u2500\u2500"), detail: "", kind: "header", value: "" },
31570
+ { key: "voice", label: "voice", kind: "boolean", value: String(merged.voice ?? false), detail: String(merged.voice ?? false), settingsKey: "voice" },
31571
+ { key: "voiceModel", label: "voiceModel", kind: "enum", value: String(merged.voiceModel ?? "glados"), detail: String(merged.voiceModel ?? "glados"), options: ["glados", "overwatch"], settingsKey: "voiceModel" },
31572
+ { key: "__h_auto__", label: c2.dim("\u2500\u2500 Autonomy \u2500\u2500"), detail: "", kind: "header", value: "" },
31573
+ { key: "commandsMode", label: "commandsMode", kind: "enum", value: String(ctx.getCommandsMode?.() ?? merged.commandsMode ?? "manual"), detail: String(ctx.getCommandsMode?.() ?? merged.commandsMode ?? "manual"), options: ["manual", "auto"], settingsKey: "commandsMode" },
31574
+ { key: "updateMode", label: "updateMode", kind: "enum", value: String(merged.updateMode ?? "auto"), detail: String(merged.updateMode ?? "auto"), options: ["auto", "manual"], settingsKey: "updateMode" },
31575
+ { key: "__h_int__", label: c2.dim("\u2500\u2500 Integrations \u2500\u2500"), detail: "", kind: "header", value: "" },
31576
+ { key: "telegramKey", label: "telegramKey", kind: "readonly", value: merged.telegramKey ? "[set]" : "[not set]", detail: merged.telegramKey ? "[set]" : "[not set]" },
31577
+ { key: "telegramAdmin", label: "telegramAdmin", kind: "readonly", value: String(merged.telegramAdmin ?? "[not set]"), detail: String(merged.telegramAdmin ?? "[not set]") },
31578
+ // -- Actions --
31579
+ { key: "__h_actions__", label: c2.dim("\u2500\u2500 Actions \u2500\u2500"), detail: "", kind: "header", value: "" },
31580
+ { key: "__save_global__", label: c2.bold("Save to global settings"), detail: "~/.open-agents/settings.json", kind: "action", value: "save_global" },
31581
+ { key: "__save_local__", label: c2.bold("Save to project settings"), detail: ".oa/settings.json", kind: "action", value: "save_local" }
31582
+ ];
31583
+ function renderConfigRow(item, focused, _isActive) {
31584
+ if (item.kind === "header") {
31585
+ return ` ${item.label}`;
31586
+ }
31587
+ const marker = focused ? selectColors.orange("\u25CF") : selectColors.dim("\u25CB");
31588
+ const nameStr = focused ? selectColors.orange(selectColors.bold(item.label)) : item.label;
31589
+ if (item.kind === "action") {
31590
+ return ` ${marker} ${nameStr} ${selectColors.dim(item.detail ?? "")}`;
31591
+ }
31592
+ let valueStr;
31593
+ if (item.kind === "boolean") {
31594
+ const val = item.value === "true";
31595
+ valueStr = val ? selectColors.green("true") : c2.red("false");
31596
+ if (focused)
31597
+ valueStr += selectColors.dim(" \u2190/\u2192/Space toggle");
31598
+ } else if (item.kind === "enum") {
31599
+ valueStr = selectColors.cyan(item.value);
31600
+ if (focused && item.options) {
31601
+ const idx = item.options.indexOf(item.value);
31602
+ const prev = item.options[(idx - 1 + item.options.length) % item.options.length];
31603
+ const next = item.options[(idx + 1) % item.options.length];
31604
+ valueStr += selectColors.dim(` \u2190 ${prev} | ${next} \u2192`);
31605
+ }
31606
+ } else if (item.kind === "readonly") {
31607
+ valueStr = selectColors.dim(item.value);
31608
+ } else {
31609
+ valueStr = item.value;
31610
+ if (focused)
31611
+ valueStr += selectColors.dim(" Enter to edit");
31612
+ }
31613
+ return ` ${marker} ${nameStr.padEnd(focused ? 20 : 20)} ${valueStr}`;
31614
+ }
31615
+ function onAction(item, action) {
31616
+ if (item.kind === "boolean") {
31617
+ const newVal = item.value === "true" ? "false" : "true";
31618
+ item.value = newVal;
31619
+ item.detail = newVal;
31620
+ if (item.settingsKey) {
31621
+ pendingChanges[item.settingsKey] = newVal === "true";
31622
+ }
31623
+ hasChanges = true;
31624
+ return true;
31625
+ }
31626
+ if (item.kind === "enum" && item.options) {
31627
+ const idx = item.options.indexOf(item.value);
31628
+ let newIdx;
31629
+ if (action === "left") {
31630
+ newIdx = (idx - 1 + item.options.length) % item.options.length;
31631
+ } else {
31632
+ newIdx = (idx + 1) % item.options.length;
31633
+ }
31634
+ item.value = item.options[newIdx];
31635
+ item.detail = item.value;
31636
+ if (item.settingsKey) {
31637
+ pendingChanges[item.settingsKey] = item.value;
31638
+ }
31639
+ hasChanges = true;
31640
+ return true;
31641
+ }
31642
+ return false;
31643
+ }
31644
+ const skipKeys = entries.filter((e) => e.kind === "header").map((e) => e.key);
31645
+ const result = await tuiSelect({
31646
+ items: entries,
31647
+ title: "Configuration",
31648
+ rl: ctx.rl,
31649
+ skipKeys,
31650
+ renderRow: renderConfigRow,
31651
+ onAction
31652
+ });
31653
+ if (result.confirmed && result.key) {
31654
+ const entry = entries.find((e) => e.key === result.key);
31655
+ if (entry?.kind === "action") {
31656
+ if (entry.value === "save_global") {
31657
+ applyConfigChanges(ctx, pendingChanges);
31658
+ ctx.saveSettings(pendingChanges);
31659
+ renderInfo("Settings saved globally.");
31660
+ hasChanges = false;
31661
+ return;
31662
+ }
31663
+ if (entry.value === "save_local") {
31664
+ applyConfigChanges(ctx, pendingChanges);
31665
+ ctx.saveLocalSettings(pendingChanges);
31666
+ renderInfo("Settings saved to project.");
31667
+ hasChanges = false;
31668
+ return;
31669
+ }
31670
+ }
31671
+ if (entry?.kind === "boolean" || entry?.kind === "enum") {
31672
+ onAction(entry, "right");
31673
+ applyConfigChanges(ctx, pendingChanges);
31674
+ ctx.saveSettings(pendingChanges);
31675
+ renderInfo(`${entry.key} set to ${c2.bold(entry.value)}`);
31676
+ return;
31677
+ }
31678
+ if (entry?.kind === "readonly") {
31679
+ renderInfo(`${entry.key} is read-only. Use the dedicated command (e.g. /model, /endpoint).`);
31680
+ return;
31681
+ }
31682
+ }
31683
+ if (hasChanges) {
31684
+ renderWarning("Unsaved config changes. Saving to global settings...");
31685
+ applyConfigChanges(ctx, pendingChanges);
31686
+ ctx.saveSettings(pendingChanges);
31687
+ renderInfo("Changes saved.");
31688
+ }
31689
+ }
31690
+ function applyConfigChanges(ctx, changes) {
31691
+ if (changes.verbose !== void 0)
31692
+ ctx.setVerbose(changes.verbose);
31693
+ if (changes.emojis !== void 0)
31694
+ ctx.setEmojis?.(changes.emojis);
31695
+ if (changes.colors !== void 0)
31696
+ ctx.setColors?.(changes.colors);
31697
+ if (changes.style !== void 0)
31698
+ ctx.setStyle?.(changes.style);
31699
+ if (changes.commandsMode !== void 0)
31700
+ ctx.setCommandsMode?.(changes.commandsMode);
31701
+ }
31499
31702
  async function listModels(ctx) {
31500
31703
  try {
31501
31704
  const models = await fetchModels(ctx.config.backendUrl, ctx.config.apiKey);
@@ -31709,6 +31912,10 @@ async function handleEndpoint(arg, ctx, local = false) {
31709
31912
  `);
31710
31913
  }
31711
31914
  process.stdout.write("\n");
31915
+ if (ctx.hasActiveTask?.()) {
31916
+ ctx.abortActiveTask?.();
31917
+ renderWarning("Active task aborted \u2014 endpoint changed. Send a new prompt to continue.");
31918
+ }
31712
31919
  try {
31713
31920
  const newModels = await fetchModels(normalizedUrl, apiKey);
31714
31921
  if (newModels.length > 0) {
@@ -32052,7 +32259,7 @@ var init_commands = __esm({
32052
32259
  // packages/cli/dist/tui/project-context.js
32053
32260
  import { existsSync as existsSync30, readFileSync as readFileSync21, readdirSync as readdirSync8 } from "node:fs";
32054
32261
  import { join as join40, basename as basename10 } from "node:path";
32055
- import { execSync as execSync26 } from "node:child_process";
32262
+ import { execSync as execSync27 } from "node:child_process";
32056
32263
  import { homedir as homedir11, platform as platform2, release } from "node:os";
32057
32264
  function getModelTier(modelName) {
32058
32265
  const m = modelName.toLowerCase();
@@ -32098,19 +32305,19 @@ function loadProjectMap(repoRoot) {
32098
32305
  }
32099
32306
  function getGitInfo(repoRoot) {
32100
32307
  try {
32101
- execSync26("git rev-parse --is-inside-work-tree", { cwd: repoRoot, stdio: "pipe" });
32308
+ execSync27("git rev-parse --is-inside-work-tree", { cwd: repoRoot, stdio: "pipe" });
32102
32309
  } catch {
32103
32310
  return "";
32104
32311
  }
32105
32312
  const lines = [];
32106
32313
  try {
32107
- const branch = execSync26("git branch --show-current", { cwd: repoRoot, encoding: "utf-8", stdio: "pipe" }).trim();
32314
+ const branch = execSync27("git branch --show-current", { cwd: repoRoot, encoding: "utf-8", stdio: "pipe" }).trim();
32108
32315
  if (branch)
32109
32316
  lines.push(`Branch: ${branch}`);
32110
32317
  } catch {
32111
32318
  }
32112
32319
  try {
32113
- const status = execSync26("git status --porcelain", { cwd: repoRoot, encoding: "utf-8", stdio: "pipe" }).trim();
32320
+ const status = execSync27("git status --porcelain", { cwd: repoRoot, encoding: "utf-8", stdio: "pipe" }).trim();
32114
32321
  if (status) {
32115
32322
  const changed = status.split("\n").length;
32116
32323
  lines.push(`Working tree: ${changed} changed file(s)`);
@@ -32120,7 +32327,7 @@ function getGitInfo(repoRoot) {
32120
32327
  } catch {
32121
32328
  }
32122
32329
  try {
32123
- const log = execSync26("git log --oneline -5 --no-decorate", { cwd: repoRoot, encoding: "utf-8", stdio: "pipe" }).trim();
32330
+ const log = execSync27("git log --oneline -5 --no-decorate", { cwd: repoRoot, encoding: "utf-8", stdio: "pipe" }).trim();
32124
32331
  if (log)
32125
32332
  lines.push(`Recent commits:
32126
32333
  ${log}`);
@@ -33537,7 +33744,7 @@ var init_carousel_descriptors = __esm({
33537
33744
  import { existsSync as existsSync32, mkdirSync as mkdirSync12, writeFileSync as writeFileSync12, readFileSync as readFileSync23, unlinkSync as unlinkSync4 } from "node:fs";
33538
33745
  import { join as join42 } from "node:path";
33539
33746
  import { homedir as homedir12, tmpdir as tmpdir6, platform as platform3 } from "node:os";
33540
- import { execSync as execSync27, spawn as nodeSpawn } from "node:child_process";
33747
+ import { execSync as execSync28, spawn as nodeSpawn } from "node:child_process";
33541
33748
  import { createRequire } from "node:module";
33542
33749
  function voiceDir() {
33543
33750
  return join42(homedir12(), ".open-agents", "voice");
@@ -34594,7 +34801,7 @@ var init_voice = __esm({
34594
34801
  }
34595
34802
  for (const player of ["paplay", "pw-play", "aplay"]) {
34596
34803
  try {
34597
- execSync27(`which ${player}`, { stdio: "pipe" });
34804
+ execSync28(`which ${player}`, { stdio: "pipe" });
34598
34805
  return [player, path];
34599
34806
  } catch {
34600
34807
  }
@@ -34623,7 +34830,7 @@ var init_voice = __esm({
34623
34830
  return this.python3Path;
34624
34831
  for (const bin of ["python3", "python"]) {
34625
34832
  try {
34626
- const path = execSync27(`which ${bin}`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
34833
+ const path = execSync28(`which ${bin}`, { stdio: "pipe", timeout: 5e3 }).toString().trim();
34627
34834
  if (path) {
34628
34835
  this.python3Path = path;
34629
34836
  return path;
@@ -34643,7 +34850,7 @@ var init_voice = __esm({
34643
34850
  return false;
34644
34851
  }
34645
34852
  try {
34646
- execSync27(`${py} -c "import mlx_audio"`, { stdio: "pipe", timeout: 1e4 });
34853
+ execSync28(`${py} -c "import mlx_audio"`, { stdio: "pipe", timeout: 1e4 });
34647
34854
  this.mlxInstalled = true;
34648
34855
  return true;
34649
34856
  } catch {
@@ -34667,7 +34874,7 @@ var init_voice = __esm({
34667
34874
  return;
34668
34875
  renderInfo("Installing MLX Audio for voice synthesis (first time setup)...");
34669
34876
  try {
34670
- execSync27(`${py} -m pip install mlx-audio --quiet`, {
34877
+ execSync28(`${py} -m pip install mlx-audio --quiet`, {
34671
34878
  stdio: "pipe",
34672
34879
  timeout: 3e5
34673
34880
  // 5 min — may need to compile
@@ -34675,7 +34882,7 @@ var init_voice = __esm({
34675
34882
  this.mlxInstalled = true;
34676
34883
  } catch (err) {
34677
34884
  try {
34678
- execSync27(`${py} -m pip install mlx-audio --user --quiet`, {
34885
+ execSync28(`${py} -m pip install mlx-audio --user --quiet`, {
34679
34886
  stdio: "pipe",
34680
34887
  timeout: 3e5
34681
34888
  });
@@ -34711,11 +34918,11 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
34711
34918
  `tts_gen.main(["--model", ${JSON.stringify(mlxModelId)}, "--text", text, "--voice", ${JSON.stringify(mlxVoice)}, "--lang_code", ${JSON.stringify(mlxLangCode)}, "--audio_path", ${JSON.stringify(wavPath)}])`
34712
34919
  ].join("; ");
34713
34920
  try {
34714
- execSync27(`${py} -c ${JSON.stringify(pyScript)} ${JSON.stringify(JSON.stringify(cleaned))}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34921
+ execSync28(`${py} -c ${JSON.stringify(pyScript)} ${JSON.stringify(JSON.stringify(cleaned))}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34715
34922
  } catch (err) {
34716
34923
  try {
34717
34924
  const safeText = cleaned.replace(/'/g, "'\\''");
34718
- execSync27(`${py} -m mlx_audio.tts.generate --model ${mlxModelId} --text '${safeText}' --voice ${mlxVoice} --lang_code ${mlxLangCode} --audio_path ${JSON.stringify(wavPath)}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34925
+ execSync28(`${py} -m mlx_audio.tts.generate --model ${mlxModelId} --text '${safeText}' --voice ${mlxVoice} --lang_code ${mlxLangCode} --audio_path ${JSON.stringify(wavPath)}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34719
34926
  } catch (err2) {
34720
34927
  return;
34721
34928
  }
@@ -34779,11 +34986,11 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
34779
34986
  `tts_gen.main(["--model", ${JSON.stringify(mlxModelId)}, "--text", text, "--voice", ${JSON.stringify(mlxVoice)}, "--lang_code", ${JSON.stringify(mlxLangCode)}, "--audio_path", ${JSON.stringify(wavPath)}])`
34780
34987
  ].join("; ");
34781
34988
  try {
34782
- execSync27(`${py} -c ${JSON.stringify(pyScript)} ${JSON.stringify(JSON.stringify(cleaned))}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34989
+ execSync28(`${py} -c ${JSON.stringify(pyScript)} ${JSON.stringify(JSON.stringify(cleaned))}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34783
34990
  } catch {
34784
34991
  try {
34785
34992
  const safeText = cleaned.replace(/'/g, "'\\''");
34786
- execSync27(`${py} -m mlx_audio.tts.generate --model ${mlxModelId} --text '${safeText}' --voice ${mlxVoice} --lang_code ${mlxLangCode} --audio_path ${JSON.stringify(wavPath)}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34993
+ execSync28(`${py} -m mlx_audio.tts.generate --model ${mlxModelId} --text '${safeText}' --voice ${mlxVoice} --lang_code ${mlxLangCode} --audio_path ${JSON.stringify(wavPath)}`, { stdio: "pipe", timeout: 6e4, cwd: tmpdir6() });
34787
34994
  } catch {
34788
34995
  return null;
34789
34996
  }
@@ -34831,7 +35038,7 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
34831
35038
  const voiceRequire = createRequire(join42(voiceDir(), "index.js"));
34832
35039
  const probeOnnx = () => {
34833
35040
  try {
34834
- const result = execSync27(`node -e "try { require('onnxruntime-node'); console.log('OK'); } catch(e) { console.log('FAIL:' + e.message); }"`, { cwd: voiceDir(), stdio: "pipe", timeout: 15e3, env: { ...process.env, NODE_PATH: join42(voiceDir(), "node_modules") } });
35041
+ const result = execSync28(`node -e "try { require('onnxruntime-node'); console.log('OK'); } catch(e) { console.log('FAIL:' + e.message); }"`, { cwd: voiceDir(), stdio: "pipe", timeout: 15e3, env: { ...process.env, NODE_PATH: join42(voiceDir(), "node_modules") } });
34835
35042
  const output = result.toString().trim();
34836
35043
  return output === "OK";
34837
35044
  } catch {
@@ -34848,7 +35055,7 @@ Error: ${err2 instanceof Error ? err2.message : String(err2)}`);
34848
35055
  } catch {
34849
35056
  renderInfo("Installing ONNX runtime for voice synthesis...");
34850
35057
  try {
34851
- execSync27("npm install --no-audit --no-fund", {
35058
+ execSync28("npm install --no-audit --no-fund", {
34852
35059
  cwd: voiceDir(),
34853
35060
  stdio: "pipe",
34854
35061
  timeout: 12e4
@@ -34873,7 +35080,7 @@ Error: ${err instanceof Error ? err.message : String(err)}`);
34873
35080
  } catch {
34874
35081
  renderInfo("Installing phonemizer for voice synthesis...");
34875
35082
  try {
34876
- execSync27("npm install --no-audit --no-fund", {
35083
+ execSync28("npm install --no-audit --no-fund", {
34877
35084
  cwd: voiceDir(),
34878
35085
  stdio: "pipe",
34879
35086
  timeout: 12e4
@@ -35957,7 +36164,7 @@ var init_promptLoader3 = __esm({
35957
36164
  // packages/cli/dist/tui/dream-engine.js
35958
36165
  import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync13, readFileSync as readFileSync25, existsSync as existsSync34, cpSync, rmSync, readdirSync as readdirSync10 } from "node:fs";
35959
36166
  import { join as join45, basename as basename12 } from "node:path";
35960
- import { execSync as execSync28 } from "node:child_process";
36167
+ import { execSync as execSync29 } from "node:child_process";
35961
36168
  function loadAutoresearchMemory(repoRoot) {
35962
36169
  const memoryPath = join45(repoRoot, ".oa", "memory", "autoresearch.json");
35963
36170
  if (!existsSync34(memoryPath))
@@ -36321,7 +36528,7 @@ var init_dream_engine = __esm({
36321
36528
  }
36322
36529
  }
36323
36530
  try {
36324
- const output = execSync28(cmd, {
36531
+ const output = execSync29(cmd, {
36325
36532
  cwd: this.repoRoot,
36326
36533
  timeout: 3e4,
36327
36534
  encoding: "utf-8",
@@ -37098,17 +37305,17 @@ ${summaryResult}
37098
37305
  try {
37099
37306
  mkdirSync14(checkpointDir, { recursive: true });
37100
37307
  try {
37101
- const gitStatus = execSync28("git status --porcelain", {
37308
+ const gitStatus = execSync29("git status --porcelain", {
37102
37309
  cwd: this.repoRoot,
37103
37310
  encoding: "utf-8",
37104
37311
  timeout: 1e4
37105
37312
  });
37106
- const gitDiff = execSync28("git diff", {
37313
+ const gitDiff = execSync29("git diff", {
37107
37314
  cwd: this.repoRoot,
37108
37315
  encoding: "utf-8",
37109
37316
  timeout: 1e4
37110
37317
  });
37111
- const gitHash = execSync28("git rev-parse HEAD 2>/dev/null || echo 'no-git'", {
37318
+ const gitHash = execSync29("git rev-parse HEAD 2>/dev/null || echo 'no-git'", {
37112
37319
  cwd: this.repoRoot,
37113
37320
  encoding: "utf-8",
37114
37321
  timeout: 5e3
@@ -40962,6 +41169,62 @@ var init_status_bar = __esm({
40962
41169
  if (this.active)
40963
41170
  this.renderFooterPreserveCursor();
40964
41171
  }
41172
+ /** Remote host metrics (when connected to a remote endpoint) */
41173
+ _remoteMetrics = null;
41174
+ _remoteMetricsTimer = null;
41175
+ /** Update remote host system metrics (from polling /v1/system/metrics) */
41176
+ setRemoteMetrics(metrics) {
41177
+ this._remoteMetrics = metrics;
41178
+ if (this.active)
41179
+ this.renderFooterPreserveCursor();
41180
+ }
41181
+ /** Clear remote metrics (e.g. when switching to local endpoint) */
41182
+ clearRemoteMetrics() {
41183
+ this._remoteMetrics = null;
41184
+ if (this._remoteMetricsTimer) {
41185
+ clearInterval(this._remoteMetricsTimer);
41186
+ this._remoteMetricsTimer = null;
41187
+ }
41188
+ if (this.active)
41189
+ this.renderFooterPreserveCursor();
41190
+ }
41191
+ /**
41192
+ * Start polling a remote endpoint for system metrics.
41193
+ * Call when /endpoint is set to a remote URL that has /v1/system/metrics.
41194
+ */
41195
+ startRemoteMetricsPolling(endpointUrl, authKey) {
41196
+ this.stopRemoteMetricsPolling();
41197
+ const poll = async () => {
41198
+ try {
41199
+ const url = new URL("/v1/system/metrics", endpointUrl);
41200
+ const headers = {};
41201
+ if (authKey)
41202
+ headers["Authorization"] = `Bearer ${authKey}`;
41203
+ const resp = await fetch(url.toString(), { headers, signal: AbortSignal.timeout(5e3) });
41204
+ if (resp.ok) {
41205
+ const data = await resp.json();
41206
+ this.setRemoteMetrics({
41207
+ cpuUtil: data.cpu?.utilization ?? 0,
41208
+ gpuUtil: data.gpu?.available ? data.gpu.utilization ?? 0 : -1,
41209
+ gpuName: data.gpu?.name ?? "",
41210
+ vramUtil: data.gpu?.available ? data.gpu.vramUtilization ?? 0 : -1,
41211
+ memUtil: data.memory?.utilization ?? 0
41212
+ });
41213
+ }
41214
+ } catch {
41215
+ }
41216
+ };
41217
+ poll();
41218
+ this._remoteMetricsTimer = setInterval(poll, 1e4);
41219
+ }
41220
+ /** Stop polling remote metrics */
41221
+ stopRemoteMetricsPolling() {
41222
+ if (this._remoteMetricsTimer) {
41223
+ clearInterval(this._remoteMetricsTimer);
41224
+ this._remoteMetricsTimer = null;
41225
+ }
41226
+ this._remoteMetrics = null;
41227
+ }
40965
41228
  /** Update token metrics from a token_usage event */
40966
41229
  updateMetrics(update) {
40967
41230
  if (update.promptTokens !== void 0)
@@ -41325,6 +41588,35 @@ var init_status_bar = __esm({
41325
41588
  empty: false
41326
41589
  });
41327
41590
  }
41591
+ if (this._remoteMetrics) {
41592
+ const rm2 = this._remoteMetrics;
41593
+ const cpuColor = rm2.cpuUtil > 80 ? c2.red : rm2.cpuUtil > 50 ? c2.yellow : c2.green;
41594
+ const cpuStr = `CPU:${cpuColor(rm2.cpuUtil + "%")}`;
41595
+ const cpuW = 4 + `${rm2.cpuUtil}%`.length;
41596
+ let gpuStr = "";
41597
+ let gpuW = 0;
41598
+ if (rm2.gpuUtil >= 0) {
41599
+ const gpuColor = rm2.gpuUtil > 80 ? c2.red : rm2.gpuUtil > 50 ? c2.yellow : c2.green;
41600
+ gpuStr = ` GPU:${gpuColor(rm2.gpuUtil + "%")}`;
41601
+ gpuW = 5 + `${rm2.gpuUtil}%`.length;
41602
+ }
41603
+ let vramStr = "";
41604
+ let vramW = 0;
41605
+ if (rm2.vramUtil >= 0) {
41606
+ const vramColor = rm2.vramUtil > 80 ? c2.red : rm2.vramUtil > 50 ? c2.yellow : c2.green;
41607
+ vramStr = ` VRAM:${vramColor(rm2.vramUtil + "%")}`;
41608
+ vramW = 6 + `${rm2.vramUtil}%`.length;
41609
+ }
41610
+ const remoteExpanded = pastel2(117, "\u{1F4E1}") + " " + cpuStr + gpuStr + vramStr;
41611
+ const remoteCompact = pastel2(117, "\u{1F4E1}") + " " + cpuStr + gpuStr;
41612
+ sections.push({
41613
+ expanded: remoteExpanded,
41614
+ compact: remoteCompact,
41615
+ expandedW: 3 + cpuW + gpuW + vramW,
41616
+ compactW: 3 + cpuW + gpuW,
41617
+ empty: false
41618
+ });
41619
+ }
41328
41620
  if (this._recording) {
41329
41621
  const dot = this._recBlink ? pastel2(210, "\u25CF") : " ";
41330
41622
  const countdown = this._countdown > 0 ? c2.dim(` ${this._countdown}s`) : "";
@@ -43020,6 +43312,12 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
43020
43312
  };
43021
43313
  const newProvider = detectProvider(url);
43022
43314
  costTracker.setProvider(newProvider.id);
43315
+ const isRemote = !url.includes("127.0.0.1") && !url.includes("localhost");
43316
+ if (isRemote) {
43317
+ statusBar.startRemoteMetricsPolling(url, apiKey);
43318
+ } else {
43319
+ statusBar.stopRemoteMetricsPolling();
43320
+ }
43023
43321
  },
43024
43322
  clearScreen() {
43025
43323
  process.stdout.write("\x1B[2J\x1B[H");
@@ -43712,6 +44010,11 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
43712
44010
  },
43713
44011
  refreshModelCache,
43714
44012
  hasActiveTask: () => activeTask !== null,
44013
+ abortActiveTask() {
44014
+ if (!activeTask)
44015
+ return;
44016
+ activeTask.runner.abort();
44017
+ },
43715
44018
  requestCompaction(strategy) {
43716
44019
  if (!activeTask)
43717
44020
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.103.8",
3
+ "version": "0.103.10",
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",
@@ -238,11 +238,13 @@ If you notice you're performing the SAME multi-step sequence for the 3rd time or
238
238
 
239
239
  ## Nexus P2P Networking (v1.5.6) — Decentralized Agent Communication + x402 Payments
240
240
 
241
- You HAVE the nexus tool. It is one of your registered tools. USE IT when asked about connecting, messaging, or networking with other agents.
241
+ You HAVE the nexus tool. USE IT when asked about connecting, messaging, or networking with other agents.
242
242
 
243
- open-agents-nexus is auto-installed on first use. Requires Node >= 22 (Promise.withResolvers).
243
+ **CRITICAL: ALWAYS call nexus(action='connect') FIRST.** It spawns the daemon process. No other action works without it.
244
244
 
245
- ### Quick Start (3 steps)
245
+ Auto-installs open-agents-nexus on first use. Requires Node >= 22.
246
+
247
+ ### Quick Start (3 steps — connect MUST be first)
246
248
  nexus(action='connect', agent_name='MyAgent')
247
249
  nexus(action='join_room', room_id='general')
248
250
  nexus(action='send_message', room_id='general', message='Hello from MyAgent!')
@@ -21,7 +21,7 @@ NEVER say "I can't do that". ALWAYS attempt the task using your tools. If a tool
21
21
  - web_search: Search the web
22
22
  - web_fetch: Fetch a web page's text
23
23
  - memory_read / memory_write: Persistent memory across sessions
24
- - nexus: P2P agent networking (connect, join_room, send_message, discover_peers, invoke_capability, register_capability, block_peer, metering_status, room_members, wallet, etc.)
24
+ - nexus: P2P agent mesh. ALWAYS call connect FIRST (spawns daemon). Then: join_room, send_message, discover_peers, expose, etc.
25
25
  - task_complete: Signal completion with a summary
26
26
  - background_run / task_status / task_output / task_stop: Background tasks
27
27
  - sub_agent: Delegate a subtask to an independent agent (use background=true for parallel work)
@@ -67,7 +67,7 @@ You are **Open Agent** (open-agents-ai), an autonomous AI coding agent running o
67
67
  - Web: search documentation and fetch web pages
68
68
  - Memory: persistent cross-session knowledge (memory_read/memory_write)
69
69
  - Skills: 250+ behavioral skills (skill_list), build new ones (skill_build)
70
- - P2P: connect to other agents via nexus (libp2p + NATS mesh)
70
+ - P2P: nexus agent mesh ALWAYS call nexus(action='connect') FIRST, then join_room/send_message/discover_peers/expose
71
71
  - Background tasks: run long commands in background, check status later
72
72
  - Voice/TTS: text-to-speech via ONNX (cross-platform) or MLX (Apple Silicon) — use /voice to enable
73
73
  - Desktop/Vision: screenshot, click UI, OCR (via explore_tools)
@@ -20,5 +20,5 @@ Rules:
20
20
  - Directory entries are RELATIVE. If you list "parent/" and see "child", the path is "parent/child" — NOT ".child".
21
21
  - Use list_directory for directories, NOT file_read. Prefer list_directory over shell ls.
22
22
  - You are **Open Agent** (open-agents-ai) — an AI coding agent running locally via Ollama/vLLM. No cloud APIs.
23
- - Core: code editing, shell commands, web search, memory, 250+ skills (skill_list), P2P networking (nexus), background tasks.
23
+ - Core: code editing, shell commands, web search, memory, 250+ skills (skill_list), P2P mesh (nexus — call connect FIRST), background tasks.
24
24
  - When asked "what can you do?", use explore_tools() and skill_list() to discover and report your actual capabilities. Do NOT hallucinate.