@phnx-labs/agents-cli 1.14.2 → 1.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -7
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +7 -0
- package/dist/commands/browser.d.ts +3 -0
- package/dist/commands/browser.js +392 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +198 -11
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +184 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +47 -14
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +123 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- 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/resources.js +1 -1
- package/dist/lib/rotate.d.ts +8 -1
- package/dist/lib/rotate.js +17 -4
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -78
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +12 -1
- package/dist/lib/teams/registry.js +12 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +12 -6
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +32 -3
- package/dist/lib/versions.js +147 -119
- package/package.json +3 -2
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
package/dist/lib/migrate.js
CHANGED
|
@@ -76,10 +76,133 @@ function migratePromptcutsIntoHooks() {
|
|
|
76
76
|
catch { /* best-effort */ }
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Move installed agent versions from the legacy single-root layout
|
|
81
|
+
* (~/.agents/versions/<agent>/<ver>/) into the system root
|
|
82
|
+
* (~/.agents-system/versions/<agent>/<ver>/).
|
|
83
|
+
*
|
|
84
|
+
* Pre-split installs put binaries and home dirs under ~/.agents/. After the
|
|
85
|
+
* split, the system code only scans ~/.agents-system/versions/, so without
|
|
86
|
+
* this migration the versions become invisible to listInstalledVersions and
|
|
87
|
+
* every command that depends on it (view, prune, run).
|
|
88
|
+
*
|
|
89
|
+
* Idempotent and non-destructive: if a same-named dest already exists we
|
|
90
|
+
* leave the legacy copy in place so the user can reconcile manually.
|
|
91
|
+
*/
|
|
92
|
+
function migrateUserVersionsToSystem() {
|
|
93
|
+
const userVersions = path.join(USER_DIR, 'versions');
|
|
94
|
+
const sysVersions = path.join(SYSTEM_DIR, 'versions');
|
|
95
|
+
if (!fs.existsSync(userVersions))
|
|
96
|
+
return;
|
|
97
|
+
let movedCount = 0;
|
|
98
|
+
let skippedCount = 0;
|
|
99
|
+
let agentEntries;
|
|
100
|
+
try {
|
|
101
|
+
agentEntries = fs.readdirSync(userVersions, { withFileTypes: true });
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
for (const agent of agentEntries) {
|
|
107
|
+
if (!agent.isDirectory())
|
|
108
|
+
continue;
|
|
109
|
+
const srcAgentDir = path.join(userVersions, agent.name);
|
|
110
|
+
const dstAgentDir = path.join(sysVersions, agent.name);
|
|
111
|
+
try {
|
|
112
|
+
fs.mkdirSync(dstAgentDir, { recursive: true, mode: 0o700 });
|
|
113
|
+
}
|
|
114
|
+
catch { /* best-effort */ }
|
|
115
|
+
let verEntries;
|
|
116
|
+
try {
|
|
117
|
+
verEntries = fs.readdirSync(srcAgentDir, { withFileTypes: true });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const ver of verEntries) {
|
|
123
|
+
if (!ver.isDirectory())
|
|
124
|
+
continue;
|
|
125
|
+
const src = path.join(srcAgentDir, ver.name);
|
|
126
|
+
const dst = path.join(dstAgentDir, ver.name);
|
|
127
|
+
if (fs.existsSync(dst)) {
|
|
128
|
+
skippedCount++;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
fs.renameSync(src, dst);
|
|
133
|
+
movedCount++;
|
|
134
|
+
}
|
|
135
|
+
catch { /* best-effort, leave legacy in place */ }
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
if (fs.readdirSync(srcAgentDir).length === 0)
|
|
139
|
+
fs.rmdirSync(srcAgentDir);
|
|
140
|
+
}
|
|
141
|
+
catch { /* best-effort */ }
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (fs.readdirSync(userVersions).length === 0)
|
|
145
|
+
fs.rmdirSync(userVersions);
|
|
146
|
+
}
|
|
147
|
+
catch { /* best-effort */ }
|
|
148
|
+
if (movedCount > 0) {
|
|
149
|
+
console.log(`Migrated ${movedCount} version dir${movedCount === 1 ? '' : 's'} from ~/.agents/versions/ to ~/.agents-system/versions/`);
|
|
150
|
+
}
|
|
151
|
+
if (skippedCount > 0) {
|
|
152
|
+
console.log(`Skipped ${skippedCount} version dir${skippedCount === 1 ? '' : 's'} already present in ~/.agents-system/versions/ (kept legacy copy at ~/.agents/versions/)`);
|
|
153
|
+
}
|
|
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
|
+
}
|
|
79
198
|
/** Run all idempotent migrations. Safe to call multiple times. */
|
|
80
199
|
export function runMigration() {
|
|
81
200
|
migrateAgentsYaml();
|
|
82
201
|
deleteSystemPromptsJson();
|
|
83
202
|
migrateSystemConfigJson();
|
|
84
203
|
migratePromptcutsIntoHooks();
|
|
204
|
+
migrateUserVersionsToSystem();
|
|
205
|
+
migrateRunsIntoRoutines();
|
|
206
|
+
migrateTrashToHidden();
|
|
207
|
+
migrateBackupsToHidden();
|
|
85
208
|
}
|
package/dist/lib/pty-client.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import { spawn, execSync } from 'child_process';
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
-
import { getSocketPath, isPtyServerRunning } from './pty-server.js';
|
|
12
|
+
import { getSocketPath, getPtyLogPath, isPtyServerRunning } from './pty-server.js';
|
|
13
13
|
const CONNECT_TIMEOUT_MS = 5000;
|
|
14
14
|
const RESPONSE_TIMEOUT_MS = 30000;
|
|
15
15
|
/**
|
|
@@ -33,7 +33,7 @@ async function ensureServer() {
|
|
|
33
33
|
return;
|
|
34
34
|
// Find the entry point to spawn the server
|
|
35
35
|
const { bin, args } = getServerSpawnArgs();
|
|
36
|
-
const logPath =
|
|
36
|
+
const logPath = getPtyLogPath();
|
|
37
37
|
const logFd = fs.openSync(logPath, 'a');
|
|
38
38
|
const child = spawn(bin, args, {
|
|
39
39
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -57,7 +57,7 @@ async function ensureServer() {
|
|
|
57
57
|
}
|
|
58
58
|
await new Promise(r => setTimeout(r, 100));
|
|
59
59
|
}
|
|
60
|
-
throw new Error('PTY server failed to start within 5 seconds. Check ~/.agents/pty.
|
|
60
|
+
throw new Error('PTY server failed to start within 5 seconds. Check ~/.agents-system/helpers/pty/logs.jsonl');
|
|
61
61
|
}
|
|
62
62
|
function getServerSpawnArgs() {
|
|
63
63
|
// Prefer the dist/index.js from the same installation as this code.
|
package/dist/lib/pty-server.js
CHANGED
|
@@ -53,9 +53,12 @@ export function captureProcessStartTime(pid) {
|
|
|
53
53
|
}
|
|
54
54
|
// --- Constants ---
|
|
55
55
|
const SENTINEL = '__AGENTS_PTY_DONE__';
|
|
56
|
+
const PTY_DIR = 'helpers/pty';
|
|
56
57
|
const SOCKET_NAME = 'pty.sock';
|
|
57
58
|
const PID_FILE = 'pty.pid';
|
|
58
|
-
const LOG_FILE = '
|
|
59
|
+
const LOG_FILE = 'logs.jsonl';
|
|
60
|
+
const LOG_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
61
|
+
const LOG_ROTATE_COUNT = 3;
|
|
59
62
|
const SESSION_IDLE_MS = 30 * 60 * 1000; // 30 min
|
|
60
63
|
const SERVER_IDLE_MS = 60 * 60 * 1000; // 1 hour
|
|
61
64
|
// --- Path helpers ---
|
|
@@ -79,17 +82,24 @@ function buildPtyEnv() {
|
|
|
79
82
|
}
|
|
80
83
|
return env;
|
|
81
84
|
}
|
|
85
|
+
/** Get the PTY helper directory, creating it if needed. */
|
|
86
|
+
function getPtyDir() {
|
|
87
|
+
const dir = path.join(getAgentsDir(), PTY_DIR);
|
|
88
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
89
|
+
return dir;
|
|
90
|
+
}
|
|
82
91
|
/** Get the unix socket path for the PTY server. */
|
|
83
92
|
export function getSocketPath() {
|
|
84
|
-
return path.join(
|
|
93
|
+
return path.join(getPtyDir(), SOCKET_NAME);
|
|
85
94
|
}
|
|
86
95
|
/** Get the path to the PTY server PID file. */
|
|
87
96
|
export function getPtyPidPath() {
|
|
88
|
-
return path.join(
|
|
97
|
+
return path.join(getPtyDir(), PID_FILE);
|
|
89
98
|
}
|
|
90
99
|
/** Get the path to the PTY server log file. */
|
|
91
100
|
export function getPtyLogPath() {
|
|
92
|
-
|
|
101
|
+
const logDir = getPtyDir();
|
|
102
|
+
return path.join(logDir, LOG_FILE);
|
|
93
103
|
}
|
|
94
104
|
/** Check if the PTY server process is alive by probing the stored PID. */
|
|
95
105
|
export function isPtyServerRunning() {
|
|
@@ -112,11 +122,30 @@ export function isPtyServerRunning() {
|
|
|
112
122
|
}
|
|
113
123
|
}
|
|
114
124
|
// --- Logging ---
|
|
125
|
+
function rotateLogsIfNeeded(logPath) {
|
|
126
|
+
try {
|
|
127
|
+
const stat = fs.statSync(logPath);
|
|
128
|
+
if (stat.size < LOG_MAX_SIZE)
|
|
129
|
+
return;
|
|
130
|
+
for (let i = LOG_ROTATE_COUNT - 1; i >= 1; i--) {
|
|
131
|
+
const older = `${logPath}.${i}`;
|
|
132
|
+
const newer = i === 1 ? logPath : `${logPath}.${i - 1}`;
|
|
133
|
+
if (fs.existsSync(newer)) {
|
|
134
|
+
fs.renameSync(newer, older);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (fs.existsSync(logPath)) {
|
|
138
|
+
fs.renameSync(logPath, `${logPath}.1`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch { }
|
|
142
|
+
}
|
|
115
143
|
function log(level, message) {
|
|
116
|
-
const
|
|
117
|
-
|
|
144
|
+
const logPath = getPtyLogPath();
|
|
145
|
+
rotateLogsIfNeeded(logPath);
|
|
146
|
+
const entry = { ts: new Date().toISOString(), level, message };
|
|
118
147
|
try {
|
|
119
|
-
fs.appendFileSync(
|
|
148
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
120
149
|
}
|
|
121
150
|
catch { }
|
|
122
151
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,136 @@
|
|
|
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 * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import * as yaml from 'yaml';
|
|
11
|
+
import { getSystemAgentsDir, getUserAgentsDir, getProjectAgentsDir, } from '../state.js';
|
|
12
|
+
/**
|
|
13
|
+
* Get the hooks.yaml path for a given layer directory.
|
|
14
|
+
*/
|
|
15
|
+
function getHooksYamlPath(layerDir) {
|
|
16
|
+
return path.join(layerDir, 'hooks.yaml');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse hooks.yaml from a directory.
|
|
20
|
+
* Returns empty object if file doesn't exist or is invalid.
|
|
21
|
+
*/
|
|
22
|
+
function parseHooksYaml(dir) {
|
|
23
|
+
const manifestPath = getHooksYamlPath(dir);
|
|
24
|
+
if (!fs.existsSync(manifestPath)) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
29
|
+
const parsed = yaml.parse(content);
|
|
30
|
+
return parsed || {};
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get layer directories for hook resolution.
|
|
38
|
+
*/
|
|
39
|
+
function getLayerDirs(cwd) {
|
|
40
|
+
return {
|
|
41
|
+
system: getSystemAgentsDir(),
|
|
42
|
+
user: getUserAgentsDir(),
|
|
43
|
+
project: cwd ? getProjectAgentsDir(cwd) : null,
|
|
44
|
+
extra: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export const HooksHandler = {
|
|
48
|
+
kind: 'hook',
|
|
49
|
+
/**
|
|
50
|
+
* List all hooks across layers, with higher layer winning on name conflict.
|
|
51
|
+
* Returns a union of all hooks, deduplicated by name.
|
|
52
|
+
*/
|
|
53
|
+
listAll(agent, cwd) {
|
|
54
|
+
const layers = getLayerDirs(cwd);
|
|
55
|
+
const result = new Map();
|
|
56
|
+
// Process in precedence order: system first (lowest), then user, then project (highest)
|
|
57
|
+
const layerOrder = [
|
|
58
|
+
{ layer: 'system', dir: layers.system },
|
|
59
|
+
{ layer: 'user', dir: layers.user },
|
|
60
|
+
{ layer: 'project', dir: layers.project },
|
|
61
|
+
];
|
|
62
|
+
for (const { layer, dir } of layerOrder) {
|
|
63
|
+
if (!dir)
|
|
64
|
+
continue;
|
|
65
|
+
const hooks = parseHooksYaml(dir);
|
|
66
|
+
for (const [name, hook] of Object.entries(hooks)) {
|
|
67
|
+
// Skip disabled hooks
|
|
68
|
+
if (hook.enabled === false) {
|
|
69
|
+
result.delete(name);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
result.set(name, {
|
|
73
|
+
name,
|
|
74
|
+
item: hook,
|
|
75
|
+
layer,
|
|
76
|
+
path: getHooksYamlPath(dir),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return Array.from(result.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a single hook by name.
|
|
84
|
+
* Returns the winning layer's version, or null if not found.
|
|
85
|
+
*/
|
|
86
|
+
resolve(agent, name, cwd) {
|
|
87
|
+
const layers = getLayerDirs(cwd);
|
|
88
|
+
// Check in reverse precedence order: project first (highest), then user, then system
|
|
89
|
+
const layerOrder = [
|
|
90
|
+
{ layer: 'project', dir: layers.project },
|
|
91
|
+
{ layer: 'user', dir: layers.user },
|
|
92
|
+
{ layer: 'system', dir: layers.system },
|
|
93
|
+
];
|
|
94
|
+
for (const { layer, dir } of layerOrder) {
|
|
95
|
+
if (!dir)
|
|
96
|
+
continue;
|
|
97
|
+
const hooks = parseHooksYaml(dir);
|
|
98
|
+
const hook = hooks[name];
|
|
99
|
+
if (hook) {
|
|
100
|
+
// If this layer disables the hook, return null (disabled trumps lower layers)
|
|
101
|
+
if (hook.enabled === false) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
name,
|
|
106
|
+
item: hook,
|
|
107
|
+
layer,
|
|
108
|
+
path: getHooksYamlPath(dir),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
},
|
|
114
|
+
/**
|
|
115
|
+
* Sync resolved hooks to the agent's version home directory.
|
|
116
|
+
* Note: Actual hook registration is handled by registerHooksToSettings in hooks.ts.
|
|
117
|
+
* This method is a no-op placeholder for the interface contract.
|
|
118
|
+
*/
|
|
119
|
+
sync(_agent, _versionHome, _cwd) {
|
|
120
|
+
// Hook syncing is done via registerHooksToSettings in the main hooks.ts module.
|
|
121
|
+
// This handler only provides resolution; registration is a separate concern.
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Hooks use YAML format across all agents.
|
|
125
|
+
*/
|
|
126
|
+
format(_agent) {
|
|
127
|
+
return 'yaml';
|
|
128
|
+
},
|
|
129
|
+
/**
|
|
130
|
+
* Hooks are stored in the hooks directory.
|
|
131
|
+
*/
|
|
132
|
+
targetDir(_agent) {
|
|
133
|
+
return 'hooks';
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
export default HooksHandler;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified resource system - exports all handlers and provides a registry.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { handlers, getHandler } from './resources/index.js';
|
|
6
|
+
* const cmds = handlers.commands.listAll('claude');
|
|
7
|
+
*/
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export { CommandsHandler, commandsHandler, type CommandItem } from './commands.js';
|
|
10
|
+
export { HooksHandler, type HookItem } from './hooks.js';
|
|
11
|
+
export { SkillsHandler, type SkillItem } from './skills.js';
|
|
12
|
+
export { RulesHandler, type RuleItem } from './rules.js';
|
|
13
|
+
export { McpHandler, getMcpConfigPath, type McpItem } from './mcp.js';
|
|
14
|
+
export { PermissionsHandler, type PermissionItem } from './permissions.js';
|
|
15
|
+
export { SubagentsHandler, subagentsHandler, type SubagentItem } from './subagents.js';
|
|
16
|
+
import type { ResourceKind, ResourceHandler } from './types.js';
|
|
17
|
+
/** All resource handlers keyed by kind. */
|
|
18
|
+
export declare const handlers: {
|
|
19
|
+
readonly command: import("./commands.js").CommandsHandler;
|
|
20
|
+
readonly commands: import("./commands.js").CommandsHandler;
|
|
21
|
+
readonly hook: ResourceHandler<import("../types.js").ManifestHook>;
|
|
22
|
+
readonly hooks: ResourceHandler<import("../types.js").ManifestHook>;
|
|
23
|
+
readonly skill: ResourceHandler<import("./skills.js").SkillItem>;
|
|
24
|
+
readonly skills: ResourceHandler<import("./skills.js").SkillItem>;
|
|
25
|
+
readonly rule: ResourceHandler<import("./rules.js").RuleItem>;
|
|
26
|
+
readonly rules: ResourceHandler<import("./rules.js").RuleItem>;
|
|
27
|
+
readonly mcp: ResourceHandler<import("./mcp.js").McpItem>;
|
|
28
|
+
readonly permission: ResourceHandler<import("../types.js").PermissionSet>;
|
|
29
|
+
readonly permissions: ResourceHandler<import("../types.js").PermissionSet>;
|
|
30
|
+
readonly subagent: import("./subagents.js").SubagentsHandler;
|
|
31
|
+
readonly subagents: import("./subagents.js").SubagentsHandler;
|
|
32
|
+
};
|
|
33
|
+
/** Get a handler by resource kind. */
|
|
34
|
+
export declare function getHandler(kind: ResourceKind): ResourceHandler<unknown> | null;
|
|
35
|
+
/** All resource kinds. */
|
|
36
|
+
export declare const RESOURCE_KINDS: ResourceKind[];
|