amalgm 0.1.54 → 0.1.56
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/bin/amalgm.js +21 -0
- package/lib/auth-store.js +38 -4
- package/lib/cli.js +52 -10
- package/lib/paths.js +29 -2
- package/lib/process-cleanup.js +26 -3
- package/lib/runtime-identity.js +145 -0
- package/lib/service.js +22 -3
- package/lib/supervisor.js +15 -1
- package/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +1 -43
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +47 -1
- package/runtime/scripts/amalgm-mcp/browser/page.js +7 -1
- package/runtime/scripts/chat-core/tooling/native-binaries.js +9 -34
- package/runtime/scripts/local-gateway.js +13 -0
package/bin/amalgm.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
function takeFlag(argv, names) {
|
|
5
|
+
for (let i = 2; i < argv.length; i += 1) {
|
|
6
|
+
const arg = argv[i];
|
|
7
|
+
if (!arg || !arg.startsWith('--')) continue;
|
|
8
|
+
const eq = arg.indexOf('=');
|
|
9
|
+
const key = eq === -1 ? arg : arg.slice(0, eq);
|
|
10
|
+
if (!names.includes(key)) continue;
|
|
11
|
+
if (eq !== -1) return arg.slice(eq + 1);
|
|
12
|
+
const next = argv[i + 1];
|
|
13
|
+
if (next && !next.startsWith('--')) return next;
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const label = takeFlag(process.argv, ['--label', '--runtime-label', '--branch']);
|
|
20
|
+
if (label && !process.env.AMALGM_RUNTIME_LABEL) process.env.AMALGM_RUNTIME_LABEL = label;
|
|
21
|
+
|
|
22
|
+
const userScope = takeFlag(process.argv, ['--user', '--user-id', '--runtime-user']);
|
|
23
|
+
if (userScope && !process.env.AMALGM_RUNTIME_USER_ID) process.env.AMALGM_RUNTIME_USER_ID = userScope;
|
|
24
|
+
|
|
4
25
|
require('../lib/cli').main(process.argv).catch((error) => {
|
|
5
26
|
const message = error && error.message ? error.message : String(error);
|
|
6
27
|
console.error(`Error: ${message}`);
|
package/lib/auth-store.js
CHANGED
|
@@ -4,9 +4,17 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
|
+
AMALGM_DIR,
|
|
8
|
+
AMALGM_HOME,
|
|
9
|
+
AMALGM_RUNTIME_LABEL,
|
|
10
|
+
AMALGM_RUNTIME_USER_SCOPE,
|
|
7
11
|
AUTH_FILE,
|
|
8
12
|
COMPUTER_FILE,
|
|
9
13
|
} = require('./paths');
|
|
14
|
+
const {
|
|
15
|
+
sanitizeScopeSegment,
|
|
16
|
+
scopedAmalgmDir,
|
|
17
|
+
} = require('./runtime-identity');
|
|
10
18
|
|
|
11
19
|
const PROXY_REFRESH_BUFFER_MS = 10 * 60 * 1000;
|
|
12
20
|
|
|
@@ -166,24 +174,50 @@ function mergeRecordAuth(record, auth) {
|
|
|
166
174
|
|
|
167
175
|
function loadComputerRecord(options = {}) {
|
|
168
176
|
const record = readJson(COMPUTER_FILE, null);
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
177
|
+
const scopedRecord = record && typeof record === 'object' ? record : null;
|
|
178
|
+
const legacyRecord = !scopedRecord ? loadLegacyComputerRecord() : null;
|
|
179
|
+
const activeRecord = scopedRecord || legacyRecord;
|
|
180
|
+
if (!activeRecord || typeof activeRecord !== 'object') return null;
|
|
181
|
+
const auth = scopedRecord ? readJson(AUTH_FILE, null) : readLegacyAuth();
|
|
182
|
+
const merged = mergeRecordAuth(activeRecord, auth);
|
|
183
|
+
if (options.migrate && (hasComputerSecrets(activeRecord) || legacyRecord)) {
|
|
173
184
|
saveComputerRecord(merged);
|
|
174
185
|
}
|
|
175
186
|
return merged;
|
|
176
187
|
}
|
|
177
188
|
|
|
189
|
+
function loadLegacyComputerRecord() {
|
|
190
|
+
if (AMALGM_RUNTIME_LABEL !== 'main') return null;
|
|
191
|
+
if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return null;
|
|
192
|
+
const legacy = readJson(path.join(AMALGM_HOME, 'computer.json'), null);
|
|
193
|
+
return legacy && typeof legacy === 'object' ? legacy : null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readLegacyAuth() {
|
|
197
|
+
if (AMALGM_RUNTIME_LABEL !== 'main') return null;
|
|
198
|
+
if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return null;
|
|
199
|
+
return readJson(path.join(AMALGM_HOME, 'auth.json'), null);
|
|
200
|
+
}
|
|
201
|
+
|
|
178
202
|
function saveComputerRecord(record) {
|
|
179
203
|
const existingAuth = readJson(AUTH_FILE, null);
|
|
180
204
|
const nextAuth = authFromRecord(record, existingAuth);
|
|
181
205
|
const nextRecord = publicComputerRecord(record);
|
|
182
206
|
writeJsonSecret(COMPUTER_FILE, nextRecord);
|
|
183
207
|
writeJsonSecret(AUTH_FILE, nextAuth);
|
|
208
|
+
mirrorLocalRecordToUserScope(nextRecord, nextAuth);
|
|
184
209
|
return mergeRecordAuth(nextRecord, nextAuth);
|
|
185
210
|
}
|
|
186
211
|
|
|
212
|
+
function mirrorLocalRecordToUserScope(record, auth) {
|
|
213
|
+
const userId = cleanString(record?.user_id) || cleanString(auth?.user_id);
|
|
214
|
+
if (!userId || AMALGM_RUNTIME_USER_SCOPE !== 'local') return;
|
|
215
|
+
const targetDir = scopedAmalgmDir(AMALGM_HOME, sanitizeScopeSegment(userId), AMALGM_RUNTIME_LABEL);
|
|
216
|
+
if (path.resolve(targetDir) === path.resolve(AMALGM_DIR)) return;
|
|
217
|
+
writeJsonSecret(path.join(targetDir, 'computer.json'), record);
|
|
218
|
+
writeJsonSecret(path.join(targetDir, 'auth.json'), auth);
|
|
219
|
+
}
|
|
220
|
+
|
|
187
221
|
function deleteComputerAuth() {
|
|
188
222
|
for (const file of [COMPUTER_FILE, AUTH_FILE]) {
|
|
189
223
|
try {
|
package/lib/cli.js
CHANGED
|
@@ -11,15 +11,24 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
11
11
|
|
|
12
12
|
const {
|
|
13
13
|
AMALGM_DIR,
|
|
14
|
+
AMALGM_BRANCH,
|
|
14
15
|
DEFAULT_APP_URL,
|
|
15
16
|
DEVICE_FILE,
|
|
16
17
|
LOG_DIR,
|
|
17
18
|
PID_FILE,
|
|
18
19
|
RUNTIME_DIR,
|
|
20
|
+
AMALGM_RUNTIME_INSTANCE_ID,
|
|
21
|
+
AMALGM_RUNTIME_LABEL,
|
|
22
|
+
AMALGM_RUNTIME_USER_SCOPE,
|
|
19
23
|
RUNTIME_STATE_FILE,
|
|
20
24
|
RUNTIME_TOKEN_FILE,
|
|
21
25
|
SHUTDOWN_REASON_FILE,
|
|
22
26
|
} = require('./paths');
|
|
27
|
+
const {
|
|
28
|
+
normalizeRuntimeLabel,
|
|
29
|
+
npmTagForRuntimeLabel,
|
|
30
|
+
sanitizeScopeSegment,
|
|
31
|
+
} = require('./runtime-identity');
|
|
23
32
|
const { startSupervisor } = require('./supervisor');
|
|
24
33
|
const {
|
|
25
34
|
getServiceStatus,
|
|
@@ -30,6 +39,7 @@ const {
|
|
|
30
39
|
} = require('./service');
|
|
31
40
|
const {
|
|
32
41
|
cleanupStaleRuntimeProcesses,
|
|
42
|
+
RUNTIME_INSTANCE_ARG,
|
|
33
43
|
runtimeServiceProcesses,
|
|
34
44
|
supervisorProcesses,
|
|
35
45
|
} = require('./process-cleanup');
|
|
@@ -94,7 +104,7 @@ function usage() {
|
|
|
94
104
|
'',
|
|
95
105
|
'Usage:',
|
|
96
106
|
' amalgm login [--app-url https://amalgm.ai] [--name "My Mac"] [--setup-code ABCD-EFGH-JKLM-NPQR] [--no-open] [--no-poll]',
|
|
97
|
-
' amalgm start [--foreground] [--local-only]',
|
|
107
|
+
' amalgm start [--foreground] [--local-only] [--branch main|preview] [--user <user-id>]',
|
|
98
108
|
' amalgm run [--local-only]',
|
|
99
109
|
' amalgm stop',
|
|
100
110
|
' amalgm status',
|
|
@@ -107,7 +117,10 @@ function usage() {
|
|
|
107
117
|
'Environment:',
|
|
108
118
|
' AMALGM_APP_URL Web app base URL for login/register',
|
|
109
119
|
' AMALGM_AUTO_UPDATE Set to 1 to update to the latest stable release before start',
|
|
110
|
-
' AMALGM_DIR
|
|
120
|
+
' AMALGM_DIR Explicit runtime state dir override',
|
|
121
|
+
' AMALGM_HOME Runtime state root (default ~/.amalgm)',
|
|
122
|
+
' AMALGM_RUNTIME_LABEL Runtime label: main, preview, or local',
|
|
123
|
+
' AMALGM_RUNTIME_USER_ID Runtime user scope',
|
|
111
124
|
' AMALGM_PROXY_TOKEN Optional short-lived proxy token override',
|
|
112
125
|
].join('\n');
|
|
113
126
|
}
|
|
@@ -155,8 +168,17 @@ function runtimeMissingFiles() {
|
|
|
155
168
|
return required.filter((file) => !fs.existsSync(file));
|
|
156
169
|
}
|
|
157
170
|
|
|
171
|
+
function runtimeStateMatchesCurrentScope(state) {
|
|
172
|
+
if (!state || typeof state !== 'object') return false;
|
|
173
|
+
const label = normalizeRuntimeLabel(state.runtime_label || state.label || state.branch);
|
|
174
|
+
if (label !== AMALGM_RUNTIME_LABEL) return false;
|
|
175
|
+
const userScope = sanitizeScopeSegment(state.user_scope || state.user_id || '');
|
|
176
|
+
return userScope === AMALGM_RUNTIME_USER_SCOPE;
|
|
177
|
+
}
|
|
178
|
+
|
|
158
179
|
function servicePorts() {
|
|
159
180
|
const state = readJson(RUNTIME_STATE_FILE, null);
|
|
181
|
+
if (!runtimeStateMatchesCurrentScope(state)) return [];
|
|
160
182
|
return runtimePortsForStatus(state, process.env);
|
|
161
183
|
}
|
|
162
184
|
|
|
@@ -172,10 +194,15 @@ function hasRequiredComputerFields(record) {
|
|
|
172
194
|
|
|
173
195
|
const SAFE_DAEMON_ENV_KEYS = [
|
|
174
196
|
'AMALGM_BIND_HOST',
|
|
197
|
+
'AMALGM_BRANCH',
|
|
175
198
|
'AMALGM_DEFAULT_CWD',
|
|
176
199
|
'AMALGM_GATEWAY_PORT',
|
|
200
|
+
'AMALGM_HOME',
|
|
177
201
|
'AMALGM_MCP_PORT',
|
|
178
202
|
'AMALGM_NATIVE_NODE_MODULES',
|
|
203
|
+
'AMALGM_RUNTIME_INSTANCE_ID',
|
|
204
|
+
'AMALGM_RUNTIME_LABEL',
|
|
205
|
+
'AMALGM_RUNTIME_USER_ID',
|
|
179
206
|
'AMALGM_SKIP_NATIVE_INSTALL',
|
|
180
207
|
'CHAT_SERVER_PORT',
|
|
181
208
|
'ELECTRON_RUN_AS_NODE',
|
|
@@ -204,6 +231,11 @@ function daemonEnv(localOnly) {
|
|
|
204
231
|
if (process.env[key]) env[key] = process.env[key];
|
|
205
232
|
}
|
|
206
233
|
env.AMALGM_DIR = AMALGM_DIR;
|
|
234
|
+
env.AMALGM_RUNTIME_INSTANCE_ID = AMALGM_RUNTIME_INSTANCE_ID;
|
|
235
|
+
env.AMALGM_RUNTIME_LABEL = AMALGM_RUNTIME_LABEL;
|
|
236
|
+
env.AMALGM_BRANCH = AMALGM_BRANCH;
|
|
237
|
+
env.AMALGM_RUNTIME_USER_ID = AMALGM_RUNTIME_USER_SCOPE;
|
|
238
|
+
env.AMALGM_USER_SCOPE = AMALGM_RUNTIME_USER_SCOPE;
|
|
207
239
|
if (localOnly) env.AMALGM_LOCAL_ONLY = 'true';
|
|
208
240
|
return env;
|
|
209
241
|
}
|
|
@@ -339,11 +371,11 @@ function isPortFree(port, host = '127.0.0.1') {
|
|
|
339
371
|
|
|
340
372
|
async function unavailableServicePorts() {
|
|
341
373
|
const results = [];
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
if (!
|
|
346
|
-
if (!(await isPortFree(port))) results.push([name, port]);
|
|
374
|
+
for (const service of runtimeServices()) {
|
|
375
|
+
if (!process.env[service.envName]) continue;
|
|
376
|
+
const port = Number(process.env[service.envName]);
|
|
377
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) continue;
|
|
378
|
+
if (!(await isPortFree(port))) results.push([service.name, port]);
|
|
347
379
|
}
|
|
348
380
|
return results;
|
|
349
381
|
}
|
|
@@ -409,7 +441,7 @@ function npmCommand() {
|
|
|
409
441
|
function npmInstallTag(options = {}) {
|
|
410
442
|
const explicit = options.tag || options.channel;
|
|
411
443
|
if (explicit && explicit !== true) return String(explicit).replace(/^@/, '');
|
|
412
|
-
return
|
|
444
|
+
return npmTagForRuntimeLabel(AMALGM_RUNTIME_LABEL);
|
|
413
445
|
}
|
|
414
446
|
|
|
415
447
|
function npmPackageVersionForTag(tag) {
|
|
@@ -864,7 +896,7 @@ async function start(options) {
|
|
|
864
896
|
ensureDir(LOG_DIR);
|
|
865
897
|
const logPath = path.join(LOG_DIR, 'daemon.log');
|
|
866
898
|
const logFd = fs.openSync(logPath, 'a');
|
|
867
|
-
const child = spawn(process.execPath, [path.join(__dirname, '..', 'bin', 'amalgm.js'), 'run'], {
|
|
899
|
+
const child = spawn(process.execPath, [path.join(__dirname, '..', 'bin', 'amalgm.js'), 'run', RUNTIME_INSTANCE_ARG], {
|
|
868
900
|
detached: true,
|
|
869
901
|
env: daemonEnv(localOnly),
|
|
870
902
|
stdio: ['ignore', logFd, logFd],
|
|
@@ -978,7 +1010,13 @@ async function waitForServices(timeoutMs) {
|
|
|
978
1010
|
let latest = [];
|
|
979
1011
|
while (Date.now() < deadline) {
|
|
980
1012
|
latest = [];
|
|
981
|
-
|
|
1013
|
+
const ports = servicePorts();
|
|
1014
|
+
if (ports.length === 0) {
|
|
1015
|
+
latest = [['runtime-state', false]];
|
|
1016
|
+
await sleep(300);
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
for (const [name, port] of ports) {
|
|
982
1020
|
latest.push([name, await checkHttp(port)]);
|
|
983
1021
|
}
|
|
984
1022
|
if (latest.every(([, ok]) => ok)) return latest;
|
|
@@ -1002,6 +1040,8 @@ async function status() {
|
|
|
1002
1040
|
: 'not installed'
|
|
1003
1041
|
}`,
|
|
1004
1042
|
);
|
|
1043
|
+
console.log(`Label: ${AMALGM_RUNTIME_LABEL} (${AMALGM_BRANCH})`);
|
|
1044
|
+
console.log(`User scope: ${AMALGM_RUNTIME_USER_SCOPE}`);
|
|
1005
1045
|
console.log(`State: ${AMALGM_DIR}`);
|
|
1006
1046
|
if (runtimeState?.gateway_url) console.log(`Gateway: ${runtimeState.gateway_url}`);
|
|
1007
1047
|
if (lastShutdown?.signal || lastShutdown?.reason) {
|
|
@@ -1162,6 +1202,8 @@ async function doctor() {
|
|
|
1162
1202
|
}
|
|
1163
1203
|
|
|
1164
1204
|
console.log('Amalgm doctor');
|
|
1205
|
+
console.log(`Label: ${AMALGM_RUNTIME_LABEL} (${AMALGM_BRANCH})`);
|
|
1206
|
+
console.log(`User scope: ${AMALGM_RUNTIME_USER_SCOPE}`);
|
|
1165
1207
|
console.log(`State: ${AMALGM_DIR}`);
|
|
1166
1208
|
for (const check of checks) {
|
|
1167
1209
|
console.log(`${check.level.padEnd(4)} ${check.name.padEnd(18)} ${check.detail}`);
|
package/lib/paths.js
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const path = require('path');
|
|
5
4
|
|
|
5
|
+
const {
|
|
6
|
+
defaultAmalgmHome,
|
|
7
|
+
discoverLegacyUserScope,
|
|
8
|
+
resolveRuntimeLabel,
|
|
9
|
+
resolveRuntimeUserScope,
|
|
10
|
+
runtimeBranchForLabel,
|
|
11
|
+
runtimeInstanceIdForDir,
|
|
12
|
+
scopedAmalgmDir,
|
|
13
|
+
} = require('./runtime-identity');
|
|
14
|
+
|
|
15
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
16
|
+
|
|
6
17
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
7
18
|
const RUNTIME_DIR = path.join(PACKAGE_ROOT, 'runtime');
|
|
8
|
-
const
|
|
19
|
+
const AMALGM_HOME = defaultAmalgmHome(process.env);
|
|
20
|
+
const AMALGM_RUNTIME_LABEL = resolveRuntimeLabel(process.env, { packageVersion: PACKAGE_VERSION });
|
|
21
|
+
const AMALGM_BRANCH = runtimeBranchForLabel(AMALGM_RUNTIME_LABEL, process.env);
|
|
22
|
+
const AMALGM_RUNTIME_USER_SCOPE = resolveRuntimeUserScope(
|
|
23
|
+
process.env,
|
|
24
|
+
discoverLegacyUserScope(AMALGM_HOME, AMALGM_RUNTIME_LABEL),
|
|
25
|
+
);
|
|
26
|
+
const AMALGM_DIR = process.env.AMALGM_DIR
|
|
27
|
+
? path.resolve(process.env.AMALGM_DIR)
|
|
28
|
+
: scopedAmalgmDir(AMALGM_HOME, AMALGM_RUNTIME_USER_SCOPE, AMALGM_RUNTIME_LABEL);
|
|
29
|
+
const AMALGM_RUNTIME_INSTANCE_ID = process.env.AMALGM_RUNTIME_INSTANCE_ID
|
|
30
|
+
|| runtimeInstanceIdForDir(AMALGM_DIR);
|
|
9
31
|
const DEVICE_FILE = path.join(AMALGM_DIR, 'device.json');
|
|
10
32
|
const COMPUTER_FILE = path.join(AMALGM_DIR, 'computer.json');
|
|
11
33
|
const AUTH_FILE = path.join(AMALGM_DIR, 'auth.json');
|
|
@@ -21,7 +43,12 @@ const SHUTDOWN_REASON_FILE = path.join(AMALGM_DIR, 'shutdown-reason.json');
|
|
|
21
43
|
const DEFAULT_APP_URL = process.env.AMALGM_APP_URL || 'https://amalgm.ai';
|
|
22
44
|
|
|
23
45
|
module.exports = {
|
|
46
|
+
AMALGM_BRANCH,
|
|
24
47
|
AMALGM_DIR,
|
|
48
|
+
AMALGM_HOME,
|
|
49
|
+
AMALGM_RUNTIME_INSTANCE_ID,
|
|
50
|
+
AMALGM_RUNTIME_LABEL,
|
|
51
|
+
AMALGM_RUNTIME_USER_SCOPE,
|
|
25
52
|
AUTH_FILE,
|
|
26
53
|
COMPUTER_FILE,
|
|
27
54
|
DEFAULT_APP_URL,
|
package/lib/process-cleanup.js
CHANGED
|
@@ -6,6 +6,7 @@ const { spawnSync } = require('child_process');
|
|
|
6
6
|
|
|
7
7
|
const {
|
|
8
8
|
AMALGM_DIR,
|
|
9
|
+
AMALGM_RUNTIME_INSTANCE_ID,
|
|
9
10
|
PACKAGE_ROOT,
|
|
10
11
|
RUNTIME_DIR,
|
|
11
12
|
} = require('./paths');
|
|
@@ -20,6 +21,7 @@ const RUNTIME_SERVICE_SUFFIXES = runtimeServiceScripts()
|
|
|
20
21
|
|
|
21
22
|
const SUPERVISOR_MARKER = path.join(PACKAGE_ROOT, 'bin', 'amalgm.js').replace(/\\/g, '/');
|
|
22
23
|
const SUPERVISOR_SUFFIX = '/bin/amalgm.js';
|
|
24
|
+
const RUNTIME_INSTANCE_ARG = `--amalgm-runtime-id=${AMALGM_RUNTIME_INSTANCE_ID}`;
|
|
23
25
|
|
|
24
26
|
function isPidRunning(pid) {
|
|
25
27
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -144,15 +146,33 @@ function normalizedCommand(command) {
|
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
function envMatchesAmalgmDir(env) {
|
|
149
|
+
if (env?.AMALGM_RUNTIME_INSTANCE_ID) return env.AMALGM_RUNTIME_INSTANCE_ID === AMALGM_RUNTIME_INSTANCE_ID;
|
|
147
150
|
if (!env || !env.AMALGM_DIR) return true;
|
|
148
151
|
return path.resolve(env.AMALGM_DIR) === path.resolve(AMALGM_DIR);
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
function commandRuntimeInstanceId(command) {
|
|
155
|
+
const normalized = normalizedCommand(command);
|
|
156
|
+
const match = normalized.match(/(?:^|\s)--amalgm-runtime-id(?:=|\s+)([^\s]+)/);
|
|
157
|
+
return match ? match[1] : '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function commandMatchesRuntimeInstance(command) {
|
|
161
|
+
const instanceId = commandRuntimeInstanceId(command);
|
|
162
|
+
if (instanceId) return instanceId === AMALGM_RUNTIME_INSTANCE_ID;
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
151
166
|
function commandLooksLikeRuntimeService(command, item = null, options = {}) {
|
|
152
167
|
const normalized = normalizedCommand(command);
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
const absoluteMatch = RUNTIME_SERVICE_MARKERS.some((marker) => normalized.includes(marker));
|
|
169
|
+
const suffixMatch = RUNTIME_SERVICE_SUFFIXES.some((marker) => normalized.includes(marker));
|
|
170
|
+
if (!absoluteMatch && !suffixMatch) return false;
|
|
171
|
+
const instanceMatch = commandMatchesRuntimeInstance(normalized);
|
|
172
|
+
if (instanceMatch !== null) return instanceMatch;
|
|
155
173
|
if (item?.env && envMatchesAmalgmDir(item.env)) return true;
|
|
174
|
+
if (item?.env) return false;
|
|
175
|
+
if (!absoluteMatch) return false;
|
|
156
176
|
if (!options.includeOrphanedForeign) return false;
|
|
157
177
|
const parent = normalizedCommand(item?.parentCommand || '');
|
|
158
178
|
return !commandLooksLikeAnySupervisor(parent);
|
|
@@ -160,7 +180,9 @@ function commandLooksLikeRuntimeService(command, item = null, options = {}) {
|
|
|
160
180
|
|
|
161
181
|
function commandLooksLikeSupervisor(command) {
|
|
162
182
|
const normalized = normalizedCommand(command);
|
|
163
|
-
|
|
183
|
+
if (!normalized.includes(SUPERVISOR_MARKER) || !/\brun\b/.test(normalized)) return false;
|
|
184
|
+
const instanceMatch = commandMatchesRuntimeInstance(normalized);
|
|
185
|
+
return instanceMatch === true;
|
|
164
186
|
}
|
|
165
187
|
|
|
166
188
|
function commandLooksLikeAnySupervisor(command) {
|
|
@@ -233,6 +255,7 @@ module.exports = {
|
|
|
233
255
|
commandLooksLikeRuntimeService,
|
|
234
256
|
commandLooksLikeSupervisor,
|
|
235
257
|
isPidRunning,
|
|
258
|
+
RUNTIME_INSTANCE_ARG,
|
|
236
259
|
runtimeServiceProcesses,
|
|
237
260
|
supervisorProcesses,
|
|
238
261
|
terminateProcesses,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const VALID_RUNTIME_LABELS = new Set(['main', 'preview', 'local']);
|
|
9
|
+
|
|
10
|
+
function normalizeRuntimeLabel(value) {
|
|
11
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
12
|
+
if (!raw) return '';
|
|
13
|
+
if (['main', 'latest', 'stable', 'prod', 'production', 'release', 'master'].includes(raw)) return 'main';
|
|
14
|
+
if (['preview', 'canary', 'native-chat', 'staging'].includes(raw)) return 'preview';
|
|
15
|
+
if (['local', 'dev', 'development'].includes(raw)) return 'local';
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function runtimeLabelFromVersion(version) {
|
|
20
|
+
const raw = String(version || '').toLowerCase();
|
|
21
|
+
return /-(?:canary|preview|alpha|beta|rc)(?:[.\-+]|$)/.test(raw) ? 'preview' : 'main';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function resolveRuntimeLabel(env = process.env, options = {}) {
|
|
25
|
+
const explicit = normalizeRuntimeLabel(
|
|
26
|
+
env.AMALGM_RUNTIME_LABEL
|
|
27
|
+
|| env.AMALGM_LABEL
|
|
28
|
+
|| env.AMALGM_CHANNEL
|
|
29
|
+
|| env.AMALGM_BRANCH,
|
|
30
|
+
);
|
|
31
|
+
if (explicit) return explicit;
|
|
32
|
+
if (options.local) return 'local';
|
|
33
|
+
return runtimeLabelFromVersion(options.packageVersion || env.npm_package_version || '');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function assertRuntimeLabel(label) {
|
|
37
|
+
if (!VALID_RUNTIME_LABELS.has(label)) {
|
|
38
|
+
throw new Error(`Unsupported Amalgm runtime label "${label}". Use main, preview, or local.`);
|
|
39
|
+
}
|
|
40
|
+
return label;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runtimeBranchForLabel(label, env = process.env) {
|
|
44
|
+
const branch = normalizeRuntimeLabel(env.AMALGM_BRANCH);
|
|
45
|
+
if (branch === 'main' || branch === 'preview') return branch;
|
|
46
|
+
if (label === 'preview') return 'preview';
|
|
47
|
+
return 'main';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function npmTagForRuntimeLabel(label) {
|
|
51
|
+
return label === 'preview' ? 'canary' : 'latest';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sanitizeScopeSegment(value, fallback = 'local') {
|
|
55
|
+
const clean = String(value || '')
|
|
56
|
+
.trim()
|
|
57
|
+
.replace(/[^A-Za-z0-9_.@-]/g, '_')
|
|
58
|
+
.replace(/^_+|_+$/g, '');
|
|
59
|
+
return clean || fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readJson(file, fallback = null) {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
65
|
+
} catch {
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function userIdFromRecord(record) {
|
|
71
|
+
return typeof record?.user_id === 'string' && record.user_id.trim()
|
|
72
|
+
? record.user_id.trim()
|
|
73
|
+
: '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function discoverLegacyUserScope(homeDir, label = '') {
|
|
77
|
+
const record = readJson(path.join(homeDir, 'computer.json'), null);
|
|
78
|
+
const fromRecord = userIdFromRecord(record);
|
|
79
|
+
if (fromRecord) return fromRecord;
|
|
80
|
+
|
|
81
|
+
const auth = readJson(path.join(homeDir, 'auth.json'), null);
|
|
82
|
+
const fromAuth = userIdFromRecord(auth);
|
|
83
|
+
if (fromAuth) return fromAuth;
|
|
84
|
+
|
|
85
|
+
const runtimeLabel = normalizeRuntimeLabel(label);
|
|
86
|
+
if (!runtimeLabel) return '';
|
|
87
|
+
try {
|
|
88
|
+
const usersDir = path.join(homeDir, 'users');
|
|
89
|
+
for (const entry of fs.readdirSync(usersDir, { withFileTypes: true })) {
|
|
90
|
+
if (!entry.isDirectory()) continue;
|
|
91
|
+
const scopedDir = path.join(usersDir, entry.name, runtimeLabel);
|
|
92
|
+
const scopedRecord = readJson(path.join(scopedDir, 'computer.json'), null);
|
|
93
|
+
const fromScopedRecord = userIdFromRecord(scopedRecord);
|
|
94
|
+
if (fromScopedRecord) return fromScopedRecord;
|
|
95
|
+
const scopedAuth = readJson(path.join(scopedDir, 'auth.json'), null);
|
|
96
|
+
const fromScopedAuth = userIdFromRecord(scopedAuth);
|
|
97
|
+
if (fromScopedAuth) return fromScopedAuth;
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// No scoped users yet.
|
|
101
|
+
}
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveRuntimeUserScope(env = process.env, fallback = '') {
|
|
106
|
+
return sanitizeScopeSegment(
|
|
107
|
+
env.AMALGM_RUNTIME_USER_ID
|
|
108
|
+
|| env.AMALGM_USER_SCOPE
|
|
109
|
+
|| env.AMALGM_USER_ID
|
|
110
|
+
|| fallback
|
|
111
|
+
|| 'local',
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function defaultAmalgmHome(env = process.env) {
|
|
116
|
+
return env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function scopedAmalgmDir(homeDir, userScope, label) {
|
|
120
|
+
return path.join(homeDir, 'users', sanitizeScopeSegment(userScope), assertRuntimeLabel(label));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function runtimeInstanceIdForDir(dir) {
|
|
124
|
+
return crypto
|
|
125
|
+
.createHash('sha256')
|
|
126
|
+
.update(path.resolve(String(dir || '')), 'utf8')
|
|
127
|
+
.digest('hex')
|
|
128
|
+
.slice(0, 16);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
VALID_RUNTIME_LABELS,
|
|
133
|
+
assertRuntimeLabel,
|
|
134
|
+
defaultAmalgmHome,
|
|
135
|
+
discoverLegacyUserScope,
|
|
136
|
+
npmTagForRuntimeLabel,
|
|
137
|
+
normalizeRuntimeLabel,
|
|
138
|
+
resolveRuntimeLabel,
|
|
139
|
+
resolveRuntimeUserScope,
|
|
140
|
+
runtimeInstanceIdForDir,
|
|
141
|
+
runtimeBranchForLabel,
|
|
142
|
+
runtimeLabelFromVersion,
|
|
143
|
+
sanitizeScopeSegment,
|
|
144
|
+
scopedAmalgmDir,
|
|
145
|
+
};
|
package/lib/service.js
CHANGED
|
@@ -7,9 +7,13 @@ const { spawn, spawnSync } = require('child_process');
|
|
|
7
7
|
|
|
8
8
|
const {
|
|
9
9
|
AMALGM_DIR,
|
|
10
|
+
AMALGM_BRANCH,
|
|
10
11
|
LOG_DIR,
|
|
11
12
|
PACKAGE_ROOT,
|
|
12
13
|
PID_FILE,
|
|
14
|
+
AMALGM_RUNTIME_INSTANCE_ID,
|
|
15
|
+
AMALGM_RUNTIME_LABEL,
|
|
16
|
+
AMALGM_RUNTIME_USER_SCOPE,
|
|
13
17
|
SERVICE_DIR,
|
|
14
18
|
SERVICE_PID_FILE,
|
|
15
19
|
SERVICE_STATE_FILE,
|
|
@@ -17,10 +21,13 @@ const {
|
|
|
17
21
|
} = require('./paths');
|
|
18
22
|
const {
|
|
19
23
|
cleanupStaleRuntimeProcesses,
|
|
24
|
+
RUNTIME_INSTANCE_ARG,
|
|
20
25
|
} = require('./process-cleanup');
|
|
21
26
|
|
|
22
27
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
23
|
-
const
|
|
28
|
+
const SERVICE_LABEL_SCOPE = `${AMALGM_RUNTIME_USER_SCOPE}.${AMALGM_RUNTIME_LABEL}`
|
|
29
|
+
.replace(/[^A-Za-z0-9_.-]/g, '_');
|
|
30
|
+
const SERVICE_LABEL = process.env.AMALGM_SERVICE_LABEL || `ai.amalgm.runtime.${SERVICE_LABEL_SCOPE}`;
|
|
24
31
|
const SERVICE_SCRIPT_FILE = path.join(SERVICE_DIR, 'amalgm-watchdog.sh');
|
|
25
32
|
const SERVICE_NODE_SCRIPT_FILE = path.join(SERVICE_DIR, 'amalgm-watchdog.cjs');
|
|
26
33
|
const SERVICE_LOG_FILE = path.join(LOG_DIR, 'service.log');
|
|
@@ -33,6 +40,10 @@ const SERVICE_ENV_KEYS = [
|
|
|
33
40
|
'AMALGM_CREATE_AUTH_WATCH_DIRS',
|
|
34
41
|
'AMALGM_DEFAULT_CWD',
|
|
35
42
|
'AMALGM_GATEWAY_PORT',
|
|
43
|
+
'AMALGM_HOME',
|
|
44
|
+
'AMALGM_RUNTIME_INSTANCE_ID',
|
|
45
|
+
'AMALGM_RUNTIME_LABEL',
|
|
46
|
+
'AMALGM_RUNTIME_USER_ID',
|
|
36
47
|
'AMALGM_NATIVE_NODE_MODULES',
|
|
37
48
|
'AMALGM_PROJECTS_DIR',
|
|
38
49
|
'AMALGM_SKIP_NATIVE_INSTALL',
|
|
@@ -224,6 +235,11 @@ function detectServiceBackend(mode = 'auto') {
|
|
|
224
235
|
function buildServiceEnv(localOnly) {
|
|
225
236
|
const env = {
|
|
226
237
|
AMALGM_DIR,
|
|
238
|
+
AMALGM_RUNTIME_INSTANCE_ID,
|
|
239
|
+
AMALGM_RUNTIME_LABEL,
|
|
240
|
+
AMALGM_BRANCH,
|
|
241
|
+
AMALGM_RUNTIME_USER_ID: AMALGM_RUNTIME_USER_SCOPE,
|
|
242
|
+
AMALGM_USER_SCOPE: AMALGM_RUNTIME_USER_SCOPE,
|
|
227
243
|
AMALGM_LOCAL_ONLY: localOnly ? 'true' : 'false',
|
|
228
244
|
AMALGM_SERVICE_MANAGED: 'true',
|
|
229
245
|
AMALGM_SERVICE_LABEL: SERVICE_LABEL,
|
|
@@ -294,6 +310,7 @@ function writePortableNodeScript(localOnly) {
|
|
|
294
310
|
maxRestartDelayMs: Math.max(5000, Number(process.env.AMALGM_SERVICE_MAX_RESTART_DELAY || 60) * 1000),
|
|
295
311
|
stableAfterMs: Math.max(10000, Number(process.env.AMALGM_SERVICE_STABLE_AFTER || 60) * 1000),
|
|
296
312
|
env: buildServiceEnv(localOnly),
|
|
313
|
+
runtimeInstanceArg: RUNTIME_INSTANCE_ARG,
|
|
297
314
|
};
|
|
298
315
|
const source = `'use strict';
|
|
299
316
|
|
|
@@ -422,7 +439,7 @@ function launch() {
|
|
|
422
439
|
append(config.serviceLog, 'starting amalgm run');
|
|
423
440
|
const fd = fs.openSync(config.daemonLog, 'a');
|
|
424
441
|
childStartedAt = Date.now();
|
|
425
|
-
child = spawn(config.nodeBin, [config.amalgmBin, 'run'], {
|
|
442
|
+
child = spawn(config.nodeBin, [config.amalgmBin, 'run', config.runtimeInstanceArg], {
|
|
426
443
|
cwd: process.env.HOME || process.cwd(),
|
|
427
444
|
env: { ...process.env, ...config.env },
|
|
428
445
|
stdio: ['ignore', fd, fd],
|
|
@@ -484,6 +501,7 @@ function writeLaunchdPlist(localOnly) {
|
|
|
484
501
|
<string>${plistEscape(process.execPath)}</string>
|
|
485
502
|
<string>${plistEscape(AMALGM_BIN)}</string>
|
|
486
503
|
<string>run</string>
|
|
504
|
+
<string>${plistEscape(RUNTIME_INSTANCE_ARG)}</string>
|
|
487
505
|
</array>
|
|
488
506
|
<key>EnvironmentVariables</key>
|
|
489
507
|
<dict>
|
|
@@ -519,6 +537,7 @@ function writeSystemdUnit(localOnly) {
|
|
|
519
537
|
AMALGM_NODE: process.execPath,
|
|
520
538
|
AMALGM_BIN,
|
|
521
539
|
AMALGM_DAEMON_LOG: path.join(LOG_DIR, 'daemon.log'),
|
|
540
|
+
AMALGM_RUNTIME_INSTANCE_ARG: RUNTIME_INSTANCE_ARG,
|
|
522
541
|
};
|
|
523
542
|
const envLines = Object.entries(env)
|
|
524
543
|
.map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`)
|
|
@@ -532,7 +551,7 @@ Wants=network-online.target
|
|
|
532
551
|
Type=simple
|
|
533
552
|
WorkingDirectory=${systemdQuote(os.homedir())}
|
|
534
553
|
${envLines}
|
|
535
|
-
ExecStart=/bin/sh -lc ${systemdQuote('exec "$AMALGM_NODE" "$AMALGM_BIN" run >> "$AMALGM_DAEMON_LOG" 2>&1')}
|
|
554
|
+
ExecStart=/bin/sh -lc ${systemdQuote('exec "$AMALGM_NODE" "$AMALGM_BIN" run "$AMALGM_RUNTIME_INSTANCE_ARG" >> "$AMALGM_DAEMON_LOG" 2>&1')}
|
|
536
555
|
Restart=always
|
|
537
556
|
RestartSec=5
|
|
538
557
|
KillSignal=SIGTERM
|
package/lib/supervisor.js
CHANGED
|
@@ -10,8 +10,12 @@ const net = require('net');
|
|
|
10
10
|
|
|
11
11
|
const {
|
|
12
12
|
AMALGM_DIR,
|
|
13
|
+
AMALGM_BRANCH,
|
|
13
14
|
LOG_DIR,
|
|
14
15
|
PID_FILE,
|
|
16
|
+
AMALGM_RUNTIME_INSTANCE_ID,
|
|
17
|
+
AMALGM_RUNTIME_LABEL,
|
|
18
|
+
AMALGM_RUNTIME_USER_SCOPE,
|
|
15
19
|
RUNTIME_DIR,
|
|
16
20
|
RUNTIME_STATE_FILE,
|
|
17
21
|
RUNTIME_TOKEN_FILE,
|
|
@@ -30,6 +34,7 @@ const {
|
|
|
30
34
|
const {
|
|
31
35
|
cleanupStaleRuntimeProcesses,
|
|
32
36
|
isPidRunning,
|
|
37
|
+
RUNTIME_INSTANCE_ARG,
|
|
33
38
|
supervisorProcesses,
|
|
34
39
|
} = require('./process-cleanup');
|
|
35
40
|
const {
|
|
@@ -204,6 +209,11 @@ function baseRuntimeEnv(record, ports, options = {}) {
|
|
|
204
209
|
const env = {
|
|
205
210
|
...safeBaseProcessEnv(),
|
|
206
211
|
AMALGM_RUNTIME_SOURCE: 'npm',
|
|
212
|
+
AMALGM_RUNTIME_INSTANCE_ID,
|
|
213
|
+
AMALGM_RUNTIME_LABEL,
|
|
214
|
+
AMALGM_BRANCH,
|
|
215
|
+
AMALGM_RUNTIME_USER_ID: AMALGM_RUNTIME_USER_SCOPE,
|
|
216
|
+
AMALGM_USER_SCOPE: AMALGM_RUNTIME_USER_SCOPE,
|
|
207
217
|
AMALGM_RUNTIME_TOKEN: record?.runtime_token || process.env.AMALGM_RUNTIME_TOKEN || '',
|
|
208
218
|
AMALGM_LOCAL_MODE: 'true',
|
|
209
219
|
AMALGM_LOCAL_ONLY: localOnly ? 'true' : 'false',
|
|
@@ -241,7 +251,7 @@ function serviceSpecs(record, ports, options = {}) {
|
|
|
241
251
|
return runtimeLaunchServices().map((service) => ({
|
|
242
252
|
name: service.name,
|
|
243
253
|
command: process.execPath,
|
|
244
|
-
args: [runtimeEntry(service.script)],
|
|
254
|
+
args: [runtimeEntry(service.script), RUNTIME_INSTANCE_ARG],
|
|
245
255
|
env: service.name === 'port-monitor' ? { ...env, PORT: process.env.PORT || '0' } : env,
|
|
246
256
|
port: ports[service.portKey],
|
|
247
257
|
healthPath: '/healthz',
|
|
@@ -608,6 +618,10 @@ async function startSupervisor(options = {}) {
|
|
|
608
618
|
pid: process.pid,
|
|
609
619
|
supervisor_pid: process.pid,
|
|
610
620
|
source: 'npm',
|
|
621
|
+
runtime_label: AMALGM_RUNTIME_LABEL,
|
|
622
|
+
label: AMALGM_RUNTIME_LABEL,
|
|
623
|
+
branch: AMALGM_BRANCH,
|
|
624
|
+
user_scope: AMALGM_RUNTIME_USER_SCOPE,
|
|
611
625
|
package_version: PACKAGE_VERSION,
|
|
612
626
|
runtime_dir: RUNTIME_DIR,
|
|
613
627
|
computer_id: record?.computer_id || null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
18
18
|
"prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
19
19
|
"pack:dry": "npm pack --dry-run",
|
|
20
|
-
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
20
|
+
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
@@ -60,57 +60,15 @@ function tabInfo(session, extra = {}) {
|
|
|
60
60
|
return info;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function executableExists(file) {
|
|
64
|
-
try {
|
|
65
|
-
fs.accessSync(file, fs.constants.X_OK);
|
|
66
|
-
return true;
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function linuxLibcSuffix() {
|
|
73
|
-
if (process.platform !== 'linux') return '';
|
|
74
|
-
const glibc = process.report?.getReport?.().header?.glibcVersionRuntime;
|
|
75
|
-
return glibc ? '' : '-musl';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function agentBrowserBinaryName() {
|
|
79
|
-
if (!['darwin', 'linux', 'win32'].includes(process.platform)) return null;
|
|
80
|
-
if (!['arm64', 'x64'].includes(process.arch)) return null;
|
|
81
|
-
const platform = process.platform === 'win32' ? 'win32' : `${process.platform}${linuxLibcSuffix()}`;
|
|
82
|
-
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
83
|
-
return `agent-browser-${platform}-${process.arch}${ext}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function asarUnpackedPath(file) {
|
|
87
|
-
const marker = `${path.sep}app.asar${path.sep}`;
|
|
88
|
-
return file.includes(marker)
|
|
89
|
-
? file.replace(marker, `${path.sep}app.asar.unpacked${path.sep}`)
|
|
90
|
-
: file;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function nativeAgentBrowserCommand(entrypoint) {
|
|
94
|
-
const binaryName = agentBrowserBinaryName();
|
|
95
|
-
if (!entrypoint || !binaryName) return null;
|
|
96
|
-
const candidate = path.join(path.dirname(entrypoint), binaryName);
|
|
97
|
-
const unpacked = asarUnpackedPath(candidate);
|
|
98
|
-
const candidates = unpacked === candidate ? [candidate] : [unpacked];
|
|
99
|
-
return candidates.find(executableExists) || null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
63
|
function agentBrowserCommand() {
|
|
103
64
|
if (process.env.AMALGM_AGENT_BROWSER_BIN) {
|
|
104
65
|
return { command: process.env.AMALGM_AGENT_BROWSER_BIN, prefix: [] };
|
|
105
66
|
}
|
|
106
67
|
|
|
107
68
|
try {
|
|
108
|
-
const entrypoint = require.resolve('agent-browser/bin/agent-browser.js');
|
|
109
|
-
const nativeCommand = nativeAgentBrowserCommand(entrypoint);
|
|
110
|
-
if (nativeCommand) return { command: nativeCommand, prefix: [] };
|
|
111
69
|
return {
|
|
112
70
|
command: process.execPath,
|
|
113
|
-
prefix: [
|
|
71
|
+
prefix: [require.resolve('agent-browser/bin/agent-browser.js')],
|
|
114
72
|
};
|
|
115
73
|
} catch {
|
|
116
74
|
return { command: 'agent-browser', prefix: [] };
|
|
@@ -1,12 +1,58 @@
|
|
|
1
1
|
const http = require('http');
|
|
2
2
|
const https = require('https');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
3
6
|
|
|
4
7
|
const DEFAULT_SESSION = `mcp-${process.pid}-default`;
|
|
5
8
|
|
|
9
|
+
function amalgmDir() {
|
|
10
|
+
return process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function bridgeConfigFile() {
|
|
14
|
+
return path.join(amalgmDir(), 'electron-browser-bridge.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isLoopbackUrl(value) {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(value);
|
|
20
|
+
return url.protocol === 'http:' && (url.hostname === '127.0.0.1' || url.hostname === 'localhost');
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pidIsRunning(pid) {
|
|
27
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return err && err.code === 'EPERM';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeConfig(data) {
|
|
37
|
+
const baseUrl = typeof data?.baseUrl === 'string' ? data.baseUrl : '';
|
|
38
|
+
const token = typeof data?.token === 'string' ? data.token : '';
|
|
39
|
+
if (!baseUrl || !token || !isLoopbackUrl(baseUrl)) return null;
|
|
40
|
+
if (typeof data?.pid === 'number' && !pidIsRunning(data.pid)) return null;
|
|
41
|
+
return { baseUrl, token };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fileConfig() {
|
|
45
|
+
try {
|
|
46
|
+
return normalizeConfig(JSON.parse(fs.readFileSync(bridgeConfigFile(), 'utf8')));
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
6
52
|
function config() {
|
|
7
53
|
const baseUrl = process.env.AMALGM_ELECTRON_BROWSER_URL;
|
|
8
54
|
const token = process.env.AMALGM_ELECTRON_BROWSER_TOKEN;
|
|
9
|
-
return
|
|
55
|
+
return normalizeConfig({ baseUrl, token }) || fileConfig();
|
|
10
56
|
}
|
|
11
57
|
|
|
12
58
|
function isConfigured() {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const electron = require('./electron-bridge');
|
|
2
2
|
const agentBrowser = require('./agent-browser');
|
|
3
3
|
|
|
4
|
+
function isPackagedDesktopRuntime() {
|
|
5
|
+
const execPath = process.execPath || '';
|
|
6
|
+
return process.env.ELECTRON_RUN_AS_NODE === '1' && execPath.includes('.app/Contents/MacOS/');
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
function backend() {
|
|
5
10
|
const requested = (process.env.AMALGM_BROWSER_BACKEND || '').toLowerCase();
|
|
6
11
|
if (requested === 'agent-browser' || requested === 'external') return agentBrowser;
|
|
@@ -8,7 +13,8 @@ function backend() {
|
|
|
8
13
|
|
|
9
14
|
// In the desktop app this bridge drives the visible in-tab Electron webview.
|
|
10
15
|
// Outside Electron, keep the agent-browser CLI fallback for headless/remote use.
|
|
11
|
-
|
|
16
|
+
if (electron.isConfigured() || isPackagedDesktopRuntime()) return electron;
|
|
17
|
+
return agentBrowser;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
const exported = {};
|
|
@@ -25,25 +25,6 @@ function executableExists(file) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function asarUnpackedPath(file) {
|
|
29
|
-
const marker = `${path.sep}app.asar${path.sep}`;
|
|
30
|
-
return file && file.includes(marker)
|
|
31
|
-
? file.replace(marker, `${path.sep}app.asar.unpacked${path.sep}`)
|
|
32
|
-
: file;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function executablePath(file) {
|
|
36
|
-
const unpacked = asarUnpackedPath(file);
|
|
37
|
-
if (unpacked !== file) return executableExists(unpacked) ? unpacked : null;
|
|
38
|
-
return executableExists(file) ? file : null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function existingPath(file) {
|
|
42
|
-
const unpacked = asarUnpackedPath(file);
|
|
43
|
-
if (unpacked !== file && fs.existsSync(unpacked)) return unpacked;
|
|
44
|
-
return fs.existsSync(file) ? file : null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
28
|
function findPackageJson(packageName) {
|
|
48
29
|
const candidateDirs = [
|
|
49
30
|
...nativeNodeModulesDirs(),
|
|
@@ -179,7 +160,7 @@ function bundledClaudeBinary() {
|
|
|
179
160
|
const root = packageRoot(packageName);
|
|
180
161
|
if (!root) return null;
|
|
181
162
|
const candidate = path.join(root, process.platform === 'win32' ? 'claude.exe' : 'claude');
|
|
182
|
-
return
|
|
163
|
+
return executableExists(candidate) ? candidate : null;
|
|
183
164
|
}
|
|
184
165
|
|
|
185
166
|
function resolveClaudeBinary() {
|
|
@@ -232,10 +213,8 @@ function codexVendorRoot() {
|
|
|
232
213
|
if (platformRoot) return path.join(platformRoot, 'vendor');
|
|
233
214
|
const wrapperRoot = packageRoot('@openai/codex');
|
|
234
215
|
if (wrapperRoot) {
|
|
235
|
-
const
|
|
236
|
-
if (
|
|
237
|
-
const localVendorRoot = existingPath(path.join(wrapperRoot, 'vendor'));
|
|
238
|
-
if (localVendorRoot) return localVendorRoot;
|
|
216
|
+
const localVendorRoot = path.join(wrapperRoot, 'vendor');
|
|
217
|
+
if (fs.existsSync(localVendorRoot)) return localVendorRoot;
|
|
239
218
|
}
|
|
240
219
|
return null;
|
|
241
220
|
}
|
|
@@ -245,8 +224,7 @@ function bundledCodexBinary() {
|
|
|
245
224
|
const vendorRoot = codexVendorRoot();
|
|
246
225
|
if (target && vendorRoot) {
|
|
247
226
|
const candidate = path.join(vendorRoot, target.triple, 'codex', target.binaryName);
|
|
248
|
-
|
|
249
|
-
if (binary) return binary;
|
|
227
|
+
if (executableExists(candidate)) return candidate;
|
|
250
228
|
}
|
|
251
229
|
return packageBin('@openai/codex', 'codex');
|
|
252
230
|
}
|
|
@@ -256,8 +234,7 @@ function bundledCodexPathDirs() {
|
|
|
256
234
|
const vendorRoot = codexVendorRoot();
|
|
257
235
|
if (!target || !vendorRoot) return [];
|
|
258
236
|
const candidate = path.join(vendorRoot, target.triple, 'path');
|
|
259
|
-
|
|
260
|
-
return rgPath ? [path.dirname(rgPath)] : [];
|
|
237
|
+
return executableExists(path.join(candidate, process.platform === 'win32' ? 'rg.exe' : 'rg')) ? [candidate] : [];
|
|
261
238
|
}
|
|
262
239
|
|
|
263
240
|
function openCodePlatform() {
|
|
@@ -314,14 +291,12 @@ function bundledOpenCodeBinary() {
|
|
|
314
291
|
const root = packageRoot(packageName);
|
|
315
292
|
if (!root) continue;
|
|
316
293
|
const candidate = path.join(root, 'bin', binaryName);
|
|
317
|
-
|
|
318
|
-
if (binary) return binary;
|
|
294
|
+
if (executableExists(candidate)) return candidate;
|
|
319
295
|
}
|
|
320
296
|
const wrapperRoot = packageRoot('opencode-ai');
|
|
321
297
|
if (wrapperRoot) {
|
|
322
298
|
const cached = path.join(wrapperRoot, 'bin', '.opencode');
|
|
323
|
-
|
|
324
|
-
if (binary) return binary;
|
|
299
|
+
if (executableExists(cached)) return cached;
|
|
325
300
|
}
|
|
326
301
|
return packageBin('opencode-ai', 'opencode');
|
|
327
302
|
}
|
|
@@ -365,12 +340,12 @@ function commandTargets() {
|
|
|
365
340
|
{
|
|
366
341
|
command: 'codex',
|
|
367
342
|
name: 'Codex',
|
|
368
|
-
path:
|
|
343
|
+
path: packageBin('@openai/codex', 'codex') || bundledCodexBinary(),
|
|
369
344
|
},
|
|
370
345
|
{
|
|
371
346
|
command: 'opencode',
|
|
372
347
|
name: 'OpenCode',
|
|
373
|
-
path:
|
|
348
|
+
path: packageBin('opencode-ai', 'opencode') || bundledOpenCodeBinary(),
|
|
374
349
|
},
|
|
375
350
|
];
|
|
376
351
|
}
|
|
@@ -51,6 +51,12 @@ const LEGACY_ARTIFACTS_FILE = path.join(AMALGM_DIR, 'artifacts.json');
|
|
|
51
51
|
const LOG_DIR = path.join(AMALGM_DIR, 'logs');
|
|
52
52
|
const BIND_HOST = process.env.AMALGM_BIND_HOST || '127.0.0.1';
|
|
53
53
|
const OWNER = process.env.AMALGM_RUNTIME_SOURCE || 'local';
|
|
54
|
+
const RUNTIME_LABEL = process.env.AMALGM_RUNTIME_LABEL || 'main';
|
|
55
|
+
const RUNTIME_BRANCH = process.env.AMALGM_BRANCH || (RUNTIME_LABEL === 'preview' ? 'preview' : 'main');
|
|
56
|
+
const RUNTIME_USER_SCOPE = process.env.AMALGM_RUNTIME_USER_ID
|
|
57
|
+
|| process.env.AMALGM_USER_SCOPE
|
|
58
|
+
|| process.env.AMALGM_USER_ID
|
|
59
|
+
|| 'local';
|
|
54
60
|
const FORCE_RUNTIME_STATE_OWNER = process.env.AMALGM_RUNTIME_FORCE_STATE_OWNER === 'true';
|
|
55
61
|
const VERSION = process.env.npm_package_version || process.env.AMALGM_RUNTIME_VERSION || '';
|
|
56
62
|
const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
|
|
@@ -330,6 +336,10 @@ function writeRuntimeState(actualPort) {
|
|
|
330
336
|
schema_version: 1,
|
|
331
337
|
owner: OWNER,
|
|
332
338
|
source: OWNER,
|
|
339
|
+
runtime_label: RUNTIME_LABEL,
|
|
340
|
+
label: RUNTIME_LABEL,
|
|
341
|
+
branch: RUNTIME_BRANCH,
|
|
342
|
+
user_scope: RUNTIME_USER_SCOPE,
|
|
333
343
|
pid: process.pid,
|
|
334
344
|
gateway_pid: process.pid,
|
|
335
345
|
supervisor_pid: process.ppid,
|
|
@@ -928,6 +938,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
928
938
|
status: 'ok',
|
|
929
939
|
service: 'local-gateway',
|
|
930
940
|
owner: OWNER,
|
|
941
|
+
runtime_label: RUNTIME_LABEL,
|
|
942
|
+
branch: RUNTIME_BRANCH,
|
|
943
|
+
user_scope: RUNTIME_USER_SCOPE,
|
|
931
944
|
pid: process.pid,
|
|
932
945
|
ports: SERVICE_PORTS,
|
|
933
946
|
});
|