@openagents-org/agent-launcher 0.2.193 → 0.2.194
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/README.md +32 -2
- package/package.json +1 -1
- package/registry.json +9 -3
- package/src/adapters/antigravity-stream.js +65 -5
- package/src/adapters/base.js +277 -9
- package/src/adapters/claude.js +152 -21
- package/src/adapters/codex.js +37 -3
- package/src/adapters/cursor-node-entry.js +64 -0
- package/src/adapters/cursor.js +31 -1
- package/src/adapters/hermes.js +354 -58
- package/src/adapters/opencode.js +25 -3
- package/src/adapters/response-artifacts.js +72 -0
- package/src/adapters/thread-history.js +49 -0
- package/src/adapters/workspace-prompt.js +102 -12
- package/src/cli-models.js +244 -0
- package/src/cli.js +69 -13
- package/src/config.js +1 -0
- package/src/daemon.js +153 -15
- package/src/index.js +10 -1
- package/src/installer.js +413 -16
- package/src/mcp-server.js +138 -9
- package/src/node-config.js +6 -4
- package/src/postinstall-stub.js +69 -0
- package/src/tools-v11/brief.js +97 -0
- package/src/tools-v11/index.js +28 -0
- package/src/tools-v11/presence-handoff.js +199 -0
- package/src/workspace-client.js +230 -18
package/src/cli.js
CHANGED
|
@@ -217,7 +217,7 @@ async function cmdCreate(connector, flags, positional) {
|
|
|
217
217
|
}
|
|
218
218
|
} else {
|
|
219
219
|
print(` agn connect ${name} <workspace-token>`);
|
|
220
|
-
print('Or pair this device first: agn
|
|
220
|
+
print('Or pair this device first: agn device connect <pairing-code>');
|
|
221
221
|
}
|
|
222
222
|
} else {
|
|
223
223
|
print(`Agent '${name}' created (type: ${type})`);
|
|
@@ -441,7 +441,7 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
441
441
|
// no server round-trip; the credential is already on disk. This is the
|
|
442
442
|
// pairing-first path; the token form below stays for manual connection.
|
|
443
443
|
if (flags.workspace && typeof flags.workspace === 'string') {
|
|
444
|
-
connectKnownWorkspace(connector, name, flags.workspace);
|
|
444
|
+
connectKnownWorkspace(connector, name, flags.workspace, flags.connection);
|
|
445
445
|
return;
|
|
446
446
|
}
|
|
447
447
|
|
|
@@ -480,7 +480,7 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
480
480
|
// CLI half of the dismissible launcher notice.
|
|
481
481
|
if (!process.env.OPENAGENTS_NO_DEPRECATION_NOTES) {
|
|
482
482
|
print('note: manual token connection is being retired in favor of device pairing.');
|
|
483
|
-
print(' Pair once with `agn
|
|
483
|
+
print(' Pair once with `agn device connect <code>`, then `agn connect <agent> --workspace <slug>`.');
|
|
484
484
|
print(' (Silence this note with OPENAGENTS_NO_DEPRECATION_NOTES=1.)');
|
|
485
485
|
}
|
|
486
486
|
|
|
@@ -504,7 +504,12 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
504
504
|
* (node.json) — a pairing registers its network on the spot so the daemon can
|
|
505
505
|
* pick up the token. Never touches the server.
|
|
506
506
|
*/
|
|
507
|
-
function connectKnownWorkspace(connector, name, ref) {
|
|
507
|
+
function connectKnownWorkspace(connector, name, ref, connectionId) {
|
|
508
|
+
const matches = require('./node-config').listPairings().filter((p) => p.workspace_slug === ref || p.workspace_id === ref);
|
|
509
|
+
if ((!connectionId && matches.length > 1) || (connectionId && !matches.some((p) => p.node_id === connectionId))) {
|
|
510
|
+
throw new Error("Choose a device connection with --connection <id>");
|
|
511
|
+
}
|
|
512
|
+
const chosen = matches.find((p) => !connectionId || p.node_id === connectionId);
|
|
508
513
|
const networks = connector.config.getNetworks() || [];
|
|
509
514
|
let net = networks.find((n) => n.slug === ref || n.id === ref);
|
|
510
515
|
|
|
@@ -538,13 +543,14 @@ function connectKnownWorkspace(connector, name, ref) {
|
|
|
538
543
|
].filter(Boolean);
|
|
539
544
|
print(`No workspace '${ref}' is known on this device.`);
|
|
540
545
|
if (known.length) print(`Known workspaces: ${known.join(', ')}`);
|
|
541
|
-
print('Pair this device first (`agn
|
|
546
|
+
print('Pair this device first (`agn device connect <code>`), or connect with a token.');
|
|
542
547
|
process.exitCode = 1;
|
|
543
548
|
return;
|
|
544
549
|
}
|
|
545
550
|
|
|
546
551
|
const slug = net.slug || ref;
|
|
547
552
|
connector.connectWorkspace(name, slug);
|
|
553
|
+
if (chosen) connector.config.updateAgent(name, { node_id: chosen.node_id });
|
|
548
554
|
print(`'${name}' connected to workspace '${net.name || slug}'`);
|
|
549
555
|
|
|
550
556
|
const pid = connector.getDaemonPid();
|
|
@@ -570,6 +576,11 @@ function workspaceWebUrl(slug, endpoint) {
|
|
|
570
576
|
: `https://workspace.openagents.org/${slug}`;
|
|
571
577
|
}
|
|
572
578
|
|
|
579
|
+
/**
|
|
580
|
+
* `agn device <connect|status>` — pair this device (formerly "node") to a
|
|
581
|
+
* workspace. `agn node`, `agn devices` and `agn nodes` are aliases so existing
|
|
582
|
+
* scripts and older docs keep working; the product wording is "device".
|
|
583
|
+
*/
|
|
573
584
|
async function cmdNode(connector, flags, positional) {
|
|
574
585
|
const sub = positional[0] || 'status';
|
|
575
586
|
const nodeCfg = require('./node-config');
|
|
@@ -577,8 +588,8 @@ async function cmdNode(connector, flags, positional) {
|
|
|
577
588
|
if (sub === 'connect') {
|
|
578
589
|
const code = positional[1] || flags.code;
|
|
579
590
|
if (!code) {
|
|
580
|
-
print('Usage: agn
|
|
581
|
-
print('Get a pairing code from your workspace: Connect a
|
|
591
|
+
print('Usage: agn device connect <pairing-code>');
|
|
592
|
+
print('Get a pairing code from your workspace: Devices → Connect a device.');
|
|
582
593
|
process.exitCode = 1;
|
|
583
594
|
return;
|
|
584
595
|
}
|
|
@@ -616,12 +627,19 @@ async function cmdNode(connector, flags, positional) {
|
|
|
616
627
|
// credential in place — but the user should hear "already paired", not
|
|
617
628
|
// a duplicate "connected".
|
|
618
629
|
const alreadyPaired = priorPairings.some(
|
|
619
|
-
(p) => p.
|
|
630
|
+
(p) => p.node_id === res.nodeId,
|
|
620
631
|
);
|
|
621
632
|
// recordPairing, not saveNode: this device can be a node in several
|
|
622
633
|
// workspaces at once, and a bare save would drop the others' pairings.
|
|
634
|
+
// Freeze legacy bindings before another owner adds a connection here.
|
|
635
|
+
const prior = priorPairings.filter((p) => p.workspace_id === res.workspaceId);
|
|
636
|
+
if (prior.length === 1) for (const agent of connector.config.getAgents()) {
|
|
637
|
+
if (!agent.node_id && (agent.network === prior[0].workspace_slug || agent.network === prior[0].workspace_id))
|
|
638
|
+
connector.config.updateAgent(agent.name, { node_id: prior[0].node_id });
|
|
639
|
+
}
|
|
623
640
|
nodeCfg.recordPairing(info.nodeKey, {
|
|
624
641
|
node_id: res.nodeId,
|
|
642
|
+
owner_email: res.ownerEmail,
|
|
625
643
|
workspace_id: res.workspaceId,
|
|
626
644
|
workspace_slug: res.workspaceSlug,
|
|
627
645
|
workspace_name: res.workspaceName,
|
|
@@ -639,7 +657,7 @@ async function cmdNode(connector, flags, positional) {
|
|
|
639
657
|
if (alreadyPaired) {
|
|
640
658
|
print(`Already paired with '${res.workspaceName}' (${res.workspaceSlug}) — refreshed this device's credential.`);
|
|
641
659
|
} else {
|
|
642
|
-
print(`
|
|
660
|
+
print(`Device connected to workspace '${res.workspaceName}' (${res.workspaceSlug})`);
|
|
643
661
|
}
|
|
644
662
|
print(` This device: ${info.hostname} (${info.deviceType})`);
|
|
645
663
|
|
|
@@ -672,10 +690,10 @@ async function cmdNode(connector, flags, positional) {
|
|
|
672
690
|
if (sub === 'status') {
|
|
673
691
|
const pairings = nodeCfg.listPairings().filter((p) => p.node_id);
|
|
674
692
|
if (!pairings.length) {
|
|
675
|
-
print('No
|
|
693
|
+
print('No device connected. Run: agn device connect <pairing-code>');
|
|
676
694
|
return;
|
|
677
695
|
}
|
|
678
|
-
print(`
|
|
696
|
+
print(`Device connected to ${pairings.length} workspace(s):`);
|
|
679
697
|
for (const p of pairings) {
|
|
680
698
|
const slug = p.workspace_slug || p.workspace_id;
|
|
681
699
|
print(` ${p.workspace_name || slug} (${slug}) — ${workspaceWebUrl(slug, p.endpoint)}`);
|
|
@@ -683,7 +701,7 @@ async function cmdNode(connector, flags, positional) {
|
|
|
683
701
|
return;
|
|
684
702
|
}
|
|
685
703
|
|
|
686
|
-
print('Usage: agn
|
|
704
|
+
print('Usage: agn device <connect|status> [pairing-code] (alias: agn node)');
|
|
687
705
|
process.exitCode = 1;
|
|
688
706
|
}
|
|
689
707
|
|
|
@@ -1024,6 +1042,37 @@ async function cmdProbe(connector, flags, positional) {
|
|
|
1024
1042
|
}
|
|
1025
1043
|
}
|
|
1026
1044
|
|
|
1045
|
+
async function cmdModels(connector, flags, positional) {
|
|
1046
|
+
const target = positional[0];
|
|
1047
|
+
if (!target) { print('Usage: agn models <type|agent> [--json]'); return; }
|
|
1048
|
+
|
|
1049
|
+
// Accept an agent name, as `agn probe` does: its CLI is asked on that
|
|
1050
|
+
// agent's own env, so a signed-in agent answers for its sign-in.
|
|
1051
|
+
let type = target;
|
|
1052
|
+
let agentEnv;
|
|
1053
|
+
try {
|
|
1054
|
+
const agent = connector.config.getAgent(target);
|
|
1055
|
+
if (agent) { type = agent.type || 'openclaw'; agentEnv = agent.env || {}; }
|
|
1056
|
+
} catch {}
|
|
1057
|
+
|
|
1058
|
+
const result = await connector.listCliModels(type, { agentEnv });
|
|
1059
|
+
|
|
1060
|
+
if (flags && flags.json) {
|
|
1061
|
+
// Machine consumers (the daemon's model sweep) read ONLY stdout JSON —
|
|
1062
|
+
// same contract as `agn probe --json`.
|
|
1063
|
+
process.stdout.write(JSON.stringify(result));
|
|
1064
|
+
if (!result.ok) process.exitCode = 1;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (!result.ok) {
|
|
1069
|
+
print(`FAILED ${result.error || ''}`.trim());
|
|
1070
|
+
process.exitCode = 1;
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
for (const m of result.models) print(m.label === m.id ? m.id : `${m.id} ${m.label}`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1027
1076
|
async function cmdVersion() {
|
|
1028
1077
|
const pkg = require('../package.json');
|
|
1029
1078
|
print(`${pkg.name} v${pkg.version}`);
|
|
@@ -1086,6 +1135,8 @@ Commands:
|
|
|
1086
1135
|
uninstall <type> Uninstall an agent runtime
|
|
1087
1136
|
search [query] Browse agent catalog
|
|
1088
1137
|
runtimes List installed runtimes
|
|
1138
|
+
device connect <code> Pair this device (formerly "node") to a workspace with a pairing code
|
|
1139
|
+
device status Show the workspaces this device is paired with (alias: node)
|
|
1089
1140
|
connect <agent> <token> Connect agent to workspace (or --workspace <slug> for a paired one)
|
|
1090
1141
|
disconnect <agent> Disconnect agent from workspace
|
|
1091
1142
|
env <type> [--set K=V] View/set env vars for agent type
|
|
@@ -1094,6 +1145,7 @@ Commands:
|
|
|
1094
1145
|
autostart [--disable] Enable/disable auto-start on login
|
|
1095
1146
|
test-llm <type> Test LLM connection
|
|
1096
1147
|
probe <type|agent> [--json] Smoke-test an agent (tiny end-to-end prompt)
|
|
1148
|
+
models <type|agent> [--json] List the models the agent's CLI offers its signed-in account
|
|
1097
1149
|
logs [agent] [--lines N] View daemon logs
|
|
1098
1150
|
workspace create [name] Create a new workspace
|
|
1099
1151
|
workspace join <token> Join workspace with token
|
|
@@ -1173,7 +1225,10 @@ async function main() {
|
|
|
1173
1225
|
search: () => cmdSearch(connector, flags, positional),
|
|
1174
1226
|
runtimes: () => cmdRuntimes(connector, flags),
|
|
1175
1227
|
connect: () => cmdConnect(connector, flags, positional),
|
|
1176
|
-
|
|
1228
|
+
device: () => cmdNode(connector, flags, positional),
|
|
1229
|
+
devices: () => cmdNode(connector, flags, positional),
|
|
1230
|
+
node: () => cmdNode(connector, flags, positional), // legacy spelling
|
|
1231
|
+
nodes: () => cmdNode(connector, flags, positional), // legacy spelling
|
|
1177
1232
|
disconnect: () => cmdDisconnect(connector, flags, positional),
|
|
1178
1233
|
logs: () => cmdLogs(connector, flags, positional),
|
|
1179
1234
|
autostart: () => cmdAutostart(connector, flags),
|
|
@@ -1183,6 +1238,7 @@ async function main() {
|
|
|
1183
1238
|
'tool-mode': () => cmdToolMode(connector, flags, positional),
|
|
1184
1239
|
'test-llm': () => cmdTestLLM(connector, flags, positional),
|
|
1185
1240
|
probe: () => cmdProbe(connector, flags, positional),
|
|
1241
|
+
models: () => cmdModels(connector, flags, positional),
|
|
1186
1242
|
update: () => cmdUpdate(connector),
|
|
1187
1243
|
'mcp-server': () => {
|
|
1188
1244
|
const { runMcpServer } = require('./mcp-server');
|
package/src/config.js
CHANGED
package/src/daemon.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
const { execSync, execFileSync } = require('child_process');
|
|
6
7
|
// spawn() here is the WSL bridge from ./wsl: same signature as
|
|
7
8
|
// child_process.spawn, and a straight pass-through unless the resolved CLI
|
|
@@ -10,10 +11,20 @@ const { spawn } = require('./wsl');
|
|
|
10
11
|
const os = require('os');
|
|
11
12
|
const { WorkspaceClient } = require('./workspace-client');
|
|
12
13
|
const { listEndpointModels } = require('./model-list');
|
|
13
|
-
const {
|
|
14
|
+
const { supportsCliModels } = require('./cli-models');
|
|
15
|
+
const { AUTH_MODE_KEY, isCliLogin, modelKeys, stripForCliLogin, typeEnvFor } = require('./env');
|
|
14
16
|
const { getEnhancedEnv, whichBinary, IS_WINDOWS, defaultAgentWorkdir } = require('./paths');
|
|
15
17
|
const { LogWriter } = require('./log-writer');
|
|
16
18
|
|
|
19
|
+
// A signed-in agent's model list is asked for again after this long. What a
|
|
20
|
+
// CLI is offered changes when the CLI is upgraded (a vendor holds new models
|
|
21
|
+
// back from old versions), so the upgrade should show within the hour.
|
|
22
|
+
const CLI_MODELS_MAX_AGE_MS = 3600 * 1000;
|
|
23
|
+
// A sign-in whose CLI has never answered is asked again sooner: the first ask
|
|
24
|
+
// may have failed for a passing reason (a slow start, an update mid-way). A
|
|
25
|
+
// little under the 10-minute sweep, so the next pass retries it.
|
|
26
|
+
const CLI_MODELS_RETRY_MS = 9 * 60 * 1000;
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
29
|
* Mask an API key for display (same shape the workspace backend uses for
|
|
19
30
|
* cloud agents): enough to recognize which key it is, never enough to use it.
|
|
@@ -111,7 +122,7 @@ class Daemon {
|
|
|
111
122
|
this._cmdInterval = null;
|
|
112
123
|
this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
|
|
113
124
|
this._displayNameInterval = null; // pulls workspace-side renames back
|
|
114
|
-
this._nodeClients = new Map(); //
|
|
125
|
+
this._nodeClients = new Map(); // node_id -> WorkspaceClient, one per private connection
|
|
115
126
|
this._runningCommands = new Set(); // node-command ids currently executing
|
|
116
127
|
this._runtimes = []; // detected agent runtimes for the node view
|
|
117
128
|
this._runtimesInterval = null;
|
|
@@ -119,6 +130,10 @@ class Daemon {
|
|
|
119
130
|
this._probeInterval = null;
|
|
120
131
|
this._probeStartupTimer = null;
|
|
121
132
|
this._probeInFlight = new Set(); // types being probed right now
|
|
133
|
+
this._cliModels = {}; // sign-in key → { models, askedAt } from the agents' own CLI
|
|
134
|
+
this._cliModelsInFlight = new Set(); // sign-in keys whose CLI is being asked right now
|
|
135
|
+
this._cliModelsStartupTimer = null;
|
|
136
|
+
this._cliModelsInterval = null;
|
|
122
137
|
this._reloadInFlight = null; // serialize concurrent _reload() calls
|
|
123
138
|
}
|
|
124
139
|
|
|
@@ -199,6 +214,98 @@ class Daemon {
|
|
|
199
214
|
this._nodeHeartbeat();
|
|
200
215
|
}
|
|
201
216
|
|
|
217
|
+
// ── Models a signed-in agent's account is offered ──────────────────────
|
|
218
|
+
// An agent on a subscription sign-in has no endpoint whose models can be
|
|
219
|
+
// listed, so its own CLI is asked (see cli-models.js). The answer rides the
|
|
220
|
+
// heartbeat's roster as agents[].cliModels, which puts it in front of the
|
|
221
|
+
// workspace's model picker without a command round trip. Kept in memory
|
|
222
|
+
// only: asking again after a restart costs a CLI start, not a model call.
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Whether this agent runs on its CLI's own sign-in: a type whose CLI can
|
|
226
|
+
* say what the account is offered, calling that CLI's default endpoint,
|
|
227
|
+
* with no key the CLI would be handed instead.
|
|
228
|
+
*/
|
|
229
|
+
_onCliSignIn(a) {
|
|
230
|
+
if (!supportsCliModels(a.type)) return false;
|
|
231
|
+
const endpoint = this._agentEndpoint(a);
|
|
232
|
+
return !endpoint.baseUrl && !endpoint.apiKey;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* One key per sign-in. Agents of a type run one CLI on one account unless
|
|
237
|
+
* their own env says otherwise (a token or a config dir of their own), so
|
|
238
|
+
* those with the same env share an answer: a device with nine Claude agents
|
|
239
|
+
* starts the CLI once, not nine times. The model each runs is left out, it
|
|
240
|
+
* does not change what the account is offered. Hashed, since that env can
|
|
241
|
+
* hold a token.
|
|
242
|
+
*/
|
|
243
|
+
_cliModelsKey(a) {
|
|
244
|
+
const skip = modelKeys(a.type, this.registry);
|
|
245
|
+
const own = Object.entries(a.env || {})
|
|
246
|
+
.filter(([k]) => !skip.has(k))
|
|
247
|
+
.sort(([x], [y]) => (x < y ? -1 : x > y ? 1 : 0));
|
|
248
|
+
return crypto.createHash('sha256').update(JSON.stringify([a.type, own])).digest('hex');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Ask an agent's CLI for its models via a child `agn models <name> --json`
|
|
253
|
+
* (off the event loop, and on that agent's own env, like a probe).
|
|
254
|
+
* Resolves true when the list it keeps for that sign-in changed.
|
|
255
|
+
*/
|
|
256
|
+
async _listCliModels(a) {
|
|
257
|
+
const key = this._cliModelsKey(a);
|
|
258
|
+
if (this._cliModelsInFlight.has(key)) return false;
|
|
259
|
+
this._cliModelsInFlight.add(key);
|
|
260
|
+
try {
|
|
261
|
+
const r = await this._runAgn(['models', a.name, '--json']);
|
|
262
|
+
let parsed = null;
|
|
263
|
+
try { parsed = JSON.parse(r.stdout.trim()); } catch {}
|
|
264
|
+
const models = parsed && parsed.ok && Array.isArray(parsed.models) ? parsed.models : [];
|
|
265
|
+
const last = this._cliModels[key];
|
|
266
|
+
// A CLI that would not answer keeps the list it last gave: that is
|
|
267
|
+
// still the best answer there is.
|
|
268
|
+
const kept = models.length ? models : ((last && last.models) || null);
|
|
269
|
+
this._cliModels[key] = { models: kept, askedAt: Date.now() };
|
|
270
|
+
return JSON.stringify(kept) !== JSON.stringify((last && last.models) || null);
|
|
271
|
+
} finally {
|
|
272
|
+
this._cliModelsInFlight.delete(key);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Ask in the background for an agent that was just created or reconfigured. */
|
|
277
|
+
_askCliModels(name) {
|
|
278
|
+
let agent = null;
|
|
279
|
+
try { agent = this.config.getAgent(name); } catch {}
|
|
280
|
+
if (!agent || !this._onCliSignIn(agent)) return;
|
|
281
|
+
// A changed list goes out on a heartbeat now, not on the next tick.
|
|
282
|
+
this._listCliModels(agent)
|
|
283
|
+
.then((changed) => (changed ? this._nodeHeartbeat() : undefined))
|
|
284
|
+
.catch(() => {});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Periodic sweep: ask for every sign-in whose list is missing or has aged
|
|
289
|
+
* out, and forget the ones no agent is on any more. Sequential, like the
|
|
290
|
+
* probe sweep: there is no hurry.
|
|
291
|
+
*/
|
|
292
|
+
async _refreshCliModels() {
|
|
293
|
+
const signIns = new Set();
|
|
294
|
+
for (const a of this.config.getAgents()) {
|
|
295
|
+
if (!a.network || !this._onCliSignIn(a)) continue;
|
|
296
|
+
const key = this._cliModelsKey(a);
|
|
297
|
+
if (signIns.has(key)) continue;
|
|
298
|
+
signIns.add(key);
|
|
299
|
+
const last = this._cliModels[key];
|
|
300
|
+
const maxAge = last && last.models ? CLI_MODELS_MAX_AGE_MS : CLI_MODELS_RETRY_MS;
|
|
301
|
+
if (last && Date.now() - last.askedAt < maxAge) continue;
|
|
302
|
+
try { await this._listCliModels(a); } catch {}
|
|
303
|
+
}
|
|
304
|
+
for (const key of Object.keys(this._cliModels)) {
|
|
305
|
+
if (!signIns.has(key)) delete this._cliModels[key];
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
202
309
|
/**
|
|
203
310
|
* Heartbeat this device (node) to EVERY workspace it is paired to,
|
|
204
311
|
* independent of any agent, so each workspace's "connected devices" view
|
|
@@ -240,7 +347,7 @@ class Daemon {
|
|
|
240
347
|
if (!info || !info.networkRef || info._credRestartPending) continue;
|
|
241
348
|
if (!this._adapters || !this._adapters[name]) continue;
|
|
242
349
|
let fresh = null;
|
|
243
|
-
try { fresh = this._resolveAgentNetwork(info.networkRef); } catch { /* keep running on the current token */ }
|
|
350
|
+
try { fresh = this._resolveAgentNetwork(info.networkRef, info.connectionId); } catch { /* keep running on the current token */ }
|
|
244
351
|
if (!fresh || !fresh.token || fresh.token === info.credentialToken) continue;
|
|
245
352
|
// Flag on the CURRENT info object: restartAgent replaces it with a fresh
|
|
246
353
|
// one on relaunch, so the flag cannot wedge the watch permanently.
|
|
@@ -255,10 +362,10 @@ class Daemon {
|
|
|
255
362
|
|
|
256
363
|
/** The WorkspaceClient for one pairing, created on first use and reused. */
|
|
257
364
|
_nodeClientFor(n) {
|
|
258
|
-
let client = this._nodeClients.get(n.
|
|
365
|
+
let client = this._nodeClients.get(n.node_id);
|
|
259
366
|
if (!client) {
|
|
260
367
|
client = new WorkspaceClient(n.endpoint);
|
|
261
|
-
this._nodeClients.set(n.
|
|
368
|
+
this._nodeClients.set(n.node_id, client);
|
|
262
369
|
}
|
|
263
370
|
return client;
|
|
264
371
|
}
|
|
@@ -282,8 +389,8 @@ class Daemon {
|
|
|
282
389
|
// is unreachable, and taking that as the workspace's word unpaired every
|
|
283
390
|
// workspace on the device at once.
|
|
284
391
|
if (_nodeGone(err)) {
|
|
285
|
-
try { nodeCfg.clearPairing(n.workspace_id); } catch {}
|
|
286
|
-
this._nodeClients.delete(n.
|
|
392
|
+
try { nodeCfg.clearPairing(n.workspace_id, n.node_id); } catch {}
|
|
393
|
+
this._nodeClients.delete(n.node_id);
|
|
287
394
|
this._log(`node ${n.node_id} no longer recognized by workspace ${n.workspace_slug || n.workspace_id} — pairing cleared`);
|
|
288
395
|
}
|
|
289
396
|
return; // nothing more to do for this pairing this tick
|
|
@@ -352,7 +459,13 @@ class Daemon {
|
|
|
352
459
|
*/
|
|
353
460
|
_agentOnNodeWorkspace(a, node) {
|
|
354
461
|
if (!a || !a.network || !node) return false;
|
|
355
|
-
|
|
462
|
+
if (a.network !== node.workspace_slug && a.network !== node.workspace_id) return false;
|
|
463
|
+
if (a.node_id) return a.node_id === node.node_id;
|
|
464
|
+
// Legacy agents may bind automatically only when the workspace is unambiguous.
|
|
465
|
+
const candidates = require('./node-config').listPairings().filter(
|
|
466
|
+
(p) => p.workspace_id === node.workspace_id && p.workspace_id,
|
|
467
|
+
);
|
|
468
|
+
return candidates.length <= 1;
|
|
356
469
|
}
|
|
357
470
|
|
|
358
471
|
/**
|
|
@@ -382,6 +495,9 @@ class Daemon {
|
|
|
382
495
|
try { typeEnv = typeEnvFor(a.type, this.envManager.load(a.type) || {}, this.registry, a.env); } catch {}
|
|
383
496
|
const model = (a.env && a.env.LLM_MODEL) || typeEnv.LLM_MODEL || null;
|
|
384
497
|
const apiKey = (a.env && a.env.LLM_API_KEY) || typeEnv.LLM_API_KEY || null;
|
|
498
|
+
// What the agent's own CLI says its signed-in account can run. Null
|
|
499
|
+
// for an agent on a key or a relay, and until its CLI has answered.
|
|
500
|
+
const listed = supportsCliModels(a.type) ? this._cliModels[this._cliModelsKey(a)] : null;
|
|
385
501
|
roster.push({
|
|
386
502
|
name: a.name,
|
|
387
503
|
type: a.type || 'unknown',
|
|
@@ -390,6 +506,7 @@ class Daemon {
|
|
|
390
506
|
workingDir: a.path || null,
|
|
391
507
|
apiKeyMasked: apiKey ? maskApiKey(apiKey) : null,
|
|
392
508
|
baseUrlHost: endpointHost(this._configuredBaseUrl(a, typeEnv)),
|
|
509
|
+
cliModels: listed && listed.models && this._onCliSignIn(a) ? listed.models : null,
|
|
393
510
|
probe: this._probes[a.name] || null,
|
|
394
511
|
});
|
|
395
512
|
}
|
|
@@ -635,7 +752,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
635
752
|
// pass and no /v1/token/resolve round-trip to fail (the outage class
|
|
636
753
|
// that used to break remote installs).
|
|
637
754
|
const r2 = await this._runAgn([
|
|
638
|
-
'connect', name, '--workspace', n.workspace_slug || n.workspace_id,
|
|
755
|
+
'connect', name, '--workspace', n.workspace_slug || n.workspace_id, '--connection', n.node_id,
|
|
639
756
|
]);
|
|
640
757
|
if (r2.code !== 0) throw new Error(r2.stderr || r2.stdout || 'connect failed');
|
|
641
758
|
ok = true;
|
|
@@ -644,6 +761,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
644
761
|
// full timeout — never hold the create result hostage). The outcome
|
|
645
762
|
// reaches the workspace via the heartbeat's agents[].probe.
|
|
646
763
|
this._probeAgent(name).then(() => this._nodeHeartbeat()).catch(() => {});
|
|
764
|
+
this._askCliModels(name);
|
|
647
765
|
} else if (action === 'start_agent') {
|
|
648
766
|
const r = await this._runAgn(['start', name]);
|
|
649
767
|
ok = r.code === 0;
|
|
@@ -674,7 +792,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
674
792
|
if (rc.code !== 0) throw new Error(rc.stderr || rc.stdout || 'recreate failed');
|
|
675
793
|
this._saveNodeAgentEnv(name, type, args);
|
|
676
794
|
const rc2 = await this._runAgn([
|
|
677
|
-
'connect', name, '--workspace', n.workspace_slug || n.workspace_id,
|
|
795
|
+
'connect', name, '--workspace', n.workspace_slug || n.workspace_id, '--connection', n.node_id,
|
|
678
796
|
]);
|
|
679
797
|
if (rc2.code !== 0) throw new Error(rc2.stderr || rc2.stdout || 'connect failed');
|
|
680
798
|
} else {
|
|
@@ -690,6 +808,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
690
808
|
// A reconfigure changes credentials/model — re-verify in the
|
|
691
809
|
// background so the workspace sees the new state without a manual test.
|
|
692
810
|
this._probeAgent(name).then(() => this._nodeHeartbeat()).catch(() => {});
|
|
811
|
+
this._askCliModels(name);
|
|
693
812
|
} else if (action === 'probe_agent') {
|
|
694
813
|
// Smoke-test on demand ("Re-test" in the workspace). Probes belong to
|
|
695
814
|
// configured agents (by name); the result rides agents[].probe.
|
|
@@ -878,6 +997,19 @@ async _runNodeCommand(n, cmd) {
|
|
|
878
997
|
3600 * 1000,
|
|
879
998
|
);
|
|
880
999
|
|
|
1000
|
+
// What each signed-in agent's account is offered, for the workspace's
|
|
1001
|
+
// model picker. Asking costs a CLI start and no model call. The pass is
|
|
1002
|
+
// frequent so an agent added on the device is picked up soon; it only
|
|
1003
|
+
// asks for a list that is missing or an hour old.
|
|
1004
|
+
this._cliModelsStartupTimer = setTimeout(
|
|
1005
|
+
() => this._refreshCliModels().catch(() => {}),
|
|
1006
|
+
20 * 1000,
|
|
1007
|
+
);
|
|
1008
|
+
this._cliModelsInterval = setInterval(
|
|
1009
|
+
() => this._refreshCliModels().catch(() => {}),
|
|
1010
|
+
10 * 60 * 1000,
|
|
1011
|
+
);
|
|
1012
|
+
|
|
881
1013
|
// Watch config file for hot-reload
|
|
882
1014
|
this._watchConfig();
|
|
883
1015
|
|
|
@@ -909,6 +1041,8 @@ async _runNodeCommand(n, cmd) {
|
|
|
909
1041
|
if (this._displayNameInterval) clearInterval(this._displayNameInterval);
|
|
910
1042
|
if (this._probeStartupTimer) clearTimeout(this._probeStartupTimer);
|
|
911
1043
|
if (this._probeInterval) clearInterval(this._probeInterval);
|
|
1044
|
+
if (this._cliModelsStartupTimer) clearTimeout(this._cliModelsStartupTimer);
|
|
1045
|
+
if (this._cliModelsInterval) clearInterval(this._cliModelsInterval);
|
|
912
1046
|
if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
|
|
913
1047
|
|
|
914
1048
|
// Kill all child processes
|
|
@@ -1164,15 +1298,18 @@ async _runNodeCommand(n, cmd) {
|
|
|
1164
1298
|
* fallback. Fields from the network entry win for display (name), the
|
|
1165
1299
|
* pairing wins for credentials (token/endpoint).
|
|
1166
1300
|
*/
|
|
1167
|
-
_resolveAgentNetwork(ref) {
|
|
1301
|
+
_resolveAgentNetwork(ref, nodeId) {
|
|
1168
1302
|
const network =
|
|
1169
1303
|
this.config.getNetworks().find((n) => n.slug === ref || n.id === ref) ||
|
|
1170
1304
|
null;
|
|
1171
1305
|
let pairing = null;
|
|
1172
1306
|
try {
|
|
1173
|
-
|
|
1174
|
-
.
|
|
1175
|
-
|
|
1307
|
+
const candidates = require('./node-config').listPairings()
|
|
1308
|
+
.filter((p) => p.workspace_slug === ref || p.workspace_id === ref);
|
|
1309
|
+
pairing = candidates.find((p) => !nodeId || p.node_id === nodeId) || null;
|
|
1310
|
+
if ((nodeId && !pairing) || (!nodeId && candidates.length > 1)) {
|
|
1311
|
+
return { ...(network || {}), token: null };
|
|
1312
|
+
}
|
|
1176
1313
|
} catch {}
|
|
1177
1314
|
if (!network && !pairing) return null;
|
|
1178
1315
|
return {
|
|
@@ -1216,7 +1353,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1216
1353
|
// Workspace-connected agents use the adapter loop (poll + CLI per message).
|
|
1217
1354
|
// Local-only agents use the spawn loop (long-running child process).
|
|
1218
1355
|
const network = agentCfg.network
|
|
1219
|
-
? this._resolveAgentNetwork(agentCfg.network)
|
|
1356
|
+
? this._resolveAgentNetwork(agentCfg.network, agentCfg.node_id)
|
|
1220
1357
|
: null;
|
|
1221
1358
|
|
|
1222
1359
|
if (network && !network.token) {
|
|
@@ -1235,6 +1372,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
1235
1372
|
// Remember which credential this adapter runs with, so the rotation
|
|
1236
1373
|
// watch can restart it when a re-pair hands the workspace a new token.
|
|
1237
1374
|
info.networkRef = agentCfg.network;
|
|
1375
|
+
info.connectionId = agentCfg.node_id;
|
|
1238
1376
|
info.credentialToken = network.token;
|
|
1239
1377
|
this._adapterLoop(name, agentCfg, info, network);
|
|
1240
1378
|
} else {
|
package/src/index.js
CHANGED
|
@@ -121,7 +121,7 @@ class AgentConnector {
|
|
|
121
121
|
if (!network || !network.id) return null;
|
|
122
122
|
const endpoint = network.endpoint || (this.workspace && this.workspace.endpoint);
|
|
123
123
|
const { WorkspaceClient } = require('./workspace-client');
|
|
124
|
-
return { client: new WorkspaceClient(endpoint), network };
|
|
124
|
+
return { client: new WorkspaceClient(endpoint, { agentName }), network };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
@@ -431,6 +431,15 @@ class AgentConnector {
|
|
|
431
431
|
const { probeAgentType } = require('./probe');
|
|
432
432
|
return probeAgentType(this, agentType, opts);
|
|
433
433
|
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Ask an agent type's own CLI which models the account it is signed in to
|
|
437
|
+
* can run. See cli-models.js.
|
|
438
|
+
*/
|
|
439
|
+
async listCliModels(agentType, opts) {
|
|
440
|
+
const { listCliModels } = require('./cli-models');
|
|
441
|
+
return listCliModels(this, agentType, opts);
|
|
442
|
+
}
|
|
434
443
|
}
|
|
435
444
|
|
|
436
445
|
const adapters = require('./adapters');
|