@sriinnu/omit 0.4.0 → 0.5.0
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/.cursor/rules/omit.mdc +1 -1
- package/.github/workflows/omit.yml +21 -0
- package/.github/workflows/test.yml +36 -0
- package/README.md +80 -5
- package/action.yml +6 -3
- package/bin/omit.mjs +32 -2
- package/lib/codemode.mjs +198 -0
- package/package.json +29 -3
- package/skills/omit/SKILL.md +1 -1
- package/skills/omit-codemode/SKILL.md +48 -0
package/.cursor/rules/omit.mdc
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: omit
|
|
2
|
+
|
|
3
|
+
on: pull_request
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
pull-requests: write
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
omit:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
# A fork's PR gets a read-only token, so the comment could only fail there.
|
|
13
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
with:
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
|
|
19
|
+
# `./`, not a tag: this runs the PR's own action.yml end to end, which
|
|
20
|
+
# nothing else does — test.yml only proves the file parses.
|
|
21
|
+
- uses: ./
|
|
@@ -38,3 +38,39 @@ jobs:
|
|
|
38
38
|
assert doc["runs"].get("using") == "composite", "runs.using must be composite"
|
|
39
39
|
print("action.yml parses and carries its required keys")
|
|
40
40
|
PY
|
|
41
|
+
|
|
42
|
+
# Same failure mode as action.yml: Claude Code tolerates an unquoted ": "
|
|
43
|
+
# in a description, but strict YAML loaders (the skills CLI, skill
|
|
44
|
+
# indexers) reject the whole file, and the skill silently disappears.
|
|
45
|
+
- name: skill and rule frontmatter parses
|
|
46
|
+
run: |
|
|
47
|
+
python3 - <<'PY'
|
|
48
|
+
import sys
|
|
49
|
+
try:
|
|
50
|
+
import yaml
|
|
51
|
+
except ImportError:
|
|
52
|
+
print("PyYAML unavailable on this runner — frontmatter was NOT validated")
|
|
53
|
+
sys.exit(0)
|
|
54
|
+
for path in ("skills/omit/SKILL.md", "skills/omit-codemode/SKILL.md", ".cursor/rules/omit.mdc"):
|
|
55
|
+
meta = yaml.safe_load(open(path).read().split("---")[1])
|
|
56
|
+
assert meta.get("description"), f"{path}: frontmatter has no description"
|
|
57
|
+
print(f"{path}: frontmatter parses")
|
|
58
|
+
PY
|
|
59
|
+
|
|
60
|
+
# The sandbox is an optional peer, so `npm test` above never installs it and
|
|
61
|
+
# the end-to-end case there is announced as skipped. This job is where a real
|
|
62
|
+
# script runs in the real sandbox, at the version the lockfile pins.
|
|
63
|
+
codemode:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
|
|
68
|
+
- uses: actions/setup-node@v4
|
|
69
|
+
with:
|
|
70
|
+
node-version: "22"
|
|
71
|
+
|
|
72
|
+
- run: npm ci
|
|
73
|
+
|
|
74
|
+
- run: node --test test/codemode.test.mjs
|
|
75
|
+
env:
|
|
76
|
+
OMIT_CODEMODE_REQUIRED: "1"
|
package/README.md
CHANGED
|
@@ -119,14 +119,16 @@ And server-side, the GitHub Action comments the verdict on every PR regardless o
|
|
|
119
119
|
```yaml
|
|
120
120
|
# .github/workflows/omit.yml
|
|
121
121
|
on: pull_request
|
|
122
|
-
permissions: { pull-requests: write }
|
|
122
|
+
permissions: { contents: read, pull-requests: write }
|
|
123
123
|
jobs:
|
|
124
124
|
omit:
|
|
125
125
|
runs-on: ubuntu-latest
|
|
126
|
+
# A fork's PR gets a read-only token, so the comment could only fail there.
|
|
127
|
+
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
126
128
|
steps:
|
|
127
129
|
- uses: actions/checkout@v4
|
|
128
130
|
with: { fetch-depth: 0 }
|
|
129
|
-
- uses: sriinnu/omit@
|
|
131
|
+
- uses: sriinnu/omit@v0.5.0
|
|
130
132
|
# with: { exec: true } # execute receipts' `run` snippets to verify them
|
|
131
133
|
# fully. Off by default: a PR's receipts are
|
|
132
134
|
# untrusted code, and this runs on pull requests.
|
|
@@ -148,6 +150,50 @@ jobs:
|
|
|
148
150
|
// the receipts; a single number would just be another uncited claim.
|
|
149
151
|
```
|
|
150
152
|
|
|
153
|
+
## Codemode (experimental)
|
|
154
|
+
|
|
155
|
+
An agent exploring a repo pays for every intermediate result: each read and each grep is a round-trip whose raw output lands in the transcript and is sent again on every later turn. With `omit codemode` the model writes a script, the reads happen inside it, and only what the script returns comes back.
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
omit codemode run <<'EOF'
|
|
159
|
+
const libs = (await tools.files({ under: 'lib' })).filter((f) => f.endsWith('.mjs'))
|
|
160
|
+
const sources = await Promise.all(libs.map((path) => tools.read({ path })))
|
|
161
|
+
return Object.fromEntries(libs.map((f, i) => [f, (sources[i].match(/^export /gm) ?? []).length]))
|
|
162
|
+
EOF
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- **Read-only.** Three tools: `files`, `read`, `grep`. They read the tree through git, so ignored files stay out, and every path is confined to the directory the server started in: no `..`, no absolute path, no symlink out.
|
|
166
|
+
- **Sandboxed.** The script runs in a QuickJS VM compiled to wasm ([`@earendil-works/pi-codemode`](https://www.npmjs.com/package/@earendil-works/pi-codemode), the sandbox behind pi's codemode): no file system, no network, no `process`. `node:vm` is not isolation, and the receipt for this dependency runs the escape to show it.
|
|
167
|
+
- **Secrets stay in.** A script may read a credentials file; it may not return it. Output that matches a secret rule is withheld whole.
|
|
168
|
+
- **Bounded.** A script has 60 seconds, and output past 20,000 characters loses its middle.
|
|
169
|
+
|
|
170
|
+
The sandbox is an optional peer that only this command loads, so `omit` itself still installs zero dependencies. It needs Node 22.19 or newer.
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
npm install -g @sriinnu/omit @earendil-works/pi-codemode
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Any agent with a shell can use it as it stands: `omit codemode run` takes a script on stdin or from a file and prints the answer, and the `omit-codemode` skill (`skills/omit-codemode/SKILL.md`) teaches the agent when to reach for it and what a script can call. Nothing has to be registered.
|
|
177
|
+
|
|
178
|
+
For a host without a shell, or one where you would rather approve a single read-only tool, the same thing is an MCP server on stdio:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
claude mcp add omit -- omit codemode # Claude Code
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```toml
|
|
185
|
+
# Codex: ~/.codex/config.toml
|
|
186
|
+
[mcp_servers.omit]
|
|
187
|
+
command = "omit"
|
|
188
|
+
args = ["codemode"]
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
// omitted: write and edit tools: a nested write is not seen by the host's
|
|
193
|
+
// hooks, so it has to carry the hazard, dependency and lint gates itself. Add
|
|
194
|
+
// them once bench/ shows the read side pays for the surface.
|
|
195
|
+
```
|
|
196
|
+
|
|
151
197
|
## The referee (experimental)
|
|
152
198
|
|
|
153
199
|
`bench/` is METHODOLOGY.md made runnable: paired agentic runs of the same tasks under baseline, omit, or **any competing skill**, metrics computed from the actual git diffs, all transcripts kept. The category argues about self-reported numbers; omit ships the measuring instrument. See `bench/README.md`.
|
|
@@ -174,6 +220,12 @@ New here? **[GETTING-STARTED.md](GETTING-STARTED.md)** has a copy-paste setup fo
|
|
|
174
220
|
/plugin install omit@omit
|
|
175
221
|
```
|
|
176
222
|
|
|
223
|
+
**Any SKILL.md-aware agent** (Claude Code, Codex, Cursor, and others, via [skills.sh](https://skills.sh)):
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
npx skills add sriinnu/omit
|
|
227
|
+
```
|
|
228
|
+
|
|
177
229
|
**Global command**: install once from GitHub, use everywhere:
|
|
178
230
|
|
|
179
231
|
```
|
|
@@ -213,9 +265,9 @@ skills/omit/SKILL.md → .claude/skills/omit/SKILL.md (project)
|
|
|
213
265
|
**Anything else**: paste the contents of `AGENTS.md` into the agent's custom-instructions/rules mechanism. It's plain markdown; there is nothing to build.
|
|
214
266
|
|
|
215
267
|
```
|
|
216
|
-
// omitted:
|
|
217
|
-
// discipline, and rule files + skills already deliver it.
|
|
218
|
-
//
|
|
268
|
+
// omitted: the discipline itself over MCP: MCP exposes tools and data; omit is
|
|
269
|
+
// a behavioral discipline, and rule files + skills already deliver it. The one
|
|
270
|
+
// MCP server omit ships is codemode, which is tooling.
|
|
219
271
|
```
|
|
220
272
|
|
|
221
273
|
## Commands (Claude Code)
|
|
@@ -223,6 +275,29 @@ skills/omit/SKILL.md → .claude/skills/omit/SKILL.md (project)
|
|
|
223
275
|
- `/omit [margin|redline|rewrite|off]`: switch or show the current mode
|
|
224
276
|
- `/omit-edit`: run an editor's pass over the current diff: flag bloat, uncited claims, missing footnotes, and cut opportunities
|
|
225
277
|
|
|
278
|
+
## Releasing
|
|
279
|
+
|
|
280
|
+
One command, three destinations (npm, GitHub, Homebrew):
|
|
281
|
+
|
|
282
|
+
npm run release -- patch # or minor / major
|
|
283
|
+
|
|
284
|
+
`scripts/release.mjs` runs the tests (via `preversion`), bumps the version,
|
|
285
|
+
and lands the bump on main **through a PR** (main takes no direct pushes).
|
|
286
|
+
Once checks pass and it merges, the script tags the merge (signed, per repo
|
|
287
|
+
policy) and pushes the tag — which triggers `publish.yml`, publishing to npm
|
|
288
|
+
**with a provenance attestation**. It then cuts the GitHub release with
|
|
289
|
+
generated notes, waits for the registry to serve the version, and updates the
|
|
290
|
+
`omit` formula in [`sriinnu/homebrew-tap`](https://github.com/sriinnu/homebrew-tap),
|
|
291
|
+
again through a PR. A failed step aborts the release, in order.
|
|
292
|
+
|
|
293
|
+
Never `npm publish` by hand: a local publish cannot attach provenance, and
|
|
294
|
+
npm will not let the same version be republished to add one later. If the CI
|
|
295
|
+
publish fails, fix CI — don't work around it locally.
|
|
296
|
+
|
|
297
|
+
If the `NPM_TOKEN` secret ever goes stale, put the new token in `~/.npmrc`
|
|
298
|
+
and run `npm run token:sync` — it verifies the token against the registry
|
|
299
|
+
before pushing, so a dead token never reaches CI.
|
|
300
|
+
|
|
226
301
|
## Prior art
|
|
227
302
|
|
|
228
303
|
The minimalism-pressure idea was popularized by [ponytail](https://github.com/DietrichGebert/ponytail), which deserves its stars. `omit` differs where it matters: shortcuts require citations, the diff is edited *after* it works, safety lines are enumerated and never cut, and what's left out is footnoted instead of silent.
|
package/action.yml
CHANGED
|
@@ -34,10 +34,13 @@ runs:
|
|
|
34
34
|
BASE="${{ inputs.base }}"
|
|
35
35
|
if [ -z "$BASE" ]; then BASE="origin/${{ github.event.pull_request.base.ref }}"; fi
|
|
36
36
|
git fetch --depth=1 origin "${{ github.event.pull_request.base.ref }}" || true
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
# Outside the workspace: the redirect creates the file before the audit
|
|
38
|
+
# runs, and the audit counts untracked files — a verdict written into
|
|
39
|
+
# the checkout reports itself as one more changed file.
|
|
40
|
+
node "${{ github.action_path }}/bin/omit.mjs" audit --base "$BASE" --markdown > "$RUNNER_TEMP/omit-verdict.md"
|
|
41
|
+
cat "$RUNNER_TEMP/omit-verdict.md"
|
|
39
42
|
- name: Comment on PR
|
|
40
43
|
shell: bash
|
|
41
44
|
env:
|
|
42
45
|
GH_TOKEN: ${{ github.token }}
|
|
43
|
-
run: gh pr comment ${{ github.event.pull_request.number }} --body-file omit-verdict.md --repo ${{ github.repository }}
|
|
46
|
+
run: gh pr comment ${{ github.event.pull_request.number }} --body-file "$RUNNER_TEMP/omit-verdict.md" --repo ${{ github.repository }}
|
package/bin/omit.mjs
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// omit leak "<cmd>" would this command print a real secret to stdout?
|
|
11
11
|
// omit hook install add the gate to .git/hooks/pre-commit
|
|
12
12
|
// omit hook install codex write .codex/hooks.json (live sentinels inside Codex CLI)
|
|
13
|
-
|
|
13
|
+
// omit codemode run [file] run one sandboxed script over read-only repo tools (stdin without a file)
|
|
14
|
+
// omit codemode the same, as an MCP server on stdio
|
|
15
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, chmodSync } from 'node:fs'
|
|
14
16
|
import { basename, dirname, join } from 'node:path'
|
|
15
17
|
import { fileURLToPath } from 'node:url'
|
|
16
18
|
import { isManifest, addedDeps, unparsedDependencyFile } from '../lib/deps.mjs'
|
|
@@ -20,6 +22,7 @@ import { findHazards } from '../lib/hazards.mjs'
|
|
|
20
22
|
import { lintFiles } from '../lib/lint.mjs'
|
|
21
23
|
import { assessCommand } from '../lib/danger.mjs'
|
|
22
24
|
import { assessLeak } from '../lib/leaks.mjs'
|
|
25
|
+
import { execute, serve } from '../lib/codemode.mjs'
|
|
23
26
|
|
|
24
27
|
const cwd = process.cwd()
|
|
25
28
|
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -658,6 +661,32 @@ function verify() {
|
|
|
658
661
|
// on purpose: a verdict rendered from a diff that could not be read is the
|
|
659
662
|
// failure this whole contract exists to prevent, and a gate that cannot read the
|
|
660
663
|
// change has to block it rather than pass it.
|
|
664
|
+
// ---------- codemode ----------
|
|
665
|
+
// The sandbox is an optional peer, loaded here and nowhere else: every other
|
|
666
|
+
// command has to keep working on a machine that never installed it.
|
|
667
|
+
async function codemode(args) {
|
|
668
|
+
if (args.length && args[0] !== 'run') die('usage: omit codemode [run [file]]')
|
|
669
|
+
let sandbox
|
|
670
|
+
try {
|
|
671
|
+
sandbox = await import('@earendil-works/pi-codemode')
|
|
672
|
+
} catch (e) {
|
|
673
|
+
if (e.code !== 'ERR_MODULE_NOT_FOUND') throw e
|
|
674
|
+
die('omit codemode needs its sandbox, which is not installed. Run: npm install @earendil-works/pi-codemode (it needs Node 22.19 or newer)')
|
|
675
|
+
}
|
|
676
|
+
const root = realpathSync(cwd)
|
|
677
|
+
// `run` is the whole feature for any agent that has a shell: one script in,
|
|
678
|
+
// one answer out, no server to register. Exit 1 marks a failed or withheld
|
|
679
|
+
// result, so a harness can tell it from an answer.
|
|
680
|
+
if (args[0] === 'run') {
|
|
681
|
+
const { text, isError } = await execute(readFileSync(args[1] ?? 0, 'utf8'), { CodemodeSandbox: sandbox.CodemodeSandbox, root })
|
|
682
|
+
console.log(text)
|
|
683
|
+
process.exitCode = isError ? 1 : 0
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
const { version } = JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8'))
|
|
687
|
+
serve({ sandbox, root, version })
|
|
688
|
+
}
|
|
689
|
+
|
|
661
690
|
const [cmd, ...rest] = process.argv.slice(2)
|
|
662
691
|
try {
|
|
663
692
|
if (cmd === 'audit') audit(rest)
|
|
@@ -667,6 +696,7 @@ try {
|
|
|
667
696
|
else if (cmd === 'guard') guard(rest)
|
|
668
697
|
else if (cmd === 'leak') leak(rest)
|
|
669
698
|
else if (cmd === 'verify') verify()
|
|
699
|
+
else if (cmd === 'codemode') await codemode(rest)
|
|
670
700
|
else if (cmd === 'hook' && rest[0] === 'install' && rest[1] === 'codex') hookInstallCodex()
|
|
671
701
|
else if (cmd === 'hook' && rest[0] === 'install' && rest[1] === undefined) hookInstall()
|
|
672
702
|
else if (cmd === 'hook' && rest[0] === 'install') {
|
|
@@ -676,7 +706,7 @@ try {
|
|
|
676
706
|
else if (cmd === 'init') init(rest[0])
|
|
677
707
|
else if (targets[cmd]) init(cmd) // back-compat: `omit cursor`
|
|
678
708
|
else {
|
|
679
|
-
console.error('usage: omit <init|audit|check|gate|lint|guard|leak|verify|hook install|hook install codex>')
|
|
709
|
+
console.error('usage: omit <init|audit|check|gate|lint|guard|leak|verify|codemode|hook install|hook install codex>')
|
|
680
710
|
process.exit(cmd ? 1 : 0)
|
|
681
711
|
}
|
|
682
712
|
} catch (e) {
|
package/lib/codemode.mjs
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// omit codemode: one MCP tool that runs a model-written script in a sandbox
|
|
2
|
+
// whose only capability is the read-only tools below.
|
|
3
|
+
//
|
|
4
|
+
// An agent exploring a repo pays for every intermediate result: each file read
|
|
5
|
+
// and each grep is a round-trip whose raw output lands in the transcript and is
|
|
6
|
+
// re-sent on every later turn. Here the reads happen inside the script and only
|
|
7
|
+
// what it returns reaches the model.
|
|
8
|
+
//
|
|
9
|
+
// The sandbox itself is not written here. Isolation is load-bearing, and
|
|
10
|
+
// node:vm is not isolation (the receipt in .omit/receipts.jsonl executes the
|
|
11
|
+
// escape), so it comes from @earendil-works/pi-codemode: a QuickJS VM compiled
|
|
12
|
+
// to wasm with no file system, network or process. It is an optional peer and
|
|
13
|
+
// only this command loads it, so the rest of the CLI stays zero-dependency.
|
|
14
|
+
//
|
|
15
|
+
// omitted: write and edit tools: a nested write is not seen by the host's
|
|
16
|
+
// hooks, so it has to carry the hazard, dependency and lint gates itself; add
|
|
17
|
+
// them once bench/ shows the read side pays for the surface.
|
|
18
|
+
// omitted: a tree walk for directories that are not git repositories: `files`
|
|
19
|
+
// and `grep` read the tree through git, which is what knows what is ignored;
|
|
20
|
+
// add one if a host turns out to run this outside repositories.
|
|
21
|
+
import { execFile } from 'node:child_process'
|
|
22
|
+
import { readFile, realpath, stat } from 'node:fs/promises'
|
|
23
|
+
import { relative, resolve, sep } from 'node:path'
|
|
24
|
+
import { createInterface } from 'node:readline'
|
|
25
|
+
import { promisify } from 'node:util'
|
|
26
|
+
import { findHazards } from './hazards.mjs'
|
|
27
|
+
|
|
28
|
+
const run = promisify(execFile)
|
|
29
|
+
|
|
30
|
+
const TIMEOUT_MS = 60_000
|
|
31
|
+
// What may reach the transcript from one script. Past it the middle is cut: a
|
|
32
|
+
// script that returns this much has not filtered anything.
|
|
33
|
+
const MAX_OUTPUT_CHARS = 20_000
|
|
34
|
+
// A read is one JSON round trip into the VM, so a multi-gigabyte log would be
|
|
35
|
+
// held three times over before the script saw a byte.
|
|
36
|
+
const MAX_READ_BYTES = 8 << 20
|
|
37
|
+
const GIT_BUFFER = 64 << 20
|
|
38
|
+
|
|
39
|
+
// load-bearing: path confinement. The lexical check refuses `..` and absolute
|
|
40
|
+
// paths before the file system is touched; the realpath check refuses a
|
|
41
|
+
// symlink inside the workspace that points out of it.
|
|
42
|
+
async function confine(root, path) {
|
|
43
|
+
if (typeof path !== 'string' || !path) throw new Error('path must be a non-empty string')
|
|
44
|
+
const outside = (full) => full !== root && !full.startsWith(root + sep)
|
|
45
|
+
if (outside(resolve(root, path))) throw new Error(`${path} is outside the workspace`)
|
|
46
|
+
let full
|
|
47
|
+
try {
|
|
48
|
+
full = await realpath(resolve(root, path))
|
|
49
|
+
} catch (e) {
|
|
50
|
+
throw new Error(`${path}: ${e.code === 'ENOENT' ? 'no such file or directory' : e.message}`)
|
|
51
|
+
}
|
|
52
|
+
if (outside(full)) throw new Error(`${path} is outside the workspace`)
|
|
53
|
+
return full
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Async on purpose: the host owns the script's deadline, and a synchronous git
|
|
57
|
+
// call would hold the event loop that enforces it. It also lets a script's
|
|
58
|
+
// Promise.all actually run its calls side by side.
|
|
59
|
+
async function git(root, args) {
|
|
60
|
+
try {
|
|
61
|
+
return (await run('git', args, { cwd: root, encoding: 'utf8', maxBuffer: GIT_BUFFER })).stdout
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (e.code === 1 && !e.stderr) return '' // `git grep` found nothing
|
|
64
|
+
if (e.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') throw new Error('too much output to hold: narrow the pattern or pass `under`')
|
|
65
|
+
throw new Error(String(e.stderr ?? '').trim() || e.message)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pathspec = async (root, under) => (under === undefined ? [] : ['--', relative(root, await confine(root, under)) || '.'])
|
|
70
|
+
|
|
71
|
+
export const toolsFor = (root) => [
|
|
72
|
+
{
|
|
73
|
+
name: 'files',
|
|
74
|
+
description: 'List files, tracked and untracked, minus what git ignores.',
|
|
75
|
+
inputSchema: { type: 'object', properties: { under: { type: 'string', description: 'only files below this directory' } } },
|
|
76
|
+
outputSchema: { type: 'array', items: { type: 'string' } },
|
|
77
|
+
execute: async ({ under } = {}) => {
|
|
78
|
+
const out = await git(root, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', ...(await pathspec(root, under))])
|
|
79
|
+
return [...new Set(out.split('\0').filter(Boolean))]
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'read',
|
|
84
|
+
description: 'Read one file as text.',
|
|
85
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
86
|
+
outputSchema: { type: 'string' },
|
|
87
|
+
execute: async ({ path } = {}) => {
|
|
88
|
+
const full = await confine(root, path)
|
|
89
|
+
const { size } = await stat(full)
|
|
90
|
+
if (size > MAX_READ_BYTES) throw new Error(`${path} is ${size} bytes, past the ${MAX_READ_BYTES} a read holds: use grep`)
|
|
91
|
+
return readFile(full, 'utf8')
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'grep',
|
|
96
|
+
description: 'Search file contents with an extended regular expression.',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: { pattern: { type: 'string' }, under: { type: 'string', description: 'only files below this directory' } },
|
|
100
|
+
required: ['pattern'],
|
|
101
|
+
},
|
|
102
|
+
outputSchema: {
|
|
103
|
+
type: 'array',
|
|
104
|
+
items: { type: 'object', properties: { path: { type: 'string' }, line: { type: 'number' }, text: { type: 'string' } }, required: ['path', 'line', 'text'] },
|
|
105
|
+
},
|
|
106
|
+
execute: async ({ pattern, under } = {}) => {
|
|
107
|
+
if (typeof pattern !== 'string' || !pattern) throw new Error('pattern must be a non-empty string')
|
|
108
|
+
// -z: a path is then followed by NUL, never by the `:` a file name may contain.
|
|
109
|
+
const out = await git(root, ['grep', '-nIz', '-E', '--untracked', '-e', pattern, ...(await pathspec(root, under))])
|
|
110
|
+
return out.split('\n').filter(Boolean).map((row) => {
|
|
111
|
+
const [path, line, ...text] = row.split('\0')
|
|
112
|
+
return { path, line: Number(line), text: text.join('\0') }
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
// What a finished script hands back to the model.
|
|
119
|
+
export function shape(text, isError) {
|
|
120
|
+
// load-bearing: secrets stay out of the transcript. A script that read a
|
|
121
|
+
// credentials file must not be able to return it, and the same rules that
|
|
122
|
+
// stop a key landing in a file decide what counts as one.
|
|
123
|
+
const secrets = findHazards(text.split('\n')).filter((h) => h.type === 'secret')
|
|
124
|
+
if (secrets.length) {
|
|
125
|
+
const where = secrets.map((h) => `${h.rule} (output line ${h.line})`).join(', ')
|
|
126
|
+
return { isError: true, text: `omit: output withheld, it carries ${where}. A transcript is not a safe place for a key: filter it out in the script and return only what you need.` }
|
|
127
|
+
}
|
|
128
|
+
if (!text) return { isError, text: '(no output: return a value or call text())' }
|
|
129
|
+
if (text.length <= MAX_OUTPUT_CHARS) return { isError, text }
|
|
130
|
+
const half = MAX_OUTPUT_CHARS / 2
|
|
131
|
+
return {
|
|
132
|
+
isError,
|
|
133
|
+
text: `${text.slice(0, half)}\n[omit: ${text.length - MAX_OUTPUT_CHARS} characters cut here. Return less: count, aggregate or filter in the script.]\n${text.slice(-half)}`,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function execute(code, { CodemodeSandbox, root }) {
|
|
138
|
+
const sandbox = new CodemodeSandbox({ tools: toolsFor(root), timeoutMs: TIMEOUT_MS })
|
|
139
|
+
try {
|
|
140
|
+
const r = await sandbox.execute(code)
|
|
141
|
+
const parts = r.output.map((item) => (item.type === 'text' ? item.text : `[${item.type} output is not supported]`))
|
|
142
|
+
if (r.ok && r.value !== undefined) parts.push(typeof r.value === 'string' ? r.value : JSON.stringify(r.value, null, 1))
|
|
143
|
+
if (!r.ok) parts.push(`Script error (${r.error.kind}): ${r.error.stack ?? r.error.message}`)
|
|
144
|
+
return shape(parts.join('\n'), !r.ok)
|
|
145
|
+
} finally {
|
|
146
|
+
await sandbox.close()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05']
|
|
151
|
+
|
|
152
|
+
// MCP over stdio is one JSON-RPC message per line, and three methods are all a
|
|
153
|
+
// tools-only server answers, so the protocol is spoken here rather than
|
|
154
|
+
// imported. stdout carries nothing but those messages.
|
|
155
|
+
export function serve({ sandbox, root, version, stdin = process.stdin, stdout = process.stdout }) {
|
|
156
|
+
const tool = {
|
|
157
|
+
name: 'codemode',
|
|
158
|
+
description:
|
|
159
|
+
'Run JavaScript that explores this repository through read-only tools, and get back only what the script returns. ' +
|
|
160
|
+
'Use it instead of many separate reads and searches: call tools in parallel with Promise.all, filter and count in the script, return the conclusion. ' +
|
|
161
|
+
`\`code\` is the body of an async function: \`await\` and \`return\` work at the top level, text(value) adds to the output, and there is no fs, network or process. Paths are relative to ${root}.\n\n` +
|
|
162
|
+
sandbox.renderDeclarations({ tools: toolsFor(root) }),
|
|
163
|
+
inputSchema: { type: 'object', properties: { code: { type: 'string', description: 'JavaScript source, the body of an async function' } }, required: ['code'] },
|
|
164
|
+
annotations: { readOnlyHint: true },
|
|
165
|
+
}
|
|
166
|
+
const invalid = (message) => Object.assign(new Error(message), { code: -32602 })
|
|
167
|
+
// A Map, not an object literal: `method` is untrusted, and "constructor"
|
|
168
|
+
// would otherwise resolve to something callable.
|
|
169
|
+
const methods = new Map([
|
|
170
|
+
['initialize', (p) => ({ protocolVersion: PROTOCOLS.includes(p?.protocolVersion) ? p.protocolVersion : PROTOCOLS[0], capabilities: { tools: {} }, serverInfo: { name: 'omit', version } })],
|
|
171
|
+
['ping', () => ({})],
|
|
172
|
+
['tools/list', () => ({ tools: [tool] })],
|
|
173
|
+
['tools/call', async (p) => {
|
|
174
|
+
if (p?.name !== tool.name) throw invalid(`unknown tool: ${p?.name}`)
|
|
175
|
+
if (typeof p.arguments?.code !== 'string') throw invalid('`code` must be a string')
|
|
176
|
+
const { text, isError } = await execute(p.arguments.code, { CodemodeSandbox: sandbox.CodemodeSandbox, root })
|
|
177
|
+
return { content: [{ type: 'text', text }], isError }
|
|
178
|
+
}],
|
|
179
|
+
])
|
|
180
|
+
const send = (message) => stdout.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`)
|
|
181
|
+
createInterface({ input: stdin }).on('line', async (line) => {
|
|
182
|
+
if (!line.trim()) return
|
|
183
|
+
let message
|
|
184
|
+
try {
|
|
185
|
+
message = JSON.parse(line)
|
|
186
|
+
} catch {
|
|
187
|
+
return send({ id: null, error: { code: -32700, message: 'parse error' } })
|
|
188
|
+
}
|
|
189
|
+
if (message?.id === undefined) return // a notification: nothing to answer
|
|
190
|
+
const method = methods.get(message.method)
|
|
191
|
+
if (!method) return send({ id: message.id, error: { code: -32601, message: `method not found: ${message.method}` } })
|
|
192
|
+
try {
|
|
193
|
+
send({ id: message.id, result: await method(message.params) })
|
|
194
|
+
} catch (e) {
|
|
195
|
+
send({ id: message.id, error: { code: Number.isInteger(e.code) ? e.code : -32603, message: e.message } })
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sriinnu/omit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Omit needless code. Editorial discipline for AI coding agents: draft less, cite everything, cut last: enforced by hooks, a pre-commit gate, and a PR bot, whatever agent writes the code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"omit": "bin/omit.mjs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "node --test"
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"preversion": "npm test",
|
|
12
|
+
"release": "node scripts/release.mjs",
|
|
13
|
+
"token:sync": "node scripts/sync-token.mjs"
|
|
11
14
|
},
|
|
12
15
|
"files": [
|
|
13
16
|
"bin",
|
|
@@ -33,12 +36,35 @@
|
|
|
33
36
|
"code-quality",
|
|
34
37
|
"agent-skill",
|
|
35
38
|
"secrets-detection",
|
|
36
|
-
"pre-commit"
|
|
39
|
+
"pre-commit",
|
|
40
|
+
"agent-skills",
|
|
41
|
+
"claude-skills",
|
|
42
|
+
"skill-md",
|
|
43
|
+
"agents-md",
|
|
44
|
+
"codex",
|
|
45
|
+
"windsurf",
|
|
46
|
+
"cline",
|
|
47
|
+
"github-copilot",
|
|
48
|
+
"llm",
|
|
49
|
+
"ai-coding",
|
|
50
|
+
"code-review",
|
|
51
|
+
"github-action"
|
|
37
52
|
],
|
|
38
53
|
"repository": {
|
|
39
54
|
"type": "git",
|
|
40
55
|
"url": "https://github.com/sriinnu/omit.git"
|
|
41
56
|
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@earendil-works/pi-codemode": "^1.0.3"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@earendil-works/pi-codemode": "^1.0.3"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@earendil-works/pi-codemode": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
42
68
|
"publishConfig": {
|
|
43
69
|
"access": "public"
|
|
44
70
|
},
|
package/skills/omit/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: omit
|
|
3
|
-
description: Editorial discipline for AI-written code: omit needless code, cite every claim, cut after it works. Use when writing or changing code, when the user says "omit", "tighten this", "simplest solution", "do less", or complains about over-engineering, bloat, or hallucinated APIs.
|
|
3
|
+
description: "Editorial discipline for AI-written code: omit needless code, cite every claim, cut after it works. Use when writing or changing code, when the user says \"omit\", \"tighten this\", \"simplest solution\", \"do less\", or complains about over-engineering, bloat, or hallucinated APIs."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# omit
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: omit-codemode
|
|
3
|
+
description: "Explore a repository with one sandboxed script instead of many separate reads and searches, so only the answer reaches the conversation. Use when a task needs three or more reads or greps, a loop over files, or a count or summary across the codebase. Read-only."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# omit codemode
|
|
7
|
+
|
|
8
|
+
*Omit needless context.*
|
|
9
|
+
|
|
10
|
+
Every separate read or grep puts its raw output into the conversation, where it is paid for again on every later turn. Write one script instead: the reads happen inside it, and only what it returns comes back.
|
|
11
|
+
|
|
12
|
+
## Run it
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
omit codemode run <<'EOF'
|
|
16
|
+
const libs = (await tools.files({ under: 'lib' })).filter((f) => f.endsWith('.mjs'))
|
|
17
|
+
const sources = await Promise.all(libs.map((path) => tools.read({ path })))
|
|
18
|
+
return Object.fromEntries(libs.map((f, i) => [f, (sources[i].match(/^export /gm) ?? []).length]))
|
|
19
|
+
EOF
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Quote the delimiter (`'EOF'`) so the shell leaves the script alone. Exit status 1 means the script failed or its result was withheld.
|
|
23
|
+
|
|
24
|
+
## The script
|
|
25
|
+
|
|
26
|
+
The body of an async function: `await` and `return` work at the top level.
|
|
27
|
+
|
|
28
|
+
| Call | Resolves to |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `tools.files({ under? })` | `string[]`: tracked and untracked files, minus what git ignores |
|
|
31
|
+
| `tools.read({ path })` | `string`: one file as text |
|
|
32
|
+
| `tools.grep({ pattern, under? })` | `{ path, line, text }[]`: extended regular expression; no match is `[]` |
|
|
33
|
+
|
|
34
|
+
`text(value)` adds to the output and `return` adds the returned value. A call that fails rejects with an `Error`; use `Promise.allSettled` to keep the calls that worked. Run independent calls together with `Promise.all`.
|
|
35
|
+
|
|
36
|
+
There is nothing else in there: no file system, network, `process` or timers. Paths are relative to the current directory and cannot leave it.
|
|
37
|
+
|
|
38
|
+
## Return the conclusion
|
|
39
|
+
|
|
40
|
+
- Return counts, names and the few lines that matter, not file contents. Past 20,000 characters the middle of the result is cut.
|
|
41
|
+
- A result that contains a secret is withheld whole. Filter it out in the script.
|
|
42
|
+
- A script has 60 seconds.
|
|
43
|
+
|
|
44
|
+
## When not to
|
|
45
|
+
|
|
46
|
+
- One file or one search: use the ordinary tool.
|
|
47
|
+
- Anything that writes. Codemode is read-only: make edits with the ordinary tools, where the sentinels see them.
|
|
48
|
+
- If the command says its sandbox is not installed, give the user the install line it prints and carry on with ordinary tools. Do not work around it.
|