ohmyjev 0.6.0 → 0.7.1
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/LICENSE +21 -0
- package/README.md +41 -19
- package/adapters/codex.ts +163 -0
- package/adapters/omp.ts +1 -10
- package/hooks/policy.ts +10 -0
- package/package.json +14 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Novacon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -13,14 +13,14 @@ d88P 88888888 "Y88P" 888 888 888 888 888 "Y88888 888 "Y8888 Y88P
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
|
-
Guardrails for Claude Code, pi and omp, decided by <a href="https://typesafe.ai">Jev</a>.<br>
|
|
16
|
+
Guardrails for Claude Code, Codex, pi and omp, decided by <a href="https://typesafe.ai">Jev</a>.<br>
|
|
17
17
|
One mod that stops irreversible and data-leaking commands, even in bypass mode, and runs them when you say yes. It also flags prompt injection, pushes back on an unverified "done" and routes effort per turn.
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
|
21
21
|
<img alt="Claude Code 2.1.287+" src="https://img.shields.io/badge/Claude_Code-2.1.287%2B-d97757">
|
|
22
22
|
<img alt="Decided by Jev" src="https://img.shields.io/badge/decided_by-Jev-101315">
|
|
23
|
-
<img alt="pi and omp" src="https://img.shields.io/badge/also_for-
|
|
23
|
+
<img alt="Codex, pi and omp" src="https://img.shields.io/badge/also_for-Codex_·_pi_·_omp-86a893">
|
|
24
24
|
<a href="https://www.npmjs.com/package/ohmyjev"><img alt="npm" src="https://img.shields.io/npm/v/ohmyjev?color=de6145"></a>
|
|
25
25
|
<img alt="MIT license" src="https://img.shields.io/badge/license-MIT-798186">
|
|
26
26
|
</p>
|
|
@@ -30,7 +30,7 @@ d88P 88888888 "Y88P" 888 888 888 888 888 "Y88888 888 "Y8888 Y88P
|
|
|
30
30
|
<a href="#quick-start">Quick start</a> ·
|
|
31
31
|
<a href="#what-you-get">What you get</a> ·
|
|
32
32
|
<a href="#install">Install</a> ·
|
|
33
|
-
<a href="#pi-and-omp">pi and omp</a> ·
|
|
33
|
+
<a href="#codex-pi-and-omp">Codex, pi and omp</a> ·
|
|
34
34
|
<a href="#using-it">Using it</a> ·
|
|
35
35
|
<a href="#settings">Settings</a> ·
|
|
36
36
|
<a href="#privacy">Privacy</a> ·
|
|
@@ -144,11 +144,29 @@ key: env TYPESAFE_API_KEY · typesafe · jev-1.13.0
|
|
|
144
144
|
If it says `key: none`, ohmyjev can't see a key. In that case every gate stays open, the router falls back to Claude
|
|
145
145
|
Code's built-in classifier (up only), and the status under the prompt shows `jev ⚠ no key`.
|
|
146
146
|
|
|
147
|
-
## pi and omp
|
|
147
|
+
## Codex, pi and omp
|
|
148
148
|
|
|
149
|
-
The same checks run in [
|
|
150
|
-
They share the decision logic and the key lookup
|
|
151
|
-
plugin, and write to the same `~/.ohmyjev` logs, so
|
|
149
|
+
The same checks run in [Codex](https://developers.openai.com/codex) as a plugin with hooks, and in [pi](https://pi.dev)
|
|
150
|
+
and [omp](https://github.com/can1357/oh-my-pi) as a native extension. They share the decision logic and the key lookup
|
|
151
|
+
(`TYPESAFE_API_KEY`, then `OPENROUTER_API_KEY`) with the Claude Code plugin, and write to the same `~/.ohmyjev` logs, so
|
|
152
|
+
`/omj stats` in any of them shows every session.
|
|
153
|
+
|
|
154
|
+
### Codex
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
codex plugin marketplace add Novacon/ohmyjev
|
|
158
|
+
codex plugin add ohmyjev@ohmyjev
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Then start Codex and run `/hooks` once: Codex skips a plugin's hooks until you trust them. ohmyjev needs Node on your
|
|
162
|
+
`PATH` and the key in the shell that starts Codex. It gates `Bash`, `apply_patch` edits and MCP calls, screens their
|
|
163
|
+
output, and runs the done-check. Saying yes after a block works the same way. Codex hooks can't change the model or add
|
|
164
|
+
a tool, so there is no router, no `ask_jev` and no `/omj` command. Put settings in `~/.codex/ohmyjev.json`, using the
|
|
165
|
+
names in [Settings](#settings):
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{ "requested": 0.9, "policies": "never touch the prod cluster" }
|
|
169
|
+
```
|
|
152
170
|
|
|
153
171
|
### omp
|
|
154
172
|
|
|
@@ -184,18 +202,19 @@ pi has no settings screen for extensions, so put yours under an `ohmyjev` key in
|
|
|
184
202
|
|
|
185
203
|
### What differs
|
|
186
204
|
|
|
187
|
-
| | Claude Code | omp | pi |
|
|
188
|
-
|
|
189
|
-
| Bash gate | Bash | `bash`, `eval`, and `github` pushes and PRs | `bash`, `powershell` |
|
|
190
|
-
| Write gate | Write, Edit, NotebookEdit | `write`, `edit` (patches and renames included), `ast_edit` | `write`, `edit` |
|
|
191
|
-
| Exfil gate | WebFetch, MCP tools | `read` of a URL, MCP tools | MCP tools (pi has no fetch tool; `curl` goes through the bash gate) |
|
|
192
|
-
| Injection screen | Bash, WebFetch, MCP, Reads outside the repo | `bash`, `github`, URL reads, MCP, reads outside the repo | `bash`, `powershell`, MCP, reads outside the repo |
|
|
193
|
-
| Done-check | ✓ | ✓ | ✓ |
|
|
194
|
-
| Router effort | ✓ | ✓ (left alone while you're on `auto`) | ✓ |
|
|
195
|
-
| Router subagent model | general-purpose subagents | subagents on the default `task` role | none: pi has no subagents |
|
|
196
|
-
| Router without a key | Claude Code's built-in classifier, up only | off: omp has no separate classifier call for extensions | the cheapest model you've set up, up only |
|
|
197
|
-
|
|
|
198
|
-
|
|
|
205
|
+
| | Claude Code | Codex | omp | pi |
|
|
206
|
+
|---|---|---|---|---|
|
|
207
|
+
| Bash gate | Bash | Bash | `bash`, `eval`, and `github` pushes and PRs | `bash`, `powershell` |
|
|
208
|
+
| Write gate | Write, Edit, NotebookEdit | `apply_patch` | `write`, `edit` (patches and renames included), `ast_edit` | `write`, `edit` |
|
|
209
|
+
| Exfil gate | WebFetch, MCP tools | MCP tools | `read` of a URL, MCP tools | MCP tools (pi has no fetch tool; `curl` goes through the bash gate) |
|
|
210
|
+
| Injection screen | Bash, WebFetch, MCP, Reads outside the repo | Bash, MCP | `bash`, `github`, URL reads, MCP, reads outside the repo | `bash`, `powershell`, MCP, reads outside the repo |
|
|
211
|
+
| Done-check | ✓ | ✓ | ✓ | ✓ |
|
|
212
|
+
| Router effort | ✓ | none: hooks can't set it | ✓ (left alone while you're on `auto`) | ✓ |
|
|
213
|
+
| Router subagent model | general-purpose subagents | none | subagents on the default `task` role | none: pi has no subagents |
|
|
214
|
+
| Router without a key | Claude Code's built-in classifier, up only | none | off: omp has no separate classifier call for extensions | the cheapest model you've set up, up only |
|
|
215
|
+
| `/omj`, `ask_jev` | ✓ | none: hooks can't add commands or tools | ✓ | ✓ |
|
|
216
|
+
| Decision lines | in the transcript | the log only | notifications | notifications |
|
|
217
|
+
| Status | under the prompt | none | in the footer | in the footer |
|
|
199
218
|
|
|
200
219
|
## Using it
|
|
201
220
|
|
|
@@ -379,6 +398,9 @@ have set up, through pi's own provider. Logs and session files stay in `~/.ohmyj
|
|
|
379
398
|
claude plugin update ohmyjev@ohmyjev # then restart Claude Code
|
|
380
399
|
claude plugin uninstall ohmyjev@ohmyjev
|
|
381
400
|
|
|
401
|
+
codex plugin marketplace upgrade ohmyjev # Codex; then trust the changed hooks in /hooks
|
|
402
|
+
codex plugin remove ohmyjev@ohmyjev
|
|
403
|
+
|
|
382
404
|
omp plugin upgrade ohmyjev # omp; restart the session
|
|
383
405
|
omp plugin uninstall ohmyjev
|
|
384
406
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ohmyjev for Codex: one command hook, run once per event (codex/hooks.json). Each run is a fresh process, so what
|
|
3
|
+
* must outlive it (the user's latest request, the call blocked last turn, this turn's tools) sits in
|
|
4
|
+
* ~/.ohmyjev/codex/<session>.json. Same decisions as the other hosts (hooks/policy.ts); every failure prints nothing,
|
|
5
|
+
* and nothing lets the call through. Codex hooks can't change the model or add tools, so there is no router or ask_jev.
|
|
6
|
+
* Built to codex/ohmyjev.mjs with `bun run build:codex`, since Codex runs it with plain node.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
9
|
+
import {
|
|
10
|
+
BASH_Q, EXFIL_Q, SCREEN_Q, STOP_Q, WRITE_Q, applyPatchPaths, callSummary, clip, denyText, gateBash, gateExfil, gateWrite,
|
|
11
|
+
judgeStop, pathDenyText, plainRead, readConfig, sanitizeSid, screen, withPolicies, withPolicyQ, type Config,
|
|
12
|
+
} from '../hooks/policy.ts'
|
|
13
|
+
import { Session, keyOf, pathAllowed, repoRoot, type Env, type Fetch } from './shared.ts'
|
|
14
|
+
|
|
15
|
+
const CLIP = 16000
|
|
16
|
+
type Tool = { tool: string; input: string; outcome: 'ok' | 'failed' }
|
|
17
|
+
type State = { request: string; previous: string[]; count: number; blockedBefore: string; lastBlocked: string; tools: Tool[]; pushedBackAt: number }
|
|
18
|
+
const EMPTY: State = { request: '', previous: [], count: 0, blockedBefore: '', lastBlocked: '', tools: [], pushedBackAt: -1 }
|
|
19
|
+
type Input = Record<string, unknown>
|
|
20
|
+
const str = (o: Input, k: string): string => (typeof o[k] === 'string' ? (o[k] as string) : '')
|
|
21
|
+
const obj = (v: unknown): Input => (v && typeof v === 'object' && !Array.isArray(v) ? (v as Input) : {})
|
|
22
|
+
|
|
23
|
+
/** Settings: ~/.codex/ohmyjev.json, a flat object of the setting names in the README. */
|
|
24
|
+
async function config(env: Env): Promise<Config> {
|
|
25
|
+
const text = await readFile(`${env.HOME ?? ''}/.codex/ohmyjev.json`, 'utf8').catch(() => '{}')
|
|
26
|
+
try {
|
|
27
|
+
return readConfig(obj(JSON.parse(text)))
|
|
28
|
+
} catch {
|
|
29
|
+
return readConfig({})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const statePath = (env: Env, sid: string) => `${env.HOME ?? ''}/.ohmyjev/codex/${sanitizeSid(sid)}.json`
|
|
34
|
+
async function load(env: Env, sid: string): Promise<State> {
|
|
35
|
+
const text = await readFile(statePath(env, sid), 'utf8').catch(() => '')
|
|
36
|
+
try {
|
|
37
|
+
return { ...EMPTY, ...(JSON.parse(text) as Partial<State>) }
|
|
38
|
+
} catch {
|
|
39
|
+
return { ...EMPTY }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ponytail: last write wins; two hooks finishing at once can drop one tool from this turn's list (done-check input only)
|
|
43
|
+
async function save(env: Env, sid: string, st: State): Promise<void> {
|
|
44
|
+
await mkdir(`${env.HOME ?? ''}/.ohmyjev/codex`, { recursive: true, mode: 0o700 }).catch(() => undefined)
|
|
45
|
+
await writeFile(statePath(env, sid), JSON.stringify(st), { mode: 0o600 }).catch(() => undefined)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const deny = (reason: string) => ({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason } })
|
|
49
|
+
|
|
50
|
+
/** One hook event in, Codex's JSON answer out; undefined means no opinion. */
|
|
51
|
+
export async function handle(input: Input, env: Env, fetchImpl?: Fetch): Promise<object | undefined> {
|
|
52
|
+
const c = await config(env)
|
|
53
|
+
const event = str(input, 'hook_event_name')
|
|
54
|
+
const sid = str(input, 'session_id') || 'codex'
|
|
55
|
+
const cwd = str(input, 'cwd') || process.cwd()
|
|
56
|
+
if (!c.enabled) return undefined
|
|
57
|
+
|
|
58
|
+
if (event === 'SessionStart')
|
|
59
|
+
return keyOf(c, env) ? undefined : { systemMessage: 'ohmyjev: no Jev key, so every call passes through. Set TYPESAFE_API_KEY in the shell that starts Codex.' }
|
|
60
|
+
|
|
61
|
+
const st = await load(env, sid)
|
|
62
|
+
if (event === 'UserPromptSubmit') {
|
|
63
|
+
const prompt = str(input, 'prompt')
|
|
64
|
+
if (!prompt.trim() || /^\/\S+\s*$/.test(prompt.trim())) return undefined
|
|
65
|
+
if (st.request) st.previous = [...st.previous, clip(st.request, 200)].slice(-5)
|
|
66
|
+
Object.assign(st, { request: prompt, count: st.count + 1, blockedBefore: st.lastBlocked, lastBlocked: '', tools: [] })
|
|
67
|
+
await save(env, sid, st)
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const x = new Session(c, env, sid, undefined, fetchImpl)
|
|
72
|
+
const tool = str(input, 'tool_name')
|
|
73
|
+
const toolInput = obj(input.tool_input)
|
|
74
|
+
|
|
75
|
+
if (event === 'PreToolUse') {
|
|
76
|
+
const blocked = async (j: { verdict: unknown; reason: string }) => {
|
|
77
|
+
if (j.verdict !== 'deny') return undefined
|
|
78
|
+
st.lastBlocked = callSummary(tool, toolInput)
|
|
79
|
+
await save(env, sid, st)
|
|
80
|
+
return deny(denyText(j.reason))
|
|
81
|
+
}
|
|
82
|
+
if (tool === 'Bash' && c.bashGate) {
|
|
83
|
+
const command = str(toolInput, 'command')
|
|
84
|
+
if (plainRead(command, c)) return undefined
|
|
85
|
+
const state = withPolicies({ command: clip(command, CLIP), cwd, ...(command.length > CLIP ? { truncated: true } : {}) }, c, st.request, st.blockedBefore)
|
|
86
|
+
const d = await x.decide('tool.call', tool, state, withPolicyQ(BASH_Q, c, st.request))
|
|
87
|
+
if (!d) return undefined
|
|
88
|
+
const j = gateBash(d.answers, c)
|
|
89
|
+
x.record(d, j.verdict, j.reason)
|
|
90
|
+
return blocked(j)
|
|
91
|
+
}
|
|
92
|
+
if (tool === 'apply_patch' && c.writeGate) {
|
|
93
|
+
const patch = str(toolInput, 'command')
|
|
94
|
+
const root = await repoRoot(cwd)
|
|
95
|
+
for (const path of applyPatchPaths(patch))
|
|
96
|
+
if (!(await pathAllowed(path, cwd, root, c, env))) {
|
|
97
|
+
x.denyByCode(tool, `${path} is outside the repo and allowPaths`)
|
|
98
|
+
return deny(pathDenyText(path))
|
|
99
|
+
}
|
|
100
|
+
const state = withPolicies({ path: applyPatchPaths(patch).join('; '), content: clip(patch, CLIP) }, c, st.request, st.blockedBefore)
|
|
101
|
+
const d = await x.decide('tool.call', tool, state, withPolicyQ(WRITE_Q, c, st.request))
|
|
102
|
+
if (!d) return undefined
|
|
103
|
+
const j = gateWrite(d.answers, c)
|
|
104
|
+
x.record(d, j.verdict, j.reason)
|
|
105
|
+
return blocked(j)
|
|
106
|
+
}
|
|
107
|
+
if (tool.startsWith('mcp__') && c.exfilGate) {
|
|
108
|
+
const state = withPolicies({ tool, input: clip(JSON.stringify(toolInput), 4000) }, c, st.request, st.blockedBefore)
|
|
109
|
+
const d = await x.decide('tool.call', tool, state, withPolicyQ(EXFIL_Q, c, st.request))
|
|
110
|
+
if (!d) return undefined
|
|
111
|
+
const j = gateExfil(d.answers, c)
|
|
112
|
+
x.record(d, j.verdict, j.reason)
|
|
113
|
+
return blocked(j)
|
|
114
|
+
}
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (event === 'PostToolUse') {
|
|
119
|
+
const response = input.tool_response
|
|
120
|
+
const code = obj(response).exit_code ?? obj(response).exitCode
|
|
121
|
+
st.tools = [...st.tools, { tool, input: clip(JSON.stringify(toolInput), 200), outcome: typeof code === 'number' && code !== 0 ? 'failed' : 'ok' } as Tool].slice(-20)
|
|
122
|
+
await save(env, sid, st)
|
|
123
|
+
if (!c.injectionScreen || !(tool === 'Bash' || tool.startsWith('mcp__'))) return undefined
|
|
124
|
+
const text = typeof response === 'string' ? response : JSON.stringify(response ?? '')
|
|
125
|
+
if (!text.trim()) return undefined
|
|
126
|
+
const d = await x.decide('tool.result', tool, { tool, content: clip(text, 6000) }, SCREEN_Q)
|
|
127
|
+
if (!d) return undefined
|
|
128
|
+
const s = screen(d.answers, c)
|
|
129
|
+
x.record(d, s.flagged ? 'flag' : null, s.reason)
|
|
130
|
+
return s.flagged ? { hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: s.note } } : undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (event === 'Stop') {
|
|
134
|
+
if (!c.doneCheck || input.stop_hook_active === true || st.pushedBackAt === st.count) return undefined
|
|
135
|
+
const state = {
|
|
136
|
+
current_request: clip(st.request, 600),
|
|
137
|
+
previous_requests: st.previous,
|
|
138
|
+
tools_this_turn: st.tools,
|
|
139
|
+
last_assistant_message: clip(str(input, 'last_assistant_message'), 1500),
|
|
140
|
+
}
|
|
141
|
+
const d = await x.decide('Stop', '', state, STOP_Q)
|
|
142
|
+
if (!d) return undefined
|
|
143
|
+
const j = judgeStop(d.answers, c, st.tools.length > 0)
|
|
144
|
+
x.record(d, j.block ? 'block' : null, j.block ?? 'stop ok')
|
|
145
|
+
if (!j.block) return undefined
|
|
146
|
+
st.pushedBackAt = st.count
|
|
147
|
+
await save(env, sid, st)
|
|
148
|
+
return { decision: 'block', reason: j.block }
|
|
149
|
+
}
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The hook command: one JSON event on stdin, the answer on stdout. Any failure prints nothing, so Codex goes on. */
|
|
154
|
+
export async function main(): Promise<void> {
|
|
155
|
+
try {
|
|
156
|
+
let text = ''
|
|
157
|
+
for await (const chunk of process.stdin) text += chunk
|
|
158
|
+
const out = await handle(obj(JSON.parse(text)), process.env)
|
|
159
|
+
if (out) process.stdout.write(JSON.stringify(out))
|
|
160
|
+
} catch {
|
|
161
|
+
// ohmyjev's own bug: no opinion
|
|
162
|
+
}
|
|
163
|
+
}
|
package/adapters/omp.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFile } from 'node:fs/promises'
|
|
5
5
|
import {
|
|
6
6
|
BASH_Q, DEFAULTS, EXFIL_Q, ROUTE_Q, SCREEN_Q, STOP_Q, SWITCHED_Q, WRITE_Q,
|
|
7
|
-
clip, decideRoute, denyText, gateBash, gateExfil, gateWrite, jevRouteLine, judgeStop, pathDenyText, callSummary, plainRead, configCommand,
|
|
7
|
+
clip, decideRoute, denyText, gateBash, gateExfil, gateWrite, jevRouteLine, judgeStop, pathDenyText, callSummary, plainRead, configCommand, applyPatchPaths,
|
|
8
8
|
OMJ_HELP, OMJ_OFF, OMJ_ON, toggleArg,
|
|
9
9
|
keepInstructions, modelOf, readConfig, routeStep, screen, stepLine, withPolicies, withPolicyQ,
|
|
10
10
|
type Config, type Route,
|
|
@@ -150,15 +150,6 @@ const exfilTool = (tool: string, input: RecordOfUnknown): boolean => urlRead(too
|
|
|
150
150
|
const githubWrite = (tool: string, input: RecordOfUnknown): boolean =>
|
|
151
151
|
tool === 'github' && GITHUB_READONLY_OPS[string(input, 'op')] !== true
|
|
152
152
|
|
|
153
|
-
function applyPatchPaths(patch: string): string[] {
|
|
154
|
-
const paths: string[] = []
|
|
155
|
-
for (const line of patch.split('\n')) {
|
|
156
|
-
const match = /^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/.exec(line.trim())
|
|
157
|
-
if (match?.[1]) paths.push(match[1])
|
|
158
|
-
}
|
|
159
|
-
return paths
|
|
160
|
-
}
|
|
161
|
-
|
|
162
153
|
function writePaths(tool: string, input: RecordOfUnknown): string[] {
|
|
163
154
|
const paths = Array.isArray(input.paths) ? input.paths.filter((p): p is string => typeof p === 'string' && p !== '') : []
|
|
164
155
|
for (const key of ['path', 'file_path', 'notebook_path']) {
|
package/hooks/policy.ts
CHANGED
|
@@ -480,6 +480,16 @@ export function hostPath(p: string): string | null {
|
|
|
480
480
|
export const parseWorktrees = (porcelain: string): string[] =>
|
|
481
481
|
porcelain.split('\n').filter(l => l.startsWith('worktree ')).map(l => l.slice('worktree '.length).trim()).filter(Boolean)
|
|
482
482
|
|
|
483
|
+
/** Every file an apply_patch patch adds, updates, deletes or moves to (omp and Codex edits). */
|
|
484
|
+
export function applyPatchPaths(patch: string): string[] {
|
|
485
|
+
const paths: string[] = []
|
|
486
|
+
for (const line of patch.split('\n')) {
|
|
487
|
+
const match = /^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/.exec(line.trim())
|
|
488
|
+
if (match?.[1]) paths.push(match[1])
|
|
489
|
+
}
|
|
490
|
+
return paths
|
|
491
|
+
}
|
|
492
|
+
|
|
483
493
|
export const isUnder = (target: string, root: string): boolean =>
|
|
484
494
|
target === root || target.startsWith(root.replace(/\/+$/, '') + '/')
|
|
485
495
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ohmyjev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Guardrails for coding agents, decided by TypeSafe's Jev: blocks destructive commands, catches prompt injection, pushes back on an unverified done and routes effort per turn. For Claude Code, pi and omp.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,8 +14,18 @@
|
|
|
14
14
|
"omp",
|
|
15
15
|
"pi",
|
|
16
16
|
"claude-code",
|
|
17
|
+
"claude-code-plugin",
|
|
18
|
+
"codex",
|
|
19
|
+
"codex-plugin",
|
|
17
20
|
"jev",
|
|
18
|
-
"guardrails"
|
|
21
|
+
"guardrails",
|
|
22
|
+
"ai-agent",
|
|
23
|
+
"ai-safety",
|
|
24
|
+
"llm-security",
|
|
25
|
+
"prompt-injection",
|
|
26
|
+
"coding-agent",
|
|
27
|
+
"agent-guardrails",
|
|
28
|
+
"bypass-permissions"
|
|
19
29
|
],
|
|
20
30
|
"files": [
|
|
21
31
|
"adapters/*.ts",
|
|
@@ -235,7 +245,8 @@
|
|
|
235
245
|
},
|
|
236
246
|
"scripts": {
|
|
237
247
|
"test": "bun test adapters && claude plugin test .",
|
|
238
|
-
"typecheck": "tsc -p . && tsc -p tsconfig.adapters.json"
|
|
248
|
+
"typecheck": "tsc -p . && tsc -p tsconfig.adapters.json",
|
|
249
|
+
"build:codex": "bun build codex/main.ts --target=node --format=esm --outfile=codex/ohmyjev.mjs"
|
|
239
250
|
},
|
|
240
251
|
"devDependencies": {
|
|
241
252
|
"@types/bun": "^1.4.2",
|