@phnx-labs/agents-cli 1.20.0 → 1.20.4
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 +81 -0
- package/README.md +4 -4
- package/dist/commands/cli.js +3 -3
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +24 -7
- package/dist/commands/exec.js +36 -16
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +86 -7
- package/dist/commands/import.js +90 -37
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +117 -4
- package/dist/commands/pull.js +4 -4
- package/dist/commands/routines.js +6 -6
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +74 -39
- package/dist/commands/skills.js +22 -5
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +48 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.js +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +169 -8
- package/dist/commands/workflows.js +29 -6
- package/dist/index.js +4 -0
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +41 -17
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/chrome.js +4 -0
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/profiles.d.ts +3 -3
- package/dist/lib/browser/profiles.js +3 -3
- package/dist/lib/browser/service.js +19 -0
- package/dist/lib/browser/types.d.ts +4 -4
- package/dist/lib/cli-resources.d.ts +36 -8
- package/dist/lib/cli-resources.js +268 -46
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +39 -11
- package/dist/lib/exec.js +90 -31
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +68 -15
- package/dist/lib/import.d.ts +21 -0
- package/dist/lib/import.js +55 -2
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +40 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +51 -1
- package/dist/lib/plugin-marketplace.d.ts +10 -0
- package/dist/lib/plugin-marketplace.js +47 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +187 -8
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/pty-server.js +27 -3
- package/dist/lib/routines-format.d.ts +17 -5
- package/dist/lib/routines-format.js +37 -16
- package/dist/lib/routines.d.ts +1 -1
- package/dist/lib/routines.js +2 -2
- package/dist/lib/runner.js +64 -10
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +18 -22
- package/dist/lib/secrets/bundles.js +75 -99
- package/dist/lib/secrets/index.d.ts +51 -27
- package/dist/lib/secrets/index.js +147 -156
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/shims.d.ts +4 -1
- package/dist/lib/shims.js +5 -35
- package/dist/lib/state.d.ts +14 -1
- package/dist/lib/state.js +49 -5
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +47 -21
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/types.d.ts +57 -1
- package/dist/lib/types.js +2 -0
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +35 -1
- package/dist/lib/versions.js +288 -64
- package/package.json +13 -12
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook profiling — reads `hook.fire` events from the daily JSONL logs that
|
|
3
|
+
* generated shims (see `cache.ts`) emit on every invocation, and aggregates
|
|
4
|
+
* per-hook timing + cache stats.
|
|
5
|
+
*
|
|
6
|
+
* Only hooks declared with `cache:` get instrumented today, because only those
|
|
7
|
+
* are wrapped by a generated shim. Hooks without `cache:` are not in the
|
|
8
|
+
* profile output — that's deliberate: opting into the primitive is what
|
|
9
|
+
* surfaces the data.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { getLogsDir } from '../state.js';
|
|
14
|
+
/**
|
|
15
|
+
* Load every `hook.fire` event from the last `days` daily log files.
|
|
16
|
+
* Lines that aren't JSON or aren't `hook.fire` events are silently skipped —
|
|
17
|
+
* the events log is multiplexed (version.switch, secrets.get, …).
|
|
18
|
+
*/
|
|
19
|
+
export function loadHookFireEvents(days = 7, logsDir = getLogsDir()) {
|
|
20
|
+
if (!fs.existsSync(logsDir))
|
|
21
|
+
return [];
|
|
22
|
+
const today = new Date();
|
|
23
|
+
const events = [];
|
|
24
|
+
for (let i = 0; i < days; i++) {
|
|
25
|
+
const d = new Date(today);
|
|
26
|
+
d.setUTCDate(d.getUTCDate() - i);
|
|
27
|
+
const yyyy = d.getUTCFullYear();
|
|
28
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
29
|
+
const dd = String(d.getUTCDate()).padStart(2, '0');
|
|
30
|
+
const file = path.join(logsDir, `events-${yyyy}-${mm}-${dd}.jsonl`);
|
|
31
|
+
if (!fs.existsSync(file))
|
|
32
|
+
continue;
|
|
33
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
34
|
+
for (const line of raw.split('\n')) {
|
|
35
|
+
if (!line)
|
|
36
|
+
continue;
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(line);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (parsed.event !== 'hook.fire')
|
|
45
|
+
continue;
|
|
46
|
+
if (typeof parsed.hook !== 'string')
|
|
47
|
+
continue;
|
|
48
|
+
if (typeof parsed.ms !== 'number')
|
|
49
|
+
continue;
|
|
50
|
+
events.push(parsed);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return events;
|
|
54
|
+
}
|
|
55
|
+
/** Percentile of a sorted-ascending array. p in [0,100]. Linear interpolation. */
|
|
56
|
+
function percentile(sorted, p) {
|
|
57
|
+
if (sorted.length === 0)
|
|
58
|
+
return 0;
|
|
59
|
+
if (sorted.length === 1)
|
|
60
|
+
return sorted[0];
|
|
61
|
+
const rank = (p / 100) * (sorted.length - 1);
|
|
62
|
+
const lo = Math.floor(rank);
|
|
63
|
+
const hi = Math.ceil(rank);
|
|
64
|
+
if (lo === hi)
|
|
65
|
+
return sorted[lo];
|
|
66
|
+
const frac = rank - lo;
|
|
67
|
+
return sorted[lo] * (1 - frac) + sorted[hi] * frac;
|
|
68
|
+
}
|
|
69
|
+
/** Aggregate fire events into a per-hook profile, sorted by p99 desc. */
|
|
70
|
+
export function aggregateHookProfile(events) {
|
|
71
|
+
const byHook = new Map();
|
|
72
|
+
for (const e of events) {
|
|
73
|
+
if (!e.hook)
|
|
74
|
+
continue;
|
|
75
|
+
if (!byHook.has(e.hook))
|
|
76
|
+
byHook.set(e.hook, []);
|
|
77
|
+
byHook.get(e.hook).push(e);
|
|
78
|
+
}
|
|
79
|
+
const rows = [];
|
|
80
|
+
for (const [hook, evs] of byHook) {
|
|
81
|
+
const sortedMs = evs.map(e => e.ms).sort((a, b) => a - b);
|
|
82
|
+
const n = evs.length;
|
|
83
|
+
const sum = sortedMs.reduce((a, b) => a + b, 0);
|
|
84
|
+
const hits = evs.filter(e => e.cache === 'hit').length;
|
|
85
|
+
const stale = evs.filter(e => e.cache === 'stale-prefetch').length;
|
|
86
|
+
const misses = evs.filter(e => e.cache === 'miss').length;
|
|
87
|
+
const errors = evs.filter(e => typeof e.exit === 'number' && e.exit !== 0).length;
|
|
88
|
+
rows.push({
|
|
89
|
+
hook,
|
|
90
|
+
n,
|
|
91
|
+
p50Ms: Math.round(percentile(sortedMs, 50)),
|
|
92
|
+
p99Ms: Math.round(percentile(sortedMs, 99)),
|
|
93
|
+
meanMs: Math.round(sum / n),
|
|
94
|
+
maxMs: sortedMs[sortedMs.length - 1],
|
|
95
|
+
cacheHitPct: Math.round((hits / n) * 100),
|
|
96
|
+
cacheStalePct: Math.round((stale / n) * 100),
|
|
97
|
+
cacheMissPct: Math.round((misses / n) * 100),
|
|
98
|
+
errorCount: errors,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
rows.sort((a, b) => b.p99Ms - a.p99Ms);
|
|
102
|
+
return rows;
|
|
103
|
+
}
|
|
104
|
+
/** Human-friendly duration: "42ms" / "1.2s" / "12s" / "2m". */
|
|
105
|
+
export function formatMs(ms) {
|
|
106
|
+
if (ms < 1000)
|
|
107
|
+
return `${ms}ms`;
|
|
108
|
+
if (ms < 10_000)
|
|
109
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
110
|
+
if (ms < 60_000)
|
|
111
|
+
return `${Math.round(ms / 1000)}s`;
|
|
112
|
+
const mins = Math.floor(ms / 60_000);
|
|
113
|
+
const secs = Math.round((ms % 60_000) / 1000);
|
|
114
|
+
return secs > 0 ? `${mins}m${secs}s` : `${mins}m`;
|
|
115
|
+
}
|
|
116
|
+
/** Format a row's cache column: `hit:97% miss:3%` or `n/a` when nothing cached. */
|
|
117
|
+
export function formatCacheColumn(row) {
|
|
118
|
+
if (row.cacheHitPct + row.cacheStalePct + row.cacheMissPct === 0)
|
|
119
|
+
return 'n/a';
|
|
120
|
+
const parts = [];
|
|
121
|
+
if (row.cacheHitPct > 0)
|
|
122
|
+
parts.push(`hit:${row.cacheHitPct}%`);
|
|
123
|
+
if (row.cacheStalePct > 0)
|
|
124
|
+
parts.push(`stale:${row.cacheStalePct}%`);
|
|
125
|
+
if (row.cacheMissPct > 0)
|
|
126
|
+
parts.push(`miss:${row.cacheMissPct}%`);
|
|
127
|
+
return parts.join(' ');
|
|
128
|
+
}
|
|
129
|
+
export const DEFAULT_SLOW_HOOK_WARN_MS = 2000;
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -124,16 +124,6 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
124
124
|
* Hooks marked `enabled: false` are dropped from the returned map.
|
|
125
125
|
*/
|
|
126
126
|
export declare function parseHookManifest(): Record<string, ManifestHook>;
|
|
127
|
-
/**
|
|
128
|
-
* Register hooks as lifecycle events in an agent's config.
|
|
129
|
-
* Reads hooks.yaml manifest, merges into the agent's config file(s).
|
|
130
|
-
* Only manages hooks whose command paths are under ~/.agents/hooks/ or
|
|
131
|
-
* ~/.agents-system/hooks/. Does not remove user-added hooks.
|
|
132
|
-
*
|
|
133
|
-
* @param agentsDirOverride - When provided, treats this single dir as the
|
|
134
|
-
* only managed hook root. Used by tests to inject a temp path. In normal
|
|
135
|
-
* operation, both user and system roots are consulted with user precedence.
|
|
136
|
-
*/
|
|
137
127
|
export declare function registerHooksToSettings(agentId: AgentId, versionHome: string, hookManifest?: Record<string, ManifestHook>, agentsDirOverride?: string): {
|
|
138
128
|
registered: string[];
|
|
139
129
|
errors: string[];
|
package/dist/lib/hooks.js
CHANGED
|
@@ -61,6 +61,29 @@ function isManagedHookCommand(command, prefixes) {
|
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
64
|
+
import { generateHookShim, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
65
|
+
import { getHookShimsDir } from './state.js';
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the command path to register for a hook.
|
|
68
|
+
*
|
|
69
|
+
* Returns either the raw script path (no `cache:` set, legacy behavior) or
|
|
70
|
+
* the path to a generated caching/timing shim. The shim is written as a
|
|
71
|
+
* side effect when `cache:` is configured. The agent-native settings file
|
|
72
|
+
* gets the same shape either way — just a different command path.
|
|
73
|
+
*/
|
|
74
|
+
function resolveHookCommand(name, hookDef, resolveScript) {
|
|
75
|
+
const scriptPath = resolveScript(hookDef.script);
|
|
76
|
+
if (!scriptPath)
|
|
77
|
+
return null;
|
|
78
|
+
const cache = parseCacheConfig(hookDef.cache);
|
|
79
|
+
if (!cache) {
|
|
80
|
+
// No caching opted in — make sure a previously generated shim from an
|
|
81
|
+
// earlier `cache:` config is gone so the JSONL doesn't keep claiming hits.
|
|
82
|
+
removeHookShim(name);
|
|
83
|
+
return scriptPath;
|
|
84
|
+
}
|
|
85
|
+
return generateHookShim({ name, scriptPath, cache });
|
|
86
|
+
}
|
|
64
87
|
/**
|
|
65
88
|
* Extensions that are NEVER hooks — docs, configuration, plain data. A file
|
|
66
89
|
* in hooks/ with one of these extensions is auxiliary content (e.g., the
|
|
@@ -646,11 +669,35 @@ const CODEX_MATCHER_EVENTS = new Set(['PreToolUse', 'PostToolUse', 'SessionStart
|
|
|
646
669
|
* only managed hook root. Used by tests to inject a temp path. In normal
|
|
647
670
|
* operation, both user and system roots are consulted with user precedence.
|
|
648
671
|
*/
|
|
672
|
+
/**
|
|
673
|
+
* Delete shim files for hooks that no longer exist in the manifest.
|
|
674
|
+
* managedPrefixes already GCs the settings.json entries pointing at orphaned
|
|
675
|
+
* shims, but the .sh files on disk would otherwise persist forever. Called
|
|
676
|
+
* once per registerHooksToSettings invocation — cheap (a single readdir).
|
|
677
|
+
*/
|
|
678
|
+
function sweepOrphanShims(manifest) {
|
|
679
|
+
const shimsDir = getHookShimsDir();
|
|
680
|
+
if (!fs.existsSync(shimsDir))
|
|
681
|
+
return;
|
|
682
|
+
const activeNames = new Set(Object.keys(manifest));
|
|
683
|
+
for (const file of fs.readdirSync(shimsDir)) {
|
|
684
|
+
if (!file.endsWith('.sh'))
|
|
685
|
+
continue;
|
|
686
|
+
const name = file.slice(0, -3);
|
|
687
|
+
if (activeNames.has(name))
|
|
688
|
+
continue;
|
|
689
|
+
try {
|
|
690
|
+
fs.unlinkSync(path.join(shimsDir, file));
|
|
691
|
+
}
|
|
692
|
+
catch { /* best effort */ }
|
|
693
|
+
}
|
|
694
|
+
}
|
|
649
695
|
export function registerHooksToSettings(agentId, versionHome, hookManifest, agentsDirOverride) {
|
|
650
696
|
const manifest = hookManifest || parseHookManifest();
|
|
651
697
|
if (Object.keys(manifest).length === 0) {
|
|
652
698
|
return { registered: [], errors: [] };
|
|
653
699
|
}
|
|
700
|
+
sweepOrphanShims(manifest);
|
|
654
701
|
const overrideRoots = agentsDirOverride ? [agentsDirOverride] : null;
|
|
655
702
|
// Scripts are copied into the version home during sync — prefer that stable
|
|
656
703
|
// local path so registered commands don't break when source dirs change.
|
|
@@ -673,6 +720,10 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
673
720
|
: [
|
|
674
721
|
...getManagedHookPrefixes(),
|
|
675
722
|
...(localHooksDir ? [localHooksDir + path.sep] : []),
|
|
723
|
+
// Generated cache/timing shims live here; needs GC coverage so that a
|
|
724
|
+
// hook whose `cache:` field is removed gets its stale shim path purged
|
|
725
|
+
// from the agent's settings file (see resolveHookCommand).
|
|
726
|
+
getHookShimsDir() + path.sep,
|
|
676
727
|
];
|
|
677
728
|
if (agentId === 'claude') {
|
|
678
729
|
return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes);
|
|
@@ -737,12 +788,13 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
737
788
|
}
|
|
738
789
|
const hooks = config.hooks;
|
|
739
790
|
// Build set of all command paths the current manifest will register.
|
|
740
|
-
// Used to garbage-collect stale entries left behind after hook renames
|
|
791
|
+
// Used to garbage-collect stale entries left behind after hook renames
|
|
792
|
+
// or after a `cache:` field is added/removed (raw script vs shim path).
|
|
741
793
|
const currentManifestPaths = new Set();
|
|
742
|
-
for (const hookDef of Object.
|
|
794
|
+
for (const [hookName, hookDef] of Object.entries(manifest)) {
|
|
743
795
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
744
796
|
continue;
|
|
745
|
-
const resolved =
|
|
797
|
+
const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
|
|
746
798
|
if (resolved)
|
|
747
799
|
currentManifestPaths.add(resolved);
|
|
748
800
|
}
|
|
@@ -766,7 +818,7 @@ function registerHooksForClaude(versionHome, manifest, resolveScript, managedPre
|
|
|
766
818
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
767
819
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
768
820
|
continue;
|
|
769
|
-
const commandPath =
|
|
821
|
+
const commandPath = resolveHookCommand(name, hookDef, resolveScript);
|
|
770
822
|
if (!commandPath) {
|
|
771
823
|
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
772
824
|
continue;
|
|
@@ -830,12 +882,13 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
830
882
|
return { registered, errors };
|
|
831
883
|
}
|
|
832
884
|
}
|
|
833
|
-
// Build set of current manifest command paths for codex to GC stale entries
|
|
885
|
+
// Build set of current manifest command paths for codex to GC stale entries.
|
|
886
|
+
// Uses resolveHookCommand so cached hooks resolve to their shim path.
|
|
834
887
|
const currentManifestPaths = new Set();
|
|
835
|
-
for (const hookDef of Object.
|
|
888
|
+
for (const [hookName, hookDef] of Object.entries(manifest)) {
|
|
836
889
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
837
890
|
continue;
|
|
838
|
-
const resolved =
|
|
891
|
+
const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
|
|
839
892
|
if (resolved)
|
|
840
893
|
currentManifestPaths.add(resolved);
|
|
841
894
|
}
|
|
@@ -853,7 +906,7 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
853
906
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
854
907
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
855
908
|
continue;
|
|
856
|
-
const commandPath =
|
|
909
|
+
const commandPath = resolveHookCommand(name, hookDef, resolveScript);
|
|
857
910
|
if (!commandPath) {
|
|
858
911
|
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
859
912
|
continue;
|
|
@@ -941,10 +994,10 @@ function registerHooksForGemini(versionHome, manifest, resolveScript, managedPre
|
|
|
941
994
|
}
|
|
942
995
|
const hooks = config.hooks;
|
|
943
996
|
const currentManifestPaths = new Set();
|
|
944
|
-
for (const hookDef of Object.
|
|
997
|
+
for (const [hookName, hookDef] of Object.entries(manifest)) {
|
|
945
998
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
946
999
|
continue;
|
|
947
|
-
const resolved =
|
|
1000
|
+
const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
|
|
948
1001
|
if (resolved)
|
|
949
1002
|
currentManifestPaths.add(resolved);
|
|
950
1003
|
}
|
|
@@ -965,7 +1018,7 @@ function registerHooksForGemini(versionHome, manifest, resolveScript, managedPre
|
|
|
965
1018
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
966
1019
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
967
1020
|
continue;
|
|
968
|
-
const commandPath =
|
|
1021
|
+
const commandPath = resolveHookCommand(name, hookDef, resolveScript);
|
|
969
1022
|
if (!commandPath) {
|
|
970
1023
|
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
971
1024
|
continue;
|
|
@@ -1039,7 +1092,7 @@ function registerHooksForAntigravity(versionHome, manifest, resolveScript, manag
|
|
|
1039
1092
|
// hooks. Only managed paths are considered for removal — user-added entries
|
|
1040
1093
|
// outside managedPrefixes are preserved.
|
|
1041
1094
|
const currentManifestPaths = new Set();
|
|
1042
|
-
for (const hookDef of Object.
|
|
1095
|
+
for (const [hookName, hookDef] of Object.entries(manifest)) {
|
|
1043
1096
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
1044
1097
|
continue;
|
|
1045
1098
|
// Only paths whose events map to a known agy event would actually be
|
|
@@ -1047,7 +1100,7 @@ function registerHooksForAntigravity(versionHome, manifest, resolveScript, manag
|
|
|
1047
1100
|
const anyMapped = hookDef.events.some((e) => ANTIGRAVITY_EVENT_MAP[e]);
|
|
1048
1101
|
if (!anyMapped)
|
|
1049
1102
|
continue;
|
|
1050
|
-
const resolved =
|
|
1103
|
+
const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
|
|
1051
1104
|
if (resolved)
|
|
1052
1105
|
currentManifestPaths.add(resolved);
|
|
1053
1106
|
}
|
|
@@ -1072,7 +1125,7 @@ function registerHooksForAntigravity(versionHome, manifest, resolveScript, manag
|
|
|
1072
1125
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
1073
1126
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
1074
1127
|
continue;
|
|
1075
|
-
const commandPath =
|
|
1128
|
+
const commandPath = resolveHookCommand(name, hookDef, resolveScript);
|
|
1076
1129
|
if (!commandPath) {
|
|
1077
1130
|
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
1078
1131
|
continue;
|
|
@@ -1128,7 +1181,7 @@ function registerHooksForGrok(versionHome, manifest, resolveScript, managedPrefi
|
|
|
1128
1181
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
1129
1182
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
1130
1183
|
continue;
|
|
1131
|
-
const commandPath =
|
|
1184
|
+
const commandPath = resolveHookCommand(name, hookDef, resolveScript);
|
|
1132
1185
|
if (!commandPath) {
|
|
1133
1186
|
errors.push(`${name}: script not found`);
|
|
1134
1187
|
continue;
|
package/dist/lib/import.d.ts
CHANGED
|
@@ -81,6 +81,27 @@ export interface AgentBinarySpec {
|
|
|
81
81
|
* node_modules/.bin/{cliCommand} -> {binaryEntry}
|
|
82
82
|
*/
|
|
83
83
|
export declare function importAgentBinary(spec: AgentBinarySpec, version: string, globalPath: string, versionDir: string): ImportBinaryResult;
|
|
84
|
+
/**
|
|
85
|
+
* Register an existing installScript-based binary (Grok, Antigravity, Cursor,
|
|
86
|
+
* etc. — anything with `npmPackage: ''` and a curl/brew installer) under the
|
|
87
|
+
* managed version path. Unlike `importAgentBinary` this skips the npm
|
|
88
|
+
* package.json walk and just symlinks the resolved PATH binary directly into
|
|
89
|
+
* `{versionDir}/node_modules/.bin/{cliCommand}`. The symlink is what makes
|
|
90
|
+
* `listInstalledVersions` consider the version Managed.
|
|
91
|
+
*
|
|
92
|
+
* Layout produced:
|
|
93
|
+
*
|
|
94
|
+
* {versionDir}/
|
|
95
|
+
* package.json # marker (private, imported, from)
|
|
96
|
+
* home/ # empty isolated $HOME
|
|
97
|
+
* node_modules/.bin/{cliCommand} -> {binaryPath}
|
|
98
|
+
*
|
|
99
|
+
* For agents whose binary lookup is special-cased elsewhere (e.g. Grok's
|
|
100
|
+
* `~/.grok/downloads/`), the symlink is still created — `getBinaryPath` won't
|
|
101
|
+
* read it for those agents, but it documents provenance and lets a future
|
|
102
|
+
* refactor consolidate the binary-resolution registry.
|
|
103
|
+
*/
|
|
104
|
+
export declare function importInstallScriptBinary(spec: AgentBinarySpec, version: string, binaryPath: string, versionDir: string): ImportBinaryResult;
|
|
84
105
|
/**
|
|
85
106
|
* Resolve the on-disk npm package directory for an agent's CLI binary by
|
|
86
107
|
* walking up from the binary, following any symlinks. Returns null if the
|
package/dist/lib/import.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* true.
|
|
18
18
|
*/
|
|
19
19
|
import * as fs from 'fs';
|
|
20
|
+
import * as os from 'os';
|
|
20
21
|
import * as path from 'path';
|
|
21
22
|
import { AGENTS } from './agents.js';
|
|
22
23
|
import { getVersionsDir } from './state.js';
|
|
@@ -42,12 +43,17 @@ export async function importAgentConfig(agentId, version) {
|
|
|
42
43
|
const configDir = agent.configDir;
|
|
43
44
|
const versionsDir = getVersionsDir();
|
|
44
45
|
const versionHome = path.join(versionsDir, agentId, version, 'home');
|
|
45
|
-
|
|
46
|
+
// Match the shim's derivation in generateShimScript: the per-version config
|
|
47
|
+
// path mirrors the original configDir's path relative to $HOME. Hardcoding
|
|
48
|
+
// `.${agentId}` broke for nested configDirs like Antigravity
|
|
49
|
+
// (`~/.gemini/antigravity-cli`) — the destination would be `.antigravity`,
|
|
50
|
+
// mismatching the shim's expectation of `.gemini/antigravity-cli`.
|
|
51
|
+
const versionConfigDir = path.join(versionHome, path.relative(os.homedir(), configDir));
|
|
46
52
|
if (fs.existsSync(versionConfigDir)) {
|
|
47
53
|
return { success: false, skipped: true, error: `${version} already installed` };
|
|
48
54
|
}
|
|
49
55
|
try {
|
|
50
|
-
fs.mkdirSync(
|
|
56
|
+
fs.mkdirSync(path.dirname(versionConfigDir), { recursive: true });
|
|
51
57
|
fs.renameSync(configDir, versionConfigDir);
|
|
52
58
|
fs.symlinkSync(versionConfigDir, configDir);
|
|
53
59
|
setGlobalDefault(agentId, version);
|
|
@@ -155,6 +161,53 @@ export function importAgentBinary(spec, version, globalPath, versionDir) {
|
|
|
155
161
|
return { success: false, error: err.message };
|
|
156
162
|
}
|
|
157
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Register an existing installScript-based binary (Grok, Antigravity, Cursor,
|
|
166
|
+
* etc. — anything with `npmPackage: ''` and a curl/brew installer) under the
|
|
167
|
+
* managed version path. Unlike `importAgentBinary` this skips the npm
|
|
168
|
+
* package.json walk and just symlinks the resolved PATH binary directly into
|
|
169
|
+
* `{versionDir}/node_modules/.bin/{cliCommand}`. The symlink is what makes
|
|
170
|
+
* `listInstalledVersions` consider the version Managed.
|
|
171
|
+
*
|
|
172
|
+
* Layout produced:
|
|
173
|
+
*
|
|
174
|
+
* {versionDir}/
|
|
175
|
+
* package.json # marker (private, imported, from)
|
|
176
|
+
* home/ # empty isolated $HOME
|
|
177
|
+
* node_modules/.bin/{cliCommand} -> {binaryPath}
|
|
178
|
+
*
|
|
179
|
+
* For agents whose binary lookup is special-cased elsewhere (e.g. Grok's
|
|
180
|
+
* `~/.grok/downloads/`), the symlink is still created — `getBinaryPath` won't
|
|
181
|
+
* read it for those agents, but it documents provenance and lets a future
|
|
182
|
+
* refactor consolidate the binary-resolution registry.
|
|
183
|
+
*/
|
|
184
|
+
export function importInstallScriptBinary(spec, version, binaryPath, versionDir) {
|
|
185
|
+
const binaryLink = path.join(versionDir, 'node_modules', '.bin', spec.cliCommand);
|
|
186
|
+
let alreadyExists = false;
|
|
187
|
+
try {
|
|
188
|
+
fs.lstatSync(binaryLink);
|
|
189
|
+
alreadyExists = true;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
/* not present */
|
|
193
|
+
}
|
|
194
|
+
if (alreadyExists) {
|
|
195
|
+
return { success: false, skipped: true, error: `${version} already installed`, resolvedFromPath: binaryPath };
|
|
196
|
+
}
|
|
197
|
+
if (!fs.existsSync(binaryPath)) {
|
|
198
|
+
return { success: false, error: `Binary does not exist: ${binaryPath}` };
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
202
|
+
fs.mkdirSync(path.join(versionDir, 'node_modules', '.bin'), { recursive: true });
|
|
203
|
+
fs.writeFileSync(path.join(versionDir, 'package.json'), JSON.stringify({ name: `agents-${spec.agentId}-${version}`, version: '1.0.0', private: true, imported: true, from: binaryPath, installScriptBased: true }, null, 2));
|
|
204
|
+
fs.symlinkSync(binaryPath, binaryLink);
|
|
205
|
+
return { success: true, resolvedFromPath: binaryPath };
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return { success: false, error: err.message };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
158
211
|
/**
|
|
159
212
|
* Resolve the on-disk npm package directory for an agent's CLI binary by
|
|
160
213
|
* walking up from the binary, following any symlinks. Returns null if the
|
package/dist/lib/mcp.d.ts
CHANGED
|
@@ -43,6 +43,21 @@ export declare function parseMcpServerConfig(filePath: string): McpYamlConfig |
|
|
|
43
43
|
* List all MCP server configs from ~/.agents/mcp/.
|
|
44
44
|
*/
|
|
45
45
|
export declare function listMcpServerConfigs(cwd?: string): InstalledMcpServer[];
|
|
46
|
+
/**
|
|
47
|
+
* Scan a repository for MCP server YAML configs.
|
|
48
|
+
* Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
|
|
49
|
+
*/
|
|
50
|
+
export declare function discoverMcpConfigsFromRepo(repoPath: string): InstalledMcpServer[];
|
|
51
|
+
/**
|
|
52
|
+
* Install an MCP YAML config from a source file into ~/.agents/mcp/.
|
|
53
|
+
* Re-serializes via writeMcpServerConfig so the on-disk filename is
|
|
54
|
+
* deterministic (sanitized from the server name).
|
|
55
|
+
*/
|
|
56
|
+
export declare function installMcpConfigCentrally(sourcePath: string): {
|
|
57
|
+
success: boolean;
|
|
58
|
+
error?: string;
|
|
59
|
+
path?: string;
|
|
60
|
+
};
|
|
46
61
|
/**
|
|
47
62
|
* Get MCP servers by name.
|
|
48
63
|
* If names is provided, returns only those servers.
|
package/dist/lib/mcp.js
CHANGED
|
@@ -113,6 +113,46 @@ export function listMcpServerConfigs(cwd = process.cwd()) {
|
|
|
113
113
|
}
|
|
114
114
|
return Array.from(results.values());
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Scan a repository for MCP server YAML configs.
|
|
118
|
+
* Looks under <repoPath>/mcp/*.yaml — same on-disk layout as ~/.agents/mcp/.
|
|
119
|
+
*/
|
|
120
|
+
export function discoverMcpConfigsFromRepo(repoPath) {
|
|
121
|
+
const dir = path.join(repoPath, 'mcp');
|
|
122
|
+
if (!fs.existsSync(dir))
|
|
123
|
+
return [];
|
|
124
|
+
const results = [];
|
|
125
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
126
|
+
if (!entry.isFile())
|
|
127
|
+
continue;
|
|
128
|
+
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
|
|
129
|
+
continue;
|
|
130
|
+
const filePath = path.join(dir, entry.name);
|
|
131
|
+
const config = parseMcpServerConfig(filePath);
|
|
132
|
+
if (config) {
|
|
133
|
+
results.push({ name: config.name, path: filePath, config, scope: 'user' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Install an MCP YAML config from a source file into ~/.agents/mcp/.
|
|
140
|
+
* Re-serializes via writeMcpServerConfig so the on-disk filename is
|
|
141
|
+
* deterministic (sanitized from the server name).
|
|
142
|
+
*/
|
|
143
|
+
export function installMcpConfigCentrally(sourcePath) {
|
|
144
|
+
try {
|
|
145
|
+
const config = parseMcpServerConfig(sourcePath);
|
|
146
|
+
if (!config) {
|
|
147
|
+
return { success: false, error: `Invalid MCP config at ${sourcePath}` };
|
|
148
|
+
}
|
|
149
|
+
const written = writeMcpServerConfig(config);
|
|
150
|
+
return { success: true, path: written };
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
return { success: false, error: err.message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
116
156
|
/**
|
|
117
157
|
* Get MCP servers by name.
|
|
118
158
|
* If names is provided, returns only those servers.
|
|
@@ -97,6 +97,19 @@ export declare function removePermissionSet(name: string): {
|
|
|
97
97
|
* Claude uses: { permissions: { allow: ["Bash(*)", "Read(**)"], deny: [] } }
|
|
98
98
|
*/
|
|
99
99
|
export declare function convertToClaudeFormat(set: PermissionSet): ClaudePermissions;
|
|
100
|
+
/**
|
|
101
|
+
* Convert canonical permission set to Gemini format.
|
|
102
|
+
* Gemini reads tool allow-lists from settings.json under `tools.allowed`.
|
|
103
|
+
* Bash permissions map to run_shell_command(prefix) — the prefix is extracted
|
|
104
|
+
* from the canonical "Bash(cmd:*)" pattern by stripping the trailing ":*".
|
|
105
|
+
* Blanket Bash grants map to bare "run_shell_command" (no prefix filter).
|
|
106
|
+
* Non-Bash tool patterns are skipped; Gemini uses different tool names.
|
|
107
|
+
*/
|
|
108
|
+
export declare function convertToGeminiFormat(set: PermissionSet): {
|
|
109
|
+
tools: {
|
|
110
|
+
allowed: string[];
|
|
111
|
+
};
|
|
112
|
+
};
|
|
100
113
|
/**
|
|
101
114
|
* Convert canonical permission set to OpenCode format.
|
|
102
115
|
* OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
|
package/dist/lib/permissions.js
CHANGED
|
@@ -14,13 +14,14 @@ import * as yaml from 'yaml';
|
|
|
14
14
|
import * as TOML from 'smol-toml';
|
|
15
15
|
import { getPermissionsDir, getUserPermissionsDir, ensureAgentsDir } from './state.js';
|
|
16
16
|
import { safeJoin } from './paths.js';
|
|
17
|
+
import { updateGeminiSettings } from './gemini-settings.js';
|
|
17
18
|
const HOME = os.homedir();
|
|
18
19
|
/** Agents that support the permissions subsystem. */
|
|
19
20
|
// antigravity: permissions in ~/.gemini/antigravity-cli/settings.json under
|
|
20
21
|
// `permissions: { allow: [...], deny: [...] }`. Serializer is a follow-up.
|
|
21
22
|
// grok: permissions via --allow/--deny CLI flags or [permission] block in
|
|
22
23
|
// ~/.grok/config.toml. Serializer is a follow-up.
|
|
23
|
-
export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'antigravity', 'grok'];
|
|
24
|
+
export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode', 'antigravity', 'grok', 'gemini'];
|
|
24
25
|
/** Filename used for Codex Starlark deny-rules generated from permission groups. */
|
|
25
26
|
export const CODEX_RULES_FILENAME = 'agents-deny.rules';
|
|
26
27
|
export function containsBroadGrants(rules) {
|
|
@@ -416,6 +417,35 @@ function parseCanonicalPattern(permission) {
|
|
|
416
417
|
}
|
|
417
418
|
/** Blanket-Bash canonical forms that mean "allow any bash command". */
|
|
418
419
|
const BLANKET_BASH_FORMS = new Set(['Bash', 'Bash(*)', 'Bash(**)']);
|
|
420
|
+
/**
|
|
421
|
+
* Convert canonical permission set to Gemini format.
|
|
422
|
+
* Gemini reads tool allow-lists from settings.json under `tools.allowed`.
|
|
423
|
+
* Bash permissions map to run_shell_command(prefix) — the prefix is extracted
|
|
424
|
+
* from the canonical "Bash(cmd:*)" pattern by stripping the trailing ":*".
|
|
425
|
+
* Blanket Bash grants map to bare "run_shell_command" (no prefix filter).
|
|
426
|
+
* Non-Bash tool patterns are skipped; Gemini uses different tool names.
|
|
427
|
+
*/
|
|
428
|
+
export function convertToGeminiFormat(set) {
|
|
429
|
+
const allowed = new Set();
|
|
430
|
+
for (const perm of set.allow) {
|
|
431
|
+
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
432
|
+
allowed.add('run_shell_command');
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const parsed = parseCanonicalPattern(perm);
|
|
436
|
+
if (!parsed || parsed.tool !== 'bash')
|
|
437
|
+
continue;
|
|
438
|
+
const colonIdx = parsed.pattern.lastIndexOf(':');
|
|
439
|
+
const prefix = colonIdx > 0 ? parsed.pattern.slice(0, colonIdx) : parsed.pattern;
|
|
440
|
+
if (prefix === '*' || prefix === '**') {
|
|
441
|
+
allowed.add('run_shell_command');
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
allowed.add(`run_shell_command(${prefix})`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { tools: { allowed: Array.from(allowed) } };
|
|
448
|
+
}
|
|
419
449
|
/**
|
|
420
450
|
* Convert canonical permission set to OpenCode format.
|
|
421
451
|
* OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
|
|
@@ -881,6 +911,26 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
881
911
|
}
|
|
882
912
|
return { success: true };
|
|
883
913
|
}
|
|
914
|
+
if (agentId === 'gemini') {
|
|
915
|
+
const geminiPerms = convertToGeminiFormat(set);
|
|
916
|
+
const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
|
|
917
|
+
updateGeminiSettings(settingsPath, (settings) => {
|
|
918
|
+
// Remove stale permissions key written by earlier versions of this serializer.
|
|
919
|
+
delete settings.permissions;
|
|
920
|
+
const tools = (typeof settings.tools === 'object' && settings.tools !== null && !Array.isArray(settings.tools))
|
|
921
|
+
? settings.tools
|
|
922
|
+
: {};
|
|
923
|
+
if (merge) {
|
|
924
|
+
const existing = Array.isArray(tools.allowed) ? tools.allowed : [];
|
|
925
|
+
tools.allowed = Array.from(new Set([...existing, ...geminiPerms.tools.allowed]));
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
tools.allowed = geminiPerms.tools.allowed;
|
|
929
|
+
}
|
|
930
|
+
settings.tools = tools;
|
|
931
|
+
});
|
|
932
|
+
return { success: true };
|
|
933
|
+
}
|
|
884
934
|
return { success: false, error: `Agent '${agentId}' does not support permissions` };
|
|
885
935
|
}
|
|
886
936
|
catch (err) {
|
|
@@ -45,6 +45,16 @@ export declare function knownMarketplacesPath(agent: AgentId, versionHome: strin
|
|
|
45
45
|
/**
|
|
46
46
|
* Copy plugin source into marketplace install dir.
|
|
47
47
|
* Source of truth remains ~/.agents/plugins/<name>/ — this is a per-version snapshot.
|
|
48
|
+
*
|
|
49
|
+
* Symlinks pointing OUTSIDE the plugin source root are dropped. They show up
|
|
50
|
+
* when plugin authors (legitimately) link prompt-side references to sibling
|
|
51
|
+
* codebases — e.g. the rush plugin's `app -> ../../../rush/app` for @app/...
|
|
52
|
+
* autocomplete in user prompts. Faithfully copying those symlinks pollutes
|
|
53
|
+
* the marketplace with gigabytes of node_modules / .next / brand-asset video
|
|
54
|
+
* that the consumer (Claude Code, OpenClaw) then walks during plugin
|
|
55
|
+
* discovery — which is the documented cause of multi-minute startup hangs.
|
|
56
|
+
*
|
|
57
|
+
* Internal symlinks (target stays inside the plugin root) are preserved.
|
|
48
58
|
*/
|
|
49
59
|
export declare function copyPluginToMarketplace(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string): string;
|
|
50
60
|
/**
|