@phnx-labs/agents-cli 1.14.3 → 1.14.5
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 +10 -0
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +7 -0
- package/dist/commands/browser.d.ts +1 -0
- package/dist/commands/browser.js +4 -0
- package/dist/commands/teams.js +25 -3
- package/dist/commands/view.js +1 -1
- package/dist/index.js +0 -0
- package/dist/lib/browser/chrome.d.ts +2 -2
- package/dist/lib/browser/chrome.js +15 -3
- package/dist/lib/browser/drivers/local.js +1 -1
- package/dist/lib/browser/drivers/ssh.js +14 -5
- package/dist/lib/browser/service.js +24 -3
- package/dist/lib/browser/types.d.ts +3 -1
- package/dist/lib/migrate.js +46 -0
- package/dist/lib/resources/commands.d.ts +46 -0
- package/dist/lib/resources/commands.js +208 -0
- package/dist/lib/resources/hooks.d.ts +12 -0
- package/dist/lib/resources/hooks.js +136 -0
- package/dist/lib/resources/index.d.ts +36 -0
- package/dist/lib/resources/index.js +69 -0
- package/dist/lib/resources/mcp.d.ts +34 -0
- package/dist/lib/resources/mcp.js +483 -0
- package/dist/lib/resources/permissions.d.ts +13 -0
- package/dist/lib/resources/permissions.js +184 -0
- package/dist/lib/resources/rules.d.ts +43 -0
- package/dist/lib/resources/rules.js +146 -0
- package/dist/lib/resources/skills.d.ts +37 -0
- package/dist/lib/resources/skills.js +238 -0
- package/dist/lib/resources/subagents.d.ts +46 -0
- package/dist/lib/resources/subagents.js +198 -0
- package/dist/lib/resources/types.d.ts +82 -0
- package/dist/lib/resources/types.js +8 -0
- package/dist/lib/state.js +3 -5
- package/dist/lib/teams/registry.d.ts +4 -0
- package/dist/lib/teams/registry.js +4 -0
- package/dist/lib/versions.d.ts +2 -1
- package/dist/lib/versions.js +20 -14
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.5
|
|
4
|
+
|
|
5
|
+
**Browser: custom binary and Electron app support**
|
|
6
|
+
|
|
7
|
+
- Added `binary` field to browser profiles for specifying custom executable paths (e.g., Electron apps like Rush)
|
|
8
|
+
- Added `electron` field to browser profiles — when true, uses existing windows instead of creating new ones (Electron doesn't support `Target.createTarget`)
|
|
9
|
+
- New `custom` browser type that requires a binary path
|
|
10
|
+
- Works with both local and SSH-based browser connections
|
|
11
|
+
- Example profile for Rush: `agents browser profiles edit rush --browser custom --binary "/Applications/Rush.app/Contents/MacOS/Rush" --electron`
|
|
12
|
+
|
|
3
13
|
## Unreleased
|
|
4
14
|
|
|
5
15
|
**System repo moved to `~/.agents-system`; `~/.agents` is now free for user-owned repos**
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerBrowserSubcommands } from './commands/browser.js';
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program.name('browser').description('Browser automation via CDP');
|
|
6
|
+
registerBrowserSubcommands(program);
|
|
7
|
+
program.parse();
|
package/dist/commands/browser.js
CHANGED
|
@@ -8,6 +8,10 @@ export function registerBrowserCommand(program) {
|
|
|
8
8
|
registerProfilesCommands(browser);
|
|
9
9
|
registerTaskCommands(browser);
|
|
10
10
|
}
|
|
11
|
+
export function registerBrowserSubcommands(program) {
|
|
12
|
+
registerProfilesCommands(program);
|
|
13
|
+
registerTaskCommands(program);
|
|
14
|
+
}
|
|
11
15
|
function registerProfilesCommands(browser) {
|
|
12
16
|
const profiles = browser
|
|
13
17
|
.command('profiles')
|
package/dist/commands/teams.js
CHANGED
|
@@ -679,12 +679,14 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
679
679
|
.description('Start a new team. No teammates yet; add them with `teams add`.')
|
|
680
680
|
.option('-d, --description <text>', 'One-line summary of what this team is working on')
|
|
681
681
|
.option('--enable-worktrees', 'Each teammate works in its own git worktree (requires --worktree on add)')
|
|
682
|
+
.option('--use-worktree <path>', 'All teammates share this existing worktree path (mutually exclusive with --enable-worktrees)')
|
|
682
683
|
.option('--json', 'Output machine-readable JSON')
|
|
683
684
|
.action(async (team, opts) => {
|
|
684
685
|
try {
|
|
685
686
|
const meta = await createTeam(team, {
|
|
686
687
|
description: opts.description,
|
|
687
688
|
enableWorktrees: opts.enableWorktrees,
|
|
689
|
+
useWorktree: opts.useWorktree,
|
|
688
690
|
});
|
|
689
691
|
if (isJsonMode(opts)) {
|
|
690
692
|
console.log(JSON.stringify({ team, ...meta }, null, 2));
|
|
@@ -694,7 +696,9 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
694
696
|
if (meta.description)
|
|
695
697
|
console.log(chalk.gray(` ${meta.description}`));
|
|
696
698
|
if (meta.enable_worktrees)
|
|
697
|
-
console.log(chalk.gray(` worktrees:
|
|
699
|
+
console.log(chalk.gray(` worktrees: per-teammate`));
|
|
700
|
+
if (meta.use_worktree)
|
|
701
|
+
console.log(chalk.gray(` worktree: ${meta.use_worktree}`));
|
|
698
702
|
console.log();
|
|
699
703
|
console.log(chalk.gray('Add your first teammate:'));
|
|
700
704
|
if (meta.enable_worktrees) {
|
|
@@ -776,12 +780,30 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
776
780
|
}
|
|
777
781
|
// Auto-create the team if it doesn't exist yet (friendlier UX than erroring).
|
|
778
782
|
await ensureTeam(team);
|
|
779
|
-
// Check if team has worktrees enabled
|
|
783
|
+
// Check if team has worktrees enabled or a shared worktree
|
|
780
784
|
const teamMeta = await getTeam(team);
|
|
781
785
|
const worktreesEnabled = teamMeta?.enable_worktrees ?? false;
|
|
786
|
+
const sharedWorktree = teamMeta?.use_worktree ?? null;
|
|
782
787
|
let worktreeName = null;
|
|
783
788
|
let worktreePath = null;
|
|
784
|
-
if (
|
|
789
|
+
if (sharedWorktree) {
|
|
790
|
+
// Team uses a shared worktree for all teammates
|
|
791
|
+
const fsp = await import('fs/promises');
|
|
792
|
+
try {
|
|
793
|
+
const stat = await fsp.stat(sharedWorktree);
|
|
794
|
+
if (!stat.isDirectory()) {
|
|
795
|
+
die(`Shared worktree path is not a directory: ${sharedWorktree}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
die(`Shared worktree path does not exist: ${sharedWorktree}`);
|
|
800
|
+
}
|
|
801
|
+
worktreePath = sharedWorktree;
|
|
802
|
+
if (opts.worktree) {
|
|
803
|
+
die(`Team '${team}' uses --use-worktree (shared). Don't pass --worktree on add.`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
else if (worktreesEnabled) {
|
|
785
807
|
if (!opts.worktree) {
|
|
786
808
|
die(`Team '${team}' has worktrees enabled. Use --worktree <name> to specify a worktree name.`);
|
|
787
809
|
}
|
package/dist/commands/view.js
CHANGED
|
@@ -336,7 +336,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
336
336
|
const available = getAvailableResources();
|
|
337
337
|
const synced = getActuallySyncedResources(filterAgentId, defaultVersion);
|
|
338
338
|
const newResources = getNewResources(available, synced);
|
|
339
|
-
if (hasNewResources(newResources, filterAgentId)) {
|
|
339
|
+
if (hasNewResources(newResources, filterAgentId, defaultVersion)) {
|
|
340
340
|
try {
|
|
341
341
|
const selection = await promptNewResourceSelection(filterAgentId, newResources);
|
|
342
342
|
if (selection && Object.keys(selection).length > 0) {
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ChromeOptions } from './types.js';
|
|
2
2
|
import type { BrowserType } from './types.js';
|
|
3
|
-
export declare function findBrowserPath(browserType: BrowserType): string;
|
|
3
|
+
export declare function findBrowserPath(browserType: BrowserType, customBinary?: string): string;
|
|
4
4
|
export interface LaunchResult {
|
|
5
5
|
pid: number;
|
|
6
6
|
port: number;
|
|
7
7
|
wsUrl: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function launchBrowser(profileName: string, browserType: BrowserType, port: number, options?: ChromeOptions, secrets?: string): Promise<LaunchResult>;
|
|
9
|
+
export declare function launchBrowser(profileName: string, browserType: BrowserType, port: number, options?: ChromeOptions, secrets?: string, customBinary?: string): Promise<LaunchResult>;
|
|
10
10
|
export declare function attachToChrome(port: number): Promise<string>;
|
|
11
11
|
export declare function killChrome(pid: number): void;
|
|
12
12
|
export declare function getRunningChromeInfo(profileName: string): {
|
|
@@ -15,6 +15,7 @@ const BROWSER_PATHS = {
|
|
|
15
15
|
chromium: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
|
|
16
16
|
brave: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
|
|
17
17
|
edge: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
|
|
18
|
+
custom: [],
|
|
18
19
|
},
|
|
19
20
|
linux: {
|
|
20
21
|
chrome: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'],
|
|
@@ -22,6 +23,7 @@ const BROWSER_PATHS = {
|
|
|
22
23
|
chromium: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
|
|
23
24
|
brave: ['/usr/bin/brave-browser', '/usr/bin/brave'],
|
|
24
25
|
edge: ['/usr/bin/microsoft-edge'],
|
|
26
|
+
custom: [],
|
|
25
27
|
},
|
|
26
28
|
win32: {
|
|
27
29
|
chrome: [
|
|
@@ -36,9 +38,19 @@ const BROWSER_PATHS = {
|
|
|
36
38
|
edge: [
|
|
37
39
|
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
38
40
|
],
|
|
41
|
+
custom: [],
|
|
39
42
|
},
|
|
40
43
|
};
|
|
41
|
-
export function findBrowserPath(browserType) {
|
|
44
|
+
export function findBrowserPath(browserType, customBinary) {
|
|
45
|
+
if (customBinary) {
|
|
46
|
+
if (!fs.existsSync(customBinary)) {
|
|
47
|
+
throw new Error(`Custom binary not found: ${customBinary}`);
|
|
48
|
+
}
|
|
49
|
+
return customBinary;
|
|
50
|
+
}
|
|
51
|
+
if (browserType === 'custom') {
|
|
52
|
+
throw new Error('browser: custom requires a binary path in the profile');
|
|
53
|
+
}
|
|
42
54
|
const platform = os.platform();
|
|
43
55
|
const platformPaths = BROWSER_PATHS[platform];
|
|
44
56
|
if (!platformPaths) {
|
|
@@ -52,8 +64,8 @@ export function findBrowserPath(browserType) {
|
|
|
52
64
|
}
|
|
53
65
|
throw new Error(`Browser "${browserType}" not found. Install it first.`);
|
|
54
66
|
}
|
|
55
|
-
export async function launchBrowser(profileName, browserType, port, options = {}, secrets) {
|
|
56
|
-
const browserPath = findBrowserPath(browserType);
|
|
67
|
+
export async function launchBrowser(profileName, browserType, port, options = {}, secrets, customBinary) {
|
|
68
|
+
const browserPath = findBrowserPath(browserType, customBinary);
|
|
57
69
|
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
58
70
|
const userDataDir = path.join(runtimeDir, 'chrome-data');
|
|
59
71
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
@@ -14,7 +14,7 @@ export async function connectLocal(endpoint, profile) {
|
|
|
14
14
|
}
|
|
15
15
|
catch {
|
|
16
16
|
const newPort = allocatePort();
|
|
17
|
-
const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, profile.chrome, profile.secrets);
|
|
17
|
+
const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, profile.chrome, profile.secrets, profile.binary);
|
|
18
18
|
const cdp = new CDPClient();
|
|
19
19
|
await cdp.connect(wsUrl);
|
|
20
20
|
return { cdp, port: newPort, pid };
|
|
@@ -12,7 +12,7 @@ export async function connectSSH(endpoint, profile) {
|
|
|
12
12
|
const remotePort = parseInt(url.searchParams.get('port') || '9222', 10);
|
|
13
13
|
const localPort = allocatePort();
|
|
14
14
|
try {
|
|
15
|
-
await ensureRemoteBrowser(user, host, profile.browser, remotePort);
|
|
15
|
+
await ensureRemoteBrowser(user, host, profile.browser, remotePort, profile.binary);
|
|
16
16
|
}
|
|
17
17
|
catch {
|
|
18
18
|
// Browser may already be running, continue
|
|
@@ -96,7 +96,7 @@ function tryConnect(port) {
|
|
|
96
96
|
socket.on('error', reject);
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
|
-
async function ensureRemoteBrowser(user, host, browserType, port) {
|
|
99
|
+
async function ensureRemoteBrowser(user, host, browserType, port, customBinary) {
|
|
100
100
|
const browserPaths = {
|
|
101
101
|
chrome: '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome',
|
|
102
102
|
comet: '/Applications/Comet.app/Contents/MacOS/Comet',
|
|
@@ -104,9 +104,18 @@ async function ensureRemoteBrowser(user, host, browserType, port) {
|
|
|
104
104
|
brave: '/Applications/Brave\\ Browser.app/Contents/MacOS/Brave\\ Browser',
|
|
105
105
|
edge: '/Applications/Microsoft\\ Edge.app/Contents/MacOS/Microsoft\\ Edge',
|
|
106
106
|
};
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
107
|
+
let browserPath;
|
|
108
|
+
if (customBinary) {
|
|
109
|
+
browserPath = customBinary.replace(/ /g, '\\ ');
|
|
110
|
+
}
|
|
111
|
+
else if (browserType === 'custom') {
|
|
112
|
+
throw new Error('browser: custom requires a binary path in the profile');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
browserPath = browserPaths[browserType];
|
|
116
|
+
if (!browserPath) {
|
|
117
|
+
throw new Error(`Unknown browser type: ${browserType}`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
const remoteCmd = `${browserPath} --remote-debugging-port=${port} '--remote-allow-origins=*' --disable-background-timer-throttling --user-data-dir=/tmp/agents-browser-${port} </dev/null >/dev/null 2>&1 &`;
|
|
112
121
|
return new Promise((resolve, reject) => {
|
|
@@ -28,7 +28,7 @@ export class BrowserService {
|
|
|
28
28
|
const task = conn.tasks.get(finalTaskId);
|
|
29
29
|
return { task: finalTaskId, windowTargetId: task.windowTargetId };
|
|
30
30
|
}
|
|
31
|
-
const { windowTargetId } = await this.createTaskWindow(conn
|
|
31
|
+
const { windowTargetId } = await this.createTaskWindow(conn, finalTaskId);
|
|
32
32
|
const task = {
|
|
33
33
|
id: finalTaskId,
|
|
34
34
|
profile: profileName,
|
|
@@ -88,6 +88,19 @@ export class BrowserService {
|
|
|
88
88
|
}
|
|
89
89
|
async navigate(taskId, url, profileName) {
|
|
90
90
|
const { conn, task } = await this.findTask(taskId, profileName);
|
|
91
|
+
if (conn.electron) {
|
|
92
|
+
const tabId = task.windowTargetId || task.tabIds[0];
|
|
93
|
+
if (!tabId) {
|
|
94
|
+
throw new Error('No existing tab to navigate in Electron app');
|
|
95
|
+
}
|
|
96
|
+
const sessionId = await this.getSessionId(conn, tabId);
|
|
97
|
+
await conn.cdp.send('Page.navigate', { url }, sessionId);
|
|
98
|
+
if (!task.tabIds.includes(tabId)) {
|
|
99
|
+
task.tabIds.push(tabId);
|
|
100
|
+
}
|
|
101
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
102
|
+
return { tabId, url };
|
|
103
|
+
}
|
|
91
104
|
const result = (await conn.cdp.send('Target.createTarget', {
|
|
92
105
|
url,
|
|
93
106
|
}));
|
|
@@ -255,6 +268,7 @@ export class BrowserService {
|
|
|
255
268
|
cdp,
|
|
256
269
|
port: existingInfo.port,
|
|
257
270
|
pid: existingInfo.pid,
|
|
271
|
+
electron: profile.electron,
|
|
258
272
|
tasks,
|
|
259
273
|
sessionCache: new Map(),
|
|
260
274
|
};
|
|
@@ -280,6 +294,7 @@ export class BrowserService {
|
|
|
280
294
|
cdp: conn.cdp,
|
|
281
295
|
port: conn.port,
|
|
282
296
|
pid: conn.pid,
|
|
297
|
+
electron: profile.electron,
|
|
283
298
|
tasks: conn.pid === 0 ? this.loadTaskState(profile.name) : new Map(),
|
|
284
299
|
sessionCache: new Map(),
|
|
285
300
|
};
|
|
@@ -291,6 +306,7 @@ export class BrowserService {
|
|
|
291
306
|
cdp: conn.cdp,
|
|
292
307
|
port: conn.port,
|
|
293
308
|
pid: conn.pid,
|
|
309
|
+
electron: profile.electron,
|
|
294
310
|
tasks: new Map(),
|
|
295
311
|
sessionCache: new Map(),
|
|
296
312
|
};
|
|
@@ -300,8 +316,13 @@ export class BrowserService {
|
|
|
300
316
|
async enableDomains(cdp) {
|
|
301
317
|
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
302
318
|
}
|
|
303
|
-
async createTaskWindow(
|
|
304
|
-
|
|
319
|
+
async createTaskWindow(conn, _taskId) {
|
|
320
|
+
if (conn.electron) {
|
|
321
|
+
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
322
|
+
const pageTarget = targetInfos.find((t) => t.type === 'page');
|
|
323
|
+
return { windowTargetId: pageTarget?.targetId };
|
|
324
|
+
}
|
|
325
|
+
const result = (await conn.cdp.send('Target.createTarget', {
|
|
305
326
|
url: 'about:blank',
|
|
306
327
|
newWindow: true,
|
|
307
328
|
}));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge';
|
|
1
|
+
export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
|
|
2
2
|
export interface BrowserProfile {
|
|
3
3
|
name: string;
|
|
4
4
|
description?: string;
|
|
5
5
|
browser: BrowserType;
|
|
6
|
+
binary?: string;
|
|
7
|
+
electron?: boolean;
|
|
6
8
|
endpoints: string[];
|
|
7
9
|
chrome?: ChromeOptions;
|
|
8
10
|
secrets?: string;
|
package/dist/lib/migrate.js
CHANGED
|
@@ -152,6 +152,49 @@ function migrateUserVersionsToSystem() {
|
|
|
152
152
|
console.log(`Skipped ${skippedCount} version dir${skippedCount === 1 ? '' : 's'} already present in ~/.agents-system/versions/ (kept legacy copy at ~/.agents/versions/)`);
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Move ~/. agents/runs/ -> ~/.agents/routines/runs/.
|
|
157
|
+
* Runs now live inside routines directory for cleaner organization.
|
|
158
|
+
*/
|
|
159
|
+
function migrateRunsIntoRoutines() {
|
|
160
|
+
const src = path.join(USER_DIR, 'runs');
|
|
161
|
+
const dest = path.join(USER_DIR, 'routines', 'runs');
|
|
162
|
+
if (!fs.existsSync(src) || fs.existsSync(dest))
|
|
163
|
+
return;
|
|
164
|
+
try {
|
|
165
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
166
|
+
fs.renameSync(src, dest);
|
|
167
|
+
}
|
|
168
|
+
catch { /* best-effort */ }
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Move ~/.agents/trash/ -> ~/.agents/.trash/.
|
|
172
|
+
* Hide the trash directory.
|
|
173
|
+
*/
|
|
174
|
+
function migrateTrashToHidden() {
|
|
175
|
+
const src = path.join(USER_DIR, 'trash');
|
|
176
|
+
const dest = path.join(USER_DIR, '.trash');
|
|
177
|
+
if (!fs.existsSync(src) || fs.existsSync(dest))
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
fs.renameSync(src, dest);
|
|
181
|
+
}
|
|
182
|
+
catch { /* best-effort */ }
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Move ~/.agents/backups/ -> ~/.agents/.backups/.
|
|
186
|
+
* Hide the backups directory.
|
|
187
|
+
*/
|
|
188
|
+
function migrateBackupsToHidden() {
|
|
189
|
+
const src = path.join(USER_DIR, 'backups');
|
|
190
|
+
const dest = path.join(USER_DIR, '.backups');
|
|
191
|
+
if (!fs.existsSync(src) || fs.existsSync(dest))
|
|
192
|
+
return;
|
|
193
|
+
try {
|
|
194
|
+
fs.renameSync(src, dest);
|
|
195
|
+
}
|
|
196
|
+
catch { /* best-effort */ }
|
|
197
|
+
}
|
|
155
198
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
156
199
|
export function runMigration() {
|
|
157
200
|
migrateAgentsYaml();
|
|
@@ -159,4 +202,7 @@ export function runMigration() {
|
|
|
159
202
|
migrateSystemConfigJson();
|
|
160
203
|
migratePromptcutsIntoHooks();
|
|
161
204
|
migrateUserVersionsToSystem();
|
|
205
|
+
migrateRunsIntoRoutines();
|
|
206
|
+
migrateTrashToHidden();
|
|
207
|
+
migrateBackupsToHidden();
|
|
162
208
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands resource handler.
|
|
3
|
+
*
|
|
4
|
+
* Commands are slash-command definitions stored as .md files (Claude/Codex/Cursor/OpenCode)
|
|
5
|
+
* or .toml files (Gemini). This handler resolves commands across layers (project > user > system),
|
|
6
|
+
* handles format conversion during sync, and provides consistent list/resolve/sync behavior.
|
|
7
|
+
*/
|
|
8
|
+
import type { AgentId, ResolvedItem, ResourceHandler, ResourceKind } from './types.js';
|
|
9
|
+
/** Command item metadata. */
|
|
10
|
+
export interface CommandItem {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
content: string;
|
|
14
|
+
format: 'md' | 'toml';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Commands resource handler implementing ResourceHandler<CommandItem>.
|
|
18
|
+
*/
|
|
19
|
+
export declare class CommandsHandler implements ResourceHandler<CommandItem> {
|
|
20
|
+
readonly kind: ResourceKind;
|
|
21
|
+
/**
|
|
22
|
+
* List all commands across layers, with higher layer winning on name conflict.
|
|
23
|
+
* Returns a union of all commands, deduplicated by name.
|
|
24
|
+
*/
|
|
25
|
+
listAll(agent: AgentId, cwd?: string): ResolvedItem<CommandItem>[];
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a single command by name.
|
|
28
|
+
* Returns the winning layer's version, or null if not found.
|
|
29
|
+
*/
|
|
30
|
+
resolve(agent: AgentId, name: string, cwd?: string): ResolvedItem<CommandItem> | null;
|
|
31
|
+
/**
|
|
32
|
+
* Sync resolved commands to the agent's version home directory.
|
|
33
|
+
* Copies/transforms commands as needed for the agent's expected format.
|
|
34
|
+
*/
|
|
35
|
+
sync(agent: AgentId, versionHome: string, cwd?: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Get the file format this resource uses for a given agent.
|
|
38
|
+
*/
|
|
39
|
+
format(agent: AgentId): 'md' | 'toml';
|
|
40
|
+
/**
|
|
41
|
+
* Get the target directory name in the agent's version home.
|
|
42
|
+
*/
|
|
43
|
+
targetDir(agent: AgentId): string;
|
|
44
|
+
}
|
|
45
|
+
/** Singleton instance of the commands handler. */
|
|
46
|
+
export declare const commandsHandler: CommandsHandler;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands resource handler.
|
|
3
|
+
*
|
|
4
|
+
* Commands are slash-command definitions stored as .md files (Claude/Codex/Cursor/OpenCode)
|
|
5
|
+
* or .toml files (Gemini). This handler resolves commands across layers (project > user > system),
|
|
6
|
+
* handles format conversion during sync, and provides consistent list/resolve/sync behavior.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { getProjectAgentsDir, getUserAgentsDir, getSystemAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
11
|
+
import { AGENTS } from '../agents.js';
|
|
12
|
+
import { markdownToToml } from '../convert.js';
|
|
13
|
+
/**
|
|
14
|
+
* Get the commands directory for a given layer root.
|
|
15
|
+
*/
|
|
16
|
+
function getCommandsDirForRoot(root) {
|
|
17
|
+
return path.join(root, 'commands');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse a command file and extract metadata.
|
|
21
|
+
*/
|
|
22
|
+
function parseCommandFile(filePath) {
|
|
23
|
+
if (!fs.existsSync(filePath))
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
27
|
+
const format = filePath.endsWith('.toml') ? 'toml' : 'md';
|
|
28
|
+
const name = path.basename(filePath).replace(/\.(md|toml)$/, '');
|
|
29
|
+
let description = '';
|
|
30
|
+
if (format === 'md') {
|
|
31
|
+
// Parse YAML frontmatter for description
|
|
32
|
+
const lines = content.split('\n');
|
|
33
|
+
if (lines[0] === '---') {
|
|
34
|
+
const endIndex = lines.slice(1).findIndex((l) => l === '---');
|
|
35
|
+
if (endIndex > 0) {
|
|
36
|
+
const frontmatter = lines.slice(1, endIndex + 1).join('\n');
|
|
37
|
+
const descMatch = frontmatter.match(/description:\s*(.+)/i);
|
|
38
|
+
if (descMatch)
|
|
39
|
+
description = descMatch[1].trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Parse TOML for description
|
|
45
|
+
const descMatch = content.match(/description\s*=\s*"([^"]+)"/);
|
|
46
|
+
if (descMatch)
|
|
47
|
+
description = descMatch[1];
|
|
48
|
+
}
|
|
49
|
+
return { name, description, content, format };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* List command files in a directory.
|
|
57
|
+
*/
|
|
58
|
+
function listCommandsInDir(dir) {
|
|
59
|
+
if (!fs.existsSync(dir))
|
|
60
|
+
return [];
|
|
61
|
+
try {
|
|
62
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
const commands = [];
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (entry.isFile()) {
|
|
66
|
+
if (entry.name.endsWith('.md')) {
|
|
67
|
+
commands.push({
|
|
68
|
+
name: entry.name.replace('.md', ''),
|
|
69
|
+
path: path.join(dir, entry.name),
|
|
70
|
+
format: 'md',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (entry.name.endsWith('.toml')) {
|
|
74
|
+
commands.push({
|
|
75
|
+
name: entry.name.replace('.toml', ''),
|
|
76
|
+
path: path.join(dir, entry.name),
|
|
77
|
+
format: 'toml',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return commands;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Commands resource handler implementing ResourceHandler<CommandItem>.
|
|
90
|
+
*/
|
|
91
|
+
export class CommandsHandler {
|
|
92
|
+
kind = 'command';
|
|
93
|
+
/**
|
|
94
|
+
* List all commands across layers, with higher layer winning on name conflict.
|
|
95
|
+
* Returns a union of all commands, deduplicated by name.
|
|
96
|
+
*/
|
|
97
|
+
listAll(agent, cwd) {
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const results = [];
|
|
100
|
+
const projectDir = getProjectAgentsDir(cwd);
|
|
101
|
+
const extraRepos = getEnabledExtraRepos();
|
|
102
|
+
// Build layer roots in precedence order: project > user > system > extras
|
|
103
|
+
const roots = [];
|
|
104
|
+
if (projectDir) {
|
|
105
|
+
roots.push({ dir: getCommandsDirForRoot(projectDir), layer: 'project' });
|
|
106
|
+
}
|
|
107
|
+
roots.push({ dir: getCommandsDirForRoot(getUserAgentsDir()), layer: 'user' });
|
|
108
|
+
roots.push({ dir: getCommandsDirForRoot(getSystemAgentsDir()), layer: 'system' });
|
|
109
|
+
for (const extra of extraRepos) {
|
|
110
|
+
roots.push({ dir: getCommandsDirForRoot(extra.dir), layer: 'system' });
|
|
111
|
+
}
|
|
112
|
+
for (const { dir, layer } of roots) {
|
|
113
|
+
const commands = listCommandsInDir(dir);
|
|
114
|
+
for (const cmd of commands) {
|
|
115
|
+
if (seen.has(cmd.name))
|
|
116
|
+
continue;
|
|
117
|
+
seen.add(cmd.name);
|
|
118
|
+
const item = parseCommandFile(cmd.path);
|
|
119
|
+
if (item) {
|
|
120
|
+
results.push({
|
|
121
|
+
name: cmd.name,
|
|
122
|
+
item,
|
|
123
|
+
layer,
|
|
124
|
+
path: cmd.path,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve a single command by name.
|
|
133
|
+
* Returns the winning layer's version, or null if not found.
|
|
134
|
+
*/
|
|
135
|
+
resolve(agent, name, cwd) {
|
|
136
|
+
const projectDir = getProjectAgentsDir(cwd);
|
|
137
|
+
const extraRepos = getEnabledExtraRepos();
|
|
138
|
+
// Build candidate paths in precedence order
|
|
139
|
+
const candidates = [];
|
|
140
|
+
if (projectDir) {
|
|
141
|
+
candidates.push({ dir: getCommandsDirForRoot(projectDir), layer: 'project' });
|
|
142
|
+
}
|
|
143
|
+
candidates.push({ dir: getCommandsDirForRoot(getUserAgentsDir()), layer: 'user' });
|
|
144
|
+
candidates.push({ dir: getCommandsDirForRoot(getSystemAgentsDir()), layer: 'system' });
|
|
145
|
+
for (const extra of extraRepos) {
|
|
146
|
+
candidates.push({ dir: getCommandsDirForRoot(extra.dir), layer: 'system' });
|
|
147
|
+
}
|
|
148
|
+
for (const { dir, layer } of candidates) {
|
|
149
|
+
// Try .md first, then .toml
|
|
150
|
+
for (const ext of ['.md', '.toml']) {
|
|
151
|
+
const filePath = path.join(dir, `${name}${ext}`);
|
|
152
|
+
const item = parseCommandFile(filePath);
|
|
153
|
+
if (item) {
|
|
154
|
+
return { name, item, layer, path: filePath };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Sync resolved commands to the agent's version home directory.
|
|
162
|
+
* Copies/transforms commands as needed for the agent's expected format.
|
|
163
|
+
*/
|
|
164
|
+
sync(agent, versionHome, cwd) {
|
|
165
|
+
const agentConfig = AGENTS[agent];
|
|
166
|
+
const targetFormat = this.format(agent);
|
|
167
|
+
const targetDir = path.join(versionHome, `.${agent}`, this.targetDir(agent));
|
|
168
|
+
// Ensure target directory exists
|
|
169
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
170
|
+
// Get all resolved commands
|
|
171
|
+
const commands = this.listAll(agent, cwd);
|
|
172
|
+
for (const resolved of commands) {
|
|
173
|
+
const ext = targetFormat === 'toml' ? '.toml' : '.md';
|
|
174
|
+
const targetPath = path.join(targetDir, `${resolved.name}${ext}`);
|
|
175
|
+
// Convert format if needed
|
|
176
|
+
if (targetFormat === 'toml' && resolved.item.format === 'md') {
|
|
177
|
+
// Convert markdown to TOML
|
|
178
|
+
const tomlContent = markdownToToml(resolved.name, resolved.item.content);
|
|
179
|
+
fs.writeFileSync(targetPath, tomlContent, 'utf-8');
|
|
180
|
+
}
|
|
181
|
+
else if (targetFormat === 'md' && resolved.item.format === 'toml') {
|
|
182
|
+
// For now, copy TOML as-is if target expects md (edge case)
|
|
183
|
+
// In practice, source commands are always .md
|
|
184
|
+
fs.copyFileSync(resolved.path, targetPath);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// Same format, copy directly
|
|
188
|
+
fs.copyFileSync(resolved.path, targetPath);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get the file format this resource uses for a given agent.
|
|
194
|
+
*/
|
|
195
|
+
format(agent) {
|
|
196
|
+
const agentConfig = AGENTS[agent];
|
|
197
|
+
return agentConfig.format === 'toml' ? 'toml' : 'md';
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Get the target directory name in the agent's version home.
|
|
201
|
+
*/
|
|
202
|
+
targetDir(agent) {
|
|
203
|
+
const agentConfig = AGENTS[agent];
|
|
204
|
+
return agentConfig.commandsSubdir || 'commands';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/** Singleton instance of the commands handler. */
|
|
208
|
+
export const commandsHandler = new CommandsHandler();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HooksHandler - ResourceHandler implementation for hooks.
|
|
3
|
+
*
|
|
4
|
+
* Hooks are declared in hooks.yaml at each layer (system, user, project).
|
|
5
|
+
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
|
+
* Non-conflicting hooks from all layers are unioned together.
|
|
7
|
+
*/
|
|
8
|
+
import type { ResourceHandler } from './types.js';
|
|
9
|
+
import type { ManifestHook } from '../types.js';
|
|
10
|
+
export type HookItem = ManifestHook;
|
|
11
|
+
export declare const HooksHandler: ResourceHandler<HookItem>;
|
|
12
|
+
export default HooksHandler;
|