@phnx-labs/agents-cli 1.20.58 → 1.20.60
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 +23 -1
- package/README.md +15 -7
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +39 -2
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +6 -6
- package/dist/index.js +2 -1
- package/dist/lib/agents.js +19 -13
- package/dist/lib/hosts/dispatch.d.ts +36 -0
- package/dist/lib/hosts/dispatch.js +40 -2
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/output/git-output.d.ts +74 -0
- package/dist/lib/output/git-output.js +213 -0
- package/dist/lib/permissions.d.ts +26 -5
- package/dist/lib/permissions.js +212 -37
- package/dist/lib/plugins.d.ts +8 -0
- package/dist/lib/plugins.js +108 -0
- package/dist/lib/project-root.js +2 -1
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +8 -2
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +46 -1
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +11 -10
- package/dist/lib/session/db.d.ts +3 -0
- package/dist/lib/session/db.js +20 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +40 -4
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/shims.js +13 -3
- package/dist/lib/skills.js +14 -1
- package/dist/lib/staleness/detectors/permissions.js +50 -3
- package/dist/lib/staleness/detectors/subagents.js +31 -12
- package/dist/lib/staleness/detectors/workflows.js +33 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -5
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +11 -1
- package/dist/lib/subagents.js +117 -26
- package/dist/lib/versions.js +12 -2
- package/dist/lib/workflows.d.ts +5 -3
- package/dist/lib/workflows.js +246 -9
- package/package.json +1 -1
package/dist/lib/permissions.js
CHANGED
|
@@ -306,14 +306,21 @@ export function buildPermissionsFromGroups(groupNames) {
|
|
|
306
306
|
// Matches lines like: - "Bash(git *)" or - "WebFetch(domain:example.com)"
|
|
307
307
|
// Handles nested quotes that break YAML parsers
|
|
308
308
|
const lines = content.split('\n');
|
|
309
|
+
let section = null;
|
|
309
310
|
for (const line of lines) {
|
|
311
|
+
const sectionMatch = line.match(/^\s*(allow|deny)\s*:\s*(?:#.*)?$/);
|
|
312
|
+
if (sectionMatch) {
|
|
313
|
+
section = sectionMatch[1];
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
310
316
|
// Match: optional whitespace, dash, whitespace, quote, content, quote
|
|
311
317
|
// Use greedy match to capture everything between first and last quote
|
|
312
318
|
const match = line.match(/^\s*-\s*"(.+)"$/);
|
|
313
319
|
if (match) {
|
|
314
320
|
const rule = match[1];
|
|
315
|
-
// 99-deny group rules go to deny, others
|
|
316
|
-
|
|
321
|
+
// 99-deny group rules go to deny, others follow their YAML section.
|
|
322
|
+
// Legacy group files used bare lists with no section; keep those as allow.
|
|
323
|
+
if (section === 'deny' || groupName === '99-deny' || groupName.includes('-deny')) {
|
|
317
324
|
allDeny.push(rule);
|
|
318
325
|
}
|
|
319
326
|
else {
|
|
@@ -486,32 +493,40 @@ function parseCanonicalPattern(permission) {
|
|
|
486
493
|
const BLANKET_BASH_FORMS = new Set(['Bash', 'Bash(*)', 'Bash(**)']);
|
|
487
494
|
/**
|
|
488
495
|
* Convert canonical permission set to Gemini format.
|
|
489
|
-
* Gemini reads tool allow
|
|
490
|
-
* Bash permissions map to
|
|
491
|
-
*
|
|
492
|
-
* Blanket Bash grants map to bare "run_shell_command" (no prefix filter).
|
|
496
|
+
* Gemini reads tool allow/deny lists from settings.json under
|
|
497
|
+
* `tools.core` / `tools.exclude`. Bash permissions map to ShellTool(pattern).
|
|
498
|
+
* Blanket Bash grants map to bare "ShellTool" (no command filter).
|
|
493
499
|
* Non-Bash tool patterns are skipped; Gemini uses different tool names.
|
|
494
500
|
*/
|
|
495
501
|
export function convertToGeminiFormat(set) {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
502
|
+
const serialize = (permissions) => {
|
|
503
|
+
const tools = new Set();
|
|
504
|
+
for (const perm of permissions) {
|
|
505
|
+
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
506
|
+
tools.add('ShellTool');
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
const parsed = parseCanonicalPattern(perm);
|
|
510
|
+
if (!parsed || parsed.tool !== 'bash')
|
|
511
|
+
continue;
|
|
512
|
+
const command = normalizeBashPattern(parsed.pattern);
|
|
513
|
+
if (command === '*') {
|
|
514
|
+
tools.add('ShellTool');
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
tools.add(`ShellTool(${command})`);
|
|
518
|
+
}
|
|
512
519
|
}
|
|
513
|
-
|
|
514
|
-
|
|
520
|
+
return Array.from(tools);
|
|
521
|
+
};
|
|
522
|
+
const core = serialize(set.allow);
|
|
523
|
+
const exclude = serialize(set.deny ?? []);
|
|
524
|
+
return {
|
|
525
|
+
tools: {
|
|
526
|
+
core,
|
|
527
|
+
...(exclude.length ? { exclude } : {}),
|
|
528
|
+
},
|
|
529
|
+
};
|
|
515
530
|
}
|
|
516
531
|
/**
|
|
517
532
|
* Strip Claude's `:*` subcommand-wildcard suffix and return a space-glob form.
|
|
@@ -525,6 +540,26 @@ function normalizeBashPattern(pattern) {
|
|
|
525
540
|
return pattern.slice(0, -2) + ' *';
|
|
526
541
|
return pattern;
|
|
527
542
|
}
|
|
543
|
+
/** Convert canonical Bash rules into Droid's command arrays. */
|
|
544
|
+
export function convertToDroidFormat(set) {
|
|
545
|
+
const commands = (permissions) => {
|
|
546
|
+
const result = new Set();
|
|
547
|
+
for (const permission of permissions) {
|
|
548
|
+
if (BLANKET_BASH_FORMS.has(permission)) {
|
|
549
|
+
result.add('*');
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
const parsed = parseCanonicalPattern(permission);
|
|
553
|
+
if (parsed?.tool === 'bash')
|
|
554
|
+
result.add(normalizeBashPattern(parsed.pattern));
|
|
555
|
+
}
|
|
556
|
+
return Array.from(result);
|
|
557
|
+
};
|
|
558
|
+
return {
|
|
559
|
+
commandAllowlist: commands(set.allow),
|
|
560
|
+
commandDenylist: commands(set.deny ?? []),
|
|
561
|
+
};
|
|
562
|
+
}
|
|
528
563
|
/**
|
|
529
564
|
* Convert canonical permission set to Antigravity format.
|
|
530
565
|
* Antigravity reads ~/.gemini/antigravity-cli/settings.json with
|
|
@@ -576,6 +611,49 @@ const ANTIGRAVITY_ACTION_BY_TOOL = {
|
|
|
576
611
|
write: 'write_file',
|
|
577
612
|
webfetch: 'read_url',
|
|
578
613
|
};
|
|
614
|
+
const GOOSE_TOOL_BY_CANONICAL = {
|
|
615
|
+
bash: 'developer__shell',
|
|
616
|
+
read: 'developer__text_editor',
|
|
617
|
+
write: 'developer__text_editor',
|
|
618
|
+
edit: 'developer__text_editor',
|
|
619
|
+
grep: 'developer__analyze',
|
|
620
|
+
glob: 'developer__analyze',
|
|
621
|
+
webfetch: 'developer__fetch',
|
|
622
|
+
mcp: undefined,
|
|
623
|
+
};
|
|
624
|
+
function canonicalToGooseTool(permission) {
|
|
625
|
+
if (BLANKET_BASH_FORMS.has(permission))
|
|
626
|
+
return 'developer__shell';
|
|
627
|
+
const parsed = parseCanonicalPattern(permission);
|
|
628
|
+
if (!parsed)
|
|
629
|
+
return null;
|
|
630
|
+
return GOOSE_TOOL_BY_CANONICAL[parsed.tool] ?? null;
|
|
631
|
+
}
|
|
632
|
+
/** Convert canonical permissions to Goose's per-tool permission.yaml shape. */
|
|
633
|
+
export function convertToGooseFormat(set) {
|
|
634
|
+
const alwaysAllow = new Set();
|
|
635
|
+
const neverAllow = new Set();
|
|
636
|
+
for (const permission of set.allow) {
|
|
637
|
+
const tool = canonicalToGooseTool(permission);
|
|
638
|
+
if (tool)
|
|
639
|
+
alwaysAllow.add(tool);
|
|
640
|
+
}
|
|
641
|
+
for (const permission of set.deny ?? []) {
|
|
642
|
+
const tool = canonicalToGooseTool(permission);
|
|
643
|
+
if (tool)
|
|
644
|
+
neverAllow.add(tool);
|
|
645
|
+
}
|
|
646
|
+
for (const tool of neverAllow) {
|
|
647
|
+
alwaysAllow.delete(tool);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
user: {
|
|
651
|
+
always_allow: Array.from(alwaysAllow).sort(),
|
|
652
|
+
ask_before: [],
|
|
653
|
+
never_allow: Array.from(neverAllow).sort(),
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
}
|
|
579
657
|
/**
|
|
580
658
|
* Convert canonical permission set to Grok format.
|
|
581
659
|
* Grok reads ~/.grok/config.toml with
|
|
@@ -919,9 +997,7 @@ function readClaudePermissions(scope = 'user', cwd, options) {
|
|
|
919
997
|
*/
|
|
920
998
|
function readOpenCodePermissions(scope = 'user', cwd, options) {
|
|
921
999
|
const home = options?.home || HOME;
|
|
922
|
-
const configPath = scope
|
|
923
|
-
? path.join(home, '.opencode', 'opencode.jsonc')
|
|
924
|
-
: path.join(cwd || process.cwd(), '.opencode', 'opencode.jsonc');
|
|
1000
|
+
const configPath = openCodeConfigPath(scope, cwd, home);
|
|
925
1001
|
if (!fs.existsSync(configPath)) {
|
|
926
1002
|
return null;
|
|
927
1003
|
}
|
|
@@ -1031,14 +1107,36 @@ export function applyClaudePermissions(set, scope = 'user', cwd, merge = true) {
|
|
|
1031
1107
|
return { success: false, error: err.message };
|
|
1032
1108
|
}
|
|
1033
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Path OpenCode actually loads for global config:
|
|
1112
|
+
* ~/.config/opencode/opencode.jsonc (or .json)
|
|
1113
|
+
* Project: <cwd>/opencode.jsonc (or .json) at project root — not .opencode/.
|
|
1114
|
+
* See https://opencode.ai/docs/config/
|
|
1115
|
+
*/
|
|
1116
|
+
export function openCodeConfigPath(scope, cwd, home = HOME) {
|
|
1117
|
+
if (scope === 'project') {
|
|
1118
|
+
const root = cwd || process.cwd();
|
|
1119
|
+
for (const name of ['opencode.jsonc', 'opencode.json']) {
|
|
1120
|
+
const candidate = path.join(root, name);
|
|
1121
|
+
if (fs.existsSync(candidate))
|
|
1122
|
+
return candidate;
|
|
1123
|
+
}
|
|
1124
|
+
return path.join(root, 'opencode.jsonc');
|
|
1125
|
+
}
|
|
1126
|
+
const globalDir = path.join(home, '.config', 'opencode');
|
|
1127
|
+
for (const name of ['opencode.jsonc', 'opencode.json']) {
|
|
1128
|
+
const candidate = path.join(globalDir, name);
|
|
1129
|
+
if (fs.existsSync(candidate))
|
|
1130
|
+
return candidate;
|
|
1131
|
+
}
|
|
1132
|
+
return path.join(globalDir, 'opencode.jsonc');
|
|
1133
|
+
}
|
|
1034
1134
|
/**
|
|
1035
1135
|
* Apply a permission set to OpenCode's opencode.jsonc.
|
|
1036
1136
|
*/
|
|
1037
1137
|
function applyOpenCodePermissions(set, scope = 'user', cwd, merge = true) {
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
: path.join(cwd || process.cwd(), '.opencode');
|
|
1041
|
-
const configPath = path.join(configDir, 'opencode.jsonc');
|
|
1138
|
+
const configPath = openCodeConfigPath(scope, cwd);
|
|
1139
|
+
const configDir = path.dirname(configPath);
|
|
1042
1140
|
try {
|
|
1043
1141
|
// Ensure directory exists
|
|
1044
1142
|
if (!fs.existsSync(configDir)) {
|
|
@@ -1172,7 +1270,10 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1172
1270
|
return { success: true };
|
|
1173
1271
|
}
|
|
1174
1272
|
if (agentId === 'opencode') {
|
|
1175
|
-
|
|
1273
|
+
// OpenCode loads ~/.config/opencode/opencode.jsonc under the version home
|
|
1274
|
+
// (HOME isolation), not ~/.opencode/opencode.jsonc.
|
|
1275
|
+
const configPath = openCodeConfigPath('user', undefined, versionHome);
|
|
1276
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1176
1277
|
let config = {};
|
|
1177
1278
|
if (fs.existsSync(configPath)) {
|
|
1178
1279
|
const content = stripJsonComments(fs.readFileSync(configPath, 'utf-8'));
|
|
@@ -1231,17 +1332,29 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1231
1332
|
const geminiPerms = convertToGeminiFormat(set);
|
|
1232
1333
|
const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
|
|
1233
1334
|
updateGeminiSettings(settingsPath, (settings) => {
|
|
1234
|
-
// Remove stale
|
|
1335
|
+
// Remove stale keys written by earlier serializer versions:
|
|
1336
|
+
// top-level `permissions`, and Gemini's pre-core/exclude `tools.allowed`.
|
|
1235
1337
|
delete settings.permissions;
|
|
1236
1338
|
const tools = (typeof settings.tools === 'object' && settings.tools !== null && !Array.isArray(settings.tools))
|
|
1237
1339
|
? settings.tools
|
|
1238
1340
|
: {};
|
|
1341
|
+
delete tools.allowed;
|
|
1239
1342
|
if (merge) {
|
|
1240
|
-
const
|
|
1241
|
-
|
|
1343
|
+
const existingCore = Array.isArray(tools.core) ? tools.core : [];
|
|
1344
|
+
const existingExclude = Array.isArray(tools.exclude) ? tools.exclude : [];
|
|
1345
|
+
tools.core = Array.from(new Set([...existingCore, ...geminiPerms.tools.core]));
|
|
1346
|
+
const mergedExclude = Array.from(new Set([...existingExclude, ...(geminiPerms.tools.exclude ?? [])]));
|
|
1347
|
+
if (mergedExclude.length)
|
|
1348
|
+
tools.exclude = mergedExclude;
|
|
1349
|
+
else
|
|
1350
|
+
delete tools.exclude;
|
|
1242
1351
|
}
|
|
1243
1352
|
else {
|
|
1244
|
-
tools.
|
|
1353
|
+
tools.core = geminiPerms.tools.core;
|
|
1354
|
+
if (geminiPerms.tools.exclude?.length)
|
|
1355
|
+
tools.exclude = geminiPerms.tools.exclude;
|
|
1356
|
+
else
|
|
1357
|
+
delete tools.exclude;
|
|
1245
1358
|
}
|
|
1246
1359
|
settings.tools = tools;
|
|
1247
1360
|
});
|
|
@@ -1306,6 +1419,47 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1306
1419
|
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
1307
1420
|
return { success: true };
|
|
1308
1421
|
}
|
|
1422
|
+
if (agentId === 'goose') {
|
|
1423
|
+
const permissionsPath = path.join(versionHome, '.config', 'goose', 'permission.yaml');
|
|
1424
|
+
let config = {};
|
|
1425
|
+
if (fs.existsSync(permissionsPath)) {
|
|
1426
|
+
const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
1427
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1428
|
+
config = parsed;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const converted = convertToGooseFormat(set).user;
|
|
1432
|
+
const user = (typeof config.user === 'object' && config.user !== null && !Array.isArray(config.user))
|
|
1433
|
+
? config.user
|
|
1434
|
+
: {};
|
|
1435
|
+
const currentAlways = Array.isArray(user.always_allow) ? user.always_allow.filter((v) => typeof v === 'string') : [];
|
|
1436
|
+
const currentAsk = Array.isArray(user.ask_before) ? user.ask_before.filter((v) => typeof v === 'string') : [];
|
|
1437
|
+
const currentNever = Array.isArray(user.never_allow) ? user.never_allow.filter((v) => typeof v === 'string') : [];
|
|
1438
|
+
if (merge) {
|
|
1439
|
+
const incomingAlways = new Set(converted.always_allow);
|
|
1440
|
+
const incomingNever = new Set(converted.never_allow);
|
|
1441
|
+
const touched = new Set([...incomingAlways, ...incomingNever]);
|
|
1442
|
+
const always = new Set(currentAlways.filter(tool => !touched.has(tool)));
|
|
1443
|
+
const ask = new Set(currentAsk.filter(tool => !touched.has(tool)));
|
|
1444
|
+
const never = new Set(currentNever.filter(tool => !touched.has(tool)));
|
|
1445
|
+
for (const tool of incomingAlways)
|
|
1446
|
+
always.add(tool);
|
|
1447
|
+
for (const tool of incomingNever)
|
|
1448
|
+
never.add(tool);
|
|
1449
|
+
user.always_allow = Array.from(always).sort();
|
|
1450
|
+
user.ask_before = Array.from(ask).sort();
|
|
1451
|
+
user.never_allow = Array.from(never).sort();
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
user.always_allow = converted.always_allow;
|
|
1455
|
+
user.ask_before = converted.ask_before;
|
|
1456
|
+
user.never_allow = converted.never_allow;
|
|
1457
|
+
}
|
|
1458
|
+
config.user = user;
|
|
1459
|
+
fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
|
|
1460
|
+
fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
|
|
1461
|
+
return { success: true };
|
|
1462
|
+
}
|
|
1309
1463
|
if (agentId === 'kimi') {
|
|
1310
1464
|
const configPath = path.join(versionHome, '.kimi-code', 'config.toml');
|
|
1311
1465
|
let config = {};
|
|
@@ -1364,6 +1518,27 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1364
1518
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
1365
1519
|
return { success: true };
|
|
1366
1520
|
}
|
|
1521
|
+
if (agentId === 'droid') {
|
|
1522
|
+
const configPath = path.join(versionHome, '.factory', 'settings.json');
|
|
1523
|
+
let config = {};
|
|
1524
|
+
if (fs.existsSync(configPath)) {
|
|
1525
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1526
|
+
}
|
|
1527
|
+
const converted = convertToDroidFormat(set);
|
|
1528
|
+
if (merge) {
|
|
1529
|
+
const existingAllow = Array.isArray(config.commandAllowlist) ? config.commandAllowlist : [];
|
|
1530
|
+
const existingDeny = Array.isArray(config.commandDenylist) ? config.commandDenylist : [];
|
|
1531
|
+
config.commandAllowlist = Array.from(new Set([...existingAllow, ...converted.commandAllowlist]));
|
|
1532
|
+
config.commandDenylist = Array.from(new Set([...existingDeny, ...converted.commandDenylist]));
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
config.commandAllowlist = converted.commandAllowlist;
|
|
1536
|
+
config.commandDenylist = converted.commandDenylist;
|
|
1537
|
+
}
|
|
1538
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1539
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
1540
|
+
return { success: true };
|
|
1541
|
+
}
|
|
1367
1542
|
if (agentId === 'kiro') {
|
|
1368
1543
|
const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
|
|
1369
1544
|
let config = {};
|
|
@@ -1496,7 +1671,7 @@ export function exportPermissionsFromPath(filePath) {
|
|
|
1496
1671
|
if (fileName === 'settings.json' && parentDir === '.claude') {
|
|
1497
1672
|
agentId = 'claude';
|
|
1498
1673
|
}
|
|
1499
|
-
else if (fileName === 'opencode.jsonc' || parentDir === '.opencode') {
|
|
1674
|
+
else if (fileName === 'opencode.jsonc' || fileName === 'opencode.json' || parentDir === 'opencode' || parentDir === '.opencode') {
|
|
1500
1675
|
agentId = 'opencode';
|
|
1501
1676
|
}
|
|
1502
1677
|
else if (fileName === 'config.toml' && parentDir === '.codex') {
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -191,6 +191,14 @@ export declare function resolveOpenCodePluginSources(pluginRoot: string): string
|
|
|
191
191
|
export declare function installOpenCodePlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
|
|
192
192
|
export declare function isOpenCodePluginInstalled(pluginName: string, versionHome: string): boolean;
|
|
193
193
|
export declare function removeOpenCodePlugin(pluginName: string, versionHome: string): boolean;
|
|
194
|
+
/**
|
|
195
|
+
* Gemini CLI extensions live under `$HOME/.gemini/extensions/<name>/` and
|
|
196
|
+
* require a `gemini-extension.json` manifest at the extension root.
|
|
197
|
+
*/
|
|
198
|
+
export declare function geminiExtensionsDir(versionHome: string): string;
|
|
199
|
+
export declare function installGeminiPlugin(plugin: DiscoveredPlugin, versionHome: string): boolean;
|
|
200
|
+
export declare function isGeminiPluginInstalled(pluginName: string, versionHome: string): boolean;
|
|
201
|
+
export declare function removeGeminiPlugin(pluginName: string, versionHome: string): boolean;
|
|
194
202
|
/**
|
|
195
203
|
* Goose auto-discovers Open Plugins at `$HOME/.agents/plugins/<name>/`.
|
|
196
204
|
* Under agents-cli version isolation HOME is the version home, so we install to:
|
package/dist/lib/plugins.js
CHANGED
|
@@ -21,6 +21,7 @@ import { shouldInstallCommandAsSkill, installCommandSkillToVersion } from './com
|
|
|
21
21
|
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, registerDroidInstalledPlugin, unregisterDroidInstalledPlugin, isDroidPluginInstalled, registerCopilotInstalledPlugin, unregisterCopilotInstalledPlugin, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
|
|
22
22
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
23
23
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
24
|
+
const GEMINI_EXTENSION_MANIFEST_FILE = 'gemini-extension.json';
|
|
24
25
|
const USER_CONFIG_FILE = '.user-config.json';
|
|
25
26
|
const SOURCE_FILE = '.source';
|
|
26
27
|
export const PLUGIN_EXEC_SURFACE_LABELS = {
|
|
@@ -502,6 +503,27 @@ export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
|
|
|
502
503
|
result.skills.push(plugin.name);
|
|
503
504
|
return result;
|
|
504
505
|
}
|
|
506
|
+
// Gemini CLI loads extensions from $HOME/.gemini/extensions/<name>/.
|
|
507
|
+
// Copy the plugin bundle as an extension and synthesize gemini-extension.json.
|
|
508
|
+
if (agent === 'gemini') {
|
|
509
|
+
const enablePlugin = options.allowExecSurfaces === true || !hasPluginExecSurfaces(inspectPluginCapabilities(plugin.root));
|
|
510
|
+
if (!enablePlugin) {
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
const ok = installGeminiPlugin(plugin, versionHome);
|
|
514
|
+
result.success = ok;
|
|
515
|
+
if (ok) {
|
|
516
|
+
result.skills = plugin.skills.map(s => `${plugin.name}:${s}`);
|
|
517
|
+
result.commands = plugin.commands.map(c => `${plugin.name}:${c}`);
|
|
518
|
+
result.agentDefs = plugin.agentDefs.map(a => `${plugin.name}:${a}`);
|
|
519
|
+
result.bin = plugin.bin;
|
|
520
|
+
result.hooks = plugin.hooks;
|
|
521
|
+
result.mcp = plugin.hasMcp;
|
|
522
|
+
result.settings = plugin.hasSettings;
|
|
523
|
+
result.permissions = pluginHasPermissions(plugin);
|
|
524
|
+
}
|
|
525
|
+
return result;
|
|
526
|
+
}
|
|
505
527
|
// Goose loads Open Plugins from $HOME/.agents/plugins/<name>/ (same layout as
|
|
506
528
|
// agents-cli's source tree). Under the shim HOME is the version home.
|
|
507
529
|
if (agent === 'goose') {
|
|
@@ -901,6 +923,82 @@ export function removeOpenCodePlugin(pluginName, versionHome) {
|
|
|
901
923
|
}
|
|
902
924
|
return removed;
|
|
903
925
|
}
|
|
926
|
+
// ─── Gemini extensions ───────────────────────────────────────────────────────
|
|
927
|
+
/**
|
|
928
|
+
* Gemini CLI extensions live under `$HOME/.gemini/extensions/<name>/` and
|
|
929
|
+
* require a `gemini-extension.json` manifest at the extension root.
|
|
930
|
+
*/
|
|
931
|
+
export function geminiExtensionsDir(versionHome) {
|
|
932
|
+
return path.join(versionHome, '.gemini', 'extensions');
|
|
933
|
+
}
|
|
934
|
+
function readPluginMcpConfigForGemini(pluginRoot) {
|
|
935
|
+
const mcpPath = path.join(pluginRoot, '.mcp.json');
|
|
936
|
+
if (!fs.existsSync(mcpPath))
|
|
937
|
+
return undefined;
|
|
938
|
+
try {
|
|
939
|
+
const parsed = JSON.parse(fs.readFileSync(mcpPath, 'utf-8'));
|
|
940
|
+
if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object')
|
|
941
|
+
return undefined;
|
|
942
|
+
return rewriteGeminiExtensionVars(parsed.mcpServers);
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
return undefined;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function rewriteGeminiExtensionVars(value) {
|
|
949
|
+
if (typeof value === 'string') {
|
|
950
|
+
return value
|
|
951
|
+
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, '${extensionPath}')
|
|
952
|
+
.replace(/\$\{CLAUDE_PLUGIN_DATA\}/g, '${extensionPath}/.data');
|
|
953
|
+
}
|
|
954
|
+
if (Array.isArray(value))
|
|
955
|
+
return value.map(rewriteGeminiExtensionVars);
|
|
956
|
+
if (value && typeof value === 'object') {
|
|
957
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewriteGeminiExtensionVars(item)]));
|
|
958
|
+
}
|
|
959
|
+
return value;
|
|
960
|
+
}
|
|
961
|
+
function writeGeminiExtensionManifest(plugin, destRoot) {
|
|
962
|
+
const manifest = {
|
|
963
|
+
name: plugin.manifest.name,
|
|
964
|
+
version: plugin.manifest.version,
|
|
965
|
+
description: plugin.manifest.description,
|
|
966
|
+
};
|
|
967
|
+
const mcpServers = readPluginMcpConfigForGemini(destRoot);
|
|
968
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
969
|
+
manifest.mcpServers = mcpServers;
|
|
970
|
+
}
|
|
971
|
+
fs.writeFileSync(path.join(destRoot, GEMINI_EXTENSION_MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
|
|
972
|
+
}
|
|
973
|
+
export function installGeminiPlugin(plugin, versionHome) {
|
|
974
|
+
const destRoot = path.join(geminiExtensionsDir(versionHome), plugin.name);
|
|
975
|
+
try {
|
|
976
|
+
if (fs.existsSync(destRoot)) {
|
|
977
|
+
fs.rmSync(destRoot, { recursive: true, force: true });
|
|
978
|
+
}
|
|
979
|
+
fs.cpSync(plugin.root, destRoot, { recursive: true });
|
|
980
|
+
const userConfig = loadUserConfig(plugin.name);
|
|
981
|
+
if (Object.keys(userConfig).length > 0) {
|
|
982
|
+
expandUserConfigInDir(destRoot, userConfig);
|
|
983
|
+
}
|
|
984
|
+
writeGeminiExtensionManifest(plugin, destRoot);
|
|
985
|
+
fs.writeFileSync(path.join(destRoot, '.agents-cli-managed'), `plugin=${plugin.name}\n`, 'utf-8');
|
|
986
|
+
return true;
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
export function isGeminiPluginInstalled(pluginName, versionHome) {
|
|
993
|
+
return fs.existsSync(path.join(geminiExtensionsDir(versionHome), pluginName, GEMINI_EXTENSION_MANIFEST_FILE));
|
|
994
|
+
}
|
|
995
|
+
export function removeGeminiPlugin(pluginName, versionHome) {
|
|
996
|
+
const destRoot = path.join(geminiExtensionsDir(versionHome), pluginName);
|
|
997
|
+
if (!fs.existsSync(destRoot))
|
|
998
|
+
return false;
|
|
999
|
+
fs.rmSync(destRoot, { recursive: true, force: true });
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
904
1002
|
// ─── Goose plugins (Open Plugins under .agents/plugins/) ─────────────────────
|
|
905
1003
|
/**
|
|
906
1004
|
* Goose auto-discovers Open Plugins at `$HOME/.agents/plugins/<name>/`.
|
|
@@ -949,6 +1047,9 @@ export function isPluginSynced(plugin, agent, versionHome) {
|
|
|
949
1047
|
if (agent === 'opencode') {
|
|
950
1048
|
return isOpenCodePluginInstalled(plugin.name, versionHome);
|
|
951
1049
|
}
|
|
1050
|
+
if (agent === 'gemini') {
|
|
1051
|
+
return isGeminiPluginInstalled(plugin.name, versionHome);
|
|
1052
|
+
}
|
|
952
1053
|
if (agent === 'goose') {
|
|
953
1054
|
return isGoosePluginInstalled(plugin.name, versionHome);
|
|
954
1055
|
}
|
|
@@ -986,6 +1087,13 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
986
1087
|
}
|
|
987
1088
|
return result;
|
|
988
1089
|
}
|
|
1090
|
+
// Gemini: remove extension directory from ~/.gemini/extensions/.
|
|
1091
|
+
if (agent === 'gemini') {
|
|
1092
|
+
if (removeGeminiPlugin(pluginName, versionHome)) {
|
|
1093
|
+
result.skills.push(pluginName);
|
|
1094
|
+
}
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|
|
989
1097
|
// Goose: remove Open Plugin directory from versionHome/.agents/plugins/.
|
|
990
1098
|
if (agent === 'goose') {
|
|
991
1099
|
if (removeGoosePlugin(pluginName, versionHome)) {
|
package/dist/lib/project-root.js
CHANGED
|
@@ -16,6 +16,7 @@ import * as path from 'path';
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import { readMeta, updateMeta } from './state.js';
|
|
18
18
|
import { getMainRepoRoot } from './git.js';
|
|
19
|
+
import { toPosix } from './platform/index.js';
|
|
19
20
|
const HOME = process.env.HOME ?? os.homedir();
|
|
20
21
|
/** Rewrite an absolute path under the local home to a `~/`-relative string; pass others through. */
|
|
21
22
|
export function toHomeRelative(abs) {
|
|
@@ -23,7 +24,7 @@ export function toHomeRelative(abs) {
|
|
|
23
24
|
if (rel === '')
|
|
24
25
|
return '~';
|
|
25
26
|
if (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
26
|
-
return `~/${rel}`;
|
|
27
|
+
return `~/${toPosix(rel)}`;
|
|
27
28
|
return abs;
|
|
28
29
|
}
|
|
29
30
|
/** Expand a leading `~`/`$HOME` against the LOCAL home. Other paths pass through unchanged. */
|
|
@@ -111,7 +111,7 @@ export function getMcpConfigPath(agent, versionHome) {
|
|
|
111
111
|
case 'codex':
|
|
112
112
|
return path.join(versionHome, '.codex', 'config.toml');
|
|
113
113
|
case 'opencode':
|
|
114
|
-
return path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
114
|
+
return path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
|
|
115
115
|
case 'cursor':
|
|
116
116
|
return path.join(versionHome, '.cursor', 'mcp.json');
|
|
117
117
|
case 'gemini':
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Permissions are stored as YAML files in permissions/ directories at each layer.
|
|
5
5
|
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
6
|
* Unlike other resources, permissions merge into agent-specific config files
|
|
7
|
-
* (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
|
|
7
|
+
* (Claude/Gemini: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
|
|
8
8
|
*/
|
|
9
9
|
import type { ResourceHandler } from './types.js';
|
|
10
10
|
import type { PermissionSet } from '../types.js';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Permissions are stored as YAML files in permissions/ directories at each layer.
|
|
5
5
|
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
6
|
* Unlike other resources, permissions merge into agent-specific config files
|
|
7
|
-
* (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
|
|
7
|
+
* (Claude/Gemini: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
@@ -65,12 +65,18 @@ function getAgentConfigPath(agent, versionHome) {
|
|
|
65
65
|
return path.join(versionHome, '.claude', 'settings.json');
|
|
66
66
|
case 'codex':
|
|
67
67
|
return path.join(versionHome, '.codex', 'config.toml');
|
|
68
|
+
case 'gemini':
|
|
69
|
+
return path.join(versionHome, '.gemini', 'settings.json');
|
|
68
70
|
case 'opencode':
|
|
69
|
-
return path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
71
|
+
return path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
|
|
70
72
|
case 'kimi':
|
|
71
73
|
return path.join(versionHome, '.kimi-code', 'config.toml');
|
|
74
|
+
case 'droid':
|
|
75
|
+
return path.join(versionHome, '.factory', 'settings.json');
|
|
72
76
|
case 'kiro':
|
|
73
77
|
return path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
|
|
78
|
+
case 'goose':
|
|
79
|
+
return path.join(versionHome, '.config', 'goose', 'permission.yaml');
|
|
74
80
|
default:
|
|
75
81
|
return null;
|
|
76
82
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Format is the same for all agents. Resolution order: project > user > system.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
|
-
import { agentConfigDirName } from '../agents.js';
|
|
8
|
+
import { AGENTS, agentConfigDirName } from '../agents.js';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as yaml from 'yaml';
|
|
11
11
|
import { getSystemSkillsDir, getUserSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
@@ -201,6 +201,11 @@ export function createSkillsHandler(provider = defaultProvider) {
|
|
|
201
201
|
return null;
|
|
202
202
|
},
|
|
203
203
|
sync(agent, versionHome, cwd) {
|
|
204
|
+
// Agents that read directly from central ~/.agents/skills/ (e.g. Gemini,
|
|
205
|
+
// Goose via the Summon extension) should not get a per-version copy.
|
|
206
|
+
if (AGENTS[agent]?.nativeAgentsSkillsDir) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
204
209
|
const targetDir = path.join(versionHome, agentConfigDirName(agent), 'skills');
|
|
205
210
|
// Ensure target directory exists
|
|
206
211
|
if (!fs.existsSync(targetDir)) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Union: All resources from all layers are combined
|
|
6
6
|
* - Override on name conflict: Higher layer wins (project > user > system)
|
|
7
7
|
*/
|
|
8
|
-
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'kiro' | 'antigravity' | 'grok' | 'kimi' | 'hermes' | 'forge';
|
|
8
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'forge';
|
|
9
9
|
export type Layer = 'system' | 'user' | 'project';
|
|
10
10
|
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
|
|
11
11
|
/** A resolved resource with its origin layer. */
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -138,6 +138,22 @@ export declare function readJob(name: string, cwd?: string): JobConfig | null;
|
|
|
138
138
|
* name, the write fails explicitly so we never choose or drop a sibling.
|
|
139
139
|
*/
|
|
140
140
|
export declare function writeJob(config: JobConfig): void;
|
|
141
|
+
/**
|
|
142
|
+
* Serialize a job config, preserving the on-disk formatting of an existing file.
|
|
143
|
+
*
|
|
144
|
+
* A full `yaml.stringify(config)` re-emits the whole document — restyling every
|
|
145
|
+
* scalar (unquoting `schedule`, re-wrapping the folded `prompt` block, reordering
|
|
146
|
+
* keys). When a routine is only being toggled (pause/resume) or re-pinned
|
|
147
|
+
* (`devices --set`), that rewrites the entire file, leaving the git-backed
|
|
148
|
+
* `~/.agents` tree perpetually dirty so `agents repo pull` refuses to sync across
|
|
149
|
+
* the fleet. To keep the diff to the field that actually changed, we edit the
|
|
150
|
+
* existing document in place and only re-render touched nodes; untouched nodes
|
|
151
|
+
* (notably the large `prompt` block) keep their byte-for-byte formatting.
|
|
152
|
+
*
|
|
153
|
+
* `existingText` is the current file contents, or null for a new file. New,
|
|
154
|
+
* unparseable, and non-mapping documents fall back to canonical `yaml.stringify`.
|
|
155
|
+
*/
|
|
156
|
+
export declare function serializeJob(output: Record<string, unknown>, existingText: string | null): string;
|
|
141
157
|
/** Delete a job config file by name. Returns true if the file existed. */
|
|
142
158
|
export declare function deleteJob(name: string): boolean;
|
|
143
159
|
/** Enable or disable a job by name. */
|