gm-oc 2.0.340 → 2.0.342

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/cli.js CHANGED
@@ -31,15 +31,23 @@ try {
31
31
  copyRecursive(path.join(srcDir, 'agents'), path.join(ocConfigDir, 'agents'));
32
32
  copyRecursive(path.join(srcDir, 'skills'), path.join(ocConfigDir, 'skills'));
33
33
  copyRecursive(path.join(srcDir, 'lang'), path.join(ocConfigDir, 'lang'));
34
+ copyRecursive(path.join(srcDir, 'bin'), path.join(ocConfigDir, 'bin'));
35
+ copyRecursive(path.join(srcDir, 'hooks'), path.join(ocConfigDir, 'hooks'));
34
36
 
35
37
  const ocJsonPath = path.join(ocConfigDir, 'opencode.json');
36
38
  let ocConfig = {};
37
- try { ocConfig = JSON.parse(fs.readFileSync(ocJsonPath, 'utf-8')); } catch (e) {}
39
+ try {
40
+ const raw = fs.readFileSync(ocJsonPath, 'utf-8');
41
+ ocConfig = JSON.parse(raw);
42
+ if (ocConfig['']) { delete ocConfig['']; }
43
+ } catch (e) {}
38
44
  delete ocConfig.mcp;
45
+ ocConfig['$schema'] = 'https://opencode.ai/config.json';
39
46
  ocConfig.default_agent = 'gm';
40
47
  const pluginMjsPath = path.join(ocConfigDir, 'plugins', 'gm-oc.mjs');
41
48
  if (!Array.isArray(ocConfig.plugin)) ocConfig.plugin = [];
42
- if (!ocConfig.plugin.includes(pluginMjsPath)) ocConfig.plugin.push(pluginMjsPath);
49
+ ocConfig.plugin = ocConfig.plugin.filter(p => !p.includes('gm-oc'));
50
+ ocConfig.plugin.push(pluginMjsPath);
43
51
  fs.writeFileSync(ocJsonPath, JSON.stringify(ocConfig, null, 2) + '\n');
44
52
 
45
53
  const oldDir = process.platform === 'win32'
package/gm-oc.mjs CHANGED
@@ -5,67 +5,17 @@ import { spawnSync } from 'child_process';
5
5
  import { tmpdir } from 'os';
6
6
 
7
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
8
  const FORBIDDEN_TOOLS = new Set(['glob','Glob','grep','Grep','search_file_content','Find','find']);
10
9
  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
10
  const FORBIDDEN_PATH_RE = ['/__tests__/','/test/','/tests/','/fixtures/','/test-data/',"/__mocks__/"];
12
11
  const DOC_BLOCK_RE = /\.(md|txt)$/;
13
12
 
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 tryLangPlugin(lang, code, cwd) {
23
- const langPluginDir = join(__dirname, '..', 'lang');
24
- const langPluginFile = join(langPluginDir, lang+'.js');
25
- if (!existsSync(langPluginFile)) return null;
26
- try {
27
- const plugin = require(langPluginFile);
28
- if (plugin && plugin.exec && plugin.exec.run) {
29
- const result = plugin.exec.run(code, cwd || process.cwd());
30
- return String(result === undefined ? '' : result);
31
- }
32
- } catch(e) {}
33
- return null;
34
- }
35
-
36
- function runExec(rawLang, code, cwd) {
37
- const lang = LANG_ALIASES[rawLang] || (rawLang || detectLang(code));
38
- const opts = { encoding: 'utf-8', timeout: 30000, ...(cwd && { cwd }) };
39
- 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)'; };
40
- const pluginResult = tryLangPlugin(lang, code, cwd);
41
- if (pluginResult !== null) return pluginResult;
42
- if (lang === 'python') return out(spawnSync('python3',['-c',code],opts));
43
- const ext = lang === 'typescript' ? 'ts' : lang === 'bash' ? 'sh' : 'mjs';
44
- const tmp = join(tmpdir(),'gm-plugin-'+Date.now()+'.'+ext);
45
- 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);}}';
46
- writeFileSync(tmp,src,'utf-8');
47
- const r = lang === 'bash' ? spawnSync('bash',[tmp],opts) : spawnSync('bun',['run',tmp],opts);
48
- try { unlinkSync(tmp); } catch(e) {}
49
- let result = out(r);
50
- if (lang !== 'bash') result = result.split(tmp).join('<script>');
51
- return result;
52
- }
53
-
54
- function safePrintf(s) {
55
- return "printf '%s' '" + String(s).replace(/\\\\/g,'\\\\\\\\').replace(/'/g,"'\\\\''")+"'";
56
- }
57
-
58
- function runThorns(dir) {
13
+ function runPlugkit(args) {
14
+ const bin = join(__dirname, '..', 'bin', 'plugkit.js');
15
+ if (!existsSync(bin)) return '';
59
16
  try {
60
- const r = spawnSync('bun',['x','mcp-thorns@latest'],{encoding:'utf-8',timeout:15000,cwd:dir});
61
- return r.killed ? '' : (r.stdout||'').trim();
62
- } catch(e) { return ''; }
63
- }
64
-
65
- function runSearch(query, dir) {
66
- try {
67
- const r = spawnSync('bun',['x','codebasesearch',query],{encoding:'utf-8',timeout:10000,cwd:dir});
68
- return (r.stdout||'').trim();
17
+ const r = spawnSync('node', [bin, ...args], { encoding: 'utf-8', timeout: 15000 });
18
+ return (r.stdout || '').trim() || (r.stderr || '').trim();
69
19
  } catch(e) { return ''; }
70
20
  }
71
21
 
@@ -76,6 +26,17 @@ export async function GmPlugin({ directory }) {
76
26
 
77
27
  return {
78
28
  'experimental.chat.system.transform': async (input, output) => {
29
+ try {
30
+ const giPath = join(directory, '.gitignore');
31
+ const entry = '.gm-stop-verified';
32
+ try {
33
+ let content = existsSync(giPath) ? readFileSync(giPath,'utf-8') : '';
34
+ if (!content.split('\n').some(l => l.trim() === entry)) {
35
+ const nc = (content.endsWith('\n') || content === '') ? content + entry + '\n' : content + '\n' + entry + '\n';
36
+ writeFileSync(giPath, nc, 'utf-8');
37
+ }
38
+ } catch(e) {}
39
+ } catch(e) {}
79
40
  try { const rules = readFileSync(agentMd,'utf-8'); if (rules) output.system.unshift(rules); } catch(e) {}
80
41
  try {
81
42
  if (existsSync(prdFile)) {
@@ -97,9 +58,12 @@ export async function GmPlugin({ directory }) {
97
58
  const prompt = textPart ? textPart.text.trim() : '';
98
59
  const parts = [];
99
60
  parts.push('Invoke the `gm` skill to begin. DO NOT use EnterPlanMode.');
100
- const thorns = runThorns(directory);
101
- if (thorns) parts.push('=== mcp-thorns ===\n'+thorns);
102
- if (prompt) { const s = runSearch(prompt, directory); if (s) parts.push('=== codebasesearch ===\n'+s); }
61
+ const insight = runPlugkit(['codeinsight', directory]);
62
+ if (insight && !insight.startsWith('Error')) parts.push('=== codeinsight ===\n'+insight);
63
+ if (prompt) {
64
+ const search = runPlugkit(['search', '--path', directory, prompt]);
65
+ if (search && !search.startsWith('No results')) parts.push('=== search ===\n'+search);
66
+ }
103
67
  const injection = '<system-reminder>\n'+parts.join('\n\n')+'\n</system-reminder>';
104
68
  if (textPart) textPart.text = injection + '\n' + textPart.text;
105
69
  else if (msg.parts) msg.parts.unshift({ type: 'text', text: injection });
@@ -107,39 +71,41 @@ export async function GmPlugin({ directory }) {
107
71
 
108
72
  'tool.execute.before': async (input, output) => {
109
73
  if (FORBIDDEN_TOOLS.has(input.tool)) {
110
- 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.') };
111
- return;
74
+ throw new Error('Use the code-search skill for codebase exploration instead of '+input.tool+'. Describe what you need in plain language.');
112
75
  }
113
76
  if (input.tool === 'EnterPlanMode') {
114
- output.args = { ...output.args, command: safePrintf('Plan mode is disabled. Use the gm skill (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) instead.') };
115
- return;
77
+ throw new Error('Plan mode is disabled. Use the gm skill (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) instead.');
116
78
  }
117
79
  if (input.tool === 'Task' && input.args?.subagent_type === 'Explore') {
118
- output.args = { ...output.args, command: safePrintf('The Explore agent is blocked. Use exec:codesearch in the Bash tool instead.\n\nexec:codesearch\n<natural language description of what to find>') };
119
- return;
80
+ throw new Error('The Explore agent is blocked. Use exec:codesearch in the Bash tool instead.\n\nexec:codesearch\n<natural language description of what to find>');
120
81
  }
121
- if (input.tool === 'write' || input.tool === 'Write') {
122
- const fp = (output.args && output.args.file_path) || '';
82
+ if (input.tool === 'write' || input.tool === 'Write' || input.tool === 'edit') {
83
+ const fp = (output.args && output.args.file_path) || (input.args && input.args.file_path) || '';
123
84
  const base = basename(fp).toLowerCase();
124
85
  const ext = extname(fp);
125
86
  const blocked = FORBIDDEN_FILE_RE.some(re => re.test(base)) || FORBIDDEN_PATH_RE.some(p => fp.includes(p))
126
87
  || (DOC_BLOCK_RE.test(ext) && !base.startsWith('claude') && !base.startsWith('readme') && !fp.includes('/skills/'));
127
88
  if (blocked) {
128
- output.args = { ...output.args, command: safePrintf('Cannot create test/doc files. Use .prd for task notes, CLAUDE.md for permanent notes.') };
129
- return;
89
+ throw new Error('Cannot create test/doc files. Use .prd for task notes, CLAUDE.md for permanent notes.');
130
90
  }
131
91
  }
132
92
  if (input.tool !== 'bash') return;
133
- const cmd = output.args && output.args.command;
93
+ const cmd = (output.args && output.args.command) || '';
134
94
  if (!cmd) return;
135
95
  if (/^\s*git(?:\s|$)/.test(cmd)) return;
136
96
  const m = cmd.match(/^exec(?::(\S+))?\n([\s\S]+)$/);
137
97
  if (!m) {
138
- output.args = { ...output.args, command: safePrintf('Bash tool can only be used with exec syntax:\n\nexec[:lang]\n<command>\n\nExamples:\nexec\nls -la\n\nexec:nodejs\nnode -e "console.log(1)"') };
98
+ output.args.command = "echo 'Bash tool can only be used with exec syntax:\n\nexec[:lang]\n<command>\n\nExamples:\nexec\nls -la\n\nexec:nodejs\nconsole.log(\"hello\")' 1>&2 && false";
139
99
  return;
140
100
  }
141
- const result = runExec(m[1]||'', m[2], output.args.workdir || directory);
142
- output.args = { ...output.args, command: safePrintf('exec:'+(m[1]||'auto')+' output:\n\n'+result) };
101
+ const lang = m[1] || 'nodejs';
102
+ const code = m[2];
103
+ const ext = lang === 'python' ? 'py' : lang === 'bash' || lang === 'sh' ? 'sh' : 'mjs';
104
+ const ts = Date.now();
105
+ const tmp = join(tmpdir(), 'plugkit-exec-' + ts + '.' + ext);
106
+ writeFileSync(tmp, code, 'utf-8');
107
+ const binJs = join(__dirname, '..', 'bin', 'plugkit.js');
108
+ output.args.command = 'node ' + binJs + ' exec --lang=' + lang + ' --file=' + tmp + ' --cwd=' + (output.args.workdir || directory);
143
109
  }
144
110
  };
145
111
  }
package/hooks/hooks.json CHANGED
@@ -1,30 +1,6 @@
1
1
  {
2
2
  "description": "Hooks for gm OpenCode extension",
3
3
  "hooks": {
4
- "tool.execute.before": [
5
- {
6
- "matcher": "*",
7
- "hooks": [
8
- {
9
- "type": "command",
10
- "command": "node ${OC_PLUGIN_ROOT}/bin/plugkit.js hook pre-tool-use",
11
- "timeout": 3600
12
- }
13
- ]
14
- }
15
- ],
16
- "session.created": [
17
- {
18
- "matcher": "*",
19
- "hooks": [
20
- {
21
- "type": "command",
22
- "command": "node ${OC_PLUGIN_ROOT}/bin/plugkit.js hook session-start",
23
- "timeout": 180000
24
- }
25
- ]
26
- }
27
- ],
28
4
  "message.updated": [
29
5
  {
30
6
  "matcher": "*",
package/opencode.json CHANGED
@@ -1,4 +1,11 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
- "default_agent": "gm"
3
+ "default_agent": "gm",
4
+ "agent": {
5
+ "gm": {
6
+ "mode": "primary",
7
+ "description": "GM state machine agent — PLAN→EXECUTE→EMIT→VERIFY→COMPLETE",
8
+ "prompt": "{file:./agents/gm.md}"
9
+ }
10
+ }
4
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-oc",
3
- "version": "2.0.340",
3
+ "version": "2.0.342",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -72,6 +72,12 @@ exec:close
72
72
  <task_id>
73
73
  ```
74
74
 
75
+ **When a task is backgrounded, you must monitor it — do not abandon it:**
76
+ 1. Drain output with `exec:status <task_id>` immediately after backgrounding
77
+ 2. If the task is still running, `exec:sleep <task_id> [seconds]` then drain again
78
+ 3. Repeat until the task exits or you have enough output to proceed
79
+ 4. `exec:close <task_id>` when the task is no longer needed
80
+
75
81
  **Runner management**:
76
82
  ```
77
83
  exec:runner