@amalgm/chat 0.1.1 → 0.2.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 (282) hide show
  1. package/AGENTS.md +98 -0
  2. package/PURPOSE.md +143 -104
  3. package/README.md +82 -54
  4. package/dist/acp/index.d.ts +7 -0
  5. package/dist/acp/index.d.ts.map +1 -0
  6. package/dist/acp/index.js +7 -0
  7. package/dist/acp/index.js.map +1 -0
  8. package/dist/acp/projection.d.ts +30 -0
  9. package/dist/acp/projection.d.ts.map +1 -0
  10. package/dist/acp/projection.js +147 -0
  11. package/dist/acp/projection.js.map +1 -0
  12. package/dist/api/conversations.d.ts +66 -0
  13. package/dist/api/conversations.d.ts.map +1 -0
  14. package/dist/api/conversations.js +202 -0
  15. package/dist/api/conversations.js.map +1 -0
  16. package/dist/api/index.d.ts +3 -3
  17. package/dist/api/index.d.ts.map +1 -1
  18. package/dist/api/index.js +1 -2
  19. package/dist/api/index.js.map +1 -1
  20. package/dist/conversations/index.d.ts +3 -0
  21. package/dist/conversations/index.d.ts.map +1 -0
  22. package/dist/conversations/index.js +3 -0
  23. package/dist/conversations/index.js.map +1 -0
  24. package/dist/conversations/transcript.d.ts +13 -0
  25. package/dist/conversations/transcript.d.ts.map +1 -0
  26. package/dist/conversations/transcript.js +171 -0
  27. package/dist/conversations/transcript.js.map +1 -0
  28. package/dist/conversations/types.d.ts +147 -0
  29. package/dist/conversations/types.d.ts.map +1 -0
  30. package/dist/conversations/types.js +9 -0
  31. package/dist/conversations/types.js.map +1 -0
  32. package/dist/events/constructors.d.ts +9 -15
  33. package/dist/events/constructors.d.ts.map +1 -1
  34. package/dist/events/constructors.js +6 -12
  35. package/dist/events/constructors.js.map +1 -1
  36. package/dist/events/index.d.ts +2 -3
  37. package/dist/events/index.d.ts.map +1 -1
  38. package/dist/events/index.js +2 -3
  39. package/dist/events/index.js.map +1 -1
  40. package/dist/events/schema.d.ts +2 -5
  41. package/dist/events/schema.d.ts.map +1 -1
  42. package/dist/events/schema.js +2 -5
  43. package/dist/events/schema.js.map +1 -1
  44. package/dist/execution/contract.d.ts +51 -0
  45. package/dist/execution/contract.d.ts.map +1 -0
  46. package/dist/execution/contract.js +127 -0
  47. package/dist/execution/contract.js.map +1 -0
  48. package/dist/execution/index.d.ts +6 -0
  49. package/dist/execution/index.d.ts.map +1 -0
  50. package/dist/execution/index.js +4 -0
  51. package/dist/execution/index.js.map +1 -0
  52. package/dist/execution/revision.d.ts +5 -0
  53. package/dist/execution/revision.d.ts.map +1 -0
  54. package/dist/execution/revision.js +17 -0
  55. package/dist/execution/revision.js.map +1 -0
  56. package/dist/execution/store.d.ts +26 -0
  57. package/dist/execution/store.d.ts.map +1 -0
  58. package/dist/execution/store.js +38 -0
  59. package/dist/execution/store.js.map +1 -0
  60. package/dist/index.d.ts +8 -23
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +7 -23
  63. package/dist/index.js.map +1 -1
  64. package/dist/input/index.d.ts +3 -0
  65. package/dist/input/index.d.ts.map +1 -0
  66. package/dist/input/index.js +2 -0
  67. package/dist/input/index.js.map +1 -0
  68. package/dist/input/prompt.d.ts +17 -0
  69. package/dist/input/prompt.d.ts.map +1 -0
  70. package/dist/input/prompt.js +104 -0
  71. package/dist/input/prompt.js.map +1 -0
  72. package/dist/normalizers/claude.d.ts +6 -9
  73. package/dist/normalizers/claude.d.ts.map +1 -1
  74. package/dist/normalizers/claude.js +5 -8
  75. package/dist/normalizers/claude.js.map +1 -1
  76. package/dist/normalizers/codex.d.ts +5 -8
  77. package/dist/normalizers/codex.d.ts.map +1 -1
  78. package/dist/normalizers/codex.js +4 -7
  79. package/dist/normalizers/codex.js.map +1 -1
  80. package/dist/normalizers/cursor.d.ts +1 -2
  81. package/dist/normalizers/cursor.d.ts.map +1 -1
  82. package/dist/normalizers/cursor.js +2 -3
  83. package/dist/normalizers/cursor.js.map +1 -1
  84. package/dist/normalizers/index.d.ts +3 -5
  85. package/dist/normalizers/index.d.ts.map +1 -1
  86. package/dist/normalizers/index.js +3 -5
  87. package/dist/normalizers/index.js.map +1 -1
  88. package/dist/normalizers/opencode.d.ts +2 -4
  89. package/dist/normalizers/opencode.d.ts.map +1 -1
  90. package/dist/normalizers/opencode.js +2 -4
  91. package/dist/normalizers/opencode.js.map +1 -1
  92. package/dist/normalizers/pi.d.ts +3 -4
  93. package/dist/normalizers/pi.d.ts.map +1 -1
  94. package/dist/normalizers/pi.js +3 -4
  95. package/dist/normalizers/pi.js.map +1 -1
  96. package/dist/sessions/chat.d.ts +176 -0
  97. package/dist/sessions/chat.d.ts.map +1 -0
  98. package/dist/sessions/chat.js +701 -0
  99. package/dist/sessions/chat.js.map +1 -0
  100. package/dist/sessions/index.d.ts +3 -0
  101. package/dist/sessions/index.d.ts.map +1 -0
  102. package/dist/sessions/index.js +2 -0
  103. package/dist/sessions/index.js.map +1 -0
  104. package/dist/transcript/index.d.ts +1 -1
  105. package/dist/transcript/index.js +1 -1
  106. package/dist/transcript/message-reducer.d.ts +1 -1
  107. package/dist/transcript/message-reducer.js +1 -1
  108. package/dist/transcript/part-accumulator.d.ts +1 -1
  109. package/dist/transcript/part-accumulator.js +1 -1
  110. package/dist/transcript/tool-display.d.ts +1 -1
  111. package/dist/transcript/tool-display.js +1 -1
  112. package/dist/transcript/tool-shape.d.ts +1 -1
  113. package/dist/transcript/tool-shape.js +1 -1
  114. package/dist/usage/finalize.d.ts +7 -9
  115. package/dist/usage/finalize.d.ts.map +1 -1
  116. package/dist/usage/finalize.js +11 -13
  117. package/dist/usage/finalize.js.map +1 -1
  118. package/dist/usage/index.d.ts +2 -3
  119. package/dist/usage/index.d.ts.map +1 -1
  120. package/dist/usage/index.js +2 -3
  121. package/dist/usage/index.js.map +1 -1
  122. package/dist/usage/model-catalog.d.ts +2 -3
  123. package/dist/usage/model-catalog.d.ts.map +1 -1
  124. package/dist/usage/model-catalog.js +10 -3
  125. package/dist/usage/model-catalog.js.map +1 -1
  126. package/dist/usage/normalize.d.ts +5 -7
  127. package/dist/usage/normalize.d.ts.map +1 -1
  128. package/dist/usage/normalize.js +3 -5
  129. package/dist/usage/normalize.js.map +1 -1
  130. package/dist/usage/ports.d.ts +14 -24
  131. package/dist/usage/ports.d.ts.map +1 -1
  132. package/dist/usage/ports.js +9 -17
  133. package/dist/usage/ports.js.map +1 -1
  134. package/dist/usage/steps.d.ts +70 -0
  135. package/dist/usage/steps.d.ts.map +1 -0
  136. package/dist/usage/steps.js +152 -0
  137. package/dist/usage/steps.js.map +1 -0
  138. package/dist/usage/tokens.d.ts +4 -7
  139. package/dist/usage/tokens.d.ts.map +1 -1
  140. package/dist/usage/tokens.js +4 -7
  141. package/dist/usage/tokens.js.map +1 -1
  142. package/docs/contracts/acp-and-step-usage.md +51 -0
  143. package/docs/contracts/conversation-persistence.md +47 -0
  144. package/docs/contracts/input-and-execution.md +177 -0
  145. package/docs/contracts/platform-authorization.md +17 -0
  146. package/host/adapters/acp-capabilities.js +30 -0
  147. package/host/adapters/acp-client.js +156 -0
  148. package/host/adapters/acp.js +284 -0
  149. package/host/adapters/claude.js +235 -0
  150. package/host/adapters/codex.js +733 -0
  151. package/host/adapters/cursor.js +347 -0
  152. package/host/adapters/input-capabilities.js +89 -0
  153. package/host/adapters/opencode.js +486 -0
  154. package/host/adapters/pi.js +519 -0
  155. package/host/adapters/prompt.js +212 -0
  156. package/host/auth.js +257 -0
  157. package/host/credentials/store.js +209 -0
  158. package/host/http.d.ts +32 -0
  159. package/host/http.js +238 -0
  160. package/host/index.d.ts +84 -0
  161. package/host/index.js +59 -0
  162. package/host/lib/runtime-paths.js +44 -0
  163. package/host/model-resolution.js +313 -0
  164. package/host/native-contract.js +185 -0
  165. package/host/native-runtime.js +194 -0
  166. package/host/permissions.js +27 -0
  167. package/host/platform-egress.js +114 -0
  168. package/host/recorder.js +61 -0
  169. package/host/sqlite/index.d.ts +69 -0
  170. package/host/sqlite/index.js +704 -0
  171. package/host/tooling/mcp-bundle.js +214 -0
  172. package/host/tooling/mcp-relay.js +83 -0
  173. package/host/tooling/native-binaries.js +667 -0
  174. package/host/tooling/native-config.js +460 -0
  175. package/host/tooling/package-import.js +111 -0
  176. package/host/tooling/runtime-home.js +26 -0
  177. package/host/tooling/system-instructions.js +69 -0
  178. package/host/tooling/system-prompt.js +35 -0
  179. package/package.json +45 -21
  180. package/skills/chat/SKILL.md +97 -0
  181. package/skills/chat/agents/openai.yaml +4 -0
  182. package/skills/chat/references/contracts.md +131 -0
  183. package/dist/api/chat-payloads.d.ts +0 -10
  184. package/dist/api/chat-payloads.d.ts.map +0 -1
  185. package/dist/api/chat-payloads.js +0 -57
  186. package/dist/api/chat-payloads.js.map +0 -1
  187. package/dist/contract/chat-input.d.ts +0 -144
  188. package/dist/contract/chat-input.d.ts.map +0 -1
  189. package/dist/contract/chat-input.js +0 -261
  190. package/dist/contract/chat-input.js.map +0 -1
  191. package/dist/contract/create.d.ts +0 -191
  192. package/dist/contract/create.d.ts.map +0 -1
  193. package/dist/contract/create.js +0 -249
  194. package/dist/contract/create.js.map +0 -1
  195. package/dist/contract/freeze.d.ts +0 -85
  196. package/dist/contract/freeze.d.ts.map +0 -1
  197. package/dist/contract/freeze.js +0 -151
  198. package/dist/contract/freeze.js.map +0 -1
  199. package/dist/contract/harnesses.d.ts +0 -21
  200. package/dist/contract/harnesses.d.ts.map +0 -1
  201. package/dist/contract/harnesses.js +0 -39
  202. package/dist/contract/harnesses.js.map +0 -1
  203. package/dist/contract/index.d.ts +0 -17
  204. package/dist/contract/index.d.ts.map +0 -1
  205. package/dist/contract/index.js +0 -17
  206. package/dist/contract/index.js.map +0 -1
  207. package/dist/contract/input.d.ts +0 -45
  208. package/dist/contract/input.d.ts.map +0 -1
  209. package/dist/contract/input.js +0 -78
  210. package/dist/contract/input.js.map +0 -1
  211. package/dist/contract/models.d.ts +0 -57
  212. package/dist/contract/models.d.ts.map +0 -1
  213. package/dist/contract/models.js +0 -380
  214. package/dist/contract/models.js.map +0 -1
  215. package/dist/contract/payload-merge.d.ts +0 -76
  216. package/dist/contract/payload-merge.d.ts.map +0 -1
  217. package/dist/contract/payload-merge.js +0 -215
  218. package/dist/contract/payload-merge.js.map +0 -1
  219. package/dist/events/tool-display.d.ts +0 -22
  220. package/dist/events/tool-display.d.ts.map +0 -1
  221. package/dist/events/tool-display.js +0 -60
  222. package/dist/events/tool-display.js.map +0 -1
  223. package/dist/events/tool-shape.d.ts +0 -48
  224. package/dist/events/tool-shape.d.ts.map +0 -1
  225. package/dist/events/tool-shape.js +0 -225
  226. package/dist/events/tool-shape.js.map +0 -1
  227. package/dist/turns/index.d.ts +0 -11
  228. package/dist/turns/index.d.ts.map +0 -1
  229. package/dist/turns/index.js +0 -11
  230. package/dist/turns/index.js.map +0 -1
  231. package/dist/turns/queries.d.ts +0 -36
  232. package/dist/turns/queries.d.ts.map +0 -1
  233. package/dist/turns/queries.js +0 -37
  234. package/dist/turns/queries.js.map +0 -1
  235. package/dist/turns/replay.d.ts +0 -105
  236. package/dist/turns/replay.d.ts.map +0 -1
  237. package/dist/turns/replay.js +0 -163
  238. package/dist/turns/replay.js.map +0 -1
  239. package/dist/turns/status.d.ts +0 -49
  240. package/dist/turns/status.d.ts.map +0 -1
  241. package/dist/turns/status.js +0 -56
  242. package/dist/turns/status.js.map +0 -1
  243. package/dist/turns/turn-store.d.ts +0 -109
  244. package/dist/turns/turn-store.d.ts.map +0 -1
  245. package/dist/turns/turn-store.js +0 -169
  246. package/dist/turns/turn-store.js.map +0 -1
  247. package/dist/warm/client-cache.d.ts +0 -47
  248. package/dist/warm/client-cache.d.ts.map +0 -1
  249. package/dist/warm/client-cache.js +0 -48
  250. package/dist/warm/client-cache.js.map +0 -1
  251. package/dist/warm/envelope-cache.d.ts +0 -79
  252. package/dist/warm/envelope-cache.d.ts.map +0 -1
  253. package/dist/warm/envelope-cache.js +0 -85
  254. package/dist/warm/envelope-cache.js.map +0 -1
  255. package/dist/warm/index.d.ts +0 -18
  256. package/dist/warm/index.d.ts.map +0 -1
  257. package/dist/warm/index.js +0 -12
  258. package/dist/warm/index.js.map +0 -1
  259. package/dist/warm/payload.d.ts +0 -137
  260. package/dist/warm/payload.d.ts.map +0 -1
  261. package/dist/warm/payload.js +0 -112
  262. package/dist/warm/payload.js.map +0 -1
  263. package/dist/warm/revision.d.ts +0 -70
  264. package/dist/warm/revision.d.ts.map +0 -1
  265. package/dist/warm/revision.js +0 -63
  266. package/dist/warm/revision.js.map +0 -1
  267. package/dist/warm/store-policy.d.ts +0 -57
  268. package/dist/warm/store-policy.d.ts.map +0 -1
  269. package/dist/warm/store-policy.js +0 -89
  270. package/dist/warm/store-policy.js.map +0 -1
  271. package/dist/warm/store.d.ts +0 -42
  272. package/dist/warm/store.d.ts.map +0 -1
  273. package/dist/warm/store.js +0 -69
  274. package/dist/warm/store.js.map +0 -1
  275. package/dist/wire/frames.d.ts +0 -208
  276. package/dist/wire/frames.d.ts.map +0 -1
  277. package/dist/wire/frames.js +0 -245
  278. package/dist/wire/frames.js.map +0 -1
  279. package/dist/wire/index.d.ts +0 -7
  280. package/dist/wire/index.d.ts.map +0 -1
  281. package/dist/wire/index.js +0 -7
  282. package/dist/wire/index.js.map +0 -1
@@ -0,0 +1,486 @@
1
+ // OpenCode serve + SDK-over-HTTP/SSE adapter. The vendor SDK is imported
2
+ // lazily because it is needed only when an OpenCode session starts.
3
+
4
+ import { spawn, spawnSync } from 'node:child_process';
5
+ import net from 'node:net';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { createEventConstructors } from '../../dist/events/index.js';
9
+ import { createOpenCodeNormalizer } from '../../dist/normalizers/opencode.js';
10
+ import { recordNativeEvent } from '../recorder.js';
11
+ import { toOpenCodeMcpConfig } from '../tooling/mcp-bundle.js';
12
+ import { bundledOpenCodeBinary, executableExists, findOnPath } from '../tooling/native-binaries.js';
13
+ import { importPackage } from '../tooling/package-import.js';
14
+ import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
15
+ import { composeSystemPrompt } from '../tooling/system-prompt.js';
16
+ import { requestPermission } from '../permissions.js';
17
+ import { nativeInputCapabilities, UnsupportedExecutionInputError } from './input-capabilities.js';
18
+ import { openCodePromptParts } from './prompt.js';
19
+
20
+ const events = createEventConstructors({ clockMs: () => Date.now() });
21
+ const { done, errorEvent, reasoningStarted } = events;
22
+ const { normalizeOpenCodeEvent, normalizeOpenCodePromptResult } = createOpenCodeNormalizer(events);
23
+
24
+ function splitModel(model, auth) {
25
+ const raw = String(model || '');
26
+ if (auth?.method === 'byok' && auth?.providerKind === 'openai_compatible') {
27
+ const bare = raw.replace(/^vercel\//, '').replace(/^openai\//, '');
28
+ const modelID = bare.startsWith('openai/') ? bare.slice('openai/'.length) : bare;
29
+ return { providerID: 'openai', modelID };
30
+ }
31
+ if (auth?.method === 'byok' && auth?.providerKind === 'anthropic_compatible') {
32
+ const bare = raw.replace(/^vercel\//, '').replace(/^anthropic\//, '');
33
+ const modelID = bare.startsWith('anthropic/') ? bare.slice('anthropic/'.length) : bare;
34
+ return { providerID: 'anthropic', modelID };
35
+ }
36
+ if (raw.startsWith('vercel/')) {
37
+ return { providerID: 'vercel', modelID: raw.slice('vercel/'.length) };
38
+ }
39
+ const parts = raw.split('/');
40
+ if (parts.length >= 2) return { providerID: parts[0], modelID: parts.slice(1).join('/') };
41
+ if (!raw) throw new Error('OpenCode model is required');
42
+ return { providerID: 'openai', modelID: raw.replace(/^openai\//, '') };
43
+ }
44
+
45
+ function selectedVariant(contract) {
46
+ const setting = contract.modelSettings?.find((candidate) => candidate.configId === 'variant');
47
+ if (!setting) return null;
48
+ if (typeof setting.value !== 'string' || !setting.value.trim()) {
49
+ throw new UnsupportedExecutionInputError(
50
+ 'opencode',
51
+ 'model.settings.variant',
52
+ 'OpenCode requires a non-empty model-catalog variant id',
53
+ );
54
+ }
55
+ return setting.value.trim();
56
+ }
57
+
58
+ function configFor(contract) {
59
+ const model = splitModel(contract.cliModel || contract.usageModelId, contract.auth);
60
+ const variant = selectedVariant(contract);
61
+ const systemPrompt = composeSystemPrompt(contract);
62
+ const permission = contract.permissionMode === 'full_access' ? 'allow' : 'ask';
63
+ const config = {
64
+ logLevel: process.env.OPENCODE_LOG_LEVEL || 'ERROR',
65
+ enabled_providers: [model.providerID],
66
+ disabled_providers: [],
67
+ model: `${model.providerID}/${model.modelID}`,
68
+ small_model: `${model.providerID}/${model.modelID}`,
69
+ share: 'disabled',
70
+ autoupdate: false,
71
+ snapshot: false,
72
+ formatter: false,
73
+ lsp: false,
74
+ permission: {
75
+ '*': permission,
76
+ bash: permission,
77
+ edit: permission,
78
+ webfetch: permission,
79
+ question: permission,
80
+ plan_enter: permission,
81
+ plan_exit: permission,
82
+ external_directory: permission,
83
+ },
84
+ agent: {
85
+ build: {
86
+ mode: 'primary',
87
+ model: `${model.providerID}/${model.modelID}`,
88
+ ...(variant ? { variant } : {}),
89
+ prompt: systemPrompt || 'You are an AI coding assistant running inside Amalgm. Answer the user directly, use tools when useful, and keep working directory changes scoped to the request. Do not reveal private planning or narrate intent with phrases like "The user is asking"; start with the answer or action.',
90
+ permission: {
91
+ edit: permission,
92
+ bash: permission,
93
+ webfetch: permission,
94
+ external_directory: permission,
95
+ },
96
+ },
97
+ title: { disable: true },
98
+ },
99
+ mcp: toOpenCodeMcpConfig(contract),
100
+ };
101
+ if (contract.authMethod !== 'provider_auth') {
102
+ config.provider = {
103
+ [model.providerID]: {
104
+ options: {
105
+ baseURL: contract.auth.baseUrl,
106
+ baseUrl: contract.auth.baseUrl,
107
+ apiKey: contract.auth.tokenRef,
108
+ timeout: 300000,
109
+ chunkTimeout: 300000,
110
+ },
111
+ },
112
+ };
113
+ }
114
+ return config;
115
+ }
116
+
117
+ function resultData(result) {
118
+ if (result?.error) throw new Error(JSON.stringify(result.error));
119
+ return result?.data;
120
+ }
121
+
122
+ function resolveBinary() {
123
+ const home = process.env.HOME || os.homedir();
124
+ const candidates = [
125
+ process.env.OPENCODE_BINARY,
126
+ bundledOpenCodeBinary(),
127
+ path.join(home, '.npm-global/bin/opencode'),
128
+ '/opt/homebrew/bin/opencode',
129
+ '/usr/local/bin/opencode',
130
+ findOnPath('opencode'),
131
+ ].filter(Boolean);
132
+ const failures = [];
133
+ for (const candidate of candidates) {
134
+ if (!executableExists(candidate)) {
135
+ failures.push(`${candidate} (missing or not executable)`);
136
+ continue;
137
+ }
138
+ const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
139
+ if (res.status === 0) return candidate;
140
+ failures.push(`${candidate} (--version failed: ${res.error?.message || res.status || res.signal || 'unknown'})`);
141
+ }
142
+ throw new Error(`OpenCode CLI binary is not available. Checked: ${failures.join(', ') || 'no candidates'}. Reinstall Amalgm or run amalgm update so the bundled OpenCode native package is present.`);
143
+ }
144
+
145
+ async function startOpenCodeServer(contract) {
146
+ const runtime = prepareHarnessRuntime(contract);
147
+ const binary = resolveBinary();
148
+ const config = configFor(contract);
149
+ const port = await freePort();
150
+ const env = {
151
+ ...runtime.env,
152
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
153
+ };
154
+ const args = ['serve', '--pure', '--hostname=127.0.0.1', `--port=${port}`];
155
+ if (config.logLevel) args.push(`--log-level=${config.logLevel}`);
156
+ const child = spawn(binary, args, {
157
+ cwd: contract.cwd,
158
+ env,
159
+ stdio: ['ignore', 'pipe', 'pipe'],
160
+ detached: process.platform !== 'win32',
161
+ windowsHide: true,
162
+ });
163
+ let output = '';
164
+ let settled = false;
165
+ const url = await new Promise((resolve, reject) => {
166
+ const timer = setTimeout(() => {
167
+ settled = true;
168
+ try { child.kill('SIGTERM'); } catch {}
169
+ reject(new Error(`Timeout waiting for opencode server to start\n${output.slice(-1000)}`));
170
+ }, 10000);
171
+ const onData = (chunk) => {
172
+ const text = chunk.toString();
173
+ output += text;
174
+ if (settled && process.env.AMALGM_CHAT_DEBUG_OPENCODE === '1' && text.trim()) console.warn('[OpenCode]', text.trim());
175
+ for (const line of output.split('\n')) {
176
+ if (!line.startsWith('opencode server listening')) continue;
177
+ const match = line.match(/on\s+(https?:\/\/[^\s]+)/);
178
+ if (!match) continue;
179
+ clearTimeout(timer);
180
+ settled = true;
181
+ resolve(match[1]);
182
+ return;
183
+ }
184
+ };
185
+ child.stdout.on('data', onData);
186
+ child.stderr.on('data', (chunk) => {
187
+ const text = chunk.toString();
188
+ output += text;
189
+ if (settled && process.env.AMALGM_CHAT_DEBUG_OPENCODE === '1' && text.trim()) console.warn('[OpenCode]', text.trim());
190
+ });
191
+ child.on('error', (err) => {
192
+ if (settled) return;
193
+ clearTimeout(timer);
194
+ settled = true;
195
+ reject(err);
196
+ });
197
+ child.on('exit', (code, signal) => {
198
+ if (settled) return;
199
+ clearTimeout(timer);
200
+ settled = true;
201
+ reject(new Error(`opencode server exited (${code ?? signal ?? 'unknown'})\n${output.slice(-1000)}`));
202
+ });
203
+ });
204
+ return {
205
+ url,
206
+ close() {
207
+ try { process.kill(-child.pid, 'SIGTERM'); } catch {}
208
+ try { child.kill('SIGTERM'); } catch {}
209
+ setTimeout(() => {
210
+ try { process.kill(-child.pid, 'SIGKILL'); } catch {}
211
+ try { child.kill('SIGKILL'); } catch {}
212
+ }, 1500).unref?.();
213
+ },
214
+ };
215
+ }
216
+
217
+ function freePort() {
218
+ return new Promise((resolve, reject) => {
219
+ const server = net.createServer();
220
+ server.listen(0, '127.0.0.1', () => {
221
+ const address = server.address();
222
+ const port = typeof address === 'object' && address ? address.port : 0;
223
+ server.close(() => resolve(port));
224
+ });
225
+ server.on('error', reject);
226
+ });
227
+ }
228
+
229
+ function timeout(ms) {
230
+ return new Promise((resolve) => setTimeout(resolve, ms));
231
+ }
232
+
233
+ function isCompactCommand(text) {
234
+ return String(text || '').trim().toLowerCase() === '/compact';
235
+ }
236
+
237
+ class OpenCodeAdapter {
238
+ constructor(options = {}) {
239
+ this.permissionPort = options.requestPermission || null;
240
+ this.inputCapabilities = nativeInputCapabilities({
241
+ authModes: ['platform', 'subscription', 'byok'],
242
+ modelSettings: ['variant'],
243
+ tools: true,
244
+ });
245
+ }
246
+
247
+ validatePrepared(contract) {
248
+ selectedVariant(contract);
249
+ }
250
+
251
+ async startInstance(contract) {
252
+ const sdk = await importPackage('@opencode-ai/sdk');
253
+ const server = await startOpenCodeServer(contract);
254
+ const client = sdk.createOpencodeClient({ baseUrl: server.url });
255
+ return { client, server, closed: false };
256
+ }
257
+
258
+ async create(contract) {
259
+ const instance = await this.startInstance(contract);
260
+ let created = null;
261
+ if (contract.providerSessionId) {
262
+ try {
263
+ created = resultData(await instance.client.session.get({
264
+ path: { id: contract.providerSessionId },
265
+ query: { directory: contract.cwd },
266
+ }));
267
+ } catch (err) {
268
+ console.warn('[OpenCode] Failed to resume persisted session; creating a new native session:', err.message);
269
+ }
270
+ }
271
+ if (!created) {
272
+ created = resultData(await instance.client.session.create({
273
+ body: { title: `Amalgm ${contract.sessionId}` },
274
+ query: { directory: contract.cwd },
275
+ }));
276
+ }
277
+ if (!created?.id) throw new Error('OpenCode SDK did not return a session id');
278
+ return {
279
+ sessionId: contract.sessionId,
280
+ providerSessionId: created.id,
281
+ instance,
282
+ abortController: null,
283
+ turnCount: 0,
284
+ };
285
+ }
286
+
287
+ async resume(session, contract) {
288
+ if (!session.instance || session.instance.closed) {
289
+ session.instance = await this.startInstance(contract);
290
+ }
291
+ return session;
292
+ }
293
+
294
+ async stop(session) {
295
+ session.abortController?.abort();
296
+ await Promise.race([session.instance.client.session.abort({
297
+ path: { id: session.providerSessionId },
298
+ query: { directory: session.cwd },
299
+ }).catch(() => {}), timeout(1500)]);
300
+ }
301
+
302
+ async destroy(session) {
303
+ await this.stop(session).catch(() => {});
304
+ session.instance?.server?.close();
305
+ if (session.instance) session.instance.closed = true;
306
+ }
307
+
308
+ async *prompt(session, input, contract) {
309
+ session.cwd = contract.cwd;
310
+ const controller = new AbortController();
311
+ session.abortController = controller;
312
+ const state = {
313
+ providerSessionId: session.providerSessionId,
314
+ userMessageId: null,
315
+ turnStartedAt: Date.now(),
316
+ contextLimit: Number(contract.contextWindow || 0) || undefined,
317
+ messageRoles: new Map(),
318
+ partTypes: new Map(),
319
+ partText: new Map(),
320
+ toolsStarted: new Set(),
321
+ toolsCompleted: new Set(),
322
+ assistantMessageIds: new Set(),
323
+ finishedAssistantIds: new Set(),
324
+ usageEmittedForAssistant: new Set(),
325
+ sawTurnActivity: false,
326
+ sawBusy: false,
327
+ doneEmitted: false,
328
+ };
329
+ const queue = [];
330
+ let complete = false;
331
+ let promptSettled = false;
332
+ let wake = null;
333
+ let promptController = null;
334
+ const wakeLoop = () => { if (wake) { wake(); wake = null; } };
335
+ const push = (e) => { queue.push(e); wakeLoop(); };
336
+ const pushNormalized = (events) => {
337
+ for (const e of events) {
338
+ if (e.type === 'done' || e.type === 'error') complete = true;
339
+ push(e);
340
+ }
341
+ };
342
+ push(reasoningStarted({ providerSessionId: session.providerSessionId }));
343
+ const finishCancelled = () => {
344
+ if (complete || state.doneEmitted) return;
345
+ state.doneEmitted = true;
346
+ complete = true;
347
+ push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
348
+ try { promptController?.abort(); } catch {}
349
+ };
350
+ controller.signal.addEventListener('abort', finishCancelled, { once: true });
351
+ const usePromptAsync = typeof session.instance.client.session.promptAsync === 'function';
352
+ const events = await session.instance.client.global.event({
353
+ signal: controller.signal,
354
+ sseMaxRetryAttempts: 0,
355
+ });
356
+ const streamTask = (async () => {
357
+ try {
358
+ for await (const raw of events.stream) {
359
+ const payload = raw?.payload || raw;
360
+ recordNativeEvent('opencode.sdk.event', raw, {
361
+ providerSessionId: session.providerSessionId,
362
+ sessionId: contract.sessionId,
363
+ assistantMessageId: contract.assistantMessageId,
364
+ eventType: payload?.type,
365
+ });
366
+ if (process.env.AMALGM_CHAT_DEBUG_OPENCODE_EVENTS === '1') {
367
+ console.warn('[OpenCode:event]', JSON.stringify(raw).slice(0, 4000));
368
+ }
369
+ if (payload?.type === 'permission.updated' || payload?.type === 'permission.asked') {
370
+ const permission = payload.properties || {};
371
+ if (permission.sessionID === session.providerSessionId) {
372
+ const decision = await requestPermission(this.permissionPort, {
373
+ adapterId: 'opencode',
374
+ providerSessionId: session.providerSessionId,
375
+ kind: permission.type || permission.permission || 'tool',
376
+ title: permission.title || `OpenCode requests ${permission.type || permission.permission || 'permission'}`,
377
+ input: permission.metadata || {},
378
+ permission,
379
+ });
380
+ const response = decision.outcome === 'allow_always'
381
+ ? 'always'
382
+ : decision.outcome === 'allow_once'
383
+ ? 'once'
384
+ : 'reject';
385
+ await session.instance.client.postSessionIdPermissionsPermissionId({
386
+ path: { id: session.providerSessionId, permissionID: permission.id },
387
+ query: { directory: contract.cwd },
388
+ body: { response },
389
+ });
390
+ }
391
+ continue;
392
+ }
393
+ pushNormalized(normalizeOpenCodeEvent(raw, state));
394
+ if (complete) break;
395
+ }
396
+ wakeLoop();
397
+ } catch (err) {
398
+ if (!controller.signal.aborted) {
399
+ if (process.env.AMALGM_CHAT_DEBUG_OPENCODE === '1') {
400
+ console.warn('[OpenCode] event stream ended:', err.message);
401
+ }
402
+ }
403
+ wakeLoop();
404
+ }
405
+ })();
406
+ const model = splitModel(contract.cliModel || contract.usageModelId, contract.auth);
407
+ const variant = selectedVariant(contract);
408
+ promptController = new AbortController();
409
+ const promptMethod = usePromptAsync ? 'promptAsync' : 'prompt';
410
+ const compactCommand = isCompactCommand(input?.text);
411
+ const promptPromise = compactCommand
412
+ ? session.instance.client.session.summarize({
413
+ path: { id: session.providerSessionId },
414
+ query: { directory: contract.cwd },
415
+ signal: promptController.signal,
416
+ body: model,
417
+ })
418
+ : session.instance.client.session[promptMethod]({
419
+ path: { id: session.providerSessionId },
420
+ query: { directory: contract.cwd },
421
+ signal: promptController.signal,
422
+ body: {
423
+ model,
424
+ ...(variant ? { variant } : {}),
425
+ agent: process.env.AMALGM_CHAT_OPENCODE_AGENT || 'build',
426
+ system: composeSystemPrompt(contract) || undefined,
427
+ parts: openCodePromptParts(input),
428
+ },
429
+ });
430
+ promptPromise.then((result) => {
431
+ const data = resultData(result);
432
+ recordNativeEvent(compactCommand ? 'opencode.sdk.summarize_result' : `opencode.sdk.${promptMethod}_result`, data, {
433
+ providerSessionId: session.providerSessionId,
434
+ sessionId: contract.sessionId,
435
+ assistantMessageId: contract.assistantMessageId,
436
+ });
437
+ if (compactCommand) {
438
+ setTimeout(() => {
439
+ if (!state.doneEmitted) {
440
+ state.doneEmitted = true;
441
+ pushNormalized([done({ providerSessionId: session.providerSessionId, stopReason: 'end_turn' })]);
442
+ }
443
+ }, 500).unref?.();
444
+ } else if (!usePromptAsync) {
445
+ pushNormalized(normalizeOpenCodePromptResult(data, state));
446
+ if (!state.doneEmitted) {
447
+ state.doneEmitted = true;
448
+ pushNormalized([done({ providerSessionId: session.providerSessionId, stopReason: 'end_turn' })]);
449
+ }
450
+ }
451
+ promptSettled = true;
452
+ wakeLoop();
453
+ return data;
454
+ }).catch((err) => {
455
+ promptSettled = true;
456
+ if (promptController.signal.aborted) {
457
+ wakeLoop();
458
+ return null;
459
+ }
460
+ if (!state.doneEmitted) {
461
+ state.doneEmitted = true;
462
+ push(errorEvent(err.message, { providerSessionId: session.providerSessionId }));
463
+ push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
464
+ }
465
+ complete = true;
466
+ wakeLoop();
467
+ });
468
+ try {
469
+ while ((!complete && (compactCommand || usePromptAsync || !promptSettled)) || queue.length) {
470
+ if (!queue.length) await new Promise((resolve) => { wake = resolve; });
471
+ else yield queue.shift();
472
+ }
473
+ } finally {
474
+ controller.abort();
475
+ promptController.abort();
476
+ session.abortController = null;
477
+ await Promise.race([promptPromise.catch(() => {}), timeout(1500)]);
478
+ await Promise.race([streamTask.catch(() => {}), timeout(1500)]);
479
+ session.turnCount = (session.turnCount || 0) + 1;
480
+ }
481
+ }
482
+ }
483
+
484
+ const __private = { configFor, selectedVariant };
485
+
486
+ export { OpenCodeAdapter, splitModel, __private };