@phnx-labs/agents-cli 1.20.63 → 1.20.65
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/CHANGELOG.md +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
package/dist/commands/feed.js
CHANGED
|
@@ -8,9 +8,20 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
8
8
|
import { loadPolicy, applyPolicyToBlock, isPhoneUrgent } from '../lib/feed-policy.js';
|
|
9
9
|
import { notifyUrgentBlock } from '../lib/notify.js';
|
|
10
10
|
import { gcMailbox } from '../lib/mailbox-gc.js';
|
|
11
|
+
import { isValidMailboxId } from '../lib/mailbox.js';
|
|
11
12
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
12
13
|
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
14
|
+
import { GLYPH, masthead } from '../lib/comms-render.js';
|
|
13
15
|
export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
|
|
16
|
+
/** Right-hand masthead summary: `N blocks · M agents`. */
|
|
17
|
+
export function formatFeedMastheadRight(blocks) {
|
|
18
|
+
const agents = new Set(blocks.map((b) => b.mailboxId)).size;
|
|
19
|
+
return `${blocks.length} block${blocks.length === 1 ? '' : 's'} · ${agents} agent${agents === 1 ? '' : 's'}`;
|
|
20
|
+
}
|
|
21
|
+
/** Reply hint matching the shared fleet-comms reply line. */
|
|
22
|
+
export function formatFeedReplyHint(mailboxId) {
|
|
23
|
+
return `↳ ag message ${mailboxId} "…"`;
|
|
24
|
+
}
|
|
14
25
|
export function parseRemoteFeed(stdout, machine) {
|
|
15
26
|
let parsed;
|
|
16
27
|
try {
|
|
@@ -28,6 +39,11 @@ export function parseRemoteFeed(stdout, machine) {
|
|
|
28
39
|
const block = item;
|
|
29
40
|
if (!block.blockId || !block.sessionId || !block.mailboxId || !block.questions?.length)
|
|
30
41
|
continue;
|
|
42
|
+
// A crafted mailboxId (path separators, `.`/`..`) would throw inside
|
|
43
|
+
// mailboxDir() when policy runs against the block, aborting the whole
|
|
44
|
+
// dispatch loop — drop it here so a malicious peer can't smuggle one in.
|
|
45
|
+
if (!isValidMailboxId(block.mailboxId))
|
|
46
|
+
continue;
|
|
31
47
|
blocks.push({ ...block, host: machine });
|
|
32
48
|
}
|
|
33
49
|
return blocks;
|
|
@@ -60,7 +76,13 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
60
76
|
const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
|
|
61
77
|
const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
|
|
62
78
|
const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
|
|
63
|
-
|
|
79
|
+
// Shared fleet-comms glyphs: ▲ open ask, ✓ answered (see comms-render GLYPH).
|
|
80
|
+
const marker = b.answer
|
|
81
|
+
? chalk.green(GLYPH.delivered)
|
|
82
|
+
: !b.parkedAt
|
|
83
|
+
? chalk.yellow(GLYPH.ask)
|
|
84
|
+
: ' ';
|
|
85
|
+
console.log(`${indent}${marker} ${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost}`.trimEnd());
|
|
64
86
|
for (const question of b.questions) {
|
|
65
87
|
const header = question.header ? chalk.gray(`[${question.header}] `) : '';
|
|
66
88
|
console.log(`${indent} ${header}${question.text}`);
|
|
@@ -77,7 +99,7 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
77
99
|
console.log(`${indent} ${chalk.gray(meta)}`);
|
|
78
100
|
}
|
|
79
101
|
if (b.answer) {
|
|
80
|
-
const verified = b.answer.verified ? chalk.green(
|
|
102
|
+
const verified = b.answer.verified ? chalk.green(GLYPH.delivered) : chalk.yellow('?');
|
|
81
103
|
const who = b.answer.answeredFrom + (b.answer.answeredBy ? ` (${b.answer.answeredBy})` : '');
|
|
82
104
|
console.log(`${indent} ${chalk.green('answered')} by ${who} ${verified}`);
|
|
83
105
|
}
|
|
@@ -98,7 +120,7 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
98
120
|
console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
|
|
99
121
|
}
|
|
100
122
|
if (!b.answer && !b.parkedAt) {
|
|
101
|
-
console.log(`${indent} ${chalk.dim(
|
|
123
|
+
console.log(`${indent} ${chalk.dim(formatFeedReplyHint(b.mailboxId))}`);
|
|
102
124
|
}
|
|
103
125
|
console.log();
|
|
104
126
|
}
|
|
@@ -224,20 +246,28 @@ export function registerFeedCommand(program) {
|
|
|
224
246
|
const policy = loadPolicy();
|
|
225
247
|
const now = new Date();
|
|
226
248
|
for (const b of blocks) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
recordNotified(b.blockId);
|
|
235
|
-
console.log(`${chalk.green('notified')} ${b.blockId}`);
|
|
249
|
+
// Wrap per-block policy so one malformed block (e.g. a crafted
|
|
250
|
+
// mailboxId that throws in mailboxDir) can't abort the whole loop and
|
|
251
|
+
// strand every remaining block's dispatch.
|
|
252
|
+
try {
|
|
253
|
+
const result = applyPolicyToBlock(b, policy, now);
|
|
254
|
+
if (result.action !== 'none') {
|
|
255
|
+
console.log(`${chalk.yellow('policy')} ${b.blockId}: ${result.action}`);
|
|
236
256
|
}
|
|
237
|
-
|
|
238
|
-
|
|
257
|
+
if (isPhoneUrgent(b, policy)) {
|
|
258
|
+
const notifyResult = await notifyUrgentBlock(b, { dryRun: opts.json });
|
|
259
|
+
if (notifyResult.ok && !notifyResult.skipped) {
|
|
260
|
+
recordNotified(b.blockId);
|
|
261
|
+
console.log(`${chalk.green('notified')} ${b.blockId}`);
|
|
262
|
+
}
|
|
263
|
+
else if (notifyResult.error) {
|
|
264
|
+
console.error(chalk.yellow(`Notification failed for ${b.blockId}: ${notifyResult.error}`));
|
|
265
|
+
}
|
|
239
266
|
}
|
|
240
267
|
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
console.error(chalk.yellow(`Skipped block ${b.blockId}: ${err.message}`));
|
|
270
|
+
}
|
|
241
271
|
}
|
|
242
272
|
}
|
|
243
273
|
for (const warning of setupWarnings) {
|
|
@@ -257,17 +287,20 @@ export function registerFeedCommand(program) {
|
|
|
257
287
|
console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
|
|
258
288
|
return;
|
|
259
289
|
}
|
|
290
|
+
// Shared fleet-comms masthead (same family as `agents mailboxes`).
|
|
291
|
+
console.log(masthead({
|
|
292
|
+
title: 'they need you',
|
|
293
|
+
accent: 'amber',
|
|
294
|
+
host: self,
|
|
295
|
+
right: formatFeedMastheadRight(blocks),
|
|
296
|
+
}));
|
|
297
|
+
console.log();
|
|
260
298
|
if (opts.flat) {
|
|
261
|
-
console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
|
|
262
299
|
for (const b of blocks)
|
|
263
300
|
renderBlock(b, self);
|
|
264
301
|
return;
|
|
265
302
|
}
|
|
266
303
|
const groups = groupBlocksByOutcome(blocks);
|
|
267
|
-
const openOutcomes = groups.filter((g) => g.counts.open > 0).length;
|
|
268
|
-
console.log(chalk.bold(`${groups.length} outcome${groups.length === 1 ? '' : 's'} · ${blocks.length} block${blocks.length === 1 ? '' : 's'}` +
|
|
269
|
-
(openOutcomes > 0 ? ` · ${openOutcomes} need you` : '') +
|
|
270
|
-
':\n'));
|
|
271
304
|
for (const g of groups)
|
|
272
305
|
renderOutcomeGroup(g, self);
|
|
273
306
|
});
|
package/dist/commands/hosts.js
CHANGED
|
@@ -11,6 +11,7 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
|
|
|
11
11
|
import { checkbox, confirm } from '@inquirer/prompts';
|
|
12
12
|
import { assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
13
13
|
import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js';
|
|
14
|
+
import { getDevice } from '../lib/devices/registry.js';
|
|
14
15
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
15
16
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
16
17
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
@@ -93,7 +94,7 @@ async function doAdd(name, target, opts) {
|
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
let spec;
|
|
96
|
-
let sshTarget;
|
|
97
|
+
let sshTarget = '';
|
|
97
98
|
if (target) {
|
|
98
99
|
assertValidSshTarget(target);
|
|
99
100
|
const { address, user } = parseTarget(target);
|
|
@@ -105,9 +106,36 @@ async function doAdd(name, target, opts) {
|
|
|
105
106
|
sshTarget = name;
|
|
106
107
|
}
|
|
107
108
|
else {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// A registered device enrolls with no target — connection details come from
|
|
110
|
+
// the device profile. The main reason to enroll one at all is the overlay
|
|
111
|
+
// metadata (capability tags); plain dispatch already works via the devices
|
|
112
|
+
// provider.
|
|
113
|
+
const device = await getDevice(name);
|
|
114
|
+
if (device && device.auth.method !== 'password') {
|
|
115
|
+
const address = device.address.dnsName ?? device.address.ip;
|
|
116
|
+
if (address) {
|
|
117
|
+
spec = {
|
|
118
|
+
name,
|
|
119
|
+
provider: 'local',
|
|
120
|
+
source: 'inline',
|
|
121
|
+
address,
|
|
122
|
+
user: device.user,
|
|
123
|
+
caps: opts.cap,
|
|
124
|
+
os: opts.os ?? (device.platform !== 'unknown' ? device.platform : undefined),
|
|
125
|
+
};
|
|
126
|
+
sshTarget = device.user ? `${device.user}@${address}` : address;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!spec) {
|
|
130
|
+
if (device?.auth.method === 'password') {
|
|
131
|
+
console.log(chalk.red(`Device "${name}" uses password auth — dispatch needs key auth. Switch it first: agents devices set ${name} --auth key`));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
console.log(chalk.red(`"${name}" is not in ~/.ssh/config or the devices registry. Pass a target: agents hosts add ${name} <user@host>`));
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
111
139
|
}
|
|
112
140
|
const probe = probeHost(sshTarget);
|
|
113
141
|
if (!spec.os && probe.os)
|
|
@@ -133,8 +161,12 @@ async function doList(json) {
|
|
|
133
161
|
// Cap TARGET at its column so a long user@host can't shove CAPS out of alignment.
|
|
134
162
|
const tgtW = stringWidth(tgt);
|
|
135
163
|
const tgtCol = tgtW > 28 ? truncateToWidth(tgt, 28) : tgt + ' '.repeat(28 - tgtW);
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
// Devices surface their registry as SOURCE (they're synced, not enrolled here).
|
|
165
|
+
const source = h.provider === 'devices' ? 'devices' : h.source;
|
|
166
|
+
const mark = h.dispatchable === false
|
|
167
|
+
? chalk.yellow(' ·password-auth (no dispatch)')
|
|
168
|
+
: h.enrolled ? '' : chalk.gray(' ·available');
|
|
169
|
+
console.log(h.name.padEnd(20) + source.padEnd(13) + tgtCol + (h.caps?.join(',') ?? '') + mark);
|
|
138
170
|
}
|
|
139
171
|
}
|
|
140
172
|
async function doCheck(name) {
|
|
@@ -163,6 +195,12 @@ async function doRemove(name) {
|
|
|
163
195
|
console.log(chalk.yellow(`"${name}" is not enrolled (nothing to remove).`));
|
|
164
196
|
return;
|
|
165
197
|
}
|
|
198
|
+
if (host.provider === 'devices') {
|
|
199
|
+
// The devices registry owns this entry — hosts remove would silently no-op.
|
|
200
|
+
console.log(chalk.yellow(`"${name}" comes from the devices registry. Remove it there: agents devices rm ${name}`));
|
|
201
|
+
process.exitCode = 1;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
166
204
|
await getProvider('local').remove(name);
|
|
167
205
|
console.log(chalk.green(`Removed ${name}`));
|
|
168
206
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents lease` — manage the disposable cloud boxes used by `agents run --lease`.
|
|
3
|
+
*
|
|
4
|
+
* Today: `agents lease gc`, which stops expired + idle "orphan" boxes that are
|
|
5
|
+
* holding a provider's server quota (the cause of the `server_limit` 403 a new
|
|
6
|
+
* lease hits). Reaping is conservative: only boxes whose lease has expired AND
|
|
7
|
+
* that have been untouched for a safety window are eligible (see `isReapSafe`),
|
|
8
|
+
* so a box a concurrent run just reused is never stopped.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
12
|
+
export declare function validateHetznerToken(token: string, fetchImpl?: typeof fetch): Promise<'valid' | 'invalid' | 'unreachable'>;
|
|
13
|
+
/**
|
|
14
|
+
* One-time credential setup for `agents run --lease` (Hetzner today). Opens the
|
|
15
|
+
* token page, collects a token, validates it against the live API, stores it in
|
|
16
|
+
* the keychain bundle `hetzner.com`, and persists it as the default lease bundle
|
|
17
|
+
* (so `--lease` needs no env var or flag afterward). Returns true on success.
|
|
18
|
+
* Never throws for expected outcomes (non-interactive, cancel, repeated failure).
|
|
19
|
+
*/
|
|
20
|
+
export declare function runLeaseSetup(opts?: {
|
|
21
|
+
provider?: string;
|
|
22
|
+
}): Promise<boolean>;
|
|
23
|
+
export declare function registerLeaseCommand(program: Command): void;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents lease` — manage the disposable cloud boxes used by `agents run --lease`.
|
|
3
|
+
*
|
|
4
|
+
* Today: `agents lease gc`, which stops expired + idle "orphan" boxes that are
|
|
5
|
+
* holding a provider's server quota (the cause of the `server_limit` 403 a new
|
|
6
|
+
* lease hits). Reaping is conservative: only boxes whose lease has expired AND
|
|
7
|
+
* that have been untouched for a safety window are eligible (see `isReapSafe`),
|
|
8
|
+
* so a box a concurrent run just reused is never stopped.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { crabboxList, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
|
|
13
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
14
|
+
import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
|
|
15
|
+
import { secretsKeychainItem } from '../lib/secrets/index.js';
|
|
16
|
+
function fmtIdle(box) {
|
|
17
|
+
if (box.lastTouchedAt === null)
|
|
18
|
+
return 'idle ?';
|
|
19
|
+
const iso = new Date(box.lastTouchedAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
|
|
20
|
+
return `idle since ${iso}Z`;
|
|
21
|
+
}
|
|
22
|
+
const HETZNER_BUNDLE = 'hetzner.com';
|
|
23
|
+
const HCLOUD_KEY = 'HCLOUD_TOKEN';
|
|
24
|
+
const HETZNER_CONSOLE_URL = 'https://console.hetzner.cloud/';
|
|
25
|
+
/** Best-effort: open a URL in the user's default browser. Never throws. */
|
|
26
|
+
function openUrl(url) {
|
|
27
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
28
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
29
|
+
try {
|
|
30
|
+
const p = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
31
|
+
p.on('error', () => { });
|
|
32
|
+
p.unref();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* best-effort */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
39
|
+
export async function validateHetznerToken(token, fetchImpl = fetch) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetchImpl('https://api.hetzner.cloud/v1/servers?per_page=1', {
|
|
42
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
43
|
+
});
|
|
44
|
+
if (res.ok)
|
|
45
|
+
return 'valid';
|
|
46
|
+
if (res.status === 401 || res.status === 403)
|
|
47
|
+
return 'invalid';
|
|
48
|
+
return 'unreachable';
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return 'unreachable';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* One-time credential setup for `agents run --lease` (Hetzner today). Opens the
|
|
56
|
+
* token page, collects a token, validates it against the live API, stores it in
|
|
57
|
+
* the keychain bundle `hetzner.com`, and persists it as the default lease bundle
|
|
58
|
+
* (so `--lease` needs no env var or flag afterward). Returns true on success.
|
|
59
|
+
* Never throws for expected outcomes (non-interactive, cancel, repeated failure).
|
|
60
|
+
*/
|
|
61
|
+
export async function runLeaseSetup(opts = {}) {
|
|
62
|
+
const provider = opts.provider ?? 'hetzner';
|
|
63
|
+
if (provider !== 'hetzner') {
|
|
64
|
+
console.error(chalk.yellow(`lease setup: only 'hetzner' is supported today (got '${provider}').`));
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (!isInteractiveTerminal()) {
|
|
68
|
+
console.error(chalk.red('lease setup needs an interactive terminal. For CI/headless, set AGENTS_LEASE_SECRETS_BUNDLE ' +
|
|
69
|
+
'or store HCLOUD_TOKEN in a keychain bundle.'));
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
console.error(chalk.bold('\nSet up leasing (Hetzner) — one time (~30s):'));
|
|
73
|
+
console.error(chalk.dim('Opening the Hetzner console. Create/select a project, then Security → API Tokens →'));
|
|
74
|
+
console.error(chalk.dim('Generate a token with Read & Write permission, and copy it.\n'));
|
|
75
|
+
openUrl(HETZNER_CONSOLE_URL);
|
|
76
|
+
const { password } = await import('@inquirer/prompts');
|
|
77
|
+
const ora = (await import('ora')).default;
|
|
78
|
+
try {
|
|
79
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
80
|
+
const token = (await password({ message: 'Paste your Hetzner API token:', mask: true })).trim();
|
|
81
|
+
if (!token) {
|
|
82
|
+
console.error(chalk.yellow('No token entered.'));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const spinner = ora('Validating token against the Hetzner API…').start();
|
|
86
|
+
const result = await validateHetznerToken(token);
|
|
87
|
+
if (result === 'invalid') {
|
|
88
|
+
spinner.fail('Token rejected by Hetzner (401/403). Try again.');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (result === 'unreachable')
|
|
92
|
+
spinner.warn('Could not reach the Hetzner API to validate — storing anyway.');
|
|
93
|
+
else
|
|
94
|
+
spinner.succeed('Token valid — Hetzner API reachable.');
|
|
95
|
+
// Store into the `hetzner.com` keychain bundle (mirrors writeSyncBundle).
|
|
96
|
+
const bundle = bundleExists(HETZNER_BUNDLE)
|
|
97
|
+
? readBundle(HETZNER_BUNDLE)
|
|
98
|
+
: { name: HETZNER_BUNDLE, description: 'Hetzner Cloud API token for crabbox leases', vars: {} };
|
|
99
|
+
const store = bundleItemStore(bundle.backend);
|
|
100
|
+
store.set(secretsKeychainItem(HETZNER_BUNDLE, HCLOUD_KEY), token);
|
|
101
|
+
bundle.vars[HCLOUD_KEY] = keychainRef(HCLOUD_KEY);
|
|
102
|
+
writeBundle(bundle);
|
|
103
|
+
setLeaseSecretsBundle(HETZNER_BUNDLE);
|
|
104
|
+
console.error(chalk.green(`\n✔ Stored in keychain bundle '${HETZNER_BUNDLE}' and set as the default lease provider.`));
|
|
105
|
+
console.error(chalk.dim(' Run `agents run <agent> "…" --lease` — no env var, no flag needed.'));
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
console.error(chalk.yellow('lease setup: no valid token after 3 attempts — aborted.'));
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
if (isPromptCancelled(e)) {
|
|
113
|
+
console.error(chalk.yellow('lease setup cancelled.'));
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export function registerLeaseCommand(program) {
|
|
120
|
+
const lease = program
|
|
121
|
+
.command('lease')
|
|
122
|
+
.description('Manage the disposable cloud boxes used by `agents run --lease`.');
|
|
123
|
+
lease
|
|
124
|
+
.command('setup')
|
|
125
|
+
.description('One-time credential setup so `agents run --lease` works with no env var or flag.')
|
|
126
|
+
.option('--provider <name>', 'Cloud provider (only hetzner today)', 'hetzner')
|
|
127
|
+
.action(async (opts) => {
|
|
128
|
+
const ok = await runLeaseSetup({ provider: opts.provider });
|
|
129
|
+
process.exit(ok ? 0 : 1);
|
|
130
|
+
});
|
|
131
|
+
lease
|
|
132
|
+
.command('gc')
|
|
133
|
+
.description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
|
|
134
|
+
.option('--dry-run', 'List reap-safe orphan boxes without stopping any', false)
|
|
135
|
+
.option('--yes', 'Stop them without the interactive confirm', false)
|
|
136
|
+
.option('--json', 'Output JSON', false)
|
|
137
|
+
.action(async (opts) => {
|
|
138
|
+
const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
|
|
139
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
140
|
+
let candidates;
|
|
141
|
+
try {
|
|
142
|
+
candidates = reapSafeOrphans(crabboxList(boxOpts), nowSecs);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
console.error(chalk.red(`lease gc: ${e.message}`));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (candidates.length === 0) {
|
|
150
|
+
if (opts.json)
|
|
151
|
+
console.log(JSON.stringify({ candidates: [], reaped: [] }));
|
|
152
|
+
else
|
|
153
|
+
console.error(chalk.gray('No reap-safe orphan boxes — nothing to collect.'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!opts.json) {
|
|
157
|
+
console.error(chalk.bold(`${candidates.length} reap-safe orphan box(es):`));
|
|
158
|
+
for (const b of candidates) {
|
|
159
|
+
console.error(` ${chalk.cyan(b.slug)} ${chalk.dim(`(${b.class ?? '?'}, ${fmtIdle(b)})`)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (opts.dryRun) {
|
|
163
|
+
if (opts.json)
|
|
164
|
+
console.log(JSON.stringify({ candidates, reaped: [] }, null, 2));
|
|
165
|
+
else
|
|
166
|
+
console.error(chalk.gray('\n--dry-run: nothing stopped. Re-run with --yes to stop them.'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Destructive: stopping boxes the agent did not create needs an explicit yes.
|
|
170
|
+
if (!opts.yes) {
|
|
171
|
+
const { isInteractiveTerminal, isPromptCancelled } = await import('./utils.js');
|
|
172
|
+
if (!isInteractiveTerminal()) {
|
|
173
|
+
console.error(chalk.yellow('Refusing to stop boxes without --yes in a non-interactive shell.'));
|
|
174
|
+
process.exit(1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
179
|
+
const ok = await confirm({ message: `Stop these ${candidates.length} box(es)?`, default: false });
|
|
180
|
+
if (!ok) {
|
|
181
|
+
console.error(chalk.yellow('Aborted — no boxes stopped.'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (isPromptCancelled(e)) {
|
|
187
|
+
console.error(chalk.yellow('Aborted — no boxes stopped.'));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
throw e;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Re-list at stop time (freshness re-check) so a box touched since the
|
|
194
|
+
// preview is not stopped out from under an active run.
|
|
195
|
+
const { candidates: stopped, reaped } = reapOrphans({ ...boxOpts, nowSecs });
|
|
196
|
+
if (opts.json)
|
|
197
|
+
console.log(JSON.stringify({ candidates: stopped, reaped }, null, 2));
|
|
198
|
+
else
|
|
199
|
+
console.error(chalk.green(`Stopped ${reaped.length}/${stopped.length} box(es): ${reaped.join(', ') || '(none)'}`));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents mailboxes` — fleet comms: a read-only window onto the agent mailbox spool.
|
|
3
|
+
*
|
|
4
|
+
* The mailbox spool (`~/.agents/.history/mailbox/<id>/{inbox,processing,consumed}`)
|
|
5
|
+
* is the transport under `agents message` / `agents feed` / `agents teams message`:
|
|
6
|
+
* one box per logical agent (session UUID / teams agentId / loop runId). This
|
|
7
|
+
* command surfaces which boxes exist, how much mail each holds, the messages that
|
|
8
|
+
* flowed BETWEEN agents (including already-consumed ones), and — with `--watch` —
|
|
9
|
+
* the live stream as it happens. Rendering rides the shared comms engine
|
|
10
|
+
* (`lib/comms-render.ts`): masthead, sparkline, aggregate, graphEdges.
|
|
11
|
+
*
|
|
12
|
+
* agents mailboxes overview: masthead + 24h sparkline + boxes + recent log
|
|
13
|
+
* agents mailboxes <id> one box in full (inbox / processing / consumed)
|
|
14
|
+
* agents mailboxes --watch live tail of cross-box traffic until ⌃C
|
|
15
|
+
* agents mailboxes --between <a> <b> one relationship as a thread, either direction
|
|
16
|
+
* agents mailboxes --graph who-talks-to-whom adjacency, busiest first
|
|
17
|
+
* --from/--to/--since filter the overview log and the --watch stream
|
|
18
|
+
*/
|
|
19
|
+
import type { Command } from 'commander';
|
|
20
|
+
export declare function registerMailboxesCommand(program: Command): void;
|