@openagents-org/agent-launcher 0.2.192 → 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/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 node connect <pairing-code>');
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 node connect <code>`, then `agn connect <agent> --workspace <slug>`.');
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 node connect <code>`), or connect with a token.');
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 node connect <pairing-code>');
581
- print('Get a pairing code from your workspace: Connect a Node.');
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.workspace_id === res.workspaceId,
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(`Node connected to workspace '${res.workspaceName}' (${res.workspaceSlug})`);
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 node connected. Run: agn node connect <pairing-code>');
693
+ print('No device connected. Run: agn device connect <pairing-code>');
676
694
  return;
677
695
  }
678
- print(`Node connected to ${pairings.length} workspace(s):`);
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 node <connect|status> [pairing-code]');
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
- node: () => cmdNode(connector, flags, positional),
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
@@ -110,6 +110,7 @@ class Config {
110
110
  agent.network = networkSlug;
111
111
  } else {
112
112
  delete agent.network;
113
+ delete agent.node_id;
113
114
  }
114
115
  this.save(config);
115
116
  }
@@ -269,9 +270,18 @@ class Config {
269
270
  const { keptLines, removed } = filterLogsByTimeRange(allLines, start, end);
270
271
 
271
272
  const nextContent = keptLines.join('\n') + (hasTrailingNewline && keptLines.length > 0 ? '\n' : '');
272
- const tempFile = `${this.logFile}.tmp`;
273
- fs.writeFileSync(tempFile, nextContent, 'utf-8');
274
- fs.renameSync(tempFile, this.logFile);
273
+ // Rewrite in place, not write-temp + rename: the running daemon's stdout
274
+ // and stderr are append-mode handles to daemon.log. A rename leaves them
275
+ // writing into the replaced file (and fails outright on Windows while they
276
+ // are open); truncating in place keeps them appending to daemon.log.
277
+ const nextBytes = Buffer.from(nextContent, 'utf-8');
278
+ const fd = fs.openSync(this.logFile, 'r+');
279
+ try {
280
+ if (nextBytes.length > 0) fs.writeSync(fd, nextBytes, 0, nextBytes.length, 0);
281
+ fs.ftruncateSync(fd, nextBytes.length);
282
+ } finally {
283
+ fs.closeSync(fd);
284
+ }
275
285
 
276
286
  return { removed, remaining: keptLines.length };
277
287
  }