@parall/openclaw-agent 1.28.0

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.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ // Bootstrap wrapper for daemon-mode OpenClaw agents.
3
+ // Sets up a per-agent OpenClaw state dir, writes openclaw.json with the
4
+ // agent's credentials, installs the bundled plugin, then execs
5
+ // `openclaw gateway run`. The daemon supervisor spawns one of these per
6
+ // attached OpenClaw agent.
7
+ import { execFileSync, spawn } from "node:child_process";
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ // ---------------------------------------------------------------------------
11
+ // 1. Read + validate ENV
12
+ // ---------------------------------------------------------------------------
13
+ const PRLL_API_URL = env("PRLL_API_URL");
14
+ const PRLL_API_KEY = env("PRLL_API_KEY");
15
+ const PRLL_ORG_ID = env("PRLL_ORG_ID");
16
+ const stateDir = env("PRLL_OPENCLAW_STATE_DIR");
17
+ const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || "";
18
+ const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || "";
19
+ const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || "0";
20
+ const pluginArchive = process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim()
21
+ || "/opt/parall-plugin/parall-plugin.tgz";
22
+ function env(name) {
23
+ const v = process.env[name]?.trim();
24
+ if (!v) {
25
+ console.error(`ERROR: Missing required environment variable: ${name}`);
26
+ process.exit(1);
27
+ }
28
+ return v;
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // 2. Create per-agent state directory
32
+ // ---------------------------------------------------------------------------
33
+ const openclawStateDir = path.join(stateDir, ".openclaw");
34
+ const configPath = path.join(openclawStateDir, "openclaw.json");
35
+ fs.mkdirSync(path.join(openclawStateDir, "sessions"), { recursive: true });
36
+ fs.mkdirSync(path.join(openclawStateDir, "workspace"), { recursive: true });
37
+ // ---------------------------------------------------------------------------
38
+ // 3. Install plugin from bundled archive
39
+ // ---------------------------------------------------------------------------
40
+ if (fs.existsSync(pluginArchive)) {
41
+ // Clean legacy extension dir before install (only when we have an archive
42
+ // to replace it — otherwise the existing install is the only copy).
43
+ const legacyExtDir = path.join(openclawStateDir, "extensions", "parall");
44
+ fs.rmSync(legacyExtDir, { recursive: true, force: true });
45
+ console.log(`Installing Parall plugin from ${pluginArchive}...`);
46
+ try {
47
+ execFileSync("openclaw", [
48
+ "plugins", "install", pluginArchive,
49
+ "--force", "--dangerously-force-unsafe-install",
50
+ ], {
51
+ env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
52
+ stdio: "inherit",
53
+ timeout: 60_000,
54
+ });
55
+ }
56
+ catch (err) {
57
+ console.error(`ERROR: Failed to install Parall plugin: ${String(err)}`);
58
+ process.exit(1);
59
+ }
60
+ }
61
+ else {
62
+ console.warn(`Plugin archive not found at ${pluginArchive} — assuming plugin is already installed.`);
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // 4. Write openclaw.json
66
+ // ---------------------------------------------------------------------------
67
+ writeOpenclawConfig();
68
+ function writeOpenclawConfig() {
69
+ let cfg = {};
70
+ try {
71
+ cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
72
+ }
73
+ catch { /* fresh config */ }
74
+ const gateway = (cfg.gateway && typeof cfg.gateway === "object") ? cfg.gateway : {};
75
+ gateway.mode = "local";
76
+ cfg.gateway = gateway;
77
+ const channels = (cfg.channels && typeof cfg.channels === "object") ? cfg.channels : {};
78
+ const parallChannel = {
79
+ parall_url: PRLL_API_URL,
80
+ api_key: PRLL_API_KEY,
81
+ org_id: PRLL_ORG_ID,
82
+ };
83
+ if (PRLL_WS_URL)
84
+ parallChannel.ws_url = PRLL_WS_URL;
85
+ channels.parall = parallChannel;
86
+ cfg.channels = channels;
87
+ const plugins = (cfg.plugins && typeof cfg.plugins === "object") ? cfg.plugins : {};
88
+ const entries = (plugins.entries && typeof plugins.entries === "object") ? plugins.entries : {};
89
+ const parallPluginConfig = {
90
+ parall_url: PRLL_API_URL,
91
+ api_key: PRLL_API_KEY,
92
+ org_id: PRLL_ORG_ID,
93
+ };
94
+ if (PRLL_WS_URL)
95
+ parallPluginConfig.ws_url = PRLL_WS_URL;
96
+ const existingParall = (entries.parall && typeof entries.parall === "object") ? entries.parall : {};
97
+ entries.parall = {
98
+ ...existingParall,
99
+ enabled: true,
100
+ hooks: { allowPromptInjection: true, allowConversationAccess: true },
101
+ config: parallPluginConfig,
102
+ };
103
+ plugins.entries = entries;
104
+ cfg.plugins = plugins;
105
+ // sqlite-vec vector index guard
106
+ const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents : {};
107
+ const defaults = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults : {};
108
+ const ms = (defaults.memorySearch && typeof defaults.memorySearch === "object") ? defaults.memorySearch : {};
109
+ const store = (ms.store && typeof ms.store === "object") ? ms.store : {};
110
+ const vector = (store.vector && typeof store.vector === "object") ? store.vector : {};
111
+ if (vector.enabled === undefined)
112
+ vector.enabled = true;
113
+ store.vector = vector;
114
+ ms.store = store;
115
+ defaults.memorySearch = ms;
116
+ agents.defaults = defaults;
117
+ cfg.agents = agents;
118
+ // Seed tools.alsoAllow
119
+ const tools = (cfg.tools && typeof cfg.tools === "object") ? cfg.tools : {};
120
+ const alsoAllow = new Set(Array.isArray(tools.alsoAllow) ? tools.alsoAllow : []);
121
+ alsoAllow.add("group:plugins");
122
+ tools.alsoAllow = Array.from(alsoAllow);
123
+ cfg.tools = tools;
124
+ const tmp = configPath + ".tmp";
125
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
126
+ fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
127
+ fs.renameSync(tmp, configPath);
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // 5. Pre-seed platform config (model + provider) before gateway reads it
131
+ // ---------------------------------------------------------------------------
132
+ // On first boot openclaw.json has no agents.defaults.model. Without this,
133
+ // OpenClaw uses its built-in default which may lack credentials through the
134
+ // Parall proxy. The plugin refreshes this during startAccount but OpenClaw's
135
+ // hot reload only reloads the model catalog, not the active model.
136
+ await preseedPlatformConfig();
137
+ async function preseedPlatformConfig() {
138
+ try {
139
+ const headers = { Authorization: `Bearer ${PRLL_API_KEY}` };
140
+ if (PRLL_SWIMLANE_NAME)
141
+ headers["X-Prll-Swimlane"] = PRLL_SWIMLANE_NAME;
142
+ const resp = await fetch(`${PRLL_API_URL}/api/v1/agents/platform-config`, {
143
+ headers,
144
+ signal: AbortSignal.timeout(15_000),
145
+ });
146
+ if (!resp.ok)
147
+ throw new Error(`platform-config ${resp.status}`);
148
+ const data = (await resp.json());
149
+ const pc = (data.config ?? data);
150
+ let cfg = {};
151
+ try {
152
+ cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
153
+ }
154
+ catch { /* fresh */ }
155
+ const ALLOWED_DEFAULTS = new Set(["model", "compaction", "memorySearch"]);
156
+ const platformDefaults = pc.agents?.defaults;
157
+ if (platformDefaults && typeof platformDefaults === "object") {
158
+ const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents : {};
159
+ const existing = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults : {};
160
+ for (const [k, v] of Object.entries(platformDefaults)) {
161
+ if (ALLOWED_DEFAULTS.has(k))
162
+ existing[k] = v;
163
+ }
164
+ agents.defaults = existing;
165
+ cfg.agents = agents;
166
+ }
167
+ const ALLOWED_MODEL_KEYS = new Set(["id", "name", "contextWindow", "maxTokens"]);
168
+ const platformModels = pc.models?.providers;
169
+ const platformParall = platformModels?.parall;
170
+ if (platformParall && typeof platformParall === "object") {
171
+ const models = (cfg.models && typeof cfg.models === "object") ? cfg.models : {};
172
+ const providers = (models.providers && typeof models.providers === "object") ? models.providers : {};
173
+ const existingParall = (providers.parall && typeof providers.parall === "object") ? providers.parall : {};
174
+ const merged = { ...existingParall, ...platformParall };
175
+ if (Array.isArray(merged.models)) {
176
+ merged.models = merged.models
177
+ .filter((m) => m && typeof m === "object")
178
+ .map((m) => {
179
+ const clean = {};
180
+ for (const [k, v] of Object.entries(m)) {
181
+ if (ALLOWED_MODEL_KEYS.has(k))
182
+ clean[k] = v;
183
+ }
184
+ return clean;
185
+ });
186
+ }
187
+ merged.apiKey = PRLL_API_KEY;
188
+ providers.parall = merged;
189
+ models.providers = providers;
190
+ cfg.models = models;
191
+ }
192
+ const tmp = configPath + ".tmp";
193
+ fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
194
+ fs.renameSync(tmp, configPath);
195
+ const model = cfg.agents?.defaults?.model ?? "none";
196
+ console.log(`Platform config pre-seeded (model: ${String(model)}).`);
197
+ }
198
+ catch (err) {
199
+ console.warn(`Platform config pre-seed skipped: ${String(err)}`);
200
+ }
201
+ }
202
+ // ---------------------------------------------------------------------------
203
+ // 6. openclaw doctor --fix (non-fatal)
204
+ // ---------------------------------------------------------------------------
205
+ try {
206
+ execFileSync("openclaw", ["doctor", "--fix"], {
207
+ env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
208
+ stdio: "inherit",
209
+ timeout: 30_000,
210
+ });
211
+ }
212
+ catch { /* non-fatal */ }
213
+ // ---------------------------------------------------------------------------
214
+ // 7. Spawn openclaw gateway run with signal forwarding
215
+ // ---------------------------------------------------------------------------
216
+ console.log("Starting OpenClaw gateway...");
217
+ const workspaceDir = process.env.PRLL_OPENCLAW_WORKSPACE_DIR?.trim() || "";
218
+ const gatewayEnv = {
219
+ ...process.env,
220
+ OPENCLAW_STATE_DIR: openclawStateDir,
221
+ };
222
+ if (workspaceDir) {
223
+ gatewayEnv.PRLL_WIKI_MOUNT_ROOT = workspaceDir;
224
+ }
225
+ if (PRLL_SWIMLANE_NAME) {
226
+ gatewayEnv.PRLL_SWIMLANE_NAME = PRLL_SWIMLANE_NAME;
227
+ }
228
+ const gatewayArgs = ["gateway", "run"];
229
+ if (gatewayPort !== "0") {
230
+ gatewayArgs.push("--port", gatewayPort);
231
+ gatewayEnv.OPENCLAW_GATEWAY_PORT = gatewayPort;
232
+ }
233
+ const cwd = workspaceDir || path.join(openclawStateDir, "workspace");
234
+ fs.mkdirSync(cwd, { recursive: true });
235
+ const child = spawn("openclaw", gatewayArgs, {
236
+ env: gatewayEnv,
237
+ cwd,
238
+ stdio: "inherit",
239
+ detached: false,
240
+ });
241
+ function forwardSignal(sig) {
242
+ try {
243
+ child.kill(sig);
244
+ }
245
+ catch { /* already gone */ }
246
+ }
247
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
248
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
249
+ child.on("close", (code, signal) => {
250
+ if (signal === "SIGTERM")
251
+ process.exit(143);
252
+ if (signal === "SIGINT")
253
+ process.exit(130);
254
+ process.exit(code ?? 1);
255
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@parall/openclaw-agent",
3
+ "version": "1.28.0",
4
+ "description": "OpenClaw bootstrap wrapper for daemon-mode Parall agents — sets up per-agent OpenClaw state and execs openclaw gateway run",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/parall-hq/parall-mono",
9
+ "directory": "ts/openclaw-agent"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "bin": {
15
+ "parall-openclaw-agent": "./dist/index.js"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "dependencies": {},
28
+ "devDependencies": {
29
+ "@types/node": "^22.0.0",
30
+ "typescript": "^5.7.0"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -b",
34
+ "start": "node dist/index.js"
35
+ }
36
+ }
package/src/index.ts ADDED
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Bootstrap wrapper for daemon-mode OpenClaw agents.
4
+ // Sets up a per-agent OpenClaw state dir, writes openclaw.json with the
5
+ // agent's credentials, installs the bundled plugin, then execs
6
+ // `openclaw gateway run`. The daemon supervisor spawns one of these per
7
+ // attached OpenClaw agent.
8
+
9
+ import { execFileSync, spawn } from "node:child_process";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // 1. Read + validate ENV
15
+ // ---------------------------------------------------------------------------
16
+
17
+ const PRLL_API_URL = env("PRLL_API_URL");
18
+ const PRLL_API_KEY = env("PRLL_API_KEY");
19
+ const PRLL_ORG_ID = env("PRLL_ORG_ID");
20
+ const stateDir = env("PRLL_OPENCLAW_STATE_DIR");
21
+
22
+ const PRLL_WS_URL = process.env.PRLL_WS_URL?.trim() || "";
23
+ const PRLL_SWIMLANE_NAME = process.env.PRLL_SWIMLANE_NAME?.trim() || "";
24
+ const gatewayPort = process.env.OPENCLAW_GATEWAY_PORT?.trim() || "0";
25
+ const pluginArchive = process.env.PRLL_OPENCLAW_PLUGIN_ARCHIVE?.trim()
26
+ || "/opt/parall-plugin/parall-plugin.tgz";
27
+
28
+ function env(name: string): string {
29
+ const v = process.env[name]?.trim();
30
+ if (!v) {
31
+ console.error(`ERROR: Missing required environment variable: ${name}`);
32
+ process.exit(1);
33
+ }
34
+ return v;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // 2. Create per-agent state directory
39
+ // ---------------------------------------------------------------------------
40
+
41
+ const openclawStateDir = path.join(stateDir, ".openclaw");
42
+ const configPath = path.join(openclawStateDir, "openclaw.json");
43
+
44
+ fs.mkdirSync(path.join(openclawStateDir, "sessions"), { recursive: true });
45
+ fs.mkdirSync(path.join(openclawStateDir, "workspace"), { recursive: true });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // 3. Install plugin from bundled archive
49
+ // ---------------------------------------------------------------------------
50
+
51
+ if (fs.existsSync(pluginArchive)) {
52
+ // Clean legacy extension dir before install (only when we have an archive
53
+ // to replace it — otherwise the existing install is the only copy).
54
+ const legacyExtDir = path.join(openclawStateDir, "extensions", "parall");
55
+ fs.rmSync(legacyExtDir, { recursive: true, force: true });
56
+
57
+ console.log(`Installing Parall plugin from ${pluginArchive}...`);
58
+ try {
59
+ execFileSync("openclaw", [
60
+ "plugins", "install", pluginArchive,
61
+ "--force", "--dangerously-force-unsafe-install",
62
+ ], {
63
+ env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
64
+ stdio: "inherit",
65
+ timeout: 60_000,
66
+ });
67
+ } catch (err) {
68
+ console.error(`ERROR: Failed to install Parall plugin: ${String(err)}`);
69
+ process.exit(1);
70
+ }
71
+ } else {
72
+ console.warn(
73
+ `Plugin archive not found at ${pluginArchive} — assuming plugin is already installed.`,
74
+ );
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // 4. Write openclaw.json
79
+ // ---------------------------------------------------------------------------
80
+
81
+ writeOpenclawConfig();
82
+
83
+ function writeOpenclawConfig(): void {
84
+ let cfg: Record<string, unknown> = {};
85
+ try {
86
+ cfg = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
87
+ } catch { /* fresh config */ }
88
+
89
+ const gateway = (cfg.gateway && typeof cfg.gateway === "object") ? cfg.gateway as Record<string, unknown> : {};
90
+ gateway.mode = "local";
91
+ cfg.gateway = gateway;
92
+
93
+ const channels = (cfg.channels && typeof cfg.channels === "object") ? cfg.channels as Record<string, unknown> : {};
94
+ const parallChannel: Record<string, unknown> = {
95
+ parall_url: PRLL_API_URL,
96
+ api_key: PRLL_API_KEY,
97
+ org_id: PRLL_ORG_ID,
98
+ };
99
+ if (PRLL_WS_URL) parallChannel.ws_url = PRLL_WS_URL;
100
+ channels.parall = parallChannel;
101
+ cfg.channels = channels;
102
+
103
+ const plugins = (cfg.plugins && typeof cfg.plugins === "object") ? cfg.plugins as Record<string, unknown> : {};
104
+ const entries = (plugins.entries && typeof plugins.entries === "object") ? plugins.entries as Record<string, unknown> : {};
105
+ const parallPluginConfig: Record<string, unknown> = {
106
+ parall_url: PRLL_API_URL,
107
+ api_key: PRLL_API_KEY,
108
+ org_id: PRLL_ORG_ID,
109
+ };
110
+ if (PRLL_WS_URL) parallPluginConfig.ws_url = PRLL_WS_URL;
111
+ const existingParall = (entries.parall && typeof entries.parall === "object") ? entries.parall as Record<string, unknown> : {};
112
+ entries.parall = {
113
+ ...existingParall,
114
+ enabled: true,
115
+ hooks: { allowPromptInjection: true, allowConversationAccess: true },
116
+ config: parallPluginConfig,
117
+ };
118
+ plugins.entries = entries;
119
+ cfg.plugins = plugins;
120
+
121
+ // sqlite-vec vector index guard
122
+ const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents as Record<string, unknown> : {};
123
+ const defaults = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults as Record<string, unknown> : {};
124
+ const ms = (defaults.memorySearch && typeof defaults.memorySearch === "object") ? defaults.memorySearch as Record<string, unknown> : {};
125
+ const store = (ms.store && typeof ms.store === "object") ? ms.store as Record<string, unknown> : {};
126
+ const vector = (store.vector && typeof store.vector === "object") ? store.vector as Record<string, unknown> : {};
127
+ if (vector.enabled === undefined) vector.enabled = true;
128
+ store.vector = vector;
129
+ ms.store = store;
130
+ defaults.memorySearch = ms;
131
+ agents.defaults = defaults;
132
+ cfg.agents = agents;
133
+
134
+ // Seed tools.alsoAllow
135
+ const tools = (cfg.tools && typeof cfg.tools === "object") ? cfg.tools as Record<string, unknown> : {};
136
+ const alsoAllow = new Set<string>(
137
+ Array.isArray(tools.alsoAllow) ? (tools.alsoAllow as string[]) : [],
138
+ );
139
+ alsoAllow.add("group:plugins");
140
+ tools.alsoAllow = Array.from(alsoAllow);
141
+ cfg.tools = tools;
142
+
143
+ const tmp = configPath + ".tmp";
144
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
145
+ fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
146
+ fs.renameSync(tmp, configPath);
147
+ }
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // 5. Pre-seed platform config (model + provider) before gateway reads it
151
+ // ---------------------------------------------------------------------------
152
+ // On first boot openclaw.json has no agents.defaults.model. Without this,
153
+ // OpenClaw uses its built-in default which may lack credentials through the
154
+ // Parall proxy. The plugin refreshes this during startAccount but OpenClaw's
155
+ // hot reload only reloads the model catalog, not the active model.
156
+
157
+ await preseedPlatformConfig();
158
+
159
+ async function preseedPlatformConfig(): Promise<void> {
160
+ try {
161
+ const headers: Record<string, string> = { Authorization: `Bearer ${PRLL_API_KEY}` };
162
+ if (PRLL_SWIMLANE_NAME) headers["X-Prll-Swimlane"] = PRLL_SWIMLANE_NAME;
163
+
164
+ const resp = await fetch(`${PRLL_API_URL}/api/v1/agents/platform-config`, {
165
+ headers,
166
+ signal: AbortSignal.timeout(15_000),
167
+ });
168
+ if (!resp.ok) throw new Error(`platform-config ${resp.status}`);
169
+ const data = (await resp.json()) as Record<string, unknown>;
170
+ const pc = (data.config ?? data) as Record<string, unknown>;
171
+
172
+ let cfg: Record<string, unknown> = {};
173
+ try { cfg = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>; } catch { /* fresh */ }
174
+
175
+ const ALLOWED_DEFAULTS = new Set(["model", "compaction", "memorySearch"]);
176
+ const platformDefaults = (pc.agents as Record<string, unknown> | undefined)?.defaults;
177
+ if (platformDefaults && typeof platformDefaults === "object") {
178
+ const agents = (cfg.agents && typeof cfg.agents === "object") ? cfg.agents as Record<string, unknown> : {};
179
+ const existing = (agents.defaults && typeof agents.defaults === "object") ? agents.defaults as Record<string, unknown> : {};
180
+ for (const [k, v] of Object.entries(platformDefaults as Record<string, unknown>)) {
181
+ if (ALLOWED_DEFAULTS.has(k)) existing[k] = v;
182
+ }
183
+ agents.defaults = existing;
184
+ cfg.agents = agents;
185
+ }
186
+
187
+ const ALLOWED_MODEL_KEYS = new Set(["id", "name", "contextWindow", "maxTokens"]);
188
+ const platformModels = (pc.models as Record<string, unknown> | undefined)?.providers as Record<string, unknown> | undefined;
189
+ const platformParall = platformModels?.parall;
190
+ if (platformParall && typeof platformParall === "object") {
191
+ const models = (cfg.models && typeof cfg.models === "object") ? cfg.models as Record<string, unknown> : {};
192
+ const providers = (models.providers && typeof models.providers === "object") ? models.providers as Record<string, unknown> : {};
193
+ const existingParall = (providers.parall && typeof providers.parall === "object") ? providers.parall as Record<string, unknown> : {};
194
+ const merged = { ...existingParall, ...(platformParall as Record<string, unknown>) };
195
+ if (Array.isArray(merged.models)) {
196
+ merged.models = (merged.models as Record<string, unknown>[])
197
+ .filter((m) => m && typeof m === "object")
198
+ .map((m) => {
199
+ const clean: Record<string, unknown> = {};
200
+ for (const [k, v] of Object.entries(m)) {
201
+ if (ALLOWED_MODEL_KEYS.has(k)) clean[k] = v;
202
+ }
203
+ return clean;
204
+ });
205
+ }
206
+ merged.apiKey = PRLL_API_KEY;
207
+ providers.parall = merged;
208
+ models.providers = providers;
209
+ cfg.models = models;
210
+ }
211
+
212
+ const tmp = configPath + ".tmp";
213
+ fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2));
214
+ fs.renameSync(tmp, configPath);
215
+ const model = ((cfg.agents as Record<string, unknown> | undefined)?.defaults as Record<string, unknown> | undefined)?.model ?? "none";
216
+ console.log(`Platform config pre-seeded (model: ${String(model)}).`);
217
+ } catch (err) {
218
+ console.warn(`Platform config pre-seed skipped: ${String(err)}`);
219
+ }
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // 6. openclaw doctor --fix (non-fatal)
224
+ // ---------------------------------------------------------------------------
225
+
226
+ try {
227
+ execFileSync("openclaw", ["doctor", "--fix"], {
228
+ env: { ...process.env, OPENCLAW_STATE_DIR: openclawStateDir },
229
+ stdio: "inherit",
230
+ timeout: 30_000,
231
+ });
232
+ } catch { /* non-fatal */ }
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // 7. Spawn openclaw gateway run with signal forwarding
236
+
237
+ // ---------------------------------------------------------------------------
238
+
239
+ console.log("Starting OpenClaw gateway...");
240
+
241
+ const workspaceDir = process.env.PRLL_OPENCLAW_WORKSPACE_DIR?.trim() || "";
242
+
243
+ const gatewayEnv: NodeJS.ProcessEnv = {
244
+ ...process.env,
245
+ OPENCLAW_STATE_DIR: openclawStateDir,
246
+ };
247
+ if (workspaceDir) {
248
+ gatewayEnv.PRLL_WIKI_MOUNT_ROOT = workspaceDir;
249
+ }
250
+ if (PRLL_SWIMLANE_NAME) {
251
+ gatewayEnv.PRLL_SWIMLANE_NAME = PRLL_SWIMLANE_NAME;
252
+ }
253
+
254
+ const gatewayArgs = ["gateway", "run"];
255
+ if (gatewayPort !== "0") {
256
+ gatewayArgs.push("--port", gatewayPort);
257
+ gatewayEnv.OPENCLAW_GATEWAY_PORT = gatewayPort;
258
+ }
259
+
260
+ const cwd = workspaceDir || path.join(openclawStateDir, "workspace");
261
+ fs.mkdirSync(cwd, { recursive: true });
262
+
263
+ const child = spawn("openclaw", gatewayArgs, {
264
+ env: gatewayEnv,
265
+ cwd,
266
+ stdio: "inherit",
267
+ detached: false,
268
+ });
269
+
270
+ function forwardSignal(sig: NodeJS.Signals): void {
271
+ try { child.kill(sig); } catch { /* already gone */ }
272
+ }
273
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
274
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
275
+
276
+ child.on("close", (code, signal) => {
277
+ if (signal === "SIGTERM") process.exit(143);
278
+ if (signal === "SIGINT") process.exit(130);
279
+ process.exit(code ?? 1);
280
+ });