amalgm 0.1.69 → 0.1.71

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 (26) hide show
  1. package/package.json +4 -3
  2. package/runtime/lib/harnesses.js +47 -37
  3. package/runtime/scripts/amalgm-mcp/agents/store.js +1 -1
  4. package/runtime/scripts/amalgm-mcp/agents/talk.js +48 -38
  5. package/runtime/scripts/amalgm-mcp/lib/computer.js +75 -0
  6. package/runtime/scripts/amalgm-mcp/lib/prefs.js +38 -28
  7. package/runtime/scripts/amalgm-mcp/tasks/executor.js +25 -18
  8. package/runtime/scripts/chat-core/adapters/claude.js +1 -1
  9. package/runtime/scripts/chat-core/adapters/codex.js +3 -1
  10. package/runtime/scripts/chat-core/adapters/opencode.js +7 -4
  11. package/runtime/scripts/chat-core/auth.js +11 -29
  12. package/runtime/scripts/chat-core/contract.js +44 -32
  13. package/runtime/scripts/chat-core/engine.js +0 -1
  14. package/runtime/scripts/chat-core/normalizers/codex.js +29 -6
  15. package/runtime/scripts/chat-core/parts.js +2 -0
  16. package/runtime/scripts/chat-core/tests/auth.test.js +50 -44
  17. package/runtime/scripts/chat-core/tests/engine.test.js +1 -1
  18. package/runtime/scripts/chat-core/tests/native-binaries.test.js +126 -0
  19. package/runtime/scripts/chat-core/tests/native-config.test.js +87 -9
  20. package/runtime/scripts/chat-core/tool-shape.js +4 -2
  21. package/runtime/scripts/chat-core/tooling/native-binaries.js +20 -3
  22. package/runtime/scripts/chat-core/tooling/native-config.js +54 -39
  23. package/runtime/scripts/chat-core/tooling/runtime-home.js +3 -1
  24. package/runtime/scripts/chat-server/model-catalog.js +10 -5
  25. package/runtime/scripts/credential-adapter.js +1 -1
  26. package/runtime/scripts/test-claude-code-models.js +17 -12
@@ -11,6 +11,7 @@ const {
11
11
  claudeNativeHookSettings,
12
12
  syncClaudeNativeConfig,
13
13
  syncCodexNativeConfig,
14
+ syncOpenCodeNativeConfig,
14
15
  } = require('../tooling/native-config');
15
16
  const { prepareHarnessRuntime } = require('../tooling/runtime-home');
16
17
 
@@ -108,6 +109,50 @@ test('prepared opencode runtime always has an isolated managed home', () => {
108
109
  });
109
110
  });
110
111
 
112
+ test('provider auth imports opencode native config into managed home', () => {
113
+ withNativeHome((home) => {
114
+ const configDir = path.join(home, '.config', 'opencode');
115
+ const dataDir = path.join(home, '.local', 'share', 'opencode');
116
+ fs.mkdirSync(configDir, { recursive: true });
117
+ fs.mkdirSync(dataDir, { recursive: true });
118
+ fs.writeFileSync(path.join(configDir, 'opencode.json'), '{"mcp":{}}');
119
+ fs.writeFileSync(path.join(dataDir, 'auth.json'), '{"provider":"test"}');
120
+
121
+ const runtimeHome = path.join(home, 'managed', 'opencode-home');
122
+ const prepared = prepareHarnessRuntime({
123
+ harness: 'opencode',
124
+ authMethod: 'provider_auth',
125
+ auth: { runtimeHome },
126
+ }, { PATH: '/bin', HOME: home });
127
+
128
+ assert.equal(prepared.runtimeHome, runtimeHome);
129
+ assert.equal(prepared.env.HOME, runtimeHome);
130
+ assert.equal(prepared.env.OPENCODE_HOME, runtimeHome);
131
+ assert.equal(prepared.env.XDG_CONFIG_HOME, path.join(runtimeHome, '.config'));
132
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.config', 'opencode', 'opencode.json')), true);
133
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'auth.json')), true);
134
+ });
135
+ });
136
+
137
+ test('non-user auth prepares a home without importing native config', () => {
138
+ withNativeHome((home) => {
139
+ const source = path.join(home, '.codex');
140
+ fs.mkdirSync(source, { recursive: true });
141
+ fs.writeFileSync(path.join(source, 'config.toml'), 'model = "native"');
142
+
143
+ const runtimeHome = path.join(home, 'managed', 'codex-home');
144
+ const prepared = prepareHarnessRuntime({
145
+ harness: 'codex',
146
+ authMethod: 'amalgm',
147
+ auth: { runtimeHome, tokenRef: 'test-token', baseUrl: 'https://example.test' },
148
+ }, { PATH: '/bin', HOME: home });
149
+
150
+ assert.equal(prepared.syncInfo, null);
151
+ assert.equal(fs.existsSync(runtimeHome), true);
152
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'config.toml')), false);
153
+ });
154
+ });
155
+
111
156
  test('codex provider config keeps native mcp config and overrides managed model provider', () => {
112
157
  withNativeHome((home) => {
113
158
  const source = path.join(home, '.codex');
@@ -147,7 +192,7 @@ test('codex provider config keeps native mcp config and overrides managed model
147
192
  });
148
193
  });
149
194
 
150
- test('codex amalgm config keeps native mcp config and overrides managed model provider', () => {
195
+ test('codex amalgm config does not import native user config', () => {
151
196
  withNativeHome((home) => {
152
197
  const source = path.join(home, '.codex');
153
198
  fs.mkdirSync(source, { recursive: true });
@@ -176,12 +221,11 @@ test('codex amalgm config keeps native mcp config and overrides managed model pr
176
221
 
177
222
  const config = fs.readFileSync(path.join(runtimeHome, 'config.toml'), 'utf8');
178
223
  assert.match(config, /model_provider = "amalgm"/);
179
- assert.match(config, /codex_hooks = true/);
180
224
  assert.doesNotMatch(config, /native-provider/);
181
- assert.match(config, /\[mcp_servers\.native\]/);
182
- assert.match(config, /\[mcp_servers\.native\.http_headers\]/);
183
- assert.match(config, /Authorization = "Bearer test"/);
184
- assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
225
+ assert.doesNotMatch(config, /codex_hooks = true/);
226
+ assert.doesNotMatch(config, /\[mcp_servers\.native\]/);
227
+ assert.doesNotMatch(config, /Authorization = "Bearer test"/);
228
+ assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), false);
185
229
  });
186
230
  });
187
231
 
@@ -209,7 +253,7 @@ test('claude extracts native hooks without enabling full filesystem settings', (
209
253
  authMethod: 'provider_auth',
210
254
  auth: { runtimeHome },
211
255
  cwd: home,
212
- cliModel: 'anthropic/claude-opus-4.7',
256
+ cliModel: 'anthropic/claude-opus-4.8',
213
257
  mcpServers: [],
214
258
  });
215
259
 
@@ -219,7 +263,7 @@ test('claude extracts native hooks without enabling full filesystem settings', (
219
263
  });
220
264
  });
221
265
 
222
- test('claude native sync skips debug symlinks', () => {
266
+ test('claude native sync does not delete existing debug symlinks', () => {
223
267
  withNativeHome((home) => {
224
268
  const source = path.join(home, '.claude');
225
269
  fs.mkdirSync(path.join(source, 'debug'), { recursive: true });
@@ -237,7 +281,41 @@ test('claude native sync skips debug symlinks', () => {
237
281
  assert.equal(result.sourceDir, source);
238
282
  assert.equal(fs.existsSync(path.join(runtimeHome, 'settings.json')), true);
239
283
  assert.equal(fs.existsSync(path.join(runtimeHome, 'debug', 'trace.txt')), false);
240
- assert.equal(fs.existsSync(path.join(runtimeHome, 'debug', 'latest')), false);
284
+ assert.equal(fs.lstatSync(path.join(runtimeHome, 'debug', 'latest')).isSymbolicLink(), true);
285
+ });
286
+ });
287
+
288
+ test('native sync never replaces existing in-home dot dirs', () => {
289
+ withNativeHome((home) => {
290
+ const source = path.join(home, '.codex');
291
+ fs.mkdirSync(source, { recursive: true });
292
+ fs.writeFileSync(path.join(source, 'config.toml'), 'model = "gpt-5.5"');
293
+
294
+ const runtimeHome = path.join(home, 'runtime-home');
295
+ fs.mkdirSync(path.join(runtimeHome, '.codex'), { recursive: true });
296
+ fs.writeFileSync(path.join(runtimeHome, '.codex', 'sentinel'), 'keep');
297
+
298
+ syncCodexNativeConfig(runtimeHome);
299
+
300
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'sentinel')), true);
301
+ assert.equal(fs.lstatSync(path.join(runtimeHome, '.codex')).isDirectory(), true);
302
+ });
303
+ });
304
+
305
+ test('opencode native sync preserves existing session state', () => {
306
+ withNativeHome((home) => {
307
+ const dataDir = path.join(home, '.local', 'share', 'opencode');
308
+ fs.mkdirSync(dataDir, { recursive: true });
309
+ fs.writeFileSync(path.join(dataDir, 'auth.json'), '{"provider":"test"}');
310
+
311
+ const runtimeHome = path.join(home, 'runtime-home');
312
+ fs.mkdirSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'session'), { recursive: true });
313
+ fs.writeFileSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'session', 'sentinel'), 'keep');
314
+
315
+ syncOpenCodeNativeConfig(runtimeHome);
316
+
317
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'auth.json')), true);
318
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.local', 'share', 'opencode', 'session', 'sentinel')), true);
241
319
  });
242
320
  });
243
321
 
@@ -52,11 +52,12 @@ function normalizeError(error, fallback = 'Tool failed') {
52
52
 
53
53
  function toolKind(name = '', input = {}, metadata = {}) {
54
54
  const clean = String(name || '').toLowerCase();
55
+ const compact = clean.replace(/[^a-z0-9]/g, '');
55
56
  const mcp = metadata.mcp || parseMcpName(name);
56
57
  if (metadata.subagent || clean === 'task' || clean.includes('spawnagent') || clean.includes('collabagent')) return 'subagent';
57
58
  if (clean.includes('todo')) return 'todo';
58
59
  if (clean.includes('question') || clean.includes('askuser')) return 'question';
59
- if (clean.includes('toolsearch')) return 'search';
60
+ if (compact.includes('toolsearch')) return 'search';
60
61
  if (clean.includes('bash') || clean.includes('command')) return 'bash';
61
62
  if (clean.includes('websearch') || clean === 'websearch') return 'web_search';
62
63
  if (clean.includes('browser') || mcp?.tool?.startsWith?.('browser_')) return 'browser';
@@ -101,6 +102,7 @@ function titleForTool({ name, input = {}, output, error, metadata = {}, fallback
101
102
  if (mcp) return titleFromMcp(mcp.tool, input);
102
103
 
103
104
  const clean = String(name || '').toLowerCase();
105
+ const compact = clean.replace(/[^a-z0-9]/g, '');
104
106
  if (metadata.action?.type === 'search') {
105
107
  return `Searching ${compactText(metadata.action.query || input.query || 'the web', 80)}`;
106
108
  }
@@ -114,7 +116,7 @@ function titleForTool({ name, input = {}, output, error, metadata = {}, fallback
114
116
  const host = hostLabel(url) || 'current page';
115
117
  return pattern ? `Finding "${compactText(pattern, 60)}" on ${host}` : `Finding text on ${host}`;
116
118
  }
117
- if (clean.includes('toolsearch')) {
119
+ if (compact.includes('toolsearch')) {
118
120
  const selected = String(input.query || '').replace(/^select:/i, '').trim();
119
121
  return selected ? `Selecting ${compactText(selected, 80)}` : 'Selecting tool';
120
122
  }
@@ -11,13 +11,22 @@ function amalgmDir() {
11
11
 
12
12
  function nativeNodeModulesDirs() {
13
13
  const resourcesPath = process.resourcesPath || '';
14
- return unique([
15
- process.env.AMALGM_NATIVE_NODE_MODULES,
16
- path.join(amalgmDir(), 'native', 'node_modules'),
14
+ const resourceNodeModules = [
17
15
  resourcesPath ? path.join(resourcesPath, 'app.asar.unpacked', 'node_modules') : '',
18
16
  resourcesPath ? path.join(resourcesPath, 'app.asar', 'node_modules') : '',
19
17
  resourcesPath ? path.join(resourcesPath, 'cli', 'node_modules') : '',
20
18
  resourcesPath ? path.join(resourcesPath, 'runtime', 'node_modules') : '',
19
+ ].filter(Boolean);
20
+ const wrapperPackages = [
21
+ '@anthropic-ai/claude-agent-sdk',
22
+ '@openai/codex',
23
+ 'opencode-ai',
24
+ ];
25
+ return unique([
26
+ process.env.AMALGM_NATIVE_NODE_MODULES,
27
+ path.join(amalgmDir(), 'native', 'node_modules'),
28
+ ...resourceNodeModules,
29
+ ...resourceNodeModules.flatMap((dir) => wrapperPackages.map((packageName) => path.join(dir, packageName, 'node_modules'))),
21
30
  ].filter(Boolean));
22
31
  }
23
32
 
@@ -619,6 +628,14 @@ function ensureNativeBinaries(options = {}) {
619
628
  }
620
629
 
621
630
  module.exports = {
631
+ __private: {
632
+ bundledClaudeBinary,
633
+ claudeTargetPackage,
634
+ codexTarget,
635
+ nativeNodeModulesDirs,
636
+ openCodePackageCandidates,
637
+ packageRoot,
638
+ },
622
639
  binaryStatus,
623
640
  bundledCodexPathDirs,
624
641
  bundledClaudeBinary: resolveClaudeBinary,
@@ -57,20 +57,12 @@ function shouldCopyConfigPath(root, source) {
57
57
  function copyConfigTree(sourceDir, targetDir) {
58
58
  if (!sourceDir || !targetDir || !exists(sourceDir)) return false;
59
59
  if (path.resolve(sourceDir) === path.resolve(targetDir)) return true;
60
- fs.mkdirSync(targetDir, { recursive: true });
61
- fs.cpSync(sourceDir, targetDir, {
62
- recursive: true,
63
- force: true,
64
- errorOnExist: false,
65
- dereference: false,
66
- filter: (source) => shouldCopyConfigPath(sourceDir, source),
67
- });
68
- return true;
60
+ return copyDirBounded(sourceDir, targetDir, { maxFiles: 500, maxBytes: 25 * 1024 * 1024 }).copied;
69
61
  }
70
62
 
71
63
  function copyFileIfPresent(source, target) {
72
64
  if (!exists(source)) return false;
73
- fs.mkdirSync(path.dirname(target), { recursive: true });
65
+ if (!ensureDirectory(path.dirname(target)) || !canWriteFileTarget(target)) return false;
74
66
  fs.copyFileSync(source, target);
75
67
  try {
76
68
  fs.chmodSync(target, fs.statSync(source).mode & 0o777);
@@ -86,10 +78,8 @@ function ensureHomeAlias(runtimeHome, dotDirName) {
86
78
  if (stat.isSymbolicLink()) {
87
79
  const target = fs.readlinkSync(alias);
88
80
  if (target === '.' || path.resolve(runtimeHome, target) === path.resolve(runtimeHome)) return;
89
- fs.rmSync(alias, { recursive: true, force: true });
90
- } else {
91
- fs.rmSync(alias, { recursive: true, force: true });
92
81
  }
82
+ return;
93
83
  } catch {}
94
84
  try {
95
85
  fs.symlinkSync('.', alias, 'dir');
@@ -98,28 +88,6 @@ function ensureHomeAlias(runtimeHome, dotDirName) {
98
88
  }
99
89
  }
100
90
 
101
- function removeOutboundSymlink(root, relativePath) {
102
- const file = path.join(root, relativePath);
103
- let stat;
104
- try {
105
- stat = fs.lstatSync(file);
106
- } catch {
107
- return;
108
- }
109
- if (!stat.isSymbolicLink()) return;
110
- let target;
111
- try {
112
- target = fs.readlinkSync(file);
113
- } catch {
114
- return;
115
- }
116
- const resolvedRoot = path.resolve(root);
117
- const resolvedTarget = path.resolve(path.dirname(file), target);
118
- if (resolvedTarget !== resolvedRoot && !resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`)) {
119
- fs.rmSync(file, { force: true });
120
- }
121
- }
122
-
123
91
  function ensureNativeKeychainAlias(runtimeHome) {
124
92
  if (process.platform !== 'darwin' || !runtimeHome) return false;
125
93
  const source = path.join(nativeHome(), 'Library', 'Keychains');
@@ -131,7 +99,7 @@ function ensureNativeKeychainAlias(runtimeHome) {
131
99
  const linkTarget = fs.readlinkSync(target);
132
100
  if (path.resolve(path.dirname(target), linkTarget) === path.resolve(source)) return true;
133
101
  }
134
- fs.rmSync(target, { recursive: true, force: true });
102
+ return false;
135
103
  } catch {}
136
104
  fs.mkdirSync(path.dirname(target), { recursive: true });
137
105
  try {
@@ -146,6 +114,29 @@ function nativeHome() {
146
114
  return process.env.AMALGM_NATIVE_HOME || os.homedir();
147
115
  }
148
116
 
117
+ function ensureDirectory(dir) {
118
+ try {
119
+ const stat = fs.lstatSync(dir);
120
+ return stat.isDirectory();
121
+ } catch {
122
+ try {
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+ }
130
+
131
+ function canWriteFileTarget(target) {
132
+ try {
133
+ const stat = fs.lstatSync(target);
134
+ return stat.isFile();
135
+ } catch {
136
+ return true;
137
+ }
138
+ }
139
+
149
140
  function copyDirBounded(sourceDir, targetDir, options = {}) {
150
141
  if (!sourceDir || !targetDir || !exists(sourceDir)) return { copied: false, files: 0, bytes: 0, truncated: false };
151
142
  const maxFiles = Number(options.maxFiles || 200);
@@ -163,7 +154,7 @@ function copyDirBounded(sourceDir, targetDir, options = {}) {
163
154
  }
164
155
  if (stat.isSymbolicLink()) return;
165
156
  if (stat.isDirectory()) {
166
- fs.mkdirSync(target, { recursive: true });
157
+ if (!ensureDirectory(target)) return;
167
158
  for (const entry of fs.readdirSync(source)) {
168
159
  walk(path.join(source, entry), path.join(target, entry));
169
160
  if (state.truncated) break;
@@ -175,7 +166,7 @@ function copyDirBounded(sourceDir, targetDir, options = {}) {
175
166
  state.truncated = true;
176
167
  return;
177
168
  }
178
- fs.mkdirSync(path.dirname(target), { recursive: true });
169
+ if (!ensureDirectory(path.dirname(target)) || !canWriteFileTarget(target)) return;
179
170
  fs.copyFileSync(source, target);
180
171
  state.files += 1;
181
172
  state.bytes += stat.size;
@@ -218,7 +209,6 @@ function syncClaudeNativeConfig(runtimeHome) {
218
209
  if (path.resolve(runtimeHome) === path.resolve(home)) return null;
219
210
  const sourceDir = path.join(home, '.claude');
220
211
  fs.mkdirSync(runtimeHome, { recursive: true });
221
- removeOutboundSymlink(runtimeHome, path.join('debug', 'latest'));
222
212
  const copied = copyConfigTree(sourceDir, runtimeHome);
223
213
  ensureHomeAlias(runtimeHome, '.claude');
224
214
  ensureNativeKeychainAlias(runtimeHome);
@@ -227,6 +217,27 @@ function syncClaudeNativeConfig(runtimeHome) {
227
217
  return copied ? { sourceDir, runtimeHome } : null;
228
218
  }
229
219
 
220
+ function syncOpenCodeNativeConfig(runtimeHome) {
221
+ if (!runtimeHome) return null;
222
+ const home = nativeHome();
223
+ fs.mkdirSync(runtimeHome, { recursive: true });
224
+ const config = copyConfigTree(
225
+ path.join(home, '.config', 'opencode'),
226
+ path.join(runtimeHome, '.config', 'opencode'),
227
+ );
228
+ const data = copyConfigTree(
229
+ path.join(home, '.local', 'share', 'opencode'),
230
+ path.join(runtimeHome, '.local', 'share', 'opencode'),
231
+ );
232
+ const dot = copyConfigTree(path.join(home, '.opencode'), path.join(runtimeHome, '.opencode'));
233
+ if (!config && !data && !dot) return null;
234
+ return {
235
+ sourceDir: path.join(home, '.config', 'opencode'),
236
+ runtimeHome,
237
+ copied: config || data || dot,
238
+ };
239
+ }
240
+
230
241
  function readJsonFile(file) {
231
242
  try {
232
243
  return JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -271,8 +282,10 @@ function claudeNativeHookSettings(options = {}) {
271
282
  function syncNativeHarnessConfig(contract) {
272
283
  const runtimeHome = contract?.auth?.runtimeHome;
273
284
  if (!runtimeHome) return null;
285
+ if (contract.authMethod !== 'provider_auth') return null;
274
286
  if (contract.harness === 'codex') return syncCodexNativeConfig(runtimeHome);
275
287
  if (contract.harness === 'claude_code') return syncClaudeNativeConfig(runtimeHome);
288
+ if (contract.harness === 'opencode') return syncOpenCodeNativeConfig(runtimeHome);
276
289
  return null;
277
290
  }
278
291
 
@@ -282,6 +295,7 @@ module.exports = {
282
295
  copyConfigTree,
283
296
  copyDirBounded,
284
297
  copyFileIfPresent,
298
+ ensureDirectory,
285
299
  ensureNativeKeychainAlias,
286
300
  ensureHomeAlias,
287
301
  shouldCopyConfigPath,
@@ -290,4 +304,5 @@ module.exports = {
290
304
  syncClaudeNativeConfig,
291
305
  syncCodexNativeConfig,
292
306
  syncNativeHarnessConfig,
307
+ syncOpenCodeNativeConfig,
293
308
  };
@@ -15,7 +15,9 @@ function managedRuntimeHome(contract) {
15
15
  function prepareHarnessRuntime(contract, baseEnv = process.env) {
16
16
  const runtimeHome = managedRuntimeHome(contract);
17
17
  fs.mkdirSync(runtimeHome, { recursive: true });
18
- const syncInfo = syncNativeHarnessConfig(contract);
18
+ const syncInfo = contract?.authMethod === 'provider_auth'
19
+ ? syncNativeHarnessConfig(contract)
20
+ : null;
19
21
  return {
20
22
  runtimeHome,
21
23
  syncInfo,
@@ -49,6 +49,8 @@ const MODEL_CATALOG = [
49
49
  { id: "anthropic/claude-opus-4.6-1m", name: "Claude Opus 4.6 (1M context)", provider: "anthropic", contextWindow: 1000000, maxOutput: 128000, inputPrice: 5, outputPrice: 25, cacheRead: 0.5, cacheWrite: 6.25, tags: ["tool-use", "reasoning", "vision", "file-input", "explicit-caching", "web-search"], released: "2026-02-05" },
50
50
  { id: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7", provider: "anthropic", contextWindow: 200000, maxOutput: 128000, inputPrice: 5, outputPrice: 25, cacheRead: 0.5, cacheWrite: 6.25, tags: ["tool-use", "reasoning", "vision", "file-input", "explicit-caching", "web-search"], released: "2026-04-16" },
51
51
  { id: "anthropic/claude-opus-4.7-1m", name: "Claude Opus 4.7 (1M context)", provider: "anthropic", contextWindow: 1000000, maxOutput: 128000, inputPrice: 5, outputPrice: 25, cacheRead: 0.5, cacheWrite: 6.25, tags: ["tool-use", "reasoning", "vision", "file-input", "explicit-caching", "web-search"], released: "2026-04-16" },
52
+ { id: "anthropic/claude-opus-4.8", name: "Claude Opus 4.8", provider: "anthropic", contextWindow: 200000, maxOutput: 128000, inputPrice: 5, outputPrice: 25, cacheRead: 0.5, cacheWrite: 6.25, tags: ["tool-use", "reasoning", "vision", "file-input", "explicit-caching", "web-search"], released: null },
53
+ { id: "anthropic/claude-opus-4.8-1m", name: "Claude Opus 4.8 (1M context)", provider: "anthropic", contextWindow: 1000000, maxOutput: 128000, inputPrice: 5, outputPrice: 25, cacheRead: 0.5, cacheWrite: 6.25, tags: ["tool-use", "reasoning", "vision", "file-input", "explicit-caching", "web-search"], released: null },
52
54
  { id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4", provider: "anthropic", contextWindow: 1000000, maxOutput: 64000, inputPrice: 3, outputPrice: 15, cacheRead: 0.3, cacheWrite: 3.75, tags: ["file-input", "reasoning", "tool-use", "vision", "explicit-caching"], released: "2025-05-22" },
53
55
  { id: "anthropic/claude-sonnet-4.5", name: "Claude Sonnet 4.5", provider: "anthropic", contextWindow: 1000000, maxOutput: 64000, inputPrice: 3, outputPrice: 15, cacheRead: 0.3, cacheWrite: 3.75, tags: ["file-input", "reasoning", "tool-use", "vision", "explicit-caching"], released: "2025-09-29" },
54
56
  { id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6", provider: "anthropic", contextWindow: 1000000, maxOutput: 128000, inputPrice: 3, outputPrice: 15, cacheRead: 0.3, cacheWrite: 3.75, tags: ["file-input", "reasoning", "tool-use", "vision", "explicit-caching", "web-search"], released: "2026-02-17" },
@@ -236,10 +238,13 @@ const MODEL_CATALOG = [
236
238
  /** Find model by exact ID or fuzzy match (strips provider prefix, date suffixes). */
237
239
  // CLI agent internal model names → catalog IDs
238
240
  const MODEL_ALIASES = {
239
- 'claude-code-opus': 'anthropic/claude-opus-4.7',
240
- 'opus': 'anthropic/claude-opus-4.7',
241
- 'opus1m': 'anthropic/claude-opus-4.7-1m',
242
- 'opus[1m]': 'anthropic/claude-opus-4.7-1m',
241
+ 'claude-code-opus': 'anthropic/claude-opus-4.8',
242
+ 'opus': 'anthropic/claude-opus-4.8',
243
+ 'opus1m': 'anthropic/claude-opus-4.8-1m',
244
+ 'opus[1m]': 'anthropic/claude-opus-4.8-1m',
245
+ 'claude-opus-4-8': 'anthropic/claude-opus-4.8',
246
+ 'claude-opus-4.8-1m': 'anthropic/claude-opus-4.8-1m',
247
+ 'claude-opus-4.8:1m': 'anthropic/claude-opus-4.8-1m',
243
248
  'claude-opus-4-7': 'anthropic/claude-opus-4.7',
244
249
  'claude-opus-4.7-1m': 'anthropic/claude-opus-4.7-1m',
245
250
  'claude-opus-4.7:1m': 'anthropic/claude-opus-4.7-1m',
@@ -255,7 +260,7 @@ function getModel(modelId) {
255
260
  if (!modelId) return null;
256
261
  const id = modelId.toLowerCase();
257
262
 
258
- // Check aliases first (e.g. claude-code-opus -> anthropic/claude-opus-4.7)
263
+ // Check aliases first (e.g. claude-code-opus -> anthropic/claude-opus-4.8)
259
264
  const aliased = MODEL_ALIASES[id];
260
265
  if (aliased) return MODEL_CATALOG.find(e => e.id === aliased) || null;
261
266
 
@@ -18,7 +18,7 @@ const VALID_AUTH_MODES = ['amalgm', 'provider_auth', 'byok'];
18
18
  const SUPPORTED_AUTH_BY_HARNESS = {
19
19
  claude_code: ['amalgm', 'byok', 'provider_auth'],
20
20
  codex: ['amalgm', 'byok', 'provider_auth'],
21
- opencode: ['amalgm', 'byok'],
21
+ opencode: ['amalgm', 'byok', 'provider_auth'],
22
22
  pi: ['amalgm', 'byok', 'provider_auth'],
23
23
  };
24
24
 
@@ -15,7 +15,7 @@ const {
15
15
  resolveModelSelection,
16
16
  } = require('./amalgm-mcp/lib/prefs');
17
17
 
18
- const OPUS_1M = 'anthropic/claude-opus-4.7-1m';
18
+ const OPUS_1M = 'anthropic/claude-opus-4.8-1m';
19
19
 
20
20
  assert.deepEqual(
21
21
  getHarnessModels('claude_code').map((model) => ({
@@ -26,21 +26,26 @@ assert.deepEqual(
26
26
  [
27
27
  { id: 'anthropic/claude-haiku-4.5', name: 'Haiku 4.5', cliModel: 'haiku' },
28
28
  { id: 'anthropic/claude-sonnet-4.6', name: 'Sonnet 4.6', cliModel: 'sonnet' },
29
- { id: 'anthropic/claude-opus-4.7', name: 'Opus 4.7', cliModel: 'opus' },
30
- { id: OPUS_1M, name: 'Opus 4.7 1M', cliModel: 'opus[1m]' },
29
+ { id: 'anthropic/claude-opus-4.8', name: 'Opus 4.8', cliModel: 'opus' },
30
+ { id: OPUS_1M, name: 'Opus 4.8 1M', cliModel: 'opus[1m]' },
31
31
  ],
32
32
  );
33
33
 
34
34
  assert.equal(canonicalModel('haiku', 'claude_code'), 'anthropic/claude-haiku-4.5');
35
35
  assert.equal(canonicalModel('sonnet', 'claude_code'), 'anthropic/claude-sonnet-4.6');
36
- assert.equal(canonicalModel('opus', 'claude_code'), 'anthropic/claude-opus-4.7');
36
+ assert.equal(canonicalModel('opus', 'claude_code'), 'anthropic/claude-opus-4.8');
37
37
  assert.equal(canonicalModel('opus1m', 'claude_code'), OPUS_1M);
38
38
  assert.equal(canonicalModel('opus[1m]', 'claude_code'), OPUS_1M);
39
- assert.equal(canonicalModel('claude-code-opus-46', 'claude_code'), 'anthropic/claude-opus-4.7');
40
- assert.equal(canonicalModel('claude-code-opus-4.6', 'claude_code'), 'anthropic/claude-opus-4.7');
41
- assert.equal(canonicalModel('claude-opus-4-6', 'claude_code'), 'anthropic/claude-opus-4.7');
42
- assert.equal(canonicalModel('anthropic/claude-opus-4-6', 'claude_code'), 'anthropic/claude-opus-4.7');
43
- assert.equal(canonicalModel('anthropic/claude-opus-4.5', 'claude_code'), 'anthropic/claude-opus-4.7');
39
+ assert.equal(canonicalModel('claude-code-opus-48', 'claude_code'), 'anthropic/claude-opus-4.8');
40
+ assert.equal(canonicalModel('claude-code-opus-4.8', 'claude_code'), 'anthropic/claude-opus-4.8');
41
+ assert.equal(canonicalModel('claude-opus-4-8', 'claude_code'), 'anthropic/claude-opus-4.8');
42
+ assert.equal(canonicalModel('claude-code-opus-47', 'claude_code'), 'anthropic/claude-opus-4.8');
43
+ assert.equal(canonicalModel('claude-code-opus-46', 'claude_code'), 'anthropic/claude-opus-4.8');
44
+ assert.equal(canonicalModel('claude-code-opus-4.6', 'claude_code'), 'anthropic/claude-opus-4.8');
45
+ assert.equal(canonicalModel('claude-opus-4-6', 'claude_code'), 'anthropic/claude-opus-4.8');
46
+ assert.equal(canonicalModel('anthropic/claude-opus-4-6', 'claude_code'), 'anthropic/claude-opus-4.8');
47
+ assert.equal(canonicalModel('anthropic/claude-opus-4.7', 'claude_code'), 'anthropic/claude-opus-4.8');
48
+ assert.equal(canonicalModel('anthropic/claude-opus-4.5', 'claude_code'), 'anthropic/claude-opus-4.8');
44
49
  assert.equal(canonicalModel('claude-code-sonnet-45', 'claude_code'), 'anthropic/claude-sonnet-4.6');
45
50
  assert.equal(canonicalModel('claude-code-sonnet-4.5', 'claude_code'), 'anthropic/claude-sonnet-4.6');
46
51
  assert.equal(canonicalModel('claude-sonnet-4-5', 'claude_code'), 'anthropic/claude-sonnet-4.6');
@@ -49,14 +54,14 @@ assert.equal(canonicalModel('claude-code-haiku-4.5', 'claude_code'), 'anthropic/
49
54
 
50
55
  assert.equal(cliModelFor({ harness: 'claude_code', modelId: 'anthropic/claude-haiku-4.5' }), 'haiku');
51
56
  assert.equal(cliModelFor({ harness: 'claude_code', modelId: 'anthropic/claude-sonnet-4.6' }), 'sonnet');
52
- assert.equal(cliModelFor({ harness: 'claude_code', modelId: 'anthropic/claude-opus-4.7' }), 'opus');
57
+ assert.equal(cliModelFor({ harness: 'claude_code', modelId: 'anthropic/claude-opus-4.8' }), 'opus');
53
58
  assert.equal(cliModelFor({ harness: 'claude_code', modelId: OPUS_1M }), 'opus[1m]');
54
- assert.equal(cliModelFor({ harness: 'claude_code', modelId: 'anthropic/claude-opus-4.7', cliModel: 'opus1m' }), 'opus[1m]');
59
+ assert.equal(cliModelFor({ harness: 'claude_code', modelId: 'anthropic/claude-opus-4.8', cliModel: 'opus1m' }), 'opus[1m]');
55
60
 
56
61
  for (const [modelId, cliModel] of [
57
62
  ['anthropic/claude-haiku-4.5', 'haiku'],
58
63
  ['anthropic/claude-sonnet-4.6', 'sonnet'],
59
- ['anthropic/claude-opus-4.7', 'opus'],
64
+ ['anthropic/claude-opus-4.8', 'opus'],
60
65
  [OPUS_1M, 'opus[1m]'],
61
66
  ]) {
62
67
  const contract = createContract({