@phnx-labs/agents-cli 1.20.49 → 1.20.51
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 +31 -0
- package/README.md +3 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +218 -97
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +43 -18
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- 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/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +25 -2
- package/dist/lib/shims.js +73 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +7 -26
- package/dist/lib/versions.js +44 -146
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
package/dist/commands/teams.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { die, relTime, truncate, isJsonMode, padRight } from '../lib/format.js';
|
|
2
3
|
import * as fs from 'fs/promises';
|
|
3
4
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
4
5
|
import * as path from 'path';
|
|
5
|
-
import { AgentManager,
|
|
6
|
+
import { AgentManager, checkCliSignedIn, collectTeamsDoctorData, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
|
|
6
7
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
7
8
|
import { runSupervisor } from '../lib/teams/supervisor.js';
|
|
8
9
|
import { debug } from '../lib/teams/debug.js';
|
|
@@ -11,7 +12,15 @@ import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary
|
|
|
11
12
|
import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
12
13
|
import { setHelpSections } from '../lib/help.js';
|
|
13
14
|
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
14
|
-
import {
|
|
15
|
+
import { resolveHost } from '../lib/hosts/registry.js';
|
|
16
|
+
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
17
|
+
import { ensureHostReady } from '../lib/hosts/ready.js';
|
|
18
|
+
import { remoteShellFor } from '../lib/hosts/remote-cmd.js';
|
|
19
|
+
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
20
|
+
import { remoteWorktreeDirty, removeRemoteWorktree, ensureRemoteRepo } from '../lib/teams/remoteWorktree.js';
|
|
21
|
+
import { getRemoteUrl } from '../lib/git.js';
|
|
22
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
23
|
+
import { isVersionInstalled, resolveVersionAlias, resolveVersionAliasLoose } from '../lib/versions.js';
|
|
15
24
|
import { AGENTS, warnAgentDeprecated } from '../lib/agents.js';
|
|
16
25
|
import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/session/discover.js';
|
|
17
26
|
import { renderSessionLog } from './sessions.js';
|
|
@@ -40,13 +49,6 @@ const VALID_EFFORTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
|
40
49
|
const VALID_CLOUD_PROVIDERS = ['rush', 'codex', 'factory'];
|
|
41
50
|
// Auto-enable JSON mode when piped / not a TTY so AI agent consumers get
|
|
42
51
|
// parseable output by default.
|
|
43
|
-
function isJsonMode(opts) {
|
|
44
|
-
return Boolean(opts.json) || !process.stdout.isTTY;
|
|
45
|
-
}
|
|
46
|
-
function die(msg, code = 1) {
|
|
47
|
-
console.error(chalk.red(msg));
|
|
48
|
-
process.exit(code);
|
|
49
|
-
}
|
|
50
52
|
function statusColor(status) {
|
|
51
53
|
switch (status) {
|
|
52
54
|
case 'pending': return chalk.blue;
|
|
@@ -57,23 +59,6 @@ function statusColor(status) {
|
|
|
57
59
|
default: return chalk.white;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
|
-
function relTime(iso) {
|
|
61
|
-
const secs = Math.floor((Date.now() - new Date(iso).getTime()) / 1000);
|
|
62
|
-
if (secs < 10)
|
|
63
|
-
return 'just now';
|
|
64
|
-
if (secs < 60)
|
|
65
|
-
return `${secs}s ago`;
|
|
66
|
-
if (secs < 3600)
|
|
67
|
-
return `${Math.floor(secs / 60)}m ago`;
|
|
68
|
-
if (secs < 86400)
|
|
69
|
-
return `${Math.floor(secs / 3600)}h ago`;
|
|
70
|
-
return `${Math.floor(secs / 86400)}d ago`;
|
|
71
|
-
}
|
|
72
|
-
function truncate(s, n) {
|
|
73
|
-
if (s.length <= n)
|
|
74
|
-
return s;
|
|
75
|
-
return s.slice(0, n - 1) + '…';
|
|
76
|
-
}
|
|
77
62
|
function compactPrompt(s, n = 160) {
|
|
78
63
|
return truncate(s.replace(/\s+/g, ' ').trim(), n);
|
|
79
64
|
}
|
|
@@ -90,9 +75,6 @@ function formatTimestamp(iso) {
|
|
|
90
75
|
minute: '2-digit',
|
|
91
76
|
});
|
|
92
77
|
}
|
|
93
|
-
function padRight(s, width) {
|
|
94
|
-
return s.length >= width ? s : s + ' '.repeat(width - s.length);
|
|
95
|
-
}
|
|
96
78
|
function fullName(type, version) {
|
|
97
79
|
const name = AGENT_NAMES[type];
|
|
98
80
|
return version ? `${name} ${version}` : name;
|
|
@@ -486,6 +468,9 @@ function printAgentDetail(a, session) {
|
|
|
486
468
|
if (a.after && a.after.length) {
|
|
487
469
|
console.log(` ${chalk.gray('after ')} ${a.after.join(', ')}`);
|
|
488
470
|
}
|
|
471
|
+
if (a.host) {
|
|
472
|
+
console.log(` ${chalk.gray('host ')} ${chalk.cyan(a.host)}`);
|
|
473
|
+
}
|
|
489
474
|
// If the agent's internal session id differs from ours (non-Claude), show
|
|
490
475
|
// it as a hint for `agents sessions <id>`.
|
|
491
476
|
if (a.remote_session_id && a.remote_session_id !== a.agent_id) {
|
|
@@ -569,7 +554,8 @@ function printAgentSummary(s) {
|
|
|
569
554
|
const duration = s.duration ? `${chalk.gray(' · ')}${chalk.white(s.duration)}` : '';
|
|
570
555
|
const errBadge = s.has_errors ? chalk.red(' !') : '';
|
|
571
556
|
const tools = chalk.gray(` · ${s.tool_count} tools`);
|
|
572
|
-
|
|
557
|
+
const hostBadge = s.host ? chalk.gray(' · on ') + chalk.cyan(s.host) : '';
|
|
558
|
+
console.log(` ${chalk.cyan(handle.padEnd(14))} ${ident.padEnd(11)} ${label}${duration}${tools}${hostBadge}${errBadge}`);
|
|
573
559
|
// Files: counts + basenames. Read is count only.
|
|
574
560
|
const fileLines = [];
|
|
575
561
|
const renderCat = (label, cat) => {
|
|
@@ -965,13 +951,55 @@ export function registerTeamsCommands(program) {
|
|
|
965
951
|
.option('-d, --description <text>', 'One-line summary of what this team is working on')
|
|
966
952
|
.option('--enable-worktrees', 'Each teammate works in its own git worktree (requires --worktree on add)')
|
|
967
953
|
.option('--use-worktree <path>', 'All teammates share this existing worktree path (mutually exclusive with --enable-worktrees)')
|
|
954
|
+
.option('--devices <list>', 'Pool of machines this team may run teammates on (comma-separated). Enables distributed auto-scheduling.')
|
|
955
|
+
.option('--hosts <list>', 'Alias for --devices.')
|
|
956
|
+
.option('--repo <urlOrPath>', 'How each device gets the code (git URL to clone, or a path). Defaults to the local checkout origin.')
|
|
968
957
|
.option('--json', 'Output machine-readable JSON')
|
|
969
958
|
.action(async (team, opts) => {
|
|
970
959
|
try {
|
|
960
|
+
// --devices / --hosts are aliases; commander can't express a two-name
|
|
961
|
+
// option that isn't a short flag, so merge them here. Split on comma,
|
|
962
|
+
// trim, drop blanks, dedupe (preserving first-seen order).
|
|
963
|
+
const rawPool = [opts.devices, opts.hosts].filter(Boolean).join(',');
|
|
964
|
+
const devices = [];
|
|
965
|
+
for (const d of rawPool.split(',').map((s) => s.trim()).filter(Boolean)) {
|
|
966
|
+
if (!devices.includes(d))
|
|
967
|
+
devices.push(d);
|
|
968
|
+
}
|
|
969
|
+
// Validate every pooled device resolves + is POSIX (v1 remote monitor is
|
|
970
|
+
// POSIX-only). A device equal to the local machine is fine (runs local),
|
|
971
|
+
// so skip the resolve/POSIX check for it.
|
|
972
|
+
for (const name of devices) {
|
|
973
|
+
if (name.toLowerCase() === machineId())
|
|
974
|
+
continue;
|
|
975
|
+
const host = await resolveHost(name);
|
|
976
|
+
if (!host) {
|
|
977
|
+
die(`Couldn't resolve pool device "${name}". Register it with \`agents devices\`, ` +
|
|
978
|
+
`enroll it with \`agents hosts add ${name}\`, or pass user@host.`);
|
|
979
|
+
}
|
|
980
|
+
if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
|
|
981
|
+
die(`Distributed teams on Windows device "${host.name}" are not supported yet — ` +
|
|
982
|
+
`the teams remote monitor is POSIX-only. Use a Linux/macOS device.`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// --repo: how each device gets the code. Default to the local checkout's
|
|
986
|
+
// origin when a pool is declared and we're inside a git repo, so the user
|
|
987
|
+
// never hand-manages a path per box. A poolless team leaves repo unset.
|
|
988
|
+
let repo = opts.repo;
|
|
989
|
+
if (!repo && devices.length > 0) {
|
|
990
|
+
const cwd = process.cwd();
|
|
991
|
+
if (await isGitRepo(cwd)) {
|
|
992
|
+
const origin = await getRemoteUrl(cwd);
|
|
993
|
+
if (origin)
|
|
994
|
+
repo = origin;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
971
997
|
const meta = await createTeam(team, {
|
|
972
998
|
description: opts.description,
|
|
973
999
|
enableWorktrees: opts.enableWorktrees,
|
|
974
1000
|
useWorktree: opts.useWorktree,
|
|
1001
|
+
devices,
|
|
1002
|
+
repo,
|
|
975
1003
|
});
|
|
976
1004
|
if (isJsonMode(opts)) {
|
|
977
1005
|
console.log(JSON.stringify({ team, ...meta }, null, 2));
|
|
@@ -984,6 +1012,10 @@ export function registerTeamsCommands(program) {
|
|
|
984
1012
|
console.log(chalk.gray(` worktrees: per-teammate`));
|
|
985
1013
|
if (meta.use_worktree)
|
|
986
1014
|
console.log(chalk.gray(` worktree: ${meta.use_worktree}`));
|
|
1015
|
+
if (meta.devices && meta.devices.length)
|
|
1016
|
+
console.log(chalk.gray(` devices: ${meta.devices.join(', ')}`));
|
|
1017
|
+
if (meta.repo)
|
|
1018
|
+
console.log(chalk.gray(` repo: ${meta.repo}`));
|
|
987
1019
|
console.log();
|
|
988
1020
|
console.log(chalk.gray('Add your first teammate:'));
|
|
989
1021
|
if (meta.enable_worktrees) {
|
|
@@ -1039,9 +1071,88 @@ export function registerTeamsCommands(program) {
|
|
|
1039
1071
|
die(`--cloud rush requires --repo <owner/repo>`);
|
|
1040
1072
|
}
|
|
1041
1073
|
}
|
|
1074
|
+
// Auto-create the team if it doesn't exist yet (friendlier UX than erroring),
|
|
1075
|
+
// then load its metadata — needed here for the distributed --repo (how each
|
|
1076
|
+
// device gets the code) before we resolve a per-teammate --device pin.
|
|
1077
|
+
await ensureTeam(team);
|
|
1078
|
+
const teamMeta = await getTeam(team);
|
|
1079
|
+
// `--device`/`--host` are aliases (addHostOption registers both). For `teams
|
|
1080
|
+
// add` the passthrough special-cases them as PLACEMENT, not routing, so the
|
|
1081
|
+
// local action reads them here. Reject a conflicting pair.
|
|
1082
|
+
const explicitDevice = (() => {
|
|
1083
|
+
const h = opts.host;
|
|
1084
|
+
const d = opts.device;
|
|
1085
|
+
if (h && d && h !== d) {
|
|
1086
|
+
die('Conflicting --host/--device values — pass just one.');
|
|
1087
|
+
}
|
|
1088
|
+
return h ?? d ?? null;
|
|
1089
|
+
})();
|
|
1090
|
+
// Distributed teams: --device <name> PINS this teammate to a machine over
|
|
1091
|
+
// SSH. Resolve + validate the placement here so a bad target fails at `add`
|
|
1092
|
+
// time, not silently at launch. Persisted (hostName/hostTarget/repoPath) so
|
|
1093
|
+
// startReady()/launchRemoteProcess dispatch over SSH. Unpinned teammates
|
|
1094
|
+
// leave these null — the launch-time scheduler resolves the pool cascade.
|
|
1095
|
+
let hostName = null;
|
|
1096
|
+
let hostTarget = null;
|
|
1097
|
+
let hostRepoPath = null;
|
|
1098
|
+
if (explicitDevice && explicitDevice.toLowerCase() !== machineId()) {
|
|
1099
|
+
if (cloudProviderId) {
|
|
1100
|
+
die(`--device and --cloud are mutually exclusive (two different remote backends). Pick one.`);
|
|
1101
|
+
}
|
|
1102
|
+
const host = await resolveHost(explicitDevice);
|
|
1103
|
+
if (!host) {
|
|
1104
|
+
die(`Couldn't resolve --device "${explicitDevice}". Register it with \`agents devices\`, ` +
|
|
1105
|
+
`enroll it with \`agents hosts add ${explicitDevice}\`, or pass user@host.`);
|
|
1106
|
+
}
|
|
1107
|
+
// POSIX-only in v1: the remote follow/monitor layer offset-tails the log
|
|
1108
|
+
// with tail/cat/kill, which don't exist under PowerShell. Refuse Windows
|
|
1109
|
+
// up front, mirroring dispatch.ts launchDetached.
|
|
1110
|
+
if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
|
|
1111
|
+
die(`Distributed teammates on Windows host "${host.name}" are not supported yet — ` +
|
|
1112
|
+
`the teams remote monitor is POSIX-only (offset-tails the remote log with tail/cat/kill). ` +
|
|
1113
|
+
`Use a Linux/macOS host, or run this teammate locally.`);
|
|
1114
|
+
}
|
|
1115
|
+
try {
|
|
1116
|
+
hostTarget = sshTargetFor(host);
|
|
1117
|
+
}
|
|
1118
|
+
catch (err) {
|
|
1119
|
+
die(`Can't resolve an ssh target for "${host.name}": ${err.message}`);
|
|
1120
|
+
}
|
|
1121
|
+
// Ensure agents-cli is present + version-matched on the host; surface
|
|
1122
|
+
// (not fail on) an agent-not-installed warning, like the run --host path.
|
|
1123
|
+
try {
|
|
1124
|
+
const { warnings } = ensureHostReady(host, { agent: parseTeammate(teammate).agent });
|
|
1125
|
+
for (const w of warnings)
|
|
1126
|
+
process.stderr.write(chalk.yellow(`[teams] warning: ${w}\n`));
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
die(`Host "${host.name}" is not ready: ${err.message}`);
|
|
1130
|
+
}
|
|
1131
|
+
// Provision the repo on the host from the team's --repo (clone into
|
|
1132
|
+
// ~/.agents/repos/<team> or reuse an existing checkout), resolving to the
|
|
1133
|
+
// ABSOLUTE git root so every later remote command works from an absolute
|
|
1134
|
+
// path (dispatch `cd`, worktree create, polling). When the team has no
|
|
1135
|
+
// --repo (the common "just send one teammate elsewhere" case, created
|
|
1136
|
+
// without a pool), fall back to THIS checkout's origin so the headline case
|
|
1137
|
+
// works with zero extra flags whenever you run `add` inside a git repo.
|
|
1138
|
+
let effectiveRepo = teamMeta?.repo ?? '';
|
|
1139
|
+
if (!effectiveRepo && (await isGitRepo(process.cwd()))) {
|
|
1140
|
+
effectiveRepo = (await getRemoteUrl(process.cwd())) ?? '';
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team);
|
|
1144
|
+
}
|
|
1145
|
+
catch (err) {
|
|
1146
|
+
die(`Couldn't provision the repo on "${host.name}": ${err.message}\n` +
|
|
1147
|
+
` Set how each device gets the code with: agents teams create ${team} --repo <url|path>`);
|
|
1148
|
+
}
|
|
1149
|
+
hostName = host.name;
|
|
1150
|
+
}
|
|
1042
1151
|
const { agent, version, profileName } = parseTeammate(teammate);
|
|
1043
1152
|
warnAgentDeprecated(agent);
|
|
1044
|
-
|
|
1153
|
+
// Version-installed check is about the LOCAL machine — a distributed (--on)
|
|
1154
|
+
// teammate's agent/version lives on the host, verified by ensureHostReady.
|
|
1155
|
+
if (version && !hostName && !isVersionInstalled(agent, version)) {
|
|
1045
1156
|
die(`${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
|
|
1046
1157
|
` Install it: agents add ${agent}@${version}\n` +
|
|
1047
1158
|
` Or see what's installed (incl. @latest): agents view ${agent}`);
|
|
@@ -1049,7 +1160,8 @@ export function registerTeamsCommands(program) {
|
|
|
1049
1160
|
// Advisory sign-in check: warn but NEVER block. Detection is unreliable
|
|
1050
1161
|
// for opaque-cred agents, so a false negative must not stop a team. Cloud
|
|
1051
1162
|
// dispatch authenticates through the provider, not the local CLI — skip it.
|
|
1052
|
-
|
|
1163
|
+
// Distributed (--on) teammates authenticate on the host, not locally — skip.
|
|
1164
|
+
if (!opts.force && !cloudProviderId && !hostName && !(await checkCliSignedIn(agent))) {
|
|
1053
1165
|
console.error(chalk.yellow(`⚠ ${AGENT_NAMES[agent]} may not be signed in (detection is unreliable). Adding anyway.`) +
|
|
1054
1166
|
chalk.gray(`\n If it fails to start, run \`${AGENTS[agent].cliCommand}\` to log in, or pass --force to silence this.`));
|
|
1055
1167
|
}
|
|
@@ -1058,7 +1170,7 @@ export function registerTeamsCommands(program) {
|
|
|
1058
1170
|
// out-of-credits / signed-out account (see throttleWarningLine). Skip bare
|
|
1059
1171
|
// targets (rotation handles them), profiles (auth-injected account isn't
|
|
1060
1172
|
// the version-home one we can read), and cloud dispatch. Warn, never block.
|
|
1061
|
-
if (!opts.force && !cloudProviderId && !profileName && version) {
|
|
1173
|
+
if (!opts.force && !cloudProviderId && !hostName && !profileName && version) {
|
|
1062
1174
|
const readiness = await checkRunAccountReadiness(agent, version);
|
|
1063
1175
|
if (!readiness.ready)
|
|
1064
1176
|
console.error(throttleWarningLine(agent, version, readiness));
|
|
@@ -1086,15 +1198,34 @@ export function registerTeamsCommands(program) {
|
|
|
1086
1198
|
catch (err) {
|
|
1087
1199
|
die(err.message);
|
|
1088
1200
|
}
|
|
1089
|
-
//
|
|
1090
|
-
await ensureTeam(team);
|
|
1091
|
-
// Check if team has worktrees enabled or a shared worktree
|
|
1092
|
-
const teamMeta = await getTeam(team);
|
|
1201
|
+
// Team already ensured + loaded above (teamMeta) for the --repo provisioning.
|
|
1093
1202
|
const worktreesEnabled = teamMeta?.enable_worktrees ?? false;
|
|
1094
1203
|
const sharedWorktree = teamMeta?.use_worktree ?? null;
|
|
1095
1204
|
let worktreeName = null;
|
|
1096
1205
|
let worktreePath = null;
|
|
1097
|
-
if (
|
|
1206
|
+
if (hostName) {
|
|
1207
|
+
// Distributed teammate: the checkout lives on the host, so we NEVER touch
|
|
1208
|
+
// the local filesystem here. A shared local worktree makes no sense for a
|
|
1209
|
+
// remote teammate; a per-teammate worktree is created ON THE HOST at launch
|
|
1210
|
+
// (createRemoteWorktree in launchRemoteProcess) — we just capture its name.
|
|
1211
|
+
if (sharedWorktree) {
|
|
1212
|
+
die(`Team '${team}' uses a shared local --use-worktree, which can't apply to a --device (remote) teammate.`);
|
|
1213
|
+
}
|
|
1214
|
+
if (worktreesEnabled) {
|
|
1215
|
+
if (!opts.worktree) {
|
|
1216
|
+
die(`Team '${team}' has worktrees enabled. Use --worktree <name> for the remote teammate (created on ${hostName}).`);
|
|
1217
|
+
}
|
|
1218
|
+
if (!opts.name) {
|
|
1219
|
+
die(`Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
|
|
1220
|
+
}
|
|
1221
|
+
worktreeName = opts.worktree;
|
|
1222
|
+
}
|
|
1223
|
+
else if (opts.worktree) {
|
|
1224
|
+
die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
1225
|
+
}
|
|
1226
|
+
// Local cwd stays null — the remote cwd is repoPath / the remote worktree.
|
|
1227
|
+
}
|
|
1228
|
+
else if (sharedWorktree) {
|
|
1098
1229
|
// Team uses a shared worktree for all teammates
|
|
1099
1230
|
const fsp = await import('fs/promises');
|
|
1100
1231
|
try {
|
|
@@ -1133,7 +1264,10 @@ export function registerTeamsCommands(program) {
|
|
|
1133
1264
|
else if (opts.worktree) {
|
|
1134
1265
|
die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
1135
1266
|
}
|
|
1136
|
-
|
|
1267
|
+
// Distributed teammates have no LOCAL cwd — their working dir lives on the
|
|
1268
|
+
// host (repoPath / the remote worktree). Local teammates default to the
|
|
1269
|
+
// worktree path, then --cwd, then the current directory.
|
|
1270
|
+
const cwd = hostName ? null : (worktreePath ?? opts.cwd ?? process.cwd());
|
|
1137
1271
|
const mgr = mkManager();
|
|
1138
1272
|
// Factory teammates: prepend the worker-skill preamble to every task
|
|
1139
1273
|
// prompt so implementers/testers/reviewers know about the Ledger, the
|
|
@@ -1182,7 +1316,7 @@ export function registerTeamsCommands(program) {
|
|
|
1182
1316
|
}
|
|
1183
1317
|
}
|
|
1184
1318
|
try {
|
|
1185
|
-
const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath, profileName);
|
|
1319
|
+
const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath, profileName, hostName, hostTarget, hostRepoPath);
|
|
1186
1320
|
if (isJsonMode(opts)) {
|
|
1187
1321
|
console.log(JSON.stringify(result, null, 2));
|
|
1188
1322
|
return;
|
|
@@ -1200,7 +1334,10 @@ export function registerTeamsCommands(program) {
|
|
|
1200
1334
|
console.log(` ${chalk.gray('agent_id')} ${chalk.cyan(shortId(result.agent_id))} ${chalk.gray(`(${result.agent_id})`)}`);
|
|
1201
1335
|
console.log(` ${chalk.gray('status ')} ${statusColor(result.status)(result.status)}`);
|
|
1202
1336
|
console.log(` ${chalk.gray('mode ')} ${opts.mode}`);
|
|
1203
|
-
console.log(` ${chalk.gray('working ')} ${cwd}`);
|
|
1337
|
+
console.log(` ${chalk.gray('working ')} ${hostName ? hostRepoPath : cwd}`);
|
|
1338
|
+
if (hostName) {
|
|
1339
|
+
console.log(` ${chalk.gray('host ')} ${chalk.cyan(hostName)}${chalk.gray(` (${hostTarget})`)}`);
|
|
1340
|
+
}
|
|
1204
1341
|
if (worktreeName) {
|
|
1205
1342
|
console.log(` ${chalk.gray('worktree')} ${chalk.cyan(worktreeName)}`);
|
|
1206
1343
|
}
|
|
@@ -1300,6 +1437,7 @@ export function registerTeamsCommands(program) {
|
|
|
1300
1437
|
started_at: a.startedAt.toISOString(),
|
|
1301
1438
|
cwd: a.cwd,
|
|
1302
1439
|
version: a.version,
|
|
1440
|
+
host: a.hostName,
|
|
1303
1441
|
})) }, null, 2));
|
|
1304
1442
|
return;
|
|
1305
1443
|
}
|
|
@@ -1317,7 +1455,10 @@ export function registerTeamsCommands(program) {
|
|
|
1317
1455
|
console.log(chalk.bold(`Team ${chalk.cyan(team)} ${chalk.gray(`(${agents.length} working)`)}`));
|
|
1318
1456
|
for (const a of agents) {
|
|
1319
1457
|
const ident = a.name || shortId(a.agentId);
|
|
1320
|
-
|
|
1458
|
+
// A distributed teammate has no local pid; show its host + remote pid.
|
|
1459
|
+
const pidStr = a.hostName
|
|
1460
|
+
? chalk.cyan(`on ${a.hostName}`) + (a.remotePid ? chalk.gray(` (pid ${a.remotePid})`) : '')
|
|
1461
|
+
: a.pid ? chalk.yellow(`pid ${a.pid}`) : chalk.gray('pid ?');
|
|
1321
1462
|
const started = chalk.gray(relTime(a.startedAt.toISOString()));
|
|
1322
1463
|
console.log(` ${chalk.magenta(padRight(fullName(a.agentType, a.version), 18))} ${chalk.white(padRight(ident, 20))} ${pidStr} ${started}`);
|
|
1323
1464
|
}
|
|
@@ -1544,13 +1685,24 @@ export function registerTeamsCommands(program) {
|
|
|
1544
1685
|
let worktreeKept = false;
|
|
1545
1686
|
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1546
1687
|
try {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1688
|
+
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
1689
|
+
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
1690
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath)) {
|
|
1691
|
+
worktreeKept = true;
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
1695
|
+
}
|
|
1550
1696
|
}
|
|
1551
1697
|
else {
|
|
1552
|
-
const
|
|
1553
|
-
|
|
1698
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1699
|
+
if (dirty) {
|
|
1700
|
+
worktreeKept = true;
|
|
1701
|
+
}
|
|
1702
|
+
else {
|
|
1703
|
+
const baseCwd = process.cwd();
|
|
1704
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1705
|
+
}
|
|
1554
1706
|
}
|
|
1555
1707
|
}
|
|
1556
1708
|
catch {
|
|
@@ -1667,12 +1819,23 @@ export function registerTeamsCommands(program) {
|
|
|
1667
1819
|
const agent = await mgr.get(a.agent_id);
|
|
1668
1820
|
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1669
1821
|
try {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1822
|
+
if (agent.hostName && agent.hostTarget && agent.repoPath) {
|
|
1823
|
+
// Distributed teammate: guard + remove the worktree ON THE HOST.
|
|
1824
|
+
if (remoteWorktreeDirty(agent.hostTarget, agent.worktreePath)) {
|
|
1825
|
+
keptWorktrees.push(agent.worktreeName);
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
removeRemoteWorktree(agent.hostTarget, agent.repoPath, agent.worktreeName);
|
|
1829
|
+
}
|
|
1673
1830
|
}
|
|
1674
1831
|
else {
|
|
1675
|
-
await
|
|
1832
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1833
|
+
if (dirty) {
|
|
1834
|
+
keptWorktrees.push(agent.worktreeName);
|
|
1835
|
+
}
|
|
1836
|
+
else {
|
|
1837
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1838
|
+
}
|
|
1676
1839
|
}
|
|
1677
1840
|
}
|
|
1678
1841
|
catch { /* best-effort */ }
|
|
@@ -1778,58 +1941,16 @@ export function registerTeamsCommands(program) {
|
|
|
1778
1941
|
.description('Check which agents are installed and available to join a team. Verifies CLI paths and shows an advisory sign-in hint.')
|
|
1779
1942
|
.option('--json', 'Output machine-readable JSON')
|
|
1780
1943
|
.action(async (opts) => {
|
|
1781
|
-
const
|
|
1782
|
-
// Deep integrity probe. `checkAllClis` reports presence (shim + stub guard),
|
|
1783
|
-
// but a GUTTED native binary (JS wrapper present, platform binary missing —
|
|
1784
|
-
// the codex/kimi optional-dep partial-extract failure) still passes that. So
|
|
1785
|
-
// actually launch the resolved default version and, if it won't run, flip the
|
|
1786
|
-
// agent to not-installed with a repair hint — otherwise doctor says "ready"
|
|
1787
|
-
// and the teammate ENOENTs at spawn. Parallel; win32 is treated as healthy by
|
|
1788
|
-
// verifyInstalledBinaryLaunches.
|
|
1789
|
-
await Promise.all(Object.entries(info).map(async ([name, entry]) => {
|
|
1790
|
-
if (!entry.installed)
|
|
1791
|
-
return;
|
|
1792
|
-
const agent = name;
|
|
1793
|
-
const version = resolveVersion(agent);
|
|
1794
|
-
if (!version)
|
|
1795
|
-
return;
|
|
1796
|
-
const health = await verifyInstalledBinaryLaunches(agent, version);
|
|
1797
|
-
if (!health.ok) {
|
|
1798
|
-
entry.installed = false;
|
|
1799
|
-
entry.path = null;
|
|
1800
|
-
entry.error = `${AGENTS[agent]?.cliCommand ?? name}@${version} is installed but its binary won't launch`
|
|
1801
|
-
+ `${health.detail ? ` (${health.detail})` : ''}. Repair: agents add ${agent}@${version}`;
|
|
1802
|
-
}
|
|
1803
|
-
}));
|
|
1804
|
-
// Advisory enrichment only. Sign-in detection is UNRELIABLE, so it never
|
|
1805
|
-
// changes the authoritative installed/ready column — it annotates. And an
|
|
1806
|
-
// agent that is actually running in a team is treated as signed in
|
|
1807
|
-
// regardless of the probe, so doctor never reports a working agent as
|
|
1808
|
-
// logged out ("don't show wrong stuff").
|
|
1809
|
-
const running = new Set();
|
|
1810
|
-
try {
|
|
1811
|
-
for (const a of await mkManager().listRunning())
|
|
1812
|
-
running.add(a.agentType);
|
|
1813
|
-
}
|
|
1814
|
-
catch { /* no teams yet — leave running empty */ }
|
|
1815
|
-
const auth = {};
|
|
1816
|
-
await Promise.all(Object.entries(info).map(async ([name, entry]) => {
|
|
1817
|
-
const isRunning = running.has(name);
|
|
1818
|
-
const probe = entry.installed && !isRunning ? await checkCliSignedIn(name) : false;
|
|
1819
|
-
auth[name] = resolveSignInAdvisory(entry.installed, isRunning, probe);
|
|
1820
|
-
}));
|
|
1944
|
+
const data = await collectTeamsDoctorData();
|
|
1821
1945
|
if (isJsonMode(opts)) {
|
|
1822
|
-
|
|
1823
|
-
for (const [name, entry] of Object.entries(info))
|
|
1824
|
-
merged[name] = { ...entry, ...auth[name] };
|
|
1825
|
-
console.log(JSON.stringify(merged, null, 2));
|
|
1946
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1826
1947
|
return;
|
|
1827
1948
|
}
|
|
1828
1949
|
console.log(chalk.bold('Who can join a team:'));
|
|
1829
|
-
for (const [name, entry] of Object.entries(
|
|
1950
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
1830
1951
|
const pretty = AGENT_NAMES[name] || name;
|
|
1831
1952
|
if (entry.installed) {
|
|
1832
|
-
const { signedIn, running: isRunning } =
|
|
1953
|
+
const { signedIn, running: isRunning } = entry;
|
|
1833
1954
|
const hint = isRunning
|
|
1834
1955
|
? chalk.gray('in use')
|
|
1835
1956
|
: signedIn
|
package/dist/commands/tmux.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* agents tmux kill-all [--yes]
|
|
22
22
|
*/
|
|
23
23
|
import chalk from 'chalk';
|
|
24
|
+
import { truncate } from '../lib/format.js';
|
|
24
25
|
import * as path from 'path';
|
|
25
26
|
import { setHelpSections } from '../lib/help.js';
|
|
26
27
|
import { assertTmuxAvailable, attachTmux, capturePane, createSession, getDefaultSocketPath, getTmuxVersion, hasSession, isTmuxInstalled, killAll, killSession, listSessions, readSessionMeta, sendKeys, splitPane, TmuxCommandError, TmuxSessionError, TmuxUnavailableError, } from '../lib/tmux/index.js';
|
|
@@ -395,9 +396,6 @@ function formatAge(secs) {
|
|
|
395
396
|
const h = Math.floor(m / 60);
|
|
396
397
|
return `${h}h ${m % 60}m`;
|
|
397
398
|
}
|
|
398
|
-
function truncate(s, n) {
|
|
399
|
-
return s.length > n ? `${s.slice(0, n - 1)}…` : s;
|
|
400
|
-
}
|
|
401
399
|
function printMeta(m) {
|
|
402
400
|
console.log(` ${chalk.bold('name')} ${m.name}`);
|
|
403
401
|
console.log(` ${chalk.bold('socket')} ${m.socket}`);
|
package/dist/commands/view.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { visibleWidth, termLink } from '../lib/format.js';
|
|
3
4
|
import ora from 'ora';
|
|
4
5
|
import * as fs from 'fs';
|
|
5
6
|
import * as path from 'path';
|
|
@@ -67,12 +68,6 @@ function profileKindAndModel(model, planWidth) {
|
|
|
67
68
|
const kind = 'profile'.padEnd(Math.max(planWidth, 'profile'.length));
|
|
68
69
|
return `${kind} ${model}`;
|
|
69
70
|
}
|
|
70
|
-
function termLink(text, filePath) {
|
|
71
|
-
if (!process.stdout.isTTY)
|
|
72
|
-
return text;
|
|
73
|
-
const url = `file://${filePath}`;
|
|
74
|
-
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
75
|
-
}
|
|
76
71
|
/**
|
|
77
72
|
* Resolve a resource path to something the IDE can open inline. When `p` is a
|
|
78
73
|
* directory, OSC 8 file:// links cause IDEs (Cursor/VS Code) to open it as a
|
|
@@ -112,9 +107,6 @@ function formatLastActive(date) {
|
|
|
112
107
|
return chalk.gray(`${days}d ago`);
|
|
113
108
|
return chalk.gray(`${days}d ago`);
|
|
114
109
|
}
|
|
115
|
-
function visibleWidth(s) {
|
|
116
|
-
return s.replace(/\u001b\[[0-9;]*m/g, '').length;
|
|
117
|
-
}
|
|
118
110
|
function compareVersions(a, b) {
|
|
119
111
|
const partsA = a.split('.').map(Number);
|
|
120
112
|
const partsB = b.split('.').map(Number);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { die } from '../lib/format.js';
|
|
2
3
|
import { execFile } from 'child_process';
|
|
3
4
|
import { promisify } from 'util';
|
|
4
5
|
import * as fs from 'fs/promises';
|
|
@@ -8,10 +9,6 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
const WORKTREE_SUBDIR = path.join('.agents', 'worktrees');
|
|
10
11
|
const BRANCH_PREFIX = 'agents/';
|
|
11
|
-
function die(msg, code = 1) {
|
|
12
|
-
console.error(chalk.red(msg));
|
|
13
|
-
process.exit(code);
|
|
14
|
-
}
|
|
15
12
|
function isValidTerminalId(id) {
|
|
16
13
|
// Allow letters, digits, dot, dash, underscore. Reject anything else so a
|
|
17
14
|
// hostile or buggy caller can't inject path traversal or shell metachars.
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -52,8 +52,6 @@ export declare function getCliPath(agentId: AgentId): Promise<string | null>;
|
|
|
52
52
|
export declare function getCliState(agentId: AgentId): Promise<CliState>;
|
|
53
53
|
/** Resolve CLI state for all registered agents in parallel. */
|
|
54
54
|
export declare function getAllCliStates(): Promise<Partial<Record<AgentId, CliState>>>;
|
|
55
|
-
/** Check whether the agent's config directory exists on disk. */
|
|
56
|
-
export declare function isConfigured(agentId: AgentId): boolean;
|
|
57
55
|
/** Info about an existing unmanaged agent installation. */
|
|
58
56
|
export interface UnmanagedInstall {
|
|
59
57
|
agentId: AgentId;
|
|
@@ -128,8 +126,6 @@ export declare function resolveLastActive(agentId: AgentId, base: string, config
|
|
|
128
126
|
* Used during init to show approximate session count to user.
|
|
129
127
|
*/
|
|
130
128
|
export declare function countSessionFiles(agentId: AgentId): number;
|
|
131
|
-
/** Check whether a named MCP server is registered with the agent's CLI. */
|
|
132
|
-
export declare function isMcpRegistered(agentId: AgentId, mcpName: string): Promise<boolean>;
|
|
133
129
|
/** Register an MCP server with an agent's CLI via `mcp add`. */
|
|
134
130
|
export declare function registerMcp(agentId: AgentId, name: string, command: string, scope?: 'user' | 'project', transport?: string, options?: {
|
|
135
131
|
home?: string;
|