oh-my-agent-opencode 0.0.0-stage → 20.38.3
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 +54 -2
- package/code-intelligence-guard.ts +24 -0
- package/code-intelligence-primer.ts +24 -0
- package/filter-test-output.sh +52 -0
- package/index.ts +132 -0
- package/keyword-detector.ts +24 -0
- package/oma.ts +484 -0
- package/package.json +37 -4
- package/persistent-mode.ts +24 -0
- package/refactor-guard.ts +24 -0
- package/scm-guard.ts +24 -0
- package/serena-primer.ts +24 -0
- package/skill-injector.ts +24 -0
- package/state-boundary.ts +24 -0
- package/test-filter.ts +24 -0
package/README.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# oh-my-agent-opencode
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The [oh-my-agent](https://github.com/first-fluke/oh-my-agent) hook bridge as an
|
|
4
|
+
npm plugin for [OpenCode](https://opencode.ai) and [Kilo Code](https://kilo.ai).
|
|
5
|
+
It registers the same in-process handlers `oma link` installs per project
|
|
6
|
+
(`.opencode/plugins/oma/`), without copying anything into the project, and
|
|
7
|
+
serves both plugin contracts from one default export: the OpenCode 2.x
|
|
8
|
+
`{ id, setup }` definition and the V1 `server()` object form Kilo Code and
|
|
9
|
+
OpenCode 1.18.29+ load.
|
|
10
|
+
|
|
11
|
+
Requires the `oma` CLI on PATH (`npm install -g oh-my-agent`) and `bun`.
|
|
12
|
+
Every hook fails open: without `oma` the host runs unchanged.
|
|
13
|
+
|
|
14
|
+
## OpenCode
|
|
15
|
+
|
|
16
|
+
Add the package to `plugin` in `.opencode/opencode.json` (project) or
|
|
17
|
+
`~/.config/opencode/opencode.json` (global). OpenCode installs npm plugins
|
|
18
|
+
on startup.
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"$schema": "https://opencode.ai/config.json",
|
|
23
|
+
"plugin": ["oh-my-agent-opencode"]
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Pin a release with `"oh-my-agent-opencode@<version>"`.
|
|
28
|
+
|
|
29
|
+
## Kilo Code
|
|
30
|
+
|
|
31
|
+
Kilo reads the same key from `.kilo/kilo.json` or
|
|
32
|
+
`~/.config/kilo/kilo.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"$schema": "https://app.kilo.ai/config.json",
|
|
37
|
+
"plugin": ["oh-my-agent-opencode"]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## What the bridge does
|
|
42
|
+
|
|
43
|
+
| OpenCode 2.x hook | Kilo / 1.x hook | oh-my-agent handler |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| session `prompt` | `chat.message` | keyword-detector (workflow activation) and skill-injector |
|
|
46
|
+
| session `context` | `experimental.chat.system.transform` | injects the detected context into the system prompt |
|
|
47
|
+
| tool `execute.before` (bash) | `tool.execute.before` | scm-guard (blocks staging likely secrets), test-filter (failures-only test output) |
|
|
48
|
+
| session `experimental.idle` | `session.idle` event | persistent-mode re-enters an unfinished workflow |
|
|
49
|
+
|
|
50
|
+
Each handler entry in this package runs
|
|
51
|
+
`oma hook script --vendor opencode --script <name>`; the handler logic lives
|
|
52
|
+
in the installed CLI, so updating `oma` updates the behaviour.
|
|
53
|
+
|
|
54
|
+
A project that already has `./plugins/oma/oma.ts` registered by `oma link`
|
|
55
|
+
does not need this package; keep one or the other.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "code-intelligence-guard.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "code-intelligence-primer.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# filter-test-output.sh — Strip passing test indicators, keep failures and summaries
|
|
3
|
+
# Pipe usage: test_command 2>&1 | bash filter-test-output.sh
|
|
4
|
+
#
|
|
5
|
+
# Supported: vitest, jest, mocha, pytest, go test, cargo test, rspec,
|
|
6
|
+
# flutter/dart test, swift test, dotnet test, gradle/mvn test,
|
|
7
|
+
# mix test, phpunit
|
|
8
|
+
# Bypass: OMA_TEST_FILTER=0
|
|
9
|
+
|
|
10
|
+
if [[ "${OMA_TEST_FILTER:-1}" == "0" ]]; then
|
|
11
|
+
cat
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
awk '
|
|
16
|
+
# vitest/jest/mocha: remove individual passing tests
|
|
17
|
+
/^[[:space:]]*[✓√✔][[:space:]]/ { next }
|
|
18
|
+
|
|
19
|
+
# jest: remove PASS file headers
|
|
20
|
+
/^[[:space:]]*PASS[[:space:]]/ { next }
|
|
21
|
+
|
|
22
|
+
# go test: remove passing tests and ok package lines
|
|
23
|
+
/^--- PASS:/ { next }
|
|
24
|
+
/^[[:space:]]*ok[[:space:]]+[a-zA-Z]/ { next }
|
|
25
|
+
|
|
26
|
+
# pytest: remove PASSED lines
|
|
27
|
+
/PASSED[[:space:]]*$/ { next }
|
|
28
|
+
|
|
29
|
+
# cargo test: remove "test ... ok" lines
|
|
30
|
+
/^test .+ \.\.\. ok$/ { next }
|
|
31
|
+
|
|
32
|
+
# flutter/dart: remove passing tests (+N without -N, excluding summary)
|
|
33
|
+
/^[0-9][0-9]:[0-9][0-9] \+[0-9]+: / && !/\-[0-9]+/ && !/All tests/ { next }
|
|
34
|
+
|
|
35
|
+
# swift: remove passed test cases
|
|
36
|
+
/Test Case .* passed/ { next }
|
|
37
|
+
|
|
38
|
+
# dotnet: remove individual Passed lines (keep "Passed!" summary)
|
|
39
|
+
/^[[:space:]]*Passed[[:space:]]/ && !/Passed!/ { next }
|
|
40
|
+
|
|
41
|
+
# gradle/mvn: remove individual test pass indicators
|
|
42
|
+
/^[[:space:]]*Tests run:.*Failures: 0.*Errors: 0/ { next }
|
|
43
|
+
|
|
44
|
+
# phpunit: remove dots-only progress lines
|
|
45
|
+
/^[.]+$/ { next }
|
|
46
|
+
|
|
47
|
+
# rspec: remove passing example dots (single-char lines)
|
|
48
|
+
/^[.]+[[:space:]]*$/ { next }
|
|
49
|
+
|
|
50
|
+
# keep everything else
|
|
51
|
+
{ print }
|
|
52
|
+
'
|
package/index.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oh-my-agent — opencode-family npm package entry (`oh-my-agent-opencode`).
|
|
3
|
+
*
|
|
4
|
+
* SSOT source. `oma emit --target opencode-package` ships this file as the
|
|
5
|
+
* package `main`/`exports` next to the bridge (`./oma.ts`) and the handler
|
|
6
|
+
* entries it spawns. It is NOT part of the per-project install, which keeps
|
|
7
|
+
* registering `./plugins/oma/oma.ts` directly.
|
|
8
|
+
*
|
|
9
|
+
* Two plugin contracts coexist in the family, and a package plugin must
|
|
10
|
+
* satisfy whichever host imports it:
|
|
11
|
+
*
|
|
12
|
+
* - OpenCode 2.x (opencode.ai/docs/build/plugins): the default export is a
|
|
13
|
+
* definition `{ id, setup(ctx) }` (`Plugin.define` is the identity). Hooks
|
|
14
|
+
* are registered through context domains — `ctx.session.hook("prompt" |
|
|
15
|
+
* "context" | "experimental.idle")`, `ctx.tool.hook("execute.before")`.
|
|
16
|
+
* A function default export is rejected ("Plugin must export a default
|
|
17
|
+
* definition with an id and an effect or setup function").
|
|
18
|
+
* - Kilo Code CLI (7.x) and OpenCode 1.18.29+: a package plugin "must
|
|
19
|
+
* default export an object with server()" — the V1 object form. `server`
|
|
20
|
+
* receives the same `{ project, client, $, directory, worktree }` input
|
|
21
|
+
* the function-style bridge takes and returns the hooks object. (Older 1.x
|
|
22
|
+
* loaders that only accept a bare function export are not served by the
|
|
23
|
+
* package; they still work with the per-project `./plugins/oma/oma.ts`.)
|
|
24
|
+
*
|
|
25
|
+
* So the default export carries both: `server` (the unchanged legacy bridge,
|
|
26
|
+
* called by the V1 loaders) and `id` + `setup` (the v2 definition, whose
|
|
27
|
+
* `setup` adapts the v2 events onto that same legacy hooks object) — one
|
|
28
|
+
* bridge implementation, zero duplicated handler logic. Dependency-free: the
|
|
29
|
+
* v2 context is typed structurally, so neither `@opencode/plugin` nor
|
|
30
|
+
* `@opencode-ai/plugin` is needed at runtime.
|
|
31
|
+
*
|
|
32
|
+
* v2 mapping (→ legacy hook the bridge already implements):
|
|
33
|
+
* session "prompt" → "chat.message" (keyword-detector, skill-injector)
|
|
34
|
+
* session "context" → "experimental.chat.system.transform" (inject)
|
|
35
|
+
* tool "execute.before" → "tool.execute.before" (scm-guard, test-filter;
|
|
36
|
+
* a throw rejects the call, as in v1)
|
|
37
|
+
* session "experimental.idle" → event session.idle (persistent-mode re-entry
|
|
38
|
+
* through ctx.session.prompt)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import legacy from "./oma.ts";
|
|
42
|
+
|
|
43
|
+
type LegacyHooks = Awaited<ReturnType<typeof legacy>>;
|
|
44
|
+
|
|
45
|
+
/** Structural subset of the OpenCode 2.x plugin context this entry uses. */
|
|
46
|
+
interface OmaV2Context {
|
|
47
|
+
location?: { directory?: string };
|
|
48
|
+
session: {
|
|
49
|
+
hook: (
|
|
50
|
+
name: string,
|
|
51
|
+
callback: (event: Record<string, unknown>) => Promise<void> | void,
|
|
52
|
+
) => Promise<unknown>;
|
|
53
|
+
prompt?: (input: Record<string, unknown>) => Promise<unknown>;
|
|
54
|
+
};
|
|
55
|
+
tool: {
|
|
56
|
+
hook: (
|
|
57
|
+
name: string,
|
|
58
|
+
callback: (event: Record<string, unknown>) => Promise<void> | void,
|
|
59
|
+
) => Promise<unknown>;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sessionIDOf(event: Record<string, unknown>): string | undefined {
|
|
64
|
+
return typeof event.sessionID === "string" ? event.sessionID : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The package plugin. V1 object loaders (Kilo, OpenCode 1.18.29+) call
|
|
69
|
+
* `server(input)` and use the returned hooks; OpenCode 2.x reads `id` and
|
|
70
|
+
* calls `setup(ctx)`, which instantiates the legacy bridge with a client
|
|
71
|
+
* adapter (so session.idle re-entry posts through `ctx.session.prompt`) and
|
|
72
|
+
* registers one v2 hook per legacy handler.
|
|
73
|
+
*/
|
|
74
|
+
export default {
|
|
75
|
+
id: "oh-my-agent",
|
|
76
|
+
server: legacy,
|
|
77
|
+
async setup(ctx: OmaV2Context): Promise<void> {
|
|
78
|
+
const directory = ctx.location?.directory ?? process.cwd();
|
|
79
|
+
const hooks: LegacyHooks = await legacy({
|
|
80
|
+
directory,
|
|
81
|
+
client: {
|
|
82
|
+
session: {
|
|
83
|
+
prompt: async ({ path, body }) => {
|
|
84
|
+
if (!ctx.session.prompt) return;
|
|
85
|
+
await ctx.session.prompt({
|
|
86
|
+
sessionID: path.id,
|
|
87
|
+
text: body.parts.map((part) => part.text).join("\n"),
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await ctx.session.hook("prompt", async (event) => {
|
|
95
|
+
const sessionID = sessionIDOf(event);
|
|
96
|
+
const prompt = event.prompt as { text?: unknown } | undefined;
|
|
97
|
+
const text = typeof prompt?.text === "string" ? prompt.text : "";
|
|
98
|
+
await hooks["chat.message"](
|
|
99
|
+
{ sessionID },
|
|
100
|
+
{ parts: [{ type: "text", text }] },
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await ctx.session.hook("context", async (event) => {
|
|
105
|
+
const sessionID = sessionIDOf(event);
|
|
106
|
+
const out: { system: string[] } = { system: [] };
|
|
107
|
+
await hooks["experimental.chat.system.transform"]({ sessionID }, out);
|
|
108
|
+
const system = event.system;
|
|
109
|
+
if (!Array.isArray(system)) return;
|
|
110
|
+
for (const text of out.system) system.push({ type: "text", text });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await ctx.tool.hook("execute.before", async (event) => {
|
|
114
|
+
const input = event.input;
|
|
115
|
+
if (!input || typeof input !== "object") return;
|
|
116
|
+
// The legacy hook mutates `output.args.command`; `args` IS the v2
|
|
117
|
+
// input object, so a test-filter rewrite lands on the real call.
|
|
118
|
+
await hooks["tool.execute.before"](
|
|
119
|
+
{ tool: typeof event.tool === "string" ? event.tool : undefined },
|
|
120
|
+
{ args: input as { command?: string } },
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await ctx.session.hook("experimental.idle", async (event) => {
|
|
125
|
+
const sessionID = sessionIDOf(event);
|
|
126
|
+
if (!sessionID) return;
|
|
127
|
+
await hooks.event({
|
|
128
|
+
event: { type: "session.idle", properties: { sessionID } },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "keyword-detector.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
package/oma.ts
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* oh-my-agent — opencode (Sst opencode) plugin bridge.
|
|
3
|
+
*
|
|
4
|
+
* SSOT source. At install time `installOpencodePlugin` copies this file to
|
|
5
|
+
* `.opencode/plugins/oma/oma.ts` alongside the core hook scripts, and
|
|
6
|
+
* `registerOpencodePlugin` adds it to `.opencode/opencode.jsonc` (the nested
|
|
7
|
+
* subdir is invisible to opencode's flat auto-discovery).
|
|
8
|
+
*
|
|
9
|
+
* Why a bridge instead of a per-vendor variants JSON entry: opencode does NOT
|
|
10
|
+
* register settings-file hooks like the other vendors. It loads in-process
|
|
11
|
+
* TypeScript plugins whose handlers are invoked with an `(input, output)` pair
|
|
12
|
+
* and are expected to MUTATE `output` (return values are ignored). So rather
|
|
13
|
+
* than the generic `installHooksFromVariant` path (events → settings file →
|
|
14
|
+
* `bun <script>` subprocess), opencode gets this thin shim that maps opencode
|
|
15
|
+
* lifecycle handlers onto oma's existing, vendor-agnostic core scripts via
|
|
16
|
+
* subprocess. All matching logic stays in the core scripts.
|
|
17
|
+
*
|
|
18
|
+
* The core scripts installed under `.opencode/plugins/oma/` are not recognised
|
|
19
|
+
* as an opencode vendor (there is no `.opencode` case in the shared
|
|
20
|
+
* `inferVendorFromScriptPath`), so they resolve to the `claude` output dialect.
|
|
21
|
+
* This bridge therefore reads the claude-shaped stdout: `additionalContext`
|
|
22
|
+
* for prompt hooks, `hookSpecificOutput.updatedInput.command` for PreToolUse,
|
|
23
|
+
* and `{ decision: "block", reason }` for the Stop equivalent. Extraction is
|
|
24
|
+
* defensive (top-level OR `hookSpecificOutput`) so a dialect shift does not
|
|
25
|
+
* silently break the bridge.
|
|
26
|
+
*
|
|
27
|
+
* Intentionally dependency-free (no @opencode-ai/plugin import) so this file
|
|
28
|
+
* works in any user project without requiring opencode as a dev dependency.
|
|
29
|
+
* Handler and client shapes are satisfied structurally via inline types.
|
|
30
|
+
*
|
|
31
|
+
* Event mapping:
|
|
32
|
+
* chat.message ← UserPromptSubmit
|
|
33
|
+
* Extracts user text from output.parts, runs keyword-detector +
|
|
34
|
+
* skill-injector, and stashes the resulting context per session (keyed
|
|
35
|
+
* by input.sessionID). The opencode session id is forwarded to the core
|
|
36
|
+
* scripts so keyword-detector's persistent-mode activation writes a
|
|
37
|
+
* session-scoped state file (an unresolved id would refuse to persist).
|
|
38
|
+
* experimental.chat.system.transform ← context injection
|
|
39
|
+
* Peeks the stashed context for input.sessionID and pushes it onto
|
|
40
|
+
* output.system. Non-consuming: the stash is refreshed (or cleared)
|
|
41
|
+
* every chat.message, so re-injection across model calls in a turn is
|
|
42
|
+
* harmless and a chat.message→transform ordering assumption cannot
|
|
43
|
+
* permanently drop context.
|
|
44
|
+
* tool.execute.before ← PreToolUse (bash only)
|
|
45
|
+
* Runs scm-guard first (throws to block `git add` of likely-secret
|
|
46
|
+
* files — throwing is opencode's documented block mechanism, see
|
|
47
|
+
* opencode.ai/docs/plugins), then test-filter on output.args.command,
|
|
48
|
+
* writing the rewritten command back onto output.args.command.
|
|
49
|
+
* Known upstream caveat: plugin hooks do not intercept subagent tool
|
|
50
|
+
* calls (anomalyco/opencode#5894), so the guard covers the primary
|
|
51
|
+
* agent only.
|
|
52
|
+
* event: "session.idle" ← Stop (BEST-EFFORT / re-entrant)
|
|
53
|
+
* Runs persistent-mode. If it returns a block decision, the workflow is
|
|
54
|
+
* still active: re-enter the loop by posting the block reason as a new
|
|
55
|
+
* prompt via client.session.prompt. Best-effort — any failure is
|
|
56
|
+
* swallowed. persistent-mode's own reinforcement cap (5) is the primary
|
|
57
|
+
* terminator; a per-session consecutive-idle counter is a runaway
|
|
58
|
+
* backstop only.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
import { spawnSync } from "node:child_process";
|
|
62
|
+
import { fileURLToPath } from "node:url";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* persistent-mode may run a goal stop gate (25s cap in persistent-mode.ts).
|
|
66
|
+
* Its budget matches the variant Stop handler timeout (30s) so this bridge
|
|
67
|
+
* never kills the hook mid-gate — the other core scripts keep 5s.
|
|
68
|
+
*/
|
|
69
|
+
const PERSISTENT_MODE_TIMEOUT_MS = 30_000;
|
|
70
|
+
|
|
71
|
+
/** Absolute path to a core script copied next to this bridge at install time. */
|
|
72
|
+
function corePath(script: string): string {
|
|
73
|
+
return fileURLToPath(new URL(`./${script}`, import.meta.url));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Directory of this bridge, exported to the handlers as OMA_HOOK_BRIDGE_DIR so
|
|
78
|
+
* test-filter finds `filter-test-output.sh` when the bridge ships as the
|
|
79
|
+
* `oh-my-agent-opencode` npm package (outside the project tree).
|
|
80
|
+
*/
|
|
81
|
+
function bridgeEnv(): NodeJS.ProcessEnv {
|
|
82
|
+
try {
|
|
83
|
+
return {
|
|
84
|
+
...process.env,
|
|
85
|
+
OMA_HOOK_BRIDGE_DIR: fileURLToPath(new URL(".", import.meta.url)),
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return process.env;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run an oma core hook script as a subprocess: feed it JSON on stdin, parse
|
|
94
|
+
* its JSON stdout. Fail-open (returns null) on any error — a broken hook must
|
|
95
|
+
* never block the agent. Spawns with `cwd` = opencode's working directory so
|
|
96
|
+
* the core scripts resolve the project (git) root the same way they do for
|
|
97
|
+
* every other vendor. Non-zero exit (persistent-mode blocks with exit 2) is
|
|
98
|
+
* expected: stdout is still captured and parsed.
|
|
99
|
+
*/
|
|
100
|
+
function runCore(
|
|
101
|
+
script: string,
|
|
102
|
+
payload: Record<string, unknown>,
|
|
103
|
+
cwd: string,
|
|
104
|
+
timeoutMs = 5000,
|
|
105
|
+
): Record<string, unknown> | null {
|
|
106
|
+
try {
|
|
107
|
+
const res = spawnSync("bun", [corePath(script)], {
|
|
108
|
+
input: JSON.stringify(payload),
|
|
109
|
+
cwd,
|
|
110
|
+
encoding: "utf-8",
|
|
111
|
+
timeout: timeoutMs,
|
|
112
|
+
env: bridgeEnv(),
|
|
113
|
+
});
|
|
114
|
+
const out = (res.stdout ?? "").trim();
|
|
115
|
+
if (!out) return null;
|
|
116
|
+
return JSON.parse(out) as Record<string, unknown>;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract injected context from a prompt-hook (keyword-detector / skill-
|
|
124
|
+
* injector) result. The core scripts emit the `claude` dialect, which carries
|
|
125
|
+
* `additionalContext` both at the top level and under `hookSpecificOutput`;
|
|
126
|
+
* read either so a dialect change does not silently drop context.
|
|
127
|
+
*/
|
|
128
|
+
function extractAdditionalContext(
|
|
129
|
+
out: Record<string, unknown> | null,
|
|
130
|
+
): string | null {
|
|
131
|
+
if (!out) return null;
|
|
132
|
+
if (typeof out.additionalContext === "string" && out.additionalContext) {
|
|
133
|
+
return out.additionalContext;
|
|
134
|
+
}
|
|
135
|
+
const hso = out.hookSpecificOutput;
|
|
136
|
+
if (hso && typeof hso === "object") {
|
|
137
|
+
const nested = (hso as Record<string, unknown>).additionalContext;
|
|
138
|
+
if (typeof nested === "string" && nested) return nested;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extract the rewritten command from a test-filter (PreToolUse) result. The
|
|
145
|
+
* `claude` dialect nests it under `hookSpecificOutput.updatedInput.command`;
|
|
146
|
+
* a top-level `updatedInput.command` is also accepted for dialect resilience.
|
|
147
|
+
*/
|
|
148
|
+
function extractUpdatedCommand(
|
|
149
|
+
out: Record<string, unknown> | null,
|
|
150
|
+
): string | null {
|
|
151
|
+
if (!out) return null;
|
|
152
|
+
const readCommand = (v: unknown): string | null => {
|
|
153
|
+
if (v && typeof v === "object") {
|
|
154
|
+
const cmd = (v as Record<string, unknown>).command;
|
|
155
|
+
if (typeof cmd === "string" && cmd) return cmd;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
const direct = readCommand(out.updatedInput);
|
|
160
|
+
if (direct) return direct;
|
|
161
|
+
const hso = out.hookSpecificOutput;
|
|
162
|
+
if (hso && typeof hso === "object") {
|
|
163
|
+
return readCommand((hso as Record<string, unknown>).updatedInput);
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract a PreToolUse deny reason from an scm-guard result. The core script
|
|
170
|
+
* emits the `claude` dialect (`hookSpecificOutput.permissionDecision: "deny"`
|
|
171
|
+
* + `permissionDecisionReason`); a top-level `permission: "deny"` +
|
|
172
|
+
* `user_message`/`reason` is also accepted for dialect resilience.
|
|
173
|
+
*/
|
|
174
|
+
function extractDenyReason(out: Record<string, unknown> | null): string | null {
|
|
175
|
+
if (!out) return null;
|
|
176
|
+
const hso = out.hookSpecificOutput;
|
|
177
|
+
if (hso && typeof hso === "object") {
|
|
178
|
+
const h = hso as Record<string, unknown>;
|
|
179
|
+
if (h.permissionDecision === "deny") {
|
|
180
|
+
return typeof h.permissionDecisionReason === "string" &&
|
|
181
|
+
h.permissionDecisionReason
|
|
182
|
+
? h.permissionDecisionReason
|
|
183
|
+
: "Blocked by oma scm-guard.";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (out.permission === "deny" || out.decision === "deny") {
|
|
187
|
+
const reason = out.user_message ?? out.reason;
|
|
188
|
+
return typeof reason === "string" && reason
|
|
189
|
+
? reason
|
|
190
|
+
: "Blocked by oma scm-guard.";
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Join the text parts of a chat.message output into the user's prompt text. */
|
|
196
|
+
function extractPromptText(output: {
|
|
197
|
+
message?: unknown;
|
|
198
|
+
parts?: Array<{ type?: string; text?: string }>;
|
|
199
|
+
}): string {
|
|
200
|
+
const parts = Array.isArray(output.parts) ? output.parts : [];
|
|
201
|
+
const texts = parts
|
|
202
|
+
.filter((p) => p && p.type === "text" && typeof p.text === "string")
|
|
203
|
+
.map((p) => p.text as string);
|
|
204
|
+
if (texts.length > 0) return stripWrappingQuotes(texts.join("\n"));
|
|
205
|
+
// Defensive fallback: some builds may surface text on the message object.
|
|
206
|
+
const msg = output.message as { text?: unknown } | undefined;
|
|
207
|
+
if (msg && typeof msg.text === "string") return stripWrappingQuotes(msg.text);
|
|
208
|
+
return "";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* `opencode run "<prompt>"` delivers the part text wrapped in literal double
|
|
213
|
+
* quotes (`"work: …"`), which defeats keyword detection anchored at the prompt
|
|
214
|
+
* start. A prompt that is entirely enclosed in one matching pair of quotes is
|
|
215
|
+
* that CLI artifact — unwrap exactly one pair; interior quotes are untouched.
|
|
216
|
+
*/
|
|
217
|
+
function stripWrappingQuotes(text: string): string {
|
|
218
|
+
const t = text.trim();
|
|
219
|
+
if (t.length >= 2 && t.startsWith('"') && t.endsWith('"')) {
|
|
220
|
+
return t.slice(1, -1);
|
|
221
|
+
}
|
|
222
|
+
return t;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Per-session bridge state ──────────────────────────────────
|
|
226
|
+
//
|
|
227
|
+
// opencode plugins are loaded once per server process, so module-level maps
|
|
228
|
+
// persist across a session's handler invocations. Both maps are keyed by the
|
|
229
|
+
// opencode sessionID and bounded so a long-lived server cannot leak memory
|
|
230
|
+
// across many sessions.
|
|
231
|
+
|
|
232
|
+
const MAX_TRACKED_SESSIONS = 256;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Context computed on chat.message, awaiting injection by the system-prompt
|
|
236
|
+
* transform. Overwrite-per-session: each chat.message replaces the entry
|
|
237
|
+
* (with `""` when nothing matched), so the transform never injects stale
|
|
238
|
+
* context and a peek does not need to consume.
|
|
239
|
+
*/
|
|
240
|
+
const pendingContext = new Map<string, string>();
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Consecutive session.idle re-entries per session, reset by any chat.message.
|
|
244
|
+
* A runaway backstop only — persistent-mode's reinforcement cap terminates the
|
|
245
|
+
* loop first under normal operation.
|
|
246
|
+
*/
|
|
247
|
+
const idleReentryCount = new Map<string, number>();
|
|
248
|
+
const MAX_CONSECUTIVE_IDLE_REENTRIES = 50;
|
|
249
|
+
|
|
250
|
+
/** Set a bounded map entry, evicting the oldest key when over capacity. */
|
|
251
|
+
function setBounded<V>(map: Map<string, V>, key: string, value: V): void {
|
|
252
|
+
if (map.has(key)) map.delete(key);
|
|
253
|
+
map.set(key, value);
|
|
254
|
+
while (map.size > MAX_TRACKED_SESSIONS) {
|
|
255
|
+
const oldest = map.keys().next().value;
|
|
256
|
+
if (oldest === undefined) break;
|
|
257
|
+
map.delete(oldest);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Minimal structural view of the opencode SDK client used for loop re-entry.
|
|
263
|
+
* Kept optional and dependency-free; the real client is a superset.
|
|
264
|
+
*/
|
|
265
|
+
type OmaOpencodeClient = {
|
|
266
|
+
session?: {
|
|
267
|
+
prompt?: (options: {
|
|
268
|
+
path: { id: string };
|
|
269
|
+
body: { parts: Array<{ type: "text"; text: string }> };
|
|
270
|
+
}) => Promise<unknown>;
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* oh-my-agent opencode plugin.
|
|
276
|
+
*
|
|
277
|
+
* Structural shape satisfies the opencode `Plugin` contract without importing
|
|
278
|
+
* @opencode-ai/plugin, keeping this bridge dependency-free in the user's
|
|
279
|
+
* project. Handlers mutate their `output` argument (return values are ignored).
|
|
280
|
+
*
|
|
281
|
+
* @satisfies Plugin
|
|
282
|
+
*/
|
|
283
|
+
export default (async ({
|
|
284
|
+
directory,
|
|
285
|
+
client,
|
|
286
|
+
}: {
|
|
287
|
+
directory?: string;
|
|
288
|
+
client?: OmaOpencodeClient;
|
|
289
|
+
}) => {
|
|
290
|
+
const cwd = directory ?? process.cwd();
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
/**
|
|
294
|
+
* chat.message ← UserPromptSubmit
|
|
295
|
+
*
|
|
296
|
+
* Runs keyword-detector (workflow activation) and skill-injector (context
|
|
297
|
+
* injection), then stashes the concatenated context for this session so
|
|
298
|
+
* `experimental.chat.system.transform` can inject it. Forwards the opencode
|
|
299
|
+
* sessionID so persistent-workflow state is written session-scoped.
|
|
300
|
+
*/
|
|
301
|
+
"chat.message": async (
|
|
302
|
+
input: { sessionID?: string },
|
|
303
|
+
output: {
|
|
304
|
+
message?: unknown;
|
|
305
|
+
parts?: Array<{ type?: string; text?: string }>;
|
|
306
|
+
},
|
|
307
|
+
): Promise<void> => {
|
|
308
|
+
const sessionID = input.sessionID ?? "";
|
|
309
|
+
// A genuine user turn resets the runaway idle backstop.
|
|
310
|
+
if (sessionID) idleReentryCount.delete(sessionID);
|
|
311
|
+
|
|
312
|
+
const prompt = extractPromptText(output);
|
|
313
|
+
const payload = {
|
|
314
|
+
prompt,
|
|
315
|
+
cwd,
|
|
316
|
+
sessionId: sessionID,
|
|
317
|
+
hook_event_name: "UserPromptSubmit",
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const parts: string[] = [];
|
|
321
|
+
const kd = extractAdditionalContext(
|
|
322
|
+
runCore("keyword-detector.ts", payload, cwd),
|
|
323
|
+
);
|
|
324
|
+
if (kd) parts.push(kd);
|
|
325
|
+
const si = extractAdditionalContext(
|
|
326
|
+
runCore("skill-injector.ts", payload, cwd),
|
|
327
|
+
);
|
|
328
|
+
if (si) parts.push(si);
|
|
329
|
+
|
|
330
|
+
// Overwrite per session — even with "" — so the transform reflects this
|
|
331
|
+
// turn's detection and never re-injects a previous turn's context.
|
|
332
|
+
if (sessionID) setBounded(pendingContext, sessionID, parts.join("\n\n"));
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* experimental.chat.system.transform ← context injection
|
|
337
|
+
*
|
|
338
|
+
* Peeks (non-consuming) the context stashed by chat.message for this
|
|
339
|
+
* session and appends it to the system prompt. chat.message runs before the
|
|
340
|
+
* system transform within a turn; peeking keeps injection idempotent across
|
|
341
|
+
* multiple model calls in a turn and prevents an ordering race from
|
|
342
|
+
* permanently dropping context (the stash is refreshed every chat.message).
|
|
343
|
+
*/
|
|
344
|
+
"experimental.chat.system.transform": async (
|
|
345
|
+
input: { sessionID?: string },
|
|
346
|
+
output: { system?: string[] },
|
|
347
|
+
): Promise<void> => {
|
|
348
|
+
const sessionID = input.sessionID;
|
|
349
|
+
if (!sessionID) return;
|
|
350
|
+
const ctx = pendingContext.get(sessionID);
|
|
351
|
+
if (ctx && Array.isArray(output.system)) {
|
|
352
|
+
output.system.push(ctx);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* tool.execute.before ← PreToolUse (bash)
|
|
358
|
+
*
|
|
359
|
+
* `input.tool` is a plain string and the command lives on
|
|
360
|
+
* `output.args.command`. First runs scm-guard — throwing is opencode's
|
|
361
|
+
* documented mechanism for blocking a tool call, so a deny becomes a
|
|
362
|
+
* `throw` (this is the ONE deliberate non-fail-open path in this bridge).
|
|
363
|
+
* Then runs the OMA test-filter for bash commands to rewrite test-runner
|
|
364
|
+
* invocations so only failures reach the model, and writes the rewritten
|
|
365
|
+
* command back. Non-bash tools pass through unchanged.
|
|
366
|
+
*/
|
|
367
|
+
"tool.execute.before": async (
|
|
368
|
+
input: { tool?: string },
|
|
369
|
+
output: { args?: { command?: string } },
|
|
370
|
+
): Promise<void> => {
|
|
371
|
+
if (input.tool !== "bash") return;
|
|
372
|
+
const command = output.args?.command;
|
|
373
|
+
if (typeof command !== "string" || !command) return;
|
|
374
|
+
|
|
375
|
+
const denyReason = extractDenyReason(
|
|
376
|
+
runCore(
|
|
377
|
+
"scm-guard.ts",
|
|
378
|
+
{
|
|
379
|
+
tool_name: "Bash",
|
|
380
|
+
tool_input: { command },
|
|
381
|
+
cwd,
|
|
382
|
+
hook_event_name: "PreToolUse",
|
|
383
|
+
},
|
|
384
|
+
cwd,
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
if (denyReason) throw new Error(denyReason);
|
|
388
|
+
|
|
389
|
+
const updated = extractUpdatedCommand(
|
|
390
|
+
runCore(
|
|
391
|
+
"test-filter.ts",
|
|
392
|
+
{
|
|
393
|
+
tool_name: "Bash",
|
|
394
|
+
tool_input: { command },
|
|
395
|
+
cwd,
|
|
396
|
+
hook_event_name: "PreToolUse",
|
|
397
|
+
},
|
|
398
|
+
cwd,
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
if (updated && output.args) {
|
|
402
|
+
output.args.command = updated;
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* event: "session.idle" ← Stop (BEST-EFFORT / re-entrant)
|
|
408
|
+
*
|
|
409
|
+
* opencode delivers session.idle over the event bus (it is no longer a
|
|
410
|
+
* top-level hook). Unlike the Claude `Stop` hook this cannot block
|
|
411
|
+
* termination, but the SDK client lets us re-enter the loop: run
|
|
412
|
+
* persistent-mode and, if a persistent workflow is still active (block
|
|
413
|
+
* decision), post the block reason back as a new prompt so the agent
|
|
414
|
+
* continues.
|
|
415
|
+
*
|
|
416
|
+
* BEST-EFFORT: every step is wrapped so a failure can never throw into
|
|
417
|
+
* opencode. persistent-mode's own reinforcement cap (5) terminates the
|
|
418
|
+
* loop under normal operation; the per-session consecutive-idle counter is
|
|
419
|
+
* a runaway backstop for pathological cases only.
|
|
420
|
+
*/
|
|
421
|
+
event: async (input: {
|
|
422
|
+
event?: { type?: string; properties?: Record<string, unknown> };
|
|
423
|
+
}): Promise<void> => {
|
|
424
|
+
const event = input.event;
|
|
425
|
+
if (event?.type !== "session.idle") return;
|
|
426
|
+
const sessionID = (event.properties as { sessionID?: string } | undefined)
|
|
427
|
+
?.sessionID;
|
|
428
|
+
if (!sessionID) return;
|
|
429
|
+
|
|
430
|
+
const count = idleReentryCount.get(sessionID) ?? 0;
|
|
431
|
+
if (count >= MAX_CONSECUTIVE_IDLE_REENTRIES) return;
|
|
432
|
+
|
|
433
|
+
const pm = runCore(
|
|
434
|
+
"persistent-mode.ts",
|
|
435
|
+
{ cwd, sessionId: sessionID, hook_event_name: "Stop" },
|
|
436
|
+
cwd,
|
|
437
|
+
PERSISTENT_MODE_TIMEOUT_MS,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
if (pm?.decision !== "block") {
|
|
441
|
+
// Workflow complete / not active — clear the backstop counter.
|
|
442
|
+
idleReentryCount.delete(sessionID);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const reason =
|
|
447
|
+
typeof pm.reason === "string" && pm.reason
|
|
448
|
+
? pm.reason
|
|
449
|
+
: "The active OMA workflow is not complete. Continue executing it.";
|
|
450
|
+
idleReentryCount.set(sessionID, count + 1);
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
await client?.session?.prompt?.({
|
|
454
|
+
path: { id: sessionID },
|
|
455
|
+
body: { parts: [{ type: "text", text: reason }] },
|
|
456
|
+
});
|
|
457
|
+
} catch {
|
|
458
|
+
// Best-effort re-entry — never throw into opencode.
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}) satisfies (ctx: {
|
|
463
|
+
directory?: string;
|
|
464
|
+
client?: OmaOpencodeClient;
|
|
465
|
+
}) => Promise<{
|
|
466
|
+
"chat.message": (
|
|
467
|
+
input: { sessionID?: string },
|
|
468
|
+
output: {
|
|
469
|
+
message?: unknown;
|
|
470
|
+
parts?: Array<{ type?: string; text?: string }>;
|
|
471
|
+
},
|
|
472
|
+
) => Promise<void>;
|
|
473
|
+
"experimental.chat.system.transform": (
|
|
474
|
+
input: { sessionID?: string },
|
|
475
|
+
output: { system?: string[] },
|
|
476
|
+
) => Promise<void>;
|
|
477
|
+
"tool.execute.before": (
|
|
478
|
+
input: { tool?: string },
|
|
479
|
+
output: { args?: { command?: string } },
|
|
480
|
+
) => Promise<void>;
|
|
481
|
+
event: (input: {
|
|
482
|
+
event?: { type?: string; properties?: Record<string, unknown> };
|
|
483
|
+
}) => Promise<void>;
|
|
484
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-agent-opencode",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "20.38.3",
|
|
4
|
+
"description": "oh-my-agent hook bridge for OpenCode and Kilo Code",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://firstfluke.com/oh-my-agent",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/first-fluke/oh-my-agent"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"opencode",
|
|
14
|
+
"opencode-plugin",
|
|
15
|
+
"kilo",
|
|
16
|
+
"kilocode",
|
|
17
|
+
"oh-my-agent"
|
|
18
|
+
],
|
|
19
|
+
"main": "./index.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./index.ts"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"index.ts",
|
|
25
|
+
"oma.ts",
|
|
26
|
+
"keyword-detector.ts",
|
|
27
|
+
"skill-injector.ts",
|
|
28
|
+
"code-intelligence-primer.ts",
|
|
29
|
+
"serena-primer.ts",
|
|
30
|
+
"state-boundary.ts",
|
|
31
|
+
"scm-guard.ts",
|
|
32
|
+
"code-intelligence-guard.ts",
|
|
33
|
+
"test-filter.ts",
|
|
34
|
+
"refactor-guard.ts",
|
|
35
|
+
"persistent-mode.ts",
|
|
36
|
+
"filter-test-output.sh",
|
|
37
|
+
"README.md"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "persistent-mode.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 35000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "refactor-guard.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
package/scm-guard.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "scm-guard.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
package/serena-primer.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "serena-primer.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "skill-injector.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "state-boundary.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|
package/test-filter.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generated by OMA. Handler implementation lives in the CLI.
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
if (import.meta.main) {
|
|
6
|
+
try {
|
|
7
|
+
const result = spawnSync(process.env.OMA_BIN || "oma", [
|
|
8
|
+
"hook", "script", "--vendor", "opencode",
|
|
9
|
+
"--script", "test-filter.ts",
|
|
10
|
+
], {
|
|
11
|
+
input: readFileSync(0),
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
env: process.env,
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
timeout: 7000,
|
|
16
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
20
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
process.stderr.write("[oma hook] standalone dispatch unavailable: " + String(error) + "\n");
|
|
23
|
+
}
|
|
24
|
+
}
|