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.
- package/package.json +4 -3
- package/runtime/lib/harnesses.js +47 -37
- package/runtime/scripts/amalgm-mcp/agents/store.js +1 -1
- package/runtime/scripts/amalgm-mcp/agents/talk.js +48 -38
- package/runtime/scripts/amalgm-mcp/lib/computer.js +75 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +38 -28
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +25 -18
- package/runtime/scripts/chat-core/adapters/claude.js +1 -1
- package/runtime/scripts/chat-core/adapters/codex.js +3 -1
- package/runtime/scripts/chat-core/adapters/opencode.js +7 -4
- package/runtime/scripts/chat-core/auth.js +11 -29
- package/runtime/scripts/chat-core/contract.js +44 -32
- package/runtime/scripts/chat-core/engine.js +0 -1
- package/runtime/scripts/chat-core/normalizers/codex.js +29 -6
- package/runtime/scripts/chat-core/parts.js +2 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +50 -44
- package/runtime/scripts/chat-core/tests/engine.test.js +1 -1
- package/runtime/scripts/chat-core/tests/native-binaries.test.js +126 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +87 -9
- package/runtime/scripts/chat-core/tool-shape.js +4 -2
- package/runtime/scripts/chat-core/tooling/native-binaries.js +20 -3
- package/runtime/scripts/chat-core/tooling/native-config.js +54 -39
- package/runtime/scripts/chat-core/tooling/runtime-home.js +3 -1
- package/runtime/scripts/chat-server/model-catalog.js +10 -5
- package/runtime/scripts/credential-adapter.js +1 -1
- 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
|
|
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.
|
|
182
|
-
assert.
|
|
183
|
-
assert.
|
|
184
|
-
assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')),
|
|
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.
|
|
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
|
|
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.
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
240
|
-
'opus': 'anthropic/claude-opus-4.
|
|
241
|
-
'opus1m': 'anthropic/claude-opus-4.
|
|
242
|
-
'opus[1m]': 'anthropic/claude-opus-4.
|
|
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.
|
|
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.
|
|
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.
|
|
30
|
-
{ id: OPUS_1M, name: 'Opus 4.
|
|
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.
|
|
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-
|
|
40
|
-
assert.equal(canonicalModel('claude-code-opus-4.
|
|
41
|
-
assert.equal(canonicalModel('claude-opus-4-
|
|
42
|
-
assert.equal(canonicalModel('
|
|
43
|
-
assert.equal(canonicalModel('
|
|
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.
|
|
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.
|
|
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.
|
|
64
|
+
['anthropic/claude-opus-4.8', 'opus'],
|
|
60
65
|
[OPUS_1M, 'opus[1m]'],
|
|
61
66
|
]) {
|
|
62
67
|
const contract = createContract({
|