gm-oc 2.0.184 → 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.
Files changed (2) hide show
  1. package/gm-oc.mjs +101 -9
  2. package/package.json +1 -1
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-oc",
3
- "version": "2.0.184",
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",