@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
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP resource handler - lists, resolves, and syncs MCP server configs across layers.
|
|
3
|
+
*
|
|
4
|
+
* MCP servers are stored as YAML files in mcp/ directories:
|
|
5
|
+
* ~/.agents-system/mcp/ (system)
|
|
6
|
+
* ~/.agents/mcp/ (user)
|
|
7
|
+
* .agents/mcp/ (project)
|
|
8
|
+
*
|
|
9
|
+
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
10
|
+
* Sync writes into agent-specific config files (settings.json, config.toml, etc).
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as yaml from 'yaml';
|
|
15
|
+
import * as TOML from 'smol-toml';
|
|
16
|
+
import { getSystemMcpDir, getUserMcpDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
17
|
+
/** Agents from resources/types.ts that support MCP. */
|
|
18
|
+
const MCP_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'cursor', 'opencode', 'openclaw'];
|
|
19
|
+
/**
|
|
20
|
+
* Parse an MCP YAML file into an McpItem.
|
|
21
|
+
*/
|
|
22
|
+
function parseMcpYaml(filePath) {
|
|
23
|
+
if (!fs.existsSync(filePath)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
28
|
+
const parsed = yaml.parse(content);
|
|
29
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
// Validate required fields
|
|
33
|
+
if (!parsed.name || !parsed.transport) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// Validate transport-specific fields
|
|
37
|
+
if (parsed.transport === 'stdio' && !parsed.command) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if ((parsed.transport === 'http' || parsed.transport === 'sse') && !parsed.url) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
name: parsed.name,
|
|
45
|
+
transport: parsed.transport,
|
|
46
|
+
command: parsed.command,
|
|
47
|
+
args: parsed.args,
|
|
48
|
+
env: parsed.env,
|
|
49
|
+
url: parsed.url,
|
|
50
|
+
headers: parsed.headers,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get layer directories for MCP resolution.
|
|
59
|
+
*/
|
|
60
|
+
function getLayerDirs(cwd) {
|
|
61
|
+
const dirs = [];
|
|
62
|
+
// Project layer (highest priority)
|
|
63
|
+
const projectDir = getProjectAgentsDir(cwd || process.cwd());
|
|
64
|
+
if (projectDir) {
|
|
65
|
+
dirs.push({ layer: 'project', dir: path.join(projectDir, 'mcp') });
|
|
66
|
+
}
|
|
67
|
+
// User layer
|
|
68
|
+
dirs.push({ layer: 'user', dir: getUserMcpDir() });
|
|
69
|
+
// Extra repos (between user and system)
|
|
70
|
+
for (const { dir } of getEnabledExtraRepos()) {
|
|
71
|
+
dirs.push({ layer: 'user', dir: path.join(dir, 'mcp') });
|
|
72
|
+
}
|
|
73
|
+
// System layer (lowest priority)
|
|
74
|
+
dirs.push({ layer: 'system', dir: getSystemMcpDir() });
|
|
75
|
+
return dirs;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Scan a directory for MCP YAML files.
|
|
79
|
+
*/
|
|
80
|
+
function scanMcpDir(dir) {
|
|
81
|
+
const results = [];
|
|
82
|
+
if (!fs.existsSync(dir)) {
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!entry.isFile())
|
|
89
|
+
continue;
|
|
90
|
+
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
|
|
91
|
+
continue;
|
|
92
|
+
const filePath = path.join(dir, entry.name);
|
|
93
|
+
const item = parseMcpYaml(filePath);
|
|
94
|
+
if (item) {
|
|
95
|
+
results.push({ name: item.name, path: filePath, item });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Directory read failed
|
|
101
|
+
}
|
|
102
|
+
return results;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get the config file path for MCP for a given agent.
|
|
106
|
+
* Different agents use different config formats and locations.
|
|
107
|
+
*/
|
|
108
|
+
export function getMcpConfigPath(agent, versionHome) {
|
|
109
|
+
switch (agent) {
|
|
110
|
+
case 'claude':
|
|
111
|
+
return path.join(versionHome, '.claude', 'settings.json');
|
|
112
|
+
case 'codex':
|
|
113
|
+
return path.join(versionHome, '.codex', 'config.toml');
|
|
114
|
+
case 'opencode':
|
|
115
|
+
return path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
116
|
+
case 'cursor':
|
|
117
|
+
return path.join(versionHome, '.cursor', 'mcp.json');
|
|
118
|
+
case 'gemini':
|
|
119
|
+
return path.join(versionHome, '.gemini', 'settings.json');
|
|
120
|
+
case 'openclaw':
|
|
121
|
+
return path.join(versionHome, '.openclaw', 'openclaw.json');
|
|
122
|
+
default:
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Strip JSON comments (for JSONC files).
|
|
128
|
+
*/
|
|
129
|
+
function stripJsonComments(content) {
|
|
130
|
+
let result = '';
|
|
131
|
+
let inString = false;
|
|
132
|
+
let escape = false;
|
|
133
|
+
let i = 0;
|
|
134
|
+
while (i < content.length) {
|
|
135
|
+
const char = content[i];
|
|
136
|
+
const next = content[i + 1];
|
|
137
|
+
if (escape) {
|
|
138
|
+
result += char;
|
|
139
|
+
escape = false;
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (char === '\\' && inString) {
|
|
144
|
+
result += char;
|
|
145
|
+
escape = true;
|
|
146
|
+
i++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (char === '"') {
|
|
150
|
+
inString = !inString;
|
|
151
|
+
result += char;
|
|
152
|
+
i++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!inString) {
|
|
156
|
+
if (char === '/' && next === '/') {
|
|
157
|
+
while (i < content.length && content[i] !== '\n') {
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (char === '/' && next === '*') {
|
|
163
|
+
i += 2;
|
|
164
|
+
while (i < content.length && !(content[i] === '*' && content[i + 1] === '/')) {
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
i += 2;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
result += char;
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Write MCP servers to Claude settings.json format.
|
|
178
|
+
*/
|
|
179
|
+
function syncToClaudeConfig(configPath, items) {
|
|
180
|
+
let config = {};
|
|
181
|
+
if (fs.existsSync(configPath)) {
|
|
182
|
+
try {
|
|
183
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
config = {};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const mcpServers = {};
|
|
190
|
+
for (const item of items) {
|
|
191
|
+
if (item.transport === 'stdio') {
|
|
192
|
+
mcpServers[item.name] = {
|
|
193
|
+
command: item.command,
|
|
194
|
+
args: item.args || [],
|
|
195
|
+
env: item.env || {},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
mcpServers[item.name] = {
|
|
200
|
+
url: item.url,
|
|
201
|
+
...(item.headers && { headers: item.headers }),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
config.mcpServers = mcpServers;
|
|
206
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
207
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Write MCP servers to Codex config.toml format.
|
|
211
|
+
*/
|
|
212
|
+
function syncToCodexConfig(configPath, items) {
|
|
213
|
+
let config = {};
|
|
214
|
+
if (fs.existsSync(configPath)) {
|
|
215
|
+
try {
|
|
216
|
+
config = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
config = {};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const mcpServers = {};
|
|
223
|
+
for (const item of items) {
|
|
224
|
+
if (item.transport === 'stdio') {
|
|
225
|
+
mcpServers[item.name] = {
|
|
226
|
+
command: item.command,
|
|
227
|
+
args: item.args || [],
|
|
228
|
+
...(item.env && { env: item.env }),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// Codex may not support HTTP MCPs
|
|
232
|
+
}
|
|
233
|
+
config.mcp_servers = mcpServers;
|
|
234
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
235
|
+
fs.writeFileSync(configPath, TOML.stringify(config), 'utf-8');
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Write MCP servers to OpenCode opencode.jsonc format.
|
|
239
|
+
*/
|
|
240
|
+
function syncToOpenCodeConfig(configPath, items) {
|
|
241
|
+
let config = {};
|
|
242
|
+
if (fs.existsSync(configPath)) {
|
|
243
|
+
try {
|
|
244
|
+
const content = stripJsonComments(fs.readFileSync(configPath, 'utf-8'));
|
|
245
|
+
config = JSON.parse(content);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
config = {};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const mcp = {};
|
|
252
|
+
for (const item of items) {
|
|
253
|
+
if (item.transport === 'stdio') {
|
|
254
|
+
// OpenCode uses command as array
|
|
255
|
+
const commandArray = [item.command, ...(item.args || [])];
|
|
256
|
+
mcp[item.name] = {
|
|
257
|
+
type: 'local',
|
|
258
|
+
command: commandArray,
|
|
259
|
+
...(item.env && { env: item.env }),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
mcp[item.name] = {
|
|
264
|
+
type: 'remote',
|
|
265
|
+
url: item.url,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
config.mcp = mcp;
|
|
270
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
271
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Write MCP servers to Cursor mcp.json format.
|
|
275
|
+
*/
|
|
276
|
+
function syncToCursorConfig(configPath, items) {
|
|
277
|
+
let config = {};
|
|
278
|
+
if (fs.existsSync(configPath)) {
|
|
279
|
+
try {
|
|
280
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
config = {};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const mcpServers = {};
|
|
287
|
+
for (const item of items) {
|
|
288
|
+
if (item.transport === 'stdio') {
|
|
289
|
+
mcpServers[item.name] = {
|
|
290
|
+
command: item.command,
|
|
291
|
+
args: item.args || [],
|
|
292
|
+
env: item.env || {},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
mcpServers[item.name] = {
|
|
297
|
+
url: item.url,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
config.mcpServers = mcpServers;
|
|
302
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
303
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Write MCP servers to Gemini settings.json format.
|
|
307
|
+
*/
|
|
308
|
+
function syncToGeminiConfig(configPath, items) {
|
|
309
|
+
let config = {};
|
|
310
|
+
if (fs.existsSync(configPath)) {
|
|
311
|
+
try {
|
|
312
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
config = {};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const mcpServers = {};
|
|
319
|
+
for (const item of items) {
|
|
320
|
+
if (item.transport === 'stdio') {
|
|
321
|
+
mcpServers[item.name] = {
|
|
322
|
+
command: item.command,
|
|
323
|
+
args: item.args || [],
|
|
324
|
+
env: item.env || {},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
mcpServers[item.name] = {
|
|
329
|
+
url: item.url,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
config.mcpServers = mcpServers;
|
|
334
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
335
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Write MCP servers to OpenClaw openclaw.json format.
|
|
339
|
+
*/
|
|
340
|
+
function syncToOpenClawConfig(configPath, items) {
|
|
341
|
+
let config = {};
|
|
342
|
+
if (fs.existsSync(configPath)) {
|
|
343
|
+
try {
|
|
344
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
config = {};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (!config.mcp || typeof config.mcp !== 'object') {
|
|
351
|
+
config.mcp = {};
|
|
352
|
+
}
|
|
353
|
+
const servers = {};
|
|
354
|
+
for (const item of items) {
|
|
355
|
+
if (item.transport === 'stdio') {
|
|
356
|
+
servers[item.name] = {
|
|
357
|
+
command: item.command,
|
|
358
|
+
args: item.args,
|
|
359
|
+
env: item.env,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
servers[item.name] = {
|
|
364
|
+
url: item.url,
|
|
365
|
+
transport: item.transport,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
config.mcp.servers = servers;
|
|
370
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
371
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* MCP resource handler implementing ResourceHandler<McpItem>.
|
|
375
|
+
*/
|
|
376
|
+
export const McpHandler = {
|
|
377
|
+
kind: 'mcp',
|
|
378
|
+
listAll(agent, cwd) {
|
|
379
|
+
if (!MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
const results = new Map();
|
|
383
|
+
const layerDirs = getLayerDirs(cwd);
|
|
384
|
+
// Process in reverse order (system first) so higher layers override
|
|
385
|
+
for (let i = layerDirs.length - 1; i >= 0; i--) {
|
|
386
|
+
const { layer, dir } = layerDirs[i];
|
|
387
|
+
const items = scanMcpDir(dir);
|
|
388
|
+
for (const { name, path: itemPath, item } of items) {
|
|
389
|
+
results.set(name, {
|
|
390
|
+
name,
|
|
391
|
+
item,
|
|
392
|
+
layer,
|
|
393
|
+
path: itemPath,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return Array.from(results.values());
|
|
398
|
+
},
|
|
399
|
+
resolve(agent, name, cwd) {
|
|
400
|
+
if (!MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
const layerDirs = getLayerDirs(cwd);
|
|
404
|
+
// Check in priority order (project first)
|
|
405
|
+
for (const { layer, dir } of layerDirs) {
|
|
406
|
+
if (!fs.existsSync(dir))
|
|
407
|
+
continue;
|
|
408
|
+
try {
|
|
409
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
410
|
+
for (const entry of entries) {
|
|
411
|
+
if (!entry.isFile())
|
|
412
|
+
continue;
|
|
413
|
+
if (!entry.name.endsWith('.yaml') && !entry.name.endsWith('.yml'))
|
|
414
|
+
continue;
|
|
415
|
+
const filePath = path.join(dir, entry.name);
|
|
416
|
+
const item = parseMcpYaml(filePath);
|
|
417
|
+
if (item && item.name === name) {
|
|
418
|
+
return {
|
|
419
|
+
name,
|
|
420
|
+
item,
|
|
421
|
+
layer,
|
|
422
|
+
path: filePath,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
},
|
|
433
|
+
sync(agent, versionHome, cwd) {
|
|
434
|
+
if (!MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const items = this.listAll(agent, cwd);
|
|
438
|
+
if (items.length === 0) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const configPath = getMcpConfigPath(agent, versionHome);
|
|
442
|
+
if (!configPath) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const mcpItems = items.map((r) => r.item);
|
|
446
|
+
switch (agent) {
|
|
447
|
+
case 'claude':
|
|
448
|
+
syncToClaudeConfig(configPath, mcpItems);
|
|
449
|
+
break;
|
|
450
|
+
case 'codex':
|
|
451
|
+
syncToCodexConfig(configPath, mcpItems);
|
|
452
|
+
break;
|
|
453
|
+
case 'opencode':
|
|
454
|
+
syncToOpenCodeConfig(configPath, mcpItems);
|
|
455
|
+
break;
|
|
456
|
+
case 'cursor':
|
|
457
|
+
syncToCursorConfig(configPath, mcpItems);
|
|
458
|
+
break;
|
|
459
|
+
case 'gemini':
|
|
460
|
+
syncToGeminiConfig(configPath, mcpItems);
|
|
461
|
+
break;
|
|
462
|
+
case 'openclaw':
|
|
463
|
+
syncToOpenClawConfig(configPath, mcpItems);
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
format(agent) {
|
|
468
|
+
switch (agent) {
|
|
469
|
+
case 'codex':
|
|
470
|
+
return 'toml';
|
|
471
|
+
default:
|
|
472
|
+
return 'json';
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
targetDir(_agent) {
|
|
476
|
+
// MCP doesn't have a target directory - it modifies config files
|
|
477
|
+
return 'mcp';
|
|
478
|
+
},
|
|
479
|
+
configPath(agent, versionHome) {
|
|
480
|
+
return getMcpConfigPath(agent, versionHome);
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
export default McpHandler;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionsHandler - ResourceHandler implementation for permissions.
|
|
3
|
+
*
|
|
4
|
+
* Permissions are stored as YAML files in permissions/ directories at each layer.
|
|
5
|
+
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
|
+
* Unlike other resources, permissions merge into agent-specific config files
|
|
7
|
+
* (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
|
|
8
|
+
*/
|
|
9
|
+
import type { ResourceHandler } from './types.js';
|
|
10
|
+
import type { PermissionSet } from '../types.js';
|
|
11
|
+
export type PermissionItem = PermissionSet;
|
|
12
|
+
export declare const PermissionsHandler: ResourceHandler<PermissionItem>;
|
|
13
|
+
export default PermissionsHandler;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionsHandler - ResourceHandler implementation for permissions.
|
|
3
|
+
*
|
|
4
|
+
* Permissions are stored as YAML files in permissions/ directories at each layer.
|
|
5
|
+
* Resolution: project > user > system (higher layer wins on name conflict).
|
|
6
|
+
* Unlike other resources, permissions merge into agent-specific config files
|
|
7
|
+
* (Claude: settings.json, Codex: config.toml, OpenCode: opencode.jsonc).
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { getSystemAgentsDir, getUserAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
|
|
12
|
+
import { parsePermissionSet, applyPermissionsToVersion, mergePermissionSets, PERMISSIONS_CAPABLE_AGENTS, } from '../permissions.js';
|
|
13
|
+
/**
|
|
14
|
+
* Get the permissions directory for a given layer root.
|
|
15
|
+
*/
|
|
16
|
+
function getPermissionsDirForRoot(root) {
|
|
17
|
+
return path.join(root, 'permissions');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get layer directories for permission resolution.
|
|
21
|
+
*/
|
|
22
|
+
function getLayerDirs(cwd) {
|
|
23
|
+
return {
|
|
24
|
+
system: getSystemAgentsDir(),
|
|
25
|
+
user: getUserAgentsDir(),
|
|
26
|
+
project: cwd ? getProjectAgentsDir(cwd) : null,
|
|
27
|
+
extra: getEnabledExtraRepos().map((e) => e.dir),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List permission files in a directory.
|
|
32
|
+
* Returns only YAML files, stripping the extension for the name.
|
|
33
|
+
*/
|
|
34
|
+
function listPermissionsInDir(dir) {
|
|
35
|
+
if (!fs.existsSync(dir))
|
|
36
|
+
return [];
|
|
37
|
+
try {
|
|
38
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
const permissions = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry.isFile())
|
|
42
|
+
continue;
|
|
43
|
+
if (entry.name.startsWith('.'))
|
|
44
|
+
continue;
|
|
45
|
+
if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
|
|
46
|
+
permissions.push({
|
|
47
|
+
name: entry.name.replace(/\.(yaml|yml)$/, ''),
|
|
48
|
+
path: path.join(dir, entry.name),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return permissions;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the config file path for an agent's permissions.
|
|
60
|
+
*/
|
|
61
|
+
function getAgentConfigPath(agent, versionHome) {
|
|
62
|
+
switch (agent) {
|
|
63
|
+
case 'claude':
|
|
64
|
+
return path.join(versionHome, '.claude', 'settings.json');
|
|
65
|
+
case 'codex':
|
|
66
|
+
return path.join(versionHome, '.codex', 'config.toml');
|
|
67
|
+
case 'opencode':
|
|
68
|
+
return path.join(versionHome, '.opencode', 'opencode.jsonc');
|
|
69
|
+
default:
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const PermissionsHandler = {
|
|
74
|
+
kind: 'permission',
|
|
75
|
+
/**
|
|
76
|
+
* List all permissions across layers, with higher layer winning on name conflict.
|
|
77
|
+
* Returns a union of all permissions, deduplicated by name.
|
|
78
|
+
*/
|
|
79
|
+
listAll(agent, cwd) {
|
|
80
|
+
const layers = getLayerDirs(cwd);
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const results = [];
|
|
83
|
+
// Build layer roots in precedence order: project > user > system > extras
|
|
84
|
+
const roots = [];
|
|
85
|
+
if (layers.project) {
|
|
86
|
+
roots.push({ dir: getPermissionsDirForRoot(layers.project), layer: 'project' });
|
|
87
|
+
}
|
|
88
|
+
roots.push({ dir: getPermissionsDirForRoot(layers.user), layer: 'user' });
|
|
89
|
+
roots.push({ dir: getPermissionsDirForRoot(layers.system), layer: 'system' });
|
|
90
|
+
for (const extraDir of layers.extra) {
|
|
91
|
+
roots.push({ dir: getPermissionsDirForRoot(extraDir), layer: 'system' });
|
|
92
|
+
}
|
|
93
|
+
for (const { dir, layer } of roots) {
|
|
94
|
+
const permissions = listPermissionsInDir(dir);
|
|
95
|
+
for (const perm of permissions) {
|
|
96
|
+
if (seen.has(perm.name))
|
|
97
|
+
continue;
|
|
98
|
+
seen.add(perm.name);
|
|
99
|
+
const item = parsePermissionSet(perm.path);
|
|
100
|
+
if (item) {
|
|
101
|
+
results.push({
|
|
102
|
+
name: perm.name,
|
|
103
|
+
item,
|
|
104
|
+
layer,
|
|
105
|
+
path: perm.path,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
111
|
+
},
|
|
112
|
+
/**
|
|
113
|
+
* Resolve a single permission by name.
|
|
114
|
+
* Returns the winning layer's version, or null if not found.
|
|
115
|
+
*/
|
|
116
|
+
resolve(agent, name, cwd) {
|
|
117
|
+
const layers = getLayerDirs(cwd);
|
|
118
|
+
// Build candidate paths in precedence order: project > user > system > extras
|
|
119
|
+
const candidates = [];
|
|
120
|
+
if (layers.project) {
|
|
121
|
+
candidates.push({ dir: getPermissionsDirForRoot(layers.project), layer: 'project' });
|
|
122
|
+
}
|
|
123
|
+
candidates.push({ dir: getPermissionsDirForRoot(layers.user), layer: 'user' });
|
|
124
|
+
candidates.push({ dir: getPermissionsDirForRoot(layers.system), layer: 'system' });
|
|
125
|
+
for (const extraDir of layers.extra) {
|
|
126
|
+
candidates.push({ dir: getPermissionsDirForRoot(extraDir), layer: 'system' });
|
|
127
|
+
}
|
|
128
|
+
for (const { dir, layer } of candidates) {
|
|
129
|
+
// Try .yaml first, then .yml
|
|
130
|
+
for (const ext of ['.yaml', '.yml']) {
|
|
131
|
+
const filePath = path.join(dir, `${name}${ext}`);
|
|
132
|
+
const item = parsePermissionSet(filePath);
|
|
133
|
+
if (item) {
|
|
134
|
+
return { name, item, layer, path: filePath };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* Sync resolved permissions to the agent's version home config file.
|
|
142
|
+
* Merges all resolved permissions into a single set and applies to the agent's config.
|
|
143
|
+
*/
|
|
144
|
+
sync(agent, versionHome, cwd) {
|
|
145
|
+
// Only sync to agents that support permissions
|
|
146
|
+
if (!PERMISSIONS_CAPABLE_AGENTS.includes(agent)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const resolved = this.listAll(agent, cwd);
|
|
150
|
+
if (resolved.length === 0)
|
|
151
|
+
return;
|
|
152
|
+
// Merge all permission sets into one
|
|
153
|
+
let merged = {
|
|
154
|
+
name: 'merged',
|
|
155
|
+
description: 'Merged from all layers',
|
|
156
|
+
allow: [],
|
|
157
|
+
deny: [],
|
|
158
|
+
};
|
|
159
|
+
for (const r of resolved) {
|
|
160
|
+
merged = mergePermissionSets(merged, r.item);
|
|
161
|
+
}
|
|
162
|
+
// Apply to the agent's config file
|
|
163
|
+
applyPermissionsToVersion(agent, merged, versionHome, true);
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Permissions use YAML format.
|
|
167
|
+
*/
|
|
168
|
+
format(_agent) {
|
|
169
|
+
return 'yaml';
|
|
170
|
+
},
|
|
171
|
+
/**
|
|
172
|
+
* Permissions directory name.
|
|
173
|
+
*/
|
|
174
|
+
targetDir(_agent) {
|
|
175
|
+
return 'permissions';
|
|
176
|
+
},
|
|
177
|
+
/**
|
|
178
|
+
* Return the config file path where permissions are merged.
|
|
179
|
+
*/
|
|
180
|
+
configPath(agent, versionHome) {
|
|
181
|
+
return getAgentConfigPath(agent, versionHome);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
export default PermissionsHandler;
|