amalgm 0.1.134 → 0.1.135
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/lib/state-migration.js +83 -16
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/config.js +97 -13
- package/runtime/scripts/amalgm-mcp/tests/config-migration.test.js +89 -0
- package/runtime/scripts/chat-core/auth.js +2 -3
- package/runtime/scripts/chat-core/tests/auth.test.js +13 -0
- package/runtime/scripts/chat-core/tests/engine.test.js +1 -1
package/lib/state-migration.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
} = require('./paths');
|
|
11
11
|
|
|
12
12
|
const MARKER_FILE = path.join(AMALGM_DIR, '.user-state-migrated.json');
|
|
13
|
+
const CLI_HOMES_BACKFILL_ID = 'cli-homes-merge-v1';
|
|
13
14
|
|
|
14
15
|
const DATA_FILES = [
|
|
15
16
|
'amalgm.db',
|
|
@@ -75,34 +76,100 @@ function copyDirIfMissing(source, target) {
|
|
|
75
76
|
return true;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
function copyEntryIfMissing(source, target) {
|
|
80
|
+
if (!fs.existsSync(source) || fs.existsSync(target)) return false;
|
|
81
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
82
|
+
fs.cpSync(source, target, {
|
|
83
|
+
recursive: fs.statSync(source).isDirectory(),
|
|
84
|
+
force: false,
|
|
85
|
+
errorOnExist: true,
|
|
86
|
+
dereference: false,
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function mergeDirContents(source, target) {
|
|
92
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) return false;
|
|
93
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
94
|
+
|
|
95
|
+
let changed = false;
|
|
96
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
97
|
+
const sourcePath = path.join(source, entry.name);
|
|
98
|
+
const targetPath = path.join(target, entry.name);
|
|
99
|
+
|
|
100
|
+
if (copyEntryIfMissing(sourcePath, targetPath)) {
|
|
101
|
+
changed = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (entry.isDirectory() && fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
|
|
106
|
+
if (mergeDirContents(sourcePath, targetPath)) changed = true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return changed;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function markerCopiedEntries(marker) {
|
|
114
|
+
return Array.isArray(marker?.copied)
|
|
115
|
+
? marker.copied.filter((entry) => typeof entry === 'string' && entry)
|
|
116
|
+
: [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function markerCompletedMigrations(marker) {
|
|
120
|
+
return new Set(
|
|
121
|
+
Array.isArray(marker?.completed_migrations)
|
|
122
|
+
? marker.completed_migrations.filter((entry) => typeof entry === 'string' && entry)
|
|
123
|
+
: [],
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
78
127
|
function migrateLegacyUserState() {
|
|
79
128
|
if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return { migrated: false, reason: 'legacy-root' };
|
|
80
129
|
if (path.resolve(AMALGM_DIR).startsWith(path.resolve(os.tmpdir()))) return { migrated: false, reason: 'temp-dir' };
|
|
81
|
-
|
|
130
|
+
|
|
131
|
+
const marker = readJson(MARKER_FILE, null);
|
|
132
|
+
const hadMarker = Boolean(marker);
|
|
133
|
+
const completedMigrations = markerCompletedMigrations(marker);
|
|
134
|
+
const needsCliHomesBackfill = !completedMigrations.has(CLI_HOMES_BACKFILL_ID);
|
|
82
135
|
|
|
83
136
|
fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
|
|
84
137
|
|
|
85
138
|
const copied = [];
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
139
|
+
if (!hadMarker) {
|
|
140
|
+
for (const fileName of DATA_FILES) {
|
|
141
|
+
const source = path.join(AMALGM_HOME, fileName);
|
|
142
|
+
const target = path.join(AMALGM_DIR, fileName);
|
|
143
|
+
if (copyFileIfUseful(source, target)) copied.push(fileName);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const dirName of DATA_DIRS) {
|
|
147
|
+
const source = path.join(AMALGM_HOME, dirName);
|
|
148
|
+
const target = path.join(AMALGM_DIR, dirName);
|
|
149
|
+
if (copyDirIfMissing(source, target)) copied.push(`${dirName}/`);
|
|
150
|
+
}
|
|
90
151
|
}
|
|
91
152
|
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
153
|
+
if (needsCliHomesBackfill) {
|
|
154
|
+
const cliHomesSource = path.join(AMALGM_HOME, 'cli-homes');
|
|
155
|
+
const cliHomesTarget = path.join(AMALGM_DIR, 'cli-homes');
|
|
156
|
+
if (mergeDirContents(cliHomesSource, cliHomesTarget)) copied.push('cli-homes/');
|
|
157
|
+
completedMigrations.add(CLI_HOMES_BACKFILL_ID);
|
|
96
158
|
}
|
|
97
159
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
160
|
+
if (!hadMarker || copied.length > 0 || needsCliHomesBackfill) {
|
|
161
|
+
writeJson(MARKER_FILE, {
|
|
162
|
+
migrated_at: marker?.migrated_at || new Date().toISOString(),
|
|
163
|
+
source: AMALGM_HOME,
|
|
164
|
+
target: AMALGM_DIR,
|
|
165
|
+
copied: [...new Set([...markerCopiedEntries(marker), ...copied])],
|
|
166
|
+
completed_migrations: Array.from(completedMigrations).sort(),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
104
169
|
|
|
105
|
-
|
|
170
|
+
if (copied.length > 0) return { migrated: true, copied };
|
|
171
|
+
if (hadMarker) return { migrated: false, reason: 'already-migrated' };
|
|
172
|
+
return { migrated: false, copied: [] };
|
|
106
173
|
}
|
|
107
174
|
|
|
108
175
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ const os = require('os');
|
|
|
12
12
|
|
|
13
13
|
const PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
|
|
14
14
|
const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
15
|
+
const CLI_HOMES_BACKFILL_ID = 'cli-homes-merge-v1';
|
|
15
16
|
|
|
16
17
|
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
17
18
|
const AMALGM_HOME = process.env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
|
|
@@ -112,25 +113,108 @@ function copyIfUseful(source, target) {
|
|
|
112
113
|
fs.copyFileSync(source, target);
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
function copyEntryIfMissing(source, target) {
|
|
117
|
+
if (!fs.existsSync(source) || fs.existsSync(target)) return false;
|
|
118
|
+
fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
119
|
+
fs.cpSync(source, target, {
|
|
120
|
+
recursive: fs.statSync(source).isDirectory(),
|
|
121
|
+
force: false,
|
|
122
|
+
errorOnExist: true,
|
|
123
|
+
dereference: false,
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeDirContents(source, target) {
|
|
129
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) return false;
|
|
130
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
131
|
+
|
|
132
|
+
let changed = false;
|
|
133
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
134
|
+
const sourcePath = path.join(source, entry.name);
|
|
135
|
+
const targetPath = path.join(target, entry.name);
|
|
136
|
+
|
|
137
|
+
if (copyEntryIfMissing(sourcePath, targetPath)) {
|
|
138
|
+
changed = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (entry.isDirectory() && fs.existsSync(targetPath) && fs.statSync(targetPath).isDirectory()) {
|
|
143
|
+
if (mergeDirContents(sourcePath, targetPath)) changed = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return changed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readJson(file, fallback = null) {
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
153
|
+
} catch {
|
|
154
|
+
return fallback;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function markerCopiedEntries(marker) {
|
|
159
|
+
return Array.isArray(marker?.copied)
|
|
160
|
+
? marker.copied.filter((entry) => typeof entry === 'string' && entry)
|
|
161
|
+
: [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function markerCompletedMigrations(marker) {
|
|
165
|
+
return new Set(
|
|
166
|
+
Array.isArray(marker?.completed_migrations)
|
|
167
|
+
? marker.completed_migrations.filter((entry) => typeof entry === 'string' && entry)
|
|
168
|
+
: [],
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
115
172
|
function migrateLegacyUserState() {
|
|
116
173
|
if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return;
|
|
117
174
|
if (path.resolve(AMALGM_DIR).startsWith(path.resolve(os.tmpdir()))) return;
|
|
118
175
|
const marker = path.join(AMALGM_DIR, '.runtime-legacy-state-migrated.json');
|
|
119
|
-
|
|
176
|
+
const markerData = readJson(marker, null);
|
|
177
|
+
const hadMarker = Boolean(markerData);
|
|
178
|
+
const completedMigrations = markerCompletedMigrations(markerData);
|
|
179
|
+
const needsCliHomesBackfill = !completedMigrations.has(CLI_HOMES_BACKFILL_ID);
|
|
120
180
|
fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
181
|
+
|
|
182
|
+
const copied = [];
|
|
183
|
+
if (!hadMarker) {
|
|
184
|
+
for (const fileName of [
|
|
185
|
+
'amalgm.db',
|
|
186
|
+
'apps.json',
|
|
187
|
+
'artifacts.json',
|
|
188
|
+
'agents.json',
|
|
189
|
+
'tasks.json',
|
|
190
|
+
'event-triggers.json',
|
|
191
|
+
'workflows.json',
|
|
192
|
+
'mcp-connections.json',
|
|
193
|
+
]) {
|
|
194
|
+
const source = path.join(AMALGM_HOME, fileName);
|
|
195
|
+
const target = path.join(AMALGM_DIR, fileName);
|
|
196
|
+
const targetExists = fs.existsSync(target);
|
|
197
|
+
copyIfUseful(source, target);
|
|
198
|
+
if (!targetExists && fs.existsSync(target)) copied.push(fileName);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (needsCliHomesBackfill) {
|
|
203
|
+
const cliHomesSource = path.join(AMALGM_HOME, 'cli-homes');
|
|
204
|
+
const cliHomesTarget = path.join(AMALGM_DIR, 'cli-homes');
|
|
205
|
+
if (mergeDirContents(cliHomesSource, cliHomesTarget)) copied.push('cli-homes/');
|
|
206
|
+
completedMigrations.add(CLI_HOMES_BACKFILL_ID);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!hadMarker || copied.length > 0 || needsCliHomesBackfill) {
|
|
210
|
+
fs.writeFileSync(marker, `${JSON.stringify({
|
|
211
|
+
migrated_at: markerData?.migrated_at || new Date().toISOString(),
|
|
212
|
+
source: AMALGM_HOME,
|
|
213
|
+
target: AMALGM_DIR,
|
|
214
|
+
copied: [...new Set([...markerCopiedEntries(markerData), ...copied])],
|
|
215
|
+
completed_migrations: Array.from(completedMigrations).sort(),
|
|
216
|
+
}, null, 2)}\n`);
|
|
132
217
|
}
|
|
133
|
-
fs.writeFileSync(marker, `${JSON.stringify({ migrated_at: new Date().toISOString(), source: AMALGM_HOME }, null, 2)}\n`);
|
|
134
218
|
}
|
|
135
219
|
|
|
136
220
|
migrateLegacyUserState();
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
10
|
+
const SCRIPT_DIR = path.resolve(__dirname, '..');
|
|
11
|
+
const TEST_TMP_DIR = path.join(__dirname, '.tmp');
|
|
12
|
+
|
|
13
|
+
function resetEnv() {
|
|
14
|
+
for (const key of Object.keys(process.env)) {
|
|
15
|
+
if (key.startsWith('AMALGM_') || key === 'HOME') delete process.env[key];
|
|
16
|
+
}
|
|
17
|
+
Object.assign(process.env, ORIGINAL_ENV);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function clearScriptModules() {
|
|
21
|
+
for (const key of Object.keys(require.cache)) {
|
|
22
|
+
if (key.startsWith(SCRIPT_DIR)) delete require.cache[key];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadConfig(root, scope = 'user-a') {
|
|
27
|
+
resetEnv();
|
|
28
|
+
const home = path.join(root, '.amalgm');
|
|
29
|
+
const userDir = path.join(home, 'users', scope);
|
|
30
|
+
process.env.HOME = root;
|
|
31
|
+
process.env.AMALGM_HOME = home;
|
|
32
|
+
process.env.AMALGM_DIR = userDir;
|
|
33
|
+
clearScriptModules();
|
|
34
|
+
return require('../config');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJson(file) {
|
|
38
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test.after(() => {
|
|
42
|
+
resetEnv();
|
|
43
|
+
clearScriptModules();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('runtime config backfills legacy cli homes after the runtime marker already exists', () => {
|
|
47
|
+
fs.mkdirSync(TEST_TMP_DIR, { recursive: true });
|
|
48
|
+
const root = fs.mkdtempSync(path.join(TEST_TMP_DIR, 'amalgm-runtime-config-'));
|
|
49
|
+
try {
|
|
50
|
+
const home = path.join(root, '.amalgm');
|
|
51
|
+
const userDir = path.join(home, 'users', 'user-a');
|
|
52
|
+
const legacyRoot = path.join(home, 'cli-homes', 'claude_code', 'home');
|
|
53
|
+
const targetRoot = path.join(userDir, 'cli-homes', 'claude_code', 'home');
|
|
54
|
+
const markerPath = path.join(userDir, '.runtime-legacy-state-migrated.json');
|
|
55
|
+
|
|
56
|
+
const legacySession = path.join(legacyRoot, 'sessions', '2026', '06', '15', 'rollout-old.jsonl');
|
|
57
|
+
const targetSession = path.join(targetRoot, 'sessions', '2026', '06', '15', 'rollout-new.jsonl');
|
|
58
|
+
const legacySettings = path.join(legacyRoot, 'settings.json');
|
|
59
|
+
const targetSettings = path.join(targetRoot, 'settings.json');
|
|
60
|
+
|
|
61
|
+
fs.mkdirSync(path.dirname(legacySession), { recursive: true });
|
|
62
|
+
fs.mkdirSync(path.dirname(targetSession), { recursive: true });
|
|
63
|
+
fs.mkdirSync(path.dirname(markerPath), { recursive: true });
|
|
64
|
+
fs.writeFileSync(legacySession, 'old session\n');
|
|
65
|
+
fs.writeFileSync(targetSession, 'new session\n');
|
|
66
|
+
fs.writeFileSync(legacySettings, '{"source":"legacy"}\n');
|
|
67
|
+
fs.writeFileSync(targetSettings, '{"source":"target"}\n');
|
|
68
|
+
fs.writeFileSync(markerPath, `${JSON.stringify({
|
|
69
|
+
migrated_at: '2026-05-26T02:00:41.477Z',
|
|
70
|
+
source: home,
|
|
71
|
+
}, null, 2)}\n`);
|
|
72
|
+
|
|
73
|
+
const config = loadConfig(root);
|
|
74
|
+
|
|
75
|
+
assert.equal(config.AMALGM_DIR, userDir);
|
|
76
|
+
assert.equal(
|
|
77
|
+
fs.readFileSync(path.join(targetRoot, 'sessions', '2026', '06', '15', 'rollout-old.jsonl'), 'utf8'),
|
|
78
|
+
'old session\n',
|
|
79
|
+
);
|
|
80
|
+
assert.equal(fs.readFileSync(targetSession, 'utf8'), 'new session\n');
|
|
81
|
+
assert.equal(fs.readFileSync(targetSettings, 'utf8'), '{"source":"target"}\n');
|
|
82
|
+
|
|
83
|
+
const marker = readJson(markerPath);
|
|
84
|
+
assert.ok(marker.copied.includes('cli-homes/'));
|
|
85
|
+
assert.ok(marker.completed_migrations.includes('cli-homes-merge-v1'));
|
|
86
|
+
} finally {
|
|
87
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -109,9 +109,8 @@ function providerAuthIdentity(harness) {
|
|
|
109
109
|
return `${harness || 'provider'}:provider-default`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
function runtimeHome({ amalgmDir, harness, agentConfigId
|
|
112
|
+
function runtimeHome({ amalgmDir, harness, agentConfigId }) {
|
|
113
113
|
const harnessHome = path.join(amalgmDir, 'cli-homes', safePathSegment(harness));
|
|
114
|
-
if (runtimeHomeLayout === 'legacy') return path.join(harnessHome, 'home');
|
|
115
114
|
return path.join(
|
|
116
115
|
harnessHome,
|
|
117
116
|
'agents',
|
|
@@ -172,7 +171,7 @@ function authEnvelope({ harness, authMethod, sessionId, localBaseUrl, proxyToken
|
|
|
172
171
|
} else if (method === 'provider_auth') {
|
|
173
172
|
tokenFingerprint = providerAuthFingerprint(harness);
|
|
174
173
|
}
|
|
175
|
-
const runtimeHomePath = runtimeHome({ amalgmDir, harness, agentConfigId
|
|
174
|
+
const runtimeHomePath = runtimeHome({ amalgmDir, harness, agentConfigId });
|
|
176
175
|
return {
|
|
177
176
|
method,
|
|
178
177
|
baseUrl,
|
|
@@ -141,6 +141,19 @@ test('provided CLI home path cannot move an agent off its canonical home', () =>
|
|
|
141
141
|
assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/agents/codex/home');
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
+
test('legacy runtime-home requests still resolve to the canonical per-agent home', () => {
|
|
145
|
+
const envelope = authEnvelope({
|
|
146
|
+
harness: 'codex',
|
|
147
|
+
authMethod: 'provider_auth',
|
|
148
|
+
sessionId: 'session-test',
|
|
149
|
+
amalgmDir: '/tmp/amalgm-test',
|
|
150
|
+
agentConfigId: 'legacy-agent',
|
|
151
|
+
runtimeHomeLayout: 'legacy',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.equal(envelope.runtimeHome, '/tmp/amalgm-test/cli-homes/codex/agents/legacy-agent/home');
|
|
155
|
+
});
|
|
156
|
+
|
|
144
157
|
test('per-agent auth envelope exposes legacy homes as match sources', () => {
|
|
145
158
|
const amalgmDir = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-home-sources-'));
|
|
146
159
|
try {
|