@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.
- package/dist/index.js +163 -63
- 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.
|
|
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
|
-
).
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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 (
|
|
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]
|
|
2435
|
+
console.error(`[acn listen] model heartbeat failed: ${r.reason}`);
|
|
2358
2436
|
return;
|
|
2359
2437
|
}
|
|
2360
|
-
|
|
2361
|
-
|
|
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 =
|
|
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 ? {
|