@phnx-labs/agents-cli 1.18.1 → 1.18.3
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 +22 -0
- package/dist/commands/doctor.js +19 -5
- package/dist/commands/exec.js +9 -4
- package/dist/commands/plugins.js +58 -14
- package/dist/commands/view.js +16 -7
- package/dist/index.js +30 -0
- package/dist/lib/hooks.js +21 -3
- package/dist/lib/migrate.js +35 -12
- package/dist/lib/plugin-marketplace.d.ts +93 -0
- package/dist/lib/plugin-marketplace.js +239 -0
- package/dist/lib/plugins.d.ts +25 -13
- package/dist/lib/plugins.js +350 -566
- package/dist/lib/shims.d.ts +3 -1
- package/dist/lib/shims.js +81 -7
- package/dist/lib/staleness/checkers/commands.d.ts +7 -0
- package/dist/lib/staleness/checkers/commands.js +27 -0
- package/dist/lib/staleness/checkers/hooks.d.ts +13 -0
- package/dist/lib/staleness/checkers/hooks.js +63 -0
- package/dist/lib/staleness/checkers/mcp.d.ts +12 -0
- package/dist/lib/staleness/checkers/mcp.js +38 -0
- package/dist/lib/staleness/checkers/permissions.d.ts +17 -0
- package/dist/lib/staleness/checkers/permissions.js +73 -0
- package/dist/lib/staleness/checkers/plugins.d.ts +11 -0
- package/dist/lib/staleness/checkers/plugins.js +39 -0
- package/dist/lib/staleness/checkers/rules.d.ts +19 -0
- package/dist/lib/staleness/checkers/rules.js +86 -0
- package/dist/lib/staleness/checkers/skills.d.ts +7 -0
- package/dist/lib/staleness/checkers/skills.js +34 -0
- package/dist/lib/staleness/checkers/subagents.d.ts +12 -0
- package/dist/lib/staleness/checkers/subagents.js +39 -0
- package/dist/lib/staleness/checkers/types.d.ts +44 -0
- package/dist/lib/staleness/checkers/types.js +20 -0
- package/dist/lib/staleness/checkers/workflows.d.ts +10 -0
- package/dist/lib/staleness/checkers/workflows.js +37 -0
- package/dist/lib/staleness/fingerprint.d.ts +38 -0
- package/dist/lib/staleness/fingerprint.js +154 -0
- package/dist/lib/staleness/index.d.ts +26 -0
- package/dist/lib/staleness/index.js +122 -0
- package/dist/lib/staleness/layers.d.ts +37 -0
- package/dist/lib/staleness/layers.js +100 -0
- package/dist/lib/staleness/types.d.ts +56 -0
- package/dist/lib/staleness/types.js +6 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/teams/agents.d.ts +11 -20
- package/dist/lib/teams/agents.js +55 -202
- package/dist/lib/teams/index.d.ts +3 -2
- package/dist/lib/teams/index.js +2 -2
- package/dist/lib/teams/persistence.d.ts +0 -38
- package/dist/lib/teams/persistence.js +7 -329
- package/dist/lib/teams/registry.js +7 -5
- package/dist/lib/types.d.ts +6 -0
- package/dist/lib/versions.js +34 -12
- package/package.json +1 -1
- package/dist/lib/sync-manifest.d.ts +0 -81
- package/dist/lib/sync-manifest.js +0 -450
package/dist/lib/plugins.js
CHANGED
|
@@ -14,6 +14,7 @@ import { execSync } from 'child_process';
|
|
|
14
14
|
import { getPluginsDir, getTrashPluginsDir } from './state.js';
|
|
15
15
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
16
16
|
import { AGENTS, PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
17
|
+
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, enablePluginInSettings, disablePluginInSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, } from './plugin-marketplace.js';
|
|
17
18
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
18
19
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
19
20
|
const USER_CONFIG_FILE = '.user-config.json';
|
|
@@ -51,6 +52,9 @@ export function buildDiscoveredPlugin(pluginRoot, manifest) {
|
|
|
51
52
|
commands: discoverPluginCommands(pluginRoot),
|
|
52
53
|
agentDefs: discoverPluginAgentDefs(pluginRoot),
|
|
53
54
|
bin: discoverPluginBin(pluginRoot),
|
|
55
|
+
mcpServers: discoverPluginMcpServers(pluginRoot),
|
|
56
|
+
lspServers: discoverPluginLspServers(pluginRoot),
|
|
57
|
+
monitors: discoverPluginMonitors(pluginRoot),
|
|
54
58
|
hasMcp: fs.existsSync(path.join(pluginRoot, '.mcp.json')),
|
|
55
59
|
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
56
60
|
};
|
|
@@ -149,6 +153,47 @@ export function discoverPluginBin(pluginRoot) {
|
|
|
149
153
|
return [];
|
|
150
154
|
return fs.readdirSync(binDir).filter(f => !f.startsWith('.'));
|
|
151
155
|
}
|
|
156
|
+
/** Discover MCP server names from .mcp.json at the plugin root. */
|
|
157
|
+
export function discoverPluginMcpServers(pluginRoot) {
|
|
158
|
+
const mcpFile = path.join(pluginRoot, '.mcp.json');
|
|
159
|
+
if (!fs.existsSync(mcpFile))
|
|
160
|
+
return [];
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
|
|
163
|
+
return parsed.mcpServers ? Object.keys(parsed.mcpServers) : [];
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Discover LSP server keys from .lsp.json at the plugin root. */
|
|
170
|
+
export function discoverPluginLspServers(pluginRoot) {
|
|
171
|
+
const lspFile = path.join(pluginRoot, '.lsp.json');
|
|
172
|
+
if (!fs.existsSync(lspFile))
|
|
173
|
+
return [];
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(fs.readFileSync(lspFile, 'utf-8'));
|
|
176
|
+
return Object.keys(parsed);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Discover monitor names from monitors/monitors.json. */
|
|
183
|
+
export function discoverPluginMonitors(pluginRoot) {
|
|
184
|
+
const monitorsFile = path.join(pluginRoot, 'monitors', 'monitors.json');
|
|
185
|
+
if (!fs.existsSync(monitorsFile))
|
|
186
|
+
return [];
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(fs.readFileSync(monitorsFile, 'utf-8'));
|
|
189
|
+
if (!Array.isArray(parsed))
|
|
190
|
+
return [];
|
|
191
|
+
return parsed.map(m => m.name).filter((n) => typeof n === 'string');
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
152
197
|
/** Return true if settings.json contains non-permission keys worth merging. */
|
|
153
198
|
function pluginHasNonPermissionSettings(pluginRoot) {
|
|
154
199
|
const settingsPath = path.join(pluginRoot, 'settings.json');
|
|
@@ -221,15 +266,18 @@ export function checkPluginDependencies(manifest) {
|
|
|
221
266
|
/**
|
|
222
267
|
* Sync a plugin to a specific agent version's home directory.
|
|
223
268
|
*
|
|
224
|
-
* For
|
|
225
|
-
* 1. Copy plugin
|
|
226
|
-
* 2.
|
|
227
|
-
* 3.
|
|
228
|
-
* 4.
|
|
229
|
-
* 5.
|
|
230
|
-
* 6.
|
|
231
|
-
*
|
|
232
|
-
*
|
|
269
|
+
* For plugins-capable agents (claude, openclaw):
|
|
270
|
+
* 1. Copy plugin source into <versionHome>/.<agent>/plugins/marketplaces/agents-cli/plugins/<name>/
|
|
271
|
+
* 2. Pre-expand ${user_config.*} variables in copied text files (Claude doesn't know this var).
|
|
272
|
+
* 3. (Re-)synthesize the marketplace.json catalog from the installed plugins.
|
|
273
|
+
* 4. Register the synthetic marketplace in known_marketplaces.json.
|
|
274
|
+
* 5. Mark <plugin>@agents-cli enabled in settings.json#enabledPlugins.
|
|
275
|
+
* 6. Migrate (remove) legacy dual-dash skills/commands/agents/bin/hooks/mcp entries.
|
|
276
|
+
*
|
|
277
|
+
* Claude/OpenClaw natively handle the plugin's skills, commands, agents, hooks,
|
|
278
|
+
* MCP servers, bin/, settings.json, and permissions once the plugin lives at the
|
|
279
|
+
* native install path and is marked enabled — see
|
|
280
|
+
* https://code.claude.com/docs/en/plugins.
|
|
233
281
|
*/
|
|
234
282
|
export function syncPluginToVersion(plugin, agent, versionHome) {
|
|
235
283
|
const result = {
|
|
@@ -247,522 +295,212 @@ export function syncPluginToVersion(plugin, agent, versionHome) {
|
|
|
247
295
|
return result;
|
|
248
296
|
}
|
|
249
297
|
const userConfig = loadUserConfig(plugin.name);
|
|
250
|
-
// 1.
|
|
251
|
-
|
|
252
|
-
// 2.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (agent === 'claude') {
|
|
274
|
-
result.settings = syncPluginSettings(plugin, agent, versionHome);
|
|
275
|
-
}
|
|
276
|
-
// 8. Sync permissions (Claude only - uses settings.json permissions)
|
|
277
|
-
if (agent === 'claude') {
|
|
278
|
-
result.permissions = syncPluginPermissions(plugin, agent, versionHome, userConfig);
|
|
279
|
-
}
|
|
280
|
-
result.success =
|
|
281
|
-
result.skills.length > 0 ||
|
|
282
|
-
result.commands.length > 0 ||
|
|
283
|
-
result.agentDefs.length > 0 ||
|
|
284
|
-
result.bin.length > 0 ||
|
|
285
|
-
result.hooks.length > 0 ||
|
|
286
|
-
result.mcp ||
|
|
287
|
-
result.settings ||
|
|
288
|
-
result.permissions;
|
|
298
|
+
// 1. Copy plugin to native marketplace install dir.
|
|
299
|
+
const installDir = copyPluginToMarketplace(plugin, agent, versionHome);
|
|
300
|
+
// 2. Pre-expand ${user_config.*} in the copy. Leave ${CLAUDE_PLUGIN_ROOT} /
|
|
301
|
+
// ${CLAUDE_PLUGIN_DATA} alone — Claude expands those natively at runtime.
|
|
302
|
+
if (Object.keys(userConfig).length > 0) {
|
|
303
|
+
expandUserConfigInDir(installDir, userConfig);
|
|
304
|
+
}
|
|
305
|
+
// 3-5. Synthesize manifest, register marketplace, enable plugin.
|
|
306
|
+
syncMarketplaceManifest(agent, versionHome);
|
|
307
|
+
registerMarketplace(agent, versionHome);
|
|
308
|
+
enablePluginInSettings(plugin.name, agent, versionHome);
|
|
309
|
+
// 6. Migrate legacy dual-dash flat layout from previous versions of agents-cli.
|
|
310
|
+
migrateLegacyFlatLayout(plugin, agent, versionHome);
|
|
311
|
+
// Populate the result shape for backward-compatible callers/reporting.
|
|
312
|
+
result.skills = plugin.skills.map(s => `${plugin.name}:${s}`);
|
|
313
|
+
result.commands = plugin.commands.map(c => `${plugin.name}:${c}`);
|
|
314
|
+
result.agentDefs = plugin.agentDefs.map(a => `${plugin.name}:${a}`);
|
|
315
|
+
result.bin = plugin.bin;
|
|
316
|
+
result.hooks = plugin.hooks;
|
|
317
|
+
result.mcp = plugin.hasMcp;
|
|
318
|
+
result.settings = plugin.hasSettings;
|
|
319
|
+
result.permissions = pluginHasPermissions(plugin);
|
|
320
|
+
result.success = true;
|
|
289
321
|
return result;
|
|
290
322
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const pluginSkillsDir = path.join(plugin.root, 'skills');
|
|
299
|
-
if (!fs.existsSync(pluginSkillsDir))
|
|
300
|
-
return synced;
|
|
301
|
-
const targetSkillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
302
|
-
fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
303
|
-
for (const skillName of plugin.skills) {
|
|
304
|
-
const srcDir = path.join(pluginSkillsDir, skillName);
|
|
305
|
-
const fsSafeName = `${plugin.name}--${skillName}`;
|
|
306
|
-
const destDir = path.join(targetSkillsDir, fsSafeName);
|
|
307
|
-
try {
|
|
308
|
-
if (fs.existsSync(destDir)) {
|
|
309
|
-
fs.rmSync(destDir, { recursive: true, force: true });
|
|
310
|
-
}
|
|
311
|
-
copyDirWithVarExpansion(srcDir, destDir, plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
312
|
-
synced.push(`${plugin.name}:${skillName}`);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
// Skip on error
|
|
316
|
-
}
|
|
323
|
+
function pluginHasPermissions(plugin) {
|
|
324
|
+
const settingsPath = path.join(plugin.root, 'settings.json');
|
|
325
|
+
if (!fs.existsSync(settingsPath))
|
|
326
|
+
return false;
|
|
327
|
+
try {
|
|
328
|
+
const parsed = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
329
|
+
return !!(parsed.permissions?.allow?.length || parsed.permissions?.deny?.length);
|
|
317
330
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Copy plugin commands into the version's commands directory.
|
|
322
|
-
* Commands are namespaced: pluginName--commandName.md
|
|
323
|
-
*/
|
|
324
|
-
function syncPluginCommands(plugin, agent, versionHome, userConfig) {
|
|
325
|
-
const synced = [];
|
|
326
|
-
const pluginCommandsDir = path.join(plugin.root, 'commands');
|
|
327
|
-
if (!fs.existsSync(pluginCommandsDir) || plugin.commands.length === 0)
|
|
328
|
-
return synced;
|
|
329
|
-
const agentConfig = AGENTS[agent];
|
|
330
|
-
const commandsTarget = path.join(versionHome, `.${agent}`, agentConfig.commandsSubdir);
|
|
331
|
-
fs.mkdirSync(commandsTarget, { recursive: true });
|
|
332
|
-
for (const cmdName of plugin.commands) {
|
|
333
|
-
const srcFile = path.join(pluginCommandsDir, `${cmdName}.md`);
|
|
334
|
-
if (!fs.existsSync(srcFile))
|
|
335
|
-
continue;
|
|
336
|
-
const destName = `${plugin.name}--${cmdName}.md`;
|
|
337
|
-
const destFile = path.join(commandsTarget, destName);
|
|
338
|
-
try {
|
|
339
|
-
let content = fs.readFileSync(srcFile, 'utf-8');
|
|
340
|
-
content = expandPluginVars(content, plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
341
|
-
fs.writeFileSync(destFile, content, 'utf-8');
|
|
342
|
-
synced.push(`${plugin.name}:${cmdName}`);
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
// Skip on error
|
|
346
|
-
}
|
|
331
|
+
catch {
|
|
332
|
+
return false;
|
|
347
333
|
}
|
|
348
|
-
return synced;
|
|
349
334
|
}
|
|
350
335
|
/**
|
|
351
|
-
*
|
|
352
|
-
*
|
|
336
|
+
* Walk a directory and replace ${user_config.*} placeholders in text files.
|
|
337
|
+
* Leaves all other variables (${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}) alone.
|
|
353
338
|
*/
|
|
354
|
-
function
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
fs.mkdirSync(agentsTarget, { recursive: true });
|
|
361
|
-
for (const agentDefName of plugin.agentDefs) {
|
|
362
|
-
const srcFile = path.join(pluginAgentsDir, `${agentDefName}.md`);
|
|
363
|
-
if (!fs.existsSync(srcFile))
|
|
339
|
+
function expandUserConfigInDir(dir, userConfig) {
|
|
340
|
+
const textExtensions = new Set(['.md', '.json', '.sh', '.py', '.js', '.ts', '.yaml', '.yml', '.toml', '.txt']);
|
|
341
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
342
|
+
const full = path.join(dir, entry.name);
|
|
343
|
+
if (entry.isDirectory()) {
|
|
344
|
+
expandUserConfigInDir(full, userConfig);
|
|
364
345
|
continue;
|
|
365
|
-
const destName = `${plugin.name}--${agentDefName}.md`;
|
|
366
|
-
const destFile = path.join(agentsTarget, destName);
|
|
367
|
-
try {
|
|
368
|
-
let content = fs.readFileSync(srcFile, 'utf-8');
|
|
369
|
-
content = expandPluginVars(content, plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
370
|
-
fs.writeFileSync(destFile, content, 'utf-8');
|
|
371
|
-
synced.push(`${plugin.name}:${agentDefName}`);
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
// Skip on error
|
|
375
346
|
}
|
|
376
|
-
|
|
377
|
-
return synced;
|
|
378
|
-
}
|
|
379
|
-
/**
|
|
380
|
-
* Copy plugin bin executables into the version home plugin-bin/<pluginName>/ directory.
|
|
381
|
-
* Records the bin path in settings.json under pluginBinPaths so callers can add it to PATH.
|
|
382
|
-
*/
|
|
383
|
-
function syncPluginBin(plugin, agent, versionHome) {
|
|
384
|
-
const synced = [];
|
|
385
|
-
const pluginBinDir = path.join(plugin.root, 'bin');
|
|
386
|
-
if (!fs.existsSync(pluginBinDir) || plugin.bin.length === 0)
|
|
387
|
-
return synced;
|
|
388
|
-
const targetBinDir = path.join(versionHome, `.${agent}`, 'plugin-bin', plugin.name);
|
|
389
|
-
fs.mkdirSync(targetBinDir, { recursive: true });
|
|
390
|
-
for (const binFile of plugin.bin) {
|
|
391
|
-
const srcFile = path.join(pluginBinDir, binFile);
|
|
392
|
-
if (!fs.existsSync(srcFile))
|
|
347
|
+
if (!textExtensions.has(path.extname(entry.name).toLowerCase()))
|
|
393
348
|
continue;
|
|
394
|
-
const destFile = path.join(targetBinDir, binFile);
|
|
395
349
|
try {
|
|
396
|
-
fs.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (synced.length === 0)
|
|
406
|
-
return synced;
|
|
407
|
-
// Note the bin path in settings.json so the calling shim can add it to PATH.
|
|
408
|
-
const configDir = path.join(versionHome, `.${agent}`);
|
|
409
|
-
const settingsPath = path.join(configDir, 'settings.json');
|
|
410
|
-
let settings = {};
|
|
411
|
-
if (fs.existsSync(settingsPath)) {
|
|
412
|
-
try {
|
|
413
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
350
|
+
const content = fs.readFileSync(full, 'utf-8');
|
|
351
|
+
if (!content.includes('${user_config.'))
|
|
352
|
+
continue;
|
|
353
|
+
const expanded = content.replace(/\$\{user_config\.([^}]+)\}/g, (_, key) => userConfig[key] ?? '');
|
|
354
|
+
if (expanded !== content) {
|
|
355
|
+
fs.writeFileSync(full, expanded, 'utf-8');
|
|
356
|
+
}
|
|
414
357
|
}
|
|
415
|
-
catch { /*
|
|
416
|
-
}
|
|
417
|
-
if (!Array.isArray(settings.pluginBinPaths)) {
|
|
418
|
-
settings.pluginBinPaths = [];
|
|
358
|
+
catch { /* skip unreadable */ }
|
|
419
359
|
}
|
|
420
|
-
const binPaths = settings.pluginBinPaths;
|
|
421
|
-
if (!binPaths.includes(targetBinDir)) {
|
|
422
|
-
binPaths.push(targetBinDir);
|
|
423
|
-
}
|
|
424
|
-
try {
|
|
425
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
426
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
427
|
-
}
|
|
428
|
-
catch { /* ignore write errors */ }
|
|
429
|
-
return synced;
|
|
430
360
|
}
|
|
431
361
|
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
362
|
+
* Remove legacy <plugin>--* entries from a version home, left by the previous
|
|
363
|
+
* flatten-based sync. Safe to call repeatedly — only deletes paths matching the
|
|
364
|
+
* plugin's prefix.
|
|
435
365
|
*/
|
|
436
|
-
function
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const settingsPath = path.join(configDir, 'settings.json');
|
|
450
|
-
let settings = {};
|
|
451
|
-
if (fs.existsSync(settingsPath)) {
|
|
452
|
-
try {
|
|
453
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
366
|
+
function migrateLegacyFlatLayout(plugin, agent, versionHome) {
|
|
367
|
+
const prefix = `${plugin.name}--`;
|
|
368
|
+
const agentRoot = path.join(versionHome, `.${agent}`);
|
|
369
|
+
// 1. skills
|
|
370
|
+
const skillsDir = path.join(agentRoot, 'skills');
|
|
371
|
+
if (fs.existsSync(skillsDir)) {
|
|
372
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
373
|
+
if (entry.isDirectory() && entry.name.startsWith(prefix)) {
|
|
374
|
+
try {
|
|
375
|
+
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
|
|
376
|
+
}
|
|
377
|
+
catch { /* skip */ }
|
|
378
|
+
}
|
|
454
379
|
}
|
|
455
|
-
catch { /* start fresh */ }
|
|
456
380
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const matcher = group.matcher || '';
|
|
468
|
-
const expandedHooks = (group.hooks || []).map(h => ({
|
|
469
|
-
...h,
|
|
470
|
-
command: expandPluginVars(h.command, plugin.root, plugin.name, agent, versionHome, userConfig),
|
|
471
|
-
}));
|
|
472
|
-
let matcherGroup = eventEntries.find(e => (e.matcher || '') === matcher);
|
|
473
|
-
if (!matcherGroup) {
|
|
474
|
-
matcherGroup = { matcher, hooks: [] };
|
|
475
|
-
eventEntries.push(matcherGroup);
|
|
476
|
-
}
|
|
477
|
-
if (!matcherGroup.hooks) {
|
|
478
|
-
matcherGroup.hooks = [];
|
|
479
|
-
}
|
|
480
|
-
for (const hook of expandedHooks) {
|
|
481
|
-
const exists = matcherGroup.hooks.some(h => h.command === hook.command);
|
|
482
|
-
if (!exists) {
|
|
483
|
-
matcherGroup.hooks.push(hook);
|
|
381
|
+
// 2. commands
|
|
382
|
+
if (agent === 'claude' || agent === 'openclaw') {
|
|
383
|
+
const cmdsDir = path.join(agentRoot, AGENTS[agent]?.commandsSubdir ?? 'commands');
|
|
384
|
+
if (fs.existsSync(cmdsDir)) {
|
|
385
|
+
for (const entry of fs.readdirSync(cmdsDir, { withFileTypes: true })) {
|
|
386
|
+
if (entry.isFile() && entry.name.startsWith(prefix) && entry.name.endsWith('.md')) {
|
|
387
|
+
try {
|
|
388
|
+
fs.unlinkSync(path.join(cmdsDir, entry.name));
|
|
389
|
+
}
|
|
390
|
+
catch { /* skip */ }
|
|
484
391
|
}
|
|
485
392
|
}
|
|
486
393
|
}
|
|
487
|
-
synced.push(event);
|
|
488
|
-
}
|
|
489
|
-
try {
|
|
490
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
491
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
492
|
-
}
|
|
493
|
-
catch { /* ignore write errors */ }
|
|
494
|
-
return synced;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Merge plugin .mcp.json MCP server definitions into Claude's settings.json.
|
|
498
|
-
* Server names are namespaced as pluginName--serverName to avoid collisions.
|
|
499
|
-
* Expands ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}, ${user_config.*} in args and env.
|
|
500
|
-
*/
|
|
501
|
-
function syncPluginMcp(plugin, agent, versionHome, userConfig) {
|
|
502
|
-
const mcpFile = path.join(plugin.root, '.mcp.json');
|
|
503
|
-
if (!fs.existsSync(mcpFile))
|
|
504
|
-
return false;
|
|
505
|
-
let pluginMcp;
|
|
506
|
-
try {
|
|
507
|
-
pluginMcp = JSON.parse(fs.readFileSync(mcpFile, 'utf-8'));
|
|
508
394
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
try {
|
|
520
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
395
|
+
// 3. agent definitions
|
|
396
|
+
const agentsDir = path.join(agentRoot, 'agents');
|
|
397
|
+
if (fs.existsSync(agentsDir)) {
|
|
398
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
399
|
+
if (entry.isFile() && entry.name.startsWith(prefix) && entry.name.endsWith('.md')) {
|
|
400
|
+
try {
|
|
401
|
+
fs.unlinkSync(path.join(agentsDir, entry.name));
|
|
402
|
+
}
|
|
403
|
+
catch { /* skip */ }
|
|
404
|
+
}
|
|
521
405
|
}
|
|
522
|
-
catch { /* start fresh */ }
|
|
523
|
-
}
|
|
524
|
-
if (!settings.mcpServers || typeof settings.mcpServers !== 'object') {
|
|
525
|
-
settings.mcpServers = {};
|
|
526
406
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const namespacedName = `${plugin.name}--${serverName}`;
|
|
531
|
-
// Expand variables inside the server config
|
|
532
|
-
const configStr = expandPluginVars(JSON.stringify(serverConfig), plugin.root, plugin.name, agent, versionHome, userConfig);
|
|
533
|
-
existing[namespacedName] = JSON.parse(configStr);
|
|
534
|
-
merged = true;
|
|
535
|
-
}
|
|
536
|
-
if (merged) {
|
|
407
|
+
// 4. plugin-bin
|
|
408
|
+
const binDir = path.join(agentRoot, 'plugin-bin', plugin.name);
|
|
409
|
+
if (fs.existsSync(binDir)) {
|
|
537
410
|
try {
|
|
538
|
-
fs.
|
|
539
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
540
|
-
}
|
|
541
|
-
catch {
|
|
542
|
-
return false;
|
|
411
|
+
fs.rmSync(binDir, { recursive: true, force: true });
|
|
543
412
|
}
|
|
413
|
+
catch { /* skip */ }
|
|
544
414
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
*/
|
|
552
|
-
function syncPluginSettings(plugin, agent, versionHome) {
|
|
553
|
-
const pluginSettingsPath = path.join(plugin.root, 'settings.json');
|
|
554
|
-
if (!fs.existsSync(pluginSettingsPath))
|
|
555
|
-
return false;
|
|
556
|
-
let pluginSettings;
|
|
415
|
+
// 5. settings.json — strip namespaced mcpServers, hooks referencing plugin
|
|
416
|
+
// root, permissions referencing plugin root, and pluginBinPaths entries.
|
|
417
|
+
const settingsPath = path.join(agentRoot, 'settings.json');
|
|
418
|
+
if (!fs.existsSync(settingsPath))
|
|
419
|
+
return;
|
|
420
|
+
let settings;
|
|
557
421
|
try {
|
|
558
|
-
|
|
422
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
559
423
|
}
|
|
560
424
|
catch {
|
|
561
|
-
return
|
|
562
|
-
}
|
|
563
|
-
// Exclude permissions — those are handled by syncPluginPermissions
|
|
564
|
-
const keysToMerge = Object.entries(pluginSettings).filter(([k]) => k !== 'permissions');
|
|
565
|
-
if (keysToMerge.length === 0)
|
|
566
|
-
return false;
|
|
567
|
-
const configDir = path.join(versionHome, `.${agent}`);
|
|
568
|
-
const settingsPath = path.join(configDir, 'settings.json');
|
|
569
|
-
let settings = {};
|
|
570
|
-
if (fs.existsSync(settingsPath)) {
|
|
571
|
-
try {
|
|
572
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
573
|
-
}
|
|
574
|
-
catch { /* start fresh */ }
|
|
425
|
+
return;
|
|
575
426
|
}
|
|
576
427
|
let changed = false;
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
return changed;
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Merge plugin permissions into Claude's settings.json.
|
|
596
|
-
* Reads the plugin's settings.json and merges permissions.allow / deny entries.
|
|
597
|
-
*/
|
|
598
|
-
function syncPluginPermissions(plugin, agent, versionHome, userConfig) {
|
|
599
|
-
const pluginSettingsPath = path.join(plugin.root, 'settings.json');
|
|
600
|
-
if (!fs.existsSync(pluginSettingsPath))
|
|
601
|
-
return false;
|
|
602
|
-
let pluginSettings;
|
|
603
|
-
try {
|
|
604
|
-
pluginSettings = JSON.parse(fs.readFileSync(pluginSettingsPath, 'utf-8'));
|
|
605
|
-
}
|
|
606
|
-
catch {
|
|
607
|
-
return false;
|
|
608
|
-
}
|
|
609
|
-
const pluginAllow = pluginSettings.permissions?.allow || [];
|
|
610
|
-
const pluginDeny = pluginSettings.permissions?.deny || [];
|
|
611
|
-
if (pluginAllow.length === 0 && pluginDeny.length === 0)
|
|
612
|
-
return false;
|
|
613
|
-
const expandedAllow = pluginAllow.map(rule => expandPluginVars(rule, plugin.root, plugin.name, agent, versionHome, userConfig));
|
|
614
|
-
const expandedDeny = pluginDeny.map(rule => expandPluginVars(rule, plugin.root, plugin.name, agent, versionHome, userConfig));
|
|
615
|
-
const configDir = path.join(versionHome, `.${agent}`);
|
|
616
|
-
const settingsPath = path.join(configDir, 'settings.json');
|
|
617
|
-
let settings = {};
|
|
618
|
-
if (fs.existsSync(settingsPath)) {
|
|
619
|
-
try {
|
|
620
|
-
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
621
|
-
}
|
|
622
|
-
catch { /* start fresh */ }
|
|
623
|
-
}
|
|
624
|
-
if (!settings.permissions || typeof settings.permissions !== 'object') {
|
|
625
|
-
settings.permissions = { allow: [], deny: [] };
|
|
626
|
-
}
|
|
627
|
-
const perms = settings.permissions;
|
|
628
|
-
if (!perms.allow)
|
|
629
|
-
perms.allow = [];
|
|
630
|
-
if (!perms.deny)
|
|
631
|
-
perms.deny = [];
|
|
632
|
-
for (const rule of expandedAllow) {
|
|
633
|
-
if (!perms.allow.includes(rule)) {
|
|
634
|
-
perms.allow.push(rule);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
for (const rule of expandedDeny) {
|
|
638
|
-
if (!perms.deny.includes(rule)) {
|
|
639
|
-
perms.deny.push(rule);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
try {
|
|
643
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
644
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
645
|
-
return true;
|
|
646
|
-
}
|
|
647
|
-
catch {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
// ─── Utility ──────────────────────────────────────────────────────────────────
|
|
652
|
-
/**
|
|
653
|
-
* Copy a directory recursively, expanding plugin variables in text file contents.
|
|
654
|
-
* Only expands variables in text files (.md, .json, .sh, .py, .js, .ts, .yaml, .yml, .toml).
|
|
655
|
-
*/
|
|
656
|
-
function copyDirWithVarExpansion(src, dest, pluginRoot, pluginName, agent, versionHome, userConfig) {
|
|
657
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
658
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
659
|
-
const textExtensions = new Set(['.md', '.json', '.sh', '.py', '.js', '.ts', '.yaml', '.yml', '.toml', '.txt']);
|
|
660
|
-
for (const entry of entries) {
|
|
661
|
-
const srcPath = path.join(src, entry.name);
|
|
662
|
-
const destPath = path.join(dest, entry.name);
|
|
663
|
-
if (entry.isDirectory()) {
|
|
664
|
-
copyDirWithVarExpansion(srcPath, destPath, pluginRoot, pluginName, agent, versionHome, userConfig);
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
668
|
-
if (textExtensions.has(ext)) {
|
|
669
|
-
let content = fs.readFileSync(srcPath, 'utf-8');
|
|
670
|
-
content = expandPluginVars(content, pluginRoot, pluginName, agent, versionHome, userConfig);
|
|
671
|
-
fs.writeFileSync(destPath, content, 'utf-8');
|
|
672
|
-
}
|
|
673
|
-
else {
|
|
674
|
-
fs.copyFileSync(srcPath, destPath);
|
|
428
|
+
const pluginRoot = plugin.root;
|
|
429
|
+
const hooksCfg = settings.hooks;
|
|
430
|
+
if (hooksCfg && typeof hooksCfg === 'object') {
|
|
431
|
+
for (const [event, entries] of Object.entries(hooksCfg)) {
|
|
432
|
+
if (!Array.isArray(entries))
|
|
433
|
+
continue;
|
|
434
|
+
const groups = entries;
|
|
435
|
+
for (const group of groups) {
|
|
436
|
+
if (!Array.isArray(group.hooks))
|
|
437
|
+
continue;
|
|
438
|
+
const orig = group.hooks.length;
|
|
439
|
+
group.hooks = group.hooks.filter(h => !(typeof h.command === 'string' && h.command.includes(pluginRoot)));
|
|
440
|
+
if (group.hooks.length !== orig)
|
|
441
|
+
changed = true;
|
|
675
442
|
}
|
|
676
|
-
const
|
|
677
|
-
if (
|
|
678
|
-
|
|
443
|
+
const kept = groups.filter(g => Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
444
|
+
if (kept.length !== groups.length) {
|
|
445
|
+
hooksCfg[event] = kept;
|
|
446
|
+
changed = true;
|
|
679
447
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
// ─── Sync status ──────────────────────────────────────────────────────────────
|
|
684
|
-
/**
|
|
685
|
-
* Check if a plugin is synced to a version by inspecting the version home.
|
|
686
|
-
* Checks skills, commands, agent defs, bin, hook commands, and permissions.
|
|
687
|
-
*/
|
|
688
|
-
export function isPluginSynced(plugin, agent, versionHome) {
|
|
689
|
-
const prefix = `${plugin.name}--`;
|
|
690
|
-
// Check 1: plugin skill directories
|
|
691
|
-
if (plugin.skills.length > 0) {
|
|
692
|
-
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
693
|
-
if (fs.existsSync(skillsDir)) {
|
|
694
|
-
for (const skillName of plugin.skills) {
|
|
695
|
-
if (fs.existsSync(path.join(skillsDir, `${prefix}${skillName}`))) {
|
|
696
|
-
return true;
|
|
697
|
-
}
|
|
448
|
+
if (Array.isArray(hooksCfg[event]) && hooksCfg[event].length === 0) {
|
|
449
|
+
delete hooksCfg[event];
|
|
450
|
+
changed = true;
|
|
698
451
|
}
|
|
699
452
|
}
|
|
700
453
|
}
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
454
|
+
const perms = settings.permissions;
|
|
455
|
+
if (perms && typeof perms === 'object') {
|
|
456
|
+
for (const key of ['allow', 'deny']) {
|
|
457
|
+
const list = perms[key];
|
|
458
|
+
if (!Array.isArray(list))
|
|
459
|
+
continue;
|
|
460
|
+
const kept = list.filter(r => !(typeof r === 'string' && r.includes(pluginRoot)));
|
|
461
|
+
if (kept.length !== list.length) {
|
|
462
|
+
perms[key] = kept;
|
|
463
|
+
changed = true;
|
|
710
464
|
}
|
|
711
465
|
}
|
|
712
466
|
}
|
|
713
|
-
|
|
714
|
-
if (
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return true;
|
|
720
|
-
}
|
|
467
|
+
const mcp = settings.mcpServers;
|
|
468
|
+
if (mcp && typeof mcp === 'object') {
|
|
469
|
+
for (const key of Object.keys(mcp)) {
|
|
470
|
+
if (key.startsWith(prefix)) {
|
|
471
|
+
delete mcp[key];
|
|
472
|
+
changed = true;
|
|
721
473
|
}
|
|
722
474
|
}
|
|
723
475
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
const settingsPath = path.join(versionHome, `.${agent}`, 'settings.json');
|
|
734
|
-
if (fs.existsSync(settingsPath)) {
|
|
735
|
-
try {
|
|
736
|
-
const content = fs.readFileSync(settingsPath, 'utf-8');
|
|
737
|
-
if (content.includes(plugin.root)) {
|
|
738
|
-
return true;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
catch { /* ignore */ }
|
|
476
|
+
if (Array.isArray(settings.pluginBinPaths)) {
|
|
477
|
+
const targetBinDir = path.join(agentRoot, 'plugin-bin', plugin.name);
|
|
478
|
+
const before = settings.pluginBinPaths.length;
|
|
479
|
+
settings.pluginBinPaths = settings.pluginBinPaths.filter(p => p !== targetBinDir);
|
|
480
|
+
if (settings.pluginBinPaths.length !== before)
|
|
481
|
+
changed = true;
|
|
482
|
+
if (settings.pluginBinPaths.length === 0) {
|
|
483
|
+
delete settings.pluginBinPaths;
|
|
484
|
+
changed = true;
|
|
742
485
|
}
|
|
743
486
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (fs.existsSync(settingsPath)) {
|
|
748
|
-
try {
|
|
749
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
750
|
-
const allow = settings.permissions?.allow || [];
|
|
751
|
-
if (allow.some((rule) => rule.includes(plugin.root))) {
|
|
752
|
-
return true;
|
|
753
|
-
}
|
|
754
|
-
// Check MCP servers
|
|
755
|
-
const mcpServers = settings.mcpServers;
|
|
756
|
-
if (mcpServers) {
|
|
757
|
-
const hasNamespacedServer = Object.keys(mcpServers).some(k => k.startsWith(prefix));
|
|
758
|
-
if (hasNamespacedServer)
|
|
759
|
-
return true;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
catch { /* ignore */ }
|
|
487
|
+
if (changed) {
|
|
488
|
+
try {
|
|
489
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
763
490
|
}
|
|
491
|
+
catch { /* ignore */ }
|
|
764
492
|
}
|
|
765
|
-
|
|
493
|
+
}
|
|
494
|
+
// ─── Sync status ──────────────────────────────────────────────────────────────
|
|
495
|
+
/**
|
|
496
|
+
* Check if a plugin is synced to a version. True when the plugin lives at the
|
|
497
|
+
* native marketplace install path. Legacy dual-dash entries are not counted —
|
|
498
|
+
* they're treated as stale and migrated away on the next sync.
|
|
499
|
+
*/
|
|
500
|
+
export function isPluginSynced(plugin, agent, versionHome) {
|
|
501
|
+
if (!PLUGINS_CAPABLE_AGENTS.includes(agent))
|
|
502
|
+
return false;
|
|
503
|
+
return isInstalledInMarketplace(plugin.name, agent, versionHome);
|
|
766
504
|
}
|
|
767
505
|
// ─── Removal ─────────────────────────────────────────────────────────────────
|
|
768
506
|
/**
|
|
@@ -781,9 +519,32 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
781
519
|
permissions: 0,
|
|
782
520
|
mcp: 0,
|
|
783
521
|
};
|
|
522
|
+
// 1. Remove the plugin from the marketplace install dir + disable it.
|
|
523
|
+
const removed = removePluginFromMarketplace(pluginName, agent, versionHome);
|
|
524
|
+
if (removed) {
|
|
525
|
+
result.skills.push(pluginName);
|
|
526
|
+
}
|
|
527
|
+
disablePluginInSettings(pluginName, agent, versionHome);
|
|
528
|
+
// 2. Refresh marketplace.json so it reflects what's left under plugins/.
|
|
529
|
+
syncMarketplaceManifest(agent, versionHome);
|
|
530
|
+
// 3. If we just removed the last plugin, drop the marketplace dir and the
|
|
531
|
+
// known_marketplaces.json entry too.
|
|
532
|
+
if (marketplaceIsEmpty(agent, versionHome)) {
|
|
533
|
+
removeEmptyMarketplaceDir(agent, versionHome);
|
|
534
|
+
unregisterMarketplace(agent, versionHome);
|
|
535
|
+
}
|
|
536
|
+
// 4. Strip any legacy dual-dash entries from prior agents-cli versions.
|
|
537
|
+
cleanLegacyFlatLayout(pluginName, pluginRoot, agent, versionHome, result);
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Strip dual-dash flat-layout entries left behind by older agents-cli sync runs.
|
|
542
|
+
* Mutates `result` to record what was removed.
|
|
543
|
+
*/
|
|
544
|
+
function cleanLegacyFlatLayout(pluginName, pluginRoot, agent, versionHome, result) {
|
|
784
545
|
const prefix = `${pluginName}--`;
|
|
785
|
-
|
|
786
|
-
const skillsDir = path.join(
|
|
546
|
+
const agentRoot = path.join(versionHome, `.${agent}`);
|
|
547
|
+
const skillsDir = path.join(agentRoot, 'skills');
|
|
787
548
|
if (fs.existsSync(skillsDir)) {
|
|
788
549
|
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
789
550
|
if (!entry.isDirectory() || !entry.name.startsWith(prefix))
|
|
@@ -792,92 +553,75 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
792
553
|
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true, force: true });
|
|
793
554
|
result.skills.push(entry.name);
|
|
794
555
|
}
|
|
795
|
-
catch { /* skip
|
|
556
|
+
catch { /* skip */ }
|
|
796
557
|
}
|
|
797
558
|
}
|
|
798
|
-
// 2. Remove synced command files
|
|
799
559
|
if (agent === 'claude' || agent === 'openclaw') {
|
|
800
|
-
const
|
|
801
|
-
const commandsDir = path.join(versionHome, `.${agent}`, agentConfig.commandsSubdir);
|
|
560
|
+
const commandsDir = path.join(agentRoot, AGENTS[agent]?.commandsSubdir ?? 'commands');
|
|
802
561
|
if (fs.existsSync(commandsDir)) {
|
|
803
562
|
for (const entry of fs.readdirSync(commandsDir, { withFileTypes: true })) {
|
|
804
|
-
if (!entry.isFile())
|
|
805
|
-
continue;
|
|
806
|
-
if (!entry.name.startsWith(prefix) || !entry.name.endsWith('.md'))
|
|
563
|
+
if (!entry.isFile() || !entry.name.startsWith(prefix) || !entry.name.endsWith('.md'))
|
|
807
564
|
continue;
|
|
808
565
|
try {
|
|
809
566
|
fs.unlinkSync(path.join(commandsDir, entry.name));
|
|
810
567
|
result.commands.push(entry.name);
|
|
811
568
|
}
|
|
812
|
-
catch { /* skip
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
// 3. Remove synced agent def files
|
|
817
|
-
if (agent === 'claude') {
|
|
818
|
-
const agentsDir = path.join(versionHome, `.${agent}`, 'agents');
|
|
819
|
-
if (fs.existsSync(agentsDir)) {
|
|
820
|
-
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
821
|
-
if (!entry.isFile())
|
|
822
|
-
continue;
|
|
823
|
-
if (!entry.name.startsWith(prefix) || !entry.name.endsWith('.md'))
|
|
824
|
-
continue;
|
|
825
|
-
try {
|
|
826
|
-
fs.unlinkSync(path.join(agentsDir, entry.name));
|
|
827
|
-
result.agentDefs.push(entry.name);
|
|
828
|
-
}
|
|
829
|
-
catch { /* skip on error */ }
|
|
569
|
+
catch { /* skip */ }
|
|
830
570
|
}
|
|
831
571
|
}
|
|
832
572
|
}
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
const
|
|
836
|
-
|
|
573
|
+
const agentsDir = path.join(agentRoot, 'agents');
|
|
574
|
+
if (fs.existsSync(agentsDir)) {
|
|
575
|
+
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
576
|
+
if (!entry.isFile() || !entry.name.startsWith(prefix) || !entry.name.endsWith('.md'))
|
|
577
|
+
continue;
|
|
837
578
|
try {
|
|
838
|
-
fs.
|
|
839
|
-
result.
|
|
579
|
+
fs.unlinkSync(path.join(agentsDir, entry.name));
|
|
580
|
+
result.agentDefs.push(entry.name);
|
|
840
581
|
}
|
|
841
|
-
catch { /* skip
|
|
582
|
+
catch { /* skip */ }
|
|
842
583
|
}
|
|
843
584
|
}
|
|
844
|
-
|
|
845
|
-
|
|
585
|
+
const binDir = path.join(agentRoot, 'plugin-bin', pluginName);
|
|
586
|
+
if (fs.existsSync(binDir)) {
|
|
587
|
+
try {
|
|
588
|
+
fs.rmSync(binDir, { recursive: true, force: true });
|
|
589
|
+
result.bin.push(binDir);
|
|
590
|
+
}
|
|
591
|
+
catch { /* skip */ }
|
|
846
592
|
}
|
|
847
|
-
|
|
848
|
-
const settingsPath = path.join(versionHome, `.${agent}`, 'settings.json');
|
|
593
|
+
const settingsPath = path.join(agentRoot, 'settings.json');
|
|
849
594
|
if (!fs.existsSync(settingsPath))
|
|
850
|
-
return
|
|
595
|
+
return;
|
|
851
596
|
let settings;
|
|
852
597
|
try {
|
|
853
598
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
854
599
|
}
|
|
855
600
|
catch {
|
|
856
|
-
return
|
|
601
|
+
return;
|
|
857
602
|
}
|
|
858
603
|
let changed = false;
|
|
859
|
-
// Strip hooks referencing plugin root
|
|
860
604
|
const hooksConfig = settings.hooks;
|
|
861
605
|
if (hooksConfig && typeof hooksConfig === 'object') {
|
|
862
606
|
for (const [event, entries] of Object.entries(hooksConfig)) {
|
|
863
607
|
if (!Array.isArray(entries))
|
|
864
608
|
continue;
|
|
865
|
-
const
|
|
866
|
-
for (const group of
|
|
609
|
+
const groups = entries;
|
|
610
|
+
for (const group of groups) {
|
|
867
611
|
if (!Array.isArray(group.hooks))
|
|
868
612
|
continue;
|
|
869
|
-
const
|
|
613
|
+
const orig = group.hooks.length;
|
|
870
614
|
group.hooks = group.hooks.filter(h => {
|
|
871
615
|
const matches = typeof h.command === 'string' && h.command.includes(pluginRoot);
|
|
872
616
|
if (matches)
|
|
873
617
|
result.hooks.push(`${event}: ${h.command}`);
|
|
874
618
|
return !matches;
|
|
875
619
|
});
|
|
876
|
-
if (group.hooks.length !==
|
|
620
|
+
if (group.hooks.length !== orig)
|
|
877
621
|
changed = true;
|
|
878
622
|
}
|
|
879
|
-
const kept =
|
|
880
|
-
if (kept.length !==
|
|
623
|
+
const kept = groups.filter(g => Array.isArray(g.hooks) && g.hooks.length > 0);
|
|
624
|
+
if (kept.length !== groups.length) {
|
|
881
625
|
hooksConfig[event] = kept;
|
|
882
626
|
changed = true;
|
|
883
627
|
}
|
|
@@ -887,15 +631,14 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
887
631
|
}
|
|
888
632
|
}
|
|
889
633
|
}
|
|
890
|
-
// Strip permissions referencing plugin root
|
|
891
634
|
const perms = settings.permissions;
|
|
892
635
|
if (perms && typeof perms === 'object') {
|
|
893
636
|
for (const key of ['allow', 'deny']) {
|
|
894
637
|
const list = perms[key];
|
|
895
638
|
if (!Array.isArray(list))
|
|
896
639
|
continue;
|
|
897
|
-
const kept = list.filter(
|
|
898
|
-
const matches = typeof
|
|
640
|
+
const kept = list.filter(r => {
|
|
641
|
+
const matches = typeof r === 'string' && r.includes(pluginRoot);
|
|
899
642
|
if (matches)
|
|
900
643
|
result.permissions += 1;
|
|
901
644
|
return !matches;
|
|
@@ -906,52 +649,83 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
|
|
|
906
649
|
}
|
|
907
650
|
}
|
|
908
651
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
delete mcpServers[serverName];
|
|
652
|
+
const mcp = settings.mcpServers;
|
|
653
|
+
if (mcp && typeof mcp === 'object') {
|
|
654
|
+
for (const key of Object.keys(mcp)) {
|
|
655
|
+
if (key.startsWith(prefix)) {
|
|
656
|
+
delete mcp[key];
|
|
915
657
|
result.mcp += 1;
|
|
916
658
|
changed = true;
|
|
917
659
|
}
|
|
918
660
|
}
|
|
919
661
|
}
|
|
920
|
-
// Strip bin path from pluginBinPaths
|
|
921
662
|
if (Array.isArray(settings.pluginBinPaths)) {
|
|
922
|
-
const
|
|
663
|
+
const targetBin = path.join(agentRoot, 'plugin-bin', pluginName);
|
|
923
664
|
const before = settings.pluginBinPaths.length;
|
|
924
|
-
settings.pluginBinPaths = settings.pluginBinPaths.filter(p => p !==
|
|
665
|
+
settings.pluginBinPaths = settings.pluginBinPaths.filter(p => p !== targetBin);
|
|
925
666
|
if (settings.pluginBinPaths.length !== before)
|
|
926
667
|
changed = true;
|
|
668
|
+
if (settings.pluginBinPaths.length === 0) {
|
|
669
|
+
delete settings.pluginBinPaths;
|
|
670
|
+
changed = true;
|
|
671
|
+
}
|
|
927
672
|
}
|
|
928
673
|
if (changed) {
|
|
929
674
|
try {
|
|
930
675
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
931
676
|
}
|
|
932
|
-
catch { /* ignore
|
|
677
|
+
catch { /* ignore */ }
|
|
933
678
|
}
|
|
934
|
-
return result;
|
|
935
679
|
}
|
|
936
680
|
// ─── Orphan cleanup ───────────────────────────────────────────────────────────
|
|
937
681
|
/**
|
|
938
|
-
* Remove orphaned plugin
|
|
939
|
-
* Soft-deletes
|
|
682
|
+
* Remove orphaned plugin entries from a version home. An entry is "orphan" if
|
|
683
|
+
* its plugin name is not in the active plugin set. Soft-deletes the affected
|
|
684
|
+
* marketplace plugin dir to ~/.agents/.trash/plugins/. Also cleans up any
|
|
685
|
+
* legacy dual-dash skills/ directories from older agents-cli versions.
|
|
940
686
|
*/
|
|
941
687
|
export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames, version) {
|
|
942
688
|
const removed = [];
|
|
689
|
+
// 1. Walk the native marketplace install dir and trash entries no longer active.
|
|
690
|
+
const mktPluginsDir = path.join(marketplaceRoot(agent, versionHome), 'plugins');
|
|
691
|
+
if (fs.existsSync(mktPluginsDir)) {
|
|
692
|
+
for (const entry of fs.readdirSync(mktPluginsDir, { withFileTypes: true })) {
|
|
693
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
694
|
+
continue;
|
|
695
|
+
if (activePluginNames.has(entry.name))
|
|
696
|
+
continue;
|
|
697
|
+
try {
|
|
698
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
699
|
+
const trashDir = path.join(getTrashPluginsDir(), agent, version || 'unknown', entry.name);
|
|
700
|
+
const trashDest = path.join(trashDir, stamp);
|
|
701
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
702
|
+
fs.renameSync(path.join(mktPluginsDir, entry.name), trashDest);
|
|
703
|
+
disablePluginInSettings(entry.name, agent, versionHome);
|
|
704
|
+
removed.push(entry.name);
|
|
705
|
+
}
|
|
706
|
+
catch { /* skip on error */ }
|
|
707
|
+
}
|
|
708
|
+
// Keep manifest in sync with on-disk state and drop the marketplace if empty.
|
|
709
|
+
if (removed.length > 0) {
|
|
710
|
+
syncMarketplaceManifest(agent, versionHome);
|
|
711
|
+
if (marketplaceIsEmpty(agent, versionHome)) {
|
|
712
|
+
removeEmptyMarketplaceDir(agent, versionHome);
|
|
713
|
+
unregisterMarketplace(agent, versionHome);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// 2. Sweep legacy dual-dash skills directories from older agents-cli versions.
|
|
943
718
|
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
944
|
-
if (
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
if (!activePluginNames.has(pluginName)) {
|
|
719
|
+
if (fs.existsSync(skillsDir)) {
|
|
720
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
721
|
+
if (!entry.isDirectory())
|
|
722
|
+
continue;
|
|
723
|
+
const dashIdx = entry.name.indexOf('--');
|
|
724
|
+
if (dashIdx === -1)
|
|
725
|
+
continue;
|
|
726
|
+
const pluginName = entry.name.slice(0, dashIdx);
|
|
727
|
+
if (activePluginNames.has(pluginName))
|
|
728
|
+
continue;
|
|
955
729
|
try {
|
|
956
730
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
957
731
|
const trashDir = path.join(getTrashPluginsDir(), agent, version || 'unknown', entry.name);
|
|
@@ -967,24 +741,34 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames,
|
|
|
967
741
|
}
|
|
968
742
|
export function diffVersionPlugins(agent, version) {
|
|
969
743
|
const versionHome = getVersionHomePath(agent, version);
|
|
970
|
-
const
|
|
744
|
+
const activePlugins = new Set(discoverPlugins().map(p => p.name));
|
|
971
745
|
const orphans = [];
|
|
972
|
-
|
|
973
|
-
|
|
746
|
+
const mktPluginsDir = path.join(marketplaceRoot(agent, versionHome), 'plugins');
|
|
747
|
+
if (fs.existsSync(mktPluginsDir)) {
|
|
748
|
+
for (const entry of fs.readdirSync(mktPluginsDir, { withFileTypes: true })) {
|
|
749
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
750
|
+
continue;
|
|
751
|
+
if (!activePlugins.has(entry.name)) {
|
|
752
|
+
orphans.push(entry.name);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
974
755
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
756
|
+
// Also surface legacy dual-dash skill dirs as orphans during migration period.
|
|
757
|
+
const skillsDir = path.join(versionHome, `.${agent}`, 'skills');
|
|
758
|
+
if (fs.existsSync(skillsDir)) {
|
|
759
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
760
|
+
if (!entry.isDirectory())
|
|
761
|
+
continue;
|
|
762
|
+
const dashIdx = entry.name.indexOf('--');
|
|
763
|
+
if (dashIdx === -1)
|
|
764
|
+
continue;
|
|
765
|
+
const pluginName = entry.name.slice(0, dashIdx);
|
|
766
|
+
if (!activePlugins.has(pluginName)) {
|
|
767
|
+
orphans.push(entry.name);
|
|
768
|
+
}
|
|
985
769
|
}
|
|
986
770
|
}
|
|
987
|
-
return { agent, version, orphans: orphans.sort() };
|
|
771
|
+
return { agent, version, orphans: Array.from(new Set(orphans)).sort() };
|
|
988
772
|
}
|
|
989
773
|
export function iterPluginsCapableVersions(filter) {
|
|
990
774
|
const pairs = [];
|