@phnx-labs/agents-cli 1.20.3 → 1.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +48 -17
  3. package/dist/commands/cli.js +1 -1
  4. package/dist/commands/cloud.js +1 -1
  5. package/dist/commands/commands.js +2 -0
  6. package/dist/commands/doctor.js +1 -1
  7. package/dist/commands/exec.js +52 -16
  8. package/dist/commands/hooks.js +6 -6
  9. package/dist/commands/import.js +90 -37
  10. package/dist/commands/inspect.d.ts +26 -0
  11. package/dist/commands/inspect.js +590 -0
  12. package/dist/commands/mcp.js +17 -16
  13. package/dist/commands/models.js +1 -1
  14. package/dist/commands/packages.js +6 -4
  15. package/dist/commands/permissions.js +13 -12
  16. package/dist/commands/plugins.d.ts +13 -0
  17. package/dist/commands/plugins.js +100 -11
  18. package/dist/commands/prune.js +3 -2
  19. package/dist/commands/pull.d.ts +12 -5
  20. package/dist/commands/pull.js +26 -422
  21. package/dist/commands/push.d.ts +14 -0
  22. package/dist/commands/push.js +30 -0
  23. package/dist/commands/repo.d.ts +1 -1
  24. package/dist/commands/repo.js +155 -112
  25. package/dist/commands/resource-view.d.ts +2 -0
  26. package/dist/commands/resource-view.js +12 -3
  27. package/dist/commands/routines.js +32 -7
  28. package/dist/commands/rules.js +1 -1
  29. package/dist/commands/sessions.js +1 -0
  30. package/dist/commands/setup.d.ts +3 -3
  31. package/dist/commands/setup.js +15 -15
  32. package/dist/commands/skills.js +6 -5
  33. package/dist/commands/subagents.js +5 -4
  34. package/dist/commands/sync.d.ts +18 -5
  35. package/dist/commands/sync.js +251 -65
  36. package/dist/commands/teams.js +1 -0
  37. package/dist/commands/tmux.d.ts +25 -0
  38. package/dist/commands/tmux.js +415 -0
  39. package/dist/commands/trash.d.ts +2 -2
  40. package/dist/commands/trash.js +1 -1
  41. package/dist/commands/versions.js +2 -2
  42. package/dist/commands/view.js +14 -4
  43. package/dist/commands/workflows.js +4 -3
  44. package/dist/commands/worktree.d.ts +4 -5
  45. package/dist/commands/worktree.js +4 -4
  46. package/dist/index.js +68 -20
  47. package/dist/lib/agents.d.ts +19 -10
  48. package/dist/lib/agents.js +102 -28
  49. package/dist/lib/auto-pull-worker.d.ts +1 -1
  50. package/dist/lib/auto-pull-worker.js +2 -2
  51. package/dist/lib/auto-pull.d.ts +1 -1
  52. package/dist/lib/auto-pull.js +1 -1
  53. package/dist/lib/beta.d.ts +1 -1
  54. package/dist/lib/beta.js +1 -1
  55. package/dist/lib/capabilities.js +2 -0
  56. package/dist/lib/commands.d.ts +28 -1
  57. package/dist/lib/commands.js +125 -20
  58. package/dist/lib/doctor-diff.js +2 -2
  59. package/dist/lib/exec.d.ts +14 -0
  60. package/dist/lib/exec.js +39 -5
  61. package/dist/lib/fuzzy.d.ts +12 -2
  62. package/dist/lib/fuzzy.js +29 -4
  63. package/dist/lib/git.js +8 -1
  64. package/dist/lib/hooks.d.ts +2 -2
  65. package/dist/lib/hooks.js +97 -10
  66. package/dist/lib/import.d.ts +21 -0
  67. package/dist/lib/import.js +55 -2
  68. package/dist/lib/mcp.js +32 -2
  69. package/dist/lib/migrate.d.ts +51 -0
  70. package/dist/lib/migrate.js +227 -1
  71. package/dist/lib/models.js +62 -15
  72. package/dist/lib/permissions.d.ts +36 -2
  73. package/dist/lib/permissions.js +217 -7
  74. package/dist/lib/plugin-marketplace.d.ts +108 -40
  75. package/dist/lib/plugin-marketplace.js +243 -94
  76. package/dist/lib/plugins.d.ts +21 -4
  77. package/dist/lib/plugins.js +130 -49
  78. package/dist/lib/profiles-presets.js +12 -12
  79. package/dist/lib/project-launch.d.ts +65 -0
  80. package/dist/lib/project-launch.js +367 -0
  81. package/dist/lib/pty-client.js +1 -1
  82. package/dist/lib/pty-server.d.ts +1 -1
  83. package/dist/lib/pty-server.js +28 -4
  84. package/dist/lib/refresh.d.ts +26 -0
  85. package/dist/lib/refresh.js +315 -0
  86. package/dist/lib/resource-patterns.d.ts +1 -1
  87. package/dist/lib/resource-patterns.js +1 -1
  88. package/dist/lib/resources/commands.js +2 -2
  89. package/dist/lib/resources/hooks.d.ts +1 -1
  90. package/dist/lib/resources/hooks.js +1 -1
  91. package/dist/lib/resources/mcp.d.ts +1 -1
  92. package/dist/lib/resources/mcp.js +5 -6
  93. package/dist/lib/resources/permissions.js +5 -2
  94. package/dist/lib/resources/rules.js +3 -2
  95. package/dist/lib/resources/skills.js +3 -2
  96. package/dist/lib/resources/types.d.ts +1 -1
  97. package/dist/lib/resources.js +2 -2
  98. package/dist/lib/rotate.d.ts +1 -1
  99. package/dist/lib/rotate.js +1 -1
  100. package/dist/lib/routines.d.ts +16 -4
  101. package/dist/lib/routines.js +67 -17
  102. package/dist/lib/rules/compile.js +22 -10
  103. package/dist/lib/rules/rules.js +3 -3
  104. package/dist/lib/runner.js +16 -3
  105. package/dist/lib/scheduler.js +15 -1
  106. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  107. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  108. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +9 -1
  109. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  110. package/dist/lib/secrets/linux.d.ts +44 -9
  111. package/dist/lib/secrets/linux.js +302 -48
  112. package/dist/lib/session/db.js +15 -2
  113. package/dist/lib/session/discover.js +118 -3
  114. package/dist/lib/session/parse.js +3 -0
  115. package/dist/lib/session/types.d.ts +1 -1
  116. package/dist/lib/session/types.js +1 -1
  117. package/dist/lib/shims.d.ts +10 -9
  118. package/dist/lib/shims.js +101 -50
  119. package/dist/lib/skills.d.ts +1 -1
  120. package/dist/lib/skills.js +10 -9
  121. package/dist/lib/staleness/detectors/commands.d.ts +3 -0
  122. package/dist/lib/staleness/detectors/commands.js +46 -0
  123. package/dist/lib/staleness/detectors/hooks.d.ts +3 -0
  124. package/dist/lib/staleness/detectors/hooks.js +44 -0
  125. package/dist/lib/staleness/detectors/mcp.d.ts +3 -0
  126. package/dist/lib/staleness/detectors/mcp.js +31 -0
  127. package/dist/lib/staleness/detectors/permissions.d.ts +3 -0
  128. package/dist/lib/staleness/detectors/permissions.js +201 -0
  129. package/dist/lib/staleness/detectors/plugins.d.ts +8 -0
  130. package/dist/lib/staleness/detectors/plugins.js +23 -0
  131. package/dist/lib/staleness/detectors/rules.d.ts +3 -0
  132. package/dist/lib/staleness/detectors/rules.js +34 -0
  133. package/dist/lib/staleness/detectors/skills.d.ts +3 -0
  134. package/dist/lib/staleness/detectors/skills.js +71 -0
  135. package/dist/lib/staleness/detectors/subagents.d.ts +3 -0
  136. package/dist/lib/staleness/detectors/subagents.js +50 -0
  137. package/dist/lib/staleness/detectors/types.d.ts +22 -0
  138. package/dist/lib/staleness/detectors/types.js +1 -0
  139. package/dist/lib/staleness/detectors/workflows.d.ts +3 -0
  140. package/dist/lib/staleness/detectors/workflows.js +28 -0
  141. package/dist/lib/staleness/registry.d.ts +26 -0
  142. package/dist/lib/staleness/registry.js +123 -0
  143. package/dist/lib/staleness/writers/commands.d.ts +3 -0
  144. package/dist/lib/staleness/writers/commands.js +111 -0
  145. package/dist/lib/staleness/writers/hooks.d.ts +3 -0
  146. package/dist/lib/staleness/writers/hooks.js +47 -0
  147. package/dist/lib/staleness/writers/kinds.d.ts +10 -0
  148. package/dist/lib/staleness/writers/kinds.js +15 -0
  149. package/dist/lib/staleness/writers/lazy-map.d.ts +13 -0
  150. package/dist/lib/staleness/writers/lazy-map.js +19 -0
  151. package/dist/lib/staleness/writers/mcp.d.ts +10 -0
  152. package/dist/lib/staleness/writers/mcp.js +19 -0
  153. package/dist/lib/staleness/writers/permissions.d.ts +13 -0
  154. package/dist/lib/staleness/writers/permissions.js +26 -0
  155. package/dist/lib/staleness/writers/plugins.d.ts +7 -0
  156. package/dist/lib/staleness/writers/plugins.js +31 -0
  157. package/dist/lib/staleness/writers/rules.d.ts +7 -0
  158. package/dist/lib/staleness/writers/rules.js +55 -0
  159. package/dist/lib/staleness/writers/skills.d.ts +3 -0
  160. package/dist/lib/staleness/writers/skills.js +81 -0
  161. package/dist/lib/staleness/writers/sources.d.ts +16 -0
  162. package/dist/lib/staleness/writers/sources.js +72 -0
  163. package/dist/lib/staleness/writers/subagents.d.ts +3 -0
  164. package/dist/lib/staleness/writers/subagents.js +53 -0
  165. package/dist/lib/staleness/writers/types.d.ts +36 -0
  166. package/dist/lib/staleness/writers/types.js +1 -0
  167. package/dist/lib/staleness/writers/workflows.d.ts +7 -0
  168. package/dist/lib/staleness/writers/workflows.js +31 -0
  169. package/dist/lib/state.d.ts +34 -11
  170. package/dist/lib/state.js +58 -13
  171. package/dist/lib/subagents.d.ts +0 -2
  172. package/dist/lib/subagents.js +6 -6
  173. package/dist/lib/teams/agents.js +1 -1
  174. package/dist/lib/teams/parsers.d.ts +1 -1
  175. package/dist/lib/tmux/binary.d.ts +67 -0
  176. package/dist/lib/tmux/binary.js +141 -0
  177. package/dist/lib/tmux/index.d.ts +8 -0
  178. package/dist/lib/tmux/index.js +8 -0
  179. package/dist/lib/tmux/paths.d.ts +17 -0
  180. package/dist/lib/tmux/paths.js +30 -0
  181. package/dist/lib/tmux/session.d.ts +122 -0
  182. package/dist/lib/tmux/session.js +305 -0
  183. package/dist/lib/types.d.ts +58 -7
  184. package/dist/lib/types.js +1 -1
  185. package/dist/lib/usage.js +1 -1
  186. package/dist/lib/versions.d.ts +4 -4
  187. package/dist/lib/versions.js +154 -491
  188. package/dist/lib/workflows.d.ts +2 -4
  189. package/dist/lib/workflows.js +3 -4
  190. package/package.json +7 -7
  191. package/scripts/postinstall.js +16 -63
  192. package/dist/commands/status.d.ts +0 -9
  193. package/dist/commands/status.js +0 -25
@@ -1,70 +1,222 @@
1
1
  /**
2
- * Native plugin marketplace install path for Claude / OpenClaw.
2
+ * Native plugin marketplaces for Claude / OpenClaw — one per DotAgents repo.
3
3
  *
4
- * Plugins managed by agents-cli are exposed as a synthetic local marketplace
5
- * named "agents-cli" under each version's plugin directory:
4
+ * Every DotAgents repo that holds plugins synthesizes its OWN synthetic local
5
+ * marketplace under each version's plugin directory, named after the repo:
6
6
  *
7
7
  * <versionHome>/.{claude,openclaw}/plugins/
8
- * known_marketplaces.json # registers the "agents-cli" marketplace
9
- * marketplaces/agents-cli/
10
- * .claude-plugin/marketplace.json # synthesized catalog
11
- * plugins/<plugin>/ # copied plugin source
8
+ * known_marketplaces.json # registers each repo's marketplace
9
+ * marketplaces/agents-cli/ # ~/.agents/plugins/* (user repo)
10
+ * marketplaces/agents-<alias>/ # ~/.agents-<alias>/plugins/* (extra repo)
11
+ * marketplaces/agents-project/ # <cwd>/.agents/plugins/* (project repo)
12
+ * .claude-plugin/marketplace.json # synthesized catalog
13
+ * plugins/<plugin>/ # copied plugin source
12
14
  *
13
- * Plus the version's settings.json gets `enabledPlugins["<plugin>@agents-cli"] = true`.
15
+ * Plus the version's settings.json gets
16
+ * `enabledPlugins["<plugin>@<marketplace>"] = true`.
14
17
  *
15
- * This produces native `/plugin:skill` slash namespacing, visibility in `/plugins`,
16
- * and `/plugin enable|disable` support matching the Claude Code spec at
17
- * https://code.claude.com/docs/en/plugins and /plugin-marketplaces.
18
+ * This produces native `/plugin:skill` slash namespacing, visibility in
19
+ * `/plugins`, `/plugin enable|disable` support, AND honest attribution (the
20
+ * user can see which repo each plugin came from) — matching the Claude Code
21
+ * spec at https://code.claude.com/docs/en/plugins and /plugin-marketplaces.
22
+ *
23
+ * The naming policy lives in one place — marketplaceNameFor(). Source-side
24
+ * discovery (discoverMarketplaces) and per-version synthesis (syncMarketplaceManifest
25
+ * / registerMarketplace / syncAllMarketplaces) all key off a MarketplaceSpec so
26
+ * the catalog name and on-disk layout are derived, never hard-coded per call.
18
27
  */
19
28
  import * as fs from 'fs';
20
29
  import * as path from 'path';
30
+ import { getPluginsDir, getEnabledExtraRepos, getProjectPluginsDir } from './state.js';
31
+ import { agentConfigDirName } from './agents.js';
32
+ /**
33
+ * Canonical name for the user-repo marketplace (~/.agents/plugins/). Kept as an
34
+ * exported constant for callers that operate on the user repo directly and for
35
+ * the `marketplaces/agents-cli/` on-disk path that existing installs already have.
36
+ */
21
37
  export const MARKETPLACE_NAME = 'agents-cli';
38
+ export const SYSTEM_MARKETPLACE_NAME = 'agents-system';
39
+ /** Marketplace name for <cwd>/.agents/plugins/*. */
40
+ export const PROJECT_MARKETPLACE_NAME = 'agents-project';
41
+ // ─── Naming policy (single source of truth) ──────────────────────────────────
42
+ /**
43
+ * Map a MarketplaceSpec to its catalog name. This is the ONLY place that
44
+ * encodes the repo → name policy; every other function derives the name here.
45
+ */
46
+ export function marketplaceNameFor(spec) {
47
+ switch (spec.kind) {
48
+ case 'user': return MARKETPLACE_NAME; // "agents-cli"
49
+ case 'extra': return `agents-${spec.alias}`; // e.g. "agents-extras"
50
+ case 'project': return PROJECT_MARKETPLACE_NAME; // "agents-project"
51
+ case 'system': return SYSTEM_MARKETPLACE_NAME; // "agents-system"
52
+ }
53
+ }
54
+ /** Resolve a spec-or-name argument to the bare marketplace name string. */
55
+ function nameOf(specOrName) {
56
+ return typeof specOrName === 'string' ? specOrName : marketplaceNameFor(specOrName);
57
+ }
58
+ function descriptionFor(spec) {
59
+ switch (spec.kind) {
60
+ case 'user': return 'Plugins from the user repo (~/.agents/plugins/)';
61
+ case 'extra': return `Plugins from extra repo "${spec.alias}" (~/.agents-${spec.alias}/plugins/)`;
62
+ case 'project': return 'Project-scoped plugins from <cwd>/.agents/plugins/';
63
+ case 'system': return 'Plugins from the system repo (~/.agents/.system/plugins/)';
64
+ }
65
+ }
66
+ // ─── Source-side discovery ────────────────────────────────────────────────────
67
+ /**
68
+ * Discover every DotAgents repo that contributes plugins, in precedence order
69
+ * (user, then each enabled extra repo, then the project repo when cwd has one).
70
+ * No agent / version is involved — this walks source-side plugin roots only.
71
+ *
72
+ * A repo is included when its plugins/ directory exists on disk. The user repo
73
+ * is always probed; extras come from getEnabledExtraRepos() (already filtered to
74
+ * enabled + on-disk repos); the project repo is included only when
75
+ * <cwd>/.agents/plugins/ exists.
76
+ */
77
+ export function discoverMarketplaces(opts = {}) {
78
+ const out = [];
79
+ // User repo — always the canonical "agents-cli" marketplace.
80
+ const userRoot = getPluginsDir();
81
+ if (dirExists(userRoot)) {
82
+ const spec = { kind: 'user' };
83
+ out.push({ spec, name: marketplaceNameFor(spec), pluginsRoot: userRoot, description: descriptionFor(spec) });
84
+ }
85
+ // Extra repos — peer ~/.agents-<alias>/ clones (and user-owned path:-repos).
86
+ for (const extra of getEnabledExtraRepos()) {
87
+ const pluginsRoot = path.join(extra.dir, 'plugins');
88
+ if (!dirExists(pluginsRoot))
89
+ continue;
90
+ const spec = { kind: 'extra', alias: extra.alias, root: pluginsRoot };
91
+ out.push({ spec, name: marketplaceNameFor(spec), pluginsRoot, description: descriptionFor(spec) });
92
+ }
93
+ // Project repo — <cwd>/.agents/plugins/.
94
+ const projectRoot = getProjectPluginsDir(opts.cwd ?? process.cwd());
95
+ if (projectRoot && dirExists(projectRoot)) {
96
+ const spec = { kind: 'project', root: projectRoot };
97
+ out.push({ spec, name: marketplaceNameFor(spec), pluginsRoot: projectRoot, description: descriptionFor(spec) });
98
+ }
99
+ return out;
100
+ }
101
+ function dirExists(p) {
102
+ try {
103
+ return fs.statSync(p).isDirectory();
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ // ─── Per-version paths ────────────────────────────────────────────────────────
22
110
  function pluginsRootForVersion(agent, versionHome) {
23
- return path.join(versionHome, `.${agent}`, 'plugins');
111
+ return path.join(versionHome, agentConfigDirName(agent), 'plugins');
24
112
  }
25
- export function marketplaceRoot(agent, versionHome) {
26
- return path.join(pluginsRootForVersion(agent, versionHome), 'marketplaces', MARKETPLACE_NAME);
113
+ export function marketplaceRoot(specOrName, agent, versionHome) {
114
+ return path.join(pluginsRootForVersion(agent, versionHome), 'marketplaces', nameOf(specOrName));
27
115
  }
28
- export function marketplaceManifestPath(agent, versionHome) {
29
- return path.join(marketplaceRoot(agent, versionHome), '.claude-plugin', 'marketplace.json');
116
+ export function marketplaceManifestPath(specOrName, agent, versionHome) {
117
+ return path.join(marketplaceRoot(specOrName, agent, versionHome), '.claude-plugin', 'marketplace.json');
30
118
  }
31
- export function pluginInstallDir(plugin, agent, versionHome) {
32
- return path.join(marketplaceRoot(agent, versionHome), 'plugins', plugin.name);
119
+ export function pluginInstallDir(plugin, specOrName, agent, versionHome) {
120
+ return path.join(marketplaceRoot(specOrName, agent, versionHome), 'plugins', plugin.name);
33
121
  }
34
122
  export function knownMarketplacesPath(agent, versionHome) {
35
123
  return path.join(pluginsRootForVersion(agent, versionHome), 'known_marketplaces.json');
36
124
  }
37
125
  function settingsPath(agent, versionHome) {
38
- return path.join(versionHome, `.${agent}`, 'settings.json');
126
+ return path.join(versionHome, agentConfigDirName(agent), 'settings.json');
39
127
  }
128
+ // ─── Copy plugin source into a marketplace ────────────────────────────────────
40
129
  /**
41
- * Copy plugin source into marketplace install dir.
42
- * Source of truth remains ~/.agents/plugins/<name>/ — this is a per-version snapshot.
130
+ * Copy plugin source into the marketplace install dir for the given spec.
131
+ * Source of truth remains the plugin's source dir — this is a per-version snapshot.
132
+ *
133
+ * Symlinks pointing OUTSIDE the plugin source root are dropped. They show up
134
+ * when plugin authors (legitimately) link prompt-side references to sibling
135
+ * codebases — e.g. the rush plugin's `app -> ../../../rush/app` for @app/...
136
+ * autocomplete in user prompts. Faithfully copying those symlinks pollutes
137
+ * the marketplace with gigabytes of node_modules / .next / brand-asset video
138
+ * that the consumer (Claude Code, OpenClaw) then walks during plugin
139
+ * discovery — which is the documented cause of multi-minute startup hangs.
140
+ *
141
+ * Internal symlinks (target stays inside the plugin root) are preserved.
43
142
  */
44
- export function copyPluginToMarketplace(plugin, agent, versionHome) {
45
- const dest = pluginInstallDir(plugin, agent, versionHome);
143
+ export function copyPluginToMarketplace(plugin, spec, agent, versionHome) {
144
+ const dest = pluginInstallDir(plugin, spec, agent, versionHome);
46
145
  fs.mkdirSync(path.dirname(dest), { recursive: true });
47
146
  if (fs.existsSync(dest)) {
48
147
  fs.rmSync(dest, { recursive: true, force: true });
49
148
  }
50
- fs.cpSync(plugin.root, dest, { recursive: true, dereference: false });
149
+ const sourceRealRoot = (() => {
150
+ try {
151
+ return fs.realpathSync(plugin.root);
152
+ }
153
+ catch {
154
+ return plugin.root;
155
+ }
156
+ })();
157
+ const skipped = [];
158
+ fs.cpSync(plugin.root, dest, {
159
+ recursive: true,
160
+ dereference: false,
161
+ filter: (src) => {
162
+ try {
163
+ const stat = fs.lstatSync(src);
164
+ if (!stat.isSymbolicLink())
165
+ return true;
166
+ const target = fs.realpathSync(src);
167
+ if (target === sourceRealRoot || target.startsWith(sourceRealRoot + path.sep)) {
168
+ return true;
169
+ }
170
+ skipped.push(path.relative(plugin.root, src) || path.basename(src));
171
+ return false;
172
+ }
173
+ catch {
174
+ // Dangling symlink or stat failure — drop it; it can't be useful in
175
+ // the marketplace and would error the consumer's walk anyway.
176
+ skipped.push(path.relative(plugin.root, src) || path.basename(src));
177
+ return false;
178
+ }
179
+ },
180
+ });
181
+ if (skipped.length > 0) {
182
+ process.stderr.write(`agents-cli: plugin '${plugin.name}' has ${skipped.length} symlink(s) ` +
183
+ `pointing outside its source root; not copied to marketplace ` +
184
+ `(would bloat consumer startup): ${skipped.join(', ')}\n`);
185
+ }
51
186
  return dest;
52
187
  }
188
+ // ─── Catalog synthesis ──────────────────────────────────────────────────────
53
189
  /**
54
- * Re-synthesize <marketplace>/.claude-plugin/marketplace.json from the list of
55
- * plugins installed under <marketplace>/plugins/. Always run after add or remove
56
- * so the manifest stays in lockstep with on-disk contents.
190
+ * Re-synthesize <marketplace>/.claude-plugin/marketplace.json from the plugins
191
+ * already installed under <marketplace>/plugins/. Always run after add or remove
192
+ * so the manifest stays in lockstep with on-disk contents. Returns the manifest
193
+ * it wrote, or null when the marketplace has no plugins dir yet.
57
194
  */
58
- export function syncMarketplaceManifest(agent, versionHome) {
59
- const root = marketplaceRoot(agent, versionHome);
195
+ export function syncMarketplaceManifest(spec, agent, versionHome) {
196
+ const name = marketplaceNameFor(spec);
197
+ const root = marketplaceRoot(spec, agent, versionHome);
60
198
  const pluginsDir = path.join(root, 'plugins');
61
199
  if (!fs.existsSync(pluginsDir))
62
200
  return null;
63
201
  const entries = [];
64
202
  for (const entry of fs.readdirSync(pluginsDir, { withFileTypes: true })) {
65
- if (!entry.isDirectory() || entry.name.startsWith('.'))
203
+ if (entry.name.startsWith('.'))
66
204
  continue;
67
- const manifestFile = path.join(pluginsDir, entry.name, '.claude-plugin', 'plugin.json');
205
+ // Follow symlinks: Dirent.isDirectory() is false for a symlink even when the
206
+ // target is a directory. statSync follows the link.
207
+ const entryPath = path.join(pluginsDir, entry.name);
208
+ let isDir = entry.isDirectory();
209
+ if (!isDir && entry.isSymbolicLink()) {
210
+ try {
211
+ isDir = fs.statSync(entryPath).isDirectory();
212
+ }
213
+ catch {
214
+ isDir = false;
215
+ }
216
+ }
217
+ if (!isDir)
218
+ continue;
219
+ const manifestFile = path.join(entryPath, '.claude-plugin', 'plugin.json');
68
220
  if (!fs.existsSync(manifestFile))
69
221
  continue;
70
222
  let manifest;
@@ -84,22 +236,25 @@ export function syncMarketplaceManifest(agent, versionHome) {
84
236
  }
85
237
  const manifest = {
86
238
  $schema: 'https://anthropic.com/claude-code/marketplace.schema.json',
87
- name: MARKETPLACE_NAME,
88
- description: 'Plugins managed by agents-cli',
239
+ name,
240
+ description: descriptionFor(spec),
89
241
  owner: { name: 'agents-cli' },
90
242
  plugins: entries.sort((a, b) => a.name.localeCompare(b.name)),
91
243
  };
92
- const manifestPath = marketplaceManifestPath(agent, versionHome);
244
+ const manifestPath = marketplaceManifestPath(spec, agent, versionHome);
93
245
  fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
94
246
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
95
247
  return manifest;
96
248
  }
249
+ // ─── Registration in known_marketplaces.json ──────────────────────────────────
97
250
  /**
98
- * Register the agents-cli marketplace in known_marketplaces.json so Claude Code
99
- * discovers it on startup. Idempotent: re-running just refreshes lastUpdated.
251
+ * Register a marketplace in known_marketplaces.json so Claude Code discovers
252
+ * it on startup. Idempotent: re-running just refreshes lastUpdated. Other
253
+ * marketplaces' entries are preserved untouched.
100
254
  */
101
- export function registerMarketplace(agent, versionHome) {
102
- const root = marketplaceRoot(agent, versionHome);
255
+ export function registerMarketplace(spec, agent, versionHome) {
256
+ const name = marketplaceNameFor(spec);
257
+ const root = marketplaceRoot(spec, agent, versionHome);
103
258
  const knownPath = knownMarketplacesPath(agent, versionHome);
104
259
  let known = {};
105
260
  if (fs.existsSync(knownPath)) {
@@ -110,7 +265,7 @@ export function registerMarketplace(agent, versionHome) {
110
265
  known = {};
111
266
  }
112
267
  }
113
- known[MARKETPLACE_NAME] = {
268
+ known[name] = {
114
269
  source: { source: 'directory', path: root },
115
270
  installLocation: root,
116
271
  lastUpdated: new Date().toISOString(),
@@ -119,10 +274,12 @@ export function registerMarketplace(agent, versionHome) {
119
274
  fs.writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n', 'utf-8');
120
275
  }
121
276
  /**
122
- * Drop the agents-cli marketplace entry from known_marketplaces.json.
123
- * Called when the last plugin under it is removed.
277
+ * Drop a marketplace entry from known_marketplaces.json. Called when the last
278
+ * plugin under it is removed. Removes only its own entry; deletes the file only
279
+ * when no entries remain.
124
280
  */
125
- export function unregisterMarketplace(agent, versionHome) {
281
+ export function unregisterMarketplace(specOrName, agent, versionHome) {
282
+ const name = nameOf(specOrName);
126
283
  const knownPath = knownMarketplacesPath(agent, versionHome);
127
284
  if (!fs.existsSync(knownPath))
128
285
  return;
@@ -133,9 +290,9 @@ export function unregisterMarketplace(agent, versionHome) {
133
290
  catch {
134
291
  return;
135
292
  }
136
- if (!(MARKETPLACE_NAME in known))
293
+ if (!(name in known))
137
294
  return;
138
- delete known[MARKETPLACE_NAME];
295
+ delete known[name];
139
296
  if (Object.keys(known).length === 0) {
140
297
  try {
141
298
  fs.unlinkSync(knownPath);
@@ -146,15 +303,38 @@ export function unregisterMarketplace(agent, versionHome) {
146
303
  fs.writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n', 'utf-8');
147
304
  }
148
305
  }
306
+ // ─── Top-level orchestration ──────────────────────────────────────────────────
149
307
  /**
150
- * Mark a plugin as enabled in <versionHome>/.{agent}/settings.json under
151
- * enabledPlugins["<plugin>@agents-cli"]: true. Reads, mutates, writes
152
- * preserving every other key.
308
+ * Discover every source-side marketplace, then for each one re-synthesize its
309
+ * catalog from the plugins already copied under the version home and register
310
+ * it in known_marketplaces.json. Returns one result per marketplace that has at
311
+ * least one plugin installed.
312
+ *
313
+ * Copying plugin source into a marketplace is the caller's responsibility
314
+ * (copyPluginToMarketplace / syncPluginToVersion) — this reconciles catalogs +
315
+ * registrations across all repos once the copies are in place. Marketplaces
316
+ * whose version-home plugins dir is empty or absent are skipped, so we never
317
+ * register a known_marketplace pointing at a directory with no catalog.
153
318
  */
154
- export function enablePluginInSettings(pluginName, agent, versionHome, options = {}) {
155
- if (!options.allowExecSurfaces && marketplacePluginHasExecSurfaces(pluginName, agent, versionHome)) {
156
- return;
319
+ export function syncAllMarketplaces(agent, versionHome, opts = {}) {
320
+ const results = [];
321
+ for (const dm of discoverMarketplaces(opts)) {
322
+ const manifest = syncMarketplaceManifest(dm.spec, agent, versionHome);
323
+ if (!manifest || manifest.plugins.length === 0)
324
+ continue;
325
+ registerMarketplace(dm.spec, agent, versionHome);
326
+ results.push({ spec: dm.spec, name: dm.name, plugins: manifest.plugins.length });
157
327
  }
328
+ return results;
329
+ }
330
+ // ─── Per-plugin settings ops ──────────────────────────────────────────────────
331
+ /**
332
+ * Mark a plugin as enabled in <versionHome>/.{agent}/settings.json under
333
+ * enabledPlugins["<plugin>@<marketplace>"]: true. Reads, mutates, writes —
334
+ * preserving every other key. Trust/exec-surface gating is the caller's
335
+ * responsibility (plugins.ts owns plugin capability inspection).
336
+ */
337
+ export function addPluginToSettings(pluginName, marketplaceName, agent, versionHome) {
158
338
  const sPath = settingsPath(agent, versionHome);
159
339
  let settings = {};
160
340
  if (fs.existsSync(sPath)) {
@@ -169,49 +349,17 @@ export function enablePluginInSettings(pluginName, agent, versionHome, options =
169
349
  settings.enabledPlugins = {};
170
350
  }
171
351
  const enabled = settings.enabledPlugins;
172
- const key = `${pluginName}@${MARKETPLACE_NAME}`;
352
+ const key = `${pluginName}@${marketplaceName}`;
173
353
  if (enabled[key] === true)
174
354
  return;
175
355
  enabled[key] = true;
176
356
  fs.mkdirSync(path.dirname(sPath), { recursive: true });
177
357
  fs.writeFileSync(sPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
178
358
  }
179
- function marketplacePluginHasExecSurfaces(pluginName, agent, versionHome) {
180
- const root = path.join(marketplaceRoot(agent, versionHome), 'plugins', pluginName);
181
- if (fs.existsSync(path.join(root, '.mcp.json')))
182
- return true;
183
- for (const dir of ['bin', 'scripts', 'permissions']) {
184
- if (fs.existsSync(path.join(root, dir)))
185
- return true;
186
- }
187
- const hooksFile = path.join(root, 'hooks', 'hooks.json');
188
- if (fs.existsSync(hooksFile))
189
- return true;
190
- const hooksDir = path.join(root, 'hooks');
191
- if (fs.existsSync(hooksDir)) {
192
- try {
193
- if (fs.readdirSync(hooksDir).some((entry) => !entry.startsWith('.')))
194
- return true;
195
- }
196
- catch {
197
- return true;
198
- }
199
- }
200
- const settingsFile = path.join(root, 'settings.json');
201
- if (!fs.existsSync(settingsFile))
202
- return false;
203
- try {
204
- const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
205
- return Object.keys(settings).some((key) => key !== 'permissions') || 'permissions' in settings;
206
- }
207
- catch {
208
- return true;
209
- }
210
- }
211
359
  /**
212
- * Remove the enabledPlugins key for this plugin. Inverse of enablePluginInSettings.
360
+ * Remove the enabledPlugins key for this plugin. Inverse of addPluginToSettings.
213
361
  */
214
- export function disablePluginInSettings(pluginName, agent, versionHome) {
362
+ export function removePluginFromSettings(pluginName, marketplaceName, agent, versionHome) {
215
363
  const sPath = settingsPath(agent, versionHome);
216
364
  if (!fs.existsSync(sPath))
217
365
  return;
@@ -225,7 +373,7 @@ export function disablePluginInSettings(pluginName, agent, versionHome) {
225
373
  const enabled = settings.enabledPlugins;
226
374
  if (!enabled)
227
375
  return;
228
- const key = `${pluginName}@${MARKETPLACE_NAME}`;
376
+ const key = `${pluginName}@${marketplaceName}`;
229
377
  if (!(key in enabled))
230
378
  return;
231
379
  delete enabled[key];
@@ -234,12 +382,13 @@ export function disablePluginInSettings(pluginName, agent, versionHome) {
234
382
  }
235
383
  fs.writeFileSync(sPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
236
384
  }
385
+ // ─── Marketplace teardown helpers ─────────────────────────────────────────────
237
386
  /**
238
387
  * Remove a plugin's installed marketplace directory. Returns true if the dir
239
388
  * existed and was removed.
240
389
  */
241
- export function removePluginFromMarketplace(pluginName, agent, versionHome) {
242
- const installed = path.join(marketplaceRoot(agent, versionHome), 'plugins', pluginName);
390
+ export function removePluginFromMarketplace(pluginName, specOrName, agent, versionHome) {
391
+ const installed = path.join(marketplaceRoot(specOrName, agent, versionHome), 'plugins', pluginName);
243
392
  if (!fs.existsSync(installed))
244
393
  return false;
245
394
  fs.rmSync(installed, { recursive: true, force: true });
@@ -248,8 +397,8 @@ export function removePluginFromMarketplace(pluginName, agent, versionHome) {
248
397
  /**
249
398
  * Return true if the marketplace has no plugins left under it.
250
399
  */
251
- export function marketplaceIsEmpty(agent, versionHome) {
252
- const pluginsDir = path.join(marketplaceRoot(agent, versionHome), 'plugins');
400
+ export function marketplaceIsEmpty(specOrName, agent, versionHome) {
401
+ const pluginsDir = path.join(marketplaceRoot(specOrName, agent, versionHome), 'plugins');
253
402
  if (!fs.existsSync(pluginsDir))
254
403
  return true;
255
404
  const remaining = fs.readdirSync(pluginsDir, { withFileTypes: true })
@@ -259,8 +408,8 @@ export function marketplaceIsEmpty(agent, versionHome) {
259
408
  /**
260
409
  * Drop the entire marketplace directory. Called after the last plugin removal.
261
410
  */
262
- export function removeEmptyMarketplaceDir(agent, versionHome) {
263
- const root = marketplaceRoot(agent, versionHome);
411
+ export function removeEmptyMarketplaceDir(specOrName, agent, versionHome) {
412
+ const root = marketplaceRoot(specOrName, agent, versionHome);
264
413
  if (!fs.existsSync(root))
265
414
  return;
266
415
  fs.rmSync(root, { recursive: true, force: true });
@@ -268,7 +417,7 @@ export function removeEmptyMarketplaceDir(agent, versionHome) {
268
417
  /**
269
418
  * Detect whether a plugin is installed via the native marketplace path.
270
419
  */
271
- export function isInstalledInMarketplace(pluginName, agent, versionHome) {
272
- const installed = path.join(marketplaceRoot(agent, versionHome), 'plugins', pluginName);
420
+ export function isInstalledInMarketplace(pluginName, specOrName, agent, versionHome) {
421
+ const installed = path.join(marketplaceRoot(specOrName, agent, versionHome), 'plugins', pluginName);
273
422
  return fs.existsSync(path.join(installed, '.claude-plugin', 'plugin.json'));
274
423
  }
@@ -8,7 +8,7 @@
8
8
  * module discovers plugins, validates their manifests, and syncs their
9
9
  * contents into agent version homes.
10
10
  */
11
- import type { AgentId, DiscoveredPlugin, PluginManifest } from './types.js';
11
+ import type { AgentId, DiscoveredPlugin, PluginManifest, MarketplaceSpec } from './types.js';
12
12
  export interface PluginCapabilities {
13
13
  hasHooks: boolean;
14
14
  hasMcp: boolean;
@@ -19,11 +19,28 @@ export interface PluginCapabilities {
19
19
  }
20
20
  export declare const PLUGIN_EXEC_SURFACE_LABELS: Record<keyof PluginCapabilities, string>;
21
21
  /**
22
- * Discover all plugins in ~/.agents/plugins/.
22
+ * Discover all plugins in a given plugins directory (e.g. ~/.agents/plugins/,
23
+ * ~/.agents/.system/plugins/, <cwd>/.agents/plugins/, ~/.agents-<alias>/plugins/).
23
24
  * A valid plugin has a .claude-plugin/plugin.json manifest.
25
+ *
26
+ * `spec` stamps marketplace provenance onto each discovered plugin. Callers that
27
+ * scan a single source dir without a marketplace identity (e.g. project-launch)
28
+ * may omit it; those plugins default to the user marketplace.
24
29
  */
25
- export declare function discoverPlugins(): DiscoveredPlugin[];
26
- export declare function buildDiscoveredPlugin(pluginRoot: string, manifest: PluginManifest): DiscoveredPlugin;
30
+ export declare function discoverPluginsInDir(pluginsDir: string, spec?: MarketplaceSpec): DiscoveredPlugin[];
31
+ /**
32
+ * Discover every plugin across ALL marketplaces — the user repo (~/.agents/),
33
+ * each enabled extra repo (~/.agents-<alias>/), and the project repo
34
+ * (<cwd>/.agents/) — stamping marketplace provenance onto each.
35
+ *
36
+ * Plugin names are NOT deduplicated across marketplaces: a `code` plugin in both
37
+ * the user repo and an extra repo yields two entries (`code@agents-cli` and
38
+ * `code@agents-<alias>`), each installing into its own marketplace directory.
39
+ */
40
+ export declare function discoverPlugins(opts?: {
41
+ cwd?: string;
42
+ }): DiscoveredPlugin[];
43
+ export declare function buildDiscoveredPlugin(pluginRoot: string, manifest: PluginManifest, spec?: MarketplaceSpec): DiscoveredPlugin;
27
44
  export declare function inspectPluginCapabilities(pluginRoot: string): PluginCapabilities;
28
45
  export declare function hasPluginExecSurfaces(capabilities: PluginCapabilities): boolean;
29
46
  export declare function pluginCapabilityLabels(capabilities: PluginCapabilities): string[];