@phnx-labs/agents-cli 1.20.63 → 1.20.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -16,7 +16,7 @@ import { truncate, padRight } from '../lib/format.js';
|
|
|
16
16
|
import ora from 'ora';
|
|
17
17
|
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
18
18
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
19
|
-
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable } from '../lib/platform/index.js';
|
|
19
|
+
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
20
20
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
21
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
22
22
|
import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
@@ -25,6 +25,7 @@ import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
|
25
25
|
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
26
26
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
27
27
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
28
|
+
import { inferSessionState } from '../lib/session/state.js';
|
|
28
29
|
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
|
|
29
30
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
30
31
|
import { parseSession } from '../lib/session/parse.js';
|
|
@@ -44,6 +45,8 @@ import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
|
44
45
|
import { registerGoCommand } from './go.js';
|
|
45
46
|
import { registerFocusCommand } from './focus.js';
|
|
46
47
|
import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
48
|
+
import { registerSessionsExportCommand } from './sessions-export.js';
|
|
49
|
+
import { registerSessionsImportCommand } from './sessions-import.js';
|
|
47
50
|
import { runBrowserSessions } from '../lib/browser/sessions-list.js';
|
|
48
51
|
const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
|
|
49
52
|
/**
|
|
@@ -1321,9 +1324,9 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
1321
1324
|
return;
|
|
1322
1325
|
}
|
|
1323
1326
|
const spinner = ora(`Parsing ${session.agent} session...`).start();
|
|
1324
|
-
|
|
1327
|
+
const parsedEvents = parseSession(session.filePath, session.agent);
|
|
1325
1328
|
spinner.stop();
|
|
1326
|
-
events = filterEvents(
|
|
1329
|
+
let events = filterEvents(parsedEvents, filters);
|
|
1327
1330
|
const agentColor = colorAgent(session.agent);
|
|
1328
1331
|
console.log('');
|
|
1329
1332
|
if (mode === 'summary') {
|
|
@@ -1365,7 +1368,11 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
1365
1368
|
// engine (plan text, PR, worktree, ticket). Pre-1.20.51 emitted a bare event
|
|
1366
1369
|
// array; consumers that JSON.parse this now read `output.events` for the
|
|
1367
1370
|
// array. See issue #743 (plan surfaced) and CHANGELOG for the shape change.
|
|
1368
|
-
|
|
1371
|
+
// `todos` (RUSH-1503) is computed from the UNFILTERED transcript so the
|
|
1372
|
+
// checklist reflects true session state regardless of any `--include` filter;
|
|
1373
|
+
// it lets the Factory panel read the CLI's checklist instead of re-parsing.
|
|
1374
|
+
const todos = inferSessionState(parsedEvents, { cwd: session.cwd }).todos;
|
|
1375
|
+
process.stdout.write(renderJson(events, todos ? { ...session, todos } : session));
|
|
1369
1376
|
}
|
|
1370
1377
|
function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
1371
1378
|
const lbl = (label ?? '').trim();
|
|
@@ -1611,11 +1618,35 @@ export async function resumeSessionInPlace(session) {
|
|
|
1611
1618
|
}
|
|
1612
1619
|
await spawnResumeCommand(resume, cwd);
|
|
1613
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
1623
|
+
*
|
|
1624
|
+
* On Windows the agent launcher is a `.cmd`/PATHEXT shim and needs shell:true.
|
|
1625
|
+
* With shell:true, Node concatenates args into the cmd.exe line unescaped
|
|
1626
|
+
* (DEP0190 + injection). A session.id derived from a filename can carry
|
|
1627
|
+
* metacharacters (`&|<>`); compose a fully-quoted line and pass an EMPTY args
|
|
1628
|
+
* array so cmd.exe cannot reparse them (RUSH-1753). See composeWin32CommandLine.
|
|
1629
|
+
*
|
|
1630
|
+
* `platform` is injectable so the win32 shell path is unit-testable on any host.
|
|
1631
|
+
*/
|
|
1632
|
+
export function resumeSpawnInvocation(cmd, platform = process.platform) {
|
|
1633
|
+
const shell = needsWindowsShell(cmd[0], platform);
|
|
1634
|
+
if (shell) {
|
|
1635
|
+
return {
|
|
1636
|
+
command: composeWin32CommandLine(cmd[0], cmd.slice(1)),
|
|
1637
|
+
args: [],
|
|
1638
|
+
shell: true,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
return { command: cmd[0], args: cmd.slice(1), shell: false };
|
|
1642
|
+
}
|
|
1614
1643
|
/**
|
|
1615
1644
|
* Spawn a resume command as a foreground takeover (inherited stdio), resolving
|
|
1616
1645
|
* when it exits. On Windows the agent launcher is a `.cmd`/PATHEXT shim that
|
|
1617
1646
|
* `spawn` can't exec directly — a bare-name `shell:false` spawn throws
|
|
1618
1647
|
* `EFTYPE`/`ENOENT` there — so we go through the shell via `needsWindowsShell`.
|
|
1648
|
+
* When shell:true, argv is composed via resumeSpawnInvocation (quoted line +
|
|
1649
|
+
* empty args) so an untrusted session.id cannot inject through cmd.exe.
|
|
1619
1650
|
* The spawn is guarded because such a failure can be thrown synchronously;
|
|
1620
1651
|
* without the guard it would surface under an unrelated "Failed to discover
|
|
1621
1652
|
* sessions" catch upstream instead of a truthful launch error.
|
|
@@ -1624,10 +1655,11 @@ function spawnResumeCommand(cmd, cwd) {
|
|
|
1624
1655
|
return new Promise((resolve) => {
|
|
1625
1656
|
let child;
|
|
1626
1657
|
try {
|
|
1627
|
-
|
|
1658
|
+
const { command, args, shell } = resumeSpawnInvocation(cmd);
|
|
1659
|
+
child = spawn(command, args, {
|
|
1628
1660
|
cwd,
|
|
1629
1661
|
stdio: 'inherit',
|
|
1630
|
-
shell
|
|
1662
|
+
shell,
|
|
1631
1663
|
});
|
|
1632
1664
|
}
|
|
1633
1665
|
catch (err) {
|
|
@@ -2122,6 +2154,8 @@ export function registerSessionsCommands(program) {
|
|
|
2122
2154
|
registerGoCommand(sessionsCmd);
|
|
2123
2155
|
registerFocusCommand(sessionsCmd);
|
|
2124
2156
|
registerSessionsInjectCommand(sessionsCmd);
|
|
2157
|
+
registerSessionsExportCommand(sessionsCmd);
|
|
2158
|
+
registerSessionsImportCommand(sessionsCmd);
|
|
2125
2159
|
}
|
|
2126
2160
|
function formatNoSessionsMessage(showAll, project) {
|
|
2127
2161
|
const projectQuery = project?.trim();
|
package/dist/commands/ssh.js
CHANGED
|
@@ -15,9 +15,11 @@ import * as os from 'os';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import ora from 'ora';
|
|
18
|
-
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
18
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
|
|
19
19
|
import { machineId } from '../lib/session/sync/config.js';
|
|
20
20
|
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
|
|
21
|
+
import { addControlToken } from '../lib/serve/token.js';
|
|
22
|
+
import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
|
|
21
23
|
import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
|
|
22
24
|
import { localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
|
|
23
25
|
import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-target.js';
|
|
@@ -26,6 +28,7 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
|
26
28
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
27
29
|
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
28
30
|
import { planFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
31
|
+
import { fleetCapacity, fmtBytes, headroom, probeFleetStats, } from '../lib/devices/health.js';
|
|
29
32
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
30
33
|
* command is running on so it stands out from the rest of the tailnet. */
|
|
31
34
|
function deviceSummary(d, isSelf = false) {
|
|
@@ -41,6 +44,80 @@ function deviceSummary(d, isSelf = false) {
|
|
|
41
44
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
42
45
|
return `${marker}${name} ${String(d.platform).padEnd(8)} ${(d.user ? d.user + '@' : '') + addr} ${online}${reach}${here}`;
|
|
43
46
|
}
|
|
47
|
+
const HEADROOM_BADGE = {
|
|
48
|
+
idle: chalk.green('○ idle'),
|
|
49
|
+
light: chalk.green('● light'),
|
|
50
|
+
busy: chalk.yellow('● busy'),
|
|
51
|
+
loaded: chalk.red('● loaded'),
|
|
52
|
+
unknown: chalk.gray('· —'),
|
|
53
|
+
};
|
|
54
|
+
/** A right-aligned percentage cell, colored by severity (green/yellow/red). */
|
|
55
|
+
function pctCell(v, width) {
|
|
56
|
+
if (v === undefined)
|
|
57
|
+
return chalk.gray('—'.padStart(width));
|
|
58
|
+
const s = `${Math.round(v)}%`.padStart(width);
|
|
59
|
+
if (v < 40)
|
|
60
|
+
return chalk.green(s);
|
|
61
|
+
if (v < 75)
|
|
62
|
+
return chalk.yellow(s);
|
|
63
|
+
return chalk.red(s);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Render the device list. When `statsMap` is provided, resource columns are
|
|
67
|
+
* appended — normalized load, memory, a headroom badge, and (in `full` mode)
|
|
68
|
+
* core count and free/total memory — so it's obvious which boxes have room.
|
|
69
|
+
* Without it (probe skipped) the classic reachability line is used. A fleet
|
|
70
|
+
* capacity summary is appended whenever stats were gathered.
|
|
71
|
+
*/
|
|
72
|
+
function renderDeviceTable(reg, names, self, statsMap, full = false) {
|
|
73
|
+
if (!statsMap)
|
|
74
|
+
return names.map((n) => deviceSummary(reg[n], n === self));
|
|
75
|
+
const lines = [];
|
|
76
|
+
const head = ' ' +
|
|
77
|
+
chalk.gray('device'.padEnd(16)) +
|
|
78
|
+
chalk.gray('platform'.padEnd(8)) +
|
|
79
|
+
' ' +
|
|
80
|
+
(full ? chalk.gray('cores'.padStart(6)) : '') +
|
|
81
|
+
chalk.gray('load'.padStart(5)) +
|
|
82
|
+
chalk.gray('mem'.padStart(6)) +
|
|
83
|
+
(full ? ' ' + chalk.gray('free/total'.padEnd(12)) : '') +
|
|
84
|
+
' ' +
|
|
85
|
+
chalk.gray('headroom');
|
|
86
|
+
lines.push(head);
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
const d = reg[name];
|
|
89
|
+
const isSelf = name === self;
|
|
90
|
+
const marker = isSelf ? chalk.cyan('▸ ') : ' ';
|
|
91
|
+
const label = isSelf ? chalk.bold.cyan(name.padEnd(16)) : chalk.bold(name.padEnd(16));
|
|
92
|
+
const plat = String(d.platform).padEnd(8);
|
|
93
|
+
const offline = d.tailscale && !d.tailscale.online;
|
|
94
|
+
const stats = statsMap.get(name);
|
|
95
|
+
if (offline) {
|
|
96
|
+
lines.push(`${marker}${label}${plat} ${chalk.gray('offline')}`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const relay = !isSelf && d.tailscale?.online && !d.tailscale.direct ? chalk.yellow(' relay') : '';
|
|
100
|
+
const cores = full ? chalk.gray(String(stats?.ncpu ?? '—').padStart(6)) : '';
|
|
101
|
+
const load = pctCell(stats?.loadPercent, 5);
|
|
102
|
+
const mem = pctCell(stats?.memPercent, 6);
|
|
103
|
+
const freeTotal = full
|
|
104
|
+
? ' ' +
|
|
105
|
+
(stats?.reachable && stats.memTotalBytes
|
|
106
|
+
? `${fmtBytes(stats.memFreeBytes)}/${fmtBytes(stats.memTotalBytes)}`.padEnd(12)
|
|
107
|
+
: chalk.gray('—'.padEnd(12)))
|
|
108
|
+
: '';
|
|
109
|
+
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
110
|
+
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
111
|
+
lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}`);
|
|
112
|
+
}
|
|
113
|
+
// Fleet capacity summary — total cores + how much RAM is free right now.
|
|
114
|
+
const cap = fleetCapacity(statsMap.values());
|
|
115
|
+
if (cap.reachable > 0) {
|
|
116
|
+
const freePct = cap.memTotalBytes > 0 ? Math.round((cap.memFreeBytes / cap.memTotalBytes) * 100) : 0;
|
|
117
|
+
lines.push(chalk.gray(` Fleet capacity: ${cap.cores} cores · ${fmtBytes(cap.memFreeBytes)} free / ${fmtBytes(cap.memTotalBytes)} RAM (${freePct}% free) across ${cap.reachable} reachable device${cap.reachable === 1 ? '' : 's'}`));
|
|
118
|
+
}
|
|
119
|
+
return lines;
|
|
120
|
+
}
|
|
44
121
|
/** Resolve a device or exit with a clear error. */
|
|
45
122
|
async function mustGetDevice(name) {
|
|
46
123
|
const d = await getDevice(name);
|
|
@@ -230,12 +307,15 @@ Typical workflow:
|
|
|
230
307
|
devicesCmd
|
|
231
308
|
.command('list')
|
|
232
309
|
.alias('ls')
|
|
233
|
-
.description('List registered devices with platform, address, and
|
|
310
|
+
.description('List registered devices with platform, address, reachability, and live resource headroom.')
|
|
234
311
|
.option('--json', 'output the registry as a JSON array (for scripts and hooks)')
|
|
312
|
+
.option('--no-stats', 'skip the live resource probe (instant; names/addresses only)')
|
|
313
|
+
.option('-f, --full', 'full mode: add per-device core count and free/total memory')
|
|
235
314
|
.action(async (opts) => {
|
|
236
315
|
const reg = await loadDevices();
|
|
237
316
|
const names = Object.keys(reg).sort();
|
|
238
317
|
if (opts.json) {
|
|
318
|
+
// Registry-only, always fast — the Factory extension polls this path.
|
|
239
319
|
process.stdout.write(JSON.stringify(names.map((n) => reg[n]), null, 2) + '\n');
|
|
240
320
|
return;
|
|
241
321
|
}
|
|
@@ -244,9 +324,26 @@ Typical workflow:
|
|
|
244
324
|
return;
|
|
245
325
|
}
|
|
246
326
|
const self = machineId();
|
|
327
|
+
let statsMap;
|
|
328
|
+
if (opts.stats !== false) {
|
|
329
|
+
// Probe only reachable devices, in parallel, bounded by the per-probe
|
|
330
|
+
// timeout — a slow box degrades to "—", it never hangs the table.
|
|
331
|
+
const probeable = planFleetTargets(reg)
|
|
332
|
+
.filter((t) => !t.skip)
|
|
333
|
+
.map((t) => t.device);
|
|
334
|
+
const spinner = isInteractiveTerminal()
|
|
335
|
+
? ora(`Probing ${probeable.length} device${probeable.length === 1 ? '' : 's'}…`).start()
|
|
336
|
+
: undefined;
|
|
337
|
+
try {
|
|
338
|
+
statsMap = await probeFleetStats(probeable, { selfName: self });
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
spinner?.stop();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
247
344
|
console.log(chalk.bold(`Devices (${names.length})`));
|
|
248
|
-
for (const
|
|
249
|
-
console.log(
|
|
345
|
+
for (const line of renderDeviceTable(reg, names, self, statsMap, opts.full))
|
|
346
|
+
console.log(line);
|
|
250
347
|
});
|
|
251
348
|
devicesCmd
|
|
252
349
|
.command('show <name>')
|
|
@@ -305,6 +402,47 @@ Typical workflow:
|
|
|
305
402
|
process.exit(1);
|
|
306
403
|
}
|
|
307
404
|
});
|
|
405
|
+
devicesCmd
|
|
406
|
+
.command('pair-ios [name]')
|
|
407
|
+
.description('Pair an iPhone/iPad cockpit (RUSH-1733): mint a control token for `agents serve --control` and mark the device control-only. The token is shown ONCE — enter it in the app. Run this on the anchor.')
|
|
408
|
+
.option('--port <n>', 'Anchor control port to advertise to the app', String(DEFAULT_SERVE_PORT))
|
|
409
|
+
.action(async (name, opts) => {
|
|
410
|
+
const label = (name || 'iphone').trim();
|
|
411
|
+
// Mint the bearer token — hash-only on disk, raw shown once (see serve/token.ts).
|
|
412
|
+
const { id, token } = addControlToken(label);
|
|
413
|
+
// If a device with this name is already registered (e.g. discovered over
|
|
414
|
+
// Tailscale as an `unknown`-platform node), mark it control-only so the
|
|
415
|
+
// fleet stops trying to dial it for sessions/placement.
|
|
416
|
+
let marked = false;
|
|
417
|
+
let unknownName = false;
|
|
418
|
+
if (name) {
|
|
419
|
+
const existing = await getDevice(name);
|
|
420
|
+
if (existing) {
|
|
421
|
+
await upsertDevice(name, { role: 'control' });
|
|
422
|
+
marked = true;
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
unknownName = true;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const port = parseInt(opts.port ?? '', 10) || DEFAULT_SERVE_PORT;
|
|
429
|
+
console.log(chalk.green(`Paired cockpit '${label}'`) + chalk.gray(` (token id ${id})`));
|
|
430
|
+
if (marked)
|
|
431
|
+
console.log(chalk.gray(`Marked device '${name}' role=control — the fleet won't dial it.`));
|
|
432
|
+
if (unknownName) {
|
|
433
|
+
console.log(chalk.yellow(`Note: no registered device named '${name}' — token minted, but no device was marked role=control.`) +
|
|
434
|
+
chalk.gray(` Run \`agents devices sync\` first if this phone should appear in the fleet.`));
|
|
435
|
+
}
|
|
436
|
+
console.log();
|
|
437
|
+
console.log(chalk.bold('Control token (shown once — enter it in the app):'));
|
|
438
|
+
console.log(' ' + chalk.cyan(token));
|
|
439
|
+
console.log();
|
|
440
|
+
console.log('On the anchor, expose the control server on your tailnet:');
|
|
441
|
+
console.log(chalk.gray(` agents serve --control --bind <anchor-tailnet-ip> --port ${port}`));
|
|
442
|
+
console.log('Then point the app at:');
|
|
443
|
+
console.log(chalk.gray(` http://<anchor-tailnet-ip>:${port} (Bearer: the token above)`));
|
|
444
|
+
console.log(chalk.yellow('Keep the control server on the tailnet — never public Funnel.'));
|
|
445
|
+
});
|
|
308
446
|
devicesCmd
|
|
309
447
|
.command('rm <name>')
|
|
310
448
|
.alias('remove')
|
|
@@ -449,7 +587,7 @@ async function runAskpass() {
|
|
|
449
587
|
process.exit(1);
|
|
450
588
|
}
|
|
451
589
|
try {
|
|
452
|
-
const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh' });
|
|
590
|
+
const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', agentOnly: isHeadlessSecretsContext() });
|
|
453
591
|
const value = env[key];
|
|
454
592
|
if (value === undefined) {
|
|
455
593
|
console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
|
package/dist/commands/usage.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Lists every installed agent with the best available usage snapshot:
|
|
5
5
|
* - claude: live OAuth API call (cached for 2 minutes)
|
|
6
6
|
* - codex: parsed from latest session log's rate_limits event
|
|
7
|
+
* - kimi: live Kimi Code /usages API call (cached for 2 minutes)
|
|
8
|
+
* - droid: live Factory billing/limits API call (cached for 2 minutes)
|
|
7
9
|
* - others: marked as "not exposed by CLI" (Gemini, OpenCode, Cursor, etc.
|
|
8
10
|
* don't publish per-account usage today)
|
|
9
11
|
*/
|
package/dist/commands/usage.js
CHANGED
|
@@ -3,8 +3,13 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
|
|
4
4
|
import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
|
|
5
5
|
import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Agents whose CLI surfaces usage data we can read today. Kept in sync with the
|
|
8
|
+
* live/last-seen sources `getUsageInfo` dispatches on in `../lib/usage.js`
|
|
9
|
+
* (claude, codex, kimi, droid) — an agent with a usage source but missing here
|
|
10
|
+
* would wrongly print "does not publish usage data" for a signed-in account.
|
|
11
|
+
*/
|
|
12
|
+
const USAGE_SUPPORTED = new Set(['claude', 'codex', 'kimi', 'droid']);
|
|
8
13
|
export function registerUsageCommand(program) {
|
|
9
14
|
addHostOption(program.command('usage [agent]'))
|
|
10
15
|
.description('Show rate-limit / quota usage per agent')
|
|
@@ -4,7 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { select, confirm, checkbox } from '@inquirer/prompts';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
|
|
8
8
|
import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
9
|
import { viewAction } from './view.js';
|
|
10
10
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
@@ -32,8 +32,12 @@ function fixSessionFilePaths(agent, version, oldVersionDir) {
|
|
|
32
32
|
}
|
|
33
33
|
function formatAccountHint(info, usage) {
|
|
34
34
|
const parts = [];
|
|
35
|
-
if (info.email)
|
|
36
|
-
|
|
35
|
+
if (info.email) {
|
|
36
|
+
// Same-email accounts can live in different orgs (personal Max vs a Team
|
|
37
|
+
// seat) — the badge is what makes the picker disambiguate them.
|
|
38
|
+
const badge = accountOrgBadge(info);
|
|
39
|
+
parts.push(badge ? `${info.email} (${badge})` : info.email);
|
|
40
|
+
}
|
|
37
41
|
const usageSummary = formatUsageSummary(info.plan, usage);
|
|
38
42
|
if (usageSummary)
|
|
39
43
|
parts.push(usageSummary);
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
* rules, hooks, and promptcuts synced to that version.
|
|
8
8
|
*/
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
|
+
import type { AccountInfo } from '../lib/agents.js';
|
|
10
11
|
import type { AgentId } from '../lib/types.js';
|
|
11
12
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
13
|
+
/**
|
|
14
|
+
* Text for the account (email) column. Prefers the real email; otherwise, for a
|
|
15
|
+
* signed-in agent whose credential carries no email but does carry an opaque
|
|
16
|
+
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
17
|
+
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
18
|
+
* have neither (Antigravity), and empty when signed out.
|
|
19
|
+
*
|
|
20
|
+
* When the account carries a Claude organizationType, the org badge is appended
|
|
21
|
+
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
22
|
+
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
23
|
+
* suffix is plain text inside the label so the existing column-width pass
|
|
24
|
+
* measures and pads it for free.
|
|
25
|
+
*/
|
|
26
|
+
export declare function accountColumnLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
12
27
|
type SyncState = 'synced' | 'new' | 'modified' | 'deleted';
|
|
13
28
|
/** Per-section filter flags. When any are true, only those sections render. */
|
|
14
29
|
export interface ViewSectionFilter {
|
|
@@ -32,6 +47,8 @@ export interface ViewJsonVersion {
|
|
|
32
47
|
signedIn: boolean;
|
|
33
48
|
email: string | null;
|
|
34
49
|
accountId?: string | null;
|
|
50
|
+
organizationType?: string | null;
|
|
51
|
+
organizationName?: string | null;
|
|
35
52
|
plan: string | null;
|
|
36
53
|
usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null;
|
|
37
54
|
overageCredits?: {
|
|
@@ -39,7 +56,7 @@ export interface ViewJsonVersion {
|
|
|
39
56
|
currency: string;
|
|
40
57
|
} | null;
|
|
41
58
|
windows: Array<{
|
|
42
|
-
key: 'session' | 'week' | 'sonnet_week';
|
|
59
|
+
key: 'session' | 'week' | 'sonnet_week' | 'month';
|
|
43
60
|
usedPercent: number;
|
|
44
61
|
resetsAt: string | null;
|
|
45
62
|
}>;
|
|
@@ -86,6 +103,15 @@ export declare function parseResourceSections(options: {
|
|
|
86
103
|
resources?: string | boolean;
|
|
87
104
|
detailed?: boolean;
|
|
88
105
|
} & ViewSectionFilter, jsonMode: boolean): Set<ResourceSection>;
|
|
106
|
+
/**
|
|
107
|
+
* Identity key for duplicate-install detection. Prefers accountKey — which
|
|
108
|
+
* encodes account AND org — over the bare email: two installs can share an
|
|
109
|
+
* email yet belong to different orgs (a personal Max plan and a Team seat),
|
|
110
|
+
* and grouping those by email alone would propose pruning a live account.
|
|
111
|
+
* Falls back to the lowercased email for agents whose credentials expose no
|
|
112
|
+
* identity key. Null when there is no usable identity.
|
|
113
|
+
*/
|
|
114
|
+
export declare function pruneGroupKey(info: Pick<AccountInfo, 'accountKey' | 'email'>): string | null;
|
|
89
115
|
/**
|
|
90
116
|
* Prune older installed versions that share an email with a newer installed
|
|
91
117
|
* version. Keeps the highest semver per email, skips the global default (with
|
package/dist/commands/view.js
CHANGED
|
@@ -4,7 +4,7 @@ import { visibleWidth, termLink } from '../lib/format.js';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
8
|
import { deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
9
|
import { readManifest } from '../lib/manifest.js';
|
|
10
10
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
@@ -34,12 +34,20 @@ const SIGNED_IN_LABEL = 'signed in';
|
|
|
34
34
|
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
35
35
|
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
36
36
|
* have neither (Antigravity), and empty when signed out.
|
|
37
|
+
*
|
|
38
|
+
* When the account carries a Claude organizationType, the org badge is appended
|
|
39
|
+
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
40
|
+
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
41
|
+
* suffix is plain text inside the label so the existing column-width pass
|
|
42
|
+
* measures and pads it for free.
|
|
37
43
|
*/
|
|
38
|
-
function accountColumnLabel(info) {
|
|
44
|
+
export function accountColumnLabel(info) {
|
|
39
45
|
if (!info)
|
|
40
46
|
return '';
|
|
41
|
-
if (info.email)
|
|
42
|
-
|
|
47
|
+
if (info.email) {
|
|
48
|
+
const badge = accountOrgBadge(info);
|
|
49
|
+
return badge ? `${info.email} (${badge})` : info.email;
|
|
50
|
+
}
|
|
43
51
|
if (info.signedIn)
|
|
44
52
|
return info.accountId ? `id:${info.accountId}` : SIGNED_IN_LABEL;
|
|
45
53
|
return '';
|
|
@@ -1149,6 +1157,8 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1149
1157
|
signedIn: info.signedIn,
|
|
1150
1158
|
email: info.email,
|
|
1151
1159
|
accountId: info.accountId,
|
|
1160
|
+
organizationType: info.organizationType ?? null,
|
|
1161
|
+
organizationName: info.organizationName ?? null,
|
|
1152
1162
|
plan: info.plan,
|
|
1153
1163
|
usageStatus: info.usageStatus,
|
|
1154
1164
|
overageCredits: info.overageCredits,
|
|
@@ -1184,6 +1194,17 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1184
1194
|
}
|
|
1185
1195
|
return out;
|
|
1186
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Identity key for duplicate-install detection. Prefers accountKey — which
|
|
1199
|
+
* encodes account AND org — over the bare email: two installs can share an
|
|
1200
|
+
* email yet belong to different orgs (a personal Max plan and a Team seat),
|
|
1201
|
+
* and grouping those by email alone would propose pruning a live account.
|
|
1202
|
+
* Falls back to the lowercased email for agents whose credentials expose no
|
|
1203
|
+
* identity key. Null when there is no usable identity.
|
|
1204
|
+
*/
|
|
1205
|
+
export function pruneGroupKey(info) {
|
|
1206
|
+
return info.accountKey ?? info.email?.toLowerCase() ?? null;
|
|
1207
|
+
}
|
|
1187
1208
|
async function buildAgentPrunePlan(agentId) {
|
|
1188
1209
|
const dirInfos = listInstalledVersionDirs(agentId);
|
|
1189
1210
|
const entries = await Promise.all(dirInfos.map(async ({ version, hasBinary }) => {
|
|
@@ -1198,16 +1219,18 @@ async function buildAgentPrunePlan(agentId) {
|
|
|
1198
1219
|
// working binary — those are the things that compete for "the live install
|
|
1199
1220
|
// for this account."
|
|
1200
1221
|
const installed = entries.filter((e) => e.hasBinary);
|
|
1201
|
-
const
|
|
1222
|
+
const byAccount = new Map();
|
|
1202
1223
|
for (const e of installed) {
|
|
1203
1224
|
if (!e.info.email)
|
|
1204
1225
|
continue;
|
|
1205
|
-
const key = e.info
|
|
1206
|
-
|
|
1226
|
+
const key = pruneGroupKey(e.info);
|
|
1227
|
+
if (!key)
|
|
1228
|
+
continue;
|
|
1229
|
+
const list = byAccount.get(key) ?? [];
|
|
1207
1230
|
list.push(e);
|
|
1208
|
-
|
|
1231
|
+
byAccount.set(key, list);
|
|
1209
1232
|
}
|
|
1210
|
-
for (const [, group] of
|
|
1233
|
+
for (const [, group] of byAccount) {
|
|
1211
1234
|
if (group.length < 2)
|
|
1212
1235
|
continue;
|
|
1213
1236
|
const sorted = [...group].sort((a, b) => compareVersions(b.version, a.version));
|
package/dist/commands/webhook.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
|
-
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
3
|
-
import { startWebhookServer } from '../lib/triggers/webhook.js';
|
|
3
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
|
|
4
|
+
import { createFileDeliveryStore, startWebhookServer } from '../lib/triggers/webhook.js';
|
|
5
|
+
import { getRuntimeStateDir } from '../lib/state.js';
|
|
4
6
|
const DEFAULT_HOST = '127.0.0.1';
|
|
5
7
|
const DEFAULT_PORT = 8787;
|
|
6
8
|
function positiveInt(value, fallback) {
|
|
@@ -10,6 +12,9 @@ function positiveInt(value, fallback) {
|
|
|
10
12
|
function readWebhookSecrets(bundleName) {
|
|
11
13
|
const { env } = readAndResolveBundleEnv(bundleName, {
|
|
12
14
|
caller: 'webhook serve',
|
|
15
|
+
// `webhook serve` is a long-running background server; when started detached
|
|
16
|
+
// (no TTY) it must resolve broker-only rather than pop an unanswerable prompt.
|
|
17
|
+
agentOnly: isHeadlessSecretsContext(),
|
|
13
18
|
});
|
|
14
19
|
const secrets = {};
|
|
15
20
|
if (env.GITHUB_WEBHOOK_SECRET)
|
|
@@ -69,6 +74,9 @@ export function registerWebhookCommand(program) {
|
|
|
69
74
|
port,
|
|
70
75
|
secrets,
|
|
71
76
|
rateLimitPerMinute: rateLimit,
|
|
77
|
+
// Durable delivery dedup: replays survive a receiver restart (an
|
|
78
|
+
// in-memory store would forget every seen delivery on restart).
|
|
79
|
+
deliveryStore: createFileDeliveryStore(path.join(getRuntimeStateDir(), 'webhook', 'deliveries.json')),
|
|
72
80
|
onDelivery: (webhook, fired) => {
|
|
73
81
|
console.log(`${new Date().toISOString()} ${webhook.source}:${webhook.event} ` +
|
|
74
82
|
`${fired.length ? `fired ${fired.map((f) => f.jobName).join(', ')}` : 'no match'}`);
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
26
26
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
27
27
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
28
28
|
const VERSION = packageJson.version;
|
|
29
|
-
import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, } from './lib/self-update.js';
|
|
29
|
+
import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPackageIntoPrefix, installPackageWithBun, verifyInstalledVersion, refreshAliasShims, downloadVerifiedTarball, } from './lib/self-update.js';
|
|
30
30
|
// Detect dev/working-tree builds and default the noisy startup steps off.
|
|
31
31
|
// Three cases trip this:
|
|
32
32
|
// 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
|
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -315,10 +315,12 @@ async function fetchNpmPackageMetadata(versionOrTag = 'latest', timeoutMs = 5000
|
|
|
315
315
|
throw new Error('Could not reach npm registry');
|
|
316
316
|
}
|
|
317
317
|
const data = await response.json();
|
|
318
|
-
if (typeof data.version !== 'string' ||
|
|
319
|
-
|
|
318
|
+
if (typeof data.version !== 'string' ||
|
|
319
|
+
typeof data.dist?.integrity !== 'string' ||
|
|
320
|
+
typeof data.dist?.tarball !== 'string') {
|
|
321
|
+
throw new Error('npm registry response did not include version, integrity, and tarball');
|
|
320
322
|
}
|
|
321
|
-
return { version: data.version, integrity: data.dist.integrity };
|
|
323
|
+
return { version: data.version, integrity: data.dist.integrity, tarball: data.dist.tarball };
|
|
322
324
|
}
|
|
323
325
|
function printResolvedPackage(metadata) {
|
|
324
326
|
console.log(chalk.gray(`Resolved: ${NPM_PACKAGE_NAME}@${metadata.version}`));
|
|
@@ -326,16 +328,32 @@ function printResolvedPackage(metadata) {
|
|
|
326
328
|
}
|
|
327
329
|
async function installResolvedPackage(metadata) {
|
|
328
330
|
const packageRoot = path.resolve(__dirname, '..');
|
|
329
|
-
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
// Download the published tarball and prove its bytes match the registry
|
|
332
|
+
// integrity BEFORE installing anything. A `name@version` spec would let the
|
|
333
|
+
// package manager fetch and install whatever the registry serves with no
|
|
334
|
+
// hash check on our side; instead we verify here and install the LOCAL, now
|
|
335
|
+
// trusted .tgz. A mismatch throws and nothing below runs — fail closed.
|
|
336
|
+
const tarball = await downloadVerifiedTarball(metadata.tarball, metadata.integrity);
|
|
337
|
+
try {
|
|
338
|
+
// Upgrade with the package manager that owns this install. A bun global
|
|
339
|
+
// install lives at <bunGlobalDir>/node_modules/... (no `lib` segment), so an
|
|
340
|
+
// `npm install --prefix` would write to <bunGlobalDir>/lib/node_modules and
|
|
341
|
+
// never touch the running copy — npm exits 0, the verify below fails.
|
|
342
|
+
if (detectPackageManager(packageRoot) === 'bun') {
|
|
343
|
+
await installPackageWithBun(tarball);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
await installPackageIntoPrefix(tarball, deriveGlobalPrefix(packageRoot));
|
|
347
|
+
}
|
|
336
348
|
}
|
|
337
|
-
|
|
338
|
-
|
|
349
|
+
finally {
|
|
350
|
+
// Best-effort cleanup of the verified tarball and its temp dir.
|
|
351
|
+
try {
|
|
352
|
+
fs.rmSync(path.dirname(tarball), { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
/* leave it for the OS temp sweep */
|
|
356
|
+
}
|
|
339
357
|
}
|
|
340
358
|
verifyInstalledVersion(packageRoot, metadata.version);
|
|
341
359
|
refreshAliasShims(packageRoot);
|
|
@@ -669,6 +687,7 @@ async function registerAllEagerCommands() {
|
|
|
669
687
|
await reg(loadPackages);
|
|
670
688
|
await reg(loadDaemon);
|
|
671
689
|
await reg(loadRoutines);
|
|
690
|
+
await reg(loadMonitors);
|
|
672
691
|
await reg(loadRun);
|
|
673
692
|
await reg(loadDefaults);
|
|
674
693
|
await reg(loadModels);
|
|
@@ -676,6 +695,7 @@ async function registerAllEagerCommands() {
|
|
|
676
695
|
await reg(loadTrash);
|
|
677
696
|
await reg(loadRestore);
|
|
678
697
|
await reg(loadDoctor);
|
|
698
|
+
await reg(loadApply);
|
|
679
699
|
await reg(loadCheck);
|
|
680
700
|
await reg(loadStatus);
|
|
681
701
|
registerExecAliasCommand(program);
|
|
@@ -707,6 +727,7 @@ async function registerAllEagerCommands() {
|
|
|
707
727
|
await reg(loadWebhook);
|
|
708
728
|
await reg(loadFunnel);
|
|
709
729
|
await reg(loadFeed);
|
|
730
|
+
await reg(loadMailboxes);
|
|
710
731
|
await reg(loadSsh);
|
|
711
732
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
712
733
|
registerJobsCronAliasCommand(program, 'cron');
|