agentloom 0.1.10 → 0.1.12

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
@@ -12,6 +12,8 @@ npx agentloom init
12
12
 
13
13
  That's all you need. Agentloom picks up your existing provider configs, migrates them into a unified `.agents/` directory, and syncs everything back out to all your tools. From here on, manage your agents, commands, rules, skills, and MCP servers in one place and run `agentloom sync` whenever you make changes.
14
14
 
15
+ `agentloom init` is the provider-to-canonical bootstrap step. After that, `agentloom sync` is one-way: it reads from `.agents/` and writes provider-native outputs. If you intentionally want to pull provider state back into canonical `.agents/`, rerun `agentloom init`.
16
+
15
17
  ## Install
16
18
 
17
19
  ```bash
@@ -72,7 +74,7 @@ Source path resolution is additive and priority-ordered:
72
74
  - Agents: `.agents/agents` -> `agents`
73
75
  - Commands: `.agents/commands` -> `commands` -> `prompts` -> provider fallbacks `.github/prompts` + `.gemini/commands`
74
76
  - Rules: `.agents/rules` -> `rules`
75
- - Skills: `.agents/skills` -> `skills` -> root `SKILL.md` fallback
77
+ - Skills: `.agents/skills` -> `skills` -> root `SKILL.md` -> root `<name>/SKILL.md` fallback
76
78
  - MCP: `.agents/mcp.json` -> `mcp.json`
77
79
 
78
80
  Aggregate `agentloom add <source>` can import command/skill/MCP-only repositories even when no `agents/` directory exists.
@@ -10,5 +10,6 @@ export async function runInitCommand(argv, cwd) {
10
10
  cwd,
11
11
  target: "all",
12
12
  skipSync: Boolean(argv["no-sync"]),
13
+ migrateProviderState: true,
13
14
  });
14
15
  }
@@ -6,4 +6,5 @@ export declare function runScopedSyncCommand(options: {
6
6
  cwd: string;
7
7
  target: EntityType | "all";
8
8
  skipSync?: boolean;
9
+ migrateProviderState?: boolean;
9
10
  }): Promise<void>;
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { parseProvidersFlag } from "../core/argv.js";
5
5
  import { getSyncHelpText } from "../core/copy.js";
6
6
  import { formatMigrationSummary, initializeCanonicalLayout, migrateProviderStateToCanonical, MigrationConflictError, } from "../core/migration.js";
7
+ import { hasInitializedCanonicalLayout, resolveScopeForSync, } from "../core/scope.js";
7
8
  import { getNonInteractiveMode, resolvePathsForCommand, } from "./entity-utils.js";
8
9
  import { formatSyncSummary, resolveProvidersForSync, syncFromCanonical, } from "../sync/index.js";
9
10
  export async function runSyncCommand(argv, cwd) {
@@ -21,29 +22,42 @@ export async function runScopedSyncCommand(options) {
21
22
  const nonInteractive = getNonInteractiveMode(options.argv);
22
23
  let cleanupDryRunPaths;
23
24
  try {
24
- const paths = await resolvePathsForCommand(options.argv, options.cwd);
25
+ const shouldMigrateProviderState = Boolean(options.migrateProviderState);
26
+ const paths = shouldMigrateProviderState
27
+ ? await resolvePathsForCommand(options.argv, options.cwd)
28
+ : await resolveScopeForSync({
29
+ cwd: options.cwd,
30
+ global: Boolean(options.argv.global),
31
+ local: Boolean(options.argv.local),
32
+ interactive: !nonInteractive,
33
+ });
25
34
  const explicitProviders = parseProvidersFlag(options.argv.providers);
26
35
  const providers = await resolveProvidersForSync({
27
36
  paths,
28
37
  explicitProviders,
29
38
  nonInteractive,
30
39
  });
40
+ if (!shouldMigrateProviderState) {
41
+ assertInitializedCanonicalStateExists(paths);
42
+ }
31
43
  const dryRun = Boolean(options.argv["dry-run"]);
32
44
  const effectivePaths = dryRun
33
45
  ? createDryRunCanonicalPaths(paths)
34
46
  : { paths, cleanup: undefined };
35
47
  cleanupDryRunPaths = effectivePaths.cleanup;
36
48
  initializeCanonicalLayout(effectivePaths.paths, providers);
37
- const migrationSummary = await migrateProviderStateToCanonical({
38
- paths: effectivePaths.paths,
39
- providers,
40
- target: options.target,
41
- yes: Boolean(options.argv.yes),
42
- nonInteractive,
43
- dryRun,
44
- materializeCanonical: dryRun,
45
- });
46
- console.log(formatMigrationSummary(migrationSummary));
49
+ if (shouldMigrateProviderState) {
50
+ const migrationSummary = await migrateProviderStateToCanonical({
51
+ paths: effectivePaths.paths,
52
+ providers,
53
+ target: options.target,
54
+ yes: Boolean(options.argv.yes),
55
+ nonInteractive,
56
+ dryRun,
57
+ materializeCanonical: dryRun,
58
+ });
59
+ console.log(formatMigrationSummary(migrationSummary));
60
+ }
47
61
  if (options.skipSync) {
48
62
  return;
49
63
  }
@@ -69,6 +83,15 @@ export async function runScopedSyncCommand(options) {
69
83
  cleanupDryRunPaths?.();
70
84
  }
71
85
  }
86
+ function assertInitializedCanonicalStateExists(paths) {
87
+ if (hasInitializedCanonicalLayout(paths)) {
88
+ return;
89
+ }
90
+ const initCommand = paths.scope === "global"
91
+ ? "agentloom init --global"
92
+ : "agentloom init --local";
93
+ throw new Error(`No initialized canonical .agents state found at ${paths.agentsRoot}.\nRun \`${initCommand}\` to bootstrap from provider configs first, or use \`agentloom add\` to create canonical content before syncing.`);
94
+ }
72
95
  function createDryRunCanonicalPaths(paths) {
73
96
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agentloom-dry-run-"));
74
97
  const tempAgentsRoot = path.join(tempRoot, ".agents");
package/dist/core/copy.js CHANGED
@@ -9,11 +9,11 @@ Usage:
9
9
 
10
10
  Aggregate commands:
11
11
  add <source> Import agents/commands/mcp/rules/skills from a source
12
- init Bootstrap canonical files, migrate providers, then sync
12
+ init Bootstrap canonical files from provider configs, then sync
13
13
  find <query> Search remote + local entities
14
14
  update [source] Refresh lockfile-managed imports
15
15
  upgrade Install the latest CLI release
16
- sync Migrate provider configs then generate provider outputs
16
+ sync Generate provider outputs from canonical .agents
17
17
  delete <source|name...> Delete imported entities by source or name(s)
18
18
 
19
19
  Entity commands:
@@ -76,7 +76,7 @@ Source discovery:
76
76
  agents: .agents/agents -> agents
77
77
  commands: .agents/commands -> commands -> prompts -> (.github/prompts + .gemini/commands fallback)
78
78
  rules: .agents/rules -> rules
79
- skills: .agents/skills -> skills -> root SKILL.md
79
+ skills: .agents/skills -> skills -> root SKILL.md -> root <name>/SKILL.md
80
80
 
81
81
  Usage:
82
82
  agentloom add <source> [options]
@@ -134,7 +134,9 @@ Behavior:
134
134
  `;
135
135
  }
136
136
  export function getSyncHelpText() {
137
- return `Migrate provider configs into canonical .agents data, then generate provider-specific outputs.
137
+ return `Generate provider-specific outputs from canonical .agents data.
138
+
139
+ Use \`agentloom init\` when you want to bootstrap or re-import provider configs into canonical state.
138
140
 
139
141
  Usage:
140
142
  agentloom sync [options]
@@ -148,7 +150,7 @@ Options:
148
150
  `;
149
151
  }
150
152
  export function getInitHelpText() {
151
- return `Bootstrap canonical .agents files, migrate provider configs into canonical state, and sync providers.
153
+ return `Bootstrap canonical .agents files from existing provider configs, then sync providers.
152
154
 
153
155
  Usage:
154
156
  agentloom init [options]
@@ -121,7 +121,7 @@ export async function importSource(options) {
121
121
  if (shouldImportSkills &&
122
122
  options.requireSkills &&
123
123
  sourceSkillsDirs.length === 0) {
124
- throw new Error(`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md, including plugin sources declared in .claude-plugin/marketplace.json).`);
124
+ throw new Error(`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, root SKILL.md, or root <name>/SKILL.md directories, including plugin sources declared in .claude-plugin/marketplace.json).`);
125
125
  }
126
126
  if (shouldImportSkills &&
127
127
  options.requireSkills &&
@@ -134,7 +134,7 @@ export async function importSource(options) {
134
134
  sourceRules.length === 0 &&
135
135
  sourceSkills.length === 0 &&
136
136
  Object.keys(sourceMcp?.mcpServers ?? {}).length === 0) {
137
- throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, or plugin sources from .claude-plugin/marketplace.json.`);
137
+ throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, root SKILL.md, root <name>/SKILL.md directories, or plugin sources from .claude-plugin/marketplace.json.`);
138
138
  }
139
139
  const shouldResolveAgents = shouldImportAgents &&
140
140
  (sourceAgents.length > 0 ||
@@ -5,5 +5,7 @@ export interface ScopeResolutionOptions {
5
5
  local?: boolean;
6
6
  interactive?: boolean;
7
7
  }
8
+ export declare function hasInitializedCanonicalLayout(paths: Pick<ScopePaths, "agentsRoot" | "agentsDir" | "commandsDir" | "rulesDir" | "skillsDir" | "mcpPath" | "lockPath" | "manifestPath">): boolean;
8
9
  export declare function buildScopePaths(cwd: string, scope: Scope, homeDir?: string): ScopePaths;
9
10
  export declare function resolveScope(options: ScopeResolutionOptions): Promise<ScopePaths>;
11
+ export declare function resolveScopeForSync(options: ScopeResolutionOptions): Promise<ScopePaths>;
@@ -3,6 +3,24 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { cancel, isCancel, select } from "@clack/prompts";
5
5
  import { getGlobalSettingsPath, readSettings } from "./settings.js";
6
+ function directoryHasEntries(dirPath) {
7
+ return fs.existsSync(dirPath) && fs.readdirSync(dirPath).length > 0;
8
+ }
9
+ export function hasInitializedCanonicalLayout(paths) {
10
+ if (!fs.existsSync(paths.agentsRoot) ||
11
+ !fs.statSync(paths.agentsRoot).isDirectory()) {
12
+ return false;
13
+ }
14
+ if (fs.existsSync(paths.mcpPath) ||
15
+ fs.existsSync(paths.lockPath) ||
16
+ fs.existsSync(paths.manifestPath)) {
17
+ return true;
18
+ }
19
+ return (directoryHasEntries(paths.agentsDir) ||
20
+ directoryHasEntries(paths.commandsDir) ||
21
+ directoryHasEntries(paths.rulesDir) ||
22
+ directoryHasEntries(paths.skillsDir));
23
+ }
6
24
  export function buildScopePaths(cwd, scope, homeDir = os.homedir()) {
7
25
  const workspaceRoot = cwd;
8
26
  const agentsRoot = scope === "local"
@@ -37,33 +55,76 @@ export async function resolveScope(options) {
37
55
  if (!interactive) {
38
56
  return buildScopePaths(cwd, hasLocalAgents ? "local" : "global");
39
57
  }
40
- const globalSettings = readSettings(getGlobalSettingsPath());
41
- const defaultScope = globalSettings.lastScope === "local" ? "local" : "global";
58
+ const defaultScope = getDefaultScope();
59
+ const selected = await promptForScopeSelection({
60
+ hasLocalAgents,
61
+ defaultScope,
62
+ });
63
+ return buildScopePaths(cwd, selected);
64
+ }
65
+ export async function resolveScopeForSync(options) {
66
+ const { cwd } = options;
67
+ if (options.global && options.local) {
68
+ throw new Error("Use either --global or --local, not both.");
69
+ }
70
+ if (options.global)
71
+ return buildScopePaths(cwd, "global");
72
+ if (options.local)
73
+ return buildScopePaths(cwd, "local");
74
+ const localPaths = buildScopePaths(cwd, "local");
75
+ const globalPaths = buildScopePaths(cwd, "global");
76
+ const hasLocalAgents = fs.existsSync(localPaths.agentsRoot);
77
+ const hasLocalCanonical = hasInitializedCanonicalLayout(localPaths);
78
+ const hasGlobalCanonical = hasInitializedCanonicalLayout(globalPaths);
79
+ const interactive = options.interactive ?? (process.stdin.isTTY && process.stdout.isTTY);
80
+ if (!interactive) {
81
+ return hasLocalAgents ? localPaths : globalPaths;
82
+ }
83
+ if (hasLocalAgents && hasGlobalCanonical) {
84
+ const selected = await promptForScopeSelection({
85
+ hasLocalAgents: true,
86
+ defaultScope: getDefaultScope(globalPaths.homeDir),
87
+ });
88
+ return buildScopePaths(cwd, selected, globalPaths.homeDir);
89
+ }
90
+ if (hasLocalCanonical)
91
+ return localPaths;
92
+ if (hasGlobalCanonical)
93
+ return globalPaths;
94
+ if (hasLocalAgents)
95
+ return localPaths;
96
+ throw new Error(`No initialized canonical .agents state found at ${localPaths.agentsRoot} or ${globalPaths.agentsRoot}.\nRun \`agentloom init --local\` or \`agentloom init --global\` to bootstrap from provider configs first, or use \`agentloom add\` to create canonical content before syncing.`);
97
+ }
98
+ function getDefaultScope(homeDir = os.homedir()) {
99
+ const globalSettings = readSettings(getGlobalSettingsPath(homeDir));
100
+ return globalSettings.lastScope === "local" ? "local" : "global";
101
+ }
102
+ async function promptForScopeSelection(options) {
42
103
  const selected = await select({
43
104
  message: "Choose scope for this command",
44
105
  options: [
45
106
  {
46
107
  value: "local",
47
108
  label: ".agents in this repository",
48
- hint: hasLocalAgents
49
- ? defaultScope === "local"
109
+ hint: options.hasLocalAgents
110
+ ? options.defaultScope === "local"
50
111
  ? "default"
51
112
  : undefined
52
- : defaultScope === "local"
113
+ : options.defaultScope === "local"
53
114
  ? "default (creates .agents)"
54
115
  : "creates .agents",
55
116
  },
56
117
  {
57
118
  value: "global",
58
119
  label: "~/.agents shared config",
59
- hint: defaultScope === "global" ? "default" : undefined,
120
+ hint: options.defaultScope === "global" ? "default" : undefined,
60
121
  },
61
122
  ],
62
- initialValue: defaultScope,
123
+ initialValue: options.defaultScope,
63
124
  });
64
125
  if (isCancel(selected)) {
65
126
  cancel("Operation cancelled.");
66
127
  process.exit(1);
67
128
  }
68
- return buildScopePaths(cwd, selected);
129
+ return selected;
69
130
  }
@@ -209,6 +209,9 @@ function discoverSourceSkillsDirsForRoot(importRoot) {
209
209
  if (fs.existsSync(rootSkill) && fs.statSync(rootSkill).isFile()) {
210
210
  return [importRoot];
211
211
  }
212
+ if (hasImmediateRootSkillDirs(importRoot)) {
213
+ return [importRoot];
214
+ }
212
215
  return [];
213
216
  }
214
217
  function discoverSourceRulesDirsForRoot(importRoot) {
@@ -225,6 +228,18 @@ function discoverSourceRulesDirsForRoot(importRoot) {
225
228
  function dedupePaths(paths) {
226
229
  return [...new Set(paths)];
227
230
  }
231
+ function hasImmediateRootSkillDirs(importRoot) {
232
+ for (const entry of fs.readdirSync(importRoot, { withFileTypes: true })) {
233
+ if (!entry.isDirectory()) {
234
+ continue;
235
+ }
236
+ const skillFile = path.join(importRoot, entry.name, "SKILL.md");
237
+ if (fs.existsSync(skillFile) && fs.statSync(skillFile).isFile()) {
238
+ return true;
239
+ }
240
+ }
241
+ return false;
242
+ }
228
243
  function isPathWithinRoot(rootPath, targetPath) {
229
244
  const relative = path.relative(rootPath, targetPath);
230
245
  return (relative === "" ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentloom",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Unified agent and MCP sync CLI for multi-provider AI tooling",
5
5
  "type": "module",
6
6
  "bin": {