@phnx-labs/agents-cli 1.20.11 → 1.20.13
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 +17 -0
- package/README.md +3 -0
- package/dist/commands/computer-actions.d.ts +3 -0
- package/dist/commands/computer-actions.js +16 -0
- package/dist/commands/exec.js +25 -4
- package/dist/commands/import.js +17 -6
- package/dist/commands/inspect.d.ts +11 -1
- package/dist/commands/inspect.js +53 -19
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +69 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +1 -0
- package/dist/commands/trash.d.ts +11 -0
- package/dist/commands/trash.js +57 -41
- package/dist/commands/versions.js +68 -20
- package/dist/commands/view.js +1 -12
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +199 -0
- package/dist/index.js +22 -1
- package/dist/lib/agents.js +70 -22
- package/dist/lib/browser/ipc.d.ts +7 -0
- package/dist/lib/browser/ipc.js +43 -27
- package/dist/lib/capabilities.js +7 -1
- package/dist/lib/command-skills.d.ts +1 -0
- package/dist/lib/command-skills.js +23 -7
- package/dist/lib/exec.d.ts +32 -1
- package/dist/lib/exec.js +79 -7
- package/dist/lib/hooks.js +37 -5
- package/dist/lib/mcp.js +33 -0
- package/dist/lib/models.js +5 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +96 -6
- package/dist/lib/platform/index.d.ts +1 -0
- package/dist/lib/platform/index.js +1 -0
- package/dist/lib/platform/winpath.d.ts +35 -0
- package/dist/lib/platform/winpath.js +86 -0
- package/dist/lib/plugins.d.ts +14 -0
- package/dist/lib/plugins.js +23 -0
- package/dist/lib/project-launch.js +110 -5
- package/dist/lib/registry.js +15 -2
- package/dist/lib/runner.js +14 -0
- package/dist/lib/sandbox.js +5 -2
- package/dist/lib/settings-manifest.d.ts +39 -0
- package/dist/lib/settings-manifest.js +163 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +16 -31
- package/dist/lib/staleness/detectors/subagents.js +16 -0
- package/dist/lib/staleness/writers/subagents.js +11 -3
- package/dist/lib/subagents.d.ts +9 -0
- package/dist/lib/subagents.js +33 -0
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +6 -0
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/versions.d.ts +15 -3
- package/dist/lib/versions.js +88 -19
- package/dist/lib/wallet/index.d.ts +78 -0
- package/dist/lib/wallet/index.js +253 -0
- package/package.json +3 -3
- package/scripts/postinstall.js +35 -7
package/dist/commands/trash.js
CHANGED
|
@@ -110,6 +110,61 @@ function humanSize(bytes) {
|
|
|
110
110
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
111
111
|
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Restore a soft-deleted version back into ~/.agents/.history/versions/.
|
|
115
|
+
* Shared by `agents trash restore` and the top-level `agents restore` alias.
|
|
116
|
+
* Exits the process with a non-zero code on any failure.
|
|
117
|
+
*/
|
|
118
|
+
export function restoreVersion(target) {
|
|
119
|
+
const parsed = parseAgentVersion(target);
|
|
120
|
+
if (!parsed) {
|
|
121
|
+
console.error(chalk.red(`Expected <agent>@<version>, got: ${target}`));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const { agent, version } = parsed;
|
|
125
|
+
const entries = listTrashEntries(agent);
|
|
126
|
+
const entry = pickLatest(entries, agent, version);
|
|
127
|
+
if (!entry) {
|
|
128
|
+
console.error(chalk.red(`No trashed copy found for ${agent}@${version}`));
|
|
129
|
+
console.error(chalk.gray('Run `agents trash list` to see what exists.'));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const dest = getVersionDir(agent, version);
|
|
133
|
+
if (fs.existsSync(dest)) {
|
|
134
|
+
console.error(chalk.red(`Cannot restore: ${dest} already exists.`));
|
|
135
|
+
console.error(chalk.gray('Move or remove the existing dir first, then re-run restore.'));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
140
|
+
fs.renameSync(entry.trashPath, dest);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(chalk.red(`Restore failed: ${err.message}`));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
// Best-effort cleanup of empty stamp/version parents in trash.
|
|
147
|
+
try {
|
|
148
|
+
const verDir = path.dirname(entry.trashPath);
|
|
149
|
+
if (fs.readdirSync(verDir).length === 0)
|
|
150
|
+
fs.rmdirSync(verDir);
|
|
151
|
+
const agentDir = path.dirname(verDir);
|
|
152
|
+
if (fs.readdirSync(agentDir).length === 0)
|
|
153
|
+
fs.rmdirSync(agentDir);
|
|
154
|
+
}
|
|
155
|
+
catch { /* best-effort */ }
|
|
156
|
+
console.log(chalk.green(`Restored ${agentLabel(agent)}@${version} to ${dest}`));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Register the top-level `agents restore` command — a shorthand for
|
|
160
|
+
* `agents trash restore` so users can undo a `remove`/`prune` directly.
|
|
161
|
+
*/
|
|
162
|
+
export function registerRestoreCommand(program) {
|
|
163
|
+
program
|
|
164
|
+
.command('restore <target>')
|
|
165
|
+
.description('Restore a soft-deleted agent version (e.g. "codex@0.141.0") removed via prune/remove')
|
|
166
|
+
.action((target) => restoreVersion(target));
|
|
167
|
+
}
|
|
113
168
|
export function registerTrashCommands(program) {
|
|
114
169
|
const trash = program
|
|
115
170
|
.command('trash')
|
|
@@ -139,49 +194,10 @@ export function registerTrashCommands(program) {
|
|
|
139
194
|
chalk.gray(`${e.stamp} ${size} ${e.trashPath}`));
|
|
140
195
|
}
|
|
141
196
|
console.log();
|
|
142
|
-
console.log(chalk.gray('Restore with: agents
|
|
197
|
+
console.log(chalk.gray('Restore with: agents restore <agent>@<version>'));
|
|
143
198
|
});
|
|
144
199
|
trash
|
|
145
200
|
.command('restore <target>')
|
|
146
201
|
.description('Restore a soft-deleted version (e.g. "claude@2.1.110") back to ~/.agents/.history/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
|
-
});
|
|
202
|
+
.action((target) => restoreVersion(target));
|
|
187
203
|
}
|
|
@@ -7,7 +7,8 @@ import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, } f
|
|
|
7
7
|
import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
8
8
|
import { viewAction } from './view.js';
|
|
9
9
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
10
|
-
import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
|
|
10
|
+
import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/versions.js';
|
|
11
|
+
import { carryForwardSettings } from '../lib/settings-manifest.js';
|
|
11
12
|
import { createShim, createVersionedAlias, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
|
|
12
13
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
13
14
|
import { tryAutoPull } from '../lib/git.js';
|
|
@@ -28,17 +29,6 @@ function fixSessionFilePaths(agent, version, oldVersionDir) {
|
|
|
28
29
|
const trashPath = path.join(trashAgentDir, stamps[0]);
|
|
29
30
|
updateSessionFilePaths(oldVersionDir, trashPath);
|
|
30
31
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Helper to get actual installed version for an agent.
|
|
33
|
-
* Returns the latest installed version, or throws if none installed.
|
|
34
|
-
*/
|
|
35
|
-
async function getInstalledVersionForAgent(agent) {
|
|
36
|
-
const versions = listInstalledVersions(agent);
|
|
37
|
-
if (versions.length > 0) {
|
|
38
|
-
return versions[versions.length - 1];
|
|
39
|
-
}
|
|
40
|
-
throw new Error(`No versions of ${agent} installed`);
|
|
41
|
-
}
|
|
42
32
|
function formatAccountHint(info, usage) {
|
|
43
33
|
const parts = [];
|
|
44
34
|
if (info.email)
|
|
@@ -91,7 +81,17 @@ function warnIfShimShadowed(agent) {
|
|
|
91
81
|
}
|
|
92
82
|
console.log(chalk.yellow(` Warning: ${AGENTS[agent].cliCommand} currently resolves to ${shadowedBy}`));
|
|
93
83
|
console.log(chalk.gray(` Managed shim: ${getShimPath(agent)}`));
|
|
94
|
-
|
|
84
|
+
const result = addShimsToPath();
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
console.log(chalk.gray(` ${getPathSetupInstructions().split('\n').join('\n ')}`));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (result.alreadyPresent) {
|
|
90
|
+
console.log(chalk.gray(` Shim PATH entry already set — ${AGENTS[agent].cliCommand} is shadowed by another binary. Remove or reorder it so ${getShimPath(agent)} takes priority.`));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
console.log(chalk.green(` Added shim directory to ${result.location}.`));
|
|
94
|
+
console.log(chalk.gray(` ${result.reloadHint}`));
|
|
95
95
|
}
|
|
96
96
|
async function versionPruneAction(specs, options, commandName) {
|
|
97
97
|
const isProject = options.project;
|
|
@@ -105,7 +105,7 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
105
105
|
}
|
|
106
106
|
const { agent, version } = parsed;
|
|
107
107
|
const agentConfig = AGENTS[agent];
|
|
108
|
-
if (version === 'latest' || !spec.includes('@')) {
|
|
108
|
+
if (version === 'latest' || version === 'oldest' || !spec.includes('@')) {
|
|
109
109
|
const versions = listInstalledVersions(agent);
|
|
110
110
|
if (versions.length === 0) {
|
|
111
111
|
console.log(chalk.gray(`No versions of ${agentLabel(agentConfig.id)} installed`));
|
|
@@ -139,7 +139,11 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
139
139
|
}
|
|
140
140
|
for (const v of toRemove) {
|
|
141
141
|
const versionDir = getVersionDir(agent, v);
|
|
142
|
-
removeVersion(agent, v);
|
|
142
|
+
const removed = removeVersion(agent, v);
|
|
143
|
+
if (!removed) {
|
|
144
|
+
console.log(chalk.red(`Failed to move ${agentLabel(agentConfig.id)}@${v} to trash — a file may be locked by a running process. Close any active sessions and try again.`));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
143
147
|
fixSessionFilePaths(agent, v, versionDir);
|
|
144
148
|
console.log(chalk.green(`Moved ${agentLabel(agentConfig.id)}@${v} to trash`));
|
|
145
149
|
moved.push({ agent, version: v });
|
|
@@ -166,7 +170,11 @@ async function versionPruneAction(specs, options, commandName) {
|
|
|
166
170
|
}
|
|
167
171
|
else {
|
|
168
172
|
const versionDir = getVersionDir(agent, version);
|
|
169
|
-
removeVersion(agent, version);
|
|
173
|
+
const removed = removeVersion(agent, version);
|
|
174
|
+
if (!removed) {
|
|
175
|
+
console.log(chalk.red(`Failed to move ${agentLabel(agentConfig.id)}@${version} to trash — a file may be locked by a running process. Close any active sessions and try again.`));
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
170
178
|
fixSessionFilePaths(agent, version, versionDir);
|
|
171
179
|
console.log(chalk.green(`Moved ${agentLabel(agentConfig.id)}@${version} to trash`));
|
|
172
180
|
moved.push({ agent, version });
|
|
@@ -228,6 +236,9 @@ export function registerVersionsCommands(program) {
|
|
|
228
236
|
# Install the latest version of an agent
|
|
229
237
|
agents add claude@latest
|
|
230
238
|
|
|
239
|
+
# Install the oldest published version of an agent
|
|
240
|
+
agents add claude@oldest
|
|
241
|
+
|
|
231
242
|
# Install a specific version (reproducibility)
|
|
232
243
|
agents add claude@2.1.112
|
|
233
244
|
|
|
@@ -259,7 +270,7 @@ export function registerVersionsCommands(program) {
|
|
|
259
270
|
console.log(chalk.yellow(`${agentLabel(agentConfig.id)} has no npm package. Install manually.`));
|
|
260
271
|
continue;
|
|
261
272
|
}
|
|
262
|
-
// Check if already installed (
|
|
273
|
+
// Check if already installed (resolve 'latest'/'oldest' against npm first)
|
|
263
274
|
let alreadyInstalled = false;
|
|
264
275
|
let installedAsVersion = version;
|
|
265
276
|
if (version === 'latest') {
|
|
@@ -269,6 +280,13 @@ export function registerVersionsCommands(program) {
|
|
|
269
280
|
installedAsVersion = latestCheck.version;
|
|
270
281
|
}
|
|
271
282
|
}
|
|
283
|
+
else if (version === 'oldest') {
|
|
284
|
+
const oldestCheck = await isOldestInstalled(agent);
|
|
285
|
+
if (oldestCheck.installed && oldestCheck.version) {
|
|
286
|
+
alreadyInstalled = true;
|
|
287
|
+
installedAsVersion = oldestCheck.version;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
272
290
|
else {
|
|
273
291
|
alreadyInstalled = isVersionInstalled(agent, version);
|
|
274
292
|
}
|
|
@@ -290,6 +308,19 @@ export function registerVersionsCommands(program) {
|
|
|
290
308
|
console.log(chalk.gray(` Created shim: ${getShimsDir()}/${agentConfig.cliCommand}`));
|
|
291
309
|
}
|
|
292
310
|
const installedVersion = result.installedVersion || version;
|
|
311
|
+
// Track the concrete version so a `--project` pin records it instead
|
|
312
|
+
// of the `latest`/`oldest` alias.
|
|
313
|
+
installedAsVersion = installedVersion;
|
|
314
|
+
// Seed the fresh version home with user settings from the current
|
|
315
|
+
// default version (settings.json, keybindings, codex config/auth).
|
|
316
|
+
// Gap-filling only — never overwrites what the new home has.
|
|
317
|
+
const carrySource = getGlobalDefault(agent);
|
|
318
|
+
if (carrySource && carrySource !== installedVersion) {
|
|
319
|
+
const carried = carryForwardSettings(agent, getVersionHomePath(agent, carrySource), getVersionHomePath(agent, installedVersion));
|
|
320
|
+
if (carried.applied.length > 0) {
|
|
321
|
+
console.log(chalk.gray(` Carried settings from ${agent}@${carrySource}: ${carried.applied.map(r => path.basename(r)).join(', ')}`));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
293
324
|
// Smart resource detection: compare available vs ACTUALLY synced (source of truth: files)
|
|
294
325
|
const available = getAvailableResources();
|
|
295
326
|
const actuallySynced = getActuallySyncedResources(agent, installedVersion);
|
|
@@ -428,14 +459,19 @@ export function registerVersionsCommands(program) {
|
|
|
428
459
|
? readManifest(process.cwd()) || createDefaultManifest()
|
|
429
460
|
: createDefaultManifest();
|
|
430
461
|
manifest.agents = manifest.agents || {};
|
|
431
|
-
manifest.agents[agent] = version === 'latest'
|
|
462
|
+
manifest.agents[agent] = (version === 'latest' || version === 'oldest')
|
|
463
|
+
? installedAsVersion
|
|
464
|
+
: version;
|
|
432
465
|
writeManifest(process.cwd(), manifest);
|
|
433
466
|
console.log(chalk.green(` Pinned ${agentLabel(agentConfig.id)}@${version} in .agents/agents.yaml`));
|
|
434
467
|
}
|
|
435
468
|
}
|
|
436
469
|
});
|
|
437
470
|
configureVersionPruneCommand(program.command('prune <specs...>'), 'prune');
|
|
438
|
-
|
|
471
|
+
// `rm` and `purge` are commander aliases for `remove` (which is itself an
|
|
472
|
+
// alias for `prune`). Native `.aliases()` keeps them in lockstep — same
|
|
473
|
+
// action, same options, no duplicate registration.
|
|
474
|
+
configureVersionPruneCommand(program.command('remove <specs...>', { hidden: true }).aliases(['rm', 'purge']), 'remove');
|
|
439
475
|
const useCmd = program
|
|
440
476
|
.command('use <agent> [version]')
|
|
441
477
|
.description('Switch the active version for an agent. This is the only command that sets the default.')
|
|
@@ -478,7 +514,7 @@ export function registerVersionsCommands(program) {
|
|
|
478
514
|
return;
|
|
479
515
|
}
|
|
480
516
|
agent = parsed.agent;
|
|
481
|
-
version = parsed.version === 'latest' ? undefined : parsed.version;
|
|
517
|
+
version = (parsed.version === 'latest' || parsed.version === 'oldest') ? undefined : parsed.version;
|
|
482
518
|
}
|
|
483
519
|
else {
|
|
484
520
|
const agentLower = agentArg.toLowerCase();
|
|
@@ -679,6 +715,18 @@ export function registerVersionsCommands(program) {
|
|
|
679
715
|
}
|
|
680
716
|
}
|
|
681
717
|
const previousDefault = getGlobalDefault(agentId);
|
|
718
|
+
// Carry user settings from the outgoing default into the target
|
|
719
|
+
// version home before switching. Gap-filling only, so versions that
|
|
720
|
+
// already have their own settings are left untouched.
|
|
721
|
+
if (previousDefault && previousDefault !== finalVersion) {
|
|
722
|
+
const carried = carryForwardSettings(agentId, getVersionHomePath(agentId, previousDefault), getVersionHomePath(agentId, finalVersion));
|
|
723
|
+
if (carried.applied.length > 0) {
|
|
724
|
+
console.log(chalk.gray(`Carried settings from ${agentId}@${previousDefault}: ${carried.applied.map(r => path.basename(r)).join(', ')}`));
|
|
725
|
+
if (carried.backupDir) {
|
|
726
|
+
console.log(chalk.gray(` Pre-merge backup: ${carried.backupDir}`));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
682
730
|
// Set global default
|
|
683
731
|
setGlobalDefault(agentId, finalVersion);
|
|
684
732
|
// Regenerate shim so it uses the latest script format
|
package/dist/commands/view.js
CHANGED
|
@@ -6,7 +6,7 @@ import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentNam
|
|
|
6
6
|
import { formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
7
7
|
import { readManifest } from '../lib/manifest.js';
|
|
8
8
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, } from '../lib/versions.js';
|
|
9
|
-
import {
|
|
9
|
+
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
10
10
|
import { getAgentResources } from '../lib/resources.js';
|
|
11
11
|
import { isCapable } from '../lib/capabilities.js';
|
|
12
12
|
import { discoverPlugins, pluginSupportsAgent } from '../lib/plugins.js';
|
|
@@ -537,17 +537,6 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
537
537
|
console.log(chalk.gray(' Run: agents add claude@latest'));
|
|
538
538
|
console.log();
|
|
539
539
|
}
|
|
540
|
-
// Show shims path status at the end (only for full list with managed versions)
|
|
541
|
-
if (versionManaged.length > 0 && !filterAgentId) {
|
|
542
|
-
const shimsDir = getShimsDir();
|
|
543
|
-
if (isShimsInPath()) {
|
|
544
|
-
console.log(chalk.gray(`Shims: ${shimsDir} (in PATH)`));
|
|
545
|
-
}
|
|
546
|
-
else {
|
|
547
|
-
console.log(chalk.yellow(`Shims: ${shimsDir} (not in PATH)`));
|
|
548
|
-
console.log(chalk.gray('Add to PATH for automatic version switching'));
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
540
|
// Check for new resources when viewing a specific agent
|
|
552
541
|
if (filterAgentId && versionManaged.length > 0) {
|
|
553
542
|
const defaultVersion = getGlobalDefault(filterAgentId);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents wallet` — a device-local credit-card vault backed by macOS Keychain.
|
|
3
|
+
*
|
|
4
|
+
* UX intent matches Apple Wallet: list cards freely (no biometric), reveal
|
|
5
|
+
* a card with Touch ID. Card numbers never leave the device. This is NOT
|
|
6
|
+
* Apple Pay — we store the real PAN, not a network DPAN, and do not generate
|
|
7
|
+
* per-transaction cryptograms. The help text reflects this.
|
|
8
|
+
*
|
|
9
|
+
* Bundle layout:
|
|
10
|
+
* ~/.agents/wallet/cards.json metadata (id, last4, brand, exp)
|
|
11
|
+
* agents-cli.secrets.wallet.<id> JSON {pan, cvc, cardholder}
|
|
12
|
+
*/
|
|
13
|
+
import type { Command } from 'commander';
|
|
14
|
+
export declare function registerWalletCommands(program: Command): void;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents wallet` — a device-local credit-card vault backed by macOS Keychain.
|
|
3
|
+
*
|
|
4
|
+
* UX intent matches Apple Wallet: list cards freely (no biometric), reveal
|
|
5
|
+
* a card with Touch ID. Card numbers never leave the device. This is NOT
|
|
6
|
+
* Apple Pay — we store the real PAN, not a network DPAN, and do not generate
|
|
7
|
+
* per-transaction cryptograms. The help text reflects this.
|
|
8
|
+
*
|
|
9
|
+
* Bundle layout:
|
|
10
|
+
* ~/.agents/wallet/cards.json metadata (id, last4, brand, exp)
|
|
11
|
+
* agents-cli.secrets.wallet.<id> JSON {pan, cvc, cardholder}
|
|
12
|
+
*/
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { addCard, detectBrand, isValidLuhn, listCards, removeCard, renameCard, showCard, } from '../lib/wallet/index.js';
|
|
15
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
16
|
+
import { setHelpSections } from '../lib/help.js';
|
|
17
|
+
function brandLabel(b) {
|
|
18
|
+
switch (b) {
|
|
19
|
+
case 'visa': return 'Visa';
|
|
20
|
+
case 'mastercard': return 'Mastercard';
|
|
21
|
+
case 'amex': return 'American Express';
|
|
22
|
+
case 'discover': return 'Discover';
|
|
23
|
+
case 'diners': return 'Diners Club';
|
|
24
|
+
case 'jcb': return 'JCB';
|
|
25
|
+
case 'unionpay': return 'UnionPay';
|
|
26
|
+
default: return 'Card';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function formatExp(month, year) {
|
|
30
|
+
return `${month}/${year.slice(-2)}`;
|
|
31
|
+
}
|
|
32
|
+
function renderRow(c) {
|
|
33
|
+
const nick = c.nickname.padEnd(24);
|
|
34
|
+
const brand = brandLabel(c.brand).padEnd(18);
|
|
35
|
+
const last4 = `•••• ${c.last4}`.padEnd(10);
|
|
36
|
+
const exp = formatExp(c.exp_month, c.exp_year);
|
|
37
|
+
return ` ${chalk.cyan(nick)} ${brand} ${last4} ${chalk.gray('exp ' + exp)} ${chalk.gray(c.id)}`;
|
|
38
|
+
}
|
|
39
|
+
async function promptString(message, validate) {
|
|
40
|
+
const { input } = await import('@inquirer/prompts');
|
|
41
|
+
return await input({ message, validate });
|
|
42
|
+
}
|
|
43
|
+
async function promptSecret(message) {
|
|
44
|
+
const { password } = await import('@inquirer/prompts');
|
|
45
|
+
return await password({ message, mask: true });
|
|
46
|
+
}
|
|
47
|
+
function requireTTY(action) {
|
|
48
|
+
if (!isInteractiveTerminal()) {
|
|
49
|
+
throw new Error(`'agents wallet ${action}' requires an interactive terminal.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function registerWalletCommands(program) {
|
|
53
|
+
const cmd = program
|
|
54
|
+
.command('wallet')
|
|
55
|
+
.description('Device-local credit-card vault backed by macOS Keychain (Touch ID required to reveal). ' +
|
|
56
|
+
'Encrypted at rest, never leaves your device. Not Apple Pay — stores real PANs, no tokenization.');
|
|
57
|
+
setHelpSections(cmd, {
|
|
58
|
+
examples: `
|
|
59
|
+
$ agents wallet add # interactive: PAN, CVC, expiry, nickname
|
|
60
|
+
$ agents wallet list # last 4 only, no Touch ID prompt
|
|
61
|
+
$ agents wallet show personal-amex # Touch ID required, reveals full card
|
|
62
|
+
$ agents wallet rename personal-amex "Travel"
|
|
63
|
+
$ agents wallet remove personal-amex
|
|
64
|
+
`,
|
|
65
|
+
});
|
|
66
|
+
cmd
|
|
67
|
+
.command('add')
|
|
68
|
+
.description('Add a card to the vault. Interactive prompt for PAN, CVC, expiry, cardholder, nickname.')
|
|
69
|
+
.option('--nickname <name>', 'Set the nickname non-interactively (still prompts for PAN/CVC)')
|
|
70
|
+
.option('--stdin-json', 'Read all fields as a JSON object on stdin (for IPC callers). Emits the new card metadata as JSON to stdout.')
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
try {
|
|
73
|
+
if (opts.stdinJson) {
|
|
74
|
+
const raw = await new Promise((resolve, reject) => {
|
|
75
|
+
const chunks = [];
|
|
76
|
+
process.stdin.on('data', (c) => {
|
|
77
|
+
chunks.push(typeof c === 'string' ? Buffer.from(c) : c);
|
|
78
|
+
});
|
|
79
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
80
|
+
process.stdin.on('error', reject);
|
|
81
|
+
});
|
|
82
|
+
const input = JSON.parse(raw);
|
|
83
|
+
const meta = addCard(input);
|
|
84
|
+
process.stdout.write(JSON.stringify({ card: meta }) + '\n');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
requireTTY('add');
|
|
88
|
+
const pan = (await promptSecret('Card number')).replace(/\s+/g, '');
|
|
89
|
+
if (!/^\d+$/.test(pan))
|
|
90
|
+
throw new Error('PAN must contain only digits.');
|
|
91
|
+
if (!isValidLuhn(pan))
|
|
92
|
+
throw new Error('PAN failed Luhn checksum — typo?');
|
|
93
|
+
const brand = detectBrand(pan);
|
|
94
|
+
console.log(chalk.gray(`Detected: ${brandLabel(brand)} •••• ${pan.slice(-4)}`));
|
|
95
|
+
const exp_month = await promptString('Expiration month (MM)', (v) => {
|
|
96
|
+
const n = Number(v);
|
|
97
|
+
return Number.isInteger(n) && n >= 1 && n <= 12 ? true : 'Month must be 1-12';
|
|
98
|
+
});
|
|
99
|
+
const exp_year = await promptString('Expiration year (YY or YYYY)', (v) => {
|
|
100
|
+
const d = v.replace(/\D/g, '');
|
|
101
|
+
return d.length === 2 || d.length === 4 ? true : 'Year must be 2 or 4 digits';
|
|
102
|
+
});
|
|
103
|
+
const cvc = (await promptSecret('CVC')).replace(/\s+/g, '');
|
|
104
|
+
if (!/^\d{3,4}$/.test(cvc))
|
|
105
|
+
throw new Error('CVC must be 3 or 4 digits.');
|
|
106
|
+
const cardholder = await promptString('Cardholder name (as printed)');
|
|
107
|
+
const nickname = opts.nickname?.trim() || await promptString('Nickname (e.g. Personal Amex)', (v) => v.trim() ? true : 'Required');
|
|
108
|
+
const meta = addCard({ nickname, pan, cvc, cardholder, exp_month, exp_year });
|
|
109
|
+
console.log(chalk.green(`Added ${brandLabel(meta.brand)} •••• ${meta.last4} as '${meta.nickname}' (id: ${meta.id})`));
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (isPromptCancelled(err))
|
|
113
|
+
return;
|
|
114
|
+
console.error(chalk.red(err.message));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
cmd
|
|
119
|
+
.command('list')
|
|
120
|
+
.alias('ls')
|
|
121
|
+
.description('List stored cards (metadata only — last 4, brand, expiry). No biometric prompt.')
|
|
122
|
+
.option('--json', 'Emit JSON to stdout')
|
|
123
|
+
.action((opts) => {
|
|
124
|
+
try {
|
|
125
|
+
const cards = listCards();
|
|
126
|
+
if (opts.json) {
|
|
127
|
+
process.stdout.write(JSON.stringify({ cards }, null, 2) + '\n');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (cards.length === 0) {
|
|
131
|
+
console.log(chalk.gray('No cards in wallet. Try: agents wallet add'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
console.log(chalk.bold(` ${'NICKNAME'.padEnd(24)} ${'BRAND'.padEnd(18)} ${'LAST4'.padEnd(10)} EXP ID`));
|
|
135
|
+
for (const c of cards)
|
|
136
|
+
console.log(renderRow(c));
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
console.error(chalk.red(err.message));
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
cmd
|
|
144
|
+
.command('show <id>')
|
|
145
|
+
.description('Reveal a card. Touch ID required. Argument is a card id or nickname.')
|
|
146
|
+
.option('--json', 'Emit JSON to stdout (still triggers Touch ID)')
|
|
147
|
+
.action((id, opts) => {
|
|
148
|
+
try {
|
|
149
|
+
const full = showCard(id);
|
|
150
|
+
if (opts.json) {
|
|
151
|
+
process.stdout.write(JSON.stringify(full, null, 2) + '\n');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(chalk.bold(` ${full.nickname}`));
|
|
156
|
+
console.log(` ${chalk.gray('Brand ')} ${brandLabel(full.brand)}`);
|
|
157
|
+
console.log(` ${chalk.gray('Number ')} ${full.pan.match(/.{1,4}/g)?.join(' ') ?? full.pan}`);
|
|
158
|
+
console.log(` ${chalk.gray('CVC ')} ${full.cvc}`);
|
|
159
|
+
console.log(` ${chalk.gray('Expires ')} ${formatExp(full.exp_month, full.exp_year)}`);
|
|
160
|
+
console.log(` ${chalk.gray('Holder ')} ${full.cardholder}`);
|
|
161
|
+
console.log();
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
console.error(chalk.red(err.message));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
cmd
|
|
169
|
+
.command('remove <id>')
|
|
170
|
+
.alias('rm')
|
|
171
|
+
.description('Remove a card from the vault. Argument is a card id or nickname.')
|
|
172
|
+
.action((id) => {
|
|
173
|
+
try {
|
|
174
|
+
const meta = removeCard(id);
|
|
175
|
+
if (!meta) {
|
|
176
|
+
console.error(chalk.red(`No card found matching '${id}'.`));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
console.log(chalk.green(`Removed '${meta.nickname}' (${brandLabel(meta.brand)} •••• ${meta.last4}).`));
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.error(chalk.red(err.message));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
cmd
|
|
187
|
+
.command('rename <id> <new-nickname>')
|
|
188
|
+
.description('Rename a card. Argument is the current id or nickname.')
|
|
189
|
+
.action((id, newNickname) => {
|
|
190
|
+
try {
|
|
191
|
+
const meta = renameCard(id, newNickname);
|
|
192
|
+
console.log(chalk.green(`Renamed to '${meta.nickname}'.`));
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
console.error(chalk.red(err.message));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,7 @@ import { registerRunCommand } from './commands/exec.js';
|
|
|
78
78
|
import { registerModelsCommand } from './commands/models.js';
|
|
79
79
|
import { registerDefaultsCommands } from './commands/defaults.js';
|
|
80
80
|
import { registerPruneCommand } from './commands/prune.js';
|
|
81
|
-
import { registerTrashCommands } from './commands/trash.js';
|
|
81
|
+
import { registerTrashCommands, registerRestoreCommand } from './commands/trash.js';
|
|
82
82
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
83
83
|
import { registerSubagentsCommands } from './commands/subagents.js';
|
|
84
84
|
import { registerPluginsCommands } from './commands/plugins.js';
|
|
@@ -93,6 +93,7 @@ import { registerBrowserCommand } from './commands/browser.js';
|
|
|
93
93
|
import { registerComputerCommand } from './commands/computer.js';
|
|
94
94
|
import { registerProfilesCommands } from './commands/profiles.js';
|
|
95
95
|
import { registerSecretsCommands } from './commands/secrets.js';
|
|
96
|
+
import { registerWalletCommands } from './commands/wallet.js';
|
|
96
97
|
import { registerHelperCommand } from './commands/helper.js';
|
|
97
98
|
import { registerFactoryCommands } from './commands/factory.js';
|
|
98
99
|
import { registerUsageCommand } from './commands/usage.js';
|
|
@@ -348,6 +349,24 @@ async function installResolvedPackage(metadata) {
|
|
|
348
349
|
await installPackageIntoPrefix(`${NPM_PACKAGE_NAME}@${metadata.version}`, prefix);
|
|
349
350
|
verifyInstalledVersion(packageRoot, metadata.version);
|
|
350
351
|
refreshAliasShims(packageRoot);
|
|
352
|
+
// The npm install above runs with --ignore-scripts, so the postinstall that
|
|
353
|
+
// installs the macOS Keychain helper never fires on upgrade. Force-refresh the
|
|
354
|
+
// helper here so a user upgrading FROM a broken build (e.g. the entitlement-less
|
|
355
|
+
// 1.20.4 helper that fails SecItemAdd with -34018) gets the fixed, signed bundle
|
|
356
|
+
// immediately — instead of waiting for the lazy staleness check in
|
|
357
|
+
// getKeychainHelperPath() to repair it on their next secret operation. The new
|
|
358
|
+
// package is already on disk, so the dynamic import resolves the freshly-installed
|
|
359
|
+
// helper module + bundle. Best-effort: an upgrade must never fail because the
|
|
360
|
+
// helper could not be reinstalled (`agents helper install --force` stays available).
|
|
361
|
+
if (process.platform === 'darwin') {
|
|
362
|
+
try {
|
|
363
|
+
const { ensureKeychainHelperInstalled } = await import('./lib/secrets/install-helper.js');
|
|
364
|
+
ensureKeychainHelperInstalled({ forceReinstall: true });
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// Non-fatal.
|
|
368
|
+
}
|
|
369
|
+
}
|
|
351
370
|
}
|
|
352
371
|
/** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
|
|
353
372
|
async function promptUpgrade(latestVersion) {
|
|
@@ -637,6 +656,7 @@ registerDefaultsCommands(program);
|
|
|
637
656
|
registerModelsCommand(program);
|
|
638
657
|
registerPruneCommand(program);
|
|
639
658
|
registerTrashCommands(program);
|
|
659
|
+
registerRestoreCommand(program);
|
|
640
660
|
registerDoctorCommand(program);
|
|
641
661
|
// Deprecated 'exec' alias for 'run'
|
|
642
662
|
program
|
|
@@ -651,6 +671,7 @@ program
|
|
|
651
671
|
});
|
|
652
672
|
registerProfilesCommands(program);
|
|
653
673
|
registerSecretsCommands(program);
|
|
674
|
+
registerWalletCommands(program);
|
|
654
675
|
registerHelperCommand(program);
|
|
655
676
|
registerBetaCommands(program);
|
|
656
677
|
registerSyncCommand(program);
|