@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -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 +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
package/dist/commands/ssh.js
CHANGED
|
@@ -31,7 +31,7 @@ import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-targe
|
|
|
31
31
|
import { clearPendingSentinel } from '../lib/devices/pending.js';
|
|
32
32
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
33
33
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
34
|
-
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
34
|
+
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
35
35
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
36
36
|
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
37
37
|
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
@@ -44,6 +44,7 @@ import { checkAllClis } from '../lib/teams/agents.js';
|
|
|
44
44
|
import { buildRemoteAgentsInvocation } from '../lib/hosts/remote-cmd.js';
|
|
45
45
|
import { sshExecAsync } from '../lib/ssh-exec.js';
|
|
46
46
|
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
47
|
+
import { collectLocalHarnessInventory, groupByAccount, renderAccountsMatrix, renderHarnessMatrix, } from '../lib/devices/harness-inventory.js';
|
|
47
48
|
import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js';
|
|
48
49
|
import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort } from './lease.js';
|
|
49
50
|
import { authCellColor, formatCheckedAge, isDeadVerdict, probeLocalFleetAuth, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
@@ -351,7 +352,7 @@ async function probeRemoteFleetStatus(target) {
|
|
|
351
352
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
352
353
|
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
353
354
|
const cmd = buildRemoteAgentsInvocation(['devices', 'status', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
354
|
-
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true });
|
|
355
|
+
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
|
|
355
356
|
if (res.code !== 0) {
|
|
356
357
|
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
357
358
|
}
|
|
@@ -361,10 +362,10 @@ async function probeRemoteHealth(target) {
|
|
|
361
362
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
362
363
|
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
363
364
|
const versionCmd = buildRemoteAgentsInvocation(['--version'], undefined, isWin ? 'windows' : undefined, env);
|
|
364
|
-
const versionRes = await sshExecAsync(target.dialTarget, versionCmd, { timeoutMs: 15000, multiplex: true });
|
|
365
|
+
const versionRes = await sshExecAsync(target.dialTarget, versionCmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
|
|
365
366
|
const version = versionRes.code === 0 ? versionRes.stdout.trim().split(/\s+/)[0] || null : null;
|
|
366
367
|
const doctorCmd = buildRemoteAgentsInvocation(['doctor', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
367
|
-
const doctorRes = await sshExecAsync(target.dialTarget, doctorCmd, { timeoutMs: 30000, multiplex: true });
|
|
368
|
+
const doctorRes = await sshExecAsync(target.dialTarget, doctorCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs: target.extraSshArgs });
|
|
368
369
|
if (doctorRes.code !== 0) {
|
|
369
370
|
throw new Error(doctorRes.timedOut ? 'timed out' : (doctorRes.stderr.trim() || `exit ${doctorRes.code ?? 'unknown'}`));
|
|
370
371
|
}
|
|
@@ -425,6 +426,7 @@ async function runFleetStatus(opts) {
|
|
|
425
426
|
// this is trusted on the default path, not just under `--refresh`.
|
|
426
427
|
skip: fleetHealthSkip(t.skip, statsMap.get(t.device.name)),
|
|
427
428
|
dialTarget: fleetDialTarget(t.device),
|
|
429
|
+
extraSshArgs: deviceIdentityArgs(t.device),
|
|
428
430
|
}));
|
|
429
431
|
const remote = await fanOutDevices(remoteTargets, probeRemoteHealth);
|
|
430
432
|
for (const result of remote) {
|
|
@@ -533,7 +535,7 @@ async function probeRemoteAuth(target) {
|
|
|
533
535
|
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
534
536
|
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
535
537
|
const cmd = buildRemoteAgentsInvocation(['devices', 'ping', '--local', '--json'], undefined, isWin ? 'windows' : undefined, env);
|
|
536
|
-
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true });
|
|
538
|
+
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
|
|
537
539
|
if (res.code !== 0) {
|
|
538
540
|
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
539
541
|
}
|
|
@@ -601,6 +603,7 @@ async function runFleetPing(opts) {
|
|
|
601
603
|
platform: t.device.platform,
|
|
602
604
|
skip: t.skip,
|
|
603
605
|
dialTarget: fleetDialTarget(t.device),
|
|
606
|
+
extraSshArgs: deviceIdentityArgs(t.device),
|
|
604
607
|
}));
|
|
605
608
|
const probeable = remoteTargets.filter((t) => !t.skip).length;
|
|
606
609
|
const spinner = isInteractiveTerminal() && !opts.json
|
|
@@ -645,6 +648,118 @@ async function runFleetPing(opts) {
|
|
|
645
648
|
if (opts.strict && anyBad)
|
|
646
649
|
process.exitCode = 1;
|
|
647
650
|
}
|
|
651
|
+
/** SSH into a host and read its raw harness rows (the `--local --json` worker). */
|
|
652
|
+
async function probeRemoteHarnesses(target, refresh) {
|
|
653
|
+
const isWin = /^win/i.test((target.platform ?? '').trim());
|
|
654
|
+
const env = isWin ? undefined : { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' };
|
|
655
|
+
const args = ['devices', 'harnesses', '--local', '--json'];
|
|
656
|
+
if (refresh)
|
|
657
|
+
args.push('--refresh');
|
|
658
|
+
const cmd = buildRemoteAgentsInvocation(args, undefined, isWin ? 'windows' : undefined, env);
|
|
659
|
+
const res = await sshExecAsync(target.dialTarget, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs: target.extraSshArgs });
|
|
660
|
+
if (res.code !== 0) {
|
|
661
|
+
throw new Error(res.timedOut ? 'timed out' : (res.stderr.trim() || `exit ${res.code ?? 'unknown'}`));
|
|
662
|
+
}
|
|
663
|
+
const parsed = JSON.parse(res.stdout);
|
|
664
|
+
return parsed.rows ?? [];
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Gather harness rows across the fleet: THIS host in process, every reachable
|
|
668
|
+
* peer over SSH. Shared by both `harnesses` and `accounts` (they differ only in
|
|
669
|
+
* how the rows are rendered). Honors an optional `--device` allowlist on both
|
|
670
|
+
* the local and remote rows. Bounded by the same per-device + overall deadlines
|
|
671
|
+
* as `fleet ping`, so one unreachable box can never stall the glance.
|
|
672
|
+
*/
|
|
673
|
+
async function collectFleetHarnesses(opts) {
|
|
674
|
+
const self = machineId();
|
|
675
|
+
const want = opts.devices?.length ? new Set(opts.devices) : null;
|
|
676
|
+
const results = [];
|
|
677
|
+
if (!want || want.has(self)) {
|
|
678
|
+
const localRows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
|
|
679
|
+
results.push({ host: self, rows: localRows });
|
|
680
|
+
}
|
|
681
|
+
const reg = await loadDevices();
|
|
682
|
+
const planned = planFleetTargets(reg);
|
|
683
|
+
let remoteTargets = remoteFleetTargets(planned, self).map((t) => ({
|
|
684
|
+
name: t.device.name,
|
|
685
|
+
platform: t.device.platform,
|
|
686
|
+
skip: t.skip,
|
|
687
|
+
dialTarget: fleetDialTarget(t.device),
|
|
688
|
+
extraSshArgs: deviceIdentityArgs(t.device),
|
|
689
|
+
}));
|
|
690
|
+
if (want)
|
|
691
|
+
remoteTargets = remoteTargets.filter((t) => want.has(t.name));
|
|
692
|
+
if (remoteTargets.length > 0) {
|
|
693
|
+
const probeable = remoteTargets.filter((t) => !t.skip).length;
|
|
694
|
+
const spinner = isInteractiveTerminal() && !opts.json
|
|
695
|
+
? ora(`Probing ${probeable} device${probeable === 1 ? '' : 's'}…`).start()
|
|
696
|
+
: undefined;
|
|
697
|
+
let remote;
|
|
698
|
+
try {
|
|
699
|
+
const fanOut = fanOutDevices(remoteTargets, (t) => probeRemoteHarnesses(t, !!opts.refresh), { perDeviceTimeoutMs: 15_000 });
|
|
700
|
+
remote = await raceFleetPingDeadline(fanOut, remoteTargets, 30_000);
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
spinner?.stop();
|
|
704
|
+
}
|
|
705
|
+
for (const r of remote) {
|
|
706
|
+
if (r.status === 'ok' && r.value) {
|
|
707
|
+
results.push({ host: r.name, rows: r.value });
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
results.push({
|
|
711
|
+
host: r.name,
|
|
712
|
+
rows: [],
|
|
713
|
+
error: r.error,
|
|
714
|
+
skipped: r.reason ? String(r.reason) : undefined,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return results;
|
|
720
|
+
}
|
|
721
|
+
async function runDevicesHarnesses(opts) {
|
|
722
|
+
if (opts.local) {
|
|
723
|
+
const rows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
|
|
724
|
+
if (opts.json)
|
|
725
|
+
console.log(JSON.stringify({ host: machineId(), rows }));
|
|
726
|
+
else
|
|
727
|
+
for (const line of renderHarnessMatrix([{ host: machineId(), rows }]))
|
|
728
|
+
console.log(line);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const results = await collectFleetHarnesses(opts);
|
|
732
|
+
if (opts.json)
|
|
733
|
+
console.log(JSON.stringify(results, null, 2));
|
|
734
|
+
else
|
|
735
|
+
for (const line of renderHarnessMatrix(results))
|
|
736
|
+
console.log(line);
|
|
737
|
+
}
|
|
738
|
+
async function runDevicesAccounts(opts) {
|
|
739
|
+
if (opts.local) {
|
|
740
|
+
const rows = await collectLocalHarnessInventory({ agents: opts.agents, refresh: opts.refresh });
|
|
741
|
+
if (opts.json)
|
|
742
|
+
console.log(JSON.stringify({ host: machineId(), accounts: groupByAccount(rows) }));
|
|
743
|
+
else
|
|
744
|
+
for (const line of renderAccountsMatrix([{ host: machineId(), rows }]))
|
|
745
|
+
console.log(line);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
const results = await collectFleetHarnesses(opts);
|
|
749
|
+
if (opts.json) {
|
|
750
|
+
const grouped = results.map((r) => ({
|
|
751
|
+
host: r.host,
|
|
752
|
+
error: r.error,
|
|
753
|
+
skipped: r.skipped,
|
|
754
|
+
accounts: groupByAccount(r.rows),
|
|
755
|
+
}));
|
|
756
|
+
console.log(JSON.stringify(grouped, null, 2));
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
for (const line of renderAccountsMatrix(results))
|
|
760
|
+
console.log(line);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
648
763
|
/** Resolve an {@link AuthCellColor} to a chalk painter. Single map for cells + labels. */
|
|
649
764
|
const CELL_PAINT = {
|
|
650
765
|
green: chalk.green,
|
|
@@ -777,6 +892,7 @@ Typical workflow:
|
|
|
777
892
|
agents devices configure mac-mini --max-agents 4 --scheduler off
|
|
778
893
|
agents devices note mac-mini "runs the releases — don't reboot"
|
|
779
894
|
agents devices set win-mini --auth password --bundle muqsit
|
|
895
|
+
agents devices set worker --auth key --identity-file ~/.ssh/worker_ed25519
|
|
780
896
|
agents devices render --write # write ~/.ssh/config.d/agents include
|
|
781
897
|
agents fleet update # roll out latest agents-cli to every online device
|
|
782
898
|
agents fleet run uname -a # run a command on every online device
|
|
@@ -1219,6 +1335,59 @@ Typical workflow:
|
|
|
1219
1335
|
const verbose = opts.verbose ?? Boolean(cmd.optsWithGlobals().verbose);
|
|
1220
1336
|
await runFleetPing({ ...opts, verbose });
|
|
1221
1337
|
});
|
|
1338
|
+
const csvList = (s) => s ? s.split(',').map((x) => x.trim()).filter(Boolean) : undefined;
|
|
1339
|
+
const harnessInvOpts = (opts) => ({
|
|
1340
|
+
agents: csvList(opts.agents),
|
|
1341
|
+
devices: csvList(opts.device ?? opts.devices),
|
|
1342
|
+
refresh: opts.refresh || opts.live,
|
|
1343
|
+
json: opts.json,
|
|
1344
|
+
local: opts.local,
|
|
1345
|
+
});
|
|
1346
|
+
const harnessesCmd = devicesCmd
|
|
1347
|
+
.command('harnesses')
|
|
1348
|
+
.description('Per device, one row per installed agent@version: account, signed-in, quota, and a single ready verdict. SSH-probes each online box.')
|
|
1349
|
+
.option('--json', 'output machine-readable JSON (per-host rows)')
|
|
1350
|
+
.option('--agents <csv>', 'only these agents (comma-separated)')
|
|
1351
|
+
.option('--device <csv>', 'only these devices (comma-separated); default: every online box')
|
|
1352
|
+
.option('--refresh', 'fetch live quota instead of the cached snapshot (slower)')
|
|
1353
|
+
.option('--live', 'alias of --refresh')
|
|
1354
|
+
.option('--local', "this host only: emit THIS box's rows (the per-host worker the fan-out reads over ssh)")
|
|
1355
|
+
.action(async (opts) => {
|
|
1356
|
+
await runDevicesHarnesses(harnessInvOpts(opts));
|
|
1357
|
+
});
|
|
1358
|
+
setHelpSections(harnessesCmd, {
|
|
1359
|
+
examples: `
|
|
1360
|
+
agents devices harnesses # every box: agent@version · account · signed · quota · ready
|
|
1361
|
+
agents devices harnesses --agents claude,codex # just these harnesses
|
|
1362
|
+
agents devices harnesses --device zion # one box
|
|
1363
|
+
agents devices harnesses --refresh # live quota (bypass the cached snapshot)
|
|
1364
|
+
agents devices harnesses --json # machine-readable, per-host rows`,
|
|
1365
|
+
notes: `
|
|
1366
|
+
"ready" = signed in AND not rate-limited — usable for a run right now.
|
|
1367
|
+
Quota is the cached usage snapshot (the daemon warms it); --refresh fetches live.
|
|
1368
|
+
Use \`agents devices accounts\` for the same data grouped by account.`,
|
|
1369
|
+
});
|
|
1370
|
+
const accountsCmd = devicesCmd
|
|
1371
|
+
.command('accounts')
|
|
1372
|
+
.description('Per device, one row per account: which harnesses share it, signed-in, quota, and ready. The identity lens on `agents devices harnesses`.')
|
|
1373
|
+
.option('--json', 'output machine-readable JSON (per-host account groups)')
|
|
1374
|
+
.option('--agents <csv>', 'only these agents (comma-separated)')
|
|
1375
|
+
.option('--device <csv>', 'only these devices (comma-separated); default: every online box')
|
|
1376
|
+
.option('--refresh', 'fetch live quota instead of the cached snapshot (slower)')
|
|
1377
|
+
.option('--live', 'alias of --refresh')
|
|
1378
|
+
.option('--local', "this host only: emit THIS box's account groups")
|
|
1379
|
+
.action(async (opts) => {
|
|
1380
|
+
await runDevicesAccounts(harnessInvOpts(opts));
|
|
1381
|
+
});
|
|
1382
|
+
setHelpSections(accountsCmd, {
|
|
1383
|
+
examples: `
|
|
1384
|
+
agents devices accounts # every box: account · agents · signed · quota · ready
|
|
1385
|
+
agents devices accounts --device mac-mini
|
|
1386
|
+
agents devices accounts --json # machine-readable, per-host account groups`,
|
|
1387
|
+
notes: `
|
|
1388
|
+
Collapses the installs that share one account (e.g. five claude versions on one
|
|
1389
|
+
email) into a single row. Use \`agents devices harnesses\` for the per-install view.`,
|
|
1390
|
+
});
|
|
1222
1391
|
devicesCmd
|
|
1223
1392
|
.command('login')
|
|
1224
1393
|
.description('Log agent CLIs into fleet boxes over SSH: drive each box\'s device-code OAuth, scrape the URL + code, and surface every pending login in one local browser page. Default drives all codes at once; --interactive walks one box at a time (codes requested just-in-time so they don\'t expire).')
|
|
@@ -1281,15 +1450,19 @@ Typical workflow:
|
|
|
1281
1450
|
.option('--auth <method>', 'key | password')
|
|
1282
1451
|
.option('--bundle <bundle>', 'secrets bundle holding the password (for --auth password)')
|
|
1283
1452
|
.option('--bundle-key <key>', "key within the bundle (default 'password')")
|
|
1453
|
+
.option('--identity-file <path>', 'private-key path for --auth key')
|
|
1454
|
+
.option('--clear-identity-file', 'return key auth to ssh-agent/default-key discovery')
|
|
1284
1455
|
.action(async (name, opts) => {
|
|
1285
1456
|
try {
|
|
1286
1457
|
const existing = await mustGetDevice(name);
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1458
|
+
const nextMethod = opts.auth ?? existing.auth.method;
|
|
1459
|
+
if (opts.identityFile && nextMethod !== 'key') {
|
|
1460
|
+
throw new Error('--identity-file requires key auth; pass --auth key in the same command.');
|
|
1461
|
+
}
|
|
1462
|
+
const auth = opts.auth || opts.bundle || opts.bundleKey || opts.identityFile || opts.clearIdentityFile
|
|
1463
|
+
? nextMethod === 'key'
|
|
1464
|
+
? { method: nextMethod, identityFile: opts.clearIdentityFile ? undefined : (opts.identityFile ?? existing.auth.identityFile) }
|
|
1465
|
+
: { method: nextMethod, bundle: opts.bundle ?? existing.auth.bundle, bundleKey: opts.bundleKey ?? existing.auth.bundleKey }
|
|
1293
1466
|
: undefined;
|
|
1294
1467
|
const d = await upsertDevice(name, {
|
|
1295
1468
|
platform: opts.platform ?? undefined,
|
package/dist/commands/teams.js
CHANGED
|
@@ -17,7 +17,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
|
|
|
17
17
|
import { setHelpSections } from '../lib/help.js';
|
|
18
18
|
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
19
19
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
20
|
-
import { isDeviceAuto,
|
|
20
|
+
import { isDeviceAuto, resolveDeviceAuto } from '../lib/smart-launch.js';
|
|
21
21
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
22
22
|
import { ensureHostReady } from '../lib/hosts/ready.js';
|
|
23
23
|
import { remoteShellFor } from '../lib/hosts/remote-cmd.js';
|
|
@@ -1354,12 +1354,12 @@ export function registerTeamsCommands(program) {
|
|
|
1354
1354
|
}
|
|
1355
1355
|
return h ?? d ?? null;
|
|
1356
1356
|
})();
|
|
1357
|
-
// `auto` is the same
|
|
1357
|
+
// `auto` is the same live fleet sentinel `agents run --device auto` resolves
|
|
1358
1358
|
// (RUSH-2185) — pick the concrete device name up front so the local-machine
|
|
1359
1359
|
// check right below (and every dieFriction message further down) sees the
|
|
1360
1360
|
// real target instead of the literal string "auto".
|
|
1361
1361
|
if (explicitDevice && isDeviceAuto(explicitDevice)) {
|
|
1362
|
-
const plan =
|
|
1362
|
+
const plan = await resolveDeviceAuto(parseTeammate(teammate).agent);
|
|
1363
1363
|
const picked = plan.host ?? machineId();
|
|
1364
1364
|
process.stderr.write(chalk.gray(`[teams] device=auto → ${picked === machineId() ? 'local' : picked}\n`));
|
|
1365
1365
|
explicitDevice = picked;
|
|
@@ -1395,10 +1395,17 @@ export function registerTeamsCommands(program) {
|
|
|
1395
1395
|
catch (err) {
|
|
1396
1396
|
dieFriction('teams', 'ssh-target-unresolvable', `Can't resolve an ssh target for "${host.name}": ${err.message}`);
|
|
1397
1397
|
}
|
|
1398
|
-
// Ensure agents-cli is present +
|
|
1399
|
-
//
|
|
1398
|
+
// Ensure agents-cli is present + the pin is installed on the host.
|
|
1399
|
+
// Bare agent names still only warn; a concrete pin fails loud so
|
|
1400
|
+
// `teams add --device` never stages a teammate the box cannot run
|
|
1401
|
+
// (RUSH-2313). parseTeammate already resolves @latest/etc. to a concrete
|
|
1402
|
+
// version when one is pinned on the teammate spec.
|
|
1400
1403
|
try {
|
|
1401
|
-
const
|
|
1404
|
+
const parsed = parseTeammate(teammate);
|
|
1405
|
+
const { warnings } = ensureHostReady(host, {
|
|
1406
|
+
agent: parsed.agent,
|
|
1407
|
+
version: parsed.version ?? undefined,
|
|
1408
|
+
});
|
|
1402
1409
|
for (const w of warnings)
|
|
1403
1410
|
process.stderr.write(chalk.yellow(`[teams] warning: ${w}\n`));
|
|
1404
1411
|
}
|
|
@@ -1417,7 +1424,9 @@ export function registerTeamsCommands(program) {
|
|
|
1417
1424
|
effectiveRepo = (await getRemoteUrl(process.cwd())) ?? '';
|
|
1418
1425
|
}
|
|
1419
1426
|
try {
|
|
1420
|
-
hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team
|
|
1427
|
+
hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team, {
|
|
1428
|
+
extraSshArgs: host.identityFile ? ['-i', host.identityFile, '-o', 'IdentitiesOnly=yes'] : [],
|
|
1429
|
+
});
|
|
1421
1430
|
}
|
|
1422
1431
|
catch (err) {
|
|
1423
1432
|
dieFriction('teams', 'repo-provision-failed', `Couldn't provision the repo on "${host.name}": ${err.message}\n` +
|
|
@@ -2018,11 +2027,12 @@ export function registerTeamsCommands(program) {
|
|
|
2018
2027
|
try {
|
|
2019
2028
|
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
2020
2029
|
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
2021
|
-
|
|
2030
|
+
const ssh = { extraSshArgs: agent.hostIdentityFile ? ['-i', agent.hostIdentityFile, '-o', 'IdentitiesOnly=yes'] : [] };
|
|
2031
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath, ssh)) {
|
|
2022
2032
|
worktreeKept = true;
|
|
2023
2033
|
}
|
|
2024
2034
|
else {
|
|
2025
|
-
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
2035
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName, true, ssh);
|
|
2026
2036
|
}
|
|
2027
2037
|
}
|
|
2028
2038
|
else {
|
|
@@ -2231,11 +2241,12 @@ export function registerTeamsCommands(program) {
|
|
|
2231
2241
|
try {
|
|
2232
2242
|
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
2233
2243
|
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
2234
|
-
|
|
2244
|
+
const ssh = { extraSshArgs: agent.hostIdentityFile ? ['-i', agent.hostIdentityFile, '-o', 'IdentitiesOnly=yes'] : [] };
|
|
2245
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath, ssh)) {
|
|
2235
2246
|
keptWorktrees.push(agent.worktreeName);
|
|
2236
2247
|
}
|
|
2237
2248
|
else {
|
|
2238
|
-
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
2249
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName, true, ssh);
|
|
2239
2250
|
}
|
|
2240
2251
|
}
|
|
2241
2252
|
else {
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
|
|
|
94
94
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
95
95
|
// first byte of output), the registry maps a command name to a thunk that
|
|
96
96
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
97
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
97
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadBench, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
98
98
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
99
99
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
100
100
|
import { getCliLaunch } from './lib/cli-entry.js';
|
|
@@ -392,15 +392,15 @@ async function showWhatsNew(fromVersion, toVersion) {
|
|
|
392
392
|
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
393
393
|
import { getUpdateCheckPath, getMigratedSentinelPath, getUserAgentsDir, getRuntimeStateDir } from './lib/state.js';
|
|
394
394
|
import { resolveBrandName, disabledCommandsForActiveBrand } from './lib/brand.js';
|
|
395
|
-
import { readUpdateCache, saveUpdateCheck, dismissUpdateVersion, shouldPromptUpgrade, findAgentsCliInstalls, resolveRunningPackageRoot, } from './lib/self-update.js';
|
|
395
|
+
import { readUpdateCache, saveUpdateCheck, dismissUpdateVersion, shouldPromptUpgrade, buildMultiInstallInventory, findAgentsCliInstalls, resolveRunningPackageRoot, } from './lib/self-update.js';
|
|
396
396
|
const UPDATE_CHECK_FILE = getUpdateCheckPath();
|
|
397
397
|
/**
|
|
398
|
-
* Warn once when
|
|
399
|
-
*
|
|
398
|
+
* Warn once when this machine contains a different agents-cli install than the
|
|
399
|
+
* copy that is currently running (or several). Divergent installs
|
|
400
400
|
* are how self-updates "succeed" without changing the command the user types.
|
|
401
|
-
* The warning re-fires only when the set of install roots
|
|
402
|
-
*
|
|
403
|
-
*
|
|
401
|
+
* The warning re-fires only when the set of install roots or their helper-copy
|
|
402
|
+
* safety changes. Dev builds are included because old dev copies can still
|
|
403
|
+
* overwrite the shared macOS helper bundle non-atomically.
|
|
404
404
|
*/
|
|
405
405
|
function maybeWarnMultiInstall() {
|
|
406
406
|
const sentinel = path.join(getRuntimeStateDir(), 'multi-install-warned');
|
|
@@ -414,31 +414,26 @@ function maybeWarnMultiInstall() {
|
|
|
414
414
|
// "/$bunfs" install. This warning is advisory — stay silent instead.
|
|
415
415
|
return;
|
|
416
416
|
}
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
for (const install of findAgentsCliInstalls(process.env.PATH || '')) {
|
|
420
|
-
if (install.version.startsWith('0.0.0-dev'))
|
|
421
|
-
continue;
|
|
422
|
-
if (!byRoot.has(install.packageRoot)) {
|
|
423
|
-
byRoot.set(install.packageRoot, { version: install.version, note: `agents on PATH: ${install.binPath}` });
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (byRoot.size < 2) {
|
|
417
|
+
const inventory = buildMultiInstallInventory(runningRoot, VERSION, findAgentsCliInstalls(process.env.PATH || ''));
|
|
418
|
+
if (inventory.length < 2) {
|
|
427
419
|
try {
|
|
428
420
|
fs.unlinkSync(sentinel);
|
|
429
421
|
}
|
|
430
422
|
catch { /* nothing recorded */ }
|
|
431
423
|
return;
|
|
432
424
|
}
|
|
433
|
-
const key =
|
|
425
|
+
const key = inventory
|
|
426
|
+
.map((info) => `${info.packageRoot}\t${info.version}\t${info.note}`)
|
|
427
|
+
.sort()
|
|
428
|
+
.join('\n');
|
|
434
429
|
try {
|
|
435
430
|
if (fs.readFileSync(sentinel, 'utf-8') === key)
|
|
436
431
|
return;
|
|
437
432
|
}
|
|
438
433
|
catch { /* not warned for this set yet */ }
|
|
439
434
|
console.error(chalk.yellow('Multiple agents-cli installs detected:'));
|
|
440
|
-
for (const
|
|
441
|
-
console.error(chalk.gray(` ${
|
|
435
|
+
for (const info of inventory) {
|
|
436
|
+
console.error(chalk.gray(` ${info.packageRoot} ${info.version} (${info.note})`));
|
|
442
437
|
}
|
|
443
438
|
console.error(chalk.gray('Upgrades apply to the running copy. Remove a stale copy with: npm uninstall -g --prefix <prefix> @phnx-labs/agents-cli'));
|
|
444
439
|
try {
|
|
@@ -967,6 +962,7 @@ async function registerAllEagerCommands() {
|
|
|
967
962
|
await reg(loadCost);
|
|
968
963
|
await reg(loadInsights);
|
|
969
964
|
await reg(loadPerf);
|
|
965
|
+
await reg(loadBench);
|
|
970
966
|
await reg(loadTrends);
|
|
971
967
|
await reg(loadOutput);
|
|
972
968
|
await reg(loadBudget);
|
package/dist/lib/activity.js
CHANGED
|
@@ -26,6 +26,7 @@ import { getActivityDir, getUserAgentsDir } from './state.js';
|
|
|
26
26
|
import { normalizeHost } from './machine-id.js';
|
|
27
27
|
import { projectKeyFromCwd } from './project-key.js';
|
|
28
28
|
import { stampProvenance } from './event-provenance.js';
|
|
29
|
+
import { pythonToolRegistryLiteral, pythonValueFlagsLiteral, } from './session/bash-command.js';
|
|
29
30
|
/** The set of milestone events, for tier classification and ordering. */
|
|
30
31
|
export const MILESTONE_EVENTS = [
|
|
31
32
|
'plan.created',
|
|
@@ -730,115 +731,30 @@ def first_line(text, limit=140):
|
|
|
730
731
|
return ""
|
|
731
732
|
|
|
732
733
|
|
|
733
|
-
# Canonical command taxonomy for Bash tool calls
|
|
734
|
-
#
|
|
734
|
+
# Canonical command taxonomy for Bash tool calls, generated from the single
|
|
735
|
+
# source of truth in lib/session/bash-command.ts's TOOL_REGISTRY (#1889) so the
|
|
736
|
+
# two can no longer drift — this hook runs standalone under python3 and can't
|
|
737
|
+
# import that module, so the literal below is generated, not hand-duplicated.
|
|
735
738
|
BASH_TOOL_REGISTRY = {
|
|
736
|
-
|
|
737
|
-
"gh": {"category": "vcs", "action": "using GitHub CLI"},
|
|
738
|
-
"bun": {"category": "build-test", "action": "running bun"},
|
|
739
|
-
"npm": {"category": "build-test", "action": "running npm"},
|
|
740
|
-
"pnpm": {"category": "build-test", "action": "running pnpm"},
|
|
741
|
-
"yarn": {"category": "build-test", "action": "running yarn"},
|
|
742
|
-
"vitest": {"category": "build-test", "action": "running vitest"},
|
|
743
|
-
"jest": {"category": "build-test", "action": "running jest"},
|
|
744
|
-
"mocha": {"category": "build-test", "action": "running mocha"},
|
|
745
|
-
"pytest": {"category": "build-test", "action": "running pytest"},
|
|
746
|
-
"cargo": {"category": "build-test", "action": "running cargo"},
|
|
747
|
-
"go": {"category": "build-test", "action": "running go"},
|
|
748
|
-
"tsc": {"category": "build-test", "action": "running tsc"},
|
|
749
|
-
"tsx": {"category": "build-test", "action": "running tsx"},
|
|
750
|
-
"node": {"category": "build-test", "action": "running node"},
|
|
751
|
-
"python": {"category": "build-test", "action": "running python"},
|
|
752
|
-
"python3": {"category": "build-test", "action": "running python"},
|
|
753
|
-
"make": {"category": "build-test", "action": "running make"},
|
|
754
|
-
"brew": {"category": "install", "action": "installing with brew"},
|
|
755
|
-
"pip": {"category": "install", "action": "installing with pip"},
|
|
756
|
-
"pip3": {"category": "install", "action": "installing with pip"},
|
|
757
|
-
"apt": {"category": "install", "action": "installing with apt"},
|
|
758
|
-
"apk": {"category": "install", "action": "installing with apk"},
|
|
759
|
-
"ssh": {"category": "remote", "action": "using ssh"},
|
|
760
|
-
"scp": {"category": "remote", "action": "using scp"},
|
|
761
|
-
"rsync": {"category": "remote", "action": "using rsync"},
|
|
762
|
-
"curl": {"category": "http", "action": "fetching with curl"},
|
|
763
|
-
"wget": {"category": "http", "action": "fetching with wget"},
|
|
764
|
-
"ffmpeg": {"category": "media", "action": "using ffmpeg"},
|
|
765
|
-
"ffprobe": {"category": "media", "action": "probing media"},
|
|
766
|
-
"magick": {"category": "media", "action": "using ImageMagick"},
|
|
767
|
-
"convert": {"category": "media", "action": "converting images"},
|
|
768
|
-
"composite": {"category": "media", "action": "compositing images"},
|
|
769
|
-
"montage": {"category": "media", "action": "montaging images"},
|
|
770
|
-
"identify": {"category": "media", "action": "identifying images"},
|
|
771
|
-
"realesrgan": {"category": "upscaling", "action": "upscaling with realesrgan"},
|
|
772
|
-
"realesrgan-ncnn-vulkan": {"category": "upscaling", "action": "upscaling with realesrgan"},
|
|
773
|
-
"waifu2x": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
774
|
-
"waifu2x-caffe": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
775
|
-
"waifu2x-converter-cpp": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
776
|
-
"swin2sr": {"category": "upscaling", "action": "upscaling with swin2sr"},
|
|
777
|
-
"resdet": {"category": "upscaling", "action": "detecting upscale"},
|
|
778
|
-
"id3v2": {"category": "metadata", "action": "editing id3 tags"},
|
|
779
|
-
"exiftool": {"category": "metadata", "action": "editing exif metadata"},
|
|
780
|
-
"metaflac": {"category": "metadata", "action": "editing flac metadata"},
|
|
781
|
-
"vorbiscomment": {"category": "metadata", "action": "editing vorbis comments"},
|
|
782
|
-
# Shell
|
|
783
|
-
"rm": {"category": "shell", "action": "removing files"},
|
|
784
|
-
"mv": {"category": "shell", "action": "moving files"},
|
|
785
|
-
"cp": {"category": "shell", "action": "copying files"},
|
|
786
|
-
"mkdir": {"category": "shell", "action": "making directories"},
|
|
787
|
-
"touch": {"category": "shell", "action": "touching files"},
|
|
788
|
-
"echo": {"category": "shell", "action": "echoing"},
|
|
789
|
-
"printf": {"category": "shell", "action": "printing"},
|
|
790
|
-
"chmod": {"category": "shell", "action": "changing permissions"},
|
|
791
|
-
"ln": {"category": "shell", "action": "linking files"},
|
|
792
|
-
"awk": {"category": "shell", "action": "running awk"},
|
|
793
|
-
"sed": {"category": "shell", "action": "running sed"},
|
|
794
|
-
"tee": {"category": "shell", "action": "teeing output"},
|
|
795
|
-
"xargs": {"category": "shell", "action": "running xargs"},
|
|
796
|
-
# Probes
|
|
797
|
-
"ls": {"category": "probe", "action": "listing files"},
|
|
798
|
-
"cat": {"category": "probe", "action": "reading files"},
|
|
799
|
-
"head": {"category": "probe", "action": "reading files"},
|
|
800
|
-
"tail": {"category": "probe", "action": "reading files"},
|
|
801
|
-
"wc": {"category": "probe", "action": "counting"},
|
|
802
|
-
"stat": {"category": "probe", "action": "statting files"},
|
|
803
|
-
"file": {"category": "probe", "action": "inspecting files"},
|
|
804
|
-
"which": {"category": "probe", "action": "locating binaries"},
|
|
805
|
-
"tree": {"category": "probe", "action": "listing files"},
|
|
806
|
-
"pwd": {"category": "probe", "action": "printing pwd"},
|
|
807
|
-
# Search
|
|
808
|
-
"grep": {"category": "search", "action": "searching"},
|
|
809
|
-
"rg": {"category": "search", "action": "searching with ripgrep"},
|
|
810
|
-
"ag": {"category": "search", "action": "searching with the silver searcher"},
|
|
811
|
-
"fd": {"category": "search", "action": "searching files"},
|
|
812
|
-
"find": {"category": "search", "action": "finding files"},
|
|
813
|
-
# Wait
|
|
814
|
-
"sleep": {"category": "wait", "action": "sleeping"},
|
|
815
|
-
"wait": {"category": "wait", "action": "waiting"},
|
|
739
|
+
${pythonToolRegistryLiteral()}
|
|
816
740
|
}
|
|
817
741
|
|
|
818
742
|
# Two-level tools mapped to the flags that consume the following token as their
|
|
819
|
-
# value, per tool; the subcommand scan skips both.
|
|
820
|
-
#
|
|
821
|
-
# tool with no such leading flags maps to an empty set. Mirrors VALUE_FLAGS in
|
|
822
|
-
# lib/session/bash-command.ts; TWO_LEVEL_TOOLS is derived from its keys.
|
|
743
|
+
# value, per tool; the subcommand scan skips both. Generated from VALUE_FLAGS in
|
|
744
|
+
# lib/session/bash-command.ts (#1889) — TWO_LEVEL_TOOLS is derived from its keys.
|
|
823
745
|
VALUE_FLAGS = {
|
|
824
|
-
|
|
825
|
-
"gh": {"-R", "--repo"},
|
|
826
|
-
"bun": {"--cwd"},
|
|
827
|
-
"npm": {"--prefix", "-w", "--workspace"},
|
|
828
|
-
"pnpm": {"--filter", "-C", "--dir"},
|
|
829
|
-
"yarn": {"--cwd"},
|
|
830
|
-
"cargo": {"--manifest-path"},
|
|
831
|
-
"docker": {"-H", "--host", "-c", "--context", "--config", "-l", "--log-level"},
|
|
832
|
-
"kubectl": {"-n", "--namespace", "--kubeconfig", "--context", "--cluster", "--user", "-s", "--server", "--as", "--token", "--cache-dir", "--request-timeout"},
|
|
833
|
-
"rush": set(),
|
|
834
|
-
"openclaw": set(),
|
|
746
|
+
${pythonValueFlagsLiteral()}
|
|
835
747
|
}
|
|
836
748
|
|
|
837
749
|
TWO_LEVEL_TOOLS = set(VALUE_FLAGS.keys())
|
|
838
750
|
|
|
839
751
|
|
|
840
752
|
def _unwrap_command(cmd):
|
|
841
|
-
"""Strip wrapper prefixes so the real executable is classified.
|
|
753
|
+
"""Strip wrapper prefixes so the real executable is classified.
|
|
754
|
+
|
|
755
|
+
Mirrors unwrapCommand() in lib/session/bash-command.ts — keep the shapes
|
|
756
|
+
aligned when extending either side (#1889).
|
|
757
|
+
"""
|
|
842
758
|
s = (cmd or "").strip()
|
|
843
759
|
ssh = re.match(r'^ssh\s+\S+\s+["\']?(.+?)["\']?\s*(?:\|.*)?$', s)
|
|
844
760
|
if ssh:
|
|
@@ -847,16 +763,46 @@ def _unwrap_command(cmd):
|
|
|
847
763
|
env = re.match(r'^([A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|' + r"'[^']*'" + r'|\S+)\s+)+(.+)$', s)
|
|
848
764
|
if env:
|
|
849
765
|
return _unwrap_command(env.group(2))
|
|
766
|
+
# export VAR=value && command / export A=1 B=2; command
|
|
767
|
+
export_prefix = re.match(
|
|
768
|
+
r'^export\s+(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|' + r"'[^']*'" + r'|\S+)\s+)*'
|
|
769
|
+
r'[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|' + r"'[^']*'" + r'|\S+)\s*(?:&&|;|\n)\s*([\s\S]+)$',
|
|
770
|
+
s,
|
|
771
|
+
)
|
|
772
|
+
if export_prefix:
|
|
773
|
+
return _unwrap_command(export_prefix.group(1))
|
|
850
774
|
# sudo / time prefix (value-taking flags such as -u user consume their argument)
|
|
851
775
|
prefix = re.match(r'^(?:sudo|time)(?:\s+(?:-[uUgGhpCrtDR]\s+\S+|-\S+))*\s+(.+)$', s)
|
|
852
776
|
if prefix:
|
|
853
777
|
return _unwrap_command(prefix.group(1))
|
|
854
|
-
|
|
778
|
+
# set -euo pipefail && command / set -x; command
|
|
779
|
+
set_prefix = re.match(r'^set\s+[^&;\n]+?(?:&&|;|\n)\s*([\s\S]+)$', s)
|
|
780
|
+
if set_prefix:
|
|
781
|
+
return _unwrap_command(set_prefix.group(1))
|
|
782
|
+
# cd foo && command (also ; and newline separators)
|
|
783
|
+
cd = re.match(r'^cd\s+\S+\s*(?:&&|;|\n)\s*([\s\S]+)$', s)
|
|
855
784
|
if cd:
|
|
856
785
|
return _unwrap_command(cd.group(1))
|
|
857
786
|
npx = re.match(r'^(?:npx|bunx)\s+(?:-\S+\s+)*(.+)$', s)
|
|
858
787
|
if npx:
|
|
859
788
|
return _unwrap_command(npx.group(1))
|
|
789
|
+
for_loop = re.match(r'^for\s+[\s\S]+?\bdo\b\s*([\s\S]+?)\s*;?\s*done\b[\s\S]*$', s)
|
|
790
|
+
if for_loop:
|
|
791
|
+
return _unwrap_command(for_loop.group(1))
|
|
792
|
+
until_loop = re.match(r'^until\s+[\s\S]+?\bdo\b\s*([\s\S]+?)\s*;?\s*done\b[\s\S]*$', s)
|
|
793
|
+
if until_loop:
|
|
794
|
+
return _unwrap_command(until_loop.group(1))
|
|
795
|
+
if_cond = re.match(
|
|
796
|
+
r'^if\s+[\s\S]+?\bthen\b\s*([\s\S]+?)\s*;?\s*(?:elif\b|else\b|fi\b)[\s\S]*$',
|
|
797
|
+
s,
|
|
798
|
+
)
|
|
799
|
+
if if_cond:
|
|
800
|
+
return _unwrap_command(if_cond.group(1))
|
|
801
|
+
subshell = re.match(r'^\(\s*([\s\S]+?)\s*\)\s*(?:&&|;|\|\|)[\s\S]*$', s) or re.match(
|
|
802
|
+
r'^\(\s*([\s\S]+?)\s*\)\s*$', s
|
|
803
|
+
)
|
|
804
|
+
if subshell:
|
|
805
|
+
return _unwrap_command(subshell.group(1))
|
|
860
806
|
return s
|
|
861
807
|
|
|
862
808
|
|