tackbox 0.1.98 → 0.1.99

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/README.md CHANGED
@@ -646,6 +646,44 @@ failure is unverified, not a no-op.
646
646
  `uvx tackbox hook` runs the cached tackbox (no `@latest`): the hook is
647
647
  fast in-loop feedback, not the authoritative gate.
648
648
 
649
+ ### Codex
650
+
651
+ Codex exposes its canonical file mutation as `apply_patch`. Wire the npm
652
+ adapter once, globally, in `~/.codex/hooks.json`:
653
+
654
+ Install the adapter once so hook execution does not depend on registry access:
655
+
656
+ ```bash
657
+ npm install --global tackbox@latest
658
+ ```
659
+
660
+ ```json
661
+ {
662
+ "hooks": {
663
+ "PreToolUse": [
664
+ {"matcher": "apply_patch",
665
+ "hooks": [{"type": "command",
666
+ "command": "tackbox-codex-hook"}]}
667
+ ],
668
+ "PostToolUse": [
669
+ {"matcher": "apply_patch",
670
+ "hooks": [{"type": "command",
671
+ "command": "tackbox-codex-hook",
672
+ "timeout": 120}]}
673
+ ]
674
+ }
675
+ }
676
+ ```
677
+
678
+ The adapter converts Codex's patch payload to `tackbox hook-protocol` and pins
679
+ the Python wheel to the resolved npm package version. Codex does not support an
680
+ `ask` decision from `PreToolUse`; an approval-gated edit therefore blocks with
681
+ the reason instead of approving itself. Make that change in an interactive host
682
+ that supports the prompt.
683
+ Before a patch, any non-allow decision blocks. After a patch, verified findings
684
+ replace the tool result; an unverified decision adds model-visible context that
685
+ the mutation may already have landed and must not be repeated.
686
+
649
687
  ### Oh My Pi
650
688
 
651
689
  ```bash
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict'
4
+
5
+ function fatal(error) {
6
+ process.exitCode = 2
7
+ process.stderr.write(`tackbox Codex hook: ${String((error && error.stack) || error)}\n`)
8
+ }
9
+
10
+ process.once('uncaughtException', fatal)
11
+ process.once('unhandledRejection', fatal)
12
+ require('../js/codex/hook').main()
@@ -0,0 +1,131 @@
1
+ 'use strict'
2
+
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
5
+ const manifest = require('../../package.json')
6
+ const protocol = require('../omp/hook')
7
+ const payload = require('../omp/payload')
8
+
9
+ const PRE = 'PreToolUse'
10
+ const POST = 'PostToolUse'
11
+ const APPLY_PATCH = 'apply_patch'
12
+ const APPROVAL_UNAVAILABLE =
13
+ 'Codex hooks cannot present a Tackbox approval prompt, so this edit is blocked. ' +
14
+ 'Make the approval-gated change in an interactive host that supports the prompt.'
15
+
16
+ function requestFromEvent(event) {
17
+ if (event === null || typeof event !== 'object' || Array.isArray(event)) {
18
+ return { failure: 'Codex supplied a non-object hook event' }
19
+ }
20
+ const phase = event.hook_event_name === PRE
21
+ ? 'pre'
22
+ : event.hook_event_name === POST
23
+ ? 'post'
24
+ : null
25
+ if (phase === null) return { ignored: true }
26
+ if (event.tool_name !== APPLY_PATCH) return { ignored: true }
27
+ if (typeof event.cwd !== 'string' || !path.isAbsolute(event.cwd)) {
28
+ return { phase, failure: 'Codex supplied no absolute cwd' }
29
+ }
30
+ if (
31
+ event.tool_input === null ||
32
+ typeof event.tool_input !== 'object' ||
33
+ Array.isArray(event.tool_input) ||
34
+ typeof event.tool_input.command !== 'string'
35
+ ) {
36
+ return { phase, failure: 'Codex supplied apply_patch without a string tool_input.command' }
37
+ }
38
+ if (event.tool_input.command.trim() === '') {
39
+ return { phase, failure: 'Codex supplied an empty apply_patch command' }
40
+ }
41
+ const normalized = payload.normalize(
42
+ APPLY_PATCH,
43
+ { input: event.tool_input.command },
44
+ event.cwd,
45
+ )
46
+ if (normalized === null) return { ignored: true }
47
+ if (normalized.failure) return { phase, failure: normalized.failure }
48
+ if (phase === 'post') normalized.succeeded = true
49
+ return {
50
+ phase,
51
+ request: protocol.request(phase, event.cwd, normalized),
52
+ }
53
+ }
54
+
55
+ async function handle(event, options = {}) {
56
+ const built = requestFromEvent(event)
57
+ if (built.ignored) return { code: 0, stdout: '', stderr: '' }
58
+ const phase = built.phase || 'pre'
59
+ const decision = built.failure
60
+ ? protocol.unverified(built.failure)
61
+ : await (options.decide || protocol.decide)(built.request, {
62
+ env: options.env || process.env,
63
+ version: options.version || manifest.version,
64
+ timers: options.timers,
65
+ })
66
+ return render(phase, decision)
67
+ }
68
+
69
+ function render(phase, decision) {
70
+ const kind = decision && decision.kind
71
+ if (kind === protocol.ALLOW) return { code: 0, stdout: '', stderr: '' }
72
+ if (phase === 'pre') {
73
+ let reason
74
+ if (kind === protocol.ASK) {
75
+ reason = `${decision.reason}\n\n${APPROVAL_UNAVAILABLE}`
76
+ } else if (kind === protocol.BLOCK || kind === protocol.UNVERIFIED) {
77
+ reason = decision.reason
78
+ } else {
79
+ reason = `tackbox returned an unrecognized pre decision ${String(kind)}`
80
+ }
81
+ return {
82
+ code: 0,
83
+ stdout: `${JSON.stringify({ decision: 'block', reason })}\n`,
84
+ stderr: '',
85
+ }
86
+ }
87
+ if (kind === protocol.BLOCK) {
88
+ return {
89
+ code: 0,
90
+ stdout: `${JSON.stringify({ decision: 'block', reason: decision.reason })}\n`,
91
+ stderr: '',
92
+ }
93
+ }
94
+ const reason = kind === protocol.UNVERIFIED
95
+ ? decision.reason
96
+ : `tackbox returned an unrecognized post decision ${String(kind)}`
97
+ return {
98
+ code: 0,
99
+ stdout: `${JSON.stringify({
100
+ hookSpecificOutput: {
101
+ hookEventName: POST,
102
+ additionalContext: protocol.unverifiedPostMessage(reason),
103
+ },
104
+ })}\n`,
105
+ stderr: '',
106
+ }
107
+ }
108
+
109
+ async function main() {
110
+ const event = protocol.parseJson(fs.readFileSync(0, 'utf8'))
111
+ if (event.error !== null) {
112
+ process.stderr.write(`tackbox Codex hook: unreadable stdin: ${event.error}\n`)
113
+ process.exitCode = 2
114
+ return
115
+ }
116
+ const result = await handle(event.value)
117
+ if (result.stdout) process.stdout.write(result.stdout)
118
+ if (result.stderr) process.stderr.write(result.stderr)
119
+ process.exitCode = result.code
120
+ }
121
+
122
+ module.exports = {
123
+ APPLY_PATCH,
124
+ APPROVAL_UNAVAILABLE,
125
+ POST,
126
+ PRE,
127
+ handle,
128
+ main,
129
+ render,
130
+ requestFromEvent,
131
+ }
package/js/omp/hook.js CHANGED
@@ -182,6 +182,21 @@ function unverified(reason) {
182
182
  }
183
183
  }
184
184
 
185
+ function unverifiedPostMessage(reason) {
186
+ const text = typeof reason === 'string' ? reason : String(reason)
187
+ if (
188
+ text.includes('The mutation may already have landed.') &&
189
+ text.includes('Do not repeat the mutation; dev.py check remains required.')
190
+ ) {
191
+ return text
192
+ }
193
+ return [
194
+ 'The mutation may already have landed.',
195
+ `Tackbox verification did not complete: ${text}`,
196
+ 'Do not repeat the mutation; dev.py check remains required.',
197
+ ].join('\n')
198
+ }
199
+
185
200
  function firstLine(text) {
186
201
  for (const line of String(text || '').split('\n')) {
187
202
  if (line.trim() !== '') return line.trim()
@@ -209,5 +224,7 @@ module.exports = {
209
224
  request,
210
225
  resolveArgv,
211
226
  asDecision,
227
+ parseJson,
212
228
  unverified,
229
+ unverifiedPostMessage,
213
230
  }
package/js/omp/index.js CHANGED
@@ -67,33 +67,18 @@ function applyPost(decision, event) {
67
67
  }
68
68
  if (kind === hook.UNVERIFIED) {
69
69
  return {
70
- content: appended(event, unverifiedPostMessage(decision.reason)),
70
+ content: appended(event, hook.unverifiedPostMessage(decision.reason)),
71
71
  }
72
72
  }
73
73
  if (kind === hook.ALLOW) return undefined
74
74
  return {
75
75
  content: appended(
76
76
  event,
77
- unverifiedPostMessage(`tackbox returned an unrecognized post decision ${String(kind)}`),
77
+ hook.unverifiedPostMessage(`tackbox returned an unrecognized post decision ${String(kind)}`),
78
78
  ),
79
79
  }
80
80
  }
81
81
 
82
- function unverifiedPostMessage(reason) {
83
- const text = typeof reason === 'string' ? reason : String(reason)
84
- if (
85
- text.includes('The mutation may already have landed.') &&
86
- text.includes('Do not repeat the mutation; dev.py check remains required.')
87
- ) {
88
- return text
89
- }
90
- return [
91
- 'The mutation may already have landed.',
92
- `Tackbox verification did not complete: ${text}`,
93
- 'Do not repeat the mutation; dev.py check remains required.',
94
- ].join('\n')
95
- }
96
-
97
82
  function appended(event, text) {
98
83
  const content = Array.isArray(event && event.content) ? event.content : []
99
84
  return [...content, { type: 'text', text }]
package/js/omp/payload.js CHANGED
@@ -4,6 +4,7 @@ const path = require('node:path')
4
4
 
5
5
  const TAGGED = /^\[(.+)#[0-9A-Fa-f]{4}\]\s*$/
6
6
  const ENVELOPE = /^\*\*\* (?:Begin|End) Patch\s*$/
7
+ const EOF_SENTINEL = /^\*\*\* End of File\s*$/
7
8
  const FILE_SENTINEL = /^\*\*\* (Update|Add|Delete) File:\s*(.+?)\s*$/
8
9
  const MOVE_SENTINEL = /^\*\*\* Move to:\s*(.+?)\s*$/
9
10
  const MV_OP = /^MV\s+(.+?)\s*$/
@@ -256,7 +257,7 @@ function parsePatchText(tool, text, cwd) {
256
257
  continue
257
258
  }
258
259
  flush()
259
- if (line.trim() === '' || ENVELOPE.test(line) || HUNK.test(line)) continue
260
+ if (line.trim() === '' || ENVELOPE.test(line) || EOF_SENTINEL.test(line) || HUNK.test(line)) continue
260
261
  const tagged = TAGGED.exec(line)
261
262
  if (tagged !== null) {
262
263
  open(tagged[1])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tackbox",
3
- "version": "0.1.98",
3
+ "version": "0.1.99",
4
4
  "description": "ESLint and Markdown lint plugins, an Oh My Pi extension, plus direct error-reporting helpers for JavaScript and TypeScript.",
5
5
  "license": "MIT",
6
6
  "main": "./js/eslint-plugin.js",
@@ -9,6 +9,7 @@
9
9
  "./report": "./js/report.js"
10
10
  },
11
11
  "bin": {
12
+ "tackbox-codex-hook": "./bin/tackbox-codex-hook.js",
12
13
  "tackbox-eslint": "./bin/tackbox-eslint.js",
13
14
  "tackbox-mdlint": "./bin/tackbox-mdlint.js"
14
15
  },
@@ -22,6 +23,7 @@
22
23
  "js/report.js",
23
24
  "js/rules/",
24
25
  "js/markdownlint-rules/",
26
+ "js/codex/",
25
27
  "js/omp/index.mjs",
26
28
  "js/omp/index.js",
27
29
  "js/omp/hook.js",