@phnx-labs/agents-cli 1.18.5 → 1.19.0
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 +13 -2
- package/README.md +22 -20
- package/dist/commands/browser.js +25 -2
- package/dist/commands/cloud.js +3 -3
- package/dist/commands/computer.d.ts +6 -0
- package/dist/commands/computer.js +477 -0
- package/dist/commands/doctor.js +19 -17
- package/dist/commands/exec.js +37 -59
- package/dist/commands/factory.js +12 -5
- package/dist/commands/import.js +6 -1
- package/dist/commands/mcp.js +9 -4
- package/dist/commands/packages.d.ts +3 -0
- package/dist/commands/packages.js +20 -12
- package/dist/commands/permissions.d.ts +2 -0
- package/dist/commands/permissions.js +20 -1
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +23 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pty.js +126 -112
- package/dist/commands/pull.js +29 -25
- package/dist/commands/repo.js +24 -26
- package/dist/commands/routines.js +29 -26
- package/dist/commands/secrets.js +66 -73
- package/dist/commands/sessions-tail.js +21 -22
- package/dist/commands/sessions.js +36 -68
- package/dist/commands/setup.js +20 -24
- package/dist/commands/teams.js +30 -39
- package/dist/commands/versions.js +60 -68
- package/dist/commands/worktree.d.ts +20 -0
- package/dist/commands/worktree.js +242 -0
- package/dist/computer.d.ts +2 -0
- package/dist/computer.js +7 -0
- package/dist/index.js +70 -26
- package/dist/lib/agents.d.ts +4 -1
- package/dist/lib/agents.js +23 -5
- package/dist/lib/browser/cdp.d.ts +15 -1
- package/dist/lib/browser/cdp.js +77 -8
- package/dist/lib/browser/chrome.js +17 -24
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +20 -8
- package/dist/lib/browser/ipc.js +38 -5
- package/dist/lib/browser/profiles.js +34 -2
- package/dist/lib/browser/runtime-state.d.ts +1 -2
- package/dist/lib/browser/runtime-state.js +11 -3
- package/dist/lib/browser/service.d.ts +5 -0
- package/dist/lib/browser/service.js +32 -4
- package/dist/lib/browser/types.d.ts +1 -1
- package/dist/lib/browser/upload.d.ts +2 -0
- package/dist/lib/browser/upload.js +34 -0
- package/dist/lib/cloud/rush.d.ts +2 -1
- package/dist/lib/cloud/rush.js +28 -9
- package/dist/lib/computer-rpc.d.ts +24 -0
- package/dist/lib/computer-rpc.js +263 -0
- package/dist/lib/daemon.js +7 -7
- package/dist/lib/exec.d.ts +2 -1
- package/dist/lib/exec.js +3 -2
- package/dist/lib/fs-atomic.d.ts +18 -0
- package/dist/lib/fs-atomic.js +76 -0
- package/dist/lib/git.js +2 -4
- package/dist/lib/help.d.ts +15 -0
- package/dist/lib/help.js +41 -0
- package/dist/lib/hooks/match.d.ts +1 -0
- package/dist/lib/hooks/match.js +57 -12
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +27 -10
- package/dist/lib/import.d.ts +1 -0
- package/dist/lib/import.js +7 -0
- package/dist/lib/manifest.js +27 -1
- package/dist/lib/mcp.d.ts +14 -0
- package/dist/lib/mcp.js +79 -14
- package/dist/lib/migrate.js +3 -3
- package/dist/lib/models.js +3 -1
- package/dist/lib/permissions.d.ts +5 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/plugin-marketplace.d.ts +3 -1
- package/dist/lib/plugin-marketplace.js +36 -1
- package/dist/lib/plugins.d.ts +19 -1
- package/dist/lib/plugins.js +99 -8
- package/dist/lib/redact.d.ts +4 -0
- package/dist/lib/redact.js +18 -0
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +15 -0
- package/dist/lib/sandbox.js +15 -5
- package/dist/lib/secrets/bundles.d.ts +7 -12
- package/dist/lib/secrets/bundles.js +45 -29
- package/dist/lib/secrets/index.js +4 -4
- package/dist/lib/session/cloud.d.ts +2 -0
- package/dist/lib/session/cloud.js +34 -6
- package/dist/lib/session/parse.js +7 -2
- package/dist/lib/session/render.d.ts +4 -1
- package/dist/lib/session/render.js +81 -35
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +29 -7
- package/dist/lib/state.d.ts +5 -5
- package/dist/lib/state.js +43 -13
- package/dist/lib/teams/agents.d.ts +1 -1
- package/dist/lib/teams/agents.js +2 -2
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/types.js +0 -2
- package/dist/lib/versions.js +65 -40
- package/dist/lib/workflows.d.ts +7 -0
- package/dist/lib/workflows.js +42 -1
- package/npm-shrinkwrap.json +3256 -0
- package/package.json +32 -26
- package/scripts/postinstall.js +8 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
4
|
+
import lockfile from 'proper-lockfile';
|
|
5
|
+
const LOCK_STALE_MS = 5_000;
|
|
6
|
+
const LOCK_RETRIES = 5;
|
|
7
|
+
// Reused across all sleepSync calls — avoids allocating a new SAB each time.
|
|
8
|
+
const _sleepBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
9
|
+
export function sleepSync(ms) {
|
|
10
|
+
Atomics.wait(_sleepBuf, 0, 0, ms);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Ensures the target file (and its parent directory) exist so proper-lockfile
|
|
14
|
+
* can create a sibling .lock directory. Created with flag 'wx' so concurrent
|
|
15
|
+
* creation races are safe (EEXIST is swallowed).
|
|
16
|
+
*/
|
|
17
|
+
export function ensureLockTarget(filePath, initialContent = '', dirMode) {
|
|
18
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, ...(dirMode != null ? { mode: dirMode } : {}) });
|
|
19
|
+
if (fs.existsSync(filePath))
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
fs.writeFileSync(filePath, initialContent, { encoding: 'utf-8', flag: 'wx' });
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err?.code !== 'EEXIST')
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Writes content to filePath via a temp file + rename so readers never see a
|
|
31
|
+
* partial write. On POSIX, rename(2) is atomic.
|
|
32
|
+
*/
|
|
33
|
+
export function atomicWriteFileSync(filePath, content) {
|
|
34
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${randomBytes(8).toString('hex')}`;
|
|
35
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
36
|
+
try {
|
|
37
|
+
fs.renameSync(tmpPath, filePath);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
try {
|
|
41
|
+
fs.unlinkSync(tmpPath);
|
|
42
|
+
}
|
|
43
|
+
catch { /* best-effort cleanup */ }
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Acquires an exclusive proper-lockfile lock on filePath, runs fn, then
|
|
49
|
+
* releases the lock. Retries up to LOCK_RETRIES times with linear back-off.
|
|
50
|
+
* Breaks stale locks older than LOCK_STALE_MS.
|
|
51
|
+
*/
|
|
52
|
+
export function withFileLock(filePath, fn) {
|
|
53
|
+
let release = null;
|
|
54
|
+
let lastError;
|
|
55
|
+
for (let attempt = 0; attempt <= LOCK_RETRIES; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
release = lockfile.lockSync(filePath, { stale: LOCK_STALE_MS });
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
lastError = err;
|
|
62
|
+
if (attempt < LOCK_RETRIES)
|
|
63
|
+
sleepSync(50 * (attempt + 1));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!release) {
|
|
67
|
+
const message = lastError instanceof Error ? lastError.message : String(lastError);
|
|
68
|
+
throw new Error(`Could not acquire lock for ${filePath}: ${message}`);
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
return fn();
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
release();
|
|
75
|
+
}
|
|
76
|
+
}
|
package/dist/lib/git.js
CHANGED
|
@@ -9,7 +9,7 @@ import simpleGit from 'simple-git';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { getPackageLocalPath } from './state.js';
|
|
12
|
-
import { DEFAULT_SYSTEM_REPO,
|
|
12
|
+
import { DEFAULT_SYSTEM_REPO, systemRepoSlug } from './types.js';
|
|
13
13
|
/**
|
|
14
14
|
* Install hooks from `.githooks/` by symlinking each entry into `.git/hooks/`.
|
|
15
15
|
*
|
|
@@ -398,11 +398,9 @@ export async function isSystemRepoOrigin(dir) {
|
|
|
398
398
|
const origin = remotes.find(r => r.name === 'origin');
|
|
399
399
|
if (!origin?.refs?.fetch)
|
|
400
400
|
return false;
|
|
401
|
-
// Check if origin points at the current or legacy system repo.
|
|
402
401
|
const url = origin.refs.fetch.toLowerCase();
|
|
403
402
|
const currentSlug = systemRepoSlug(DEFAULT_SYSTEM_REPO).toLowerCase();
|
|
404
|
-
|
|
405
|
-
return url.includes(currentSlug) || url.includes(mirrorSlug);
|
|
403
|
+
return url.includes(currentSlug);
|
|
406
404
|
}
|
|
407
405
|
catch {
|
|
408
406
|
/* not a git repo or no remotes */
|
package/dist/lib/help.d.ts
CHANGED
|
@@ -18,5 +18,20 @@ export interface CommandGroup {
|
|
|
18
18
|
* any group fall back to a plain "Commands:" section below the groups.
|
|
19
19
|
*/
|
|
20
20
|
export declare function registerCommandGroups(parent: Command, groups: readonly CommandGroup[]): void;
|
|
21
|
+
/** Examples + Notes blocks attached to a command via setHelpSections. */
|
|
22
|
+
interface HelpSections {
|
|
23
|
+
examples?: string;
|
|
24
|
+
notes?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Attach an Examples block (rendered between the description and Arguments)
|
|
28
|
+
* and/or a Notes block (rendered at the very end, after Options) to a command.
|
|
29
|
+
*
|
|
30
|
+
* Bodies are normalized: the shared leading indent is stripped, then every line
|
|
31
|
+
* is re-indented by two spaces. Callers can pass natural multiline template
|
|
32
|
+
* literals without babysitting whitespace.
|
|
33
|
+
*/
|
|
34
|
+
export declare function setHelpSections(cmd: Command, sections: HelpSections): void;
|
|
21
35
|
/** Apply standardized help formatting to the root command and all subcommands. */
|
|
22
36
|
export declare function applyGlobalHelpConventions(root: Command): void;
|
|
37
|
+
export {};
|
package/dist/lib/help.js
CHANGED
|
@@ -7,6 +7,40 @@ const commandGroupRegistry = new WeakMap();
|
|
|
7
7
|
export function registerCommandGroups(parent, groups) {
|
|
8
8
|
commandGroupRegistry.set(parent, groups);
|
|
9
9
|
}
|
|
10
|
+
const helpSectionRegistry = new WeakMap();
|
|
11
|
+
/**
|
|
12
|
+
* Attach an Examples block (rendered between the description and Arguments)
|
|
13
|
+
* and/or a Notes block (rendered at the very end, after Options) to a command.
|
|
14
|
+
*
|
|
15
|
+
* Bodies are normalized: the shared leading indent is stripped, then every line
|
|
16
|
+
* is re-indented by two spaces. Callers can pass natural multiline template
|
|
17
|
+
* literals without babysitting whitespace.
|
|
18
|
+
*/
|
|
19
|
+
export function setHelpSections(cmd, sections) {
|
|
20
|
+
helpSectionRegistry.set(cmd, sections);
|
|
21
|
+
}
|
|
22
|
+
/** Strip a uniform leading indent from a block and trim surrounding blank lines. */
|
|
23
|
+
function dedent(body) {
|
|
24
|
+
const lines = body.replace(/^\n+/, '').replace(/\s+$/, '').split('\n');
|
|
25
|
+
let minIndent = Infinity;
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
if (line.trim() === '')
|
|
28
|
+
continue;
|
|
29
|
+
const indent = line.match(/^[ \t]*/)?.[0].length ?? 0;
|
|
30
|
+
if (indent < minIndent)
|
|
31
|
+
minIndent = indent;
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isFinite(minIndent) || minIndent === 0)
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
return lines.map((line) => (line.length >= minIndent ? line.slice(minIndent) : line)).join('\n');
|
|
36
|
+
}
|
|
37
|
+
/** Re-indent a dedented block by two spaces so it sits under a section heading. */
|
|
38
|
+
function indentBlock(body) {
|
|
39
|
+
return body
|
|
40
|
+
.split('\n')
|
|
41
|
+
.map((line) => (line.length === 0 ? '' : ` ${line}`))
|
|
42
|
+
.join('\n');
|
|
43
|
+
}
|
|
10
44
|
/** Format help output with Commands listed before Options for better discoverability. */
|
|
11
45
|
function formatHelpCommandsFirst(cmd, helper) {
|
|
12
46
|
const termWidth = helper.padWidth(cmd, helper);
|
|
@@ -49,6 +83,10 @@ function formatHelpCommandsFirst(cmd, helper) {
|
|
|
49
83
|
if (commandDescription.length > 0) {
|
|
50
84
|
output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']);
|
|
51
85
|
}
|
|
86
|
+
const sections = helpSectionRegistry.get(cmd);
|
|
87
|
+
if (sections?.examples) {
|
|
88
|
+
output = output.concat(['Examples:', indentBlock(dedent(sections.examples)), '']);
|
|
89
|
+
}
|
|
52
90
|
const argumentList = helper
|
|
53
91
|
.visibleArguments(cmd)
|
|
54
92
|
.filter((a) => !isHidden(a))
|
|
@@ -109,6 +147,9 @@ function formatHelpCommandsFirst(cmd, helper) {
|
|
|
109
147
|
output = output.concat(['Global Options:', formatList(globalOptionList), '']);
|
|
110
148
|
}
|
|
111
149
|
}
|
|
150
|
+
if (sections?.notes) {
|
|
151
|
+
output = output.concat(['Notes:', indentBlock(dedent(sections.notes)), '']);
|
|
152
|
+
}
|
|
112
153
|
return output.join('\n');
|
|
113
154
|
}
|
|
114
155
|
/** Recursively apply help conventions (-h flag, no help subcommand, custom formatter). */
|
|
@@ -21,6 +21,7 @@ export interface HookInput {
|
|
|
21
21
|
tool_args?: unknown;
|
|
22
22
|
cwd?: string;
|
|
23
23
|
}
|
|
24
|
+
export declare function isSafeHookRegex(source: string): boolean;
|
|
24
25
|
/**
|
|
25
26
|
* Decide whether a hook with the given match config should fire on this input.
|
|
26
27
|
* Pure function — no side effects, no IO except git/cwd checks for predicates
|
package/dist/lib/hooks/match.js
CHANGED
|
@@ -44,6 +44,59 @@ function findProjectRoot(start) {
|
|
|
44
44
|
dir = parent;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
function maxGroupDepth(source) {
|
|
48
|
+
let depth = 0;
|
|
49
|
+
let max = 0;
|
|
50
|
+
let escaped = false;
|
|
51
|
+
let inClass = false;
|
|
52
|
+
for (const ch of source) {
|
|
53
|
+
if (escaped) {
|
|
54
|
+
escaped = false;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (ch === '\\') {
|
|
58
|
+
escaped = true;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === '[') {
|
|
62
|
+
inClass = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === ']') {
|
|
66
|
+
inClass = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (inClass)
|
|
70
|
+
continue;
|
|
71
|
+
if (ch === '(') {
|
|
72
|
+
depth += 1;
|
|
73
|
+
max = Math.max(max, depth);
|
|
74
|
+
}
|
|
75
|
+
else if (ch === ')' && depth > 0) {
|
|
76
|
+
depth -= 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return max;
|
|
80
|
+
}
|
|
81
|
+
export function isSafeHookRegex(source) {
|
|
82
|
+
if (source.length > 200)
|
|
83
|
+
return false;
|
|
84
|
+
if (maxGroupDepth(source) > 3)
|
|
85
|
+
return false;
|
|
86
|
+
if (/\((?:\?:)?[^)]*[*+][?+*{,\d}]*[^)]*\)\s*(?:[+*]|\{\d*,?\d*\})/.test(source))
|
|
87
|
+
return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function compileHookRegex(source) {
|
|
91
|
+
if (!isSafeHookRegex(source))
|
|
92
|
+
return null;
|
|
93
|
+
try {
|
|
94
|
+
return new RegExp(source);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
47
100
|
/**
|
|
48
101
|
* Decide whether a hook with the given match config should fire on this input.
|
|
49
102
|
* Pure function — no side effects, no IO except git/cwd checks for predicates
|
|
@@ -63,13 +116,9 @@ export function shouldFire(matches, input) {
|
|
|
63
116
|
}
|
|
64
117
|
if (matches.prompt_matches !== undefined) {
|
|
65
118
|
const prompt = input.prompt ?? '';
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
re = new RegExp(matches.prompt_matches);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
119
|
+
const re = compileHookRegex(matches.prompt_matches);
|
|
120
|
+
if (!re)
|
|
71
121
|
return false;
|
|
72
|
-
}
|
|
73
122
|
if (!re.test(prompt))
|
|
74
123
|
return false;
|
|
75
124
|
}
|
|
@@ -86,13 +135,9 @@ export function shouldFire(matches, input) {
|
|
|
86
135
|
const serialized = typeof input.tool_args === 'string'
|
|
87
136
|
? input.tool_args
|
|
88
137
|
: JSON.stringify(input.tool_args ?? '');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
re = new RegExp(matches.tool_args_match);
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
138
|
+
const re = compileHookRegex(matches.tool_args_match);
|
|
139
|
+
if (!re)
|
|
94
140
|
return false;
|
|
95
|
-
}
|
|
96
141
|
if (!re.test(serialized))
|
|
97
142
|
return false;
|
|
98
143
|
}
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* parsing those manifests, registering hooks into agent-native settings files,
|
|
8
8
|
* and syncing them across version switches.
|
|
9
9
|
*/
|
|
10
|
+
export declare function resolveHookScriptPath(script: string): string | null;
|
|
10
11
|
import type { AgentId, InstalledHook, ManifestHook } from './types.js';
|
|
11
12
|
export type HookEntry = {
|
|
12
13
|
name: string;
|
package/dist/lib/hooks.js
CHANGED
|
@@ -20,12 +20,22 @@ function getCentralHooksDir() { return getUserHooksDir(); }
|
|
|
20
20
|
* Resolve a hook script's absolute path. Checks user dir first, then enabled
|
|
21
21
|
* extra repos in insertion order, then system dir. Returns null if not found.
|
|
22
22
|
*/
|
|
23
|
-
function
|
|
23
|
+
function resolveContainedHookPath(hooksRoot, script) {
|
|
24
|
+
const resolvedRoot = path.resolve(hooksRoot);
|
|
25
|
+
const candidate = path.join(hooksRoot, script);
|
|
26
|
+
const resolved = path.resolve(candidate);
|
|
27
|
+
if (!resolved.startsWith(resolvedRoot + path.sep))
|
|
28
|
+
return null;
|
|
29
|
+
if (!fs.existsSync(resolved))
|
|
30
|
+
return null;
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
export function resolveHookScriptPath(script) {
|
|
24
34
|
const extraDirs = getEnabledExtraRepos().map(e => e.dir);
|
|
25
35
|
for (const root of [getUserAgentsDir(), ...extraDirs, getSystemAgentsDir()]) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
36
|
+
const resolved = resolveContainedHookPath(path.join(root, 'hooks'), script);
|
|
37
|
+
if (resolved)
|
|
38
|
+
return resolved;
|
|
29
39
|
}
|
|
30
40
|
return null;
|
|
31
41
|
}
|
|
@@ -586,14 +596,17 @@ export function listCentralHooks() {
|
|
|
586
596
|
*/
|
|
587
597
|
export function parseHookManifest() {
|
|
588
598
|
const merged = {};
|
|
599
|
+
const systemHooks = {};
|
|
589
600
|
// System layer: hooks: section of agents.yaml (npm-shipped, separate repo).
|
|
590
601
|
const systemPath = path.join(getSystemAgentsDir(), 'agents.yaml');
|
|
591
602
|
if (fs.existsSync(systemPath)) {
|
|
592
603
|
try {
|
|
593
604
|
const meta = yaml.parse(fs.readFileSync(systemPath, 'utf-8'));
|
|
594
605
|
if (meta?.hooks)
|
|
595
|
-
for (const [name, def] of Object.entries(meta.hooks))
|
|
606
|
+
for (const [name, def] of Object.entries(meta.hooks)) {
|
|
607
|
+
systemHooks[name] = def;
|
|
596
608
|
merged[name] = def;
|
|
609
|
+
}
|
|
597
610
|
}
|
|
598
611
|
catch { /* skip unreadable manifest */ }
|
|
599
612
|
}
|
|
@@ -603,8 +616,13 @@ export function parseHookManifest() {
|
|
|
603
616
|
try {
|
|
604
617
|
const meta = yaml.parse(fs.readFileSync(userMetaPath, 'utf-8'));
|
|
605
618
|
if (meta?.hooks)
|
|
606
|
-
for (const [name, def] of Object.entries(meta.hooks))
|
|
619
|
+
for (const [name, def] of Object.entries(meta.hooks)) {
|
|
620
|
+
if (systemHooks[name] && def.override !== true) {
|
|
621
|
+
const action = def.enabled === false ? 'disables' : 'shadows';
|
|
622
|
+
console.warn(`[agents hooks] User-layer hook '${name}' ${action} system-shipped hook. Set 'override: true' to silence this warning.`);
|
|
623
|
+
}
|
|
607
624
|
merged[name] = def;
|
|
625
|
+
}
|
|
608
626
|
}
|
|
609
627
|
catch { /* skip unreadable meta */ }
|
|
610
628
|
}
|
|
@@ -641,12 +659,11 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
641
659
|
: null;
|
|
642
660
|
const resolveScript = (script) => {
|
|
643
661
|
if (overrideRoots) {
|
|
644
|
-
|
|
645
|
-
return fs.existsSync(candidate) ? candidate : null;
|
|
662
|
+
return resolveContainedHookPath(path.join(overrideRoots[0], 'hooks'), script);
|
|
646
663
|
}
|
|
647
664
|
if (localHooksDir) {
|
|
648
|
-
const local =
|
|
649
|
-
if (
|
|
665
|
+
const local = resolveContainedHookPath(localHooksDir, script);
|
|
666
|
+
if (local)
|
|
650
667
|
return local;
|
|
651
668
|
}
|
|
652
669
|
return resolveHookScriptPath(script);
|
package/dist/lib/import.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface ImportBinaryResult {
|
|
|
28
28
|
error?: string;
|
|
29
29
|
resolvedFromPath?: string;
|
|
30
30
|
}
|
|
31
|
+
export declare function isValidImportVersion(version: string): boolean;
|
|
31
32
|
/**
|
|
32
33
|
* Move an agent's config dir into the managed version structure and symlink it
|
|
33
34
|
* back to its original location. Sets the imported version as the global
|
package/dist/lib/import.js
CHANGED
|
@@ -22,6 +22,10 @@ import { AGENTS } from './agents.js';
|
|
|
22
22
|
import { getVersionsDir } from './state.js';
|
|
23
23
|
import { setGlobalDefault } from './versions.js';
|
|
24
24
|
import { createShim, createVersionedAlias, ensureShimCurrent, switchHomeFileSymlinks } from './shims.js';
|
|
25
|
+
const IMPORT_VERSION_RE = /^(?:latest|[A-Za-z0-9._+-]{1,64})$/;
|
|
26
|
+
export function isValidImportVersion(version) {
|
|
27
|
+
return IMPORT_VERSION_RE.test(version);
|
|
28
|
+
}
|
|
25
29
|
/**
|
|
26
30
|
* Move an agent's config dir into the managed version structure and symlink it
|
|
27
31
|
* back to its original location. Sets the imported version as the global
|
|
@@ -31,6 +35,9 @@ import { createShim, createVersionedAlias, ensureShimCurrent, switchHomeFileSyml
|
|
|
31
35
|
* No-op (returns skipped=true) if the version's config dir is already created.
|
|
32
36
|
*/
|
|
33
37
|
export async function importAgentConfig(agentId, version) {
|
|
38
|
+
if (!isValidImportVersion(version)) {
|
|
39
|
+
return { success: false, error: `Invalid version: ${JSON.stringify(version)}` };
|
|
40
|
+
}
|
|
34
41
|
const agent = AGENTS[agentId];
|
|
35
42
|
const configDir = agent.configDir;
|
|
36
43
|
const versionsDir = getVersionsDir();
|
package/dist/lib/manifest.js
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as yaml from 'yaml';
|
|
9
|
+
import { ensureLockTarget, atomicWriteFileSync, withFileLock } from './fs-atomic.js';
|
|
9
10
|
import { safeJoin } from './paths.js';
|
|
10
11
|
/** Canonical filename for the manifest in any agents repo or project root. */
|
|
11
12
|
export const MANIFEST_FILENAME = 'agents.yaml';
|
|
13
|
+
// Per-path re-entrancy depth so withManifestLock is safe against recursive calls.
|
|
14
|
+
const manifestLockDepth = new Map();
|
|
12
15
|
/** Parse a YAML string into a typed Manifest object. */
|
|
13
16
|
export function parseManifest(content) {
|
|
14
17
|
return yaml.parse(content);
|
|
@@ -26,11 +29,34 @@ export function readManifest(repoPath) {
|
|
|
26
29
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
27
30
|
return parseManifest(content);
|
|
28
31
|
}
|
|
32
|
+
function withManifestLock(filePath, fn) {
|
|
33
|
+
const depth = manifestLockDepth.get(filePath) ?? 0;
|
|
34
|
+
if (depth > 0) {
|
|
35
|
+
manifestLockDepth.set(filePath, depth + 1);
|
|
36
|
+
try {
|
|
37
|
+
return fn();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
manifestLockDepth.set(filePath, depth);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Project manifests are shared (no restricted dir mode unlike ~/.agents).
|
|
44
|
+
ensureLockTarget(filePath);
|
|
45
|
+
return withFileLock(filePath, () => {
|
|
46
|
+
manifestLockDepth.set(filePath, 1);
|
|
47
|
+
try {
|
|
48
|
+
return fn();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
manifestLockDepth.delete(filePath);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
29
55
|
/** Write a Manifest object to agents.yaml in the given directory. */
|
|
30
56
|
export function writeManifest(repoPath, manifest) {
|
|
31
57
|
const manifestPath = safeJoin(repoPath, MANIFEST_FILENAME);
|
|
32
58
|
const content = serializeManifest(manifest);
|
|
33
|
-
|
|
59
|
+
withManifestLock(manifestPath, () => atomicWriteFileSync(manifestPath, content));
|
|
34
60
|
}
|
|
35
61
|
/** Create a Manifest with sensible defaults for a fresh agents repo. */
|
|
36
62
|
export function createDefaultManifest() {
|
package/dist/lib/mcp.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ export interface InstalledMcpServer {
|
|
|
25
25
|
config: McpYamlConfig;
|
|
26
26
|
scope?: 'user' | 'project';
|
|
27
27
|
}
|
|
28
|
+
export interface McpCommandSpec {
|
|
29
|
+
command: string;
|
|
30
|
+
args: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface McpTargetOperationResult {
|
|
33
|
+
agentId: AgentId;
|
|
34
|
+
version?: string;
|
|
35
|
+
success: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
28
38
|
/**
|
|
29
39
|
* Parse an MCP server config from a YAML file.
|
|
30
40
|
*/
|
|
@@ -41,6 +51,10 @@ export declare function listMcpServerConfigs(cwd?: string): InstalledMcpServer[]
|
|
|
41
51
|
export declare function getMcpServersByName(names?: string[], options?: {
|
|
42
52
|
cwd?: string;
|
|
43
53
|
}): InstalledMcpServer[];
|
|
54
|
+
export declare function registerMcpCommandToTargets(targets: {
|
|
55
|
+
directAgents: AgentId[];
|
|
56
|
+
versionSelections: Map<AgentId, string[]>;
|
|
57
|
+
}, name: string, commandSpec: McpCommandSpec, scope?: 'user' | 'project', transport?: string): Promise<McpTargetOperationResult[]>;
|
|
44
58
|
/**
|
|
45
59
|
* Install MCP servers to an agent.
|
|
46
60
|
* For Claude/Codex: uses CLI commands (claude mcp add, codex mcp add)
|
package/dist/lib/mcp.js
CHANGED
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { execFileSync } from 'child_process';
|
|
14
14
|
import { getMcpDir, getUserMcpDir, getProjectAgentsDir, getVersionsDir } from './state.js';
|
|
15
|
+
import { getBinaryPath, getVersionHomePath } from './versions.js';
|
|
15
16
|
import { MCP_CAPABLE_AGENTS, AGENTS } from './agents.js';
|
|
16
17
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
17
18
|
/**
|
|
@@ -21,28 +22,60 @@ export function parseMcpServerConfig(filePath) {
|
|
|
21
22
|
if (!fs.existsSync(filePath)) {
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
25
|
+
let parsed;
|
|
24
26
|
try {
|
|
25
27
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
parsed = yaml.parse(content);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return validateMcpYamlConfig(parsed);
|
|
34
|
+
}
|
|
35
|
+
function validateMcpYamlConfig(parsed) {
|
|
36
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const config = parsed;
|
|
40
|
+
if (typeof config.name !== 'string' || config.name.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
if (config.transport !== 'stdio' && config.transport !== 'http')
|
|
43
|
+
return null;
|
|
44
|
+
const result = {
|
|
45
|
+
name: config.name,
|
|
46
|
+
transport: config.transport,
|
|
47
|
+
};
|
|
48
|
+
if (config.transport === 'stdio') {
|
|
49
|
+
if (config.command === undefined || config.command === '')
|
|
32
50
|
return null;
|
|
51
|
+
if (typeof config.command !== 'string') {
|
|
52
|
+
throw new Error(`Invalid MCP config '${config.name}': command must be a string`);
|
|
33
53
|
}
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
54
|
+
result.command = config.command;
|
|
55
|
+
if (config.args !== undefined) {
|
|
56
|
+
if (!Array.isArray(config.args) || !config.args.every((arg) => typeof arg === 'string')) {
|
|
57
|
+
throw new Error(`Invalid MCP config '${config.name}': args must be a string array`);
|
|
58
|
+
}
|
|
59
|
+
result.args = config.args;
|
|
37
60
|
}
|
|
38
|
-
if (
|
|
39
|
-
|
|
61
|
+
if (config.env !== undefined) {
|
|
62
|
+
if (!isStringRecord(config.env)) {
|
|
63
|
+
throw new Error(`Invalid MCP config '${config.name}': env must be a string map`);
|
|
64
|
+
}
|
|
65
|
+
result.env = config.env;
|
|
40
66
|
}
|
|
41
|
-
return parsed;
|
|
42
67
|
}
|
|
43
|
-
|
|
44
|
-
|
|
68
|
+
else {
|
|
69
|
+
if (typeof config.url !== 'string' || config.url.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
result.url = config.url;
|
|
45
72
|
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function isStringRecord(value) {
|
|
76
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
77
|
+
return false;
|
|
78
|
+
return Object.values(value).every((item) => typeof item === 'string');
|
|
46
79
|
}
|
|
47
80
|
/**
|
|
48
81
|
* List all MCP server configs from ~/.agents/mcp/.
|
|
@@ -151,6 +184,38 @@ function installMcpViaCodex(binaryPath, server, versionHome) {
|
|
|
151
184
|
}
|
|
152
185
|
// Note: Codex may not support HTTP MCPs
|
|
153
186
|
}
|
|
187
|
+
export async function registerMcpCommandToTargets(targets, name, commandSpec, scope = 'user', transport = 'stdio') {
|
|
188
|
+
const results = [];
|
|
189
|
+
for (const agentId of targets.directAgents) {
|
|
190
|
+
const result = registerMcpCommand(agentId, name, commandSpec, scope, transport);
|
|
191
|
+
results.push({ agentId, success: result.success, error: result.error });
|
|
192
|
+
}
|
|
193
|
+
for (const [agentId, versions] of targets.versionSelections) {
|
|
194
|
+
for (const version of versions) {
|
|
195
|
+
const result = registerMcpCommand(agentId, name, commandSpec, scope, transport, {
|
|
196
|
+
home: getVersionHomePath(agentId, version),
|
|
197
|
+
binary: getBinaryPath(agentId, version),
|
|
198
|
+
});
|
|
199
|
+
results.push({ agentId, version, success: result.success, error: result.error });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
function registerMcpCommand(agentId, name, commandSpec, scope, transport, options = {}) {
|
|
205
|
+
try {
|
|
206
|
+
const bin = options.binary || AGENTS[agentId].cliCommand;
|
|
207
|
+
const commandArgs = [commandSpec.command, ...commandSpec.args];
|
|
208
|
+
const args = agentId === 'claude'
|
|
209
|
+
? ['mcp', 'add', '--transport', transport, '--scope', scope, name, '--', ...commandArgs]
|
|
210
|
+
: ['mcp', 'add', name, '--', ...commandArgs];
|
|
211
|
+
const env = options.home ? { ...process.env, HOME: options.home } : process.env;
|
|
212
|
+
execFileSync(bin, args, { stdio: 'pipe', timeout: 30000, env });
|
|
213
|
+
return { success: true };
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
return { success: false, error: err.message };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
154
219
|
/**
|
|
155
220
|
* Install MCP server to Gemini config file.
|
|
156
221
|
*/
|
package/dist/lib/migrate.js
CHANGED
|
@@ -828,7 +828,7 @@ function migratePluginsBackToUserRoot() {
|
|
|
828
828
|
* ~/.agents/drive/ -> ~/.agents/.cache/drive/
|
|
829
829
|
* ~/.agents/terminals/ -> ~/.agents/.cache/terminals/
|
|
830
830
|
* ~/.agents/logs/ -> ~/.agents/.cache/logs/
|
|
831
|
-
* ~/.agents/
|
|
831
|
+
* ~/.agents/companion/ -> ~/.agents/.cache/companion/
|
|
832
832
|
* ~/.agents/runtime/ -> ~/.agents/.cache/state/
|
|
833
833
|
* ~/.agents/cache/ -> ~/.agents/.cache/ (flatten — already a cache subdir)
|
|
834
834
|
* ~/.agents/helpers/{daemon,pty,...} -> ~/.agents/.cache/helpers/...
|
|
@@ -858,7 +858,7 @@ function migrateRuntimeToCache() {
|
|
|
858
858
|
// ~/.agents/terminals/live-terminals.json and would race with the move on
|
|
859
859
|
// VS Code restart. Leave the path where the extension expects it.
|
|
860
860
|
moveDirOnce(path.join(USER_DIR, 'logs'), path.join(CACHE_DIR, 'logs'));
|
|
861
|
-
moveDirOnce(path.join(USER_DIR, '
|
|
861
|
+
moveDirOnce(path.join(USER_DIR, 'companion'), path.join(CACHE_DIR, 'companion'));
|
|
862
862
|
moveDirOnce(path.join(USER_DIR, 'runtime'), path.join(CACHE_DIR, 'state'));
|
|
863
863
|
// Pre-existing user `cache/` dir (claude usage cache, cloud-runs, etc.) — flatten
|
|
864
864
|
// so it's not confused with the new bucket. Its contents merge into .cache/.
|
|
@@ -905,7 +905,7 @@ function migrateRuntimeToCache() {
|
|
|
905
905
|
moveDirOnce(path.join(SYSTEM_DIR, '.fetch'), path.join(CACHE_DIR, '.fetch'));
|
|
906
906
|
moveDirOnce(path.join(SYSTEM_DIR, 'browser'), path.join(CACHE_DIR, 'browser'));
|
|
907
907
|
moveDirOnce(path.join(SYSTEM_DIR, 'state'), path.join(CACHE_DIR, 'state'));
|
|
908
|
-
moveDirOnce(path.join(SYSTEM_DIR, '
|
|
908
|
+
moveDirOnce(path.join(SYSTEM_DIR, 'companion'), path.join(CACHE_DIR, 'companion'));
|
|
909
909
|
moveFileOnce(path.join(SYSTEM_DIR, '.cli-version-cache.json'), path.join(CACHE_DIR, '.cli-version-cache.json'));
|
|
910
910
|
moveFileOnce(path.join(SYSTEM_DIR, '.update-check'), path.join(CACHE_DIR, '.update-check'));
|
|
911
911
|
moveFileOnce(path.join(SYSTEM_DIR, '.migrated'), path.join(CACHE_DIR, '.migrated'));
|
package/dist/lib/models.js
CHANGED
|
@@ -210,7 +210,9 @@ function extractClaudeCatalog(text) {
|
|
|
210
210
|
displayNames[constMatch[5]] = constMatch[6];
|
|
211
211
|
}
|
|
212
212
|
const perCloud = {};
|
|
213
|
-
|
|
213
|
+
// The record may carry additional trailing fields (e.g. gateway, eagerInputStreaming)
|
|
214
|
+
// in newer Claude bundles, so the regex does not anchor at the closing brace.
|
|
215
|
+
const perCloudRe = /\{firstParty:"(claude-[^"]+)",bedrock:"([^"]+)"(?:,vertex:"([^"]+)")?(?:,foundry:"([^"]+)")?(?:,anthropicAws:"([^"]+)")?(?:,mantle:(?:null|"([^"]*)"))?/g;
|
|
214
216
|
let m;
|
|
215
217
|
while ((m = perCloudRe.exec(text)) !== null) {
|
|
216
218
|
const id = m[1];
|
|
@@ -3,6 +3,11 @@ import type { AgentId, PermissionSet, InstalledPermission, ClaudePermissions, Op
|
|
|
3
3
|
export declare const PERMISSIONS_CAPABLE_AGENTS: AgentId[];
|
|
4
4
|
/** Filename used for Codex Starlark deny-rules generated from permission groups. */
|
|
5
5
|
export declare const CODEX_RULES_FILENAME = "agents-deny.rules";
|
|
6
|
+
export type ParsedRules = PermissionSet;
|
|
7
|
+
export declare function containsBroadGrants(rules: ParsedRules): {
|
|
8
|
+
broad: string[];
|
|
9
|
+
reason: string;
|
|
10
|
+
} | null;
|
|
6
11
|
/**
|
|
7
12
|
* Convert canonical deny rules to Codex Starlark .rules format.
|
|
8
13
|
* E.g. "Bash(git reset:*)" -> prefix_rule(pattern=["git", "reset"], decision="forbidden")
|