amalgm 0.0.0 → 0.0.1

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 (107) hide show
  1. package/README.md +37 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1000 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +467 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +29 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +306 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +128 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +165 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +141 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +138 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +98 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +393 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +80 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
  53. package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
  54. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  55. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  56. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  57. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  58. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  59. package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  61. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
  63. package/runtime/scripts/chat-core/adapters/claude.js +163 -0
  64. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  65. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  66. package/runtime/scripts/chat-core/auth.js +177 -0
  67. package/runtime/scripts/chat-core/contract.js +326 -0
  68. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  69. package/runtime/scripts/chat-core/egress.js +87 -0
  70. package/runtime/scripts/chat-core/engine.js +195 -0
  71. package/runtime/scripts/chat-core/event-schema.js +231 -0
  72. package/runtime/scripts/chat-core/events.js +190 -0
  73. package/runtime/scripts/chat-core/index.js +11 -0
  74. package/runtime/scripts/chat-core/input.js +50 -0
  75. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  76. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  77. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  78. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  79. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  80. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  81. package/runtime/scripts/chat-core/parts.js +253 -0
  82. package/runtime/scripts/chat-core/recorder.js +65 -0
  83. package/runtime/scripts/chat-core/runtime.js +86 -0
  84. package/runtime/scripts/chat-core/server.js +163 -0
  85. package/runtime/scripts/chat-core/sse.js +196 -0
  86. package/runtime/scripts/chat-core/stores.js +100 -0
  87. package/runtime/scripts/chat-core/tool-display.js +149 -0
  88. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  89. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  90. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  91. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  92. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  93. package/runtime/scripts/chat-core/usage.js +343 -0
  94. package/runtime/scripts/chat-server/config.js +110 -0
  95. package/runtime/scripts/chat-server/db.js +529 -0
  96. package/runtime/scripts/chat-server/index.js +33 -0
  97. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  98. package/runtime/scripts/chat-server.js +75 -0
  99. package/runtime/scripts/credential-adapter.js +129 -0
  100. package/runtime/scripts/fs-watcher.js +888 -0
  101. package/runtime/scripts/local-gateway.js +852 -0
  102. package/runtime/scripts/platform-context.txt +246 -0
  103. package/runtime/scripts/port-monitor.js +175 -0
  104. package/runtime/scripts/proxy-token-store.js +162 -0
  105. package/runtime/scripts/runtime-auth.js +163 -0
  106. package/runtime/scripts/test-claude-code-models.js +87 -0
  107. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,412 @@
1
+ 'use strict';
2
+
3
+ const { spawn, spawnSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const net = require('net');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { done, errorEvent, reasoningStarted } = require('../events');
9
+ const { openCodeParts } = require('../input');
10
+ const { runtimeEnv } = require('../auth');
11
+ const { normalizeOpenCodeEvent, normalizeOpenCodePromptResult } = require('../normalizers/opencode');
12
+ const { recordNativeEvent } = require('../recorder');
13
+ const { toOpenCodeMcpConfig } = require('../tooling/mcp-bundle');
14
+ const { bundledOpenCodeBinary, executableExists } = require('../tooling/native-binaries');
15
+ const { composeSystemPrompt } = require('../tooling/system-prompt');
16
+
17
+ function splitModel(model, auth) {
18
+ const raw = String(model || '');
19
+ if (auth?.method === 'byok' && auth?.providerKind === 'openai_compatible') {
20
+ const bare = raw.replace(/^vercel\//, '').replace(/^openai\//, '');
21
+ const modelID = bare.startsWith('openai/') ? bare.slice('openai/'.length) : bare;
22
+ return { providerID: 'openai', modelID };
23
+ }
24
+ if (auth?.method === 'byok' && auth?.providerKind === 'anthropic_compatible') {
25
+ const bare = raw.replace(/^vercel\//, '').replace(/^anthropic\//, '');
26
+ const modelID = bare.startsWith('anthropic/') ? bare.slice('anthropic/'.length) : bare;
27
+ return { providerID: 'anthropic', modelID };
28
+ }
29
+ if (raw.startsWith('vercel/')) {
30
+ return { providerID: 'vercel', modelID: raw.slice('vercel/'.length) };
31
+ }
32
+ const parts = raw.split('/');
33
+ if (parts.length >= 2) return { providerID: parts[0], modelID: parts.slice(1).join('/') };
34
+ if (!raw) throw new Error('OpenCode model is required');
35
+ return { providerID: 'openai', modelID: raw.replace(/^openai\//, '') };
36
+ }
37
+
38
+ function configFor(contract) {
39
+ const model = splitModel(contract.cliModel || contract.usageModelId, contract.auth);
40
+ const systemPrompt = composeSystemPrompt(contract);
41
+ return {
42
+ logLevel: process.env.OPENCODE_LOG_LEVEL || 'ERROR',
43
+ enabled_providers: [model.providerID],
44
+ disabled_providers: [],
45
+ model: `${model.providerID}/${model.modelID}`,
46
+ small_model: `${model.providerID}/${model.modelID}`,
47
+ share: 'disabled',
48
+ autoupdate: false,
49
+ snapshot: false,
50
+ formatter: false,
51
+ lsp: false,
52
+ permission: {
53
+ '*': 'allow',
54
+ bash: 'allow',
55
+ edit: 'allow',
56
+ webfetch: 'allow',
57
+ question: 'allow',
58
+ plan_enter: 'allow',
59
+ plan_exit: 'allow',
60
+ external_directory: 'allow',
61
+ },
62
+ agent: {
63
+ build: {
64
+ mode: 'primary',
65
+ model: `${model.providerID}/${model.modelID}`,
66
+ 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.',
67
+ permission: {
68
+ edit: 'allow',
69
+ bash: 'allow',
70
+ webfetch: 'allow',
71
+ external_directory: 'allow',
72
+ },
73
+ },
74
+ title: { disable: true },
75
+ },
76
+ mcp: toOpenCodeMcpConfig(contract),
77
+ provider: {
78
+ [model.providerID]: {
79
+ options: {
80
+ baseURL: contract.auth.baseUrl,
81
+ baseUrl: contract.auth.baseUrl,
82
+ apiKey: contract.auth.tokenRef,
83
+ timeout: 300000,
84
+ chunkTimeout: 300000,
85
+ },
86
+ },
87
+ },
88
+ };
89
+ }
90
+
91
+ function resultData(result) {
92
+ if (result?.error) throw new Error(JSON.stringify(result.error));
93
+ return result?.data;
94
+ }
95
+
96
+ function resolveBinary() {
97
+ const home = process.env.HOME || os.homedir();
98
+ const candidates = [
99
+ process.env.OPENCODE_BINARY,
100
+ bundledOpenCodeBinary(),
101
+ path.join(home, '.npm-global/bin/opencode'),
102
+ '/opt/homebrew/bin/opencode',
103
+ '/usr/local/bin/opencode',
104
+ 'opencode',
105
+ ].filter(Boolean);
106
+ for (const candidate of candidates) {
107
+ if (candidate.includes('/') && !executableExists(candidate)) continue;
108
+ const res = spawnSync(candidate, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
109
+ if (res.status === 0) return candidate;
110
+ }
111
+ return 'opencode';
112
+ }
113
+
114
+ async function startOpenCodeServer(contract) {
115
+ const binary = resolveBinary();
116
+ const config = configFor(contract);
117
+ const port = await freePort();
118
+ const env = {
119
+ ...runtimeEnv(contract),
120
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
121
+ };
122
+ const args = ['serve', '--pure', '--hostname=127.0.0.1', `--port=${port}`];
123
+ if (config.logLevel) args.push(`--log-level=${config.logLevel}`);
124
+ const child = spawn(binary, args, {
125
+ cwd: contract.cwd,
126
+ env,
127
+ stdio: ['ignore', 'pipe', 'pipe'],
128
+ detached: process.platform !== 'win32',
129
+ windowsHide: true,
130
+ });
131
+ let output = '';
132
+ let settled = false;
133
+ const url = await new Promise((resolve, reject) => {
134
+ const timer = setTimeout(() => {
135
+ settled = true;
136
+ try { child.kill('SIGTERM'); } catch {}
137
+ reject(new Error(`Timeout waiting for opencode server to start\n${output.slice(-1000)}`));
138
+ }, 10000);
139
+ const onData = (chunk) => {
140
+ const text = chunk.toString();
141
+ output += text;
142
+ if (settled && process.env.CHAT_CORE_DEBUG_OPENCODE === '1' && text.trim()) console.warn('[OpenCode]', text.trim());
143
+ for (const line of output.split('\n')) {
144
+ if (!line.startsWith('opencode server listening')) continue;
145
+ const match = line.match(/on\s+(https?:\/\/[^\s]+)/);
146
+ if (!match) continue;
147
+ clearTimeout(timer);
148
+ settled = true;
149
+ resolve(match[1]);
150
+ return;
151
+ }
152
+ };
153
+ child.stdout.on('data', onData);
154
+ child.stderr.on('data', (chunk) => {
155
+ const text = chunk.toString();
156
+ output += text;
157
+ if (settled && process.env.CHAT_CORE_DEBUG_OPENCODE === '1' && text.trim()) console.warn('[OpenCode]', text.trim());
158
+ });
159
+ child.on('error', (err) => {
160
+ if (settled) return;
161
+ clearTimeout(timer);
162
+ settled = true;
163
+ reject(err);
164
+ });
165
+ child.on('exit', (code, signal) => {
166
+ if (settled) return;
167
+ clearTimeout(timer);
168
+ settled = true;
169
+ reject(new Error(`opencode server exited (${code ?? signal ?? 'unknown'})\n${output.slice(-1000)}`));
170
+ });
171
+ });
172
+ return {
173
+ url,
174
+ close() {
175
+ try { process.kill(-child.pid, 'SIGTERM'); } catch {}
176
+ try { child.kill('SIGTERM'); } catch {}
177
+ setTimeout(() => {
178
+ try { process.kill(-child.pid, 'SIGKILL'); } catch {}
179
+ try { child.kill('SIGKILL'); } catch {}
180
+ }, 1500).unref?.();
181
+ },
182
+ };
183
+ }
184
+
185
+ function freePort() {
186
+ return new Promise((resolve, reject) => {
187
+ const server = net.createServer();
188
+ server.listen(0, '127.0.0.1', () => {
189
+ const address = server.address();
190
+ const port = typeof address === 'object' && address ? address.port : 0;
191
+ server.close(() => resolve(port));
192
+ });
193
+ server.on('error', reject);
194
+ });
195
+ }
196
+
197
+ function timeout(ms) {
198
+ return new Promise((resolve) => setTimeout(resolve, ms));
199
+ }
200
+
201
+ function isCompactCommand(text) {
202
+ return String(text || '').trim().toLowerCase() === '/compact';
203
+ }
204
+
205
+ class OpenCodeAdapter {
206
+ async startInstance(contract) {
207
+ const sdk = await import('@opencode-ai/sdk');
208
+ const server = await startOpenCodeServer(contract);
209
+ const client = sdk.createOpencodeClient({ baseUrl: server.url });
210
+ return { client, server, closed: false };
211
+ }
212
+
213
+ async create(contract) {
214
+ const instance = await this.startInstance(contract);
215
+ let created = null;
216
+ if (contract.providerSessionId) {
217
+ try {
218
+ created = resultData(await instance.client.session.get({
219
+ path: { id: contract.providerSessionId },
220
+ query: { directory: contract.cwd },
221
+ }));
222
+ } catch (err) {
223
+ console.warn('[OpenCode] Failed to resume persisted session; creating a new native session:', err.message);
224
+ }
225
+ }
226
+ if (!created) {
227
+ created = resultData(await instance.client.session.create({
228
+ body: { title: `Amalgm ${contract.sessionId}` },
229
+ query: { directory: contract.cwd },
230
+ }));
231
+ }
232
+ if (!created?.id) throw new Error('OpenCode SDK did not return a session id');
233
+ return {
234
+ sessionId: contract.sessionId,
235
+ providerSessionId: created.id,
236
+ instance,
237
+ abortController: null,
238
+ turnCount: 0,
239
+ };
240
+ }
241
+
242
+ async resume(session, contract) {
243
+ if (!session.instance || session.instance.closed) {
244
+ session.instance = await this.startInstance(contract);
245
+ }
246
+ return session;
247
+ }
248
+
249
+ async stop(session) {
250
+ session.abortController?.abort();
251
+ await Promise.race([session.instance.client.session.abort({
252
+ path: { id: session.providerSessionId },
253
+ query: { directory: session.cwd },
254
+ }).catch(() => {}), timeout(1500)]);
255
+ }
256
+
257
+ async destroy(session) {
258
+ await this.stop(session).catch(() => {});
259
+ session.instance?.server?.close();
260
+ if (session.instance) session.instance.closed = true;
261
+ }
262
+
263
+ async *prompt(session, input, contract) {
264
+ session.cwd = contract.cwd;
265
+ const controller = new AbortController();
266
+ session.abortController = controller;
267
+ const state = {
268
+ providerSessionId: session.providerSessionId,
269
+ userMessageId: null,
270
+ turnStartedAt: Date.now(),
271
+ contextLimit: Number(contract.contextWindow || 0) || undefined,
272
+ messageRoles: new Map(),
273
+ partTypes: new Map(),
274
+ partText: new Map(),
275
+ toolsStarted: new Set(),
276
+ toolsCompleted: new Set(),
277
+ assistantMessageIds: new Set(),
278
+ finishedAssistantIds: new Set(),
279
+ usageEmittedForAssistant: new Set(),
280
+ sawTurnActivity: false,
281
+ sawBusy: false,
282
+ doneEmitted: false,
283
+ };
284
+ const queue = [];
285
+ let complete = false;
286
+ let promptSettled = false;
287
+ let wake = null;
288
+ let promptController = null;
289
+ const wakeLoop = () => { if (wake) { wake(); wake = null; } };
290
+ const push = (e) => { queue.push(e); wakeLoop(); };
291
+ const pushNormalized = (events) => {
292
+ for (const e of events) {
293
+ if (e.type === 'done' || e.type === 'error') complete = true;
294
+ push(e);
295
+ }
296
+ };
297
+ push(reasoningStarted({ providerSessionId: session.providerSessionId }));
298
+ const finishCancelled = () => {
299
+ if (complete || state.doneEmitted) return;
300
+ state.doneEmitted = true;
301
+ complete = true;
302
+ push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
303
+ try { promptController?.abort(); } catch {}
304
+ };
305
+ controller.signal.addEventListener('abort', finishCancelled, { once: true });
306
+ const usePromptAsync = typeof session.instance.client.session.promptAsync === 'function';
307
+ const events = await session.instance.client.global.event({
308
+ signal: controller.signal,
309
+ sseMaxRetryAttempts: 0,
310
+ });
311
+ const streamTask = (async () => {
312
+ try {
313
+ for await (const raw of events.stream) {
314
+ recordNativeEvent('opencode.sdk.event', raw, {
315
+ providerSessionId: session.providerSessionId,
316
+ sessionId: contract.sessionId,
317
+ assistantMessageId: contract.assistantMessageId,
318
+ eventType: raw?.payload?.type || raw?.type,
319
+ });
320
+ if (process.env.CHAT_CORE_DEBUG_OPENCODE_EVENTS === '1') {
321
+ console.warn('[OpenCode:event]', JSON.stringify(raw).slice(0, 4000));
322
+ }
323
+ pushNormalized(normalizeOpenCodeEvent(raw, state));
324
+ if (complete) break;
325
+ }
326
+ wakeLoop();
327
+ } catch (err) {
328
+ if (!controller.signal.aborted) {
329
+ if (process.env.CHAT_CORE_DEBUG_OPENCODE === '1') {
330
+ console.warn('[OpenCode] event stream ended:', err.message);
331
+ }
332
+ }
333
+ wakeLoop();
334
+ }
335
+ })();
336
+ const model = splitModel(contract.cliModel || contract.usageModelId, contract.auth);
337
+ promptController = new AbortController();
338
+ const promptMethod = usePromptAsync ? 'promptAsync' : 'prompt';
339
+ const compactCommand = isCompactCommand(input?.text);
340
+ const promptPromise = compactCommand
341
+ ? session.instance.client.session.summarize({
342
+ path: { id: session.providerSessionId },
343
+ query: { directory: contract.cwd },
344
+ signal: promptController.signal,
345
+ body: model,
346
+ })
347
+ : session.instance.client.session[promptMethod]({
348
+ path: { id: session.providerSessionId },
349
+ query: { directory: contract.cwd },
350
+ signal: promptController.signal,
351
+ body: {
352
+ model,
353
+ agent: process.env.CHAT_CORE_OPENCODE_AGENT || 'build',
354
+ system: composeSystemPrompt(contract) || undefined,
355
+ parts: openCodeParts(input),
356
+ },
357
+ });
358
+ promptPromise.then((result) => {
359
+ const data = resultData(result);
360
+ recordNativeEvent(compactCommand ? 'opencode.sdk.summarize_result' : `opencode.sdk.${promptMethod}_result`, data, {
361
+ providerSessionId: session.providerSessionId,
362
+ sessionId: contract.sessionId,
363
+ assistantMessageId: contract.assistantMessageId,
364
+ });
365
+ if (compactCommand) {
366
+ setTimeout(() => {
367
+ if (!state.doneEmitted) {
368
+ state.doneEmitted = true;
369
+ pushNormalized([done({ providerSessionId: session.providerSessionId, stopReason: 'end_turn' })]);
370
+ }
371
+ }, 500).unref?.();
372
+ } else if (!usePromptAsync) {
373
+ pushNormalized(normalizeOpenCodePromptResult(data, state));
374
+ if (!state.doneEmitted) {
375
+ state.doneEmitted = true;
376
+ pushNormalized([done({ providerSessionId: session.providerSessionId, stopReason: 'end_turn' })]);
377
+ }
378
+ }
379
+ promptSettled = true;
380
+ wakeLoop();
381
+ return data;
382
+ }).catch((err) => {
383
+ promptSettled = true;
384
+ if (promptController.signal.aborted) {
385
+ wakeLoop();
386
+ return null;
387
+ }
388
+ if (!state.doneEmitted) {
389
+ state.doneEmitted = true;
390
+ push(errorEvent(err.message, { providerSessionId: session.providerSessionId }));
391
+ push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
392
+ }
393
+ complete = true;
394
+ wakeLoop();
395
+ });
396
+ try {
397
+ while ((!complete && (compactCommand || usePromptAsync || !promptSettled)) || queue.length) {
398
+ if (!queue.length) await new Promise((resolve) => { wake = resolve; });
399
+ else yield queue.shift();
400
+ }
401
+ } finally {
402
+ controller.abort();
403
+ promptController.abort();
404
+ session.abortController = null;
405
+ await Promise.race([promptPromise.catch(() => {}), timeout(1500)]);
406
+ await Promise.race([streamTask.catch(() => {}), timeout(1500)]);
407
+ session.turnCount = (session.turnCount || 0) + 1;
408
+ }
409
+ }
410
+ }
411
+
412
+ module.exports = { OpenCodeAdapter, splitModel };
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { resolveCredential } = require('./credentials/store');
8
+
9
+ const AUTH_BY_HARNESS = {
10
+ claude_code: ['amalgm', 'byok', 'provider_auth'],
11
+ codex: ['amalgm', 'byok', 'provider_auth'],
12
+ opencode: ['amalgm', 'byok'],
13
+ };
14
+
15
+ function shellValue(raw) {
16
+ const value = String(raw ?? '').trim();
17
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
18
+ return value.slice(1, -1);
19
+ }
20
+ return value;
21
+ }
22
+
23
+ function readByokFile(amalgmDir) {
24
+ const out = {};
25
+ try {
26
+ const raw = fs.readFileSync(path.join(amalgmDir, '.amalgm-credentials'), 'utf8');
27
+ for (const line of raw.split(/\r?\n/)) {
28
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
29
+ if (match) out[match[1]] = shellValue(match[2]);
30
+ }
31
+ } catch {}
32
+ return out;
33
+ }
34
+
35
+ function byokKeyFor(harness, values) {
36
+ if (harness === 'claude_code') return values.ANTHROPIC_API_KEY || '';
37
+ if (harness === 'codex') return values.OPENAI_API_KEY || values.CODEX_API_KEY || '';
38
+ return '';
39
+ }
40
+
41
+ function providerKindForModel(modelId) {
42
+ const provider = String(modelId || '').split('/')[0];
43
+ if (provider === 'anthropic') return 'anthropic_compatible';
44
+ if (provider === 'openai') return 'openai_compatible';
45
+ return provider ? 'ai_gateway' : null;
46
+ }
47
+
48
+ function resolveByokCredential(amalgmDir, { credentialId, harness, modelId }) {
49
+ if (credentialId) return resolveCredential(amalgmDir, { credentialId, harness });
50
+ const preferred = providerKindForModel(modelId);
51
+ if (preferred) {
52
+ const credential = resolveCredential(amalgmDir, { harness, providerKind: preferred });
53
+ if (credential) return credential;
54
+ if (preferred === 'ai_gateway') return null;
55
+ }
56
+ if (harness === 'opencode' && preferred !== 'ai_gateway') {
57
+ const gatewayCredential = resolveCredential(amalgmDir, { harness, providerKind: 'ai_gateway' });
58
+ if (gatewayCredential) return gatewayCredential;
59
+ }
60
+ return resolveCredential(amalgmDir, { harness });
61
+ }
62
+
63
+ function fingerprint(value) {
64
+ return value ? crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 16) : null;
65
+ }
66
+
67
+ function coerceAuth(harness, requested) {
68
+ const supported = AUTH_BY_HARNESS[harness] || ['amalgm'];
69
+ return requested && supported.includes(requested) ? requested : supported[0];
70
+ }
71
+
72
+ function runtimeHome({ amalgmDir, sessionId, harness, authMethod, tokenFingerprint }) {
73
+ if (authMethod === 'provider_auth' && harness !== 'codex') return null;
74
+ const suffix = tokenFingerprint || 'default';
75
+ return path.join(amalgmDir, 'runtime', sessionId, harness, suffix);
76
+ }
77
+
78
+ function providerAuthFingerprint(harness) {
79
+ if (harness !== 'codex') return null;
80
+ try {
81
+ const raw = fs.readFileSync(path.join(os.homedir(), '.codex', 'auth.json'), 'utf8');
82
+ return fingerprint(raw);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken, proxyBaseUrl, amalgmDir, credentialId, modelId }) {
89
+ const method = coerceAuth(harness, authMethod);
90
+ const byok = readByokFile(amalgmDir);
91
+ let baseUrl = null;
92
+ let forwardBaseUrl = null;
93
+ let tokenRef = null;
94
+ let tokenFingerprint = null;
95
+ let credential = null;
96
+ if (method === 'amalgm') {
97
+ const providerPath = harness === 'claude_code'
98
+ ? '/anthropic'
99
+ : harness === 'opencode'
100
+ ? '/ai_gateway/v3/ai'
101
+ : '/openai/v1';
102
+ forwardBaseUrl = `${String(proxyBaseUrl || '').replace(/\/$/, '')}${providerPath}`;
103
+ baseUrl = `${String(localBaseUrl || '').replace(/\/$/, '')}/egress/${encodeURIComponent(sessionId)}${providerPath}`;
104
+ tokenRef = `session-local-${fingerprint(`${sessionId}:${proxyToken}`) || 'token'}`;
105
+ tokenFingerprint = fingerprint(proxyToken);
106
+ } else if (method === 'byok') {
107
+ credential = resolveByokCredential(amalgmDir, { credentialId, harness, modelId });
108
+ tokenRef = credential?.key || byokKeyFor(harness, byok);
109
+ if (!tokenRef) {
110
+ throw new Error(`No BYOK credential configured for ${harness}`);
111
+ }
112
+ tokenFingerprint = fingerprint(tokenRef);
113
+ baseUrl = credential?.baseUrl || (harness === 'claude_code' ? 'https://api.anthropic.com' : 'https://api.openai.com/v1');
114
+ } else if (method === 'provider_auth') {
115
+ tokenFingerprint = providerAuthFingerprint(harness);
116
+ }
117
+ return {
118
+ method,
119
+ baseUrl,
120
+ credentialId: credential?.id || credentialId || null,
121
+ credentialName: credential?.name || null,
122
+ providerKind: credential?.providerKind || null,
123
+ forwardBaseUrl,
124
+ tokenRef,
125
+ tokenFingerprint,
126
+ runtimeHome: runtimeHome({ amalgmDir, sessionId, harness, authMethod: method, tokenFingerprint }),
127
+ };
128
+ }
129
+
130
+ function safeBaseEnv(baseEnv = process.env) {
131
+ const env = {};
132
+ for (const key of ['PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'TMPDIR']) {
133
+ if (baseEnv[key]) env[key] = baseEnv[key];
134
+ }
135
+ return env;
136
+ }
137
+
138
+ function runtimeEnv(contract, baseEnv = process.env) {
139
+ const env = contract.authMethod === 'provider_auth' ? { ...baseEnv } : safeBaseEnv(baseEnv);
140
+ for (const key of ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL', 'AI_GATEWAY_API_KEY', 'AI_GATEWAY_BASE_URL']) {
141
+ delete env[key];
142
+ }
143
+ if (contract.auth.runtimeHome) {
144
+ fs.mkdirSync(contract.auth.runtimeHome, { recursive: true });
145
+ env.HOME = contract.auth.runtimeHome;
146
+ env.CODEX_HOME = contract.auth.runtimeHome;
147
+ env.CLAUDE_CONFIG_DIR = contract.auth.runtimeHome;
148
+ env.OPENCODE_CONFIG_DIR = contract.auth.runtimeHome;
149
+ }
150
+ env.IS_SANDBOX = '1';
151
+ if (baseEnv.AMALGM_RUNTIME_TOKEN) env.AMALGM_RUNTIME_TOKEN = baseEnv.AMALGM_RUNTIME_TOKEN;
152
+ if (contract.authMethod === 'amalgm' || contract.authMethod === 'byok') {
153
+ if (contract.harness === 'claude_code') {
154
+ if (contract.authMethod === 'amalgm') {
155
+ env.ANTHROPIC_AUTH_TOKEN = contract.auth.tokenRef || '';
156
+ } else {
157
+ env.ANTHROPIC_API_KEY = contract.auth.tokenRef || '';
158
+ }
159
+ if (contract.auth.baseUrl) env.ANTHROPIC_BASE_URL = contract.auth.baseUrl;
160
+ } else if (contract.harness === 'opencode') {
161
+ env.AI_GATEWAY_API_KEY = contract.auth.tokenRef || '';
162
+ if (contract.auth.baseUrl) env.AI_GATEWAY_BASE_URL = contract.auth.baseUrl;
163
+ } else {
164
+ env.OPENAI_API_KEY = contract.auth.tokenRef || '';
165
+ if (contract.auth.baseUrl) env.OPENAI_BASE_URL = contract.auth.baseUrl;
166
+ }
167
+ }
168
+ return env;
169
+ }
170
+
171
+ module.exports = {
172
+ AUTH_BY_HARNESS,
173
+ authEnvelope,
174
+ coerceAuth,
175
+ fingerprint,
176
+ runtimeEnv,
177
+ };