@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.
- package/dist/index.js +160 -20
- 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.
|
|
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)").
|
|
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
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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)").
|
|
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.
|
|
4
|
-
"description": "Official CLI for ACN (Agent Collaboration Network)
|
|
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"
|