gm-oc 2.0.181 → 2.0.185

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/gm-oc.mjs CHANGED
@@ -1,25 +1,117 @@
1
- import { readFileSync, existsSync } from 'fs';
2
- import { join, dirname } from 'path';
1
+ import { readFileSync, existsSync, writeFileSync, unlinkSync } from 'fs';
2
+ import { join, dirname, extname, basename } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { spawnSync } from 'child_process';
5
+ import { tmpdir } from 'os';
4
6
 
5
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const LANG_ALIASES = { js:'nodejs',javascript:'nodejs',ts:'typescript',node:'nodejs',py:'python',sh:'bash',shell:'bash',zsh:'bash' };
9
+ const FORBIDDEN_TOOLS = new Set(['glob','Glob','grep','Grep','search_file_content','Find','find']);
10
+ const FORBIDDEN_FILE_RE = [/\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/, /^(jest|vitest|mocha|ava|jasmine|tap)\.(config|setup)/, /\.(snap|stub|mock|fixture)\.(js|ts|json)$/];
11
+ const FORBIDDEN_PATH_RE = ['/__tests__/','/test/','/tests/','/fixtures/','/test-data/',"/__mocks__/"];
12
+ const DOC_BLOCK_RE = /\.(md|txt)$/;
13
+
14
+ function detectLang(src) {
15
+ if (/^\s*(import |from |export |const |let |var |function |class |async |await |console\.|process\.)/.test(src)) return 'nodejs';
16
+ if (/^\s*(import |def |print\(|class |if __name__)/.test(src)) return 'python';
17
+ return 'bash';
18
+ }
19
+
20
+ function stripFooter(s) { return s ? s.replace(/\n\[Running tools\][\s\S]*$/, '').trimEnd() : ''; }
21
+
22
+ function runExec(rawLang, code, cwd) {
23
+ const lang = LANG_ALIASES[rawLang] || (rawLang || detectLang(code));
24
+ const opts = { encoding: 'utf-8', timeout: 30000, ...(cwd && { cwd }) };
25
+ const out = (r) => { const o = (r.stdout||'').trimEnd(), e = stripFooter(r.stderr||'').trimEnd(); return o && e ? o+'\n[stderr]\n'+e : o||e||'(no output)'; };
26
+ if (lang === 'python') return out(spawnSync('python3',['-c',code],opts));
27
+ const ext = lang === 'typescript' ? 'ts' : lang === 'bash' ? 'sh' : 'mjs';
28
+ const tmp = join(tmpdir(),'gm-plugin-'+Date.now()+'.'+ext);
29
+ const src = lang === 'bash' ? code : 'const __r=await(async()=>{\n'+code+'\n})();if(__r!==undefined){if(typeof __r==="object"){console.log(JSON.stringify(__r,null,2));}else{console.log(__r);}}';
30
+ writeFileSync(tmp,src,'utf-8');
31
+ const r = lang === 'bash' ? spawnSync('bash',[tmp],opts) : spawnSync('bun',['run',tmp],opts);
32
+ try { unlinkSync(tmp); } catch(e) {}
33
+ let result = out(r);
34
+ if (lang !== 'bash') result = result.split(tmp).join('<script>');
35
+ return result;
36
+ }
37
+
38
+ function safePrintf(s) {
39
+ return "printf '%s' '" + String(s).replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"'\\\\''")+"'";
40
+ }
41
+
42
+ function runThorns(dir) {
43
+ try {
44
+ const r = spawnSync('bun',['x','mcp-thorns@latest'],{encoding:'utf-8',timeout:15000,cwd:dir});
45
+ return r.killed ? '' : (r.stdout||'').trim();
46
+ } catch(e) { return ''; }
47
+ }
48
+
49
+ function runSearch(query, dir) {
50
+ try {
51
+ const r = spawnSync('bun',['x','codebasesearch',query],{encoding:'utf-8',timeout:10000,cwd:dir});
52
+ return (r.stdout||'').trim();
53
+ } catch(e) { return ''; }
54
+ }
6
55
 
7
56
  export async function GmPlugin({ directory }) {
8
57
  const agentMd = join(__dirname, '..', 'agents', 'gm.md');
9
58
  const prdFile = join(directory, '.prd');
59
+ const injectedSessions = new Set();
10
60
 
11
61
  return {
12
62
  'experimental.chat.system.transform': async (input, output) => {
13
- try {
14
- const rules = readFileSync(agentMd, 'utf-8');
15
- if (rules) output.system.unshift(rules);
16
- } catch (e) {}
63
+ try { const rules = readFileSync(agentMd,'utf-8'); if (rules) output.system.unshift(rules); } catch(e) {}
17
64
  try {
18
65
  if (existsSync(prdFile)) {
19
- const prd = readFileSync(prdFile, 'utf-8').trim();
20
- if (prd) output.system.push('\nPENDING WORK (.prd):\n' + prd);
66
+ const prd = readFileSync(prdFile,'utf-8').trim();
67
+ if (prd) output.system.push('\nPENDING WORK (.prd):\n'+prd);
68
+ }
69
+ } catch(e) {}
70
+ },
71
+
72
+ 'experimental.chat.messages.transform': async (input, output) => {
73
+ const msgs = output.messages;
74
+ const lastUserIdx = msgs ? msgs.findLastIndex(m => m.info && m.info.role === 'user') : -1;
75
+ if (lastUserIdx === -1) return;
76
+ const msg = msgs[lastUserIdx];
77
+ const sessionID = msg.info && msg.info.sessionID;
78
+ if (sessionID && injectedSessions.has(sessionID)) return;
79
+ if (sessionID) injectedSessions.add(sessionID);
80
+ const textPart = msg.parts && msg.parts.find(p => p.type === 'text' && p.text && p.text.trim());
81
+ const prompt = textPart ? textPart.text.trim() : '';
82
+ const parts = [];
83
+ parts.push('Invoke the `gm` skill to begin. DO NOT use EnterPlanMode.');
84
+ const thorns = runThorns(directory);
85
+ if (thorns) parts.push('=== mcp-thorns ===\n'+thorns);
86
+ if (prompt) { const s = runSearch(prompt, directory); if (s) parts.push('=== codebasesearch ===\n'+s); }
87
+ const injection = '<system-reminder>\n'+parts.join('\n\n')+'\n</system-reminder>';
88
+ if (textPart) textPart.text = injection + '\n' + textPart.text;
89
+ else if (msg.parts) msg.parts.unshift({ type: 'text', text: injection });
90
+ },
91
+
92
+ 'tool.execute.before': async (input, output) => {
93
+ if (FORBIDDEN_TOOLS.has(input.tool)) {
94
+ output.args = { ...output.args, command: safePrintf('Use the code-search skill for codebase exploration instead of '+input.tool+'. Describe what you need in plain language.') };
95
+ return;
96
+ }
97
+ if (input.tool === 'write' || input.tool === 'Write') {
98
+ const fp = (output.args && output.args.file_path) || '';
99
+ const base = basename(fp).toLowerCase();
100
+ const ext = extname(fp);
101
+ const blocked = FORBIDDEN_FILE_RE.some(re => re.test(base)) || FORBIDDEN_PATH_RE.some(p => fp.includes(p))
102
+ || (DOC_BLOCK_RE.test(ext) && !base.startsWith('claude') && !base.startsWith('readme') && !fp.includes('/skills/'));
103
+ if (blocked) {
104
+ output.args = { ...output.args, command: safePrintf('Cannot create test/doc files. Use .prd for task notes, CLAUDE.md for permanent notes.') };
105
+ return;
21
106
  }
22
- } catch (e) {}
107
+ }
108
+ if (input.tool !== 'bash') return;
109
+ const cmd = output.args && output.args.command;
110
+ if (!cmd) return;
111
+ const m = cmd.match(/^exec(?::(\S+))?\n([\s\S]+)$/);
112
+ if (!m) return;
113
+ const result = runExec(m[1]||'', m[2], output.args.workdir || directory);
114
+ output.args = { ...output.args, command: safePrintf('exec:'+(m[1]||'auto')+' output:\n\n'+result) };
23
115
  }
24
116
  };
25
117
  }
@@ -9,26 +9,17 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const { execSync } = require('child_process');
11
11
 
12
- const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || process.env.GEMINI_PROJECT_DIR || process.env.OC_PLUGIN_ROOT || process.env.KILO_PLUGIN_ROOT || path.join(__dirname, '..');
13
12
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.GEMINI_PROJECT_DIR || process.env.OC_PROJECT_DIR || process.env.KILO_PROJECT_DIR;
14
13
 
15
- const COMPACT_CONTEXT = 'use gm agent | ref: TOOL_INVARIANTS | codesearch for exploration | bun x gm-exec for execution';
16
- const PLAN_MODE_BLOCK = 'DO NOT use EnterPlanMode. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) instead. Plan mode is blocked.';
17
-
18
14
  const ensureGitignore = () => {
19
15
  if (!projectDir) return;
20
16
  const gitignorePath = path.join(projectDir, '.gitignore');
21
17
  const entry = '.gm-stop-verified';
22
18
  try {
23
- let content = '';
24
- if (fs.existsSync(gitignorePath)) {
25
- content = fs.readFileSync(gitignorePath, 'utf-8');
26
- }
19
+ let content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
27
20
  if (!content.split('\n').some(line => line.trim() === entry)) {
28
- const newContent = content.endsWith('\n') || content === ''
29
- ? content + entry + '\n'
30
- : content + '\n' + entry + '\n';
31
- fs.writeFileSync(gitignorePath, newContent);
21
+ content = (content.endsWith('\n') || content === '' ? content : content + '\n') + entry + '\n';
22
+ fs.writeFileSync(gitignorePath, content);
32
23
  }
33
24
  } catch (e) {}
34
25
  };
@@ -39,14 +30,23 @@ const runThorns = () => {
39
30
  const thornsBin = fs.existsSync(localThorns) ? `node ${localThorns}` : 'bun x mcp-thorns@latest';
40
31
  try {
41
32
  const out = execSync(`${thornsBin} ${projectDir}`, {
42
- encoding: 'utf-8',
43
- stdio: ['pipe', 'pipe', 'pipe'],
44
- timeout: 15000,
45
- killSignal: 'SIGTERM'
33
+ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 15000, killSignal: 'SIGTERM'
46
34
  });
47
35
  return `=== mcp-thorns ===\n${out.trim()}`;
48
36
  } catch (e) {
49
- if (e.killed) return '=== mcp-thorns ===\nSkipped (timeout)';
37
+ return e.killed ? '=== mcp-thorns ===\nSkipped (timeout)' : '';
38
+ }
39
+ };
40
+
41
+ const runCodeSearch = (prompt) => {
42
+ if (!prompt || !projectDir) return '';
43
+ try {
44
+ const out = execSync(`bun x codebasesearch ${JSON.stringify(prompt)}`, {
45
+ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10000, killSignal: 'SIGTERM',
46
+ cwd: projectDir
47
+ });
48
+ return `=== codebasesearch ===\n${out.trim()}`;
49
+ } catch (e) {
50
50
  return '';
51
51
  }
52
52
  };
@@ -55,7 +55,6 @@ const emit = (additionalContext) => {
55
55
  const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
56
56
  const isOpenCode = process.env.OC_PROJECT_DIR !== undefined;
57
57
  const isKilo = process.env.KILO_PROJECT_DIR !== undefined;
58
-
59
58
  if (isGemini) {
60
59
  console.log(JSON.stringify({ systemMessage: additionalContext }, null, 2));
61
60
  } else if (isOpenCode || isKilo) {
@@ -66,13 +65,25 @@ const emit = (additionalContext) => {
66
65
  };
67
66
 
68
67
  try {
68
+ let prompt = '';
69
+ try {
70
+ const input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf-8'));
71
+ prompt = input.prompt || input.message || input.userMessage || '';
72
+ } catch (e) {}
73
+
69
74
  ensureGitignore();
75
+
70
76
  const parts = [];
77
+ parts.push('Invoke the `gm` skill to begin. DO NOT use EnterPlanMode. DO NOT use gm subagent directly — use the `gm` skill via the Skill tool.');
78
+
79
+ const search = runCodeSearch(prompt);
80
+ if (search) parts.push(search);
81
+
71
82
  const thorns = runThorns();
72
83
  if (thorns) parts.push(thorns);
73
- parts.push('use gm agent | ' + COMPACT_CONTEXT + ' | ' + PLAN_MODE_BLOCK);
84
+
74
85
  emit(parts.join('\n\n'));
75
86
  } catch (error) {
76
- emit('use gm agent | hook error: ' + error.message);
87
+ emit('Invoke the `gm` skill to begin. Hook error: ' + error.message);
77
88
  process.exit(0);
78
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-oc",
3
- "version": "2.0.181",
3
+ "version": "2.0.185",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",