amalgm 0.1.57 → 0.1.58

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/cli.js CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  AMALGM_DIR,
14
14
  AMALGM_BRANCH,
15
15
  AMALGM_HOME,
16
+ AMALGM_RUNTIME_STATE_DIR,
16
17
  DEFAULT_APP_URL,
17
18
  DEVICE_FILE,
18
19
  LOG_DIR,
@@ -44,6 +45,7 @@ const {
44
45
  runtimeServiceProcesses,
45
46
  supervisorProcesses,
46
47
  } = require('./process-cleanup');
48
+ const { migrateLegacyUserState } = require('./state-migration');
47
49
  const {
48
50
  runtimePortsForStatus,
49
51
  runtimeServiceNames,
@@ -138,6 +140,7 @@ function ensureDir(dir, mode = 0o700) {
138
140
  function ensureBaseDirs() {
139
141
  ensureDir(AMALGM_DIR, 0o700);
140
142
  ensureDir(LOG_DIR, 0o700);
143
+ migrateLegacyUserState();
141
144
  }
142
145
 
143
146
  function readJson(file, fallback = null) {
@@ -203,6 +206,7 @@ const SAFE_DAEMON_ENV_KEYS = [
203
206
  'AMALGM_NATIVE_NODE_MODULES',
204
207
  'AMALGM_RUNTIME_INSTANCE_ID',
205
208
  'AMALGM_RUNTIME_LABEL',
209
+ 'AMALGM_RUNTIME_STATE_DIR',
206
210
  'AMALGM_RUNTIME_USER_ID',
207
211
  'AMALGM_SKIP_NATIVE_INSTALL',
208
212
  'CHAT_SERVER_PORT',
@@ -232,6 +236,8 @@ function daemonEnv(localOnly) {
232
236
  if (process.env[key]) env[key] = process.env[key];
233
237
  }
234
238
  env.AMALGM_DIR = AMALGM_DIR;
239
+ env.AMALGM_HOME = AMALGM_HOME;
240
+ env.AMALGM_RUNTIME_STATE_DIR = AMALGM_RUNTIME_STATE_DIR;
235
241
  env.AMALGM_RUNTIME_INSTANCE_ID = AMALGM_RUNTIME_INSTANCE_ID;
236
242
  env.AMALGM_RUNTIME_LABEL = AMALGM_RUNTIME_LABEL;
237
243
  env.AMALGM_BRANCH = AMALGM_BRANCH;
@@ -1073,6 +1079,7 @@ async function status() {
1073
1079
  console.log(`Label: ${AMALGM_RUNTIME_LABEL} (${AMALGM_BRANCH})`);
1074
1080
  console.log(`User scope: ${AMALGM_RUNTIME_USER_SCOPE}`);
1075
1081
  console.log(`State: ${AMALGM_DIR}`);
1082
+ console.log(`Runtime state: ${AMALGM_RUNTIME_STATE_DIR}`);
1076
1083
  if (runtimeState?.gateway_url) console.log(`Gateway: ${runtimeState.gateway_url}`);
1077
1084
  if (lastShutdown?.signal || lastShutdown?.reason) {
1078
1085
  console.log(
@@ -1235,6 +1242,7 @@ async function doctor() {
1235
1242
  console.log(`Label: ${AMALGM_RUNTIME_LABEL} (${AMALGM_BRANCH})`);
1236
1243
  console.log(`User scope: ${AMALGM_RUNTIME_USER_SCOPE}`);
1237
1244
  console.log(`State: ${AMALGM_DIR}`);
1245
+ console.log(`Runtime state: ${AMALGM_RUNTIME_STATE_DIR}`);
1238
1246
  for (const check of checks) {
1239
1247
  console.log(`${check.level.padEnd(4)} ${check.name.padEnd(18)} ${check.detail}`);
1240
1248
  }
package/lib/paths.js CHANGED
@@ -10,6 +10,7 @@ const {
10
10
  runtimeBranchForLabel,
11
11
  runtimeInstanceIdForDir,
12
12
  scopedAmalgmDir,
13
+ scopedRuntimeDir,
13
14
  } = require('./runtime-identity');
14
15
 
15
16
  const PACKAGE_VERSION = require('../package.json').version;
@@ -26,28 +27,32 @@ const AMALGM_RUNTIME_USER_SCOPE = resolveRuntimeUserScope(
26
27
  const AMALGM_DIR = process.env.AMALGM_DIR
27
28
  ? path.resolve(process.env.AMALGM_DIR)
28
29
  : scopedAmalgmDir(AMALGM_HOME, AMALGM_RUNTIME_USER_SCOPE, AMALGM_RUNTIME_LABEL);
30
+ const AMALGM_RUNTIME_STATE_DIR = process.env.AMALGM_RUNTIME_STATE_DIR
31
+ ? path.resolve(process.env.AMALGM_RUNTIME_STATE_DIR)
32
+ : scopedRuntimeDir(AMALGM_HOME, AMALGM_RUNTIME_USER_SCOPE, AMALGM_RUNTIME_LABEL);
29
33
  const AMALGM_RUNTIME_INSTANCE_ID = process.env.AMALGM_RUNTIME_INSTANCE_ID
30
- || runtimeInstanceIdForDir(AMALGM_DIR);
34
+ || runtimeInstanceIdForDir(AMALGM_RUNTIME_STATE_DIR);
31
35
  // Physical machine identity is intentionally shared across runtime labels.
32
- // Runtime credentials/state below remain scoped by user + label.
36
+ // User data below is scoped by user; live runtime files are scoped by label.
33
37
  const DEVICE_FILE = path.join(AMALGM_HOME, 'device.json');
34
38
  const COMPUTER_FILE = path.join(AMALGM_DIR, 'computer.json');
35
39
  const AUTH_FILE = path.join(AMALGM_DIR, 'auth.json');
36
- const RUNTIME_TOKEN_FILE = path.join(AMALGM_DIR, 'runtime-token.json');
37
- const PID_FILE = path.join(AMALGM_DIR, 'amalgm.pid');
38
- const RUNTIME_STATE_FILE = path.join(AMALGM_DIR, 'runtime-state.json');
39
- const LOG_DIR = path.join(AMALGM_DIR, 'logs');
40
- const SERVICE_DIR = path.join(AMALGM_DIR, 'service');
41
- const SERVICE_PID_FILE = path.join(AMALGM_DIR, 'amalgm-service.pid');
42
- const SERVICE_STATE_FILE = path.join(AMALGM_DIR, 'service-state.json');
43
- const SERVICE_STOP_FILE = path.join(AMALGM_DIR, 'amalgm-service.stop');
44
- const SHUTDOWN_REASON_FILE = path.join(AMALGM_DIR, 'shutdown-reason.json');
40
+ const RUNTIME_TOKEN_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'runtime-token.json');
41
+ const PID_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'amalgm.pid');
42
+ const RUNTIME_STATE_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'runtime-state.json');
43
+ const LOG_DIR = path.join(AMALGM_RUNTIME_STATE_DIR, 'logs');
44
+ const SERVICE_DIR = path.join(AMALGM_RUNTIME_STATE_DIR, 'service');
45
+ const SERVICE_PID_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'amalgm-service.pid');
46
+ const SERVICE_STATE_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'service-state.json');
47
+ const SERVICE_STOP_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'amalgm-service.stop');
48
+ const SHUTDOWN_REASON_FILE = path.join(AMALGM_RUNTIME_STATE_DIR, 'shutdown-reason.json');
45
49
  const DEFAULT_APP_URL = process.env.AMALGM_APP_URL || 'https://amalgm.ai';
46
50
 
47
51
  module.exports = {
48
52
  AMALGM_BRANCH,
49
53
  AMALGM_DIR,
50
54
  AMALGM_HOME,
55
+ AMALGM_RUNTIME_STATE_DIR,
51
56
  AMALGM_RUNTIME_INSTANCE_ID,
52
57
  AMALGM_RUNTIME_LABEL,
53
58
  AMALGM_RUNTIME_USER_SCOPE,
@@ -88,11 +88,19 @@ function discoverLegacyUserScope(homeDir, label = '') {
88
88
  const usersDir = path.join(homeDir, 'users');
89
89
  for (const entry of fs.readdirSync(usersDir, { withFileTypes: true })) {
90
90
  if (!entry.isDirectory()) continue;
91
- const scopedDir = path.join(usersDir, entry.name, runtimeLabel);
92
- const scopedRecord = readJson(path.join(scopedDir, 'computer.json'), null);
91
+ const userDir = path.join(usersDir, entry.name);
92
+ const userRecord = readJson(path.join(userDir, 'computer.json'), null);
93
+ const fromUserRecord = userIdFromRecord(userRecord);
94
+ if (fromUserRecord) return fromUserRecord;
95
+ const userAuth = readJson(path.join(userDir, 'auth.json'), null);
96
+ const fromUserAuth = userIdFromRecord(userAuth);
97
+ if (fromUserAuth) return fromUserAuth;
98
+
99
+ const oldScopedDir = path.join(userDir, runtimeLabel);
100
+ const scopedRecord = readJson(path.join(oldScopedDir, 'computer.json'), null);
93
101
  const fromScopedRecord = userIdFromRecord(scopedRecord);
94
102
  if (fromScopedRecord) return fromScopedRecord;
95
- const scopedAuth = readJson(path.join(scopedDir, 'auth.json'), null);
103
+ const scopedAuth = readJson(path.join(oldScopedDir, 'auth.json'), null);
96
104
  const fromScopedAuth = userIdFromRecord(scopedAuth);
97
105
  if (fromScopedAuth) return fromScopedAuth;
98
106
  }
@@ -117,7 +125,12 @@ function defaultAmalgmHome(env = process.env) {
117
125
  }
118
126
 
119
127
  function scopedAmalgmDir(homeDir, userScope, label) {
120
- return path.join(homeDir, 'users', sanitizeScopeSegment(userScope), assertRuntimeLabel(label));
128
+ assertRuntimeLabel(label);
129
+ return path.join(homeDir, 'users', sanitizeScopeSegment(userScope));
130
+ }
131
+
132
+ function scopedRuntimeDir(homeDir, userScope, label) {
133
+ return path.join(scopedAmalgmDir(homeDir, userScope, label), 'runtimes', assertRuntimeLabel(label));
121
134
  }
122
135
 
123
136
  function runtimeInstanceIdForDir(dir) {
@@ -142,4 +155,5 @@ module.exports = {
142
155
  runtimeLabelFromVersion,
143
156
  sanitizeScopeSegment,
144
157
  scopedAmalgmDir,
158
+ scopedRuntimeDir,
145
159
  };
package/lib/service.js CHANGED
@@ -7,12 +7,14 @@ const { spawn, spawnSync } = require('child_process');
7
7
 
8
8
  const {
9
9
  AMALGM_DIR,
10
+ AMALGM_HOME,
10
11
  AMALGM_BRANCH,
11
12
  LOG_DIR,
12
13
  PACKAGE_ROOT,
13
14
  PID_FILE,
14
15
  AMALGM_RUNTIME_INSTANCE_ID,
15
16
  AMALGM_RUNTIME_LABEL,
17
+ AMALGM_RUNTIME_STATE_DIR,
16
18
  AMALGM_RUNTIME_USER_SCOPE,
17
19
  SERVICE_DIR,
18
20
  SERVICE_PID_FILE,
@@ -43,6 +45,7 @@ const SERVICE_ENV_KEYS = [
43
45
  'AMALGM_HOME',
44
46
  'AMALGM_RUNTIME_INSTANCE_ID',
45
47
  'AMALGM_RUNTIME_LABEL',
48
+ 'AMALGM_RUNTIME_STATE_DIR',
46
49
  'AMALGM_RUNTIME_USER_ID',
47
50
  'AMALGM_NATIVE_NODE_MODULES',
48
51
  'AMALGM_PROJECTS_DIR',
@@ -235,6 +238,8 @@ function detectServiceBackend(mode = 'auto') {
235
238
  function buildServiceEnv(localOnly) {
236
239
  const env = {
237
240
  AMALGM_DIR,
241
+ AMALGM_HOME,
242
+ AMALGM_RUNTIME_STATE_DIR,
238
243
  AMALGM_RUNTIME_INSTANCE_ID,
239
244
  AMALGM_RUNTIME_LABEL,
240
245
  AMALGM_BRANCH,
@@ -261,6 +266,8 @@ function writePortableScript(localOnly) {
261
266
  '#!/bin/sh',
262
267
  'set -u',
263
268
  `AMALGM_DIR=${shellQuote(AMALGM_DIR)}`,
269
+ `AMALGM_HOME=${shellQuote(AMALGM_HOME)}`,
270
+ `AMALGM_RUNTIME_STATE_DIR=${shellQuote(AMALGM_RUNTIME_STATE_DIR)}`,
264
271
  `LOG_DIR=${shellQuote(LOG_DIR)}`,
265
272
  `PID_FILE=${shellQuote(SERVICE_PID_FILE)}`,
266
273
  `WORKER_PID_FILE=${shellQuote(PID_FILE)}`,
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const {
8
+ AMALGM_DIR,
9
+ AMALGM_HOME,
10
+ } = require('./paths');
11
+
12
+ const MARKER_FILE = path.join(AMALGM_DIR, '.user-state-migrated.json');
13
+
14
+ const DATA_FILES = [
15
+ 'amalgm.db',
16
+ 'apps.json',
17
+ 'artifacts.json',
18
+ 'agents.json',
19
+ 'tasks.json',
20
+ 'event-triggers.json',
21
+ 'workflows.json',
22
+ 'mcp-connections.json',
23
+ 'computer.json',
24
+ 'auth.json',
25
+ '.amalgm-credentials',
26
+ ];
27
+
28
+ const DATA_DIRS = [
29
+ 'apps',
30
+ 'browser',
31
+ 'browser-sessions',
32
+ 'contexts',
33
+ 'harness-configs',
34
+ 'memories',
35
+ '.memories',
36
+ 'task-runs',
37
+ 'toolbox',
38
+ 'uploads',
39
+ ];
40
+
41
+ function readJson(file, fallback = null) {
42
+ try {
43
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
44
+ } catch {
45
+ return fallback;
46
+ }
47
+ }
48
+
49
+ function writeJson(file, data) {
50
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
51
+ fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
52
+ }
53
+
54
+ function copyFileIfUseful(source, target) {
55
+ if (!fs.existsSync(source)) return false;
56
+
57
+ const targetExists = fs.existsSync(target);
58
+ if (targetExists && path.basename(source) === 'amalgm.db') {
59
+ const sourceSize = fs.statSync(source).size;
60
+ const targetSize = fs.statSync(target).size;
61
+ if (targetSize > 64 * 1024 || sourceSize <= targetSize) return false;
62
+ } else if (targetExists && fs.statSync(target).size > 64) {
63
+ return false;
64
+ }
65
+
66
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
67
+ fs.copyFileSync(source, target);
68
+ return true;
69
+ }
70
+
71
+ function copyDirIfMissing(source, target) {
72
+ if (!fs.existsSync(source) || fs.existsSync(target)) return false;
73
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
74
+ fs.cpSync(source, target, { recursive: true, force: false, errorOnExist: true });
75
+ return true;
76
+ }
77
+
78
+ function migrateLegacyUserState() {
79
+ if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return { migrated: false, reason: 'legacy-root' };
80
+ if (path.resolve(AMALGM_DIR).startsWith(path.resolve(os.tmpdir()))) return { migrated: false, reason: 'temp-dir' };
81
+ if (readJson(MARKER_FILE, null)) return { migrated: false, reason: 'already-migrated' };
82
+
83
+ fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
84
+
85
+ const copied = [];
86
+ for (const fileName of DATA_FILES) {
87
+ const source = path.join(AMALGM_HOME, fileName);
88
+ const target = path.join(AMALGM_DIR, fileName);
89
+ if (copyFileIfUseful(source, target)) copied.push(fileName);
90
+ }
91
+
92
+ for (const dirName of DATA_DIRS) {
93
+ const source = path.join(AMALGM_HOME, dirName);
94
+ const target = path.join(AMALGM_DIR, dirName);
95
+ if (copyDirIfMissing(source, target)) copied.push(`${dirName}/`);
96
+ }
97
+
98
+ writeJson(MARKER_FILE, {
99
+ migrated_at: new Date().toISOString(),
100
+ source: AMALGM_HOME,
101
+ target: AMALGM_DIR,
102
+ copied,
103
+ });
104
+
105
+ return { migrated: copied.length > 0, copied };
106
+ }
107
+
108
+ module.exports = {
109
+ migrateLegacyUserState,
110
+ };
package/lib/supervisor.js CHANGED
@@ -11,10 +11,12 @@ const net = require('net');
11
11
  const {
12
12
  AMALGM_DIR,
13
13
  AMALGM_BRANCH,
14
+ AMALGM_HOME,
14
15
  LOG_DIR,
15
16
  PID_FILE,
16
17
  AMALGM_RUNTIME_INSTANCE_ID,
17
18
  AMALGM_RUNTIME_LABEL,
19
+ AMALGM_RUNTIME_STATE_DIR,
18
20
  AMALGM_RUNTIME_USER_SCOPE,
19
21
  RUNTIME_DIR,
20
22
  RUNTIME_STATE_FILE,
@@ -221,6 +223,11 @@ function baseRuntimeEnv(record, ports, options = {}) {
221
223
  AMALGM_AUTH_BACKUP_ENABLED: process.env.AMALGM_AUTH_BACKUP_ENABLED || 'false',
222
224
  AMALGM_CREATE_AUTH_WATCH_DIRS: process.env.AMALGM_CREATE_AUTH_WATCH_DIRS || 'false',
223
225
  AMALGM_DIR,
226
+ AMALGM_HOME,
227
+ AMALGM_RUNTIME_STATE_DIR,
228
+ AMALGM_RUNTIME_STATE_FILE: RUNTIME_STATE_FILE,
229
+ AMALGM_RUNTIME_TOKEN_FILE: RUNTIME_TOKEN_FILE,
230
+ AMALGM_LOG_DIR: LOG_DIR,
224
231
  AMALGM_RUNTIME_DIR: RUNTIME_DIR,
225
232
  AMALGM_WORKSPACES_DIR: workspaceRoot,
226
233
  AMALGM_PROJECTS_DIR: workspaceRoot,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
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-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"
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/state-migration.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 && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
21
21
  },
22
22
  "engines": {
23
23
  "node": ">=20"
@@ -17,6 +17,7 @@ const {
17
17
  } = require('../config');
18
18
  const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
19
19
  const { appendStateEvent } = require('../state/events');
20
+ const { canonicalProjectPath } = require('../../lib/project-paths');
20
21
 
21
22
  function ensureAppsDirs() {
22
23
  ensureDir(APPS_DIR);
@@ -55,6 +56,7 @@ function normalizeApp(app) {
55
56
  return {
56
57
  ...app,
57
58
  kind: 'app',
59
+ ...(app.cwd ? { cwd: canonicalProjectPath(app.cwd) } : {}),
58
60
  appRef,
59
61
  app_ref: appRef,
60
62
  appUrl: publicUrl,
@@ -8,6 +8,7 @@ const { appendStateEvent, insertStateEvent, publishStateEvent } = require('../st
8
8
  const { normalizeTaskSchedule } = require('../tasks/schedule-normalization');
9
9
  const { compileWorkflowText, splitEventRef } = require('../workflows/compiler');
10
10
  const { getWebhookUrl } = require('../events/webhook-url');
11
+ const { canonicalProjectPath } = require('../../lib/project-paths');
11
12
  const { validateCellReferences } = require('./cell-references');
12
13
  const { validateWorkflowToolActions } = require('./tool-actions');
13
14
 
@@ -127,7 +128,7 @@ function normalizeWorkflowRecord(input = {}, existing = null) {
127
128
  name: cleanString(input.name) || existing?.name || 'Untitled workflow',
128
129
  description: input.description ?? existing?.description ?? '',
129
130
  enabled: input.enabled ?? existing?.enabled ?? true,
130
- projectPath: input.projectPath ?? existing?.projectPath ?? null,
131
+ projectPath: canonicalProjectPath(input.projectPath ?? existing?.projectPath ?? null) || null,
131
132
  workflowText,
132
133
  workflowIr,
133
134
  compilerErrors,
@@ -166,7 +167,7 @@ function rowToWorkflow(row) {
166
167
  name: row.name,
167
168
  description: row.description || '',
168
169
  enabled: row.enabled === 1,
169
- projectPath: row.project_path || null,
170
+ projectPath: canonicalProjectPath(row.project_path) || null,
170
171
  workflowText: row.workflow_text || parsed.workflowText,
171
172
  workflowIr: parseJson(row.workflow_ir_json, parsed.workflowIr || null),
172
173
  compilerErrors: parseJson(row.compiler_errors_json, parsed.compilerErrors || null),
@@ -276,7 +277,7 @@ function normalizeTriggerRecord(input = {}, automation, existing = null) {
276
277
  name: cleanString(input.name) || existing?.name || automation.name || 'Automation trigger',
277
278
  description: input.description ?? existing?.description ?? '',
278
279
  enabled: input.enabled ?? existing?.enabled ?? automation.enabled !== false,
279
- projectPath: input.projectPath ?? existing?.projectPath ?? automation.projectPath ?? null,
280
+ projectPath: canonicalProjectPath(input.projectPath ?? existing?.projectPath ?? automation.projectPath ?? null) || null,
280
281
  createdAt: isoOrNull(input.createdAt) || existing?.createdAt || timestamp,
281
282
  updatedAt: existing ? timestamp : (isoOrNull(input.updatedAt) || null),
282
283
  lastFiredAt: isoOrNull(input.lastFiredAt) || existing?.lastFiredAt || null,
@@ -429,7 +430,7 @@ function rowToAutomationBase(row) {
429
430
  name: row.name,
430
431
  description: row.description || '',
431
432
  enabled: row.enabled === 1,
432
- projectPath: row.project_path || null,
433
+ projectPath: canonicalProjectPath(row.project_path) || null,
433
434
  workflowId: row.workflow_id,
434
435
  createdAt: row.created_at,
435
436
  updatedAt: row.updated_at,
@@ -856,7 +857,7 @@ function rowToAutomationRun(row) {
856
857
  status: row.status,
857
858
  startedAt: row.started_at || parsed.startedAt || null,
858
859
  finishedAt: row.finished_at || parsed.finishedAt || null,
859
- projectPath: row.project_path || parsed.projectPath || null,
860
+ projectPath: canonicalProjectPath(row.project_path || parsed.projectPath) || null,
860
861
  error: row.error || parsed.error || null,
861
862
  createdAt: row.created_at,
862
863
  updatedAt: row.updated_at,
@@ -6,6 +6,7 @@
6
6
  * Same pattern as scripts/credential-adapter.js.
7
7
  */
8
8
 
9
+ const fs = require('fs');
9
10
  const path = require('path');
10
11
  const os = require('os');
11
12
 
@@ -13,6 +14,7 @@ const PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
13
14
  const MCP_PROTOCOL_VERSION = '2024-11-05';
14
15
 
15
16
  const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
17
+ const AMALGM_HOME = process.env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
16
18
 
17
19
  // Storage file/dir layout under AMALGM_DIR.
18
20
  const STORAGE_DIR = AMALGM_DIR; // tasks.json etc. live directly under ~/.amalgm
@@ -90,10 +92,41 @@ const APP_ROUTE_SYNC_INTERVAL_MS = parseInt(
90
92
  10,
91
93
  );
92
94
 
95
+ function copyIfUseful(source, target) {
96
+ if (!fs.existsSync(source)) return;
97
+ if (fs.existsSync(target) && fs.statSync(target).size > 64 * 1024) return;
98
+ fs.mkdirSync(path.dirname(target), { recursive: true, mode: 0o700 });
99
+ fs.copyFileSync(source, target);
100
+ }
101
+
102
+ function migrateLegacyUserState() {
103
+ if (path.resolve(AMALGM_DIR) === path.resolve(AMALGM_HOME)) return;
104
+ if (path.resolve(AMALGM_DIR).startsWith(path.resolve(os.tmpdir()))) return;
105
+ const marker = path.join(AMALGM_DIR, '.runtime-legacy-state-migrated.json');
106
+ if (fs.existsSync(marker)) return;
107
+ fs.mkdirSync(AMALGM_DIR, { recursive: true, mode: 0o700 });
108
+ for (const fileName of [
109
+ 'amalgm.db',
110
+ 'apps.json',
111
+ 'artifacts.json',
112
+ 'agents.json',
113
+ 'tasks.json',
114
+ 'event-triggers.json',
115
+ 'workflows.json',
116
+ 'mcp-connections.json',
117
+ ]) {
118
+ copyIfUseful(path.join(AMALGM_HOME, fileName), path.join(AMALGM_DIR, fileName));
119
+ }
120
+ fs.writeFileSync(marker, `${JSON.stringify({ migrated_at: new Date().toISOString(), source: AMALGM_HOME }, null, 2)}\n`);
121
+ }
122
+
123
+ migrateLegacyUserState();
124
+
93
125
  module.exports = {
94
126
  PORT,
95
127
  MCP_PROTOCOL_VERSION,
96
128
  AMALGM_DIR,
129
+ AMALGM_HOME,
97
130
  STORAGE_DIR,
98
131
  LOCAL_DB_FILE,
99
132
  TASKS_FILE,
@@ -7,6 +7,7 @@
7
7
 
8
8
  const { openLocalDb } = require('../state/db');
9
9
  const { insertStateEvent, publishStateEvent } = require('../state/events');
10
+ const { canonicalProjectPath } = require('../../lib/project-paths');
10
11
 
11
12
  const DEFAULT_SELECTED_MODELS = {
12
13
  claude_code: 'anthropic/claude-opus-4.7',
@@ -382,7 +383,7 @@ function normalizePreferences(input) {
382
383
  last_used: {
383
384
  harness: lastUsedHarness,
384
385
  model: lastUsedModel,
385
- cwd: cleanString(rawLastUsed.cwd ?? raw.last_cwd ?? raw.selected_cwd),
386
+ cwd: canonicalProjectPath(cleanString(rawLastUsed.cwd ?? raw.last_cwd ?? raw.selected_cwd)),
386
387
  },
387
388
  usage,
388
389
  recent_prefs: recentPrefs,
@@ -6,6 +6,10 @@ const path = require('path');
6
6
  const { AMALGM_COMPUTER_ID } = require('../config');
7
7
  const { openLocalDb } = require('../state/db');
8
8
  const { insertStateEvent, publishStateEvent } = require('../state/events');
9
+ const {
10
+ canonicalProjectPath,
11
+ isLegacyManagedWorkspacePath,
12
+ } = require('../../lib/project-paths');
9
13
 
10
14
  function cleanString(value) {
11
15
  return typeof value === 'string' && value.trim() ? value.trim() : '';
@@ -65,17 +69,18 @@ function normalizeWorkspaceRecord(input, options = {}) {
65
69
  throw new Error('Workspace path must be an absolute local path');
66
70
  }
67
71
 
68
- const resolvedPath = path.resolve(localPath);
72
+ const legacyManagedPath = isLegacyManagedWorkspacePath(localPath);
73
+ const resolvedPath = canonicalProjectPath(localPath);
69
74
  const computerId = stableComputerId(input.computerId || input.computer_id || options.computerId);
70
75
  const uri = cleanString(input.uri) || workspaceUriForPath(resolvedPath, computerId);
71
76
  const id = cleanString(input.id) || workspaceIdForUri(uri);
72
77
  const createdAt = cleanString(input.createdAt || input.created_at) || nowIso();
73
78
  const updatedAt = nowIso();
74
79
  const managed = input.managed !== undefined
75
- ? Boolean(input.managed)
80
+ ? Boolean(input.managed) && !legacyManagedPath
76
81
  : isManagedWorkspacePath(resolvedPath, options.workspaceRoot);
77
82
  const status = cleanString(input.status) || (fs.existsSync(resolvedPath) ? 'ready' : 'missing');
78
- const source = cleanString(input.source) || (managed ? 'managed' : 'computer');
83
+ const source = legacyManagedPath ? 'computer' : (cleanString(input.source) || (managed ? 'managed' : 'computer'));
79
84
  const name = cleanString(input.name) || basename(resolvedPath);
80
85
 
81
86
  return {
@@ -102,18 +107,20 @@ function normalizeWorkspaceRecord(input, options = {}) {
102
107
  function rowToWorkspace(row) {
103
108
  if (!row) return null;
104
109
  const parsed = parseJson(row.workspace_json, {});
110
+ const resolvedPath = row.path ? canonicalProjectPath(row.path) : row.path;
111
+ const legacyManagedPath = row.path ? isLegacyManagedWorkspacePath(row.path) : false;
105
112
  return {
106
113
  ...parsed,
107
114
  id: row.id,
108
115
  name: row.name,
109
116
  uri: row.uri,
110
- path: row.path,
111
- localPath: row.path,
117
+ path: resolvedPath,
118
+ localPath: resolvedPath,
112
119
  locationType: row.location_type,
113
120
  computerId: row.computer_id || parsed.computerId || null,
114
- source: row.source,
115
- managed: row.managed === 1,
116
- status: row.status,
121
+ source: legacyManagedPath ? 'computer' : row.source,
122
+ managed: legacyManagedPath ? false : row.managed === 1,
123
+ status: resolvedPath && fs.existsSync(resolvedPath) ? 'ready' : row.status,
117
124
  createdAt: row.created_at,
118
125
  updatedAt: row.updated_at,
119
126
  };
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { AMALGM_DIR, DEFAULT_CWD, PROXY_BASE_URL, PROXY_TOKEN } = require('../chat-server/config');
7
+ const { canonicalProjectPath } = require('../lib/project-paths');
7
8
  const { authEnvelope, coerceAuth, fingerprint } = require('./auth');
8
9
  const { runtimePort } = require('../../lib/runtime-manifest');
9
10
 
@@ -241,15 +242,23 @@ function existingDirectory(candidate) {
241
242
  }
242
243
  }
243
244
 
245
+ function safeProcessCwd() {
246
+ try {
247
+ return process.cwd();
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+
244
253
  function resolveCwd(rawCwd, options = {}) {
245
- const fallback = existingDirectory(expandHome(options.cwd))
246
- || existingDirectory(expandHome(DEFAULT_CWD))
247
- || existingDirectory(process.cwd())
254
+ const fallback = existingDirectory(canonicalProjectPath(expandHome(options.cwd)))
255
+ || existingDirectory(canonicalProjectPath(expandHome(DEFAULT_CWD)))
256
+ || existingDirectory(safeProcessCwd())
248
257
  || os.homedir();
249
258
  const expanded = expandHome(rawCwd);
250
259
  if (!expanded) return fallback;
251
260
  const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(fallback, expanded);
252
- return existingDirectory(absolute) || fallback;
261
+ return existingDirectory(canonicalProjectPath(absolute)) || fallback;
253
262
  }
254
263
 
255
264
  function nullableString(value) {
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ function cleanString(value) {
8
+ return typeof value === 'string' && value.trim() ? value.trim() : '';
9
+ }
10
+
11
+ function expandHome(value) {
12
+ const input = cleanString(value);
13
+ if (!input) return '';
14
+ if (input === '~') return os.homedir();
15
+ if (input.startsWith('~/')) return path.join(os.homedir(), input.slice(2));
16
+ return input;
17
+ }
18
+
19
+ function isWithin(root, target) {
20
+ const relative = path.relative(root, target);
21
+ return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
22
+ }
23
+
24
+ function legacyWorkspaceRoots() {
25
+ const home = process.env.AMALGM_HOME || path.join(os.homedir(), '.amalgm');
26
+ return [
27
+ process.env.AMALGM_LEGACY_WORKSPACES_DIR,
28
+ path.join(home, 'workspaces'),
29
+ path.join(home, 'projects'),
30
+ ]
31
+ .filter(Boolean)
32
+ .map((entry) => path.resolve(expandHome(entry)));
33
+ }
34
+
35
+ function legacyManagedRelativePath(localPath) {
36
+ const resolved = path.resolve(expandHome(localPath));
37
+ const currentRoot = cleanString(process.env.AMALGM_WORKSPACES_DIR || process.env.AMALGM_PROJECTS_DIR);
38
+
39
+ for (const root of legacyWorkspaceRoots()) {
40
+ if (currentRoot && path.resolve(root) === path.resolve(expandHome(currentRoot))) continue;
41
+ if (!isWithin(root, resolved)) continue;
42
+ const relative = path.relative(root, resolved);
43
+ return relative && !relative.startsWith('..') ? relative : '';
44
+ }
45
+
46
+ return '';
47
+ }
48
+
49
+ function canonicalProjectPath(value) {
50
+ const expanded = expandHome(value);
51
+ if (!expanded) return '';
52
+ const absolute = path.isAbsolute(expanded) ? path.resolve(expanded) : path.resolve(expanded);
53
+ const legacyRelative = legacyManagedRelativePath(absolute);
54
+
55
+ if (legacyRelative) {
56
+ const homeCandidate = path.join(os.homedir(), legacyRelative);
57
+ if (fs.existsSync(homeCandidate)) return path.resolve(homeCandidate);
58
+ }
59
+
60
+ return absolute;
61
+ }
62
+
63
+ function isLegacyManagedWorkspacePath(value) {
64
+ return Boolean(cleanString(value) && legacyManagedRelativePath(value));
65
+ }
66
+
67
+ module.exports = {
68
+ canonicalProjectPath,
69
+ cleanString,
70
+ expandHome,
71
+ isLegacyManagedWorkspacePath,
72
+ };
@@ -0,0 +1,47 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ function normalizeRuntimeLabel(value) {
7
+ const raw = String(value || '').trim().toLowerCase();
8
+ if (['preview', 'canary', 'native-chat', 'staging'].includes(raw)) return 'preview';
9
+ if (['local', 'dev', 'development'].includes(raw)) return 'local';
10
+ return 'main';
11
+ }
12
+
13
+ function amalgmDir() {
14
+ return process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
15
+ }
16
+
17
+ function runtimeLabel() {
18
+ return normalizeRuntimeLabel(process.env.AMALGM_RUNTIME_LABEL || process.env.AMALGM_BRANCH);
19
+ }
20
+
21
+ function runtimeStateDir() {
22
+ return (
23
+ process.env.AMALGM_RUNTIME_STATE_DIR
24
+ || path.join(amalgmDir(), 'runtimes', runtimeLabel())
25
+ );
26
+ }
27
+
28
+ function runtimeStateFile() {
29
+ return process.env.AMALGM_RUNTIME_STATE_FILE || path.join(runtimeStateDir(), 'runtime-state.json');
30
+ }
31
+
32
+ function runtimeTokenFile() {
33
+ return process.env.AMALGM_RUNTIME_TOKEN_FILE || path.join(runtimeStateDir(), 'runtime-token.json');
34
+ }
35
+
36
+ function runtimeLogDir() {
37
+ return process.env.AMALGM_LOG_DIR || path.join(runtimeStateDir(), 'logs');
38
+ }
39
+
40
+ module.exports = {
41
+ amalgmDir,
42
+ runtimeLabel,
43
+ runtimeLogDir,
44
+ runtimeStateDir,
45
+ runtimeStateFile,
46
+ runtimeTokenFile,
47
+ };
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * One discoverable loopback entrypoint for the local Amalgm runtime. The
7
7
  * individual services still run as separate processes, but clients only need
8
- * to find this gateway port from ~/.amalgm/runtime-state.json.
8
+ * to find this gateway port from the label-scoped runtime-state.json.
9
9
  */
10
10
 
11
11
  const crypto = require('crypto');
@@ -23,6 +23,10 @@ const {
23
23
  runtimeAuthHeaders,
24
24
  } = require('./runtime-auth');
25
25
  const { runtimePort } = require('../lib/runtime-manifest');
26
+ const {
27
+ runtimeLogDir,
28
+ runtimeStateFile,
29
+ } = require('./lib/runtime-paths');
26
30
 
27
31
  function loadPty() {
28
32
  const candidates = [
@@ -45,10 +49,10 @@ function loadPty() {
45
49
  const pty = loadPty();
46
50
 
47
51
  const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
48
- const STATE_FILE = path.join(AMALGM_DIR, 'runtime-state.json');
52
+ const STATE_FILE = runtimeStateFile();
49
53
  const APPS_FILE = path.join(AMALGM_DIR, 'apps.json');
50
54
  const LEGACY_ARTIFACTS_FILE = path.join(AMALGM_DIR, 'artifacts.json');
51
- const LOG_DIR = path.join(AMALGM_DIR, 'logs');
55
+ const LOG_DIR = runtimeLogDir();
52
56
  const BIND_HOST = process.env.AMALGM_BIND_HOST || '127.0.0.1';
53
57
  const OWNER = process.env.AMALGM_RUNTIME_SOURCE || 'local';
54
58
  const RUNTIME_LABEL = process.env.AMALGM_RUNTIME_LABEL || 'main';