amalgm 0.1.44 → 0.1.47

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.
@@ -9,8 +9,9 @@ const { promptText } = require('../input');
9
9
  const { runtimeEnv } = require('../auth');
10
10
  const { codexErrorMessage, normalizeCodexNotification } = require('../normalizers/codex');
11
11
  const { recordNativeEvent } = require('../recorder');
12
- const { toCodexMcpToml } = require('../tooling/mcp-bundle');
12
+ const { relayedMcpServers, toCodexMcpToml } = require('../tooling/mcp-bundle');
13
13
  const { bundledCodexBinary, bundledCodexPathDirs, executableExists } = require('../tooling/native-binaries');
14
+ const { syncCodexNativeConfig } = require('../tooling/native-config');
14
15
  const { composeSystemPrompt } = require('../tooling/system-prompt');
15
16
 
16
17
  class JsonLineRpc {
@@ -176,11 +177,167 @@ function isCompactCommand(text) {
176
177
  return String(text || '').trim().toLowerCase() === '/compact';
177
178
  }
178
179
 
180
+ function readTextFile(file) {
181
+ try {
182
+ return fs.readFileSync(file, 'utf8');
183
+ } catch {
184
+ return '';
185
+ }
186
+ }
187
+
188
+ function tomlSectionName(line) {
189
+ const match = String(line || '').match(/^\s*\[([^\]]+)\]\s*$/);
190
+ return match ? match[1].trim() : null;
191
+ }
192
+
193
+ function removeTopLevelKeys(toml, keys) {
194
+ const remove = new Set(keys);
195
+ let inTopLevel = true;
196
+ return String(toml || '')
197
+ .split(/\r?\n/)
198
+ .filter((line) => {
199
+ if (tomlSectionName(line)) inTopLevel = false;
200
+ if (!inTopLevel) return true;
201
+ const match = line.match(/^\s*([A-Za-z0-9_-]+)\s*=/);
202
+ return !match || !remove.has(match[1]);
203
+ })
204
+ .join('\n');
205
+ }
206
+
207
+ function removeTomlSections(toml, sectionNames) {
208
+ const remove = new Set(sectionNames);
209
+ let skipping = false;
210
+ const out = [];
211
+ for (const line of String(toml || '').split(/\r?\n/)) {
212
+ const section = tomlSectionName(line);
213
+ if (section) {
214
+ skipping = [...remove].some((name) => section === name || section.startsWith(`${name}.`));
215
+ }
216
+ if (!skipping) out.push(line);
217
+ }
218
+ return out.join('\n');
219
+ }
220
+
221
+ function ensureFeatureBoolean(toml, key, value) {
222
+ const lines = String(toml || '').split(/\r?\n/);
223
+ const sectionIndex = lines.findIndex((line) => tomlSectionName(line) === 'features');
224
+ const rendered = `${key} = ${value ? 'true' : 'false'}`;
225
+ if (sectionIndex === -1) {
226
+ return `${String(toml || '').trimEnd()}\n\n[features]\n${rendered}\n`;
227
+ }
228
+ let insertAt = lines.length;
229
+ for (let i = sectionIndex + 1; i < lines.length; i += 1) {
230
+ if (tomlSectionName(lines[i])) {
231
+ insertAt = i;
232
+ break;
233
+ }
234
+ if (new RegExp(`^\\s*${key}\\s*=`).test(lines[i])) {
235
+ lines[i] = rendered;
236
+ return lines.join('\n');
237
+ }
238
+ }
239
+ lines.splice(insertAt, 0, rendered);
240
+ return lines.join('\n');
241
+ }
242
+
243
+ function tomlQuoted(value) {
244
+ return JSON.stringify(String(value || ''));
245
+ }
246
+
247
+ function hookTrustBlocks(toml) {
248
+ const lines = String(toml || '').split(/\r?\n/);
249
+ const blocks = [];
250
+ for (let i = 0; i < lines.length; i += 1) {
251
+ const section = tomlSectionName(lines[i]);
252
+ const match = section?.match(/^hooks\.state\."([^"]+)"$/);
253
+ if (!match) continue;
254
+ let end = lines.length;
255
+ for (let j = i + 1; j < lines.length; j += 1) {
256
+ if (tomlSectionName(lines[j])) {
257
+ end = j;
258
+ break;
259
+ }
260
+ }
261
+ blocks.push({ key: match[1], lines: lines.slice(i, end) });
262
+ }
263
+ return blocks;
264
+ }
265
+
266
+ function mirrorCodexHookTrust(toml, sourceDir, runtimeHome) {
267
+ if (!sourceDir || !runtimeHome) return toml;
268
+ const sourceHookPrefix = `${path.join(sourceDir, 'hooks.json')}:`;
269
+ const runtimePrefixes = [
270
+ `${path.join(runtimeHome, 'hooks.json')}:`,
271
+ `${path.join(runtimeHome, '.codex', 'hooks.json')}:`,
272
+ ];
273
+ const blocks = hookTrustBlocks(toml);
274
+ if (blocks.length === 0) return toml;
275
+ const existing = new Set(blocks.map((block) => block.key));
276
+ const additions = [];
277
+ for (const block of blocks) {
278
+ if (!block.key.startsWith(sourceHookPrefix)) continue;
279
+ const suffix = block.key.slice(sourceHookPrefix.length);
280
+ for (const prefix of runtimePrefixes) {
281
+ const nextKey = `${prefix}${suffix}`;
282
+ if (existing.has(nextKey)) continue;
283
+ existing.add(nextKey);
284
+ const lines = [...block.lines];
285
+ lines[0] = `[hooks.state.${tomlQuoted(nextKey)}]`;
286
+ additions.push(lines.join('\n'));
287
+ }
288
+ }
289
+ if (additions.length === 0) return toml;
290
+ const parent = /\n\s*\[hooks\.state\]\s*(?:\n|$)/.test(`\n${toml}`) ? '' : '\n[hooks.state]\n';
291
+ return `${String(toml || '').trimEnd()}${parent}\n${additions.join('\n\n')}\n`;
292
+ }
293
+
294
+ function generatedMcpSectionNames(contract) {
295
+ return relayedMcpServers(contract).map((server) => `mcp_servers.${server.name}`);
296
+ }
297
+
298
+ function buildCodexConfig(contract, existingConfig, syncInfo) {
299
+ const mcpToml = toCodexMcpToml(contract);
300
+ const topLevelKeys = [
301
+ ...(contract.authMethod === 'provider_auth' ? [] : ['model_provider']),
302
+ ...(contract.authMethod === 'amalgm' ? ['model_context_window', 'model_auto_compact_token_limit'] : []),
303
+ ];
304
+ let config = removeTopLevelKeys(existingConfig, topLevelKeys);
305
+ config = removeTomlSections(config, [
306
+ ...(contract.authMethod === 'amalgm' ? ['model_providers.amalgm'] : []),
307
+ ...generatedMcpSectionNames(contract),
308
+ ]);
309
+ config = mirrorCodexHookTrust(config, syncInfo?.sourceDir, contract.auth.runtimeHome);
310
+ if (fs.existsSync(path.join(contract.auth.runtimeHome, 'hooks.json'))) {
311
+ config = ensureFeatureBoolean(config, 'codex_hooks', true);
312
+ }
313
+ const generated = contract.authMethod === 'amalgm'
314
+ ? [
315
+ 'model_provider = "amalgm"',
316
+ ...modelWindowConfigLines(contract),
317
+ '',
318
+ '[model_providers.amalgm]',
319
+ 'name = "amalgm"',
320
+ `base_url = "${contract.auth.baseUrl}"`,
321
+ 'env_key = "OPENAI_API_KEY"',
322
+ 'wire_api = "responses"',
323
+ 'requires_openai_auth = false',
324
+ ].join('\n')
325
+ : contract.authMethod === 'byok'
326
+ ? 'model_provider = "openai"'
327
+ : '';
328
+ return [
329
+ generated.trimEnd(),
330
+ String(config || '').trim(),
331
+ String(mcpToml || '').trim(),
332
+ ].filter(Boolean).join('\n\n') + '\n';
333
+ }
334
+
179
335
  function writeConfig(contract) {
180
336
  const home = contract.auth.runtimeHome;
181
337
  if (!home) return;
182
338
  fs.mkdirSync(home, { recursive: true });
183
- const mcpToml = toCodexMcpToml(contract);
339
+ const syncInfo = syncCodexNativeConfig(home);
340
+ const configPath = path.join(home, 'config.toml');
184
341
  if (contract.authMethod === 'provider_auth') {
185
342
  const sourceAuth = path.join(os.homedir(), '.codex', 'auth.json');
186
343
  const targetAuth = path.join(home, 'auth.json');
@@ -188,34 +345,10 @@ function writeConfig(contract) {
188
345
  fs.copyFileSync(sourceAuth, targetAuth);
189
346
  fs.chmodSync(targetAuth, 0o600);
190
347
  }
191
- fs.writeFileSync(path.join(home, 'config.toml'), [
192
- 'model_provider = "openai"',
193
- '',
194
- mcpToml,
195
- ].join('\n'), { mode: 0o600 });
348
+ fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
196
349
  return;
197
350
  }
198
- if (contract.authMethod === 'amalgm') {
199
- fs.writeFileSync(path.join(home, 'config.toml'), [
200
- 'model_provider = "amalgm"',
201
- ...modelWindowConfigLines(contract),
202
- '',
203
- '[model_providers.amalgm]',
204
- 'name = "amalgm"',
205
- `base_url = "${contract.auth.baseUrl}"`,
206
- 'env_key = "OPENAI_API_KEY"',
207
- 'wire_api = "responses"',
208
- 'requires_openai_auth = false',
209
- '',
210
- mcpToml,
211
- ].join('\n'), { mode: 0o600 });
212
- } else {
213
- fs.writeFileSync(path.join(home, 'config.toml'), [
214
- 'model_provider = "openai"',
215
- '',
216
- mcpToml,
217
- ].join('\n'), { mode: 0o600 });
218
- }
351
+ fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
219
352
  fs.writeFileSync(path.join(home, 'auth.json'), JSON.stringify({
220
353
  auth_mode: 'apikey',
221
354
  OPENAI_API_KEY: contract.auth.tokenRef,
@@ -350,4 +483,15 @@ class CodexAdapter {
350
483
  }
351
484
  }
352
485
 
353
- module.exports = { CodexAdapter, __private: { modelWindowConfigLines, writeConfig } };
486
+ module.exports = {
487
+ CodexAdapter,
488
+ __private: {
489
+ buildCodexConfig,
490
+ ensureFeatureBoolean,
491
+ mirrorCodexHookTrust,
492
+ modelWindowConfigLines,
493
+ removeTomlSections,
494
+ removeTopLevelKeys,
495
+ writeConfig,
496
+ },
497
+ };
@@ -70,7 +70,7 @@ function coerceAuth(harness, requested) {
70
70
  }
71
71
 
72
72
  function runtimeHome({ amalgmDir, sessionId, harness, authMethod, tokenFingerprint }) {
73
- if (authMethod === 'provider_auth' && harness !== 'codex') return null;
73
+ if (authMethod === 'provider_auth' && !['claude_code', 'codex'].includes(harness)) return null;
74
74
  const suffix = tokenFingerprint || 'default';
75
75
  return path.join(amalgmDir, 'runtime', sessionId, harness, suffix);
76
76
  }
@@ -5,6 +5,7 @@ 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
7
  const { authEnvelope, coerceAuth, fingerprint } = require('./auth');
8
+ const { runtimePort } = require('../../lib/runtime-manifest');
8
9
 
9
10
  function agentToHarness(agent) {
10
11
  const clean = String(agent || '').trim();
@@ -323,7 +324,7 @@ function createContract(payload, options = {}) {
323
324
  const fastMode = payload.fastMode === true;
324
325
  const cliModel = cliModelFor({ harness, modelId, cliModel: payload.cliModel, reasoningEffort });
325
326
  const cwd = resolveCwd(payload.cwd, options);
326
- const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || process.env.CHAT_SERVER_PORT || 8084}`;
327
+ const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || runtimePort('chat-server')}`;
327
328
  const origin = normalizeOrigin({ ...payload, cwd });
328
329
  const auth = authEnvelope({
329
330
  harness,
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { getRuntimeToken } = require('../../runtime-auth');
4
+ const { runtimePort } = require('../../../lib/runtime-manifest');
4
5
 
5
6
  function headerArrayToRecord(headers) {
6
7
  const out = {};
@@ -49,12 +50,11 @@ function normalizeServer(server) {
49
50
  }
50
51
 
51
52
  function localBaseUrl(contract) {
52
- return String(contract.localBaseUrl || `http://127.0.0.1:${process.env.CHAT_SERVER_PORT || 8084}`).replace(/\/$/, '');
53
+ return String(contract.localBaseUrl || `http://127.0.0.1:${runtimePort('chat-server')}`).replace(/\/$/, '');
53
54
  }
54
55
 
55
56
  function amalgmMcpServer(contract) {
56
- const port = process.env.AMALGM_MCP_PORT || '8083';
57
- const url = process.env.AMALGM_MCP_URL || `http://127.0.0.1:${port}/mcp`;
57
+ const url = process.env.AMALGM_MCP_URL || `http://127.0.0.1:${runtimePort('amalgm-mcp')}/mcp`;
58
58
  const headers = [
59
59
  { name: 'x-amalgm-runtime-token', value: getRuntimeToken() },
60
60
  { name: 'x-amalgm-session-id', value: contract.sessionId },
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const EXCLUDED_DIR_NAMES = new Set([
8
+ '.cache',
9
+ '.npm',
10
+ '.tmp',
11
+ 'cache',
12
+ 'log',
13
+ 'logs',
14
+ 'projects',
15
+ 'sessions',
16
+ 'shell_snapshots',
17
+ 'tmp',
18
+ ]);
19
+
20
+ const EXCLUDED_FILE_PATTERNS = [
21
+ /^logs?_\d+\.sqlite(?:-.+)?$/i,
22
+ /^state_\d+\.sqlite(?:-.+)?$/i,
23
+ /^.*\.db-(?:shm|wal)$/i,
24
+ /^.*\.sqlite-(?:shm|wal)$/i,
25
+ ];
26
+
27
+ function exists(p) {
28
+ try {
29
+ fs.accessSync(p);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ function shouldCopyConfigPath(root, source) {
37
+ const relative = path.relative(root, source);
38
+ if (!relative || relative === '.') return true;
39
+ const parts = relative.split(path.sep);
40
+ if (parts.some((part) => EXCLUDED_DIR_NAMES.has(part))) return false;
41
+ const basename = path.basename(source);
42
+ return !EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(basename));
43
+ }
44
+
45
+ function copyConfigTree(sourceDir, targetDir) {
46
+ if (!sourceDir || !targetDir || !exists(sourceDir)) return false;
47
+ if (path.resolve(sourceDir) === path.resolve(targetDir)) return true;
48
+ fs.mkdirSync(targetDir, { recursive: true });
49
+ fs.cpSync(sourceDir, targetDir, {
50
+ recursive: true,
51
+ force: true,
52
+ errorOnExist: false,
53
+ dereference: false,
54
+ filter: (source) => shouldCopyConfigPath(sourceDir, source),
55
+ });
56
+ return true;
57
+ }
58
+
59
+ function copyFileIfPresent(source, target) {
60
+ if (!exists(source)) return false;
61
+ fs.mkdirSync(path.dirname(target), { recursive: true });
62
+ fs.copyFileSync(source, target);
63
+ try {
64
+ fs.chmodSync(target, fs.statSync(source).mode & 0o777);
65
+ } catch {}
66
+ return true;
67
+ }
68
+
69
+ function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
70
+ if (!runtimeHome || !dotDirName || !sourceDir || !exists(sourceDir)) return;
71
+ const alias = path.join(runtimeHome, dotDirName);
72
+ try {
73
+ const stat = fs.lstatSync(alias);
74
+ if (stat.isSymbolicLink()) {
75
+ const target = fs.readlinkSync(alias);
76
+ if (target === '.' || path.resolve(runtimeHome, target) === path.resolve(runtimeHome)) return;
77
+ fs.rmSync(alias, { recursive: true, force: true });
78
+ } else if (stat.isDirectory()) {
79
+ copyConfigTree(sourceDir, alias);
80
+ return;
81
+ } else {
82
+ fs.rmSync(alias, { recursive: true, force: true });
83
+ }
84
+ } catch {}
85
+ try {
86
+ fs.symlinkSync('.', alias, 'dir');
87
+ } catch {
88
+ copyConfigTree(sourceDir, alias);
89
+ }
90
+ }
91
+
92
+ function syncCodexNativeConfig(runtimeHome) {
93
+ if (!runtimeHome) return null;
94
+ const nativeHome = os.homedir();
95
+ const sourceDir = path.join(nativeHome, '.codex');
96
+ fs.mkdirSync(runtimeHome, { recursive: true });
97
+ const copied = copyConfigTree(sourceDir, runtimeHome);
98
+ copyIntoHomeAlias(runtimeHome, '.codex', sourceDir);
99
+ copyConfigTree(path.join(nativeHome, '.codex-supermemory'), path.join(runtimeHome, '.codex-supermemory'));
100
+ copyFileIfPresent(path.join(nativeHome, '.codex-supermemory.log'), path.join(runtimeHome, '.codex-supermemory.log'));
101
+ return copied ? { sourceDir, runtimeHome } : null;
102
+ }
103
+
104
+ function syncClaudeNativeConfig(runtimeHome) {
105
+ if (!runtimeHome) return null;
106
+ const nativeHome = os.homedir();
107
+ const sourceDir = path.join(nativeHome, '.claude');
108
+ fs.mkdirSync(runtimeHome, { recursive: true });
109
+ const copied = copyConfigTree(sourceDir, runtimeHome);
110
+ copyIntoHomeAlias(runtimeHome, '.claude', sourceDir);
111
+ copyFileIfPresent(path.join(nativeHome, '.claude.json'), path.join(runtimeHome, '.claude.json'));
112
+ copyConfigTree(path.join(nativeHome, '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
113
+ return copied ? { sourceDir, runtimeHome } : null;
114
+ }
115
+
116
+ function syncNativeHarnessConfig(contract) {
117
+ const runtimeHome = contract?.auth?.runtimeHome;
118
+ if (!runtimeHome) return null;
119
+ if (contract.harness === 'codex') return syncCodexNativeConfig(runtimeHome);
120
+ if (contract.harness === 'claude_code') return syncClaudeNativeConfig(runtimeHome);
121
+ return null;
122
+ }
123
+
124
+ module.exports = {
125
+ __private: {
126
+ copyConfigTree,
127
+ copyFileIfPresent,
128
+ shouldCopyConfigPath,
129
+ },
130
+ syncClaudeNativeConfig,
131
+ syncCodexNativeConfig,
132
+ syncNativeHarnessConfig,
133
+ };
@@ -14,6 +14,7 @@ const {
14
14
  proxyBaseUrl,
15
15
  readProxyToken,
16
16
  } = require('../proxy-token-store');
17
+ const { runtimePort } = require('../../lib/runtime-manifest');
17
18
 
18
19
  // ── Paths ──────────────────────────────────────────────────────────────────
19
20
  // Set by the orchestrator (entrypoint.sh or electron/main.ts).
@@ -24,7 +25,7 @@ const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || '/workspace';
24
25
 
25
26
  // ── Server ──────────────────────────────────────────────────────────────────
26
27
 
27
- const PORT = parseInt(process.env.CHAT_SERVER_PORT || '8084', 10);
28
+ const PORT = runtimePort('chat-server');
28
29
  const BIND_HOST = process.env.AMALGM_BIND_HOST || '0.0.0.0';
29
30
 
30
31
  // ── API keys / proxy ────────────────────────────────────────────────────────
@@ -22,6 +22,7 @@ const {
22
22
  isAuthorizedRuntimeRequest,
23
23
  runtimeAuthHeaders,
24
24
  } = require('./runtime-auth');
25
+ const { runtimePort } = require('../lib/runtime-manifest');
25
26
 
26
27
  function loadPty() {
27
28
  const candidates = [
@@ -51,7 +52,7 @@ const BIND_HOST = process.env.AMALGM_BIND_HOST || '127.0.0.1';
51
52
  const OWNER = process.env.AMALGM_RUNTIME_SOURCE || 'local';
52
53
  const VERSION = process.env.npm_package_version || process.env.AMALGM_RUNTIME_VERSION || '';
53
54
  const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
54
- const PORT = Number.parseInt(process.env.AMALGM_GATEWAY_PORT || '28781', 10);
55
+ const PORT = runtimePort('local-gateway');
55
56
  const RUNTIME_TOKEN_HEADER = 'x-amalgm-runtime-token';
56
57
  const DEFAULT_DIAGNOSTIC_LOG_TAIL_BYTES = 256 * 1024;
57
58
  const MAX_DIAGNOSTIC_LOG_TAIL_BYTES = 2 * 1024 * 1024;
@@ -61,7 +62,6 @@ const DIAGNOSTIC_LOG_FILES = Object.freeze({
61
62
  'chat-server': 'chat-server.log',
62
63
  'local-gateway': 'local-gateway.log',
63
64
  'amalgm-mcp': 'amalgm-mcp.log',
64
- 'fs-watcher': 'fs-watcher.log',
65
65
  'port-monitor': 'port-monitor.log',
66
66
  });
67
67
 
@@ -77,10 +77,9 @@ const DIAGNOSTIC_LOG_ALIASES = Object.freeze({
77
77
 
78
78
  const SERVICE_PORTS = {
79
79
  gateway: PORT,
80
- portMonitor: Number.parseInt(process.env.PORT_MONITOR_PORT || '8081', 10),
81
- fsWatcher: Number.parseInt(process.env.FS_WATCHER_PORT || '8082', 10),
82
- mcp: Number.parseInt(process.env.AMALGM_MCP_PORT || '8083', 10),
83
- chat: Number.parseInt(process.env.CHAT_SERVER_PORT || '8084', 10),
80
+ portMonitor: runtimePort('port-monitor'),
81
+ mcp: runtimePort('amalgm-mcp'),
82
+ chat: runtimePort('chat-server'),
84
83
  };
85
84
  const LOCAL_AGENT_APP_PORT = Number.parseInt(process.env.AMALGM_LOCAL_APP_PORT || '3456', 10);
86
85
 
@@ -275,6 +274,14 @@ function readJson(file, fallback = null) {
275
274
  }
276
275
  }
277
276
 
277
+ function runtimeStateOwnedHere(previous) {
278
+ if (!previous || typeof previous !== 'object' || Object.keys(previous).length === 0) return true;
279
+ return previous.pid === process.pid
280
+ || previous.gateway_pid === process.pid
281
+ || previous.pid === process.ppid
282
+ || previous.supervisor_pid === process.ppid;
283
+ }
284
+
278
285
  function ensurePtySpawnHelperExecutable() {
279
286
  try {
280
287
  const nodePtyRoot = path.dirname(require.resolve(`${pty.packageName}/package.json`));
@@ -294,6 +301,10 @@ function ensurePtySpawnHelperExecutable() {
294
301
  function writeRuntimeState(actualPort) {
295
302
  const computer = readJson(path.join(AMALGM_DIR, 'computer.json'), null);
296
303
  const previous = readJson(STATE_FILE, {});
304
+ if (!runtimeStateOwnedHere(previous)) {
305
+ console.warn('[local-gateway] runtime-state belongs to another Amalgm runtime; skipping write');
306
+ return;
307
+ }
297
308
  writeJsonSecret(STATE_FILE, {
298
309
  ...previous,
299
310
  schema_version: 1,
@@ -312,7 +323,6 @@ function writeRuntimeState(actualPort) {
312
323
  ports: {
313
324
  gateway: actualPort,
314
325
  port_monitor: SERVICE_PORTS.portMonitor,
315
- fs_watcher: SERVICE_PORTS.fsWatcher,
316
326
  amalgm_mcp: SERVICE_PORTS.mcp,
317
327
  chat_server: SERVICE_PORTS.chat,
318
328
  },
@@ -550,12 +560,6 @@ function serviceForPath(pathname) {
550
560
  if (MCP_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) {
551
561
  return { port: SERVICE_PORTS.mcp, stripPrefix: '' };
552
562
  }
553
- if (pathname === '/fs-watcher' || pathname.startsWith('/fs-watcher/')) {
554
- return {
555
- port: SERVICE_PORTS.fsWatcher,
556
- stripPrefix: '/fs-watcher',
557
- };
558
- }
559
563
  if (PORT_MONITOR_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) {
560
564
  return {
561
565
  port: SERVICE_PORTS.portMonitor,
@@ -971,10 +975,6 @@ server.on('upgrade', (req, socket, head) => {
971
975
  bridgePtyWebSocket(req, ws, url.pathname);
972
976
  return;
973
977
  }
974
- if (url.pathname === '/ws/fs' || url.pathname === '/fs-watcher/ws') {
975
- proxyWebSocket(req, ws, { port: SERVICE_PORTS.fsWatcher, stripPrefix: '' }, '/');
976
- return;
977
- }
978
978
  const target = serviceForPath(url.pathname);
979
979
  if (!target) {
980
980
  ws.close(1008, 'Unknown local gateway route');
@@ -4,27 +4,26 @@
4
4
  * Polls `ss -tlnp` every 2s and pushes
5
5
  * port_opened / port_closed events over SSE.
6
6
  *
7
- * Runs alongside chat-server on a separate port (8081).
7
+ * Runs alongside chat-server on the port-monitor runtime port.
8
8
  */
9
9
 
10
10
  const http = require('http');
11
11
  const fs = require('fs');
12
12
  const { execSync } = require('child_process');
13
13
  const { authorizeRuntimeHttp } = require('./runtime-auth');
14
+ const { runtimePort } = require('../lib/runtime-manifest');
14
15
 
15
- const PORT = parseInt(process.env.PORT_MONITOR_PORT || '8081', 10);
16
+ const PORT = runtimePort('port-monitor');
16
17
  const BIND_HOST = process.env.AMALGM_BIND_HOST || '0.0.0.0';
17
18
  const AGENT_PORT = parseInt(process.env.PORT || '8080');
18
- const FS_WATCHER_PORT = parseInt(process.env.FS_WATCHER_PORT || '8082', 10);
19
- const AMALGM_MCP_PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
20
- const CHAT_SERVER_PORT = parseInt(process.env.CHAT_SERVER_PORT || '8084', 10);
21
- const AMALGM_GATEWAY_PORT = parseInt(process.env.AMALGM_GATEWAY_PORT || '28781', 10);
19
+ const AMALGM_MCP_PORT = runtimePort('amalgm-mcp');
20
+ const CHAT_SERVER_PORT = runtimePort('chat-server');
21
+ const AMALGM_GATEWAY_PORT = runtimePort('local-gateway');
22
22
  const LOCAL_AGENT_APP_PORT = parseInt(process.env.AMALGM_LOCAL_APP_PORT || '3456', 10);
23
23
  const INTERNAL_SERVICE_PORTS = new Set([
24
24
  AGENT_PORT,
25
25
  LOCAL_AGENT_APP_PORT,
26
26
  PORT,
27
- FS_WATCHER_PORT,
28
27
  AMALGM_MCP_PORT,
29
28
  CHAT_SERVER_PORT,
30
29
  AMALGM_GATEWAY_PORT,
@@ -33,7 +32,7 @@ const INTERNAL_SERVICE_PORTS = new Set([
33
32
  const COMMON_DEV_PORTS = new Set([3000, 3001, 3002, 3003, 4000, 4200, 4321, 5000, 5001, 5173, 5174, 6006, 7000, 8000, 8001, 8888]);
34
33
  const BLOCKED_PREVIEW_PROCESSES = new Set(['.opencode', 'controlce', 'figma_age', 'figma_agent', 'loom', 'opencode', 'rapportd']);
35
34
  const DEV_PREVIEW_PROCESS_RE = /^(node|bun|deno|npm|pnpm|yarn|vite|next|astro|nuxt|svelte|remix|webpack|rspack|parcel|serve|http-server|tsx|python|python3|uvicorn|gunicorn|flask|django|ruby|rails|puma|php|java|go|air|cargo|trunk|dotnet|nginx)$/i;
36
- const INTERNAL_COMMAND_RE = /(?:^|\s)(?:node(?:-[^\s]+)?\s+)?[^\s]*(?:\/|\\)runtime(?:\/|\\)scripts(?:\/|\\)(?:port-monitor|fs-watcher|chat-server|local-gateway|amalgm-mcp(?:\/|\\)index)\.js(?:\s|$)/i;
35
+ const INTERNAL_COMMAND_RE = /(?:^|\s)(?:node(?:-[^\s]+)?\s+)?[^\s]*(?:\/|\\)runtime(?:\/|\\)scripts(?:\/|\\)(?:port-monitor|chat-server|local-gateway|amalgm-mcp(?:\/|\\)index)\.js(?:\s|$)/i;
37
36
  const BLOCKED_COMMAND_RE = /(?:agent-browser|chromium|chrome)(?:\s|$)/i;
38
37
 
39
38
  const knownPorts = new Map();