oh-my-opencode 4.5.12 → 4.6.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 (147) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +194 -0
  2. package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
  3. package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
  4. package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
  5. package/.agents/skills/opencode-qa/references/sdk.md +96 -0
  6. package/.agents/skills/opencode-qa/references/server-api.md +200 -0
  7. package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
  8. package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
  9. package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
  10. package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
  11. package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
  12. package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
  13. package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
  14. package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
  15. package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
  16. package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
  17. package/README.ja.md +13 -3
  18. package/README.ko.md +13 -3
  19. package/README.md +24 -14
  20. package/README.ru.md +13 -3
  21. package/README.zh-cn.md +13 -3
  22. package/bin/oh-my-opencode.js +4 -3
  23. package/bin/oh-my-opencode.test.ts +35 -7
  24. package/bin/platform.d.ts +1 -1
  25. package/bin/platform.js +4 -4
  26. package/bin/platform.test.ts +31 -9
  27. package/dist/cli/cleanup-command.d.ts +4 -0
  28. package/dist/cli/cleanup.d.ts +11 -0
  29. package/dist/cli/cli-program.d.ts +2 -1
  30. package/dist/cli/index.js +1837 -450
  31. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  32. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  33. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  34. package/dist/cli/install-codex/codex-config-mcp.d.ts +1 -0
  35. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-config-reasoning.d.ts +1 -0
  37. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  38. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  39. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  40. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  41. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  42. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  43. package/dist/cli/install-codex/index.d.ts +4 -0
  44. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  45. package/dist/cli/install-codex/types.d.ts +20 -0
  46. package/dist/cli/run/event-state.d.ts +1 -0
  47. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  48. package/dist/cli/run/prompt-start.d.ts +7 -0
  49. package/dist/cli/star-request.d.ts +9 -0
  50. package/dist/config/schema/hooks.d.ts +0 -1
  51. package/dist/create-hooks.d.ts +0 -1
  52. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  53. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  54. package/dist/hooks/index.d.ts +0 -1
  55. package/dist/index.js +267 -114
  56. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  57. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  58. package/dist/plugin/messages-transform.d.ts +8 -1
  59. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  60. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  61. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  62. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  63. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  64. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  65. package/dist/shared/prompt-async-gate.d.ts +1 -1
  66. package/package.json +22 -17
  67. package/packages/git-bash-mcp/dist/cli.js +367 -0
  68. package/packages/omo-codex/plugin/.mcp.json +11 -0
  69. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  70. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  71. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  72. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  73. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  74. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  75. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  76. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  77. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  78. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  79. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  80. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  81. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  82. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  83. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  84. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  85. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  86. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  87. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  88. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  89. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +5 -4
  90. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  91. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -2
  92. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +1 -0
  93. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  94. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -1
  95. package/packages/omo-codex/plugin/components/ultrawork/directive.md +31 -5
  96. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  97. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  99. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +27 -205
  100. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +230 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  102. package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
  103. package/packages/omo-codex/plugin/package-lock.json +19 -0
  104. package/packages/omo-codex/plugin/package.json +3 -1
  105. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  106. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  107. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +87 -0
  108. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +27 -2
  109. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +20 -0
  110. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +27 -205
  111. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +230 -0
  112. package/packages/omo-codex/plugin/test/aggregate.test.mjs +23 -8
  113. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +56 -11
  114. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  115. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  116. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  117. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  118. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +66 -0
  119. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +32 -2
  120. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  121. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  122. package/packages/omo-codex/scripts/install/config.mjs +36 -1
  123. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  124. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  125. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  126. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  127. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  128. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  129. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  130. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  131. package/packages/omo-codex/scripts/install/reasoning-config.mjs +14 -0
  132. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  133. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  134. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  135. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  136. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +62 -0
  137. package/packages/omo-codex/scripts/install-config.test.mjs +206 -0
  138. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +129 -0
  139. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  140. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  141. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  142. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  143. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  144. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  145. package/packages/shared-skills/skills/review-work/SKILL.md +27 -2
  146. package/packages/shared-skills/skills/start-work/SKILL.md +20 -0
  147. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -0,0 +1,96 @@
1
+ # opencode SDK (@opencode-ai/sdk) - reference only
2
+
3
+ A TypeScript/Bun way to drive opencode for QA. Prefer the tested CLI/curl scripts for portability; reach for the SDK when you want typed access from a Bun script.
4
+
5
+ > IMPORTANT: method signatures differ between SDK versions and between the published docs and the generated client. ALWAYS check the installed version's types (node_modules/@opencode-ai/sdk) before relying on a signature, and verify against `GET /doc` (the OpenAPI spec the SDK is generated from).
6
+
7
+ ## Entry points and exports
8
+
9
+ Package `@opencode-ai/sdk` subpath exports:
10
+
11
+ - `.` (src/index.ts)
12
+ - `./client`
13
+ - `./server`
14
+ - `./v2` (src/v2/index.ts)
15
+ - `./v2/client`
16
+ - `./v2/server`
17
+ - `./v2/gen/client`
18
+
19
+ Root and v2 entries export `createOpencode()`, `createOpencodeClient(...)`, `createOpencodeServer(...)`.
20
+
21
+ `createOpencodeServer()` spawns `opencode serve ...` and waits for the startup line. `createOpencodeClient({ baseUrl })` wraps the generated client, rewrites directory/workspace headers, installs error interception.
22
+
23
+ ## Two ways to connect
24
+
25
+ ```ts
26
+ import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk/v2"
27
+
28
+ // A) embedded server (spawns opencode serve)
29
+ const server = await createOpencodeServer()
30
+ const client = createOpencodeClient({ baseUrl: server.url })
31
+ // ... use client ...
32
+ server.close()
33
+
34
+ // B) connect to an already-running server
35
+ const client2 = createOpencodeClient({ baseUrl: "http://127.0.0.1:4096" })
36
+ ```
37
+
38
+ ## Client namespaces
39
+
40
+ Top-level on OpencodeClient:
41
+
42
+ auth, app, global, event, config, experimental, tool, worktree, find, file, instance, path, vcs, command, lsp, formatter, mcp, project, pty, question, permission, provider, session, part, sync, v2, tui.
43
+
44
+ ## Useful methods (shapes vary by version)
45
+
46
+ - `client.global.health()`, `client.global.event()`
47
+ - `client.app.log(...)`, `client.app.agents(...)`, `client.app.skills(...)`
48
+ - `client.config.get()`, `client.config.providers()`
49
+ - `client.event.subscribe()` - SSE on /event; iterate `for await (const event of events.stream) { event.type, event.properties }`
50
+ - `client.session` (legacy surface): list, create, status, get, update, delete, children, todo, diff, messages, message, deleteMessage, prompt, promptAsync, command, shell, fork, abort, init, share, unshare, summarize, revert, unrevert
51
+ - `client.v2.session` (newer read/stream surface): list, prompt, compact, wait, context, messages
52
+ - `client.part.delete(...)`, `client.part.update(...)`
53
+
54
+ ## Minimal QA snippet
55
+
56
+ Arg shapes may differ by version. Treat this as a starting point, not a contract.
57
+
58
+ ```ts
59
+ import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk/v2"
60
+
61
+ const server = await createOpencodeServer()
62
+ const client = createOpencodeClient({ baseUrl: server.url })
63
+
64
+ try {
65
+ const session = await client.session.create({ title: "QA session" })
66
+ await client.session.promptAsync({
67
+ sessionID: session.id,
68
+ parts: [{ type: "text", text: "Say hello in one line." }],
69
+ })
70
+
71
+ const sessions = await client.session.list({ limit: 10 })
72
+ console.log(sessions[0]?.title)
73
+
74
+ const messages = await client.v2.session.messages({
75
+ sessionID: session.id,
76
+ limit: 20,
77
+ })
78
+ console.log(messages.items.length)
79
+ } finally {
80
+ server.close()
81
+ }
82
+ ```
83
+
84
+ ## Key types
85
+
86
+ - Legacy Session: id, slug, projectID, directory, title, version, time.created/updated, optional workspaceID, path, parentID, summary, cost, tokens, share, agent, model, metadata, permission, revert.
87
+ - Message = UserMessage | AssistantMessage (role "user" | "assistant"; assistant adds time.completed?, modelID, providerID, agent, tokens, finish?, error?).
88
+ - Part union: TextPart, ReasoningPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart, AgentPart, RetryPart, CompactionPart, SubtaskPart.
89
+
90
+ ## How it is generated
91
+
92
+ `packages/sdk/js/script/build.ts` runs `bun dev generate > openapi.json` from the opencode repo, feeds it to `@hey-api/openapi-ts.createClient`, writes output to `packages/sdk/js/src/v2/gen`, patches an SSE generic, prettifies and typechecks. Regenerate with `./packages/sdk/js/script/build.ts`.
93
+
94
+ ---
95
+
96
+ For version-stable QA, prefer the curl/CLI scripts in this skill; cross-check any SDK call against `GET /doc`.
@@ -0,0 +1,200 @@
1
+ # opencode HTTP server API for QA (Case B)
2
+
3
+ ## Table of Contents
4
+
5
+ - [Start a server](#start-a-server)
6
+ - [Authentication](#authentication)
7
+ - [Per-request workspace routing](#per-request-workspace-routing)
8
+ - [Introspect the API](#introspect-the-api)
9
+ - [Tested smoke calls](#tested-smoke-calls)
10
+ - [Route catalog](#route-catalog)
11
+ - [Triggering a prompt over HTTP](#triggering-a-prompt-over-http)
12
+
13
+ ## Start a server
14
+
15
+ Run the server with a fixed port and host:
16
+
17
+ ```bash
18
+ opencode serve --port 4096 --hostname 127.0.0.1
19
+ ```
20
+
21
+ Output:
22
+
23
+ ```
24
+ opencode server listening on http://127.0.0.1:4096
25
+ ```
26
+
27
+ Port 0 means the server will pick 4096, then fall back to a free port if that one is taken.
28
+
29
+ A bundled isolated smoke test is available at `scripts/server-smoke.sh`. It spawns an isolated server, checks `/global/health`, checks that `/doc` returns at least 100 paths, and confirms that no-auth requests get 401, then tears the server down.
30
+
31
+ ## Authentication
32
+
33
+ Set the environment variable `OPENCODE_SERVER_PASSWORD` to require authentication. If it is unset, the server runs UNSECURED and prints a warning.
34
+
35
+ The username defaults to `opencode`. Override it with `OPENCODE_SERVER_USERNAME`.
36
+
37
+ Two ways to authenticate:
38
+
39
+ 1. HTTP Basic Auth: `-u opencode:$PASS`
40
+ 2. Query parameter: `?auth_token=<base64(user:pass)>`
41
+
42
+ Unauthenticated requests to protected routes return HTTP 401. This was verified.
43
+
44
+ ## Per-request workspace routing
45
+
46
+ Most instance routes need the target project directory. Pass it as either:
47
+
48
+ - Query parameter: `?directory=$PWD`
49
+ - Header: `x-opencode-directory: $PWD`
50
+
51
+ Aliases also work: `x-opencode-workspace` header or `?workspace=` query parameter.
52
+
53
+ The server resolves an instance per request, so a single `serve` process can handle many projects.
54
+
55
+ ## Introspect the API
56
+
57
+ The `/doc` endpoint returns the full OpenAPI spec. To list all documented paths:
58
+
59
+ ```bash
60
+ curl -s -u opencode:$PASS http://127.0.0.1:4096/doc | jq '.paths | keys'
61
+ ```
62
+
63
+ On v1.15.13 this returned 113 paths. This is the source of truth for exact request and response schemas.
64
+
65
+ ## Tested smoke calls
66
+
67
+ ```bash
68
+ curl -s -u opencode:$PASS http://127.0.0.1:4096/global/health | jq .
69
+ # {"healthy":true,"version":"1.15.13"}
70
+
71
+ curl -s -u opencode:$PASS http://127.0.0.1:4096/doc | jq '.paths|length'
72
+ # 113
73
+
74
+ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4096/session?directory=$PWD
75
+ # 401 (no credentials)
76
+
77
+ curl -s -u opencode:$PASS "http://127.0.0.1:4096/session?directory=$PWD" | jq 'length'
78
+ ```
79
+
80
+ ## Route catalog
81
+
82
+ This mirrors the structure returned by `/doc`. Each entry is grouped as `method path - purpose`.
83
+
84
+ ### Global
85
+
86
+ - `GET /global/health` - health check
87
+ - `GET /global/event` - server-wide SSE event stream
88
+ - `GET /global/config` - read global configuration
89
+ - `PATCH /global/config` - update global configuration
90
+ - `POST /global/dispose` - dispose the server instance
91
+ - `GET /doc` - OpenAPI specification
92
+
93
+ ### Session
94
+
95
+ - `GET /session` - list sessions
96
+ - `GET /session/status` - session status overview
97
+ - `GET /session/:id` - get session by ID
98
+ - `GET /session/:id/children` - list child sessions
99
+ - `GET /session/:id/todo` - get session todo items
100
+ - `GET /session/:id/diff` - get session diff
101
+ - `GET /session/:id/message` - list session messages
102
+ - `GET /session/:id/message/:messageID` - get a specific message
103
+ - `POST /session` - create a new session
104
+ - `DELETE /session/:id` - delete a session
105
+ - `PATCH /session/:id` - update a session
106
+ - `POST /session/:id/fork` - fork a session
107
+ - `POST /session/:id/abort` - abort a session
108
+ - `POST /session/:id/init` - initialize a session
109
+ - `POST /session/:id/share` - share a session
110
+ - `POST /session/:id/summarize` - summarize a session
111
+ - `POST /session/:id/revert` - revert a session
112
+ - `POST /session/:id/unrevert` - unrevert a session
113
+ - `DELETE /session/:id/share` - unshare a session
114
+
115
+ ### Prompting
116
+
117
+ - `POST /session/:id/message` - send a prompt; streams JSON
118
+ - `POST /session/:id/prompt_async` - fire-and-forget prompt; returns 204
119
+ - `POST /session/:id/command` - execute a command in a session
120
+ - `POST /session/:id/shell` - run a shell command in a session
121
+
122
+ ### Files and find
123
+
124
+ - `GET /find` - text search via ripgrep
125
+ - `GET /find/file` - file search
126
+ - `GET /find/symbol` - symbol search
127
+ - `GET /file` - file metadata
128
+ - `GET /file/content` - file contents
129
+ - `GET /file/status` - file status
130
+
131
+ ### Instance and app
132
+
133
+ - `GET /path` - path resolution
134
+ - `GET /vcs` - version control info
135
+ - `GET /vcs/status` - VCS status
136
+ - `GET /vcs/diff` - VCS diff
137
+ - `GET /command` - available commands
138
+ - `GET /agent` - available agents
139
+ - `GET /skill` - available skills
140
+ - `GET /lsp` - LSP info
141
+ - `GET /formatter` - formatter info
142
+
143
+ ### Permission and question
144
+
145
+ - `GET /permission` - list pending permission requests
146
+ - `POST /permission/:requestID/reply` - reply to a permission request
147
+ - `GET /question` - list pending questions
148
+ - `POST /question/:requestID/reply` - reply to a question
149
+ - `POST /question/:requestID/reject` - reject a question
150
+
151
+ ### TUI control
152
+
153
+ These endpoints drive a running TUI over HTTP.
154
+
155
+ - `POST /tui/append-prompt` - append text to the TUI prompt
156
+ - `POST /tui/submit-prompt` - submit the current TUI prompt
157
+ - `POST /tui/execute-command` - execute a TUI command
158
+ - `POST /tui/show-toast` - show a toast in the TUI
159
+ - `GET /tui/control/next` - get the next TUI control event
160
+ - `POST /tui/control/response` - respond to a TUI control event
161
+
162
+ ### PTY
163
+
164
+ - `GET /pty` - list PTY sessions
165
+ - `POST /pty` - create a PTY session
166
+ - `GET /pty/:id` - get PTY session info
167
+ - `DELETE /pty/:id` - delete a PTY session
168
+ - `POST /pty/:id/connect-token` - generate a PTY connect token
169
+ - `GET /pty/:id/connect` - WebSocket connection to the PTY
170
+
171
+ ### Event
172
+
173
+ - `GET /event` - instance-level SSE event stream
174
+
175
+ ### V2 API
176
+
177
+ - `GET /api/session` - list sessions (v2)
178
+ - `POST /api/session/:id/prompt` - prompt a session (v2)
179
+ - `POST /api/session/:id/compact` - compact a session (v2)
180
+ - `POST /api/session/:id/wait` - wait for a session (v2)
181
+ - `GET /api/session/:id/context` - get session context (v2)
182
+ - `GET /api/session/:id/message` - get session messages (v2)
183
+ - `GET /api/model` - list models (v2)
184
+ - `GET /api/provider` - list providers (v2)
185
+
186
+ ## Triggering a prompt over HTTP (for hook and event QA)
187
+
188
+ Use `prompt_async` so the event stream is not blocked.
189
+
190
+ ```bash
191
+ curl -X POST -u opencode:$PASS -H 'Content-Type: application/json' \
192
+ -d '{"parts":[{"type":"text","text":"hello"}]}' \
193
+ "http://127.0.0.1:4096/session/<ses_id>/prompt_async?directory=$PWD"
194
+ ```
195
+
196
+ This returns HTTP 204. Watching events is covered in `references/events-hooks.md`.
197
+
198
+ ---
199
+
200
+ Schemas are authoritative in `GET /doc`; for the event stream see `references/events-hooks.md`.
@@ -0,0 +1,218 @@
1
+ # opencode Test Harness (how opencode QAs itself)
2
+
3
+ This is reference material for writing and running tests against the opencode source. The skill's own QA scripts (CLI, curl, sqlite) do not require this, but it is the authoritative pattern when you need a unit or integration test.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Runner and the root guard](#runner-and-the-root-guard)
8
+ 2. [Test bootstrap (in-memory, isolated)](#test-bootstrap-in-memory-isolated)
9
+ 3. [Effect-based harness (test/lib/effect.ts)](#effect-based-harness-testlibeffectts)
10
+ 4. [Instance and tmpdir fixtures (test/fixture/fixture.ts)](#instance-and-tmpdir-fixtures-testfixturefixturets)
11
+ 5. [CLI subprocess harness (test/lib/cli-process.ts)](#cli-subprocess-harness-testlibcli-processts)
12
+ 6. [Fake LLM server (test/lib/llm-server.ts)](#fake-llm-server-testlibllm-serverts)
13
+ 7. [Representative test shapes](#representative-test-shapes)
14
+ 8. [App e2e (Playwright)](#app-e2e-playwright)
15
+ 9. [Test style conventions](#test-style-conventions)
16
+
17
+ ## Runner and the root guard
18
+
19
+ The runner is `bun test` (Bun built-in, not vitest or jest).
20
+
21
+ Tests cannot run from the repo root. Two guards enforce this:
22
+
23
+ - `bunfig.toml` at repo root sets `root = "./do-not-run-tests-from-root"`
24
+ - Root `package.json` has `"test": "echo 'do not run tests from root' && exit 1"`
25
+
26
+ Run from a package directory instead:
27
+
28
+ ```bash
29
+ cd packages/opencode && bun test --timeout 30000
30
+ ```
31
+
32
+ Run a single file:
33
+
34
+ ```bash
35
+ bun test test/tool/read.test.ts
36
+ ```
37
+
38
+ Filter by test name:
39
+
40
+ ```bash
41
+ bun test --grep "truncates large file"
42
+ ```
43
+
44
+ CI variant:
45
+
46
+ ```bash
47
+ bun run test:ci
48
+ ```
49
+
50
+ Turbo dependency: `opencode#test` depends on `^build`.
51
+
52
+ ## Test bootstrap (in-memory, isolated)
53
+
54
+ The preload file is `packages/opencode/test/preload.ts`. It is wired via `packages/opencode/bunfig.toml`:
55
+
56
+ ```toml
57
+ [test]
58
+ preload = ["@opentui/solid/preload", "./test/preload.ts"]
59
+ ```
60
+
61
+ What it does:
62
+
63
+ - Sets `XDG_DATA_HOME`, `XDG_CACHE_HOME`, `XDG_CONFIG_HOME`, and `XDG_STATE_HOME` to temp directories
64
+ - Sets `OPENCODE_TEST_HOME`
65
+ - Sets `OPENCODE_DB=":memory:"` (SQLite in-memory)
66
+ - Wipes all provider API keys from `process.env`
67
+ - Sets `OPENCODE_EXPERIMENTAL_EVENT_SYSTEM=true`
68
+ - Sets `OPENCODE_EXPERIMENTAL_WORKSPACES=true`
69
+ - Initializes `Log.init({ print: false })`
70
+ - Calls `initProjectors()`
71
+
72
+ ## Effect-based harness (test/lib/effect.ts)
73
+
74
+ The `it` factory wraps `bun:test` with three variants:
75
+
76
+ - `it.effect(name, body)` ... TestClock + TestConsole (isolated time)
77
+ - `it.live(name, body)` ... real clock + TestConsole
78
+ - `it.instance(name, body, opts)` ... real clock + scoped tmpdir + a real Instance context
79
+
80
+ `testEffect(layer)` builds an `it` bound to an Effect layer:
81
+
82
+ ```typescript
83
+ const it = testEffect(Layer.mergeAll(readLayer(), testInstanceStoreLayer))
84
+ ```
85
+
86
+ ## Instance and tmpdir fixtures (test/fixture/fixture.ts)
87
+
88
+ - `tmpdirScoped(options?)` ... scoped temp directory. Optional `git: true`, optional `config` (writes `opencode.json`), optional `init`.
89
+ - `provideInstance(directory)(effect)` ... runs an Effect inside a real instance for that directory.
90
+ - `withTmpdirInstance({ git?, config?, init? })(effect)` ... one-liner: make tmpdir, optional git init + config, provide instance.
91
+ - `testInstanceStoreLayer` ... instance store with a no-op bootstrap.
92
+
93
+ ## CLI subprocess harness (test/lib/cli-process.ts)
94
+
95
+ `cliIt.live(name, body, timeoutMs?)` and `cliIt.concurrent(...)` spawn the real CLI (`bun run --conditions=browser src/index.ts`) in an isolated environment.
96
+
97
+ Exposed helpers:
98
+
99
+ - `opencode.run()`
100
+ - `opencode.serve()`
101
+ - `opencode.acp()`
102
+ - `expectExit`
103
+ - `parseJsonEvents`
104
+
105
+ Isolation environment keys:
106
+
107
+ - `OPENCODE_TEST_HOME`
108
+ - `OPENCODE_CONFIG_CONTENT` (inline provider config)
109
+ - `OPENCODE_DISABLE_PROJECT_CONFIG=1`
110
+ - `OPENCODE_PURE=1`
111
+ - `OPENCODE_DISABLE_AUTOUPDATE=1`
112
+ - `OPENCODE_DISABLE_AUTOCOMPACT=1`
113
+ - `OPENCODE_DISABLE_MODELS_FETCH=1`
114
+
115
+ Real example from `packages/opencode/test/cli/serve/serve-process.test.ts`:
116
+
117
+ ```typescript
118
+ cliIt.live("spawns serve and health responds", async ({ opencode, expectExit }) => {
119
+ const server = await opencode.serve()
120
+ expect(server.port).toBeGreaterThan(0)
121
+ const res = await fetch(`${server.url}/global/health`)
122
+ expect(res.status).toBe(200)
123
+ })
124
+ ```
125
+
126
+ ## Fake LLM server (test/lib/llm-server.ts)
127
+
128
+ `TestLLMServer` is an in-process OpenAI-compatible SSE server to mock model responses deterministically.
129
+
130
+ Methods:
131
+
132
+ - `llm.text("hello")`
133
+ - `llm.tool("read", { filePath: "x" })`
134
+ - `llm.pushMatch(matchFn, reply)`
135
+
136
+ This is how tests avoid real provider calls.
137
+
138
+ ## Representative test shapes
139
+
140
+ ### 1. Tool test
141
+
142
+ From `packages/opencode/test/tool/read.test.ts`:
143
+
144
+ ```typescript
145
+ const it = testEffect(Layer.mergeAll(readLayer(), testInstanceStoreLayer))
146
+
147
+ it.instance("truncates large file over maxReadFileSize", () =>
148
+ Effect.gen(function* () {
149
+ const test = yield* TestInstance
150
+ // ... exercise the read tool, assert truncation
151
+ })
152
+ )
153
+ ```
154
+
155
+ ### 2. Session/event test
156
+
157
+ From `packages/opencode/test/session/session.test.ts`:
158
+
159
+ ```typescript
160
+ test("session.created fires after session.create", async () => {
161
+ const deferred = Deferred.unsafeMake<void>(FiberId.none)
162
+ // ... listen for session.created event
163
+ await session.create({})
164
+ // ... assert deferred resolves
165
+ })
166
+ ```
167
+
168
+ ### 3. Plain unit test
169
+
170
+ From `packages/opencode/test/cli/run/runtime.boot.test.ts`:
171
+
172
+ ```typescript
173
+ import { describe, expect, mock, spyOn, test } from "bun:test"
174
+
175
+ test("boots runtime without errors", () => {
176
+ // ... standard assertions, no Effect
177
+ })
178
+ ```
179
+
180
+ ## App e2e (Playwright)
181
+
182
+ The app lives in `packages/app` (SolidJS). Config is `packages/app/playwright.config.ts`. It starts the Vite dev server via `webServer`; the backend is expected at `localhost:4096`.
183
+
184
+ Commands (run from `packages/app`):
185
+
186
+ ```bash
187
+ bunx playwright install chromium
188
+ bun run test:e2e:local
189
+ ```
190
+
191
+ Filter with grep:
192
+
193
+ ```bash
194
+ bun run test:e2e:local -- --grep "settings"
195
+ ```
196
+
197
+ UI mode:
198
+
199
+ ```bash
200
+ bun run test:e2e:ui
201
+ ```
202
+
203
+ App unit tests:
204
+
205
+ ```bash
206
+ bun run test:unit
207
+ ```
208
+
209
+ This equals `bun test --preload ./happydom.ts ./src`.
210
+
211
+ ## Test style conventions
212
+
213
+ Per opencode AGENTS.md:
214
+
215
+ - Avoid mocks where possible. Test the real implementation. Do not duplicate logic into tests.
216
+ - Run `bun typecheck` from the package directory (uses tsgo). Never run bare `tsc`.
217
+
218
+ For runtime or scriptable QA without writing tests, use the opencode-qa scripts (Cases A-D in SKILL.md).
@@ -0,0 +1,52 @@
1
+ # QAing the opencode TUI under tmux (Case C)
2
+
3
+ ## Verdict first (be honest)
4
+
5
+ - tmux CAN launch the opencode TUI and `capture-pane` reads the rendered frame; `send-keys` delivers keystrokes to the composer. This is proven and good for SMOKE checks: did it boot, does it render, does it accept input.
6
+ - The TUI is a 60fps full-screen app (built on @opentui/solid) with a custom renderer, animations, and a worker thread. Asserting on conversation OUTPUT by scraping the frame is FRAGILE and not recommended.
7
+ - For real behavior assertions prefer: `opencode run` (Case A, references/cli-commands.md), the server API + SSE (Case B, references/server-api.md + events-hooks.md), or the TUI control HTTP API (below). The TUI talks to the same server, so API-level QA is equivalent to driving the screen.
8
+
9
+ ## Safety: isolate so QA never pollutes the real DB
10
+
11
+ Launching the real TUI would create sessions in the real ~/.local/share/opencode DB. Run it under an isolated XDG sandbox. The bundled `scripts/tui-smoke.sh` does exactly this and verifies the real session count is unchanged before/after.
12
+
13
+ ## Smoke test (bundled)
14
+
15
+ - `scripts/tui-smoke.sh --self-test` launches the TUI under tmux in an isolated sandbox, polls capture-pane for a render marker (version string / "Ask anything" / footer), sends a sentinel keystroke, then kills the tmux session and confirms the real DB is untouched.
16
+
17
+ ## Manual tmux recipe (fenced) - for ad hoc smoke
18
+
19
+ ```
20
+ SESS=oqa_tui_demo
21
+ DIR=$(mktemp -d)
22
+ tmux new-session -d -s "$SESS" -x 200 -y 50
23
+ # isolate XDG so no real session is written
24
+ tmux send-keys -t "$SESS" "XDG_DATA_HOME=$DIR/data XDG_CONFIG_HOME=$DIR/cfg XDG_STATE_HOME=$DIR/state XDG_CACHE_HOME=$DIR/cache OPENCODE_DISABLE_AUTOUPDATE=1 OPENCODE_DISABLE_MODELS_FETCH=1 opencode $DIR" Enter
25
+ sleep 7
26
+ tmux capture-pane -t "$SESS" -p | sed -n '1,30p' # inspect the rendered frame
27
+ tmux send-keys -t "$SESS" "hello" # type into the composer
28
+ sleep 1
29
+ tmux capture-pane -t "$SESS" -p | sed -n '1,30p'
30
+ tmux kill-session -t "$SESS" # teardown (kills the TUI)
31
+ rm -rf "$DIR"
32
+ ```
33
+
34
+ Explain: capture-pane -p prints the visible frame; send-keys injects input; kill-session tears down the process tree. Always teardown and remove the temp dir.
35
+
36
+ ## The reliable alternative: TUI control HTTP API
37
+
38
+ A running TUI is a client of the local server, so you can drive it over HTTP without scraping the screen:
39
+
40
+ - POST /tui/append-prompt - append text to the composer
41
+ - POST /tui/submit-prompt - submit the composer
42
+ - POST /tui/execute-command - run a TUI command
43
+ - POST /tui/show-toast - show a toast
44
+ - GET /tui/control/next + POST /tui/control/response - the control channel
45
+
46
+ Use these (with auth + ?directory=) to deterministically drive a TUI you launched, then assert via the event stream (references/events-hooks.md).
47
+
48
+ ## Headless component testing (for source-level TUI tests)
49
+
50
+ opencode unit-tests TUI components headlessly with @opentui/core/testing `createTestRenderer` (see packages/opencode/test/cli/tui/, e.g. app-lifecycle.test.ts). This is the route for asserting TUI component behavior in the source repo; see references/testing-harness.md.
51
+
52
+ Bottom line: tmux for smoke, server/SSE or /tui/* control for assertions, createTestRenderer for source unit tests.
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # db-session-by-id.sh - investigate an opencode session by its id (ses_...).
3
+ # Read-only against the LIVE opencode DB via `opencode db ... --format json`.
4
+ #
5
+ # Usage:
6
+ # db-session-by-id.sh ses_3a4ee6335ffedFB8f76BPU1Eb3
7
+ # db-session-by-id.sh --self-test
8
+ #
9
+ # Output: a JSON array with one row (id, slug, title, directory, agent, model,
10
+ # cost, token counts, human-readable created/updated times) or [] if not found.
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
13
+ . "$SCRIPT_DIR/lib/common.sh"
14
+
15
+ oqa_session_by_id() {
16
+ local id esc
17
+ id="$1"
18
+ esc="$(oqa_sql_escape "$id")"
19
+ oqa_db_query "SELECT
20
+ id, slug, title, directory, agent,
21
+ json_extract(model,'\$.modelID') AS model,
22
+ json_extract(model,'\$.providerID') AS provider,
23
+ cost, tokens_input, tokens_output,
24
+ datetime(time_created/1000,'unixepoch') AS created,
25
+ datetime(time_updated/1000,'unixepoch') AS updated
26
+ FROM session WHERE id='$esc'"
27
+ }
28
+
29
+ oqa_self_test() {
30
+ oqa_require opencode jq || return 1
31
+ local id out got
32
+ id="$(oqa_db_query "SELECT id FROM session ORDER BY time_created DESC LIMIT 1" | jq -r '.[0].id // empty')"
33
+ if [ -z "$id" ]; then
34
+ oqa_log "FAIL: no sessions in DB to test against"; return 1
35
+ fi
36
+ out="$(oqa_session_by_id "$id")"
37
+ got="$(printf '%s' "$out" | jq -r '.[0].id // empty')"
38
+ if [ "$got" = "$id" ]; then
39
+ oqa_pass "db-session-by-id round-trips id ($id)"
40
+ return 0
41
+ fi
42
+ oqa_log "FAIL: expected id '$id', got '$got'"; return 1
43
+ }
44
+
45
+ case "${1:-}" in
46
+ --self-test) oqa_self_test; exit $? ;;
47
+ -h|--help|"")
48
+ sed -n '2,12p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
49
+ [ -z "${1:-}" ] && exit 2 || exit 0 ;;
50
+ *)
51
+ oqa_require opencode jq || exit 1
52
+ oqa_session_by_id "$1" ;;
53
+ esac
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # db-session-by-name.sh - find opencode sessions whose TITLE matches a substring.
3
+ # Read-only against the LIVE opencode DB. Title search is cheap (the session
4
+ # table is small; ~21k rows scan in milliseconds), so no bounding is needed.
5
+ #
6
+ # Usage:
7
+ # db-session-by-name.sh "auth refactor" # newest 20 matches
8
+ # db-session-by-name.sh "auth refactor" 50 # newest 50 matches
9
+ # db-session-by-name.sh --self-test
10
+ #
11
+ # Output: JSON array of {id, title, created, updated} ordered newest first.
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ . "$SCRIPT_DIR/lib/common.sh"
15
+
16
+ oqa_session_by_name() {
17
+ local needle limit esc
18
+ needle="$1"
19
+ limit="${2:-20}"
20
+ case "$limit" in (*[!0-9]*|"") limit=20 ;; esac
21
+ esc="$(oqa_sql_escape "$needle")"
22
+ oqa_db_query "SELECT
23
+ id, title,
24
+ datetime(time_created/1000,'unixepoch') AS created,
25
+ datetime(time_updated/1000,'unixepoch') AS updated
26
+ FROM session
27
+ WHERE title LIKE '%$esc%'
28
+ ORDER BY time_created DESC
29
+ LIMIT $limit"
30
+ }
31
+
32
+ oqa_self_test() {
33
+ oqa_require opencode jq || return 1
34
+ # Derive a guaranteed-present needle: the first 5 chars of a real title.
35
+ local needle out n
36
+ needle="$(oqa_db_query "SELECT substr(title,1,5) AS t FROM session WHERE length(title)>=5 ORDER BY time_created DESC LIMIT 1" | jq -r '.[0].t // empty')"
37
+ if [ -z "$needle" ]; then
38
+ oqa_log "FAIL: could not derive a title needle"; return 1
39
+ fi
40
+ out="$(oqa_session_by_name "$needle" 5)"
41
+ n="$(printf '%s' "$out" | jq 'length')"
42
+ if [ "${n:-0}" -ge 1 ]; then
43
+ oqa_pass "db-session-by-name found $n row(s) for needle '$needle'"
44
+ return 0
45
+ fi
46
+ oqa_log "FAIL: expected >=1 row for needle '$needle', got '$n'"; return 1
47
+ }
48
+
49
+ case "${1:-}" in
50
+ --self-test) oqa_self_test; exit $? ;;
51
+ -h|--help|"")
52
+ sed -n '2,14p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
53
+ [ -z "${1:-}" ] && exit 2 || exit 0 ;;
54
+ *)
55
+ oqa_require opencode jq || exit 1
56
+ oqa_session_by_name "$1" "${2:-20}" ;;
57
+ esac