oh-my-opencode 4.5.12 → 4.7.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 (189) 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/bin/version-mismatch.js +47 -0
  28. package/bin/version-mismatch.test.ts +120 -0
  29. package/dist/cli/cleanup-command.d.ts +4 -0
  30. package/dist/cli/cleanup.d.ts +11 -0
  31. package/dist/cli/cli-program.d.ts +2 -1
  32. package/dist/cli/codex-ulw-loop.d.ts +12 -0
  33. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
  34. package/dist/cli/index.js +2189 -529
  35. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  37. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  38. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  39. package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -0
  40. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  41. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  42. package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
  43. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  44. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  45. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  46. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  47. package/dist/cli/install-codex/index.d.ts +4 -0
  48. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  49. package/dist/cli/install-codex/types.d.ts +20 -0
  50. package/dist/cli/run/event-state.d.ts +1 -0
  51. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  52. package/dist/cli/run/prompt-start.d.ts +7 -0
  53. package/dist/cli/star-request.d.ts +9 -0
  54. package/dist/config/schema/hooks.d.ts +0 -1
  55. package/dist/create-hooks.d.ts +0 -1
  56. package/dist/features/background-agent/concurrency.d.ts +1 -0
  57. package/dist/features/background-agent/process-cleanup.d.ts +6 -0
  58. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  59. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  60. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  61. package/dist/features/opencode-skill-loader/index.d.ts +1 -0
  62. package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
  63. package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
  64. package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
  65. package/dist/hooks/comment-checker/cli.d.ts +1 -0
  66. package/dist/hooks/index.d.ts +0 -1
  67. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  68. package/dist/index.js +1077 -563
  69. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  70. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  71. package/dist/plugin/messages-transform.d.ts +8 -1
  72. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  73. package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
  74. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  75. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  76. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  77. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  78. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  79. package/dist/shared/prompt-async-gate.d.ts +1 -1
  80. package/dist/tools/skill/description-formatter.d.ts +5 -1
  81. package/dist/tools/skill/types.d.ts +1 -0
  82. package/package.json +22 -18
  83. package/packages/ast-grep-mcp/dist/cli.js +53 -9
  84. package/packages/git-bash-mcp/dist/cli.js +367 -0
  85. package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
  86. package/packages/omo-codex/plugin/.mcp.json +11 -0
  87. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  88. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  89. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  90. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  91. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  92. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  93. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  94. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  95. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  96. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  97. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  98. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  99. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  100. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  101. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  102. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  103. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  104. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
  105. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  106. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  107. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  108. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  109. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +6 -5
  110. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  111. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  112. package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
  113. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +4 -1
  114. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  115. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +9 -8
  116. package/packages/omo-codex/plugin/components/ultrawork/directive.md +32 -6
  117. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  118. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  119. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  120. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +28 -205
  121. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +231 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  125. package/packages/omo-codex/plugin/hooks/hooks.json +35 -2
  126. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  127. package/packages/omo-codex/plugin/package-lock.json +19 -0
  128. package/packages/omo-codex/plugin/package.json +3 -1
  129. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  130. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  131. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  132. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  133. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +89 -0
  134. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  135. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  136. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  137. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  138. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  139. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  140. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +33 -8
  141. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +25 -5
  142. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +28 -205
  143. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +231 -0
  144. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  145. package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -20
  146. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  147. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +58 -11
  148. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  149. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  150. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  151. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  152. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  153. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +67 -0
  154. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +54 -2
  155. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  156. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  157. package/packages/omo-codex/scripts/install/config.mjs +23 -1
  158. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  159. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  160. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  161. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  162. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  163. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  164. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  165. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  166. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  167. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  168. package/packages/omo-codex/scripts/install/reasoning-config.mjs +72 -0
  169. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  170. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  171. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  172. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  173. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  174. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +141 -0
  175. package/packages/omo-codex/scripts/install-config.test.mjs +205 -0
  176. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +157 -0
  177. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  178. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  179. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  180. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  181. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  182. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  183. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  184. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  185. package/packages/shared-skills/skills/review-work/SKILL.md +33 -8
  186. package/packages/shared-skills/skills/start-work/SKILL.md +25 -5
  187. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  188. package/postinstall.mjs +36 -3
  189. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bash
2
+ # db-session-by-text.sh - find opencode message TEXT by content.
3
+ #
4
+ # Message text lives inside the `part` table as JSON blobs
5
+ # (json_extract(data,'$.text') for text parts). The `part` table holds the bulk
6
+ # of the DB (tens of GB of tool output), so an UNBOUNDED text scan is refused.
7
+ # You MUST scope the search to a bounded, recent set of sessions:
8
+ # --session ses_... one session (indexed, instant)
9
+ # --recent N the N most-recent sessions (default 25)
10
+ # --since "<window>" sessions created within a window (e.g. "7 days"),
11
+ # capped at the 200 most-recent in that window
12
+ #
13
+ # All bounded modes use `part.session_id IN (SELECT id FROM session ORDER BY
14
+ # time_created DESC LIMIT ...)`, which drives the part_session_idx on exactly
15
+ # the newest sessions. (A naive JOIN with `WHERE session.time_created >= X`
16
+ # scans oldest-first and can take ~50s; the IN-subquery form returns in ~20ms.)
17
+ #
18
+ # Usage:
19
+ # db-session-by-text.sh --session ses_3a4e... "ULTRAWORK"
20
+ # db-session-by-text.sh --recent 50 "permission denied"
21
+ # db-session-by-text.sh --since "7 days" --limit 50 "TODO"
22
+ # db-session-by-text.sh --self-test
23
+ #
24
+ # Output: JSON array of {session_id, part_id, snippet} (snippet = first 120
25
+ # chars of the matching text part).
26
+
27
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
28
+ . "$SCRIPT_DIR/lib/common.sh"
29
+
30
+ oqa_text_scoped() {
31
+ local sid needle limit esc_id esc_txt
32
+ sid="$1"; needle="$2"; limit="${3:-50}"
33
+ esc_id="$(oqa_sql_escape "$sid")"
34
+ esc_txt="$(oqa_sql_escape "$needle")"
35
+ oqa_db_query "SELECT
36
+ p.session_id AS session_id,
37
+ p.id AS part_id,
38
+ substr(json_extract(p.data,'\$.text'),1,120) AS snippet
39
+ FROM part p
40
+ WHERE p.session_id='$esc_id'
41
+ AND json_extract(p.data,'\$.type')='text'
42
+ AND json_extract(p.data,'\$.text') LIKE '%$esc_txt%'
43
+ LIMIT $limit"
44
+ }
45
+
46
+ oqa_text_recent() {
47
+ local n needle limit esc_txt
48
+ n="$1"; needle="$2"; limit="${3:-50}"
49
+ case "$n" in (*[!0-9]*|"") n=25 ;; esac
50
+ esc_txt="$(oqa_sql_escape "$needle")"
51
+ oqa_db_query "SELECT
52
+ p.session_id AS session_id,
53
+ p.id AS part_id,
54
+ substr(json_extract(p.data,'\$.text'),1,120) AS snippet
55
+ FROM part p
56
+ WHERE p.session_id IN (SELECT id FROM session ORDER BY time_created DESC LIMIT $n)
57
+ AND json_extract(p.data,'\$.type')='text'
58
+ AND json_extract(p.data,'\$.text') LIKE '%$esc_txt%'
59
+ LIMIT $limit"
60
+ }
61
+
62
+ oqa_text_since() {
63
+ local window needle limit esc_win esc_txt
64
+ window="$1"; needle="$2"; limit="${3:-50}"
65
+ esc_win="$(oqa_sql_escape "$window")"
66
+ esc_txt="$(oqa_sql_escape "$needle")"
67
+ # Cap at the 200 most-recent sessions inside the window and drive
68
+ # part_session_idx via an IN-subquery (newest-first) to stay fast.
69
+ oqa_db_query "SELECT
70
+ p.session_id AS session_id,
71
+ p.id AS part_id,
72
+ substr(json_extract(p.data,'\$.text'),1,120) AS snippet
73
+ FROM part p
74
+ WHERE p.session_id IN (
75
+ SELECT id FROM session
76
+ WHERE time_created >= (strftime('%s','now','-$esc_win') * 1000)
77
+ ORDER BY time_created DESC LIMIT 200)
78
+ AND json_extract(p.data,'\$.type')='text'
79
+ AND json_extract(p.data,'\$.text') LIKE '%$esc_txt%'
80
+ LIMIT $limit"
81
+ }
82
+
83
+ oqa_text_main() {
84
+ local sid="" window="" recent="" limit=50 needle=""
85
+ while [ $# -gt 0 ]; do
86
+ case "$1" in
87
+ --session) sid="$2"; shift 2 ;;
88
+ --recent) recent="$2"; shift 2 ;;
89
+ --since) window="$2"; shift 2 ;;
90
+ --limit) limit="$2"; shift 2 ;;
91
+ *) needle="$1"; shift ;;
92
+ esac
93
+ done
94
+ if [ -z "$needle" ]; then
95
+ oqa_log "error: missing search text"; return 2
96
+ fi
97
+ if [ -n "$sid" ]; then
98
+ oqa_text_scoped "$sid" "$needle" "$limit"; return 0
99
+ fi
100
+ if [ -n "$recent" ]; then
101
+ oqa_text_recent "$recent" "$needle" "$limit"; return 0
102
+ fi
103
+ if [ -n "$window" ]; then
104
+ oqa_text_since "$window" "$needle" "$limit"; return 0
105
+ fi
106
+ oqa_log "error: refusing an unbounded global text scan over the multi-GB part table."
107
+ oqa_log " scope it with --session <ses_id>, --recent <N>, or --since \"<N days|hours>\"."
108
+ return 2
109
+ }
110
+
111
+ oqa_self_test() {
112
+ oqa_require opencode jq || return 1
113
+ local fails=0
114
+
115
+ # 1) scoped: find a recent session with text parts, derive a needle from one.
116
+ local sid needle out n
117
+ sid="$(oqa_db_query "SELECT session_id AS s FROM part WHERE json_extract(data,'\$.type')='text' ORDER BY rowid DESC LIMIT 1" | jq -r '.[0].s // empty')"
118
+ if [ -z "$sid" ]; then oqa_log "FAIL: no text parts found"; return 1; fi
119
+ needle="$(oqa_db_query "SELECT substr(json_extract(data,'\$.text'),1,8) AS t FROM part WHERE session_id='$(oqa_sql_escape "$sid")' AND json_extract(data,'\$.type')='text' AND length(json_extract(data,'\$.text'))>=8 LIMIT 1" | jq -r '.[0].t // empty')"
120
+ if [ -z "$needle" ]; then oqa_log "FAIL: could not derive a text needle"; return 1; fi
121
+ out="$(oqa_text_main --session "$sid" "$needle")"
122
+ n="$(printf '%s' "$out" | jq 'length')"
123
+ if [ "${n:-0}" -ge 1 ]; then oqa_pass "scoped text search found $n row(s) in $sid"; else oqa_log "FAIL: scoped search empty for '$needle' in $sid"; fails=$((fails+1)); fi
124
+
125
+ # 2) unbounded refusal: no --session/--since must exit 2.
126
+ oqa_text_main "$needle" >/dev/null 2>&1
127
+ if [ "$?" -eq 2 ]; then oqa_pass "unbounded global scan refused (exit 2)"; else oqa_log "FAIL: unbounded scan was not refused"; fails=$((fails+1)); fi
128
+
129
+ # 3) bounded --recent search completes well under a hard 30s budget, even in
130
+ # the worst case (no early match) because the IN-subquery caps the scan to
131
+ # the newest N sessions and drives part_session_idx.
132
+ local t0 t1
133
+ t0=$(date +%s)
134
+ oqa_text_main --recent 25 --limit 5 "oqa_no_match_$(date +%s)_zzz" >/dev/null 2>&1
135
+ t1=$(date +%s)
136
+ if [ $((t1 - t0)) -le 30 ]; then
137
+ oqa_pass "bounded --recent 25 worst-case search completed in $((t1-t0))s (<=30s)"
138
+ else
139
+ oqa_log "FAIL: bounded --recent search took $((t1-t0))s (>30s)"; fails=$((fails+1))
140
+ fi
141
+ # also prove --recent returns real matches for the derived needle
142
+ out="$(oqa_text_main --recent 25 --limit 5 "$needle" 2>/dev/null)"
143
+ n="$(printf '%s' "$out" | jq 'length')"
144
+ if [ "${n:-0}" -ge 1 ]; then oqa_pass "bounded --recent 25 found $n row(s) for '$needle'"; else oqa_log "FAIL: --recent found no rows for '$needle'"; fails=$((fails+1)); fi
145
+
146
+ [ "$fails" -eq 0 ] && { oqa_pass "db-session-by-text"; return 0; }
147
+ oqa_log "db-session-by-text had $fails failure(s)"; return 1
148
+ }
149
+
150
+ case "${1:-}" in
151
+ --self-test) oqa_self_test; exit $? ;;
152
+ -h|--help|"")
153
+ sed -n '2,24p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
154
+ [ -z "${1:-}" ] && exit 2 || exit 0 ;;
155
+ *)
156
+ oqa_require opencode jq || exit 1
157
+ oqa_text_main "$@" ;;
158
+ esac
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ # export-roundtrip.sh - export a session as clean JSON and verify it round-trips.
3
+ #
4
+ # `opencode export <id>` prints a human line ("Exporting session: ...") to
5
+ # STDERR and the JSON document to STDOUT, so suppress stderr before piping to jq.
6
+ # The JSON shape is { info: {id, slug, projectID, directory, title, tokens,
7
+ # time, ...}, messages: [...] }.
8
+ #
9
+ # Usage:
10
+ # export-roundtrip.sh ses_3a4e22ad5ffebMKLt0tL7exPjZ # prints clean JSON
11
+ # export-roundtrip.sh --self-test
12
+ #
13
+ # Tip: redirect to a file for archival: export-roundtrip.sh <id> > session.json
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ . "$SCRIPT_DIR/lib/common.sh"
17
+
18
+ oqa_export() {
19
+ # stderr carries the "Exporting session:" banner; drop it for clean JSON.
20
+ opencode export "$1" 2>/dev/null
21
+ }
22
+
23
+ oqa_self_test() {
24
+ oqa_require opencode jq || return 1
25
+ local id out got title msgtype
26
+ id="$(oqa_db_query "SELECT id FROM session ORDER BY time_created DESC LIMIT 1" | jq -r '.[0].id // empty')"
27
+ if [ -z "$id" ]; then oqa_log "FAIL: no sessions to export"; return 1; fi
28
+
29
+ out="$(oqa_export "$id")"
30
+ # 1) stdout must be valid JSON (stderr banner excluded).
31
+ if ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then
32
+ oqa_log "FAIL: export stdout is not valid JSON for $id"; return 1
33
+ fi
34
+ # 2) the info.id must round-trip.
35
+ got="$(printf '%s' "$out" | jq -r '.info.id // empty')"
36
+ if [ "$got" != "$id" ]; then
37
+ oqa_log "FAIL: export .info.id '$got' != '$id'"; return 1
38
+ fi
39
+ # 3) info.title is a string and messages is an array.
40
+ title="$(printf '%s' "$out" | jq -r '.info.title|type' 2>/dev/null)"
41
+ msgtype="$(printf '%s' "$out" | jq -r '.messages|type' 2>/dev/null)"
42
+ if [ "$title" = "string" ] && { [ "$msgtype" = "array" ] || [ "$msgtype" = "null" ]; }; then
43
+ oqa_pass "export round-trips $id (info.id matches, valid JSON)"
44
+ return 0
45
+ fi
46
+ oqa_log "FAIL: unexpected shape (title=$title messages=$msgtype)"; return 1
47
+ }
48
+
49
+ case "${1:-}" in
50
+ --self-test) oqa_self_test; exit $? ;;
51
+ -h|--help|"")
52
+ sed -n '2,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
53
+ [ -z "${1:-}" ] && exit 2 || exit 0 ;;
54
+ *)
55
+ oqa_require opencode jq || exit 1
56
+ oqa_export "$1" ;;
57
+ esac
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env bash
2
+ # common.sh - shared helpers for opencode-qa scripts.
3
+ #
4
+ # Source it from a sibling script:
5
+ # SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ # . "$SCRIPT_DIR/lib/common.sh"
7
+ #
8
+ # SAFETY MODEL (read this):
9
+ # - DB-read helpers (oqa_db_path / oqa_db_query) hit the LIVE opencode DB
10
+ # READ-ONLY. That is safe and intended for session investigation.
11
+ # - Anything that SPAWNS opencode (serve / run / tui) must run under an
12
+ # ISOLATED XDG sandbox (oqa_mk_isolated_xdg) so QA never writes junk
13
+ # sessions into the real ~/.local/share/opencode DB.
14
+ # - oqa_cleanup runs on EXIT and tears down servers, tmux sessions, curl
15
+ # watchers, and every temp dir created via the helpers.
16
+
17
+ # No `set -e`: these scripts deliberately probe failure paths (401, refused).
18
+ set -uo pipefail
19
+
20
+ OQA_TMPDIRS=()
21
+ OQA_TMUX_SESSIONS=()
22
+ OQA_CURL_PIDS=()
23
+ OQA_SERVER_PID=""
24
+
25
+ oqa_log() { printf '%s\n' "$*" >&2; }
26
+ oqa_pass() { printf 'PASS: %s\n' "$*"; }
27
+ oqa_fail() { printf 'FAIL: %s\n' "$*" >&2; return 1; }
28
+
29
+ # oqa_require <bin>... -> 0 if all present, else 1 (names the missing ones).
30
+ oqa_require() {
31
+ local missing=0 b
32
+ for b in "$@"; do
33
+ if ! command -v "$b" >/dev/null 2>&1; then
34
+ oqa_log "missing dependency: $b"
35
+ missing=1
36
+ fi
37
+ done
38
+ return "$missing"
39
+ }
40
+
41
+ # Absolute path of the active opencode DB (resolves channel / OPENCODE_DB).
42
+ oqa_db_path() {
43
+ opencode db path 2>/dev/null | head -1
44
+ }
45
+
46
+ # Escape a value for safe embedding inside a single-quoted SQL literal.
47
+ oqa_sql_escape() {
48
+ local s="${1//\'/\'\'}"
49
+ printf '%s' "$s"
50
+ }
51
+
52
+ # Run a read-only SQL query against the active DB; emit JSON rows.
53
+ # Usage: oqa_db_query "SELECT ... LIMIT 5"
54
+ oqa_db_query() {
55
+ opencode db "$1" --format json 2>/dev/null
56
+ }
57
+
58
+ # Create an isolated XDG sandbox so a spawned opencode never touches the real
59
+ # DB. Sets globals OQA_XDG_ROOT + OQA_PROJ and exports XDG_*; registers the
60
+ # root for cleanup.
61
+ #
62
+ # IMPORTANT: call this DIRECTLY, never via $(...). Command substitution runs in
63
+ # a subshell, which would discard the exports and the cleanup registration.
64
+ # oqa_mk_isolated_xdg # good
65
+ # root="$OQA_XDG_ROOT" # read the global afterwards
66
+ oqa_mk_isolated_xdg() {
67
+ local root
68
+ root="$(mktemp -d -t oqa-xdg.XXXXXX)" || return 1
69
+ OQA_TMPDIRS+=("$root")
70
+ mkdir -p "$root/data" "$root/config" "$root/cache" "$root/state" "$root/proj"
71
+ export OQA_XDG_ROOT="$root"
72
+ export XDG_DATA_HOME="$root/data"
73
+ export XDG_CONFIG_HOME="$root/config"
74
+ export XDG_CACHE_HOME="$root/cache"
75
+ export XDG_STATE_HOME="$root/state"
76
+ export OQA_PROJ="$root/proj"
77
+ # keep the sandbox offline + fast
78
+ export OPENCODE_DISABLE_AUTOUPDATE=1
79
+ export OPENCODE_DISABLE_MODELS_FETCH=1
80
+ }
81
+
82
+ # Print a free TCP port on 127.0.0.1.
83
+ oqa_free_port() {
84
+ if command -v python3 >/dev/null 2>&1; then
85
+ python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",0)); print(s.getsockname()[1]); s.close()'
86
+ elif command -v bun >/dev/null 2>&1; then
87
+ bun -e 'const s=Bun.listen({hostname:"127.0.0.1",port:0,socket:{data(){}}});console.log(s.port);s.stop()'
88
+ else
89
+ # last resort: a high random port (small race window)
90
+ printf '%s' "$(( (RANDOM % 20000) + 40000 ))"
91
+ fi
92
+ }
93
+
94
+ # Poll an HTTP url until it accepts a connection (any status) or times out.
95
+ # Usage: oqa_wait_http <url> [user:pass] [timeout_s]
96
+ oqa_wait_http() {
97
+ local url="$1" auth="${2:-}" timeout="${3:-25}" deadline
98
+ deadline=$(( $(date +%s) + timeout ))
99
+ while [ "$(date +%s)" -lt "$deadline" ]; do
100
+ if [ -n "$auth" ]; then
101
+ curl -s -o /dev/null -u "$auth" "$url" && return 0
102
+ else
103
+ curl -s -o /dev/null "$url" && return 0
104
+ fi
105
+ sleep 0.2
106
+ done
107
+ return 1
108
+ }
109
+
110
+ # Start an isolated, password-protected headless server.
111
+ # Sets globals OQA_SERVER_URL / OQA_SERVER_PASS / OQA_SERVER_PORT / OQA_SERVER_PID.
112
+ # Returns 1 if it never becomes ready.
113
+ #
114
+ # IMPORTANT: call this DIRECTLY, never via $(...). The PID + exports must land
115
+ # in the caller's shell so oqa_cleanup can kill the server on exit.
116
+ # oqa_start_server || { oqa_log "no server"; exit 1; }
117
+ # url="$OQA_SERVER_URL"
118
+ oqa_start_server() {
119
+ oqa_mk_isolated_xdg || return 1
120
+ local port pass
121
+ port="$(oqa_free_port)"
122
+ pass="oqa-${RANDOM}${RANDOM}"
123
+ OPENCODE_SERVER_PASSWORD="$pass" opencode serve --port "$port" --hostname 127.0.0.1 \
124
+ >"$XDG_STATE_HOME/serve.log" 2>&1 &
125
+ OQA_SERVER_PID=$!
126
+ export OQA_SERVER_PORT="$port"
127
+ export OQA_SERVER_PASS="$pass"
128
+ export OQA_SERVER_URL="http://127.0.0.1:$port"
129
+ if ! oqa_wait_http "$OQA_SERVER_URL/global/health" "opencode:$pass" 30; then
130
+ oqa_log "server failed to start; log follows:"
131
+ cat "$XDG_STATE_HOME/serve.log" >&2 2>/dev/null || true
132
+ return 1
133
+ fi
134
+ }
135
+
136
+ # Teardown everything the helpers created. Safe to call multiple times.
137
+ oqa_cleanup() {
138
+ if [ -n "${OQA_SERVER_PID:-}" ]; then
139
+ kill "$OQA_SERVER_PID" 2>/dev/null || true
140
+ sleep 0.3
141
+ kill -9 "$OQA_SERVER_PID" 2>/dev/null || true
142
+ OQA_SERVER_PID=""
143
+ fi
144
+ local s p d
145
+ for s in "${OQA_TMUX_SESSIONS[@]:-}"; do
146
+ [ -n "$s" ] && tmux kill-session -t "$s" 2>/dev/null || true
147
+ done
148
+ for p in "${OQA_CURL_PIDS[@]:-}"; do
149
+ [ -n "$p" ] && kill "$p" 2>/dev/null || true
150
+ done
151
+ for d in "${OQA_TMPDIRS[@]:-}"; do
152
+ [ -n "$d" ] && rm -rf "$d" 2>/dev/null || true
153
+ done
154
+ OQA_TMPDIRS=()
155
+ OQA_TMUX_SESSIONS=()
156
+ OQA_CURL_PIDS=()
157
+ }
158
+ trap oqa_cleanup EXIT
159
+
160
+ # ---- self-check ------------------------------------------------------------
161
+ # Run: bash scripts/lib/common.sh --self-check
162
+ oqa__self_check() {
163
+ local fails=0
164
+
165
+ if oqa_require opencode sqlite3 curl jq tmux; then
166
+ oqa_pass "dependencies present (opencode sqlite3 curl jq tmux)"
167
+ else
168
+ oqa_log "FAIL: missing dependencies"; fails=$((fails+1))
169
+ fi
170
+
171
+ local dbp; dbp="$(oqa_db_path)"
172
+ if [ -n "$dbp" ] && [ -f "$dbp" ]; then
173
+ oqa_pass "oqa_db_path -> $dbp"
174
+ else
175
+ oqa_log "FAIL: oqa_db_path returned '$dbp'"; fails=$((fails+1))
176
+ fi
177
+
178
+ local esc; esc="$(oqa_sql_escape "a'b'c")"
179
+ if [ "$esc" = "a''b''c" ]; then
180
+ oqa_pass "oqa_sql_escape quotes single quotes"
181
+ else
182
+ oqa_log "FAIL: oqa_sql_escape -> '$esc'"; fails=$((fails+1))
183
+ fi
184
+
185
+ local port; port="$(oqa_free_port)"
186
+ if [ "$port" -gt 0 ] 2>/dev/null; then
187
+ oqa_pass "oqa_free_port -> $port"
188
+ else
189
+ oqa_log "FAIL: oqa_free_port -> '$port'"; fails=$((fails+1))
190
+ fi
191
+
192
+ # isolation + trap teardown: an inner shell creates a sandbox (calling the
193
+ # helper DIRECTLY so the cleanup registration survives) and exits; the EXIT
194
+ # trap must remove it. We pass the sandbox path out via a marker file.
195
+ local marker isodir
196
+ marker="$(mktemp -t oqa-marker.XXXXXX)"
197
+ bash -c '. "'"${BASH_SOURCE[0]}"'"; oqa_mk_isolated_xdg; printf "%s" "$OQA_XDG_ROOT" > "'"$marker"'"'
198
+ isodir="$(cat "$marker" 2>/dev/null)"; rm -f "$marker"
199
+ if [ -n "$isodir" ] && [ ! -d "$isodir" ]; then
200
+ oqa_pass "isolated XDG sandbox auto-removed on exit ($isodir)"
201
+ else
202
+ oqa_log "FAIL: sandbox not cleaned: '$isodir' (exists=$([ -d "$isodir" ] && echo yes || echo no))"; fails=$((fails+1))
203
+ fi
204
+
205
+ if [ "$fails" -eq 0 ]; then
206
+ oqa_pass "common.sh self-check"
207
+ return 0
208
+ fi
209
+ oqa_log "common.sh self-check had $fails failure(s)"
210
+ return 1
211
+ }
212
+
213
+ if [ "${1:-}" = "--self-check" ]; then
214
+ oqa__self_check
215
+ exit $?
216
+ fi
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+ # server-smoke.sh - boot an ISOLATED opencode HTTP server and verify the core
3
+ # API surface end to end. Uses an isolated XDG sandbox + a random password, so
4
+ # it never touches the real ~/.local/share/opencode DB, and tears the server
5
+ # down on exit.
6
+ #
7
+ # Checks:
8
+ # 1. GET /global/health -> {"healthy":true,"version":...}
9
+ # 2. GET /doc -> OpenAPI spec with >=100 paths
10
+ # 3. GET /session (no credentials) -> HTTP 401 (auth is enforced)
11
+ #
12
+ # Usage:
13
+ # server-smoke.sh # run the smoke test
14
+ # server-smoke.sh --self-test # same thing (alias for the QA sweep)
15
+
16
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ . "$SCRIPT_DIR/lib/common.sh"
18
+
19
+ oqa_server_smoke() {
20
+ oqa_require opencode curl jq || return 1
21
+ if ! oqa_start_server; then
22
+ oqa_log "FAIL: server did not become ready"; return 1
23
+ fi
24
+ local url="$OQA_SERVER_URL" auth="opencode:$OQA_SERVER_PASS" fails=0
25
+
26
+ local healthy version
27
+ healthy="$(curl -s -u "$auth" "$url/global/health" | jq -r '.healthy // false')"
28
+ version="$(curl -s -u "$auth" "$url/global/health" | jq -r '.version // "?"')"
29
+ if [ "$healthy" = "true" ]; then
30
+ oqa_pass "GET /global/health healthy=true version=$version ($url)"
31
+ else
32
+ oqa_log "FAIL: /global/health healthy=$healthy"; fails=$((fails+1))
33
+ fi
34
+
35
+ local npaths
36
+ npaths="$(curl -s -u "$auth" "$url/doc" | jq '.paths | length' 2>/dev/null)"
37
+ if [ "${npaths:-0}" -ge 100 ]; then
38
+ oqa_pass "GET /doc lists $npaths documented paths (>=100)"
39
+ else
40
+ oqa_log "FAIL: /doc path count=$npaths"; fails=$((fails+1))
41
+ fi
42
+
43
+ local code
44
+ code="$(curl -s -o /dev/null -w '%{http_code}' "$url/session?directory=$OQA_PROJ")"
45
+ if [ "$code" = "401" ]; then
46
+ oqa_pass "unauthenticated GET /session rejected with HTTP 401"
47
+ else
48
+ oqa_log "FAIL: unauthenticated /session returned $code (expected 401)"; fails=$((fails+1))
49
+ fi
50
+
51
+ if [ "$fails" -eq 0 ]; then
52
+ oqa_pass "server-smoke"
53
+ return 0
54
+ fi
55
+ oqa_log "server-smoke had $fails failure(s)"; return 1
56
+ }
57
+
58
+ case "${1:-}" in
59
+ -h|--help)
60
+ sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
61
+ exit 0 ;;
62
+ *)
63
+ oqa_server_smoke; exit $? ;;
64
+ esac
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env bash
2
+ # sse-hook-probe.sh - QA opencode's event stream (the plumbing behind hooks).
3
+ #
4
+ # opencode publishes lifecycle events over Server-Sent Events at GET /event
5
+ # (per-instance) and GET /global/event. Plugins observe the same events via the
6
+ # `event` hook, so confirming an event on the wire is how you prove a hook
7
+ # would have fired.
8
+ #
9
+ # Two modes:
10
+ # (default / --self-test) Spawn an ISOLATED server and assert the stream
11
+ # opens with a `server.connected` event. No real DB
12
+ # is touched. This proves the SSE plumbing works.
13
+ # --attach <url> Watch an ALREADY-RUNNING server's /event stream for
14
+ # a specific event type (default: server.connected).
15
+ # Use this against your real server to verify a hook
16
+ # or action. Pair it with a prompt in another shell:
17
+ # curl -X POST -u opencode:$PASS \
18
+ # -H 'Content-Type: application/json' \
19
+ # -d '{"parts":[{"type":"text","text":"hi"}]}' \
20
+ # "<url>/session/<ses_id>/prompt_async?directory=<dir>"
21
+ # then watch for e.g. message.part.updated.
22
+ #
23
+ # --attach options:
24
+ # --password <p> server password (user defaults to "opencode")
25
+ # --user <u> server username (default: opencode)
26
+ # --directory <d> instance directory (default: $PWD)
27
+ # --event <type> event type to wait for (default: server.connected)
28
+ # --timeout <s> seconds to wait (default: 15)
29
+ #
30
+ # Usage:
31
+ # sse-hook-probe.sh --self-test
32
+ # sse-hook-probe.sh --attach http://127.0.0.1:4096 --password secret \
33
+ # --directory "$PWD" --event message.part.updated --timeout 30
34
+
35
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
36
+ . "$SCRIPT_DIR/lib/common.sh"
37
+
38
+ # Watch an SSE stream for an event type. Args: url auth directory event timeout
39
+ # Returns 0 if seen, 1 otherwise. Always kills its curl watcher.
40
+ oqa_sse_watch() {
41
+ local url="$1" auth="$2" dir="$3" want="$4" timeout="${5:-15}"
42
+ local out cpid found="" deadline
43
+ out="$(mktemp -t oqa-sse.XXXXXX)"; OQA_TMPDIRS+=("$out")
44
+ if [ -n "$auth" ]; then
45
+ curl -sN -u "$auth" "$url/event?directory=$dir" >"$out" 2>/dev/null &
46
+ else
47
+ curl -sN "$url/event?directory=$dir" >"$out" 2>/dev/null &
48
+ fi
49
+ cpid=$!; OQA_CURL_PIDS+=("$cpid")
50
+ deadline=$(( $(date +%s) + timeout ))
51
+ while [ "$(date +%s)" -lt "$deadline" ]; do
52
+ if grep -q "\"$want\"" "$out" 2>/dev/null; then found=1; break; fi
53
+ kill -0 "$cpid" 2>/dev/null || break
54
+ sleep 0.2
55
+ done
56
+ kill "$cpid" 2>/dev/null || true
57
+ if [ -n "$found" ]; then
58
+ printf 'first matching event: '
59
+ grep -m1 "\"$want\"" "$out" | sed 's/^data: //' | jq -c '{type: .type}' 2>/dev/null || true
60
+ return 0
61
+ fi
62
+ oqa_log "stream head (no '$want' within ${timeout}s):"; head -5 "$out" >&2
63
+ return 1
64
+ }
65
+
66
+ oqa_self_test() {
67
+ oqa_require opencode curl jq || return 1
68
+ if ! oqa_start_server; then oqa_log "FAIL: server did not start"; return 1; fi
69
+ if oqa_sse_watch "$OQA_SERVER_URL" "opencode:$OQA_SERVER_PASS" "$OQA_PROJ" "server.connected" 15; then
70
+ oqa_pass "SSE /event opened and delivered server.connected"
71
+ return 0
72
+ fi
73
+ oqa_log "FAIL: did not observe server.connected"; return 1
74
+ }
75
+
76
+ oqa_attach_mode() {
77
+ local url="" user="opencode" pass="" dir="$PWD" event="server.connected" timeout=15
78
+ shift # drop --attach
79
+ url="$1"; shift || true
80
+ while [ $# -gt 0 ]; do
81
+ case "$1" in
82
+ --password) pass="$2"; shift 2 ;;
83
+ --user) user="$2"; shift 2 ;;
84
+ --directory) dir="$2"; shift 2 ;;
85
+ --event) event="$2"; shift 2 ;;
86
+ --timeout) timeout="$2"; shift 2 ;;
87
+ *) oqa_log "unknown option: $1"; shift ;;
88
+ esac
89
+ done
90
+ [ -n "$url" ] || { oqa_log "error: --attach requires a URL"; return 2; }
91
+ local auth=""; [ -n "$pass" ] && auth="$user:$pass"
92
+ oqa_log "watching $url/event?directory=$dir for '$event' (<=${timeout}s)"
93
+ if oqa_sse_watch "$url" "$auth" "$dir" "$event" "$timeout"; then
94
+ oqa_pass "observed '$event' on $url"
95
+ return 0
96
+ fi
97
+ oqa_log "FAIL: '$event' not observed"; return 1
98
+ }
99
+
100
+ case "${1:-}" in
101
+ --attach) oqa_attach_mode "$@"; exit $? ;;
102
+ -h|--help)
103
+ sed -n '2,34p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
104
+ exit 0 ;;
105
+ *) oqa_self_test; exit $? ;;
106
+ esac