@phnx-labs/agents-cli 1.22.46 → 1.22.47
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 +279 -0
- package/LICENSE +102 -182
- package/README.md +31 -18
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +4 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.js +7 -5
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +18 -31
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +70 -3
- package/dist/lib/accounting/usage.js +235 -63
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +14 -6
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +5 -3
- package/dist/lib/exec.js +32 -16
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +3 -1
- package/dist/lib/startup/command-registry.js +5 -2
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
package/dist/commands/ssh.js
CHANGED
|
@@ -18,10 +18,12 @@ import ora from 'ora';
|
|
|
18
18
|
import { getCliVersion } from '../lib/version.js';
|
|
19
19
|
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
20
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
21
|
+
import { assertRegistrableDeviceName } from '../lib/devices/registry.js';
|
|
21
22
|
import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
|
|
23
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../lib/devices/interactive-host.js';
|
|
22
24
|
import { registerFleetCaptureCommand } from './fleet-capture.js';
|
|
23
25
|
import { registerFleetApplyAlias } from './apply.js';
|
|
24
|
-
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
26
|
+
import { addIgnored, getDevice, loadDevices, loadIgnored, loadIgnoredEntries, removeDevice, removeIgnored, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
25
27
|
import { resolveDeviceProfile } from '../lib/devices/resolve-profile.js';
|
|
26
28
|
import { collectReachabilityWriteBacks, deviceOnlineState } from '../lib/devices/reachability.js';
|
|
27
29
|
import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
|
|
@@ -47,7 +49,7 @@ import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
|
47
49
|
import { listTasks, resolveTaskRef } from '../lib/hosts/tasks.js';
|
|
48
50
|
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
49
51
|
import { stopDispatchedTask } from '../lib/hosts/dispatch.js';
|
|
50
|
-
import { terminalWidth, truncateToWidth } from '../lib/session/width.js';
|
|
52
|
+
import { stringWidth, stripAnsi, terminalWidth, truncateToWidth } from '../lib/session/width.js';
|
|
51
53
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
52
54
|
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
53
55
|
import { collectLocalHarnessInventory, groupByAccount, renderAccountsMatrix, renderHarnessMatrix, } from '../lib/devices/harness-inventory.js';
|
|
@@ -113,30 +115,99 @@ function pctCell(v, width) {
|
|
|
113
115
|
return chalk.yellow(s);
|
|
114
116
|
return chalk.red(s);
|
|
115
117
|
}
|
|
118
|
+
/** Floor for the `spec` column; the real width is measured from the rows.
|
|
119
|
+
*
|
|
120
|
+
* A fixed width cannot work here: `fmtBytes` emits an optional decimal, so a
|
|
121
|
+
* spec string runs from `8c 16G 256G` (11) to `10c 23.5G 460G` (14) depending
|
|
122
|
+
* purely on the hardware behind it. Anything narrower than the widest row
|
|
123
|
+
* pushes load/mem/disk/headroom out of alignment row-to-row AND against the
|
|
124
|
+
* header — which defeats the scannability this column exists for. */
|
|
125
|
+
const SPEC_WIDTH_MIN = 12;
|
|
126
|
+
/** The static hardware as one compact cell — `12c 64G 1T`: cores, total RAM,
|
|
127
|
+
* total root disk via fmtBytes. `—` when the probe never saw the box (specs
|
|
128
|
+
* ride the long-TTL static tier, so a warm cache almost always has them).
|
|
129
|
+
* Unpadded; {@link renderDeviceTable} pads to the measured column width. */
|
|
130
|
+
function specText(stats) {
|
|
131
|
+
if (!stats?.reachable || !stats.ncpu)
|
|
132
|
+
return '—';
|
|
133
|
+
return `${stats.ncpu}c ${fmtBytes(stats.memTotalBytes)} ${fmtBytes(stats.diskTotalBytes)}`;
|
|
134
|
+
}
|
|
135
|
+
/** The widest spec string across the rows actually being rendered, floored at
|
|
136
|
+
* SPEC_WIDTH_MIN so a short fleet still lines up with the header. */
|
|
137
|
+
function specColumnWidth(names, statsMap) {
|
|
138
|
+
return Math.max(SPEC_WIDTH_MIN, ...names.map((n) => stringWidth(specText(statsMap.get(n)))));
|
|
139
|
+
}
|
|
140
|
+
/** Colour + pad one spec cell to the measured column width. */
|
|
141
|
+
function specCell(stats, width) {
|
|
142
|
+
const text = specText(stats);
|
|
143
|
+
return (text === '—' ? chalk.gray : chalk.greenBright)(text.padEnd(width));
|
|
144
|
+
}
|
|
145
|
+
/** The one-line `description` config value per device (absent when unset). */
|
|
146
|
+
function listDeviceDescriptions(names) {
|
|
147
|
+
const out = {};
|
|
148
|
+
for (const n of names) {
|
|
149
|
+
const v = getConfigValue('description', { device: n }).value;
|
|
150
|
+
if (typeof v === 'string' && v.length > 0)
|
|
151
|
+
out[n] = v;
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Fit a device row to the terminal width. The description is the FIRST thing
|
|
157
|
+
* to truncate, then the role tag; the fixed columns (device, platform, spec,
|
|
158
|
+
* load/mem/disk numbers, headroom, and the ▸/★/←/relay markers) never
|
|
159
|
+
* truncate. `role` and `desc` arrive fully rendered (ansi + leading gap).
|
|
160
|
+
*/
|
|
161
|
+
function fitDeviceRow(fixed, role, desc, width) {
|
|
162
|
+
const w = (s) => stringWidth(stripAnsi(s));
|
|
163
|
+
let over = w(fixed) + w(role) + w(desc) - width;
|
|
164
|
+
if (over > 0 && desc) {
|
|
165
|
+
// `desc` is ' ' + gray(text) — truncate the text, keep the gap.
|
|
166
|
+
const text = stripAnsi(desc).slice(2);
|
|
167
|
+
const budget = w(text) - over;
|
|
168
|
+
desc = budget > 0 ? ' ' + chalk.gray(truncateToWidth(text, budget)) : '';
|
|
169
|
+
over = w(fixed) + w(role) + w(desc) - width;
|
|
170
|
+
}
|
|
171
|
+
// A partial role word ("work…") reads as a bug — when it can't fit whole,
|
|
172
|
+
// it drops entirely. The numbers are never touched.
|
|
173
|
+
if (over > 0 && role)
|
|
174
|
+
role = '';
|
|
175
|
+
return fixed + role + desc;
|
|
176
|
+
}
|
|
116
177
|
/**
|
|
117
178
|
* Render the device list. When `statsMap` is provided, resource columns are
|
|
118
|
-
* appended —
|
|
119
|
-
*
|
|
179
|
+
* appended — a `spec` cell (cores / total RAM / total disk), normalized load,
|
|
180
|
+
* memory and disk-used percentages, a headroom badge, and (in `full` mode)
|
|
181
|
+
* free/total memory — with the per-device role and `description` riding the
|
|
182
|
+
* tail, so it's obvious which boxes have room and what each box is for.
|
|
120
183
|
* Without it (probe skipped) the classic reachability line is used. A fleet
|
|
121
184
|
* capacity summary is appended whenever stats were gathered.
|
|
122
185
|
*/
|
|
123
|
-
function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost) {
|
|
186
|
+
export function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost, opts = {}) {
|
|
124
187
|
if (!statsMap) {
|
|
125
188
|
const roles = listConfiguredDeviceRoles(names);
|
|
126
189
|
return names.map((n) => deviceSummary(reg[n], n === self, undefined, n === interactiveHost, roles));
|
|
127
190
|
}
|
|
128
191
|
const deviceRoles = listConfiguredDeviceRoles(names);
|
|
192
|
+
const specWidth = specColumnWidth(names, statsMap);
|
|
193
|
+
const descriptions = listDeviceDescriptions(names);
|
|
194
|
+
const width = opts.width ?? terminalWidth();
|
|
129
195
|
const lines = [];
|
|
130
196
|
const head = ' ' +
|
|
131
197
|
chalk.gray('device'.padEnd(16)) +
|
|
132
198
|
chalk.gray('platform'.padEnd(8)) +
|
|
133
199
|
' ' +
|
|
134
|
-
|
|
200
|
+
chalk.gray('spec'.padEnd(specWidth)) +
|
|
135
201
|
chalk.gray('load'.padStart(5)) +
|
|
136
202
|
chalk.gray('mem'.padStart(6)) +
|
|
203
|
+
chalk.gray('disk'.padStart(5)) +
|
|
137
204
|
(full ? ' ' + chalk.gray('free/total'.padEnd(12)) : '') +
|
|
138
205
|
' ' +
|
|
139
|
-
chalk.gray('headroom')
|
|
206
|
+
chalk.gray('headroom') +
|
|
207
|
+
' ' +
|
|
208
|
+
chalk.gray('role') +
|
|
209
|
+
' ' +
|
|
210
|
+
chalk.gray('description');
|
|
140
211
|
lines.push(head);
|
|
141
212
|
for (const name of names) {
|
|
142
213
|
// Effective profile: the operator's central config (ssh.*/platform/user)
|
|
@@ -147,18 +218,20 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
|
|
|
147
218
|
const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
|
|
148
219
|
const plat = String(d.platform).padEnd(8);
|
|
149
220
|
const stats = statsMap.get(name);
|
|
221
|
+
const role = roleTag(name, deviceRoles);
|
|
222
|
+
const desc = descriptions[name] ? ' ' + chalk.gray(descriptions[name]) : '';
|
|
150
223
|
// Prefer this run's live probe, then the written-back verdict, over the
|
|
151
224
|
// stale tailscale.online snapshot — so a reachable box never renders
|
|
152
225
|
// "offline" while its live load/mem sit one column over (RUSH-1965).
|
|
153
226
|
const offline = deviceOnlineState(d, stats) === 'offline';
|
|
154
227
|
if (offline) {
|
|
155
|
-
lines.push(`${marker}${label}${plat} ${chalk.gray('offline')}
|
|
228
|
+
lines.push(fitDeviceRow(`${marker}${label}${plat} ${chalk.gray('offline')}`, role, desc, width));
|
|
156
229
|
continue;
|
|
157
230
|
}
|
|
158
231
|
const relay = !isSelf && d.tailscale?.online && !d.tailscale.direct ? chalk.yellow(' relay') : '';
|
|
159
|
-
const cores = full ? chalk.gray(String(stats?.ncpu ?? '—').padStart(6)) : '';
|
|
160
232
|
const load = pctCell(stats?.loadPercent, 5);
|
|
161
233
|
const mem = pctCell(stats?.memPercent, 6);
|
|
234
|
+
const disk = pctCell(stats?.diskUsedPercent, 5);
|
|
162
235
|
const freeTotal = full
|
|
163
236
|
? ' ' +
|
|
164
237
|
(stats?.reachable && stats.memTotalBytes
|
|
@@ -168,13 +241,21 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
|
|
|
168
241
|
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
169
242
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
170
243
|
const interactive = name === interactiveHost ? chalk.yellow(' ★ interactive') : '';
|
|
171
|
-
lines.push(`${marker}${label}${plat} ${
|
|
244
|
+
lines.push(fitDeviceRow(`${marker}${label}${plat} ${specCell(stats, specWidth)}${load}${mem}${disk}${freeTotal} ${badge}${relay}${here}${interactive}`, role, desc, width));
|
|
172
245
|
}
|
|
173
|
-
// Fleet capacity summary — total cores
|
|
246
|
+
// Fleet capacity summary — total cores, how much RAM is free right now, and
|
|
247
|
+
// total free root disk across the reachable fleet.
|
|
174
248
|
const cap = fleetCapacity(statsMap.values());
|
|
175
249
|
if (cap.reachable > 0) {
|
|
176
250
|
const freePct = cap.memTotalBytes > 0 ? Math.round((cap.memFreeBytes / cap.memTotalBytes) * 100) : 0;
|
|
177
|
-
|
|
251
|
+
let diskFreeBytes = 0;
|
|
252
|
+
for (const s of statsMap.values())
|
|
253
|
+
if (s.reachable)
|
|
254
|
+
diskFreeBytes += s.diskFreeBytes ?? 0;
|
|
255
|
+
lines.push(chalk.gray(` Fleet capacity: ${cap.cores} cores · ${fmtBytes(cap.memFreeBytes)} free / ${fmtBytes(cap.memTotalBytes)} RAM (${freePct}% free) · ${fmtBytes(diskFreeBytes)} disk free across ${cap.reachable} reachable device${cap.reachable === 1 ? '' : 's'}`));
|
|
256
|
+
}
|
|
257
|
+
if (opts.ignoredCount) {
|
|
258
|
+
lines.push(chalk.gray(` ${opts.ignoredCount} ignored node${opts.ignoredCount === 1 ? '' : 's'} not listed — 'agents devices ignored'`));
|
|
178
259
|
}
|
|
179
260
|
return lines;
|
|
180
261
|
}
|
|
@@ -975,6 +1056,7 @@ function registerDevicesCommands(program) {
|
|
|
975
1056
|
agents devices sync # pick which tailscale nodes to keep (TTY)
|
|
976
1057
|
agents devices sync --yes # register all non-ignored nodes
|
|
977
1058
|
agents devices ignore ipad165 # dismiss a node so it's never re-suggested
|
|
1059
|
+
agents devices ignored # list dismissed nodes (when / which machine)
|
|
978
1060
|
|
|
979
1061
|
Inspect:
|
|
980
1062
|
agents devices list # what's registered (★ = interactive host)
|
|
@@ -990,6 +1072,7 @@ function registerDevicesCommands(program) {
|
|
|
990
1072
|
agents devices config win-mini ssh.auth password
|
|
991
1073
|
agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
|
|
992
1074
|
agents devices config mac-mini auto-launch.enabled off
|
|
1075
|
+
agents devices describe mark-1 "gpu box — cuda 12.4" # one-line purpose, shown in the list
|
|
993
1076
|
agents devices config mac-mini interactive.host zion # where agents show YOU artifacts
|
|
994
1077
|
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
995
1078
|
|
|
@@ -1002,10 +1085,10 @@ function registerDevicesCommands(program) {
|
|
|
1002
1085
|
registerLeaseCommand(devicesCmd);
|
|
1003
1086
|
registerSnapshotCommand(devicesCmd);
|
|
1004
1087
|
registerCommandGroups(devicesCmd, [
|
|
1005
|
-
{ title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', 'rm'] },
|
|
1088
|
+
{ title: 'Discover & register', names: ['sync', 'register', 'add', 'ignore', 'unignore', 'ignored', 'rm'] },
|
|
1006
1089
|
{ title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
|
|
1007
1090
|
{ title: 'Disposable devices', names: ['lease'] },
|
|
1008
|
-
{ title: 'Configure a device', names: ['config', 'render'] },
|
|
1091
|
+
{ title: 'Configure a device', names: ['config', 'describe', 'render'] },
|
|
1009
1092
|
{ title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply'] },
|
|
1010
1093
|
]);
|
|
1011
1094
|
devicesCmd
|
|
@@ -1085,6 +1168,47 @@ function registerDevicesCommands(program) {
|
|
|
1085
1168
|
setDeviceDiscoveryStatus(name, undefined);
|
|
1086
1169
|
console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
|
|
1087
1170
|
});
|
|
1171
|
+
const ignoredCmd = devicesCmd
|
|
1172
|
+
.command('ignored')
|
|
1173
|
+
.description('List dismissed tailscale nodes — what was dismissed, when, and on which machine.')
|
|
1174
|
+
.option('--json', 'output machine-readable JSON')
|
|
1175
|
+
.action((opts) => {
|
|
1176
|
+
try {
|
|
1177
|
+
const entries = loadIgnoredEntries();
|
|
1178
|
+
if (opts.json) {
|
|
1179
|
+
process.stdout.write(JSON.stringify(entries, null, 2) + '\n');
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (entries.length === 0) {
|
|
1183
|
+
console.log(chalk.gray("No ignored nodes. 'agents devices ignore <name>' dismisses one — it won't be re-suggested."));
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
console.log(chalk.bold(`Ignored nodes (${entries.length})`));
|
|
1187
|
+
for (const e of entries) {
|
|
1188
|
+
const age = formatCheckedAge(Date.parse(e.ignoredAt));
|
|
1189
|
+
console.log(` ${chalk.bold(e.name.padEnd(24))} ${chalk.gray(`${age} · dismissed on ${e.ignoredOn}`)}`);
|
|
1190
|
+
}
|
|
1191
|
+
console.log(chalk.gray("Undo one with 'agents devices unignore <name>'."));
|
|
1192
|
+
}
|
|
1193
|
+
catch (err) {
|
|
1194
|
+
console.error(chalk.red(err.message));
|
|
1195
|
+
process.exit(1);
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
setHelpSections(ignoredCmd, {
|
|
1199
|
+
examples: `
|
|
1200
|
+
agents devices ignored # what was dismissed, when, and on which machine
|
|
1201
|
+
agents devices ignored --json # machine-readable [{ name, ignoredAt, ignoredOn }]
|
|
1202
|
+
agents devices unignore old-laptop # undo a dismissal
|
|
1203
|
+
`,
|
|
1204
|
+
notes: `
|
|
1205
|
+
Dismissals live in the tracked central agents.yaml (fleet.ignored) and
|
|
1206
|
+
sync with 'agents repo push/pull', so a node dismissed on one box stays
|
|
1207
|
+
dismissed everywhere. An ignored node is not a device — it never enters
|
|
1208
|
+
the registry, so 'agents devices list' never shows it; this command is
|
|
1209
|
+
where dismissals are visible.
|
|
1210
|
+
`,
|
|
1211
|
+
});
|
|
1088
1212
|
// ─── devices config (unified settings surface) ────────────────────────────
|
|
1089
1213
|
//
|
|
1090
1214
|
// ONE command for every per-device setting: `agents devices config <name>
|
|
@@ -1461,6 +1585,8 @@ function registerDevicesCommands(program) {
|
|
|
1461
1585
|
`,
|
|
1462
1586
|
notes: `
|
|
1463
1587
|
Keys: role (worker|personal), see 'agents devices role',
|
|
1588
|
+
description (one line saying what the box is for — see
|
|
1589
|
+
'agents devices describe'; renders in the devices list tail),
|
|
1464
1590
|
agents.max-concurrent, scheduler.enabled, daemon.enabled,
|
|
1465
1591
|
watchdog.enabled, tmux.enabled, browser.remote-control,
|
|
1466
1592
|
browser.task-idle-minutes, browser.profile,
|
|
@@ -1529,6 +1655,40 @@ function registerDevicesCommands(program) {
|
|
|
1529
1655
|
box stays excluded, since that is what the mark is for.
|
|
1530
1656
|
`,
|
|
1531
1657
|
});
|
|
1658
|
+
const describeCmd = devicesCmd
|
|
1659
|
+
.command('describe <name> [text...]')
|
|
1660
|
+
.description('Show or set the one-line description of what a device is FOR ("gpu box — cuda 12.4"). ' +
|
|
1661
|
+
'Rendered as the tail column of `agents devices list` and synced fleet-wide. ' +
|
|
1662
|
+
'Same key as `agents devices config <name> description` — one store, two names.')
|
|
1663
|
+
.option('--unset', 'remove the description (the device falls back to the fleet default / unset)')
|
|
1664
|
+
.option('--json', 'output machine-readable JSON')
|
|
1665
|
+
.action(async (name, textParts, opts) => {
|
|
1666
|
+
try {
|
|
1667
|
+
// Thin sugar over the 'description' config key — the same engine that
|
|
1668
|
+
// backs `agents devices config <name> description`, not a second path.
|
|
1669
|
+
await runDevicesConfig(name, 'description', textParts ?? [], opts);
|
|
1670
|
+
}
|
|
1671
|
+
catch (err) {
|
|
1672
|
+
console.error(chalk.red(err.message));
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
setHelpSections(describeCmd, {
|
|
1677
|
+
examples: `
|
|
1678
|
+
agents devices describe mark-1 "gpu box — cuda 12.4" # set it (80 chars, one line)
|
|
1679
|
+
agents devices describe mark-1 # read it back
|
|
1680
|
+
agents devices describe mark-1 --unset # remove it
|
|
1681
|
+
agents devices describe mark-1 --json # machine-readable
|
|
1682
|
+
agents devices config mark-1 description "gpu box" # equivalent — same store
|
|
1683
|
+
`,
|
|
1684
|
+
notes: `
|
|
1685
|
+
The description is the device-scope 'description' config key: stored in
|
|
1686
|
+
the tracked per-device doc (devices/<name>/agents.yaml config: block),
|
|
1687
|
+
synced fleet-wide with 'agents repo push/pull', and rendered as the tail
|
|
1688
|
+
column of 'agents devices list'. It replaces on each set — for appended
|
|
1689
|
+
long-form scratch use 'agents devices config <name> notes "…"'.
|
|
1690
|
+
`,
|
|
1691
|
+
});
|
|
1532
1692
|
/** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
|
|
1533
1693
|
const configTombstoneNotice = (retired, replacement) => {
|
|
1534
1694
|
console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
|
|
@@ -1753,6 +1913,7 @@ function registerDevicesCommands(program) {
|
|
|
1753
1913
|
process.stdout.write(JSON.stringify(names.map((name) => {
|
|
1754
1914
|
const config = deviceConfigJson(name);
|
|
1755
1915
|
const health = statsMap?.get(name);
|
|
1916
|
+
const description = getConfigValue('description', { device: name }).value;
|
|
1756
1917
|
return {
|
|
1757
1918
|
...resolveDeviceProfile(reg[name]),
|
|
1758
1919
|
interactive: name === interactiveHost,
|
|
@@ -1760,6 +1921,7 @@ function registerDevicesCommands(program) {
|
|
|
1760
1921
|
// marked (absent when unmarked), `autoPool` is the answer that
|
|
1761
1922
|
// matters to a caller — may `--device auto` pick this box.
|
|
1762
1923
|
...(jsonRoles[name] ? { role: jsonRoles[name] } : {}),
|
|
1924
|
+
...(typeof description === 'string' && description ? { description } : {}),
|
|
1763
1925
|
autoPool: autoPool.has(name),
|
|
1764
1926
|
...(config ? { config } : {}),
|
|
1765
1927
|
...(health ? { health: { ...health, headroom: headroom(health) } } : {}),
|
|
@@ -1768,7 +1930,10 @@ function registerDevicesCommands(program) {
|
|
|
1768
1930
|
return;
|
|
1769
1931
|
}
|
|
1770
1932
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
1771
|
-
|
|
1933
|
+
// Dismissed nodes are not devices (never in the registry) — surface their
|
|
1934
|
+
// count under the table so a "missing" node is explainable from the list.
|
|
1935
|
+
const ignoredCount = loadIgnoredEntries().length;
|
|
1936
|
+
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full, interactiveHost, { ignoredCount }))
|
|
1772
1937
|
console.log(line);
|
|
1773
1938
|
if (freshness?.servedFromCache && freshness.oldestFetchedAt != null) {
|
|
1774
1939
|
console.log(chalk.gray(` updated ${formatCheckedAge(freshness.oldestFetchedAt)} — pass --refresh (--live) for a live probe`));
|
|
@@ -1789,7 +1954,7 @@ function registerDevicesCommands(program) {
|
|
|
1789
1954
|
devicesCmd
|
|
1790
1955
|
.command('list')
|
|
1791
1956
|
.alias('ls')
|
|
1792
|
-
.description('List registered devices with platform,
|
|
1957
|
+
.description('List registered devices with platform, spec (cores/RAM/disk), live load/mem/disk headroom, role, and description.')
|
|
1793
1958
|
.option('--json', 'output effective device profiles, config, and health as a JSON array')
|
|
1794
1959
|
.option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
|
|
1795
1960
|
.option('--refresh', 'force a live probe of every device, bypassing the cache')
|
|
@@ -1924,6 +2089,10 @@ email) into a single row. Use \`agents devices harnesses\` for the per-install v
|
|
|
1924
2089
|
.option('--platform <platform>', 'windows | linux | macos')
|
|
1925
2090
|
.action(async (name, target, opts) => {
|
|
1926
2091
|
try {
|
|
2092
|
+
// The one place a device name is CHOSEN rather than observed, so the one
|
|
2093
|
+
// place the reserved-sentinel policy belongs. upsertDevice itself stays
|
|
2094
|
+
// shape-only — `devices sync` feeds it tailnet node names in a loop.
|
|
2095
|
+
assertRegistrableDeviceName(name);
|
|
1927
2096
|
const { host, user } = splitUserHost(target);
|
|
1928
2097
|
const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
|
|
1929
2098
|
const d = await upsertDevice(name, {
|
|
@@ -2190,6 +2359,15 @@ the target when it exists, else the remote home. Same portable-cwd rule as
|
|
|
2190
2359
|
// nothing to resolve "self" to, and mis-report the pick as "Unknown
|
|
2191
2360
|
// device").
|
|
2192
2361
|
let target = name;
|
|
2362
|
+
if (isDeviceInteractive(name)) {
|
|
2363
|
+
const pinned = resolveInteractiveDevice();
|
|
2364
|
+
if (!pinned) {
|
|
2365
|
+
console.error(chalk.red(interactiveUnsetError()));
|
|
2366
|
+
process.exit(1);
|
|
2367
|
+
}
|
|
2368
|
+
process.stderr.write(chalk.gray(`[agents] device=interactive → ${pinned}\n`));
|
|
2369
|
+
target = pinned;
|
|
2370
|
+
}
|
|
2193
2371
|
if (isDeviceAuto(name)) {
|
|
2194
2372
|
const plan = resolveDeviceAffinity({});
|
|
2195
2373
|
if (!plan.host) {
|
package/dist/commands/teams.js
CHANGED
|
@@ -18,6 +18,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
|
|
|
18
18
|
import { setHelpSections } from '../lib/help.js';
|
|
19
19
|
import { createWorktree, commitsBehindDefault, isGitRepo, hasUncommittedChanges, removeWorktree, worktreeCheckoutExists, worktreeExists, } from '../lib/teams/worktree.js';
|
|
20
20
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
21
|
+
import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, } from '../lib/devices/interactive-host.js';
|
|
21
22
|
import { isDeviceAuto, resolveDeviceAuto } from '../lib/smart-launch.js';
|
|
22
23
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
23
24
|
import { ensureHostReady } from '../lib/hosts/ready.js';
|
|
@@ -1369,7 +1370,22 @@ export function registerTeamsCommands(program) {
|
|
|
1369
1370
|
// trim, drop blanks, dedupe (preserving first-seen order).
|
|
1370
1371
|
const rawPool = [opts.devices, opts.hosts].filter(Boolean).join(',');
|
|
1371
1372
|
const devices = [];
|
|
1372
|
-
for (const
|
|
1373
|
+
for (const raw of rawPool.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
1374
|
+
// Resolve `interactive` to the concrete device HERE, the same way the
|
|
1375
|
+
// single `--device` pin does. A pool is a set of specific machines the
|
|
1376
|
+
// scheduler picks between, so persisting the sentinel would let the
|
|
1377
|
+
// pool's membership change silently the next time `interactive.host`
|
|
1378
|
+
// is re-pinned — and every later friction message would quote
|
|
1379
|
+
// "interactive" instead of a box the user can act on.
|
|
1380
|
+
let d = raw;
|
|
1381
|
+
if (isDeviceInteractive(d)) {
|
|
1382
|
+
const pinned = resolveInteractiveDevice();
|
|
1383
|
+
if (!pinned) {
|
|
1384
|
+
dieFriction('teams', 'pool-device-interactive-unset', interactiveUnsetError());
|
|
1385
|
+
}
|
|
1386
|
+
process.stderr.write(chalk.gray(`[teams] pool device=interactive → ${pinned}\n`));
|
|
1387
|
+
d = pinned;
|
|
1388
|
+
}
|
|
1373
1389
|
if (!devices.includes(d))
|
|
1374
1390
|
devices.push(d);
|
|
1375
1391
|
}
|
|
@@ -1444,7 +1460,7 @@ export function registerTeamsCommands(program) {
|
|
|
1444
1460
|
.alias('a')
|
|
1445
1461
|
.description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
|
|
1446
1462
|
.option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
|
|
1447
|
-
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (
|
|
1463
|
+
.option('-m, --mode <mode>', `Permissions: plan (read-only) | edit (can write files) | auto (more autonomous than edit, mechanism per-harness — see 'agents modes <agent>') | skip (bypass all permission prompts). 'full' accepted as alias for skip. Teammates run headless: plan works headless on claude/codex/cursor/droid/opencode; kimi/grok/antigravity have no headless plan mode and auto-downgrade a plan request to auto.`, 'edit')
|
|
1448
1464
|
.option('-e, --effort <effort>', `Reasoning intensity: ${VALID_EFFORTS.join('|')}`, 'medium')
|
|
1449
1465
|
.option('--model <model>', 'Cost tier (cheap|default|best|ultra) or a concrete id (e.g. claude-opus-4-8); tiers resolve per harness+version to a supported model')
|
|
1450
1466
|
.option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
|
|
@@ -1507,6 +1523,22 @@ export function registerTeamsCommands(program) {
|
|
|
1507
1523
|
process.stderr.write(chalk.gray(`[teams] device=auto → ${picked === machineId() ? 'local' : picked}\n`));
|
|
1508
1524
|
explicitDevice = picked;
|
|
1509
1525
|
}
|
|
1526
|
+
// `interactive` needs the same up-front resolution, and for a sharper
|
|
1527
|
+
// reason: `teams add`/`create` bail out of the fleet passthrough
|
|
1528
|
+
// (passthrough.ts, the teams subcommand guard), so the sentinel block
|
|
1529
|
+
// there never runs and this is the only resolver. Left literal, the
|
|
1530
|
+
// local-machine check below compares 'interactive' against the machine id,
|
|
1531
|
+
// takes the remote branch on the pinned box itself, and pins the teammate
|
|
1532
|
+
// to an SSH hop into this same machine — skipping three local preflights
|
|
1533
|
+
// on the way.
|
|
1534
|
+
if (explicitDevice && isDeviceInteractive(explicitDevice)) {
|
|
1535
|
+
const pinned = resolveInteractiveDevice();
|
|
1536
|
+
if (!pinned) {
|
|
1537
|
+
dieFriction('teams', 'device-interactive-unset', interactiveUnsetError());
|
|
1538
|
+
}
|
|
1539
|
+
process.stderr.write(chalk.gray(`[teams] device=interactive → ${pinned === machineId() ? 'local' : pinned}\n`));
|
|
1540
|
+
explicitDevice = pinned;
|
|
1541
|
+
}
|
|
1510
1542
|
// Distributed teams: --device <name> PINS this teammate to a machine over
|
|
1511
1543
|
// SSH. Resolve + validate the placement here so a bad target fails at `add`
|
|
1512
1544
|
// time, not silently at launch. Persisted (hostName/hostTarget/repoPath) so
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -10,10 +10,12 @@ import type { Command } from 'commander';
|
|
|
10
10
|
import { accountDisplayLabel } from '../lib/agents.js';
|
|
11
11
|
import type { AccountInfo } from '../lib/agents.js';
|
|
12
12
|
import type { AgentId } from '../lib/types.js';
|
|
13
|
+
import type { FormatUsageSummaryOpts, UsageInfo } from '../lib/accounting/usage.js';
|
|
13
14
|
import type { ResourceSection, ViewJsonAgent } from '../lib/view-types.js';
|
|
14
15
|
export type { ResourceItemJson, ResourceSection, SyncState, VersionResourcesJson, ViewJsonAgent, ViewJsonVersion } from '../lib/view-types.js';
|
|
15
16
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
16
17
|
import { type ByokUsageResult } from '../lib/byok-usage.js';
|
|
18
|
+
export declare function viewUsageSummaryOptions(agentId: AgentId, signedIn: boolean, usageInfo: UsageInfo | undefined, maxWindows: number | undefined): FormatUsageSummaryOpts;
|
|
17
19
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
18
20
|
export declare const accountColumnLabel: typeof accountDisplayLabel;
|
|
19
21
|
/**
|
package/dist/commands/view.js
CHANGED
|
@@ -8,7 +8,7 @@ import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanag
|
|
|
8
8
|
import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
|
|
9
9
|
import { machineId } from '../lib/machine-id.js';
|
|
10
10
|
import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
|
|
11
|
-
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
|
|
11
|
+
import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
|
|
12
12
|
import { readManifest } from '../lib/manifest.js';
|
|
13
13
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/installations/versions.js';
|
|
14
14
|
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/installations/shims.js';
|
|
@@ -32,6 +32,19 @@ import { renderHarnessDetail } from './harness.js';
|
|
|
32
32
|
import { confirm } from '@inquirer/prompts';
|
|
33
33
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
34
34
|
import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
|
|
35
|
+
export function viewUsageSummaryOptions(agentId, signedIn, usageInfo, maxWindows) {
|
|
36
|
+
const headless = isUsageHeadlessScopeError(usageInfo?.error);
|
|
37
|
+
const benignState = usageInfo ? getUsageBenignState(usageInfo) : null;
|
|
38
|
+
return {
|
|
39
|
+
unavailable: agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot && !headless && !benignState,
|
|
40
|
+
unverified: !headless && !!usageInfo?.snapshot && !!usageInfo.error,
|
|
41
|
+
headless,
|
|
42
|
+
maxWindows,
|
|
43
|
+
errorKind: classifyUsageErrorKind(usageInfo?.error),
|
|
44
|
+
errorDetail: usageInfo?.error ?? null,
|
|
45
|
+
benignState,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
35
48
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
36
49
|
export const accountColumnLabel = accountDisplayLabel;
|
|
37
50
|
/**
|
|
@@ -535,15 +548,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
535
548
|
const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
|
|
536
549
|
const usageKey = getUsageLookupKey(info);
|
|
537
550
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
538
|
-
const
|
|
539
|
-
const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot && !headless;
|
|
540
|
-
const usageUnverified = !headless && !!usageInfo?.snapshot && !!usageInfo.error;
|
|
541
|
-
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
|
|
542
|
-
unavailable: usageUnavailable,
|
|
543
|
-
unverified: usageUnverified,
|
|
544
|
-
headless,
|
|
545
|
-
maxWindows: usageWindowCap,
|
|
546
|
-
});
|
|
551
|
+
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, !!info?.signedIn, usageInfo, usageWindowCap));
|
|
547
552
|
maxUsageWidth = Math.max(maxUsageWidth, stringWidth(usageStr));
|
|
548
553
|
const statusStr = formatUsageStatusBadge(info?.usageStatus);
|
|
549
554
|
maxStatusWidth = Math.max(maxStatusWidth, stringWidth(statusStr));
|
|
@@ -603,15 +608,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
603
608
|
}
|
|
604
609
|
const hasEmail = !!vInfo?.email;
|
|
605
610
|
const signedIn = !!vInfo?.signedIn;
|
|
606
|
-
const
|
|
607
|
-
const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot && !headless;
|
|
608
|
-
const usageUnverified = !headless && !!usageInfo?.snapshot && !!usageInfo.error;
|
|
609
|
-
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, {
|
|
610
|
-
unavailable: usageUnavailable,
|
|
611
|
-
unverified: usageUnverified,
|
|
612
|
-
headless,
|
|
613
|
-
maxWindows: usageWindowCap,
|
|
614
|
-
});
|
|
611
|
+
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, viewUsageSummaryOptions(agentId, signedIn, usageInfo, usageWindowCap));
|
|
615
612
|
const hasUsage = usageStr.length > 0;
|
|
616
613
|
// Only show lastActive for versions with an actual logged-in account.
|
|
617
614
|
// Otherwise it reflects install time (misleading "just now" for fresh installs).
|
|
@@ -731,12 +728,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
731
728
|
gMaxStatusWidth = Math.max(gMaxStatusWidth, stringWidth(formatUsageStatusBadge(gInfo?.usageStatus)));
|
|
732
729
|
const gUsageKey = getUsageLookupKey(gInfo);
|
|
733
730
|
const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
|
|
734
|
-
const
|
|
735
|
-
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
|
|
736
|
-
unverified: !gHeadless && !!gUsage?.snapshot && !!gUsage.error,
|
|
737
|
-
headless: gHeadless,
|
|
738
|
-
maxWindows: usageWindowCap,
|
|
739
|
-
});
|
|
731
|
+
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, viewUsageSummaryOptions(agentId, !!gInfo?.signedIn, gUsage, usageWindowCap));
|
|
740
732
|
gMaxUsageWidth = Math.max(gMaxUsageWidth, stringWidth(gUsageStr));
|
|
741
733
|
const gDisplay = accountColumnLabel(gInfo);
|
|
742
734
|
if (gDisplay)
|
|
@@ -754,12 +746,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
754
746
|
const parts = [` ${verLabel}${padding}`];
|
|
755
747
|
const gUsageKey = getUsageLookupKey(gInfo);
|
|
756
748
|
const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
|
|
757
|
-
const
|
|
758
|
-
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
|
|
759
|
-
unverified: !gHeadless && !!gUsage?.snapshot && !!gUsage.error,
|
|
760
|
-
headless: gHeadless,
|
|
761
|
-
maxWindows: usageWindowCap,
|
|
762
|
-
});
|
|
749
|
+
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, viewUsageSummaryOptions(agentId, !!gInfo?.signedIn, gUsage, usageWindowCap));
|
|
763
750
|
const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
|
|
764
751
|
if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
|
|
765
752
|
const gDisplay = accountColumnLabel(gInfo);
|
|
@@ -144,8 +144,7 @@ export function registerWatchdogCommand(program) {
|
|
|
144
144
|
.option('--cooldown <dur>', 'Minimum time between nudges to the same session', humanMs(DEFAULT_THRESHOLDS.cooldownMs))
|
|
145
145
|
.option('--dormant <dur>', 'Idle time after which a session is left alone (dormant)', humanMs(DEFAULT_THRESHOLDS.dormantMs))
|
|
146
146
|
.option('--text <text>', 'Nudge text delivered into the terminal', 'Continue.')
|
|
147
|
-
.option('--smart', '
|
|
148
|
-
.option('--smart-agent <agent>', 'Agent the --smart decider runs as', 'claude')
|
|
147
|
+
.option('--smart-agent <agent>', 'Agent the watchdog decider runs as', 'claude')
|
|
149
148
|
.option('--allow-ghostty-focus', 'Permit the coarse, focus-stealing Ghostty path (off by default)')
|
|
150
149
|
.option('--verbose', 'Show healthy and non-actionable session inspections too')
|
|
151
150
|
.option('--json', 'Emit the tick result as JSON (for the menu-bar / scripts)')
|
|
@@ -163,7 +162,6 @@ export function registerWatchdogCommand(program) {
|
|
|
163
162
|
const tickOnce = async (willInject, sessions) => runWatchdogPass({
|
|
164
163
|
nudge: willInject,
|
|
165
164
|
nudgeText: opts.text,
|
|
166
|
-
smart: opts.smart === true,
|
|
167
165
|
smartAgent: opts.smartAgent,
|
|
168
166
|
thresholds,
|
|
169
167
|
allowGhosttyFocus: opts.allowGhosttyFocus === true,
|
|
@@ -233,17 +231,14 @@ export function registerWatchdogCommand(program) {
|
|
|
233
231
|
agents watchdog rotate off
|
|
234
232
|
`,
|
|
235
233
|
notes: `
|
|
236
|
-
Decision path:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
WORKFLOW.md in project/user workflows/ to override the prompt + model);
|
|
245
|
-
absent one, the built-in prompt runs via 'agents run --mode plan'. Pass
|
|
246
|
-
--smart to force every stalled candidate through the brain.
|
|
234
|
+
Decision path: the watchdog is an AGENT, not a heuristic script. Every idle
|
|
235
|
+
session on this machine (its task + transcript tail) is handed to ONE
|
|
236
|
+
'agents run --mode plan' call per tick, which judges each: idle-but-unfinished
|
|
237
|
+
(was given a task, went quiet, not finished/handed off) -> NUDGE that drives
|
|
238
|
+
it to finish; idle-and-done or genuinely-needs-human -> SKIP. The agent is a
|
|
239
|
+
customizable 'watchdog' workflow (drop a WORKFLOW.md in project/user
|
|
240
|
+
workflows/ to override the prompt + model); absent one, the built-in prompt
|
|
241
|
+
runs. Nothing is nudged when nothing is idle (no agent is spawned).
|
|
247
242
|
|
|
248
243
|
Delivery (answer-router): a running agent is steered via its mailbox; a
|
|
249
244
|
parked-on-question agent is answered into its EXACT split -- tmux / iTerm /
|
|
@@ -19,6 +19,10 @@ export interface NativeAccountCapability {
|
|
|
19
19
|
scope: 'version' | 'device' | 'unsupported';
|
|
20
20
|
status: 'supported' | 'conditional' | 'discovery-only' | 'unsupported';
|
|
21
21
|
}
|
|
22
|
+
/** Native-login selector coverage required by RUSH-3053. */
|
|
23
|
+
export declare const NATIVE_ACCOUNT_SELECTOR_AGENTS: readonly ["claude", "codex", "cursor", "grok", "kimi"];
|
|
24
|
+
/** Explicit dispositions for config-isolated harnesses outside that selector. */
|
|
25
|
+
export declare const NATIVE_ACCOUNT_SELECTOR_EXCLUSIONS: Partial<Record<AgentId, string>>;
|
|
22
26
|
/** Canonical truth for native-account naming and attachment semantics. */
|
|
23
27
|
export declare const NATIVE_ACCOUNT_CAPABILITIES: Record<AgentId, NativeAccountCapability>;
|
|
24
28
|
export declare function nativeAccountCapability(agent: AgentId): NativeAccountCapability;
|