@agentprojectcontext/apx 1.15.6 → 1.16.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 (221) hide show
  1. package/package.json +40 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/whisper-server.py +57 -6
  30. package/src/overlay/index.html +44 -0
  31. package/src/overlay/main.js +480 -0
  32. package/src/overlay/package.json +3 -0
  33. package/src/overlay/preload.js +34 -0
  34. package/src/overlay/renderer.js +371 -0
  35. package/src/overlay/style.css +250 -0
  36. package/src/tui/_shims/cli-error.ts +6 -0
  37. package/src/tui/_shims/cli-logo.ts +18 -0
  38. package/src/tui/_shims/cli-ui.ts +1 -0
  39. package/src/tui/_shims/config-console-state.ts +7 -0
  40. package/src/tui/_shims/core-any.ts +30 -0
  41. package/src/tui/_shims/core-binary.ts +13 -0
  42. package/src/tui/_shims/core-flag.ts +3 -0
  43. package/src/tui/_shims/core-log.ts +14 -0
  44. package/src/tui/_shims/lsp-language.ts +1 -0
  45. package/src/tui/_shims/opencode-any.ts +135 -0
  46. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  47. package/src/tui/_shims/plugin-tui.ts +13 -0
  48. package/src/tui/_shims/provider-provider.ts +10 -0
  49. package/src/tui/_shims/session-retry.ts +1 -0
  50. package/src/tui/_shims/session-schema.ts +15 -0
  51. package/src/tui/_shims/session-session.ts +3 -0
  52. package/src/tui/_shims/snapshot.ts +4 -0
  53. package/src/tui/_shims/tool-any.ts +18 -0
  54. package/src/tui/_shims/util-error.ts +7 -0
  55. package/src/tui/_shims/util-filesystem.ts +79 -0
  56. package/src/tui/_shims/util-format.ts +7 -0
  57. package/src/tui/_shims/util-iife.ts +3 -0
  58. package/src/tui/_shims/util-locale.ts +10 -0
  59. package/src/tui/_shims/util-process.ts +38 -0
  60. package/src/tui/app.tsx +783 -0
  61. package/src/tui/asset/charge.wav +0 -0
  62. package/src/tui/asset/pulse-a.wav +0 -0
  63. package/src/tui/asset/pulse-b.wav +0 -0
  64. package/src/tui/asset/pulse-c.wav +0 -0
  65. package/src/tui/attach.ts +100 -0
  66. package/src/tui/component/bg-pulse-render.ts +436 -0
  67. package/src/tui/component/bg-pulse.tsx +99 -0
  68. package/src/tui/component/border.tsx +21 -0
  69. package/src/tui/component/dialog-agent.tsx +31 -0
  70. package/src/tui/component/dialog-console-org.tsx +103 -0
  71. package/src/tui/component/dialog-mcp.tsx +85 -0
  72. package/src/tui/component/dialog-model.tsx +175 -0
  73. package/src/tui/component/dialog-provider.tsx +456 -0
  74. package/src/tui/component/dialog-retry-action.tsx +160 -0
  75. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  76. package/src/tui/component/dialog-session-list.tsx +323 -0
  77. package/src/tui/component/dialog-session-rename.tsx +31 -0
  78. package/src/tui/component/dialog-skill.tsx +36 -0
  79. package/src/tui/component/dialog-stash.tsx +87 -0
  80. package/src/tui/component/dialog-status.tsx +168 -0
  81. package/src/tui/component/dialog-tag.tsx +44 -0
  82. package/src/tui/component/dialog-theme-list.tsx +50 -0
  83. package/src/tui/component/dialog-variant.tsx +39 -0
  84. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  85. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  86. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  87. package/src/tui/component/error-component.tsx +92 -0
  88. package/src/tui/component/logo.tsx +896 -0
  89. package/src/tui/component/plugin-route-missing.tsx +14 -0
  90. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  91. package/src/tui/component/prompt/cwd.ts +0 -0
  92. package/src/tui/component/prompt/frecency.tsx +90 -0
  93. package/src/tui/component/prompt/history.tsx +108 -0
  94. package/src/tui/component/prompt/index.tsx +1809 -0
  95. package/src/tui/component/prompt/part.ts +16 -0
  96. package/src/tui/component/prompt/stash.tsx +101 -0
  97. package/src/tui/component/prompt/traits.ts +35 -0
  98. package/src/tui/component/spinner.tsx +24 -0
  99. package/src/tui/component/startup-loading.tsx +63 -0
  100. package/src/tui/component/todo-item.tsx +32 -0
  101. package/src/tui/component/use-connected.tsx +9 -0
  102. package/src/tui/component/workspace-label.tsx +19 -0
  103. package/src/tui/config/cwd.ts +5 -0
  104. package/src/tui/config/keybind.ts +432 -0
  105. package/src/tui/config/tui-migrate.ts +154 -0
  106. package/src/tui/config/tui-schema.ts +34 -0
  107. package/src/tui/config/tui.ts +46 -0
  108. package/src/tui/context/aggregate-failures.ts +34 -0
  109. package/src/tui/context/args.tsx +15 -0
  110. package/src/tui/context/command-palette.tsx +163 -0
  111. package/src/tui/context/directory.ts +15 -0
  112. package/src/tui/context/editor-zed.ts +283 -0
  113. package/src/tui/context/editor.ts +468 -0
  114. package/src/tui/context/event-apx.ts +22 -0
  115. package/src/tui/context/event.ts +6 -0
  116. package/src/tui/context/exit.tsx +60 -0
  117. package/src/tui/context/helper.tsx +25 -0
  118. package/src/tui/context/kv.tsx +81 -0
  119. package/src/tui/context/local.tsx +608 -0
  120. package/src/tui/context/path-format.tsx +39 -0
  121. package/src/tui/context/project-apx.tsx +48 -0
  122. package/src/tui/context/project.tsx +7 -0
  123. package/src/tui/context/prompt.tsx +18 -0
  124. package/src/tui/context/route.tsx +52 -0
  125. package/src/tui/context/sdk-apx.tsx +185 -0
  126. package/src/tui/context/sdk.tsx +6 -0
  127. package/src/tui/context/sync-apx.tsx +178 -0
  128. package/src/tui/context/sync-v2.tsx +16 -0
  129. package/src/tui/context/sync.tsx +118 -0
  130. package/src/tui/context/theme/aura.json +69 -0
  131. package/src/tui/context/theme/ayu.json +80 -0
  132. package/src/tui/context/theme/carbonfox.json +248 -0
  133. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  134. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  135. package/src/tui/context/theme/catppuccin.json +112 -0
  136. package/src/tui/context/theme/cobalt2.json +225 -0
  137. package/src/tui/context/theme/cursor.json +249 -0
  138. package/src/tui/context/theme/dracula.json +219 -0
  139. package/src/tui/context/theme/everforest.json +241 -0
  140. package/src/tui/context/theme/flexoki.json +237 -0
  141. package/src/tui/context/theme/github.json +233 -0
  142. package/src/tui/context/theme/gruvbox.json +242 -0
  143. package/src/tui/context/theme/kanagawa.json +77 -0
  144. package/src/tui/context/theme/lucent-orng.json +234 -0
  145. package/src/tui/context/theme/material.json +235 -0
  146. package/src/tui/context/theme/matrix.json +77 -0
  147. package/src/tui/context/theme/mercury.json +252 -0
  148. package/src/tui/context/theme/monokai.json +221 -0
  149. package/src/tui/context/theme/nightowl.json +221 -0
  150. package/src/tui/context/theme/nord.json +223 -0
  151. package/src/tui/context/theme/one-dark.json +84 -0
  152. package/src/tui/context/theme/opencode.json +245 -0
  153. package/src/tui/context/theme/orng.json +249 -0
  154. package/src/tui/context/theme/osaka-jade.json +93 -0
  155. package/src/tui/context/theme/palenight.json +222 -0
  156. package/src/tui/context/theme/rosepine.json +234 -0
  157. package/src/tui/context/theme/solarized.json +223 -0
  158. package/src/tui/context/theme/synthwave84.json +226 -0
  159. package/src/tui/context/theme/tokyonight.json +243 -0
  160. package/src/tui/context/theme/vercel.json +245 -0
  161. package/src/tui/context/theme/vesper.json +218 -0
  162. package/src/tui/context/theme/zenburn.json +223 -0
  163. package/src/tui/context/theme.tsx +1247 -0
  164. package/src/tui/context/tui-config.tsx +9 -0
  165. package/src/tui/event.ts +16 -0
  166. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  167. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  168. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  169. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  170. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  171. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  172. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  173. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  174. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  175. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  176. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  177. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  178. package/src/tui/keymap.tsx +166 -0
  179. package/src/tui/layer.ts +6 -0
  180. package/src/tui/plugin/api.tsx +381 -0
  181. package/src/tui/plugin/command-shim.ts +109 -0
  182. package/src/tui/plugin/internal.ts +33 -0
  183. package/src/tui/plugin/runtime.ts +1069 -0
  184. package/src/tui/plugin/slots.tsx +60 -0
  185. package/src/tui/routes/home.tsx +96 -0
  186. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  187. package/src/tui/routes/session/dialog-message.tsx +108 -0
  188. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  189. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  190. package/src/tui/routes/session/footer.tsx +91 -0
  191. package/src/tui/routes/session/index.tsx +188 -0
  192. package/src/tui/routes/session/permission.tsx +722 -0
  193. package/src/tui/routes/session/question.tsx +490 -0
  194. package/src/tui/routes/session/sidebar.tsx +102 -0
  195. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  196. package/src/tui/run.ts +84 -0
  197. package/src/tui/thread.ts +261 -0
  198. package/src/tui/tsconfig.json +40 -0
  199. package/src/tui/ui/dialog-alert.tsx +66 -0
  200. package/src/tui/ui/dialog-confirm.tsx +108 -0
  201. package/src/tui/ui/dialog-export-options.tsx +217 -0
  202. package/src/tui/ui/dialog-help.tsx +40 -0
  203. package/src/tui/ui/dialog-prompt.tsx +101 -0
  204. package/src/tui/ui/dialog-select.tsx +553 -0
  205. package/src/tui/ui/dialog.tsx +211 -0
  206. package/src/tui/ui/link.tsx +34 -0
  207. package/src/tui/ui/spinner.ts +368 -0
  208. package/src/tui/ui/toast.tsx +111 -0
  209. package/src/tui/util/clipboard.ts +217 -0
  210. package/src/tui/util/editor.ts +37 -0
  211. package/src/tui/util/model.ts +23 -0
  212. package/src/tui/util/provider-origin.ts +7 -0
  213. package/src/tui/util/revert-diff.ts +18 -0
  214. package/src/tui/util/scroll.ts +25 -0
  215. package/src/tui/util/selection.ts +65 -0
  216. package/src/tui/util/signal.ts +41 -0
  217. package/src/tui/util/sound.ts +156 -0
  218. package/src/tui/util/transcript.ts +112 -0
  219. package/src/tui/validate-session.ts +29 -0
  220. package/src/tui/win32.ts +130 -0
  221. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,156 @@
1
+ import { Player } from "cli-sound"
2
+ import { mkdirSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { basename, join } from "node:path"
5
+ import { Process } from "@/util/process"
6
+ import { which } from "@/util/which"
7
+ import pulseA from "../asset/pulse-a.wav" with { type: "file" }
8
+ import pulseB from "../asset/pulse-b.wav" with { type: "file" }
9
+ import pulseC from "../asset/pulse-c.wav" with { type: "file" }
10
+ import charge from "../asset/charge.wav" with { type: "file" }
11
+
12
+ const FILE = [pulseA, pulseB, pulseC]
13
+
14
+ const HUM = charge
15
+ const DIR = join(tmpdir(), "opencode-sfx")
16
+
17
+ const LIST = [
18
+ "ffplay",
19
+ "mpv",
20
+ "mpg123",
21
+ "mpg321",
22
+ "mplayer",
23
+ "afplay",
24
+ "play",
25
+ "omxplayer",
26
+ "aplay",
27
+ "cmdmp3",
28
+ "cvlc",
29
+ "powershell.exe",
30
+ ] as const
31
+
32
+ type Kind = (typeof LIST)[number]
33
+
34
+ function args(kind: Kind, file: string, volume: number) {
35
+ if (kind === "ffplay") return [kind, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file]
36
+ if (kind === "mpv")
37
+ return [kind, "--no-video", "--audio-display=no", "--volume", String(Math.round(volume * 100)), file]
38
+ if (kind === "mpg123" || kind === "mpg321") return [kind, "-g", String(Math.round(volume * 100)), file]
39
+ if (kind === "mplayer") return [kind, "-vo", "null", "-volume", String(Math.round(volume * 100)), file]
40
+ if (kind === "afplay" || kind === "omxplayer" || kind === "aplay" || kind === "cmdmp3") return [kind, file]
41
+ if (kind === "play") return [kind, "-v", String(volume), file]
42
+ if (kind === "cvlc") return [kind, `--gain=${volume}`, "--play-and-exit", file]
43
+ return [kind, "-c", `(New-Object Media.SoundPlayer '${file.replace(/'/g, "''")}').PlaySync()`]
44
+ }
45
+
46
+ let item: Player | null | undefined
47
+ let kind: Kind | null | undefined
48
+ let proc: Process.Child | undefined
49
+ let tail: ReturnType<typeof setTimeout> | undefined
50
+ let cache: Promise<{ hum: string; pulse: string[] }> | undefined
51
+ let seq = 0
52
+ let shot = 0
53
+
54
+ function load() {
55
+ if (item !== undefined) return item
56
+ try {
57
+ item = new Player({ volume: 0.35 })
58
+ } catch {
59
+ item = null
60
+ }
61
+ return item
62
+ }
63
+
64
+ async function file(path: string) {
65
+ mkdirSync(DIR, { recursive: true })
66
+ const next = join(DIR, basename(path))
67
+ const out = Bun.file(next)
68
+ if (await out.exists()) return next
69
+ await Bun.write(out, Bun.file(path))
70
+ return next
71
+ }
72
+
73
+ function asset() {
74
+ cache ??= Promise.all([file(HUM), Promise.all(FILE.map(file))]).then(([hum, pulse]) => ({ hum, pulse }))
75
+ return cache
76
+ }
77
+
78
+ function pick() {
79
+ if (kind !== undefined) return kind
80
+ kind = LIST.find((item) => which(item)) ?? null
81
+ return kind
82
+ }
83
+
84
+ function run(file: string, volume: number) {
85
+ const kind = pick()
86
+ if (!kind) return
87
+ return Process.spawn(args(kind, file, volume), {
88
+ stdin: "ignore",
89
+ stdout: "ignore",
90
+ stderr: "ignore",
91
+ })
92
+ }
93
+
94
+ function clear() {
95
+ if (!tail) return
96
+ clearTimeout(tail)
97
+ tail = undefined
98
+ }
99
+
100
+ function play(file: string, volume: number) {
101
+ const item = load()
102
+ if (!item) return run(file, volume)?.exited
103
+ return item.play(file, { volume }).catch(() => run(file, volume)?.exited)
104
+ }
105
+
106
+ export function start() {
107
+ stop()
108
+ const id = ++seq
109
+ void asset().then(({ hum }) => {
110
+ if (id !== seq) return
111
+ const next = run(hum, 0.24)
112
+ if (!next) return
113
+ proc = next
114
+ void next.exited.then(
115
+ () => {
116
+ if (id !== seq) return
117
+ if (proc === next) proc = undefined
118
+ },
119
+ () => {
120
+ if (id !== seq) return
121
+ if (proc === next) proc = undefined
122
+ },
123
+ )
124
+ })
125
+ }
126
+
127
+ export function stop(delay = 0) {
128
+ seq++
129
+ clear()
130
+ if (!proc) return
131
+ const next = proc
132
+ if (delay <= 0) {
133
+ proc = undefined
134
+ void Process.stop(next).catch(() => undefined)
135
+ return
136
+ }
137
+ tail = setTimeout(() => {
138
+ tail = undefined
139
+ if (proc === next) proc = undefined
140
+ void Process.stop(next).catch(() => undefined)
141
+ }, delay)
142
+ }
143
+
144
+ export function pulse(scale = 1) {
145
+ stop(140)
146
+ const index = shot++ % FILE.length
147
+ void asset()
148
+ .then(({ pulse }) => play(pulse[index], 0.26 + 0.14 * scale))
149
+ .catch(() => undefined)
150
+ }
151
+
152
+ export function dispose() {
153
+ stop()
154
+ }
155
+
156
+ export * as Sound from "./sound"
@@ -0,0 +1,112 @@
1
+ import type { AssistantMessage, Part, Provider, UserMessage } from "@opencode-ai/sdk/v2"
2
+ import { Locale } from "@/util/locale"
3
+ import * as Model from "./model"
4
+
5
+ export type TranscriptOptions = {
6
+ thinking: boolean
7
+ toolDetails: boolean
8
+ assistantMetadata: boolean
9
+ providers?: Provider[]
10
+ }
11
+
12
+ export type SessionInfo = {
13
+ id: string
14
+ title: string
15
+ time: {
16
+ created: number
17
+ updated: number
18
+ }
19
+ }
20
+
21
+ export type MessageWithParts = {
22
+ info: UserMessage | AssistantMessage
23
+ parts: Part[]
24
+ }
25
+
26
+ export function formatTranscript(
27
+ session: SessionInfo,
28
+ messages: MessageWithParts[],
29
+ options: TranscriptOptions,
30
+ ): string {
31
+ const providers = Model.index(options.providers)
32
+ let transcript = `# ${session.title}\n\n`
33
+ transcript += `**Session ID:** ${session.id}\n`
34
+ transcript += `**Created:** ${new Date(session.time.created).toLocaleString()}\n`
35
+ transcript += `**Updated:** ${new Date(session.time.updated).toLocaleString()}\n\n`
36
+ transcript += `---\n\n`
37
+
38
+ for (const msg of messages) {
39
+ transcript += formatMessage(msg.info, msg.parts, options, providers)
40
+ transcript += `---\n\n`
41
+ }
42
+
43
+ return transcript
44
+ }
45
+
46
+ export function formatMessage(
47
+ msg: UserMessage | AssistantMessage,
48
+ parts: Part[],
49
+ options: TranscriptOptions,
50
+ providers?: Provider[] | ReadonlyMap<string, Provider>,
51
+ ): string {
52
+ let result = ""
53
+
54
+ if (msg.role === "user") {
55
+ result += `## User\n\n`
56
+ } else {
57
+ result += formatAssistantHeader(msg, options.assistantMetadata, providers ?? options.providers)
58
+ }
59
+
60
+ for (const part of parts) {
61
+ result += formatPart(part, options)
62
+ }
63
+
64
+ return result
65
+ }
66
+
67
+ export function formatAssistantHeader(
68
+ msg: AssistantMessage,
69
+ includeMetadata: boolean,
70
+ providers?: Provider[] | ReadonlyMap<string, Provider>,
71
+ ): string {
72
+ if (!includeMetadata) {
73
+ return `## Assistant\n\n`
74
+ }
75
+
76
+ const duration =
77
+ msg.time.completed && msg.time.created ? ((msg.time.completed - msg.time.created) / 1000).toFixed(1) + "s" : ""
78
+
79
+ const modelName = Model.name(providers, msg.providerID, msg.modelID)
80
+
81
+ return `## Assistant (${Locale.titlecase(msg.agent)} · ${modelName}${duration ? ` · ${duration}` : ""})\n\n`
82
+ }
83
+
84
+ export function formatPart(part: Part, options: TranscriptOptions): string {
85
+ if (part.type === "text" && !part.synthetic) {
86
+ return `${part.text}\n\n`
87
+ }
88
+
89
+ if (part.type === "reasoning") {
90
+ if (options.thinking) {
91
+ return `_Thinking:_\n\n${part.text}\n\n`
92
+ }
93
+ return ""
94
+ }
95
+
96
+ if (part.type === "tool") {
97
+ let result = `**Tool: ${part.tool}**\n`
98
+ if (options.toolDetails && part.state.input) {
99
+ result += `\n**Input:**\n\`\`\`json\n${JSON.stringify(part.state.input, null, 2)}\n\`\`\`\n`
100
+ }
101
+ if (options.toolDetails && part.state.status === "completed" && part.state.output) {
102
+ result += `\n**Output:**\n\`\`\`\n${part.state.output}\n\`\`\`\n`
103
+ }
104
+ if (options.toolDetails && part.state.status === "error" && part.state.error) {
105
+ result += `\n**Error:**\n\`\`\`\n${part.state.error}\n\`\`\`\n`
106
+ }
107
+ result += `\n`
108
+ return result
109
+ }
110
+
111
+ return ""
112
+ }
@@ -0,0 +1,29 @@
1
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2"
2
+ import { SessionID } from "@/session/schema"
3
+ import { Schema } from "effect"
4
+
5
+ const decodeSessionID = Schema.decodeUnknownSync(SessionID)
6
+
7
+ export async function validateSession(input: {
8
+ url: string
9
+ sessionID?: string
10
+ directory?: string
11
+ fetch?: typeof fetch
12
+ headers?: RequestInit["headers"]
13
+ }) {
14
+ if (!input.sessionID) return
15
+
16
+ let sessionID: SessionID
17
+ try {
18
+ sessionID = decodeSessionID(input.sessionID)
19
+ } catch (error) {
20
+ throw new Error(`Invalid session ID: ${error instanceof Error ? error.message : "unknown error"}`, { cause: error })
21
+ }
22
+
23
+ await createOpencodeClient({
24
+ baseUrl: input.url,
25
+ directory: input.directory,
26
+ fetch: input.fetch,
27
+ headers: input.headers,
28
+ }).session.get({ sessionID }, { throwOnError: true })
29
+ }
@@ -0,0 +1,130 @@
1
+ import { dlopen, ptr } from "bun:ffi"
2
+ import type { ReadStream } from "node:tty"
3
+
4
+ const STD_INPUT_HANDLE = -10
5
+ const ENABLE_PROCESSED_INPUT = 0x0001
6
+
7
+ const kernel = () =>
8
+ dlopen("kernel32.dll", {
9
+ GetStdHandle: { args: ["i32"], returns: "ptr" },
10
+ GetConsoleMode: { args: ["ptr", "ptr"], returns: "i32" },
11
+ SetConsoleMode: { args: ["ptr", "u32"], returns: "i32" },
12
+ FlushConsoleInputBuffer: { args: ["ptr"], returns: "i32" },
13
+ })
14
+
15
+ let k32: ReturnType<typeof kernel> | undefined
16
+
17
+ function load() {
18
+ if (process.platform !== "win32") return false
19
+ try {
20
+ k32 ??= kernel()
21
+ return true
22
+ } catch {
23
+ return false
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Clear ENABLE_PROCESSED_INPUT on the console stdin handle.
29
+ */
30
+ export function win32DisableProcessedInput() {
31
+ if (process.platform !== "win32") return
32
+ if (!process.stdin.isTTY) return
33
+ if (!load()) return
34
+
35
+ const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
36
+ const buf = new Uint32Array(1)
37
+ if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
38
+
39
+ const mode = buf[0]!
40
+ if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
41
+ k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
42
+ }
43
+
44
+ /**
45
+ * Discard any queued console input (mouse events, key presses, etc.).
46
+ */
47
+ export function win32FlushInputBuffer() {
48
+ if (process.platform !== "win32") return
49
+ if (!process.stdin.isTTY) return
50
+ if (!load()) return
51
+
52
+ const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
53
+ k32!.symbols.FlushConsoleInputBuffer(handle)
54
+ }
55
+
56
+ let unhook: (() => void) | undefined
57
+
58
+ /**
59
+ * Keep ENABLE_PROCESSED_INPUT disabled.
60
+ *
61
+ * On Windows, Ctrl+C becomes a CTRL_C_EVENT (instead of stdin input) when
62
+ * ENABLE_PROCESSED_INPUT is set. Various runtimes can re-apply console modes
63
+ * (sometimes on a later tick), and the flag is console-global, not per-process.
64
+ *
65
+ * We combine:
66
+ * - A `setRawMode(...)` hook to re-clear after known raw-mode toggles.
67
+ * - A low-frequency poll as a backstop for native/external mode changes.
68
+ */
69
+ export function win32InstallCtrlCGuard() {
70
+ if (process.platform !== "win32") return
71
+ if (!process.stdin.isTTY) return
72
+ if (!load()) return
73
+ if (unhook) return unhook
74
+
75
+ const stdin = process.stdin as ReadStream
76
+ const original = stdin.setRawMode
77
+
78
+ const handle = k32!.symbols.GetStdHandle(STD_INPUT_HANDLE)
79
+ const buf = new Uint32Array(1)
80
+
81
+ if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
82
+ const initial = buf[0]!
83
+
84
+ const enforce = () => {
85
+ if (k32!.symbols.GetConsoleMode(handle, ptr(buf)) === 0) return
86
+ const mode = buf[0]!
87
+ if ((mode & ENABLE_PROCESSED_INPUT) === 0) return
88
+ k32!.symbols.SetConsoleMode(handle, mode & ~ENABLE_PROCESSED_INPUT)
89
+ }
90
+
91
+ // Some runtimes can re-apply console modes on the next tick; enforce twice.
92
+ const later = () => {
93
+ enforce()
94
+ setImmediate(enforce)
95
+ }
96
+
97
+ let wrapped: ReadStream["setRawMode"] | undefined
98
+
99
+ if (typeof original === "function") {
100
+ wrapped = (mode: boolean) => {
101
+ const result = original.call(stdin, mode)
102
+ later()
103
+ return result
104
+ }
105
+
106
+ stdin.setRawMode = wrapped
107
+ }
108
+
109
+ // Ensure it's cleared immediately too (covers any earlier mode changes).
110
+ later()
111
+
112
+ const interval = setInterval(enforce, 100)
113
+ interval.unref()
114
+
115
+ let done = false
116
+ unhook = () => {
117
+ if (done) return
118
+ done = true
119
+
120
+ clearInterval(interval)
121
+ if (wrapped && stdin.setRawMode === wrapped) {
122
+ stdin.setRawMode = original
123
+ }
124
+
125
+ k32!.symbols.SetConsoleMode(handle, initial)
126
+ unhook = undefined
127
+ }
128
+
129
+ return unhook
130
+ }
@@ -0,0 +1,104 @@
1
+ import { Installation } from "@/installation"
2
+ import { Server } from "@/server/server"
3
+ import * as Log from "@opencode-ai/core/util/log"
4
+ import { InstanceRuntime } from "@/project/instance-runtime"
5
+ import { WithInstance } from "@/project/with-instance"
6
+ import { Rpc } from "@/util/rpc"
7
+ import { upgrade } from "@/cli/upgrade"
8
+ import { Config } from "@/config/config"
9
+ import { GlobalBus } from "@/bus/global"
10
+ import { ServerAuth } from "@/server/auth"
11
+ import { writeHeapSnapshot } from "node:v8"
12
+ import { Heap } from "@/cli/heap"
13
+ import { AppRuntime } from "@/effect/app-runtime"
14
+ import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
15
+ import { Effect } from "effect"
16
+ import { disposeAllInstancesAndEmitGlobalDisposed } from "@/server/global-lifecycle"
17
+
18
+ ensureProcessMetadata("worker")
19
+
20
+ await Log.init({
21
+ print: process.argv.includes("--print-logs"),
22
+ dev: Installation.isLocal(),
23
+ level: (() => {
24
+ if (Installation.isLocal()) return "DEBUG"
25
+ return "INFO"
26
+ })(),
27
+ })
28
+
29
+ Heap.start()
30
+
31
+ process.on("unhandledRejection", (e) => {
32
+ Log.Default.error("rejection", {
33
+ e: e instanceof Error ? e.message : e,
34
+ })
35
+ })
36
+
37
+ process.on("uncaughtException", (e) => {
38
+ Log.Default.error("exception", {
39
+ e: e instanceof Error ? e.message : e,
40
+ })
41
+ })
42
+
43
+ // Subscribe to global events and forward them via RPC
44
+ GlobalBus.on("event", (event) => {
45
+ Rpc.emit("global.event", event)
46
+ })
47
+
48
+ let server: Awaited<ReturnType<typeof Server.listen>> | undefined
49
+
50
+ export const rpc = {
51
+ async fetch(input: { url: string; method: string; headers: Record<string, string>; body?: string }) {
52
+ const headers = { ...input.headers }
53
+ const auth = ServerAuth.header()
54
+ if (auth && !headers["authorization"] && !headers["Authorization"]) {
55
+ headers["Authorization"] = auth
56
+ }
57
+ const request = new Request(input.url, {
58
+ method: input.method,
59
+ headers,
60
+ body: input.body,
61
+ })
62
+ const response = await Server.Default().app.fetch(request)
63
+ const body = await response.text()
64
+ return {
65
+ status: response.status,
66
+ headers: Object.fromEntries(response.headers.entries()),
67
+ body,
68
+ }
69
+ },
70
+ snapshot() {
71
+ const result = writeHeapSnapshot("server.heapsnapshot")
72
+ return result
73
+ },
74
+ async server(input: { port: number; hostname: string; mdns?: boolean; cors?: string[] }) {
75
+ if (server) await server.stop(true)
76
+ server = await Server.listen(input)
77
+ return { url: server.url.toString() }
78
+ },
79
+ async checkUpgrade(input: { directory: string }) {
80
+ await WithInstance.provide({
81
+ directory: input.directory,
82
+ fn: async () => {
83
+ await upgrade().catch(() => {})
84
+ },
85
+ })
86
+ },
87
+ async reload() {
88
+ await AppRuntime.runPromise(
89
+ Effect.gen(function* () {
90
+ const cfg = yield* Config.Service
91
+ yield* cfg.invalidate()
92
+ yield* disposeAllInstancesAndEmitGlobalDisposed({ swallowErrors: true })
93
+ }),
94
+ )
95
+ },
96
+ async shutdown() {
97
+ Log.Default.info("worker shutting down")
98
+
99
+ await InstanceRuntime.disposeAllInstances()
100
+ if (server) await server.stop(true)
101
+ },
102
+ }
103
+
104
+ Rpc.listen(rpc)