gm-cc 2.0.246 → 2.0.250
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-plugin/marketplace.json +1 -1
- package/hooks/hooks.json +12 -7
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/bootstrap.js +45 -0
- package/scripts/postinstall-kilo.js +55 -42
- package/scripts/postinstall-oc.js +57 -42
- package/scripts/postinstall.js +137 -101
- package/hooks/pre-tool-use-hook.js +0 -504
- package/hooks/prompt-submit-hook.js +0 -177
- package/hooks/session-start-hook.js +0 -188
- package/hooks/stop-hook-git.js +0 -190
- package/hooks/stop-hook.js +0 -57
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
if (process.env.AGENTGUI_SUBPROCESS === '1') {
|
|
4
|
-
console.log(JSON.stringify({ additionalContext: '' }));
|
|
5
|
-
process.exit(0);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const os = require('os');
|
|
11
|
-
const { execSync, spawnSync } = require('child_process');
|
|
12
|
-
|
|
13
|
-
const IS_WIN = process.platform === 'win32';
|
|
14
|
-
const TOOLS_DIR = path.join(os.homedir(), '.claude', 'gm-tools');
|
|
15
|
-
|
|
16
|
-
function localBin(name) {
|
|
17
|
-
const ext = IS_WIN ? '.exe' : '';
|
|
18
|
-
return path.join(TOOLS_DIR, 'node_modules', '.bin', name + ext);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function runLocal(name, args, opts = {}) {
|
|
22
|
-
const bin = localBin(name);
|
|
23
|
-
if (fs.existsSync(bin)) {
|
|
24
|
-
return spawnSync(bin, args, { encoding: 'utf8', windowsHide: true, timeout: 30000, ...opts });
|
|
25
|
-
}
|
|
26
|
-
return spawnSync('bun', ['x', name, ...args], { encoding: 'utf8', windowsHide: true, timeout: 30000, ...opts });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function plugkitBin() { return path.join(TOOLS_DIR, IS_WIN ? 'plugkit.exe' : 'plugkit'); }
|
|
30
|
-
|
|
31
|
-
function runPlugkit(args, opts = {}) {
|
|
32
|
-
const bin = plugkitBin();
|
|
33
|
-
if (fs.existsSync(bin)) {
|
|
34
|
-
return spawnSync(bin, args, { encoding: 'utf8', windowsHide: true, timeout: 30000, ...opts });
|
|
35
|
-
}
|
|
36
|
-
return spawnSync('plugkit', args, { encoding: 'utf8', windowsHide: true, timeout: 30000, ...opts });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.GEMINI_PROJECT_DIR || process.env.OC_PROJECT_DIR || process.env.KILO_PROJECT_DIR;
|
|
40
|
-
|
|
41
|
-
function loadLangPlugins(dir) {
|
|
42
|
-
if (!dir) return [];
|
|
43
|
-
const langDir = path.join(dir, 'lang');
|
|
44
|
-
if (!fs.existsSync(langDir)) return [];
|
|
45
|
-
try {
|
|
46
|
-
return fs.readdirSync(langDir)
|
|
47
|
-
.filter(f => f.endsWith('.js') && f !== 'loader.js' && f !== 'SPEC.md')
|
|
48
|
-
.reduce((acc, f) => {
|
|
49
|
-
try {
|
|
50
|
-
const p = require(path.join(langDir, f));
|
|
51
|
-
if (p && typeof p.id === 'string' && p.exec && p.exec.match instanceof RegExp && typeof p.exec.run === 'function') acc.push(p);
|
|
52
|
-
} catch (_) {}
|
|
53
|
-
return acc;
|
|
54
|
-
}, []);
|
|
55
|
-
} catch (_) { return []; }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function walkFiles(dir, exts, depth) {
|
|
59
|
-
if (depth <= 0 || !fs.existsSync(dir)) return [];
|
|
60
|
-
let results = [];
|
|
61
|
-
try {
|
|
62
|
-
for (const f of fs.readdirSync(dir)) {
|
|
63
|
-
if (f.startsWith('.') || f === 'node_modules') continue;
|
|
64
|
-
const full = path.join(dir, f);
|
|
65
|
-
const stat = fs.statSync(full);
|
|
66
|
-
if (stat.isDirectory()) results = results.concat(walkFiles(full, exts, depth - 1));
|
|
67
|
-
else if (exts.some(e => f.endsWith(e))) results.push({ path: full, mtime: stat.mtimeMs });
|
|
68
|
-
}
|
|
69
|
-
} catch (_) {}
|
|
70
|
-
return results;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function getLangPluginContext(dir) {
|
|
74
|
-
const plugins = loadLangPlugins(dir);
|
|
75
|
-
if (!plugins.length) return '';
|
|
76
|
-
const parts = [];
|
|
77
|
-
for (const p of plugins) {
|
|
78
|
-
if (p.context) {
|
|
79
|
-
const ctx = typeof p.context === 'function' ? p.context() : p.context;
|
|
80
|
-
if (ctx) parts.push(String(ctx).slice(0, 2000));
|
|
81
|
-
}
|
|
82
|
-
if (p.lsp && p.extensions && p.extensions.length) {
|
|
83
|
-
try {
|
|
84
|
-
const files = walkFiles(dir, p.extensions, 4)
|
|
85
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
86
|
-
.slice(0, 3);
|
|
87
|
-
const diags = [];
|
|
88
|
-
for (const f of files) {
|
|
89
|
-
try {
|
|
90
|
-
const code = fs.readFileSync(f.path, 'utf-8');
|
|
91
|
-
const results = p.lsp.check(code, dir);
|
|
92
|
-
if (Array.isArray(results)) {
|
|
93
|
-
for (const d of results) diags.push(`${path.relative(dir, f.path)}:${d.line}:${d.col}: ${d.severity}: ${d.message}`);
|
|
94
|
-
}
|
|
95
|
-
} catch (_) {}
|
|
96
|
-
}
|
|
97
|
-
if (diags.length) parts.push(`=== ${p.id} LSP diagnostics ===\n${diags.join('\n').slice(0, 3000)}`);
|
|
98
|
-
} catch (_) {}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return parts.join('\n\n');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const ensureGitignore = () => {
|
|
105
|
-
if (!projectDir) return;
|
|
106
|
-
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
107
|
-
const entry = '.gm-stop-verified';
|
|
108
|
-
try {
|
|
109
|
-
let content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
|
|
110
|
-
if (!content.split('\n').some(line => line.trim() === entry)) {
|
|
111
|
-
content = (content.endsWith('\n') || content === '' ? content : content + '\n') + entry + '\n';
|
|
112
|
-
fs.writeFileSync(gitignorePath, content);
|
|
113
|
-
}
|
|
114
|
-
} catch (e) {}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const runThorns = () => {
|
|
118
|
-
if (!projectDir || !fs.existsSync(projectDir)) return '';
|
|
119
|
-
try {
|
|
120
|
-
const r = runPlugkit(['codeinsight', projectDir], { timeout: 15000 });
|
|
121
|
-
const out = ((r.stdout || '') + (r.stderr || '')).trim();
|
|
122
|
-
return out ? `=== codeinsight ===\n${out}` : '';
|
|
123
|
-
} catch (e) {
|
|
124
|
-
return '';
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const runCodeSearch = (prompt) => {
|
|
129
|
-
if (!prompt || !projectDir) return '';
|
|
130
|
-
try {
|
|
131
|
-
const r = runPlugkit(['search', '--path', projectDir, prompt], { timeout: 10000, cwd: projectDir });
|
|
132
|
-
const out = ((r.stdout || '') + (r.stderr || '')).trim();
|
|
133
|
-
return out ? `=== search ===\n${out}` : '';
|
|
134
|
-
} catch (e) {
|
|
135
|
-
return '';
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const emit = (additionalContext) => {
|
|
140
|
-
const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
|
|
141
|
-
const isOpenCode = process.env.OC_PROJECT_DIR !== undefined;
|
|
142
|
-
const isKilo = process.env.KILO_PROJECT_DIR !== undefined;
|
|
143
|
-
if (isGemini) {
|
|
144
|
-
console.log(JSON.stringify({ systemMessage: additionalContext }, null, 2));
|
|
145
|
-
} else if (isOpenCode || isKilo) {
|
|
146
|
-
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'message.updated', additionalContext } }, null, 2));
|
|
147
|
-
} else {
|
|
148
|
-
console.log(JSON.stringify({ additionalContext }, null, 2));
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
let prompt = '';
|
|
154
|
-
try {
|
|
155
|
-
const input = JSON.parse(fs.readFileSync(0, 'utf-8'));
|
|
156
|
-
prompt = input.prompt || input.message || input.userMessage || '';
|
|
157
|
-
} catch (e) {}
|
|
158
|
-
|
|
159
|
-
ensureGitignore();
|
|
160
|
-
|
|
161
|
-
const parts = [];
|
|
162
|
-
parts.push('Use the Skill tool with skill: "gm" to begin — do NOT use the Agent tool to load skills. Skills are invoked via the Skill tool only, never as agents. DO NOT use EnterPlanMode.');
|
|
163
|
-
|
|
164
|
-
const search = runCodeSearch(prompt);
|
|
165
|
-
if (search) parts.push(search);
|
|
166
|
-
|
|
167
|
-
const thorns = runThorns();
|
|
168
|
-
if (thorns) parts.push(thorns);
|
|
169
|
-
|
|
170
|
-
const langCtx = getLangPluginContext(projectDir);
|
|
171
|
-
if (langCtx) parts.push(langCtx);
|
|
172
|
-
|
|
173
|
-
emit(parts.join('\n\n'));
|
|
174
|
-
} catch (error) {
|
|
175
|
-
emit('Invoke the `gm` skill to begin. Hook error: ' + error.message);
|
|
176
|
-
process.exit(0);
|
|
177
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const { spawnSync } = require('child_process');
|
|
7
|
-
|
|
8
|
-
const IS_WIN = process.platform === 'win32';
|
|
9
|
-
const TOOLS_DIR = path.join(os.homedir(), '.claude', 'gm-tools');
|
|
10
|
-
|
|
11
|
-
function localBin(name) {
|
|
12
|
-
const ext = IS_WIN ? '.exe' : '';
|
|
13
|
-
return path.join(TOOLS_DIR, 'node_modules', '.bin', name + ext);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function pkgEntry(name) {
|
|
17
|
-
try {
|
|
18
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(TOOLS_DIR, 'node_modules', name, 'package.json'), 'utf8'));
|
|
19
|
-
const binVal = pkg.bin;
|
|
20
|
-
const rel = typeof binVal === 'string' ? binVal : (binVal?.[name] || Object.values(binVal || {})[0]);
|
|
21
|
-
if (rel) return path.join(TOOLS_DIR, 'node_modules', name, rel);
|
|
22
|
-
} catch {}
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function runLocal(name, args, opts = {}) {
|
|
27
|
-
if (IS_WIN) {
|
|
28
|
-
const entry = pkgEntry(name);
|
|
29
|
-
if (entry && fs.existsSync(entry)) {
|
|
30
|
-
return spawnSync('bun', [entry, ...args], { encoding: 'utf8', windowsHide: true, timeout: 30000, ...opts });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
const bin = localBin(name);
|
|
34
|
-
if (fs.existsSync(bin)) {
|
|
35
|
-
return spawnSync(bin, args, { encoding: 'utf8', windowsHide: true, timeout: 30000, ...opts });
|
|
36
|
-
}
|
|
37
|
-
return spawnSync('bun', ['x', name, ...args], { encoding: 'utf8', windowsHide: true, timeout: 30000, ...opts });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const MANAGED_PKGS = ['agent-browser'];
|
|
41
|
-
const PKG_JSON = path.join(TOOLS_DIR, 'package.json');
|
|
42
|
-
|
|
43
|
-
const PLUGKIT_REPO = 'AnEntrypoint/rs-plugkit';
|
|
44
|
-
const archMap = { x64: 'x86_64', arm64: 'aarch64', ia32: 'x86_64' };
|
|
45
|
-
const plugkitTargets = {
|
|
46
|
-
win32: a => `plugkit-x86_64-pc-windows-msvc/plugkit.exe`,
|
|
47
|
-
darwin: a => `plugkit-x86_64-unknown-linux-gnu/plugkit`,
|
|
48
|
-
linux: a => `plugkit-x86_64-unknown-linux-gnu/plugkit`,
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
function plugkitBin() { return path.join(TOOLS_DIR, IS_WIN ? 'plugkit.exe' : 'plugkit'); }
|
|
52
|
-
|
|
53
|
-
function downloadBin(assetPath, dest) {
|
|
54
|
-
const https = require('https');
|
|
55
|
-
const url = `https://github.com/${PLUGKIT_REPO}/releases/latest/download/${assetPath}`;
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
const follow = (u) => https.get(u, { headers: { 'User-Agent': 'gm' } }, res => {
|
|
58
|
-
if (res.statusCode >= 300 && res.statusCode < 400) return follow(res.headers.location);
|
|
59
|
-
const chunks = [];
|
|
60
|
-
res.on('data', c => chunks.push(c));
|
|
61
|
-
res.on('end', () => { try { fs.writeFileSync(dest, Buffer.concat(chunks)); fs.chmodSync(dest, 0o755); } catch {} resolve(); });
|
|
62
|
-
}).on('error', () => resolve());
|
|
63
|
-
follow(url);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function ensurePlugkit() {
|
|
68
|
-
const bin = plugkitBin();
|
|
69
|
-
if (!fs.existsSync(bin)) {
|
|
70
|
-
const assetPath = plugkitTargets[process.platform]?.(archMap[process.arch] || 'x86_64') || plugkitTargets.linux('x86_64');
|
|
71
|
-
await downloadBin(assetPath, bin);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function ensureTools() {
|
|
76
|
-
try { fs.mkdirSync(TOOLS_DIR, { recursive: true }); } catch {}
|
|
77
|
-
if (!fs.existsSync(PKG_JSON)) {
|
|
78
|
-
try { fs.writeFileSync(PKG_JSON, JSON.stringify({ name: 'gm-tools', version: '1.0.0', private: true })); } catch {}
|
|
79
|
-
}
|
|
80
|
-
const missing = MANAGED_PKGS.filter(p => !fs.existsSync(localBin(p)));
|
|
81
|
-
if (missing.length > 0) {
|
|
82
|
-
try {
|
|
83
|
-
spawnSync('bun', ['add', ...missing.map(p => p + '@latest')], {
|
|
84
|
-
cwd: TOOLS_DIR, encoding: 'utf8', timeout: 180000, windowsHide: true
|
|
85
|
-
});
|
|
86
|
-
} catch {}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
ensureTools();
|
|
91
|
-
ensurePlugkit().catch(() => {});
|
|
92
|
-
|
|
93
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.GEMINI_PROJECT_DIR || process.env.OC_PROJECT_DIR || process.env.KILO_PROJECT_DIR;
|
|
94
|
-
|
|
95
|
-
function loadLangPlugins(dir) {
|
|
96
|
-
if (!dir) return [];
|
|
97
|
-
const langDir = path.join(dir, 'lang');
|
|
98
|
-
if (!fs.existsSync(langDir)) return [];
|
|
99
|
-
try {
|
|
100
|
-
return fs.readdirSync(langDir)
|
|
101
|
-
.filter(f => f.endsWith('.js') && f !== 'loader.js' && f !== 'SPEC.md')
|
|
102
|
-
.reduce((acc, f) => {
|
|
103
|
-
try {
|
|
104
|
-
const p = require(path.join(langDir, f));
|
|
105
|
-
if (p && typeof p.id === 'string' && p.exec && p.exec.match instanceof RegExp && typeof p.exec.run === 'function') acc.push(p);
|
|
106
|
-
} catch (_) {}
|
|
107
|
-
return acc;
|
|
108
|
-
}, []);
|
|
109
|
-
} catch (_) { return []; }
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function getLangPluginContext(dir) {
|
|
113
|
-
const plugins = loadLangPlugins(dir);
|
|
114
|
-
return plugins
|
|
115
|
-
.filter(p => p.context)
|
|
116
|
-
.map(p => { const ctx = typeof p.context === 'function' ? p.context() : p.context; return ctx ? String(ctx).slice(0, 2000) : ''; })
|
|
117
|
-
.filter(Boolean)
|
|
118
|
-
.join('\n\n');
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const ensureGitignore = () => {
|
|
122
|
-
if (!projectDir) return;
|
|
123
|
-
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
124
|
-
const entry = '.gm-stop-verified';
|
|
125
|
-
try {
|
|
126
|
-
let content = '';
|
|
127
|
-
if (fs.existsSync(gitignorePath)) {
|
|
128
|
-
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
129
|
-
}
|
|
130
|
-
if (!content.split('\n').some(line => line.trim() === entry)) {
|
|
131
|
-
const newContent = content.endsWith('\n') || content === ''
|
|
132
|
-
? content + entry + '\n'
|
|
133
|
-
: content + '\n' + entry + '\n';
|
|
134
|
-
fs.writeFileSync(gitignorePath, newContent);
|
|
135
|
-
}
|
|
136
|
-
} catch (e) {}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
ensureGitignore();
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
let outputs = [];
|
|
143
|
-
|
|
144
|
-
outputs.push('Use the Skill tool with skill: "gm" to begin — do NOT use the Agent tool to load skills. Skills are invoked via the Skill tool only, never as agents. All code execution uses exec:<lang> via the Bash tool — never direct Bash(node ...) or Bash(npm ...) or Bash(npx ...) or Bash(plugkit ...).');
|
|
145
|
-
|
|
146
|
-
if (projectDir && fs.existsSync(projectDir)) {
|
|
147
|
-
try {
|
|
148
|
-
const bin = plugkitBin();
|
|
149
|
-
const r = fs.existsSync(bin)
|
|
150
|
-
? spawnSync(bin, ['codeinsight', projectDir], { encoding: 'utf8', windowsHide: true, timeout: 15000 })
|
|
151
|
-
: spawnSync('plugkit', ['codeinsight', projectDir], { encoding: 'utf8', windowsHide: true, timeout: 15000 });
|
|
152
|
-
const insightOutput = ((r.stdout || '') + (r.stderr || '')).trim();
|
|
153
|
-
if (insightOutput) {
|
|
154
|
-
outputs.push(`=== This is your initial insight of the repository, look at every possible aspect of this for initial opinionation and to offset the need for code exploration ===\n${insightOutput}`);
|
|
155
|
-
}
|
|
156
|
-
} catch (e) {}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const langCtx = getLangPluginContext(projectDir);
|
|
160
|
-
if (langCtx) outputs.push(langCtx);
|
|
161
|
-
|
|
162
|
-
const additionalContext = outputs.join('\n\n');
|
|
163
|
-
|
|
164
|
-
const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
|
|
165
|
-
const isOpenCode = process.env.OC_PLUGIN_ROOT !== undefined;
|
|
166
|
-
const isKilo = process.env.KILO_PLUGIN_ROOT !== undefined;
|
|
167
|
-
|
|
168
|
-
if (isGemini) {
|
|
169
|
-
console.log(JSON.stringify({ systemMessage: additionalContext }, null, 2));
|
|
170
|
-
} else if (isOpenCode || isKilo) {
|
|
171
|
-
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'session.created', additionalContext } }, null, 2));
|
|
172
|
-
} else {
|
|
173
|
-
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext } }, null, 2));
|
|
174
|
-
}
|
|
175
|
-
} catch (error) {
|
|
176
|
-
const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
|
|
177
|
-
const isOpenCode = process.env.OC_PLUGIN_ROOT !== undefined;
|
|
178
|
-
const isKilo = process.env.KILO_PLUGIN_ROOT !== undefined;
|
|
179
|
-
|
|
180
|
-
if (isGemini) {
|
|
181
|
-
console.log(JSON.stringify({ systemMessage: `Error executing hook: ${error.message}` }, null, 2));
|
|
182
|
-
} else if (isOpenCode || isKilo) {
|
|
183
|
-
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'session.created', additionalContext: `Error executing hook: ${error.message}` } }, null, 2));
|
|
184
|
-
} else {
|
|
185
|
-
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: `Error executing hook: ${error.message}` } }, null, 2));
|
|
186
|
-
}
|
|
187
|
-
process.exit(0);
|
|
188
|
-
}
|
package/hooks/stop-hook-git.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
if (process.env.AGENTGUI_SUBPROCESS === '1') {
|
|
4
|
-
console.log(JSON.stringify({ decision: 'approve' }));
|
|
5
|
-
process.exit(0);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const { execSync } = require('child_process');
|
|
9
|
-
const fs = require('fs');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const crypto = require('crypto');
|
|
13
|
-
|
|
14
|
-
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
15
|
-
|
|
16
|
-
const getCounterPath = () => {
|
|
17
|
-
const hash = crypto.createHash('md5').update(projectDir).digest('hex');
|
|
18
|
-
return path.join(os.tmpdir(), `gm-git-block-counter-${hash}.json`);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const readCounter = () => {
|
|
22
|
-
try {
|
|
23
|
-
const counterPath = getCounterPath();
|
|
24
|
-
if (fs.existsSync(counterPath)) {
|
|
25
|
-
const data = fs.readFileSync(counterPath, 'utf-8');
|
|
26
|
-
return JSON.parse(data);
|
|
27
|
-
}
|
|
28
|
-
} catch (e) {}
|
|
29
|
-
return { count: 0, lastGitHash: null };
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const writeCounter = (data) => {
|
|
33
|
-
try {
|
|
34
|
-
const counterPath = getCounterPath();
|
|
35
|
-
fs.writeFileSync(counterPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
36
|
-
} catch (e) {}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const getCurrentGitHash = () => {
|
|
40
|
-
try {
|
|
41
|
-
const hash = execSync('git rev-parse HEAD', {
|
|
42
|
-
cwd: projectDir,
|
|
43
|
-
stdio: 'pipe',
|
|
44
|
-
encoding: 'utf-8'
|
|
45
|
-
}).trim();
|
|
46
|
-
return hash;
|
|
47
|
-
} catch (e) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const resetCounterIfCommitted = (currentHash) => {
|
|
53
|
-
const counter = readCounter();
|
|
54
|
-
if (counter.lastGitHash && currentHash && counter.lastGitHash !== currentHash) {
|
|
55
|
-
counter.count = 0;
|
|
56
|
-
counter.lastGitHash = currentHash;
|
|
57
|
-
writeCounter(counter);
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const incrementCounter = (currentHash) => {
|
|
64
|
-
const counter = readCounter();
|
|
65
|
-
counter.count = (counter.count || 0) + 1;
|
|
66
|
-
counter.lastGitHash = currentHash;
|
|
67
|
-
writeCounter(counter);
|
|
68
|
-
return counter.count;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const getGitStatus = () => {
|
|
72
|
-
try {
|
|
73
|
-
execSync('git rev-parse --git-dir', {
|
|
74
|
-
cwd: projectDir,
|
|
75
|
-
stdio: 'pipe'
|
|
76
|
-
});
|
|
77
|
-
} catch (e) {
|
|
78
|
-
return { isRepo: false };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const status = execSync('git status --porcelain', {
|
|
83
|
-
cwd: projectDir,
|
|
84
|
-
stdio: 'pipe',
|
|
85
|
-
encoding: 'utf-8'
|
|
86
|
-
}).trim();
|
|
87
|
-
|
|
88
|
-
const isDirty = status.length > 0;
|
|
89
|
-
|
|
90
|
-
let unpushedCount = 0;
|
|
91
|
-
try {
|
|
92
|
-
const unpushed = execSync('git rev-list --count @{u}..HEAD', {
|
|
93
|
-
cwd: projectDir,
|
|
94
|
-
stdio: 'pipe',
|
|
95
|
-
encoding: 'utf-8'
|
|
96
|
-
}).trim();
|
|
97
|
-
unpushedCount = parseInt(unpushed, 10) || 0;
|
|
98
|
-
} catch (e) {
|
|
99
|
-
unpushedCount = -1;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let behindCount = 0;
|
|
103
|
-
try {
|
|
104
|
-
const behind = execSync('git rev-list --count HEAD..@{u}', {
|
|
105
|
-
cwd: projectDir,
|
|
106
|
-
stdio: 'pipe',
|
|
107
|
-
encoding: 'utf-8'
|
|
108
|
-
}).trim();
|
|
109
|
-
behindCount = parseInt(behind, 10) || 0;
|
|
110
|
-
} catch (e) {}
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
isRepo: true,
|
|
114
|
-
isDirty,
|
|
115
|
-
unpushedCount,
|
|
116
|
-
behindCount,
|
|
117
|
-
statusOutput: status
|
|
118
|
-
};
|
|
119
|
-
} catch (e) {
|
|
120
|
-
return { isRepo: true, isDirty: false, unpushedCount: 0, behindCount: 0 };
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const run = () => {
|
|
125
|
-
const gitStatus = getGitStatus();
|
|
126
|
-
if (!gitStatus.isRepo) return { ok: true };
|
|
127
|
-
|
|
128
|
-
const currentHash = getCurrentGitHash();
|
|
129
|
-
resetCounterIfCommitted(currentHash);
|
|
130
|
-
|
|
131
|
-
const issues = [];
|
|
132
|
-
if (gitStatus.isDirty) {
|
|
133
|
-
issues.push('Uncommitted changes exist');
|
|
134
|
-
}
|
|
135
|
-
if (gitStatus.unpushedCount > 0) {
|
|
136
|
-
issues.push(`${gitStatus.unpushedCount} commit(s) not pushed`);
|
|
137
|
-
}
|
|
138
|
-
if (gitStatus.unpushedCount === -1) {
|
|
139
|
-
issues.push('Unable to verify push status - may have unpushed commits');
|
|
140
|
-
}
|
|
141
|
-
if (gitStatus.behindCount > 0) {
|
|
142
|
-
issues.push(`${gitStatus.behindCount} upstream change(s) not pulled`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (issues.length > 0) {
|
|
146
|
-
const blockCount = incrementCounter(currentHash);
|
|
147
|
-
return {
|
|
148
|
-
ok: false,
|
|
149
|
-
reason: `${issues.join(', ')}, must push to remote`,
|
|
150
|
-
blockCount
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const counter = readCounter();
|
|
155
|
-
if (counter.count > 0) {
|
|
156
|
-
counter.count = 0;
|
|
157
|
-
writeCounter(counter);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return { ok: true };
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
const result = run();
|
|
165
|
-
if (!result.ok) {
|
|
166
|
-
if (result.blockCount === 1) {
|
|
167
|
-
console.log(JSON.stringify({
|
|
168
|
-
decision: 'block',
|
|
169
|
-
reason: `Git: ${result.reason}`
|
|
170
|
-
}, null, 2));
|
|
171
|
-
process.exit(2);
|
|
172
|
-
} else if (result.blockCount > 1) {
|
|
173
|
-
console.log(JSON.stringify({
|
|
174
|
-
decision: 'approve',
|
|
175
|
-
reason: `⚠️ Git warning (attempt #${result.blockCount}): ${result.reason} - Please commit and push your changes.`
|
|
176
|
-
}, null, 2));
|
|
177
|
-
process.exit(0);
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
console.log(JSON.stringify({
|
|
181
|
-
decision: 'approve'
|
|
182
|
-
}, null, 2));
|
|
183
|
-
process.exit(0);
|
|
184
|
-
}
|
|
185
|
-
} catch (e) {
|
|
186
|
-
console.log(JSON.stringify({
|
|
187
|
-
decision: 'approve'
|
|
188
|
-
}, null, 2));
|
|
189
|
-
process.exit(0);
|
|
190
|
-
}
|
package/hooks/stop-hook.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
if (process.env.AGENTGUI_SUBPROCESS === '1') {
|
|
4
|
-
console.log(JSON.stringify({ decision: 'approve' }));
|
|
5
|
-
process.exit(0);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const fs = require('fs');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
|
|
11
|
-
const projectDir = process.cwd();
|
|
12
|
-
const prdFile = path.resolve(projectDir, '.prd');
|
|
13
|
-
|
|
14
|
-
let aborted = false;
|
|
15
|
-
process.on('SIGTERM', () => { aborted = true; });
|
|
16
|
-
process.on('SIGINT', () => { aborted = true; });
|
|
17
|
-
|
|
18
|
-
const run = () => {
|
|
19
|
-
if (aborted) return { ok: true };
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
if (fs.existsSync(prdFile)) {
|
|
23
|
-
const prdContent = fs.readFileSync(prdFile, 'utf-8').trim();
|
|
24
|
-
if (prdContent.length > 0) {
|
|
25
|
-
return {
|
|
26
|
-
ok: false,
|
|
27
|
-
reason: `Work items remain in ${prdFile}. Remove completed items as they finish. Delete the file when all items are done.\n\n${prdContent}`
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return { ok: true };
|
|
32
|
-
} catch (error) {
|
|
33
|
-
return { ok: true };
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const result = run();
|
|
39
|
-
|
|
40
|
-
if (!result.ok) {
|
|
41
|
-
console.log(JSON.stringify({
|
|
42
|
-
decision: 'block',
|
|
43
|
-
reason: result.reason
|
|
44
|
-
}, null, 2));
|
|
45
|
-
process.exit(2);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log(JSON.stringify({
|
|
49
|
-
decision: 'approve'
|
|
50
|
-
}, null, 2));
|
|
51
|
-
process.exit(0);
|
|
52
|
-
} catch (e) {
|
|
53
|
-
console.log(JSON.stringify({
|
|
54
|
-
decision: 'approve'
|
|
55
|
-
}, null, 2));
|
|
56
|
-
process.exit(0);
|
|
57
|
-
}
|