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,393 @@
1
+ /**
2
+ * User model preferences — mirrors the Zustand harnessPreferences store.
3
+ *
4
+ * Hydrates on boot (non-blocking) from Supabase. Used to pick the right model
5
+ * for task/event/agent runs without requiring the caller to specify it.
6
+ */
7
+
8
+ const { AMALGM_USER_ID } = require('../config');
9
+ const { hasSupabase, supabaseSelect } = require('./supabase');
10
+
11
+ const DEFAULT_SELECTED_MODELS = {
12
+ claude_code: 'anthropic/claude-opus-4.7',
13
+ codex: 'openai/gpt-5.4-thinking-xhigh',
14
+ opencode: 'opencode-moonshotai-kimi-k2.5',
15
+ pi: 'pi-anthropic-claude-sonnet-4.6',
16
+ amp: 'amp-default',
17
+ cursor: 'cursor/composer-2-fast',
18
+ };
19
+
20
+ const GATEWAY_PROVIDER_PREFIXES = [
21
+ 'prime-intellect', 'arcee-ai',
22
+ 'alibaba', 'amazon', 'anthropic', 'bytedance', 'cohere', 'deepseek',
23
+ 'google', 'inception', 'kwaipilot', 'meituan', 'meta', 'minimax',
24
+ 'mistral', 'moonshotai', 'morph', 'nvidia', 'openai',
25
+ 'perplexity', 'xai', 'xiaomi', 'zai',
26
+ ];
27
+
28
+ const CLAUDE_OPUS_4_7_1M_MODEL_ID = 'anthropic/claude-opus-4.7-1m';
29
+
30
+ const CLAUDE_LEGACY_ALIASES = {
31
+ 'claude-code-opus': 'anthropic/claude-opus-4.7',
32
+ 'claude-code-opus-47': 'anthropic/claude-opus-4.7',
33
+ 'claude-code-opus-46': 'anthropic/claude-opus-4.7',
34
+ 'claude-code-opus-45': 'anthropic/claude-opus-4.7',
35
+ 'claude-code-opus-4.7': 'anthropic/claude-opus-4.7',
36
+ 'claude-code-opus-4-7': 'anthropic/claude-opus-4.7',
37
+ 'claude-code-opus-4.6': 'anthropic/claude-opus-4.7',
38
+ 'claude-code-opus-4-6': 'anthropic/claude-opus-4.7',
39
+ 'claude-code-opus-4.5': 'anthropic/claude-opus-4.7',
40
+ 'claude-code-opus-4-5': 'anthropic/claude-opus-4.7',
41
+ 'claude-code-opus1m': CLAUDE_OPUS_4_7_1M_MODEL_ID,
42
+ 'claude-code-opus-1m': CLAUDE_OPUS_4_7_1M_MODEL_ID,
43
+ 'claude-code-opus-4.7-1m': CLAUDE_OPUS_4_7_1M_MODEL_ID,
44
+ 'claude-code-opus-4-7-1m': CLAUDE_OPUS_4_7_1M_MODEL_ID,
45
+ 'claude-code-sonnet': 'anthropic/claude-sonnet-4.6',
46
+ 'claude-code-sonnet-45': 'anthropic/claude-sonnet-4.6',
47
+ 'claude-code-sonnet-4.6': 'anthropic/claude-sonnet-4.6',
48
+ 'claude-code-sonnet-4-6': 'anthropic/claude-sonnet-4.6',
49
+ 'claude-code-sonnet-4.5': 'anthropic/claude-sonnet-4.6',
50
+ 'claude-code-sonnet-4-5': 'anthropic/claude-sonnet-4.6',
51
+ 'claude-code-haiku': 'anthropic/claude-haiku-4.5',
52
+ 'claude-code-haiku-4.5': 'anthropic/claude-haiku-4.5',
53
+ 'claude-code-haiku-4-5': 'anthropic/claude-haiku-4.5',
54
+ 'anthropic/claude-opus-4.6': 'anthropic/claude-opus-4.7',
55
+ 'anthropic/claude-opus-4.5': 'anthropic/claude-opus-4.7',
56
+ 'anthropic/claude-sonnet-4.5': 'anthropic/claude-sonnet-4.6',
57
+ 'claude-opus-4.7-1m': CLAUDE_OPUS_4_7_1M_MODEL_ID,
58
+ 'claude-opus-4-7-1m': CLAUDE_OPUS_4_7_1M_MODEL_ID,
59
+ opus: 'anthropic/claude-opus-4.7',
60
+ opus1m: CLAUDE_OPUS_4_7_1M_MODEL_ID,
61
+ 'opus[1m]': CLAUDE_OPUS_4_7_1M_MODEL_ID,
62
+ sonnet: 'anthropic/claude-sonnet-4.6',
63
+ 'sonnet[1m]': 'anthropic/claude-sonnet-4.6',
64
+ haiku: 'anthropic/claude-haiku-4.5',
65
+ };
66
+
67
+ const CLAUDE_HARNESS_MODELS = {
68
+ 'anthropic/claude-opus-4.7': 'opus',
69
+ [CLAUDE_OPUS_4_7_1M_MODEL_ID]: 'opus[1m]',
70
+ 'anthropic/claude-opus-4.6': 'opus',
71
+ 'anthropic/claude-opus-4.5': 'opus',
72
+ 'anthropic/claude-sonnet-4.6': 'sonnet',
73
+ 'anthropic/claude-sonnet-4.5': 'sonnet',
74
+ 'anthropic/claude-haiku-4.5': 'haiku',
75
+ };
76
+
77
+ const CODEX_THINKING_SUFFIX_RE = /:thinking-(low|medium|high|xhigh)$/i;
78
+ const CODEX_LEGACY_THINKING_SUFFIX_RE = /-thinking-(low|medium|high|xhigh)$/i;
79
+
80
+ const CURSOR_MODEL_ALIASES = {
81
+ 'cursor-opus-4.6-thinking': 'anthropic/claude-opus-4.6-high-thinking',
82
+ 'cursor-opus-4.6': 'anthropic/claude-opus-4.6-high',
83
+ 'cursor-opus-4.5-thinking': 'anthropic/claude-opus-4.5-high-thinking',
84
+ 'cursor-opus-4.5': 'anthropic/claude-opus-4.5-high',
85
+ 'cursor-sonnet-4.5-thinking': 'anthropic/claude-sonnet-4.5-thinking',
86
+ 'cursor-sonnet-4.5': 'anthropic/claude-sonnet-4.5',
87
+ 'cursor-gemini-3-pro': 'google/gemini-3-pro',
88
+ 'cursor-grok': 'xai/grok',
89
+ 'cursor-gpt-5.4': 'openai/gpt-5.4-medium',
90
+ 'cursor-anthropic/claude-opus-4.7': 'anthropic/claude-opus-4.7-xhigh',
91
+ 'cursor-anthropic/claude-opus-4.6': 'anthropic/claude-opus-4.6-high',
92
+ 'cursor-anthropic/claude-opus-4.5': 'anthropic/claude-opus-4.5-high',
93
+ 'cursor-anthropic/claude-sonnet-4.6': 'anthropic/claude-sonnet-4.6-medium',
94
+ 'cursor-anthropic/claude-sonnet-4.5': 'anthropic/claude-sonnet-4.5',
95
+ 'cursor-anthropic/claude-sonnet-4': 'anthropic/claude-sonnet-4',
96
+ 'cursor-openai/gpt-5.5': 'openai/gpt-5.5-medium',
97
+ 'cursor-openai/gpt-5.4': 'openai/gpt-5.4-medium',
98
+ 'cursor-openai/gpt-5.4-mini': 'openai/gpt-5.4-mini-medium',
99
+ 'cursor-openai/gpt-5.4-nano': 'openai/gpt-5.4-nano-medium',
100
+ 'cursor-openai/gpt-5.3-codex': 'openai/gpt-5.3-codex',
101
+ 'cursor-openai/gpt-5.2': 'openai/gpt-5.2',
102
+ 'cursor-google/gemini-3.1-pro-preview': 'google/gemini-3.1-pro',
103
+ 'cursor-google/gemini-3.1-pro': 'google/gemini-3.1-pro',
104
+ 'cursor-google/gemini-3-flash': 'google/gemini-3-flash',
105
+ 'cursor-xai/grok-4.20': 'xai/grok-4.20',
106
+ 'cursor-moonshotai/kimi-k2.5': 'moonshotai/kimi-k2.5',
107
+ };
108
+
109
+ const _selectedModels = { ...DEFAULT_SELECTED_MODELS };
110
+ let _pinnedHarness = null;
111
+ let _pinnedModel = null;
112
+ let _prefsHydrated = false;
113
+
114
+ function cleanModelId(value) {
115
+ return String(value || '').trim().replace(/^vercel\//i, '').toLowerCase();
116
+ }
117
+
118
+ function normalizeClaudeModelPart(modelPart) {
119
+ const hasOneMillionContext = /(?:\[\s*1m\s*\]|:1m|-1m)$/i.test(modelPart);
120
+ const base = modelPart
121
+ .replace(/\[\s*1m\s*\]$/i, '')
122
+ .replace(/:1m$/i, '')
123
+ .replace(/-1m$/i, '')
124
+ .replace(/-\d{8}$/, '')
125
+ .replace(/-(\d+)-(\d+)(?=$|[-:])/, '-$1.$2');
126
+ const currentBase = {
127
+ 'claude-opus-4.6': 'claude-opus-4.7',
128
+ 'claude-opus-4.5': 'claude-opus-4.7',
129
+ 'claude-sonnet-4.5': 'claude-sonnet-4.6',
130
+ }[base] || base;
131
+ return hasOneMillionContext && currentBase === 'claude-opus-4.7'
132
+ ? currentBase + '-1m'
133
+ : currentBase;
134
+ }
135
+
136
+ function normalizeClaudeModelId(value) {
137
+ const cleaned = cleanModelId(value);
138
+ if (CLAUDE_LEGACY_ALIASES[cleaned]) return CLAUDE_LEGACY_ALIASES[cleaned];
139
+ if (cleaned.startsWith('anthropic/')) {
140
+ return 'anthropic/' + normalizeClaudeModelPart(cleaned.slice('anthropic/'.length));
141
+ }
142
+ if (cleaned.startsWith('claude-')) {
143
+ return 'anthropic/' + normalizeClaudeModelPart(cleaned);
144
+ }
145
+ return value;
146
+ }
147
+
148
+ function codexLegacyBaseToCanonical(value) {
149
+ const cleaned = cleanModelId(value);
150
+ if (cleaned.startsWith('openai/')) return cleaned;
151
+ if (cleaned.startsWith('codex-')) return 'openai/' + cleaned.slice('codex-'.length);
152
+ if (/^(gpt-|gpt\d|o-|o\d)/i.test(cleaned)) return 'openai/' + cleaned;
153
+ return value;
154
+ }
155
+
156
+ function codexThinkingVariantId(baseModelId, effort) {
157
+ return codexLegacyBaseToCanonical(baseModelId) + '-thinking-' + String(effort).toLowerCase();
158
+ }
159
+
160
+ function codexGatewayModelId(modelId) {
161
+ return modelId.replace(CODEX_THINKING_SUFFIX_RE, '').replace(CODEX_LEGACY_THINKING_SUFFIX_RE, '');
162
+ }
163
+
164
+ function codexHarnessModelId(modelId, effort) {
165
+ const baseModel = codexGatewayModelId(modelId).replace(/^openai\//, '');
166
+ if (effort && /^gpt-5\.[45](?:-mini)?$/i.test(baseModel)) {
167
+ return `${baseModel}/${effort}`;
168
+ }
169
+ return baseModel;
170
+ }
171
+
172
+ function cursorCliModelToCanonicalModelId(value) {
173
+ const cleaned = cleanModelId(value);
174
+ const cliModel = cleaned.startsWith('cursor-') ? cleaned.slice('cursor-'.length) : cleaned;
175
+ const prefixed = 'cursor-' + cliModel;
176
+ if (CURSOR_MODEL_ALIASES[prefixed]) return CURSOR_MODEL_ALIASES[prefixed];
177
+ if (cliModel.startsWith('cursor/')) return cliModel;
178
+ if (['auto', 'composer-2-fast', 'composer-2', 'composer-1.5', 'composer-1'].includes(cliModel)) {
179
+ return 'cursor/' + cliModel;
180
+ }
181
+ if (/^(gpt-|gpt\d|o-|o\d)/i.test(cliModel)) return 'openai/' + cliModel;
182
+ if (cliModel.startsWith('gemini-')) return 'google/' + cliModel;
183
+ if (cliModel === 'kimi-k2.5') return 'moonshotai/kimi-k2.5';
184
+ if (cliModel === 'grok') return 'xai/grok';
185
+ if (cliModel.startsWith('grok-4-20')) return 'xai/' + cliModel.replace(/^grok-4-20/, 'grok-4.20');
186
+ const opus47Match = cliModel.match(/^claude-opus-4-7(?:-(.+))?$/);
187
+ if (opus47Match) return 'anthropic/claude-opus-4.7' + (opus47Match[1] ? '-' + opus47Match[1] : '');
188
+ const claudeMatch = cliModel.match(/^claude-(4(?:\.\d+)?)-(opus|sonnet)(?:-(.+))?$/);
189
+ if (claudeMatch) {
190
+ const [, version, family, suffix] = claudeMatch;
191
+ return 'anthropic/claude-' + family + '-' + version + (suffix ? '-' + suffix : '');
192
+ }
193
+ if (cliModel.startsWith('claude-')) {
194
+ return 'anthropic/' + cliModel.replace(/-(\d+)-(\d+)(?=$|-)/, '-$1.$2');
195
+ }
196
+ return 'cursor/' + cliModel;
197
+ }
198
+
199
+ function normalizeCursorModelId(value) {
200
+ const cleaned = cleanModelId(value);
201
+ if (CURSOR_MODEL_ALIASES[cleaned]) return CURSOR_MODEL_ALIASES[cleaned];
202
+ if (cleaned.includes('/') && !cleaned.startsWith('cursor-')) return cleaned;
203
+ return cursorCliModelToCanonicalModelId(cleaned);
204
+ }
205
+
206
+ function cursorCanonicalModelIdToCliModel(value) {
207
+ const canonical = normalizeCursorModelId(value);
208
+ if (canonical.startsWith('cursor/')) return canonical.slice('cursor/'.length);
209
+ if (canonical.startsWith('cursor-')) return canonical.slice('cursor-'.length);
210
+ if (canonical.startsWith('openai/')) return canonical.slice('openai/'.length);
211
+ if (canonical.startsWith('google/')) return canonical.slice('google/'.length);
212
+ if (canonical.startsWith('moonshotai/')) return canonical.slice('moonshotai/'.length);
213
+ if (canonical.startsWith('xai/')) return canonical.slice('xai/'.length).replace(/^grok-4\.20/, 'grok-4-20');
214
+ if (canonical.startsWith('anthropic/')) {
215
+ const model = canonical.slice('anthropic/'.length);
216
+ const opus47Match = model.match(/^claude-opus-4\.7(?:-(.+))?$/);
217
+ if (opus47Match) return 'claude-opus-4-7' + (opus47Match[1] ? '-' + opus47Match[1] : '');
218
+ const claudeMatch = model.match(/^claude-(opus|sonnet)-(4(?:\.\d+)?)(?:-(.+))?$/);
219
+ if (claudeMatch) {
220
+ const [, family, version, suffix] = claudeMatch;
221
+ return 'claude-' + version + '-' + family + (suffix ? '-' + suffix : '');
222
+ }
223
+ }
224
+ return canonical;
225
+ }
226
+
227
+ function normalizeModelSelection(harnessId, modelId) {
228
+ if (!modelId) return { modelId: null, reasoningEffort: null };
229
+ const cleaned = cleanModelId(modelId);
230
+
231
+ if (harnessId === 'claude_code') {
232
+ return { modelId: normalizeClaudeModelId(cleaned), reasoningEffort: null };
233
+ }
234
+
235
+ if (harnessId === 'codex') {
236
+ const legacyThinkingMatch = cleaned.match(/^(codex-[a-z0-9.-]+(?:-[a-z0-9.-]+)*)-thinking-(low|medium|high|xhigh)$/i);
237
+ if (legacyThinkingMatch) {
238
+ const effort = legacyThinkingMatch[2].toLowerCase();
239
+ return {
240
+ modelId: codexThinkingVariantId(legacyThinkingMatch[1], effort),
241
+ reasoningEffort: effort,
242
+ };
243
+ }
244
+ const canonicalThinkingMatch = cleaned.match(/^((?:openai\/)?(?:gpt|o)[a-z0-9.-]+(?:-[a-z0-9.-]+)*)(?::thinking-|-thinking-)(low|medium|high|xhigh)$/i);
245
+ if (canonicalThinkingMatch) {
246
+ const effort = canonicalThinkingMatch[2].toLowerCase();
247
+ return {
248
+ modelId: codexThinkingVariantId(canonicalThinkingMatch[1], effort),
249
+ reasoningEffort: effort,
250
+ };
251
+ }
252
+ const slashThinkingMatch = cleaned.match(/^((?:openai\/)?(?:gpt|o)[a-z0-9.-]+(?:-[a-z0-9.-]+)*)\/(low|medium|high|xhigh)$/i);
253
+ if (slashThinkingMatch) {
254
+ const effort = slashThinkingMatch[2].toLowerCase();
255
+ return {
256
+ modelId: codexThinkingVariantId(slashThinkingMatch[1], effort),
257
+ reasoningEffort: effort,
258
+ };
259
+ }
260
+ return { modelId: codexLegacyBaseToCanonical(cleaned), reasoningEffort: null };
261
+ }
262
+
263
+ if (harnessId === 'cursor') {
264
+ return { modelId: normalizeCursorModelId(cleaned), reasoningEffort: null };
265
+ }
266
+
267
+ return { modelId, reasoningEffort: null };
268
+ }
269
+
270
+ function inferHarnessForModel(modelId, fallbackHarnessId) {
271
+ const cleaned = cleanModelId(modelId);
272
+ if (cleaned.startsWith('cursor-') || cleaned.startsWith('cursor/')) return 'cursor';
273
+ if (cleaned.startsWith('amp-')) return 'amp';
274
+ if (cleaned.startsWith('opencode-')) return 'opencode';
275
+ if (cleaned.startsWith('pi-')) return 'pi';
276
+ if (
277
+ cleaned.startsWith('claude-') ||
278
+ cleaned.startsWith('anthropic/claude-') ||
279
+ ['opus', 'opus1m', 'opus[1m]', 'sonnet', 'sonnet[1m]', 'haiku'].includes(cleaned)
280
+ ) {
281
+ return 'claude_code';
282
+ }
283
+ if (cleaned.startsWith('codex-') || cleaned.startsWith('openai/') || /^(gpt-|gpt\d|o-|o\d)/i.test(cleaned)) {
284
+ return 'codex';
285
+ }
286
+ return fallbackHarnessId;
287
+ }
288
+
289
+ function resolveCliModel(harnessId, modelId) {
290
+ const selection = normalizeModelSelection(harnessId, modelId);
291
+ const selectedModelId = selection.modelId || modelId;
292
+ if (harnessId === 'claude_code') {
293
+ return CLAUDE_HARNESS_MODELS[selectedModelId] ||
294
+ selectedModelId.replace(/^anthropic\//, '').replace(/-(\d+)\.(\d+)(?=$|-)/, '-$1-$2');
295
+ }
296
+ if (harnessId === 'codex') {
297
+ return codexHarnessModelId(selectedModelId, selection.reasoningEffort);
298
+ }
299
+ if (harnessId === 'cursor') {
300
+ return cursorCanonicalModelIdToCliModel(selectedModelId);
301
+ }
302
+ if (harnessId === 'amp') {
303
+ return selectedModelId;
304
+ }
305
+ // OpenCode / Pi: strip harness prefix and convert first dash to slash.
306
+ // e.g. "opencode-alibaba-qwen3-coder" → "vercel/alibaba/qwen3-coder"
307
+ for (const prefix of ['opencode-', 'pi-']) {
308
+ if (selectedModelId.startsWith(prefix)) {
309
+ const remainder = selectedModelId.slice(prefix.length);
310
+ const provider = GATEWAY_PROVIDER_PREFIXES.find((candidate) =>
311
+ remainder.startsWith(candidate + '-'));
312
+ if (provider) {
313
+ const gatewayId = provider + '/' + remainder.slice(provider.length + 1);
314
+ return prefix === 'opencode-' ? 'vercel/' + gatewayId : gatewayId;
315
+ }
316
+ }
317
+ }
318
+ return selectedModelId;
319
+ }
320
+
321
+ async function hydrateModelPreferences() {
322
+ if (_prefsHydrated || !hasSupabase()) return;
323
+ _prefsHydrated = true;
324
+ try {
325
+ const rows = await supabaseSelect(
326
+ 'user_preferences',
327
+ `user_id=eq.${AMALGM_USER_ID}&select=preferences`,
328
+ );
329
+ if (!rows) return;
330
+ const preferences = rows?.[0]?.preferences;
331
+ const recentPrefs = preferences?.recent_prefs;
332
+ if (recentPrefs && typeof recentPrefs === 'object') {
333
+ for (const [h, m] of Object.entries(recentPrefs)) {
334
+ if (m) {
335
+ const normalized = normalizeModelSelection(h, m).modelId || m;
336
+ if (_selectedModels[h] !== normalized) _selectedModels[h] = normalized;
337
+ }
338
+ }
339
+ }
340
+ _pinnedHarness = preferences?.autopilot?.harness || null;
341
+ const rawPinnedModel = preferences?.autopilot?.model || null;
342
+ if (rawPinnedModel) {
343
+ const harnessForPinned =
344
+ _pinnedHarness ||
345
+ inferHarnessForModel(rawPinnedModel, preferences?.last_used?.harness || null);
346
+ _pinnedModel = harnessForPinned
347
+ ? normalizeModelSelection(harnessForPinned, rawPinnedModel).modelId || rawPinnedModel
348
+ : rawPinnedModel;
349
+ } else {
350
+ _pinnedModel = null;
351
+ }
352
+ console.log(
353
+ '[AmalgmMCP] Hydrated user prefs:',
354
+ JSON.stringify({
355
+ selectedModels: _selectedModels,
356
+ pinnedHarness: _pinnedHarness,
357
+ pinnedModel: _pinnedModel,
358
+ }),
359
+ );
360
+ } catch (err) {
361
+ console.warn('[AmalgmMCP] Failed to hydrate user prefs:', err.message);
362
+ }
363
+ }
364
+
365
+ function getSelectedModel(harnessId) {
366
+ if (_pinnedHarness === harnessId && _pinnedModel) return _pinnedModel;
367
+ return _selectedModels[harnessId] || DEFAULT_SELECTED_MODELS[harnessId] || null;
368
+ }
369
+
370
+ function resolveModelSelection(harnessId, uiModelId) {
371
+ const selectedModelId =
372
+ uiModelId ||
373
+ getSelectedModel(harnessId) ||
374
+ DEFAULT_SELECTED_MODELS[harnessId] ||
375
+ null;
376
+ if (!selectedModelId) {
377
+ return { modelId: null, cliModel: null, reasoningEffort: null };
378
+ }
379
+ const normalized = normalizeModelSelection(harnessId, selectedModelId);
380
+ const modelId = normalized.modelId || selectedModelId;
381
+ return {
382
+ modelId,
383
+ cliModel: resolveCliModel(harnessId, modelId),
384
+ reasoningEffort: normalized.reasoningEffort || null,
385
+ };
386
+ }
387
+
388
+ module.exports = {
389
+ DEFAULT_SELECTED_MODELS,
390
+ hydrateModelPreferences,
391
+ getSelectedModel,
392
+ resolveModelSelection,
393
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Storage primitives — atomic JSON and JSONL I/O on the local disk.
3
+ *
4
+ * Replaces the container-era /workspace + /persist dual-write. Locally we
5
+ * only ever write to ~/.amalgm/ (AMALGM_DIR). Single writer, no mirror.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ /**
12
+ * Ensure a directory exists (mkdir -p).
13
+ */
14
+ function ensureDir(dir) {
15
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+
18
+ /**
19
+ * Read a JSON file, returning `fallback` on ENOENT or parse error.
20
+ */
21
+ function readJson(file, fallback) {
22
+ try {
23
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
24
+ } catch {
25
+ return fallback;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Write JSON atomically: tmp file + rename. Falls back to non-atomic on
31
+ * rename failure (e.g. cross-device; shouldn't happen locally).
32
+ */
33
+ function writeJsonAtomic(file, data) {
34
+ const json = JSON.stringify(data, null, 2);
35
+ const tmp = file + '.tmp';
36
+ fs.writeFileSync(tmp, json);
37
+ fs.renameSync(tmp, file);
38
+ }
39
+
40
+ /**
41
+ * Delete any leftover `.{name}.tmp` files from crashed writes.
42
+ */
43
+ function cleanupStaleTmp(dir, prefix = '.') {
44
+ try {
45
+ for (const entry of fs.readdirSync(dir)) {
46
+ if (entry.startsWith(prefix) && entry.endsWith('.tmp')) {
47
+ fs.rmSync(path.join(dir, entry), { force: true });
48
+ }
49
+ }
50
+ } catch {
51
+ // Best-effort cleanup.
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Append a JSON entry as a JSONL line.
57
+ */
58
+ function appendJsonl(file, entry) {
59
+ ensureDir(path.dirname(file));
60
+ fs.appendFileSync(file, JSON.stringify(entry) + '\n');
61
+ }
62
+
63
+ /**
64
+ * Read the last N entries from a JSONL file. Returns [] if missing.
65
+ */
66
+ function tailJsonl(file, limit = 20) {
67
+ if (!fs.existsSync(file)) return [];
68
+ try {
69
+ const lines = fs.readFileSync(file, 'utf-8').trim().split('\n').filter(Boolean);
70
+ return lines
71
+ .slice(-limit)
72
+ .map((l) => {
73
+ try {
74
+ return JSON.parse(l);
75
+ } catch {
76
+ return null;
77
+ }
78
+ })
79
+ .filter(Boolean);
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ ensureDir,
87
+ readJson,
88
+ writeJsonAtomic,
89
+ cleanupStaleTmp,
90
+ appendJsonl,
91
+ tailJsonl,
92
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Supabase REST client — optional.
3
+ *
4
+ * Local-first: this module is usable but *not required*. If Supabase isn't
5
+ * configured, hasSupabase() returns false and every helper no-ops (returns
6
+ * null). Task runs and agent conversations still work; they just don't create
7
+ * a `sessions` row in the cloud DB.
8
+ *
9
+ * When the user is signed in and the orchestrator provides an HMAC proxy
10
+ * token, writes go through the Fly.io proxy → Supabase.
11
+ */
12
+
13
+ const {
14
+ SUPABASE_URL,
15
+ SUPABASE_AUTH_HEADERS,
16
+ PROXY_BASE_URL,
17
+ PROXY_TOKEN,
18
+ AMALGM_USER_ID,
19
+ } = require('../config');
20
+ const {
21
+ DEFAULT_PROXY_BASE_URL,
22
+ ensureFreshProxyToken,
23
+ readProxyToken,
24
+ } = require('../../proxy-token-store');
25
+
26
+ function hasSupabase() {
27
+ return !!(
28
+ AMALGM_USER_ID
29
+ && (
30
+ (SUPABASE_URL && SUPABASE_AUTH_HEADERS.apikey)
31
+ || readProxyToken()
32
+ || PROXY_TOKEN
33
+ )
34
+ );
35
+ }
36
+
37
+ async function supabaseContext() {
38
+ if (!AMALGM_USER_ID) return null;
39
+ if (SUPABASE_URL && SUPABASE_AUTH_HEADERS.apikey) {
40
+ return { url: SUPABASE_URL, headers: SUPABASE_AUTH_HEADERS };
41
+ }
42
+ const token = await ensureFreshProxyToken({ logger: console }) || readProxyToken() || PROXY_TOKEN;
43
+ const proxyBase = (PROXY_BASE_URL || DEFAULT_PROXY_BASE_URL).replace(/\/$/, '');
44
+ if (!token || !proxyBase) return null;
45
+ return {
46
+ url: `${proxyBase}/supabase`,
47
+ headers: { Authorization: `Bearer ${token}` },
48
+ };
49
+ }
50
+
51
+ async function supabaseInsert(table, data) {
52
+ const ctx = await supabaseContext();
53
+ if (!ctx) return null;
54
+ const res = await fetch(`${ctx.url}/rest/v1/${table}`, {
55
+ method: 'POST',
56
+ headers: {
57
+ ...ctx.headers,
58
+ 'Content-Type': 'application/json',
59
+ Prefer: 'return=representation',
60
+ },
61
+ body: JSON.stringify(data),
62
+ });
63
+ if (!res.ok) {
64
+ const text = await res.text().catch(() => '');
65
+ console.error(
66
+ `[AmalgmMCP:Supabase] INSERT ${table} failed: ${res.status} ${text.slice(0, 200)}`,
67
+ );
68
+ return null;
69
+ }
70
+ const rows = await res.json();
71
+ return Array.isArray(rows) ? rows[0] : rows;
72
+ }
73
+
74
+ async function supabasePatch(table, matchCol, matchVal, data) {
75
+ const ctx = await supabaseContext();
76
+ if (!ctx) return null;
77
+ const res = await fetch(`${ctx.url}/rest/v1/${table}?${matchCol}=eq.${matchVal}`, {
78
+ method: 'PATCH',
79
+ headers: {
80
+ ...ctx.headers,
81
+ 'Content-Type': 'application/json',
82
+ Prefer: 'return=representation',
83
+ },
84
+ body: JSON.stringify(data),
85
+ });
86
+ if (!res.ok) {
87
+ const text = await res.text().catch(() => '');
88
+ console.error(
89
+ `[AmalgmMCP:Supabase] PATCH ${table} failed: ${res.status} ${text.slice(0, 200)}`,
90
+ );
91
+ return null;
92
+ }
93
+ return res.json();
94
+ }
95
+
96
+ async function supabaseSelect(table, query) {
97
+ const ctx = await supabaseContext();
98
+ if (!ctx) return null;
99
+ const res = await fetch(`${ctx.url}/rest/v1/${table}?${query}`, {
100
+ headers: ctx.headers,
101
+ });
102
+ if (!res.ok) {
103
+ const text = await res.text().catch(() => '');
104
+ console.error(
105
+ `[AmalgmMCP:Supabase] SELECT ${table} failed: ${res.status} ${text.slice(0, 200)}`,
106
+ );
107
+ return null;
108
+ }
109
+ return res.json();
110
+ }
111
+
112
+ module.exports = {
113
+ hasSupabase,
114
+ supabaseContext,
115
+ supabaseInsert,
116
+ supabasePatch,
117
+ supabaseSelect,
118
+ };