@phnx-labs/agents-cli 1.15.0 → 1.16.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 +78 -39
- package/README.md +6 -6
- package/dist/commands/alias.js +2 -2
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +546 -75
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +81 -39
- package/dist/commands/plugins.js +48 -15
- package/dist/commands/prune.js +23 -1
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +37 -1
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +22 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/view.js +61 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -20
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +29 -1
- package/dist/lib/browser/chrome.js +5 -2
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +9 -4
- package/dist/lib/browser/drivers/ssh.js +9 -2
- package/dist/lib/browser/ipc.js +144 -23
- package/dist/lib/browser/profiles.d.ts +5 -2
- package/dist/lib/browser/profiles.js +77 -37
- package/dist/lib/browser/service.d.ts +81 -13
- package/dist/lib/browser/service.js +738 -131
- package/dist/lib/browser/types.d.ts +81 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -0
- package/dist/lib/commands.js +6 -2
- package/dist/lib/daemon.js +2 -3
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.js +2 -2
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +125 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1178 -21
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +8 -8
- package/dist/lib/permissions.js +8 -8
- package/dist/lib/plugins.d.ts +30 -1
- package/dist/lib/plugins.js +75 -3
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.js +8 -3
- package/dist/lib/session/discover.js +30 -15
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +6 -6
- package/dist/lib/skills.js +6 -2
- package/dist/lib/state.d.ts +86 -14
- package/dist/lib/state.js +150 -23
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +32 -3
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.js +2 -2
- package/dist/lib/versions.js +20 -21
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
|
@@ -16,12 +16,17 @@ export interface BrowserProfile {
|
|
|
16
16
|
export interface ChromeOptions {
|
|
17
17
|
headless?: boolean;
|
|
18
18
|
args?: string[];
|
|
19
|
+
viewport?: {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
19
23
|
}
|
|
20
24
|
export interface Task {
|
|
21
25
|
id: string;
|
|
26
|
+
name: string;
|
|
22
27
|
profile: string;
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
tabs: Record<string, string>;
|
|
29
|
+
currentTabId?: string;
|
|
25
30
|
createdAt: number;
|
|
26
31
|
pid: number;
|
|
27
32
|
}
|
|
@@ -36,17 +41,39 @@ export interface ProfileStatus {
|
|
|
36
41
|
running: boolean;
|
|
37
42
|
port?: number;
|
|
38
43
|
pid?: number;
|
|
44
|
+
/** The port declared in the profile's first endpoint, when it differs from the running port. */
|
|
45
|
+
configuredPort?: number;
|
|
39
46
|
tasks: TaskStatus[];
|
|
40
47
|
}
|
|
41
48
|
export interface TaskStatus {
|
|
42
49
|
id: string;
|
|
50
|
+
name: string;
|
|
43
51
|
tabCount: number;
|
|
52
|
+
currentTabId?: string;
|
|
44
53
|
createdAt: number;
|
|
54
|
+
endedAt?: number;
|
|
55
|
+
domains?: string[];
|
|
56
|
+
tabs?: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
url: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
current?: boolean;
|
|
61
|
+
}>;
|
|
45
62
|
}
|
|
46
|
-
export
|
|
63
|
+
export interface HistoricalTask {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
profile: string;
|
|
67
|
+
createdAt: number;
|
|
68
|
+
endedAt: number;
|
|
69
|
+
domains: string[];
|
|
70
|
+
tabCount: number;
|
|
71
|
+
}
|
|
72
|
+
export type IPCAction = 'start' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download';
|
|
47
73
|
export interface IPCRequest {
|
|
48
74
|
action: IPCAction;
|
|
49
75
|
task?: string;
|
|
76
|
+
taskName?: string;
|
|
50
77
|
profile?: string;
|
|
51
78
|
url?: string;
|
|
52
79
|
tabId?: string;
|
|
@@ -57,6 +84,20 @@ export interface IPCRequest {
|
|
|
57
84
|
key?: string;
|
|
58
85
|
interactive?: boolean;
|
|
59
86
|
limit?: number;
|
|
87
|
+
width?: number;
|
|
88
|
+
height?: number;
|
|
89
|
+
deviceName?: string;
|
|
90
|
+
mobile?: boolean;
|
|
91
|
+
deviceScaleFactor?: number;
|
|
92
|
+
level?: 'log' | 'info' | 'warn' | 'error';
|
|
93
|
+
clear?: boolean;
|
|
94
|
+
filter?: string;
|
|
95
|
+
urlPattern?: string;
|
|
96
|
+
maxChars?: number;
|
|
97
|
+
waitType?: 'time' | 'selector' | 'url' | 'function' | 'load';
|
|
98
|
+
waitValue?: string | number;
|
|
99
|
+
timeout?: number;
|
|
100
|
+
downloadPath?: string;
|
|
60
101
|
}
|
|
61
102
|
export interface IPCResponse {
|
|
62
103
|
ok: boolean;
|
|
@@ -66,10 +107,47 @@ export interface IPCResponse {
|
|
|
66
107
|
windowTargetId?: string;
|
|
67
108
|
tabs?: TabInfo[];
|
|
68
109
|
profiles?: ProfileStatus[];
|
|
110
|
+
history?: HistoricalTask[];
|
|
69
111
|
result?: unknown;
|
|
70
112
|
path?: string;
|
|
71
113
|
refs?: string;
|
|
114
|
+
logs?: ConsoleEntry[];
|
|
115
|
+
errors?: ErrorEntry[];
|
|
116
|
+
requests?: NetworkRequest[];
|
|
117
|
+
body?: string;
|
|
118
|
+
downloadPath?: string;
|
|
119
|
+
devices?: string[];
|
|
120
|
+
}
|
|
121
|
+
export interface ConsoleEntry {
|
|
122
|
+
level: 'log' | 'info' | 'warn' | 'error';
|
|
123
|
+
text: string;
|
|
124
|
+
timestamp: number;
|
|
125
|
+
url?: string;
|
|
126
|
+
line?: number;
|
|
127
|
+
}
|
|
128
|
+
export interface ErrorEntry {
|
|
129
|
+
message: string;
|
|
130
|
+
stack?: string;
|
|
131
|
+
timestamp: number;
|
|
132
|
+
url?: string;
|
|
133
|
+
line?: number;
|
|
134
|
+
}
|
|
135
|
+
export interface NetworkRequest {
|
|
136
|
+
id: string;
|
|
137
|
+
url: string;
|
|
138
|
+
method: string;
|
|
139
|
+
status?: number;
|
|
140
|
+
mimeType?: string;
|
|
141
|
+
timestamp: number;
|
|
142
|
+
}
|
|
143
|
+
export interface DeviceDescriptor {
|
|
144
|
+
width: number;
|
|
145
|
+
height: number;
|
|
146
|
+
deviceScaleFactor: number;
|
|
147
|
+
mobile: boolean;
|
|
72
148
|
}
|
|
73
149
|
export declare const TASK_ID_REGEX: RegExp;
|
|
74
150
|
export declare function isValidTaskId(id: string): boolean;
|
|
75
151
|
export declare function generateTaskId(): string;
|
|
152
|
+
export declare function generateShortId(): string;
|
|
153
|
+
export declare function generateFunName(): string;
|
|
@@ -5,3 +5,19 @@ export function isValidTaskId(id) {
|
|
|
5
5
|
export function generateTaskId() {
|
|
6
6
|
return crypto.randomUUID().slice(0, 8);
|
|
7
7
|
}
|
|
8
|
+
export function generateShortId() {
|
|
9
|
+
return crypto.randomUUID().split('-')[0]; // 8 chars
|
|
10
|
+
}
|
|
11
|
+
const ADJECTIVES = [
|
|
12
|
+
'swift', 'cosmic', 'jolly', 'quiet', 'bold', 'bright', 'calm', 'eager',
|
|
13
|
+
'golden', 'happy', 'keen', 'lucky', 'noble', 'proud', 'quick', 'royal',
|
|
14
|
+
];
|
|
15
|
+
const NOUNS = [
|
|
16
|
+
'falcon', 'comet', 'tiger', 'nebula', 'phoenix', 'river', 'summit', 'wave',
|
|
17
|
+
'aurora', 'breeze', 'crystal', 'dragon', 'ember', 'forest', 'glacier', 'harbor',
|
|
18
|
+
];
|
|
19
|
+
export function generateFunName() {
|
|
20
|
+
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
21
|
+
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
22
|
+
return `${adj}-${noun}`;
|
|
23
|
+
}
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as path from 'path';
|
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
|
-
import {
|
|
12
|
+
import { getCloudDir } from '../state.js';
|
|
13
13
|
import { resolveDispatchRepos } from './types.js';
|
|
14
14
|
import { parseSSE } from './stream.js';
|
|
15
15
|
import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
@@ -20,7 +20,7 @@ const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
|
20
20
|
// Persistent consent record for uploading Claude OAuth blobs to Rush Cloud.
|
|
21
21
|
// Created on first explicit consent (env var or flag); subsequent dispatches
|
|
22
22
|
// see it and proceed without re-prompting.
|
|
23
|
-
const RUSH_CONSENT_PATH = path.join(
|
|
23
|
+
const RUSH_CONSENT_PATH = path.join(getCloudDir(), 'rush-consent.json');
|
|
24
24
|
const RUSH_CONSENT_ENV = 'AGENTS_RUSH_UPLOAD_TOKENS';
|
|
25
25
|
function hasRushUploadConsent(opts) {
|
|
26
26
|
if (process.env[RUSH_CONSENT_ENV] === '1')
|
package/dist/lib/cloud/store.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import Database from '../sqlite.js';
|
|
11
|
-
import {
|
|
12
|
-
const CLOUD_DIR =
|
|
11
|
+
import { getCloudDir } from '../state.js';
|
|
12
|
+
const CLOUD_DIR = getCloudDir();
|
|
13
13
|
const DB_PATH = path.join(CLOUD_DIR, 'tasks.db');
|
|
14
14
|
const SCHEMA = `
|
|
15
15
|
CREATE TABLE IF NOT EXISTS tasks (
|
package/dist/lib/commands.d.ts
CHANGED
|
@@ -79,6 +79,7 @@ export declare function installCommandToVersion(agent: AgentId, version: string,
|
|
|
79
79
|
};
|
|
80
80
|
/**
|
|
81
81
|
* Remove a single command from a specific version home.
|
|
82
|
+
* Soft-deletes to ~/.agents/.trash/commands/.
|
|
82
83
|
*/
|
|
83
84
|
export declare function removeCommandFromVersion(agent: AgentId, version: string, commandName: string): {
|
|
84
85
|
success: boolean;
|
package/dist/lib/commands.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as path from 'path';
|
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { AGENTS, COMMANDS_CAPABLE_AGENTS, ensureCommandsDir } from './agents.js';
|
|
13
13
|
import { markdownToToml } from './convert.js';
|
|
14
|
-
import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir } from './state.js';
|
|
14
|
+
import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir, getTrashCommandsDir } from './state.js';
|
|
15
15
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
16
16
|
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
17
17
|
/** Parse command metadata (name, description) from YAML frontmatter or TOML headers. */
|
|
@@ -279,6 +279,7 @@ export function installCommandToVersion(agent, version, commandName, method = 'c
|
|
|
279
279
|
}
|
|
280
280
|
/**
|
|
281
281
|
* Remove a single command from a specific version home.
|
|
282
|
+
* Soft-deletes to ~/.agents/.trash/commands/.
|
|
282
283
|
*/
|
|
283
284
|
export function removeCommandFromVersion(agent, version, commandName) {
|
|
284
285
|
const versionHome = getVersionHomePath(agent, version);
|
|
@@ -292,7 +293,10 @@ export function removeCommandFromVersion(agent, version, commandName) {
|
|
|
292
293
|
return { success: true };
|
|
293
294
|
}
|
|
294
295
|
try {
|
|
295
|
-
|
|
296
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
297
|
+
const trashDir = path.join(getTrashCommandsDir(), agent, version, commandName);
|
|
298
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
299
|
+
fs.renameSync(targetPath, path.join(trashDir, `${commandName}${ext}.${stamp}`));
|
|
296
300
|
}
|
|
297
301
|
catch (err) {
|
|
298
302
|
return { success: false, error: err.message };
|
package/dist/lib/daemon.js
CHANGED
|
@@ -10,13 +10,12 @@ import { spawn, execSync, execFileSync } from 'child_process';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
13
|
-
import {
|
|
13
|
+
import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
14
14
|
import { listJobs as listAllJobs } from './routines.js';
|
|
15
15
|
import { JobScheduler } from './scheduler.js';
|
|
16
16
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
17
17
|
import { BrowserService } from './browser/service.js';
|
|
18
18
|
import { BrowserIPCServer } from './browser/ipc.js';
|
|
19
|
-
const DAEMON_DIR = 'helpers/daemon';
|
|
20
19
|
const PID_FILE = 'daemon.pid';
|
|
21
20
|
const LOCK_FILE = 'daemon.lock';
|
|
22
21
|
const LOG_FILE = 'logs.jsonl';
|
|
@@ -25,7 +24,7 @@ const LOG_ROTATE_COUNT = 3;
|
|
|
25
24
|
const PLIST_NAME = 'com.phnx-labs.agents-daemon';
|
|
26
25
|
const SYSTEMD_UNIT = 'agents-daemon.service';
|
|
27
26
|
function getDaemonDir() {
|
|
28
|
-
const dir =
|
|
27
|
+
const dir = getDaemonDirRoot();
|
|
29
28
|
fs.mkdirSync(dir, { recursive: true });
|
|
30
29
|
return dir;
|
|
31
30
|
}
|
package/dist/lib/doctor-diff.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import * as fs from 'fs';
|
|
23
23
|
import * as path from 'path';
|
|
24
24
|
import { AGENTS } from './agents.js';
|
|
25
|
-
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir,
|
|
25
|
+
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir, getEffectivePromptcutsPath, } from './state.js';
|
|
26
26
|
import { getAvailableResources, getActuallySyncedResources, getVersionHomePath, } from './versions.js';
|
|
27
27
|
import { markdownToToml } from './convert.js';
|
|
28
28
|
import { resolveImports, supportsRulesImports } from './rules/compile.js';
|
|
@@ -426,10 +426,10 @@ function diffPresenceOnly(kind, available, synced) {
|
|
|
426
426
|
return rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
427
427
|
}
|
|
428
428
|
function diffPromptcuts() {
|
|
429
|
-
const
|
|
430
|
-
if (!
|
|
429
|
+
const sourcePath = getEffectivePromptcutsPath();
|
|
430
|
+
if (!fs.existsSync(sourcePath))
|
|
431
431
|
return [];
|
|
432
|
-
return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath
|
|
432
|
+
return [{ kind: 'promptcuts', name: 'promptcuts.yaml', status: 'ok', sourcePath }];
|
|
433
433
|
}
|
|
434
434
|
export function diffVersionResources(agent, version, options = {}) {
|
|
435
435
|
const cwd = options.cwd ?? process.cwd();
|
package/dist/lib/events.js
CHANGED
|
@@ -15,8 +15,8 @@ import * as fs from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import * as os from 'os';
|
|
17
17
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const LOGS_DIR = path.join(
|
|
18
|
+
// Logs live under the cache bucket — they're regenerable telemetry.
|
|
19
|
+
const LOGS_DIR = path.join(os.homedir(), '.agents', '.cache', 'logs');
|
|
20
20
|
/** Default retention period in days. */
|
|
21
21
|
const DEFAULT_RETENTION_DAYS = 30;
|
|
22
22
|
/** Default max length for truncated strings. */
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare function installHookToVersion(agent: AgentId, version: string, ho
|
|
|
66
66
|
};
|
|
67
67
|
/**
|
|
68
68
|
* Remove a single hook (script + data file) from a specific version home.
|
|
69
|
+
* Soft-deletes to ~/.agents/.trash/hooks/.
|
|
69
70
|
*/
|
|
70
71
|
export declare function removeHookFromVersion(agent: AgentId, version: string, hookName: string): {
|
|
71
72
|
success: boolean;
|
|
@@ -113,10 +114,11 @@ export declare function installHooksCentrally(source: string): Promise<{
|
|
|
113
114
|
*/
|
|
114
115
|
export declare function listCentralHooks(): HookEntry[];
|
|
115
116
|
/**
|
|
116
|
-
* Parse
|
|
117
|
-
* and user
|
|
118
|
-
*
|
|
119
|
-
*
|
|
117
|
+
* Parse hook manifests. Reads system hooks from ~/.agents-system/hooks.yaml
|
|
118
|
+
* (npm-shipped defaults) and user hooks from the `hooks:` section of
|
|
119
|
+
* ~/.agents/agents.yaml. Merges with user-wins-on-key-collision precedence.
|
|
120
|
+
* A user entry with `enabled: false` disables the system-shipped hook of
|
|
121
|
+
* the same name without forking the system file.
|
|
120
122
|
*
|
|
121
123
|
* Hooks marked `enabled: false` are dropped from the returned map.
|
|
122
124
|
*/
|
|
@@ -124,10 +126,12 @@ export declare function parseHookManifest(): Record<string, ManifestHook>;
|
|
|
124
126
|
/**
|
|
125
127
|
* Register hooks as lifecycle events in an agent's config.
|
|
126
128
|
* Reads hooks.yaml manifest, merges into the agent's config file(s).
|
|
127
|
-
* Only manages hooks whose command paths are under ~/.agents/hooks
|
|
128
|
-
* Does not remove user-added hooks.
|
|
129
|
+
* Only manages hooks whose command paths are under ~/.agents/hooks/ or
|
|
130
|
+
* ~/.agents-system/hooks/. Does not remove user-added hooks.
|
|
129
131
|
*
|
|
130
|
-
* @param agentsDirOverride -
|
|
132
|
+
* @param agentsDirOverride - When provided, treats this single dir as the
|
|
133
|
+
* only managed hook root. Used by tests to inject a temp path. In normal
|
|
134
|
+
* operation, both user and system roots are consulted with user precedence.
|
|
131
135
|
*/
|
|
132
136
|
export declare function registerHooksToSettings(agentId: AgentId, versionHome: string, hookManifest?: Record<string, ManifestHook>, agentsDirOverride?: string): {
|
|
133
137
|
registered: string[];
|
package/dist/lib/hooks.js
CHANGED
|
@@ -14,8 +14,42 @@ import * as TOML from 'smol-toml';
|
|
|
14
14
|
import { AGENTS, HOOKS_CAPABLE_AGENTS } from './agents.js';
|
|
15
15
|
import { supports, explainSkip } from './capabilities.js';
|
|
16
16
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
17
|
-
import {
|
|
17
|
+
import { getHooksDir as getSystemHooksDir, getUserHooksDir, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getTrashHooksDir } from './state.js';
|
|
18
18
|
function getCentralHooksDir() { return getUserHooksDir(); }
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a hook script's absolute path by checking the user dir first
|
|
21
|
+
* (where `installHooksCentrally` lands new files) and falling back to the
|
|
22
|
+
* system dir (where npm-shipped defaults live). Returns null if neither
|
|
23
|
+
* exists. Mirrors the precedence used by `listCentralHooks`.
|
|
24
|
+
*/
|
|
25
|
+
function resolveHookScriptPath(script) {
|
|
26
|
+
for (const root of [getUserAgentsDir(), getSystemAgentsDir()]) {
|
|
27
|
+
const candidate = path.join(root, 'hooks', script);
|
|
28
|
+
if (fs.existsSync(candidate))
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Prefixes used for stale-entry cleanup in agent settings files. A registered
|
|
35
|
+
* hook command is considered "managed by us" if it lives under either
|
|
36
|
+
* `~/.agents/hooks/` (user) or `~/.agents-system/hooks/` (system). Cleanup
|
|
37
|
+
* filters use this list so leftover entries from either dir get garbage
|
|
38
|
+
* collected on rewrite.
|
|
39
|
+
*/
|
|
40
|
+
function getManagedHookPrefixes() {
|
|
41
|
+
return [
|
|
42
|
+
path.join(getUserAgentsDir(), 'hooks') + path.sep,
|
|
43
|
+
path.join(getSystemAgentsDir(), 'hooks') + path.sep,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function isManagedHookCommand(command, prefixes) {
|
|
47
|
+
for (const prefix of prefixes) {
|
|
48
|
+
if (command.startsWith(prefix))
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
19
53
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
20
54
|
const SCRIPT_EXTENSIONS = new Set([
|
|
21
55
|
'.sh',
|
|
@@ -366,10 +400,32 @@ export function installHookToVersion(agent, version, hookName) {
|
|
|
366
400
|
}
|
|
367
401
|
/**
|
|
368
402
|
* Remove a single hook (script + data file) from a specific version home.
|
|
403
|
+
* Soft-deletes to ~/.agents/.trash/hooks/.
|
|
369
404
|
*/
|
|
370
405
|
export function removeHookFromVersion(agent, version, hookName) {
|
|
371
406
|
try {
|
|
372
|
-
|
|
407
|
+
const hooksDir = getVersionHooksDir(agent, version);
|
|
408
|
+
if (!fs.existsSync(hooksDir))
|
|
409
|
+
return { success: true };
|
|
410
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
411
|
+
const trashDir = path.join(getTrashHooksDir(), agent, version, hookName, stamp);
|
|
412
|
+
let moved = false;
|
|
413
|
+
const files = fs.readdirSync(hooksDir);
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
const ext = path.extname(file);
|
|
416
|
+
const base = path.basename(file, ext);
|
|
417
|
+
if (base === hookName) {
|
|
418
|
+
const fullPath = path.join(hooksDir, file);
|
|
419
|
+
const stat = fs.statSync(fullPath);
|
|
420
|
+
if (stat.isFile()) {
|
|
421
|
+
if (!moved) {
|
|
422
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
423
|
+
moved = true;
|
|
424
|
+
}
|
|
425
|
+
fs.renameSync(fullPath, path.join(trashDir, file));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
373
429
|
}
|
|
374
430
|
catch (err) {
|
|
375
431
|
return { success: false, error: err.message };
|
|
@@ -502,32 +558,37 @@ export function listCentralHooks() {
|
|
|
502
558
|
return results;
|
|
503
559
|
}
|
|
504
560
|
/**
|
|
505
|
-
* Parse
|
|
506
|
-
* and user
|
|
507
|
-
*
|
|
508
|
-
*
|
|
561
|
+
* Parse hook manifests. Reads system hooks from ~/.agents-system/hooks.yaml
|
|
562
|
+
* (npm-shipped defaults) and user hooks from the `hooks:` section of
|
|
563
|
+
* ~/.agents/agents.yaml. Merges with user-wins-on-key-collision precedence.
|
|
564
|
+
* A user entry with `enabled: false` disables the system-shipped hook of
|
|
565
|
+
* the same name without forking the system file.
|
|
509
566
|
*
|
|
510
567
|
* Hooks marked `enabled: false` are dropped from the returned map.
|
|
511
568
|
*/
|
|
512
569
|
export function parseHookManifest() {
|
|
513
570
|
const merged = {};
|
|
514
|
-
// System
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (!fs.existsSync(manifestPath))
|
|
518
|
-
continue;
|
|
571
|
+
// System layer: hooks: section of agents.yaml (npm-shipped, separate repo).
|
|
572
|
+
const systemPath = path.join(getSystemAgentsDir(), 'agents.yaml');
|
|
573
|
+
if (fs.existsSync(systemPath)) {
|
|
519
574
|
try {
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
for (const [name, def] of Object.entries(parsed)) {
|
|
525
|
-
merged[name] = def;
|
|
526
|
-
}
|
|
575
|
+
const meta = yaml.parse(fs.readFileSync(systemPath, 'utf-8'));
|
|
576
|
+
if (meta?.hooks)
|
|
577
|
+
for (const [name, def] of Object.entries(meta.hooks))
|
|
578
|
+
merged[name] = def;
|
|
527
579
|
}
|
|
528
|
-
catch {
|
|
529
|
-
|
|
580
|
+
catch { /* skip unreadable manifest */ }
|
|
581
|
+
}
|
|
582
|
+
// User layer: hooks: section of agents.yaml.
|
|
583
|
+
const userMetaPath = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
584
|
+
if (fs.existsSync(userMetaPath)) {
|
|
585
|
+
try {
|
|
586
|
+
const meta = yaml.parse(fs.readFileSync(userMetaPath, 'utf-8'));
|
|
587
|
+
if (meta?.hooks)
|
|
588
|
+
for (const [name, def] of Object.entries(meta.hooks))
|
|
589
|
+
merged[name] = def;
|
|
530
590
|
}
|
|
591
|
+
catch { /* skip unreadable meta */ }
|
|
531
592
|
}
|
|
532
593
|
// Strip disabled hooks so they never reach the registrar.
|
|
533
594
|
for (const [name, def] of Object.entries(merged)) {
|
|
@@ -542,25 +603,37 @@ const CODEX_MATCHER_EVENTS = new Set(['PreToolUse', 'PostToolUse', 'SessionStart
|
|
|
542
603
|
/**
|
|
543
604
|
* Register hooks as lifecycle events in an agent's config.
|
|
544
605
|
* Reads hooks.yaml manifest, merges into the agent's config file(s).
|
|
545
|
-
* Only manages hooks whose command paths are under ~/.agents/hooks
|
|
546
|
-
* Does not remove user-added hooks.
|
|
606
|
+
* Only manages hooks whose command paths are under ~/.agents/hooks/ or
|
|
607
|
+
* ~/.agents-system/hooks/. Does not remove user-added hooks.
|
|
547
608
|
*
|
|
548
|
-
* @param agentsDirOverride -
|
|
609
|
+
* @param agentsDirOverride - When provided, treats this single dir as the
|
|
610
|
+
* only managed hook root. Used by tests to inject a temp path. In normal
|
|
611
|
+
* operation, both user and system roots are consulted with user precedence.
|
|
549
612
|
*/
|
|
550
613
|
export function registerHooksToSettings(agentId, versionHome, hookManifest, agentsDirOverride) {
|
|
551
614
|
const manifest = hookManifest || parseHookManifest();
|
|
552
615
|
if (Object.keys(manifest).length === 0) {
|
|
553
616
|
return { registered: [], errors: [] };
|
|
554
617
|
}
|
|
555
|
-
const
|
|
618
|
+
const overrideRoots = agentsDirOverride ? [agentsDirOverride] : null;
|
|
619
|
+
const resolveScript = (script) => {
|
|
620
|
+
if (overrideRoots) {
|
|
621
|
+
const candidate = path.join(overrideRoots[0], 'hooks', script);
|
|
622
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
623
|
+
}
|
|
624
|
+
return resolveHookScriptPath(script);
|
|
625
|
+
};
|
|
626
|
+
const managedPrefixes = overrideRoots
|
|
627
|
+
? [path.join(overrideRoots[0], 'hooks') + path.sep]
|
|
628
|
+
: getManagedHookPrefixes();
|
|
556
629
|
if (agentId === 'claude') {
|
|
557
|
-
return registerHooksForClaude(versionHome, manifest,
|
|
630
|
+
return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes);
|
|
558
631
|
}
|
|
559
632
|
if (agentId === 'codex') {
|
|
560
|
-
return registerHooksForCodex(versionHome, manifest,
|
|
633
|
+
return registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes);
|
|
561
634
|
}
|
|
562
635
|
if (agentId === 'gemini') {
|
|
563
|
-
return registerHooksForGemini(versionHome, manifest,
|
|
636
|
+
return registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes);
|
|
564
637
|
}
|
|
565
638
|
return { registered: [], errors: [] };
|
|
566
639
|
}
|
|
@@ -574,7 +647,7 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
574
647
|
const GEMINI_EVENT_MAP = {
|
|
575
648
|
UserPromptSubmit: 'BeforeAgent',
|
|
576
649
|
};
|
|
577
|
-
function registerHooksForClaude(versionHome, manifest,
|
|
650
|
+
function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
578
651
|
const registered = [];
|
|
579
652
|
const errors = [];
|
|
580
653
|
const configDir = path.join(versionHome, '.claude');
|
|
@@ -595,14 +668,15 @@ function registerHooksForClaude(versionHome, manifest, agentsDir) {
|
|
|
595
668
|
const hooks = config.hooks;
|
|
596
669
|
// Build set of all command paths the current manifest will register.
|
|
597
670
|
// Used to garbage-collect stale entries left behind after hook renames.
|
|
598
|
-
const managedHooksPrefix = path.join(agentsDir, 'hooks') + path.sep;
|
|
599
671
|
const currentManifestPaths = new Set();
|
|
600
672
|
for (const hookDef of Object.values(manifest)) {
|
|
601
673
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
602
674
|
continue;
|
|
603
|
-
|
|
675
|
+
const resolved = resolveScript(hookDef.script);
|
|
676
|
+
if (resolved)
|
|
677
|
+
currentManifestPaths.add(resolved);
|
|
604
678
|
}
|
|
605
|
-
// Remove stale entries: any hook command under
|
|
679
|
+
// Remove stale entries: any hook command under a managed root that isn't
|
|
606
680
|
// in the current manifest is a leftover from a renamed/deleted hook script.
|
|
607
681
|
for (const eventEntries of Object.values(hooks)) {
|
|
608
682
|
if (!Array.isArray(eventEntries))
|
|
@@ -610,7 +684,7 @@ function registerHooksForClaude(versionHome, manifest, agentsDir) {
|
|
|
610
684
|
for (const group of eventEntries) {
|
|
611
685
|
if (!group.hooks)
|
|
612
686
|
continue;
|
|
613
|
-
group.hooks = group.hooks.filter((h) => !h.command
|
|
687
|
+
group.hooks = group.hooks.filter((h) => !isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command));
|
|
614
688
|
}
|
|
615
689
|
}
|
|
616
690
|
// Remove empty matcher groups left after cleanup
|
|
@@ -622,9 +696,9 @@ function registerHooksForClaude(versionHome, manifest, agentsDir) {
|
|
|
622
696
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
623
697
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
624
698
|
continue;
|
|
625
|
-
const commandPath =
|
|
626
|
-
if (!
|
|
627
|
-
errors.push(`${name}: script not found
|
|
699
|
+
const commandPath = resolveScript(hookDef.script);
|
|
700
|
+
if (!commandPath) {
|
|
701
|
+
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
628
702
|
continue;
|
|
629
703
|
}
|
|
630
704
|
for (const event of hookDef.events) {
|
|
@@ -662,7 +736,7 @@ function registerHooksForClaude(versionHome, manifest, agentsDir) {
|
|
|
662
736
|
}
|
|
663
737
|
return { registered, errors };
|
|
664
738
|
}
|
|
665
|
-
function registerHooksForCodex(versionHome, manifest,
|
|
739
|
+
function registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
666
740
|
const registered = [];
|
|
667
741
|
const errors = [];
|
|
668
742
|
const configDir = path.join(versionHome, '.codex');
|
|
@@ -687,19 +761,20 @@ function registerHooksForCodex(versionHome, manifest, agentsDir) {
|
|
|
687
761
|
}
|
|
688
762
|
}
|
|
689
763
|
// Build set of current manifest command paths for codex to GC stale entries
|
|
690
|
-
const managedHooksPrefix = path.join(agentsDir, 'hooks') + path.sep;
|
|
691
764
|
const currentManifestPaths = new Set();
|
|
692
765
|
for (const hookDef of Object.values(manifest)) {
|
|
693
766
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
694
767
|
continue;
|
|
695
|
-
|
|
768
|
+
const resolved = resolveScript(hookDef.script);
|
|
769
|
+
if (resolved)
|
|
770
|
+
currentManifestPaths.add(resolved);
|
|
696
771
|
}
|
|
697
772
|
// Remove stale entries from all event groups
|
|
698
773
|
for (const eventGroups of Object.values(hooksFile.hooks)) {
|
|
699
774
|
for (const group of eventGroups) {
|
|
700
775
|
if (!group.hooks)
|
|
701
776
|
continue;
|
|
702
|
-
group.hooks = group.hooks.filter((h) => !h.command
|
|
777
|
+
group.hooks = group.hooks.filter((h) => !isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command));
|
|
703
778
|
}
|
|
704
779
|
}
|
|
705
780
|
for (const [event, eventGroups] of Object.entries(hooksFile.hooks)) {
|
|
@@ -708,9 +783,9 @@ function registerHooksForCodex(versionHome, manifest, agentsDir) {
|
|
|
708
783
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
709
784
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
710
785
|
continue;
|
|
711
|
-
const commandPath =
|
|
712
|
-
if (!
|
|
713
|
-
errors.push(`${name}: script not found
|
|
786
|
+
const commandPath = resolveScript(hookDef.script);
|
|
787
|
+
if (!commandPath) {
|
|
788
|
+
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
714
789
|
continue;
|
|
715
790
|
}
|
|
716
791
|
const timeout = hookDef.timeout || 600;
|
|
@@ -784,7 +859,7 @@ function registerHooksForCodex(versionHome, manifest, agentsDir) {
|
|
|
784
859
|
}
|
|
785
860
|
return { registered, errors };
|
|
786
861
|
}
|
|
787
|
-
function registerHooksForGemini(versionHome, manifest,
|
|
862
|
+
function registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
788
863
|
const registered = [];
|
|
789
864
|
const errors = [];
|
|
790
865
|
const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
|
|
@@ -795,12 +870,13 @@ function registerHooksForGemini(versionHome, manifest, agentsDir) {
|
|
|
795
870
|
config.hooks = {};
|
|
796
871
|
}
|
|
797
872
|
const hooks = config.hooks;
|
|
798
|
-
const managedHooksPrefix = path.join(agentsDir, 'hooks') + path.sep;
|
|
799
873
|
const currentManifestPaths = new Set();
|
|
800
874
|
for (const hookDef of Object.values(manifest)) {
|
|
801
875
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
802
876
|
continue;
|
|
803
|
-
|
|
877
|
+
const resolved = resolveScript(hookDef.script);
|
|
878
|
+
if (resolved)
|
|
879
|
+
currentManifestPaths.add(resolved);
|
|
804
880
|
}
|
|
805
881
|
for (const eventEntries of Object.values(hooks)) {
|
|
806
882
|
if (!Array.isArray(eventEntries))
|
|
@@ -808,7 +884,7 @@ function registerHooksForGemini(versionHome, manifest, agentsDir) {
|
|
|
808
884
|
for (const group of eventEntries) {
|
|
809
885
|
if (!group.hooks)
|
|
810
886
|
continue;
|
|
811
|
-
group.hooks = group.hooks.filter((h) => !h.command
|
|
887
|
+
group.hooks = group.hooks.filter((h) => !isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command));
|
|
812
888
|
}
|
|
813
889
|
}
|
|
814
890
|
for (const [event, eventEntries] of Object.entries(hooks)) {
|
|
@@ -819,9 +895,9 @@ function registerHooksForGemini(versionHome, manifest, agentsDir) {
|
|
|
819
895
|
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
820
896
|
if (!hookDef.events || hookDef.events.length === 0)
|
|
821
897
|
continue;
|
|
822
|
-
const commandPath =
|
|
823
|
-
if (!
|
|
824
|
-
errors.push(`${name}: script not found
|
|
898
|
+
const commandPath = resolveScript(hookDef.script);
|
|
899
|
+
if (!commandPath) {
|
|
900
|
+
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
825
901
|
continue;
|
|
826
902
|
}
|
|
827
903
|
const timeoutMs = (hookDef.timeout || 600) * 1000;
|
package/dist/lib/migrate.d.ts
CHANGED