gm-gc 2.0.368 → 2.0.369
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/gemini-extension.json +1 -1
- package/hooks/hooks.json +49 -23
- package/hooks/pre-tool-use-hook.js +52 -0
- package/hooks/prompt-submit-hook.js +49 -0
- package/hooks/session-start-hook.js +22 -0
- package/hooks/stop-hook-git.js +45 -0
- package/hooks/stop-hook.js +21 -0
- package/package.json +1 -1
package/gemini-extension.json
CHANGED
package/hooks/hooks.json
CHANGED
|
@@ -1,31 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "Hooks for gm Gemini CLI extension",
|
|
3
3
|
"hooks": {
|
|
4
|
-
"BeforeTool":
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"timeout": 60000
|
|
18
|
-
},
|
|
19
|
-
"SessionEnd": [
|
|
4
|
+
"BeforeTool": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node ${extensionPath}/hooks/pre-tool-use-hook.js",
|
|
11
|
+
"timeout": 3600
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"SessionStart": [
|
|
20
17
|
{
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
"matcher": "*",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "node ${extensionPath}/hooks/session-start-hook.js",
|
|
23
|
+
"timeout": 180000
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"BeforeAgent": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "*",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node ${extensionPath}/hooks/prompt-submit-hook.js",
|
|
35
|
+
"timeout": 60000
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"SessionEnd": [
|
|
25
41
|
{
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
42
|
+
"matcher": "*",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "node ${extensionPath}/hooks/stop-hook.js",
|
|
47
|
+
"timeout": 300000
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "node ${extensionPath}/hooks/stop-hook-git.js",
|
|
52
|
+
"timeout": 60000
|
|
53
|
+
}
|
|
54
|
+
]
|
|
29
55
|
}
|
|
30
56
|
]
|
|
31
57
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const writeTools = ['write_file'];
|
|
5
|
+
const forbiddenTools = ['find', 'Find', 'Glob', 'Grep', 'glob', 'search_file_content'];
|
|
6
|
+
const run = () => {
|
|
7
|
+
try {
|
|
8
|
+
const input = fs.readFileSync(0, 'utf-8');
|
|
9
|
+
const data = JSON.parse(input);
|
|
10
|
+
const { tool_name, tool_input } = data;
|
|
11
|
+
if (!tool_name) return { allow: true };
|
|
12
|
+
if (forbiddenTools.includes(tool_name)) {
|
|
13
|
+
return { deny: true, reason: 'Use the code-search skill for codebase exploration instead of Grep/Glob/find. Describe what you need in plain language.' };
|
|
14
|
+
}
|
|
15
|
+
if (writeTools.includes(tool_name)) {
|
|
16
|
+
const file_path = tool_input?.file_path || '';
|
|
17
|
+
const ext = path.extname(file_path);
|
|
18
|
+
const inSkillsDir = file_path.includes('/skills/');
|
|
19
|
+
const base = path.basename(file_path).toLowerCase();
|
|
20
|
+
if ((ext === '.md' || ext === '.txt' || base.startsWith('features_list')) &&
|
|
21
|
+
!base.startsWith('claude') && !base.startsWith('readme') && !inSkillsDir) {
|
|
22
|
+
return { deny: true, reason: 'Cannot create documentation files. Only CLAUDE.md and readme.md are maintained.' };
|
|
23
|
+
}
|
|
24
|
+
if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/.test(base) ||
|
|
25
|
+
file_path.includes('/__tests__/') || file_path.includes('/test/') ||
|
|
26
|
+
file_path.includes('/tests/') || file_path.includes('/__mocks__/')) {
|
|
27
|
+
return { deny: true, reason: 'Test files forbidden on disk. Use real services for all testing.' };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (tool_name === 'run_shell_command') {
|
|
31
|
+
const command = (tool_input?.command || '').trim();
|
|
32
|
+
const isExec = command.startsWith('exec:');
|
|
33
|
+
const isGit = /^(git |gh )/.test(command);
|
|
34
|
+
if (!isExec && !isGit) {
|
|
35
|
+
return { deny: true, reason: 'run_shell_command only allows exec:<lang> syntax or git/gh. Use exec:nodejs, exec:bash, exec:python etc. for code execution.' };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { allow: true };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { allow: true };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
const result = run();
|
|
45
|
+
if (result.deny) {
|
|
46
|
+
console.log(JSON.stringify({ decision: 'deny', reason: result.reason }));
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
process.exit(0);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const pluginRoot = process.env.GEMINI_PROJECT_DIR ? path.join(__dirname, '..') : (process.env.CLAUDE_PLUGIN_ROOT || path.join(__dirname, '..'));
|
|
6
|
+
const projectDir = process.env.GEMINI_PROJECT_DIR || process.cwd();
|
|
7
|
+
const readStdinPrompt = () => {
|
|
8
|
+
try { return JSON.parse(fs.readFileSync(0, 'utf-8')).prompt || ''; } catch (e) { return ''; }
|
|
9
|
+
};
|
|
10
|
+
const readGmAgent = () => {
|
|
11
|
+
try { return fs.readFileSync(path.join(pluginRoot, 'agents/gm.md'), 'utf-8'); } catch (e) { return ''; }
|
|
12
|
+
};
|
|
13
|
+
const runMcpThorns = () => {
|
|
14
|
+
if (!projectDir || !fs.existsSync(projectDir)) return '';
|
|
15
|
+
try {
|
|
16
|
+
let out;
|
|
17
|
+
try { out = execSync('bun x mcp-thorns', { encoding: 'utf-8', stdio: 'pipe', cwd: projectDir, timeout: 180000 }); }
|
|
18
|
+
catch (e) { out = execSync('npx -y mcp-thorns', { encoding: 'utf-8', stdio: 'pipe', cwd: projectDir, timeout: 180000 }); }
|
|
19
|
+
return '=== Repository analysis ===\n' + out;
|
|
20
|
+
} catch (e) { return ''; }
|
|
21
|
+
};
|
|
22
|
+
const runCodeSearch = (query) => {
|
|
23
|
+
if (!query || !projectDir) return '';
|
|
24
|
+
try {
|
|
25
|
+
const q = query.replace(/"/g, '\\"').substring(0, 200);
|
|
26
|
+
let out;
|
|
27
|
+
try { out = execSync(`bun x codebasesearch "${q}"`, { encoding: 'utf-8', stdio: 'pipe', cwd: projectDir, timeout: 55000 }); }
|
|
28
|
+
catch (e) { out = execSync(`npx -y codebasesearch "${q}"`, { encoding: 'utf-8', stdio: 'pipe', cwd: projectDir, timeout: 55000 }); }
|
|
29
|
+
const lines = out.split('\n');
|
|
30
|
+
const start = lines.findIndex(l => l.includes('Searching for:'));
|
|
31
|
+
return start >= 0 ? lines.slice(start).join('\n').trim() : out.trim();
|
|
32
|
+
} catch (e) { return ''; }
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
const prompt = readStdinPrompt();
|
|
36
|
+
const parts = [];
|
|
37
|
+
const gm = readGmAgent();
|
|
38
|
+
if (gm) parts.push(gm);
|
|
39
|
+
const thorns = runMcpThorns();
|
|
40
|
+
if (thorns) parts.push(thorns);
|
|
41
|
+
parts.push('use gm agent | ref: TOOL_INVARIANTS | codesearch for exploration | exec: for execution');
|
|
42
|
+
if (prompt) {
|
|
43
|
+
const sr = runCodeSearch(prompt);
|
|
44
|
+
if (sr) parts.push('=== Semantic code search results ===\n' + sr);
|
|
45
|
+
}
|
|
46
|
+
console.log(JSON.stringify({ systemMessage: parts.join('\n\n') }, null, 2));
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.log(JSON.stringify({ systemMessage: 'use gm agent' }, null, 2));
|
|
49
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
const pluginRoot = path.join(__dirname, '..');
|
|
6
|
+
const projectDir = process.env.GEMINI_PROJECT_DIR || process.cwd();
|
|
7
|
+
try {
|
|
8
|
+
const parts = [];
|
|
9
|
+
try { parts.push(fs.readFileSync(path.join(pluginRoot, 'agents/gm.md'), 'utf-8')); } catch (e) {}
|
|
10
|
+
if (projectDir && fs.existsSync(projectDir)) {
|
|
11
|
+
try {
|
|
12
|
+
let out;
|
|
13
|
+
try { out = execSync('bun x mcp-thorns@latest', { encoding: 'utf-8', stdio: 'pipe', cwd: projectDir, timeout: 180000 }); }
|
|
14
|
+
catch (e) { out = execSync('npx -y mcp-thorns@latest', { encoding: 'utf-8', stdio: 'pipe', cwd: projectDir, timeout: 180000 }); }
|
|
15
|
+
parts.push('=== This is your initial insight of the repository ===\n' + out);
|
|
16
|
+
} catch (e) {}
|
|
17
|
+
}
|
|
18
|
+
parts.push('Use gm as a philosophy to coordinate all plans and the gm subagent to create and execute all plans');
|
|
19
|
+
console.log(JSON.stringify({ systemMessage: parts.join('\n\n') }, null, 2));
|
|
20
|
+
} catch (e) {
|
|
21
|
+
console.log(JSON.stringify({ systemMessage: 'use gm agent' }, null, 2));
|
|
22
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const projectDir = process.env.GEMINI_PROJECT_DIR || process.cwd();
|
|
7
|
+
const counterPath = path.join(require('os').tmpdir(), 'gm-gc-git-' + crypto.createHash('md5').update(projectDir).digest('hex') + '.json');
|
|
8
|
+
const readCounter = () => { try { return JSON.parse(fs.readFileSync(counterPath, 'utf-8')); } catch (e) { return { count: 0, lastHash: null }; } };
|
|
9
|
+
const writeCounter = (d) => { try { fs.writeFileSync(counterPath, JSON.stringify(d)); } catch (e) {} };
|
|
10
|
+
const gitHash = () => { try { return execSync('git rev-parse HEAD', { cwd: projectDir, stdio: 'pipe', encoding: 'utf-8' }).trim(); } catch (e) { return null; } };
|
|
11
|
+
const getStatus = () => {
|
|
12
|
+
try { execSync('git rev-parse --git-dir', { cwd: projectDir, stdio: 'pipe' }); } catch (e) { return null; }
|
|
13
|
+
const status = execSync('git status --porcelain', { cwd: projectDir, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
14
|
+
let unpushed = 0;
|
|
15
|
+
try { unpushed = parseInt(execSync('git rev-list --count @{u}..HEAD', { cwd: projectDir, stdio: 'pipe', encoding: 'utf-8' }).trim()) || 0; } catch (e) { unpushed = -1; }
|
|
16
|
+
return { isDirty: status.length > 0, unpushed };
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
const st = getStatus();
|
|
20
|
+
if (!st) { console.log(JSON.stringify({ decision: 'approve' })); process.exit(0); }
|
|
21
|
+
const hash = gitHash();
|
|
22
|
+
const counter = readCounter();
|
|
23
|
+
if (counter.lastHash && hash && counter.lastHash !== hash) { counter.count = 0; counter.lastHash = hash; writeCounter(counter); }
|
|
24
|
+
const issues = [];
|
|
25
|
+
if (st.isDirty) issues.push('uncommitted changes');
|
|
26
|
+
if (st.unpushed > 0) issues.push(st.unpushed + ' commit(s) not pushed');
|
|
27
|
+
if (st.unpushed === -1) issues.push('push status unknown');
|
|
28
|
+
if (issues.length > 0) {
|
|
29
|
+
counter.count = (counter.count || 0) + 1;
|
|
30
|
+
counter.lastHash = hash;
|
|
31
|
+
writeCounter(counter);
|
|
32
|
+
if (counter.count === 1) {
|
|
33
|
+
console.log(JSON.stringify({ decision: 'block', reason: 'Git: ' + issues.join(', ') + '. Commit and push before ending session.' }, null, 2));
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
console.log(JSON.stringify({ decision: 'approve', reason: 'Git warning #' + counter.count + ': ' + issues.join(', ') }, null, 2));
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
if (counter.count > 0) { counter.count = 0; writeCounter(counter); }
|
|
40
|
+
console.log(JSON.stringify({ decision: 'approve' }, null, 2));
|
|
41
|
+
process.exit(0);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.log(JSON.stringify({ decision: 'approve' }, null, 2));
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const prdFile = path.resolve(process.cwd(), '.prd');
|
|
5
|
+
let aborted = false;
|
|
6
|
+
process.on('SIGTERM', () => { aborted = true; });
|
|
7
|
+
process.on('SIGINT', () => { aborted = true; });
|
|
8
|
+
try {
|
|
9
|
+
if (!aborted && fs.existsSync(prdFile)) {
|
|
10
|
+
const content = fs.readFileSync(prdFile, 'utf-8').trim();
|
|
11
|
+
if (content.length > 0) {
|
|
12
|
+
console.log(JSON.stringify({ decision: 'block', reason: 'Work items remain in .prd. Remove completed items as they finish. Current items:\n\n' + content }, null, 2));
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
console.log(JSON.stringify({ decision: 'approve' }, null, 2));
|
|
17
|
+
process.exit(0);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.log(JSON.stringify({ decision: 'approve' }, null, 2));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|