pantheon-opencode 1.2.1 → 1.3.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.
Files changed (37) hide show
  1. package/AGENTS.md +10 -0
  2. package/README.md +53 -15
  3. package/bin/pantheon-init.mjs +14 -0
  4. package/opencode.json +17 -1
  5. package/package.json +13 -4
  6. package/scripts/changelog-extract.mjs +112 -0
  7. package/scripts/doctor.mjs +303 -13
  8. package/scripts/hooks/audit-imports.sh +43 -0
  9. package/scripts/hooks/format-multi-language.sh +96 -0
  10. package/scripts/hooks/log-session-start.sh +42 -0
  11. package/scripts/hooks/on-subagent-delegation-start.sh +201 -0
  12. package/scripts/hooks/on-subagent-delegation-stop.sh +419 -0
  13. package/scripts/hooks/run-type-check.sh +21 -0
  14. package/scripts/hooks/scan-secrets.sh +97 -0
  15. package/scripts/hooks/validate-post-conditions.sh +21 -0
  16. package/scripts/hooks/validate-talos-scope.sh +50 -0
  17. package/scripts/hooks/validate-tool-safety.sh +46 -0
  18. package/scripts/install/opencode.mjs +58 -29
  19. package/scripts/install/plugin.mjs +4 -2
  20. package/scripts/install-mcp.mjs +104 -56
  21. package/scripts/mcp_persistence_server.py +1 -0
  22. package/scripts/prune.mjs +359 -0
  23. package/scripts/version-check.mjs +146 -0
  24. package/scripts/versioning.mjs +13 -14
  25. package/src/agents/zeus.md +33 -5
  26. package/src/mcp/requirements-mcp.txt +8 -5
  27. package/src/mcp/requirements-vision.txt +3 -3
  28. package/src/plugins/hook-runner.ts +186 -0
  29. package/src/plugins/pantheon-hooks.ts +1503 -0
  30. package/src/plugins/tui/dist/tui.d.ts +8 -0
  31. package/src/plugins/tui/dist/tui.d.ts.map +1 -0
  32. package/src/plugins/tui/dist/tui.js +1198 -0
  33. package/src/plugins/tui/dist/tui.js.map +1 -0
  34. package/src/plugins/tui/dist/tui.tsx +1204 -0
  35. package/src/plugins/tui/package.json +2 -2
  36. package/src/plugins/tui/src/index.tsx +16 -1
  37. package/src/mcp/requirements-mcp-core.txt +0 -5
@@ -0,0 +1,186 @@
1
+ /**
2
+ * hook-runner.ts — version-proof runner for the Pantheon security-hook layer.
3
+ *
4
+ * The previous plugin used the Bun Shell `$` injected by the opencode plugin
5
+ * API (`$.quiet().nothrow().timeout(...)`). `.timeout()` does not exist on the
6
+ * BunShell interface, so every hook crashed with a TypeError and the layer was
7
+ * removed (24-25/07). This runner uses ONLY `node:child_process` — stable
8
+ * across every Node >= 18 runtime and every opencode plugin loader version.
9
+ *
10
+ * Claude Code stdin protocol: the hook scripts receive
11
+ * `{tool_name, tool_input, agent_id, session_id}` as JSON on stdin. Scripts
12
+ * that read env vars instead (log-session-start.sh, on-subagent-delegation-*)
13
+ * get their fields mapped into the child env (SESSION_ID / AGENT_NAME /
14
+ * TASK_DESC) so both protocols work end to end.
15
+ *
16
+ * IMPORTANT (verified empirically): the `input:` option on the async
17
+ * `execFile()` API is silently ignored by Node — it only exists on
18
+ * spawnSync/execFileSync. This runner therefore writes stdin explicitly to a
19
+ * spawned child, which delivers the payload reliably.
20
+ *
21
+ * Hooks NEVER throw: every failure (missing script, timeout, non-zero exit)
22
+ * is returned as a structured HookResult so the opencode session can never be
23
+ * taken down by the security layer.
24
+ */
25
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
26
+ import path from 'node:path'
27
+ import type { Readable } from 'node:stream'
28
+ import { fileURLToPath } from 'node:url'
29
+
30
+ export const HOOK_TIMEOUT_MS = 15_000
31
+ export const HOOK_MAX_BUFFER = 10 * 1024 * 1024
32
+
33
+ /** Claude Code hook protocol payload. */
34
+ export type HookPayload = {
35
+ tool_name?: string
36
+ tool_input?: unknown
37
+ agent_id?: string
38
+ session_id?: string
39
+ }
40
+
41
+ export type HookResult = {
42
+ code: number
43
+ stdout: string
44
+ stderr: string
45
+ signal: NodeJS.Signals | null
46
+ timedOut: boolean
47
+ }
48
+
49
+ export type RunHookOptions = {
50
+ env?: Record<string, string | undefined>
51
+ cwd?: string
52
+ timeout?: number
53
+ }
54
+
55
+ /**
56
+ * Resolve the hooks directory relative to this module — robust to any npm
57
+ * global-install prefix because it anchors on import.meta.url, not __dirname
58
+ * or process.cwd().
59
+ */
60
+ export function resolveHooksDir(): string {
61
+ return fileURLToPath(new URL('../../scripts/hooks/', import.meta.url))
62
+ }
63
+
64
+ /**
65
+ * Map the Claude Code protocol payload into the child environment. Scripts
66
+ * that read env vars (log-session-start.sh, delegation audit scripts) rely on
67
+ * these; scripts that read stdin receive the full JSON via `input` in runHook.
68
+ * PATH is always preserved so `python3`/`bash` resolution inside the scripts
69
+ * is never affected by a stripped-down environment.
70
+ */
71
+ export function hookEnv(
72
+ payload: HookPayload,
73
+ extra?: Record<string, string | undefined>,
74
+ ): NodeJS.ProcessEnv {
75
+ return {
76
+ ...process.env,
77
+ PATH: process.env.PATH ?? '',
78
+ SESSION_ID: payload.session_id ?? '',
79
+ AGENT_NAME: payload.agent_id ?? '',
80
+ TASK_DESC: payload.tool_name ?? '',
81
+ ...(extra ?? {}),
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Run one hook script with the Claude Code protocol payload on stdin.
87
+ *
88
+ * @param script file name inside scripts/hooks/ (e.g. "validate-tool-safety.sh")
89
+ * @param payload protocol payload translated from the opencode hook event
90
+ * @param opts optional env overrides / cwd / timeout
91
+ * @returns HookResult — NEVER throws; resolve with a non-zero code instead
92
+ */
93
+ export function runHook(
94
+ script: string,
95
+ payload: HookPayload,
96
+ opts: RunHookOptions = {},
97
+ ): Promise<HookResult> {
98
+ const scriptPath = path.join(opts.cwd ?? resolveHooksDir(), script)
99
+ const timeout = opts.timeout ?? HOOK_TIMEOUT_MS
100
+
101
+ return new Promise<HookResult>((resolve) => {
102
+ const stdoutBuf: { value: string } = { value: '' }
103
+ const stderrBuf: { value: string } = { value: '' }
104
+ let settled = false
105
+
106
+ const settle = (result: HookResult) => {
107
+ if (!settled) {
108
+ settled = true
109
+ resolve(result)
110
+ }
111
+ }
112
+
113
+ const spawnOpts = {
114
+ timeout,
115
+ killSignal: 'SIGKILL' as const,
116
+ env: hookEnv(payload, opts.env),
117
+ // stdio:'pipe' is equivalent to ['pipe','pipe','pipe'] — it guarantees
118
+ // the child's stdin is OUR pipe, NEVER the opencode TUI's terminal
119
+ // (which would leave `cat`-style hooks blocked reading forever and hit
120
+ // the opencode core hook timeout).
121
+ stdio: 'pipe' as const,
122
+ windowsHide: true,
123
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
124
+ }
125
+
126
+ // stdio:'pipe' picks the ChildProcessWithoutNullStreams overload — do not
127
+ // widen these option literals or child.stdin/stdout become nullable.
128
+ let child: ChildProcessWithoutNullStreams
129
+ try {
130
+ child = spawn(scriptPath, [], spawnOpts)
131
+ } catch (err) {
132
+ // Synchronous spawn failure (invalid args etc.) — never propagate.
133
+ settle({
134
+ code: 1,
135
+ stdout: '',
136
+ stderr: `spawn failed: ${err instanceof Error ? err.message : String(err)}`,
137
+ signal: null,
138
+ timedOut: false,
139
+ })
140
+ return
141
+ }
142
+
143
+ // The child may exit before we write stdin — swallow EPIPE instead of
144
+ // crashing the plugin process with an unhandled stream error.
145
+ child.stdin.on('error', () => {})
146
+
147
+ const collect = (stream: Readable, buf: { value: string }) => {
148
+ stream.setEncoding('utf-8')
149
+ stream.on('data', (chunk: string) => {
150
+ if (buf.value.length < HOOK_MAX_BUFFER) {
151
+ buf.value += chunk.slice(0, HOOK_MAX_BUFFER - buf.value.length)
152
+ }
153
+ })
154
+ }
155
+ collect(child.stdout, stdoutBuf)
156
+ collect(child.stderr, stderrBuf)
157
+
158
+ child.on('error', (err: NodeJS.ErrnoException) => {
159
+ // ENOENT / EACCES — script missing or not executable. Report, never throw.
160
+ settle({
161
+ code: 1,
162
+ stdout: stdoutBuf.value,
163
+ stderr: stderrBuf.value || `hook script failed to spawn: ${err.message}`,
164
+ signal: null,
165
+ timedOut: false,
166
+ })
167
+ })
168
+
169
+ child.on('close', (code, signal) => {
170
+ settle({
171
+ code: code ?? 1,
172
+ stdout: stdoutBuf.value,
173
+ stderr: stderrBuf.value,
174
+ signal,
175
+ timedOut: code === null && signal === 'SIGKILL',
176
+ })
177
+ })
178
+
179
+ // Deliver the Claude Code protocol JSON on stdin. stdin.end() runs
180
+ // UNCONDITIONALLY — even for an empty payload {} or a script that never
181
+ // reads stdin — so the child can never block waiting for input and hit
182
+ // the runtime hook timeout (reported as "Hook X timed out after 30000ms").
183
+ child.stdin.write(JSON.stringify(payload))
184
+ child.stdin.end()
185
+ })
186
+ }