@phnx-labs/agents-cli 1.20.57 → 1.20.59
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 +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/defaults.js +24 -0
- package/dist/commands/exec.js +29 -5
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +34 -25
- package/dist/commands/versions.js +11 -3
- package/dist/commands/view.js +19 -4
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +42 -14
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +65 -17
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/hosts/dispatch.d.ts +21 -0
- package/dist/lib/hosts/dispatch.js +88 -5
- 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 +31 -1
- package/dist/lib/permissions.js +210 -9
- package/dist/lib/project-root.d.ts +65 -0
- package/dist/lib/project-root.js +134 -0
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/agent.d.ts +26 -23
- package/dist/lib/secrets/agent.js +196 -216
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +12 -10
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +1 -0
- 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/parse.js +38 -15
- package/dist/lib/session/state.d.ts +4 -1
- package/dist/lib/session/state.js +18 -1
- package/dist/lib/session/types.d.ts +10 -0
- package/dist/lib/staleness/detectors/permissions.js +64 -1
- package/dist/lib/staleness/detectors/subagents.js +30 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -1
- 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 +23 -0
- package/dist/lib/subagents.js +161 -12
- package/dist/lib/teams/agents.d.ts +14 -0
- package/dist/lib/teams/agents.js +158 -22
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/versions.d.ts +39 -0
- package/dist/lib/versions.js +199 -12
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -11
package/dist/lib/permissions.js
CHANGED
|
@@ -440,6 +440,37 @@ export function convertToClaudeFormat(set) {
|
|
|
440
440
|
}
|
|
441
441
|
return { permissions };
|
|
442
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Map a canonical rule to Cursor CLI syntax.
|
|
445
|
+
* Cursor uses Shell(...) instead of Bash(...); other tools keep TitleCase names.
|
|
446
|
+
* https://cursor.com/docs/cli/reference/permissions
|
|
447
|
+
*/
|
|
448
|
+
function canonicalToCursorRule(perm) {
|
|
449
|
+
if (perm === 'Bash' || perm.startsWith('Bash(')) {
|
|
450
|
+
return perm.replace(/^Bash/, 'Shell');
|
|
451
|
+
}
|
|
452
|
+
// Canonical WebSearch maps to WebFetch family for network allow.
|
|
453
|
+
if (perm.startsWith('WebSearch(')) {
|
|
454
|
+
return perm.replace(/^WebSearch/, 'WebFetch');
|
|
455
|
+
}
|
|
456
|
+
// Cursor has no Edit prefix — file writes use Write(...).
|
|
457
|
+
if (perm === 'Edit' || perm.startsWith('Edit(')) {
|
|
458
|
+
return perm.replace(/^Edit/, 'Write');
|
|
459
|
+
}
|
|
460
|
+
return perm;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Convert canonical permission set to Cursor CLI format
|
|
464
|
+
* (`~/.cursor/cli-config.json` permissions.allow/deny).
|
|
465
|
+
*/
|
|
466
|
+
export function convertToCursorFormat(set) {
|
|
467
|
+
return {
|
|
468
|
+
permissions: {
|
|
469
|
+
allow: set.allow.map(canonicalToCursorRule),
|
|
470
|
+
deny: (set.deny ?? []).map(canonicalToCursorRule),
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
}
|
|
443
474
|
/**
|
|
444
475
|
* Parse canonical permission pattern to extract tool and pattern.
|
|
445
476
|
* "Bash(git *)" -> { tool: "bash", pattern: "git *" }
|
|
@@ -494,6 +525,26 @@ function normalizeBashPattern(pattern) {
|
|
|
494
525
|
return pattern.slice(0, -2) + ' *';
|
|
495
526
|
return pattern;
|
|
496
527
|
}
|
|
528
|
+
/** Convert canonical Bash rules into Droid's command arrays. */
|
|
529
|
+
export function convertToDroidFormat(set) {
|
|
530
|
+
const commands = (permissions) => {
|
|
531
|
+
const result = new Set();
|
|
532
|
+
for (const permission of permissions) {
|
|
533
|
+
if (BLANKET_BASH_FORMS.has(permission)) {
|
|
534
|
+
result.add('*');
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const parsed = parseCanonicalPattern(permission);
|
|
538
|
+
if (parsed?.tool === 'bash')
|
|
539
|
+
result.add(normalizeBashPattern(parsed.pattern));
|
|
540
|
+
}
|
|
541
|
+
return Array.from(result);
|
|
542
|
+
};
|
|
543
|
+
return {
|
|
544
|
+
commandAllowlist: commands(set.allow),
|
|
545
|
+
commandDenylist: commands(set.deny ?? []),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
497
548
|
/**
|
|
498
549
|
* Convert canonical permission set to Antigravity format.
|
|
499
550
|
* Antigravity reads ~/.gemini/antigravity-cli/settings.json with
|
|
@@ -592,6 +643,57 @@ function canonicalToGrokRule(perm, action) {
|
|
|
592
643
|
}
|
|
593
644
|
return { action, tool, pattern };
|
|
594
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* Convert canonical permissions to Kiro CLI v3 capability rules.
|
|
648
|
+
* Kiro stores these rules in ~/.kiro/settings/permissions.yaml.
|
|
649
|
+
*/
|
|
650
|
+
export function convertToKiroFormat(set) {
|
|
651
|
+
const rules = [];
|
|
652
|
+
for (const perm of set.allow) {
|
|
653
|
+
const rule = canonicalToKiroRule(perm, 'allow');
|
|
654
|
+
if (rule)
|
|
655
|
+
rules.push(rule);
|
|
656
|
+
}
|
|
657
|
+
for (const perm of set.deny ?? []) {
|
|
658
|
+
const rule = canonicalToKiroRule(perm, 'deny');
|
|
659
|
+
if (rule)
|
|
660
|
+
rules.push(rule);
|
|
661
|
+
}
|
|
662
|
+
return { rules };
|
|
663
|
+
}
|
|
664
|
+
function canonicalToKiroRule(perm, effect) {
|
|
665
|
+
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
666
|
+
return { capability: 'shell', effect };
|
|
667
|
+
}
|
|
668
|
+
const parsed = parseCanonicalPreserveCase(perm);
|
|
669
|
+
const capability = KIRO_CAPABILITY_BY_TOOL[parsed.tool.toLowerCase()];
|
|
670
|
+
if (!capability)
|
|
671
|
+
return null;
|
|
672
|
+
if (parsed.pattern === null || parsed.pattern === '*' || parsed.pattern === '**') {
|
|
673
|
+
return { capability, effect };
|
|
674
|
+
}
|
|
675
|
+
const lowerTool = parsed.tool.toLowerCase();
|
|
676
|
+
const pattern = lowerTool === 'bash'
|
|
677
|
+
? normalizeBashPattern(parsed.pattern)
|
|
678
|
+
: (lowerTool === 'webfetch' || lowerTool === 'websearch') && parsed.pattern.startsWith('domain:')
|
|
679
|
+
? parsed.pattern.slice('domain:'.length)
|
|
680
|
+
: parsed.pattern;
|
|
681
|
+
return { capability, effect, match: [pattern] };
|
|
682
|
+
}
|
|
683
|
+
const KIRO_CAPABILITY_BY_TOOL = {
|
|
684
|
+
bash: 'shell',
|
|
685
|
+
read: 'fs_read',
|
|
686
|
+
grep: 'fs_read',
|
|
687
|
+
glob: 'fs_read',
|
|
688
|
+
write: 'fs_write',
|
|
689
|
+
edit: 'fs_write',
|
|
690
|
+
notebookedit: 'fs_write',
|
|
691
|
+
webfetch: 'web_fetch',
|
|
692
|
+
websearch: 'web_search',
|
|
693
|
+
mcp: 'mcp',
|
|
694
|
+
subagent: 'subagent',
|
|
695
|
+
skill: 'skill',
|
|
696
|
+
};
|
|
595
697
|
/**
|
|
596
698
|
* Parse a canonical permission string preserving the tool's original casing.
|
|
597
699
|
* `parseCanonicalPattern` lowercases the tool name, which is fine for Grok
|
|
@@ -837,9 +939,7 @@ function readClaudePermissions(scope = 'user', cwd, options) {
|
|
|
837
939
|
*/
|
|
838
940
|
function readOpenCodePermissions(scope = 'user', cwd, options) {
|
|
839
941
|
const home = options?.home || HOME;
|
|
840
|
-
const configPath = scope
|
|
841
|
-
? path.join(home, '.opencode', 'opencode.jsonc')
|
|
842
|
-
: path.join(cwd || process.cwd(), '.opencode', 'opencode.jsonc');
|
|
942
|
+
const configPath = openCodeConfigPath(scope, cwd, home);
|
|
843
943
|
if (!fs.existsSync(configPath)) {
|
|
844
944
|
return null;
|
|
845
945
|
}
|
|
@@ -949,14 +1049,36 @@ export function applyClaudePermissions(set, scope = 'user', cwd, merge = true) {
|
|
|
949
1049
|
return { success: false, error: err.message };
|
|
950
1050
|
}
|
|
951
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Path OpenCode actually loads for global config:
|
|
1054
|
+
* ~/.config/opencode/opencode.jsonc (or .json)
|
|
1055
|
+
* Project: <cwd>/opencode.jsonc (or .json) at project root — not .opencode/.
|
|
1056
|
+
* See https://opencode.ai/docs/config/
|
|
1057
|
+
*/
|
|
1058
|
+
export function openCodeConfigPath(scope, cwd, home = HOME) {
|
|
1059
|
+
if (scope === 'project') {
|
|
1060
|
+
const root = cwd || process.cwd();
|
|
1061
|
+
for (const name of ['opencode.jsonc', 'opencode.json']) {
|
|
1062
|
+
const candidate = path.join(root, name);
|
|
1063
|
+
if (fs.existsSync(candidate))
|
|
1064
|
+
return candidate;
|
|
1065
|
+
}
|
|
1066
|
+
return path.join(root, 'opencode.jsonc');
|
|
1067
|
+
}
|
|
1068
|
+
const globalDir = path.join(home, '.config', 'opencode');
|
|
1069
|
+
for (const name of ['opencode.jsonc', 'opencode.json']) {
|
|
1070
|
+
const candidate = path.join(globalDir, name);
|
|
1071
|
+
if (fs.existsSync(candidate))
|
|
1072
|
+
return candidate;
|
|
1073
|
+
}
|
|
1074
|
+
return path.join(globalDir, 'opencode.jsonc');
|
|
1075
|
+
}
|
|
952
1076
|
/**
|
|
953
1077
|
* Apply a permission set to OpenCode's opencode.jsonc.
|
|
954
1078
|
*/
|
|
955
1079
|
function applyOpenCodePermissions(set, scope = 'user', cwd, merge = true) {
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
: path.join(cwd || process.cwd(), '.opencode');
|
|
959
|
-
const configPath = path.join(configDir, 'opencode.jsonc');
|
|
1080
|
+
const configPath = openCodeConfigPath(scope, cwd);
|
|
1081
|
+
const configDir = path.dirname(configPath);
|
|
960
1082
|
try {
|
|
961
1083
|
// Ensure directory exists
|
|
962
1084
|
if (!fs.existsSync(configDir)) {
|
|
@@ -1090,7 +1212,10 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1090
1212
|
return { success: true };
|
|
1091
1213
|
}
|
|
1092
1214
|
if (agentId === 'opencode') {
|
|
1093
|
-
|
|
1215
|
+
// OpenCode loads ~/.config/opencode/opencode.jsonc under the version home
|
|
1216
|
+
// (HOME isolation), not ~/.opencode/opencode.jsonc.
|
|
1217
|
+
const configPath = openCodeConfigPath('user', undefined, versionHome);
|
|
1218
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1094
1219
|
let config = {};
|
|
1095
1220
|
if (fs.existsSync(configPath)) {
|
|
1096
1221
|
const content = stripJsonComments(fs.readFileSync(configPath, 'utf-8'));
|
|
@@ -1258,6 +1383,82 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1258
1383
|
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
1259
1384
|
return { success: true };
|
|
1260
1385
|
}
|
|
1386
|
+
if (agentId === 'cursor') {
|
|
1387
|
+
// Cursor CLI permissions live in ~/.cursor/cli-config.json
|
|
1388
|
+
const configPath = path.join(configDir, 'cli-config.json');
|
|
1389
|
+
let config = {};
|
|
1390
|
+
if (fs.existsSync(configPath)) {
|
|
1391
|
+
try {
|
|
1392
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1393
|
+
}
|
|
1394
|
+
catch { /* start fresh */ }
|
|
1395
|
+
}
|
|
1396
|
+
const converted = convertToCursorFormat(set);
|
|
1397
|
+
if (merge && config.permissions && typeof config.permissions === 'object') {
|
|
1398
|
+
const existing = config.permissions;
|
|
1399
|
+
const allow = new Set([...(existing.allow || []), ...converted.permissions.allow]);
|
|
1400
|
+
const deny = new Set([...(existing.deny || []), ...converted.permissions.deny]);
|
|
1401
|
+
config.permissions = { allow: [...allow], deny: [...deny] };
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
config.permissions = converted.permissions;
|
|
1405
|
+
}
|
|
1406
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1407
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
1408
|
+
return { success: true };
|
|
1409
|
+
}
|
|
1410
|
+
if (agentId === 'droid') {
|
|
1411
|
+
const configPath = path.join(versionHome, '.factory', 'settings.json');
|
|
1412
|
+
let config = {};
|
|
1413
|
+
if (fs.existsSync(configPath)) {
|
|
1414
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1415
|
+
}
|
|
1416
|
+
const converted = convertToDroidFormat(set);
|
|
1417
|
+
if (merge) {
|
|
1418
|
+
const existingAllow = Array.isArray(config.commandAllowlist) ? config.commandAllowlist : [];
|
|
1419
|
+
const existingDeny = Array.isArray(config.commandDenylist) ? config.commandDenylist : [];
|
|
1420
|
+
config.commandAllowlist = Array.from(new Set([...existingAllow, ...converted.commandAllowlist]));
|
|
1421
|
+
config.commandDenylist = Array.from(new Set([...existingDeny, ...converted.commandDenylist]));
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
config.commandAllowlist = converted.commandAllowlist;
|
|
1425
|
+
config.commandDenylist = converted.commandDenylist;
|
|
1426
|
+
}
|
|
1427
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1428
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
1429
|
+
return { success: true };
|
|
1430
|
+
}
|
|
1431
|
+
if (agentId === 'kiro') {
|
|
1432
|
+
const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
|
|
1433
|
+
let config = {};
|
|
1434
|
+
if (fs.existsSync(permissionsPath)) {
|
|
1435
|
+
const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
1436
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1437
|
+
config = parsed;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const newRules = convertToKiroFormat(set).rules;
|
|
1441
|
+
if (merge) {
|
|
1442
|
+
const existingRules = Array.isArray(config.rules) ? config.rules : [];
|
|
1443
|
+
const seen = new Set();
|
|
1444
|
+
config.rules = [...existingRules, ...newRules].filter((rule) => {
|
|
1445
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule))
|
|
1446
|
+
return false;
|
|
1447
|
+
const record = rule;
|
|
1448
|
+
const key = `${String(record.effect)}|${String(record.capability)}|${JSON.stringify(record.match ?? [])}|${JSON.stringify(record.exclude ?? [])}`;
|
|
1449
|
+
if (seen.has(key))
|
|
1450
|
+
return false;
|
|
1451
|
+
seen.add(key);
|
|
1452
|
+
return true;
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
config.rules = newRules;
|
|
1457
|
+
}
|
|
1458
|
+
fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
|
|
1459
|
+
fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
|
|
1460
|
+
return { success: true };
|
|
1461
|
+
}
|
|
1261
1462
|
return { success: false, error: `Agent '${agentId}' does not support permissions` };
|
|
1262
1463
|
}
|
|
1263
1464
|
catch (err) {
|
|
@@ -1359,7 +1560,7 @@ export function exportPermissionsFromPath(filePath) {
|
|
|
1359
1560
|
if (fileName === 'settings.json' && parentDir === '.claude') {
|
|
1360
1561
|
agentId = 'claude';
|
|
1361
1562
|
}
|
|
1362
|
-
else if (fileName === 'opencode.jsonc' || parentDir === '.opencode') {
|
|
1563
|
+
else if (fileName === 'opencode.jsonc' || fileName === 'opencode.json' || parentDir === 'opencode' || parentDir === '.opencode') {
|
|
1363
1564
|
agentId = 'opencode';
|
|
1364
1565
|
}
|
|
1365
1566
|
else if (fileName === 'config.toml' && parentDir === '.codex') {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects-root resolution for the `agents run --project <slug>` shorthand.
|
|
3
|
+
*
|
|
4
|
+
* Projects follow a predictable layout — `<root>/<repo>` (e.g.
|
|
5
|
+
* `~/src/github.com/<user>/<repo>`), with git worktrees under
|
|
6
|
+
* `<repo>/.agents/worktrees/<slug>`. The root is auto-inferred from the repo you
|
|
7
|
+
* launch inside (the directory ABOVE the git root) and cached in `agents.yaml`
|
|
8
|
+
* so later runs resolve a bare slug from anywhere. It is stored home-relative
|
|
9
|
+
* (`~/…`) when it sits under `$HOME`, so the SAME value resolves on a remote
|
|
10
|
+
* host whose home differs (`/home/<user>` vs `/Users/<user>`): a `--host` run
|
|
11
|
+
* keeps the `~` and lets the remote login shell expand it (see `remoteCdPrefix`
|
|
12
|
+
* in `hosts/dispatch.ts`), while a local run expands `~` against the local home.
|
|
13
|
+
*/
|
|
14
|
+
/** Rewrite an absolute path under the local home to a `~/`-relative string; pass others through. */
|
|
15
|
+
export declare function toHomeRelative(abs: string): string;
|
|
16
|
+
/** Expand a leading `~`/`$HOME` against the LOCAL home. Other paths pass through unchanged. */
|
|
17
|
+
export declare function expandLocalHome(p: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Make a `--cwd`/`--project` value portable to a remote host: an absolute path
|
|
20
|
+
* under the LOCAL home (which the local shell already expanded from `~`) becomes
|
|
21
|
+
* `~/…` so the *remote* shell re-roots it at its own home. Paths already anchored
|
|
22
|
+
* at `~`/`$HOME` pass through; other absolute or relative paths are left as-is
|
|
23
|
+
* (used verbatim on the host). Explicit `--remote-cwd` is NOT run through this —
|
|
24
|
+
* it is a literal remote path by contract.
|
|
25
|
+
*/
|
|
26
|
+
export declare function toRemotePortable(p: string): string;
|
|
27
|
+
/** The configured projects root (home-relative or absolute), or undefined when unset. */
|
|
28
|
+
export declare function getProjectRoot(): string | undefined;
|
|
29
|
+
/** Set (override) the cached projects root. Stored home-relative when under `$HOME`. */
|
|
30
|
+
export declare function setProjectRoot(rootPath: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* Infer the projects root from `cwd`: the directory ABOVE the git repo root
|
|
33
|
+
* (cwd inside `~/src/github.com/user/repo` → `~/src/github.com/user`). Returns a
|
|
34
|
+
* home-relative string when under `$HOME`; undefined when `cwd` is not in a repo.
|
|
35
|
+
*/
|
|
36
|
+
export declare function inferProjectRoot(cwd: string): Promise<string | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the projects root, auto-inferring and caching on first use. Throws an
|
|
39
|
+
* actionable error when it is neither configured nor inferrable from `cwd`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ensureProjectRoot(cwd: string): Promise<string>;
|
|
42
|
+
export interface ProjectRef {
|
|
43
|
+
slug: string;
|
|
44
|
+
worktree?: string;
|
|
45
|
+
}
|
|
46
|
+
/** Parse a `--project` value of the form `<slug>[@<worktree>]`. */
|
|
47
|
+
export declare function parseProjectRef(ref: string): ProjectRef;
|
|
48
|
+
/**
|
|
49
|
+
* Join a root + `--project` ref into a working directory. Pure (no I/O) so the
|
|
50
|
+
* slug/worktree layout is unit-testable. `forRemote` keeps the path
|
|
51
|
+
* home-relative (`~/…`) for the remote shell to expand; otherwise it is expanded
|
|
52
|
+
* against the local home into an absolute path.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildProjectPath(root: string, ref: string, forRemote: boolean): string;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a `--project` ref to a working directory, inferring/caching the root.
|
|
57
|
+
*
|
|
58
|
+
* `forRemote: true` returns a home-relative path (`~/…`) so the REMOTE login
|
|
59
|
+
* shell expands `~`/`$HOME` to its own home. `forRemote: false` returns an
|
|
60
|
+
* absolute local path and verifies it exists (so a mistyped slug fails loudly).
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveProjectRef(ref: string, opts: {
|
|
63
|
+
forRemote: boolean;
|
|
64
|
+
cwd?: string;
|
|
65
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects-root resolution for the `agents run --project <slug>` shorthand.
|
|
3
|
+
*
|
|
4
|
+
* Projects follow a predictable layout — `<root>/<repo>` (e.g.
|
|
5
|
+
* `~/src/github.com/<user>/<repo>`), with git worktrees under
|
|
6
|
+
* `<repo>/.agents/worktrees/<slug>`. The root is auto-inferred from the repo you
|
|
7
|
+
* launch inside (the directory ABOVE the git root) and cached in `agents.yaml`
|
|
8
|
+
* so later runs resolve a bare slug from anywhere. It is stored home-relative
|
|
9
|
+
* (`~/…`) when it sits under `$HOME`, so the SAME value resolves on a remote
|
|
10
|
+
* host whose home differs (`/home/<user>` vs `/Users/<user>`): a `--host` run
|
|
11
|
+
* keeps the `~` and lets the remote login shell expand it (see `remoteCdPrefix`
|
|
12
|
+
* in `hosts/dispatch.ts`), while a local run expands `~` against the local home.
|
|
13
|
+
*/
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import { readMeta, updateMeta } from './state.js';
|
|
18
|
+
import { getMainRepoRoot } from './git.js';
|
|
19
|
+
import { toPosix } from './platform/index.js';
|
|
20
|
+
const HOME = process.env.HOME ?? os.homedir();
|
|
21
|
+
/** Rewrite an absolute path under the local home to a `~/`-relative string; pass others through. */
|
|
22
|
+
export function toHomeRelative(abs) {
|
|
23
|
+
const rel = path.relative(HOME, abs);
|
|
24
|
+
if (rel === '')
|
|
25
|
+
return '~';
|
|
26
|
+
if (!rel.startsWith('..') && !path.isAbsolute(rel))
|
|
27
|
+
return `~/${toPosix(rel)}`;
|
|
28
|
+
return abs;
|
|
29
|
+
}
|
|
30
|
+
/** Expand a leading `~`/`$HOME` against the LOCAL home. Other paths pass through unchanged. */
|
|
31
|
+
export function expandLocalHome(p) {
|
|
32
|
+
if (p === '~' || p === '$HOME')
|
|
33
|
+
return HOME;
|
|
34
|
+
if (p.startsWith('~/'))
|
|
35
|
+
return path.join(HOME, p.slice(2));
|
|
36
|
+
if (p.startsWith('$HOME/'))
|
|
37
|
+
return path.join(HOME, p.slice(6));
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Make a `--cwd`/`--project` value portable to a remote host: an absolute path
|
|
42
|
+
* under the LOCAL home (which the local shell already expanded from `~`) becomes
|
|
43
|
+
* `~/…` so the *remote* shell re-roots it at its own home. Paths already anchored
|
|
44
|
+
* at `~`/`$HOME` pass through; other absolute or relative paths are left as-is
|
|
45
|
+
* (used verbatim on the host). Explicit `--remote-cwd` is NOT run through this —
|
|
46
|
+
* it is a literal remote path by contract.
|
|
47
|
+
*/
|
|
48
|
+
export function toRemotePortable(p) {
|
|
49
|
+
if (p.startsWith('~') || p.startsWith('$HOME'))
|
|
50
|
+
return p;
|
|
51
|
+
if (path.isAbsolute(p))
|
|
52
|
+
return toHomeRelative(p);
|
|
53
|
+
return p;
|
|
54
|
+
}
|
|
55
|
+
/** The configured projects root (home-relative or absolute), or undefined when unset. */
|
|
56
|
+
export function getProjectRoot() {
|
|
57
|
+
return readMeta().projectRoot;
|
|
58
|
+
}
|
|
59
|
+
/** Set (override) the cached projects root. Stored home-relative when under `$HOME`. */
|
|
60
|
+
export function setProjectRoot(rootPath) {
|
|
61
|
+
const stored = toHomeRelative(path.resolve(expandLocalHome(rootPath)));
|
|
62
|
+
updateMeta({ projectRoot: stored });
|
|
63
|
+
return stored;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Infer the projects root from `cwd`: the directory ABOVE the git repo root
|
|
67
|
+
* (cwd inside `~/src/github.com/user/repo` → `~/src/github.com/user`). Returns a
|
|
68
|
+
* home-relative string when under `$HOME`; undefined when `cwd` is not in a repo.
|
|
69
|
+
*/
|
|
70
|
+
export async function inferProjectRoot(cwd) {
|
|
71
|
+
try {
|
|
72
|
+
const mainRoot = await getMainRepoRoot(cwd);
|
|
73
|
+
return toHomeRelative(path.dirname(mainRoot));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the projects root, auto-inferring and caching on first use. Throws an
|
|
81
|
+
* actionable error when it is neither configured nor inferrable from `cwd`.
|
|
82
|
+
*/
|
|
83
|
+
export async function ensureProjectRoot(cwd) {
|
|
84
|
+
const existing = getProjectRoot();
|
|
85
|
+
if (existing)
|
|
86
|
+
return existing;
|
|
87
|
+
const inferred = await inferProjectRoot(cwd);
|
|
88
|
+
if (!inferred) {
|
|
89
|
+
throw new Error('Could not determine your projects root. Run once from inside a project ' +
|
|
90
|
+
'(a git repo under your projects dir) so it can be inferred, or set it:\n' +
|
|
91
|
+
' agents defaults project-root ~/src/github.com/<you>');
|
|
92
|
+
}
|
|
93
|
+
updateMeta({ projectRoot: inferred });
|
|
94
|
+
process.stderr.write(`[project] cached projects root: ${inferred}\n`);
|
|
95
|
+
return inferred;
|
|
96
|
+
}
|
|
97
|
+
/** Parse a `--project` value of the form `<slug>[@<worktree>]`. */
|
|
98
|
+
export function parseProjectRef(ref) {
|
|
99
|
+
const at = ref.indexOf('@');
|
|
100
|
+
if (at === -1)
|
|
101
|
+
return { slug: ref };
|
|
102
|
+
return { slug: ref.slice(0, at), worktree: ref.slice(at + 1) || undefined };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Join a root + `--project` ref into a working directory. Pure (no I/O) so the
|
|
106
|
+
* slug/worktree layout is unit-testable. `forRemote` keeps the path
|
|
107
|
+
* home-relative (`~/…`) for the remote shell to expand; otherwise it is expanded
|
|
108
|
+
* against the local home into an absolute path.
|
|
109
|
+
*/
|
|
110
|
+
export function buildProjectPath(root, ref, forRemote) {
|
|
111
|
+
const { slug, worktree } = parseProjectRef(ref);
|
|
112
|
+
if (!slug)
|
|
113
|
+
throw new Error(`Invalid --project value: "${ref}"`);
|
|
114
|
+
let rel = `${root}/${slug}`;
|
|
115
|
+
if (worktree)
|
|
116
|
+
rel += `/.agents/worktrees/${worktree}`;
|
|
117
|
+
return forRemote ? rel : path.resolve(expandLocalHome(rel));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolve a `--project` ref to a working directory, inferring/caching the root.
|
|
121
|
+
*
|
|
122
|
+
* `forRemote: true` returns a home-relative path (`~/…`) so the REMOTE login
|
|
123
|
+
* shell expands `~`/`$HOME` to its own home. `forRemote: false` returns an
|
|
124
|
+
* absolute local path and verifies it exists (so a mistyped slug fails loudly).
|
|
125
|
+
*/
|
|
126
|
+
export async function resolveProjectRef(ref, opts) {
|
|
127
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
128
|
+
const root = await ensureProjectRoot(cwd);
|
|
129
|
+
const resolved = buildProjectPath(root, ref, opts.forRemote);
|
|
130
|
+
if (!opts.forRemote && !fs.existsSync(resolved)) {
|
|
131
|
+
throw new Error(`Project path not found: ${resolved}`);
|
|
132
|
+
}
|
|
133
|
+
return resolved;
|
|
134
|
+
}
|
|
@@ -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':
|
|
@@ -66,9 +66,13 @@ function getAgentConfigPath(agent, versionHome) {
|
|
|
66
66
|
case 'codex':
|
|
67
67
|
return path.join(versionHome, '.codex', 'config.toml');
|
|
68
68
|
case 'opencode':
|
|
69
|
-
return path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
69
|
+
return path.join(versionHome, '.config', 'opencode', 'opencode.jsonc');
|
|
70
70
|
case 'kimi':
|
|
71
71
|
return path.join(versionHome, '.kimi-code', 'config.toml');
|
|
72
|
+
case 'droid':
|
|
73
|
+
return path.join(versionHome, '.factory', 'settings.json');
|
|
74
|
+
case 'kiro':
|
|
75
|
+
return path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
|
|
72
76
|
default:
|
|
73
77
|
return null;
|
|
74
78
|
}
|
|
@@ -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' | 'antigravity' | 'grok' | 'kimi' | 'hermes' | 'forge';
|
|
8
|
+
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'kiro' | '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. */
|
|
@@ -74,23 +74,23 @@ export interface AgentStatusEntry {
|
|
|
74
74
|
expiresAt: number;
|
|
75
75
|
keyCount: number;
|
|
76
76
|
}
|
|
77
|
-
/** True if
|
|
77
|
+
/** True if a legacy standalone-broker launchd plist is still installed. */
|
|
78
78
|
export declare function secretsAgentServiceInstalled(): boolean;
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
80
|
+
* Retire the legacy standalone secrets-agent launchd service: bootout the job
|
|
81
|
+
* (falling back to the legacy `unload`) and remove its plist so the always-on
|
|
82
|
+
* daemon owns the broker socket. Idempotent and best-effort — a no-op when no
|
|
83
|
+
* legacy plist is present. Does NOT wipe held bundles: the booted-out process's
|
|
84
|
+
* memory is gone anyway, and the daemon-hosted broker starts fresh.
|
|
84
85
|
*/
|
|
85
|
-
export declare function
|
|
86
|
+
export declare function retireLegacySecretsAgentService(): void;
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* and
|
|
88
|
+
* Stop the persistent broker for `agents secrets stop`: wipe whatever the broker
|
|
89
|
+
* holds (forces Touch ID again on the next read), then retire any legacy
|
|
90
|
+
* standalone service. The daemon-hosted broker itself is left running — it is
|
|
91
|
+
* the always-on backbone, and stopping it would take down unrelated background
|
|
92
|
+
* work (routines, browser IPC, session-sync).
|
|
91
93
|
*/
|
|
92
|
-
export declare function kickstartSecretsAgentService(): void;
|
|
93
|
-
/** Stop + remove the persistent broker service, and wipe whatever it held. */
|
|
94
94
|
export declare function uninstallSecretsAgentService(): Promise<void>;
|
|
95
95
|
export type Request = {
|
|
96
96
|
cmd: 'ping';
|
|
@@ -171,7 +171,9 @@ export declare function shouldWipeOnWatchEvent(chunk: string): boolean;
|
|
|
171
171
|
*/
|
|
172
172
|
export declare function runSecretsAgent(opts?: {
|
|
173
173
|
service?: boolean;
|
|
174
|
-
}): Promise<
|
|
174
|
+
}): Promise<{
|
|
175
|
+
close(): void;
|
|
176
|
+
} | null>;
|
|
175
177
|
/**
|
|
176
178
|
* Host the secrets broker inside the always-on daemon (#416).
|
|
177
179
|
*
|
|
@@ -184,11 +186,11 @@ export declare function runSecretsAgent(opts?: {
|
|
|
184
186
|
* (those would kill the daemon — the daemon is the always-on backbone and
|
|
185
187
|
* manages its own version/lifecycle). The sweep only TTL-evicts.
|
|
186
188
|
*
|
|
187
|
-
* The caller (`runDaemon`)
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* null off-darwin (nothing to broker without biometry).
|
|
189
|
+
* The caller (`runDaemon`) normally invokes this only when no broker answers
|
|
190
|
+
* its initial ping. Binding still arbitrates ownership through the same shared
|
|
191
|
+
* path as the standalone service: a live owner wins, while only an unreachable
|
|
192
|
+
* stale socket is reclaimed. Returns a handle the daemon closes on shutdown,
|
|
193
|
+
* or null off-darwin (nothing to broker without biometry).
|
|
192
194
|
*/
|
|
193
195
|
export declare function startHostedBroker(): Promise<{
|
|
194
196
|
close(): void;
|
|
@@ -277,10 +279,11 @@ export declare function agentPing(): Promise<{
|
|
|
277
279
|
* Ensure a broker is running and reachable. Returns true once the socket answers
|
|
278
280
|
* a ping. macOS only.
|
|
279
281
|
*
|
|
280
|
-
* Prefers the
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* Only when the
|
|
284
|
-
*
|
|
282
|
+
* Prefers the always-on daemon, which hosts the broker socket (#416): retire any
|
|
283
|
+
* legacy standalone launchd service so the daemon owns the socket, then bring the
|
|
284
|
+
* daemon up (Path 0) — one supervised backbone that survives the whole login
|
|
285
|
+
* session, so subsequent reads never cold-start. Only when the daemon can't be
|
|
286
|
+
* used do we fall back to a one-off detached broker (Path 1) — the model that
|
|
287
|
+
* gets starved under heavy load, so it's last.
|
|
285
288
|
*/
|
|
286
289
|
export declare function ensureAgentRunning(timeoutMs?: number): Promise<boolean>;
|