@phnx-labs/agents-cli 1.20.85 → 1.20.87
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 +168 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +44 -5
- package/dist/commands/models.js +1 -1
- package/dist/commands/sessions-browser.d.ts +18 -0
- package/dist/commands/sessions-browser.js +126 -24
- package/dist/commands/sessions-picker.d.ts +21 -8
- package/dist/commands/sessions-picker.js +88 -11
- package/dist/commands/sessions.d.ts +19 -0
- package/dist/commands/sessions.js +147 -18
- package/dist/commands/ssh.js +59 -1
- package/dist/commands/teams-picker.d.ts +2 -0
- package/dist/commands/teams-picker.js +2 -1
- package/dist/commands/teams.d.ts +4 -1
- package/dist/commands/teams.js +106 -70
- package/dist/commands/view.js +14 -3
- package/dist/index.js +31 -1
- package/dist/lib/claude-account-token.d.ts +12 -0
- package/dist/lib/claude-account-token.js +63 -0
- package/dist/lib/devices/registry.d.ts +25 -0
- package/dist/lib/devices/registry.js +82 -1
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +13 -0
- package/dist/lib/exec.js +10 -1
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/models.d.ts +21 -0
- package/dist/lib/models.js +133 -4
- 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/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +2 -2
- package/dist/lib/session/db.d.ts +21 -0
- package/dist/lib/session/db.js +45 -4
- package/dist/lib/session/parse.d.ts +11 -0
- package/dist/lib/session/parse.js +24 -7
- package/dist/lib/session/remote-list.d.ts +7 -0
- package/dist/lib/session/remote-list.js +8 -4
- package/dist/lib/session/state.d.ts +6 -5
- package/dist/lib/session/state.js +84 -11
- package/dist/lib/session/team-filter.d.ts +22 -3
- package/dist/lib/session/team-filter.js +106 -17
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/signin-badge.d.ts +17 -0
- package/dist/lib/signin-badge.js +19 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/usage.js +1 -60
- package/package.json +1 -1
package/dist/commands/teams.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import {
|
|
2
|
+
import { dieFriction, relTime, truncate, isJsonMode, padRight } from '../lib/format.js';
|
|
3
3
|
import * as fs from 'fs/promises';
|
|
4
4
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
5
5
|
import * as path from 'path';
|
|
@@ -32,6 +32,7 @@ import { buildPreview as buildSessionPreview } from './sessions-picker.js';
|
|
|
32
32
|
import { parseExecEnv } from '../lib/exec.js';
|
|
33
33
|
import { checkRunAccountReadiness } from '../lib/rotate.js';
|
|
34
34
|
import { teamPicker, printTeamTable } from './teams-picker.js';
|
|
35
|
+
import { teamSpawners } from '../lib/session/db.js';
|
|
35
36
|
import { itemPicker } from '../lib/picker.js';
|
|
36
37
|
import { profileExists, readProfile } from '../lib/profiles.js';
|
|
37
38
|
import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, } from './utils.js';
|
|
@@ -187,10 +188,10 @@ function parseTeammate(spec) {
|
|
|
187
188
|
};
|
|
188
189
|
}
|
|
189
190
|
catch (err) {
|
|
190
|
-
|
|
191
|
+
dieFriction('teams', 'profile-malformed', `Profile '${name}' is malformed: ${err.message}`);
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
|
-
|
|
194
|
+
dieFriction('teams', 'unknown-teammate', `Unknown teammate '${spec}'. Available agents: ${VALID_AGENTS.join(', ')}.\n` +
|
|
194
195
|
` Use 'claude', 'kimi@latest', 'kimi@0.19.2' (a version from 'agents view'), or a profile name.`);
|
|
195
196
|
}
|
|
196
197
|
function shortId(id) {
|
|
@@ -840,7 +841,9 @@ function buildTasksFromSnapshots(agents) {
|
|
|
840
841
|
}
|
|
841
842
|
return tasks.sort((a, b) => new Date(b.modified_at).getTime() - new Date(a.modified_at).getTime());
|
|
842
843
|
}
|
|
843
|
-
export function buildTeamRowsFromSnapshots(registry, agents
|
|
844
|
+
export function buildTeamRowsFromSnapshots(registry, agents,
|
|
845
|
+
/** team name -> the session that spawned it (see teamSpawners). */
|
|
846
|
+
spawners) {
|
|
844
847
|
const byTeam = new Map();
|
|
845
848
|
for (const agent of agents) {
|
|
846
849
|
const details = byTeam.get(agent.task_name) || [];
|
|
@@ -854,6 +857,7 @@ export function buildTeamRowsFromSnapshots(registry, agents) {
|
|
|
854
857
|
team,
|
|
855
858
|
agents: byTeam.get(team.task_name) || [],
|
|
856
859
|
description: registry[team.task_name]?.description,
|
|
860
|
+
spawnedBy: spawners?.get(team.task_name)?.shortId,
|
|
857
861
|
})),
|
|
858
862
|
names: teams.map((team) => team.task_name),
|
|
859
863
|
};
|
|
@@ -908,7 +912,17 @@ async function loadTeamAgentSnapshots() {
|
|
|
908
912
|
// fallbacks for `status` / `start`; full status stays lazy until a team is picked.
|
|
909
913
|
async function loadTeamRows(_mgr) {
|
|
910
914
|
const [registry, agents] = await Promise.all([loadTeams(), loadTeamAgentSnapshots()]);
|
|
911
|
-
|
|
915
|
+
// Lineage is read off the session index, not the team registry: the registry is
|
|
916
|
+
// per-machine runtime state, while the transcript that ran `teams create` is
|
|
917
|
+
// indexed, survives the teammate cleanup, and covers teams created before this.
|
|
918
|
+
let spawners;
|
|
919
|
+
try {
|
|
920
|
+
spawners = teamSpawners();
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
// The index is an enrichment here — a missing/locked DB just drops the column.
|
|
924
|
+
}
|
|
925
|
+
return buildTeamRowsFromSnapshots(registry, agents, spawners);
|
|
912
926
|
}
|
|
913
927
|
// Picker fallback for `teams logs` when the teammate ref is omitted. Shows a
|
|
914
928
|
// flat list of every teammate with their team context; Enter picks one.
|
|
@@ -1090,7 +1104,16 @@ export function registerTeamsCommands(program) {
|
|
|
1090
1104
|
arr.push(a);
|
|
1091
1105
|
byTeam.set(a.task_name, arr);
|
|
1092
1106
|
}
|
|
1093
|
-
|
|
1107
|
+
// Same lineage enrichment as loadTeamRows — without it this listing is the
|
|
1108
|
+
// one `teams list` surface that silently drops the "spawned by" column.
|
|
1109
|
+
let spawners;
|
|
1110
|
+
try {
|
|
1111
|
+
spawners = teamSpawners();
|
|
1112
|
+
}
|
|
1113
|
+
catch {
|
|
1114
|
+
// The session index is an enrichment here; a missing one drops the column.
|
|
1115
|
+
}
|
|
1116
|
+
let rows = buildTeamRowsFromSnapshots(registry, everyAgent, spawners).rows;
|
|
1094
1117
|
// --- query: substring match on team name ---
|
|
1095
1118
|
if (query) {
|
|
1096
1119
|
const q = query.toLowerCase();
|
|
@@ -1112,7 +1135,7 @@ export function registerTeamsCommands(program) {
|
|
|
1112
1135
|
const want = opts.status.toLowerCase();
|
|
1113
1136
|
const validStatuses = ['working', 'done', 'failed', 'empty'];
|
|
1114
1137
|
if (!validStatuses.includes(want)) {
|
|
1115
|
-
|
|
1138
|
+
dieFriction('teams', 'invalid-status-filter', `Invalid --status '${opts.status}'. Use one of: ${validStatuses.join(', ')}`);
|
|
1116
1139
|
}
|
|
1117
1140
|
rows = rows.filter((row) => classifyTeamStatus(row.team) === want);
|
|
1118
1141
|
}
|
|
@@ -1120,13 +1143,13 @@ export function registerTeamsCommands(program) {
|
|
|
1120
1143
|
if (opts.since) {
|
|
1121
1144
|
const cutoff = parseTimeFilter(opts.since);
|
|
1122
1145
|
if (!cutoff)
|
|
1123
|
-
|
|
1146
|
+
dieFriction('teams', 'invalid-since-filter', `Could not parse --since '${opts.since}'`);
|
|
1124
1147
|
rows = rows.filter((row) => new Date(row.team.modified_at).getTime() >= cutoff);
|
|
1125
1148
|
}
|
|
1126
1149
|
if (opts.until) {
|
|
1127
1150
|
const cutoff = parseTimeFilter(opts.until);
|
|
1128
1151
|
if (!cutoff)
|
|
1129
|
-
|
|
1152
|
+
dieFriction('teams', 'invalid-until-filter', `Could not parse --until '${opts.until}'`);
|
|
1130
1153
|
rows = rows.filter((row) => new Date(row.team.modified_at).getTime() <= cutoff);
|
|
1131
1154
|
}
|
|
1132
1155
|
rows = rows.slice(0, limit);
|
|
@@ -1194,11 +1217,11 @@ export function registerTeamsCommands(program) {
|
|
|
1194
1217
|
continue;
|
|
1195
1218
|
const host = await resolveHost(name);
|
|
1196
1219
|
if (!host) {
|
|
1197
|
-
|
|
1220
|
+
dieFriction('teams', 'pool-device-not-resolvable', `Couldn't resolve pool device "${name}". Register it with \`agents devices\`, ` +
|
|
1198
1221
|
`enroll it with \`agents hosts add ${name}\`, or pass user@host.`);
|
|
1199
1222
|
}
|
|
1200
1223
|
if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
|
|
1201
|
-
|
|
1224
|
+
dieFriction('teams', 'pool-device-windows-unsupported', `Distributed teams on Windows device "${host.name}" are not supported yet — ` +
|
|
1202
1225
|
`the teams remote monitor is POSIX-only. Use a Linux/macOS device.`);
|
|
1203
1226
|
}
|
|
1204
1227
|
}
|
|
@@ -1246,7 +1269,7 @@ export function registerTeamsCommands(program) {
|
|
|
1246
1269
|
}
|
|
1247
1270
|
}
|
|
1248
1271
|
catch (err) {
|
|
1249
|
-
|
|
1272
|
+
dieFriction('teams', 'create-failed', err.message);
|
|
1250
1273
|
}
|
|
1251
1274
|
});
|
|
1252
1275
|
// add
|
|
@@ -1273,29 +1296,29 @@ export function registerTeamsCommands(program) {
|
|
|
1273
1296
|
// silently ignoring it — see remoteCwdOnAddError. `!== undefined` so even an
|
|
1274
1297
|
// explicit empty value (`--remote-cwd ""`) is rejected, not silently dropped.
|
|
1275
1298
|
if (opts.remoteCwd !== undefined) {
|
|
1276
|
-
|
|
1299
|
+
dieFriction('teams', 'remote-cwd-on-add', remoteCwdOnAddError(team));
|
|
1277
1300
|
}
|
|
1278
1301
|
if (!VALID_MODES.includes(opts.mode)) {
|
|
1279
|
-
|
|
1302
|
+
dieFriction('teams', 'invalid-mode', `Invalid mode '${opts.mode}'. Use one of: ${VALID_MODES.join(', ')}`);
|
|
1280
1303
|
}
|
|
1281
1304
|
if (!VALID_EFFORTS.includes(opts.effort)) {
|
|
1282
|
-
|
|
1305
|
+
dieFriction('teams', 'invalid-effort', `Invalid effort '${opts.effort}'. Use one of: ${VALID_EFFORTS.join(', ')}`);
|
|
1283
1306
|
}
|
|
1284
1307
|
let taskType = null;
|
|
1285
1308
|
if (opts.taskType) {
|
|
1286
1309
|
if (!VALID_TASK_TYPES.includes(opts.taskType)) {
|
|
1287
|
-
|
|
1310
|
+
dieFriction('teams', 'invalid-task-type', `Invalid task-type '${opts.taskType}'. Use one of: ${VALID_TASK_TYPES.join(', ')}`);
|
|
1288
1311
|
}
|
|
1289
1312
|
taskType = opts.taskType;
|
|
1290
1313
|
}
|
|
1291
1314
|
let cloudProviderId = null;
|
|
1292
1315
|
if (opts.cloud) {
|
|
1293
1316
|
if (!VALID_CLOUD_PROVIDERS.includes(opts.cloud)) {
|
|
1294
|
-
|
|
1317
|
+
dieFriction('teams', 'invalid-cloud-provider', `Invalid cloud provider '${opts.cloud}'. Use one of: ${VALID_CLOUD_PROVIDERS.join(', ')}`);
|
|
1295
1318
|
}
|
|
1296
1319
|
cloudProviderId = opts.cloud;
|
|
1297
1320
|
if (cloudProviderId === 'rush' && !opts.repo) {
|
|
1298
|
-
|
|
1321
|
+
dieFriction('teams', 'cloud-rush-needs-repo', `--cloud rush requires --repo <owner/repo>`);
|
|
1299
1322
|
}
|
|
1300
1323
|
}
|
|
1301
1324
|
// Auto-create the team if it doesn't exist yet (friendlier UX than erroring),
|
|
@@ -1310,7 +1333,7 @@ export function registerTeamsCommands(program) {
|
|
|
1310
1333
|
const h = opts.host;
|
|
1311
1334
|
const d = opts.device;
|
|
1312
1335
|
if (h && d && h !== d) {
|
|
1313
|
-
|
|
1336
|
+
dieFriction('teams', 'conflicting-host-device', 'Conflicting --host/--device values — pass just one.');
|
|
1314
1337
|
}
|
|
1315
1338
|
return h ?? d ?? null;
|
|
1316
1339
|
})();
|
|
@@ -1324,18 +1347,18 @@ export function registerTeamsCommands(program) {
|
|
|
1324
1347
|
let hostRepoPath = null;
|
|
1325
1348
|
if (explicitDevice && explicitDevice.toLowerCase() !== machineId()) {
|
|
1326
1349
|
if (cloudProviderId) {
|
|
1327
|
-
|
|
1350
|
+
dieFriction('teams', 'device-cloud-mutually-exclusive', `--device and --cloud are mutually exclusive (two different remote backends). Pick one.`);
|
|
1328
1351
|
}
|
|
1329
1352
|
const host = await resolveHost(explicitDevice);
|
|
1330
1353
|
if (!host) {
|
|
1331
|
-
|
|
1354
|
+
dieFriction('teams', 'device-not-resolvable', `Couldn't resolve --device "${explicitDevice}". Register it with \`agents devices\`, ` +
|
|
1332
1355
|
`enroll it with \`agents hosts add ${explicitDevice}\`, or pass user@host.`);
|
|
1333
1356
|
}
|
|
1334
1357
|
// POSIX-only in v1: the remote follow/monitor layer offset-tails the log
|
|
1335
1358
|
// with tail/cat/kill, which don't exist under PowerShell. Refuse Windows
|
|
1336
1359
|
// up front, mirroring dispatch.ts launchDetached.
|
|
1337
1360
|
if (remoteShellFor(host.os ?? resolveRemoteOsSync(host.name)) === 'powershell') {
|
|
1338
|
-
|
|
1361
|
+
dieFriction('teams', 'device-windows-unsupported', `Distributed teammates on Windows host "${host.name}" are not supported yet — ` +
|
|
1339
1362
|
`the teams remote monitor is POSIX-only (offset-tails the remote log with tail/cat/kill). ` +
|
|
1340
1363
|
`Use a Linux/macOS host, or run this teammate locally.`);
|
|
1341
1364
|
}
|
|
@@ -1343,7 +1366,7 @@ export function registerTeamsCommands(program) {
|
|
|
1343
1366
|
hostTarget = sshTargetFor(host);
|
|
1344
1367
|
}
|
|
1345
1368
|
catch (err) {
|
|
1346
|
-
|
|
1369
|
+
dieFriction('teams', 'ssh-target-unresolvable', `Can't resolve an ssh target for "${host.name}": ${err.message}`);
|
|
1347
1370
|
}
|
|
1348
1371
|
// Ensure agents-cli is present + version-matched on the host; surface
|
|
1349
1372
|
// (not fail on) an agent-not-installed warning, like the run --host path.
|
|
@@ -1353,7 +1376,7 @@ export function registerTeamsCommands(program) {
|
|
|
1353
1376
|
process.stderr.write(chalk.yellow(`[teams] warning: ${w}\n`));
|
|
1354
1377
|
}
|
|
1355
1378
|
catch (err) {
|
|
1356
|
-
|
|
1379
|
+
dieFriction('teams', 'host-not-ready', `Host "${host.name}" is not ready: ${err.message}`);
|
|
1357
1380
|
}
|
|
1358
1381
|
// Provision the repo on the host from the team's --repo (clone into
|
|
1359
1382
|
// ~/.agents/repos/<team> or reuse an existing checkout), resolving to the
|
|
@@ -1370,7 +1393,7 @@ export function registerTeamsCommands(program) {
|
|
|
1370
1393
|
hostRepoPath = ensureRemoteRepo(hostTarget, effectiveRepo, team);
|
|
1371
1394
|
}
|
|
1372
1395
|
catch (err) {
|
|
1373
|
-
|
|
1396
|
+
dieFriction('teams', 'repo-provision-failed', `Couldn't provision the repo on "${host.name}": ${err.message}\n` +
|
|
1374
1397
|
` Set how each device gets the code with: agents teams create ${team} --repo <url|path>`);
|
|
1375
1398
|
}
|
|
1376
1399
|
hostName = host.name;
|
|
@@ -1380,7 +1403,7 @@ export function registerTeamsCommands(program) {
|
|
|
1380
1403
|
// Version-installed check is about the LOCAL machine — a distributed (--on)
|
|
1381
1404
|
// teammate's agent/version lives on the host, verified by ensureHostReady.
|
|
1382
1405
|
if (version && !hostName && !isVersionInstalled(agent, version)) {
|
|
1383
|
-
|
|
1406
|
+
dieFriction('teams', 'agent-version-not-installed', `${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
|
|
1384
1407
|
` Install it: agents add ${agent}@${version}\n` +
|
|
1385
1408
|
` Or see what's installed (incl. @latest): agents view ${agent}`);
|
|
1386
1409
|
}
|
|
@@ -1404,26 +1427,26 @@ export function registerTeamsCommands(program) {
|
|
|
1404
1427
|
}
|
|
1405
1428
|
if (opts.name !== undefined) {
|
|
1406
1429
|
if (!opts.name || !/^[A-Za-z0-9_-]+$/.test(opts.name)) {
|
|
1407
|
-
|
|
1430
|
+
dieFriction('teams', 'invalid-teammate-name', `Invalid teammate name '${opts.name}'. Use letters, numbers, '-', or '_'.`);
|
|
1408
1431
|
}
|
|
1409
1432
|
}
|
|
1410
1433
|
if (opts.worktree !== undefined) {
|
|
1411
1434
|
if (!opts.worktree || !/^[A-Za-z0-9_-]+$/.test(opts.worktree)) {
|
|
1412
|
-
|
|
1435
|
+
dieFriction('teams', 'invalid-worktree-name', `Invalid worktree name '${opts.worktree}'. Use letters, numbers, '-', or '_'.`);
|
|
1413
1436
|
}
|
|
1414
1437
|
}
|
|
1415
1438
|
const after = opts.after
|
|
1416
1439
|
? opts.after.split(',').map((s) => s.trim()).filter(Boolean)
|
|
1417
1440
|
: [];
|
|
1418
1441
|
if (after.length > 0 && !opts.name) {
|
|
1419
|
-
|
|
1442
|
+
dieFriction('teams', 'after-requires-name', "--after requires --name (dependencies reference teammates by name).");
|
|
1420
1443
|
}
|
|
1421
1444
|
let envOverrides;
|
|
1422
1445
|
try {
|
|
1423
1446
|
envOverrides = parseExecEnv(opts.env);
|
|
1424
1447
|
}
|
|
1425
1448
|
catch (err) {
|
|
1426
|
-
|
|
1449
|
+
dieFriction('teams', 'invalid-env', err.message);
|
|
1427
1450
|
}
|
|
1428
1451
|
// Team already ensured + loaded above (teamMeta) for the --repo provisioning.
|
|
1429
1452
|
const worktreesEnabled = teamMeta?.enable_worktrees ?? false;
|
|
@@ -1436,19 +1459,19 @@ export function registerTeamsCommands(program) {
|
|
|
1436
1459
|
// remote teammate; a per-teammate worktree is created ON THE HOST at launch
|
|
1437
1460
|
// (createRemoteWorktree in launchRemoteProcess) — we just capture its name.
|
|
1438
1461
|
if (sharedWorktree) {
|
|
1439
|
-
|
|
1462
|
+
dieFriction('teams', 'shared-worktree-remote-conflict', `Team '${team}' uses a shared local --use-worktree, which can't apply to a --device (remote) teammate.`);
|
|
1440
1463
|
}
|
|
1441
1464
|
if (worktreesEnabled) {
|
|
1442
1465
|
if (!opts.worktree) {
|
|
1443
|
-
|
|
1466
|
+
dieFriction('teams', 'worktree-required-remote', `Team '${team}' has worktrees enabled. Use --worktree <name> for the remote teammate (created on ${hostName}).`);
|
|
1444
1467
|
}
|
|
1445
1468
|
if (!opts.name) {
|
|
1446
|
-
|
|
1469
|
+
dieFriction('teams', 'name-required-remote', `Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
|
|
1447
1470
|
}
|
|
1448
1471
|
worktreeName = opts.worktree;
|
|
1449
1472
|
}
|
|
1450
1473
|
else if (opts.worktree) {
|
|
1451
|
-
|
|
1474
|
+
dieFriction('teams', 'worktree-requires-enable-worktrees', `--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
1452
1475
|
}
|
|
1453
1476
|
// Local cwd stays null — the remote cwd is repoPath / the remote worktree.
|
|
1454
1477
|
}
|
|
@@ -1458,38 +1481,38 @@ export function registerTeamsCommands(program) {
|
|
|
1458
1481
|
try {
|
|
1459
1482
|
const stat = await fsp.stat(sharedWorktree);
|
|
1460
1483
|
if (!stat.isDirectory()) {
|
|
1461
|
-
|
|
1484
|
+
dieFriction('teams', 'shared-worktree-not-dir', `Shared worktree path is not a directory: ${sharedWorktree}`);
|
|
1462
1485
|
}
|
|
1463
1486
|
}
|
|
1464
1487
|
catch {
|
|
1465
|
-
|
|
1488
|
+
dieFriction('teams', 'shared-worktree-missing', `Shared worktree path does not exist: ${sharedWorktree}`);
|
|
1466
1489
|
}
|
|
1467
1490
|
worktreePath = sharedWorktree;
|
|
1468
1491
|
if (opts.worktree) {
|
|
1469
|
-
|
|
1492
|
+
dieFriction('teams', 'worktree-on-shared-team', `Team '${team}' uses --use-worktree (shared). Don't pass --worktree on add.`);
|
|
1470
1493
|
}
|
|
1471
1494
|
}
|
|
1472
1495
|
else if (worktreesEnabled) {
|
|
1473
1496
|
if (!opts.worktree) {
|
|
1474
|
-
|
|
1497
|
+
dieFriction('teams', 'worktree-required', `Team '${team}' has worktrees enabled. Use --worktree <name> to specify a worktree name.`);
|
|
1475
1498
|
}
|
|
1476
1499
|
if (!opts.name) {
|
|
1477
|
-
|
|
1500
|
+
dieFriction('teams', 'name-required', `Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
|
|
1478
1501
|
}
|
|
1479
1502
|
const baseCwd = opts.cwd ?? process.cwd();
|
|
1480
1503
|
if (!(await isGitRepo(baseCwd))) {
|
|
1481
|
-
|
|
1504
|
+
dieFriction('teams', 'worktree-needs-repo', `Worktrees require a git repository. ${baseCwd} is not inside a git repo.`);
|
|
1482
1505
|
}
|
|
1483
1506
|
try {
|
|
1484
1507
|
worktreeName = opts.worktree;
|
|
1485
1508
|
worktreePath = await createWorktree(baseCwd, worktreeName);
|
|
1486
1509
|
}
|
|
1487
1510
|
catch (err) {
|
|
1488
|
-
|
|
1511
|
+
dieFriction('teams', 'worktree-create-failed', `Failed to create worktree '${opts.worktree}': ${err.message}`);
|
|
1489
1512
|
}
|
|
1490
1513
|
}
|
|
1491
1514
|
else if (opts.worktree) {
|
|
1492
|
-
|
|
1515
|
+
dieFriction('teams', 'worktree-requires-enable-worktrees', `--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
1493
1516
|
}
|
|
1494
1517
|
// Distributed teammates have no LOCAL cwd — their working dir lives on the
|
|
1495
1518
|
// host (repoPath / the remote worktree). Local teammates default to the
|
|
@@ -1541,7 +1564,7 @@ export function registerTeamsCommands(program) {
|
|
|
1541
1564
|
cloudSessionId = cloudTask.id;
|
|
1542
1565
|
}
|
|
1543
1566
|
catch (err) {
|
|
1544
|
-
|
|
1567
|
+
dieFriction('teams', 'cloud-dispatch-failed', `Cloud dispatch failed: ${err.message}`);
|
|
1545
1568
|
}
|
|
1546
1569
|
}
|
|
1547
1570
|
try {
|
|
@@ -1595,7 +1618,7 @@ export function registerTeamsCommands(program) {
|
|
|
1595
1618
|
}
|
|
1596
1619
|
}
|
|
1597
1620
|
catch (err) {
|
|
1598
|
-
|
|
1621
|
+
dieFriction('teams', 'add-failed', `Could not add ${fullName(agent, version)} to ${team}: ${err.message}`);
|
|
1599
1622
|
}
|
|
1600
1623
|
});
|
|
1601
1624
|
// status
|
|
@@ -1605,20 +1628,22 @@ export function registerTeamsCommands(program) {
|
|
|
1605
1628
|
.option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
|
|
1606
1629
|
.option('-s, --since <iso>', 'Cursor from a previous status call; only show updates after this timestamp (enables efficient polling)')
|
|
1607
1630
|
.option('--agent-id <id>', 'Show only this one teammate (by UUID or UUID prefix)')
|
|
1631
|
+
.option('--parent-session <id>', 'Show the teammates spawned BY this session, across teams. Resolves only teammates whose record survives the teams cleanup window.')
|
|
1608
1632
|
.option('-v, --verbose', 'Emit the full per-teammate detail (prompt, all file paths, all messages). Default is a compact summary.')
|
|
1609
1633
|
.option('--json', 'Output machine-readable JSON')
|
|
1610
1634
|
.action(async (team, opts) => {
|
|
1611
1635
|
const filter = opts.filter;
|
|
1612
1636
|
const mgr = mkManager();
|
|
1613
1637
|
// No team given → drop into the picker (TTY) or fail clearly (script).
|
|
1614
|
-
|
|
1638
|
+
// --parent-session is its own lookup key, so it needs no team at all.
|
|
1639
|
+
if (!team && !opts.parentSession) {
|
|
1615
1640
|
const picked = await pickTeamOr(mgr, 'agents teams status');
|
|
1616
1641
|
if (!picked)
|
|
1617
1642
|
return;
|
|
1618
1643
|
team = picked;
|
|
1619
1644
|
}
|
|
1620
1645
|
try {
|
|
1621
|
-
const result = await handleStatus(mgr, team, filter, opts.since);
|
|
1646
|
+
const result = await handleStatus(mgr, team, filter, opts.since, opts.parentSession);
|
|
1622
1647
|
const agents = opts.agentId
|
|
1623
1648
|
? result.agents.filter((a) => a.agent_id.startsWith(opts.agentId))
|
|
1624
1649
|
: result.agents;
|
|
@@ -1633,20 +1658,31 @@ export function registerTeamsCommands(program) {
|
|
|
1633
1658
|
console.log(JSON.stringify(payload, null, 2));
|
|
1634
1659
|
return;
|
|
1635
1660
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1661
|
+
// A --parent-session lookup spans teams, so there is no single team to
|
|
1662
|
+
// check for existence — its label is the session that spawned them.
|
|
1663
|
+
const label = team ?? `session ${opts.parentSession.slice(0, 8)}`;
|
|
1664
|
+
if (team) {
|
|
1665
|
+
const exists = await teamExists(team);
|
|
1666
|
+
if (!exists && result.agents.length === 0) {
|
|
1667
|
+
console.log(chalk.yellow(`No team called '${team}'. Create it with: agents teams create ${team}`));
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
else if (result.agents.length === 0) {
|
|
1672
|
+
console.log(chalk.yellow(`No teammates recorded for ${label}.`));
|
|
1673
|
+
console.log(chalk.gray('Teammate records are cleaned up after 7 days; an older team may have aged out.'));
|
|
1639
1674
|
return;
|
|
1640
1675
|
}
|
|
1641
1676
|
if (opts.verbose) {
|
|
1642
|
-
await printTeamStatus(
|
|
1677
|
+
await printTeamStatus(label, filtered);
|
|
1643
1678
|
}
|
|
1644
1679
|
else {
|
|
1645
|
-
printTeamSummary(
|
|
1680
|
+
printTeamSummary(label, toTaskStatusSummary(filtered));
|
|
1646
1681
|
}
|
|
1647
1682
|
}
|
|
1648
1683
|
catch (err) {
|
|
1649
|
-
|
|
1684
|
+
// `team` is undefined for a --parent-session lookup, which spans teams.
|
|
1685
|
+
dieFriction('teams', 'status-failed', `Could not check on ${team ? `team ${team}` : 'the requested session'}: ${err.message}`);
|
|
1650
1686
|
}
|
|
1651
1687
|
});
|
|
1652
1688
|
// active — list every live teammate across every team, grouped by team.
|
|
@@ -1908,18 +1944,18 @@ export function registerTeamsCommands(program) {
|
|
|
1908
1944
|
}
|
|
1909
1945
|
const lookup = await mgr.resolveAgentIdInTask(team, ref);
|
|
1910
1946
|
if (lookup.kind === 'none') {
|
|
1911
|
-
|
|
1947
|
+
dieFriction('teams', 'teammate-not-found', `No teammate matching '${ref}' in team ${team}`, 2);
|
|
1912
1948
|
}
|
|
1913
1949
|
if (lookup.kind === 'ambiguous') {
|
|
1914
1950
|
const shorts = lookup.matches.map(shortId).join(', ');
|
|
1915
|
-
|
|
1951
|
+
dieFriction('teams', 'teammate-ambiguous', `'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
|
|
1916
1952
|
}
|
|
1917
1953
|
const agentId = lookup.agentId;
|
|
1918
1954
|
const agent = await mgr.get(agentId);
|
|
1919
1955
|
const display = agent?.name || shortId(agentId);
|
|
1920
1956
|
const stopRes = await handleStop(mgr, team, agentId);
|
|
1921
1957
|
if ('error' in stopRes)
|
|
1922
|
-
|
|
1958
|
+
dieFriction('teams', 'stop-failed', stopRes.error);
|
|
1923
1959
|
// Clean up worktree if this teammate had one
|
|
1924
1960
|
let worktreeKept = false;
|
|
1925
1961
|
if (agent?.worktreeName && agent?.worktreePath) {
|
|
@@ -1977,10 +2013,10 @@ export function registerTeamsCommands(program) {
|
|
|
1977
2013
|
const mgr = mkManager();
|
|
1978
2014
|
const lookup = await mgr.resolveAgentIdInTask(team, ref);
|
|
1979
2015
|
if (lookup.kind === 'none')
|
|
1980
|
-
|
|
2016
|
+
dieFriction('teams', 'teammate-not-found', `No teammate matching '${ref}' in team ${team}`, 2);
|
|
1981
2017
|
if (lookup.kind === 'ambiguous') {
|
|
1982
2018
|
const shorts = lookup.matches.map(shortId).join(', ');
|
|
1983
|
-
|
|
2019
|
+
dieFriction('teams', 'teammate-ambiguous', `'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
|
|
1984
2020
|
}
|
|
1985
2021
|
const agentId = lookup.agentId;
|
|
1986
2022
|
// mgr.get reconciles the teammate's status (PID + start-time guard / remote
|
|
@@ -1988,20 +2024,20 @@ export function registerTeamsCommands(program) {
|
|
|
1988
2024
|
// is a fact, not a guess.
|
|
1989
2025
|
const agent = await mgr.get(agentId);
|
|
1990
2026
|
if (!agent)
|
|
1991
|
-
|
|
2027
|
+
dieFriction('teams', 'teammate-vanished', `Teammate ${shortId(agentId)} vanished from team ${team}.`);
|
|
1992
2028
|
const display = agent.name || shortId(agentId);
|
|
1993
2029
|
const status = agent.status;
|
|
1994
2030
|
const hasMessage = message != null && message.trim().length > 0;
|
|
1995
2031
|
const route = decideTeamMessageRoute(status, hasMessage);
|
|
1996
2032
|
switch (route.kind) {
|
|
1997
2033
|
case 'not-started':
|
|
1998
|
-
|
|
2034
|
+
dieFriction('teams', 'teammate-not-started', `Teammate '${display}' hasn't started yet (waiting on --after deps). Run \`agents teams start ${team}\` to launch it.`);
|
|
1999
2035
|
return;
|
|
2000
2036
|
case 'need-message':
|
|
2001
2037
|
if (status === AgentStatus.RUNNING) {
|
|
2002
|
-
|
|
2038
|
+
dieFriction('teams', 'steer-needs-message', `Teammate '${display}' is running — pass a message to steer it.`);
|
|
2003
2039
|
}
|
|
2004
|
-
|
|
2040
|
+
dieFriction('teams', 'resume-needs-message', `Teammate '${display}' is ${status} — pass a message to resume it: \`agents teams resume ${team} ${display} "<message>"\`.`);
|
|
2005
2041
|
return;
|
|
2006
2042
|
case 'steer': {
|
|
2007
2043
|
// Running -> steer via mailbox; never re-launch (that forks a 2nd session).
|
|
@@ -2020,7 +2056,7 @@ export function registerTeamsCommands(program) {
|
|
|
2020
2056
|
await mgr.resumeTeammate(agentId, message);
|
|
2021
2057
|
}
|
|
2022
2058
|
catch (err) {
|
|
2023
|
-
|
|
2059
|
+
dieFriction('teams', 'resume-failed', err.message);
|
|
2024
2060
|
}
|
|
2025
2061
|
if (isJsonMode(opts)) {
|
|
2026
2062
|
console.log(JSON.stringify({ team, agent_id: agentId, name: agent.name ?? null, action: 'resume', prior_status: status }, null, 2));
|
|
@@ -2079,18 +2115,18 @@ export function registerTeamsCommands(program) {
|
|
|
2079
2115
|
}
|
|
2080
2116
|
const lookup = await mgr.resolveAgentIdInTask(team, ref);
|
|
2081
2117
|
if (lookup.kind === 'none') {
|
|
2082
|
-
|
|
2118
|
+
dieFriction('teams', 'teammate-not-found', `No teammate matching '${ref}' in team ${team}`, 2);
|
|
2083
2119
|
}
|
|
2084
2120
|
if (lookup.kind === 'ambiguous') {
|
|
2085
2121
|
const shorts = lookup.matches.map(shortId).join(', ');
|
|
2086
|
-
|
|
2122
|
+
dieFriction('teams', 'teammate-ambiguous', `'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
|
|
2087
2123
|
}
|
|
2088
2124
|
const agentId = lookup.agentId;
|
|
2089
2125
|
const agent = await mgr.get(agentId);
|
|
2090
2126
|
const display = agent?.name || shortId(agentId);
|
|
2091
2127
|
// Require agent to be stopped first
|
|
2092
2128
|
if (agent?.status === 'running' || agent?.status === 'pending') {
|
|
2093
|
-
|
|
2129
|
+
dieFriction('teams', 'remove-still-running', `Teammate '${display}' is still ${agent.status}. Run 'agents teams stop ${team} ${display}' first.`);
|
|
2094
2130
|
}
|
|
2095
2131
|
if (!opts.keepLogs) {
|
|
2096
2132
|
try {
|
|
@@ -2128,7 +2164,7 @@ export function registerTeamsCommands(program) {
|
|
|
2128
2164
|
}
|
|
2129
2165
|
const stopRes = await handleStop(mgr, team);
|
|
2130
2166
|
if ('error' in stopRes)
|
|
2131
|
-
|
|
2167
|
+
dieFriction('teams', 'stop-failed', stopRes.error);
|
|
2132
2168
|
const status = await handleStatus(mgr, team, 'all');
|
|
2133
2169
|
// Clean up worktrees for all teammates
|
|
2134
2170
|
const baseCwd = process.cwd();
|
|
@@ -2176,7 +2212,7 @@ export function registerTeamsCommands(program) {
|
|
|
2176
2212
|
return;
|
|
2177
2213
|
}
|
|
2178
2214
|
if (!existed && stopRes.stopped.length === 0 && status.agents.length === 0) {
|
|
2179
|
-
|
|
2215
|
+
dieFriction('teams', 'team-not-found', `No team called '${team}'`, 2);
|
|
2180
2216
|
}
|
|
2181
2217
|
console.log(chalk.green(`Team ${chalk.cyan(team)} disbanded.`));
|
|
2182
2218
|
if (stopRes.stopped.length)
|
|
@@ -2214,11 +2250,11 @@ export function registerTeamsCommands(program) {
|
|
|
2214
2250
|
else {
|
|
2215
2251
|
const resolved = await resolveTeammateAcrossTeams(base, teammateRef, opts.team);
|
|
2216
2252
|
if (resolved.kind === 'none') {
|
|
2217
|
-
|
|
2253
|
+
dieFriction('teams', 'teammate-notes-not-found', `No notes on record for teammate '${teammateRef}'`, 2);
|
|
2218
2254
|
}
|
|
2219
2255
|
if (resolved.kind === 'ambiguous') {
|
|
2220
2256
|
const hints = resolved.candidates.map((c) => `${c.team}/${c.display}`).join(', ');
|
|
2221
|
-
|
|
2257
|
+
dieFriction('teams', 'teammate-notes-ambiguous', `'${teammateRef}' matches multiple teammates: ${hints}.\n` +
|
|
2222
2258
|
` Narrow it with --team <team>, or pass a UUID prefix.`, 2);
|
|
2223
2259
|
}
|
|
2224
2260
|
agentId = resolved.agentId;
|
|
@@ -2249,7 +2285,7 @@ export function registerTeamsCommands(program) {
|
|
|
2249
2285
|
process.stdout.write(lines.slice(-n).join('\n'));
|
|
2250
2286
|
}
|
|
2251
2287
|
catch {
|
|
2252
|
-
|
|
2288
|
+
dieFriction('teams', 'teammate-notes-not-found', `No notes on record for teammate '${teammateRef ?? agentId}' (looked in ${logPath})`, 2);
|
|
2253
2289
|
}
|
|
2254
2290
|
});
|
|
2255
2291
|
// doctor
|
package/dist/commands/view.js
CHANGED
|
@@ -5,7 +5,7 @@ import ora from 'ora';
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanagedCliState, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
|
-
import { loginHint } from '../lib/signin-badge.js';
|
|
8
|
+
import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
|
|
9
9
|
import { machineId } from '../lib/machine-id.js';
|
|
10
10
|
import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
|
|
11
11
|
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
@@ -530,8 +530,19 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
530
530
|
if (runDefaults.mode)
|
|
531
531
|
runDefaultBits.push(`mode:${runDefaults.mode}`);
|
|
532
532
|
if (!hasEmail && !hasUsage && !signedIn) {
|
|
533
|
-
//
|
|
534
|
-
|
|
533
|
+
// No per-version credential. That is NOT the same as unusable: Claude
|
|
534
|
+
// Code authenticates from `CLAUDE_CODE_OAUTH_TOKEN` when the
|
|
535
|
+
// environment carries one, and a run then succeeds against whatever
|
|
536
|
+
// account minted that token — while `signedIn` (agents.ts: `!!email`,
|
|
537
|
+
// read from this version home's `.claude.json`) stays false because no
|
|
538
|
+
// account was ever written here. Reporting that as "logged out" reads
|
|
539
|
+
// as a locked-out account and sends people hunting a login that is not
|
|
540
|
+
// missing; naming the ambient token instead points at the real state —
|
|
541
|
+
// every version on this box resolves to the SAME account, so balanced
|
|
542
|
+
// rotation across them is not actually rotating.
|
|
543
|
+
parts.push(chalk.gray(ambientClaudeToken(agentId)
|
|
544
|
+
? '(no per-version login — using ambient CLAUDE_CODE_OAUTH_TOKEN)'
|
|
545
|
+
: '(logged out — log in with: ' + loginHint(agentId) + ')'));
|
|
535
546
|
}
|
|
536
547
|
else {
|
|
537
548
|
if (hasEmail || hasUsage || hasActive || signedIn) {
|
package/dist/index.js
CHANGED
|
@@ -98,7 +98,7 @@ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedbac
|
|
|
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';
|
|
101
|
-
import { emit, redactArgs } from './lib/events.js';
|
|
101
|
+
import { emit, emitFriction, redactArgs } from './lib/events.js';
|
|
102
102
|
// Transparent shim delegate: the generated Windows `.cmd` shims invoke
|
|
103
103
|
// `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
|
|
104
104
|
// parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
|
|
@@ -691,6 +691,32 @@ function registerResourcesTombstoneCommand(p) {
|
|
|
691
691
|
await program.parseAsync(['node', 'agents', ...args]);
|
|
692
692
|
});
|
|
693
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Hidden `agents _internal <sub>` namespace for machine-to-machine calls that
|
|
696
|
+
* are not user-facing. The first subcommand is `friction`, used by shell guard
|
|
697
|
+
* hooks (git-guard, rm-guard, …) to self-report a block into the event log
|
|
698
|
+
* before they exit 2 — they run before any `agents` process exists, so they
|
|
699
|
+
* cannot emit in-process.
|
|
700
|
+
*/
|
|
701
|
+
function registerInternalCommand(p) {
|
|
702
|
+
const internal = p.command('_internal', { hidden: true });
|
|
703
|
+
internal
|
|
704
|
+
.command('friction')
|
|
705
|
+
.option('--surface <surface>', 'Subsystem that hit the failure (e.g. guard, teams)')
|
|
706
|
+
.option('--id <failureId>', 'Stable failure slug (e.g. git.reset-hard)')
|
|
707
|
+
.option('--error <message>', 'Human-readable failure reason')
|
|
708
|
+
.option('--command <command>', 'The command that was blocked')
|
|
709
|
+
.action((opts) => {
|
|
710
|
+
if (!opts.surface || !opts.id) {
|
|
711
|
+
process.exit(0); // fail-open: never break the caller
|
|
712
|
+
}
|
|
713
|
+
emitFriction(opts.surface, opts.id, {
|
|
714
|
+
...(opts.error ? { error: opts.error } : {}),
|
|
715
|
+
...(opts.command ? { command: opts.command } : {}),
|
|
716
|
+
});
|
|
717
|
+
process.exit(0);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
694
720
|
/** Self-upgrade command (`agents upgrade [version]`). */
|
|
695
721
|
function registerUpgradeCommand(p) {
|
|
696
722
|
p.command('upgrade')
|
|
@@ -784,6 +810,9 @@ async function registerEagerForRequest(name) {
|
|
|
784
810
|
registerResourcesTombstoneCommand(program);
|
|
785
811
|
await reg(loadView);
|
|
786
812
|
return true;
|
|
813
|
+
case '_internal':
|
|
814
|
+
registerInternalCommand(program);
|
|
815
|
+
return true;
|
|
787
816
|
case 'upgrade':
|
|
788
817
|
registerUpgradeCommand(program);
|
|
789
818
|
return true;
|
|
@@ -878,6 +907,7 @@ async function registerAllEagerCommands() {
|
|
|
878
907
|
await reg(loadSsh);
|
|
879
908
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
880
909
|
registerJobsCronAliasCommand(program, 'cron');
|
|
910
|
+
registerInternalCommand(program);
|
|
881
911
|
registerUpgradeCommand(program);
|
|
882
912
|
await reg(loadPull);
|
|
883
913
|
await reg(loadPush);
|