agentloom 0.1.0 → 0.1.2

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 (53) hide show
  1. package/README.md +91 -72
  2. package/bin/cli.mjs +3 -2
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +149 -17
  5. package/dist/commands/add.d.ts +7 -0
  6. package/dist/commands/add.js +122 -31
  7. package/dist/commands/agent.d.ts +2 -0
  8. package/dist/commands/agent.js +85 -0
  9. package/dist/commands/command.d.ts +2 -0
  10. package/dist/commands/command.js +98 -0
  11. package/dist/commands/delete.d.ts +9 -0
  12. package/dist/commands/delete.js +444 -0
  13. package/dist/commands/entity-utils.d.ts +13 -0
  14. package/dist/commands/entity-utils.js +58 -0
  15. package/dist/commands/find.d.ts +21 -0
  16. package/dist/commands/find.js +944 -0
  17. package/dist/commands/mcp.js +133 -55
  18. package/dist/commands/skills.d.ts +2 -1
  19. package/dist/commands/skills.js +105 -9
  20. package/dist/commands/sync.d.ts +6 -0
  21. package/dist/commands/sync.js +12 -10
  22. package/dist/commands/update.d.ts +7 -0
  23. package/dist/commands/update.js +286 -21
  24. package/dist/core/argv.d.ts +2 -1
  25. package/dist/core/argv.js +42 -2
  26. package/dist/core/commands.d.ts +13 -0
  27. package/dist/core/commands.js +65 -0
  28. package/dist/core/copy.d.ts +6 -0
  29. package/dist/core/copy.js +126 -65
  30. package/dist/core/importer.d.ts +28 -1
  31. package/dist/core/importer.js +1104 -41
  32. package/dist/core/lockfile.js +86 -3
  33. package/dist/core/manage-agents-bootstrap.d.ts +10 -0
  34. package/dist/core/manage-agents-bootstrap.js +40 -0
  35. package/dist/core/manifest.js +7 -1
  36. package/dist/core/router.d.ts +16 -0
  37. package/dist/core/router.js +66 -0
  38. package/dist/core/scope.d.ts +1 -1
  39. package/dist/core/scope.js +12 -8
  40. package/dist/core/settings.d.ts +4 -3
  41. package/dist/core/settings.js +10 -8
  42. package/dist/core/skills.d.ts +23 -0
  43. package/dist/core/skills.js +328 -0
  44. package/dist/core/sources.d.ts +3 -1
  45. package/dist/core/sources.js +31 -1
  46. package/dist/core/telemetry.d.ts +26 -0
  47. package/dist/core/telemetry.js +124 -0
  48. package/dist/core/version-notifier.d.ts +1 -0
  49. package/dist/core/version-notifier.js +22 -4
  50. package/dist/sync/index.d.ts +7 -1
  51. package/dist/sync/index.js +395 -131
  52. package/dist/types.d.ts +16 -1
  53. package/package.json +5 -4
@@ -1,49 +1,127 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { confirm, isCancel } from "@clack/prompts";
4
+ import { cancel, isCancel, multiselect } from "@clack/prompts";
5
5
  import TOML from "@iarna/toml";
6
6
  import YAML from "yaml";
7
+ import { ALL_PROVIDERS } from "../types.js";
7
8
  import { getProviderConfig, isProviderEnabled, parseAgentsDir, } from "../core/agents.js";
9
+ import { parseCommandsDir } from "../core/commands.js";
8
10
  import { ensureDir, isObject, readJsonIfExists, relativePosix, removeFileIfExists, slugify, toPosixPath, writeJsonAtomic, writeTextAtomic, } from "../core/fs.js";
9
11
  import { readManifest, writeManifest } from "../core/manifest.js";
10
12
  import { readCanonicalMcp, resolveMcpForProvider } from "../core/mcp.js";
11
- import { getGlobalSettingsPath, readSettings, updateLastScope, } from "../core/settings.js";
13
+ import { getGlobalSettingsPath, readSettings, updateLastScope, updateLastScopeBestEffort, } from "../core/settings.js";
14
+ export async function resolveProvidersForSync(options) {
15
+ const settings = readSettings(options.paths.settingsPath);
16
+ return resolveProviders({
17
+ explicitProviders: options.explicitProviders,
18
+ settings,
19
+ nonInteractive: options.nonInteractive,
20
+ });
21
+ }
12
22
  export async function syncFromCanonical(options) {
13
23
  const agents = parseAgentsDir(options.paths.agentsDir);
24
+ const commands = parseCommandsDir(options.paths.commandsDir);
14
25
  const mcp = readCanonicalMcp(options.paths);
15
26
  const manifest = readManifest(options.paths);
16
- const settings = readSettings(options.paths.settingsPath);
17
- const providers = resolveProviders(options.providers, settings);
27
+ const effectiveManifest = {
28
+ ...manifest,
29
+ generatedByEntity: normalizeGeneratedByEntity(manifest),
30
+ };
31
+ const providers = await resolveProvidersForSync({
32
+ paths: options.paths,
33
+ explicitProviders: options.providers,
34
+ nonInteractive: options.nonInteractive,
35
+ });
36
+ const target = options.target ?? "all";
18
37
  const nextManifest = {
19
38
  version: 1,
20
39
  generatedFiles: [],
40
+ generatedByEntity: {},
21
41
  codex: {
22
- roles: [],
23
- mcpServers: [],
42
+ roles: [...(effectiveManifest.codex?.roles ?? [])],
43
+ mcpServers: [...(effectiveManifest.codex?.mcpServers ?? [])],
24
44
  },
25
45
  };
26
- const generated = new Set();
27
- for (const provider of providers) {
28
- syncProviderAgents({
29
- provider,
46
+ const generatedAgents = new Set();
47
+ const generatedCommands = new Set();
48
+ const generatedMcp = new Set();
49
+ if (target === "all" || target === "agent") {
50
+ for (const provider of providers) {
51
+ syncProviderAgents({
52
+ provider,
53
+ paths: options.paths,
54
+ agents,
55
+ generated: generatedAgents,
56
+ dryRun: !!options.dryRun,
57
+ });
58
+ }
59
+ }
60
+ if (target === "all" || target === "command") {
61
+ for (const provider of providers) {
62
+ syncProviderCommands({
63
+ provider,
64
+ paths: options.paths,
65
+ commands,
66
+ generated: generatedCommands,
67
+ dryRun: !!options.dryRun,
68
+ });
69
+ }
70
+ }
71
+ if (target === "all" || target === "mcp") {
72
+ syncProviderMcp({
73
+ providers,
30
74
  paths: options.paths,
31
- agents,
32
- generated,
75
+ mcp,
76
+ generated: generatedMcp,
33
77
  dryRun: !!options.dryRun,
34
78
  });
35
79
  }
36
- syncProviderMcp({
37
- providers,
38
- paths: options.paths,
39
- agents,
40
- mcp,
41
- generated,
42
- manifest,
43
- nextManifest,
44
- dryRun: !!options.dryRun,
45
- });
46
- nextManifest.generatedFiles = [...generated].sort();
80
+ if (providers.includes("codex")) {
81
+ const includeRoles = target === "all" || target === "agent";
82
+ const includeMcp = target === "all" || target === "mcp";
83
+ if (includeRoles || includeMcp) {
84
+ syncCodex({
85
+ paths: options.paths,
86
+ agents,
87
+ resolvedMcp: resolveMcpForProvider(mcp, "codex"),
88
+ generated: includeRoles ? generatedAgents : generatedMcp,
89
+ manifest: effectiveManifest,
90
+ nextManifest,
91
+ dryRun: !!options.dryRun,
92
+ includeRoles,
93
+ includeMcp,
94
+ });
95
+ }
96
+ else {
97
+ nextManifest.codex = {
98
+ roles: [...(effectiveManifest.codex?.roles ?? [])],
99
+ mcpServers: [...(effectiveManifest.codex?.mcpServers ?? [])],
100
+ };
101
+ }
102
+ }
103
+ const previousByEntity = normalizeGeneratedByEntity(effectiveManifest);
104
+ const nextByEntity = {
105
+ ...previousByEntity,
106
+ };
107
+ if (target === "all" || target === "agent") {
108
+ nextByEntity.agent = [...generatedAgents].sort();
109
+ }
110
+ if (target === "all" || target === "command") {
111
+ nextByEntity.command = [...generatedCommands].sort();
112
+ }
113
+ if (target === "all" || target === "mcp") {
114
+ nextByEntity.mcp = [...generatedMcp].sort();
115
+ }
116
+ nextManifest.generatedByEntity = pruneGeneratedByEntity(nextByEntity);
117
+ nextManifest.generatedFiles = [
118
+ ...new Set([
119
+ ...(nextManifest.generatedByEntity.agent ?? []),
120
+ ...(nextManifest.generatedByEntity.command ?? []),
121
+ ...(nextManifest.generatedByEntity.mcp ?? []),
122
+ ...(nextManifest.generatedByEntity.skill ?? []),
123
+ ]),
124
+ ].sort();
47
125
  const removedFiles = await removeStaleGeneratedFiles({
48
126
  oldManifest: manifest,
49
127
  newManifest: nextManifest,
@@ -54,7 +132,10 @@ export async function syncFromCanonical(options) {
54
132
  if (!options.dryRun) {
55
133
  writeManifest(options.paths, nextManifest);
56
134
  updateLastScope(options.paths.settingsPath, options.paths.scope, providers);
57
- updateLastScope(getGlobalSettingsPath(options.paths.homeDir), options.paths.scope);
135
+ const globalSettingsPath = getGlobalSettingsPath(options.paths.homeDir);
136
+ if (options.paths.settingsPath !== globalSettingsPath) {
137
+ updateLastScopeBestEffort(globalSettingsPath, options.paths.scope, providers);
138
+ }
58
139
  }
59
140
  return {
60
141
  providers,
@@ -62,14 +143,56 @@ export async function syncFromCanonical(options) {
62
143
  removedFiles,
63
144
  };
64
145
  }
65
- function resolveProviders(explicitProviders, settings) {
66
- if (explicitProviders && explicitProviders.length > 0) {
67
- return [...new Set(explicitProviders)];
146
+ const PROVIDER_LABELS = {
147
+ cursor: "Cursor",
148
+ claude: "Claude",
149
+ codex: "Codex",
150
+ opencode: "OpenCode",
151
+ gemini: "Gemini",
152
+ copilot: "Copilot",
153
+ };
154
+ const MULTISELECT_HELP_TEXT = "↑↓ move, space select, enter confirm";
155
+ function withMultiselectHelp(message) {
156
+ return `${message}\n${MULTISELECT_HELP_TEXT}`;
157
+ }
158
+ async function resolveProviders(options) {
159
+ if (options.explicitProviders && options.explicitProviders.length > 0) {
160
+ return normalizeProviderSelection(options.explicitProviders);
68
161
  }
69
- if (settings.defaultProviders && settings.defaultProviders.length > 0) {
70
- return [...new Set(settings.defaultProviders)];
162
+ const defaults = normalizeProviderSelection(options.settings.defaultProviders);
163
+ const initialSelection = defaults.length > 0 ? defaults : [...ALL_PROVIDERS];
164
+ const nonInteractive = options.nonInteractive ?? !(process.stdin.isTTY && process.stdout.isTTY);
165
+ if (nonInteractive) {
166
+ return initialSelection;
71
167
  }
72
- return ["cursor", "claude", "codex", "opencode", "gemini", "copilot"];
168
+ const selected = await multiselect({
169
+ message: withMultiselectHelp("Select providers to sync"),
170
+ options: ALL_PROVIDERS.map((provider) => ({
171
+ value: provider,
172
+ label: PROVIDER_LABELS[provider],
173
+ })),
174
+ initialValues: initialSelection,
175
+ required: true,
176
+ });
177
+ if (isCancel(selected)) {
178
+ cancel("Operation cancelled.");
179
+ process.exit(1);
180
+ }
181
+ const normalized = normalizeProviderSelection(Array.isArray(selected) ? selected : []);
182
+ if (normalized.length === 0) {
183
+ throw new Error("At least one provider must be selected.");
184
+ }
185
+ return normalized;
186
+ }
187
+ function normalizeProviderSelection(providers) {
188
+ const selected = new Set();
189
+ for (const provider of providers ?? []) {
190
+ const normalized = provider.trim().toLowerCase();
191
+ if (ALL_PROVIDERS.includes(normalized)) {
192
+ selected.add(normalized);
193
+ }
194
+ }
195
+ return [...selected];
73
196
  }
74
197
  function syncProviderAgents(options) {
75
198
  const providerDir = getProviderAgentsDir(options.paths, options.provider);
@@ -84,9 +207,7 @@ function syncProviderAgents(options) {
84
207
  }
85
208
  const fileName = options.provider === "copilot"
86
209
  ? `${slugify(agent.name) || "agent"}.agent.md`
87
- : options.provider === "cursor"
88
- ? `${slugify(agent.name) || "agent"}.mdc`
89
- : `${slugify(agent.name) || "agent"}.md`;
210
+ : `${slugify(agent.name) || "agent"}.md`;
90
211
  const outputPath = path.join(providerDir, fileName);
91
212
  const content = buildProviderAgentContent(options.provider, agent, providerConfig ?? {});
92
213
  if (!options.dryRun) {
@@ -97,15 +218,6 @@ function syncProviderAgents(options) {
97
218
  }
98
219
  }
99
220
  function buildProviderAgentContent(provider, agent, providerConfig) {
100
- if (provider === "cursor") {
101
- const frontmatter = {
102
- description: agent.description,
103
- alwaysApply: false,
104
- ...providerConfig,
105
- };
106
- const fm = YAML.stringify(frontmatter).trimEnd();
107
- return `---\n${fm}\n---\n\n${agent.body.trimStart()}${agent.body.endsWith("\n") ? "" : "\n"}`;
108
- }
109
221
  const frontmatter = {
110
222
  name: agent.name,
111
223
  description: agent.description,
@@ -120,8 +232,8 @@ function getProviderAgentsDir(paths, provider) {
120
232
  switch (provider) {
121
233
  case "cursor":
122
234
  return paths.scope === "local"
123
- ? path.join(workspaceRoot, ".cursor", "rules")
124
- : path.join(home, ".cursor", "rules");
235
+ ? path.join(workspaceRoot, ".cursor", "agents")
236
+ : path.join(home, ".cursor", "agents");
125
237
  case "claude":
126
238
  return paths.scope === "local"
127
239
  ? path.join(workspaceRoot, ".claude", "agents")
@@ -146,8 +258,74 @@ function getProviderAgentsDir(paths, provider) {
146
258
  return path.join(workspaceRoot, ".agents", "unknown");
147
259
  }
148
260
  }
261
+ function syncProviderCommands(options) {
262
+ const providerDir = getProviderCommandsDir(options.paths, options.provider);
263
+ for (const command of options.commands) {
264
+ const fileName = mapProviderCommandFileName(options.provider, command.fileName);
265
+ const outputPath = path.join(providerDir, fileName);
266
+ if (!options.dryRun) {
267
+ ensureDir(path.dirname(outputPath));
268
+ writeTextAtomic(outputPath, command.content);
269
+ }
270
+ options.generated.add(outputPath);
271
+ }
272
+ }
273
+ function mapProviderCommandFileName(provider, fileName) {
274
+ const lower = fileName.toLowerCase();
275
+ if (provider === "copilot") {
276
+ if (lower.endsWith(".prompt.md"))
277
+ return fileName;
278
+ if (lower.endsWith(".md")) {
279
+ return `${fileName.slice(0, -3)}.prompt.md`;
280
+ }
281
+ if (lower.endsWith(".mdc")) {
282
+ return `${fileName.slice(0, -4)}.prompt.md`;
283
+ }
284
+ const ext = path.extname(fileName);
285
+ if (ext) {
286
+ return `${fileName.slice(0, -ext.length)}.prompt.md`;
287
+ }
288
+ return `${fileName}.prompt.md`;
289
+ }
290
+ if (lower.endsWith(".mdc")) {
291
+ return `${fileName.slice(0, -4)}.md`;
292
+ }
293
+ return fileName;
294
+ }
295
+ function getProviderCommandsDir(paths, provider) {
296
+ const workspaceRoot = paths.workspaceRoot;
297
+ const home = paths.homeDir;
298
+ switch (provider) {
299
+ case "cursor":
300
+ return paths.scope === "local"
301
+ ? path.join(workspaceRoot, ".cursor", "commands")
302
+ : path.join(home, ".cursor", "commands");
303
+ case "claude":
304
+ return paths.scope === "local"
305
+ ? path.join(workspaceRoot, ".claude", "commands")
306
+ : path.join(home, ".claude", "commands");
307
+ case "codex":
308
+ return path.join(home, ".codex", "prompts");
309
+ case "opencode":
310
+ return paths.scope === "local"
311
+ ? path.join(workspaceRoot, ".opencode", "commands")
312
+ : path.join(home, ".config", "opencode", "commands");
313
+ case "gemini":
314
+ return paths.scope === "local"
315
+ ? path.join(workspaceRoot, ".gemini", "commands")
316
+ : path.join(home, ".gemini", "commands");
317
+ case "copilot":
318
+ return paths.scope === "local"
319
+ ? path.join(workspaceRoot, ".github", "prompts")
320
+ : path.join(home, ".github", "prompts");
321
+ default:
322
+ return path.join(workspaceRoot, ".agents", "unknown", "commands");
323
+ }
324
+ }
149
325
  function syncProviderMcp(options) {
150
326
  for (const provider of options.providers) {
327
+ if (provider === "codex")
328
+ continue;
151
329
  const resolved = resolveMcpForProvider(options.mcp, provider);
152
330
  if (provider === "cursor") {
153
331
  const outputPath = options.paths.scope === "local"
@@ -187,18 +365,6 @@ function syncProviderMcp(options) {
187
365
  options.generated.add(settingsPath);
188
366
  continue;
189
367
  }
190
- if (provider === "codex") {
191
- syncCodex({
192
- paths: options.paths,
193
- agents: options.agents,
194
- resolvedMcp: resolved,
195
- generated: options.generated,
196
- manifest: options.manifest,
197
- nextManifest: options.nextManifest,
198
- dryRun: options.dryRun,
199
- });
200
- continue;
201
- }
202
368
  if (provider === "opencode") {
203
369
  const outputPath = options.paths.scope === "local"
204
370
  ? path.join(options.paths.workspaceRoot, ".opencode", "opencode.json")
@@ -310,63 +476,72 @@ function syncCodex(options) {
310
476
  features.multi_agent = true;
311
477
  parsed.features = features;
312
478
  const agentsTable = isObject(parsed.agents) ? { ...parsed.agents } : {};
313
- const previousRoles = new Set(options.manifest.codex?.roles ?? []);
314
- const nextRoles = [];
315
- const enabledCodexRoles = new Set(options.agents
316
- .filter((agent) => isProviderEnabled(agent.frontmatter, "codex"))
317
- .map((agent) => slugify(agent.name))
318
- .filter((role) => role.length > 0));
319
- for (const oldRole of previousRoles) {
320
- if (!enabledCodexRoles.has(oldRole)) {
321
- delete agentsTable[oldRole];
322
- }
323
- }
324
- for (const agent of options.agents) {
325
- if (!isProviderEnabled(agent.frontmatter, "codex"))
326
- continue;
327
- const codexConfig = getProviderConfig(agent.frontmatter, "codex") ?? {};
328
- const role = slugify(agent.name);
329
- if (!role)
330
- continue;
331
- const roleTomlPath = path.join(codexAgentsDir, `${role}.toml`);
332
- const roleInstructionsPath = path.join(codexAgentsDir, `${role}.instructions.md`);
333
- const roleToml = buildCodexRoleToml(roleInstructionsPath, codexConfig);
334
- if (!options.dryRun) {
335
- ensureDir(codexAgentsDir);
336
- writeTextAtomic(roleInstructionsPath, `${agent.body.trimStart()}\n`);
337
- writeTextAtomic(roleTomlPath, TOML.stringify(roleToml));
338
- }
339
- options.generated.add(roleTomlPath);
340
- options.generated.add(roleInstructionsPath);
341
- agentsTable[role] = {
342
- description: agent.description,
343
- config_file: `./agents/${role}.toml`,
344
- };
345
- nextRoles.push(role);
346
- }
347
- parsed.agents = agentsTable;
348
- const previousServers = new Set(options.manifest.codex?.mcpServers ?? []);
479
+ const trackedRoles = resolveTrackedCodexEntries(options.manifest.codex?.roles, Object.keys(agentsTable));
349
480
  const mcpServers = isObject(parsed.mcp_servers)
350
481
  ? { ...parsed.mcp_servers }
351
482
  : {};
352
- for (const oldServer of previousServers) {
353
- if (!Object.prototype.hasOwnProperty.call(options.resolvedMcp, oldServer)) {
354
- delete mcpServers[oldServer];
483
+ const trackedServers = resolveTrackedCodexEntries(options.manifest.codex?.mcpServers, Object.keys(mcpServers));
484
+ let nextRoles = [...trackedRoles];
485
+ if (options.includeRoles) {
486
+ const previousRoles = new Set(trackedRoles);
487
+ nextRoles = [];
488
+ const enabledCodexRoles = new Set(options.agents
489
+ .filter((agent) => isProviderEnabled(agent.frontmatter, "codex"))
490
+ .map((agent) => slugify(agent.name))
491
+ .filter((role) => role.length > 0));
492
+ for (const oldRole of previousRoles) {
493
+ if (!enabledCodexRoles.has(oldRole)) {
494
+ delete agentsTable[oldRole];
495
+ }
496
+ }
497
+ for (const agent of options.agents) {
498
+ if (!isProviderEnabled(agent.frontmatter, "codex"))
499
+ continue;
500
+ const codexConfig = getProviderConfig(agent.frontmatter, "codex") ?? {};
501
+ const role = slugify(agent.name);
502
+ if (!role)
503
+ continue;
504
+ const roleTomlPath = path.join(codexAgentsDir, `${role}.toml`);
505
+ const roleInstructionsPath = path.join(codexAgentsDir, `${role}.instructions.md`);
506
+ const roleToml = buildCodexRoleToml(roleInstructionsPath, codexConfig);
507
+ if (!options.dryRun) {
508
+ ensureDir(codexAgentsDir);
509
+ writeTextAtomic(roleInstructionsPath, `${agent.body.trimStart()}\n`);
510
+ writeTextAtomic(roleTomlPath, TOML.stringify(roleToml));
511
+ }
512
+ options.generated.add(roleTomlPath);
513
+ options.generated.add(roleInstructionsPath);
514
+ agentsTable[role] = {
515
+ description: agent.description,
516
+ config_file: `./agents/${role}.toml`,
517
+ };
518
+ nextRoles.push(role);
519
+ }
520
+ parsed.agents = agentsTable;
521
+ }
522
+ let nextServers = [...trackedServers];
523
+ if (options.includeMcp) {
524
+ const previousServers = new Set(trackedServers);
525
+ for (const oldServer of previousServers) {
526
+ if (!Object.prototype.hasOwnProperty.call(options.resolvedMcp, oldServer)) {
527
+ delete mcpServers[oldServer];
528
+ }
355
529
  }
530
+ for (const [serverName, config] of Object.entries(options.resolvedMcp)) {
531
+ const mapped = {};
532
+ if (typeof config.url === "string")
533
+ mapped.url = config.url;
534
+ if (typeof config.command === "string")
535
+ mapped.command = config.command;
536
+ if (Array.isArray(config.args))
537
+ mapped.args = config.args;
538
+ if (isObject(config.env))
539
+ mapped.env = config.env;
540
+ mcpServers[serverName] = mapped;
541
+ }
542
+ parsed.mcp_servers = mcpServers;
543
+ nextServers = Object.keys(options.resolvedMcp).sort();
356
544
  }
357
- for (const [serverName, config] of Object.entries(options.resolvedMcp)) {
358
- const mapped = {};
359
- if (typeof config.url === "string")
360
- mapped.url = config.url;
361
- if (typeof config.command === "string")
362
- mapped.command = config.command;
363
- if (Array.isArray(config.args))
364
- mapped.args = config.args;
365
- if (isObject(config.env))
366
- mapped.env = config.env;
367
- mcpServers[serverName] = mapped;
368
- }
369
- parsed.mcp_servers = mcpServers;
370
545
  if (!options.dryRun) {
371
546
  ensureDir(codexDir);
372
547
  writeTextAtomic(codexConfigPath, TOML.stringify(parsed));
@@ -374,9 +549,13 @@ function syncCodex(options) {
374
549
  options.generated.add(codexConfigPath);
375
550
  options.nextManifest.codex = {
376
551
  roles: nextRoles.sort(),
377
- mcpServers: Object.keys(options.resolvedMcp).sort(),
552
+ mcpServers: nextServers.sort(),
378
553
  };
379
554
  }
555
+ function resolveTrackedCodexEntries(trackedEntries, fallbackEntries) {
556
+ const tracked = Array.isArray(trackedEntries) ? trackedEntries : [];
557
+ return [...new Set([...tracked, ...fallbackEntries])].sort();
558
+ }
380
559
  function buildCodexRoleToml(roleInstructionsPath, providerConfig) {
381
560
  const roleToml = {
382
561
  model_instructions_file: `./${path.basename(roleInstructionsPath)}`,
@@ -423,31 +602,34 @@ function maybeWriteJson(filePath, payload, dryRun) {
423
602
  async function removeStaleGeneratedFiles(options) {
424
603
  const oldSet = new Set(options.oldManifest.generatedFiles);
425
604
  const newSet = new Set(options.newManifest.generatedFiles);
426
- const stale = [...oldSet].filter((filePath) => !newSet.has(filePath));
427
- const removed = [];
428
- for (const filePath of stale) {
429
- if (!fs.existsSync(filePath))
430
- continue;
431
- if (options.dryRun) {
432
- removed.push(filePath);
433
- continue;
434
- }
435
- if (!options.yes && !options.nonInteractive) {
436
- const shouldDelete = await confirm({
437
- message: `Remove stale generated file ${toPosixPath(filePath)}?`,
438
- initialValue: true,
439
- });
440
- if (isCancel(shouldDelete)) {
441
- continue;
442
- }
443
- if (!shouldDelete) {
444
- continue;
445
- }
605
+ const stale = [...oldSet]
606
+ .filter((filePath) => !newSet.has(filePath))
607
+ .filter((filePath) => fs.existsSync(filePath));
608
+ if (stale.length === 0)
609
+ return [];
610
+ if (options.dryRun)
611
+ return stale;
612
+ if (!options.yes && !options.nonInteractive) {
613
+ const selected = await multiselect({
614
+ message: withMultiselectHelp("Remove stale generated files?"),
615
+ options: stale.map((filePath) => ({
616
+ value: filePath,
617
+ label: toPosixPath(filePath),
618
+ })),
619
+ initialValues: stale,
620
+ });
621
+ if (isCancel(selected))
622
+ return [];
623
+ const toRemove = Array.isArray(selected) ? selected : [];
624
+ for (const filePath of toRemove) {
625
+ removeFileIfExists(filePath);
446
626
  }
627
+ return toRemove;
628
+ }
629
+ for (const filePath of stale) {
447
630
  removeFileIfExists(filePath);
448
- removed.push(filePath);
449
631
  }
450
- return removed;
632
+ return stale;
451
633
  }
452
634
  function getVsCodeSettingsPath(homeDir) {
453
635
  switch (os.platform()) {
@@ -464,6 +646,88 @@ function getVsCodeSettingsPath(homeDir) {
464
646
  return path.join(homeDir, ".config", "Code", "User", "settings.json");
465
647
  }
466
648
  }
649
+ function normalizeGeneratedByEntity(manifest) {
650
+ const source = manifest.generatedByEntity;
651
+ if (!source || typeof source !== "object") {
652
+ return inferGeneratedByEntityFromLegacyFiles(manifest.generatedFiles);
653
+ }
654
+ return {
655
+ agent: Array.isArray(source.agent) ? [...source.agent] : [],
656
+ command: Array.isArray(source.command) ? [...source.command] : [],
657
+ mcp: Array.isArray(source.mcp) ? [...source.mcp] : [],
658
+ skill: Array.isArray(source.skill) ? [...source.skill] : [],
659
+ };
660
+ }
661
+ function inferGeneratedByEntityFromLegacyFiles(generatedFiles) {
662
+ const byEntity = {
663
+ agent: [],
664
+ command: [],
665
+ mcp: [],
666
+ skill: [],
667
+ };
668
+ for (const filePath of generatedFiles) {
669
+ for (const entity of classifyLegacyGeneratedFile(filePath)) {
670
+ byEntity[entity]?.push(filePath);
671
+ }
672
+ }
673
+ return pruneGeneratedByEntity(byEntity);
674
+ }
675
+ function classifyLegacyGeneratedFile(filePath) {
676
+ const normalized = toPosixPath(filePath).toLowerCase();
677
+ if (isLegacyCodexConfigPath(normalized)) {
678
+ return ["agent", "mcp"];
679
+ }
680
+ if (isLegacyCommandOutputPath(normalized)) {
681
+ return ["command"];
682
+ }
683
+ if (isLegacyAgentOutputPath(normalized)) {
684
+ return ["agent"];
685
+ }
686
+ if (isLegacyMcpOutputPath(normalized)) {
687
+ return ["mcp"];
688
+ }
689
+ // Preserve unknown generated paths during scoped syncs.
690
+ return ["agent", "command", "mcp"];
691
+ }
692
+ function isLegacyCommandOutputPath(normalizedPath) {
693
+ return (normalizedPath.includes("/.cursor/commands/") ||
694
+ normalizedPath.includes("/.claude/commands/") ||
695
+ normalizedPath.includes("/.opencode/commands/") ||
696
+ normalizedPath.includes("/.gemini/commands/") ||
697
+ normalizedPath.includes("/.github/prompts/") ||
698
+ normalizedPath.includes("/.codex/prompts/"));
699
+ }
700
+ function isLegacyAgentOutputPath(normalizedPath) {
701
+ return (normalizedPath.includes("/.cursor/agents/") ||
702
+ normalizedPath.includes("/.cursor/rules/") ||
703
+ normalizedPath.includes("/.claude/agents/") ||
704
+ normalizedPath.includes("/.opencode/agents/") ||
705
+ normalizedPath.includes("/.gemini/agents/") ||
706
+ normalizedPath.includes("/.github/agents/") ||
707
+ normalizedPath.includes("/.codex/agents/"));
708
+ }
709
+ function isLegacyMcpOutputPath(normalizedPath) {
710
+ return (normalizedPath.endsWith("/.cursor/mcp.json") ||
711
+ normalizedPath.endsWith("/.mcp.json") ||
712
+ normalizedPath.endsWith("/.claude/settings.json") ||
713
+ normalizedPath.endsWith("/.opencode/opencode.json") ||
714
+ normalizedPath.endsWith("/.gemini/settings.json") ||
715
+ normalizedPath.endsWith("/.vscode/mcp.json") ||
716
+ normalizedPath.endsWith("/code/user/settings.json"));
717
+ }
718
+ function isLegacyCodexConfigPath(normalizedPath) {
719
+ return normalizedPath.endsWith("/.codex/config.toml");
720
+ }
721
+ function pruneGeneratedByEntity(value) {
722
+ const next = {};
723
+ for (const entity of ["agent", "command", "mcp", "skill"]) {
724
+ const files = value[entity];
725
+ if (!files || files.length === 0)
726
+ continue;
727
+ next[entity] = [...new Set(files)].sort();
728
+ }
729
+ return next;
730
+ }
467
731
  export function formatSyncSummary(summary, agentsRoot) {
468
732
  const generated = summary.generatedFiles
469
733
  .map((filePath) => relativePosix(agentsRoot, filePath))