gm-codex 2.0.177 → 2.0.179

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.
@@ -11,16 +11,31 @@ const writeTools = ['Write', 'write_file'];
11
11
  const searchTools = ['glob', 'search_file_content', 'Search', 'search'];
12
12
  const forbiddenTools = ['find', 'Find', 'Glob', 'Grep'];
13
13
 
14
+ const allow = (additionalContext) => ({
15
+ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', ...(additionalContext && { additionalContext }) }
16
+ });
17
+ const deny = (reason) => isGemini
18
+ ? { decision: 'deny', reason }
19
+ : { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } };
20
+ const allowWithNoop = (context) => ({
21
+ hookSpecificOutput: {
22
+ hookEventName: 'PreToolUse',
23
+ permissionDecision: 'allow',
24
+ additionalContext: context,
25
+ updatedInput: { command: 'echo ""' }
26
+ }
27
+ });
28
+
14
29
  const run = () => {
15
30
  try {
16
31
  const input = fs.readFileSync(0, 'utf-8');
17
32
  const data = JSON.parse(input);
18
33
  const { tool_name, tool_input } = data;
19
34
 
20
- if (!tool_name) return { allow: true };
35
+ if (!tool_name) return allow();
21
36
 
22
37
  if (forbiddenTools.includes(tool_name)) {
23
- return { block: true, reason: 'Use the code-search skill for codebase exploration instead of Grep/Glob/find. Describe what you need in plain language — it understands intent, not just patterns.' };
38
+ return deny('Use the code-search skill for codebase exploration instead of Grep/Glob/find. Describe what you need in plain language — it understands intent, not just patterns.');
24
39
  }
25
40
 
26
41
  if (writeTools.includes(tool_name)) {
@@ -30,7 +45,7 @@ const run = () => {
30
45
  const base = path.basename(file_path).toLowerCase();
31
46
  if ((ext === '.md' || ext === '.txt' || base.startsWith('features_list')) &&
32
47
  !base.startsWith('claude') && !base.startsWith('readme') && !inSkillsDir) {
33
- return { block: true, reason: 'Cannot create documentation files. Only CLAUDE.md and readme.md are maintained. For task-specific notes, use .prd. For permanent reference material, add to CLAUDE.md.' };
48
+ return deny('Cannot create documentation files. Only CLAUDE.md and readme.md are maintained. For task-specific notes, use .prd. For permanent reference material, add to CLAUDE.md.');
34
49
  }
35
50
  if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/.test(base) ||
36
51
  /^(jest|vitest|mocha|ava|jasmine|tap)\.(config|setup)/.test(base) ||
@@ -38,24 +53,24 @@ const run = () => {
38
53
  file_path.includes('/tests/') || file_path.includes('/fixtures/') ||
39
54
  file_path.includes('/test-data/') || file_path.includes('/__mocks__/') ||
40
55
  /\.(snap|stub|mock|fixture)\.(js|ts|json)$/.test(base)) {
41
- return { block: true, reason: 'Test files forbidden on disk. Use Bash tool with real services for all testing.' };
56
+ return deny('Test files forbidden on disk. Use Bash tool with real services for all testing.');
42
57
  }
43
58
  }
44
59
 
45
- if (searchTools.includes(tool_name)) return { allow: true };
60
+ if (searchTools.includes(tool_name)) return allow();
46
61
 
47
62
  if (tool_name === 'Task' && (tool_input?.subagent_type || '') === 'Explore') {
48
- return { block: true, reason: 'Use gm:thorns-overview for codebase insight, then use gm:code-search' };
63
+ return deny('Use gm:thorns-overview for codebase insight, then use gm:code-search');
49
64
  }
50
65
 
51
66
  if (tool_name === 'EnterPlanMode') {
52
- return { block: true, reason: 'Plan mode is disabled. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) via gm:gm subagent instead.' };
67
+ return deny('Plan mode is disabled. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) via gm:gm subagent instead.');
53
68
  }
54
69
 
55
70
  if (tool_name === 'Skill') {
56
71
  const skill = (tool_input?.skill || '').toLowerCase().replace(/^gm:/, '');
57
72
  if (skill === 'explore' || skill === 'search') {
58
- return { block: true, reason: 'Use the code-search skill for codebase exploration. Describe what you need in plain language — it understands intent, not just patterns.' };
73
+ return deny('Use the code-search skill for codebase exploration. Describe what you need in plain language — it understands intent, not just patterns.');
59
74
  }
60
75
  }
61
76
 
@@ -66,7 +81,7 @@ const run = () => {
66
81
  const rawLang = (execMatch[1] || '').toLowerCase();
67
82
  const code = execMatch[2];
68
83
  if (/^\s*agent-browser\s/.test(code)) {
69
- return { block: true, reason: `Do not call agent-browser via exec:bash. Use exec:agent-browser instead:\n\nexec:agent-browser\n<plain JS here>\n\nThe code is piped directly to the browser eval. No base64, no flags, no shell wrapping.` };
84
+ return deny(`Do not call agent-browser via exec:bash. Use exec:agent-browser instead:\n\nexec:agent-browser\n<plain JS here>\n\nThe code is piped directly to the browser eval. No base64, no flags, no shell wrapping.`);
70
85
  }
71
86
  const cwd = tool_input?.cwd;
72
87
  const detectLang = (src) => {
@@ -75,7 +90,7 @@ const run = () => {
75
90
  if (/^\s*(echo |ls |cd |mkdir |rm |cat |grep |find |export |source |#!)/.test(src)) return 'bash';
76
91
  return 'nodejs';
77
92
  };
78
- const aliases = { js: 'nodejs', javascript: 'nodejs', ts: 'typescript', node: 'nodejs', py: 'python', sh: 'bash', shell: 'bash', zsh: 'bash', powershell: 'bash', ps1: 'bash', cmd: 'bash', browser: 'agent-browser', ab: 'agent-browser' };
93
+ const aliases = { js: 'nodejs', javascript: 'nodejs', ts: 'typescript', node: 'nodejs', py: 'python', sh: 'bash', shell: 'bash', zsh: 'bash', powershell: 'bash', ps1: 'bash', cmd: 'bash', browser: 'agent-browser', ab: 'agent-browser', codesearch: 'codesearch', search: 'search', status: 'status', sleep: 'sleep', close: 'close', runner: 'runner' };
79
94
  const lang = aliases[rawLang] || rawLang || detectLang(code);
80
95
  const IS_WIN = process.platform === 'win32';
81
96
  const stripFooter = (s) => s.replace(/\n\[Running tools\][\s\S]*$/, '').trimEnd();
@@ -93,7 +108,7 @@ const run = () => {
93
108
  const r = spawnSync('bun', ['x', 'gm-exec', 'exec', `--lang=${l}`, `--file=${tmp}`, ...(cwd ? [`--cwd=${cwd}`] : [])], { encoding: 'utf-8', timeout: 65000 });
94
109
  try { fs.unlinkSync(tmp); } catch (e) {}
95
110
  let out = stripFooter((r.stdout || '') + (r.stderr || ''));
96
- const bg = out.match(/Command running in background with ID:\s*(\S+)/);
111
+ const bg = out.match(/Task ID:\s*(task_\S+)/);
97
112
  if (bg) {
98
113
  spawnSync('bun', ['x', 'gm-exec', 'sleep', bg[1], '60'], { encoding: 'utf-8', timeout: 70000 });
99
114
  const sr = spawnSync('bun', ['x', 'gm-exec', 'status', bg[1]], { encoding: 'utf-8', timeout: 15000 });
@@ -108,6 +123,32 @@ const run = () => {
108
123
  try { const d = Buffer.from(t, 'base64').toString('utf-8'); return /[\x00-\x08\x0b\x0e-\x1f]/.test(d) ? s : d; } catch { return s; }
109
124
  };
110
125
  const safeCode = decodeB64(code);
126
+ if (['codesearch', 'search'].includes(lang)) {
127
+ const query = safeCode.trim();
128
+ const r = spawnSync('bun', ['x', 'codebasesearch', query], { encoding: 'utf-8', timeout: 30000, ...(cwd && { cwd }) });
129
+ return allowWithNoop(`exec:${lang} output:\n\n${stripFooter((r.stdout || '') + (r.stderr || '')) || '(no results)'}`);
130
+ }
131
+ if (lang === 'status') {
132
+ const taskId = safeCode.trim();
133
+ const r = spawnSync('bun', ['x', 'gm-exec', 'status', taskId], { encoding: 'utf-8', timeout: 15000 });
134
+ return allowWithNoop(`exec:status output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
135
+ }
136
+ if (lang === 'sleep') {
137
+ const parts = safeCode.trim().split(/\s+/);
138
+ const args = ['x', 'gm-exec', 'sleep', ...parts];
139
+ const r = spawnSync('bun', args, { encoding: 'utf-8', timeout: 70000 });
140
+ return allowWithNoop(`exec:sleep output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
141
+ }
142
+ if (lang === 'close') {
143
+ const taskId = safeCode.trim();
144
+ const r = spawnSync('bun', ['x', 'gm-exec', 'close', taskId], { encoding: 'utf-8', timeout: 15000 });
145
+ return allowWithNoop(`exec:close output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
146
+ }
147
+ if (lang === 'runner') {
148
+ const sub = safeCode.trim();
149
+ const r = spawnSync('bun', ['x', 'gm-exec', 'runner', sub], { encoding: 'utf-8', timeout: 15000 });
150
+ return allowWithNoop(`exec:runner output:\n\n${stripFooter((r.stdout || '') + (r.stderr || ''))}`);
151
+ }
111
152
  try {
112
153
  let result;
113
154
  if (lang === 'bash') {
@@ -134,35 +175,31 @@ const run = () => {
134
175
  } else {
135
176
  result = runWithFile(lang, safeCode);
136
177
  }
137
- return { block: true, reason: `exec ran successfully. Output:\n\n${result || '(no output)'}` };
178
+ return allowWithNoop(`exec:${lang} output:\n\n${result || '(no output)'}`);
138
179
  } catch (e) {
139
- return { block: true, reason: `exec ran. Error:\n\n${(e.stdout || '') + (e.stderr || '') || e.message || '(exec failed)'}` };
180
+ return allowWithNoop(`exec:${lang} error:\n\n${(e.stdout || '') + (e.stderr || '') || e.message || '(exec failed)'}`);
140
181
  }
141
182
  }
142
183
 
143
184
  if (!/^exec(\s|:)/.test(command) && !/^bun x gm-exec(@[^\s]*)?(\s|$)/.test(command) && !/^git /.test(command) && !/^bun x codebasesearch/.test(command) && !/(\bclaude\b)/.test(command) && !/^npm install .* \/config\/.gmweb/.test(command) && !/^bun install --cwd \/config\/.gmweb/.test(command)) {
144
185
  let helpText = '';
145
186
  try { helpText = '\n\n' + execSync('bun x gm-exec --help', { timeout: 10000 }).toString().trim(); } catch (e) {}
146
- return { block: true, reason: `Bash is restricted to exec:<lang> and git.\n\nexec:<lang> syntax (lang auto-detected if omitted):\n exec:nodejs / exec:python / exec:bash / exec:typescript\n exec:go / exec:rust / exec:java / exec:c / exec:cpp\n exec:agent-browser ← plain JS piped to browser eval (NO base64)\n exec ← auto-detects language\n\nNEVER encode agent-browser code as base64 — pass plain JS directly.\n\nbun x gm-exec${helpText}\n\nAll other Bash commands are blocked.` };
187
+ return deny(`Bash is restricted to exec:<lang> and git.\n\nexec:<lang> syntax (lang auto-detected if omitted):\n exec:nodejs / exec:python / exec:bash / exec:typescript\n exec:go / exec:rust / exec:java / exec:c / exec:cpp\n exec:agent-browser ← plain JS piped to browser eval (NO base64)\n exec ← auto-detects language\n\nTask management shortcuts (body = args):\n exec:status\n <task_id>\n\n exec:sleep\n <task_id> [seconds] [--next-output]\n\n exec:close\n <task_id>\n\n exec:runner\n start|stop|status\n\nCode search shortcut:\n exec:codesearch\n <natural language query>\n\nNEVER encode agent-browser code as base64 — pass plain JS directly.\n\nbun x gm-exec${helpText}\n\nAll other Bash commands are blocked.`);
147
188
  }
148
189
  }
149
190
 
150
191
  const allowedTools = ['agent-browser', 'Skill', 'code-search', 'electron', 'TaskOutput', 'ReadMcpResourceTool', 'ListMcpResourcesTool'];
151
- if (allowedTools.includes(tool_name)) return { allow: true };
192
+ if (allowedTools.includes(tool_name)) return allow();
152
193
 
153
- return { allow: true };
194
+ return allow();
154
195
  } catch (error) {
155
- return { allow: true };
196
+ return allow();
156
197
  }
157
198
  };
158
199
 
159
200
  try {
160
201
  const result = run();
161
- if (result.block) {
162
- console.log(JSON.stringify({ decision: isGemini ? 'deny' : 'block', reason: result.reason }));
163
- process.exit(0);
164
- }
165
- if (isGemini) console.log(JSON.stringify({ decision: 'allow' }));
202
+ console.log(JSON.stringify(result));
166
203
  process.exit(0);
167
204
  } catch (error) {
168
205
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-codex",
3
- "version": "2.0.177",
3
+ "version": "2.0.179",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.177",
3
+ "version": "2.0.179",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": {
6
6
  "name": "AnEntrypoint",