@phnx-labs/agents-cli 1.20.71 → 1.20.73
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 +325 -1
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/commands.js +2 -0
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +39 -10
- package/dist/commands/mcp.js +2 -0
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +8 -0
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +26 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/skills.js +2 -0
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/status.js +8 -2
- package/dist/commands/subagents.js +3 -1
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/uninstall.d.ts +11 -0
- package/dist/commands/uninstall.js +158 -0
- package/dist/commands/utils.d.ts +29 -0
- package/dist/commands/utils.js +24 -0
- package/dist/commands/versions.js +120 -11
- package/dist/index.js +83 -12
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.js +16 -5
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +14 -5
- package/dist/lib/daemon.js +52 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/logs.d.ts +12 -0
- package/dist/lib/hosts/logs.js +18 -0
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +291 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +36 -0
- package/dist/lib/shims.js +120 -16
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +73 -0
- package/dist/lib/uninstall.d.ts +84 -0
- package/dist/lib/uninstall.js +347 -0
- package/dist/lib/usage.d.ts +13 -1
- package/dist/lib/usage.js +32 -14
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +27 -0
- package/dist/lib/versions.js +248 -79
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/commands/feed.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { ensureFeedPublishHook, listBlocks, recordNotified } from '../lib/feed.js';
|
|
2
|
+
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified } from '../lib/feed.js';
|
|
3
|
+
import { ensureActivityLogHook, readRecentActivity, formatActivityLine } from '../lib/activity.js';
|
|
3
4
|
import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
|
|
4
5
|
import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
|
|
5
6
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
@@ -12,6 +13,9 @@ import { isValidMailboxId } from '../lib/mailbox.js';
|
|
|
12
13
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
13
14
|
import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
|
|
14
15
|
import { GLYPH, masthead } from '../lib/comms-render.js';
|
|
16
|
+
import { discoverSessions } from '../lib/session/discover.js';
|
|
17
|
+
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
18
|
+
import { buildSessionSignals, rankFeedBlocks, synthesizeControlCards, } from '../lib/feed-ranking.js';
|
|
15
19
|
export const FEED_NO_FANOUT_ENV = 'AGENTS_FEED_LOCAL';
|
|
16
20
|
/** Right-hand masthead summary: `N blocks · M agents`. */
|
|
17
21
|
export function formatFeedMastheadRight(blocks) {
|
|
@@ -58,6 +62,36 @@ export function mergeFeedBlocks(...groups) {
|
|
|
58
62
|
}
|
|
59
63
|
return [...byIdentity.values()].sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
|
|
60
64
|
}
|
|
65
|
+
function matchesControlTarget(signal, target) {
|
|
66
|
+
return [
|
|
67
|
+
signal.mailboxId,
|
|
68
|
+
signal.sessionId,
|
|
69
|
+
signal.cloudTaskId,
|
|
70
|
+
signal.pid !== undefined ? String(signal.pid) : undefined,
|
|
71
|
+
].some((value) => value === target);
|
|
72
|
+
}
|
|
73
|
+
export async function controlFeedSession(action, target, signals) {
|
|
74
|
+
const signal = signals.find((s) => matchesControlTarget(s, target));
|
|
75
|
+
if (!signal)
|
|
76
|
+
throw new Error(`No live feed session matches '${target}'.`);
|
|
77
|
+
if (signal.cloudProvider && signal.cloudTaskId) {
|
|
78
|
+
const provider = resolveProvider(signal.cloudProvider);
|
|
79
|
+
await provider.cancel(signal.cloudTaskId);
|
|
80
|
+
return `${action === 'pause' ? 'paused' : 'killed'} cloud task ${signal.cloudTaskId}`;
|
|
81
|
+
}
|
|
82
|
+
if (!signal.pid) {
|
|
83
|
+
throw new Error(`Session '${target}' has no local pid or cancellable cloud task.`);
|
|
84
|
+
}
|
|
85
|
+
if (action === 'pause') {
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
throw new Error('Pause is not supported for local Windows processes; use --kill.');
|
|
88
|
+
}
|
|
89
|
+
process.kill(signal.pid, 'SIGSTOP');
|
|
90
|
+
return `paused pid ${signal.pid}`;
|
|
91
|
+
}
|
|
92
|
+
process.kill(signal.pid, 'SIGTERM');
|
|
93
|
+
return `killed pid ${signal.pid}`;
|
|
94
|
+
}
|
|
61
95
|
function hostToken(host) {
|
|
62
96
|
return normalizeHost(host.split('@').pop() || host);
|
|
63
97
|
}
|
|
@@ -69,6 +103,16 @@ export function remoteFeedHostsToDial(hosts, self) {
|
|
|
69
103
|
return undefined;
|
|
70
104
|
return hosts.filter((host) => hostToken(host) !== self);
|
|
71
105
|
}
|
|
106
|
+
export function prepareLocalFeedBlocks(localBlocks, opts) {
|
|
107
|
+
const filter = filterBlocksForFeed(localBlocks, {
|
|
108
|
+
apply: opts.includeLocal && (!opts.all || opts.dispatch === true),
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
visible: opts.all ? localBlocks : filter.surfaced,
|
|
112
|
+
dispatch: filter.surfaced,
|
|
113
|
+
filter,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
72
116
|
function renderBlock(b, localHost, indent = '') {
|
|
73
117
|
const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
|
|
74
118
|
const runtime = chalk.gray(b.runtime);
|
|
@@ -76,13 +120,16 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
76
120
|
const cls = b.blockClass ? chalk.gray(`(${b.blockClass})`) : '';
|
|
77
121
|
const consequence = b.consequence && b.consequence !== 'normal' ? chalk.red(`[${b.consequence}]`) : '';
|
|
78
122
|
const cost = b.costOfDelay ? chalk.gray(`cost:${b.costOfDelay}`) : '';
|
|
123
|
+
const rank = b.delayRank ? chalk.gray(`rank:${Math.round(b.delayRank.score)}`) : '';
|
|
79
124
|
// Shared fleet-comms glyphs: ▲ open ask, ✓ answered (see comms-render GLYPH).
|
|
80
125
|
const marker = b.answer
|
|
81
126
|
? chalk.green(GLYPH.delivered)
|
|
82
|
-
:
|
|
83
|
-
? chalk.
|
|
84
|
-
:
|
|
85
|
-
|
|
127
|
+
: b.kind === 'control'
|
|
128
|
+
? chalk.red('!')
|
|
129
|
+
: !b.parkedAt
|
|
130
|
+
? chalk.yellow(GLYPH.ask)
|
|
131
|
+
: ' ';
|
|
132
|
+
console.log(`${indent}${marker} ${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age} ${cls} ${consequence} ${cost} ${rank}`.trimEnd());
|
|
86
133
|
for (const question of b.questions) {
|
|
87
134
|
const header = question.header ? chalk.gray(`[${question.header}] `) : '';
|
|
88
135
|
console.log(`${indent} ${header}${question.text}`);
|
|
@@ -119,7 +166,15 @@ function renderBlock(b, localHost, indent = '') {
|
|
|
119
166
|
if (b.notifiedAt) {
|
|
120
167
|
console.log(`${indent} ${chalk.dim('notified')} ${relTime(b.notifiedAt)}`);
|
|
121
168
|
}
|
|
122
|
-
if (
|
|
169
|
+
if (b.runaway) {
|
|
170
|
+
console.log(`${indent} ${chalk.red('runaway:')} ${b.runaway.reason}`);
|
|
171
|
+
console.log(`${indent} ${chalk.dim(`control: ag feed --pause ${b.mailboxId} · ag feed --kill ${b.mailboxId}`)}`);
|
|
172
|
+
}
|
|
173
|
+
if (b.needy) {
|
|
174
|
+
console.log(`${indent} ${chalk.yellow('needy:')} ${b.needy.askCountLastHour}/${b.needy.threshold} asks in the last hour`);
|
|
175
|
+
console.log(`${indent} ${chalk.dim(`inspect: ag sessions ${b.sessionId}`)}`);
|
|
176
|
+
}
|
|
177
|
+
if (!b.answer && !b.parkedAt && b.kind !== 'control') {
|
|
123
178
|
console.log(`${indent} ${chalk.dim(formatFeedReplyHint(b.mailboxId))}`);
|
|
124
179
|
}
|
|
125
180
|
console.log();
|
|
@@ -169,6 +224,8 @@ export function registerFeedCommand(program) {
|
|
|
169
224
|
.option('-H, --host <target...>', 'Scope to remote machine(s) over SSH; repeatable')
|
|
170
225
|
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
171
226
|
.option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
|
|
227
|
+
.option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
|
|
228
|
+
.option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task')
|
|
172
229
|
.action(async (opts) => {
|
|
173
230
|
if (opts.device?.length)
|
|
174
231
|
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
@@ -176,11 +233,16 @@ export function registerFeedCommand(program) {
|
|
|
176
233
|
const includeLocal = shouldIncludeLocalFeed(opts.host, self);
|
|
177
234
|
const setupWarnings = [];
|
|
178
235
|
if (includeLocal) {
|
|
236
|
+
// Feed and activity hooks are independent -- install both, and register
|
|
237
|
+
// the manifest as long as at least one wrote its entries (don't couple
|
|
238
|
+
// activity registration to the feed hook succeeding).
|
|
179
239
|
const hookInstall = ensureFeedPublishHook();
|
|
180
|
-
|
|
240
|
+
const activityInstall = ensureActivityLogHook();
|
|
241
|
+
if (hookInstall.error)
|
|
181
242
|
setupWarnings.push(hookInstall.error);
|
|
182
|
-
|
|
183
|
-
|
|
243
|
+
if (activityInstall.error)
|
|
244
|
+
setupWarnings.push(activityInstall.error);
|
|
245
|
+
if (!hookInstall.error || !activityInstall.error) {
|
|
184
246
|
const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
|
|
185
247
|
import('../lib/hooks.js'),
|
|
186
248
|
import('../lib/versions.js'),
|
|
@@ -199,6 +261,17 @@ export function registerFeedCommand(program) {
|
|
|
199
261
|
if (includeLocal) {
|
|
200
262
|
sessions = await getActiveSessions();
|
|
201
263
|
}
|
|
264
|
+
const sessionMetas = includeLocal && sessions.length > 0 ? await discoverSessions({ all: true, limit: 5000 }) : [];
|
|
265
|
+
const localSignals = buildSessionSignals(sessions, sessionMetas);
|
|
266
|
+
if (opts.pause || opts.kill) {
|
|
267
|
+
if (!includeLocal) {
|
|
268
|
+
throw new Error('Feed controls run on the local machine. Re-run against the target host with --local.');
|
|
269
|
+
}
|
|
270
|
+
const action = opts.pause ? 'pause' : 'kill';
|
|
271
|
+
const target = opts.pause ?? opts.kill ?? '';
|
|
272
|
+
console.log(await controlFeedSession(action, target, localSignals));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
202
275
|
if (opts.dispatch && includeLocal) {
|
|
203
276
|
// Liveness sweep: drop messages to dead agents and retire stale blocks
|
|
204
277
|
// before we render the feed.
|
|
@@ -208,8 +281,26 @@ export function registerFeedCommand(program) {
|
|
|
208
281
|
console.log(chalk.yellow(`gc: ${gcResult.messagesDroppedDead} dead messages, ${gcResult.blocksRemoved} stale blocks removed`));
|
|
209
282
|
}
|
|
210
283
|
}
|
|
211
|
-
|
|
212
|
-
|
|
284
|
+
let localBlocks = includeLocal
|
|
285
|
+
? [...listBlocks(), ...synthesizeControlCards(localSignals, listAskStats())]
|
|
286
|
+
: [];
|
|
287
|
+
// Fill missing ticket/PR/worktree from live session meta before local
|
|
288
|
+
// policy mutates the store, so outcome keys land even when the publish
|
|
289
|
+
// hook had no deliverable stamp.
|
|
290
|
+
if (sessions.length > 0) {
|
|
291
|
+
localBlocks = enrichBlocksFromSessions(localBlocks, sessionHintsFromActive(sessions));
|
|
292
|
+
}
|
|
293
|
+
// Stall suppression (RUSH-1477) must only mutate blocks owned by this
|
|
294
|
+
// machine. Remote peers run their own `feed --json`; never enqueue a
|
|
295
|
+
// policy answer into a local mailbox for a remote agent.
|
|
296
|
+
const preparedLocal = prepareLocalFeedBlocks(localBlocks, {
|
|
297
|
+
includeLocal,
|
|
298
|
+
all: opts.all,
|
|
299
|
+
dispatch: opts.dispatch,
|
|
300
|
+
});
|
|
301
|
+
const visibleLocalBlocks = preparedLocal.visible;
|
|
302
|
+
const dispatchBlocks = preparedLocal.dispatch;
|
|
303
|
+
let blocks = visibleLocalBlocks;
|
|
213
304
|
const forceLocal = opts.local === true || process.env[FEED_NO_FANOUT_ENV] === '1';
|
|
214
305
|
if (!forceLocal) {
|
|
215
306
|
const remoteHosts = remoteFeedHostsToDial(opts.host, self);
|
|
@@ -221,31 +312,18 @@ export function registerFeedCommand(program) {
|
|
|
221
312
|
hosts: remoteHosts,
|
|
222
313
|
parse: parseRemoteFeed,
|
|
223
314
|
});
|
|
224
|
-
blocks = mergeFeedBlocks(
|
|
315
|
+
blocks = mergeFeedBlocks(visibleLocalBlocks, remote.items);
|
|
225
316
|
}
|
|
226
317
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (sessions.length > 0) {
|
|
230
|
-
blocks = enrichBlocksFromSessions(blocks, sessionHintsFromActive(sessions));
|
|
231
|
-
}
|
|
232
|
-
// Stall suppression (RUSH-1477): auto-answer "should I…?" / "what's next?"
|
|
233
|
-
// so they never render. Applied on every feed read unless --all; --dispatch
|
|
234
|
-
// also applies so unattended ticks drain the stall backlog.
|
|
235
|
-
const filter = filterBlocksForFeed(blocks, {
|
|
236
|
-
apply: !opts.all || opts.dispatch === true,
|
|
237
|
-
});
|
|
238
|
-
if (!opts.all) {
|
|
239
|
-
blocks = filter.surfaced;
|
|
240
|
-
}
|
|
241
|
-
const digest = suppressionDigest(filter);
|
|
318
|
+
blocks = rankFeedBlocks(blocks, localSignals);
|
|
319
|
+
const digest = suppressionDigest(preparedLocal.filter);
|
|
242
320
|
if (digest && !opts.json) {
|
|
243
321
|
console.log(chalk.dim(digest));
|
|
244
322
|
}
|
|
245
323
|
if (opts.dispatch) {
|
|
246
324
|
const policy = loadPolicy();
|
|
247
325
|
const now = new Date();
|
|
248
|
-
for (const b of
|
|
326
|
+
for (const b of dispatchBlocks) {
|
|
249
327
|
// Wrap per-block policy so one malformed block (e.g. a crafted
|
|
250
328
|
// mailboxId that throws in mailboxDir) can't abort the whole loop and
|
|
251
329
|
// strand every remaining block's dispatch.
|
|
@@ -285,6 +363,8 @@ export function registerFeedCommand(program) {
|
|
|
285
363
|
}
|
|
286
364
|
if (blocks.length === 0) {
|
|
287
365
|
console.log(chalk.gray(digest ? 'No open blocks after stall suppression.' : 'No open blocks.'));
|
|
366
|
+
if (includeLocal)
|
|
367
|
+
renderActivityLane();
|
|
288
368
|
return;
|
|
289
369
|
}
|
|
290
370
|
// Shared fleet-comms masthead (same family as `agents mailboxes`).
|
|
@@ -300,8 +380,29 @@ export function registerFeedCommand(program) {
|
|
|
300
380
|
renderBlock(b, self);
|
|
301
381
|
return;
|
|
302
382
|
}
|
|
303
|
-
const groups = groupBlocksByOutcome(blocks)
|
|
383
|
+
const groups = groupBlocksByOutcome(blocks).sort((a, b) => {
|
|
384
|
+
const ar = Math.max(...a.blocks.map((block) => block.delayRank?.score ?? 0));
|
|
385
|
+
const br = Math.max(...b.blocks.map((block) => block.delayRank?.score ?? 0));
|
|
386
|
+
return br - ar;
|
|
387
|
+
});
|
|
304
388
|
for (const g of groups)
|
|
305
389
|
renderOutcomeGroup(g, self);
|
|
390
|
+
if (includeLocal)
|
|
391
|
+
renderActivityLane();
|
|
306
392
|
});
|
|
307
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Print a compact "recent activity" lane under the feed: the last few milestone
|
|
396
|
+
* events (plans, PRs, worktrees, sub-agents) from the append-only activity logs.
|
|
397
|
+
* Read-only tail of the logs -- no transcript re-parsing. Silent when empty.
|
|
398
|
+
*/
|
|
399
|
+
function renderActivityLane() {
|
|
400
|
+
const events = readRecentActivity({ sinceMs: Date.now() - 24 * 60 * 60 * 1000, limit: 6 })
|
|
401
|
+
.filter((e) => e.tier === 'milestone');
|
|
402
|
+
if (events.length === 0)
|
|
403
|
+
return;
|
|
404
|
+
console.log(chalk.bold('\n recent activity'));
|
|
405
|
+
for (const ev of events)
|
|
406
|
+
console.log(formatActivityLine(ev, { showHost: true }));
|
|
407
|
+
console.log(chalk.gray(' → agents activity for the full stream'));
|
|
408
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
3
|
+
import { AgentManager } from '../lib/teams/agents.js';
|
|
4
|
+
import { handleStatus, handleTasks } from '../lib/teams/api.js';
|
|
5
|
+
import { listAskStats, listBlocks } from '../lib/feed.js';
|
|
6
|
+
import { buildSessionSignals, synthesizeControlCards } from '../lib/feed-ranking.js';
|
|
7
|
+
import { discoverSessions } from '../lib/session/discover.js';
|
|
8
|
+
import { buildHqFloor } from '../lib/hq/floor.js';
|
|
9
|
+
import { die } from '../lib/format.js';
|
|
10
|
+
async function collectFloorSnapshot() {
|
|
11
|
+
const manager = new AgentManager();
|
|
12
|
+
const [sessions, tasksResult] = await Promise.all([
|
|
13
|
+
getActiveSessions(),
|
|
14
|
+
handleTasks(manager, 1000),
|
|
15
|
+
]);
|
|
16
|
+
const teammatesByTeam = new Map();
|
|
17
|
+
await Promise.all(tasksResult.tasks.map(async (team) => {
|
|
18
|
+
const status = await handleStatus(manager, team.task_name, 'all');
|
|
19
|
+
teammatesByTeam.set(team.task_name, status.agents);
|
|
20
|
+
}));
|
|
21
|
+
const sessionMetas = sessions.length > 0 ? await discoverSessions({ all: true, limit: 5000 }) : [];
|
|
22
|
+
const blocks = [
|
|
23
|
+
...listBlocks(),
|
|
24
|
+
...synthesizeControlCards(buildSessionSignals(sessions, sessionMetas), listAskStats()),
|
|
25
|
+
];
|
|
26
|
+
return buildHqFloor({
|
|
27
|
+
sessions,
|
|
28
|
+
teams: tasksResult.tasks,
|
|
29
|
+
teammatesByTeam,
|
|
30
|
+
blocks,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function registerHqCommand(program) {
|
|
34
|
+
const hq = program
|
|
35
|
+
.command('hq')
|
|
36
|
+
.description('Machine-readable bridge for Agents HQ floor management.');
|
|
37
|
+
hq
|
|
38
|
+
.command('floor')
|
|
39
|
+
.description('Emit the live floor snapshot: rooms, agents, ambient events, and runnable actions.')
|
|
40
|
+
.option('--json', 'Output machine-readable JSON')
|
|
41
|
+
.action(async (opts) => {
|
|
42
|
+
try {
|
|
43
|
+
const snapshot = await collectFloorSnapshot();
|
|
44
|
+
if (opts.json || !process.stdout.isTTY) {
|
|
45
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(chalk.bold('Agents HQ floor'));
|
|
49
|
+
console.log(chalk.gray(`${snapshot.counters.agents} agents, ${snapshot.counters.rooms} rooms, ${snapshot.counters.needsInput} need input`));
|
|
50
|
+
for (const room of snapshot.rooms) {
|
|
51
|
+
console.log(`${chalk.cyan(room.name)} ${chalk.gray(`${room.counts.agents} agents, ${room.counts.running} running, ${room.counts.needsInput} need input`)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
die(err.message);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
package/dist/commands/lease.d.ts
CHANGED
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
* so a box a concurrent run just reused is never stopped.
|
|
9
9
|
*/
|
|
10
10
|
import type { Command } from 'commander';
|
|
11
|
+
import { type CrabboxBox } from '../lib/crabbox/cli.js';
|
|
12
|
+
/** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
|
|
13
|
+
export declare function fmtDurationShort(secs: number): string;
|
|
14
|
+
/** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
|
|
15
|
+
export declare function fmtIdleShort(box: CrabboxBox, nowSecs: number): string;
|
|
16
|
+
/** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
|
|
17
|
+
export declare function fmtExpiresShort(box: CrabboxBox, nowSecs: number): string;
|
|
18
|
+
/** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
|
|
19
|
+
export declare function boxAddress(box: CrabboxBox): string | undefined;
|
|
20
|
+
/** Human status: "ready" when usable, else the raw bootstrap state/status. */
|
|
21
|
+
export declare function boxStatus(box: CrabboxBox): string;
|
|
22
|
+
/**
|
|
23
|
+
* Warm boxes eligible for reuse: `ready` and the lease has not expired.
|
|
24
|
+
* Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
|
|
25
|
+
* freshest box (an untouched `lastTouchedAt` sorts last).
|
|
26
|
+
*/
|
|
27
|
+
export declare function reusableBoxes(boxes: CrabboxBox[], nowSecs: number): CrabboxBox[];
|
|
28
|
+
/** One aligned row for the reuse picker / `agents lease list`. */
|
|
29
|
+
export declare function formatBoxRow(box: CrabboxBox, nowSecs: number): string;
|
|
11
30
|
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
12
31
|
export declare function validateHetznerToken(token: string, fetchImpl?: typeof fetch): Promise<'valid' | 'invalid' | 'unreachable'>;
|
|
13
32
|
/**
|
package/dist/commands/lease.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* that have been untouched for a safety window are eligible (see `isReapSafe`),
|
|
8
8
|
* so a box a concurrent run just reused is never stopped.
|
|
9
9
|
*/
|
|
10
|
-
import { spawn } from 'child_process';
|
|
11
10
|
import chalk from 'chalk';
|
|
12
|
-
import {
|
|
11
|
+
import { openUrl } from '../lib/open-url.js';
|
|
12
|
+
import { crabboxList, crabboxStop, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
|
|
13
13
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
14
14
|
import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
|
|
15
15
|
import { secretsKeychainItem } from '../lib/secrets/index.js';
|
|
@@ -19,21 +19,104 @@ function fmtIdle(box) {
|
|
|
19
19
|
const iso = new Date(box.lastTouchedAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
|
|
20
20
|
return `idle since ${iso}Z`;
|
|
21
21
|
}
|
|
22
|
+
// ── Shared box helpers (consumed by exec.ts's reuse picker + ssh.ts's devices
|
|
23
|
+
// section, so the reuse/format logic lives in exactly one place) ─────────────
|
|
24
|
+
/** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
|
|
25
|
+
export function fmtDurationShort(secs) {
|
|
26
|
+
const s = Math.max(0, Math.round(secs));
|
|
27
|
+
if (s < 60)
|
|
28
|
+
return `${s}s`;
|
|
29
|
+
const m = Math.round(s / 60);
|
|
30
|
+
if (m < 60)
|
|
31
|
+
return `${m}m`;
|
|
32
|
+
const h = Math.floor(m / 60);
|
|
33
|
+
const rem = m % 60;
|
|
34
|
+
return rem ? `${h}h ${rem}m` : `${h}h`;
|
|
35
|
+
}
|
|
36
|
+
/** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
|
|
37
|
+
export function fmtIdleShort(box, nowSecs) {
|
|
38
|
+
if (box.lastTouchedAt === null)
|
|
39
|
+
return 'idle ?';
|
|
40
|
+
return `idle ${fmtDurationShort(nowSecs - box.lastTouchedAt)}`;
|
|
41
|
+
}
|
|
42
|
+
/** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
|
|
43
|
+
export function fmtExpiresShort(box, nowSecs) {
|
|
44
|
+
if (box.expiresAt === null)
|
|
45
|
+
return 'expires ?';
|
|
46
|
+
const left = box.expiresAt - nowSecs;
|
|
47
|
+
return left <= 0 ? 'expired' : `expires ${fmtDurationShort(left)}`;
|
|
48
|
+
}
|
|
49
|
+
/** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
|
|
50
|
+
export function boxAddress(box) {
|
|
51
|
+
return box.tailscaleFQDN || box.tailscaleIPv4 || box.ip || undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Human status: "ready" when usable, else the raw bootstrap state/status. */
|
|
54
|
+
export function boxStatus(box) {
|
|
55
|
+
return box.ready ? 'ready' : box.state || box.status || 'pending';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Warm boxes eligible for reuse: `ready` and the lease has not expired.
|
|
59
|
+
* Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
|
|
60
|
+
* freshest box (an untouched `lastTouchedAt` sorts last).
|
|
61
|
+
*/
|
|
62
|
+
export function reusableBoxes(boxes, nowSecs) {
|
|
63
|
+
return boxes
|
|
64
|
+
.filter((b) => b.ready && (b.expiresAt === null || b.expiresAt > nowSecs))
|
|
65
|
+
.sort((a, b) => (b.lastTouchedAt ?? 0) - (a.lastTouchedAt ?? 0));
|
|
66
|
+
}
|
|
67
|
+
/** One aligned row for the reuse picker / `agents lease list`. */
|
|
68
|
+
export function formatBoxRow(box, nowSecs) {
|
|
69
|
+
const slug = box.slug.padEnd(16);
|
|
70
|
+
const cls = (box.class ?? '?').padEnd(10);
|
|
71
|
+
const addr = (boxAddress(box) ?? '—').padEnd(24);
|
|
72
|
+
const status = boxStatus(box).padEnd(8);
|
|
73
|
+
const idle = fmtIdleShort(box, nowSecs).padEnd(12);
|
|
74
|
+
return `${slug} ${cls} ${addr} ${status} ${idle} ${fmtExpiresShort(box, nowSecs)}`;
|
|
75
|
+
}
|
|
22
76
|
const HETZNER_BUNDLE = 'hetzner.com';
|
|
23
77
|
const HCLOUD_KEY = 'HCLOUD_TOKEN';
|
|
24
78
|
const HETZNER_CONSOLE_URL = 'https://console.hetzner.cloud/';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
79
|
+
const TAILSCALE_BUNDLE = 'tailscale.com';
|
|
80
|
+
const TAILSCALE_KEY = 'CRABBOX_TAILSCALE_AUTH_KEY';
|
|
81
|
+
const TAILSCALE_KEYS_URL = 'https://login.tailscale.com/admin/settings/keys';
|
|
82
|
+
/**
|
|
83
|
+
* Optional Tailscale setup for private-network leases (`--tailscale`). Collects
|
|
84
|
+
* an EPHEMERAL, pre-authorized, `tag:crabbox` auth key and stores it in the
|
|
85
|
+
* `tailscale.com` keychain bundle under `CRABBOX_TAILSCALE_AUTH_KEY` (the exact
|
|
86
|
+
* key `crabboxEnv` auto-injects). Blank input skips — public-IP leases still
|
|
87
|
+
* work with no Tailscale key. Never throws for cancel; mirrors the Hetzner
|
|
88
|
+
* capture above but does no live validation (Tailscale has no cheap probe).
|
|
89
|
+
*/
|
|
90
|
+
async function captureTailscaleAuthKey() {
|
|
91
|
+
console.error(chalk.bold('\nOptional: private-network leases over Tailscale'));
|
|
92
|
+
console.error(chalk.dim('Mint an EPHEMERAL, pre-authorized auth key tagged `tag:crabbox` in the Tailscale admin,'));
|
|
93
|
+
console.error(chalk.dim('then paste it to reach leased boxes only over your tailnet (`--tailscale`). Leave blank to skip.\n'));
|
|
94
|
+
const { password } = await import('@inquirer/prompts');
|
|
95
|
+
let key;
|
|
29
96
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
p.unref();
|
|
97
|
+
openUrl(TAILSCALE_KEYS_URL);
|
|
98
|
+
key = (await password({ message: 'Paste a Tailscale auth key (blank to skip):', mask: true })).trim();
|
|
33
99
|
}
|
|
34
|
-
catch {
|
|
35
|
-
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (isPromptCancelled(e)) {
|
|
102
|
+
console.error(chalk.yellow('Skipped Tailscale setup.'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
if (!key) {
|
|
108
|
+
console.error(chalk.dim('Skipped Tailscale setup — public-IP leases only.'));
|
|
109
|
+
return;
|
|
36
110
|
}
|
|
111
|
+
const bundle = bundleExists(TAILSCALE_BUNDLE)
|
|
112
|
+
? readBundle(TAILSCALE_BUNDLE)
|
|
113
|
+
: { name: TAILSCALE_BUNDLE, description: 'Tailscale ephemeral auth key for crabbox tailnet leases', vars: {} };
|
|
114
|
+
const store = bundleItemStore(bundle.backend);
|
|
115
|
+
store.set(secretsKeychainItem(TAILSCALE_BUNDLE, TAILSCALE_KEY), key);
|
|
116
|
+
bundle.vars[TAILSCALE_KEY] = keychainRef(TAILSCALE_KEY);
|
|
117
|
+
writeBundle(bundle);
|
|
118
|
+
console.error(chalk.green(`✔ Stored Tailscale auth key in keychain bundle '${TAILSCALE_BUNDLE}'.`));
|
|
119
|
+
console.error(chalk.dim(' Add --tailscale to a lease (reuse defaults to it) to reach the box over your tailnet.'));
|
|
37
120
|
}
|
|
38
121
|
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
39
122
|
export async function validateHetznerToken(token, fetchImpl = fetch) {
|
|
@@ -103,6 +186,8 @@ export async function runLeaseSetup(opts = {}) {
|
|
|
103
186
|
setLeaseSecretsBundle(HETZNER_BUNDLE);
|
|
104
187
|
console.error(chalk.green(`\n✔ Stored in keychain bundle '${HETZNER_BUNDLE}' and set as the default lease provider.`));
|
|
105
188
|
console.error(chalk.dim(' Run `agents run <agent> "…" --lease` — no env var, no flag needed.'));
|
|
189
|
+
// Also offer to capture a Tailscale auth key for private-network leases.
|
|
190
|
+
await captureTailscaleAuthKey();
|
|
106
191
|
return true;
|
|
107
192
|
}
|
|
108
193
|
console.error(chalk.yellow('lease setup: no valid token after 3 attempts — aborted.'));
|
|
@@ -128,6 +213,48 @@ export function registerLeaseCommand(program) {
|
|
|
128
213
|
const ok = await runLeaseSetup({ provider: opts.provider });
|
|
129
214
|
process.exit(ok ? 0 : 1);
|
|
130
215
|
});
|
|
216
|
+
lease
|
|
217
|
+
.command('list')
|
|
218
|
+
.alias('ls')
|
|
219
|
+
.description('List warm crabbox boxes you can reuse with `agents run --box <slug>`.')
|
|
220
|
+
.option('--json', 'Output JSON', false)
|
|
221
|
+
.action((opts) => {
|
|
222
|
+
const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
|
|
223
|
+
let boxes;
|
|
224
|
+
try {
|
|
225
|
+
boxes = crabboxList(boxOpts);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.error(chalk.red(`lease list: ${e.message}`));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (opts.json) {
|
|
233
|
+
console.log(JSON.stringify(boxes, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (boxes.length === 0) {
|
|
237
|
+
console.error(chalk.gray('No crabbox boxes. `agents run <agent> "…" --lease` provisions one.'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
241
|
+
console.log(chalk.bold(`Warm boxes (${boxes.length})`));
|
|
242
|
+
for (const b of boxes)
|
|
243
|
+
console.log(' ' + formatBoxRow(b, nowSecs));
|
|
244
|
+
console.log(chalk.gray(' Reuse: agents run <agent> "…" --box <slug> · Stop: agents lease stop <slug>'));
|
|
245
|
+
});
|
|
246
|
+
lease
|
|
247
|
+
.command('stop <slug>')
|
|
248
|
+
.description('Stop (release) a leased crabbox box now.')
|
|
249
|
+
.action((slug) => {
|
|
250
|
+
const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
|
|
251
|
+
const ok = crabboxStop(slug, boxOpts);
|
|
252
|
+
if (ok)
|
|
253
|
+
console.error(chalk.green(`Stopped box ${slug}.`));
|
|
254
|
+
else
|
|
255
|
+
console.error(chalk.red(`Could not stop box ${slug} (already gone, or crabbox is unavailable).`));
|
|
256
|
+
process.exit(ok ? 0 : 1);
|
|
257
|
+
});
|
|
131
258
|
lease
|
|
132
259
|
.command('gc')
|
|
133
260
|
.description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { readStdinSync } from '../lib/format.js';
|
|
4
|
+
import { clearVaultKey, createVault, getVaultSession, joinVault, unlock, vaultExists, vaultPath, } from '../lib/secrets/vault.js';
|
|
5
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
6
|
+
async function promptPassword(message) {
|
|
7
|
+
if (!isInteractiveTerminal()) {
|
|
8
|
+
throw new Error('A password is required. Run from a TTY or pass --password-stdin.');
|
|
9
|
+
}
|
|
10
|
+
const { password } = await import('@inquirer/prompts');
|
|
11
|
+
return password({ message, mask: true });
|
|
12
|
+
}
|
|
13
|
+
async function readPassword(opts, message) {
|
|
14
|
+
if (opts.passwordStdin) {
|
|
15
|
+
const value = readStdinSync();
|
|
16
|
+
if (!value)
|
|
17
|
+
throw new Error('No password received on stdin.');
|
|
18
|
+
return value.replace(/\r?\n$/, '');
|
|
19
|
+
}
|
|
20
|
+
return promptPassword(message);
|
|
21
|
+
}
|
|
22
|
+
async function chooseFreshLoginMode(opts) {
|
|
23
|
+
if (opts.create && opts.join)
|
|
24
|
+
throw new Error('--create and --join are mutually exclusive.');
|
|
25
|
+
if (opts.create)
|
|
26
|
+
return 'create';
|
|
27
|
+
if (opts.join)
|
|
28
|
+
return 'join';
|
|
29
|
+
if (!isInteractiveTerminal()) {
|
|
30
|
+
throw new Error('No synced secrets file exists. Pass --create or --join <vault.age> in a non-interactive shell.');
|
|
31
|
+
}
|
|
32
|
+
const { select } = await import('@inquirer/prompts');
|
|
33
|
+
return select({
|
|
34
|
+
message: 'No synced secrets login found. What do you want to do?',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: 'Create a new login', value: 'create' },
|
|
37
|
+
{ name: 'Join with an existing synced secrets file', value: 'join' },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function formatRemaining(expiresAt) {
|
|
42
|
+
const ms = expiresAt - Date.now();
|
|
43
|
+
if (ms <= 0)
|
|
44
|
+
return 'expired';
|
|
45
|
+
const mins = Math.round(ms / 60000);
|
|
46
|
+
if (mins < 60)
|
|
47
|
+
return `${mins} minute${mins === 1 ? '' : 's'}`;
|
|
48
|
+
const hours = Math.round(mins / 60);
|
|
49
|
+
if (hours < 24)
|
|
50
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
51
|
+
const days = Math.round(hours / 24);
|
|
52
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
53
|
+
}
|
|
54
|
+
export function registerLoginCommands(program) {
|
|
55
|
+
program
|
|
56
|
+
.command('login')
|
|
57
|
+
.description('Unlock synced secrets for this shell session')
|
|
58
|
+
.option('--create', 'Create a new encrypted synced-secrets file at ~/.agents/vault.age')
|
|
59
|
+
.option('--join <path>', 'Copy and unlock an existing vault.age file')
|
|
60
|
+
.option('--force', 'Replace an existing synced-secrets file when used with --create or --join')
|
|
61
|
+
.option('--password-stdin', 'Read the master password from stdin')
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
try {
|
|
64
|
+
if (opts.force && !opts.create && !opts.join) {
|
|
65
|
+
throw new Error('--force can only be used with --create or --join.');
|
|
66
|
+
}
|
|
67
|
+
if (vaultExists() && !opts.create && !opts.join) {
|
|
68
|
+
const password = await readPassword(opts, 'Master password');
|
|
69
|
+
unlock(password);
|
|
70
|
+
const session = getVaultSession();
|
|
71
|
+
const ttl = session.loggedIn ? formatRemaining(session.expiresAt) : '8 hours';
|
|
72
|
+
console.log(chalk.green(`Logged in. Synced secrets unlocked for ${ttl}.`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const mode = await chooseFreshLoginMode(opts);
|
|
76
|
+
if (mode === 'join') {
|
|
77
|
+
let source = opts.join;
|
|
78
|
+
if (!source) {
|
|
79
|
+
if (!isInteractiveTerminal())
|
|
80
|
+
throw new Error('--join needs a vault.age path.');
|
|
81
|
+
const { input } = await import('@inquirer/prompts');
|
|
82
|
+
source = await input({
|
|
83
|
+
message: 'Path to existing vault.age',
|
|
84
|
+
validate: (value) => fs.existsSync(value) || 'File not found.',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const password = await readPassword(opts, 'Master password');
|
|
88
|
+
joinVault(password, source, { overwrite: opts.force });
|
|
89
|
+
console.log(chalk.green(`Logged in. Copied synced secrets file to ${vaultPath()}.`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const password = await readPassword(opts, 'Choose a master password');
|
|
93
|
+
createVault(password, { overwrite: opts.force });
|
|
94
|
+
console.log(chalk.green(`Logged in. Created synced secrets file at ${vaultPath()}.`));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (isPromptCancelled(err))
|
|
98
|
+
return;
|
|
99
|
+
console.error(chalk.red(err.message));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
program
|
|
104
|
+
.command('logout')
|
|
105
|
+
.description('Forget the cached synced-secrets key')
|
|
106
|
+
.action(() => {
|
|
107
|
+
clearVaultKey();
|
|
108
|
+
console.log(chalk.green('Logged out. Synced secrets are locked.'));
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command('whoami')
|
|
112
|
+
.description('Show synced-secrets login status')
|
|
113
|
+
.action(() => {
|
|
114
|
+
const session = getVaultSession();
|
|
115
|
+
if (!session.loggedIn) {
|
|
116
|
+
console.log(chalk.yellow('Not logged in.'));
|
|
117
|
+
console.log(chalk.gray(`File: ${vaultPath()}${vaultExists() ? '' : ' (not created)'}`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.green(`Logged in (${formatRemaining(session.expiresAt)} remaining).`));
|
|
121
|
+
console.log(chalk.gray(`File: ${vaultPath()}`));
|
|
122
|
+
});
|
|
123
|
+
}
|