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.
package/README.md CHANGED
@@ -37,7 +37,6 @@ amalgm run
37
37
  The runtime supervisor keeps the declared local essentials alive:
38
38
 
39
39
  - port monitor on `8081`
40
- - filesystem/auth watcher on `8082`
41
40
  - Amalgm MCP on `8083`
42
41
  - chat server on `8084`
43
42
  - events/previews/artifact tunnel to `wire.events.amalgm.ai`
package/lib/cli.js CHANGED
@@ -179,8 +179,6 @@ const SAFE_DAEMON_ENV_KEYS = [
179
179
  'AMALGM_SKIP_NATIVE_INSTALL',
180
180
  'CHAT_SERVER_PORT',
181
181
  'ELECTRON_RUN_AS_NODE',
182
- 'FS_WATCHER_PORT',
183
- 'FS_WATCHER_ROOT',
184
182
  'HOME',
185
183
  'LANG',
186
184
  'LC_ALL',
@@ -1,131 +1,3 @@
1
1
  'use strict';
2
2
 
3
- const RUNTIME_SERVICES = [
4
- {
5
- name: 'local-gateway',
6
- script: 'scripts/local-gateway.js',
7
- portKey: 'gateway',
8
- envName: 'AMALGM_GATEWAY_PORT',
9
- stateKey: 'gateway',
10
- defaultPort: 28781,
11
- launchOrder: 50,
12
- },
13
- {
14
- name: 'port-monitor',
15
- script: 'scripts/port-monitor.js',
16
- portKey: 'portMonitor',
17
- envName: 'PORT_MONITOR_PORT',
18
- stateKey: 'port_monitor',
19
- defaultPort: 8081,
20
- launchOrder: 10,
21
- },
22
- {
23
- name: 'fs-watcher',
24
- script: 'scripts/fs-watcher.js',
25
- portKey: 'fsWatcher',
26
- envName: 'FS_WATCHER_PORT',
27
- stateKey: 'fs_watcher',
28
- defaultPort: 8082,
29
- launchOrder: 20,
30
- },
31
- {
32
- name: 'amalgm-mcp',
33
- script: 'scripts/amalgm-mcp/index.js',
34
- portKey: 'amalgmMcp',
35
- envName: 'AMALGM_MCP_PORT',
36
- stateKey: 'amalgm_mcp',
37
- defaultPort: 8083,
38
- launchOrder: 40,
39
- },
40
- {
41
- name: 'chat-server',
42
- script: 'scripts/chat-server.js',
43
- portKey: 'chatServer',
44
- envName: 'CHAT_SERVER_PORT',
45
- stateKey: 'chat_server',
46
- defaultPort: 8084,
47
- launchOrder: 30,
48
- },
49
- ];
50
-
51
- function runtimeServices() {
52
- return RUNTIME_SERVICES.map((service) => ({ ...service }));
53
- }
54
-
55
- function runtimeLaunchServices() {
56
- return runtimeServices().sort((left, right) => left.launchOrder - right.launchOrder);
57
- }
58
-
59
- function runtimeServiceNames() {
60
- return RUNTIME_SERVICES.map((service) => service.name);
61
- }
62
-
63
- function runtimeServiceScripts() {
64
- return RUNTIME_SERVICES.map((service) => service.script);
65
- }
66
-
67
- function runtimeServiceByName(name) {
68
- return RUNTIME_SERVICES.find((service) => service.name === name) || null;
69
- }
70
-
71
- function runtimePortsFromState(state) {
72
- const statePorts = state?.ports || {};
73
- const values = {};
74
- for (const service of RUNTIME_SERVICES) {
75
- if (service.stateKey === 'gateway' && Number.isInteger(state?.gateway_port)) {
76
- values[service.name] = Number(state.gateway_port);
77
- continue;
78
- }
79
- const port = Number(statePorts[service.stateKey] || 0);
80
- if (Number.isInteger(port) && port > 0) values[service.name] = port;
81
- }
82
- return values;
83
- }
84
-
85
- function runtimePortsFromEnv(env = process.env) {
86
- const values = {};
87
- for (const service of RUNTIME_SERVICES) {
88
- const port = Number(env[service.envName] || service.defaultPort);
89
- if (Number.isInteger(port) && port > 0) values[service.name] = port;
90
- }
91
- return values;
92
- }
93
-
94
- function runtimePortsForStatus(state, env = process.env) {
95
- const fromState = runtimePortsFromState(state);
96
- const fromEnv = runtimePortsFromEnv(env);
97
- return RUNTIME_SERVICES
98
- .map((service) => [service.name, fromState[service.name] || fromEnv[service.name]])
99
- .filter(([, port]) => Number.isInteger(port) && port > 0);
100
- }
101
-
102
- function runtimePortsState(ports) {
103
- const statePorts = {};
104
- for (const service of RUNTIME_SERVICES) {
105
- const port = Number(ports?.[service.portKey] || 0);
106
- if (!Number.isInteger(port) || port <= 0) continue;
107
- if (service.stateKey !== 'gateway') statePorts[service.stateKey] = port;
108
- }
109
- return {
110
- gateway_port: Number(ports?.gateway || 0) || undefined,
111
- gateway_url: ports?.gateway ? `http://127.0.0.1:${ports.gateway}` : undefined,
112
- ports: statePorts,
113
- services: RUNTIME_SERVICES.map((service) => ({
114
- name: service.name,
115
- port: Number(ports?.[service.portKey] || 0) || null,
116
- health_path: '/healthz',
117
- })),
118
- };
119
- }
120
-
121
- module.exports = {
122
- runtimeLaunchServices,
123
- runtimePortsForStatus,
124
- runtimePortsFromEnv,
125
- runtimePortsFromState,
126
- runtimePortsState,
127
- runtimeServiceByName,
128
- runtimeServiceNames,
129
- runtimeServiceScripts,
130
- runtimeServices,
131
- };
3
+ module.exports = require('../runtime/lib/runtime-manifest');
package/lib/service.js CHANGED
@@ -39,7 +39,6 @@ const SERVICE_ENV_KEYS = [
39
39
  'AMALGM_WORKSPACES_DIR',
40
40
  'CHAT_SERVER_PORT',
41
41
  'ELECTRON_RUN_AS_NODE',
42
- 'FS_WATCHER_PORT',
43
42
  'LANG',
44
43
  'LC_ALL',
45
44
  'NODE_PATH',
package/lib/supervisor.js CHANGED
@@ -224,8 +224,6 @@ function baseRuntimeEnv(record, ports, options = {}) {
224
224
  CHAT_SERVER_PORT: String(ports.chatServer),
225
225
  AMALGM_MCP_PORT: String(ports.amalgmMcp),
226
226
  PORT_MONITOR_PORT: String(ports.portMonitor),
227
- FS_WATCHER_PORT: String(ports.fsWatcher),
228
- FS_WATCHER_ROOT: process.env.FS_WATCHER_ROOT || defaultCwd,
229
227
  AMALGM_RUNTIME_VERSION: require('../package.json').version,
230
228
  };
231
229
 
@@ -628,7 +626,6 @@ async function startSupervisor(options = {}) {
628
626
  AMALGM_GATEWAY_PORT: String(ports.gateway),
629
627
  AMALGM_MCP_PORT: String(ports.amalgmMcp),
630
628
  CHAT_SERVER_PORT: String(ports.chatServer),
631
- FS_WATCHER_PORT: String(ports.fsWatcher),
632
629
  PORT_MONITOR_PORT: String(ports.portMonitor),
633
630
  });
634
631
  updateRuntimeState(runtimePortsState(ports));
@@ -5,13 +5,17 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const { WebSocket } = require('ws');
7
7
  const { RUNTIME_STATE_FILE } = require('./paths');
8
+ const { runtimePort, runtimeServiceByName } = require('./runtime-manifest');
8
9
 
9
- const DEFAULT_TARGET_PORT = 8084;
10
+ const DEFAULT_TARGET_PORT = runtimeServiceByName('chat-server').defaultPort;
10
11
  const DEFAULT_CHAT_TUNNEL_URL = 'wss://amalgm-chat-gateway.fly.dev/wire';
11
12
  const CONNECT_TIMEOUT_MS = 20_000;
12
13
  const WATCHDOG_INTERVAL_MS = 15_000;
13
14
  const HEARTBEAT_TIMEOUT_MS = 75_000;
14
- const ALLOWED_TARGET_PORTS = new Set([8083, 8084]);
15
+ const ALLOWED_TARGET_PORTS = new Set([
16
+ runtimeServiceByName('amalgm-mcp').defaultPort,
17
+ runtimeServiceByName('chat-server').defaultPort,
18
+ ]);
15
19
  const HOP_BY_HOP_HEADERS = new Set([
16
20
  'connection',
17
21
  'keep-alive',
@@ -62,22 +66,18 @@ function responseHeaders(headers = {}) {
62
66
  }
63
67
 
64
68
  function readRuntimePorts() {
65
- const envPort = (name) => {
66
- const port = Number(process.env[name]);
67
- return Number.isInteger(port) && port > 0 && port <= 65535 ? port : undefined;
68
- };
69
69
  try {
70
70
  const parsed = JSON.parse(fs.readFileSync(RUNTIME_STATE_FILE, 'utf8'));
71
71
  return {
72
- gateway: Number(parsed?.gateway_port || parsed?.ports?.gateway) || envPort('AMALGM_GATEWAY_PORT'),
73
- mcp: Number(parsed?.ports?.amalgm_mcp) || envPort('AMALGM_MCP_PORT'),
74
- chat: Number(parsed?.ports?.chat_server) || envPort('CHAT_SERVER_PORT'),
72
+ gateway: Number(parsed?.gateway_port || parsed?.ports?.gateway) || runtimePort('local-gateway'),
73
+ mcp: Number(parsed?.ports?.amalgm_mcp) || runtimePort('amalgm-mcp'),
74
+ chat: Number(parsed?.ports?.chat_server) || runtimePort('chat-server'),
75
75
  };
76
76
  } catch {
77
77
  return {
78
- gateway: envPort('AMALGM_GATEWAY_PORT'),
79
- mcp: envPort('AMALGM_MCP_PORT'),
80
- chat: envPort('CHAT_SERVER_PORT'),
78
+ gateway: runtimePort('local-gateway'),
79
+ mcp: runtimePort('amalgm-mcp'),
80
+ chat: runtimePort('chat-server'),
81
81
  };
82
82
  }
83
83
  }
@@ -7,12 +7,16 @@ const path = require('path');
7
7
  const { WebSocket } = require('ws');
8
8
 
9
9
  const { AMALGM_DIR, RUNTIME_STATE_FILE } = require('./paths');
10
+ const { runtimePort, runtimeServiceByName } = require('./runtime-manifest');
10
11
 
11
- const DEFAULT_TARGET_PORT = 8083;
12
+ const DEFAULT_TARGET_PORT = runtimeServiceByName('amalgm-mcp').defaultPort;
12
13
  const CONNECT_TIMEOUT_MS = 20_000;
13
14
  const WATCHDOG_INTERVAL_MS = 15_000;
14
15
  const HEARTBEAT_TIMEOUT_MS = 75_000;
15
- const CORE_TARGET_PORTS = new Set([8083, 8084]);
16
+ const CORE_TARGET_PORTS = new Set([
17
+ runtimeServiceByName('amalgm-mcp').defaultPort,
18
+ runtimeServiceByName('chat-server').defaultPort,
19
+ ]);
16
20
  const HOP_BY_HOP_HEADERS = new Set([
17
21
  'connection',
18
22
  'keep-alive',
@@ -149,22 +153,18 @@ function readArtifactRoutes() {
149
153
  }
150
154
 
151
155
  function readRuntimePorts() {
152
- const envPort = (name) => {
153
- const port = Number(process.env[name]);
154
- return Number.isInteger(port) && port > 0 && port <= 65535 ? port : undefined;
155
- };
156
156
  try {
157
157
  const parsed = JSON.parse(fs.readFileSync(RUNTIME_STATE_FILE, 'utf8'));
158
158
  return {
159
- gateway: Number(parsed?.gateway_port || parsed?.ports?.gateway) || envPort('AMALGM_GATEWAY_PORT'),
160
- mcp: Number(parsed?.ports?.amalgm_mcp) || envPort('AMALGM_MCP_PORT'),
161
- chat: Number(parsed?.ports?.chat_server) || envPort('CHAT_SERVER_PORT'),
159
+ gateway: Number(parsed?.gateway_port || parsed?.ports?.gateway) || runtimePort('local-gateway'),
160
+ mcp: Number(parsed?.ports?.amalgm_mcp) || runtimePort('amalgm-mcp'),
161
+ chat: Number(parsed?.ports?.chat_server) || runtimePort('chat-server'),
162
162
  };
163
163
  } catch {
164
164
  return {
165
- gateway: envPort('AMALGM_GATEWAY_PORT'),
166
- mcp: envPort('AMALGM_MCP_PORT'),
167
- chat: envPort('CHAT_SERVER_PORT'),
165
+ gateway: runtimePort('local-gateway'),
166
+ mcp: runtimePort('amalgm-mcp'),
167
+ chat: runtimePort('chat-server'),
168
168
  };
169
169
  }
170
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.44",
3
+ "version": "0.1.47",
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/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/fs-watcher.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-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"
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ const RUNTIME_SERVICES = [
4
+ {
5
+ name: 'local-gateway',
6
+ script: 'scripts/local-gateway.js',
7
+ portKey: 'gateway',
8
+ envName: 'AMALGM_GATEWAY_PORT',
9
+ stateKey: 'gateway',
10
+ defaultPort: 28781,
11
+ launchOrder: 50,
12
+ },
13
+ {
14
+ name: 'port-monitor',
15
+ script: 'scripts/port-monitor.js',
16
+ portKey: 'portMonitor',
17
+ envName: 'PORT_MONITOR_PORT',
18
+ stateKey: 'port_monitor',
19
+ defaultPort: 8081,
20
+ launchOrder: 10,
21
+ },
22
+ {
23
+ name: 'amalgm-mcp',
24
+ script: 'scripts/amalgm-mcp/index.js',
25
+ portKey: 'amalgmMcp',
26
+ envName: 'AMALGM_MCP_PORT',
27
+ stateKey: 'amalgm_mcp',
28
+ defaultPort: 8083,
29
+ launchOrder: 40,
30
+ },
31
+ {
32
+ name: 'chat-server',
33
+ script: 'scripts/chat-server.js',
34
+ portKey: 'chatServer',
35
+ envName: 'CHAT_SERVER_PORT',
36
+ stateKey: 'chat_server',
37
+ defaultPort: 8084,
38
+ launchOrder: 30,
39
+ },
40
+ ];
41
+
42
+ function cloneService(service) {
43
+ return { ...service };
44
+ }
45
+
46
+ function serviceByName(name) {
47
+ return RUNTIME_SERVICES.find((service) => service.name === name) || null;
48
+ }
49
+
50
+ function parsePort(value, fallback = null) {
51
+ const port = Number.parseInt(String(value ?? ''), 10);
52
+ if (Number.isInteger(port) && port > 0 && port <= 65535) return port;
53
+ return fallback;
54
+ }
55
+
56
+ function runtimeServices() {
57
+ return RUNTIME_SERVICES.map(cloneService);
58
+ }
59
+
60
+ function runtimeLaunchServices() {
61
+ return runtimeServices().sort((left, right) => left.launchOrder - right.launchOrder);
62
+ }
63
+
64
+ function runtimeServiceNames() {
65
+ return RUNTIME_SERVICES.map((service) => service.name);
66
+ }
67
+
68
+ function runtimeServiceScripts() {
69
+ return RUNTIME_SERVICES.map((service) => service.script);
70
+ }
71
+
72
+ function runtimeServiceByName(name) {
73
+ const service = serviceByName(name);
74
+ return service ? cloneService(service) : null;
75
+ }
76
+
77
+ function runtimePort(name, env = process.env) {
78
+ const service = serviceByName(name);
79
+ if (!service) return null;
80
+ return parsePort(env?.[service.envName], service.defaultPort);
81
+ }
82
+
83
+ function runtimePortByKey(portKey, env = process.env) {
84
+ const service = RUNTIME_SERVICES.find((candidate) => candidate.portKey === portKey);
85
+ return service ? runtimePort(service.name, env) : null;
86
+ }
87
+
88
+ function runtimePorts(env = process.env) {
89
+ const values = {};
90
+ for (const service of RUNTIME_SERVICES) {
91
+ values[service.portKey] = runtimePort(service.name, env);
92
+ }
93
+ return values;
94
+ }
95
+
96
+ function runtimePortsFromState(state) {
97
+ const statePorts = state?.ports || {};
98
+ const values = {};
99
+ for (const service of RUNTIME_SERVICES) {
100
+ if (service.stateKey === 'gateway' && parsePort(state?.gateway_port)) {
101
+ values[service.name] = parsePort(state.gateway_port);
102
+ continue;
103
+ }
104
+ const port = parsePort(statePorts[service.stateKey]);
105
+ if (port) values[service.name] = port;
106
+ }
107
+ return values;
108
+ }
109
+
110
+ function runtimePortsFromEnv(env = process.env) {
111
+ const values = {};
112
+ for (const service of RUNTIME_SERVICES) {
113
+ values[service.name] = runtimePort(service.name, env);
114
+ }
115
+ return values;
116
+ }
117
+
118
+ function runtimePortsForStatus(state, env = process.env) {
119
+ const fromState = runtimePortsFromState(state);
120
+ const fromEnv = runtimePortsFromEnv(env);
121
+ return RUNTIME_SERVICES
122
+ .map((service) => [service.name, fromState[service.name] || fromEnv[service.name]])
123
+ .filter(([, port]) => Number.isInteger(port) && port > 0);
124
+ }
125
+
126
+ function runtimePortsState(ports) {
127
+ const statePorts = {};
128
+ for (const service of RUNTIME_SERVICES) {
129
+ const port = parsePort(ports?.[service.portKey]);
130
+ if (!port) continue;
131
+ if (service.stateKey !== 'gateway') statePorts[service.stateKey] = port;
132
+ }
133
+ return {
134
+ gateway_port: parsePort(ports?.gateway) || undefined,
135
+ gateway_url: ports?.gateway ? `http://127.0.0.1:${ports.gateway}` : undefined,
136
+ ports: statePorts,
137
+ services: RUNTIME_SERVICES.map((service) => ({
138
+ name: service.name,
139
+ port: parsePort(ports?.[service.portKey]),
140
+ health_path: '/healthz',
141
+ })),
142
+ };
143
+ }
144
+
145
+ module.exports = {
146
+ parsePort,
147
+ runtimeLaunchServices,
148
+ runtimePort,
149
+ runtimePortByKey,
150
+ runtimePorts,
151
+ runtimePortsForStatus,
152
+ runtimePortsFromEnv,
153
+ runtimePortsFromState,
154
+ runtimePortsState,
155
+ runtimeServiceByName,
156
+ runtimeServiceNames,
157
+ runtimeServiceScripts,
158
+ runtimeServices,
159
+ };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const PHASE_BY_NATIVE_EVENT = {
8
+ UserPromptSubmit: 'userSubmit',
9
+ UserSubmit: 'userSubmit',
10
+ userSubmit: 'userSubmit',
11
+ Stop: 'responseComplete',
12
+ ResponseComplete: 'responseComplete',
13
+ responseComplete: 'responseComplete',
14
+ };
15
+
16
+ function readJson(filePath) {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ function compactCommand(command) {
25
+ const trimmed = String(command || '').trim();
26
+ if (!trimmed) return '';
27
+ const home = os.homedir();
28
+ return home ? trimmed.replaceAll(home, '~') : trimmed;
29
+ }
30
+
31
+ function commandLabel(command) {
32
+ const compact = compactCommand(command);
33
+ if (!compact) return 'Hook';
34
+ const parts = compact.split(/\s+/);
35
+ const script = parts.find((part) => /[./][^ ]+\.(?:js|mjs|cjs|sh|py|ts)$/i.test(part));
36
+ return script ? path.basename(script) : compact;
37
+ }
38
+
39
+ function appendHook(acc, sourcePath, harnessId, event, command, matcher, options = {}) {
40
+ const cleanCommand = String(command || '').trim();
41
+ if (!cleanCommand) return;
42
+ const phase = PHASE_BY_NATIVE_EVENT[event] || event;
43
+ const timeout = Number(options.timeout);
44
+ acc.push({
45
+ id: `${harnessId}:${sourcePath}:${event}:${acc.length}`,
46
+ harnessId,
47
+ source: 'native',
48
+ sourcePath,
49
+ event,
50
+ phase,
51
+ type: typeof options.type === 'string' && options.type.trim() ? options.type.trim() : 'command',
52
+ command: cleanCommand,
53
+ label: commandLabel(cleanCommand),
54
+ matcher: typeof matcher === 'string' ? matcher : '',
55
+ ...(Number.isFinite(timeout) ? { timeout } : {}),
56
+ ...(typeof options.statusMessage === 'string' && options.statusMessage.trim()
57
+ ? { statusMessage: options.statusMessage.trim() }
58
+ : {}),
59
+ enabled: true,
60
+ });
61
+ }
62
+
63
+ function isHookEventName(event) {
64
+ const clean = String(event || '').trim();
65
+ return Boolean(
66
+ PHASE_BY_NATIVE_EVENT[clean]
67
+ || /(?:prompt|submit|response|complete|stop|hook)/i.test(clean),
68
+ );
69
+ }
70
+
71
+ function hasCommandShape(value) {
72
+ return !!value
73
+ && typeof value === 'object'
74
+ && !Array.isArray(value)
75
+ && (
76
+ typeof value.command === 'string'
77
+ || typeof value.event === 'string'
78
+ || Array.isArray(value.hooks)
79
+ );
80
+ }
81
+
82
+ function extractCommands(value, context, acc) {
83
+ if (typeof value === 'string') {
84
+ if (!isHookEventName(context.event)) return;
85
+ appendHook(acc, context.sourcePath, context.harnessId, context.event, value, context.matcher);
86
+ return;
87
+ }
88
+
89
+ if (Array.isArray(value)) {
90
+ for (const item of value) extractCommands(item, context, acc);
91
+ return;
92
+ }
93
+
94
+ if (!value || typeof value !== 'object') return;
95
+
96
+ const matcher = typeof value.matcher === 'string' ? value.matcher : context.matcher;
97
+ const event = typeof value.event === 'string' ? value.event : context.event;
98
+ if (typeof value.command === 'string') {
99
+ appendHook(acc, context.sourcePath, context.harnessId, event, value.command, matcher, value);
100
+ }
101
+ if (Array.isArray(value.hooks)) {
102
+ for (const hook of value.hooks) extractCommands(hook, { ...context, event, matcher }, acc);
103
+ }
104
+ }
105
+
106
+ function hooksFromObject(rawHooks, context) {
107
+ const hooks = [];
108
+ if (Array.isArray(rawHooks)) {
109
+ extractCommands(rawHooks, context, hooks);
110
+ return hooks;
111
+ }
112
+ if (!rawHooks || typeof rawHooks !== 'object') return hooks;
113
+
114
+ if (Array.isArray(rawHooks.hooks)) {
115
+ extractCommands(rawHooks.hooks, context, hooks);
116
+ } else if (rawHooks.hooks && typeof rawHooks.hooks === 'object') {
117
+ hooks.push(...hooksFromObject(rawHooks.hooks, context));
118
+ }
119
+
120
+ for (const [event, value] of Object.entries(rawHooks)) {
121
+ if (event === 'hooks') continue;
122
+ if (!isHookEventName(event) && !hasCommandShape(value) && !Array.isArray(value)) continue;
123
+ extractCommands(value, { ...context, event }, hooks);
124
+ }
125
+ return hooks;
126
+ }
127
+
128
+ function collectCodexHooks(homeDir) {
129
+ const sourcePath = path.join(homeDir, '.codex', 'hooks.json');
130
+ const data = readJson(sourcePath);
131
+ const hooks = hooksFromObject(data?.hooks ?? data, {
132
+ harnessId: 'codex',
133
+ sourcePath,
134
+ event: '',
135
+ matcher: '',
136
+ });
137
+ return {
138
+ harnessId: 'codex',
139
+ sources: fs.existsSync(sourcePath) ? [sourcePath] : [],
140
+ hooks,
141
+ };
142
+ }
143
+
144
+ function collectClaudeHooks(homeDir) {
145
+ const sourcePaths = [
146
+ path.join(homeDir, '.claude', 'settings.json'),
147
+ path.join(homeDir, '.claude', 'settings.local.json'),
148
+ path.join(homeDir, '.claude.json'),
149
+ ];
150
+ const hooks = [];
151
+ const sources = [];
152
+
153
+ for (const sourcePath of sourcePaths) {
154
+ const data = readJson(sourcePath);
155
+ if (!data) continue;
156
+ sources.push(sourcePath);
157
+ hooks.push(...hooksFromObject(data.hooks, {
158
+ harnessId: 'claude_code',
159
+ sourcePath,
160
+ event: '',
161
+ matcher: '',
162
+ }));
163
+ }
164
+
165
+ return {
166
+ harnessId: 'claude_code',
167
+ sources,
168
+ hooks,
169
+ };
170
+ }
171
+
172
+ function collectNativeHooks() {
173
+ const homeDir = os.homedir();
174
+ return {
175
+ codex: collectCodexHooks(homeDir),
176
+ claude_code: collectClaudeHooks(homeDir),
177
+ };
178
+ }
179
+
180
+ module.exports = {
181
+ collectNativeHooks,
182
+ };
@@ -9,6 +9,7 @@ const {
9
9
  resolveAgent,
10
10
  updateAgent,
11
11
  } = require('./store');
12
+ const { hydrateModelPreferences } = require('../lib/prefs');
12
13
  const credentialAdapter = require('../../credential-adapter');
13
14
 
14
15
  function coerceAuthMethodForHarness(harnessId, authMethod) {
@@ -50,6 +51,7 @@ function normalizeToolConfig(tools, legacyMcp) {
50
51
  }
51
52
 
52
53
  async function handleList(sendJson) {
54
+ await hydrateModelPreferences();
53
55
  sendJson(200, { agents: getAllAgentsWithBuiltins() });
54
56
  }
55
57
 
@@ -112,8 +114,11 @@ async function handleUpdate(body, sendJson) {
112
114
  const existing = resolveAgent(agent_id);
113
115
  if (!existing) return sendJson(404, { error: `Agent not found: ${agent_id}` });
114
116
 
117
+ const targetHarnessId = updates.baseHarnessId || existing.baseHarnessId;
115
118
  if (updates.authMethod !== undefined) {
116
- updates.authMethod = coerceAuthMethodForHarness(existing.baseHarnessId, updates.authMethod);
119
+ updates.authMethod = coerceAuthMethodForHarness(targetHarnessId, updates.authMethod);
120
+ } else if (updates.baseHarnessId !== undefined) {
121
+ updates.authMethod = coerceAuthMethodForHarness(targetHarnessId, existing.authMethod);
117
122
  }
118
123
 
119
124
  let agent;