@phnx-labs/agents-cli 1.20.63 → 1.20.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
package/dist/lib/daemon.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
|
14
14
|
import { isAlive, killTree, backgroundSpawnOptions } from './platform/index.js';
|
|
15
15
|
import { listJobs as listAllJobs } from './routines.js';
|
|
16
16
|
import { JobScheduler } from './scheduler.js';
|
|
17
|
+
import { MonitorEngine } from './monitors/engine.js';
|
|
17
18
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
18
19
|
import { detectOverdueJobs, notifyOverdue } from './overdue.js';
|
|
19
20
|
import { BrowserService } from './browser/service.js';
|
|
@@ -293,6 +294,21 @@ export async function runDaemon() {
|
|
|
293
294
|
process.exit(0);
|
|
294
295
|
}
|
|
295
296
|
log('INFO', `Daemon started (PID: ${process.pid})`);
|
|
297
|
+
// RUSH-1759: the launchd plist / systemd unit no longer bake the Claude OAuth
|
|
298
|
+
// token onto disk. Obtain it here from the secure `claude` secrets bundle and
|
|
299
|
+
// inject into this process's env so every routine run this daemon spawns still
|
|
300
|
+
// receives it (via the sandbox allowlist), without the token ever being
|
|
301
|
+
// persisted in the service manifest. A read from a file-backed store (Linux)
|
|
302
|
+
// needs no prompt; on macOS it resolves broker-only from an unlocked
|
|
303
|
+
// secrets-agent and is otherwise absent (leaving the daemon on its existing
|
|
304
|
+
// interactive OAuth session), matching the detached-start path. Never blocks.
|
|
305
|
+
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
306
|
+
const oauthToken = readDaemonClaudeOAuthToken();
|
|
307
|
+
if (oauthToken) {
|
|
308
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
309
|
+
log('INFO', 'Loaded Claude OAuth token from secrets bundle for routine runs');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
296
312
|
// Reap any stray duplicate daemon of this install that slipped past the start
|
|
297
313
|
// lock or was orphaned by a hard-crash — before it can double-fire jobs.
|
|
298
314
|
try {
|
|
@@ -329,7 +345,12 @@ export async function runDaemon() {
|
|
|
329
345
|
log('WARN', `Secrets broker host skipped: ${err.message}`);
|
|
330
346
|
}
|
|
331
347
|
const scheduler = new JobScheduler(async (config) => {
|
|
332
|
-
|
|
348
|
+
const jobLabel = config.command
|
|
349
|
+
? 'command'
|
|
350
|
+
: config.workflow
|
|
351
|
+
? `workflow: ${config.workflow}`
|
|
352
|
+
: `agent: ${config.agent}`;
|
|
353
|
+
log('INFO', `Triggering job '${config.name}' (${jobLabel})`);
|
|
333
354
|
try {
|
|
334
355
|
const meta = await executeJobDetached(config);
|
|
335
356
|
log('INFO', `Job '${config.name}' spawned (run: ${meta.runId}, PID: ${meta.pid})`);
|
|
@@ -344,6 +365,16 @@ export async function runDaemon() {
|
|
|
344
365
|
for (const job of scheduled) {
|
|
345
366
|
log('INFO', ` ${job.name} -> next: ${job.nextRun?.toISOString() || 'unknown'}`);
|
|
346
367
|
}
|
|
368
|
+
// Monitor engine: event-triggered watchers, beside the cron scheduler. Same
|
|
369
|
+
// daemon, same dispatch seam — a monitor is a routine whose trigger is a
|
|
370
|
+
// watched source instead of a clock. Reloads on SIGHUP alongside the scheduler.
|
|
371
|
+
const monitorEngine = new MonitorEngine((level, message) => log(level, message));
|
|
372
|
+
try {
|
|
373
|
+
monitorEngine.start();
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
log('ERROR', `Monitor engine failed to start: ${err.message}`);
|
|
377
|
+
}
|
|
347
378
|
// Backlog detection: any enabled recurring job whose most-recent expected
|
|
348
379
|
// fire is older than its most-recent recorded run is overdue. Happens when
|
|
349
380
|
// the laptop was off or the daemon crashed through a scheduled fire.
|
|
@@ -598,6 +629,12 @@ export async function runDaemon() {
|
|
|
598
629
|
scheduler.reloadAll();
|
|
599
630
|
const reloaded = scheduler.listScheduled();
|
|
600
631
|
log('INFO', `Reloaded ${reloaded.length} jobs`);
|
|
632
|
+
try {
|
|
633
|
+
monitorEngine.reload();
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
log('ERROR', `Monitor engine reload failed: ${err.message}`);
|
|
637
|
+
}
|
|
601
638
|
// Drop the memoized R2 config so rotated/added sync credentials are re-read
|
|
602
639
|
// on the next cycle instead of waiting for a restart.
|
|
603
640
|
void import('./session/sync/config.js').then(m => m.clearR2ConfigCache());
|
|
@@ -605,6 +642,7 @@ export async function runDaemon() {
|
|
|
605
642
|
const handleShutdown = async () => {
|
|
606
643
|
log('INFO', 'Daemon shutting down');
|
|
607
644
|
scheduler.stopAll();
|
|
645
|
+
monitorEngine.stop();
|
|
608
646
|
await browserIPC.stop();
|
|
609
647
|
clearInterval(monitorInterval);
|
|
610
648
|
clearInterval(syncInterval);
|
|
@@ -675,15 +713,18 @@ export function writeOwnerOnlyServiceManifest(filePath, content) {
|
|
|
675
713
|
fs.rmSync(filePath, { force: true });
|
|
676
714
|
fs.writeFileSync(filePath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
677
715
|
}
|
|
678
|
-
/**
|
|
679
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Generate a macOS launchd plist for auto-starting the daemon.
|
|
718
|
+
*
|
|
719
|
+
* The plist never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
720
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
721
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
722
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the
|
|
723
|
+
* Keychain-backed secure store and never touches the unit file.
|
|
724
|
+
*/
|
|
725
|
+
export function generateLaunchdPlist(agentsBin = getAgentsBinPath()) {
|
|
680
726
|
const launch = getDaemonLaunch(agentsBin);
|
|
681
727
|
const logPath = getLogPath();
|
|
682
|
-
const oauthEntry = oauthToken
|
|
683
|
-
? `
|
|
684
|
-
<key>${DAEMON_OAUTH_KEY}</key>
|
|
685
|
-
<string>${xmlEscape(oauthToken)}</string>`
|
|
686
|
-
: '';
|
|
687
728
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
688
729
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
689
730
|
<plist version="1.0">
|
|
@@ -705,7 +746,7 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
705
746
|
<key>EnvironmentVariables</key>
|
|
706
747
|
<dict>
|
|
707
748
|
<key>PATH</key>
|
|
708
|
-
<string>${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${os.homedir()}/.bun/bin</string
|
|
749
|
+
<string>${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${os.homedir()}/.bun/bin</string>
|
|
709
750
|
</dict>
|
|
710
751
|
</dict>
|
|
711
752
|
</plist>`;
|
|
@@ -714,13 +755,18 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
714
755
|
function systemdExecArg(value) {
|
|
715
756
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
716
757
|
}
|
|
717
|
-
/**
|
|
718
|
-
|
|
758
|
+
/**
|
|
759
|
+
* Generate a Linux systemd user unit for auto-starting the daemon.
|
|
760
|
+
*
|
|
761
|
+
* The unit never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
762
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
763
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
764
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the secure
|
|
765
|
+
* store and never touches the unit file.
|
|
766
|
+
*/
|
|
767
|
+
export function generateSystemdUnit(agentsBin = getAgentsBinPath()) {
|
|
719
768
|
const launch = getDaemonLaunch(agentsBin);
|
|
720
769
|
const execStart = [launch.command, ...launch.args].map(systemdExecArg).join(' ');
|
|
721
|
-
const oauthLine = oauthToken
|
|
722
|
-
? `\nEnvironment=${DAEMON_OAUTH_KEY}=${oauthToken}`
|
|
723
|
-
: '';
|
|
724
770
|
return `[Unit]
|
|
725
771
|
Description=Agents Daemon - Scheduled Job Runner
|
|
726
772
|
After=network.target
|
|
@@ -730,7 +776,7 @@ Type=simple
|
|
|
730
776
|
ExecStart=${execStart}
|
|
731
777
|
Restart=always
|
|
732
778
|
RestartSec=10
|
|
733
|
-
Environment=PATH=${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin
|
|
779
|
+
Environment=PATH=${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin
|
|
734
780
|
|
|
735
781
|
[Install]
|
|
736
782
|
WantedBy=default.target`;
|
|
@@ -847,9 +893,10 @@ function startDaemonLocked(agentsBin) {
|
|
|
847
893
|
if (!fs.existsSync(plistDir)) {
|
|
848
894
|
fs.mkdirSync(plistDir, { recursive: true });
|
|
849
895
|
}
|
|
850
|
-
// The plist
|
|
851
|
-
// create owner-only atomically
|
|
852
|
-
|
|
896
|
+
// The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
|
|
897
|
+
// token itself at startup); still create owner-only atomically to match the
|
|
898
|
+
// detached path and keep the log/PATH surface owner-private.
|
|
899
|
+
writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
|
|
853
900
|
try {
|
|
854
901
|
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
855
902
|
}
|
|
@@ -876,8 +923,9 @@ function startDaemonLocked(agentsBin) {
|
|
|
876
923
|
if (!fs.existsSync(unitDir)) {
|
|
877
924
|
fs.mkdirSync(unitDir, { recursive: true });
|
|
878
925
|
}
|
|
879
|
-
//
|
|
880
|
-
|
|
926
|
+
// Carries no credential (RUSH-1759 — the daemon reads the OAuth token
|
|
927
|
+
// itself at startup); owner-only to keep the PATH/log surface private.
|
|
928
|
+
writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
|
|
881
929
|
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
882
930
|
execFileSync('systemctl', ['--user', 'enable', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
883
931
|
execFileSync('systemctl', ['--user', 'start', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* throws (misconfigured auth, etc.) become `failed` rows — they never abort
|
|
8
8
|
* the remaining devices.
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
11
|
-
export type FleetSkipReason = 'offline' | 'no-address';
|
|
10
|
+
import { type DeviceProfile, type DeviceRegistry } from './registry.js';
|
|
11
|
+
export type FleetSkipReason = 'offline' | 'no-address' | 'control';
|
|
12
12
|
/** npm dist-tags / semver pins only — rejects shell metacharacters. */
|
|
13
13
|
export declare const FLEET_VERSION_RE: RegExp;
|
|
14
14
|
export interface FleetTarget {
|
|
@@ -27,6 +27,7 @@ export interface FleetRunResult {
|
|
|
27
27
|
/**
|
|
28
28
|
* Classify each registered device for a fleet operation.
|
|
29
29
|
*
|
|
30
|
+
* - Control-only device (a cockpit, e.g. a paired iPhone) → skip `control`
|
|
30
31
|
* - Tailscale-offline → skip `offline`
|
|
31
32
|
* - No address → skip `no-address`
|
|
32
33
|
* - Everything else is a target (including this machine, reached over ssh when
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* the remaining devices.
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from 'child_process';
|
|
11
|
+
import { isControlDevice } from './registry.js';
|
|
11
12
|
import { buildSshInvocation, sshTargetFor, writeAskpassShim } from './connect.js';
|
|
12
13
|
/** npm dist-tags / semver pins only — rejects shell metacharacters. */
|
|
13
14
|
export const FLEET_VERSION_RE = /^[A-Za-z0-9._-]+$/;
|
|
14
15
|
/**
|
|
15
16
|
* Classify each registered device for a fleet operation.
|
|
16
17
|
*
|
|
18
|
+
* - Control-only device (a cockpit, e.g. a paired iPhone) → skip `control`
|
|
17
19
|
* - Tailscale-offline → skip `offline`
|
|
18
20
|
* - No address → skip `no-address`
|
|
19
21
|
* - Everything else is a target (including this machine, reached over ssh when
|
|
@@ -23,6 +25,11 @@ export function planFleetTargets(reg) {
|
|
|
23
25
|
const names = Object.keys(reg).sort();
|
|
24
26
|
return names.map((name) => {
|
|
25
27
|
const device = reg[name];
|
|
28
|
+
// A control device drives the fleet but never runs agents — never a target
|
|
29
|
+
// for update/run/stats, whatever its platform reads as.
|
|
30
|
+
if (isControlDevice(device)) {
|
|
31
|
+
return { device, skip: 'control' };
|
|
32
|
+
}
|
|
26
33
|
if (device.tailscale && !device.tailscale.online) {
|
|
27
34
|
return { device, skip: 'offline' };
|
|
28
35
|
}
|
|
@@ -42,6 +49,8 @@ export function skipLabel(reason) {
|
|
|
42
49
|
return 'offline';
|
|
43
50
|
case 'no-address':
|
|
44
51
|
return 'no address';
|
|
52
|
+
case 'control':
|
|
53
|
+
return 'control device';
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
/**
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device resource probing for `agents devices list`.
|
|
3
|
+
*
|
|
4
|
+
* One SSH round-trip per device gathers load average, memory pressure, and core
|
|
5
|
+
* count (mac + linux), parsed into a {@link DeviceStats}. Probes run in parallel
|
|
6
|
+
* with a bounded timeout so the list stays responsive — a slow or hung box
|
|
7
|
+
* degrades to "no stats" instead of blocking the whole table.
|
|
8
|
+
*
|
|
9
|
+
* The parsers are pure and unit-tested (health.test.ts). They mirror the ones in
|
|
10
|
+
* the Factory extension (apps/factory/src/core/deviceHealth.ts) — kept as a
|
|
11
|
+
* separate copy on purpose: the CLI does not import across packages.
|
|
12
|
+
*/
|
|
13
|
+
import type { DeviceProfile } from './registry.js';
|
|
14
|
+
/** Default per-device probe budget. Short enough that the list never hangs on a
|
|
15
|
+
* wedged box, long enough for a cold relayed SSH handshake. */
|
|
16
|
+
export declare const PROBE_TIMEOUT_MS = 2500;
|
|
17
|
+
export interface DeviceStats {
|
|
18
|
+
host: string;
|
|
19
|
+
reachable: boolean;
|
|
20
|
+
loadAvg1?: number;
|
|
21
|
+
ncpu?: number;
|
|
22
|
+
/** loadAvg1 / ncpu * 100 — load normalized to core count (the "has room" number). */
|
|
23
|
+
loadPercent?: number;
|
|
24
|
+
memPercent?: number;
|
|
25
|
+
memTotalBytes?: number;
|
|
26
|
+
memFreeBytes?: number;
|
|
27
|
+
fetchedAt: number;
|
|
28
|
+
}
|
|
29
|
+
/** Compact human byte size: 512M, 64G, 1.5T (binary units, ≤1 decimal). */
|
|
30
|
+
export declare function fmtBytes(bytes: number | undefined): string;
|
|
31
|
+
export declare function parseUptime(out: string): {
|
|
32
|
+
loadAvg1?: number;
|
|
33
|
+
};
|
|
34
|
+
interface MemStats {
|
|
35
|
+
memPercent?: number;
|
|
36
|
+
memTotalBytes?: number;
|
|
37
|
+
memFreeBytes?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare function parseVmStat(out: string): MemStats;
|
|
40
|
+
export declare function parseLinuxMemInfo(out: string): MemStats;
|
|
41
|
+
export declare function parseNcpu(out: string): {
|
|
42
|
+
ncpu?: number;
|
|
43
|
+
};
|
|
44
|
+
/** Assemble a DeviceStats from the three snippet sections. */
|
|
45
|
+
export declare function parseProbeOutput(host: string, stdout: string, fetchedAt: number): DeviceStats;
|
|
46
|
+
export interface FleetCapacity {
|
|
47
|
+
reachable: number;
|
|
48
|
+
cores: number;
|
|
49
|
+
memTotalBytes: number;
|
|
50
|
+
memFreeBytes: number;
|
|
51
|
+
}
|
|
52
|
+
/** Sum cores and memory across reachable devices for the summary footer. */
|
|
53
|
+
export declare function fleetCapacity(statsList: Iterable<DeviceStats>): FleetCapacity;
|
|
54
|
+
/** Headroom bucket from the worst of normalized-load and memory. */
|
|
55
|
+
export type Headroom = 'idle' | 'light' | 'busy' | 'loaded' | 'unknown';
|
|
56
|
+
export declare function headroom(stats: DeviceStats | undefined): Headroom;
|
|
57
|
+
/** Probe one device over the same ssh path as `agents ssh <name>`. Never throws;
|
|
58
|
+
* an unreachable/slow/misconfigured device resolves to `reachable: false`. */
|
|
59
|
+
export declare function probeDeviceStats(device: DeviceProfile, opts?: {
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
now?: number;
|
|
62
|
+
}): Promise<DeviceStats>;
|
|
63
|
+
/** Probe the local machine directly (no ssh round-trip) — used for the "this
|
|
64
|
+
* machine" row so it always shows real numbers even if it isn't ssh-reachable
|
|
65
|
+
* from itself. */
|
|
66
|
+
export declare function probeLocalStats(host: string, opts?: {
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
now?: number;
|
|
69
|
+
}): Promise<DeviceStats>;
|
|
70
|
+
/** Probe many devices concurrently; returns a name→stats map. Bounded by the
|
|
71
|
+
* per-probe timeout, so total wall time ≈ the slowest single probe. The device
|
|
72
|
+
* named `selfName` is probed locally instead of over ssh. */
|
|
73
|
+
export declare function probeFleetStats(devices: DeviceProfile[], opts?: {
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
selfName?: string;
|
|
76
|
+
}): Promise<Map<string, DeviceStats>>;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device resource probing for `agents devices list`.
|
|
3
|
+
*
|
|
4
|
+
* One SSH round-trip per device gathers load average, memory pressure, and core
|
|
5
|
+
* count (mac + linux), parsed into a {@link DeviceStats}. Probes run in parallel
|
|
6
|
+
* with a bounded timeout so the list stays responsive — a slow or hung box
|
|
7
|
+
* degrades to "no stats" instead of blocking the whole table.
|
|
8
|
+
*
|
|
9
|
+
* The parsers are pure and unit-tested (health.test.ts). They mirror the ones in
|
|
10
|
+
* the Factory extension (apps/factory/src/core/deviceHealth.ts) — kept as a
|
|
11
|
+
* separate copy on purpose: the CLI does not import across packages.
|
|
12
|
+
*/
|
|
13
|
+
import { execFile } from 'child_process';
|
|
14
|
+
import { buildSshInvocation, writeAskpassShim } from './connect.js';
|
|
15
|
+
/** Default per-device probe budget. Short enough that the list never hangs on a
|
|
16
|
+
* wedged box, long enough for a cold relayed SSH handshake. */
|
|
17
|
+
export const PROBE_TIMEOUT_MS = 2_500;
|
|
18
|
+
const SEP = '---AGSTAT---';
|
|
19
|
+
/** One-shot remote snapshot: load, then memory (mac vm_stat else linux
|
|
20
|
+
* meminfo), then core count (linux nproc else mac hw.ncpu). */
|
|
21
|
+
const PROBE_SNIPPET = `uptime; echo ${SEP}; (vm_stat 2>/dev/null || cat /proc/meminfo 2>/dev/null); echo ${SEP}; (nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null)`;
|
|
22
|
+
/** Compact human byte size: 512M, 64G, 1.5T (binary units, ≤1 decimal). */
|
|
23
|
+
export function fmtBytes(bytes) {
|
|
24
|
+
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
25
|
+
return '—';
|
|
26
|
+
const units = ['B', 'K', 'M', 'G', 'T', 'P'];
|
|
27
|
+
let v = bytes;
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
30
|
+
v /= 1024;
|
|
31
|
+
i++;
|
|
32
|
+
}
|
|
33
|
+
const s = v >= 100 || i <= 1 ? Math.round(v).toString() : v.toFixed(1).replace(/\.0$/, '');
|
|
34
|
+
return `${s}${units[i]}`;
|
|
35
|
+
}
|
|
36
|
+
export function parseUptime(out) {
|
|
37
|
+
const m = out.match(/load average[s]?:\s*([0-9]+[.,][0-9]+|[0-9]+)/i);
|
|
38
|
+
if (!m)
|
|
39
|
+
return {};
|
|
40
|
+
const v = parseFloat(m[1].replace(',', '.'));
|
|
41
|
+
if (!Number.isFinite(v))
|
|
42
|
+
return {};
|
|
43
|
+
return { loadAvg1: v };
|
|
44
|
+
}
|
|
45
|
+
export function parseVmStat(out) {
|
|
46
|
+
const pageSize = parseInt(out.match(/page size of\s+([0-9]+)\s+bytes/)?.[1] ?? '4096', 10);
|
|
47
|
+
const active = out.match(/Pages active:\s+([0-9]+)/);
|
|
48
|
+
const wired = out.match(/Pages wired down:\s+([0-9]+)/);
|
|
49
|
+
const compressed = out.match(/Pages occupied by compressor:\s+([0-9]+)/);
|
|
50
|
+
const free = out.match(/Pages free:\s+([0-9]+)/);
|
|
51
|
+
if (!active || !wired || !compressed || !free)
|
|
52
|
+
return {};
|
|
53
|
+
// macOS reclaims inactive + speculative pages on demand, so they count as
|
|
54
|
+
// available — folding them into "free" (as Activity Monitor / vm_pressure do)
|
|
55
|
+
// rather than "used" keeps the headroom bucket honest on a Mac.
|
|
56
|
+
const inactive = out.match(/Pages inactive:\s+([0-9]+)/);
|
|
57
|
+
const speculative = out.match(/Pages speculative:\s+([0-9]+)/);
|
|
58
|
+
const usedPages = parseInt(active[1], 10) + parseInt(wired[1], 10) + parseInt(compressed[1], 10);
|
|
59
|
+
const freePages = parseInt(free[1], 10) +
|
|
60
|
+
parseInt(inactive?.[1] ?? '0', 10) +
|
|
61
|
+
parseInt(speculative?.[1] ?? '0', 10);
|
|
62
|
+
const totalPages = usedPages + freePages;
|
|
63
|
+
if (totalPages <= 0)
|
|
64
|
+
return {};
|
|
65
|
+
return {
|
|
66
|
+
memPercent: (usedPages / totalPages) * 100,
|
|
67
|
+
memTotalBytes: totalPages * pageSize,
|
|
68
|
+
memFreeBytes: freePages * pageSize,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function parseLinuxMemInfo(out) {
|
|
72
|
+
const total = out.match(/^MemTotal:\s+([0-9]+)/im);
|
|
73
|
+
const available = out.match(/^MemAvailable:\s+([0-9]+)/im);
|
|
74
|
+
if (!total || !available)
|
|
75
|
+
return {};
|
|
76
|
+
const tKb = parseInt(total[1], 10);
|
|
77
|
+
const aKb = parseInt(available[1], 10);
|
|
78
|
+
if (tKb <= 0)
|
|
79
|
+
return {};
|
|
80
|
+
return {
|
|
81
|
+
memPercent: Math.max(0, Math.min(100, ((tKb - aKb) / tKb) * 100)),
|
|
82
|
+
memTotalBytes: tKb * 1024,
|
|
83
|
+
memFreeBytes: aKb * 1024,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function parseNcpu(out) {
|
|
87
|
+
const n = parseInt(out.trim().split(/\s+/)[0] ?? '', 10);
|
|
88
|
+
return Number.isFinite(n) && n > 0 ? { ncpu: n } : {};
|
|
89
|
+
}
|
|
90
|
+
/** Assemble a DeviceStats from the three snippet sections. */
|
|
91
|
+
export function parseProbeOutput(host, stdout, fetchedAt) {
|
|
92
|
+
const [uptimePart = '', memPart = '', ncpuPart = ''] = stdout.split(SEP);
|
|
93
|
+
const { loadAvg1 } = parseUptime(uptimePart);
|
|
94
|
+
const mem = memPart.includes('MemTotal') ? parseLinuxMemInfo(memPart) : parseVmStat(memPart);
|
|
95
|
+
const { ncpu } = parseNcpu(ncpuPart);
|
|
96
|
+
const loadPercent = loadAvg1 !== undefined && ncpu ? (loadAvg1 / ncpu) * 100 : undefined;
|
|
97
|
+
return {
|
|
98
|
+
host,
|
|
99
|
+
reachable: true,
|
|
100
|
+
loadAvg1,
|
|
101
|
+
ncpu,
|
|
102
|
+
loadPercent,
|
|
103
|
+
memPercent: mem.memPercent,
|
|
104
|
+
memTotalBytes: mem.memTotalBytes,
|
|
105
|
+
memFreeBytes: mem.memFreeBytes,
|
|
106
|
+
fetchedAt,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Sum cores and memory across reachable devices for the summary footer. */
|
|
110
|
+
export function fleetCapacity(statsList) {
|
|
111
|
+
const cap = { reachable: 0, cores: 0, memTotalBytes: 0, memFreeBytes: 0 };
|
|
112
|
+
for (const s of statsList) {
|
|
113
|
+
if (!s.reachable)
|
|
114
|
+
continue;
|
|
115
|
+
cap.reachable++;
|
|
116
|
+
cap.cores += s.ncpu ?? 0;
|
|
117
|
+
cap.memTotalBytes += s.memTotalBytes ?? 0;
|
|
118
|
+
cap.memFreeBytes += s.memFreeBytes ?? 0;
|
|
119
|
+
}
|
|
120
|
+
return cap;
|
|
121
|
+
}
|
|
122
|
+
export function headroom(stats) {
|
|
123
|
+
if (!stats || !stats.reachable)
|
|
124
|
+
return 'unknown';
|
|
125
|
+
const signals = [stats.loadPercent, stats.memPercent].filter((v) => typeof v === 'number');
|
|
126
|
+
if (signals.length === 0)
|
|
127
|
+
return 'unknown';
|
|
128
|
+
const worst = Math.max(...signals);
|
|
129
|
+
if (worst < 15)
|
|
130
|
+
return 'idle';
|
|
131
|
+
if (worst < 40)
|
|
132
|
+
return 'light';
|
|
133
|
+
if (worst < 75)
|
|
134
|
+
return 'busy';
|
|
135
|
+
return 'loaded';
|
|
136
|
+
}
|
|
137
|
+
/** Probe one device over the same ssh path as `agents ssh <name>`. Never throws;
|
|
138
|
+
* an unreachable/slow/misconfigured device resolves to `reachable: false`. */
|
|
139
|
+
export function probeDeviceStats(device, opts = {}) {
|
|
140
|
+
const host = device.name;
|
|
141
|
+
const fetchedAt = opts.now ?? Date.now();
|
|
142
|
+
let args;
|
|
143
|
+
let env;
|
|
144
|
+
try {
|
|
145
|
+
const shim = writeAskpassShim();
|
|
146
|
+
// buildSshInvocation joins the cmd with spaces and hands the string to the
|
|
147
|
+
// remote login shell, which evaluates the snippet's `;`/`||` directly — no
|
|
148
|
+
// `sh -c` wrapper needed (and a wrapper would only re-quote the first token).
|
|
149
|
+
({ args, env } = buildSshInvocation(device, [PROBE_SNIPPET], shim));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return Promise.resolve({ host, reachable: false, fetchedAt });
|
|
153
|
+
}
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
execFile('ssh', args, { encoding: 'utf-8', env: { ...process.env, ...env }, timeout: opts.timeoutMs ?? PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
156
|
+
if (err || !stdout)
|
|
157
|
+
return resolve({ host, reachable: false, fetchedAt });
|
|
158
|
+
resolve(parseProbeOutput(host, stdout, fetchedAt));
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Probe the local machine directly (no ssh round-trip) — used for the "this
|
|
163
|
+
* machine" row so it always shows real numbers even if it isn't ssh-reachable
|
|
164
|
+
* from itself. */
|
|
165
|
+
export function probeLocalStats(host, opts = {}) {
|
|
166
|
+
const fetchedAt = opts.now ?? Date.now();
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
execFile('sh', ['-c', PROBE_SNIPPET], { encoding: 'utf-8', timeout: opts.timeoutMs ?? PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
169
|
+
if (err || !stdout)
|
|
170
|
+
return resolve({ host, reachable: false, fetchedAt });
|
|
171
|
+
resolve(parseProbeOutput(host, stdout, fetchedAt));
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** Probe many devices concurrently; returns a name→stats map. Bounded by the
|
|
176
|
+
* per-probe timeout, so total wall time ≈ the slowest single probe. The device
|
|
177
|
+
* named `selfName` is probed locally instead of over ssh. */
|
|
178
|
+
export async function probeFleetStats(devices, opts = {}) {
|
|
179
|
+
const entries = await Promise.all(devices.map(async (d) => {
|
|
180
|
+
const stats = d.name === opts.selfName
|
|
181
|
+
? await probeLocalStats(d.name, opts)
|
|
182
|
+
: await probeDeviceStats(d, opts);
|
|
183
|
+
return [d.name, stats];
|
|
184
|
+
}));
|
|
185
|
+
return new Map(entries);
|
|
186
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/** Operating-system family of a device, used to pick the remote shell. */
|
|
2
2
|
export type DevicePlatform = 'windows' | 'linux' | 'macos' | 'unknown';
|
|
3
|
+
/**
|
|
4
|
+
* What a device is *for*. A `worker` (the default) can run agents — it's dialed
|
|
5
|
+
* over SSH for sessions and eligible for run/team placement. A `control` device
|
|
6
|
+
* is a cockpit that drives the fleet but never executes agents itself (an
|
|
7
|
+
* iPhone/iPad running the companion app): it appears in the fleet but is never
|
|
8
|
+
* dialed for SSH nor scheduled work. Absent `role` means `worker`.
|
|
9
|
+
*/
|
|
10
|
+
export type DeviceRole = 'worker' | 'control';
|
|
3
11
|
/** Remote shell dialect derived from the platform. */
|
|
4
12
|
export type DeviceShell = 'powershell' | 'posix';
|
|
5
13
|
/** How `agents ssh` authenticates to a device. Both are first-class, fully
|
|
@@ -41,9 +49,15 @@ export interface DeviceProfile {
|
|
|
41
49
|
address: DeviceAddress;
|
|
42
50
|
auth: DeviceAuth;
|
|
43
51
|
tailscale?: DeviceTailscale;
|
|
52
|
+
/** What the device is for. Absent means `worker` (see {@link DeviceRole}). */
|
|
53
|
+
role?: DeviceRole;
|
|
44
54
|
createdAt: string;
|
|
45
55
|
updatedAt: string;
|
|
46
56
|
}
|
|
57
|
+
/** A device's effective role, defaulting to `worker` when unset. */
|
|
58
|
+
export declare function deviceRole(d: DeviceProfile): DeviceRole;
|
|
59
|
+
/** True for a control-only device (a cockpit) that must never be dialed/scheduled. */
|
|
60
|
+
export declare function isControlDevice(d: DeviceProfile): boolean;
|
|
47
61
|
/** Map of device name to profile. */
|
|
48
62
|
export type DeviceRegistry = Record<string, DeviceProfile>;
|
|
49
63
|
/** Throw if `name` is not usable as an ssh alias (no spaces, quotes, etc.). */
|
|
@@ -74,6 +88,7 @@ export interface DeviceInput {
|
|
|
74
88
|
address?: DeviceAddress;
|
|
75
89
|
auth?: DeviceAuth;
|
|
76
90
|
tailscale?: DeviceTailscale;
|
|
91
|
+
role?: DeviceRole;
|
|
77
92
|
}
|
|
78
93
|
/**
|
|
79
94
|
* Create the device if absent, otherwise merge the supplied fields into the
|
|
@@ -19,6 +19,14 @@ import * as path from 'path';
|
|
|
19
19
|
import { randomBytes } from 'crypto';
|
|
20
20
|
import lockfile from 'proper-lockfile';
|
|
21
21
|
import { getDevicesRegistryPath, getDevicesIgnoredPath } from '../state.js';
|
|
22
|
+
/** A device's effective role, defaulting to `worker` when unset. */
|
|
23
|
+
export function deviceRole(d) {
|
|
24
|
+
return d.role ?? 'worker';
|
|
25
|
+
}
|
|
26
|
+
/** True for a control-only device (a cockpit) that must never be dialed/scheduled. */
|
|
27
|
+
export function isControlDevice(d) {
|
|
28
|
+
return deviceRole(d) === 'control';
|
|
29
|
+
}
|
|
22
30
|
function registryPath() {
|
|
23
31
|
return getDevicesRegistryPath();
|
|
24
32
|
}
|
|
@@ -170,6 +178,7 @@ export async function upsertDevice(name, input) {
|
|
|
170
178
|
address: input.address ?? prev?.address ?? { via: 'manual' },
|
|
171
179
|
auth: input.auth ?? prev?.auth ?? { method: 'key' },
|
|
172
180
|
tailscale: input.tailscale ?? prev?.tailscale,
|
|
181
|
+
role: input.role ?? prev?.role,
|
|
173
182
|
createdAt: prev?.createdAt ?? now,
|
|
174
183
|
updatedAt: now,
|
|
175
184
|
};
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -120,6 +120,15 @@ export interface ExecOptions {
|
|
|
120
120
|
mcpConfigPath?: string;
|
|
121
121
|
/** Raw args captured after `--` on the command line, forwarded verbatim to the underlying agent CLI. */
|
|
122
122
|
passthroughArgs?: string[];
|
|
123
|
+
/**
|
|
124
|
+
* Tee-and-tail the child's stdout even when no budget cap is active, so the
|
|
125
|
+
* caller can scan it for rate/usage-limit messages. Claude prints billing
|
|
126
|
+
* refusals ("monthly spend limit", "out of usage credits") to STDOUT, not
|
|
127
|
+
* stderr — a fallback chain that only inspects stderr never cascades on
|
|
128
|
+
* them. Set by runWithFallback for every chain entry; harmless elsewhere
|
|
129
|
+
* (output is mirrored to the parent's stdout exactly like stdio:'inherit').
|
|
130
|
+
*/
|
|
131
|
+
captureStdoutTail?: boolean;
|
|
123
132
|
/**
|
|
124
133
|
* Escape hatch for the interactive tmux spawn-wrap (see shouldWrapInTmux):
|
|
125
134
|
* when true, spawn the agent directly instead of inside a shared-socket tmux
|
|
@@ -150,7 +159,7 @@ export declare function resolveInteractive(options: Pick<ExecOptions, 'interacti
|
|
|
150
159
|
* @param piped true when the parent's stdout is NOT a TTY (output piped)
|
|
151
160
|
* @param capsActive true when a budget watcher is attached (caps configured)
|
|
152
161
|
*/
|
|
153
|
-
export declare function shouldTapStdout(interactive: boolean, piped: boolean, capsActive: boolean): boolean;
|
|
162
|
+
export declare function shouldTapStdout(interactive: boolean, piped: boolean, capsActive: boolean, captureTail?: boolean): boolean;
|
|
154
163
|
/** Parse an array of KEY=VALUE strings into an env record. Returns undefined for empty input. */
|
|
155
164
|
export declare function parseExecEnv(entries: string[]): Record<string, string> | undefined;
|
|
156
165
|
/**
|
|
@@ -274,8 +283,16 @@ export declare function shouldWrapInTmux(ctx: TmuxWrapContext): boolean;
|
|
|
274
283
|
* `#{pane_pid}`, clean signal delivery on detach/kill).
|
|
275
284
|
* Keys are filtered to valid identifiers so exported shell functions
|
|
276
285
|
* (`BASH_FUNC_*%%`) can't make `env` choke.
|
|
286
|
+
*
|
|
287
|
+
* `redactEnvValues` replaces every value with a `<redacted>` marker while keeping
|
|
288
|
+
* the KEY names. The env map here carries resolved secrets bundles (options.env),
|
|
289
|
+
* so the real string would embed secret VALUES — which get persisted verbatim
|
|
290
|
+
* into SessionMeta.cmd on disk (tmux/session.ts). The launched command uses the
|
|
291
|
+
* real values; the stored/informational copy uses the redacted form (RUSH-1758).
|
|
277
292
|
*/
|
|
278
|
-
export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv
|
|
293
|
+
export declare function buildTmuxAgentCommand(executable: string, args: string[], env: NodeJS.ProcessEnv, opts?: {
|
|
294
|
+
redactEnvValues?: boolean;
|
|
295
|
+
}): string;
|
|
279
296
|
/**
|
|
280
297
|
* Trim a raw `tmux capture-pane` dump to its last `maxLines` non-empty lines
|
|
281
298
|
* (right-stripping each). Used by runInTmux to recap a fast-failed agent's
|