open-agents-ai 0.66.0 → 0.67.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.
Files changed (2) hide show
  1. package/dist/index.js +626 -50
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -19129,14 +19129,14 @@ var require_extension = __commonJS({
19129
19129
  var require_websocket = __commonJS({
19130
19130
  "node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket.js"(exports, module) {
19131
19131
  "use strict";
19132
- var EventEmitter4 = __require("events");
19132
+ var EventEmitter5 = __require("events");
19133
19133
  var https = __require("https");
19134
19134
  var http = __require("http");
19135
19135
  var net = __require("net");
19136
19136
  var tls = __require("tls");
19137
- var { randomBytes: randomBytes5, createHash: createHash2 } = __require("crypto");
19137
+ var { randomBytes: randomBytes6, createHash: createHash2 } = __require("crypto");
19138
19138
  var { Duplex, Readable } = __require("stream");
19139
- var { URL: URL2 } = __require("url");
19139
+ var { URL: URL3 } = __require("url");
19140
19140
  var PerMessageDeflate = require_permessage_deflate();
19141
19141
  var Receiver2 = require_receiver();
19142
19142
  var Sender2 = require_sender();
@@ -19161,7 +19161,7 @@ var require_websocket = __commonJS({
19161
19161
  var protocolVersions = [8, 13];
19162
19162
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
19163
19163
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
19164
- var WebSocket2 = class _WebSocket extends EventEmitter4 {
19164
+ var WebSocket2 = class _WebSocket extends EventEmitter5 {
19165
19165
  /**
19166
19166
  * Create a new `WebSocket`.
19167
19167
  *
@@ -19629,11 +19629,11 @@ var require_websocket = __commonJS({
19629
19629
  );
19630
19630
  }
19631
19631
  let parsedUrl;
19632
- if (address instanceof URL2) {
19632
+ if (address instanceof URL3) {
19633
19633
  parsedUrl = address;
19634
19634
  } else {
19635
19635
  try {
19636
- parsedUrl = new URL2(address);
19636
+ parsedUrl = new URL3(address);
19637
19637
  } catch (e) {
19638
19638
  throw new SyntaxError(`Invalid URL: ${address}`);
19639
19639
  }
@@ -19664,7 +19664,7 @@ var require_websocket = __commonJS({
19664
19664
  }
19665
19665
  }
19666
19666
  const defaultPort = isSecure ? 443 : 80;
19667
- const key = randomBytes5(16).toString("base64");
19667
+ const key = randomBytes6(16).toString("base64");
19668
19668
  const request = isSecure ? https.request : http.request;
19669
19669
  const protocolSet = /* @__PURE__ */ new Set();
19670
19670
  let perMessageDeflate;
@@ -19770,7 +19770,7 @@ var require_websocket = __commonJS({
19770
19770
  req.abort();
19771
19771
  let addr;
19772
19772
  try {
19773
- addr = new URL2(location, address);
19773
+ addr = new URL3(location, address);
19774
19774
  } catch (e) {
19775
19775
  const err = new SyntaxError(`Invalid URL: ${location}`);
19776
19776
  emitErrorAndClose(websocket, err);
@@ -20158,7 +20158,7 @@ var require_subprotocol = __commonJS({
20158
20158
  var require_websocket_server = __commonJS({
20159
20159
  "node_modules/.pnpm/ws@8.19.0/node_modules/ws/lib/websocket-server.js"(exports, module) {
20160
20160
  "use strict";
20161
- var EventEmitter4 = __require("events");
20161
+ var EventEmitter5 = __require("events");
20162
20162
  var http = __require("http");
20163
20163
  var { Duplex } = __require("stream");
20164
20164
  var { createHash: createHash2 } = __require("crypto");
@@ -20171,7 +20171,7 @@ var require_websocket_server = __commonJS({
20171
20171
  var RUNNING = 0;
20172
20172
  var CLOSING = 1;
20173
20173
  var CLOSED = 2;
20174
- var WebSocketServer2 = class extends EventEmitter4 {
20174
+ var WebSocketServer2 = class extends EventEmitter5 {
20175
20175
  /**
20176
20176
  * Create a `WebSocketServer` instance.
20177
20177
  *
@@ -22173,8 +22173,278 @@ var init_voice_session = __esm({
22173
22173
  }
22174
22174
  });
22175
22175
 
22176
- // packages/cli/dist/tui/call-agent.js
22176
+ // packages/cli/dist/tui/expose.js
22177
+ import { createServer as createServer2, request as httpRequest } from "node:http";
22178
+ import { spawn as spawn11 } from "node:child_process";
22177
22179
  import { EventEmitter as EventEmitter3 } from "node:events";
22180
+ import { randomBytes as randomBytes5 } from "node:crypto";
22181
+ import { URL as URL2 } from "node:url";
22182
+ var DEFAULT_TARGETS, ExposeGateway;
22183
+ var init_expose = __esm({
22184
+ "packages/cli/dist/tui/expose.js"() {
22185
+ "use strict";
22186
+ init_render();
22187
+ DEFAULT_TARGETS = {
22188
+ ollama: "http://127.0.0.1:11434",
22189
+ vllm: "http://127.0.0.1:8000",
22190
+ llvm: "http://127.0.0.1:8080",
22191
+ custom: "http://127.0.0.1:11434"
22192
+ };
22193
+ ExposeGateway = class extends EventEmitter3 {
22194
+ options;
22195
+ server = null;
22196
+ cloudflaredProcess = null;
22197
+ _tunnelUrl = null;
22198
+ _authKey;
22199
+ _targetUrl;
22200
+ _kind;
22201
+ _stats = {
22202
+ status: "standby",
22203
+ totalRequests: 0,
22204
+ activeConnections: 0,
22205
+ errors: 0,
22206
+ modelUsage: /* @__PURE__ */ new Map()
22207
+ };
22208
+ get tunnelUrl() {
22209
+ return this._tunnelUrl;
22210
+ }
22211
+ get authKey() {
22212
+ return this._authKey;
22213
+ }
22214
+ get stats() {
22215
+ return this._stats;
22216
+ }
22217
+ get kind() {
22218
+ return this._kind;
22219
+ }
22220
+ get isActive() {
22221
+ return this._stats.status === "active";
22222
+ }
22223
+ constructor(options) {
22224
+ super();
22225
+ this.options = options;
22226
+ this._kind = options.kind;
22227
+ this._targetUrl = options.targetUrl ?? DEFAULT_TARGETS[options.kind];
22228
+ if (options.authKey === void 0 || options.authKey === "") {
22229
+ this._authKey = randomBytes5(24).toString("base64url");
22230
+ } else {
22231
+ this._authKey = options.authKey;
22232
+ }
22233
+ }
22234
+ // ── Lifecycle ───────────────────────────────────────────────────────────
22235
+ async start() {
22236
+ if (this.server)
22237
+ throw new Error("Gateway already running");
22238
+ const port = await this.findFreePort();
22239
+ this.server = this.createProxyServer(port);
22240
+ await new Promise((resolve28, reject) => {
22241
+ this.server.listen(port, "127.0.0.1", () => resolve28());
22242
+ this.server.on("error", reject);
22243
+ });
22244
+ this._tunnelUrl = await this.startCloudflared(port);
22245
+ this._stats.status = "active";
22246
+ this.emitStats();
22247
+ return this._tunnelUrl;
22248
+ }
22249
+ async stop() {
22250
+ if (this.cloudflaredProcess) {
22251
+ this.cloudflaredProcess.kill("SIGTERM");
22252
+ this.cloudflaredProcess = null;
22253
+ }
22254
+ if (this.server) {
22255
+ await new Promise((resolve28) => {
22256
+ this.server.close(() => resolve28());
22257
+ });
22258
+ this.server = null;
22259
+ }
22260
+ this._tunnelUrl = null;
22261
+ this._stats.status = "standby";
22262
+ this._stats.activeConnections = 0;
22263
+ this.emitStats();
22264
+ }
22265
+ // ── Proxy server ────────────────────────────────────────────────────────
22266
+ createProxyServer(localPort) {
22267
+ const target = new URL2(this._targetUrl);
22268
+ return createServer2((req, res) => {
22269
+ const authHeader = req.headers.authorization;
22270
+ const url = new URL2(req.url ?? "/", `http://127.0.0.1:${localPort}`);
22271
+ const queryKey = url.searchParams.get("key");
22272
+ const providedKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : queryKey;
22273
+ if (providedKey !== this._authKey) {
22274
+ this._stats.errors++;
22275
+ res.writeHead(401, { "Content-Type": "application/json" });
22276
+ res.end(JSON.stringify({ error: "Unauthorized \u2014 provide Bearer token or ?key= parameter" }));
22277
+ this.emitStats();
22278
+ return;
22279
+ }
22280
+ this._stats.totalRequests++;
22281
+ this._stats.activeConnections++;
22282
+ this.emitStats();
22283
+ this.extractModelFromRequest(req);
22284
+ url.searchParams.delete("key");
22285
+ const forwardPath = url.pathname + url.search;
22286
+ const proxyReq = httpRequest({
22287
+ hostname: target.hostname,
22288
+ port: target.port || (target.protocol === "https:" ? 443 : 80),
22289
+ path: forwardPath,
22290
+ method: req.method,
22291
+ headers: {
22292
+ ...req.headers,
22293
+ host: target.host,
22294
+ // Remove auth header — the backend doesn't need it
22295
+ authorization: void 0
22296
+ }
22297
+ }, (proxyRes) => {
22298
+ res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
22299
+ proxyRes.pipe(res);
22300
+ proxyRes.on("end", () => {
22301
+ this._stats.activeConnections = Math.max(0, this._stats.activeConnections - 1);
22302
+ this.emitStats();
22303
+ });
22304
+ });
22305
+ proxyReq.on("error", (err) => {
22306
+ this._stats.errors++;
22307
+ this._stats.activeConnections = Math.max(0, this._stats.activeConnections - 1);
22308
+ if (!res.headersSent) {
22309
+ res.writeHead(502, { "Content-Type": "application/json" });
22310
+ res.end(JSON.stringify({ error: `Backend unreachable: ${err.message}` }));
22311
+ }
22312
+ this._stats.status = "error";
22313
+ this.emitStats();
22314
+ setTimeout(() => {
22315
+ if (this.server && this._stats.status === "error") {
22316
+ this._stats.status = "active";
22317
+ this.emitStats();
22318
+ }
22319
+ }, 5e3);
22320
+ });
22321
+ req.pipe(proxyReq);
22322
+ });
22323
+ }
22324
+ /** Best-effort model name extraction from JSON body for usage tracking */
22325
+ extractModelFromRequest(req) {
22326
+ if (req.method !== "POST")
22327
+ return;
22328
+ const chunks = [];
22329
+ const origEmit = req.emit.bind(req);
22330
+ let peeked = false;
22331
+ const stats = this._stats;
22332
+ req.emit = function(event, ...args) {
22333
+ if (event === "data" && !peeked) {
22334
+ peeked = true;
22335
+ try {
22336
+ const chunk = args[0];
22337
+ chunks.push(chunk);
22338
+ const text = Buffer.concat(chunks).toString();
22339
+ const modelMatch = text.match(/"model"\s*:\s*"([^"]+)"/);
22340
+ if (modelMatch) {
22341
+ const model = modelMatch[1];
22342
+ const count = stats.modelUsage.get(model) ?? 0;
22343
+ stats.modelUsage.set(model, count + 1);
22344
+ }
22345
+ } catch {
22346
+ }
22347
+ }
22348
+ return origEmit(event, ...args);
22349
+ };
22350
+ }
22351
+ // ── Cloudflared ─────────────────────────────────────────────────────────
22352
+ startCloudflared(port) {
22353
+ return new Promise((resolve28, reject) => {
22354
+ const timeout = setTimeout(() => {
22355
+ reject(new Error("Cloudflared tunnel start timeout (30s)"));
22356
+ }, 3e4);
22357
+ this.cloudflaredProcess = spawn11("cloudflared", [
22358
+ "tunnel",
22359
+ "--url",
22360
+ `http://127.0.0.1:${port}`
22361
+ ], {
22362
+ stdio: ["ignore", "pipe", "pipe"]
22363
+ });
22364
+ let urlFound = false;
22365
+ const handleOutput = (data) => {
22366
+ const text = data.toString();
22367
+ const urlMatch = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
22368
+ if (urlMatch && !urlFound) {
22369
+ urlFound = true;
22370
+ clearTimeout(timeout);
22371
+ resolve28(urlMatch[0]);
22372
+ }
22373
+ };
22374
+ this.cloudflaredProcess.stdout?.on("data", handleOutput);
22375
+ this.cloudflaredProcess.stderr?.on("data", handleOutput);
22376
+ this.cloudflaredProcess.on("error", (err) => {
22377
+ clearTimeout(timeout);
22378
+ reject(new Error(`Failed to start cloudflared: ${err.message}`));
22379
+ });
22380
+ this.cloudflaredProcess.on("close", (code) => {
22381
+ if (!urlFound) {
22382
+ clearTimeout(timeout);
22383
+ reject(new Error(`Cloudflared exited with code ${code} before providing URL`));
22384
+ }
22385
+ if (this._stats.status === "active") {
22386
+ this._stats.status = "error";
22387
+ this.emitStats();
22388
+ }
22389
+ });
22390
+ });
22391
+ }
22392
+ // ── Helpers ─────────────────────────────────────────────────────────────
22393
+ findFreePort() {
22394
+ return new Promise((resolve28, reject) => {
22395
+ const srv = createServer2();
22396
+ srv.listen(0, "127.0.0.1", () => {
22397
+ const addr = srv.address();
22398
+ if (addr && typeof addr === "object") {
22399
+ const port = addr.port;
22400
+ srv.close(() => resolve28(port));
22401
+ } else {
22402
+ srv.close(() => reject(new Error("Could not find free port")));
22403
+ }
22404
+ });
22405
+ srv.on("error", reject);
22406
+ });
22407
+ }
22408
+ emitStats() {
22409
+ this.emit("stats", { ...this._stats, modelUsage: new Map(this._stats.modelUsage) });
22410
+ }
22411
+ /** Format connection info for display */
22412
+ formatConnectionInfo() {
22413
+ const lines = [];
22414
+ lines.push(` ${c2.green("\u25CF")} Expose gateway active`);
22415
+ lines.push(` ${c2.cyan("Backend".padEnd(12))} ${this._kind} (${this._targetUrl})`);
22416
+ lines.push(` ${c2.cyan("Tunnel".padEnd(12))} ${c2.bold(this._tunnelUrl ?? "starting...")}`);
22417
+ lines.push(` ${c2.cyan("Auth Key".padEnd(12))} ${c2.bold(this._authKey)}`);
22418
+ lines.push("");
22419
+ lines.push(` ${c2.dim("Remote users connect with:")}`);
22420
+ lines.push(` ${c2.bold(`/endpoint ${this._tunnelUrl ?? "<tunnel-url>"} --auth ${this._authKey}`)}`);
22421
+ lines.push("");
22422
+ lines.push(` ${c2.yellow("!")} ${c2.dim("Keep this key secure \u2014 it grants full access to your models.")}`);
22423
+ return lines.join("\n");
22424
+ }
22425
+ /** Format usage stats for display */
22426
+ formatStats() {
22427
+ const s = this._stats;
22428
+ const lines = [];
22429
+ lines.push(` ${c2.bold("Expose Gateway Stats")}`);
22430
+ lines.push(` ${c2.cyan("Status".padEnd(16))} ${s.status === "active" ? c2.green("active") : s.status === "error" ? c2.red("error") : c2.yellow("standby")}`);
22431
+ lines.push(` ${c2.cyan("Total requests".padEnd(16))} ${s.totalRequests}`);
22432
+ lines.push(` ${c2.cyan("Active conns".padEnd(16))} ${s.activeConnections}`);
22433
+ lines.push(` ${c2.cyan("Errors".padEnd(16))} ${s.errors}`);
22434
+ if (s.modelUsage.size > 0) {
22435
+ lines.push(` ${c2.cyan("Models used:")}`);
22436
+ for (const [model, count] of s.modelUsage) {
22437
+ lines.push(` ${model}: ${count} requests`);
22438
+ }
22439
+ }
22440
+ return lines.join("\n");
22441
+ }
22442
+ };
22443
+ }
22444
+ });
22445
+
22446
+ // packages/cli/dist/tui/call-agent.js
22447
+ import { EventEmitter as EventEmitter4 } from "node:events";
22178
22448
  import crypto2 from "node:crypto";
22179
22449
  function adaptTool(tool) {
22180
22450
  return {
@@ -22234,7 +22504,7 @@ var init_call_agent = __esm({
22234
22504
  }
22235
22505
  };
22236
22506
  _globalFeed = null;
22237
- CallSubAgent = class extends EventEmitter3 {
22507
+ CallSubAgent = class extends EventEmitter4 {
22238
22508
  tier;
22239
22509
  clientId;
22240
22510
  runner = null;
@@ -23269,7 +23539,7 @@ var init_oa_directory = __esm({
23269
23539
 
23270
23540
  // packages/cli/dist/tui/setup.js
23271
23541
  import * as readline from "node:readline";
23272
- import { execSync as execSync20, spawn as spawn11 } from "node:child_process";
23542
+ import { execSync as execSync20, spawn as spawn12 } from "node:child_process";
23273
23543
  import { existsSync as existsSync22, writeFileSync as writeFileSync8, mkdirSync as mkdirSync8 } from "node:fs";
23274
23544
  import { join as join31 } from "node:path";
23275
23545
  import { homedir as homedir10, platform } from "node:os";
@@ -23755,7 +24025,7 @@ async function doSetup(config, rl) {
23755
24025
  ${c2.cyan("\u25CF")} Starting ollama serve in background...
23756
24026
  `);
23757
24027
  try {
23758
- const child = spawn11("ollama", ["serve"], { stdio: "ignore", detached: true });
24028
+ const child = spawn12("ollama", ["serve"], { stdio: "ignore", detached: true });
23759
24029
  child.unref();
23760
24030
  await new Promise((resolve28) => setTimeout(resolve28, 3e3));
23761
24031
  try {
@@ -23783,7 +24053,7 @@ async function doSetup(config, rl) {
23783
24053
  ${c2.cyan("\u25CF")} Starting ollama serve...
23784
24054
  `);
23785
24055
  try {
23786
- const child = spawn11("ollama", ["serve"], { stdio: "ignore", detached: true });
24056
+ const child = spawn12("ollama", ["serve"], { stdio: "ignore", detached: true });
23787
24057
  child.unref();
23788
24058
  await new Promise((resolve28) => setTimeout(resolve28, 3e3));
23789
24059
  try {
@@ -24897,6 +25167,71 @@ async function handleSlashCommand(input, ctx) {
24897
25167
  }
24898
25168
  return "handled";
24899
25169
  }
25170
+ case "expose": {
25171
+ if (!ctx.exposeStart) {
25172
+ renderWarning("Expose gateway not available in this context.");
25173
+ return "handled";
25174
+ }
25175
+ if (arg === "stop" || arg === "off") {
25176
+ if (ctx.isExposeActive?.()) {
25177
+ await ctx.exposeStop?.();
25178
+ renderInfo("Expose gateway stopped.");
25179
+ } else {
25180
+ renderWarning("No active expose gateway.");
25181
+ }
25182
+ return "handled";
25183
+ }
25184
+ if (arg === "status") {
25185
+ const info = ctx.getExposeInfo?.();
25186
+ if (info) {
25187
+ process.stdout.write("\n" + info.stats + "\n\n");
25188
+ } else {
25189
+ renderWarning("No active expose gateway.");
25190
+ }
25191
+ return "handled";
25192
+ }
25193
+ if (!arg) {
25194
+ const info = ctx.getExposeInfo?.();
25195
+ if (info && ctx.isExposeActive?.()) {
25196
+ process.stdout.write("\n" + info.stats + "\n\n");
25197
+ } else {
25198
+ renderInfo("Usage: /expose <backend> [--key <auth-key>]");
25199
+ renderInfo(" /expose ollama Expose local Ollama (11434)");
25200
+ renderInfo(" /expose vllm Expose local vLLM (8000)");
25201
+ renderInfo(" /expose llvm Expose local LLVM (8080)");
25202
+ renderInfo(" /expose host:port Expose custom address");
25203
+ renderInfo(" /expose stop Stop the gateway");
25204
+ renderInfo(" /expose status Show usage stats");
25205
+ renderInfo(" --key <key> Use specific auth key");
25206
+ renderInfo(" --key Auto-generate auth key");
25207
+ }
25208
+ return "handled";
25209
+ }
25210
+ if (ctx.isExposeActive?.()) {
25211
+ await ctx.exposeStop?.();
25212
+ }
25213
+ const exposeParts = arg.split(/\s+/);
25214
+ const kindOrUrl = exposeParts[0];
25215
+ let exposeAuthKey;
25216
+ const keyIdx = exposeParts.indexOf("--key");
25217
+ if (keyIdx !== -1) {
25218
+ exposeAuthKey = exposeParts[keyIdx + 1] ?? "";
25219
+ }
25220
+ try {
25221
+ const tunnelUrl = await ctx.exposeStart(kindOrUrl, exposeAuthKey);
25222
+ if (tunnelUrl) {
25223
+ const info = ctx.getExposeInfo?.();
25224
+ if (info) {
25225
+ process.stdout.write("\n" + info.stats + "\n\n");
25226
+ }
25227
+ } else {
25228
+ renderError("Failed to start expose gateway.");
25229
+ }
25230
+ } catch (err) {
25231
+ renderError(`Expose error: ${err instanceof Error ? err.message : String(err)}`);
25232
+ }
25233
+ return "handled";
25234
+ }
24900
25235
  case "bruteforce":
24901
25236
  case "brute": {
24902
25237
  const isOn = ctx.bruteForceToggle();
@@ -33578,7 +33913,7 @@ var init_status_bar = __esm({
33578
33913
  return this.toolCalls > 0;
33579
33914
  }
33580
33915
  };
33581
- StatusBar = class {
33916
+ StatusBar = class _StatusBar {
33582
33917
  metrics = {
33583
33918
  promptTokens: 0,
33584
33919
  completionTokens: 0,
@@ -33794,6 +34129,20 @@ var init_status_bar = __esm({
33794
34129
  if (this.active)
33795
34130
  this.renderFooterPreserveCursor();
33796
34131
  }
34132
+ /** Expose gateway status — shown after capability emojis */
34133
+ _expose = null;
34134
+ /** Update expose gateway status */
34135
+ setExposeStatus(status) {
34136
+ this._expose = status;
34137
+ if (this.active)
34138
+ this.renderFooterPreserveCursor();
34139
+ }
34140
+ /** Clear expose gateway status */
34141
+ clearExposeStatus() {
34142
+ this._expose = null;
34143
+ if (this.active)
34144
+ this.renderFooterPreserveCursor();
34145
+ }
33797
34146
  /** Update token metrics from a token_usage event */
33798
34147
  updateMetrics(update) {
33799
34148
  if (update.promptTokens !== void 0)
@@ -33963,68 +34312,239 @@ var init_status_bar = __esm({
33963
34312
  const pos = this.rowPositions(rows);
33964
34313
  process.stdout.write(`\x1B[${pos.inputStartRow};1H\x1B[2K`);
33965
34314
  }
33966
- /** Build the metrics line string */
34315
+ /** Strip ANSI escape codes to measure visible character width */
34316
+ static visWidth(s) {
34317
+ const stripped = s.replace(/\x1B\[[0-9;]*m/g, "");
34318
+ let w = 0;
34319
+ for (const ch of stripped) {
34320
+ const cp = ch.codePointAt(0) ?? 0;
34321
+ if (cp >= 127744 && cp <= 129784)
34322
+ w += 2;
34323
+ else
34324
+ w += 1;
34325
+ }
34326
+ return w;
34327
+ }
34328
+ /** Format number compactly: 1234 → "1.2K", 567 → "567" */
34329
+ static compactNum(n) {
34330
+ if (n < 0)
34331
+ return "0";
34332
+ if (n < 1e3)
34333
+ return String(n);
34334
+ if (n < 1e4)
34335
+ return `${(n / 1e3).toFixed(1)}K`;
34336
+ if (n < 1e6)
34337
+ return `${Math.round(n / 1e3)}K`;
34338
+ return `${(n / 1e6).toFixed(1)}M`;
34339
+ }
34340
+ /** Build the metrics line string with adaptive compaction */
33967
34341
  buildMetricsLine() {
33968
34342
  const m = this.metrics;
34343
+ const termWidth = getTermWidth();
33969
34344
  const pastel2 = (code, s) => `\x1B[38;5;${code}m${s}\x1B[0m`;
33970
34345
  const pipe = pastel2(60, " \u2502 ");
33971
- const tokIn = m.promptTokens > 0 ? m.promptTokens.toLocaleString() : `~${Math.max(m.estimatedContextTokens, 0).toLocaleString()}`;
33972
- const tokInLabel = pastel2(117, "In: ") + c2.bold(tokIn);
34346
+ const pipeW = 3;
34347
+ const sections = [];
34348
+ const tokInRaw = m.promptTokens > 0 ? m.promptTokens : Math.max(m.estimatedContextTokens, 0);
34349
+ const tokInPrefix = m.promptTokens > 0 ? "" : "~";
34350
+ const tokInExpanded = pastel2(117, "In: ") + c2.bold(tokInPrefix + tokInRaw.toLocaleString());
34351
+ const tokInCompact = pastel2(117, "I:") + c2.bold(tokInPrefix + _StatusBar.compactNum(tokInRaw));
34352
+ sections.push({
34353
+ expanded: tokInExpanded,
34354
+ compact: tokInCompact,
34355
+ expandedW: 4 + (tokInPrefix + tokInRaw.toLocaleString()).length,
34356
+ compactW: 2 + (tokInPrefix + _StatusBar.compactNum(tokInRaw)).length,
34357
+ empty: false
34358
+ });
33973
34359
  const effectiveOut = this.effectiveCompletionTokens;
33974
- const tokOut = effectiveOut > 0 ? effectiveOut.toLocaleString() : `~${Math.ceil(m.totalTokens > 0 ? m.totalTokens - m.promptTokens : m.estimatedContextTokens * 0.3).toLocaleString()}`;
33975
- const tokOutLabel = pastel2(151, "Out: ") + c2.bold(tokOut);
34360
+ const tokOutRaw = effectiveOut > 0 ? effectiveOut : Math.ceil(m.totalTokens > 0 ? m.totalTokens - m.promptTokens : m.estimatedContextTokens * 0.3);
34361
+ const tokOutPrefix = effectiveOut > 0 ? "" : "~";
34362
+ const tokOutExpanded = pastel2(151, "Out: ") + c2.bold(tokOutPrefix + Math.max(0, tokOutRaw).toLocaleString());
34363
+ const tokOutCompact = pastel2(151, "O:") + c2.bold(tokOutPrefix + _StatusBar.compactNum(Math.max(0, tokOutRaw)));
34364
+ sections.push({
34365
+ expanded: tokOutExpanded,
34366
+ compact: tokOutCompact,
34367
+ expandedW: 5 + (tokOutPrefix + Math.max(0, tokOutRaw).toLocaleString()).length,
34368
+ compactW: 2 + (tokOutPrefix + _StatusBar.compactNum(Math.max(0, tokOutRaw))).length,
34369
+ empty: false
34370
+ });
33976
34371
  const ctxUsed = m.estimatedContextTokens;
33977
34372
  const ctxTotal = m.contextWindowSize;
33978
- let ctxLabel;
34373
+ let ctxExpanded;
34374
+ let ctxCompact;
34375
+ let ctxExpandedW;
34376
+ let ctxCompactW;
33979
34377
  if (ctxTotal <= 0) {
33980
- ctxLabel = pastel2(153, "Ctx: ") + c2.dim("--");
34378
+ ctxExpanded = pastel2(153, "Ctx: ") + c2.dim("--");
34379
+ ctxCompact = pastel2(153, "Ctx:") + c2.dim("--");
34380
+ ctxExpandedW = 7;
34381
+ ctxCompactW = 6;
33981
34382
  } else {
33982
34383
  const ctxPct = Math.max(0, Math.min(100, Math.round((1 - ctxUsed / ctxTotal) * 100)));
33983
34384
  const ctxColor = ctxPct > 50 ? c2.green : ctxPct > 20 ? c2.yellow : c2.red;
33984
- ctxLabel = pastel2(153, "Ctx: ") + c2.bold(`${ctxUsed.toLocaleString()}/${ctxTotal.toLocaleString()}`) + ` ${ctxColor(`${ctxPct}%`)}`;
33985
- }
33986
- let costLabel = "";
33987
- if (m.hasPricing && m.estimatedCost !== void 0) {
33988
- const costStr = m.estimatedCost < 0.01 ? `$${m.estimatedCost.toFixed(4)}` : m.estimatedCost < 1 ? `$${m.estimatedCost.toFixed(3)}` : `$${m.estimatedCost.toFixed(2)}`;
33989
- costLabel = pipe + pastel2(222, "Cost: ") + c2.bold(costStr);
34385
+ const ctxFullStr = `${ctxUsed.toLocaleString()}/${ctxTotal.toLocaleString()}`;
34386
+ ctxExpanded = pastel2(153, "Ctx: ") + c2.bold(ctxFullStr) + ` ${ctxColor(`${ctxPct}%`)}`;
34387
+ ctxCompact = pastel2(153, "Ctx:") + ctxColor(`${ctxPct}%`);
34388
+ ctxExpandedW = 5 + ctxFullStr.length + 1 + `${ctxPct}%`.length;
34389
+ ctxCompactW = 4 + `${ctxPct}%`.length;
33990
34390
  }
33991
- let snrLabel = "";
34391
+ sections.push({ expanded: ctxExpanded, compact: ctxCompact, expandedW: ctxExpandedW, compactW: ctxCompactW, empty: false });
33992
34392
  if (this._snr) {
33993
34393
  const snrPct = Math.round(this._snr.ratio * 100);
33994
34394
  const snrColor = snrPct >= 70 ? c2.green : snrPct >= 40 ? c2.yellow : c2.red;
33995
34395
  const dPrimeStr = this._snr.dPrime.toFixed(1);
33996
34396
  const capStr = this._snr.capacityWarning ? c2.red(" !CAP") : "";
33997
- snrLabel = pipe + pastel2(183, "SNR: ") + snrColor(c2.bold(`${snrPct}%`)) + c2.dim(` d'${dPrimeStr}`) + capStr;
34397
+ const snrExpanded = pastel2(183, "SNR: ") + snrColor(c2.bold(`${snrPct}%`)) + c2.dim(` d'${dPrimeStr}`) + capStr;
34398
+ const snrCompact = snrColor(`${snrPct}%`);
34399
+ const expW = 5 + `${snrPct}%`.length + 3 + dPrimeStr.length + (this._snr.capacityWarning ? 5 : 0);
34400
+ sections.push({ expanded: snrExpanded, compact: snrCompact, expandedW: expW, compactW: `${snrPct}%`.length, empty: false });
33998
34401
  }
33999
- let emotionLabel = "";
34000
34402
  if (this._emotion) {
34001
- emotionLabel = pipe + this._emotion.emoji + " " + pastel2(183, this._emotion.label);
34403
+ const emotionExpanded = this._emotion.emoji + " " + pastel2(183, this._emotion.label);
34404
+ const emotionCompact = this._emotion.emoji;
34405
+ sections.push({
34406
+ expanded: emotionExpanded,
34407
+ compact: emotionCompact,
34408
+ expandedW: 2 + 1 + this._emotion.label.length,
34409
+ // emoji(2) + space + label
34410
+ compactW: 2,
34411
+ // just emoji
34412
+ empty: false
34413
+ });
34002
34414
  }
34003
- let speedLabel = "";
34004
34415
  if (this._speedTracker.hasData) {
34005
34416
  const ratio = this._speedTracker.getSpeedRatio();
34006
34417
  if (ratio > 0) {
34007
34418
  const ratioStr = ratio >= 10 ? `${Math.round(ratio)}x` : `${ratio.toFixed(1)}x`;
34008
34419
  const ratioColor = ratio >= 2 ? c2.green : ratio >= 1 ? c2.yellow : c2.red;
34009
- speedLabel = pipe + pastel2(218, "Exp: ") + ratioColor(c2.bold(ratioStr));
34420
+ const speedExpanded = pastel2(218, "Exp: ") + ratioColor(c2.bold(ratioStr));
34421
+ sections.push({
34422
+ expanded: speedExpanded,
34423
+ compact: speedExpanded,
34424
+ expandedW: 5 + ratioStr.length,
34425
+ compactW: 5 + ratioStr.length,
34426
+ empty: false
34427
+ });
34010
34428
  }
34011
34429
  }
34012
- let recordingLabel = "";
34430
+ if (m.hasPricing && m.estimatedCost !== void 0) {
34431
+ const costStr = m.estimatedCost < 0.01 ? `$${m.estimatedCost.toFixed(4)}` : m.estimatedCost < 1 ? `$${m.estimatedCost.toFixed(3)}` : `$${m.estimatedCost.toFixed(2)}`;
34432
+ const costExpanded = pastel2(222, "Cost: ") + c2.bold(costStr);
34433
+ const costCompact = pastel2(222, "") + c2.bold(costStr);
34434
+ sections.push({
34435
+ expanded: costExpanded,
34436
+ compact: costCompact,
34437
+ expandedW: 6 + costStr.length,
34438
+ compactW: costStr.length,
34439
+ empty: false
34440
+ });
34441
+ }
34442
+ if (this._modelName) {
34443
+ const capParts = [];
34444
+ if (this._caps.vision)
34445
+ capParts.push("\u{1F441}");
34446
+ if (this._caps.toolUse)
34447
+ capParts.push("\u{1F527}");
34448
+ if (this._caps.thinking)
34449
+ capParts.push("\u{1F9E0}");
34450
+ const capsStr = capParts.length > 0 ? " " + capParts.join(" ") : "";
34451
+ const capsVisW = capParts.length > 0 ? 1 + capParts.length * 2 + (capParts.length - 1) : 0;
34452
+ const paramMatch = this._modelName.match(/(\d+\.?\d*[bBmM])/);
34453
+ const paramStr = paramMatch ? paramMatch[1] : this._modelName.split(":")[0]?.split("/").pop() ?? this._modelName;
34454
+ const modelExpanded = pastel2(146, this._modelName) + (capsStr ? pastel2(60, " ") + pastel2(183, capParts.join(" ")) : "");
34455
+ const modelCompact = pastel2(146, paramStr) + (capsStr ? pastel2(60, " ") + pastel2(183, capParts.join(" ")) : "");
34456
+ sections.push({
34457
+ expanded: modelExpanded,
34458
+ compact: modelCompact,
34459
+ expandedW: this._modelName.length + capsVisW,
34460
+ compactW: paramStr.length + capsVisW,
34461
+ empty: false
34462
+ });
34463
+ } else {
34464
+ const capParts = [];
34465
+ if (this._caps.vision)
34466
+ capParts.push("\u{1F441}");
34467
+ if (this._caps.toolUse)
34468
+ capParts.push("\u{1F527}");
34469
+ if (this._caps.thinking)
34470
+ capParts.push("\u{1F9E0}");
34471
+ if (capParts.length > 0) {
34472
+ const capsStr = pastel2(183, capParts.join(" "));
34473
+ const capsVisW = capParts.length * 2 + (capParts.length - 1);
34474
+ sections.push({ expanded: capsStr, compact: capsStr, expandedW: capsVisW, compactW: capsVisW, empty: false });
34475
+ }
34476
+ }
34477
+ if (this._expose) {
34478
+ const statusEmoji = this._expose.status === "active" ? "\u{1F7E2}" : this._expose.status === "error" ? "\u{1F534}" : "\u{1F7E0}";
34479
+ const models = Array.from(this._expose.modelUsage.keys());
34480
+ const modelParams = models.map((mdl) => {
34481
+ const pm = mdl.match(/(\d+[bBmM])/);
34482
+ return pm ? pm[1] : mdl.split(":")[0]?.split("/").pop() ?? mdl;
34483
+ });
34484
+ const modelStr = modelParams.join(",");
34485
+ const reqStr = this._expose.totalRequests > 0 ? ` ${this._expose.totalRequests}req` : "";
34486
+ const connStr = this._expose.activeConnections > 0 ? ` ${this._expose.activeConnections}conn` : "";
34487
+ const exposeExpanded = statusEmoji + pastel2(183, (modelStr ? " " + modelStr : "") + reqStr + connStr);
34488
+ const exposeCompact = statusEmoji + (modelStr ? pastel2(183, " " + modelStr) : "");
34489
+ const expFullStr = (modelStr ? " " + modelStr : "") + reqStr + connStr;
34490
+ const expCompStr = modelStr ? " " + modelStr : "";
34491
+ sections.push({
34492
+ expanded: exposeExpanded,
34493
+ compact: exposeCompact,
34494
+ expandedW: 2 + expFullStr.length,
34495
+ // emoji(2) + text
34496
+ compactW: 2 + expCompStr.length,
34497
+ empty: false
34498
+ });
34499
+ }
34013
34500
  if (this._recording) {
34014
34501
  const dot = this._recBlink ? pastel2(210, "\u25CF") : " ";
34015
34502
  const countdown = this._countdown > 0 ? c2.dim(` ${this._countdown}s`) : "";
34016
- recordingLabel = pipe + dot + pastel2(210, " REC") + countdown;
34017
- }
34018
- const modelLabel = this._modelName ? pipe + pastel2(146, this._modelName) : "";
34019
- const capParts = [];
34020
- if (this._caps.vision)
34021
- capParts.push("\u{1F441}");
34022
- if (this._caps.toolUse)
34023
- capParts.push("\u{1F527}");
34024
- if (this._caps.thinking)
34025
- capParts.push("\u{1F9E0}");
34026
- const capsLabel = capParts.length > 0 ? pipe + pastel2(183, capParts.join(" ")) : "";
34027
- return ` ${tokInLabel}${pipe}${tokOutLabel}${pipe}${ctxLabel}${snrLabel}${emotionLabel}${speedLabel}${costLabel}${modelLabel}${capsLabel}${recordingLabel}`;
34503
+ const recStr = dot + pastel2(210, " REC") + countdown;
34504
+ const recW = 1 + 4 + (this._countdown > 0 ? 1 + `${this._countdown}s`.length : 0);
34505
+ sections.push({ expanded: recStr, compact: recStr, expandedW: recW, compactW: recW, empty: false });
34506
+ }
34507
+ const activeIndices = sections.map((s, i) => !s.empty ? i : -1).filter((i) => i >= 0);
34508
+ const isCompact = new Array(sections.length).fill(false);
34509
+ const calcTotalWidth = () => {
34510
+ let w = 1;
34511
+ for (let j = 0; j < activeIndices.length; j++) {
34512
+ const idx = activeIndices[j];
34513
+ const sec = sections[idx];
34514
+ if (j > 0)
34515
+ w += pipeW;
34516
+ w += isCompact[idx] ? sec.compactW : sec.expandedW;
34517
+ }
34518
+ return w;
34519
+ };
34520
+ let totalW = calcTotalWidth();
34521
+ for (let j = activeIndices.length - 1; j >= 0 && totalW > termWidth; j--) {
34522
+ const idx = activeIndices[j];
34523
+ if (!isCompact[idx] && sections[idx].compactW < sections[idx].expandedW) {
34524
+ isCompact[idx] = true;
34525
+ totalW = calcTotalWidth();
34526
+ }
34527
+ }
34528
+ for (let j = 0; j < activeIndices.length; j++) {
34529
+ const idx = activeIndices[j];
34530
+ if (isCompact[idx]) {
34531
+ const savings = sections[idx].expandedW - sections[idx].compactW;
34532
+ if (totalW + savings <= termWidth) {
34533
+ isCompact[idx] = false;
34534
+ totalW += savings;
34535
+ } else {
34536
+ break;
34537
+ }
34538
+ }
34539
+ }
34540
+ let result = " ";
34541
+ for (let j = 0; j < activeIndices.length; j++) {
34542
+ const idx = activeIndices[j];
34543
+ if (j > 0)
34544
+ result += pipe;
34545
+ result += isCompact[idx] ? sections[idx].compact : sections[idx].expanded;
34546
+ }
34547
+ return result;
34028
34548
  }
34029
34549
  // -------------------------------------------------------------------------
34030
34550
  // Private
@@ -35145,6 +35665,7 @@ async function startInteractive(config, repoPath) {
35145
35665
  });
35146
35666
  const voiceEngine = new VoiceEngine();
35147
35667
  let voiceSession = null;
35668
+ let exposeGateway = null;
35148
35669
  let adminSessionKey = null;
35149
35670
  const callSubAgents = /* @__PURE__ */ new Map();
35150
35671
  const streamRenderer = new StreamRenderer();
@@ -35305,6 +35826,7 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
35305
35826
  "/pause",
35306
35827
  "/stop",
35307
35828
  "/resume",
35829
+ "/expose",
35308
35830
  "/full-send-bless",
35309
35831
  "/bless",
35310
35832
  "/telegram",
@@ -35991,6 +36513,55 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
35991
36513
  getCallUrl() {
35992
36514
  return voiceSession?.tunnelUrl ?? null;
35993
36515
  },
36516
+ async exposeStart(kindOrUrl, authKey) {
36517
+ const knownKinds = ["ollama", "vllm", "llvm"];
36518
+ let kind;
36519
+ let targetUrl;
36520
+ if (knownKinds.includes(kindOrUrl)) {
36521
+ kind = kindOrUrl;
36522
+ } else {
36523
+ kind = "custom";
36524
+ targetUrl = kindOrUrl.includes("://") ? kindOrUrl : `http://${kindOrUrl}`;
36525
+ }
36526
+ exposeGateway = new ExposeGateway({ kind, targetUrl, authKey });
36527
+ exposeGateway.on("stats", (stats) => {
36528
+ statusBar.setExposeStatus({
36529
+ status: stats.status,
36530
+ totalRequests: stats.totalRequests,
36531
+ activeConnections: stats.activeConnections,
36532
+ modelUsage: stats.modelUsage
36533
+ });
36534
+ });
36535
+ try {
36536
+ const url = await exposeGateway.start();
36537
+ writeContent(() => {
36538
+ process.stdout.write("\n" + exposeGateway.formatConnectionInfo() + "\n\n");
36539
+ });
36540
+ return url;
36541
+ } catch (err) {
36542
+ exposeGateway = null;
36543
+ throw err;
36544
+ }
36545
+ },
36546
+ async exposeStop() {
36547
+ if (exposeGateway) {
36548
+ await exposeGateway.stop();
36549
+ statusBar.clearExposeStatus();
36550
+ exposeGateway = null;
36551
+ }
36552
+ },
36553
+ isExposeActive() {
36554
+ return exposeGateway?.isActive ?? false;
36555
+ },
36556
+ getExposeInfo() {
36557
+ if (!exposeGateway)
36558
+ return null;
36559
+ return {
36560
+ tunnelUrl: exposeGateway.tunnelUrl,
36561
+ authKey: exposeGateway.authKey,
36562
+ stats: exposeGateway.formatStats()
36563
+ };
36564
+ },
35994
36565
  async sendCallButton(chatId, url) {
35995
36566
  if (telegramBridge?.isActive) {
35996
36567
  await telegramBridge.sendCallButton(chatId, url);
@@ -36657,6 +37228,10 @@ Rationale: ${proposal.rationale}${provenanceNote}`;
36657
37228
  showPrompt();
36658
37229
  }
36659
37230
  rl.on("close", () => {
37231
+ if (exposeGateway?.isActive) {
37232
+ exposeGateway.stop().catch(() => {
37233
+ });
37234
+ }
36660
37235
  statusBar.deactivate();
36661
37236
  const closeRows = process.stdout.rows ?? 24;
36662
37237
  process.stdout.write(`\x1B[${closeRows - 4};1H\x1B[J
@@ -36745,6 +37320,7 @@ var init_interactive = __esm({
36745
37320
  init_dist();
36746
37321
  init_listen();
36747
37322
  init_voice_session();
37323
+ init_expose();
36748
37324
  init_call_agent();
36749
37325
  init_config();
36750
37326
  init_updater();
@@ -37408,7 +37984,7 @@ var serve_exports = {};
37408
37984
  __export(serve_exports, {
37409
37985
  serveCommand: () => serveCommand
37410
37986
  });
37411
- import { spawn as spawn12 } from "node:child_process";
37987
+ import { spawn as spawn13 } from "node:child_process";
37412
37988
  async function serveCommand(opts, config) {
37413
37989
  const backendType = config.backendType;
37414
37990
  if (backendType === "ollama") {
@@ -37501,7 +38077,7 @@ async function serveVllm(opts, config) {
37501
38077
  }
37502
38078
  async function runVllmServer(args, verbose) {
37503
38079
  return new Promise((resolve28, reject) => {
37504
- const child = spawn12("python", args, {
38080
+ const child = spawn13("python", args, {
37505
38081
  stdio: verbose ? "inherit" : ["ignore", "pipe", "pipe"],
37506
38082
  env: { ...process.env }
37507
38083
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.66.0",
3
+ "version": "0.67.0",
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",