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 +10 -2
- package/gm-oc.mjs +38 -72
- package/hooks/hooks.json +0 -24
- package/opencode.json +8 -1
- package/package.json +1 -1
- package/skills/gm/SKILL.md +6 -0
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 {
|
|
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
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
if (
|
|
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('
|
|
61
|
-
return r.
|
|
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
|
|
101
|
-
if (
|
|
102
|
-
if (prompt) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
142
|
-
|
|
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
package/skills/gm/SKILL.md
CHANGED
|
@@ -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
|