gm-cc 2.0.74 → 2.0.76

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.
@@ -4,7 +4,7 @@
4
4
  "name": "AnEntrypoint"
5
5
  },
6
6
  "description": "State machine agent with hooks, skills, and automated git enforcement",
7
- "version": "2.0.74",
7
+ "version": "2.0.76",
8
8
  "metadata": {
9
9
  "description": "State machine agent with hooks, skills, and automated git enforcement"
10
10
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.74",
3
+ "version": "2.0.76",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": {
6
6
  "name": "AnEntrypoint",
package/README.md CHANGED
@@ -50,6 +50,32 @@ After running `bun x gm-cc@latest`, your project will have:
50
50
 
51
51
  Each hook runs automatically at the appropriate session event. No manual trigger needed.
52
52
 
53
+ ### Project Provisioning (Explicit Installation)
54
+
55
+ To explicitly add all gm-cc hooks, agents, and skills to an existing project:
56
+
57
+ ```bash
58
+ bun x gm-cc@latest -- -p
59
+ ```
60
+
61
+ Or with a global installation:
62
+
63
+ ```bash
64
+ gm-cc -p
65
+ ```
66
+
67
+ This creates a project-local `.claude/settings.json` that configures hooks to use `${CLAUDE_PROJECT_DIR}` for project-specific environments. Useful for:
68
+ - Explicitly provisioning gm-cc to an existing project
69
+ - Overriding global plugin settings for a specific project
70
+ - Team workflows requiring consistent project-level configuration
71
+
72
+ The `-p` flag copies:
73
+ - All hooks to `.claude/hooks/`
74
+ - All agents to `.claude/agents/`
75
+ - All skills to `.claude/skills/`
76
+ - MCP configuration to `.claude/.mcp.json`
77
+ - Project-specific hook settings to `.claude/settings.json`
78
+
53
79
  ## File Installation (Manual Setup)
54
80
 
55
81
  If you prefer manual file management, clone the repository and copy files directly:
package/cli.js CHANGED
@@ -3,100 +3,227 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
 
6
- const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
7
- const claudeDir = path.join(homeDir, '.claude');
8
- const pluginsDir = path.join(claudeDir, 'plugins');
9
- const destDir = path.join(pluginsDir, 'gm-cc');
10
-
11
- const srcDir = __dirname;
12
- const isUpgrade = fs.existsSync(destDir);
13
-
14
- console.log(isUpgrade ? 'Upgrading gm-cc plugin...' : 'Installing gm-cc plugin...');
15
-
16
- try {
17
- fs.mkdirSync(destDir, { recursive: true });
18
-
19
- const filesToCopy = [
20
- 'agents',
21
- 'hooks',
22
- 'skills',
23
- '.mcp.json',
24
- '.claude-plugin',
25
- 'README.md',
26
- 'CLAUDE.md'
27
- ];
28
-
29
- function copyRecursive(src, dst) {
30
- if (!fs.existsSync(src)) return;
31
- if (fs.statSync(src).isDirectory()) {
32
- fs.mkdirSync(dst, { recursive: true });
33
- fs.readdirSync(src).forEach(f => copyRecursive(path.join(src, f), path.join(dst, f)));
34
- } else {
35
- fs.copyFileSync(src, dst);
6
+ const args = process.argv.slice(2);
7
+ const isProvision = args.includes('-p') || args.includes('--provision');
8
+
9
+ if (isProvision) {
10
+ provisionProject();
11
+ } else {
12
+ installGlobally();
13
+ }
14
+
15
+ function provisionProject() {
16
+ const projectDir = process.cwd();
17
+ const claudeDir = path.join(projectDir, '.claude');
18
+ const srcDir = __dirname;
19
+
20
+ console.log('Provisioning gm-cc to current project...');
21
+
22
+ try {
23
+ const filesToCopy = [
24
+ 'agents',
25
+ 'hooks',
26
+ 'skills',
27
+ '.mcp.json'
28
+ ];
29
+
30
+ function copyRecursive(src, dst) {
31
+ if (!fs.existsSync(src)) return;
32
+ if (fs.statSync(src).isDirectory()) {
33
+ fs.mkdirSync(dst, { recursive: true });
34
+ fs.readdirSync(src).forEach(f => copyRecursive(path.join(src, f), path.join(dst, f)));
35
+ } else {
36
+ fs.copyFileSync(src, dst);
37
+ }
36
38
  }
37
- }
38
39
 
39
- filesToCopy.forEach(name => copyRecursive(path.join(srcDir, name), path.join(destDir, name)));
40
+ filesToCopy.forEach(name => copyRecursive(path.join(srcDir, name), path.join(claudeDir, name)));
40
41
 
41
- // Remove stale root-level plugin.json (moved to .claude-plugin/plugin.json)
42
- const stalePluginJson = path.join(destDir, 'plugin.json');
43
- if (fs.existsSync(stalePluginJson)) fs.unlinkSync(stalePluginJson);
42
+ // Update .gitignore to exclude .gm-stop-verified
43
+ const gitignorePath = path.join(projectDir, '.gitignore');
44
+ const gitignoreEntry = '.gm-stop-verified';
44
45
 
45
- // Register in settings.json (enabledPlugins only, no hook injection)
46
- const settingsPath = path.join(claudeDir, 'settings.json');
47
- let settings = {};
48
- if (fs.existsSync(settingsPath)) {
49
- try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); } catch (e) {}
50
- }
51
- if (!settings.enabledPlugins) settings.enabledPlugins = {};
52
- settings.enabledPlugins['gm@gm-cc'] = true;
53
- // Remove stale hook entries (handled by plugin hooks.json)
54
- if (settings.hooks) delete settings.hooks;
55
- // Register marketplace so Claude Code resolves gm@gm-cc locally
56
- if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
57
- settings.extraKnownMarketplaces['gm-cc'] = { source: { source: 'directory', path: destDir } };
58
- fs.mkdirSync(claudeDir, { recursive: true });
59
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
60
- console.log('✓ Plugin registered in ~/.claude/settings.json');
61
-
62
- // Write installed_plugins.json so Claude Code loads from local cache
63
- const pluginVersion = require('./package.json').version;
64
- const installedPluginsPath = path.join(pluginsDir, 'installed_plugins.json');
65
- let installedPlugins = { version: 2, plugins: {} };
66
- if (fs.existsSync(installedPluginsPath)) {
67
- try { installedPlugins = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8')); } catch (e) {}
46
+ let gitignoreContent = '';
47
+ if (fs.existsSync(gitignorePath)) {
48
+ gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
49
+ }
50
+
51
+ if (!gitignoreContent.includes(gitignoreEntry)) {
52
+ if (gitignoreContent && !gitignoreContent.endsWith('\n')) {
53
+ gitignoreContent += '\n';
54
+ }
55
+ gitignoreContent += gitignoreEntry + '\n';
56
+ fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
57
+ }
58
+
59
+ // Generate project-specific settings.json with hook configuration
60
+ const settingsPath = path.join(claudeDir, 'settings.json');
61
+ let settings = {};
62
+ if (fs.existsSync(settingsPath)) {
63
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); } catch (e) {}
64
+ }
65
+
66
+ // Configure hooks to use project-local paths with ${CLAUDE_PROJECT_DIR}
67
+ if (!settings.hooks) settings.hooks = {};
68
+ settings.hooks.PreToolUse = [{
69
+ matcher: '*',
70
+ hooks: [{
71
+ type: 'command',
72
+ command: 'node ${CLAUDE_PROJECT_DIR}/.claude/hooks/pre-tool-use-hook.js',
73
+ timeout: 3600
74
+ }]
75
+ }];
76
+ settings.hooks.SessionStart = [{
77
+ matcher: '*',
78
+ hooks: [{
79
+ type: 'command',
80
+ command: 'node ${CLAUDE_PROJECT_DIR}/.claude/hooks/session-start-hook.js',
81
+ timeout: 10000
82
+ }]
83
+ }];
84
+ settings.hooks.UserPromptSubmit = [{
85
+ matcher: '*',
86
+ hooks: [{
87
+ type: 'command',
88
+ command: 'node ${CLAUDE_PROJECT_DIR}/.claude/hooks/prompt-submit-hook.js',
89
+ timeout: 3600
90
+ }]
91
+ }];
92
+ settings.hooks.Stop = [{
93
+ matcher: '*',
94
+ hooks: [
95
+ {
96
+ type: 'command',
97
+ command: 'node ${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-hook.js',
98
+ timeout: 300000
99
+ },
100
+ {
101
+ type: 'command',
102
+ command: 'node ${CLAUDE_PROJECT_DIR}/.claude/hooks/stop-hook-git.js',
103
+ timeout: 60000
104
+ }
105
+ ]
106
+ }];
107
+
108
+ fs.mkdirSync(claudeDir, { recursive: true });
109
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
110
+
111
+ console.log('✓ Hooks copied to .claude/hooks/');
112
+ console.log('✓ Agents copied to .claude/agents/');
113
+ console.log('✓ Skills copied to .claude/skills/');
114
+ console.log('✓ MCP configuration written to .claude/.mcp.json');
115
+ console.log('✓ Hook configuration written to .claude/settings.json');
116
+ console.log('✓ .gitignore updated');
117
+ console.log('');
118
+ console.log('Provisioning complete! Your project now has:');
119
+ console.log(' • All gm-cc hooks in .claude/hooks/');
120
+ console.log(' • All agents in .claude/agents/');
121
+ console.log(' • All skills in .claude/skills/');
122
+ console.log(' • Hook configuration in .claude/settings.json');
123
+ console.log('');
124
+ console.log('Restart Claude Code to activate the hooks.');
125
+ } catch (e) {
126
+ console.error('Provisioning failed:', e.message);
127
+ process.exit(1);
68
128
  }
69
- if (!installedPlugins.plugins || Array.isArray(installedPlugins.plugins)) installedPlugins.plugins = {};
70
- const now = new Date().toISOString();
71
- const existing = Array.isArray(installedPlugins.plugins['gm@gm-cc']) ? installedPlugins.plugins['gm@gm-cc'][0] : null;
72
- // Also write cache dir so Claude Code finds it without network fetch
73
- const cacheDir = path.join(pluginsDir, 'cache', 'gm-cc', 'gm', pluginVersion);
74
- const filesToCache = ['agents', 'hooks', 'skills', '.mcp.json', '.claude-plugin', 'README.md', 'CLAUDE.md'];
75
- function copyRecursiveCache(src, dst) {
76
- if (!fs.existsSync(src)) return;
77
- if (fs.statSync(src).isDirectory()) {
78
- fs.mkdirSync(dst, { recursive: true });
79
- fs.readdirSync(src).forEach(f => copyRecursiveCache(path.join(src, f), path.join(dst, f)));
80
- } else { fs.copyFileSync(src, dst); }
129
+ }
130
+
131
+ function installGlobally() {
132
+ const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
133
+ const claudeDir = path.join(homeDir, '.claude');
134
+ const pluginsDir = path.join(claudeDir, 'plugins');
135
+ const destDir = path.join(pluginsDir, 'gm-cc');
136
+
137
+ const srcDir = __dirname;
138
+ const isUpgrade = fs.existsSync(destDir);
139
+
140
+ console.log(isUpgrade ? 'Upgrading gm-cc plugin...' : 'Installing gm-cc plugin...');
141
+
142
+ try {
143
+ fs.mkdirSync(destDir, { recursive: true });
144
+
145
+ const filesToCopy = [
146
+ 'agents',
147
+ 'hooks',
148
+ 'skills',
149
+ '.mcp.json',
150
+ '.claude-plugin',
151
+ 'README.md',
152
+ 'CLAUDE.md'
153
+ ];
154
+
155
+ function copyRecursive(src, dst) {
156
+ if (!fs.existsSync(src)) return;
157
+ if (fs.statSync(src).isDirectory()) {
158
+ fs.mkdirSync(dst, { recursive: true });
159
+ fs.readdirSync(src).forEach(f => copyRecursive(path.join(src, f), path.join(dst, f)));
160
+ } else {
161
+ fs.copyFileSync(src, dst);
162
+ }
163
+ }
164
+
165
+ filesToCopy.forEach(name => copyRecursive(path.join(srcDir, name), path.join(destDir, name)));
166
+
167
+ // Remove stale root-level plugin.json (moved to .claude-plugin/plugin.json)
168
+ const stalePluginJson = path.join(destDir, 'plugin.json');
169
+ if (fs.existsSync(stalePluginJson)) fs.unlinkSync(stalePluginJson);
170
+
171
+ // Register in settings.json (enabledPlugins only, no hook injection)
172
+ const settingsPath = path.join(claudeDir, 'settings.json');
173
+ let settings = {};
174
+ if (fs.existsSync(settingsPath)) {
175
+ try { settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); } catch (e) {}
176
+ }
177
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
178
+ settings.enabledPlugins['gm@gm-cc'] = true;
179
+ // Remove stale hook entries (handled by plugin hooks.json)
180
+ if (settings.hooks) delete settings.hooks;
181
+ // Register marketplace so Claude Code resolves gm@gm-cc locally
182
+ if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
183
+ settings.extraKnownMarketplaces['gm-cc'] = { source: { source: 'directory', path: destDir } };
184
+ fs.mkdirSync(claudeDir, { recursive: true });
185
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
186
+ console.log('✓ Plugin registered in ~/.claude/settings.json');
187
+
188
+ // Write installed_plugins.json so Claude Code loads from local cache
189
+ const pluginVersion = require('./package.json').version;
190
+ const installedPluginsPath = path.join(pluginsDir, 'installed_plugins.json');
191
+ let installedPlugins = { version: 2, plugins: {} };
192
+ if (fs.existsSync(installedPluginsPath)) {
193
+ try { installedPlugins = JSON.parse(fs.readFileSync(installedPluginsPath, 'utf-8')); } catch (e) {}
194
+ }
195
+ if (!installedPlugins.plugins || Array.isArray(installedPlugins.plugins)) installedPlugins.plugins = {};
196
+ const now = new Date().toISOString();
197
+ const existing = Array.isArray(installedPlugins.plugins['gm@gm-cc']) ? installedPlugins.plugins['gm@gm-cc'][0] : null;
198
+ // Also write cache dir so Claude Code finds it without network fetch
199
+ const cacheDir = path.join(pluginsDir, 'cache', 'gm-cc', 'gm', pluginVersion);
200
+ const filesToCache = ['agents', 'hooks', 'skills', '.mcp.json', '.claude-plugin', 'README.md', 'CLAUDE.md'];
201
+ function copyRecursiveCache(src, dst) {
202
+ if (!fs.existsSync(src)) return;
203
+ if (fs.statSync(src).isDirectory()) {
204
+ fs.mkdirSync(dst, { recursive: true });
205
+ fs.readdirSync(src).forEach(f => copyRecursiveCache(path.join(src, f), path.join(dst, f)));
206
+ } else { fs.copyFileSync(src, dst); }
207
+ }
208
+ fs.mkdirSync(cacheDir, { recursive: true });
209
+ filesToCache.forEach(name => copyRecursiveCache(path.join(destDir, name), path.join(cacheDir, name)));
210
+ // Remove stale root-level plugin.json from cache (moved to .claude-plugin/plugin.json)
211
+ const staleCachePluginJson = path.join(cacheDir, 'plugin.json');
212
+ if (fs.existsSync(staleCachePluginJson)) fs.unlinkSync(staleCachePluginJson);
213
+ installedPlugins.plugins['gm@gm-cc'] = [{
214
+ scope: 'user',
215
+ installPath: cacheDir,
216
+ version: pluginVersion,
217
+ installedAt: existing?.installedAt || now,
218
+ lastUpdated: now
219
+ }];
220
+ fs.writeFileSync(installedPluginsPath, JSON.stringify(installedPlugins, null, 2), 'utf-8');
221
+ console.log('✓ Plugin registered in installed_plugins.json');
222
+
223
+ console.log(`✓ gm-cc ${isUpgrade ? 'upgraded' : 'installed'} to ${destDir}`);
224
+ console.log('Restart Claude Code to activate the gm plugin.');
225
+ } catch (e) {
226
+ console.error('Installation failed:', e.message);
227
+ process.exit(1);
81
228
  }
82
- fs.mkdirSync(cacheDir, { recursive: true });
83
- filesToCache.forEach(name => copyRecursiveCache(path.join(destDir, name), path.join(cacheDir, name)));
84
- // Remove stale root-level plugin.json from cache (moved to .claude-plugin/plugin.json)
85
- const staleCachePluginJson = path.join(cacheDir, 'plugin.json');
86
- if (fs.existsSync(staleCachePluginJson)) fs.unlinkSync(staleCachePluginJson);
87
- installedPlugins.plugins['gm@gm-cc'] = [{
88
- scope: 'user',
89
- installPath: cacheDir,
90
- version: pluginVersion,
91
- installedAt: existing?.installedAt || now,
92
- lastUpdated: now
93
- }];
94
- fs.writeFileSync(installedPluginsPath, JSON.stringify(installedPlugins, null, 2), 'utf-8');
95
- console.log('✓ Plugin registered in installed_plugins.json');
96
-
97
- console.log(`✓ gm-cc ${isUpgrade ? 'upgraded' : 'installed'} to ${destDir}`);
98
- console.log('Restart Claude Code to activate the gm plugin.');
99
- } catch (e) {
100
- console.error('Installation failed:', e.message);
101
- process.exit(1);
102
229
  }
@@ -31,7 +31,7 @@ const runCodeSearch = (query, cwd) => {
31
31
  const escaped = query.replace(/"/g, '\\"').substring(0, 200);
32
32
  let out;
33
33
  try {
34
- out = execSync(`bun x codebasesearch@latest "${escaped}"`, {
34
+ out = execSync(`bun x codebasesearch "${escaped}"`, {
35
35
  encoding: 'utf-8',
36
36
  stdio: ['pipe', 'pipe', 'pipe'],
37
37
  cwd,
@@ -40,7 +40,7 @@ const runCodeSearch = (query, cwd) => {
40
40
  });
41
41
  } catch (bunErr) {
42
42
  if (bunErr.killed) return '';
43
- out = execSync(`npx -y codebasesearch@latest "${escaped}"`, {
43
+ out = execSync(`npx -y codebasesearch "${escaped}"`, {
44
44
  encoding: 'utf-8',
45
45
  stdio: ['pipe', 'pipe', 'pipe'],
46
46
  cwd,
@@ -71,7 +71,7 @@ When exploring unfamiliar code, finding similar patterns, understanding integrat
71
71
  try {
72
72
  let thornOutput;
73
73
  try {
74
- thornOutput = execSync(`bun x mcp-thorns@latest`, {
74
+ thornOutput = execSync(`bun x mcp-thorns`, {
75
75
  encoding: 'utf-8',
76
76
  stdio: ['pipe', 'pipe', 'pipe'],
77
77
  cwd: projectDir,
@@ -83,7 +83,7 @@ When exploring unfamiliar code, finding similar patterns, understanding integrat
83
83
  thornOutput = '=== mcp-thorns ===\nSkipped (3min timeout)';
84
84
  } else {
85
85
  try {
86
- thornOutput = execSync(`npx -y mcp-thorns@latest`, {
86
+ thornOutput = execSync(`npx -y mcp-thorns`, {
87
87
  encoding: 'utf-8',
88
88
  stdio: ['pipe', 'pipe', 'pipe'],
89
89
  cwd: projectDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-cc",
3
- "version": "2.0.74",
3
+ "version": "2.0.76",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -49,5 +49,12 @@
49
49
  ],
50
50
  "peerDependencies": {
51
51
  "@anthropic-ai/claude-code": "*"
52
+ },
53
+ "dependencies": {
54
+ "mcp-thorns": "latest",
55
+ "codebasesearch": "latest"
56
+ },
57
+ "scripts": {
58
+ "postinstall": "node scripts/postinstall.js"
52
59
  }
53
60
  }
@@ -2,38 +2,19 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
-
6
- /**
7
- * Postinstall script for gm-cc
8
- * Implements Mode 1: Standalone .claude/ directory installation
9
- *
10
- * When installed via npm in a project:
11
- * - Copies agents/, hooks/, .mcp.json to project's .claude/
12
- * - Updates .gitignore with .gm-stop-verified
13
- * - Runs silently, never breaks npm install
14
- * - Safe to run multiple times (idempotent)
15
- */
5
+ const { execSync } = require('child_process');
16
6
 
17
7
  function isInsideNodeModules() {
18
- // Check if __dirname contains /node_modules/ in its path
19
- // Example: /project/node_modules/gm-cc/scripts
20
8
  return __dirname.includes(path.sep + 'node_modules' + path.sep);
21
9
  }
22
10
 
23
11
  function getProjectRoot() {
24
- // From /project/node_modules/gm-cc/scripts
25
- // Navigate to /project
26
- if (!isInsideNodeModules()) {
27
- return null;
28
- }
29
-
30
- // Find the node_modules parent (project root)
12
+ if (!isInsideNodeModules()) return null;
31
13
  let current = __dirname;
32
- while (current !== path.dirname(current)) { // While not at root
14
+ while (current !== path.dirname(current)) {
33
15
  current = path.dirname(current);
34
- const parent = path.dirname(current);
35
16
  if (path.basename(current) === 'node_modules') {
36
- return parent;
17
+ return path.dirname(current);
37
18
  }
38
19
  }
39
20
  return null;
@@ -43,39 +24,26 @@ function safeCopyFile(src, dst) {
43
24
  try {
44
25
  const content = fs.readFileSync(src, 'utf-8');
45
26
  const dstDir = path.dirname(dst);
46
- if (!fs.existsSync(dstDir)) {
47
- fs.mkdirSync(dstDir, { recursive: true });
48
- }
27
+ if (!fs.existsSync(dstDir)) fs.mkdirSync(dstDir, { recursive: true });
49
28
  fs.writeFileSync(dst, content, 'utf-8');
50
29
  return true;
51
30
  } catch (err) {
52
- // Silently skip errors
53
31
  return false;
54
32
  }
55
33
  }
56
34
 
57
35
  function safeCopyDirectory(src, dst) {
58
36
  try {
59
- if (!fs.existsSync(src)) {
60
- return false; // Source doesn't exist, skip
61
- }
62
-
37
+ if (!fs.existsSync(src)) return false;
63
38
  fs.mkdirSync(dst, { recursive: true });
64
- const entries = fs.readdirSync(src, { withFileTypes: true });
65
-
66
- entries.forEach(entry => {
39
+ fs.readdirSync(src, { withFileTypes: true }).forEach(entry => {
67
40
  const srcPath = path.join(src, entry.name);
68
41
  const dstPath = path.join(dst, entry.name);
69
-
70
- if (entry.isDirectory()) {
71
- safeCopyDirectory(srcPath, dstPath);
72
- } else if (entry.isFile()) {
73
- safeCopyFile(srcPath, dstPath);
74
- }
42
+ if (entry.isDirectory()) safeCopyDirectory(srcPath, dstPath);
43
+ else if (entry.isFile()) safeCopyFile(srcPath, dstPath);
75
44
  });
76
45
  return true;
77
46
  } catch (err) {
78
- // Silently skip errors
79
47
  return false;
80
48
  }
81
49
  }
@@ -84,45 +52,22 @@ function updateGitignore(projectRoot) {
84
52
  try {
85
53
  const gitignorePath = path.join(projectRoot, '.gitignore');
86
54
  const entry = '.gm-stop-verified';
87
-
88
- // Read existing content
89
- let content = '';
90
- if (fs.existsSync(gitignorePath)) {
91
- content = fs.readFileSync(gitignorePath, 'utf-8');
92
- }
93
-
94
- // Check if entry already exists
95
- if (content.includes(entry)) {
96
- return true; // Already there
97
- }
98
-
99
- // Append entry
100
- if (content && !content.endsWith('\n')) {
101
- content += '\n';
102
- }
103
- content += entry + '\n';
104
-
105
- fs.writeFileSync(gitignorePath, content, 'utf-8');
55
+ let content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
56
+ if (content.includes(entry)) return true;
57
+ if (content && !content.endsWith('\n')) content += '\n';
58
+ fs.writeFileSync(gitignorePath, content + entry + '\n', 'utf-8');
106
59
  return true;
107
60
  } catch (err) {
108
- // Silently skip errors
109
61
  return false;
110
62
  }
111
63
  }
112
64
 
113
65
  function install() {
114
- // Only run if inside node_modules
115
- if (!isInsideNodeModules()) {
116
- return; // Silent exit
117
- }
118
-
66
+ if (!isInsideNodeModules()) return;
119
67
  const projectRoot = getProjectRoot();
120
- if (!projectRoot) {
121
- return; // Silent exit
122
- }
123
-
68
+ if (!projectRoot) return;
124
69
  const claudeDir = path.join(projectRoot, '.claude');
125
- const sourceDir = __dirname.replace(/[\/]scripts$/, ''); // Remove /scripts
70
+ const sourceDir = __dirname.replace(/[/\\]scripts$/, '');
126
71
 
127
72
  // Copy files
128
73
  safeCopyDirectory(path.join(sourceDir, 'agents'), path.join(claudeDir, 'agents'));
@@ -131,8 +76,26 @@ function install() {
131
76
 
132
77
  // Update .gitignore
133
78
  updateGitignore(projectRoot);
134
-
79
+
80
+ // Warm bun x cache for packages used by hooks
81
+ warmBunCache();
82
+
135
83
  // Silent success
136
84
  }
137
85
 
86
+ function warmBunCache() {
87
+ const packages = ['mcp-thorns@latest', 'codebasesearch@latest'];
88
+ for (const pkg of packages) {
89
+ try {
90
+ execSync(`bun x ${pkg} --version`, {
91
+ encoding: 'utf-8',
92
+ stdio: 'pipe',
93
+ timeout: 60000
94
+ });
95
+ } catch (e) {
96
+ // Silent - cache warming is best-effort
97
+ }
98
+ }
99
+ }
100
+
138
101
  install();