@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.
- package/.claude/hooks/sync-marketplace-agents.cjs +117 -56
- package/CHANGELOG.md +20 -0
- package/CLAUDE.md +33 -16
- package/package.json +1 -1
|
@@ -1,82 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Sync Marketplace Agents Hook
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
16
|
-
const
|
|
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
|
-
*
|
|
26
|
+
* Get hash of installed_plugins.json content
|
|
21
27
|
*/
|
|
22
|
-
function
|
|
23
|
-
|
|
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
|
-
*
|
|
37
|
+
* Check if plugins have changed since last sync
|
|
28
38
|
*/
|
|
29
|
-
function
|
|
30
|
-
|
|
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
|
-
*
|
|
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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
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
|
-
}
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// Ignore parse errors
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
return
|
|
84
|
+
return plugins;
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
/**
|
|
72
|
-
* Scan marketplace plugins for agents (
|
|
73
|
-
* Supports
|
|
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
|
|
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
|
|
228
|
-
if (
|
|
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
|
-
|
|
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
|
-
//
|
|
237
|
-
|
|
299
|
+
// Save sync state
|
|
300
|
+
saveSyncState();
|
|
238
301
|
|
|
239
|
-
// Output info
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
**
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
```
|