@phnx-labs/agents-cli 1.20.12 → 1.20.14
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 +30 -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/doctor.js +51 -7
- package/dist/commands/exec.js +25 -4
- package/dist/commands/import.js +17 -6
- package/dist/commands/inspect.d.ts +28 -1
- package/dist/commands/inspect.js +330 -47
- 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/prune.js +8 -5
- 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.d.ts +1 -0
- package/dist/commands/view.js +56 -12
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +199 -0
- package/dist/index.js +4 -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.d.ts +21 -1
- package/dist/lib/hooks.js +69 -7
- 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 +24 -0
- package/dist/lib/plugins.js +37 -2
- package/dist/lib/project-launch.js +110 -5
- package/dist/lib/registry.js +15 -2
- package/dist/lib/rotate.d.ts +7 -0
- package/dist/lib/rotate.js +17 -7
- 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
|
@@ -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';
|
|
@@ -655,6 +656,7 @@ registerDefaultsCommands(program);
|
|
|
655
656
|
registerModelsCommand(program);
|
|
656
657
|
registerPruneCommand(program);
|
|
657
658
|
registerTrashCommands(program);
|
|
659
|
+
registerRestoreCommand(program);
|
|
658
660
|
registerDoctorCommand(program);
|
|
659
661
|
// Deprecated 'exec' alias for 'run'
|
|
660
662
|
program
|
|
@@ -669,6 +671,7 @@ program
|
|
|
669
671
|
});
|
|
670
672
|
registerProfilesCommands(program);
|
|
671
673
|
registerSecretsCommands(program);
|
|
674
|
+
registerWalletCommands(program);
|
|
672
675
|
registerHelperCommand(program);
|
|
673
676
|
registerBetaCommands(program);
|
|
674
677
|
registerSyncCommand(program);
|
package/dist/lib/agents.js
CHANGED
|
@@ -90,41 +90,44 @@ function findInPath(command) {
|
|
|
90
90
|
return null;
|
|
91
91
|
}
|
|
92
92
|
/** Grok-specific binary resolution.
|
|
93
|
-
* Grok does not live in node_modules/.bin. Its versioned binaries live in
|
|
94
|
-
*
|
|
95
|
-
*
|
|
93
|
+
* Grok does not live in node_modules/.bin. Its versioned binaries live in each
|
|
94
|
+
* managed version home under `.grok/downloads/`, so detection must not follow
|
|
95
|
+
* the host ~/.grok config symlink.
|
|
96
96
|
*/
|
|
97
97
|
function resolveGrokBinary(version) {
|
|
98
|
-
const grokDownloads = path.join(HOME, '.grok', 'downloads');
|
|
99
|
-
if (!fs.existsSync(grokDownloads))
|
|
100
|
-
return null;
|
|
101
|
-
const entries = fs.readdirSync(grokDownloads);
|
|
102
|
-
// Prefer exact version match in filename
|
|
103
98
|
if (version && version !== 'latest') {
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
99
|
+
const binaryPath = getBinaryPath('grok', version);
|
|
100
|
+
if (fs.existsSync(binaryPath))
|
|
101
|
+
return binaryPath;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const resolvedVersion = resolveVersion('grok', process.cwd());
|
|
105
|
+
if (resolvedVersion) {
|
|
106
|
+
const binaryPath = getBinaryPath('grok', resolvedVersion);
|
|
107
|
+
if (fs.existsSync(binaryPath))
|
|
108
|
+
return binaryPath;
|
|
109
|
+
}
|
|
110
|
+
const grokVersionsDir = path.join(getVersionsDir(), 'grok');
|
|
111
|
+
if (!fs.existsSync(grokVersionsDir))
|
|
112
|
+
return null;
|
|
113
113
|
let latest = null;
|
|
114
114
|
let latestMtime = 0;
|
|
115
|
-
for (const
|
|
116
|
-
if (!
|
|
115
|
+
for (const entry of fs.readdirSync(grokVersionsDir, { withFileTypes: true })) {
|
|
116
|
+
if (!entry.isDirectory())
|
|
117
|
+
continue;
|
|
118
|
+
const binaryPath = getBinaryPath('grok', entry.name);
|
|
119
|
+
if (!fs.existsSync(binaryPath))
|
|
117
120
|
continue;
|
|
118
121
|
try {
|
|
119
|
-
const stat = fs.statSync(
|
|
122
|
+
const stat = fs.statSync(binaryPath);
|
|
120
123
|
if (stat.mtimeMs > latestMtime) {
|
|
121
124
|
latestMtime = stat.mtimeMs;
|
|
122
|
-
latest =
|
|
125
|
+
latest = binaryPath;
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
catch { }
|
|
126
129
|
}
|
|
127
|
-
return latest
|
|
130
|
+
return latest;
|
|
128
131
|
}
|
|
129
132
|
function splitCommandLine(command) {
|
|
130
133
|
const args = [];
|
|
@@ -482,6 +485,44 @@ export const AGENTS = {
|
|
|
482
485
|
rulesImports: false,
|
|
483
486
|
},
|
|
484
487
|
},
|
|
488
|
+
// Factory AI Droid CLI (`droid`) — agentic coding CLI from factory.ai.
|
|
489
|
+
// Install: `curl -fsSL https://app.factory.ai/cli | sh` (no npm package).
|
|
490
|
+
// Binary is NOT in node_modules/.bin — resolved via resolveDroidBinary().
|
|
491
|
+
// Config: `~/.factory/` (settings.json, mcp.json, droids/, commands/).
|
|
492
|
+
// Memory: native AGENTS.md. Subagents = custom droids (top-level .md files
|
|
493
|
+
// in ~/.factory/droids/). Config isolation rides the ~/.factory symlink
|
|
494
|
+
// switch (no FACTORY_HOME env var exists). Headless: `droid exec "<prompt>"`
|
|
495
|
+
// with --auto low|medium|high, -o stream-json, -m <model>, -r <effort>.
|
|
496
|
+
droid: {
|
|
497
|
+
id: 'droid',
|
|
498
|
+
name: 'Droid',
|
|
499
|
+
color: 'yellowBright',
|
|
500
|
+
cliCommand: 'droid',
|
|
501
|
+
npmPackage: '',
|
|
502
|
+
installScript: 'curl -fsSL https://app.factory.ai/cli | sh',
|
|
503
|
+
configDir: path.join(HOME, '.factory'),
|
|
504
|
+
commandsDir: path.join(HOME, '.factory', 'commands'),
|
|
505
|
+
commandsSubdir: 'commands',
|
|
506
|
+
skillsDir: '', // no skills concept
|
|
507
|
+
hooksDir: 'hooks',
|
|
508
|
+
instructionsFile: 'AGENTS.md',
|
|
509
|
+
format: 'markdown',
|
|
510
|
+
variableSyntax: '$ARGUMENTS',
|
|
511
|
+
supportsHooks: false,
|
|
512
|
+
capabilities: {
|
|
513
|
+
hooks: false,
|
|
514
|
+
mcp: true,
|
|
515
|
+
allowlist: false,
|
|
516
|
+
skills: false,
|
|
517
|
+
commands: true,
|
|
518
|
+
plugins: false,
|
|
519
|
+
subagents: true,
|
|
520
|
+
rules: { file: 'AGENTS.md' },
|
|
521
|
+
workflows: false,
|
|
522
|
+
modes: ['plan', 'edit', 'auto', 'skip'],
|
|
523
|
+
rulesImports: false,
|
|
524
|
+
},
|
|
525
|
+
},
|
|
485
526
|
};
|
|
486
527
|
/** All registered agent IDs derived from the AGENTS registry. */
|
|
487
528
|
export const ALL_AGENT_IDS = Object.keys(AGENTS);
|
|
@@ -1353,6 +1394,9 @@ export function getUserMcpConfigPath(agentId) {
|
|
|
1353
1394
|
case 'grok':
|
|
1354
1395
|
// grok mcp.json — exact field schema verified at first install
|
|
1355
1396
|
return path.join(agent.configDir, 'mcp.json');
|
|
1397
|
+
case 'droid':
|
|
1398
|
+
// Factory AI Droid stores MCPs in ~/.factory/mcp.json
|
|
1399
|
+
return path.join(agent.configDir, 'mcp.json');
|
|
1356
1400
|
default:
|
|
1357
1401
|
// Gemini and others use settings.json
|
|
1358
1402
|
return path.join(agent.configDir, 'settings.json');
|
|
@@ -1387,6 +1431,8 @@ export function getMcpConfigPathForHome(agentId, home) {
|
|
|
1387
1431
|
return path.join(home, '.gemini', 'antigravity-cli', 'mcp_config.json');
|
|
1388
1432
|
case 'grok':
|
|
1389
1433
|
return path.join(home, '.grok', 'config.toml');
|
|
1434
|
+
case 'droid':
|
|
1435
|
+
return path.join(home, '.factory', 'mcp.json');
|
|
1390
1436
|
default:
|
|
1391
1437
|
return path.join(home, agentConfigDirName(agentId), 'settings.json');
|
|
1392
1438
|
}
|
|
@@ -1423,6 +1469,8 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
|
|
|
1423
1469
|
return path.join(cwd, '.gemini', 'antigravity-cli', 'mcp_config.json');
|
|
1424
1470
|
case 'grok':
|
|
1425
1471
|
return path.join(cwd, '.grok', 'config.toml');
|
|
1472
|
+
case 'droid':
|
|
1473
|
+
return path.join(cwd, '.factory', 'mcp.json');
|
|
1426
1474
|
default:
|
|
1427
1475
|
return path.join(cwd, `.${agentId}`, 'settings.json');
|
|
1428
1476
|
}
|
|
@@ -16,4 +16,11 @@ export declare class BrowserIPCServer {
|
|
|
16
16
|
stop(): Promise<void>;
|
|
17
17
|
private handleRequest;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Decide whether a running daemon is stale and must be restarted. A daemon
|
|
21
|
+
* is stale when it reports a concrete version that differs from this CLI's.
|
|
22
|
+
* `undefined`/`'unknown'` means the daemon is too old to answer the `version`
|
|
23
|
+
* action reliably — don't churn it on that ambiguous signal.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shouldRestartStaleDaemon(daemonVersion: string | undefined, clientVersion: string): boolean;
|
|
19
26
|
export declare function sendIPCRequest(request: IPCRequest, opts?: IPCRequestOptions): Promise<IPCResponse>;
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { IS_WINDOWS, ipcEndpoint } from '../platform/index.js';
|
|
5
5
|
import { getHelpersDir } from '../state.js';
|
|
6
|
-
import { startDaemon } from '../daemon.js';
|
|
6
|
+
import { startDaemon, stopDaemon } from '../daemon.js';
|
|
7
7
|
import { getCliVersion } from '../version.js';
|
|
8
8
|
const SOCKET_NAME = 'browser.sock';
|
|
9
9
|
export class BrowserDaemonNotRunningError extends Error {
|
|
@@ -453,41 +453,51 @@ export class BrowserIPCServer {
|
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
455
|
}
|
|
456
|
-
let
|
|
456
|
+
let versionReconciledThisProcess = false;
|
|
457
457
|
/**
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
462
|
-
* to a dev-build CLI for an entire session and nothing surfaced it.
|
|
458
|
+
* Decide whether a running daemon is stale and must be restarted. A daemon
|
|
459
|
+
* is stale when it reports a concrete version that differs from this CLI's.
|
|
460
|
+
* `undefined`/`'unknown'` means the daemon is too old to answer the `version`
|
|
461
|
+
* action reliably — don't churn it on that ambiguous signal.
|
|
463
462
|
*/
|
|
464
|
-
|
|
465
|
-
if (
|
|
463
|
+
export function shouldRestartStaleDaemon(daemonVersion, clientVersion) {
|
|
464
|
+
if (!daemonVersion || daemonVersion === 'unknown')
|
|
465
|
+
return false;
|
|
466
|
+
return daemonVersion !== clientVersion;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Reconcile the running daemon's version with ours. If the daemon is serving
|
|
470
|
+
* stale code, stop and restart it so this request — and the rest of the
|
|
471
|
+
* session — runs the current build. Runs at most once per CLI process. The
|
|
472
|
+
* whole reason this exists: a launchd-managed daemon kept serving stale code
|
|
473
|
+
* to a dev-build CLI for an entire session and nothing surfaced it (#291).
|
|
474
|
+
*/
|
|
475
|
+
async function reconcileDaemonVersion(socketPath) {
|
|
476
|
+
if (versionReconciledThisProcess)
|
|
466
477
|
return;
|
|
467
|
-
|
|
478
|
+
versionReconciledThisProcess = true;
|
|
479
|
+
let daemon;
|
|
468
480
|
try {
|
|
469
|
-
const resp = await sendRawIPCRequest({ action: 'version' });
|
|
470
|
-
|
|
471
|
-
const client = getCliVersion();
|
|
472
|
-
if (!daemon || daemon === 'unknown' || daemon === client)
|
|
473
|
-
return;
|
|
474
|
-
process.stderr.write(`\nwarning: browser daemon is on ${daemon} but this CLI is on ${client}.\n` +
|
|
475
|
-
` Run \`agents daemon restart\` to load the current code.\n\n`);
|
|
481
|
+
const resp = await sendRawIPCRequest({ action: 'version' }, { autoStartDaemon: false });
|
|
482
|
+
daemon = resp.version;
|
|
476
483
|
}
|
|
477
484
|
catch {
|
|
478
|
-
//
|
|
479
|
-
|
|
485
|
+
// Daemon unreachable or too old to speak 'version' — leave it alone.
|
|
486
|
+
return;
|
|
480
487
|
}
|
|
488
|
+
const client = getCliVersion();
|
|
489
|
+
if (!shouldRestartStaleDaemon(daemon, client))
|
|
490
|
+
return;
|
|
491
|
+
process.stderr.write(`\nbrowser daemon was on ${daemon}, this CLI is on ${client} — restarting it to load current code.\n\n`);
|
|
492
|
+
stopDaemon();
|
|
493
|
+
startDaemon();
|
|
494
|
+
if (!(await isDaemonReachable())) {
|
|
495
|
+
await waitForSocket(socketPath, 6000);
|
|
496
|
+
}
|
|
497
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
481
498
|
}
|
|
482
499
|
export async function sendIPCRequest(request, opts = {}) {
|
|
483
|
-
|
|
484
|
-
// Run the version check after the user's request returns — keeps the
|
|
485
|
-
// critical path zero-overhead and ensures `start` doesn't get blocked
|
|
486
|
-
// on a daemon-restart warning that the user hasn't read yet.
|
|
487
|
-
if (request.action !== 'version') {
|
|
488
|
-
maybeWarnVersionMismatch().catch(() => { });
|
|
489
|
-
}
|
|
490
|
-
return result;
|
|
500
|
+
return sendRawIPCRequest(request, opts);
|
|
491
501
|
}
|
|
492
502
|
async function sendRawIPCRequest(request, opts = {}) {
|
|
493
503
|
const socketPath = getSocketPath();
|
|
@@ -510,6 +520,12 @@ async function sendRawIPCRequest(request, opts = {}) {
|
|
|
510
520
|
}
|
|
511
521
|
await new Promise((r) => setTimeout(r, 300));
|
|
512
522
|
}
|
|
523
|
+
// Before serving a real request, make sure the daemon isn't running stale
|
|
524
|
+
// code. Skips the internal `version` probe (avoids recursion) and callers
|
|
525
|
+
// that opt out of auto-start. No-ops once reconciled or when versions match.
|
|
526
|
+
if (request.action !== 'version' && autoStartDaemon) {
|
|
527
|
+
await reconcileDaemonVersion(socketPath);
|
|
528
|
+
}
|
|
513
529
|
return new Promise((resolve, reject) => {
|
|
514
530
|
const socket = net.createConnection(endpoint);
|
|
515
531
|
let buffer = '';
|
package/dist/lib/capabilities.js
CHANGED
|
@@ -24,7 +24,13 @@ function compareVersions(a, b) {
|
|
|
24
24
|
return 0;
|
|
25
25
|
}
|
|
26
26
|
function getCapability(agent, cap) {
|
|
27
|
-
|
|
27
|
+
// Guard against unknown agent ids (e.g. a caller passing "claude@2.1.168"
|
|
28
|
+
// instead of "claude"). Without this, AGENTS[agent] is undefined and the
|
|
29
|
+
// property access throws an opaque TypeError instead of reporting false.
|
|
30
|
+
const def = AGENTS[agent];
|
|
31
|
+
if (!def)
|
|
32
|
+
return false;
|
|
33
|
+
return def.capabilities[cap];
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
30
36
|
* True when the agent supports the capability on at least some version.
|
|
@@ -7,6 +7,7 @@ export declare function shouldInstallCommandAsSkill(agent: AgentId, version: str
|
|
|
7
7
|
export declare function commandSkillName(commandName: string): string;
|
|
8
8
|
export declare function buildCommandSkillContent(commandName: string, sourcePath: string): string;
|
|
9
9
|
export declare function skillSourceExists(skillName: string, skillSourceDirs: Array<string | null | undefined>): boolean;
|
|
10
|
+
export declare function readSkillSourceCommandMarker(skillName: string, skillSourceDirs: Array<string | null | undefined>): string | null;
|
|
10
11
|
export declare function installCommandSkillToVersion(agentDir: string, commandName: string, sourcePath: string, skillSourceDirs?: Array<string | null | undefined>): {
|
|
11
12
|
success: boolean;
|
|
12
13
|
skipped?: boolean;
|
|
@@ -74,18 +74,34 @@ export function buildCommandSkillContent(commandName, sourcePath) {
|
|
|
74
74
|
'',
|
|
75
75
|
].join('\n');
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
function findSkillSourceDir(skillName, skillSourceDirs) {
|
|
78
|
+
for (const dir of skillSourceDirs) {
|
|
79
79
|
if (!dir)
|
|
80
|
-
|
|
80
|
+
continue;
|
|
81
81
|
const candidate = path.join(dir, skillName);
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
if (fs.existsSync(candidate) && fs.lstatSync(candidate).isDirectory()) {
|
|
83
|
+
return candidate;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
export function skillSourceExists(skillName, skillSourceDirs) {
|
|
89
|
+
return findSkillSourceDir(skillName, skillSourceDirs) !== null;
|
|
90
|
+
}
|
|
91
|
+
export function readSkillSourceCommandMarker(skillName, skillSourceDirs) {
|
|
92
|
+
const sourceDir = findSkillSourceDir(skillName, skillSourceDirs);
|
|
93
|
+
if (!sourceDir)
|
|
94
|
+
return null;
|
|
95
|
+
return readSkillCommandMarker(path.join(sourceDir, 'SKILL.md'));
|
|
84
96
|
}
|
|
85
97
|
export function installCommandSkillToVersion(agentDir, commandName, sourcePath, skillSourceDirs = []) {
|
|
86
98
|
const skillName = commandSkillName(commandName);
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
const existingSkillSource = findSkillSourceDir(skillName, skillSourceDirs);
|
|
100
|
+
if (existingSkillSource) {
|
|
101
|
+
const sourceMarker = readSkillCommandMarker(path.join(existingSkillSource, 'SKILL.md'));
|
|
102
|
+
if (sourceMarker !== commandName) {
|
|
103
|
+
return { success: true, skipped: true, error: `Skill '${skillName}' already exists` };
|
|
104
|
+
}
|
|
89
105
|
}
|
|
90
106
|
const skillsDir = safeJoin(agentDir, 'skills');
|
|
91
107
|
const skillDir = safeJoin(skillsDir, skillName);
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -12,6 +12,29 @@ export type ExecMode = Mode;
|
|
|
12
12
|
* boundary rather than silently picking a wrong code path.
|
|
13
13
|
*/
|
|
14
14
|
export declare function normalizeMode(input: string | null | undefined): Mode;
|
|
15
|
+
/**
|
|
16
|
+
* Detect the headless-plan stall footgun.
|
|
17
|
+
*
|
|
18
|
+
* A slash command (e.g. `/code:commit`) run headless under the IMPLICIT default
|
|
19
|
+
* `plan` mode hangs forever: plan is read-only, so the agent calls ExitPlanMode
|
|
20
|
+
* to start working, and in a headless run there is no TTY to approve it. The
|
|
21
|
+
* process just sits there. Callers use this to fail fast with a fix instead.
|
|
22
|
+
*
|
|
23
|
+
* Returns the offending command token (e.g. `/code:commit`) when the run should
|
|
24
|
+
* be blocked, else null. Guards are deliberately narrow:
|
|
25
|
+
* - interactive runs / no prompt -> not headless, never blocks
|
|
26
|
+
* - explicit --mode (modeIsDefault false) -> respected; `--mode plan` is a
|
|
27
|
+
* legitimate read-only command run and must not be blocked
|
|
28
|
+
* - resolved mode is not `plan` -> only plan stalls at ExitPlanMode
|
|
29
|
+
* - prompt is not a slash command -> natural-language read-only prompts
|
|
30
|
+
* ("summarize commits") are a valid default-plan use and must not be blocked
|
|
31
|
+
*/
|
|
32
|
+
export declare function headlessPlanStallCommand(args: {
|
|
33
|
+
prompt: string | undefined;
|
|
34
|
+
interactive: boolean | undefined;
|
|
35
|
+
mode: string;
|
|
36
|
+
modeIsDefault: boolean;
|
|
37
|
+
}): string | null;
|
|
15
38
|
/**
|
|
16
39
|
* Resolve a requested mode against an agent's capability table.
|
|
17
40
|
*
|
|
@@ -45,11 +68,12 @@ export interface ExecOptions {
|
|
|
45
68
|
version?: string;
|
|
46
69
|
/** Omit to launch the CLI interactively -- no prompt, no --print, stdio fully inherited. */
|
|
47
70
|
prompt?: string;
|
|
48
|
-
/** Force interactive mode even when a prompt is provided. */
|
|
71
|
+
/** Force interactive mode even when a prompt is provided. Wins over `headless`. */
|
|
49
72
|
interactive?: boolean;
|
|
50
73
|
mode: ExecMode;
|
|
51
74
|
effort: ExecEffort;
|
|
52
75
|
cwd?: string;
|
|
76
|
+
/** Force headless mode even when no prompt is provided (e.g. piping via stdin). */
|
|
53
77
|
headless?: boolean;
|
|
54
78
|
json?: boolean;
|
|
55
79
|
model?: string;
|
|
@@ -59,6 +83,13 @@ export interface ExecOptions {
|
|
|
59
83
|
verbose?: boolean;
|
|
60
84
|
env?: Record<string, string>;
|
|
61
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve interactive vs headless. Explicit flags are definitive and win over
|
|
88
|
+
* inference: `--interactive` forces interactive, `--headless` forces headless.
|
|
89
|
+
* With neither flag, prompt presence decides (prompt -> headless, none -> interactive).
|
|
90
|
+
* `--interactive` takes precedence over `--headless`; the CLI layer rejects passing both.
|
|
91
|
+
*/
|
|
92
|
+
export declare function resolveInteractive(options: Pick<ExecOptions, 'interactive' | 'headless' | 'prompt'>): boolean;
|
|
62
93
|
/** Parse an array of KEY=VALUE strings into an env record. Returns undefined for empty input. */
|
|
63
94
|
export declare function parseExecEnv(entries: string[]): Record<string, string> | undefined;
|
|
64
95
|
/**
|