amalgm 0.1.47 → 0.1.49
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 +1 -1
- package/runtime/scripts/chat-core/adapters/claude.js +7 -4
- package/runtime/scripts/chat-core/adapters/codex.js +12 -4
- package/runtime/scripts/chat-core/auth.js +3 -1
- package/runtime/scripts/chat-core/tests/auth.test.js +42 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +127 -0
- package/runtime/scripts/chat-core/tooling/native-config.js +129 -18
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude }
|
|
|
8
8
|
const { recordNativeEvent } = require('../recorder');
|
|
9
9
|
const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
|
|
10
10
|
const { bundledClaudeBinary } = require('../tooling/native-binaries');
|
|
11
|
-
const {
|
|
11
|
+
const { claudeNativeHookSettings } = require('../tooling/native-config');
|
|
12
12
|
const { importPackage } = require('../tooling/package-import');
|
|
13
13
|
const { composeSystemPrompt } = require('../tooling/system-prompt');
|
|
14
14
|
|
|
@@ -32,15 +32,18 @@ class ClaudeAdapter {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
options(contract, extra = {}) {
|
|
35
|
-
syncNativeHarnessConfig(contract);
|
|
36
35
|
const systemPrompt = composeSystemPrompt(contract);
|
|
37
36
|
const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
|
|
37
|
+
const settings = {
|
|
38
|
+
...(claudeNativeHookSettings({ cwd: contract.cwd }) || {}),
|
|
39
|
+
...(contract.fastMode ? { fastMode: true, fastModePerSessionOptIn: true } : {}),
|
|
40
|
+
};
|
|
38
41
|
return {
|
|
39
42
|
cwd: contract.cwd,
|
|
40
43
|
env: runtimeEnv(contract),
|
|
41
44
|
model: contract.cliModel,
|
|
42
45
|
...(contract.reasoningEffort ? { effort: contract.reasoningEffort } : {}),
|
|
43
|
-
...(
|
|
46
|
+
...(Object.keys(settings).length > 0 ? { settings } : {}),
|
|
44
47
|
...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
|
|
45
48
|
...(systemPrompt ? { systemPrompt: { type: 'preset', preset: 'claude_code', append: systemPrompt } } : {}),
|
|
46
49
|
mcpServers: toClaudeMcpServers(contract),
|
|
@@ -49,7 +52,7 @@ class ClaudeAdapter {
|
|
|
49
52
|
...(process.env.CHAT_CORE_DEBUG_CLAUDE === '1'
|
|
50
53
|
? { debug: true, debugFile: path.join(contract.auth.runtimeHome || process.cwd(), 'claude-debug.log') }
|
|
51
54
|
: {}),
|
|
52
|
-
settingSources: [
|
|
55
|
+
settingSources: [],
|
|
53
56
|
strictMcpConfig: false,
|
|
54
57
|
...extra,
|
|
55
58
|
};
|
|
@@ -263,6 +263,10 @@ function hookTrustBlocks(toml) {
|
|
|
263
263
|
return blocks;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
function hookTrustToml(toml) {
|
|
267
|
+
return hookTrustBlocks(toml).map((block) => block.lines.join('\n')).join('\n\n');
|
|
268
|
+
}
|
|
269
|
+
|
|
266
270
|
function mirrorCodexHookTrust(toml, sourceDir, runtimeHome) {
|
|
267
271
|
if (!sourceDir || !runtimeHome) return toml;
|
|
268
272
|
const sourceHookPrefix = `${path.join(sourceDir, 'hooks.json')}:`;
|
|
@@ -298,7 +302,7 @@ function generatedMcpSectionNames(contract) {
|
|
|
298
302
|
function buildCodexConfig(contract, existingConfig, syncInfo) {
|
|
299
303
|
const mcpToml = toCodexMcpToml(contract);
|
|
300
304
|
const topLevelKeys = [
|
|
301
|
-
|
|
305
|
+
'model_provider',
|
|
302
306
|
...(contract.authMethod === 'amalgm' ? ['model_context_window', 'model_auto_compact_token_limit'] : []),
|
|
303
307
|
];
|
|
304
308
|
let config = removeTopLevelKeys(existingConfig, topLevelKeys);
|
|
@@ -324,6 +328,8 @@ function buildCodexConfig(contract, existingConfig, syncInfo) {
|
|
|
324
328
|
].join('\n')
|
|
325
329
|
: contract.authMethod === 'byok'
|
|
326
330
|
? 'model_provider = "openai"'
|
|
331
|
+
: contract.authMethod === 'provider_auth'
|
|
332
|
+
? 'model_provider = "openai"'
|
|
327
333
|
: '';
|
|
328
334
|
return [
|
|
329
335
|
generated.trimEnd(),
|
|
@@ -337,18 +343,19 @@ function writeConfig(contract) {
|
|
|
337
343
|
if (!home) return;
|
|
338
344
|
fs.mkdirSync(home, { recursive: true });
|
|
339
345
|
const syncInfo = syncCodexNativeConfig(home);
|
|
346
|
+
const nativeHookTrust = hookTrustToml(readTextFile(syncInfo?.sourceConfigPath));
|
|
340
347
|
const configPath = path.join(home, 'config.toml');
|
|
341
348
|
if (contract.authMethod === 'provider_auth') {
|
|
342
|
-
const sourceAuth = path.join(os.homedir(), '.codex', 'auth.json');
|
|
349
|
+
const sourceAuth = path.join(syncInfo?.sourceDir || path.join(os.homedir(), '.codex'), 'auth.json');
|
|
343
350
|
const targetAuth = path.join(home, 'auth.json');
|
|
344
351
|
if (fs.existsSync(sourceAuth)) {
|
|
345
352
|
fs.copyFileSync(sourceAuth, targetAuth);
|
|
346
353
|
fs.chmodSync(targetAuth, 0o600);
|
|
347
354
|
}
|
|
348
|
-
fs.writeFileSync(configPath, buildCodexConfig(contract,
|
|
355
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, nativeHookTrust, syncInfo), { mode: 0o600 });
|
|
349
356
|
return;
|
|
350
357
|
}
|
|
351
|
-
fs.writeFileSync(configPath, buildCodexConfig(contract,
|
|
358
|
+
fs.writeFileSync(configPath, buildCodexConfig(contract, nativeHookTrust, syncInfo), { mode: 0o600 });
|
|
352
359
|
fs.writeFileSync(path.join(home, 'auth.json'), JSON.stringify({
|
|
353
360
|
auth_mode: 'apikey',
|
|
354
361
|
OPENAI_API_KEY: contract.auth.tokenRef,
|
|
@@ -488,6 +495,7 @@ module.exports = {
|
|
|
488
495
|
__private: {
|
|
489
496
|
buildCodexConfig,
|
|
490
497
|
ensureFeatureBoolean,
|
|
498
|
+
hookTrustToml,
|
|
491
499
|
mirrorCodexHookTrust,
|
|
492
500
|
modelWindowConfigLines,
|
|
493
501
|
removeTomlSections,
|
|
@@ -70,7 +70,9 @@ function coerceAuth(harness, requested) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function runtimeHome({ amalgmDir, sessionId, harness, authMethod, tokenFingerprint }) {
|
|
73
|
-
|
|
73
|
+
// Claude Code provider auth is tied to the user's real home/keychain context.
|
|
74
|
+
// Isolating HOME makes the bundled Claude binary look logged out on macOS.
|
|
75
|
+
if (authMethod === 'provider_auth' && harness !== 'codex') return null;
|
|
74
76
|
const suffix = tokenFingerprint || 'default';
|
|
75
77
|
return path.join(amalgmDir, 'runtime', sessionId, harness, suffix);
|
|
76
78
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const test = require('node:test');
|
|
5
|
+
const { authEnvelope, runtimeEnv } = require('../auth');
|
|
6
|
+
|
|
7
|
+
test('claude provider auth keeps the user home environment', () => {
|
|
8
|
+
const envelope = authEnvelope({
|
|
9
|
+
harness: 'claude_code',
|
|
10
|
+
authMethod: 'provider_auth',
|
|
11
|
+
sessionId: 'session-test',
|
|
12
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
assert.equal(envelope.runtimeHome, null);
|
|
16
|
+
|
|
17
|
+
const env = runtimeEnv({
|
|
18
|
+
harness: 'claude_code',
|
|
19
|
+
authMethod: 'provider_auth',
|
|
20
|
+
auth: envelope,
|
|
21
|
+
}, {
|
|
22
|
+
HOME: '/Users/example',
|
|
23
|
+
PATH: '/bin',
|
|
24
|
+
CLAUDE_CONFIG_DIR: '/Users/example/.claude',
|
|
25
|
+
ANTHROPIC_API_KEY: 'should-not-leak',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
assert.equal(env.HOME, '/Users/example');
|
|
29
|
+
assert.equal(env.CLAUDE_CONFIG_DIR, '/Users/example/.claude');
|
|
30
|
+
assert.equal(env.ANTHROPIC_API_KEY, undefined);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('codex provider auth still receives an isolated runtime home', () => {
|
|
34
|
+
const envelope = authEnvelope({
|
|
35
|
+
harness: 'codex',
|
|
36
|
+
authMethod: 'provider_auth',
|
|
37
|
+
sessionId: 'session-test',
|
|
38
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.match(envelope.runtimeHome, /\/tmp\/amalgm-test\/runtime\/session-test\/codex\/[^/]+$/);
|
|
42
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const { ClaudeAdapter } = require('../adapters/claude');
|
|
9
|
+
const { __private: codexPrivate } = require('../adapters/codex');
|
|
10
|
+
const {
|
|
11
|
+
claudeNativeHookSettings,
|
|
12
|
+
syncCodexNativeConfig,
|
|
13
|
+
} = require('../tooling/native-config');
|
|
14
|
+
|
|
15
|
+
function withNativeHome(fn) {
|
|
16
|
+
const previousNativeHome = process.env.AMALGM_NATIVE_HOME;
|
|
17
|
+
const previousHome = process.env.HOME;
|
|
18
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-native-config-'));
|
|
19
|
+
process.env.AMALGM_NATIVE_HOME = root;
|
|
20
|
+
process.env.HOME = root;
|
|
21
|
+
try {
|
|
22
|
+
return fn(root);
|
|
23
|
+
} finally {
|
|
24
|
+
if (previousNativeHome === undefined) delete process.env.AMALGM_NATIVE_HOME;
|
|
25
|
+
else process.env.AMALGM_NATIVE_HOME = previousNativeHome;
|
|
26
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
27
|
+
else process.env.HOME = previousHome;
|
|
28
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test('codex native sync copies hook support without bulk runtime state', () => {
|
|
33
|
+
withNativeHome((home) => {
|
|
34
|
+
const source = path.join(home, '.codex');
|
|
35
|
+
fs.mkdirSync(path.join(source, 'sessions'), { recursive: true });
|
|
36
|
+
fs.mkdirSync(path.join(source, 'worktrees'), { recursive: true });
|
|
37
|
+
fs.mkdirSync(path.join(source, 'plugins'), { recursive: true });
|
|
38
|
+
fs.mkdirSync(path.join(source, 'supermemory'), { recursive: true });
|
|
39
|
+
fs.writeFileSync(path.join(source, 'hooks.json'), '{"hooks":{}}');
|
|
40
|
+
fs.writeFileSync(path.join(source, 'supermemory.json'), '{"projectContainerTag":"test"}');
|
|
41
|
+
fs.writeFileSync(path.join(source, 'supermemory', 'recall.js'), 'console.log("recall")');
|
|
42
|
+
fs.writeFileSync(path.join(source, 'sessions', 'huge.jsonl'), 'nope');
|
|
43
|
+
fs.writeFileSync(path.join(source, 'worktrees', 'tree'), 'nope');
|
|
44
|
+
fs.writeFileSync(path.join(source, 'plugins', 'cache'), 'nope');
|
|
45
|
+
|
|
46
|
+
const runtimeHome = path.join(home, 'runtime-home');
|
|
47
|
+
fs.mkdirSync(path.join(runtimeHome, 'sessions'), { recursive: true });
|
|
48
|
+
fs.writeFileSync(path.join(runtimeHome, 'sessions', 'legacy.jsonl'), 'old');
|
|
49
|
+
|
|
50
|
+
const result = syncCodexNativeConfig(runtimeHome);
|
|
51
|
+
|
|
52
|
+
assert.equal(result.sourceDir, source);
|
|
53
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'hooks.json')), true);
|
|
54
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory.json')), true);
|
|
55
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory', 'recall.js')), true);
|
|
56
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'sessions')), false);
|
|
57
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'worktrees')), false);
|
|
58
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, 'plugins')), false);
|
|
59
|
+
assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'hooks.json')), true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('codex provider config keeps hooks but drops native model and mcp config', () => {
|
|
64
|
+
withNativeHome((home) => {
|
|
65
|
+
const source = path.join(home, '.codex');
|
|
66
|
+
fs.mkdirSync(source, { recursive: true });
|
|
67
|
+
const hooksPath = path.join(source, 'hooks.json');
|
|
68
|
+
fs.writeFileSync(hooksPath, '{"hooks":{"UserPromptSubmit":[]}}');
|
|
69
|
+
fs.writeFileSync(path.join(source, 'auth.json'), '{"auth_mode":"chatgpt"}');
|
|
70
|
+
fs.writeFileSync(path.join(source, 'config.toml'), [
|
|
71
|
+
'model_provider = "native-provider"',
|
|
72
|
+
'',
|
|
73
|
+
'[mcp_servers.native]',
|
|
74
|
+
'command = "native-mcp"',
|
|
75
|
+
'',
|
|
76
|
+
`[hooks.state.${JSON.stringify(`${hooksPath}:UserPromptSubmit:0`)}]`,
|
|
77
|
+
'trusted = true',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n'));
|
|
80
|
+
|
|
81
|
+
const runtimeHome = path.join(home, 'runtime-home');
|
|
82
|
+
codexPrivate.writeConfig({
|
|
83
|
+
authMethod: 'provider_auth',
|
|
84
|
+
auth: { runtimeHome },
|
|
85
|
+
mcpServers: [],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const config = fs.readFileSync(path.join(runtimeHome, 'config.toml'), 'utf8');
|
|
89
|
+
assert.match(config, /model_provider = "openai"/);
|
|
90
|
+
assert.match(config, /codex_hooks = true/);
|
|
91
|
+
assert.doesNotMatch(config, /native-provider/);
|
|
92
|
+
assert.doesNotMatch(config, /mcp_servers\.native/);
|
|
93
|
+
assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('claude extracts native hooks without enabling full filesystem settings', () => {
|
|
98
|
+
withNativeHome((home) => {
|
|
99
|
+
const source = path.join(home, '.claude');
|
|
100
|
+
fs.mkdirSync(source, { recursive: true });
|
|
101
|
+
fs.writeFileSync(path.join(source, 'settings.json'), JSON.stringify({
|
|
102
|
+
permissions: { allow: ['Bash(*)'] },
|
|
103
|
+
hooks: {
|
|
104
|
+
UserPromptSubmit: [{
|
|
105
|
+
hooks: [{ type: 'command', command: 'echo recall', timeout: 1 }],
|
|
106
|
+
}],
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
const hookSettings = claudeNativeHookSettings({ cwd: home });
|
|
111
|
+
assert.deepEqual(Object.keys(hookSettings.hooks), ['UserPromptSubmit']);
|
|
112
|
+
assert.equal(hookSettings.permissions, undefined);
|
|
113
|
+
|
|
114
|
+
const adapter = new ClaudeAdapter();
|
|
115
|
+
const options = adapter.options({
|
|
116
|
+
authMethod: 'provider_auth',
|
|
117
|
+
auth: { runtimeHome: null },
|
|
118
|
+
cwd: home,
|
|
119
|
+
cliModel: 'anthropic/claude-opus-4.7',
|
|
120
|
+
mcpServers: [],
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
assert.deepEqual(options.settingSources, []);
|
|
124
|
+
assert.equal(options.settings.permissions, undefined);
|
|
125
|
+
assert.equal(options.settings.hooks.UserPromptSubmit[0].hooks[0].command, 'echo recall');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -9,12 +9,18 @@ const EXCLUDED_DIR_NAMES = new Set([
|
|
|
9
9
|
'.npm',
|
|
10
10
|
'.tmp',
|
|
11
11
|
'cache',
|
|
12
|
+
'generated_images',
|
|
12
13
|
'log',
|
|
13
14
|
'logs',
|
|
15
|
+
'memories',
|
|
16
|
+
'node_modules',
|
|
17
|
+
'plugins',
|
|
14
18
|
'projects',
|
|
15
19
|
'sessions',
|
|
16
20
|
'shell_snapshots',
|
|
21
|
+
'skills',
|
|
17
22
|
'tmp',
|
|
23
|
+
'worktrees',
|
|
18
24
|
]);
|
|
19
25
|
|
|
20
26
|
const EXCLUDED_FILE_PATTERNS = [
|
|
@@ -66,8 +72,8 @@ function copyFileIfPresent(source, target) {
|
|
|
66
72
|
return true;
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
function
|
|
70
|
-
if (!runtimeHome || !dotDirName
|
|
75
|
+
function ensureHomeAlias(runtimeHome, dotDirName) {
|
|
76
|
+
if (!runtimeHome || !dotDirName) return;
|
|
71
77
|
const alias = path.join(runtimeHome, dotDirName);
|
|
72
78
|
try {
|
|
73
79
|
const stat = fs.lstatSync(alias);
|
|
@@ -75,9 +81,6 @@ function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
|
|
|
75
81
|
const target = fs.readlinkSync(alias);
|
|
76
82
|
if (target === '.' || path.resolve(runtimeHome, target) === path.resolve(runtimeHome)) return;
|
|
77
83
|
fs.rmSync(alias, { recursive: true, force: true });
|
|
78
|
-
} else if (stat.isDirectory()) {
|
|
79
|
-
copyConfigTree(sourceDir, alias);
|
|
80
|
-
return;
|
|
81
84
|
} else {
|
|
82
85
|
fs.rmSync(alias, { recursive: true, force: true });
|
|
83
86
|
}
|
|
@@ -85,34 +88,138 @@ function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
|
|
|
85
88
|
try {
|
|
86
89
|
fs.symlinkSync('.', alias, 'dir');
|
|
87
90
|
} catch {
|
|
88
|
-
|
|
91
|
+
fs.mkdirSync(alias, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function nativeHome() {
|
|
96
|
+
return process.env.AMALGM_NATIVE_HOME || os.homedir();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pruneLegacyCodexRuntimeHome(runtimeHome) {
|
|
100
|
+
for (const name of EXCLUDED_DIR_NAMES) {
|
|
101
|
+
try {
|
|
102
|
+
fs.rmSync(path.join(runtimeHome, name), { recursive: true, force: true });
|
|
103
|
+
} catch {}
|
|
89
104
|
}
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
function copyDirBounded(sourceDir, targetDir, options = {}) {
|
|
108
|
+
if (!sourceDir || !targetDir || !exists(sourceDir)) return { copied: false, files: 0, bytes: 0, truncated: false };
|
|
109
|
+
const maxFiles = Number(options.maxFiles || 200);
|
|
110
|
+
const maxBytes = Number(options.maxBytes || 10 * 1024 * 1024);
|
|
111
|
+
const state = { copied: false, files: 0, bytes: 0, truncated: false };
|
|
112
|
+
const root = path.resolve(sourceDir);
|
|
113
|
+
|
|
114
|
+
function walk(source, target) {
|
|
115
|
+
if (state.truncated || !shouldCopyConfigPath(root, source)) return;
|
|
116
|
+
let stat;
|
|
117
|
+
try {
|
|
118
|
+
stat = fs.lstatSync(source);
|
|
119
|
+
} catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (stat.isSymbolicLink()) return;
|
|
123
|
+
if (stat.isDirectory()) {
|
|
124
|
+
fs.mkdirSync(target, { recursive: true });
|
|
125
|
+
for (const entry of fs.readdirSync(source)) {
|
|
126
|
+
walk(path.join(source, entry), path.join(target, entry));
|
|
127
|
+
if (state.truncated) break;
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!stat.isFile()) return;
|
|
132
|
+
if (state.files + 1 > maxFiles || state.bytes + stat.size > maxBytes) {
|
|
133
|
+
state.truncated = true;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
137
|
+
fs.copyFileSync(source, target);
|
|
138
|
+
state.files += 1;
|
|
139
|
+
state.bytes += stat.size;
|
|
140
|
+
state.copied = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
walk(sourceDir, targetDir);
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
|
|
92
147
|
function syncCodexNativeConfig(runtimeHome) {
|
|
93
148
|
if (!runtimeHome) return null;
|
|
94
|
-
const
|
|
95
|
-
const sourceDir = path.join(nativeHome, '.codex');
|
|
149
|
+
const sourceDir = path.join(nativeHome(), '.codex');
|
|
96
150
|
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
if (!exists(sourceDir)) return null;
|
|
152
|
+
|
|
153
|
+
pruneLegacyCodexRuntimeHome(runtimeHome);
|
|
154
|
+
const copiedFiles = [
|
|
155
|
+
copyFileIfPresent(path.join(sourceDir, 'hooks.json'), path.join(runtimeHome, 'hooks.json')),
|
|
156
|
+
copyFileIfPresent(path.join(sourceDir, 'supermemory.json'), path.join(runtimeHome, 'supermemory.json')),
|
|
157
|
+
].filter(Boolean).length;
|
|
158
|
+
const supermemory = copyDirBounded(path.join(sourceDir, 'supermemory'), path.join(runtimeHome, 'supermemory'));
|
|
159
|
+
copyDirBounded(path.join(nativeHome(), '.codex-supermemory'), path.join(runtimeHome, '.codex-supermemory'));
|
|
160
|
+
ensureHomeAlias(runtimeHome, '.codex');
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
sourceDir,
|
|
164
|
+
runtimeHome,
|
|
165
|
+
sourceConfigPath: path.join(sourceDir, 'config.toml'),
|
|
166
|
+
copied: copiedFiles + (supermemory.copied ? 1 : 0) > 0,
|
|
167
|
+
truncated: supermemory.truncated,
|
|
168
|
+
};
|
|
102
169
|
}
|
|
103
170
|
|
|
104
171
|
function syncClaudeNativeConfig(runtimeHome) {
|
|
105
172
|
if (!runtimeHome) return null;
|
|
106
|
-
const
|
|
107
|
-
const sourceDir = path.join(nativeHome, '.claude');
|
|
173
|
+
const sourceDir = path.join(nativeHome(), '.claude');
|
|
108
174
|
fs.mkdirSync(runtimeHome, { recursive: true });
|
|
109
175
|
const copied = copyConfigTree(sourceDir, runtimeHome);
|
|
110
|
-
|
|
111
|
-
copyFileIfPresent(path.join(nativeHome, '.claude.json'), path.join(runtimeHome, '.claude.json'));
|
|
112
|
-
copyConfigTree(path.join(nativeHome, '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
|
|
176
|
+
ensureHomeAlias(runtimeHome, '.claude');
|
|
177
|
+
copyFileIfPresent(path.join(nativeHome(), '.claude.json'), path.join(runtimeHome, '.claude.json'));
|
|
178
|
+
copyConfigTree(path.join(nativeHome(), '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
|
|
113
179
|
return copied ? { sourceDir, runtimeHome } : null;
|
|
114
180
|
}
|
|
115
181
|
|
|
182
|
+
function readJsonFile(file) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cloneJson(value) {
|
|
191
|
+
if (value == null) return value;
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(JSON.stringify(value));
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function mergeHookMaps(target, hooks) {
|
|
200
|
+
if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return;
|
|
201
|
+
for (const [event, matchers] of Object.entries(hooks)) {
|
|
202
|
+
if (!Array.isArray(matchers)) continue;
|
|
203
|
+
const cloned = cloneJson(matchers);
|
|
204
|
+
if (!Array.isArray(cloned)) continue;
|
|
205
|
+
target[event] = [...(Array.isArray(target[event]) ? target[event] : []), ...cloned];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function claudeNativeHookSettings(options = {}) {
|
|
210
|
+
const sources = [
|
|
211
|
+
path.join(nativeHome(), '.claude', 'settings.json'),
|
|
212
|
+
options.cwd ? path.join(options.cwd, '.claude', 'settings.json') : null,
|
|
213
|
+
options.cwd ? path.join(options.cwd, '.claude', 'settings.local.json') : null,
|
|
214
|
+
].filter(Boolean);
|
|
215
|
+
const hooks = {};
|
|
216
|
+
for (const source of sources) {
|
|
217
|
+
const data = readJsonFile(source);
|
|
218
|
+
mergeHookMaps(hooks, data?.hooks);
|
|
219
|
+
}
|
|
220
|
+
return Object.keys(hooks).length > 0 ? { hooks } : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
116
223
|
function syncNativeHarnessConfig(contract) {
|
|
117
224
|
const runtimeHome = contract?.auth?.runtimeHome;
|
|
118
225
|
if (!runtimeHome) return null;
|
|
@@ -123,10 +230,14 @@ function syncNativeHarnessConfig(contract) {
|
|
|
123
230
|
|
|
124
231
|
module.exports = {
|
|
125
232
|
__private: {
|
|
233
|
+
claudeNativeHookSettings,
|
|
126
234
|
copyConfigTree,
|
|
235
|
+
copyDirBounded,
|
|
127
236
|
copyFileIfPresent,
|
|
237
|
+
ensureHomeAlias,
|
|
128
238
|
shouldCopyConfigPath,
|
|
129
239
|
},
|
|
240
|
+
claudeNativeHookSettings,
|
|
130
241
|
syncClaudeNativeConfig,
|
|
131
242
|
syncCodexNativeConfig,
|
|
132
243
|
syncNativeHarnessConfig,
|