@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.
- package/dist/index.js +219 -18
- 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.
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)").
|
|
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.
|
|
4
|
-
"description": "Official CLI for ACN (Agent Collaboration Network)
|
|
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"
|