@acnlabs/acn-cli 1.0.0 → 1.0.2

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 +219 -18
  2. package/package.json +2 -2
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.0",
34
+ version: "1.0.2",
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,21 +429,129 @@ function joinCommand() {
429
429
 
430
430
  // src/commands/heartbeat.ts
431
431
  var import_commander3 = require("commander");
432
- function heartbeatCommand() {
433
- 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)").action(async (opts) => {
434
- const config = loadConfig();
435
- const agentId = opts.agentId ?? config.agent_id;
436
- if (!agentId) {
437
- console.error("No agent ID found. Run `acn join` first or pass --agent-id.");
438
- process.exit(1);
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}` };
439
480
  }
481
+ let preferred;
482
+ let supported;
440
483
  try {
441
- const res = await acnPost(`/agents/${agentId}/heartbeat`);
442
- output(res, `Heartbeat sent for agent ${agentId}`);
443
- } catch (err) {
444
- handleError(err);
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;
445
490
  }
446
- });
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
510
+ function heartbeatCommand() {
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(
512
+ "-m, --model <modelId>",
513
+ "Declare runtime model (Host Catalog id) for Host Pricing prefill \u2014 self-reported (env: ACN_PREFERRED_MODEL)"
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
+ }
553
+ }
554
+ );
447
555
  }
448
556
 
449
557
  // src/commands/rotate-key.ts
@@ -1474,12 +1582,23 @@ function extractChatEnvelope(message) {
1474
1582
  if (!chatId || !replyPath) return null;
1475
1583
  if (replyChannel !== CHAT_REPLY_CHANNEL) return null;
1476
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
+ }
1477
1594
  return {
1478
1595
  chat_id: chatId,
1479
1596
  reply_path: replyPath,
1480
1597
  reply_channel: CHAT_REPLY_CHANNEL,
1481
1598
  gateway_message_id: asNonEmptyString(ap.message_id) ?? asNonEmptyString(ap.messageId),
1482
- user_text: extractUserText(message)
1599
+ user_text: extractUserText(message),
1600
+ requested_model: requested ? requested.slice(0, 200) : null,
1601
+ max_output_tokens: maxOut
1483
1602
  };
1484
1603
  }
1485
1604
  function normalizeEvent(body, opts = {}) {
@@ -1604,6 +1723,20 @@ function asNonNegInt(v) {
1604
1723
  }
1605
1724
  return null;
1606
1725
  }
1726
+ function extractModelId(payload) {
1727
+ const rec = asRecord2(payload);
1728
+ if (!rec) return void 0;
1729
+ const usageRec = asRecord2(rec.usage);
1730
+ for (const raw of [
1731
+ usageRec?.model_id,
1732
+ usageRec?.model,
1733
+ rec.model_id,
1734
+ rec.model
1735
+ ]) {
1736
+ if (typeof raw === "string" && raw.trim()) return raw.trim().slice(0, 200);
1737
+ }
1738
+ return void 0;
1739
+ }
1607
1740
  function extractUsage(payload) {
1608
1741
  const rec = asRecord2(payload);
1609
1742
  if (!rec) return void 0;
@@ -1619,13 +1752,19 @@ function extractUsage(payload) {
1619
1752
  if (ms === "peer_self" || ms === "gateway" || ms === "runtime_attested" || ms === "protocol") {
1620
1753
  out.meter_source = ms;
1621
1754
  }
1755
+ const modelId = extractModelId(payload);
1756
+ if (modelId) out.model_id = modelId;
1622
1757
  return out;
1623
1758
  }
1624
1759
  function parseCompletePayload(payload) {
1625
1760
  const content = extractContent(payload);
1626
1761
  if (!content) return { ok: false, reason: "complete_missing_content" };
1627
1762
  const usage = extractUsage(payload);
1628
- return { ok: true, result: usage ? { content, usage } : { content } };
1763
+ const modelId = extractModelId(payload);
1764
+ const result = { content };
1765
+ if (usage) result.usage = usage;
1766
+ else if (modelId) result.modelId = modelId;
1767
+ return { ok: true, result };
1629
1768
  }
1630
1769
  async function mintAgentJwt(opts, fetchFn = fetch) {
1631
1770
  const now = Math.floor(Date.now() / 1e3);
@@ -1779,11 +1918,17 @@ async function postWriteback(event, complete, opts, deps) {
1779
1918
  reply_to_id: replyToId
1780
1919
  };
1781
1920
  if (complete.usage) {
1782
- body.usage = {
1921
+ const usageBody = {
1783
1922
  input_tokens: complete.usage.input_tokens,
1784
1923
  output_tokens: complete.usage.output_tokens,
1785
1924
  meter_source: complete.usage.meter_source ?? "peer_self"
1786
1925
  };
1926
+ if (complete.usage.model_id) {
1927
+ usageBody.model_id = complete.usage.model_id;
1928
+ }
1929
+ body.usage = usageBody;
1930
+ } else if (complete.modelId) {
1931
+ body.usage = { model_id: complete.modelId };
1787
1932
  }
1788
1933
  const postOnce = async (token) => {
1789
1934
  const controller = new AbortController();
@@ -2262,6 +2407,7 @@ function rawToString(data) {
2262
2407
  return Buffer.from(data).toString("utf-8");
2263
2408
  }
2264
2409
  var KEEPALIVE_INTERVAL_MS = 3e4;
2410
+ var MODEL_HEARTBEAT_INTERVAL_MS = 15 * 6e4;
2265
2411
  var INITIAL_BACKOFF_MS = 1e3;
2266
2412
  var MAX_BACKOFF_MS = 3e4;
2267
2413
  function runListener(cfg) {
@@ -2274,15 +2420,47 @@ function runListener(cfg) {
2274
2420
  headers: { Authorization: `Bearer ${cfg.apiKey}` }
2275
2421
  });
2276
2422
  let keepalive;
2423
+ let modelHeartbeat;
2424
+ const sendModelHeartbeat = () => {
2425
+ if (stopped) return;
2426
+ if (!cfg.preferredModel && cfg.supportedModels === void 0) return;
2427
+ void postAgentHeartbeat({
2428
+ baseUrl: cfg.baseUrl,
2429
+ agentId: cfg.agentId,
2430
+ apiKey: cfg.apiKey,
2431
+ preferredModel: cfg.preferredModel,
2432
+ supportedModels: cfg.supportedModels
2433
+ }).then((r) => {
2434
+ if (!r.ok) {
2435
+ console.error(`[acn listen] model heartbeat failed: ${r.reason}`);
2436
+ return;
2437
+ }
2438
+ const note = formatModelHeartbeatLog({
2439
+ preferred: r.preferred_model ?? cfg.preferredModel,
2440
+ supported: r.supported_models ?? cfg.supportedModels
2441
+ });
2442
+ console.error(`[acn listen] model heartbeat ok${note}`);
2443
+ });
2444
+ };
2277
2445
  ws.on("open", () => {
2278
2446
  const mode = cfg.runtime ? `runtime=${cfg.runtime.runtime}` : cfg.forward ? `forward=${cfg.forward}` : `exec`;
2279
- console.error(`[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl} (${mode})`);
2447
+ const modelNote = formatModelHeartbeatLog({
2448
+ preferred: cfg.preferredModel,
2449
+ supported: cfg.supportedModels
2450
+ });
2451
+ console.error(
2452
+ `[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl} (${mode})${modelNote}`
2453
+ );
2280
2454
  backoff = INITIAL_BACKOFF_MS;
2281
2455
  keepalive = setInterval(() => {
2282
2456
  if (ws.readyState === import_ws.default.OPEN) {
2283
2457
  ws.send(JSON.stringify({ type: "ping" }));
2284
2458
  }
2285
2459
  }, KEEPALIVE_INTERVAL_MS);
2460
+ if (cfg.preferredModel || cfg.supportedModels !== void 0) {
2461
+ sendModelHeartbeat();
2462
+ modelHeartbeat = setInterval(sendModelHeartbeat, MODEL_HEARTBEAT_INTERVAL_MS);
2463
+ }
2286
2464
  });
2287
2465
  ws.on("message", (data) => {
2288
2466
  let frame;
@@ -2304,6 +2482,7 @@ function runListener(cfg) {
2304
2482
  });
2305
2483
  ws.on("close", (code, reason) => {
2306
2484
  if (keepalive) clearInterval(keepalive);
2485
+ if (modelHeartbeat) clearInterval(modelHeartbeat);
2307
2486
  if (stopped) return;
2308
2487
  if (code === 4401 || code === 4403 || code === 4429) {
2309
2488
  console.error(
@@ -2389,7 +2568,16 @@ function listenCommand() {
2389
2568
  "--chat-complete-timeout <ms>",
2390
2569
  `Host complete timeout in ms (default ${DEFAULT_COMPLETE_TIMEOUT_MS})`,
2391
2570
  String(DEFAULT_COMPLETE_TIMEOUT_MS)
2392
- ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(
2571
+ ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").option(
2572
+ "-m, --model <modelId>",
2573
+ "Declare runtime model (Host Catalog id) on connect + every 15m via REST heartbeat (self-reported; env: ACN_PREFERRED_MODEL)"
2574
+ ).option(
2575
+ "--supported-models <ids>",
2576
+ "Comma-separated Host Catalog model ids this runtime can run (Interfaze composer dropdown; env: ACN_SUPPORTED_MODELS)"
2577
+ ).option(
2578
+ "--clear-supported-models",
2579
+ "Clear metadata.supported_models on the server (sends empty list)"
2580
+ ).action(
2393
2581
  (opts) => {
2394
2582
  const config = loadConfig();
2395
2583
  const apiKey = config.api_key;
@@ -2479,10 +2667,23 @@ function listenCommand() {
2479
2667
  agentId,
2480
2668
  audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
2481
2669
  });
2670
+ const preferredModel = resolvePreferredModel({ model: opts.model });
2671
+ if (opts.clearSupportedModels && opts.supportedModels?.trim()) {
2672
+ console.error(
2673
+ "Use either --supported-models or --clear-supported-models, not both."
2674
+ );
2675
+ process.exit(1);
2676
+ }
2677
+ const supportedModels = resolveSupportedModels({
2678
+ models: opts.supportedModels,
2679
+ clear: !!opts.clearSupportedModels
2680
+ });
2482
2681
  runListener({
2483
2682
  agentId,
2484
2683
  apiKey,
2485
2684
  baseUrl: config.base_url,
2685
+ preferredModel,
2686
+ supportedModels,
2486
2687
  forward: opts.forward,
2487
2688
  exec: opts.exec,
2488
2689
  runtime: opts.runtime ? {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "1.0.0",
4
- "description": "Official CLI for ACN (Agent Collaboration Network) zero-integration agent access",
3
+ "version": "1.0.2",
4
+ "description": "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "acn": "dist/index.js"