@acnlabs/acn-cli 0.14.2 → 1.0.1

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 +160 -20
  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: "0.14.2",
34
+ version: "1.0.1",
35
35
  description: "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
36
36
  main: "dist/index.js",
37
37
  bin: {
@@ -430,7 +430,10 @@ function joinCommand() {
430
430
  // src/commands/heartbeat.ts
431
431
  var import_commander3 = require("commander");
432
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) => {
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)").option(
434
+ "-m, --model <modelId>",
435
+ "Declare runtime model (Host Catalog id) for Host Pricing prefill \u2014 self-reported (env: ACN_PREFERRED_MODEL)"
436
+ ).action(async (opts) => {
434
437
  const config = loadConfig();
435
438
  const agentId = opts.agentId ?? config.agent_id;
436
439
  if (!agentId) {
@@ -438,8 +441,11 @@ function heartbeatCommand() {
438
441
  process.exit(1);
439
442
  }
440
443
  try {
441
- const res = await acnPost(`/agents/${agentId}/heartbeat`);
442
- output(res, `Heartbeat sent for agent ${agentId}`);
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}`);
443
449
  } catch (err) {
444
450
  handleError(err);
445
451
  }
@@ -1560,7 +1566,7 @@ function validateChatWritebackOptions(opts) {
1560
1566
  const hasUrl = Boolean(opts.chatCompleteUrl?.trim());
1561
1567
  const hasExec = Boolean(opts.chatCompleteExec?.trim());
1562
1568
  if (hasUrl === hasExec) {
1563
- return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."}).';
1569
+ return '--chat-writeback requires exactly one of --chat-complete-url or --chat-complete-exec (host returns JSON {"content":"..."} and optional usage).';
1564
1570
  }
1565
1571
  return null;
1566
1572
  }
@@ -1594,6 +1600,59 @@ function extractContent(payload) {
1594
1600
  }
1595
1601
  return null;
1596
1602
  }
1603
+ function asNonNegInt(v) {
1604
+ if (typeof v === "number" && Number.isFinite(v) && v >= 0) {
1605
+ return Math.floor(v);
1606
+ }
1607
+ if (typeof v === "string" && v.trim() !== "") {
1608
+ const n = Number(v);
1609
+ if (Number.isFinite(n) && n >= 0) return Math.floor(n);
1610
+ }
1611
+ return null;
1612
+ }
1613
+ function extractModelId(payload) {
1614
+ const rec = asRecord2(payload);
1615
+ if (!rec) return void 0;
1616
+ const usageRec = asRecord2(rec.usage);
1617
+ for (const raw of [
1618
+ usageRec?.model_id,
1619
+ usageRec?.model,
1620
+ rec.model_id,
1621
+ rec.model
1622
+ ]) {
1623
+ if (typeof raw === "string" && raw.trim()) return raw.trim().slice(0, 200);
1624
+ }
1625
+ return void 0;
1626
+ }
1627
+ function extractUsage(payload) {
1628
+ const rec = asRecord2(payload);
1629
+ if (!rec) return void 0;
1630
+ 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);
1633
+ if (input === null && output2 === null) return void 0;
1634
+ const out = {
1635
+ input_tokens: input ?? 0,
1636
+ output_tokens: output2 ?? 0
1637
+ };
1638
+ const ms = usageRec.meter_source;
1639
+ if (ms === "peer_self" || ms === "gateway" || ms === "runtime_attested" || ms === "protocol") {
1640
+ out.meter_source = ms;
1641
+ }
1642
+ const modelId = extractModelId(payload);
1643
+ if (modelId) out.model_id = modelId;
1644
+ return out;
1645
+ }
1646
+ function parseCompletePayload(payload) {
1647
+ const content = extractContent(payload);
1648
+ if (!content) return { ok: false, reason: "complete_missing_content" };
1649
+ const usage = extractUsage(payload);
1650
+ const modelId = extractModelId(payload);
1651
+ const result = { content };
1652
+ if (usage) result.usage = usage;
1653
+ else if (modelId) result.modelId = modelId;
1654
+ return { ok: true, result };
1655
+ }
1597
1656
  async function mintAgentJwt(opts, fetchFn = fetch) {
1598
1657
  const now = Math.floor(Date.now() / 1e3);
1599
1658
  if (cachedJwt && cachedJwt.agentId === opts.agentId && cachedJwt.expEpochSec > now + 60) {
@@ -1661,9 +1720,7 @@ async function completeViaHttp(event, opts, deps) {
1661
1720
  } catch {
1662
1721
  return { ok: false, reason: "complete_invalid_json" };
1663
1722
  }
1664
- const content = extractContent(parsed);
1665
- if (!content) return { ok: false, reason: "complete_missing_content" };
1666
- return { ok: true, content };
1723
+ return parseCompletePayload(parsed);
1667
1724
  } catch (err) {
1668
1725
  const msg = err instanceof Error ? err.message : String(err);
1669
1726
  if (controller.signal.aborted || /abort|timeout/i.test(msg)) {
@@ -1711,12 +1768,7 @@ function completeViaExec(event, opts, deps) {
1711
1768
  const text = Buffer.concat(stdout).toString("utf-8").trim();
1712
1769
  try {
1713
1770
  const parsed = JSON.parse(text);
1714
- const content = extractContent(parsed);
1715
- if (!content) {
1716
- finish({ ok: false, reason: "complete_missing_content" });
1717
- return;
1718
- }
1719
- finish({ ok: true, content });
1771
+ finish(parseCompletePayload(parsed));
1720
1772
  } catch {
1721
1773
  finish({ ok: false, reason: "complete_invalid_json" });
1722
1774
  }
@@ -1724,7 +1776,7 @@ function completeViaExec(event, opts, deps) {
1724
1776
  child.stdin?.end(body);
1725
1777
  });
1726
1778
  }
1727
- async function postWriteback(event, content, opts, deps) {
1779
+ async function postWriteback(event, complete, opts, deps) {
1728
1780
  const chat = event.chat;
1729
1781
  if (!chat) return { ok: false, reason: "no_chat_envelope" };
1730
1782
  if (!isAllowedChatReplyPath(chat.chat_id, chat.reply_path)) {
@@ -1747,6 +1799,24 @@ async function postWriteback(event, content, opts, deps) {
1747
1799
  return { ok: false, reason: "reply_url_origin_mismatch" };
1748
1800
  }
1749
1801
  const timeoutMs = opts.writebackTimeoutMs ?? DEFAULT_WRITEBACK_TIMEOUT_MS;
1802
+ const replyToId = chat.gateway_message_id ?? event.message_id;
1803
+ const body = {
1804
+ content: complete.content,
1805
+ reply_to_id: replyToId
1806
+ };
1807
+ if (complete.usage) {
1808
+ const usageBody = {
1809
+ input_tokens: complete.usage.input_tokens,
1810
+ output_tokens: complete.usage.output_tokens,
1811
+ meter_source: complete.usage.meter_source ?? "peer_self"
1812
+ };
1813
+ if (complete.usage.model_id) {
1814
+ usageBody.model_id = complete.usage.model_id;
1815
+ }
1816
+ body.usage = usageBody;
1817
+ } else if (complete.modelId) {
1818
+ body.usage = { model_id: complete.modelId };
1819
+ }
1750
1820
  const postOnce = async (token) => {
1751
1821
  const controller = new AbortController();
1752
1822
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -1757,7 +1827,7 @@ async function postWriteback(event, content, opts, deps) {
1757
1827
  "content-type": "application/json",
1758
1828
  Authorization: `Bearer ${token}`
1759
1829
  },
1760
- body: JSON.stringify({ content }),
1830
+ body: JSON.stringify(body),
1761
1831
  signal: controller.signal
1762
1832
  });
1763
1833
  if (res.status === 200 || res.status === 201) {
@@ -1813,15 +1883,16 @@ async function handleChatWriteback(event, opts, deps = {}) {
1813
1883
  );
1814
1884
  return { ok: false, reason: completed.reason };
1815
1885
  }
1816
- const written = await postWriteback(event, completed.content, opts, deps);
1886
+ const written = await postWriteback(event, completed.result, opts, deps);
1817
1887
  if (!written.ok) {
1818
1888
  logFn(
1819
1889
  `[acn listen] chat_writeback_failed chat_id=${event.chat.chat_id} message_id=${event.message_id} reason=${written.reason}`
1820
1890
  );
1821
1891
  return written;
1822
1892
  }
1893
+ const usageNote = completed.result.usage ? ` usage_in=${completed.result.usage.input_tokens} usage_out=${completed.result.usage.output_tokens}` : "";
1823
1894
  logFn(
1824
- `[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}`
1895
+ `[acn listen] chat_writeback_ok chat_id=${event.chat.chat_id} message_id=${event.message_id} http=${written.httpStatus}${usageNote}`
1825
1896
  );
1826
1897
  return written;
1827
1898
  }
@@ -2223,8 +2294,46 @@ function rawToString(data) {
2223
2294
  return Buffer.from(data).toString("utf-8");
2224
2295
  }
2225
2296
  var KEEPALIVE_INTERVAL_MS = 3e4;
2297
+ var MODEL_HEARTBEAT_INTERVAL_MS = 15 * 6e4;
2226
2298
  var INITIAL_BACKOFF_MS = 1e3;
2227
2299
  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
+ }
2228
2337
  function runListener(cfg) {
2229
2338
  const wsUrl = toWebsocketUrl(cfg.baseUrl, cfg.agentId);
2230
2339
  let backoff = INITIAL_BACKOFF_MS;
@@ -2235,15 +2344,40 @@ function runListener(cfg) {
2235
2344
  headers: { Authorization: `Bearer ${cfg.apiKey}` }
2236
2345
  });
2237
2346
  let keepalive;
2347
+ let modelHeartbeat;
2348
+ const sendModelHeartbeat = () => {
2349
+ if (!cfg.preferredModel || stopped) return;
2350
+ void postAgentHeartbeat({
2351
+ baseUrl: cfg.baseUrl,
2352
+ agentId: cfg.agentId,
2353
+ apiKey: cfg.apiKey,
2354
+ preferredModel: cfg.preferredModel
2355
+ }).then((r) => {
2356
+ if (!r.ok) {
2357
+ console.error(`[acn listen] preferred_model heartbeat failed: ${r.reason}`);
2358
+ return;
2359
+ }
2360
+ console.error(
2361
+ `[acn listen] preferred_model heartbeat ok model=${r.preferred_model ?? cfg.preferredModel}`
2362
+ );
2363
+ });
2364
+ };
2238
2365
  ws.on("open", () => {
2239
2366
  const mode = cfg.runtime ? `runtime=${cfg.runtime.runtime}` : cfg.forward ? `forward=${cfg.forward}` : `exec`;
2240
- console.error(`[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl} (${mode})`);
2367
+ const modelNote = cfg.preferredModel ? ` preferred_model=${cfg.preferredModel}` : "";
2368
+ console.error(
2369
+ `[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl} (${mode})${modelNote}`
2370
+ );
2241
2371
  backoff = INITIAL_BACKOFF_MS;
2242
2372
  keepalive = setInterval(() => {
2243
2373
  if (ws.readyState === import_ws.default.OPEN) {
2244
2374
  ws.send(JSON.stringify({ type: "ping" }));
2245
2375
  }
2246
2376
  }, KEEPALIVE_INTERVAL_MS);
2377
+ if (cfg.preferredModel) {
2378
+ sendModelHeartbeat();
2379
+ modelHeartbeat = setInterval(sendModelHeartbeat, MODEL_HEARTBEAT_INTERVAL_MS);
2380
+ }
2247
2381
  });
2248
2382
  ws.on("message", (data) => {
2249
2383
  let frame;
@@ -2265,6 +2399,7 @@ function runListener(cfg) {
2265
2399
  });
2266
2400
  ws.on("close", (code, reason) => {
2267
2401
  if (keepalive) clearInterval(keepalive);
2402
+ if (modelHeartbeat) clearInterval(modelHeartbeat);
2268
2403
  if (stopped) return;
2269
2404
  if (code === 4401 || code === 4403 || code === 4429) {
2270
2405
  console.error(
@@ -2350,7 +2485,10 @@ function listenCommand() {
2350
2485
  "--chat-complete-timeout <ms>",
2351
2486
  `Host complete timeout in ms (default ${DEFAULT_COMPLETE_TIMEOUT_MS})`,
2352
2487
  String(DEFAULT_COMPLETE_TIMEOUT_MS)
2353
- ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").action(
2488
+ ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").option(
2489
+ "-m, --model <modelId>",
2490
+ "Declare runtime model (Host Catalog id) on connect + every 15m via REST heartbeat (self-reported; env: ACN_PREFERRED_MODEL)"
2491
+ ).action(
2354
2492
  (opts) => {
2355
2493
  const config = loadConfig();
2356
2494
  const apiKey = config.api_key;
@@ -2440,10 +2578,12 @@ function listenCommand() {
2440
2578
  agentId,
2441
2579
  audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
2442
2580
  });
2581
+ const preferredModel = resolvePreferredModel({ model: opts.model });
2443
2582
  runListener({
2444
2583
  agentId,
2445
2584
  apiKey,
2446
2585
  baseUrl: config.base_url,
2586
+ preferredModel,
2447
2587
  forward: opts.forward,
2448
2588
  exec: opts.exec,
2449
2589
  runtime: opts.runtime ? {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@acnlabs/acn-cli",
3
- "version": "0.14.2",
4
- "description": "Official CLI for ACN (Agent Collaboration Network) zero-integration agent access",
3
+ "version": "1.0.1",
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"