agentloom 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -60,7 +60,7 @@ Global scope uses `~/.agents` with the same file layout.
60
60
  - `agentloom update [source]`
61
61
  - `agentloom upgrade`
62
62
  - `agentloom sync`
63
- - `agentloom delete <source|name>`
63
+ - `agentloom delete <source|name...>`
64
64
 
65
65
  Aggregate `add` imports discoverable entities from a source (agents, commands, rules, MCP servers, skills). In interactive sessions, each entity supports two tracking modes:
66
66
 
@@ -117,6 +117,7 @@ agentloom mcp add farnoodma/agents --mcps browser
117
117
  agentloom rule add farnoodma/agents --rules always-test
118
118
  agentloom skill add farnoodma/agents --skills pr-review
119
119
  agentloom delete farnoodma/agents
120
+ agentloom command delete review audit
120
121
  agentloom mcp server add browser-tools --command npx --arg browser-tools-mcp
121
122
  ```
122
123
 
@@ -7,7 +7,8 @@ import { runScopedFindCommand } from "./find.js";
7
7
  import { runScopedSyncCommand } from "./sync.js";
8
8
  import { runScopedUpdateCommand } from "./update.js";
9
9
  export async function runAgentCommand(argv, cwd) {
10
- const action = argv._[1];
10
+ const rawAction = argv._[1];
11
+ const action = rawAction === "remove" ? "delete" : rawAction;
11
12
  if (argv.help || !action) {
12
13
  console.log("Usage:\n agentloom agent <add|list|delete|find|update|sync> [options]");
13
14
  return;
@@ -7,7 +7,8 @@ import { runScopedFindCommand } from "./find.js";
7
7
  import { runScopedSyncCommand } from "./sync.js";
8
8
  import { runScopedUpdateCommand } from "./update.js";
9
9
  export async function runCommandCommand(argv, cwd) {
10
- const action = argv._[1];
10
+ const rawAction = argv._[1];
11
+ const action = rawAction === "remove" ? "delete" : rawAction;
11
12
  if (argv.help) {
12
13
  if (action === "add") {
13
14
  console.log(getCommandAddHelpText());
@@ -2,16 +2,26 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { multiselect, isCancel, cancel, select } from "@clack/prompts";
4
4
  import { parseAgentsDir } from "../core/agents.js";
5
- import { parseCommandsDir } from "../core/commands.js";
5
+ import { getStringArrayFlag } from "../core/argv.js";
6
+ import { parseCommandsDir, stripCommandFileExtension, } from "../core/commands.js";
6
7
  import { formatUsageError } from "../core/copy.js";
7
8
  import { readLockfile, writeLockfile } from "../core/lockfile.js";
8
9
  import { readCanonicalMcp, writeCanonicalMcp } from "../core/mcp.js";
10
+ import { getProviderCommandsDir } from "../core/provider-paths.js";
9
11
  import { normalizeRuleSelector, parseRulesDir, stripRuleFileExtension, } from "../core/rules.js";
10
12
  import { parseSourceSpec } from "../core/sources.js";
11
13
  import { parseSkillsDir } from "../core/skills.js";
12
14
  import { getNonInteractiveMode, resolvePathsForCommand, runPostMutationSync, } from "./entity-utils.js";
13
15
  const ALL_ENTITIES = ["agent", "command", "mcp", "rule", "skill"];
14
16
  const MULTISELECT_HELP_TEXT = "↑↓ move, space select, enter confirm";
17
+ class MissingEntityError extends Error {
18
+ candidate;
19
+ constructor(candidate, message) {
20
+ super(message);
21
+ this.candidate = candidate;
22
+ this.name = "MissingEntityError";
23
+ }
24
+ }
15
25
  function withMultiselectHelp(message) {
16
26
  return `${message}\n${MULTISELECT_HELP_TEXT}`;
17
27
  }
@@ -32,13 +42,13 @@ export async function runScopedDeleteCommand(options) {
32
42
  });
33
43
  }
34
44
  async function runEntityAwareDelete(options) {
35
- const candidate = getDeleteCandidate(options.argv, options.sourceIndex);
36
- if (!candidate) {
45
+ const candidates = getDeleteCandidates(options.argv, options.sourceIndex);
46
+ if (candidates.length === 0) {
37
47
  throw new Error(formatUsageError({
38
- issue: "Missing required <source|name>.",
48
+ issue: "Missing required <source|name...>.",
39
49
  usage: options.target === "all"
40
- ? "agentloom delete <source|name> [options]"
41
- : `agentloom ${options.target} delete <source|name> [options]`,
50
+ ? "agentloom delete <source|name...> [options]"
51
+ : `agentloom ${options.target} delete <source|name...> [options]`,
42
52
  example: options.target === "all"
43
53
  ? "agentloom delete farnoodma/agents"
44
54
  : `agentloom ${options.target} delete reviewer`,
@@ -46,61 +56,69 @@ async function runEntityAwareDelete(options) {
46
56
  }
47
57
  const nonInteractive = getNonInteractiveMode(options.argv);
48
58
  const paths = await resolvePathsForCommand(options.argv, options.cwd);
49
- const lockfile = readLockfile(paths);
50
- const sourceMatches = lockfile.entries.filter((entry) => matchesSource(entry, candidate));
51
- const sourceMode = sourceMatches.length > 0 ||
52
- (typeof options.argv.source === "string" &&
53
- options.argv.source.trim() !== "");
54
- const entities = await resolveEntitiesForDelete({
55
- argv: options.argv,
56
- target: options.target,
57
- sourceMode,
58
- nonInteractive,
59
- });
60
- if (entities.length === 0) {
61
- console.log("No entities selected for deletion.");
62
- return;
63
- }
64
- if (sourceMode) {
65
- await deleteBySource({
66
- paths,
67
- sourceFilter: candidate,
68
- lockfile,
69
- lockEntries: sourceMatches,
70
- entities,
71
- });
72
- }
73
- else {
74
- await deleteByName({
75
- candidate,
59
+ const explicitSourceFilters = getStringArrayFlag(options.argv.source);
60
+ const missingCandidates = [];
61
+ for (const candidate of candidates) {
62
+ const lockfile = readLockfile(paths);
63
+ const sourceMatches = lockfile.entries.filter((entry) => matchesSource(entry, candidate));
64
+ const sourceMode = sourceMatches.length > 0 || explicitSourceFilters.length > 0;
65
+ const entities = await resolveEntitiesForDelete({
76
66
  argv: options.argv,
77
- paths,
78
- entities,
67
+ target: options.target,
68
+ sourceMode,
79
69
  nonInteractive,
80
70
  });
71
+ if (entities.length === 0) {
72
+ console.log("No entities selected for deletion.");
73
+ return;
74
+ }
75
+ if (sourceMode) {
76
+ await deleteBySource({
77
+ paths,
78
+ sourceFilter: candidate,
79
+ lockfile,
80
+ lockEntries: sourceMatches,
81
+ entities,
82
+ });
83
+ continue;
84
+ }
85
+ try {
86
+ await deleteByName({
87
+ candidate,
88
+ argv: options.argv,
89
+ paths,
90
+ entities,
91
+ nonInteractive,
92
+ });
93
+ }
94
+ catch (error) {
95
+ if (error instanceof MissingEntityError) {
96
+ missingCandidates.push(error.candidate);
97
+ continue;
98
+ }
99
+ throw error;
100
+ }
81
101
  }
82
102
  await runPostMutationSync({
83
103
  argv: options.argv,
84
104
  paths,
85
105
  target: options.target,
86
106
  });
107
+ if (missingCandidates.length > 0) {
108
+ console.warn(`Couldn't delete these because they don't exist: ${[...new Set(missingCandidates)].join(", ")}`);
109
+ }
87
110
  }
88
- function getDeleteCandidate(argv, sourceIndex) {
89
- const fromSourceFlag = typeof argv.source === "string" && argv.source.trim().length > 0
90
- ? argv.source.trim()
91
- : undefined;
92
- if (fromSourceFlag)
111
+ function getDeleteCandidates(argv, sourceIndex) {
112
+ const fromSourceFlag = getStringArrayFlag(argv.source);
113
+ if (fromSourceFlag.length > 0)
93
114
  return fromSourceFlag;
94
- const fromNameFlag = typeof argv.name === "string" && argv.name.trim().length > 0
95
- ? argv.name.trim()
96
- : undefined;
97
- if (fromNameFlag)
115
+ const fromNameFlag = getStringArrayFlag(argv.name);
116
+ if (fromNameFlag.length > 0)
98
117
  return fromNameFlag;
99
- const positional = argv._[sourceIndex];
100
- if (typeof positional !== "string" || positional.trim().length === 0) {
101
- return undefined;
102
- }
103
- return positional.trim();
118
+ return argv._.slice(sourceIndex)
119
+ .filter((item) => typeof item === "string")
120
+ .map((item) => item.trim())
121
+ .filter(Boolean);
104
122
  }
105
123
  async function resolveEntitiesForDelete(options) {
106
124
  if (options.target !== "all")
@@ -196,7 +214,7 @@ async function deleteByName(options) {
196
214
  const matches = detectNameMatches(options.paths, options.candidate);
197
215
  const matchingEntities = selectedEntities.filter((entity) => matches.includes(entity));
198
216
  if (matchingEntities.length === 0) {
199
- throw new Error(`No installed entity named "${options.candidate}" found.`);
217
+ throw new MissingEntityError(options.candidate, `No installed entity named "${options.candidate}" found.`);
200
218
  }
201
219
  if (matchingEntities.length > 1) {
202
220
  if (options.nonInteractive) {
@@ -230,7 +248,7 @@ async function deleteByName(options) {
230
248
  else if (entity === "mcp") {
231
249
  const existing = Object.keys(mcp.mcpServers).find((name) => normalizeName(name) === normalizeName(options.candidate));
232
250
  if (!existing) {
233
- throw new Error(`MCP server "${options.candidate}" was not found.`);
251
+ throw new MissingEntityError(options.candidate, `MCP server "${options.candidate}" was not found.`);
234
252
  }
235
253
  delete mcp.mcpServers[existing];
236
254
  }
@@ -261,7 +279,7 @@ function deleteAgentByName(paths, name) {
261
279
  normalizeName(agent.fileName.replace(/\.md$/i, "")) ===
262
280
  normalizeName(name));
263
281
  if (!target) {
264
- throw new Error(`Agent "${name}" was not found in canonical agents.`);
282
+ throw new MissingEntityError(name, `Agent "${name}" was not found in canonical agents.`);
265
283
  }
266
284
  removeIfExists(target.sourcePath);
267
285
  }
@@ -271,16 +289,17 @@ function deleteCommandByName(paths, name) {
271
289
  normalizeName(command.fileName.replace(/\.(md|mdc)$/i, "")) ===
272
290
  normalizeName(name));
273
291
  if (!target) {
274
- throw new Error(`Command "${name}" was not found in canonical commands.`);
292
+ throw new MissingEntityError(name, `Command "${name}" was not found in canonical commands.`);
275
293
  }
276
294
  removeIfExists(target.sourcePath);
295
+ removeLegacyGeminiCommandVariants(paths, target.fileName);
277
296
  }
278
297
  function deleteSkillByName(paths, name) {
279
298
  const skills = parseSkillsDir(paths.skillsDir);
280
299
  const target = skills.find((skill) => normalizeName(skill.name) === normalizeName(name) ||
281
300
  normalizeName(path.basename(skill.sourcePath)) === normalizeName(name));
282
301
  if (!target) {
283
- throw new Error(`Skill "${name}" was not found in canonical skills.`);
302
+ throw new MissingEntityError(name, `Skill "${name}" was not found in canonical skills.`);
284
303
  }
285
304
  removeIfExists(target.sourcePath);
286
305
  }
@@ -288,7 +307,7 @@ function deleteRuleByName(paths, name) {
288
307
  const rules = parseRulesDir(paths.rulesDir);
289
308
  const target = rules.find((rule) => ruleMatchesCandidate(rule, name));
290
309
  if (!target) {
291
- throw new Error(`Rule "${name}" was not found in canonical rules.`);
310
+ throw new MissingEntityError(name, `Rule "${name}" was not found in canonical rules.`);
292
311
  }
293
312
  removeIfExists(target.sourcePath);
294
313
  return target;
@@ -298,6 +317,12 @@ function removeIfExists(filePath) {
298
317
  return;
299
318
  fs.rmSync(filePath, { recursive: true, force: true });
300
319
  }
320
+ function removeLegacyGeminiCommandVariants(paths, fileName) {
321
+ const commandsDir = getProviderCommandsDir(paths, "gemini");
322
+ const stem = stripCommandFileExtension(path.basename(fileName));
323
+ removeIfExists(path.join(commandsDir, `${stem}.md`));
324
+ removeIfExists(path.join(commandsDir, `${stem}.mdc`));
325
+ }
301
326
  function normalizeName(value) {
302
327
  return value.trim().toLowerCase();
303
328
  }
@@ -9,7 +9,8 @@ import { runScopedFindCommand } from "./find.js";
9
9
  import { runScopedSyncCommand } from "./sync.js";
10
10
  import { runScopedUpdateCommand } from "./update.js";
11
11
  export async function runMcpCommand(argv, cwd) {
12
- const action = argv._[1];
12
+ const rawAction = argv._[1];
13
+ const action = rawAction === "remove" ? "delete" : rawAction;
13
14
  if (argv.help) {
14
15
  if (action === "add") {
15
16
  console.log(getMcpAddHelpText());
@@ -93,7 +94,8 @@ export async function runMcpCommand(argv, cwd) {
93
94
  });
94
95
  }
95
96
  async function runMcpServerCommand(argv, cwd) {
96
- const action = argv._[2];
97
+ const rawAction = argv._[2];
98
+ const action = rawAction === "remove" ? "delete" : rawAction;
97
99
  if (argv.help || !action) {
98
100
  console.log(getMcpServerHelpText());
99
101
  return;
@@ -7,7 +7,8 @@ import { runScopedFindCommand } from "./find.js";
7
7
  import { runScopedSyncCommand } from "./sync.js";
8
8
  import { runScopedUpdateCommand } from "./update.js";
9
9
  export async function runRuleCommand(argv, cwd) {
10
- const action = argv._[1];
10
+ const rawAction = argv._[1];
11
+ const action = rawAction === "remove" ? "delete" : rawAction;
11
12
  if (argv.help || !action) {
12
13
  console.log("Usage:\n agentloom rule <add|list|delete|find|update|sync> [options]");
13
14
  return;
@@ -8,7 +8,8 @@ import { runScopedFindCommand } from "./find.js";
8
8
  import { runScopedSyncCommand } from "./sync.js";
9
9
  import { runScopedUpdateCommand } from "./update.js";
10
10
  export async function runSkillCommand(argv, cwd) {
11
- const action = argv._[1];
11
+ const rawAction = argv._[1];
12
+ const action = rawAction === "remove" ? "delete" : rawAction;
12
13
  if (argv.help || !action) {
13
14
  console.log("Usage:\n agentloom skill <add|list|delete|find|update|sync> [options]");
14
15
  return;
@@ -5,6 +5,7 @@ import { normalizeCommandSelector, stripCommandFileExtension, } from "../core/co
5
5
  import { normalizeRuleSelector, stripRuleFileExtension, } from "../core/rules.js";
6
6
  import { readLockfile } from "../core/lockfile.js";
7
7
  import { prepareSource, parseSourceSpec } from "../core/sources.js";
8
+ import { sendAddTelemetryEvent } from "../core/telemetry.js";
8
9
  import { getUpdateHelpText } from "../core/copy.js";
9
10
  import { resolveProvidersForSync } from "../sync/index.js";
10
11
  import { getNonInteractiveMode, resolvePathsForCommand, runPostMutationSync, } from "./entity-utils.js";
@@ -58,21 +59,28 @@ async function runEntityAwareUpdate(options) {
58
59
  console.log("No lock entries matched the requested source filter.");
59
60
  return;
60
61
  }
62
+ console.log(`Checking ${entries.length} lock entr${entries.length === 1 ? "y" : "ies"} for updates...`);
61
63
  let updated = 0;
62
64
  let skipped = 0;
63
- for (const entry of entries) {
65
+ for (const [index, entry] of entries.entries()) {
66
+ const progressPrefix = formatUpdateProgressPrefix(index, entries.length);
67
+ const entryLabel = formatLockEntryLabel(entry);
64
68
  if (!entryIncludesTarget(entry, options.target)) {
69
+ console.log(`${progressPrefix} Skipping ${entryLabel} (does not track ${options.target}).`);
65
70
  skipped += 1;
66
71
  continue;
67
72
  }
73
+ console.log(`${progressPrefix} Checking ${entryLabel}...`);
68
74
  const probe = prepareSource({
69
75
  source: entry.source,
70
76
  ref: entry.requestedRef,
71
77
  subdir: entry.subdir,
72
78
  });
73
- const hasNewCommit = probe.resolvedCommit !== entry.resolvedCommit;
79
+ const latestCommit = probe.resolvedCommit;
80
+ const hasNewCommit = latestCommit !== entry.resolvedCommit;
74
81
  probe.cleanup();
75
82
  if (!hasNewCommit) {
83
+ console.log(`${progressPrefix} Up to date at ${formatShortCommit(entry.resolvedCommit)}.`);
76
84
  skipped += 1;
77
85
  continue;
78
86
  }
@@ -82,9 +90,11 @@ async function runEntityAwareUpdate(options) {
82
90
  !updatePlan.importMcp &&
83
91
  !updatePlan.importRules &&
84
92
  !updatePlan.importSkills) {
93
+ console.log(`${progressPrefix} Skipping ${entryLabel} (no tracked entities selected for update).`);
85
94
  skipped += 1;
86
95
  continue;
87
96
  }
97
+ console.log(`${progressPrefix} Updating ${entryLabel} (${formatShortCommit(entry.resolvedCommit)} -> ${formatShortCommit(latestCommit)})...`);
88
98
  try {
89
99
  const importOptions = {
90
100
  source: entry.source,
@@ -136,7 +146,12 @@ async function runEntityAwareUpdate(options) {
136
146
  if (updatePlan.skillRenameMap) {
137
147
  importOptions.skillRenameMap = updatePlan.skillRenameMap;
138
148
  }
139
- await importSource(importOptions);
149
+ const summary = await importSource(importOptions);
150
+ await sendAddTelemetryEvent({
151
+ rawSource: entry.source,
152
+ summary,
153
+ });
154
+ console.log(`${progressPrefix} Updated ${entryLabel} to ${formatShortCommit(summary.resolvedCommit)}.`);
140
155
  updated += 1;
141
156
  }
142
157
  catch (err) {
@@ -158,6 +173,25 @@ async function runEntityAwareUpdate(options) {
158
173
  });
159
174
  }
160
175
  }
176
+ function formatUpdateProgressPrefix(index, total) {
177
+ return `[${index + 1}/${total}]`;
178
+ }
179
+ function formatLockEntryLabel(entry) {
180
+ const base = entry.source;
181
+ const refSuffix = typeof entry.requestedRef === "string" && entry.requestedRef.length > 0
182
+ ? `@${entry.requestedRef}`
183
+ : "";
184
+ const subdirSuffix = typeof entry.subdir === "string" && entry.subdir.length > 0
185
+ ? ` (${entry.subdir})`
186
+ : "";
187
+ return `${base}${refSuffix}${subdirSuffix}`;
188
+ }
189
+ function formatShortCommit(commit) {
190
+ const normalized = commit.trim();
191
+ if (normalized.length <= 12)
192
+ return normalized;
193
+ return normalized.slice(0, 12);
194
+ }
161
195
  function buildEntryUpdatePlan(entry, target) {
162
196
  const includeAgents = shouldUpdateEntity(entry, "agent", target);
163
197
  const includeCommands = shouldUpdateEntity(entry, "command", target);
package/dist/core/copy.js CHANGED
@@ -14,7 +14,7 @@ Aggregate commands:
14
14
  update [source] Refresh lockfile-managed imports
15
15
  upgrade Install the latest CLI release
16
16
  sync Migrate provider configs then generate provider outputs
17
- delete <source|name> Delete imported entities by source or name
17
+ delete <source|name...> Delete imported entities by source or name(s)
18
18
 
19
19
  Entity commands:
20
20
  agent <add|list|delete|find|update|sync>
@@ -40,8 +40,8 @@ Common options:
40
40
  --rules <csv> Rule selectors for add/delete
41
41
  --skills <csv> Skill selectors for add/delete
42
42
  --selection-mode <mode> Add mode: all (default) or custom
43
- --source <value> Explicit source filter for update/delete
44
- --name <value> Explicit name filter for delete
43
+ --source <csv> Explicit source filter(s) for update/delete
44
+ --name <csv> Explicit name filter(s) for delete
45
45
  --entity <type> Delete disambiguation for aggregate delete
46
46
 
47
47
  Examples:
@@ -192,7 +192,7 @@ export function getCommandDeleteHelpText() {
192
192
  return `Delete command imports by source or name.
193
193
 
194
194
  Usage:
195
- agentloom command delete <source|name> [options]
195
+ agentloom command delete <source|name...> [options]
196
196
  `;
197
197
  }
198
198
  export function getMcpHelpText() {
@@ -417,6 +417,8 @@ export async function importSource(options) {
417
417
  }
418
418
  }
419
419
  }
420
+ const selectedSubsetOfSourceAgents = sourceAgents.length > 0 &&
421
+ selection.selectedAgents.length < sourceAgents.length;
420
422
  const selectedSubsetOfSourceCommands = sourceCommands.length > 0 &&
421
423
  selectedSourceCommandFiles.length < sourceCommands.length;
422
424
  const selectedSubsetOfSourceMcp = sourceMcpServerNames.length > 0 &&
@@ -441,18 +443,48 @@ export async function importSource(options) {
441
443
  ? selectedSourceSkills
442
444
  : undefined;
443
445
  const lockfile = readLockfile(options.paths);
444
- const isCommandOnlyImport = !shouldImportAgents &&
445
- shouldImportCommands &&
446
- !shouldImportMcp &&
447
- !shouldImportRules &&
448
- !shouldImportSkills;
449
- const existingEntry = isCommandOnlyImport
450
- ? findRelaxedCommandEntry(lockfile.entries, {
446
+ const importedEntities = [
447
+ shouldImportAgents ? "agent" : null,
448
+ shouldImportCommands ? "command" : null,
449
+ shouldImportMcp ? "mcp" : null,
450
+ shouldImportRules ? "rule" : null,
451
+ shouldImportSkills ? "skill" : null,
452
+ ].filter(Boolean);
453
+ const singleEntityImport = importedEntities.length === 1 ? importedEntities[0] : undefined;
454
+ const isAgentOnlyImport = singleEntityImport === "agent";
455
+ const isCommandOnlyImport = singleEntityImport === "command";
456
+ const isMcpOnlyImport = singleEntityImport === "mcp";
457
+ const isRuleOnlyImport = singleEntityImport === "rule";
458
+ const isSkillOnlyImport = singleEntityImport === "skill";
459
+ const relaxedSingleEntityEntries = singleEntityImport
460
+ ? findRelaxedEntityEntries(lockfile.entries, {
451
461
  source: prepared.spec.source,
452
462
  sourceType: prepared.spec.type,
453
463
  subdir: options.subdir,
454
464
  requestedAgents: options.agents,
465
+ entity: singleEntityImport,
455
466
  })
467
+ : [];
468
+ let relaxedSingleEntityEntry = relaxedSingleEntityEntries[0];
469
+ if (relaxedSingleEntityEntries.length > 1) {
470
+ const canonicalEntry = relaxedSingleEntityEntries[0];
471
+ const redundantEntries = relaxedSingleEntityEntries.slice(1);
472
+ const collapsibleRedundantEntries = redundantEntries.filter((entry) => !isMixedEntryForEntity(entry, singleEntityImport));
473
+ const consolidatedEntry = mergeRelaxedEntityEntriesForLock({
474
+ canonicalEntry,
475
+ redundantEntries: collapsibleRedundantEntries,
476
+ entity: singleEntityImport,
477
+ });
478
+ const canonicalEntryIndex = lockfile.entries.indexOf(canonicalEntry);
479
+ if (canonicalEntryIndex >= 0) {
480
+ lockfile.entries[canonicalEntryIndex] = consolidatedEntry;
481
+ }
482
+ const redundantEntriesSet = new Set(collapsibleRedundantEntries);
483
+ lockfile.entries = lockfile.entries.filter((entry) => !redundantEntriesSet.has(entry));
484
+ relaxedSingleEntityEntry = consolidatedEntry;
485
+ }
486
+ const existingEntry = singleEntityImport
487
+ ? relaxedSingleEntityEntry
456
488
  : findMatchingLockEntry(lockfile.entries, {
457
489
  source: prepared.spec.source,
458
490
  sourceType: prepared.spec.type,
@@ -472,8 +504,34 @@ export async function importSource(options) {
472
504
  (existingEntry?.importedAgents.length ?? 0) === 0 &&
473
505
  (existingEntry?.importedMcpServers.length ?? 0) === 0 &&
474
506
  (existingEntry?.importedSkills.length ?? 0) === 0;
507
+ const shouldMergeAgentOnlyEntry = isAgentOnlyImport &&
508
+ Boolean(existingEntry) &&
509
+ (selection.requestedAgentsForLock !== undefined ||
510
+ selectedSubsetOfSourceAgents);
511
+ const shouldMergeMcpOnlyEntry = isMcpOnlyImport &&
512
+ Boolean(existingEntry) &&
513
+ (existingEntry?.importedAgents.length ?? 0) === 0 &&
514
+ (existingEntry?.importedCommands.length ?? 0) === 0 &&
515
+ (existingEntry?.importedRules.length ?? 0) === 0 &&
516
+ (existingEntry?.importedSkills.length ?? 0) === 0 &&
517
+ (shouldPersistMcpSelection || selectedSubsetOfSourceMcp);
518
+ const shouldMergeRuleOnlyEntry = isRuleOnlyImport &&
519
+ Boolean(existingEntry) &&
520
+ (existingEntry?.importedAgents.length ?? 0) === 0 &&
521
+ (existingEntry?.importedCommands.length ?? 0) === 0 &&
522
+ (existingEntry?.importedMcpServers.length ?? 0) === 0 &&
523
+ (existingEntry?.importedSkills.length ?? 0) === 0 &&
524
+ (shouldPersistRuleSelection || selectedSubsetOfSourceRules);
525
+ const shouldMergeSkillOnlyEntry = isSkillOnlyImport &&
526
+ Boolean(existingEntry) &&
527
+ (shouldPersistSkillSelection || selectedSubsetOfSourceSkills);
475
528
  const lockImportedAgents = shouldImportAgents
476
- ? importedAgents
529
+ ? shouldMergeAgentOnlyEntry
530
+ ? uniqueStrings([
531
+ ...(existingEntry?.importedAgents ?? []),
532
+ ...importedAgents,
533
+ ])
534
+ : importedAgents
477
535
  : (existingEntry?.importedAgents ?? []);
478
536
  const lockImportedCommands = shouldImportCommands
479
537
  ? shouldMergeCommandOnlyEntry
@@ -484,13 +542,30 @@ export async function importSource(options) {
484
542
  : importedCommands
485
543
  : (existingEntry?.importedCommands ?? []);
486
544
  const lockImportedMcpServers = shouldImportMcp
487
- ? importedMcpServers
545
+ ? shouldMergeMcpOnlyEntry
546
+ ? uniqueStrings([
547
+ ...(existingEntry?.importedMcpServers ?? []),
548
+ ...importedMcpServers,
549
+ ])
550
+ : importedMcpServers
488
551
  : (existingEntry?.importedMcpServers ?? []);
489
552
  const lockImportedRules = shouldImportRules
490
- ? importedRules
553
+ ? shouldMergeRuleOnlyEntry
554
+ ? uniqueStrings([
555
+ ...(existingEntry?.importedRules ?? []),
556
+ ...importedRules,
557
+ ])
558
+ : importedRules
491
559
  : (existingEntry?.importedRules ?? []);
492
560
  const lockImportedSkills = shouldImportSkills
493
- ? importedSkills
561
+ ? shouldMergeSkillOnlyEntry
562
+ ? mergeImportedSkills({
563
+ existingImportedSkills: existingEntry?.importedSkills,
564
+ importedSkills,
565
+ selectedSkills,
566
+ existingSkillRenameMap: existingEntry?.skillRenameMap,
567
+ })
568
+ : importedSkills
494
569
  : (existingEntry?.importedSkills ?? []);
495
570
  let lockSelectedSourceCommands;
496
571
  if (shouldImportCommands) {
@@ -516,29 +591,92 @@ export async function importSource(options) {
516
591
  else {
517
592
  lockSelectedSourceCommands = existingEntry?.selectedSourceCommands;
518
593
  }
519
- const lockSelectedSourceMcpServers = shouldImportMcp
520
- ? shouldPersistMcpSelection || selectedSubsetOfSourceMcp
521
- ? [...selectedSourceMcpServers]
522
- : undefined
523
- : existingEntry?.selectedSourceMcpServers;
524
- const lockSelectedSourceRules = shouldImportRules
525
- ? shouldPersistRuleSelection || selectedSubsetOfSourceRules
526
- ? [...selectedSourceRules]
527
- : undefined
528
- : existingEntry?.selectedSourceRules;
529
- const lockSelectedSourceSkills = shouldImportSkills
530
- ? shouldPersistSkillSelection || selectedSubsetOfSourceSkills
531
- ? [...selectedSourceSkills]
532
- : undefined
533
- : existingEntry?.selectedSourceSkills;
594
+ let lockSelectedSourceMcpServers;
595
+ if (shouldImportMcp) {
596
+ if (shouldMergeMcpOnlyEntry) {
597
+ if (shouldPersistMcpSelection || selectedSubsetOfSourceMcp) {
598
+ lockSelectedSourceMcpServers = uniqueStrings([
599
+ ...(existingEntry?.selectedSourceMcpServers ?? []),
600
+ ...selectedSourceMcpServers,
601
+ ]);
602
+ }
603
+ else {
604
+ lockSelectedSourceMcpServers = undefined;
605
+ }
606
+ }
607
+ else if (shouldPersistMcpSelection || selectedSubsetOfSourceMcp) {
608
+ lockSelectedSourceMcpServers = [...selectedSourceMcpServers];
609
+ }
610
+ else {
611
+ lockSelectedSourceMcpServers = undefined;
612
+ }
613
+ }
614
+ else {
615
+ lockSelectedSourceMcpServers = existingEntry?.selectedSourceMcpServers;
616
+ }
617
+ let lockSelectedSourceRules;
618
+ if (shouldImportRules) {
619
+ if (shouldMergeRuleOnlyEntry) {
620
+ if (shouldPersistRuleSelection || selectedSubsetOfSourceRules) {
621
+ lockSelectedSourceRules = uniqueStrings([
622
+ ...(existingEntry?.selectedSourceRules ?? []),
623
+ ...selectedSourceRules,
624
+ ]);
625
+ }
626
+ else {
627
+ lockSelectedSourceRules = undefined;
628
+ }
629
+ }
630
+ else if (shouldPersistRuleSelection || selectedSubsetOfSourceRules) {
631
+ lockSelectedSourceRules = [...selectedSourceRules];
632
+ }
633
+ else {
634
+ lockSelectedSourceRules = undefined;
635
+ }
636
+ }
637
+ else {
638
+ lockSelectedSourceRules = existingEntry?.selectedSourceRules;
639
+ }
640
+ let lockSelectedSourceSkills;
641
+ if (shouldImportSkills) {
642
+ if (shouldMergeSkillOnlyEntry) {
643
+ if (shouldPersistSkillSelection || selectedSubsetOfSourceSkills) {
644
+ lockSelectedSourceSkills = uniqueStrings([
645
+ ...(existingEntry?.selectedSourceSkills ?? []),
646
+ ...selectedSourceSkills,
647
+ ]);
648
+ }
649
+ else {
650
+ lockSelectedSourceSkills = undefined;
651
+ }
652
+ }
653
+ else if (shouldPersistSkillSelection || selectedSubsetOfSourceSkills) {
654
+ lockSelectedSourceSkills = [...selectedSourceSkills];
655
+ }
656
+ else {
657
+ lockSelectedSourceSkills = undefined;
658
+ }
659
+ }
660
+ else {
661
+ lockSelectedSourceSkills = existingEntry?.selectedSourceSkills;
662
+ }
534
663
  const lockRuleRenameMap = shouldImportRules
535
- ? normalizeRuleRenameMap(importedRuleRenameMap)
664
+ ? shouldMergeRuleOnlyEntry
665
+ ? mergeRuleRenameMaps(existingEntry?.ruleRenameMap, importedRuleRenameMap)
666
+ : normalizeRuleRenameMap(importedRuleRenameMap)
536
667
  : existingEntry?.ruleRenameMap;
537
668
  const lockSkillsProviders = shouldImportSkills
538
- ? (skillsProvidersForLock ?? existingEntry?.skillsProviders)
669
+ ? shouldMergeSkillOnlyEntry
670
+ ? normalizeSkillsProviders([
671
+ ...(existingEntry?.skillsProviders ?? []),
672
+ ...(skillsProvidersForLock ?? []),
673
+ ])
674
+ : (skillsProvidersForLock ?? existingEntry?.skillsProviders)
539
675
  : existingEntry?.skillsProviders;
540
676
  const lockSkillRenameMap = shouldImportSkills
541
- ? normalizeSkillRenameMap(importedSkillRenameMap)
677
+ ? shouldMergeSkillOnlyEntry
678
+ ? mergeSkillRenameMaps(existingEntry?.skillRenameMap, importedSkillRenameMap)
679
+ : normalizeSkillRenameMap(importedSkillRenameMap)
542
680
  : existingEntry?.skillRenameMap;
543
681
  let lockCommandRenameMap;
544
682
  if (shouldImportCommands) {
@@ -553,7 +691,12 @@ export async function importSource(options) {
553
691
  lockCommandRenameMap = existingEntry?.commandRenameMap;
554
692
  }
555
693
  const lockRequestedAgents = shouldImportAgents
556
- ? selection.requestedAgentsForLock
694
+ ? shouldMergeAgentOnlyEntry
695
+ ? uniqueStrings([
696
+ ...(existingEntry?.requestedAgents ?? []),
697
+ ...(selection.requestedAgentsForLock ?? []),
698
+ ])
699
+ : selection.requestedAgentsForLock
557
700
  : existingEntry?.requestedAgents;
558
701
  const trackedEntities = computeTrackedEntitiesForLock({
559
702
  requestedAgents: lockRequestedAgents,
@@ -1305,18 +1448,171 @@ function findMatchingLockEntry(entries, key) {
1305
1448
  wildcardWhenRightIsUndefined: true,
1306
1449
  }));
1307
1450
  }
1308
- function findRelaxedCommandEntry(entries, key) {
1451
+ function findRelaxedEntityEntries(entries, key) {
1309
1452
  const matches = entries.filter((entry) => entry.source === key.source &&
1310
1453
  entry.sourceType === key.sourceType &&
1311
1454
  entry.subdir === key.subdir &&
1312
- sameRequestedAgentsForMatch(entry.requestedAgents, key.requestedAgents));
1313
- if (matches.length === 0)
1314
- return undefined;
1315
- const mixed = matches.find((entry) => entry.importedAgents.length > 0 ||
1316
- entry.importedMcpServers.length > 0 ||
1317
- entry.importedRules.length > 0 ||
1318
- entry.importedSkills.length > 0);
1319
- return mixed ?? matches[0];
1455
+ (key.entity === "agent" ||
1456
+ sameRequestedAgentsForMatch(entry.requestedAgents, key.requestedAgents)));
1457
+ if (matches.length <= 1)
1458
+ return matches;
1459
+ return [...matches].sort((left, right) => {
1460
+ const leftMixed = isMixedEntryForEntity(left, key.entity) ? 1 : 0;
1461
+ const rightMixed = isMixedEntryForEntity(right, key.entity) ? 1 : 0;
1462
+ if (leftMixed !== rightMixed) {
1463
+ return rightMixed - leftMixed;
1464
+ }
1465
+ const leftScore = scoreEntryForEntity(left, key.entity);
1466
+ const rightScore = scoreEntryForEntity(right, key.entity);
1467
+ if (leftScore !== rightScore) {
1468
+ return rightScore - leftScore;
1469
+ }
1470
+ return 0;
1471
+ });
1472
+ }
1473
+ function isMixedEntryForEntity(entry, entity) {
1474
+ return ((entity !== "agent" &&
1475
+ (entry.importedAgents.length > 0 ||
1476
+ (entry.requestedAgents?.length ?? 0) > 0)) ||
1477
+ (entity !== "command" &&
1478
+ (entry.importedCommands.length > 0 ||
1479
+ (entry.selectedSourceCommands?.length ?? 0) > 0 ||
1480
+ Object.keys(entry.commandRenameMap ?? {}).length > 0)) ||
1481
+ (entity !== "mcp" &&
1482
+ (entry.importedMcpServers.length > 0 ||
1483
+ (entry.selectedSourceMcpServers?.length ?? 0) > 0)) ||
1484
+ (entity !== "rule" &&
1485
+ (entry.importedRules.length > 0 ||
1486
+ (entry.selectedSourceRules?.length ?? 0) > 0 ||
1487
+ Object.keys(entry.ruleRenameMap ?? {}).length > 0)) ||
1488
+ (entity !== "skill" &&
1489
+ (entry.importedSkills.length > 0 ||
1490
+ (entry.selectedSourceSkills?.length ?? 0) > 0 ||
1491
+ (entry.skillsProviders?.length ?? 0) > 0 ||
1492
+ Object.keys(entry.skillRenameMap ?? {}).length > 0)));
1493
+ }
1494
+ function scoreEntryForEntity(entry, entity) {
1495
+ if (entity === "agent") {
1496
+ return (entry.importedAgents.length * 100 + (entry.requestedAgents?.length ?? 0));
1497
+ }
1498
+ if (entity === "command") {
1499
+ return (entry.importedCommands.length * 100 +
1500
+ (entry.selectedSourceCommands?.length ?? 0) * 10 +
1501
+ Object.keys(entry.commandRenameMap ?? {}).length);
1502
+ }
1503
+ if (entity === "mcp") {
1504
+ return (entry.importedMcpServers.length * 100 +
1505
+ (entry.selectedSourceMcpServers?.length ?? 0) * 10);
1506
+ }
1507
+ if (entity === "rule") {
1508
+ return (entry.importedRules.length * 100 +
1509
+ (entry.selectedSourceRules?.length ?? 0) * 10 +
1510
+ Object.keys(entry.ruleRenameMap ?? {}).length);
1511
+ }
1512
+ return (entry.importedSkills.length * 100 +
1513
+ (entry.selectedSourceSkills?.length ?? 0) * 10 +
1514
+ (entry.skillsProviders?.length ?? 0) * 3 +
1515
+ Object.keys(entry.skillRenameMap ?? {}).length);
1516
+ }
1517
+ function mergeRelaxedEntityEntriesForLock(options) {
1518
+ if (options.redundantEntries.length === 0) {
1519
+ return options.canonicalEntry;
1520
+ }
1521
+ let mergedEntry = {
1522
+ ...options.canonicalEntry,
1523
+ };
1524
+ if (options.entity === "agent") {
1525
+ const mergedRequestedAgents = uniqueStrings([
1526
+ ...(mergedEntry.requestedAgents ?? []),
1527
+ ...options.redundantEntries.flatMap((entry) => entry.requestedAgents ?? []),
1528
+ ]);
1529
+ mergedEntry = {
1530
+ ...mergedEntry,
1531
+ importedAgents: uniqueStrings([
1532
+ ...mergedEntry.importedAgents,
1533
+ ...options.redundantEntries.flatMap((entry) => entry.importedAgents),
1534
+ ]),
1535
+ requestedAgents: mergedRequestedAgents.length > 0 ? mergedRequestedAgents : undefined,
1536
+ };
1537
+ }
1538
+ else if (options.entity === "command") {
1539
+ const mergedSelectedSourceCommands = uniqueStrings([
1540
+ ...(mergedEntry.selectedSourceCommands ?? []),
1541
+ ...options.redundantEntries.flatMap((entry) => entry.selectedSourceCommands ?? []),
1542
+ ]);
1543
+ const mergedCommandRenameMap = Object.assign({}, mergedEntry.commandRenameMap ?? {}, ...options.redundantEntries.map((entry) => entry.commandRenameMap ?? {}));
1544
+ mergedEntry = {
1545
+ ...mergedEntry,
1546
+ importedCommands: uniqueStrings([
1547
+ ...mergedEntry.importedCommands,
1548
+ ...options.redundantEntries.flatMap((entry) => entry.importedCommands),
1549
+ ]),
1550
+ selectedSourceCommands: mergedSelectedSourceCommands.length > 0
1551
+ ? mergedSelectedSourceCommands
1552
+ : undefined,
1553
+ commandRenameMap: normalizeCommandRenameMap(mergedCommandRenameMap),
1554
+ };
1555
+ }
1556
+ else if (options.entity === "mcp") {
1557
+ const mergedSelectedSourceMcpServers = uniqueStrings([
1558
+ ...(mergedEntry.selectedSourceMcpServers ?? []),
1559
+ ...options.redundantEntries.flatMap((entry) => entry.selectedSourceMcpServers ?? []),
1560
+ ]);
1561
+ mergedEntry = {
1562
+ ...mergedEntry,
1563
+ importedMcpServers: uniqueStrings([
1564
+ ...mergedEntry.importedMcpServers,
1565
+ ...options.redundantEntries.flatMap((entry) => entry.importedMcpServers),
1566
+ ]),
1567
+ selectedSourceMcpServers: mergedSelectedSourceMcpServers.length > 0
1568
+ ? mergedSelectedSourceMcpServers
1569
+ : undefined,
1570
+ };
1571
+ }
1572
+ else if (options.entity === "rule") {
1573
+ const mergedSelectedSourceRules = uniqueStrings([
1574
+ ...(mergedEntry.selectedSourceRules ?? []),
1575
+ ...options.redundantEntries.flatMap((entry) => entry.selectedSourceRules ?? []),
1576
+ ]);
1577
+ const mergedRuleRenameMap = Object.assign({}, mergedEntry.ruleRenameMap ?? {}, ...options.redundantEntries.map((entry) => entry.ruleRenameMap ?? {}));
1578
+ mergedEntry = {
1579
+ ...mergedEntry,
1580
+ importedRules: uniqueStrings([
1581
+ ...mergedEntry.importedRules,
1582
+ ...options.redundantEntries.flatMap((entry) => entry.importedRules),
1583
+ ]),
1584
+ selectedSourceRules: mergedSelectedSourceRules.length > 0
1585
+ ? mergedSelectedSourceRules
1586
+ : undefined,
1587
+ ruleRenameMap: normalizeRuleRenameMap(mergedRuleRenameMap),
1588
+ };
1589
+ }
1590
+ else {
1591
+ const mergedSelectedSourceSkills = uniqueStrings([
1592
+ ...(mergedEntry.selectedSourceSkills ?? []),
1593
+ ...options.redundantEntries.flatMap((entry) => entry.selectedSourceSkills ?? []),
1594
+ ]);
1595
+ const mergedSkillsProviders = normalizeSkillsProviders([
1596
+ ...(mergedEntry.skillsProviders ?? []),
1597
+ ...options.redundantEntries.flatMap((entry) => entry.skillsProviders ?? []),
1598
+ ]);
1599
+ const mergedSkillRenameMap = Object.assign({}, mergedEntry.skillRenameMap ?? {}, ...options.redundantEntries.map((entry) => entry.skillRenameMap ?? {}));
1600
+ mergedEntry = {
1601
+ ...mergedEntry,
1602
+ importedSkills: uniqueStrings([
1603
+ ...mergedEntry.importedSkills,
1604
+ ...options.redundantEntries.flatMap((entry) => entry.importedSkills),
1605
+ ]),
1606
+ selectedSourceSkills: mergedSelectedSourceSkills.length > 0
1607
+ ? mergedSelectedSourceSkills
1608
+ : undefined,
1609
+ skillsProviders: mergedSkillsProviders && mergedSkillsProviders.length > 0
1610
+ ? mergedSkillsProviders
1611
+ : undefined,
1612
+ skillRenameMap: normalizeSkillRenameMap(mergedSkillRenameMap),
1613
+ };
1614
+ }
1615
+ return mergedEntry;
1320
1616
  }
1321
1617
  function sameRequestedAgentsForMatch(left, right) {
1322
1618
  const normalizedLeft = normalizeRequestedAgentsForMatch(left);
@@ -1421,6 +1717,13 @@ function mergeCommandRenameMaps(existing, updates) {
1421
1717
  };
1422
1718
  return normalizeCommandRenameMap(merged);
1423
1719
  }
1720
+ function mergeRuleRenameMaps(existing, updates) {
1721
+ const merged = {
1722
+ ...(existing ?? {}),
1723
+ ...(updates ?? {}),
1724
+ };
1725
+ return normalizeRuleRenameMap(merged);
1726
+ }
1424
1727
  function resolveMappedTargetFileName(sourceFileName, renameMap) {
1425
1728
  if (!renameMap)
1426
1729
  return undefined;
@@ -1495,6 +1798,53 @@ function normalizeSkillRenameMap(renameMap) {
1495
1798
  return undefined;
1496
1799
  return Object.fromEntries(normalizedEntries);
1497
1800
  }
1801
+ function mergeSkillRenameMaps(existing, updates) {
1802
+ const merged = {
1803
+ ...(existing ?? {}),
1804
+ ...(updates ?? {}),
1805
+ };
1806
+ return normalizeSkillRenameMap(merged);
1807
+ }
1808
+ function mergeImportedSkills(options) {
1809
+ if (!options.existingImportedSkills ||
1810
+ options.existingImportedSkills.length === 0) {
1811
+ return [...options.importedSkills];
1812
+ }
1813
+ const coveredSelectors = new Set();
1814
+ for (const skill of options.selectedSkills) {
1815
+ const byName = normalizeSkillSelector(skill.name);
1816
+ if (byName)
1817
+ coveredSelectors.add(byName);
1818
+ const bySourceDir = normalizeSkillSelector(skill.sourceDirName);
1819
+ if (bySourceDir)
1820
+ coveredSelectors.add(bySourceDir);
1821
+ }
1822
+ if (coveredSelectors.size === 0) {
1823
+ return [...options.importedSkills];
1824
+ }
1825
+ const selectedImportedTargets = new Set();
1826
+ for (const [sourceSelector, importedName] of Object.entries(options.existingSkillRenameMap ?? {})) {
1827
+ const normalizedSourceSelector = normalizeSkillSelector(sourceSelector);
1828
+ const normalizedImportedName = normalizeSkillSelector(importedName);
1829
+ if (!normalizedSourceSelector ||
1830
+ !normalizedImportedName ||
1831
+ !coveredSelectors.has(normalizedSourceSelector)) {
1832
+ continue;
1833
+ }
1834
+ selectedImportedTargets.add(normalizedImportedName);
1835
+ }
1836
+ const retained = options.existingImportedSkills.filter((importedSkillName) => {
1837
+ const selector = normalizeSkillSelector(importedSkillName);
1838
+ if (!selector)
1839
+ return true;
1840
+ if (coveredSelectors.has(selector))
1841
+ return false;
1842
+ if (selectedImportedTargets.has(selector))
1843
+ return false;
1844
+ return true;
1845
+ });
1846
+ return uniqueStrings([...retained, ...options.importedSkills]);
1847
+ }
1498
1848
  function resolveMappedTargetSkillName(sourceSkill, selectedSkills, renameMap) {
1499
1849
  if (!renameMap)
1500
1850
  return undefined;
@@ -914,6 +914,15 @@ async function migrateMcp(options, summary) {
914
914
  writeCanonicalMcp(options.paths, merged);
915
915
  }
916
916
  }
917
+ const MANAGED_MCP_CANONICAL_KEYS_BY_PROVIDER = {
918
+ cursor: ["url", "command", "args", "env"],
919
+ claude: ["type", "url", "command", "args", "env"],
920
+ codex: ["url", "command", "args", "env"],
921
+ opencode: ["url", "command", "args", "env"],
922
+ gemini: ["url", "command", "args", "env"],
923
+ copilot: ["type", "url", "command", "args", "env", "tools"],
924
+ pi: ["url", "command", "args", "env"],
925
+ };
917
926
  function collectProviderMcpServers(paths, providers) {
918
927
  let detected = 0;
919
928
  const servers = new Map();
@@ -930,13 +939,13 @@ function collectProviderMcpServers(paths, providers) {
930
939
  }
931
940
  function readProviderMcp(paths, provider) {
932
941
  if (provider === "cursor") {
933
- return readJsonMcpServers(getCursorMcpPath(paths));
942
+ return readJsonMcpServers(getCursorMcpPath(paths), provider);
934
943
  }
935
944
  if (provider === "claude") {
936
- return readJsonMcpServers(getClaudeMcpPath(paths));
945
+ return readJsonMcpServers(getClaudeMcpPath(paths), provider);
937
946
  }
938
947
  if (provider === "copilot") {
939
- return readJsonMcpServers(getCopilotMcpPath(paths));
948
+ return readJsonMcpServers(getCopilotMcpPath(paths), provider);
940
949
  }
941
950
  if (provider === "opencode") {
942
951
  return readOpenCodeMcp(getOpenCodeConfigPath(paths));
@@ -948,16 +957,16 @@ function readProviderMcp(paths, provider) {
948
957
  return readCodexMcp(getCodexConfigPath(paths));
949
958
  }
950
959
  if (provider === "pi") {
951
- return readJsonMcpServers(getPiMcpPath(paths));
960
+ return readJsonMcpServers(getPiMcpPath(paths), provider);
952
961
  }
953
962
  return {};
954
963
  }
955
- function readJsonMcpServers(filePath) {
964
+ function readJsonMcpServers(filePath, provider) {
956
965
  const parsed = readJsonIfExists(filePath);
957
966
  if (!parsed || !isObject(parsed.mcpServers)) {
958
967
  return {};
959
968
  }
960
- return normalizeMcpServerRecord(parsed.mcpServers);
969
+ return normalizeManagedMcpServerRecord(parsed.mcpServers, MANAGED_MCP_CANONICAL_KEYS_BY_PROVIDER[provider]);
961
970
  }
962
971
  function readOpenCodeMcp(filePath) {
963
972
  const parsed = readJsonIfExists(filePath);
@@ -1013,17 +1022,31 @@ function readCodexMcp(filePath) {
1013
1022
  const parsed = raw.trim() ? TOML.parse(raw) : {};
1014
1023
  if (!isObject(parsed.mcp_servers))
1015
1024
  return {};
1016
- return normalizeMcpServerRecord(parsed.mcp_servers);
1025
+ return normalizeManagedMcpServerRecord(parsed.mcp_servers, MANAGED_MCP_CANONICAL_KEYS_BY_PROVIDER.codex);
1017
1026
  }
1018
- function normalizeMcpServerRecord(raw) {
1027
+ function normalizeManagedMcpServerRecord(raw, allowedKeys) {
1019
1028
  const servers = {};
1020
1029
  for (const [name, config] of Object.entries(raw)) {
1021
- if (!isObject(config))
1030
+ const normalized = pickManagedMcpFields(config, allowedKeys);
1031
+ if (!normalized)
1022
1032
  continue;
1023
- servers[name] = cloneRecord(config);
1033
+ servers[name] = normalized;
1024
1034
  }
1025
1035
  return servers;
1026
1036
  }
1037
+ function pickManagedMcpFields(config, allowedKeys) {
1038
+ if (!isObject(config)) {
1039
+ return null;
1040
+ }
1041
+ const next = {};
1042
+ for (const key of allowedKeys) {
1043
+ const value = config[key];
1044
+ if (value !== undefined) {
1045
+ next[key] = cloneRecord(value);
1046
+ }
1047
+ }
1048
+ return Object.keys(next).length > 0 ? next : null;
1049
+ }
1027
1050
  function normalizeCanonicalServer(server) {
1028
1051
  if (!server) {
1029
1052
  return {
@@ -21,6 +21,9 @@ const ENTITY_NOUN_ALIASES = {
21
21
  rules: "rule",
22
22
  skills: "skill",
23
23
  };
24
+ const VERB_ALIASES = {
25
+ remove: "delete",
26
+ };
24
27
  const ENTITY_VERBS = new Set([
25
28
  "add",
26
29
  "list",
@@ -34,7 +37,7 @@ export function parseCommandRoute(argv) {
34
37
  const rawRoot = argv[0]?.trim().toLowerCase();
35
38
  if (!rawRoot)
36
39
  return null;
37
- const root = ENTITY_NOUN_ALIASES[rawRoot] ?? rawRoot;
40
+ const root = ENTITY_NOUN_ALIASES[rawRoot] ?? VERB_ALIASES[rawRoot] ?? rawRoot;
38
41
  if (AGGREGATE_VERBS.has(root)) {
39
42
  return {
40
43
  mode: "aggregate",
@@ -44,7 +47,8 @@ export function parseCommandRoute(argv) {
44
47
  if (!ENTITY_NOUNS.has(root)) {
45
48
  return null;
46
49
  }
47
- const action = argv[1]?.trim().toLowerCase();
50
+ const rawAction = argv[1]?.trim().toLowerCase();
51
+ const action = rawAction ? (VERB_ALIASES[rawAction] ?? rawAction) : rawAction;
48
52
  if (!action || action === "--help" || action === "-h" || action === "help") {
49
53
  return {
50
54
  mode: "entity",
@@ -53,7 +57,10 @@ export function parseCommandRoute(argv) {
53
57
  };
54
58
  }
55
59
  if (root === "mcp" && action === "server") {
56
- const serverVerb = argv[2]?.trim().toLowerCase();
60
+ const rawServerVerb = argv[2]?.trim().toLowerCase();
61
+ const serverVerb = rawServerVerb
62
+ ? (VERB_ALIASES[rawServerVerb] ?? rawServerVerb)
63
+ : rawServerVerb;
57
64
  if (!serverVerb ||
58
65
  serverVerb === "--help" ||
59
66
  serverVerb === "-h" ||
@@ -361,8 +361,10 @@ function syncProviderMcp(options) {
361
361
  const resolved = resolveMcpForProvider(options.mcp, provider);
362
362
  if (provider === "cursor") {
363
363
  const outputPath = getCursorMcpPath(options.paths);
364
+ const existing = readJsonIfExists(outputPath) ?? {};
364
365
  const payload = {
365
- mcpServers: mapMcpServers(resolved, ["url", "command", "args", "env"]),
366
+ ...existing,
367
+ mcpServers: mergeManagedMcpServerEntries(existing.mcpServers, mapMcpServers(resolved, ["url", "command", "args", "env"]), ["url", "command", "args", "env"]),
366
368
  };
367
369
  maybeWriteJson(outputPath, payload, options.dryRun);
368
370
  options.generated.add(outputPath);
@@ -381,19 +383,24 @@ function syncProviderMcp(options) {
381
383
  continue;
382
384
  }
383
385
  const mcpPath = getClaudeMcpPath(options.paths);
384
- const claudeServers = mapMcpServers(resolved, [
386
+ const existingMcp = readJsonIfExists(mcpPath) ?? {};
387
+ const managedClaudeServers = mapMcpServers(resolved, [
385
388
  "type",
386
389
  "url",
387
390
  "command",
388
391
  "args",
389
392
  "env",
390
393
  ]);
391
- for (const [serverName, config] of Object.entries(claudeServers)) {
394
+ for (const [serverName, config] of Object.entries(managedClaudeServers)) {
392
395
  if (!("type" in config) && typeof config.url === "string") {
393
396
  config.type = "http";
394
397
  }
395
398
  }
396
- maybeWriteJson(mcpPath, { mcpServers: claudeServers }, options.dryRun);
399
+ const claudeServers = mergeManagedMcpServerEntries(existingMcp.mcpServers, managedClaudeServers, ["type", "url", "command", "args", "env"]);
400
+ maybeWriteJson(mcpPath, {
401
+ ...existingMcp,
402
+ mcpServers: claudeServers,
403
+ }, options.dryRun);
397
404
  options.generated.add(mcpPath);
398
405
  settings.enabledMcpjsonServers = Object.keys(claudeServers).sort();
399
406
  maybeWriteJson(settingsPath, settings, options.dryRun);
@@ -403,28 +410,34 @@ function syncProviderMcp(options) {
403
410
  if (provider === "opencode") {
404
411
  const outputPath = getOpenCodeConfigPath(options.paths);
405
412
  const existing = readJsonIfExists(outputPath) ?? {};
406
- const mcp = {};
413
+ const managedMcp = {};
407
414
  for (const [serverName, config] of Object.entries(resolved)) {
408
415
  if (typeof config.url === "string") {
409
- mcp[serverName] = {
416
+ managedMcp[serverName] = {
410
417
  type: "remote",
411
418
  url: config.url,
412
419
  };
413
420
  }
414
421
  else {
415
- mcp[serverName] = {
422
+ managedMcp[serverName] = {
416
423
  type: "local",
417
424
  command: config.command,
418
425
  args: Array.isArray(config.args) ? config.args : undefined,
419
426
  };
420
427
  }
421
428
  if (isObject(config.env)) {
422
- mcp[serverName].environment = config.env;
429
+ managedMcp[serverName].environment = config.env;
423
430
  }
424
431
  }
425
432
  const payload = {
426
433
  ...existing,
427
- mcp,
434
+ mcp: mergeManagedMcpServerEntries(existing.mcp, managedMcp, [
435
+ "type",
436
+ "url",
437
+ "command",
438
+ "args",
439
+ "environment",
440
+ ]),
428
441
  };
429
442
  maybeWriteJson(outputPath, payload, options.dryRun);
430
443
  options.generated.add(outputPath);
@@ -453,7 +466,7 @@ function syncProviderMcp(options) {
453
466
  const payload = {
454
467
  ...existing,
455
468
  experimental,
456
- mcpServers,
469
+ mcpServers: mergeManagedMcpServerEntries(existing.mcpServers, mcpServers, ["httpUrl", "command", "args", "env"]),
457
470
  };
458
471
  maybeWriteJson(outputPath, payload, options.dryRun);
459
472
  options.generated.add(outputPath);
@@ -461,6 +474,8 @@ function syncProviderMcp(options) {
461
474
  }
462
475
  if (provider === "copilot") {
463
476
  const profileMcpPath = getCopilotMcpPath(options.paths);
477
+ const managedKeys = ["type", "url", "command", "args", "env", "tools"];
478
+ const existingProfileMcp = readJsonIfExists(profileMcpPath) ?? {};
464
479
  const copilotServers = mapMcpServers(resolved, [
465
480
  "type",
466
481
  "url",
@@ -477,20 +492,25 @@ function syncProviderMcp(options) {
477
492
  config.type = config.url ? "http" : "local";
478
493
  }
479
494
  }
480
- maybeWriteJson(profileMcpPath, { mcpServers: copilotServers }, options.dryRun);
495
+ maybeWriteJson(profileMcpPath, {
496
+ ...existingProfileMcp,
497
+ mcpServers: mergeManagedMcpServerEntries(existingProfileMcp.mcpServers, copilotServers, managedKeys),
498
+ }, options.dryRun);
481
499
  options.generated.add(profileMcpPath);
482
500
  if (options.paths.scope === "global") {
483
501
  const settingsPath = getVsCodeSettingsPath(options.paths.homeDir);
484
502
  const settings = readJsonIfExists(settingsPath) ?? {};
485
- settings["mcp.servers"] = copilotServers;
503
+ settings["mcp.servers"] = mergeManagedMcpServerEntries(settings["mcp.servers"], copilotServers, managedKeys);
486
504
  maybeWriteJson(settingsPath, settings, options.dryRun);
487
505
  options.generated.add(settingsPath);
488
506
  }
489
507
  }
490
508
  if (provider === "pi") {
491
509
  const outputPath = getPiMcpPath(options.paths);
510
+ const existing = readJsonIfExists(outputPath) ?? {};
492
511
  const payload = {
493
- mcpServers: mapMcpServers(resolved, ["url", "command", "args", "env"]),
512
+ ...existing,
513
+ mcpServers: mergeManagedMcpServerEntries(existing.mcpServers, mapMcpServers(resolved, ["url", "command", "args", "env"]), ["url", "command", "args", "env"]),
494
514
  };
495
515
  maybeWriteJson(outputPath, payload, options.dryRun);
496
516
  options.generated.add(outputPath);
@@ -813,22 +833,19 @@ function syncCodex(options) {
813
833
  let nextServers = [...trackedServers];
814
834
  if (options.includeMcp) {
815
835
  const previousServers = new Set(trackedServers);
836
+ const managedCodexMcpServers = mapMcpServers(options.resolvedMcp, [
837
+ "url",
838
+ "command",
839
+ "args",
840
+ "env",
841
+ ]);
816
842
  for (const oldServer of previousServers) {
817
843
  if (!Object.prototype.hasOwnProperty.call(options.resolvedMcp, oldServer)) {
818
844
  delete mcpServers[oldServer];
819
845
  }
820
846
  }
821
- for (const [serverName, config] of Object.entries(options.resolvedMcp)) {
822
- const mapped = {};
823
- if (typeof config.url === "string")
824
- mapped.url = config.url;
825
- if (typeof config.command === "string")
826
- mapped.command = config.command;
827
- if (Array.isArray(config.args))
828
- mapped.args = config.args;
829
- if (isObject(config.env))
830
- mapped.env = config.env;
831
- mcpServers[serverName] = mapped;
847
+ for (const [serverName, config] of Object.entries(managedCodexMcpServers)) {
848
+ mcpServers[serverName] = mergeManagedMcpServerEntry(mcpServers[serverName], config, ["url", "command", "args", "env"]);
832
849
  }
833
850
  parsed.mcp_servers = mcpServers;
834
851
  nextServers = Object.keys(options.resolvedMcp).sort();
@@ -895,6 +912,28 @@ function mapMcpServers(servers, allowedKeys) {
895
912
  }
896
913
  return mapped;
897
914
  }
915
+ function mergeManagedMcpServerEntries(existingServers, nextServers, managedKeys) {
916
+ const merged = {};
917
+ for (const [serverName, config] of Object.entries(nextServers)) {
918
+ merged[serverName] = mergeManagedMcpServerEntry(isObject(existingServers) ? existingServers[serverName] : undefined, config, managedKeys);
919
+ }
920
+ return merged;
921
+ }
922
+ function mergeManagedMcpServerEntry(existingConfig, nextConfig, managedKeys) {
923
+ const merged = isObject(existingConfig) ? cloneSyncValue(existingConfig) : {};
924
+ for (const key of managedKeys) {
925
+ delete merged[key];
926
+ }
927
+ for (const [key, value] of Object.entries(nextConfig)) {
928
+ if (value !== undefined) {
929
+ merged[key] = cloneSyncValue(value);
930
+ }
931
+ }
932
+ return merged;
933
+ }
934
+ function cloneSyncValue(value) {
935
+ return JSON.parse(JSON.stringify(value));
936
+ }
898
937
  function maybeWriteJson(filePath, payload, dryRun) {
899
938
  if (dryRun)
900
939
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentloom",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
5
5
  "type": "module",
6
6
  "bin": {