@hailer/mcp 0.1.6 → 0.1.8

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.
@@ -1,82 +1,98 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Sync Marketplace Agents Hook
4
- * Runs once per session on first prompt, scans ENABLED plugins,
5
- * updates CLAUDE.md with available marketplace agents.
4
+ * Watches installed_plugins.json for changes and updates CLAUDE.md
5
+ * with available marketplace agents when plugins are installed/uninstalled.
6
6
  */
7
7
 
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
+ const crypto = require('crypto');
11
12
 
12
- // Session marker - unique per Claude Code restart (uses parent PID)
13
- const SESSION_MARKER = path.join(os.tmpdir(), `claude-agent-sync-${process.ppid}`);
14
13
  const PLUGINS_DIR = path.join(os.homedir(), '.claude', 'plugins', 'marketplaces');
15
- const CLAUDE_MD = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), 'CLAUDE.md');
16
- const PROJECT_SETTINGS = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', 'settings.json');
14
+ const INSTALLED_PLUGINS = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
15
+ const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
16
+ const CLAUDE_MD = path.join(PROJECT_DIR, 'CLAUDE.md');
17
+ const PROJECT_SETTINGS = path.join(PROJECT_DIR, '.claude', 'settings.json');
17
18
  const USER_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
18
19
 
20
+ // Store sync state in user's home, keyed by project path hash
21
+ const SYNC_STATE_DIR = path.join(os.homedir(), '.claude', 'sync-state');
22
+ const PROJECT_HASH = crypto.createHash('md5').update(PROJECT_DIR).digest('hex').slice(0, 12);
23
+ const SYNC_STATE = path.join(SYNC_STATE_DIR, `${PROJECT_HASH}.state`);
24
+
19
25
  /**
20
- * Check if already synced this session
26
+ * Get hash of installed_plugins.json content
21
27
  */
22
- function alreadySynced() {
23
- return fs.existsSync(SESSION_MARKER);
28
+ function getInstalledPluginsHash() {
29
+ if (!fs.existsSync(INSTALLED_PLUGINS)) {
30
+ return 'empty';
31
+ }
32
+ const content = fs.readFileSync(INSTALLED_PLUGINS, 'utf-8');
33
+ return crypto.createHash('md5').update(content).digest('hex');
24
34
  }
25
35
 
26
36
  /**
27
- * Mark as synced
37
+ * Check if plugins have changed since last sync
28
38
  */
29
- function markSynced() {
30
- fs.writeFileSync(SESSION_MARKER, new Date().toISOString());
39
+ function pluginsChanged() {
40
+ const currentHash = getInstalledPluginsHash();
41
+
42
+ if (!fs.existsSync(SYNC_STATE)) {
43
+ return true; // First run
44
+ }
45
+
46
+ const lastHash = fs.readFileSync(SYNC_STATE, 'utf-8').trim();
47
+ return currentHash !== lastHash;
31
48
  }
32
49
 
33
50
  /**
34
- * Get enabled plugins from settings.json files
51
+ * Save current sync state
52
+ */
53
+ function saveSyncState() {
54
+ const currentHash = getInstalledPluginsHash();
55
+ // Ensure sync state directory exists
56
+ if (!fs.existsSync(SYNC_STATE_DIR)) {
57
+ fs.mkdirSync(SYNC_STATE_DIR, { recursive: true });
58
+ }
59
+ fs.writeFileSync(SYNC_STATE, currentHash);
60
+ }
61
+
62
+ /**
63
+ * Get installed plugins from installed_plugins.json
35
64
  * Returns Set of "plugin@marketplace" strings
36
65
  */
37
- function getEnabledPlugins() {
38
- const enabled = new Set();
39
-
40
- // Check project settings
41
- if (fs.existsSync(PROJECT_SETTINGS)) {
42
- try {
43
- const settings = JSON.parse(fs.readFileSync(PROJECT_SETTINGS, 'utf-8'));
44
- if (settings.enabledPlugins) {
45
- for (const [key, value] of Object.entries(settings.enabledPlugins)) {
46
- if (value === true) {
47
- enabled.add(key); // format: "plugin@marketplace"
48
- }
49
- }
50
- }
51
- } catch (e) { /* ignore parse errors */ }
66
+ function getInstalledPlugins() {
67
+ const plugins = new Set();
68
+
69
+ if (!fs.existsSync(INSTALLED_PLUGINS)) {
70
+ return plugins;
52
71
  }
53
72
 
54
- // Check user settings
55
- if (fs.existsSync(USER_SETTINGS)) {
56
- try {
57
- const settings = JSON.parse(fs.readFileSync(USER_SETTINGS, 'utf-8'));
58
- if (settings.enabledPlugins) {
59
- for (const [key, value] of Object.entries(settings.enabledPlugins)) {
60
- if (value === true) {
61
- enabled.add(key);
62
- }
63
- }
73
+ try {
74
+ const data = JSON.parse(fs.readFileSync(INSTALLED_PLUGINS, 'utf-8'));
75
+ if (data.plugins) {
76
+ for (const pluginKey of Object.keys(data.plugins)) {
77
+ plugins.add(pluginKey);
64
78
  }
65
- } catch (e) { /* ignore parse errors */ }
79
+ }
80
+ } catch (e) {
81
+ // Ignore parse errors
66
82
  }
67
83
 
68
- return enabled;
84
+ return plugins;
69
85
  }
70
86
 
71
87
  /**
72
- * Scan marketplace plugins for agents (only enabled plugins)
73
- * Supports both flat and nested structures:
88
+ * Scan marketplace plugins for agents (all discovered plugins)
89
+ * Supports three structures:
74
90
  * - Flat: {marketplace}/agents/ (when plugin name == marketplace name)
91
+ * - Root-level: {marketplace}/{plugin}/agents/ (plugin dirs at marketplace root)
75
92
  * - Nested: {marketplace}/plugins/{plugin}/agents/
76
93
  */
77
- function scanMarketplaceAgents() {
94
+ function scanMarketplaceAgents(enabledPlugins) {
78
95
  const agents = [];
79
- const enabledPlugins = getEnabledPlugins();
80
96
 
81
97
  if (!fs.existsSync(PLUGINS_DIR)) {
82
98
  return agents;
@@ -86,6 +102,7 @@ function scanMarketplaceAgents() {
86
102
 
87
103
  for (const marketplace of marketplaces) {
88
104
  const marketplacePath = path.join(PLUGINS_DIR, marketplace);
105
+ if (!fs.statSync(marketplacePath).isDirectory()) continue;
89
106
 
90
107
  // Try flat structure first: {marketplace}/agents/
91
108
  const flatAgentsPath = path.join(marketplacePath, 'agents');
@@ -113,6 +130,45 @@ function scanMarketplaceAgents() {
113
130
  }
114
131
  }
115
132
 
133
+ // Try root-level plugins: {marketplace}/{plugin}/agents/
134
+ // Each subdirectory with .claude-plugin is a plugin
135
+ const marketplaceContents = fs.readdirSync(marketplacePath);
136
+ for (const item of marketplaceContents) {
137
+ if (item === '.claude-plugin' || item === '.git' || item === 'plugins') continue;
138
+
139
+ const itemPath = path.join(marketplacePath, item);
140
+ if (!fs.statSync(itemPath).isDirectory()) continue;
141
+
142
+ const pluginJsonPath = path.join(itemPath, '.claude-plugin', 'plugin.json');
143
+
144
+ // Check if this is a plugin (has .claude-plugin/plugin.json)
145
+ if (fs.existsSync(pluginJsonPath)) {
146
+ const pluginKey = `${item}@${marketplace}`;
147
+ if (!enabledPlugins.has(pluginKey)) continue;
148
+
149
+ const agentsPath = path.join(itemPath, 'agents');
150
+ if (!fs.existsSync(agentsPath)) continue;
151
+
152
+ const agentFiles = fs.readdirSync(agentsPath).filter(f => f.endsWith('.md'));
153
+
154
+ for (const agentFile of agentFiles) {
155
+ const agentName = path.basename(agentFile, '.md');
156
+ const fullPath = path.join(agentsPath, agentFile);
157
+ const content = fs.readFileSync(fullPath, 'utf-8');
158
+ const frontmatter = parseFrontmatter(content);
159
+
160
+ agents.push({
161
+ marketplace,
162
+ plugin: item,
163
+ name: agentName,
164
+ fullName: `${item}:${agentName}`,
165
+ description: frontmatter.description || '',
166
+ model: frontmatter.model || 'sonnet'
167
+ });
168
+ }
169
+ }
170
+ }
171
+
116
172
  // Try nested structure: {marketplace}/plugins/{plugin}/agents/
117
173
  const pluginsPath = path.join(marketplacePath, 'plugins');
118
174
  if (fs.existsSync(pluginsPath)) {
@@ -124,7 +180,10 @@ function scanMarketplaceAgents() {
124
180
  continue;
125
181
  }
126
182
 
127
- const agentsPath = path.join(pluginsPath, plugin, 'agents');
183
+ const pluginPath = path.join(pluginsPath, plugin);
184
+ if (!fs.statSync(pluginPath).isDirectory()) continue;
185
+
186
+ const agentsPath = path.join(pluginPath, 'agents');
128
187
  if (!fs.existsSync(agentsPath)) continue;
129
188
 
130
189
  const agentFiles = fs.readdirSync(agentsPath).filter(f => f.endsWith('.md'));
@@ -224,24 +283,26 @@ function updateClaudeMd(agents) {
224
283
  * Main
225
284
  */
226
285
  function main() {
227
- // Skip if already synced this session
228
- if (alreadySynced()) {
286
+ // Skip if installed_plugins.json hasn't changed
287
+ if (!pluginsChanged()) {
229
288
  process.exit(0);
230
289
  }
231
290
 
232
291
  try {
233
- const agents = scanMarketplaceAgents();
292
+ // Get actually installed plugins from installed_plugins.json
293
+ const installedPlugins = getInstalledPlugins();
294
+
295
+ // Scan agents only from installed plugins
296
+ const agents = scanMarketplaceAgents(installedPlugins);
234
297
  const result = updateClaudeMd(agents);
235
298
 
236
- // Mark as synced
237
- markSynced();
299
+ // Save sync state
300
+ saveSyncState();
238
301
 
239
- // Output info if agents found
240
- if (agents.length > 0) {
241
- console.error(`[sync-agents] Found ${agents.length} marketplace agents`);
242
- if (result.updated) {
243
- console.error(`[sync-agents] Updated CLAUDE.md`);
244
- }
302
+ // Output info
303
+ console.error(`[sync-agents] Detected plugin changes, found ${agents.length} marketplace agents`);
304
+ if (result.updated) {
305
+ console.error(`[sync-agents] Updated CLAUDE.md`);
245
306
  }
246
307
 
247
308
  process.exit(0);
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.1.8] - 15-12-2025
8
+
9
+ ### Fixed
10
+
11
+ - Corrected marketplace git URL to `git@gitlab.com:hailer-repos/hailer-mcp-marketplace.git`
12
+
13
+ ## [0.1.7] - 15-12-2025
14
+
15
+ ### Changed
16
+
17
+ - **Marketplace Sync Hook**:
18
+ - Changed from session-based to hash-based sync (tracks `installed_plugins.json` changes)
19
+ - Added support for root-level plugin structure: `{marketplace}/{plugin}/agents/`
20
+ - Per-project sync state to avoid unnecessary re-syncs
21
+ - Improved logging when plugin changes detected
22
+
23
+ - **Plugin Documentation**:
24
+ - Restructured CLAUDE.md with separate `<plugin-setup>` and `<plugin-contributing>` sections
25
+ - Clearer instructions for marketplace installation and contribution
26
+
7
27
  ## [0.1.5] - 12-12-2025
8
28
 
9
29
  ### Added
package/CLAUDE.md CHANGED
@@ -158,33 +158,50 @@ WITHOUT AGENTS: Use MCP tools directly
158
158
  <plugin-marketplace>
159
159
  Community agents are shared via the Hailer Agent Marketplace (separate git repo).
160
160
 
161
- **Install marketplace:**
162
- ```
163
- /plugin marketplace add gitlab:hailer-repos/hailer-mcp-marketplace
164
- /plugin install hailer-agents
165
- ```
166
- Then restart Claude Code.
161
+ **Marketplace URL:** `git@gitlab.com:hailer-repos/hailer-mcp-marketplace.git`
162
+
163
+ **Installed marketplace agents:**
164
+ No marketplace agents installed.
167
165
 
168
166
  **Use plugin agents:**
169
167
  ```
170
168
  Task(subagent_type="plugin:agent-name", prompt="...", model="haiku|sonnet")
171
169
  ```
172
170
 
173
- **Installed marketplace agents:**
174
- No marketplace agents installed.
171
+ **Agent sources:**
172
+ - Default agents: `.claude/agents/` (shipped via npm)
173
+ - Community agents: Plugin marketplace (installed separately)
174
+ </plugin-marketplace>
175
175
 
176
+ <plugin-setup>
177
+ When user asks to install plugins or set up the marketplace:
176
178
 
179
+ 1. Tell user to run these slash commands:
180
+ ```
181
+ /plugin marketplace add git@gitlab.com:hailer-repos/hailer-mcp-marketplace.git
182
+ /plugin install <plugin-name>
183
+ ```
177
184
 
185
+ 2. Tell user: "Restart Claude Code to load the new agents."
178
186
 
179
- **Contribute agents:**
180
- 1. Clone `gitlab:hailer-repos/hailer-mcp-marketplace`
181
- 2. Add agent to `agents/your-agent.md`
182
- 3. Push to repo
187
+ Available plugins: `permissions-handler`, `hailer-agents`
183
188
 
184
- **Agent sources:**
185
- - Default agents: `.claude/agents/` (shipped via npm)
186
- - Community agents: Plugin marketplace (installed separately)
187
- </plugin-marketplace>
189
+ After restart, sync hook updates this file automatically with installed agents.
190
+ </plugin-setup>
191
+
192
+ <plugin-contributing>
193
+ To contribute agents to the marketplace:
194
+
195
+ 1. Clone: `git clone git@gitlab.com:hailer-repos/hailer-mcp-marketplace.git`
196
+ 2. Create plugin structure:
197
+ ```
198
+ my-plugin/
199
+ .claude-plugin/plugin.json
200
+ agents/agent-my-agent.md
201
+ ```
202
+ 3. Follow agent structure from `<agent-structure>` section
203
+ 4. Push to repo
204
+ </plugin-contributing>
188
205
 
189
206
  <directory>
190
207
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hailer/mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "config": {
5
5
  "docker": {
6
6
  "registry": "registry.gitlab.com/hailer-repos/hailer-mcp"