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 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
- if (!record || typeof record !== 'object') return null;
170
- const auth = readJson(AUTH_FILE, null);
171
- const merged = mergeRecordAuth(record, auth);
172
- if (options.migrate && hasComputerSecrets(record)) {
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 Runtime state dir (default ~/.amalgm)',
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 explicitEnvByService = new Map(runtimeServices().map((service) => [service.name, service.envName]));
343
- for (const [name, port] of servicePorts()) {
344
- const envName = explicitEnvByService.get(name);
345
- if (!envName || !process.env[envName]) continue;
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 'latest';
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
- for (const [name, port] of servicePorts()) {
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 AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
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,
@@ -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
- if (RUNTIME_SERVICE_MARKERS.some((marker) => normalized.includes(marker))) return true;
154
- if (!RUNTIME_SERVICE_SUFFIXES.some((marker) => normalized.includes(marker))) return false;
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
- return normalized.includes(SUPERVISOR_MARKER) && /\brun\b/.test(normalized);
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 SERVICE_LABEL = 'ai.amalgm.runtime';
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.54",
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: [entrypoint],
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 baseUrl && token ? { baseUrl, token } : null;
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
- return electron.isConfigured() ? electron : agentBrowser;
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 executablePath(candidate);
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 nestedVendorRoot = existingPath(path.join(wrapperRoot, 'node_modules', target.packageName, 'vendor'));
236
- if (nestedVendorRoot) return nestedVendorRoot;
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
- const binary = executablePath(candidate);
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
- const rgPath = executablePath(path.join(candidate, process.platform === 'win32' ? 'rg.exe' : 'rg'));
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
- const binary = executablePath(candidate);
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
- const binary = executablePath(cached);
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: bundledCodexBinary() || packageBin('@openai/codex', 'codex'),
343
+ path: packageBin('@openai/codex', 'codex') || bundledCodexBinary(),
369
344
  },
370
345
  {
371
346
  command: 'opencode',
372
347
  name: 'OpenCode',
373
- path: bundledOpenCodeBinary() || packageBin('opencode-ai', 'opencode'),
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
  });