@phnx-labs/agents-cli 1.20.3 → 1.20.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 +27 -0
- package/README.md +48 -17
- package/dist/commands/cli.js +1 -1
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +2 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/exec.js +52 -16
- package/dist/commands/hooks.js +6 -6
- package/dist/commands/import.js +90 -37
- package/dist/commands/inspect.d.ts +26 -0
- package/dist/commands/inspect.js +590 -0
- package/dist/commands/mcp.js +17 -16
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -4
- package/dist/commands/permissions.js +13 -12
- package/dist/commands/plugins.d.ts +13 -0
- package/dist/commands/plugins.js +100 -11
- package/dist/commands/prune.js +3 -2
- package/dist/commands/pull.d.ts +12 -5
- package/dist/commands/pull.js +26 -422
- package/dist/commands/push.d.ts +14 -0
- package/dist/commands/push.js +30 -0
- package/dist/commands/repo.d.ts +1 -1
- package/dist/commands/repo.js +155 -112
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +12 -3
- package/dist/commands/routines.js +32 -7
- package/dist/commands/rules.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup.d.ts +3 -3
- package/dist/commands/setup.js +15 -15
- package/dist/commands/skills.js +6 -5
- package/dist/commands/subagents.js +5 -4
- package/dist/commands/sync.d.ts +18 -5
- package/dist/commands/sync.js +251 -65
- package/dist/commands/teams.js +1 -0
- package/dist/commands/tmux.d.ts +25 -0
- package/dist/commands/tmux.js +415 -0
- package/dist/commands/trash.d.ts +2 -2
- package/dist/commands/trash.js +1 -1
- package/dist/commands/versions.js +2 -2
- package/dist/commands/view.js +14 -4
- package/dist/commands/workflows.js +4 -3
- package/dist/commands/worktree.d.ts +4 -5
- package/dist/commands/worktree.js +4 -4
- package/dist/index.js +68 -20
- package/dist/lib/agents.d.ts +19 -10
- package/dist/lib/agents.js +102 -28
- package/dist/lib/auto-pull-worker.d.ts +1 -1
- package/dist/lib/auto-pull-worker.js +2 -2
- package/dist/lib/auto-pull.d.ts +1 -1
- package/dist/lib/auto-pull.js +1 -1
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/capabilities.js +2 -0
- package/dist/lib/commands.d.ts +28 -1
- package/dist/lib/commands.js +125 -20
- package/dist/lib/doctor-diff.js +2 -2
- package/dist/lib/exec.d.ts +14 -0
- package/dist/lib/exec.js +39 -5
- package/dist/lib/fuzzy.d.ts +12 -2
- package/dist/lib/fuzzy.js +29 -4
- package/dist/lib/git.js +8 -1
- package/dist/lib/hooks.d.ts +2 -2
- package/dist/lib/hooks.js +97 -10
- package/dist/lib/import.d.ts +21 -0
- package/dist/lib/import.js +55 -2
- package/dist/lib/mcp.js +32 -2
- package/dist/lib/migrate.d.ts +51 -0
- package/dist/lib/migrate.js +227 -1
- package/dist/lib/models.js +62 -15
- package/dist/lib/permissions.d.ts +36 -2
- package/dist/lib/permissions.js +217 -7
- package/dist/lib/plugin-marketplace.d.ts +108 -40
- package/dist/lib/plugin-marketplace.js +243 -94
- package/dist/lib/plugins.d.ts +21 -4
- package/dist/lib/plugins.js +130 -49
- package/dist/lib/profiles-presets.js +12 -12
- package/dist/lib/project-launch.d.ts +65 -0
- package/dist/lib/project-launch.js +367 -0
- package/dist/lib/pty-client.js +1 -1
- package/dist/lib/pty-server.d.ts +1 -1
- package/dist/lib/pty-server.js +28 -4
- package/dist/lib/refresh.d.ts +26 -0
- package/dist/lib/refresh.js +315 -0
- package/dist/lib/resource-patterns.d.ts +1 -1
- package/dist/lib/resource-patterns.js +1 -1
- package/dist/lib/resources/commands.js +2 -2
- package/dist/lib/resources/hooks.d.ts +1 -1
- package/dist/lib/resources/hooks.js +1 -1
- package/dist/lib/resources/mcp.d.ts +1 -1
- package/dist/lib/resources/mcp.js +5 -6
- package/dist/lib/resources/permissions.js +5 -2
- package/dist/lib/resources/rules.js +3 -2
- package/dist/lib/resources/skills.js +3 -2
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.js +2 -2
- package/dist/lib/rotate.d.ts +1 -1
- package/dist/lib/rotate.js +1 -1
- package/dist/lib/routines.d.ts +16 -4
- package/dist/lib/routines.js +67 -17
- package/dist/lib/rules/compile.js +22 -10
- package/dist/lib/rules/rules.js +3 -3
- package/dist/lib/runner.js +16 -3
- package/dist/lib/scheduler.js +15 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +9 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/secrets/linux.d.ts +44 -9
- package/dist/lib/secrets/linux.js +302 -48
- package/dist/lib/session/db.js +15 -2
- package/dist/lib/session/discover.js +118 -3
- package/dist/lib/session/parse.js +3 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +10 -9
- package/dist/lib/shims.js +101 -50
- package/dist/lib/skills.d.ts +1 -1
- package/dist/lib/skills.js +10 -9
- package/dist/lib/staleness/detectors/commands.d.ts +3 -0
- package/dist/lib/staleness/detectors/commands.js +46 -0
- package/dist/lib/staleness/detectors/hooks.d.ts +3 -0
- package/dist/lib/staleness/detectors/hooks.js +44 -0
- package/dist/lib/staleness/detectors/mcp.d.ts +3 -0
- package/dist/lib/staleness/detectors/mcp.js +31 -0
- package/dist/lib/staleness/detectors/permissions.d.ts +3 -0
- package/dist/lib/staleness/detectors/permissions.js +201 -0
- package/dist/lib/staleness/detectors/plugins.d.ts +8 -0
- package/dist/lib/staleness/detectors/plugins.js +23 -0
- package/dist/lib/staleness/detectors/rules.d.ts +3 -0
- package/dist/lib/staleness/detectors/rules.js +34 -0
- package/dist/lib/staleness/detectors/skills.d.ts +3 -0
- package/dist/lib/staleness/detectors/skills.js +71 -0
- package/dist/lib/staleness/detectors/subagents.d.ts +3 -0
- package/dist/lib/staleness/detectors/subagents.js +50 -0
- package/dist/lib/staleness/detectors/types.d.ts +22 -0
- package/dist/lib/staleness/detectors/types.js +1 -0
- package/dist/lib/staleness/detectors/workflows.d.ts +3 -0
- package/dist/lib/staleness/detectors/workflows.js +28 -0
- package/dist/lib/staleness/registry.d.ts +26 -0
- package/dist/lib/staleness/registry.js +123 -0
- package/dist/lib/staleness/writers/commands.d.ts +3 -0
- package/dist/lib/staleness/writers/commands.js +111 -0
- package/dist/lib/staleness/writers/hooks.d.ts +3 -0
- package/dist/lib/staleness/writers/hooks.js +47 -0
- package/dist/lib/staleness/writers/kinds.d.ts +10 -0
- package/dist/lib/staleness/writers/kinds.js +15 -0
- package/dist/lib/staleness/writers/lazy-map.d.ts +13 -0
- package/dist/lib/staleness/writers/lazy-map.js +19 -0
- package/dist/lib/staleness/writers/mcp.d.ts +10 -0
- package/dist/lib/staleness/writers/mcp.js +19 -0
- package/dist/lib/staleness/writers/permissions.d.ts +13 -0
- package/dist/lib/staleness/writers/permissions.js +26 -0
- package/dist/lib/staleness/writers/plugins.d.ts +7 -0
- package/dist/lib/staleness/writers/plugins.js +31 -0
- package/dist/lib/staleness/writers/rules.d.ts +7 -0
- package/dist/lib/staleness/writers/rules.js +55 -0
- package/dist/lib/staleness/writers/skills.d.ts +3 -0
- package/dist/lib/staleness/writers/skills.js +81 -0
- package/dist/lib/staleness/writers/sources.d.ts +16 -0
- package/dist/lib/staleness/writers/sources.js +72 -0
- package/dist/lib/staleness/writers/subagents.d.ts +3 -0
- package/dist/lib/staleness/writers/subagents.js +53 -0
- package/dist/lib/staleness/writers/types.d.ts +36 -0
- package/dist/lib/staleness/writers/types.js +1 -0
- package/dist/lib/staleness/writers/workflows.d.ts +7 -0
- package/dist/lib/staleness/writers/workflows.js +31 -0
- package/dist/lib/state.d.ts +34 -11
- package/dist/lib/state.js +58 -13
- package/dist/lib/subagents.d.ts +0 -2
- package/dist/lib/subagents.js +6 -6
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/binary.d.ts +67 -0
- package/dist/lib/tmux/binary.js +141 -0
- package/dist/lib/tmux/index.d.ts +8 -0
- package/dist/lib/tmux/index.js +8 -0
- package/dist/lib/tmux/paths.d.ts +17 -0
- package/dist/lib/tmux/paths.js +30 -0
- package/dist/lib/tmux/session.d.ts +122 -0
- package/dist/lib/tmux/session.js +305 -0
- package/dist/lib/types.d.ts +58 -7
- package/dist/lib/types.js +1 -1
- package/dist/lib/usage.js +1 -1
- package/dist/lib/versions.d.ts +4 -4
- package/dist/lib/versions.js +154 -491
- package/dist/lib/workflows.d.ts +2 -4
- package/dist/lib/workflows.js +3 -4
- package/package.json +7 -7
- package/scripts/postinstall.js +16 -63
- package/dist/commands/status.d.ts +0 -9
- package/dist/commands/status.js +0 -25
package/dist/lib/versions.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Version management module for agents-cli.
|
|
3
3
|
*
|
|
4
4
|
* Handles installing, removing, listing, and switching between agent CLI versions.
|
|
5
|
-
* Each version is installed into an isolated directory under ~/.agents
|
|
5
|
+
* Each version is installed into an isolated directory under ~/.agents/.system/versions/{agent}/{version}/
|
|
6
6
|
* with its own HOME directory for config isolation. Resources (commands, skills, hooks, memory,
|
|
7
7
|
* MCP servers, permissions, subagents, plugins) from ~/.agents/ are synced into version homes
|
|
8
8
|
* via copies or conversions (not symlinks).
|
|
@@ -21,27 +21,22 @@ import * as yaml from 'yaml';
|
|
|
21
21
|
import { exec, execFile } from 'child_process';
|
|
22
22
|
import { promisify } from 'util';
|
|
23
23
|
import chalk from 'chalk';
|
|
24
|
-
import * as TOML from 'smol-toml';
|
|
25
24
|
import { checkbox, select } from '@inquirer/prompts';
|
|
26
25
|
import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset } from './state.js';
|
|
27
26
|
import { defaultPatterns, expandPatterns } from './resource-patterns.js';
|
|
28
27
|
import { listResources } from './resources.js';
|
|
29
|
-
import { AGENTS, getAccountEmail,
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { markdownToToml } from './convert.js';
|
|
28
|
+
import { AGENTS, getAccountEmail, resolveAgentName, formatAgentError } from './agents.js';
|
|
29
|
+
import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
30
|
+
import { parseMcpServerConfig } from './mcp.js';
|
|
33
31
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
34
|
-
import {
|
|
35
|
-
import { WORKFLOW_CAPABLE_AGENTS, listInstalledWorkflows, syncWorkflowToVersion } from './workflows.js';
|
|
36
|
-
import { registerHooksToSettings } from './hooks.js';
|
|
32
|
+
import { importInstallScriptBinary } from './import.js';
|
|
37
33
|
import { supports, explainSkip } from './capabilities.js';
|
|
38
|
-
import { discoverPlugins
|
|
39
|
-
import { composeRulesFromState } from './rules/compose.js';
|
|
34
|
+
import { discoverPlugins } from './plugins.js';
|
|
40
35
|
import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
|
|
41
|
-
import { PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
42
36
|
import { emit } from './events.js';
|
|
43
37
|
import { safeJoin } from './paths.js';
|
|
44
|
-
import {
|
|
38
|
+
import { listCommandSkillsInVersion, shouldInstallCommandAsSkill } from './command-skills.js';
|
|
39
|
+
import { getWriter, getDetector } from './staleness/registry.js';
|
|
45
40
|
/** Promisified exec for running shell commands. */
|
|
46
41
|
const execAsync = promisify(exec);
|
|
47
42
|
const execFileAsync = promisify(execFile);
|
|
@@ -280,10 +275,8 @@ function skillDirsMatch(src, dest) {
|
|
|
280
275
|
* This is the source of truth - not the tracking in agents.yaml.
|
|
281
276
|
*/
|
|
282
277
|
export function getActuallySyncedResources(agent, version, options = {}) {
|
|
283
|
-
const agentConfig = AGENTS[agent];
|
|
284
278
|
const versionHome = path.join(getVersionsDir(), agent, version, 'home');
|
|
285
|
-
const
|
|
286
|
-
const projectAgentsDir = getProjectAgentsDir(options.cwd || process.cwd());
|
|
279
|
+
const cwd = options.cwd || process.cwd();
|
|
287
280
|
const result = {
|
|
288
281
|
commands: [],
|
|
289
282
|
skills: [],
|
|
@@ -296,219 +289,20 @@ export function getActuallySyncedResources(agent, version, options = {}) {
|
|
|
296
289
|
workflows: [],
|
|
297
290
|
promptcuts: false,
|
|
298
291
|
};
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
// Skills - check what directories exist AND content matches central source
|
|
316
|
-
const skillsDir = path.join(configDir, 'skills');
|
|
317
|
-
const centralSkillsDir = getSkillsDir();
|
|
318
|
-
const projectSkillsDir = projectAgentsDir ? path.join(projectAgentsDir, 'skills') : null;
|
|
319
|
-
const userAgentsDir = getUserAgentsDir();
|
|
320
|
-
const extraRepos = getEnabledExtraRepos();
|
|
321
|
-
if (fs.existsSync(skillsDir)) {
|
|
322
|
-
const installedSkills = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
323
|
-
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
324
|
-
.map(d => d.name);
|
|
325
|
-
for (const skill of installedSkills) {
|
|
326
|
-
const versionSkillDir = path.join(skillsDir, skill);
|
|
327
|
-
const sourceCandidates = [
|
|
328
|
-
projectSkillsDir ? path.join(projectSkillsDir, skill) : null,
|
|
329
|
-
path.join(userAgentsDir, 'skills', skill),
|
|
330
|
-
path.join(centralSkillsDir, skill),
|
|
331
|
-
...extraRepos.map((e) => path.join(e.dir, 'skills', skill)),
|
|
332
|
-
];
|
|
333
|
-
const sourceDir = sourceCandidates.find((p) => p && fs.existsSync(p)) || null;
|
|
334
|
-
if (!sourceDir) {
|
|
335
|
-
// True orphan — no source in project, primary, or any extra. Still
|
|
336
|
-
// count as synced so version-home cleanup knows it's accounted for.
|
|
337
|
-
result.skills.push(skill);
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
const allMatch = skillDirsMatch(sourceDir, versionSkillDir);
|
|
341
|
-
if (allMatch) {
|
|
342
|
-
result.skills.push(skill);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Hooks - check what files exist AND content matches central source
|
|
347
|
-
const hooksDir = path.join(configDir, 'hooks');
|
|
348
|
-
const centralHooksDir = getHooksDir();
|
|
349
|
-
const projectHooksDir = projectAgentsDir ? path.join(projectAgentsDir, 'hooks') : null;
|
|
350
|
-
const userHooksDir = path.join(userAgentsDir, 'hooks');
|
|
351
|
-
if (fs.existsSync(hooksDir)) {
|
|
352
|
-
const installedHooks = fs.readdirSync(hooksDir).filter(f => !f.startsWith('.'));
|
|
353
|
-
for (const hook of installedHooks) {
|
|
354
|
-
const projectFile = projectHooksDir ? path.join(projectHooksDir, hook) : null;
|
|
355
|
-
const centralFile = path.join(centralHooksDir, hook);
|
|
356
|
-
const userFile = path.join(userHooksDir, hook);
|
|
357
|
-
const versionFile = path.join(hooksDir, hook);
|
|
358
|
-
const hasProject = projectFile ? fs.existsSync(projectFile) : false;
|
|
359
|
-
const hasUser = fs.existsSync(userFile);
|
|
360
|
-
const hasCentral = fs.existsSync(centralFile);
|
|
361
|
-
const sourceFile = hasProject ? projectFile : hasUser ? userFile : centralFile;
|
|
362
|
-
if (!hasProject && !hasCentral && !hasUser) {
|
|
363
|
-
result.hooks.push(hook);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
try {
|
|
367
|
-
const centralContent = fs.readFileSync(sourceFile, 'utf-8');
|
|
368
|
-
const versionContent = fs.readFileSync(versionFile, 'utf-8');
|
|
369
|
-
if (centralContent === versionContent) {
|
|
370
|
-
result.hooks.push(hook);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
// If read fails, consider not synced
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
// Rules — single composed instruction file per agent. If the file exists in
|
|
379
|
-
// the version home, we consider the active preset synced. Available presets
|
|
380
|
-
// are surfaced from rules.yaml; this set is the subset that materialized.
|
|
381
|
-
const instrFile = path.join(configDir, agentConfig.instructionsFile);
|
|
382
|
-
if (fs.existsSync(instrFile)) {
|
|
383
|
-
const activePreset = getActiveRulesPreset(agent, version);
|
|
384
|
-
result.memory.push(activePreset);
|
|
385
|
-
}
|
|
386
|
-
// MCP - use canonical config path + parser per agent
|
|
387
|
-
if (MCP_CAPABLE_AGENTS.includes(agent)) {
|
|
388
|
-
const mcpConfigPath = getMcpConfigPathForHome(agent, versionHome);
|
|
389
|
-
if (fs.existsSync(mcpConfigPath)) {
|
|
390
|
-
try {
|
|
391
|
-
const servers = parseMcpConfig(agent, mcpConfigPath);
|
|
392
|
-
result.mcp = Object.keys(servers);
|
|
393
|
-
}
|
|
394
|
-
catch {
|
|
395
|
-
// Ignore parse errors
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
// Permissions - check agent-specific config files
|
|
400
|
-
const settingsPath = path.join(configDir, 'settings.json');
|
|
401
|
-
if (PERMISSIONS_CAPABLE_AGENTS.includes(agent)) {
|
|
402
|
-
if (agent === 'claude' && fs.existsSync(settingsPath)) {
|
|
403
|
-
// Claude: check settings.json permissions.allow and deny
|
|
404
|
-
try {
|
|
405
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
406
|
-
const allowRules = settings.permissions?.allow || [];
|
|
407
|
-
const denyRules = settings.permissions?.deny || [];
|
|
408
|
-
if (allowRules.length > 0 || denyRules.length > 0) {
|
|
409
|
-
const permGroups = discoverPermissionGroups();
|
|
410
|
-
const appliedGroups = [];
|
|
411
|
-
for (const group of permGroups) {
|
|
412
|
-
const groupSet = buildPermissionsFromGroups([group.name]);
|
|
413
|
-
// Empty groups (like header files) are considered synced if ANY permissions are applied
|
|
414
|
-
if (groupSet.allow.length === 0 && (!groupSet.deny || groupSet.deny.length === 0)) {
|
|
415
|
-
appliedGroups.push(group.name);
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
const hasAllowRule = groupSet.allow.some(rule => allowRules.includes(rule));
|
|
419
|
-
const hasDenyRule = groupSet.deny?.some(rule => denyRules.includes(rule)) || false;
|
|
420
|
-
if (hasAllowRule || hasDenyRule) {
|
|
421
|
-
appliedGroups.push(group.name);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
result.permissions = appliedGroups;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
// Ignore parse errors
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
else if (agent === 'codex') {
|
|
432
|
-
// Codex: config.toml for approval_policy/sandbox_mode, .rules for deny
|
|
433
|
-
const codexConfigPath = path.join(configDir, 'config.toml');
|
|
434
|
-
const codexRulesPath = path.join(configDir, 'rules', CODEX_RULES_FILENAME);
|
|
435
|
-
const hasConfig = fs.existsSync(codexConfigPath);
|
|
436
|
-
const hasRules = fs.existsSync(codexRulesPath);
|
|
437
|
-
if (hasConfig || hasRules) {
|
|
438
|
-
try {
|
|
439
|
-
// Codex format is lossy — all groups merge into a few keys.
|
|
440
|
-
// If any permission artifacts exist, all groups were applied together.
|
|
441
|
-
let hasPermKeys = false;
|
|
442
|
-
if (hasConfig) {
|
|
443
|
-
const content = fs.readFileSync(codexConfigPath, 'utf-8');
|
|
444
|
-
const config = TOML.parse(content);
|
|
445
|
-
hasPermKeys = !!(config.approval_policy || config.sandbox_mode || config.sandbox_workspace_write);
|
|
446
|
-
}
|
|
447
|
-
if (hasPermKeys || hasRules) {
|
|
448
|
-
result.permissions = discoverPermissionGroups().map(g => g.name);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
catch {
|
|
452
|
-
// Ignore parse errors
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
else if (agent === 'opencode') {
|
|
457
|
-
// OpenCode: opencode.jsonc for permission.bash
|
|
458
|
-
const opencodeConfigPath = path.join(configDir, 'opencode.jsonc');
|
|
459
|
-
if (fs.existsSync(opencodeConfigPath)) {
|
|
460
|
-
try {
|
|
461
|
-
const content = fs.readFileSync(opencodeConfigPath, 'utf-8');
|
|
462
|
-
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
463
|
-
const config = JSON.parse(stripped);
|
|
464
|
-
if (config.permission && Object.keys(config.permission.bash || {}).length > 0) {
|
|
465
|
-
result.permissions = discoverPermissionGroups().map(g => g.name);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
catch {
|
|
469
|
-
// Ignore parse errors
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
// Subagents - check agent-specific locations
|
|
475
|
-
if (SUBAGENT_CAPABLE_AGENTS.includes(agent)) {
|
|
476
|
-
if (agent === 'claude') {
|
|
477
|
-
const agentsDir = path.join(configDir, 'agents');
|
|
478
|
-
if (fs.existsSync(agentsDir)) {
|
|
479
|
-
result.subagents = fs.readdirSync(agentsDir)
|
|
480
|
-
.filter(f => f.endsWith('.md'))
|
|
481
|
-
.map(f => f.replace('.md', ''));
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
else if (agent === 'openclaw') {
|
|
485
|
-
// OpenClaw: directories with AGENTS.md
|
|
486
|
-
const openclawDir = path.join(versionHome, '.openclaw');
|
|
487
|
-
if (fs.existsSync(openclawDir)) {
|
|
488
|
-
result.subagents = fs.readdirSync(openclawDir, { withFileTypes: true })
|
|
489
|
-
.filter(d => d.isDirectory() && fs.existsSync(path.join(openclawDir, d.name, 'AGENTS.md')))
|
|
490
|
-
.map(d => d.name);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
// Plugins - check which discovered plugins have their skills in the version
|
|
495
|
-
if (PLUGINS_CAPABLE_AGENTS.includes(agent)) {
|
|
496
|
-
const allPlugins = discoverPlugins();
|
|
497
|
-
for (const plugin of allPlugins) {
|
|
498
|
-
if (isPluginSynced(plugin, agent, versionHome)) {
|
|
499
|
-
result.plugins.push(plugin.name);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
// Workflows - check {versionHome}/workflows/ for synced workflow directories
|
|
504
|
-
if (WORKFLOW_CAPABLE_AGENTS.includes(agent)) {
|
|
505
|
-
const workflowsDir = path.join(versionHome, 'workflows');
|
|
506
|
-
if (fs.existsSync(workflowsDir)) {
|
|
507
|
-
result.workflows = fs.readdirSync(workflowsDir, { withFileTypes: true })
|
|
508
|
-
.filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
|
|
509
|
-
.map(d => d.name);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
292
|
+
// Dispatch each kind through DETECTORS. The registry guarantees a detector
|
|
293
|
+
// exists for every supported (agent, kind) pair; unsupported pairs leave
|
|
294
|
+
// the field empty. The previous per-agent if-ladder silently dropped
|
|
295
|
+
// antigravity/gemini/grok detection — see PR description for details.
|
|
296
|
+
const ctx = { version, versionHome, cwd };
|
|
297
|
+
result.commands = getDetector('commands', agent)?.list(ctx) ?? [];
|
|
298
|
+
result.skills = getDetector('skills', agent)?.list(ctx) ?? [];
|
|
299
|
+
result.hooks = getDetector('hooks', agent)?.list(ctx) ?? [];
|
|
300
|
+
result.memory = getDetector('rules', agent)?.list(ctx) ?? [];
|
|
301
|
+
result.mcp = getDetector('mcp', agent)?.list(ctx) ?? [];
|
|
302
|
+
result.permissions = getDetector('permissions', agent)?.list(ctx) ?? [];
|
|
303
|
+
result.subagents = getDetector('subagents', agent)?.list(ctx) ?? [];
|
|
304
|
+
result.plugins = getDetector('plugins', agent)?.list(ctx) ?? [];
|
|
305
|
+
result.workflows = getDetector('workflows', agent)?.list(ctx) ?? [];
|
|
512
306
|
return result;
|
|
513
307
|
}
|
|
514
308
|
/**
|
|
@@ -659,9 +453,9 @@ export function hasNewResources(diff, agent, version) {
|
|
|
659
453
|
const hooksApply = agent ? supports(agent, 'hooks', version).ok : true;
|
|
660
454
|
const mcpApply = agent ? supports(agent, 'mcp', version).ok : true;
|
|
661
455
|
const permsApply = agent ? supports(agent, 'allowlist', version).ok : true;
|
|
662
|
-
const subagentsApply = agent ?
|
|
456
|
+
const subagentsApply = agent ? supports(agent, 'subagents', version).ok : true;
|
|
663
457
|
const pluginsApply = agent ? supports(agent, 'plugins', version).ok : true;
|
|
664
|
-
const workflowsApply = agent ?
|
|
458
|
+
const workflowsApply = agent ? supports(agent, 'workflows', version).ok : true;
|
|
665
459
|
return ((diff.commands.length > 0 && commandsApply) ||
|
|
666
460
|
diff.skills.length > 0 ||
|
|
667
461
|
(diff.hooks.length > 0 && hooksApply) ||
|
|
@@ -682,8 +476,9 @@ function buildNewResourcesSummary(newResources, agent, version) {
|
|
|
682
476
|
// Use version-aware gates so Codex >= 0.117.0 (which converts commands to skills) doesn't
|
|
683
477
|
// double-count and so "16 commands" never appears in the summary when commands have
|
|
684
478
|
// already been emitted as skills in the version home.
|
|
685
|
-
const commandsApply =
|
|
479
|
+
const commandsApply = supports(agent, 'commands', version).ok;
|
|
686
480
|
const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
|
|
481
|
+
const rulesApply = supports(agent, 'rules', version).ok;
|
|
687
482
|
if (newResources.commands.length > 0 && (commandsApply || commandsAsSkills)) {
|
|
688
483
|
parts.push(`${newResources.commands.length} command${newResources.commands.length === 1 ? '' : 's'}`);
|
|
689
484
|
}
|
|
@@ -693,22 +488,22 @@ function buildNewResourcesSummary(newResources, agent, version) {
|
|
|
693
488
|
if (newResources.hooks.length > 0 && agentConfig.supportsHooks) {
|
|
694
489
|
parts.push(`${newResources.hooks.length} hook${newResources.hooks.length === 1 ? '' : 's'}`);
|
|
695
490
|
}
|
|
696
|
-
if (newResources.memory.length > 0 &&
|
|
491
|
+
if (newResources.memory.length > 0 && rulesApply) {
|
|
697
492
|
parts.push(`${newResources.memory.length} rule file${newResources.memory.length === 1 ? '' : 's'}`);
|
|
698
493
|
}
|
|
699
|
-
if (newResources.mcp.length > 0 &&
|
|
494
|
+
if (newResources.mcp.length > 0 && supports(agent, 'mcp', version).ok) {
|
|
700
495
|
parts.push(`${newResources.mcp.length} MCP${newResources.mcp.length === 1 ? '' : 's'}`);
|
|
701
496
|
}
|
|
702
|
-
if (newResources.permissions.length > 0 &&
|
|
497
|
+
if (newResources.permissions.length > 0 && supports(agent, 'allowlist', version).ok) {
|
|
703
498
|
parts.push(`${newResources.permissions.length} permission group${newResources.permissions.length === 1 ? '' : 's'}`);
|
|
704
499
|
}
|
|
705
|
-
if (newResources.subagents.length > 0 &&
|
|
500
|
+
if (newResources.subagents.length > 0 && supports(agent, 'subagents', version).ok) {
|
|
706
501
|
parts.push(`${newResources.subagents.length} subagent${newResources.subagents.length === 1 ? '' : 's'}`);
|
|
707
502
|
}
|
|
708
|
-
if (newResources.plugins.length > 0 &&
|
|
503
|
+
if (newResources.plugins.length > 0 && supports(agent, 'plugins', version).ok) {
|
|
709
504
|
parts.push(`${newResources.plugins.length} plugin${newResources.plugins.length === 1 ? '' : 's'}`);
|
|
710
505
|
}
|
|
711
|
-
if (newResources.workflows.length > 0 &&
|
|
506
|
+
if (newResources.workflows.length > 0 && supports(agent, 'workflows', version).ok) {
|
|
712
507
|
parts.push(`${newResources.workflows.length} workflow${newResources.workflows.length === 1 ? '' : 's'}`);
|
|
713
508
|
}
|
|
714
509
|
return parts.join(', ');
|
|
@@ -723,9 +518,10 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
723
518
|
// Version-aware gates. When version is known, prefer per-version capability checks; the
|
|
724
519
|
// commands branch is allowed when either native commands are supported OR when the
|
|
725
520
|
// version emits commands as converted skills (Codex >= 0.117.0).
|
|
726
|
-
const commandsApply =
|
|
521
|
+
const commandsApply = supports(agent, 'commands', version).ok;
|
|
727
522
|
const commandsAsSkills = version ? shouldInstallCommandAsSkill(agent, version) : false;
|
|
728
523
|
const commandsBranch = commandsApply || commandsAsSkills;
|
|
524
|
+
const rulesBranch = supports(agent, 'rules', version).ok;
|
|
729
525
|
// Get permission group info for display
|
|
730
526
|
const permissionGroups = discoverPermissionGroups();
|
|
731
527
|
const newPermissionGroups = permissionGroups.filter(g => newResources.permissions.includes(g.name));
|
|
@@ -755,17 +551,17 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
755
551
|
selection.skills = newResources.skills;
|
|
756
552
|
if (newResources.hooks.length > 0 && agentConfig.supportsHooks)
|
|
757
553
|
selection.hooks = newResources.hooks;
|
|
758
|
-
if (newResources.memory.length > 0 &&
|
|
554
|
+
if (newResources.memory.length > 0 && rulesBranch)
|
|
759
555
|
selection.memory = newResources.memory;
|
|
760
|
-
if (newResources.mcp.length > 0 &&
|
|
556
|
+
if (newResources.mcp.length > 0 && supports(agent, 'mcp', version).ok)
|
|
761
557
|
selection.mcp = newResources.mcp;
|
|
762
|
-
if (newResources.permissions.length > 0 &&
|
|
558
|
+
if (newResources.permissions.length > 0 && supports(agent, 'allowlist', version).ok)
|
|
763
559
|
selection.permissions = newResources.permissions;
|
|
764
|
-
if (newResources.subagents.length > 0 &&
|
|
560
|
+
if (newResources.subagents.length > 0 && supports(agent, 'subagents', version).ok)
|
|
765
561
|
selection.subagents = newResources.subagents;
|
|
766
|
-
if (newResources.plugins.length > 0 &&
|
|
562
|
+
if (newResources.plugins.length > 0 && supports(agent, 'plugins', version).ok)
|
|
767
563
|
selection.plugins = newResources.plugins;
|
|
768
|
-
if (newResources.workflows.length > 0 &&
|
|
564
|
+
if (newResources.workflows.length > 0 && supports(agent, 'workflows', version).ok)
|
|
769
565
|
selection.workflows = newResources.workflows;
|
|
770
566
|
return selection;
|
|
771
567
|
}
|
|
@@ -794,7 +590,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
794
590
|
if (selected.length > 0)
|
|
795
591
|
selection.hooks = selected;
|
|
796
592
|
}
|
|
797
|
-
if (newResources.memory.length > 0 &&
|
|
593
|
+
if (newResources.memory.length > 0 && rulesBranch) {
|
|
798
594
|
const selected = await checkbox({
|
|
799
595
|
message: 'Select new rule files to sync:',
|
|
800
596
|
choices: newResources.memory.map(m => ({ name: m, value: m, checked: true })),
|
|
@@ -802,7 +598,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
802
598
|
if (selected.length > 0)
|
|
803
599
|
selection.memory = selected;
|
|
804
600
|
}
|
|
805
|
-
if (newResources.mcp.length > 0 &&
|
|
601
|
+
if (newResources.mcp.length > 0 && supports(agent, 'mcp', version).ok) {
|
|
806
602
|
const selected = await checkbox({
|
|
807
603
|
message: 'Select new MCPs to sync:',
|
|
808
604
|
choices: newResources.mcp.map(m => ({ name: m, value: m, checked: true })),
|
|
@@ -810,7 +606,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
810
606
|
if (selected.length > 0)
|
|
811
607
|
selection.mcp = selected;
|
|
812
608
|
}
|
|
813
|
-
if (newResources.permissions.length > 0 &&
|
|
609
|
+
if (newResources.permissions.length > 0 && supports(agent, 'allowlist', version).ok) {
|
|
814
610
|
const selected = await checkbox({
|
|
815
611
|
message: 'Select new permission groups to sync:',
|
|
816
612
|
choices: newPermissionGroups.map(g => ({
|
|
@@ -822,7 +618,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
822
618
|
if (selected.length > 0)
|
|
823
619
|
selection.permissions = selected;
|
|
824
620
|
}
|
|
825
|
-
if (newResources.subagents.length > 0 &&
|
|
621
|
+
if (newResources.subagents.length > 0 && supports(agent, 'subagents', version).ok) {
|
|
826
622
|
const selected = await checkbox({
|
|
827
623
|
message: 'Select new subagents to sync:',
|
|
828
624
|
choices: newResources.subagents.map(s => ({ name: s, value: s, checked: true })),
|
|
@@ -830,7 +626,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
830
626
|
if (selected.length > 0)
|
|
831
627
|
selection.subagents = selected;
|
|
832
628
|
}
|
|
833
|
-
if (newResources.plugins.length > 0 &&
|
|
629
|
+
if (newResources.plugins.length > 0 && supports(agent, 'plugins', version).ok) {
|
|
834
630
|
const allPlugins = discoverPlugins();
|
|
835
631
|
const pluginMap = new Map(allPlugins.map(p => [p.name, p]));
|
|
836
632
|
const selected = await checkbox({
|
|
@@ -844,7 +640,7 @@ export async function promptNewResourceSelection(agent, newResources, version) {
|
|
|
844
640
|
if (selected.length > 0)
|
|
845
641
|
selection.plugins = selected;
|
|
846
642
|
}
|
|
847
|
-
if (newResources.workflows.length > 0 &&
|
|
643
|
+
if (newResources.workflows.length > 0 && supports(agent, 'workflows', version).ok) {
|
|
848
644
|
const selected = await checkbox({
|
|
849
645
|
message: 'Select new workflows to sync:',
|
|
850
646
|
choices: newResources.workflows.map(w => ({ name: w, value: w, checked: true })),
|
|
@@ -866,14 +662,14 @@ export async function promptResourceSelection(agent) {
|
|
|
866
662
|
const permissionGroups = discoverPermissionGroups();
|
|
867
663
|
const totalPermissionRules = permissionGroups.reduce((sum, g) => sum + g.ruleCount, 0);
|
|
868
664
|
const categories = [
|
|
869
|
-
{ key: 'commands', label: 'Commands', available:
|
|
665
|
+
{ key: 'commands', label: 'Commands', available: supports(agent, 'commands').ok && available.commands.length > 0, displayCount: `${available.commands.length} available` },
|
|
870
666
|
{ key: 'skills', label: 'Skills', available: available.skills.length > 0, displayCount: `${available.skills.length} available` },
|
|
871
667
|
{ key: 'hooks', label: 'Hooks', available: agentConfig.supportsHooks && available.hooks.length > 0, displayCount: `${available.hooks.length} available` },
|
|
872
|
-
{ key: 'memory', label: 'Rules', available:
|
|
873
|
-
{ key: 'mcp', label: 'MCPs', available:
|
|
874
|
-
{ key: 'permissions', label: 'Permissions', available:
|
|
875
|
-
{ key: 'subagents', label: 'Subagents', available:
|
|
876
|
-
{ key: 'plugins', label: 'Plugins', available:
|
|
668
|
+
{ key: 'memory', label: 'Rules', available: supports(agent, 'rules').ok && available.memory.length > 0, displayCount: `${available.memory.length} available` },
|
|
669
|
+
{ key: 'mcp', label: 'MCPs', available: supports(agent, 'mcp').ok && available.mcp.length > 0, displayCount: `${available.mcp.length} available` },
|
|
670
|
+
{ key: 'permissions', label: 'Permissions', available: supports(agent, 'allowlist').ok && permissionGroups.length > 0, displayCount: `${permissionGroups.length} groups, ${totalPermissionRules} rules` },
|
|
671
|
+
{ key: 'subagents', label: 'Subagents', available: supports(agent, 'subagents').ok && available.subagents.length > 0, displayCount: `${available.subagents.length} available` },
|
|
672
|
+
{ key: 'plugins', label: 'Plugins', available: supports(agent, 'plugins').ok && available.plugins.length > 0, displayCount: `${available.plugins.length} available` },
|
|
877
673
|
];
|
|
878
674
|
const availableCategories = categories.filter(c => c.available);
|
|
879
675
|
if (availableCategories.length === 0) {
|
|
@@ -1050,7 +846,7 @@ export async function getLatestNpmVersion(agent) {
|
|
|
1050
846
|
if (!agentConfig.npmPackage)
|
|
1051
847
|
return null;
|
|
1052
848
|
try {
|
|
1053
|
-
const { stdout } = await execFileAsync('npm', ['view', agentConfig.npmPackage, 'version']);
|
|
849
|
+
const { stdout } = await execFileAsync('npm', ['view', agentConfig.npmPackage, 'version'], { shell: process.platform === 'win32' });
|
|
1054
850
|
return stdout.trim();
|
|
1055
851
|
}
|
|
1056
852
|
catch {
|
|
@@ -1176,6 +972,26 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1176
972
|
const versionDir = getVersionDir(agent, installedVersion);
|
|
1177
973
|
fs.mkdirSync(versionDir, { recursive: true });
|
|
1178
974
|
fs.mkdirSync(path.join(versionDir, 'home'), { recursive: true });
|
|
975
|
+
// Symlink the installed binary into the version's node_modules/.bin so
|
|
976
|
+
// listInstalledVersions (which checks getBinaryPath) sees this version as
|
|
977
|
+
// installed. Without this, `agents add antigravity@latest` succeeds
|
|
978
|
+
// but `agents view` shows the agent under "Not Managed" because
|
|
979
|
+
// listInstalledVersions returns [] — the installer drops the binary in
|
|
980
|
+
// ~/.local/bin (or similar) rather than the version's node_modules/.bin.
|
|
981
|
+
// Grok is special-cased in getBinaryPath itself (binary lives in
|
|
982
|
+
// ~/.grok/downloads), so we skip the symlink there.
|
|
983
|
+
if (agent !== 'grok') {
|
|
984
|
+
try {
|
|
985
|
+
const { stdout: whichOut } = await execFileAsync(process.platform === 'win32' ? 'where' : 'which', [agentConfig.cliCommand]);
|
|
986
|
+
const installedBinary = whichOut.trim().split('\n')[0];
|
|
987
|
+
if (installedBinary && fs.existsSync(installedBinary)) {
|
|
988
|
+
importInstallScriptBinary({ agentId: agent, npmPackage: agentConfig.npmPackage, cliCommand: agentConfig.cliCommand }, installedVersion, installedBinary, versionDir);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
/* binary missing from PATH — install script failed silently; surface via the existing version.install error path below isn't possible here since the script returned 0. Leave the version dir empty so getBinaryPath check correctly reports it uninstalled. */
|
|
993
|
+
}
|
|
994
|
+
}
|
|
1179
995
|
createVersionedAlias(agent, installedVersion);
|
|
1180
996
|
emit('version.install', { agent, version: installedVersion });
|
|
1181
997
|
return { success: true, installedVersion };
|
|
@@ -1198,8 +1014,9 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1198
1014
|
: `${agentConfig.npmPackage}@${version}`;
|
|
1199
1015
|
try {
|
|
1200
1016
|
// Check npm is available
|
|
1017
|
+
const winShell = process.platform === 'win32';
|
|
1201
1018
|
try {
|
|
1202
|
-
await execFileAsync('
|
|
1019
|
+
await execFileAsync('npm', ['--version'], { shell: winShell });
|
|
1203
1020
|
}
|
|
1204
1021
|
catch {
|
|
1205
1022
|
return {
|
|
@@ -1209,7 +1026,7 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1209
1026
|
};
|
|
1210
1027
|
}
|
|
1211
1028
|
onProgress?.(`Installing ${packageSpec}...`);
|
|
1212
|
-
const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir });
|
|
1029
|
+
const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir, shell: winShell });
|
|
1213
1030
|
// Determine the actual installed version
|
|
1214
1031
|
let installedVersion = version;
|
|
1215
1032
|
if (version === 'latest') {
|
|
@@ -1282,10 +1099,10 @@ function removeInstallArtifacts(versionDir) {
|
|
|
1282
1099
|
}
|
|
1283
1100
|
}
|
|
1284
1101
|
/**
|
|
1285
|
-
* Soft-delete a version directory by moving it to ~/.agents
|
|
1102
|
+
* Soft-delete a version directory by moving it to ~/.agents/.system/trash/versions/.
|
|
1286
1103
|
* Returns the trash path on success or null on failure / no source.
|
|
1287
1104
|
*
|
|
1288
|
-
* Trash layout: ~/.agents
|
|
1105
|
+
* Trash layout: ~/.agents/.system/trash/versions/<agent>/<version>/<timestamp>/
|
|
1289
1106
|
* The timestamp suffix lets a user soft-delete the same version twice (after
|
|
1290
1107
|
* re-install) without collision and gives a chronological audit trail.
|
|
1291
1108
|
*
|
|
@@ -1314,7 +1131,7 @@ export function softDeleteVersionDir(agent, version) {
|
|
|
1314
1131
|
* Remove a specific version of an agent.
|
|
1315
1132
|
*
|
|
1316
1133
|
* Soft-delete only: moves the entire version directory (including `home/`)
|
|
1317
|
-
* to ~/.agents
|
|
1134
|
+
* to ~/.agents/.system/trash/versions/. Recoverable via `agents trash restore`.
|
|
1318
1135
|
* Nothing is hard-deleted.
|
|
1319
1136
|
*/
|
|
1320
1137
|
export function removeVersion(agent, version) {
|
|
@@ -1453,7 +1270,7 @@ export function resolveVersionAliasLoose(agent, raw) {
|
|
|
1453
1270
|
return raw;
|
|
1454
1271
|
}
|
|
1455
1272
|
/**
|
|
1456
|
-
* Get version specified in a project-root agents.yaml (not the user ~/.agents
|
|
1273
|
+
* Get version specified in a project-root agents.yaml (not the user ~/.agents/.system/agents.yaml).
|
|
1457
1274
|
*/
|
|
1458
1275
|
export function getProjectVersion(agent, startPath) {
|
|
1459
1276
|
const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
@@ -1519,8 +1336,7 @@ export async function getInstalledVersion(agent, version) {
|
|
|
1519
1336
|
async function getCliVersionFromPath(agent) {
|
|
1520
1337
|
const agentConfig = AGENTS[agent];
|
|
1521
1338
|
try {
|
|
1522
|
-
await execFileAsync('
|
|
1523
|
-
const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000 });
|
|
1339
|
+
const { stdout } = await execFileAsync(agentConfig.cliCommand, ['--version'], { timeout: 3000, shell: process.platform === 'win32' });
|
|
1524
1340
|
const match = stdout.match(/(\d+\.\d+\.\d+)/);
|
|
1525
1341
|
return match ? match[1] : null;
|
|
1526
1342
|
}
|
|
@@ -1818,59 +1634,23 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1818
1634
|
}
|
|
1819
1635
|
return [];
|
|
1820
1636
|
};
|
|
1821
|
-
// Sync commands
|
|
1637
|
+
// Sync commands — dispatch through WRITERS.commands. The writer dispatches
|
|
1638
|
+
// between native (file copy / TOML conversion) and commands-as-skills
|
|
1639
|
+
// (grok, Codex >= 0.117.0) based on `shouldInstallCommandAsSkill`. The
|
|
1640
|
+
// previous COMMANDS_CAPABLE_AGENTS gate excluded grok even though it
|
|
1641
|
+
// takes the commands-as-skills path — silently dropping every command.
|
|
1642
|
+
const commandsWriter = getWriter('commands', agent);
|
|
1822
1643
|
const commandsToSync = selection
|
|
1823
1644
|
? resolveSelection(selection.commands, available.commands)
|
|
1824
1645
|
: available.commands; // No selection = sync all
|
|
1825
|
-
if (commandsToSync.length > 0 &&
|
|
1646
|
+
if (commandsToSync.length > 0 && commandsWriter) {
|
|
1826
1647
|
const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1827
1648
|
const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
|
|
1828
1649
|
if (commandsAsSkills) {
|
|
1829
1650
|
removePath(commandsTarget);
|
|
1830
1651
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
}
|
|
1834
|
-
const syncedCommands = [];
|
|
1835
|
-
for (const cmd of commandsToSync) {
|
|
1836
|
-
// Commands are content that gets injected into the agent's prompt
|
|
1837
|
-
// surface (slash commands, skill bodies). We intentionally do NOT pull
|
|
1838
|
-
// from the project's own .agents/commands/ directory: a cloned public
|
|
1839
|
-
// repo could ship a command whose body instructs the agent to do
|
|
1840
|
-
// something harmful the next time the user invokes it. Commands must
|
|
1841
|
-
// come from the user's central ~/.agents/commands/, the system layer,
|
|
1842
|
-
// or an explicitly enabled extra repo. Same defense as hooks below.
|
|
1843
|
-
const candidates = [
|
|
1844
|
-
safeJoin(path.join(userAgentsDir, 'commands'), `${cmd}.md`),
|
|
1845
|
-
safeJoin(getCommandsDir(), `${cmd}.md`),
|
|
1846
|
-
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'commands'), `${cmd}.md`)),
|
|
1847
|
-
];
|
|
1848
|
-
const srcFile = candidates.find((p) => p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink()) || null;
|
|
1849
|
-
if (!srcFile)
|
|
1850
|
-
continue;
|
|
1851
|
-
if (commandsAsSkills) {
|
|
1852
|
-
// Project skills dir is intentionally excluded for the same reason
|
|
1853
|
-
// commands are: the body of a project skill becomes agent context.
|
|
1854
|
-
const skillSourceDirs = [
|
|
1855
|
-
path.join(userAgentsDir, 'skills'),
|
|
1856
|
-
getSkillsDir(),
|
|
1857
|
-
...extraRepos.map((e) => path.join(e.dir, 'skills')),
|
|
1858
|
-
];
|
|
1859
|
-
const installed = installCommandSkillToVersion(agentDir, cmd, srcFile, skillSourceDirs);
|
|
1860
|
-
if (!installed.success)
|
|
1861
|
-
continue;
|
|
1862
|
-
}
|
|
1863
|
-
else if (agentConfig.format === 'toml') {
|
|
1864
|
-
const content = fs.readFileSync(srcFile, 'utf-8');
|
|
1865
|
-
const tomlContent = markdownToToml(cmd, content);
|
|
1866
|
-
fs.writeFileSync(safeJoin(commandsTarget, `${cmd}.toml`), tomlContent);
|
|
1867
|
-
}
|
|
1868
|
-
else {
|
|
1869
|
-
fs.copyFileSync(srcFile, safeJoin(commandsTarget, `${cmd}.md`));
|
|
1870
|
-
}
|
|
1871
|
-
syncedCommands.push(cmd);
|
|
1872
|
-
}
|
|
1873
|
-
result.commands = syncedCommands.length > 0;
|
|
1652
|
+
const r = commandsWriter.write({ version, versionHome, selection: commandsToSync, cwd });
|
|
1653
|
+
result.commands = r.synced.length > 0;
|
|
1874
1654
|
}
|
|
1875
1655
|
// Orphan-sweep stale top-level command files from previous syncs under a
|
|
1876
1656
|
// different cwd. Only runs in "full sync" mode — i.e. when the caller did
|
|
@@ -1879,7 +1659,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1879
1659
|
// alone), so the sweep would be a contract violation there. The
|
|
1880
1660
|
// cross-project leak always comes from the no-selection shim auto-sync at
|
|
1881
1661
|
// launch.
|
|
1882
|
-
if (!userPassedSelection &&
|
|
1662
|
+
if (!userPassedSelection && commandsWriter && !shouldInstallCommandAsSkill(agent, version)) {
|
|
1883
1663
|
const commandsTargetSweep = path.join(agentDir, agentConfig.commandsSubdir);
|
|
1884
1664
|
if (fs.existsSync(commandsTargetSweep)) {
|
|
1885
1665
|
const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
|
|
@@ -1896,51 +1676,20 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1896
1676
|
}
|
|
1897
1677
|
}
|
|
1898
1678
|
}
|
|
1899
|
-
// Sync skills
|
|
1679
|
+
// Sync skills — dispatch through WRITERS.skills. Agents that natively read
|
|
1680
|
+
// ~/.agents/skills/ (Gemini) are not registered; we clear the version-home
|
|
1681
|
+
// skills dir for them so a stale per-version copy never shadows central.
|
|
1682
|
+
const skillsWriter = getWriter('skills', agent);
|
|
1683
|
+
const skillsToSync = selection
|
|
1684
|
+
? resolveSelection(selection.skills, available.skills)
|
|
1685
|
+
: available.skills;
|
|
1900
1686
|
if (agentConfig.nativeAgentsSkillsDir) {
|
|
1901
|
-
|
|
1902
|
-
const skillsTarget = path.join(agentDir, 'skills');
|
|
1903
|
-
removePath(skillsTarget);
|
|
1687
|
+
removePath(path.join(agentDir, 'skills'));
|
|
1904
1688
|
}
|
|
1905
|
-
else {
|
|
1906
|
-
const skillsToSync = selection
|
|
1907
|
-
? resolveSelection(selection.skills, available.skills)
|
|
1908
|
-
: available.skills;
|
|
1689
|
+
else if (skillsWriter) {
|
|
1909
1690
|
if (skillsToSync.length > 0) {
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
// parent symlink before touching children so removePath(destDir) cannot
|
|
1913
|
-
// delete the central source through it.
|
|
1914
|
-
try {
|
|
1915
|
-
if (fs.lstatSync(skillsTarget).isSymbolicLink()) {
|
|
1916
|
-
removePath(skillsTarget);
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
catch { /* target does not exist yet */ }
|
|
1920
|
-
fs.mkdirSync(skillsTarget, { recursive: true });
|
|
1921
|
-
const syncedSkills = [];
|
|
1922
|
-
for (const skill of skillsToSync) {
|
|
1923
|
-
// Same defense as commands and hooks: don't pull skills from the
|
|
1924
|
-
// project's .agents/skills/ directory. A skill's contents (SKILL.md
|
|
1925
|
-
// and any auxiliary scripts) get loaded into the agent's tool/context
|
|
1926
|
-
// surface, and a malicious public repo could ship a SKILL.md whose
|
|
1927
|
-
// body coerces the agent. Trusted layers only.
|
|
1928
|
-
const skillCandidates = [
|
|
1929
|
-
safeJoin(path.join(userAgentsDir, 'skills'), skill),
|
|
1930
|
-
safeJoin(getSkillsDir(), skill),
|
|
1931
|
-
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'skills'), skill)),
|
|
1932
|
-
];
|
|
1933
|
-
const srcDir = skillCandidates.find((p) => fs.existsSync(p) &&
|
|
1934
|
-
!fs.lstatSync(p).isSymbolicLink() &&
|
|
1935
|
-
fs.lstatSync(p).isDirectory()) || null;
|
|
1936
|
-
if (!srcDir)
|
|
1937
|
-
continue;
|
|
1938
|
-
const destDir = safeJoin(skillsTarget, skill);
|
|
1939
|
-
removePath(destDir);
|
|
1940
|
-
copyDir(srcDir, destDir);
|
|
1941
|
-
syncedSkills.push(skill);
|
|
1942
|
-
}
|
|
1943
|
-
result.skills = syncedSkills.length > 0;
|
|
1691
|
+
const r = skillsWriter.write({ version, versionHome, selection: skillsToSync, cwd });
|
|
1692
|
+
result.skills = r.synced.length > 0;
|
|
1944
1693
|
}
|
|
1945
1694
|
// Orphan-sweep stale skill directories from previous syncs under a
|
|
1946
1695
|
// different cwd. Only runs in "full sync" mode (no explicit selection) —
|
|
@@ -1958,9 +1707,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1958
1707
|
}
|
|
1959
1708
|
}
|
|
1960
1709
|
}
|
|
1961
|
-
// Sync hooks
|
|
1710
|
+
// Sync hooks — dispatch through WRITERS.hooks. supports() gate enforces
|
|
1711
|
+
// the version cutoff (codex >= 0.116.0, gemini >= 0.26.0).
|
|
1962
1712
|
const hooksGate = supports(agent, 'hooks', version);
|
|
1963
|
-
|
|
1713
|
+
const hooksWriter = getWriter('hooks', agent);
|
|
1714
|
+
if (agentConfig.supportsHooks && hooksWriter) {
|
|
1964
1715
|
if (!hooksGate.ok) {
|
|
1965
1716
|
console.warn(explainSkip(agent, 'hooks', hooksGate, version) + ' -- skipped');
|
|
1966
1717
|
}
|
|
@@ -1969,35 +1720,13 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
1969
1720
|
? resolveSelection(selection.hooks, available.hooks)
|
|
1970
1721
|
: available.hooks;
|
|
1971
1722
|
if (hooksToSync.length > 0) {
|
|
1972
|
-
const
|
|
1973
|
-
const hooksTarget = path.join(agentDir, 'hooks');
|
|
1974
|
-
fs.mkdirSync(hooksTarget, { recursive: true });
|
|
1975
|
-
const syncedHooks = [];
|
|
1976
|
-
for (const hook of hooksToSync) {
|
|
1977
|
-
// Hooks are executable shell scripts that run on agent events. We
|
|
1978
|
-
// intentionally do NOT pull from the project's own .agents/hooks/
|
|
1979
|
-
// directory: that would let any cloned public repo plant an
|
|
1980
|
-
// executable that fires the next time the user runs `agents use`
|
|
1981
|
-
// inside that repo. Hooks must come from the user's central
|
|
1982
|
-
// ~/.agents/hooks/ or an explicitly enabled extra repo.
|
|
1983
|
-
const candidates = [
|
|
1984
|
-
safeJoin(path.join(userAgentsDir, 'hooks'), hook),
|
|
1985
|
-
safeJoin(centralHooks, hook),
|
|
1986
|
-
...extraRepos.map((e) => safeJoin(path.join(e.dir, 'hooks'), hook)),
|
|
1987
|
-
];
|
|
1988
|
-
const srcFile = candidates.find((p) => p && fs.existsSync(p) && !fs.lstatSync(p).isSymbolicLink()) || null;
|
|
1989
|
-
if (!srcFile)
|
|
1990
|
-
continue;
|
|
1991
|
-
const destFile = safeJoin(hooksTarget, hook);
|
|
1992
|
-
fs.copyFileSync(srcFile, destFile);
|
|
1993
|
-
fs.chmodSync(destFile, 0o755);
|
|
1994
|
-
syncedHooks.push(hook);
|
|
1995
|
-
}
|
|
1723
|
+
const r = hooksWriter.write({ version, versionHome, selection: hooksToSync, cwd });
|
|
1996
1724
|
// Remove orphan files from version home. The trusted set is the
|
|
1997
1725
|
// manifest-declared hook list (`available.hooks`) — auxiliary files
|
|
1998
1726
|
// like README.md or promptcuts.yaml may exist alongside hooks at the
|
|
1999
1727
|
// source but are not hooks and must not linger in version homes from
|
|
2000
1728
|
// older syncs.
|
|
1729
|
+
const hooksTarget = path.join(agentDir, 'hooks');
|
|
2001
1730
|
const trustedHookNames = new Set(available.hooks);
|
|
2002
1731
|
if (fs.existsSync(hooksTarget)) {
|
|
2003
1732
|
for (const file of fs.readdirSync(hooksTarget).filter(f => !f.startsWith('.'))) {
|
|
@@ -2006,23 +1735,19 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2006
1735
|
}
|
|
2007
1736
|
}
|
|
2008
1737
|
}
|
|
2009
|
-
result.hooks =
|
|
2010
|
-
// Register hooks into agent-native settings.json/hooks.json. Gemini
|
|
2011
|
-
// shipped hooks in 0.26.0; gate already passed above so this is safe.
|
|
2012
|
-
// Grok auto-discovers from ~/.grok/hooks/ so the script copy above
|
|
2013
|
-
// is sufficient — no settings.json registration needed.
|
|
2014
|
-
if (agent === 'claude' || agent === 'codex' || agent === 'gemini' || agent === 'antigravity') {
|
|
2015
|
-
registerHooksToSettings(agent, versionHome);
|
|
2016
|
-
}
|
|
1738
|
+
result.hooks = r.synced.length > 0;
|
|
2017
1739
|
}
|
|
2018
1740
|
}
|
|
2019
1741
|
}
|
|
2020
|
-
// Sync rules —
|
|
2021
|
-
// single
|
|
2022
|
-
//
|
|
2023
|
-
//
|
|
1742
|
+
// Sync rules — dispatch through WRITERS.rules. The registry routes to the
|
|
1743
|
+
// single-target writer for any agent that declares `rules: { file }` in
|
|
1744
|
+
// its capability matrix (grok included; the previous gate used the wrong
|
|
1745
|
+
// CAPABLE_AGENTS list and silently skipped it). Project rules are NOT
|
|
1746
|
+
// synced into the version home — they are composed into the workspace at
|
|
1747
|
+
// agents-run time (see compileRulesForProject).
|
|
2024
1748
|
const skipMemory = selection && (selection.memory === undefined || (Array.isArray(selection.memory) && selection.memory.length === 0));
|
|
2025
|
-
|
|
1749
|
+
const rulesWriter = getWriter('rules', agent);
|
|
1750
|
+
if (!skipMemory && rulesWriter) {
|
|
2026
1751
|
try {
|
|
2027
1752
|
// If selection.memory names a single preset, treat it as a one-shot
|
|
2028
1753
|
// override; otherwise read the persisted active preset.
|
|
@@ -2030,13 +1755,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2030
1755
|
? selection.memory[0]
|
|
2031
1756
|
: null;
|
|
2032
1757
|
const preset = overridePreset || getActiveRulesPreset(agent, version);
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2035
|
-
const destFile = safeJoin(agentDir, targetName);
|
|
2036
|
-
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
2037
|
-
removePath(destFile);
|
|
2038
|
-
fs.writeFileSync(destFile, composed.content);
|
|
2039
|
-
result.memory.push(targetName);
|
|
1758
|
+
const r = rulesWriter.write({ version, versionHome, selection: { preset }, cwd });
|
|
1759
|
+
result.memory.push(...r.synced);
|
|
2040
1760
|
// rulesPreset is tracked separately via setActiveRulesPreset.
|
|
2041
1761
|
}
|
|
2042
1762
|
catch (err) {
|
|
@@ -2068,6 +1788,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2068
1788
|
console.warn(`${PERMISSION_PRESET_ENV_VAR}=${activePresetName} but no recipe at ~/.agents/permissions/presets/${activePresetName}.yaml — falling back to all groups`);
|
|
2069
1789
|
}
|
|
2070
1790
|
}
|
|
1791
|
+
const permissionsWriter = getWriter('permissions', agent);
|
|
2071
1792
|
let permsToSync;
|
|
2072
1793
|
if (selection) {
|
|
2073
1794
|
permsToSync = resolveSelection(selection.permissions, allGroupNames);
|
|
@@ -2082,18 +1803,12 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2082
1803
|
}
|
|
2083
1804
|
}
|
|
2084
1805
|
else {
|
|
2085
|
-
permsToSync =
|
|
2086
|
-
? (presetFilteredGroups ?? allGroupNames)
|
|
2087
|
-
: [];
|
|
1806
|
+
permsToSync = permissionsWriter ? (presetFilteredGroups ?? allGroupNames) : [];
|
|
2088
1807
|
}
|
|
2089
|
-
if (permsToSync.length > 0 &&
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
const permResult = applyPermsToVersion(agent, builtPerms, versionHome, true);
|
|
2094
|
-
result.permissions = permResult.success;
|
|
2095
|
-
// permissions patterns already written via ensureVersionResourcePatterns above.
|
|
2096
|
-
}
|
|
1808
|
+
if (permsToSync.length > 0 && permissionsWriter) {
|
|
1809
|
+
const r = permissionsWriter.write({ version, versionHome, selection: permsToSync, cwd });
|
|
1810
|
+
result.permissions = r.synced.length > 0;
|
|
1811
|
+
// permissions patterns already written via ensureVersionResourcePatterns above.
|
|
2097
1812
|
}
|
|
2098
1813
|
// Install MCP servers (if agent supports them)
|
|
2099
1814
|
// For Claude/Codex: uses CLI commands (claude mcp add, codex mcp add)
|
|
@@ -2108,57 +1823,29 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2108
1823
|
// user entry, so name-collision shadowing is not fully closed here —
|
|
2109
1824
|
// tracked separately for a follow-up in lib/mcp.ts.)
|
|
2110
1825
|
const projectScopedMcpNames = new Set(getScopedMcpResources(cwd).filter(r => r.scope === 'project').map(r => r.name));
|
|
1826
|
+
const mcpWriter = getWriter('mcp', agent);
|
|
2111
1827
|
const mcpToSyncAll = selection
|
|
2112
1828
|
? resolveSelection(selection.mcp, available.mcp)
|
|
2113
|
-
: (
|
|
1829
|
+
: (mcpWriter ? available.mcp : []);
|
|
2114
1830
|
const mcpToSync = mcpToSyncAll.filter(n => !projectScopedMcpNames.has(n));
|
|
2115
|
-
if (mcpToSync.length > 0 &&
|
|
2116
|
-
const
|
|
2117
|
-
result.mcp =
|
|
1831
|
+
if (mcpToSync.length > 0 && mcpWriter) {
|
|
1832
|
+
const r = mcpWriter.write({ version, versionHome, selection: mcpToSync, cwd });
|
|
1833
|
+
result.mcp = r.synced;
|
|
2118
1834
|
// mcp patterns already written via ensureVersionResourcePatterns above.
|
|
2119
1835
|
}
|
|
2120
|
-
// Sync subagents
|
|
2121
|
-
//
|
|
2122
|
-
//
|
|
2123
|
-
|
|
2124
|
-
// to plant a subagent the user later invokes. Same defense as hooks.
|
|
1836
|
+
// Sync subagents — dispatch through WRITERS.subagents. listInstalledSubagents
|
|
1837
|
+
// reads only user + system layers (project excluded for the same defense
|
|
1838
|
+
// as commands/skills/hooks).
|
|
1839
|
+
const subagentsWriter = getWriter('subagents', agent);
|
|
2125
1840
|
const subagentsToSync = selection
|
|
2126
1841
|
? resolveSelection(selection.subagents, available.subagents)
|
|
2127
|
-
: (
|
|
2128
|
-
if (subagentsToSync.length > 0 &&
|
|
2129
|
-
const
|
|
2130
|
-
|
|
2131
|
-
for
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
continue;
|
|
2135
|
-
try {
|
|
2136
|
-
if (agent === 'claude') {
|
|
2137
|
-
// Claude: flatten to single .md file
|
|
2138
|
-
const agentsDir = path.join(agentDir, 'agents');
|
|
2139
|
-
fs.mkdirSync(agentsDir, { recursive: true });
|
|
2140
|
-
const transformed = transformSubagentForClaude(subagent.path);
|
|
2141
|
-
fs.writeFileSync(safeJoin(agentsDir, `${subagent.name}.md`), transformed);
|
|
2142
|
-
result.subagents.push(subagent.name);
|
|
2143
|
-
}
|
|
2144
|
-
else if (agent === 'openclaw') {
|
|
2145
|
-
// OpenClaw: copy full directory, rename AGENT.md -> AGENTS.md
|
|
2146
|
-
const targetDir = safeJoin(path.join(versionHome, '.openclaw'), subagent.name);
|
|
2147
|
-
const syncResult = syncSubagentToOpenclaw(subagent.path, targetDir);
|
|
2148
|
-
if (syncResult.success) {
|
|
2149
|
-
result.subagents.push(subagent.name);
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
}
|
|
2153
|
-
catch { /* resource sync failed for this item */ }
|
|
2154
|
-
}
|
|
2155
|
-
// Orphan-sweep stale subagents. Same selection-mode guard as the
|
|
2156
|
-
// commands/skills sweeps above. Claude stores them as flat .md files
|
|
2157
|
-
// under `<agentDir>/agents/`; OpenClaw stores them as subdirs at the
|
|
2158
|
-
// same level as commands/skills/hooks/plugins (no isolated parent dir),
|
|
2159
|
-
// which means a directory-readdir sweep would unsafely hit unrelated
|
|
2160
|
-
// resources. For OpenClaw we lean on the existing per-name copy path —
|
|
2161
|
-
// if the user wants strict isolation on OpenClaw, track via manifest.
|
|
1842
|
+
: (subagentsWriter ? available.subagents : []);
|
|
1843
|
+
if (subagentsToSync.length > 0 && subagentsWriter) {
|
|
1844
|
+
const r = subagentsWriter.write({ version, versionHome, selection: subagentsToSync, cwd });
|
|
1845
|
+
result.subagents.push(...r.synced);
|
|
1846
|
+
// Orphan-sweep for Claude only — see comment on commands/skills sweep
|
|
1847
|
+
// for the no-selection guard. OpenClaw stores subagents as siblings of
|
|
1848
|
+
// other resources so a readdir sweep would over-reach.
|
|
2162
1849
|
if (!userPassedSelection && agent === 'claude') {
|
|
2163
1850
|
const claudeAgentsDir = path.join(agentDir, 'agents');
|
|
2164
1851
|
if (fs.existsSync(claudeAgentsDir)) {
|
|
@@ -2175,48 +1862,24 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2175
1862
|
}
|
|
2176
1863
|
}
|
|
2177
1864
|
}
|
|
2178
|
-
// subagent patterns already written via ensureVersionResourcePatterns above.
|
|
2179
1865
|
}
|
|
2180
|
-
// Sync plugins
|
|
1866
|
+
// Sync plugins — dispatch through WRITERS.plugins.
|
|
1867
|
+
const pluginsWriter = getWriter('plugins', agent);
|
|
2181
1868
|
const pluginsToSync = selection
|
|
2182
1869
|
? resolveSelection(selection.plugins, available.plugins)
|
|
2183
|
-
: (
|
|
2184
|
-
if (pluginsToSync.length > 0 &&
|
|
2185
|
-
const
|
|
2186
|
-
|
|
2187
|
-
// Clean orphaned plugin skills from plugins that no longer exist
|
|
2188
|
-
const activePluginNames = new Set(allPlugins.map(p => p.name));
|
|
2189
|
-
cleanOrphanedPluginSkills(agent, versionHome, activePluginNames);
|
|
2190
|
-
for (const name of pluginsToSync) {
|
|
2191
|
-
const plugin = pluginMap.get(name);
|
|
2192
|
-
if (!plugin || !pluginSupportsAgent(plugin, agent))
|
|
2193
|
-
continue;
|
|
2194
|
-
const pluginResult = syncPluginToVersion(plugin, agent, versionHome, { version });
|
|
2195
|
-
if (pluginResult.success) {
|
|
2196
|
-
result.plugins.push(name);
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
// plugin patterns already written via ensureVersionResourcePatterns above.
|
|
1870
|
+
: (pluginsWriter ? available.plugins : []);
|
|
1871
|
+
if (pluginsToSync.length > 0 && pluginsWriter) {
|
|
1872
|
+
const r = pluginsWriter.write({ version, versionHome, selection: pluginsToSync, cwd });
|
|
1873
|
+
result.plugins.push(...r.synced);
|
|
2200
1874
|
}
|
|
2201
|
-
// Sync workflows
|
|
1875
|
+
// Sync workflows — dispatch through WRITERS.workflows.
|
|
1876
|
+
const workflowsWriter = getWriter('workflows', agent);
|
|
2202
1877
|
const workflowsToSync = selection
|
|
2203
1878
|
? resolveSelection(selection.workflows, available.workflows)
|
|
2204
|
-
: (
|
|
2205
|
-
if (workflowsToSync.length > 0 &&
|
|
2206
|
-
const
|
|
2207
|
-
|
|
2208
|
-
const workflow = allWorkflows.get(name);
|
|
2209
|
-
if (!workflow)
|
|
2210
|
-
continue;
|
|
2211
|
-
try {
|
|
2212
|
-
const syncResult = syncWorkflowToVersion(workflow.path, name, agent, versionHome);
|
|
2213
|
-
if (syncResult.success) {
|
|
2214
|
-
result.workflows.push(name);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
catch { /* resource sync failed for this item */ }
|
|
2218
|
-
}
|
|
2219
|
-
// workflow patterns already written via ensureVersionResourcePatterns above.
|
|
1879
|
+
: (workflowsWriter ? available.workflows : []);
|
|
1880
|
+
if (workflowsToSync.length > 0 && workflowsWriter) {
|
|
1881
|
+
const r = workflowsWriter.write({ version, versionHome, selection: workflowsToSync, cwd });
|
|
1882
|
+
result.workflows.push(...r.synced);
|
|
2220
1883
|
}
|
|
2221
1884
|
// Write manifest after a full sync (no user-passed selection) so the next
|
|
2222
1885
|
// launch can skip the slow path. Pattern-derived selections still count as
|