@acnlabs/acn-cli 1.0.1 → 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 +163 -63
  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.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,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 = {}) {
@@ -2297,43 +2410,6 @@ var KEEPALIVE_INTERVAL_MS = 3e4;
2297
2410
  var MODEL_HEARTBEAT_INTERVAL_MS = 15 * 6e4;
2298
2411
  var INITIAL_BACKOFF_MS = 1e3;
2299
2412
  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
2413
  function runListener(cfg) {
2338
2414
  const wsUrl = toWebsocketUrl(cfg.baseUrl, cfg.agentId);
2339
2415
  let backoff = INITIAL_BACKOFF_MS;
@@ -2346,25 +2422,32 @@ function runListener(cfg) {
2346
2422
  let keepalive;
2347
2423
  let modelHeartbeat;
2348
2424
  const sendModelHeartbeat = () => {
2349
- if (!cfg.preferredModel || stopped) return;
2425
+ if (stopped) return;
2426
+ if (!cfg.preferredModel && cfg.supportedModels === void 0) return;
2350
2427
  void postAgentHeartbeat({
2351
2428
  baseUrl: cfg.baseUrl,
2352
2429
  agentId: cfg.agentId,
2353
2430
  apiKey: cfg.apiKey,
2354
- preferredModel: cfg.preferredModel
2431
+ preferredModel: cfg.preferredModel,
2432
+ supportedModels: cfg.supportedModels
2355
2433
  }).then((r) => {
2356
2434
  if (!r.ok) {
2357
- console.error(`[acn listen] preferred_model heartbeat failed: ${r.reason}`);
2435
+ console.error(`[acn listen] model heartbeat failed: ${r.reason}`);
2358
2436
  return;
2359
2437
  }
2360
- console.error(
2361
- `[acn listen] preferred_model heartbeat ok model=${r.preferred_model ?? cfg.preferredModel}`
2362
- );
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}`);
2363
2443
  });
2364
2444
  };
2365
2445
  ws.on("open", () => {
2366
2446
  const mode = cfg.runtime ? `runtime=${cfg.runtime.runtime}` : cfg.forward ? `forward=${cfg.forward}` : `exec`;
2367
- const modelNote = cfg.preferredModel ? ` preferred_model=${cfg.preferredModel}` : "";
2447
+ const modelNote = formatModelHeartbeatLog({
2448
+ preferred: cfg.preferredModel,
2449
+ supported: cfg.supportedModels
2450
+ });
2368
2451
  console.error(
2369
2452
  `[acn listen] connected as ${cfg.agentId} \u2192 ${wsUrl} (${mode})${modelNote}`
2370
2453
  );
@@ -2374,7 +2457,7 @@ function runListener(cfg) {
2374
2457
  ws.send(JSON.stringify({ type: "ping" }));
2375
2458
  }
2376
2459
  }, KEEPALIVE_INTERVAL_MS);
2377
- if (cfg.preferredModel) {
2460
+ if (cfg.preferredModel || cfg.supportedModels !== void 0) {
2378
2461
  sendModelHeartbeat();
2379
2462
  modelHeartbeat = setInterval(sendModelHeartbeat, MODEL_HEARTBEAT_INTERVAL_MS);
2380
2463
  }
@@ -2488,6 +2571,12 @@ function listenCommand() {
2488
2571
  ).option("-i, --agent-id <id>", "Agent ID (defaults to config)").option(
2489
2572
  "-m, --model <modelId>",
2490
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)"
2491
2580
  ).action(
2492
2581
  (opts) => {
2493
2582
  const config = loadConfig();
@@ -2579,11 +2668,22 @@ function listenCommand() {
2579
2668
  audience: process.env.ACN_CHAT_JWT_AUDIENCE?.trim() || process.env.AGENTPLANET_JWT_AUDIENCE?.trim()
2580
2669
  });
2581
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
+ });
2582
2681
  runListener({
2583
2682
  agentId,
2584
2683
  apiKey,
2585
2684
  baseUrl: config.base_url,
2586
2685
  preferredModel,
2686
+ supportedModels,
2587
2687
  forward: opts.forward,
2588
2688
  exec: opts.exec,
2589
2689
  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.2",
4
4
  "description": "Official CLI for ACN (Agent Collaboration Network) \u2014 zero-integration agent access",
5
5
  "main": "dist/index.js",
6
6
  "bin": {