@acnlabs/acn-cli 1.0.1 → 1.0.3

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 +195 -65
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "@acnlabs/acn-cli",
34
- version: "1.0.1",
34
+ version: "1.0.3",
35
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -429,27 +429,129 @@ function joinCommand() {
429
429
 
430
430
  // src/commands/heartbeat.ts
431
431
  var import_commander3 = require("commander");
432
+
433
+ // src/commands/model-heartbeat.ts
434
+ function resolvePreferredModel(opts) {
435
+ const env = opts?.env ?? process.env;
436
+ const fromFlag = opts?.model?.trim();
437
+ if (fromFlag) return fromFlag.slice(0, 200);
438
+ const fromEnv = env.ACN_PREFERRED_MODEL?.trim();
439
+ if (fromEnv) return fromEnv.slice(0, 200);
440
+ return void 0;
441
+ }
442
+ function resolveSupportedModels(opts) {
443
+ if (opts?.clear) return [];
444
+ const env = opts?.env ?? process.env;
445
+ const raw = (opts?.models?.trim() || env.ACN_SUPPORTED_MODELS?.trim() || "").trim();
446
+ if (!raw) return void 0;
447
+ const out = [];
448
+ const seen = /* @__PURE__ */ new Set();
449
+ for (const part of raw.split(/[,;\s]+/)) {
450
+ const mid = part.trim().slice(0, 200);
451
+ if (!mid) continue;
452
+ const key = mid.toLowerCase();
453
+ if (seen.has(key)) continue;
454
+ seen.add(key);
455
+ out.push(mid);
456
+ if (out.length >= 50) break;
457
+ }
458
+ return out.length ? out : void 0;
459
+ }
460
+ async function postAgentHeartbeat(opts) {
461
+ const fetchFn = opts.fetchFn ?? fetch;
462
+ const origin = opts.baseUrl.replace(/\/+$/, "");
463
+ const url = `${origin}/api/v1/agents/${opts.agentId}/heartbeat`;
464
+ const headers = {
465
+ Authorization: `Bearer ${opts.apiKey}`,
466
+ "Content-Type": "application/json"
467
+ };
468
+ const payload = {};
469
+ if (opts.preferredModel && opts.preferredModel.trim()) {
470
+ payload.preferred_model = opts.preferredModel.trim().slice(0, 200);
471
+ }
472
+ if (opts.supportedModels != null) {
473
+ payload.supported_models = opts.supportedModels;
474
+ }
475
+ const body = Object.keys(payload).length ? JSON.stringify(payload) : void 0;
476
+ try {
477
+ const res = await fetchFn(url, { method: "POST", headers, body });
478
+ if (!res.ok) {
479
+ return { ok: false, reason: `http_${res.status}` };
480
+ }
481
+ let preferred;
482
+ let supported;
483
+ try {
484
+ const json = await res.json();
485
+ preferred = json.preferred_model;
486
+ supported = json.supported_models;
487
+ } catch {
488
+ preferred = opts.preferredModel ?? null;
489
+ supported = opts.supportedModels ?? null;
490
+ }
491
+ return { ok: true, preferred_model: preferred, supported_models: supported };
492
+ } catch (err) {
493
+ return {
494
+ ok: false,
495
+ reason: err instanceof Error ? err.message : String(err)
496
+ };
497
+ }
498
+ }
499
+ function formatModelHeartbeatLog(opts) {
500
+ const preferred = (opts.preferred ?? "").trim();
501
+ const supported = opts.supported ?? [];
502
+ const bits = [
503
+ preferred ? `preferred_model=${preferred}` : "",
504
+ supported.length ? `supported_models=${supported.join(",")}` : ""
505
+ ].filter(Boolean);
506
+ return bits.length ? ` ${bits.join(" ")}` : "";
507
+ }
508
+
509
+ // src/commands/heartbeat.ts
432
510
  function heartbeatCommand() {
433
511
  return new import_commander3.Command("heartbeat").description("Send a heartbeat to keep this agent online").option("-i, --agent-id <id>", "Agent ID (defaults to value in ~/.acn/config.json)").option(
434
512
  "-m, --model <modelId>",
435
513
  "Declare runtime model (Host Catalog id) for Host Pricing prefill \u2014 self-reported (env: ACN_PREFERRED_MODEL)"
436
- ).action(async (opts) => {
437
- const config = loadConfig();
438
- const agentId = opts.agentId ?? config.agent_id;
439
- if (!agentId) {
440
- console.error("No agent ID found. Run `acn join` first or pass --agent-id.");
441
- process.exit(1);
442
- }
443
- try {
444
- const model = opts.model && opts.model.trim() || process.env.ACN_PREFERRED_MODEL?.trim() || "";
445
- const body = model ? { preferred_model: model.slice(0, 200) } : void 0;
446
- const res = await acnPost(`/agents/${agentId}/heartbeat`, body);
447
- const modelNote = res.preferred_model ? ` (preferred_model=${res.preferred_model})` : "";
448
- output(res, `Heartbeat sent for agent ${agentId}${modelNote}`);
449
- } catch (err) {
450
- handleError(err);
514
+ ).option(
515
+ "--supported-models <ids>",
516
+ "Comma-separated models this runtime can run (Interfaze composer; env: ACN_SUPPORTED_MODELS)"
517
+ ).option(
518
+ "--clear-supported-models",
519
+ "Clear metadata.supported_models on the server (sends empty list)"
520
+ ).action(
521
+ async (opts) => {
522
+ const config = loadConfig();
523
+ const agentId = opts.agentId ?? config.agent_id;
524
+ if (!agentId) {
525
+ console.error("No agent ID found. Run `acn join` first or pass --agent-id.");
526
+ process.exit(1);
527
+ }
528
+ try {
529
+ if (opts.clearSupportedModels && opts.supportedModels?.trim()) {
530
+ console.error(
531
+ "Use either --supported-models or --clear-supported-models, not both."
532
+ );
533
+ process.exit(1);
534
+ }
535
+ const preferred = resolvePreferredModel({ model: opts.model });
536
+ const supported = resolveSupportedModels({
537
+ models: opts.supportedModels,
538
+ clear: !!opts.clearSupportedModels
539
+ });
540
+ const body = {};
541
+ if (preferred) body.preferred_model = preferred;
542
+ if (supported !== void 0) body.supported_models = supported;
543
+ const res = await acnPost(`/agents/${agentId}/heartbeat`, Object.keys(body).length ? body : void 0);
544
+ const notes = [
545
+ res.preferred_model ? `preferred_model=${res.preferred_model}` : "",
546
+ res.supported_models != null ? `supported_models=${res.supported_models.join(",") || "(cleared)"}` : ""
547
+ ].filter(Boolean);
548
+ const modelNote = notes.length ? ` (${notes.join(" ")})` : "";
549
+ output(res, `Heartbeat sent for agent ${agentId}${modelNote}`);
550
+ } catch (err) {
551
+ handleError(err);
552
+ }
451
553
  }
452
- });
554
+ );
453
555
  }
454
556
 
455
557
  // src/commands/rotate-key.ts
@@ -1480,12 +1582,23 @@ function extractChatEnvelope(message) {
1480
1582
  if (!chatId || !replyPath) return null;
1481
1583
  if (replyChannel !== CHAT_REPLY_CHANNEL) return null;
1482
1584
  if (!isAllowedChatReplyPath(chatId, replyPath)) return null;
1585
+ const requested = asNonEmptyString(ap.requested_model);
1586
+ let maxOut = null;
1587
+ const rawMax = ap.max_output_tokens;
1588
+ if (typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax > 0) {
1589
+ maxOut = Math.floor(rawMax);
1590
+ } else if (typeof rawMax === "string" && rawMax.trim()) {
1591
+ const n = Number.parseInt(rawMax.trim(), 10);
1592
+ if (Number.isFinite(n) && n > 0) maxOut = n;
1593
+ }
1483
1594
  return {
1484
1595
  chat_id: chatId,
1485
1596
  reply_path: replyPath,
1486
1597
  reply_channel: CHAT_REPLY_CHANNEL,
1487
1598
  gateway_message_id: asNonEmptyString(ap.message_id) ?? asNonEmptyString(ap.messageId),
1488
- user_text: extractUserText(message)
1599
+ user_text: extractUserText(message),
1600
+ requested_model: requested ? requested.slice(0, 200) : null,
1601
+ max_output_tokens: maxOut
1489
1602
  };
1490
1603
  }
1491
1604
  function normalizeEvent(body, opts = {}) {
@@ -1628,8 +1741,8 @@ function extractUsage(payload) {
1628
1741
  const rec = asRecord2(payload);
1629
1742
  if (!rec) return void 0;
1630
1743
  const usageRec = asRecord2(rec.usage) ?? rec;
1631
- const input = asNonNegInt(usageRec.input_tokens) ?? asNonNegInt(usageRec.prompt_tokens);
1632
- const output2 = asNonNegInt(usageRec.output_tokens) ?? asNonNegInt(usageRec.completion_tokens);
1744
+ const input = asNonNegInt(usageRec.input_tokens) ?? asNonNegInt(usageRec.prompt_tokens) ?? asNonNegInt(usageRec.input);
1745
+ const output2 = asNonNegInt(usageRec.output_tokens) ?? asNonNegInt(usageRec.completion_tokens) ?? asNonNegInt(usageRec.output);
1633
1746
  if (input === null && output2 === null) return void 0;
1634
1747
  const out = {
1635
1748
  input_tokens: input ?? 0,
@@ -1641,6 +1754,18 @@ function extractUsage(payload) {
1641
1754
  }
1642
1755
  const modelId = extractModelId(payload);
1643
1756
  if (modelId) out.model_id = modelId;
1757
+ const reasoning = asNonNegInt(usageRec.reasoning_tokens) ?? asNonNegInt(usageRec.reasoningTokens);
1758
+ const cacheRead = asNonNegInt(usageRec.cache_read_tokens) ?? asNonNegInt(usageRec.cacheRead);
1759
+ const cacheWrite = asNonNegInt(usageRec.cache_write_tokens) ?? asNonNegInt(usageRec.cacheWrite);
1760
+ const total = asNonNegInt(usageRec.total_tokens) ?? asNonNegInt(usageRec.total);
1761
+ const duration = asNonNegInt(usageRec.duration_ms) ?? asNonNegInt(usageRec.durationMs) ?? asNonNegInt(rec.duration_ms) ?? asNonNegInt(rec.durationMs);
1762
+ const provider = typeof usageRec.provider === "string" && usageRec.provider.trim() || typeof rec.provider === "string" && rec.provider.trim() || "";
1763
+ if (reasoning !== null) out.reasoning_tokens = reasoning;
1764
+ if (cacheRead !== null) out.cache_read_tokens = cacheRead;
1765
+ if (cacheWrite !== null) out.cache_write_tokens = cacheWrite;
1766
+ if (total !== null) out.total_tokens = total;
1767
+ if (duration !== null) out.duration_ms = duration;
1768
+ if (provider) out.provider = provider.slice(0, 80);
1644
1769
  return out;
1645
1770
  }
1646
1771
  function parseCompletePayload(payload) {
@@ -1813,6 +1938,24 @@ async function postWriteback(event, complete, opts, deps) {
1813
1938
  if (complete.usage.model_id) {
1814
1939
  usageBody.model_id = complete.usage.model_id;
1815
1940
  }
1941
+ if (complete.usage.reasoning_tokens != null) {
1942
+ usageBody.reasoning_tokens = complete.usage.reasoning_tokens;
1943
+ }
1944
+ if (complete.usage.cache_read_tokens != null) {
1945
+ usageBody.cache_read_tokens = complete.usage.cache_read_tokens;
1946
+ }
1947
+ if (complete.usage.cache_write_tokens != null) {
1948
+ usageBody.cache_write_tokens = complete.usage.cache_write_tokens;
1949
+ }
1950
+ if (complete.usage.total_tokens != null) {
1951
+ usageBody.total_tokens = complete.usage.total_tokens;
1952
+ }
1953
+ if (complete.usage.duration_ms != null) {
1954
+ usageBody.duration_ms = complete.usage.duration_ms;
1955
+ }
1956
+ if (complete.usage.provider) {
1957
+ usageBody.provider = complete.usage.provider;
1958
+ }
1816
1959
  body.usage = usageBody;
1817
1960
  } else if (complete.modelId) {
1818
1961
  body.usage = { model_id: complete.modelId };
@@ -2297,43 +2440,6 @@ var KEEPALIVE_INTERVAL_MS = 3e4;
2297
2440
  var MODEL_HEARTBEAT_INTERVAL_MS = 15 * 6e4;
2298
2441
  var INITIAL_BACKOFF_MS = 1e3;
2299
2442
  var MAX_BACKOFF_MS = 3e4;
2300
- function resolvePreferredModel(opts) {
2301
- const env = opts?.env ?? process.env;
2302
- const fromFlag = opts?.model?.trim();
2303
- if (fromFlag) return fromFlag.slice(0, 200);
2304
- const fromEnv = env.ACN_PREFERRED_MODEL?.trim();
2305
- if (fromEnv) return fromEnv.slice(0, 200);
2306
- return void 0;
2307
- }
2308
- async function postAgentHeartbeat(opts) {
2309
- const fetchFn = opts.fetchFn ?? fetch;
2310
- const origin = opts.baseUrl.replace(/\/+$/, "");
2311
- const url = `${origin}/api/v1/agents/${opts.agentId}/heartbeat`;
2312
- const headers = {
2313
- Authorization: `Bearer ${opts.apiKey}`,
2314
- "Content-Type": "application/json"
2315
- };
2316
- const body = opts.preferredModel && opts.preferredModel.trim() ? JSON.stringify({ preferred_model: opts.preferredModel.trim().slice(0, 200) }) : void 0;
2317
- try {
2318
- const res = await fetchFn(url, { method: "POST", headers, body });
2319
- if (!res.ok) {
2320
- return { ok: false, reason: `http_${res.status}` };
2321
- }
2322
- let preferred;
2323
- try {
2324
- const json = await res.json();
2325
- preferred = json.preferred_model;
2326
- } catch {
2327
- preferred = opts.preferredModel ?? null;
2328
- }
2329
- return { ok: true, preferred_model: preferred };
2330
- } catch (err) {
2331
- return {
2332
- ok: false,
2333
- reason: err instanceof Error ? err.message : String(err)
2334
- };
2335
- }
2336
- }
2337
2443
  function runListener(cfg) {
2338
2444
  const wsUrl = toWebsocketUrl(cfg.baseUrl, cfg.agentId);
2339
2445
  let backoff = INITIAL_BACKOFF_MS;
@@ -2346,25 +2452,32 @@ function runListener(cfg) {
2346
2452
  let keepalive;
2347
2453
  let modelHeartbeat;
2348
2454
  const sendModelHeartbeat = () => {
2349
- if (!cfg.preferredModel || stopped) return;
2455
+ if (stopped) return;
2456
+ if (!cfg.preferredModel && cfg.supportedModels === void 0) return;
2350
2457
  void postAgentHeartbeat({
2351
2458
  baseUrl: cfg.baseUrl,
2352
2459
  agentId: cfg.agentId,
2353
2460
  apiKey: cfg.apiKey,
2354
- preferredModel: cfg.preferredModel
2461
+ preferredModel: cfg.preferredModel,
2462
+ supportedModels: cfg.supportedModels
2355
2463
  }).then((r) => {
2356
2464
  if (!r.ok) {
2357
- console.error(`[acn listen] preferred_model heartbeat failed: ${r.reason}`);
2465
+ console.error(`[acn listen] model heartbeat failed: ${r.reason}`);
2358
2466
  return;
2359
2467
  }
2360
- console.error(
2361
- `[acn listen] preferred_model heartbeat ok model=${r.preferred_model ?? cfg.preferredModel}`
2362
- );
2468
+ const note = formatModelHeartbeatLog({
2469
+ preferred: r.preferred_model ?? cfg.preferredModel,
2470
+ supported: r.supported_models ?? cfg.supportedModels
2471
+ });
2472
+ console.error(`[acn listen] model heartbeat ok${note}`);
2363
2473
  });
2364
2474
  };
2365
2475
  ws.on("open", () => {
2366
2476
  const mode = cfg.runtime ? `runtime=${cfg.runtime.runtime}` : cfg.forward ? `forward=${cfg.forward}` : `exec`;
2367
- const modelNote = cfg.preferredModel ? ` preferred_model=${cfg.preferredModel}` : "";
2477
+ const modelNote = formatModelHeartbeatLog({
2478
+ preferred: cfg.preferredModel,
2479
+ supported: cfg.supportedModels
2480
+ });
2368
2481
  console.error(
2369
2482
  `[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl} (${mode})${modelNote}`
2370
2483
  );
@@ -2374,7 +2487,7 @@ function runListener(cfg) {
2374
2487
  ws.send(JSON.stringify({ type: "ping" }));
2375
2488
  }
2376
2489
  }, KEEPALIVE_INTERVAL_MS);
2377
- if (cfg.preferredModel) {
2490
+ if (cfg.preferredModel || cfg.supportedModels !== void 0) {
2378
2491
  sendModelHeartbeat();
2379
2492
  modelHeartbeat = setInterval(sendModelHeartbeat, MODEL_HEARTBEAT_INTERVAL_MS);
2380
2493
  }
@@ -2488,6 +2601,12 @@ function listenCommand() {
2488
2601
  ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").option(
2489
2602
  "-m, --model <modelId>",
2490
2603
  "Declare runtime model (Host Catalog id) on connect + every 15m via REST heartbeat (self-reported; env: ACN_PREFERRED_MODEL)"
2604
+ ).option(
2605
+ "--supported-models <ids>",
2606
+ "Comma-separated Host Catalog model ids this runtime can run (Interfaze composer dropdown; env: ACN_SUPPORTED_MODELS)"
2607
+ ).option(
2608
+ "--clear-supported-models",
2609
+ "Clear metadata.supported_models on the server (sends empty list)"
2491
2610
  ).action(
2492
2611
  (opts) => {
2493
2612
  const config = loadConfig();
@@ -2579,11 +2698,22 @@ function listenCommand() {
2579
2698
  audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
2580
2699
  });
2581
2700
  const preferredModel = resolvePreferredModel({ model: opts.model });
2701
+ if (opts.clearSupportedModels && opts.supportedModels?.trim()) {
2702
+ console.error(
2703
+ "Use either --supported-models or --clear-supported-models, not both."
2704
+ );
2705
+ process.exit(1);
2706
+ }
2707
+ const supportedModels = resolveSupportedModels({
2708
+ models: opts.supportedModels,
2709
+ clear: !!opts.clearSupportedModels
2710
+ });
2582
2711
  runListener({
2583
2712
  agentId,
2584
2713
  apiKey,
2585
2714
  baseUrl: config.base_url,
2586
2715
  preferredModel,
2716
+ supportedModels,
2587
2717
  forward: opts.forward,
2588
2718
  exec: opts.exec,
2589
2719
  runtime: opts.runtime ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {