@phnx-labs/agents-cli 1.20.72 → 1.20.73

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.
Files changed (219) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +8 -4
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/feed.d.ts +13 -0
  16. package/dist/commands/feed.js +130 -29
  17. package/dist/commands/hq.d.ts +2 -0
  18. package/dist/commands/hq.js +58 -0
  19. package/dist/commands/lease.d.ts +19 -0
  20. package/dist/commands/lease.js +138 -11
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.js +123 -0
  23. package/dist/commands/logs.js +1 -1
  24. package/dist/commands/mine.d.ts +27 -0
  25. package/dist/commands/mine.js +207 -0
  26. package/dist/commands/monitors.js +79 -49
  27. package/dist/commands/plugins.js +1 -0
  28. package/dist/commands/profiles.js +158 -0
  29. package/dist/commands/repo.js +2 -60
  30. package/dist/commands/resources.d.ts +5 -0
  31. package/dist/commands/resources.js +95 -0
  32. package/dist/commands/routines.d.ts +2 -0
  33. package/dist/commands/routines.js +139 -37
  34. package/dist/commands/secrets.js +234 -47
  35. package/dist/commands/send.d.ts +15 -0
  36. package/dist/commands/send.js +62 -0
  37. package/dist/commands/sessions-tail.d.ts +1 -0
  38. package/dist/commands/sessions-tail.js +20 -9
  39. package/dist/commands/sessions.js +18 -3
  40. package/dist/commands/setup-mine.d.ts +18 -0
  41. package/dist/commands/setup-mine.js +106 -0
  42. package/dist/commands/setup-share.js +2 -2
  43. package/dist/commands/setup.js +3 -1
  44. package/dist/commands/share.d.ts +8 -1
  45. package/dist/commands/share.js +122 -37
  46. package/dist/commands/ssh.d.ts +9 -0
  47. package/dist/commands/ssh.js +196 -26
  48. package/dist/commands/sync.js +1 -1
  49. package/dist/commands/teams.js +11 -2
  50. package/dist/index.js +79 -10
  51. package/dist/lib/activity.d.ts +124 -0
  52. package/dist/lib/activity.js +542 -0
  53. package/dist/lib/agents.js +16 -5
  54. package/dist/lib/artifact-actions.d.ts +1 -1
  55. package/dist/lib/artifact-actions.js +1 -1
  56. package/dist/lib/ask-classifier.d.ts +2 -1
  57. package/dist/lib/ask-classifier.js +3 -3
  58. package/dist/lib/auth-health.d.ts +32 -4
  59. package/dist/lib/auth-health.js +40 -1
  60. package/dist/lib/auto-pull.js +8 -1
  61. package/dist/lib/brand.d.ts +40 -0
  62. package/dist/lib/brand.js +122 -0
  63. package/dist/lib/browser/drivers/ssh.js +4 -7
  64. package/dist/lib/browser/service.js +6 -8
  65. package/dist/lib/channels/providers/index.d.ts +2 -0
  66. package/dist/lib/channels/providers/index.js +20 -0
  67. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  68. package/dist/lib/channels/providers/mailbox.js +26 -0
  69. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  70. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  71. package/dist/lib/channels/providers/rush.d.ts +6 -0
  72. package/dist/lib/channels/providers/rush.js +55 -0
  73. package/dist/lib/channels/registry.d.ts +42 -0
  74. package/dist/lib/channels/registry.js +20 -0
  75. package/dist/lib/channels/resolve.d.ts +11 -0
  76. package/dist/lib/channels/resolve.js +12 -0
  77. package/dist/lib/cli-resources.d.ts +18 -0
  78. package/dist/lib/cli-resources.js +53 -4
  79. package/dist/lib/cloud/codex.js +6 -3
  80. package/dist/lib/cloud/factory.d.ts +1 -0
  81. package/dist/lib/cloud/factory.js +10 -4
  82. package/dist/lib/cloud/rush.d.ts +3 -1
  83. package/dist/lib/cloud/rush.js +6 -1
  84. package/dist/lib/cloud/types.d.ts +2 -0
  85. package/dist/lib/codex-home.d.ts +35 -0
  86. package/dist/lib/codex-home.js +136 -0
  87. package/dist/lib/crabbox/cli.d.ts +44 -0
  88. package/dist/lib/crabbox/cli.js +120 -20
  89. package/dist/lib/crabbox/lease.d.ts +52 -1
  90. package/dist/lib/crabbox/lease.js +117 -16
  91. package/dist/lib/crabbox/progress.d.ts +31 -0
  92. package/dist/lib/crabbox/progress.js +76 -0
  93. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  94. package/dist/lib/crabbox/runtimes.js +33 -8
  95. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  96. package/dist/lib/crabbox/setup-copy.js +127 -0
  97. package/dist/lib/daemon.d.ts +5 -5
  98. package/dist/lib/daemon.js +9 -9
  99. package/dist/lib/drive-sync.js +6 -3
  100. package/dist/lib/event-stream.d.ts +36 -0
  101. package/dist/lib/event-stream.js +68 -0
  102. package/dist/lib/events.d.ts +1 -1
  103. package/dist/lib/exec.d.ts +11 -0
  104. package/dist/lib/exec.js +23 -2
  105. package/dist/lib/feed-policy.d.ts +1 -1
  106. package/dist/lib/feed-policy.js +14 -9
  107. package/dist/lib/feed-ranking.d.ts +32 -0
  108. package/dist/lib/feed-ranking.js +224 -0
  109. package/dist/lib/feed.d.ts +35 -2
  110. package/dist/lib/feed.js +66 -3
  111. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  112. package/dist/lib/fleet/auth-sync.js +92 -0
  113. package/dist/lib/fleet/remote-login.d.ts +187 -0
  114. package/dist/lib/fleet/remote-login.js +556 -0
  115. package/dist/lib/format.d.ts +30 -3
  116. package/dist/lib/format.js +34 -5
  117. package/dist/lib/fs-atomic.d.ts +7 -2
  118. package/dist/lib/fs-atomic.js +8 -12
  119. package/dist/lib/git.d.ts +16 -0
  120. package/dist/lib/git.js +79 -2
  121. package/dist/lib/heal.js +11 -3
  122. package/dist/lib/hosts/progress.d.ts +28 -10
  123. package/dist/lib/hosts/progress.js +79 -22
  124. package/dist/lib/hosts/remote-cmd.js +4 -0
  125. package/dist/lib/hq/floor.d.ts +87 -0
  126. package/dist/lib/hq/floor.js +226 -0
  127. package/dist/lib/menubar/install-menubar.js +14 -20
  128. package/dist/lib/notify.d.ts +1 -0
  129. package/dist/lib/notify.js +3 -3
  130. package/dist/lib/open-url.d.ts +2 -0
  131. package/dist/lib/open-url.js +19 -0
  132. package/dist/lib/openclaw-keychain.d.ts +56 -0
  133. package/dist/lib/openclaw-keychain.js +236 -0
  134. package/dist/lib/overdue.js +10 -0
  135. package/dist/lib/permissions.d.ts +44 -1
  136. package/dist/lib/permissions.js +284 -7
  137. package/dist/lib/project-launch.d.ts +6 -12
  138. package/dist/lib/project-launch.js +13 -228
  139. package/dist/lib/project-resources.d.ts +7 -0
  140. package/dist/lib/project-resources.js +291 -0
  141. package/dist/lib/pty-client.d.ts +27 -0
  142. package/dist/lib/pty-client.js +136 -10
  143. package/dist/lib/refresh.js +14 -10
  144. package/dist/lib/resource-profiles.d.ts +26 -0
  145. package/dist/lib/resource-profiles.js +157 -0
  146. package/dist/lib/resources/permissions.js +7 -1
  147. package/dist/lib/resources/types.d.ts +1 -1
  148. package/dist/lib/resources.d.ts +1 -1
  149. package/dist/lib/resources.js +32 -3
  150. package/dist/lib/routines.d.ts +11 -0
  151. package/dist/lib/routines.js +56 -15
  152. package/dist/lib/runner.d.ts +1 -0
  153. package/dist/lib/runner.js +66 -5
  154. package/dist/lib/secrets/bundles.d.ts +16 -3
  155. package/dist/lib/secrets/bundles.js +206 -37
  156. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  157. package/dist/lib/secrets/icloud-import.js +9 -6
  158. package/dist/lib/secrets/mcp.js +1 -1
  159. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  160. package/dist/lib/secrets/vault-age-helper.js +34 -0
  161. package/dist/lib/secrets/vault.d.ts +49 -0
  162. package/dist/lib/secrets/vault.js +397 -0
  163. package/dist/lib/self-heal/checks/path.js +3 -1
  164. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  165. package/dist/lib/self-heal/checks/shims.js +19 -11
  166. package/dist/lib/session/cloud.d.ts +2 -2
  167. package/dist/lib/session/cloud.js +2 -2
  168. package/dist/lib/session/db.d.ts +4 -0
  169. package/dist/lib/session/db.js +44 -7
  170. package/dist/lib/session/discover.d.ts +2 -0
  171. package/dist/lib/session/discover.js +114 -3
  172. package/dist/lib/session/stream-render.d.ts +3 -0
  173. package/dist/lib/session/stream-render.js +130 -0
  174. package/dist/lib/session/types.d.ts +6 -0
  175. package/dist/lib/share/analytics.d.ts +13 -0
  176. package/dist/lib/share/analytics.js +45 -0
  177. package/dist/lib/share/config.d.ts +19 -5
  178. package/dist/lib/share/config.js +44 -8
  179. package/dist/lib/share/provision.d.ts +58 -6
  180. package/dist/lib/share/provision.js +97 -22
  181. package/dist/lib/share/publish.d.ts +36 -13
  182. package/dist/lib/share/publish.js +55 -8
  183. package/dist/lib/share/worker-template.js +83 -17
  184. package/dist/lib/shims.d.ts +9 -0
  185. package/dist/lib/shims.js +91 -13
  186. package/dist/lib/ssh-exec.d.ts +14 -0
  187. package/dist/lib/ssh-exec.js +57 -0
  188. package/dist/lib/staleness/detectors/hooks.js +25 -1
  189. package/dist/lib/staleness/detectors/permissions.js +66 -0
  190. package/dist/lib/staleness/detectors/workflows.js +20 -0
  191. package/dist/lib/staleness/writers/hooks.js +58 -4
  192. package/dist/lib/staleness/writers/permissions.js +2 -2
  193. package/dist/lib/staleness/writers/skills.js +1 -1
  194. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  195. package/dist/lib/staleness/writers/sources.js +68 -1
  196. package/dist/lib/star-nudge.d.ts +45 -0
  197. package/dist/lib/star-nudge.js +91 -0
  198. package/dist/lib/startup/command-registry.d.ts +6 -1
  199. package/dist/lib/startup/command-registry.js +17 -3
  200. package/dist/lib/state.d.ts +2 -0
  201. package/dist/lib/state.js +3 -0
  202. package/dist/lib/subagents-registry.d.ts +2 -0
  203. package/dist/lib/subagents-registry.js +3 -0
  204. package/dist/lib/teams/parsers.js +214 -6
  205. package/dist/lib/teams/supervisor.d.ts +7 -0
  206. package/dist/lib/teams/supervisor.js +2 -1
  207. package/dist/lib/template.d.ts +1 -1
  208. package/dist/lib/template.js +1 -1
  209. package/dist/lib/triggers/webhook.js +36 -3
  210. package/dist/lib/types.d.ts +73 -0
  211. package/dist/lib/version.d.ts +40 -0
  212. package/dist/lib/version.js +94 -16
  213. package/dist/lib/versions.d.ts +11 -0
  214. package/dist/lib/versions.js +208 -76
  215. package/dist/lib/workflows.d.ts +2 -0
  216. package/dist/lib/workflows.js +88 -0
  217. package/package.json +2 -1
  218. package/dist/commands/daemon.d.ts +0 -10
  219. package/dist/commands/daemon.js +0 -121
@@ -24,6 +24,7 @@ import { checkbox, select } from '@inquirer/prompts';
24
24
  import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getSkillsDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset, getHomeDir } from './state.js';
25
25
  import { defaultPatterns, expandPatterns } from './resource-patterns.js';
26
26
  import { listResources } from './resources.js';
27
+ import { activeRulesPreset, filterNamesForActiveResourceProfile } from './resource-profiles.js';
27
28
  // VERSION_RE + compareVersions are owned by the agent-spec engine primitives
28
29
  // (single source of truth). Re-exported below so existing importers of
29
30
  // `compareVersions` from './versions.js' keep working.
@@ -34,34 +35,44 @@ import { parseMcpServerConfig, isProjectMcpTrusted } from './mcp.js';
34
35
  import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
35
36
  import { importInstallScriptBinary } from './import.js';
36
37
  import { IS_WINDOWS, composeWin32CommandLine } from './platform/index.js';
38
+ import { listInstalledSubagents } from './subagents.js';
39
+ import { listInstalledWorkflows } from './workflows.js';
37
40
  import { pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
38
41
  import { supports, explainSkip } from './capabilities.js';
39
- import { discoverPlugins } from './plugins.js';
42
+ import { discoverPlugins, marketplaceSpecForName } from './plugins.js';
40
43
  import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
41
44
  import { emit } from './events.js';
42
45
  import { safeJoin } from './paths.js';
43
46
  import { readSkillSourceCommandMarker, shouldInstallCommandAsSkill } from './command-skills.js';
44
47
  import { getWriter, getDetector } from './staleness/registry.js';
45
48
  import { syncMemoryToVersionHome } from './memory.js';
49
+ import { listPluginSkillNames, resolveCommandSource, resolveSkillSource } from './staleness/writers/sources.js';
50
+ import { syncProjectResourcesToAgent } from './project-resources.js';
46
51
  /** Promisified exec for running shell commands. */
47
52
  const execAsync = promisify(exec);
48
53
  const execFileAsync = promisify(execFile);
49
54
  const RULES_DOC_FILENAME = 'README.md';
50
- function getResourceBases(cwd) {
55
+ function getLayeredResourceBases(cwd) {
51
56
  const projectAgentsDir = getProjectAgentsDir(cwd);
52
57
  const userBase = getUserAgentsDir();
53
58
  const systemBase = getAgentsDir();
54
59
  const resourceBases = [];
55
60
  if (projectAgentsDir) {
56
- resourceBases.push({ scope: 'project', base: projectAgentsDir });
61
+ resourceBases.push({ source: 'project', base: projectAgentsDir });
57
62
  }
58
- resourceBases.push({ scope: 'user', base: userBase });
59
- resourceBases.push({ scope: 'user', base: systemBase });
63
+ resourceBases.push({ source: 'user', base: userBase });
64
+ resourceBases.push({ source: 'system', base: systemBase });
60
65
  for (const extra of getEnabledExtraRepos()) {
61
- resourceBases.push({ scope: 'user', base: extra.dir });
66
+ resourceBases.push({ source: extra.alias, base: extra.dir });
62
67
  }
63
68
  return resourceBases;
64
69
  }
70
+ function getResourceBases(cwd) {
71
+ return getLayeredResourceBases(cwd).map(({ base, source }) => ({
72
+ base,
73
+ scope: source === 'project' ? 'project' : 'user',
74
+ }));
75
+ }
65
76
  function getScopedMcpResources(cwd) {
66
77
  const resources = new Map();
67
78
  for (const { base, scope } of getResourceBases(cwd)) {
@@ -79,6 +90,55 @@ function getScopedMcpResources(cwd) {
79
90
  }
80
91
  return Array.from(resources.values());
81
92
  }
93
+ function sourceMapFromResources(kind, cwd) {
94
+ return new Map(listResources(kind, cwd).map(r => [r.name, r.source]));
95
+ }
96
+ function sourceMapFromLayeredDirectory(cwd, relativePath, listNames) {
97
+ const resources = new Map();
98
+ for (const { base, source } of getLayeredResourceBases(cwd)) {
99
+ const dir = path.join(base, ...relativePath);
100
+ if (!fs.existsSync(dir))
101
+ continue;
102
+ for (const name of listNames(dir)) {
103
+ if (!resources.has(name))
104
+ resources.set(name, source);
105
+ }
106
+ }
107
+ return resources;
108
+ }
109
+ function sourceMapFromPermissionGroups(cwd) {
110
+ return sourceMapFromLayeredDirectory(cwd, ['permissions', 'groups'], (dir) => fs.readdirSync(dir)
111
+ .filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
112
+ .map(f => f.replace(/\.(yaml|yml)$/, '')));
113
+ }
114
+ function sourceMapFromWorkflows(cwd) {
115
+ return sourceMapFromLayeredDirectory(cwd, ['workflows'], (dir) => fs.readdirSync(dir, { withFileTypes: true })
116
+ .filter(d => d.isDirectory() && fs.existsSync(path.join(dir, d.name, 'WORKFLOW.md')))
117
+ .map(d => d.name));
118
+ }
119
+ function sourceMapFromPluginSkills(plugins, activePluginNames, cwd) {
120
+ const sourceRank = new Map([
121
+ ['user', 0],
122
+ ['system', 1],
123
+ ['project', 3],
124
+ ]);
125
+ const entries = plugins
126
+ .filter(plugin => activePluginNames.has(plugin.name))
127
+ .map(plugin => {
128
+ const spec = marketplaceSpecForName(plugin.marketplace, cwd);
129
+ const source = spec.kind === 'extra' ? spec.alias : spec.kind;
130
+ return { plugin, source, rank: sourceRank.get(source) ?? 2 };
131
+ })
132
+ .sort((a, b) => a.rank - b.rank);
133
+ const sources = new Map();
134
+ for (const { plugin, source } of entries) {
135
+ for (const skill of plugin.skills) {
136
+ if (!sources.has(skill))
137
+ sources.set(skill, source);
138
+ }
139
+ }
140
+ return sources;
141
+ }
82
142
  /**
83
143
  * Get all available resources from ~/.agents/.
84
144
  */
@@ -110,7 +170,7 @@ export function getAvailableResources(cwd = process.cwd()) {
110
170
  commandNames.add(name);
111
171
  }
112
172
  }
113
- result.commands = Array.from(commandNames);
173
+ result.commands = filterNamesForActiveResourceProfile('commands', Array.from(commandNames), sourceMapFromResources('commands', cwd));
114
174
  // Skills (directories, excluding hidden)
115
175
  const skillNames = new Set();
116
176
  for (const { base } of resourceBases) {
@@ -124,13 +184,13 @@ export function getAvailableResources(cwd = process.cwd()) {
124
184
  skillNames.add(name);
125
185
  }
126
186
  }
127
- result.skills = Array.from(skillNames);
128
- // Hooks (files). A hook is an actual script: known script extension, OR
129
- // executable bit on a file with a non-data extension. Auxiliary content
130
- // like `README.md` (docs) or `promptcuts.yaml` (data read directly by the
131
- // expand-promptcuts script) lives in hooks/ but is not a hook. Older sync
132
- // runs chmod 0o755'd everything they copied, so an exec bit alone can no
133
- // longer be trusted as the signal.
187
+ result.skills = filterNamesForActiveResourceProfile('skills', Array.from(skillNames), sourceMapFromResources('skills', cwd));
188
+ // Hooks. A hook is either a directory bundle or an actual script file: known
189
+ // script extension, OR executable bit on a file with a non-data extension.
190
+ // Auxiliary content like `README.md` (docs) or `promptcuts.yaml` (data read
191
+ // directly by the expand-promptcuts script) lives in hooks/ but is not a
192
+ // hook. Older sync runs chmod 0o755'd everything they copied, so an exec bit
193
+ // alone can no longer be trusted as the signal.
134
194
  const NON_SCRIPT_EXTS = new Set(['.md', '.markdown', '.rst', '.txt', '.yaml', '.yml', '.json', '.toml', '.ini', '.conf']);
135
195
  const SCRIPT_EXTS = new Set(['.sh', '.bash', '.zsh', '.py', '.js', '.ts', '.mjs', '.cjs', '.rb', '.pl', '.ps1']);
136
196
  const hookNames = new Set();
@@ -142,7 +202,13 @@ export function getAvailableResources(cwd = process.cwd()) {
142
202
  if (name.startsWith('.'))
143
203
  continue;
144
204
  try {
145
- const stat = fs.statSync(path.join(hooksDir, name));
205
+ const stat = fs.lstatSync(path.join(hooksDir, name));
206
+ if (stat.isSymbolicLink())
207
+ continue;
208
+ if (stat.isDirectory()) {
209
+ hookNames.add(name);
210
+ continue;
211
+ }
146
212
  if (!stat.isFile())
147
213
  continue;
148
214
  const ext = path.extname(name).toLowerCase();
@@ -156,7 +222,7 @@ export function getAvailableResources(cwd = process.cwd()) {
156
222
  catch { /* ignore unreadable */ }
157
223
  }
158
224
  }
159
- result.hooks = Array.from(hookNames);
225
+ result.hooks = filterNamesForActiveResourceProfile('hooks', Array.from(hookNames), sourceMapFromResources('hooks', cwd));
160
226
  // Rules — list available presets across layers (project > user > extras > system).
161
227
  // The composer selects exactly one preset per sync; this list drives the
162
228
  // resource-count display and `agents rules switch` picker. Routes through
@@ -184,22 +250,12 @@ export function getAvailableResources(cwd = process.cwd()) {
184
250
  // malformed rules.yaml — skip silently; the composer will surface the error.
185
251
  }
186
252
  }
187
- result.memory = Array.from(presetNames);
188
- result.mcp = getScopedMcpResources(cwd).map(resource => resource.name);
253
+ result.memory = filterNamesForActiveResourceProfile('memory', Array.from(presetNames));
254
+ const scopedMcp = getScopedMcpResources(cwd);
255
+ result.mcp = filterNamesForActiveResourceProfile('mcp', scopedMcp.map(resource => resource.name), new Map(scopedMcp.map(resource => [resource.name, resource.scope])));
189
256
  // Permission groups (from permissions/groups/*.yaml)
190
- const permissionNames = new Set();
191
- for (const { base } of resourceBases) {
192
- const permsGroupsDir = path.join(base, 'permissions', 'groups');
193
- if (!fs.existsSync(permsGroupsDir))
194
- continue;
195
- const names = fs.readdirSync(permsGroupsDir)
196
- .filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
197
- .map(f => f.replace(/\.(yaml|yml)$/, ''));
198
- for (const name of names) {
199
- permissionNames.add(name);
200
- }
201
- }
202
- result.permissions = Array.from(permissionNames);
257
+ const permissionSources = sourceMapFromPermissionGroups(cwd);
258
+ result.permissions = filterNamesForActiveResourceProfile('permissions', Array.from(permissionSources.keys()), permissionSources);
203
259
  // Subagents (directories with AGENT.md)
204
260
  const subagentNames = new Set();
205
261
  for (const { base } of resourceBases) {
@@ -213,24 +269,19 @@ export function getAvailableResources(cwd = process.cwd()) {
213
269
  subagentNames.add(name);
214
270
  }
215
271
  }
216
- result.subagents = Array.from(subagentNames);
272
+ result.subagents = filterNamesForActiveResourceProfile('subagents', Array.from(subagentNames), sourceMapFromResources('subagents', cwd));
217
273
  // Workflows (directories with WORKFLOW.md)
218
- const workflowNames = new Set();
219
- for (const { base } of resourceBases) {
220
- const workflowsDir = path.join(base, 'workflows');
221
- if (!fs.existsSync(workflowsDir))
222
- continue;
223
- const names = fs.readdirSync(workflowsDir, { withFileTypes: true })
224
- .filter(d => d.isDirectory() && fs.existsSync(path.join(workflowsDir, d.name, 'WORKFLOW.md')))
225
- .map(d => d.name);
226
- for (const name of names) {
227
- workflowNames.add(name);
228
- }
229
- }
230
- result.workflows = Array.from(workflowNames);
274
+ const workflowSources = sourceMapFromWorkflows(cwd);
275
+ result.workflows = filterNamesForActiveResourceProfile('workflows', Array.from(workflowSources.keys()), workflowSources);
231
276
  // Plugins (directories with .claude-plugin/plugin.json)
232
277
  const allPlugins = discoverPlugins();
233
- result.plugins = allPlugins.map(p => p.name);
278
+ result.plugins = filterNamesForActiveResourceProfile('plugins', allPlugins.map(p => p.name), new Map(allPlugins.map(p => [p.name, 'user'])));
279
+ const activePlugins = new Set(result.plugins);
280
+ const pluginSkillNames = filterNamesForActiveResourceProfile('skills', listPluginSkillNames({ plugins: activePlugins }), sourceMapFromPluginSkills(allPlugins, activePlugins, cwd));
281
+ for (const name of pluginSkillNames) {
282
+ if (!skillNames.has(name))
283
+ result.skills.push(name);
284
+ }
234
285
  // Promptcuts — present if either layer exists. Reads merge user + system
235
286
  // with user precedence (see readMergedPromptcuts); writes always go to user.
236
287
  result.promptcuts = fs.existsSync(getUserPromptcutsPath()) || fs.existsSync(getPromptcutsPath());
@@ -1490,16 +1541,23 @@ export async function installVersion(agent, version, onProgress, opts) {
1490
1541
  return { success: false, installedVersion: version, error: err.message };
1491
1542
  }
1492
1543
  }
1544
+ // Version-dir entries that are STATE, not install output, and so must survive a
1545
+ // clean reinstall: `home/`, and the `.isolated` marker that is the single source
1546
+ // of truth for "this copy is walled off". Losing the marker would silently demote
1547
+ // an isolated copy to a normal one on its first repair — after which shim
1548
+ // self-heal would hand it a bare `<agent>` shim and a PATH entry.
1549
+ const PRESERVED_ON_CLEAN_REINSTALL = new Set(['home', '.isolated']);
1493
1550
  /**
1494
1551
  * Remove install artifacts from a version directory, preserving `home/` which
1495
1552
  * contains the user's conversation history, sessions, history.jsonl, tasks,
1496
- * todos, file-history, etc. Used by the install pipeline (NOT by removeVersion)
1497
- * to clean up staging artifacts when a fresh install collides with an existing
1498
- * dir. removeVersion uses soft-delete instead.
1553
+ * todos, file-history, etc. (and the `.isolated` marker see
1554
+ * PRESERVED_ON_CLEAN_REINSTALL). Used by the install pipeline (NOT by
1555
+ * removeVersion) to clean up staging artifacts when a fresh install collides
1556
+ * with an existing dir. removeVersion uses soft-delete instead.
1499
1557
  */
1500
1558
  function removeInstallArtifacts(versionDir) {
1501
1559
  for (const entry of fs.readdirSync(versionDir)) {
1502
- if (entry === 'home')
1560
+ if (PRESERVED_ON_CLEAN_REINSTALL.has(entry))
1503
1561
  continue;
1504
1562
  fs.rmSync(path.join(versionDir, entry), { recursive: true, force: true });
1505
1563
  }
@@ -1611,9 +1669,12 @@ async function reconcileGlobalBinaryVersions(agent) {
1611
1669
  }
1612
1670
  if (foldedAny) {
1613
1671
  invalidateInstalledVersionsCache(agent);
1614
- // Keep the recorded default pointing at a dir that still exists on disk.
1672
+ // Keep the recorded default pointing at a dir that still exists on disk
1673
+ // but never promote an isolated copy into the default slot (same rule as the
1674
+ // post-removal promotion filter and `agents use`). Leaving it unset is right:
1675
+ // an isolated-only agent has no default by design.
1615
1676
  const def = getGlobalDefault(agent);
1616
- if (!def || !fs.existsSync(getVersionDir(agent, def))) {
1677
+ if ((!def || !fs.existsSync(getVersionDir(agent, def))) && !isVersionIsolated(agent, survivor)) {
1617
1678
  setGlobalDefault(agent, survivor);
1618
1679
  }
1619
1680
  }
@@ -2014,6 +2075,17 @@ export async function verifyInstalledBinaryLaunches(agent, version) {
2014
2075
  * 4. Nothing runnable installed → install `latest`, pin it, return it.
2015
2076
  * 5. Give up → return null (caller surfaces a clear error).
2016
2077
  *
2078
+ * Isolation boundary (both directions — steps 3/4 are the only mutating ones):
2079
+ * - An ISOLATED target gets step 2 and nothing else. Falling back would let
2080
+ * `agents run <agent>@<isolated>` silently repoint the user's NORMAL default,
2081
+ * and installing `latest` would materialize a version they never asked for.
2082
+ * A failed repair returns null so the caller says so plainly.
2083
+ * - An isolated version is never a fallback CANDIDATE either, whatever the
2084
+ * target is: promoting one to the global default is exactly what `agents use`
2085
+ * refuses and what removeVersion's promotion filter excludes, so it must not
2086
+ * happen here by the back door (the daemon's launch-health pass calls this
2087
+ * unattended every ~6h).
2088
+ *
2017
2089
  * Gated to npm-package agents: their native binary ships as an optional per-arch
2018
2090
  * dependency whose tarball can extract partially (interrupted/raced install) —
2019
2091
  * the exact failure this repairs. Agents with a global/native binary (grok,
@@ -2025,14 +2097,28 @@ export async function ensureAgentRunnable(agent, version, log) {
2025
2097
  return version;
2026
2098
  if ((await verifyInstalledBinaryLaunches(agent, version)).ok)
2027
2099
  return version;
2100
+ // Read the marker BEFORE the repair: the reinstall rewrites the version dir,
2101
+ // so isolation must be decided from the pre-repair state, not re-read after.
2102
+ const targetIsolated = isVersionIsolated(agent, version);
2028
2103
  log?.(`${cfg.name}@${version} is broken (platform binary missing) — repairing…`);
2029
2104
  const repair = await installVersion(agent, version, undefined, { clean: true });
2030
2105
  if (repair.success && (await verifyInstalledBinaryLaunches(agent, version)).ok) {
2031
2106
  log?.(`repaired ${cfg.name}@${version}.`);
2032
2107
  return version;
2033
2108
  }
2109
+ // An isolated copy is walled off from the rest of the setup: repairing it in
2110
+ // place is the ONLY thing we may do. No fallback, no install, no default
2111
+ // switch — surface the failure and let the caller tell the user.
2112
+ if (targetIsolated) {
2113
+ log?.(`${cfg.name}@${version} is an isolated install and could not be repaired — leaving your default ${cfg.name} untouched.`);
2114
+ return null;
2115
+ }
2034
2116
  // In-place repair failed → adopt another installed version that launches.
2035
- const others = listInstalledVersions(agent).filter(v => v !== version).sort(compareVersions).reverse();
2117
+ // Isolated copies are excluded: they are deliberately not promotable.
2118
+ const others = listInstalledVersions(agent)
2119
+ .filter(v => v !== version && !isVersionIsolated(agent, v))
2120
+ .sort(compareVersions)
2121
+ .reverse();
2036
2122
  for (const cand of others) {
2037
2123
  if ((await verifyInstalledBinaryLaunches(agent, cand)).ok) {
2038
2124
  setGlobalDefault(agent, cand);
@@ -2041,9 +2127,28 @@ export async function ensureAgentRunnable(agent, version, log) {
2041
2127
  }
2042
2128
  }
2043
2129
  // Nothing runnable installed → last resort: install latest and pin it.
2130
+ //
2131
+ // …unless `latest` is a version the user already holds as an ISOLATED copy. A
2132
+ // normal and an isolated install of the same version share one on-disk dir
2133
+ // (see the coexistence refusal in commands/versions.ts), so installing would
2134
+ // commandeer that copy and pinning it would hand an isolated install the
2135
+ // global default — the same breach the candidate filter above prevents.
2136
+ // Resolved BEFORE installing so the isolated copy is not even rebuilt.
2137
+ const latestVersion = await getLatestNpmVersion(agent);
2138
+ if (latestVersion && isVersionIsolated(agent, latestVersion)) {
2139
+ log?.(`no runnable ${cfg.name} version installed — ${cfg.name}@${latestVersion} is the latest, but you hold it as an isolated copy, so it can't become your default.`);
2140
+ log?.(`install one explicitly: agents add ${agent}@latest`);
2141
+ return null;
2142
+ }
2044
2143
  log?.(`no runnable ${cfg.name} version installed — installing ${cfg.name}@latest…`);
2045
2144
  const latest = await installVersion(agent, 'latest', undefined, { clean: true });
2046
2145
  if (latest.success) {
2146
+ // Belt-and-braces: `latest` could have moved between the probe above and the
2147
+ // install. Never pin an isolated version, whatever the resolution said.
2148
+ if (isVersionIsolated(agent, latest.installedVersion)) {
2149
+ log?.(`${cfg.name}@${latest.installedVersion} is an isolated copy and can't be set as your default.`);
2150
+ return null;
2151
+ }
2047
2152
  setGlobalDefault(agent, latest.installedVersion);
2048
2153
  log?.(`installed ${cfg.name}@${latest.installedVersion} and set it as the default.`);
2049
2154
  return latest.installedVersion;
@@ -2119,9 +2224,10 @@ export function listRepoNames() {
2119
2224
  * `expandPatterns` matches `source:*` patterns against. This is the single
2120
2225
  * source of truth for how each kind attributes its source layer:
2121
2226
  * - commands/skills/hooks/subagents → real layer from `listResources`
2122
- * - permissions → always the system repo
2227
+ * - permissions → real layer from permissions/groups
2123
2228
  * - mcp → project vs user scope preserved
2124
- * - plugins/workflows → user repo
2229
+ * - plugins → user repo
2230
+ * - workflows → real layer from workflow directories
2125
2231
  * Both the persisted-pattern sync path and `buildRepoScopedSelection` use it
2126
2232
  * so the attribution can't drift between the two.
2127
2233
  */
@@ -2132,14 +2238,24 @@ function resourceSourceMap(kind, cwd, available) {
2132
2238
  case 'hooks':
2133
2239
  case 'subagents':
2134
2240
  return new Map(listResources(kind, cwd).map(r => [r.name, r.source]));
2135
- case 'permissions':
2136
- return new Map(available.permissions.map(n => [n, 'system']));
2241
+ case 'permissions': {
2242
+ const sources = sourceMapFromPermissionGroups(cwd);
2243
+ return new Map(available.permissions.flatMap((n) => {
2244
+ const source = sources.get(n);
2245
+ return source ? [[n, source]] : [];
2246
+ }));
2247
+ }
2137
2248
  case 'mcp':
2138
2249
  return new Map(getScopedMcpResources(cwd).map(r => [r.name, r.scope]));
2139
2250
  case 'plugins':
2140
2251
  return new Map(available.plugins.map(n => [n, 'user']));
2141
- case 'workflows':
2142
- return new Map(available.workflows.map(n => [n, 'user']));
2252
+ case 'workflows': {
2253
+ const sources = sourceMapFromWorkflows(cwd);
2254
+ return new Map(available.workflows.flatMap((n) => {
2255
+ const source = sources.get(n);
2256
+ return source ? [[n, source]] : [];
2257
+ }));
2258
+ }
2143
2259
  }
2144
2260
  }
2145
2261
  /**
@@ -2239,11 +2355,10 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2239
2355
  // only sets fields that aren't already present, preserving user edits).
2240
2356
  {
2241
2357
  const extraAliases = extraRepos.map(e => e.alias);
2242
- const allLayers = defaultPatterns(extraAliases);
2243
2358
  const noProject = defaultPatterns(extraAliases, false);
2244
2359
  ensureVersionResourcePatterns(agent, version, {
2245
- commands: allLayers,
2246
- skills: allLayers,
2360
+ commands: noProject,
2361
+ skills: noProject,
2247
2362
  hooks: noProject, // hooks: no project layer (security)
2248
2363
  subagents: noProject,
2249
2364
  plugins: noProject,
@@ -2295,6 +2410,9 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2295
2410
  }
2296
2411
  }
2297
2412
  }
2413
+ if (projectAgentsDir) {
2414
+ syncProjectResourcesToAgent(agent, version, projectAgentsDir);
2415
+ }
2298
2416
  // Fast guard: skip the entire sync when the caller requested a full sync and
2299
2417
  // nothing has changed since the last full sync. Pattern-derived selections
2300
2418
  // still count as full syncs because they are the persisted intended scope,
@@ -2347,6 +2465,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2347
2465
  }
2348
2466
  return [];
2349
2467
  };
2468
+ const trustedCommandNames = (names) => names.filter((name) => resolveCommandSource(name) !== null);
2350
2469
  // Sync commands — dispatch through WRITERS.commands. The writer dispatches
2351
2470
  // between native (file copy / TOML conversion) and commands-as-skills
2352
2471
  // (grok, Codex >= 0.117.0) based on `shouldInstallCommandAsSkill`. The
@@ -2354,8 +2473,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2354
2473
  // takes the commands-as-skills path — silently dropping every command.
2355
2474
  const commandsWriter = getWriter('commands', agent);
2356
2475
  const commandsToSync = selection
2357
- ? resolveSelection(selection.commands, available.commands)
2358
- : available.commands; // No selection = sync all
2476
+ ? trustedCommandNames(resolveSelection(selection.commands, available.commands))
2477
+ : trustedCommandNames(available.commands); // No selection = sync all trusted commands, excluding project-only commands
2359
2478
  const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
2360
2479
  if (commandsToSync.length > 0 && commandsWriter) {
2361
2480
  const commandsTarget = path.join(agentDir, agentConfig.commandsSubdir);
@@ -2393,9 +2512,18 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2393
2512
  // ~/.agents/skills/ (Gemini) are not registered; we clear the version-home
2394
2513
  // skills dir for them so a stale per-version copy never shadows central.
2395
2514
  const skillsWriter = getWriter('skills', agent);
2396
- let skillsToSync = selection
2397
- ? resolveSelection(selection.skills, available.skills)
2398
- : available.skills;
2515
+ const pluginsWriter = getWriter('plugins', agent);
2516
+ const pluginsToSync = selection
2517
+ ? resolveSelection(selection.plugins, available.plugins)
2518
+ : (pluginsWriter ? available.plugins : []);
2519
+ const pluginSkillsToSync = listPluginSkillNames({ agent, plugins: new Set(pluginsToSync) });
2520
+ const trustedSkillNames = (names) => names.filter((name) => resolveSkillSource(name, { agent, plugins: new Set(pluginsToSync) }) !== null);
2521
+ const selectedSkillsToSync = selection
2522
+ ? trustedSkillNames(resolveSelection(selection.skills, available.skills))
2523
+ : trustedSkillNames(available.skills);
2524
+ let skillsToSync = userPassedSelection
2525
+ ? selectedSkillsToSync
2526
+ : Array.from(new Set([...selectedSkillsToSync, ...pluginSkillsToSync]));
2399
2527
  if (commandsAsSkills && commandsToSync.length > 0 && skillsToSync.length > 0) {
2400
2528
  const commandNames = new Set(commandsToSync);
2401
2529
  const skillRoots = [
@@ -2488,7 +2616,7 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2488
2616
  const overridePreset = Array.isArray(selection?.memory) && selection.memory.length === 1 && selection.memory[0] !== 'AGENTS'
2489
2617
  ? selection.memory[0]
2490
2618
  : null;
2491
- const preset = overridePreset || getActiveRulesPreset(agent, version);
2619
+ const preset = overridePreset || activeRulesPreset() || getActiveRulesPreset(agent, version);
2492
2620
  const r = rulesWriter.write({ version, versionHome, selection: { preset }, cwd });
2493
2621
  result.memory.push(...r.synced);
2494
2622
  // rulesPreset is tracked separately via setActiveRulesPreset.
@@ -2575,9 +2703,11 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2575
2703
  // as commands/skills/hooks).
2576
2704
  const subagentsWriter = getWriter('subagents', agent);
2577
2705
  const subagentsGate = supports(agent, 'subagents', version);
2706
+ const installedSubagentNames = new Set(listInstalledSubagents().map((subagent) => subagent.name));
2707
+ const trustedSubagentNames = (names) => names.filter((name) => installedSubagentNames.has(name));
2578
2708
  const subagentsRequested = selection
2579
- ? resolveSelection(selection.subagents, available.subagents)
2580
- : (subagentsWriter ? available.subagents : []);
2709
+ ? trustedSubagentNames(resolveSelection(selection.subagents, available.subagents))
2710
+ : (subagentsWriter ? trustedSubagentNames(available.subagents) : []);
2581
2711
  const subagentsToSync = subagentsGate.ok ? subagentsRequested : [];
2582
2712
  if (subagentsRequested.length > 0 && !subagentsGate.ok) {
2583
2713
  console.warn(explainSkip(agent, 'subagents', subagentsGate, version) + ' -- skipped');
@@ -2606,10 +2736,6 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2606
2736
  }
2607
2737
  }
2608
2738
  // Sync plugins — dispatch through WRITERS.plugins.
2609
- const pluginsWriter = getWriter('plugins', agent);
2610
- const pluginsToSync = selection
2611
- ? resolveSelection(selection.plugins, available.plugins)
2612
- : (pluginsWriter ? available.plugins : []);
2613
2739
  if (pluginsToSync.length > 0 && pluginsWriter) {
2614
2740
  const r = pluginsWriter.write({ version, versionHome, selection: pluginsToSync, cwd });
2615
2741
  result.plugins.push(...r.synced);
@@ -2617,9 +2743,15 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
2617
2743
  // Sync workflows — dispatch through WRITERS.workflows.
2618
2744
  const workflowsWriter = getWriter('workflows', agent);
2619
2745
  const workflowsGate = supports(agent, 'workflows', version);
2746
+ const trustedWorkflowNames = (names) => {
2747
+ if (names.length === 0)
2748
+ return [];
2749
+ const installedWorkflowNames = new Set(listInstalledWorkflows().keys());
2750
+ return names.filter((name) => installedWorkflowNames.has(name));
2751
+ };
2620
2752
  const workflowsRequested = selection
2621
- ? resolveSelection(selection.workflows, available.workflows)
2622
- : (workflowsWriter ? available.workflows : []);
2753
+ ? trustedWorkflowNames(resolveSelection(selection.workflows, available.workflows))
2754
+ : (workflowsWriter ? trustedWorkflowNames(available.workflows) : []);
2623
2755
  const workflowsToSync = workflowsGate.ok ? workflowsRequested : [];
2624
2756
  if (workflowsRequested.length > 0 && !workflowsGate.ok) {
2625
2757
  console.warn(explainSkip(agent, 'workflows', workflowsGate, version) + ' -- skipped');
@@ -294,6 +294,8 @@ export declare function transformWorkflowForKimi(workflowPath: string, name: str
294
294
  * user-authored workflow of the same name.
295
295
  */
296
296
  export declare function transformWorkflowForAntigravity(workflowPath: string, name: string): string;
297
+ /** Convert a canonical agents-cli workflow bundle into an OpenClaw Lobster file. */
298
+ export declare function transformWorkflowForOpenClaw(workflowPath: string, name: string): string;
297
299
  /**
298
300
  * Resolve an `agents run <workflow>` reference.
299
301
  *
@@ -376,6 +376,7 @@ function containsFlowDiagram(content) {
376
376
  return /```(?:mermaid|d2)\b/i.test(content);
377
377
  }
378
378
  const KIMI_WORKFLOW_MARKER = 'agents_workflow';
379
+ const OPENCLAW_WORKFLOW_MARKER_ENV = 'AGENTS_CLI_WORKFLOW';
379
380
  /** Convert a canonical agents-cli workflow bundle into a Kimi flow skill. */
380
381
  export function transformWorkflowForKimi(workflowPath, name) {
381
382
  const fm = parseWorkflowFrontmatter(workflowPath);
@@ -414,6 +415,35 @@ export function transformWorkflowForAntigravity(workflowPath, name) {
414
415
  }).trim();
415
416
  return `---\n${frontmatter}\n---\n\n${body.trim()}\n`;
416
417
  }
418
+ /** Convert a canonical agents-cli workflow bundle into an OpenClaw Lobster file. */
419
+ export function transformWorkflowForOpenClaw(workflowPath, name) {
420
+ const fm = parseWorkflowFrontmatter(workflowPath);
421
+ if (!fm)
422
+ throw new Error(`Invalid WORKFLOW.md in ${workflowPath}`);
423
+ const body = getWorkflowBody(workflowPath) || fm.description || name;
424
+ return yaml.stringify({
425
+ name: fm.name || name,
426
+ args: {
427
+ agent: {
428
+ default: 'main',
429
+ },
430
+ prompt: {
431
+ default: '',
432
+ },
433
+ },
434
+ env: {
435
+ [OPENCLAW_WORKFLOW_MARKER_ENV]: name,
436
+ AGENTS_WORKFLOW_DESCRIPTION: fm.description || name,
437
+ AGENTS_WORKFLOW_BODY: body.trim(),
438
+ },
439
+ steps: [
440
+ {
441
+ id: 'run_openclaw',
442
+ command: 'openclaw agent --agent "$LOBSTER_ARG_AGENT" --message "$(printf \'%s\\n\\n%s\\n\' "$AGENTS_WORKFLOW_BODY" "$LOBSTER_ARG_PROMPT")"',
443
+ },
444
+ ],
445
+ });
446
+ }
417
447
  function expandWorkflowPath(ref) {
418
448
  if (ref === '~')
419
449
  return process.env.HOME ?? ref;
@@ -590,6 +620,8 @@ function workflowTargetRoot(agent, versionHome) {
590
620
  return path.join(versionHome, '.kimi-code', 'skills');
591
621
  if (agent === 'antigravity')
592
622
  return antigravityWorkflowsDir();
623
+ if (agent === 'openclaw')
624
+ return path.join(versionHome, '.openclaw', 'workflows');
593
625
  return path.join(versionHome, 'workflows');
594
626
  }
595
627
  /** List workflow names synced into a specific agent version home. */
@@ -630,6 +662,20 @@ export function listWorkflowsForAgent(agent, versionHome) {
630
662
  return [];
631
663
  }
632
664
  }
665
+ if (agent === 'openclaw') {
666
+ const dir = workflowTargetRoot(agent, versionHome);
667
+ if (!fs.existsSync(dir))
668
+ return [];
669
+ try {
670
+ return fs.readdirSync(dir, { withFileTypes: true })
671
+ .filter(d => d.isFile() && d.name.endsWith('.lobster') && !d.name.startsWith('.'))
672
+ .map(d => d.name.slice(0, -'.lobster'.length))
673
+ .filter(base => openclawWorkflowMarker(path.join(dir, `${base}.lobster`)) === base);
674
+ }
675
+ catch {
676
+ return [];
677
+ }
678
+ }
633
679
  const workflowsDir = workflowTargetRoot(agent, versionHome);
634
680
  if (!fs.existsSync(workflowsDir))
635
681
  return [];
@@ -764,6 +810,19 @@ export function syncWorkflowToVersion(workflowPath, name, agent, versionHome) {
764
810
  fs.writeFileSync(targetFile, transformWorkflowForAntigravity(workflowPath, name), 'utf-8');
765
811
  return { success: true };
766
812
  }
813
+ if (agent === 'openclaw') {
814
+ const targetDir = workflowTargetRoot(agent, versionHome);
815
+ const targetFile = path.join(targetDir, `${name}.lobster`);
816
+ if (fs.existsSync(targetFile)) {
817
+ const marker = openclawWorkflowMarker(targetFile);
818
+ if (marker !== name) {
819
+ return { success: false, error: `OpenClaw workflow '${name}' already exists and is not managed by agents-cli` };
820
+ }
821
+ }
822
+ fs.mkdirSync(targetDir, { recursive: true });
823
+ fs.writeFileSync(targetFile, transformWorkflowForOpenClaw(workflowPath, name), 'utf-8');
824
+ return { success: true };
825
+ }
767
826
  const targetDir = path.join(workflowTargetRoot(agent, versionHome), name);
768
827
  fs.mkdirSync(workflowTargetRoot(agent, versionHome), { recursive: true });
769
828
  if (fs.existsSync(targetDir)) {
@@ -812,6 +871,22 @@ export function removeWorkflowFromVersion(agent, version, name) {
812
871
  return { success: false, error: err.message };
813
872
  }
814
873
  }
874
+ if (agent === 'openclaw') {
875
+ const targetFile = path.join(workflowTargetRoot(agent, versionHome), `${name}.lobster`);
876
+ if (!fs.existsSync(targetFile)) {
877
+ return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
878
+ }
879
+ if (openclawWorkflowMarker(targetFile) !== name) {
880
+ return { success: false, error: `OpenClaw workflow '${name}' is not managed by agents-cli` };
881
+ }
882
+ try {
883
+ fs.rmSync(targetFile, { force: true });
884
+ return { success: true };
885
+ }
886
+ catch (err) {
887
+ return { success: false, error: err.message };
888
+ }
889
+ }
815
890
  const targetPath = path.join(workflowTargetRoot(agent, versionHome), name);
816
891
  if (!fs.existsSync(targetPath)) {
817
892
  return { success: false, error: `Workflow '${name}' not synced to ${agent}@${version}` };
@@ -860,6 +935,19 @@ function antigravityWorkflowMarker(filePath) {
860
935
  return null;
861
936
  }
862
937
  }
938
+ function openclawWorkflowMarker(filePath) {
939
+ try {
940
+ const parsed = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
941
+ if (!parsed || typeof parsed !== 'object' || !parsed.env || typeof parsed.env !== 'object' || Array.isArray(parsed.env)) {
942
+ return null;
943
+ }
944
+ const marker = parsed.env[OPENCLAW_WORKFLOW_MARKER_ENV];
945
+ return typeof marker === 'string' ? marker : null;
946
+ }
947
+ catch {
948
+ return null;
949
+ }
950
+ }
863
951
  /** Iterate all installed (agent, version) pairs that support workflows. */
864
952
  export function iterWorkflowsCapableVersions(filter) {
865
953
  const result = [];