@phnx-labs/agents-cli 1.14.1 → 1.14.3
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/README.md +31 -3
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +388 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/exec.js +17 -17
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +206 -12
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +162 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +46 -13
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +77 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +43 -26
- package/dist/lib/rotate.js +99 -44
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -76
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +8 -1
- package/dist/lib/teams/registry.js +8 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +13 -7
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +30 -2
- package/dist/lib/versions.js +127 -105
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
package/dist/commands/teams.js
CHANGED
|
@@ -5,7 +5,8 @@ import { AgentManager, checkAllClis, getAgentsDir, VALID_TASK_TYPES, } from '../
|
|
|
5
5
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
6
6
|
import { runSupervisor } from '../lib/teams/supervisor.js';
|
|
7
7
|
import { handleSpawn, handleStatus, handleStop, handleTasks, } from '../lib/teams/api.js';
|
|
8
|
-
import { createTeam, ensureTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
8
|
+
import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
9
|
+
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
9
10
|
import { isVersionInstalled, resolveVersionAlias, resolveVersionAliasLoose } from '../lib/versions.js';
|
|
10
11
|
import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/session/discover.js';
|
|
11
12
|
import { buildPreview as buildSessionPreview } from './sessions-picker.js';
|
|
@@ -542,7 +543,8 @@ Examples:
|
|
|
542
543
|
# Pull the live log of one teammate
|
|
543
544
|
agents teams logs frontend
|
|
544
545
|
|
|
545
|
-
# A teammate is stuck —
|
|
546
|
+
# A teammate is stuck — stop them, then remove if needed
|
|
547
|
+
agents teams stop pricing-page frontend
|
|
546
548
|
agents teams remove pricing-page frontend
|
|
547
549
|
|
|
548
550
|
# Ship done — wind everyone down
|
|
@@ -676,10 +678,14 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
676
678
|
.aliases(['c', 'new'])
|
|
677
679
|
.description('Start a new team. No teammates yet; add them with `teams add`.')
|
|
678
680
|
.option('-d, --description <text>', 'One-line summary of what this team is working on')
|
|
681
|
+
.option('--enable-worktrees', 'Each teammate works in its own git worktree (requires --worktree on add)')
|
|
679
682
|
.option('--json', 'Output machine-readable JSON')
|
|
680
683
|
.action(async (team, opts) => {
|
|
681
684
|
try {
|
|
682
|
-
const meta = await createTeam(team,
|
|
685
|
+
const meta = await createTeam(team, {
|
|
686
|
+
description: opts.description,
|
|
687
|
+
enableWorktrees: opts.enableWorktrees,
|
|
688
|
+
});
|
|
683
689
|
if (isJsonMode(opts)) {
|
|
684
690
|
console.log(JSON.stringify({ team, ...meta }, null, 2));
|
|
685
691
|
return;
|
|
@@ -687,9 +693,16 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
687
693
|
console.log(chalk.green(`New team: ${chalk.cyan(team)}`));
|
|
688
694
|
if (meta.description)
|
|
689
695
|
console.log(chalk.gray(` ${meta.description}`));
|
|
696
|
+
if (meta.enable_worktrees)
|
|
697
|
+
console.log(chalk.gray(` worktrees: enabled`));
|
|
690
698
|
console.log();
|
|
691
699
|
console.log(chalk.gray('Add your first teammate:'));
|
|
692
|
-
|
|
700
|
+
if (meta.enable_worktrees) {
|
|
701
|
+
console.log(chalk.gray(` agents teams add ${team} claude "your task here" --name alice --worktree feature-name`));
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
console.log(chalk.gray(` agents teams add ${team} claude "your task here"`));
|
|
705
|
+
}
|
|
693
706
|
}
|
|
694
707
|
catch (err) {
|
|
695
708
|
die(err.message);
|
|
@@ -706,6 +719,7 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
706
719
|
.option('--model <model>', 'Override the effort tier and use this specific model (e.g. claude-opus-4-6)')
|
|
707
720
|
.option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
|
|
708
721
|
.option('--cwd <dir>', 'Working directory for this teammate (default: current directory)')
|
|
722
|
+
.option('--worktree <name>', 'Run this teammate in a dedicated git worktree (required when team has --enable-worktrees)')
|
|
709
723
|
.option('--after <names>', "DAG dependencies: comma-separated teammate names to wait for. Stages as PENDING; run 'teams start' to launch when ready.")
|
|
710
724
|
.option('--task-type <type>', `Factory label: ${VALID_TASK_TYPES.join('|')}. Drives planner fan-out + test-oracle bugfix loop.`)
|
|
711
725
|
.option('--cloud <provider>', `Dispatch to cloud backend instead of local CLI: ${VALID_CLOUD_PROVIDERS.join('|')}`)
|
|
@@ -762,7 +776,34 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
762
776
|
}
|
|
763
777
|
// Auto-create the team if it doesn't exist yet (friendlier UX than erroring).
|
|
764
778
|
await ensureTeam(team);
|
|
765
|
-
|
|
779
|
+
// Check if team has worktrees enabled
|
|
780
|
+
const teamMeta = await getTeam(team);
|
|
781
|
+
const worktreesEnabled = teamMeta?.enable_worktrees ?? false;
|
|
782
|
+
let worktreeName = null;
|
|
783
|
+
let worktreePath = null;
|
|
784
|
+
if (worktreesEnabled) {
|
|
785
|
+
if (!opts.worktree) {
|
|
786
|
+
die(`Team '${team}' has worktrees enabled. Use --worktree <name> to specify a worktree name.`);
|
|
787
|
+
}
|
|
788
|
+
if (!opts.name) {
|
|
789
|
+
die(`Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
|
|
790
|
+
}
|
|
791
|
+
const baseCwd = opts.cwd ?? process.cwd();
|
|
792
|
+
if (!(await isGitRepo(baseCwd))) {
|
|
793
|
+
die(`Worktrees require a git repository. ${baseCwd} is not inside a git repo.`);
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
worktreeName = opts.worktree;
|
|
797
|
+
worktreePath = await createWorktree(baseCwd, worktreeName);
|
|
798
|
+
}
|
|
799
|
+
catch (err) {
|
|
800
|
+
die(`Failed to create worktree '${opts.worktree}': ${err.message}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
else if (opts.worktree) {
|
|
804
|
+
die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
805
|
+
}
|
|
806
|
+
const cwd = worktreePath ?? opts.cwd ?? process.cwd();
|
|
766
807
|
const mgr = mkManager();
|
|
767
808
|
// Factory teammates: prepend the worker-skill preamble to every task
|
|
768
809
|
// prompt so implementers/testers/reviewers know about the Ledger, the
|
|
@@ -811,7 +852,7 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
811
852
|
}
|
|
812
853
|
}
|
|
813
854
|
try {
|
|
814
|
-
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);
|
|
855
|
+
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);
|
|
815
856
|
if (isJsonMode(opts)) {
|
|
816
857
|
console.log(JSON.stringify(result, null, 2));
|
|
817
858
|
return;
|
|
@@ -830,6 +871,9 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
830
871
|
console.log(` ${chalk.gray('status ')} ${statusColor(result.status)(result.status)}`);
|
|
831
872
|
console.log(` ${chalk.gray('mode ')} ${opts.mode}`);
|
|
832
873
|
console.log(` ${chalk.gray('working ')} ${cwd}`);
|
|
874
|
+
if (worktreeName) {
|
|
875
|
+
console.log(` ${chalk.gray('worktree')} ${chalk.cyan(worktreeName)}`);
|
|
876
|
+
}
|
|
833
877
|
if (result.task_type) {
|
|
834
878
|
console.log(` ${chalk.gray('task ')} ${chalk.cyan(result.task_type)}`);
|
|
835
879
|
}
|
|
@@ -988,11 +1032,90 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
988
1032
|
console.error(chalk.yellow(`Stopped by signal after ${result.waves} waves.`));
|
|
989
1033
|
}
|
|
990
1034
|
});
|
|
1035
|
+
// stop
|
|
1036
|
+
teams
|
|
1037
|
+
.command('stop [team] [teammate]')
|
|
1038
|
+
.description('Stop a running teammate. Can be restarted later. Cleans up worktree if no uncommitted changes.')
|
|
1039
|
+
.option('--json', 'Output machine-readable JSON')
|
|
1040
|
+
.action(async (team, ref, opts) => {
|
|
1041
|
+
const mgr = mkManager();
|
|
1042
|
+
if (!team) {
|
|
1043
|
+
const { names } = await loadTeamRows(mgr);
|
|
1044
|
+
requireDestructiveArg({
|
|
1045
|
+
argName: 'team',
|
|
1046
|
+
command: 'agents teams stop',
|
|
1047
|
+
itemNoun: 'team',
|
|
1048
|
+
available: names,
|
|
1049
|
+
emptyHint: "You don't have any teams yet.",
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
if (!ref) {
|
|
1053
|
+
const roster = await mgr.listByTask(team);
|
|
1054
|
+
const running = roster.filter((a) => a.status === 'running');
|
|
1055
|
+
requireDestructiveArg({
|
|
1056
|
+
argName: 'teammate',
|
|
1057
|
+
command: `agents teams stop ${team}`,
|
|
1058
|
+
itemNoun: 'teammate',
|
|
1059
|
+
available: running.map((a) => a.name || shortId(a.agentId)),
|
|
1060
|
+
emptyHint: `Team ${team} has no running teammates.`,
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
const lookup = await mgr.resolveAgentIdInTask(team, ref);
|
|
1064
|
+
if (lookup.kind === 'none') {
|
|
1065
|
+
die(`No teammate matching '${ref}' in team ${team}`, 2);
|
|
1066
|
+
}
|
|
1067
|
+
if (lookup.kind === 'ambiguous') {
|
|
1068
|
+
const shorts = lookup.matches.map(shortId).join(', ');
|
|
1069
|
+
die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
|
|
1070
|
+
}
|
|
1071
|
+
const agentId = lookup.agentId;
|
|
1072
|
+
const agent = await mgr.get(agentId);
|
|
1073
|
+
const display = agent?.name || shortId(agentId);
|
|
1074
|
+
const stopRes = await handleStop(mgr, team, agentId);
|
|
1075
|
+
if ('error' in stopRes)
|
|
1076
|
+
die(stopRes.error);
|
|
1077
|
+
// Clean up worktree if this teammate had one
|
|
1078
|
+
let worktreeKept = false;
|
|
1079
|
+
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1080
|
+
try {
|
|
1081
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1082
|
+
if (dirty) {
|
|
1083
|
+
worktreeKept = true;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
const baseCwd = process.cwd();
|
|
1087
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
// best-effort cleanup
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (isJsonMode(opts)) {
|
|
1095
|
+
console.log(JSON.stringify({
|
|
1096
|
+
team,
|
|
1097
|
+
agent_id: agentId,
|
|
1098
|
+
name: agent?.name ?? null,
|
|
1099
|
+
stopped: stopRes.stopped.length > 0,
|
|
1100
|
+
worktree_kept: worktreeKept,
|
|
1101
|
+
}, null, 2));
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (stopRes.stopped.length) {
|
|
1105
|
+
console.log(chalk.green(`Stopped ${chalk.cyan(display)} in team ${chalk.cyan(team)}.`));
|
|
1106
|
+
}
|
|
1107
|
+
else if (stopRes.already_stopped.length) {
|
|
1108
|
+
console.log(chalk.gray(`${display} was already stopped.`));
|
|
1109
|
+
}
|
|
1110
|
+
if (worktreeKept && agent?.worktreeName) {
|
|
1111
|
+
console.log(chalk.yellow(`Worktree '${agent.worktreeName}' has uncommitted changes. Keeping it at: ${agent.worktreePath}`));
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
991
1114
|
// remove
|
|
992
1115
|
teams
|
|
993
1116
|
.command('remove [team] [teammate]')
|
|
994
1117
|
.alias('rm')
|
|
995
|
-
.description("Remove a teammate
|
|
1118
|
+
.description("Remove a stopped teammate's logs and metadata. Use 'stop' first to end a running teammate.")
|
|
996
1119
|
.option('--keep-logs', 'Keep their log files on disk (default: delete them)')
|
|
997
1120
|
.option('--json', 'Output machine-readable JSON')
|
|
998
1121
|
.action(async (team, ref, opts) => {
|
|
@@ -1009,12 +1132,13 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1009
1132
|
}
|
|
1010
1133
|
if (!ref) {
|
|
1011
1134
|
const roster = await mgr.listByTask(team);
|
|
1135
|
+
const stopped = roster.filter((a) => a.status !== 'running' && a.status !== 'pending');
|
|
1012
1136
|
requireDestructiveArg({
|
|
1013
1137
|
argName: 'teammate',
|
|
1014
1138
|
command: `agents teams remove ${team}`,
|
|
1015
|
-
itemNoun: 'teammate',
|
|
1016
|
-
available:
|
|
1017
|
-
emptyHint: `Team ${team} has no teammates.`,
|
|
1139
|
+
itemNoun: 'stopped teammate',
|
|
1140
|
+
available: stopped.map((a) => a.name || shortId(a.agentId)),
|
|
1141
|
+
emptyHint: `Team ${team} has no stopped teammates. Use 'agents teams stop' first.`,
|
|
1018
1142
|
});
|
|
1019
1143
|
}
|
|
1020
1144
|
const lookup = await mgr.resolveAgentIdInTask(team, ref);
|
|
@@ -1026,13 +1150,13 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1026
1150
|
die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
|
|
1027
1151
|
}
|
|
1028
1152
|
const agentId = lookup.agentId;
|
|
1029
|
-
// Look up the display handle (name if they had one) before we tear down.
|
|
1030
1153
|
const agent = await mgr.get(agentId);
|
|
1031
1154
|
const display = agent?.name || shortId(agentId);
|
|
1032
|
-
|
|
1033
|
-
if ('
|
|
1034
|
-
die(
|
|
1035
|
-
|
|
1155
|
+
// Require agent to be stopped first
|
|
1156
|
+
if (agent?.status === 'running' || agent?.status === 'pending') {
|
|
1157
|
+
die(`Teammate '${display}' is still ${agent.status}. Run 'agents teams stop ${team} ${display}' first.`);
|
|
1158
|
+
}
|
|
1159
|
+
if (!opts.keepLogs) {
|
|
1036
1160
|
try {
|
|
1037
1161
|
const dir = path.join(await getAgentsDir(), agentId);
|
|
1038
1162
|
await fs.rm(dir, { recursive: true, force: true });
|
|
@@ -1042,15 +1166,10 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1042
1166
|
}
|
|
1043
1167
|
}
|
|
1044
1168
|
if (isJsonMode(opts)) {
|
|
1045
|
-
console.log(JSON.stringify({ team, agent_id: agentId, name: agent?.name ?? null,
|
|
1169
|
+
console.log(JSON.stringify({ team, agent_id: agentId, name: agent?.name ?? null, removed: true }, null, 2));
|
|
1046
1170
|
return;
|
|
1047
1171
|
}
|
|
1048
|
-
|
|
1049
|
-
console.log(chalk.green(`${display} has left team ${chalk.cyan(team)} (was working, now stopped).`));
|
|
1050
|
-
}
|
|
1051
|
-
else {
|
|
1052
|
-
console.log(chalk.green(`${display} has left team ${chalk.cyan(team)}.`));
|
|
1053
|
-
}
|
|
1172
|
+
console.log(chalk.green(`Removed ${chalk.cyan(display)} from team ${chalk.cyan(team)}.`));
|
|
1054
1173
|
});
|
|
1055
1174
|
// disband
|
|
1056
1175
|
teams
|
|
@@ -1075,6 +1194,24 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1075
1194
|
if ('error' in stopRes)
|
|
1076
1195
|
die(stopRes.error);
|
|
1077
1196
|
const status = await handleStatus(mgr, team, 'all');
|
|
1197
|
+
// Clean up worktrees for all teammates
|
|
1198
|
+
const baseCwd = process.cwd();
|
|
1199
|
+
const keptWorktrees = [];
|
|
1200
|
+
for (const a of status.agents) {
|
|
1201
|
+
const agent = await mgr.get(a.agent_id);
|
|
1202
|
+
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1203
|
+
try {
|
|
1204
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1205
|
+
if (dirty) {
|
|
1206
|
+
keptWorktrees.push(agent.worktreeName);
|
|
1207
|
+
}
|
|
1208
|
+
else {
|
|
1209
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
catch { /* best-effort */ }
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1078
1215
|
const removedIds = [];
|
|
1079
1216
|
if (!opts.keepLogs) {
|
|
1080
1217
|
const base = await getAgentsDir();
|
|
@@ -1099,6 +1236,9 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1099
1236
|
console.log(chalk.gray(` Stopped ${stopRes.stopped.length} working teammate(s).`));
|
|
1100
1237
|
if (removedIds.length)
|
|
1101
1238
|
console.log(chalk.gray(` Cleared ${removedIds.length} teammate log(s).`));
|
|
1239
|
+
if (keptWorktrees.length) {
|
|
1240
|
+
console.log(chalk.yellow(` Kept ${keptWorktrees.length} worktree(s) with uncommitted changes: ${keptWorktrees.join(', ')}`));
|
|
1241
|
+
}
|
|
1102
1242
|
});
|
|
1103
1243
|
// logs
|
|
1104
1244
|
teams
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents trash` — list and restore soft-deleted version directories.
|
|
3
|
+
*
|
|
4
|
+
* `removeVersion` moves a version dir to ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/
|
|
5
|
+
* instead of hard-deleting. These commands let the user inspect what's there
|
|
6
|
+
* and put a soft-deleted version back. The trash never auto-expires; only
|
|
7
|
+
* `rm -rf ~/.agents-system/trash/` removes bytes from disk.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
export declare function registerTrashCommands(program: Command): void;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { resolveAgentName, agentLabel } from '../lib/agents.js';
|
|
5
|
+
import { getTrashVersionsDir } from '../lib/state.js';
|
|
6
|
+
import { getVersionDir } from '../lib/versions.js';
|
|
7
|
+
function listTrashEntries(filterAgent) {
|
|
8
|
+
const root = getTrashVersionsDir();
|
|
9
|
+
if (!fs.existsSync(root))
|
|
10
|
+
return [];
|
|
11
|
+
const out = [];
|
|
12
|
+
let agentDirs;
|
|
13
|
+
try {
|
|
14
|
+
agentDirs = fs.readdirSync(root, { withFileTypes: true });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
for (const agentEntry of agentDirs) {
|
|
20
|
+
if (!agentEntry.isDirectory())
|
|
21
|
+
continue;
|
|
22
|
+
const agent = resolveAgentName(agentEntry.name);
|
|
23
|
+
if (!agent)
|
|
24
|
+
continue;
|
|
25
|
+
if (filterAgent && agent !== filterAgent)
|
|
26
|
+
continue;
|
|
27
|
+
const agentDir = path.join(root, agentEntry.name);
|
|
28
|
+
let versionDirs;
|
|
29
|
+
try {
|
|
30
|
+
versionDirs = fs.readdirSync(agentDir, { withFileTypes: true });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
for (const ver of versionDirs) {
|
|
36
|
+
if (!ver.isDirectory())
|
|
37
|
+
continue;
|
|
38
|
+
const versionDir = path.join(agentDir, ver.name);
|
|
39
|
+
let stampDirs;
|
|
40
|
+
try {
|
|
41
|
+
stampDirs = fs.readdirSync(versionDir, { withFileTypes: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const stamp of stampDirs) {
|
|
47
|
+
if (!stamp.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
out.push({
|
|
50
|
+
agent,
|
|
51
|
+
version: ver.name,
|
|
52
|
+
stamp: stamp.name,
|
|
53
|
+
trashPath: path.join(versionDir, stamp.name),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out.sort((a, b) => a.stamp.localeCompare(b.stamp)).reverse();
|
|
59
|
+
}
|
|
60
|
+
function pickLatest(entries, agent, version) {
|
|
61
|
+
const matches = entries.filter((e) => e.agent === agent && e.version === version);
|
|
62
|
+
if (matches.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
return matches[0];
|
|
65
|
+
}
|
|
66
|
+
function parseAgentVersion(target) {
|
|
67
|
+
const at = target.indexOf('@');
|
|
68
|
+
if (at < 1 || at === target.length - 1)
|
|
69
|
+
return null;
|
|
70
|
+
const agent = resolveAgentName(target.slice(0, at));
|
|
71
|
+
if (!agent)
|
|
72
|
+
return null;
|
|
73
|
+
return { agent, version: target.slice(at + 1) };
|
|
74
|
+
}
|
|
75
|
+
function dirSizeBytes(dir) {
|
|
76
|
+
let total = 0;
|
|
77
|
+
const stack = [dir];
|
|
78
|
+
while (stack.length > 0) {
|
|
79
|
+
const cur = stack.pop();
|
|
80
|
+
let stat;
|
|
81
|
+
try {
|
|
82
|
+
stat = fs.lstatSync(cur);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (stat.isDirectory()) {
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = fs.readdirSync(cur);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const e of entries)
|
|
96
|
+
stack.push(path.join(cur, e));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
total += stat.size;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return total;
|
|
103
|
+
}
|
|
104
|
+
function humanSize(bytes) {
|
|
105
|
+
if (bytes < 1024)
|
|
106
|
+
return `${bytes} B`;
|
|
107
|
+
if (bytes < 1024 * 1024)
|
|
108
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
109
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
110
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
111
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
112
|
+
}
|
|
113
|
+
export function registerTrashCommands(program) {
|
|
114
|
+
const trash = program
|
|
115
|
+
.command('trash')
|
|
116
|
+
.description('Inspect and restore soft-deleted agent version directories');
|
|
117
|
+
trash
|
|
118
|
+
.command('list [agent]')
|
|
119
|
+
.description('List soft-deleted version directories (optionally filtered to one agent)')
|
|
120
|
+
.action((agentArg) => {
|
|
121
|
+
let filter;
|
|
122
|
+
if (agentArg) {
|
|
123
|
+
const a = resolveAgentName(agentArg);
|
|
124
|
+
if (!a) {
|
|
125
|
+
console.error(chalk.red(`Unknown agent: ${agentArg}`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
filter = a;
|
|
129
|
+
}
|
|
130
|
+
const entries = listTrashEntries(filter);
|
|
131
|
+
if (entries.length === 0) {
|
|
132
|
+
console.log(chalk.gray('Trash is empty.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(chalk.bold(`Soft-deleted versions (${entries.length})`));
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
const size = humanSize(dirSizeBytes(e.trashPath));
|
|
138
|
+
console.log(` ${agentLabel(e.agent)}@${e.version} ` +
|
|
139
|
+
chalk.gray(`${e.stamp} ${size} ${e.trashPath}`));
|
|
140
|
+
}
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.gray('Restore with: agents trash restore <agent>@<version>'));
|
|
143
|
+
});
|
|
144
|
+
trash
|
|
145
|
+
.command('restore <target>')
|
|
146
|
+
.description('Restore a soft-deleted version (e.g. "claude@2.1.110") back to ~/.agents-system/versions/')
|
|
147
|
+
.action((target) => {
|
|
148
|
+
const parsed = parseAgentVersion(target);
|
|
149
|
+
if (!parsed) {
|
|
150
|
+
console.error(chalk.red(`Expected <agent>@<version>, got: ${target}`));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const { agent, version } = parsed;
|
|
154
|
+
const entries = listTrashEntries(agent);
|
|
155
|
+
const entry = pickLatest(entries, agent, version);
|
|
156
|
+
if (!entry) {
|
|
157
|
+
console.error(chalk.red(`No trashed copy found for ${agent}@${version}`));
|
|
158
|
+
console.error(chalk.gray('Run `agents trash list` to see what exists.'));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
const dest = getVersionDir(agent, version);
|
|
162
|
+
if (fs.existsSync(dest)) {
|
|
163
|
+
console.error(chalk.red(`Cannot restore: ${dest} already exists.`));
|
|
164
|
+
console.error(chalk.gray('Move or remove the existing dir first, then re-run restore.'));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
169
|
+
fs.renameSync(entry.trashPath, dest);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.error(chalk.red(`Restore failed: ${err.message}`));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
// Best-effort cleanup of empty stamp/version parents in trash.
|
|
176
|
+
try {
|
|
177
|
+
const verDir = path.dirname(entry.trashPath);
|
|
178
|
+
if (fs.readdirSync(verDir).length === 0)
|
|
179
|
+
fs.rmdirSync(verDir);
|
|
180
|
+
const agentDir = path.dirname(verDir);
|
|
181
|
+
if (fs.readdirSync(agentDir).length === 0)
|
|
182
|
+
fs.rmdirSync(agentDir);
|
|
183
|
+
}
|
|
184
|
+
catch { /* best-effort */ }
|
|
185
|
+
console.log(chalk.green(`Restored ${agentLabel(agent)}@${version} to ${dest}`));
|
|
186
|
+
});
|
|
187
|
+
}
|
package/dist/commands/view.js
CHANGED
|
@@ -6,12 +6,12 @@ import * as yaml from 'yaml';
|
|
|
6
6
|
import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
7
7
|
import { formatUsageSection, formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
8
8
|
import { readManifest } from '../lib/manifest.js';
|
|
9
|
-
import { listInstalledVersions, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, } from '../lib/versions.js';
|
|
9
|
+
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, } from '../lib/versions.js';
|
|
10
10
|
import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
11
11
|
import { getAgentResources } from '../lib/resources.js';
|
|
12
12
|
import { getAgentsDir, getUserAgentsDir, getPromptcutsPath } from '../lib/state.js';
|
|
13
13
|
import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
|
|
14
|
-
import {
|
|
14
|
+
import { getCentralRulesFileName } from '../lib/rules/rules.js';
|
|
15
15
|
import { getConfiguredRunStrategy } from '../lib/rotate.js';
|
|
16
16
|
import { confirm } from '@inquirer/prompts';
|
|
17
17
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -426,7 +426,7 @@ async function showAgentResources(agentId, requestedVersion) {
|
|
|
426
426
|
}
|
|
427
427
|
else {
|
|
428
428
|
// Rules files: map agent-specific name (CLAUDE.md) back to canonical (AGENTS.md)
|
|
429
|
-
const centralName =
|
|
429
|
+
const centralName = getCentralRulesFileName(agentId);
|
|
430
430
|
relativePath = `rules/${centralName}`;
|
|
431
431
|
}
|
|
432
432
|
const matchesPath = (f) => f === relativePath || f.startsWith(relativePath + '/');
|
|
@@ -653,14 +653,21 @@ async function collectAgentsJson(filterAgentId) {
|
|
|
653
653
|
return out;
|
|
654
654
|
}
|
|
655
655
|
async function buildAgentPrunePlan(agentId) {
|
|
656
|
-
const
|
|
656
|
+
const dirInfos = listInstalledVersionDirs(agentId);
|
|
657
|
+
const entries = await Promise.all(dirInfos.map(async ({ version, hasBinary }) => {
|
|
657
658
|
const home = getVersionHomePath(agentId, version);
|
|
658
659
|
const info = await getAccountInfo(agentId, home);
|
|
659
|
-
return { version, info };
|
|
660
|
+
return { version, info, hasBinary };
|
|
660
661
|
}));
|
|
661
662
|
const globalDefault = getGlobalDefault(agentId);
|
|
663
|
+
const toPrune = [];
|
|
664
|
+
const skippedDefaults = [];
|
|
665
|
+
// Duplicate-account detection runs only over installs that actually have a
|
|
666
|
+
// working binary — those are the things that compete for "the live install
|
|
667
|
+
// for this account."
|
|
668
|
+
const installed = entries.filter((e) => e.hasBinary);
|
|
662
669
|
const byEmail = new Map();
|
|
663
|
-
for (const e of
|
|
670
|
+
for (const e of installed) {
|
|
664
671
|
if (!e.info.email)
|
|
665
672
|
continue;
|
|
666
673
|
const key = e.info.email.toLowerCase();
|
|
@@ -668,8 +675,6 @@ async function buildAgentPrunePlan(agentId) {
|
|
|
668
675
|
list.push(e);
|
|
669
676
|
byEmail.set(key, list);
|
|
670
677
|
}
|
|
671
|
-
const toPrune = [];
|
|
672
|
-
const skippedDefaults = [];
|
|
673
678
|
for (const [, group] of byEmail) {
|
|
674
679
|
if (group.length < 2)
|
|
675
680
|
continue;
|
|
@@ -682,6 +687,7 @@ async function buildAgentPrunePlan(agentId) {
|
|
|
682
687
|
email: older.info.email,
|
|
683
688
|
keeper,
|
|
684
689
|
isDefault: older.version === globalDefault,
|
|
690
|
+
reason: 'duplicate',
|
|
685
691
|
};
|
|
686
692
|
if (plan.isDefault)
|
|
687
693
|
skippedDefaults.push(plan);
|
|
@@ -689,6 +695,23 @@ async function buildAgentPrunePlan(agentId) {
|
|
|
689
695
|
toPrune.push(plan);
|
|
690
696
|
}
|
|
691
697
|
}
|
|
698
|
+
// Home-only leftovers: dirs without a binary. These are residue from a
|
|
699
|
+
// prior removeVersion before the soft-delete migration, plus any hand-edited
|
|
700
|
+
// installs. Surface them so the user can move them to trash.
|
|
701
|
+
for (const e of entries) {
|
|
702
|
+
if (e.hasBinary)
|
|
703
|
+
continue;
|
|
704
|
+
if (e.version === globalDefault)
|
|
705
|
+
continue; // never auto-suggest the default
|
|
706
|
+
toPrune.push({
|
|
707
|
+
agentId,
|
|
708
|
+
version: e.version,
|
|
709
|
+
email: e.info.email || '',
|
|
710
|
+
keeper: '',
|
|
711
|
+
isDefault: false,
|
|
712
|
+
reason: 'home-leftover',
|
|
713
|
+
});
|
|
714
|
+
}
|
|
692
715
|
return { agentId, toPrune, skippedDefaults };
|
|
693
716
|
}
|
|
694
717
|
async function executePrunePlan(plan) {
|
|
@@ -720,11 +743,17 @@ function printPrunePlan(plan, isFirst) {
|
|
|
720
743
|
}
|
|
721
744
|
if (plan.toPrune.length === 0)
|
|
722
745
|
return;
|
|
723
|
-
const heading = isFirst ? `Will
|
|
746
|
+
const heading = isFirst ? `Will move to trash for ${agentLabel(plan.agentId)}:` : `Also found candidates for ${agentLabel(plan.agentId)}:`;
|
|
724
747
|
console.log(chalk.bold(heading));
|
|
725
748
|
for (const p of plan.toPrune) {
|
|
726
|
-
|
|
727
|
-
|
|
749
|
+
if (p.reason === 'duplicate') {
|
|
750
|
+
console.log(` ${agentLabel(p.agentId)}@${p.version} ${chalk.cyan(p.email)} ` +
|
|
751
|
+
chalk.gray(`— duplicate, keeping ${p.agentId}@${p.keeper}`));
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
console.log(` ${agentLabel(p.agentId)}@${p.version} ` +
|
|
755
|
+
chalk.gray(`— home-only leftover (no binary; transcripts preserved in trash)`));
|
|
756
|
+
}
|
|
728
757
|
}
|
|
729
758
|
console.log();
|
|
730
759
|
}
|
|
@@ -746,7 +775,7 @@ export async function pruneDuplicates(filterAgentId, yes, dryRun) {
|
|
|
746
775
|
spinner.stop();
|
|
747
776
|
const actionable = plans.filter((p) => p.toPrune.length > 0 || p.skippedDefaults.length > 0);
|
|
748
777
|
if (actionable.length === 0) {
|
|
749
|
-
console.log(chalk.gray('Nothing to prune — no
|
|
778
|
+
console.log(chalk.gray('Nothing to prune — no duplicate-account installs and no home-only leftovers.'));
|
|
750
779
|
return;
|
|
751
780
|
}
|
|
752
781
|
const totalCandidates = actionable.reduce((n, plan) => n + plan.toPrune.length, 0);
|
|
@@ -844,7 +873,11 @@ export async function viewAction(agentArg, options) {
|
|
|
844
873
|
console.log(chalk.red(formatAgentError(agentName)));
|
|
845
874
|
process.exit(1);
|
|
846
875
|
}
|
|
847
|
-
|
|
876
|
+
// Keep 'default' as-is since showAgentResources handles it; resolveVersionAlias
|
|
877
|
+
// returns undefined for 'default' which would skip the detailed view.
|
|
878
|
+
const requestedVersion = parts[1] === 'default'
|
|
879
|
+
? 'default'
|
|
880
|
+
: (resolveVersionAlias(agentId, parts[1]) ?? null);
|
|
848
881
|
if (prune) {
|
|
849
882
|
if (requestedVersion) {
|
|
850
883
|
console.log(chalk.red('--prune does not take a @version suffix.'));
|