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 +0 -1
- package/lib/cli.js +0 -2
- package/lib/runtime-manifest.js +1 -129
- package/lib/service.js +0 -1
- package/lib/supervisor.js +0 -3
- package/lib/tunnel-chat.js +12 -12
- package/lib/tunnel-events.js +12 -12
- package/package.json +2 -2
- package/runtime/lib/runtime-manifest.js +159 -0
- package/runtime/scripts/amalgm-mcp/agents/hooks.js +182 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +6 -1
- package/runtime/scripts/amalgm-mcp/agents/store.js +61 -31
- package/runtime/scripts/amalgm-mcp/agents/talk.js +12 -22
- package/runtime/scripts/amalgm-mcp/agents/tools.js +8 -13
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +3 -2
- package/runtime/scripts/amalgm-mcp/config.js +3 -2
- package/runtime/scripts/amalgm-mcp/index.js +2 -2
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +12 -48
- package/runtime/scripts/chat-core/adapters/claude.js +3 -1
- package/runtime/scripts/chat-core/adapters/codex.js +173 -29
- package/runtime/scripts/chat-core/auth.js +1 -1
- package/runtime/scripts/chat-core/contract.js +2 -1
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +3 -3
- package/runtime/scripts/chat-core/tooling/native-config.js +133 -0
- package/runtime/scripts/chat-server/config.js +2 -1
- package/runtime/scripts/local-gateway.js +17 -17
- package/runtime/scripts/port-monitor.js +7 -8
- package/runtime/scripts/fs-watcher.js +0 -923
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
package/lib/runtime-manifest.js
CHANGED
|
@@ -1,131 +1,3 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
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
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));
|
package/lib/tunnel-chat.js
CHANGED
|
@@ -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 =
|
|
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([
|
|
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) ||
|
|
73
|
-
mcp: Number(parsed?.ports?.amalgm_mcp) ||
|
|
74
|
-
chat: Number(parsed?.ports?.chat_server) ||
|
|
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:
|
|
79
|
-
mcp:
|
|
80
|
-
chat:
|
|
78
|
+
gateway: runtimePort('local-gateway'),
|
|
79
|
+
mcp: runtimePort('amalgm-mcp'),
|
|
80
|
+
chat: runtimePort('chat-server'),
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
}
|
package/lib/tunnel-events.js
CHANGED
|
@@ -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 =
|
|
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([
|
|
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) ||
|
|
160
|
-
mcp: Number(parsed?.ports?.amalgm_mcp) ||
|
|
161
|
-
chat: Number(parsed?.ports?.chat_server) ||
|
|
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:
|
|
166
|
-
mcp:
|
|
167
|
-
chat:
|
|
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.
|
|
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/
|
|
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(
|
|
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;
|