agentloom 0.1.8 → 0.1.9

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";
@@ -136,7 +137,11 @@ async function runEntityAwareUpdate(options) {
136
137
  if (updatePlan.skillRenameMap) {
137
138
  importOptions.skillRenameMap = updatePlan.skillRenameMap;
138
139
  }
139
- await importSource(importOptions);
140
+ const summary = await importSource(importOptions);
141
+ await sendAddTelemetryEvent({
142
+ rawSource: entry.source,
143
+ summary,
144
+ });
140
145
  updated += 1;
141
146
  }
142
147
  catch (err) {
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;
@@ -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" ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentloom",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
5
5
  "type": "module",
6
6
  "bin": {