@parall/daemon 1.30.0 → 1.31.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.
- package/bundle/manifest.json +12 -11
- package/bundle/parall-claude-agent.js +131 -14
- package/bundle/parall-codex-agent.js +120 -11
- package/bundle/parall-daemon.js +895 -273
- package/bundle/parall-openclaw-agent.js +2 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +52 -2
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +17 -0
- package/dist/index.js +30 -3
- package/dist/runtimes.d.ts +9 -8
- package/dist/runtimes.d.ts.map +1 -1
- package/dist/runtimes.js +47 -83
- package/dist/supervisor.d.ts +4 -0
- package/dist/supervisor.d.ts.map +1 -1
- package/dist/supervisor.js +33 -0
- package/dist/updater-manifest.d.ts +39 -0
- package/dist/updater-manifest.d.ts.map +1 -0
- package/dist/updater-manifest.js +94 -0
- package/dist/updater.d.ts +60 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +409 -0
- package/package.json +6 -6
package/bundle/parall-daemon.js
CHANGED
|
@@ -1,4 +1,622 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ts/daemon/dist/config.js
|
|
13
|
+
var config_exports = {};
|
|
14
|
+
__export(config_exports, {
|
|
15
|
+
agentClaudeCredentialsFileFor: () => agentClaudeCredentialsFileFor,
|
|
16
|
+
agentClaudeHomeFor: () => agentClaudeHomeFor,
|
|
17
|
+
agentStateDirFor: () => agentStateDirFor,
|
|
18
|
+
agentWorkspaceDirFor: () => agentWorkspaceDirFor,
|
|
19
|
+
daemonConfigDir: () => daemonConfigDir,
|
|
20
|
+
daemonConfigPath: () => daemonConfigPath,
|
|
21
|
+
resolveBundleDir: () => resolveBundleDir,
|
|
22
|
+
resolveClaudeDaemonConfig: () => resolveClaudeDaemonConfig,
|
|
23
|
+
resolveWsUrl: () => resolveWsUrl,
|
|
24
|
+
sharedClaudeCredentialsFileFor: () => sharedClaudeCredentialsFileFor
|
|
25
|
+
});
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as os from "node:os";
|
|
28
|
+
import * as path from "node:path";
|
|
29
|
+
function resolvePath(value) {
|
|
30
|
+
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
31
|
+
}
|
|
32
|
+
function parseMs(value, fallback) {
|
|
33
|
+
if (!value)
|
|
34
|
+
return fallback;
|
|
35
|
+
const n = Number(value);
|
|
36
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
37
|
+
}
|
|
38
|
+
function parseMsAllowZero(value, fallback) {
|
|
39
|
+
if (value === void 0)
|
|
40
|
+
return fallback;
|
|
41
|
+
const n = Number(value);
|
|
42
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
43
|
+
}
|
|
44
|
+
function daemonConfigDir(env = process.env) {
|
|
45
|
+
return path.join(env.HOME || os.homedir(), ".parall-daemon");
|
|
46
|
+
}
|
|
47
|
+
function daemonConfigPath(env = process.env) {
|
|
48
|
+
return path.join(daemonConfigDir(env), "config.json");
|
|
49
|
+
}
|
|
50
|
+
function tryLoadConfigFile(env) {
|
|
51
|
+
const cfgPath = daemonConfigPath(env);
|
|
52
|
+
let content;
|
|
53
|
+
try {
|
|
54
|
+
content = fs.readFileSync(cfgPath, "utf-8");
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.code === "ENOENT")
|
|
57
|
+
return null;
|
|
58
|
+
console.error(`Failed to read daemon config at ${cfgPath}: ${String(err)}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(content);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`Failed to parse daemon config at ${cfgPath}: ${String(err)}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function resolveClaudeDaemonConfig(env = process.env) {
|
|
69
|
+
let apiUrl = env.PRLL_API_URL?.trim() || "";
|
|
70
|
+
let apiKey = env.PRLL_API_KEY?.trim() || "";
|
|
71
|
+
if (!apiUrl || !apiKey) {
|
|
72
|
+
const file = tryLoadConfigFile(env);
|
|
73
|
+
if (file) {
|
|
74
|
+
if (!apiUrl && file.api_url)
|
|
75
|
+
apiUrl = file.api_url.trim();
|
|
76
|
+
if (!apiKey && file.api_key)
|
|
77
|
+
apiKey = file.api_key.trim();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!apiUrl)
|
|
81
|
+
throw new Error("Missing required env var: PRLL_API_URL");
|
|
82
|
+
if (!apiKey)
|
|
83
|
+
throw new Error("Missing required env var: PRLL_API_KEY");
|
|
84
|
+
if (!apiKey.startsWith("mck_")) {
|
|
85
|
+
throw new Error(`PRLL_API_KEY does not look like a Machine bearer (expected prefix "mck_"). Daemon mode requires a machine-scoped key issued via POST /machines/{id}/keys.`);
|
|
86
|
+
}
|
|
87
|
+
const rootClaudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
|
|
88
|
+
const rootStateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(rootClaudeHome, ".parall-agent"));
|
|
89
|
+
return {
|
|
90
|
+
apiUrl,
|
|
91
|
+
apiKey,
|
|
92
|
+
agentBin: env.PRLL_CLAUDE_AGENT_BIN?.trim() || "parall-claude-agent",
|
|
93
|
+
rootStateDir,
|
|
94
|
+
rootClaudeHome,
|
|
95
|
+
wsUrl: env.PRLL_WS_URL?.trim() || void 0,
|
|
96
|
+
swimlaneName: env.PRLL_SWIMLANE_NAME?.trim() || void 0,
|
|
97
|
+
pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 3e4),
|
|
98
|
+
heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 3e4),
|
|
99
|
+
restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5e3),
|
|
100
|
+
restartBackoffMaxMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MAX_MS, 5 * 6e4),
|
|
101
|
+
bootstrapBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MS, 2e3),
|
|
102
|
+
bootstrapBackoffMaxMs: parseMs(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MAX_MS, 6e4),
|
|
103
|
+
supervisorRestartBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS, 5e3),
|
|
104
|
+
supervisorRestartBackoffMaxMs: parseMs(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MAX_MS, 5 * 6e4),
|
|
105
|
+
updateCdnUrl: env.PRLL_DAEMON_UPDATE_CDN_URL?.trim() || ((env.PRLL_DAEMON_UPDATE_CHANNEL?.trim() ?? "production") === "staging" ? "https://releases.staging.prll.sh/daemon/staging" : "https://releases.parall.com/daemon/production"),
|
|
106
|
+
updateIntervalMs: parseMsAllowZero(env.PRLL_DAEMON_UPDATE_INTERVAL_MS, 6 * 60 * 6e4),
|
|
107
|
+
updateDisabled: env.PRLL_DAEMON_UPDATE_DISABLED === "true" || !!env.KUBERNETES_SERVICE_HOST
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function assertSafeAgentId(agentId) {
|
|
111
|
+
if (!/^[A-Za-z0-9_-]+$/.test(agentId)) {
|
|
112
|
+
throw new Error(`Invalid agentId for filesystem path: ${agentId}`);
|
|
113
|
+
}
|
|
114
|
+
return agentId;
|
|
115
|
+
}
|
|
116
|
+
function agentStateDirFor(rootStateDir, agentId) {
|
|
117
|
+
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId));
|
|
118
|
+
}
|
|
119
|
+
function agentClaudeHomeFor(rootClaudeHome, agentId) {
|
|
120
|
+
return path.join(rootClaudeHome, "agents", assertSafeAgentId(agentId));
|
|
121
|
+
}
|
|
122
|
+
function sharedClaudeCredentialsFileFor(rootClaudeHome) {
|
|
123
|
+
return path.join(rootClaudeHome, ".claude", ".credentials.json");
|
|
124
|
+
}
|
|
125
|
+
function agentClaudeCredentialsFileFor(agentClaudeHome) {
|
|
126
|
+
return path.join(agentClaudeHome, ".claude", ".credentials.json");
|
|
127
|
+
}
|
|
128
|
+
function agentWorkspaceDirFor(rootStateDir, agentId) {
|
|
129
|
+
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
|
|
130
|
+
}
|
|
131
|
+
function resolveBundleDir(env = process.env) {
|
|
132
|
+
if (env.PRLL_DAEMON_BUNDLE_DIR)
|
|
133
|
+
return resolvePath(env.PRLL_DAEMON_BUNDLE_DIR);
|
|
134
|
+
return path.join(daemonConfigDir(env), "bundle");
|
|
135
|
+
}
|
|
136
|
+
function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
|
|
137
|
+
const base = explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
|
|
138
|
+
if (!swimlaneName)
|
|
139
|
+
return base;
|
|
140
|
+
const url = new URL(base);
|
|
141
|
+
url.searchParams.set("swimlane", swimlaneName);
|
|
142
|
+
return url.toString();
|
|
143
|
+
}
|
|
144
|
+
var init_config = __esm({
|
|
145
|
+
"ts/daemon/dist/config.js"() {
|
|
146
|
+
"use strict";
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ts/daemon/dist/updater-manifest.js
|
|
151
|
+
import { verify } from "node:crypto";
|
|
152
|
+
function canonicalize(obj) {
|
|
153
|
+
if (Array.isArray(obj))
|
|
154
|
+
return obj.map(canonicalize);
|
|
155
|
+
if (obj !== null && typeof obj === "object") {
|
|
156
|
+
const sorted = {};
|
|
157
|
+
for (const k of Object.keys(obj).sort()) {
|
|
158
|
+
sorted[k] = canonicalize(obj[k]);
|
|
159
|
+
}
|
|
160
|
+
return sorted;
|
|
161
|
+
}
|
|
162
|
+
return obj;
|
|
163
|
+
}
|
|
164
|
+
function verifyManifestSignature(manifest, publicKey) {
|
|
165
|
+
const { signature, ...rest } = manifest;
|
|
166
|
+
const canonical = JSON.stringify(canonicalize(rest));
|
|
167
|
+
try {
|
|
168
|
+
return verify(null, Buffer.from(canonical), publicKey, Buffer.from(signature, "base64"));
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function semverCompare(a, b) {
|
|
174
|
+
const pa = parseSemver(a);
|
|
175
|
+
const pb = parseSemver(b);
|
|
176
|
+
if (!pa || !pb)
|
|
177
|
+
return null;
|
|
178
|
+
for (let i = 0; i < 3; i++) {
|
|
179
|
+
if (pa.nums[i] < pb.nums[i])
|
|
180
|
+
return -1;
|
|
181
|
+
if (pa.nums[i] > pb.nums[i])
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
if (!pa.pre && !pb.pre)
|
|
185
|
+
return 0;
|
|
186
|
+
if (!pa.pre)
|
|
187
|
+
return 1;
|
|
188
|
+
if (!pb.pre)
|
|
189
|
+
return -1;
|
|
190
|
+
if (pa.pre < pb.pre)
|
|
191
|
+
return -1;
|
|
192
|
+
if (pa.pre > pb.pre)
|
|
193
|
+
return 1;
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
function parseSemver(v) {
|
|
197
|
+
const parts = v.split(".");
|
|
198
|
+
if (parts.length < 3)
|
|
199
|
+
return null;
|
|
200
|
+
const major = Number(parts[0]);
|
|
201
|
+
const minor = Number(parts[1]);
|
|
202
|
+
const rest = parts.slice(2).join(".");
|
|
203
|
+
const hyphen = rest.indexOf("-");
|
|
204
|
+
let patchStr;
|
|
205
|
+
let pre;
|
|
206
|
+
if (hyphen >= 0) {
|
|
207
|
+
patchStr = rest.slice(0, hyphen);
|
|
208
|
+
pre = rest.slice(hyphen + 1);
|
|
209
|
+
} else {
|
|
210
|
+
patchStr = rest;
|
|
211
|
+
}
|
|
212
|
+
const patch = Number(patchStr);
|
|
213
|
+
if (isNaN(major) || isNaN(minor) || isNaN(patch))
|
|
214
|
+
return null;
|
|
215
|
+
return { nums: [major, minor, patch], pre };
|
|
216
|
+
}
|
|
217
|
+
var init_updater_manifest = __esm({
|
|
218
|
+
"ts/daemon/dist/updater-manifest.js"() {
|
|
219
|
+
"use strict";
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ts/daemon/dist/updater.js
|
|
224
|
+
var updater_exports = {};
|
|
225
|
+
__export(updater_exports, {
|
|
226
|
+
DaemonUpdater: () => DaemonUpdater
|
|
227
|
+
});
|
|
228
|
+
import * as fs6 from "node:fs";
|
|
229
|
+
import * as path6 from "node:path";
|
|
230
|
+
import * as https from "node:https";
|
|
231
|
+
import * as http from "node:http";
|
|
232
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
233
|
+
var SIGNING_PUBLIC_KEY, DaemonUpdater;
|
|
234
|
+
var init_updater = __esm({
|
|
235
|
+
"ts/daemon/dist/updater.js"() {
|
|
236
|
+
"use strict";
|
|
237
|
+
init_updater_manifest();
|
|
238
|
+
SIGNING_PUBLIC_KEY = process.env.PRLL_DAEMON_SIGNING_PUBLIC_KEY ?? "";
|
|
239
|
+
DaemonUpdater = class {
|
|
240
|
+
bundleDir;
|
|
241
|
+
cdnBaseUrl;
|
|
242
|
+
log;
|
|
243
|
+
signingEnabled;
|
|
244
|
+
updating = false;
|
|
245
|
+
periodicTimer = null;
|
|
246
|
+
constructor(bundleDir, cdnBaseUrl, log2, signingEnabled) {
|
|
247
|
+
this.bundleDir = bundleDir;
|
|
248
|
+
this.cdnBaseUrl = cdnBaseUrl;
|
|
249
|
+
this.log = log2;
|
|
250
|
+
this.signingEnabled = signingEnabled;
|
|
251
|
+
}
|
|
252
|
+
// --- Public API ---
|
|
253
|
+
/**
|
|
254
|
+
* Start periodic update checks. Call once after supervisor bootstrap.
|
|
255
|
+
*/
|
|
256
|
+
startPeriodicCheck(intervalMs) {
|
|
257
|
+
if (intervalMs <= 0 || this.periodicTimer)
|
|
258
|
+
return;
|
|
259
|
+
this.periodicTimer = setInterval(() => {
|
|
260
|
+
this.checkAndApply().catch((err) => {
|
|
261
|
+
this.log.warn(`periodic update check failed: ${String(err)}`);
|
|
262
|
+
});
|
|
263
|
+
}, intervalMs);
|
|
264
|
+
this.periodicTimer.unref?.();
|
|
265
|
+
}
|
|
266
|
+
stopPeriodicCheck() {
|
|
267
|
+
if (this.periodicTimer) {
|
|
268
|
+
clearInterval(this.periodicTimer);
|
|
269
|
+
this.periodicTimer = null;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Check CDN for a newer version. Returns true if update was applied
|
|
274
|
+
* (caller should exit for service manager restart).
|
|
275
|
+
*/
|
|
276
|
+
async checkAndApply(targetVersion) {
|
|
277
|
+
if (this.updating)
|
|
278
|
+
return false;
|
|
279
|
+
this.updating = true;
|
|
280
|
+
try {
|
|
281
|
+
return await this.doUpdate(targetVersion);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
this.log.warn(`update check failed: ${String(err)}`);
|
|
284
|
+
return false;
|
|
285
|
+
} finally {
|
|
286
|
+
this.updating = false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Handle WS-triggered update. Adds jitter for non-mandatory updates.
|
|
291
|
+
*/
|
|
292
|
+
async triggerUpdate(targetVersion, mandatory) {
|
|
293
|
+
if (!mandatory) {
|
|
294
|
+
const jitter = Math.random() * 18e4;
|
|
295
|
+
await new Promise((r) => setTimeout(r, jitter));
|
|
296
|
+
}
|
|
297
|
+
return this.checkAndApply(targetVersion);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if we need to roll back from a failed update.
|
|
301
|
+
* Call on every daemon startup, before bootstrap.
|
|
302
|
+
* Returns true if rollback was performed (caller should exit immediately).
|
|
303
|
+
*/
|
|
304
|
+
checkRollback() {
|
|
305
|
+
const state = this.loadUpdateState();
|
|
306
|
+
const current = this.loadLocalManifest();
|
|
307
|
+
if (!state || !current)
|
|
308
|
+
return false;
|
|
309
|
+
if (!state.pending_version || state.pending_version !== current.version)
|
|
310
|
+
return false;
|
|
311
|
+
state.boot_count = (state.boot_count ?? 0) + 1;
|
|
312
|
+
this.saveUpdateState(state);
|
|
313
|
+
if (state.boot_count < 3)
|
|
314
|
+
return false;
|
|
315
|
+
if (!state.previous_version)
|
|
316
|
+
return false;
|
|
317
|
+
const previousDir = path6.join(this.bundleDir, "versions", state.previous_version);
|
|
318
|
+
if (!fs6.existsSync(previousDir))
|
|
319
|
+
return false;
|
|
320
|
+
this.log.warn(`rollback: ${current.version} failed ${state.boot_count} boots, reverting to ${state.previous_version}`);
|
|
321
|
+
if (process.platform === "win32") {
|
|
322
|
+
const currentDir = path6.join(this.bundleDir, "current");
|
|
323
|
+
if (fs6.existsSync(currentDir)) {
|
|
324
|
+
for (const file of fs6.readdirSync(currentDir)) {
|
|
325
|
+
fs6.unlinkSync(path6.join(currentDir, file));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
fs6.mkdirSync(currentDir, { recursive: true });
|
|
329
|
+
for (const file of fs6.readdirSync(previousDir)) {
|
|
330
|
+
fs6.copyFileSync(path6.join(previousDir, file), path6.join(currentDir, file));
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
this.swapSymlink(state.previous_version);
|
|
334
|
+
}
|
|
335
|
+
state.confirmed_version = state.previous_version;
|
|
336
|
+
state.pending_version = void 0;
|
|
337
|
+
state.boot_count = 0;
|
|
338
|
+
state.rollback_from = current.version;
|
|
339
|
+
state.rollback_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
340
|
+
this.saveUpdateState(state);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Confirm the current version after successful health check.
|
|
345
|
+
* Clears pending state so rollback won't trigger.
|
|
346
|
+
*/
|
|
347
|
+
confirmVersion() {
|
|
348
|
+
const current = this.loadLocalManifest();
|
|
349
|
+
if (!current)
|
|
350
|
+
return;
|
|
351
|
+
const state = this.loadUpdateState() ?? {};
|
|
352
|
+
state.confirmed_version = current.version;
|
|
353
|
+
state.confirmed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
354
|
+
state.pending_version = void 0;
|
|
355
|
+
state.boot_count = 0;
|
|
356
|
+
this.saveUpdateState(state);
|
|
357
|
+
this.log.info(`update: confirmed version ${current.version}`);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Get the current local manifest version (for heartbeat reporting).
|
|
361
|
+
*/
|
|
362
|
+
getLocalVersion() {
|
|
363
|
+
return this.loadLocalManifest()?.version;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Check CDN for available update without downloading.
|
|
367
|
+
* Returns remote version info for CLI --check display.
|
|
368
|
+
*/
|
|
369
|
+
async checkAvailable() {
|
|
370
|
+
const local = this.loadLocalManifest();
|
|
371
|
+
const manifestUrl = `${this.cdnBaseUrl}/latest/manifest.json`;
|
|
372
|
+
try {
|
|
373
|
+
const remoteJson = await this.httpGet(manifestUrl);
|
|
374
|
+
const remote = JSON.parse(remoteJson);
|
|
375
|
+
const current = local?.version;
|
|
376
|
+
if (current) {
|
|
377
|
+
const cmp = semverCompare(remote.version, current);
|
|
378
|
+
return { available: cmp !== null && cmp > 0, currentVersion: current, remoteVersion: remote.version };
|
|
379
|
+
}
|
|
380
|
+
return { available: true, remoteVersion: remote.version };
|
|
381
|
+
} catch {
|
|
382
|
+
return { available: false, currentVersion: local?.version };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// --- Internal ---
|
|
386
|
+
async doUpdate(targetVersion) {
|
|
387
|
+
const local = this.loadLocalManifest();
|
|
388
|
+
const manifestUrl = targetVersion ? `${this.cdnBaseUrl}/${targetVersion}/manifest.json` : `${this.cdnBaseUrl}/latest/manifest.json`;
|
|
389
|
+
let remoteJson;
|
|
390
|
+
try {
|
|
391
|
+
remoteJson = await this.httpGet(manifestUrl);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
this.log.warn(`update: failed to fetch manifest from ${manifestUrl}: ${String(err)}`);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
let remote;
|
|
397
|
+
try {
|
|
398
|
+
remote = JSON.parse(remoteJson);
|
|
399
|
+
} catch {
|
|
400
|
+
this.log.warn("update: failed to parse remote manifest");
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
if (local) {
|
|
404
|
+
const cmp = semverCompare(remote.version, local.version);
|
|
405
|
+
if (cmp === null || cmp <= 0)
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
const state = this.loadUpdateState();
|
|
409
|
+
if (state?.rollback_from === remote.version) {
|
|
410
|
+
this.log.info(`update: skipping ${remote.version} (previously rolled back)`);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
if (this.signingEnabled) {
|
|
414
|
+
if (!SIGNING_PUBLIC_KEY) {
|
|
415
|
+
this.log.error("update: signing enabled but PRLL_DAEMON_SIGNING_PUBLIC_KEY is empty \u2014 rejecting update");
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
if (!verifyManifestSignature(remote, SIGNING_PUBLIC_KEY)) {
|
|
419
|
+
this.log.error(`update: signature verification failed for ${remote.version}`);
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const stagingDir = path6.join(this.bundleDir, "staging");
|
|
424
|
+
this.cleanDir(stagingDir);
|
|
425
|
+
fs6.mkdirSync(stagingDir, { recursive: true });
|
|
426
|
+
for (const [filename, meta] of Object.entries(remote.files)) {
|
|
427
|
+
const filePath = path6.join(stagingDir, filename);
|
|
428
|
+
const fileUrl = `${this.cdnBaseUrl}/${remote.version}/${filename}`;
|
|
429
|
+
try {
|
|
430
|
+
await this.downloadFile(fileUrl, filePath);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
this.log.error(`update: download failed for ${filename}: ${String(err)}`);
|
|
433
|
+
this.cleanDir(stagingDir);
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
const content = fs6.readFileSync(filePath);
|
|
437
|
+
const hash = createHash2("sha256").update(content).digest("hex");
|
|
438
|
+
if (hash !== meta.sha256) {
|
|
439
|
+
this.log.error(`update: sha256 mismatch for ${filename} (expected ${meta.sha256}, got ${hash})`);
|
|
440
|
+
this.cleanDir(stagingDir);
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
if (content.length !== meta.size) {
|
|
444
|
+
this.log.error(`update: size mismatch for ${filename} (expected ${meta.size}, got ${content.length})`);
|
|
445
|
+
this.cleanDir(stagingDir);
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
fs6.writeFileSync(path6.join(stagingDir, "manifest.json"), JSON.stringify(remote, null, 2));
|
|
450
|
+
this.atomicSwap(stagingDir, remote.version);
|
|
451
|
+
const newState = {
|
|
452
|
+
confirmed_version: local?.version ?? state?.confirmed_version,
|
|
453
|
+
previous_version: local?.version,
|
|
454
|
+
pending_version: remote.version,
|
|
455
|
+
pending_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
456
|
+
boot_count: 0
|
|
457
|
+
};
|
|
458
|
+
this.saveUpdateState(newState);
|
|
459
|
+
this.log.info(`update: ${local?.version ?? "unknown"} \u2192 ${remote.version} applied, restarting`);
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
atomicSwap(stagingDir, newVersion) {
|
|
463
|
+
const versionsDir = path6.join(this.bundleDir, "versions");
|
|
464
|
+
const targetDir = path6.join(versionsDir, newVersion);
|
|
465
|
+
const currentLink = path6.join(this.bundleDir, "current");
|
|
466
|
+
fs6.mkdirSync(versionsDir, { recursive: true });
|
|
467
|
+
if (fs6.existsSync(targetDir)) {
|
|
468
|
+
fs6.rmSync(targetDir, { recursive: true });
|
|
469
|
+
}
|
|
470
|
+
fs6.renameSync(stagingDir, targetDir);
|
|
471
|
+
if (process.platform === "win32") {
|
|
472
|
+
const currentDir = currentLink;
|
|
473
|
+
if (fs6.existsSync(currentDir)) {
|
|
474
|
+
for (const file of fs6.readdirSync(currentDir)) {
|
|
475
|
+
fs6.unlinkSync(path6.join(currentDir, file));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
fs6.mkdirSync(currentDir, { recursive: true });
|
|
479
|
+
for (const file of fs6.readdirSync(targetDir)) {
|
|
480
|
+
fs6.copyFileSync(path6.join(targetDir, file), path6.join(currentDir, file));
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
this.swapSymlink(newVersion);
|
|
484
|
+
}
|
|
485
|
+
this.pruneOldVersions(versionsDir, newVersion);
|
|
486
|
+
}
|
|
487
|
+
swapSymlink(version) {
|
|
488
|
+
const currentLink = path6.join(this.bundleDir, "current");
|
|
489
|
+
const tmpLink = `${currentLink}.new`;
|
|
490
|
+
try {
|
|
491
|
+
fs6.unlinkSync(tmpLink);
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
fs6.symlinkSync(`versions/${version}`, tmpLink);
|
|
495
|
+
fs6.renameSync(tmpLink, currentLink);
|
|
496
|
+
}
|
|
497
|
+
pruneOldVersions(versionsDir, currentVersion) {
|
|
498
|
+
const state = this.loadUpdateState();
|
|
499
|
+
const keep = /* @__PURE__ */ new Set([currentVersion]);
|
|
500
|
+
if (state?.previous_version)
|
|
501
|
+
keep.add(state.previous_version);
|
|
502
|
+
if (state?.confirmed_version)
|
|
503
|
+
keep.add(state.confirmed_version);
|
|
504
|
+
try {
|
|
505
|
+
for (const entry of fs6.readdirSync(versionsDir)) {
|
|
506
|
+
if (!keep.has(entry)) {
|
|
507
|
+
fs6.rmSync(path6.join(versionsDir, entry), { recursive: true });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// --- Manifest & State I/O ---
|
|
514
|
+
loadLocalManifest() {
|
|
515
|
+
const currentDir = path6.join(this.bundleDir, "current");
|
|
516
|
+
const manifestPath = path6.join(currentDir, "manifest.json");
|
|
517
|
+
try {
|
|
518
|
+
return JSON.parse(fs6.readFileSync(manifestPath, "utf-8"));
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
loadUpdateState() {
|
|
524
|
+
const statePath = path6.join(this.bundleDir, "update-state.json");
|
|
525
|
+
try {
|
|
526
|
+
return JSON.parse(fs6.readFileSync(statePath, "utf-8"));
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
saveUpdateState(state) {
|
|
532
|
+
const statePath = path6.join(this.bundleDir, "update-state.json");
|
|
533
|
+
fs6.mkdirSync(path6.dirname(statePath), { recursive: true });
|
|
534
|
+
fs6.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
535
|
+
}
|
|
536
|
+
// --- HTTP helpers ---
|
|
537
|
+
httpGet(url, maxRedirects = 5) {
|
|
538
|
+
return new Promise((resolve5, reject) => {
|
|
539
|
+
const mod = url.startsWith("https") ? https : http;
|
|
540
|
+
const req = mod.get(url, (res) => {
|
|
541
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
542
|
+
if (res.headers.location && maxRedirects > 0) {
|
|
543
|
+
this.httpGet(res.headers.location, maxRedirects - 1).then(resolve5, reject);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
res.resume();
|
|
547
|
+
reject(new Error(`too many redirects or missing location for ${url}`));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (res.statusCode !== 200) {
|
|
551
|
+
res.resume();
|
|
552
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const chunks = [];
|
|
556
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
557
|
+
res.on("end", () => resolve5(Buffer.concat(chunks).toString("utf-8")));
|
|
558
|
+
res.on("error", reject);
|
|
559
|
+
});
|
|
560
|
+
req.on("error", reject);
|
|
561
|
+
req.setTimeout(3e4, () => {
|
|
562
|
+
req.destroy(new Error(`timeout fetching ${url}`));
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
downloadFile(url, dest, maxRedirects = 5) {
|
|
567
|
+
return new Promise((resolve5, reject) => {
|
|
568
|
+
const mod = url.startsWith("https") ? https : http;
|
|
569
|
+
const req = mod.get(url, (res) => {
|
|
570
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
571
|
+
if (res.headers.location && maxRedirects > 0) {
|
|
572
|
+
this.downloadFile(res.headers.location, dest, maxRedirects - 1).then(resolve5, reject);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
res.resume();
|
|
576
|
+
reject(new Error(`too many redirects or missing location for ${url}`));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (res.statusCode !== 200) {
|
|
580
|
+
res.resume();
|
|
581
|
+
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const file = fs6.createWriteStream(dest);
|
|
585
|
+
res.pipe(file);
|
|
586
|
+
file.on("finish", () => {
|
|
587
|
+
file.close();
|
|
588
|
+
resolve5();
|
|
589
|
+
});
|
|
590
|
+
file.on("error", (err) => {
|
|
591
|
+
fs6.unlinkSync(dest);
|
|
592
|
+
reject(err);
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
req.on("error", reject);
|
|
596
|
+
req.setTimeout(12e4, () => {
|
|
597
|
+
req.destroy(new Error(`timeout downloading ${url}`));
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
cleanDir(dir) {
|
|
602
|
+
try {
|
|
603
|
+
fs6.rmSync(dir, { recursive: true });
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// ts/agent-core/dist/provider-config.js
|
|
612
|
+
function clearAllProviderCreds(env) {
|
|
613
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
614
|
+
delete env.ANTHROPIC_BASE_URL;
|
|
615
|
+
delete env.ANTHROPIC_API_KEY;
|
|
616
|
+
delete env.OPENAI_API_KEY;
|
|
617
|
+
delete env.OPENAI_BASE_URL;
|
|
618
|
+
delete env.PRLL_CLAUDE_ALLOW_API_KEY;
|
|
619
|
+
}
|
|
2
620
|
|
|
3
621
|
// ts/agent-core/dist/logger.js
|
|
4
622
|
function createLogger(prefix) {
|
|
@@ -40,6 +658,7 @@ var ENDPOINTS = {
|
|
|
40
658
|
ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
|
|
41
659
|
ORG_MEMBER_CHATS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/chats`,
|
|
42
660
|
ORG_MEMBER_TASKS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/tasks`,
|
|
661
|
+
REF_SEARCH: (orgId) => `${API_BASE}/orgs/${orgId}/refs/search`,
|
|
43
662
|
// Direct messages (org-scoped, atomic find-or-create + send)
|
|
44
663
|
DM: (orgId) => `${API_BASE}/orgs/${orgId}/dm`,
|
|
45
664
|
// Onboarding
|
|
@@ -122,6 +741,7 @@ var ENDPOINTS = {
|
|
|
122
741
|
MACHINE_KEY: (orgId, machineId, keyId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys/${keyId}`,
|
|
123
742
|
MACHINE_RUNTIME_AUTH_SESSIONS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth-sessions`,
|
|
124
743
|
MACHINE_RUNTIME_AUTH_SESSION_COMPLETE: (orgId, machineId, sessionId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth-sessions/${sessionId}/complete`,
|
|
744
|
+
MACHINE_REQUEST_UPDATE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/request-update`,
|
|
125
745
|
MACHINE_BROWSE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/browse`,
|
|
126
746
|
// Machine self-control-plane (mck_-scoped). The bearer token implicitly
|
|
127
747
|
// identifies the Machine, so there is no `:mid` URL parameter — these are
|
|
@@ -167,6 +787,12 @@ var ENDPOINTS = {
|
|
|
167
787
|
INVITATION_ACCEPT: (id) => `${API_BASE}/invitations/${id}/accept`,
|
|
168
788
|
INVITATION_DECLINE: (id) => `${API_BASE}/invitations/${id}/decline`,
|
|
169
789
|
INVITATION_BY_TOKEN: (token) => `${API_BASE}/invitations/by-token/${token}`,
|
|
790
|
+
// Org-level shareable invite link
|
|
791
|
+
ORG_INVITE_LINK: (orgId) => `${API_BASE}/orgs/${orgId}/invite-link`,
|
|
792
|
+
ORG_INVITE_LINK_REGENERATE: (orgId) => `${API_BASE}/orgs/${orgId}/invite-link/regenerate`,
|
|
793
|
+
ORG_INVITE_LINK_JOIN_REQUESTS: (orgId) => `${API_BASE}/orgs/${orgId}/invite-link/join-requests`,
|
|
794
|
+
ORG_INVITE_LINK_JOIN_REQUEST_DECIDE: (orgId, jrId) => `${API_BASE}/orgs/${orgId}/invite-link/join-requests/${jrId}/decide`,
|
|
795
|
+
INVITE_LINK_JOIN: `${API_BASE}/invite-link/join`,
|
|
170
796
|
// Wikis (org-scoped, served by wiki-service)
|
|
171
797
|
WIKIS: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
|
|
172
798
|
WIKI: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
|
|
@@ -286,6 +912,8 @@ var WS_EVENTS = {
|
|
|
286
912
|
INVITATION_ACCEPTED: "invitation.accepted",
|
|
287
913
|
INVITATION_DECLINED: "invitation.declined",
|
|
288
914
|
INVITATION_REVOKED: "invitation.revoked",
|
|
915
|
+
ORG_JOIN_REQUEST_NEW: "org.join_request.new",
|
|
916
|
+
ORG_INVITE_LINK_JOINED: "org.invite_link.joined",
|
|
289
917
|
AGENT_CONFIG_UPDATE: "agent_config.update",
|
|
290
918
|
PRESENCE_UPDATE: "presence.update",
|
|
291
919
|
WIKI_CHANGESET_CREATED: "wiki.changeset.created",
|
|
@@ -314,6 +942,8 @@ var WS_EVENTS = {
|
|
|
314
942
|
MACHINE_STOP: "machine.stop",
|
|
315
943
|
MACHINE_WORKSPACE_SETUP_REQUESTED: "machine.workspace.setup.requested",
|
|
316
944
|
MACHINE_FILESYSTEM_BROWSE: "machine.filesystem.browse",
|
|
945
|
+
MACHINE_UPDATE: "machine.update",
|
|
946
|
+
MACHINE_CONFIG_UPDATED: "machine.config.updated",
|
|
317
947
|
AGENT_NEW_SESSION: "agent.new_session"
|
|
318
948
|
};
|
|
319
949
|
|
|
@@ -397,10 +1027,10 @@ var ParallClient = class _ParallClient {
|
|
|
397
1027
|
* REFRESH_THRESHOLD_S, refresh it **before** sending the request.
|
|
398
1028
|
* No-op when the token is still fresh, missing, or un-parseable.
|
|
399
1029
|
*/
|
|
400
|
-
async ensureFreshToken(
|
|
1030
|
+
async ensureFreshToken(path8) {
|
|
401
1031
|
if (!this.token || !this.getRefreshToken)
|
|
402
1032
|
return;
|
|
403
|
-
const pathSuffix =
|
|
1033
|
+
const pathSuffix = path8.replace(/^\/api\/v1/, "");
|
|
404
1034
|
if (_ParallClient.AUTH_PATHS.has(pathSuffix))
|
|
405
1035
|
return;
|
|
406
1036
|
const exp = _ParallClient.decodeJwtExp(this.token);
|
|
@@ -432,11 +1062,11 @@ var ParallClient = class _ParallClient {
|
|
|
432
1062
|
this.refreshPromise = null;
|
|
433
1063
|
}
|
|
434
1064
|
}
|
|
435
|
-
async request(method,
|
|
1065
|
+
async request(method, path8, body, query, retried = false, opts) {
|
|
436
1066
|
if (!retried) {
|
|
437
|
-
await this.ensureFreshToken(
|
|
1067
|
+
await this.ensureFreshToken(path8);
|
|
438
1068
|
}
|
|
439
|
-
let url = `${this.baseUrl}${
|
|
1069
|
+
let url = `${this.baseUrl}${path8}`;
|
|
440
1070
|
if (query) {
|
|
441
1071
|
const params = new URLSearchParams();
|
|
442
1072
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -461,12 +1091,12 @@ var ParallClient = class _ParallClient {
|
|
|
461
1091
|
throw _ParallClient.normalizeFetchError(err);
|
|
462
1092
|
}
|
|
463
1093
|
if (res.status === 401) {
|
|
464
|
-
const pathSuffix =
|
|
1094
|
+
const pathSuffix = path8.replace(/^\/api\/v1/, "");
|
|
465
1095
|
const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
|
|
466
1096
|
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
467
1097
|
const refreshed = await this.tryRefresh();
|
|
468
1098
|
if (refreshed) {
|
|
469
|
-
return this.request(method,
|
|
1099
|
+
return this.request(method, path8, body, query, true, opts);
|
|
470
1100
|
}
|
|
471
1101
|
}
|
|
472
1102
|
if (this.onTokenExpired && !isAuthPath) {
|
|
@@ -503,15 +1133,15 @@ var ParallClient = class _ParallClient {
|
|
|
503
1133
|
* hit the 100 MiB cap, so a longer 5-minute timeout is used so a
|
|
504
1134
|
* 50 MiB blob on a slow connection doesn't get chopped at 15 s.
|
|
505
1135
|
*/
|
|
506
|
-
async multipartRequest(method,
|
|
1136
|
+
async multipartRequest(method, path8, body, retried = false) {
|
|
507
1137
|
if (!retried) {
|
|
508
|
-
await this.ensureFreshToken(
|
|
1138
|
+
await this.ensureFreshToken(path8);
|
|
509
1139
|
}
|
|
510
1140
|
const { "Content-Type": _drop, ...headers } = this.buildHeaders();
|
|
511
1141
|
void _drop;
|
|
512
1142
|
let res;
|
|
513
1143
|
try {
|
|
514
|
-
res = await fetch(`${this.baseUrl}${
|
|
1144
|
+
res = await fetch(`${this.baseUrl}${path8}`, {
|
|
515
1145
|
method,
|
|
516
1146
|
headers,
|
|
517
1147
|
body,
|
|
@@ -521,12 +1151,12 @@ var ParallClient = class _ParallClient {
|
|
|
521
1151
|
throw _ParallClient.normalizeFetchError(err);
|
|
522
1152
|
}
|
|
523
1153
|
if (res.status === 401) {
|
|
524
|
-
const pathSuffix =
|
|
1154
|
+
const pathSuffix = path8.replace(/^\/api\/v1/, "");
|
|
525
1155
|
const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
|
|
526
1156
|
if (!retried && !isAuthPath && this.getRefreshToken) {
|
|
527
1157
|
const refreshed = await this.tryRefresh();
|
|
528
1158
|
if (refreshed) {
|
|
529
|
-
return this.multipartRequest(method,
|
|
1159
|
+
return this.multipartRequest(method, path8, body, true);
|
|
530
1160
|
}
|
|
531
1161
|
}
|
|
532
1162
|
if (this.onTokenExpired && !isAuthPath) {
|
|
@@ -643,6 +1273,9 @@ var ParallClient = class _ParallClient {
|
|
|
643
1273
|
const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_ONLINE(orgId));
|
|
644
1274
|
return res.user_ids ?? [];
|
|
645
1275
|
}
|
|
1276
|
+
async searchRefs(orgId, params) {
|
|
1277
|
+
return this.request("GET", ENDPOINTS.REF_SEARCH(orgId), void 0, params);
|
|
1278
|
+
}
|
|
646
1279
|
async removeOrgMember(orgId, userId) {
|
|
647
1280
|
return this.request("DELETE", ENDPOINTS.ORG_MEMBER(orgId, userId));
|
|
648
1281
|
}
|
|
@@ -696,6 +1329,34 @@ var ParallClient = class _ParallClient {
|
|
|
696
1329
|
async getInvitationByToken(token) {
|
|
697
1330
|
return this.request("GET", ENDPOINTS.INVITATION_BY_TOKEN(token));
|
|
698
1331
|
}
|
|
1332
|
+
// ---- Org-level shareable invite link ----
|
|
1333
|
+
/** Fetch the org's current invite link. Auto-creates on first call
|
|
1334
|
+
* so the settings UI never sees an empty state. */
|
|
1335
|
+
async getOrgInviteLink(orgId) {
|
|
1336
|
+
return this.request("GET", ENDPOINTS.ORG_INVITE_LINK(orgId));
|
|
1337
|
+
}
|
|
1338
|
+
/** Rotate the token in place. The old token stops resolving immediately. */
|
|
1339
|
+
async regenerateOrgInviteLink(orgId) {
|
|
1340
|
+
return this.request("POST", ENDPOINTS.ORG_INVITE_LINK_REGENERATE(orgId));
|
|
1341
|
+
}
|
|
1342
|
+
/** Toggle the require-approval flag on the org's invite link. */
|
|
1343
|
+
async updateOrgInviteLink(orgId, data) {
|
|
1344
|
+
return this.request("PATCH", ENDPOINTS.ORG_INVITE_LINK(orgId), data);
|
|
1345
|
+
}
|
|
1346
|
+
/** Redeem an invite-link token. Caller must be authenticated; the
|
|
1347
|
+
* token is the lookup key so this endpoint is NOT org-scoped. */
|
|
1348
|
+
async joinByInviteLink(token) {
|
|
1349
|
+
return this.request("POST", ENDPOINTS.INVITE_LINK_JOIN, { token });
|
|
1350
|
+
}
|
|
1351
|
+
/** Admin: list pending invite-link join requests for an org. */
|
|
1352
|
+
async listOrgJoinRequests(orgId) {
|
|
1353
|
+
const res = await this.request("GET", ENDPOINTS.ORG_INVITE_LINK_JOIN_REQUESTS(orgId));
|
|
1354
|
+
return res.data;
|
|
1355
|
+
}
|
|
1356
|
+
/** Admin: approve or reject a pending invite-link join request. */
|
|
1357
|
+
async decideOrgJoinRequest(orgId, jrId, decision) {
|
|
1358
|
+
return this.request("POST", ENDPOINTS.ORG_INVITE_LINK_JOIN_REQUEST_DECIDE(orgId, jrId), { decision });
|
|
1359
|
+
}
|
|
699
1360
|
// ---- Direct Messages (org-scoped) ----
|
|
700
1361
|
async sendDirectMessage(orgId, req) {
|
|
701
1362
|
return this.request("POST", ENDPOINTS.DM(orgId), req);
|
|
@@ -965,19 +1626,20 @@ var ParallClient = class _ParallClient {
|
|
|
965
1626
|
}
|
|
966
1627
|
// ---- Daemon-mode Machine management (org-scoped, user auth) ----
|
|
967
1628
|
/**
|
|
968
|
-
* Create a new daemon-mode Machine. Returns the Machine row +
|
|
969
|
-
* mck_ token.
|
|
1629
|
+
* Create a new self-hosted daemon-mode Machine. Returns the Machine row +
|
|
1630
|
+
* one-shot mck_ token.
|
|
970
1631
|
*/
|
|
971
1632
|
async createMachine(orgId, opts) {
|
|
972
1633
|
return this.request("POST", ENDPOINTS.MACHINES(orgId), opts);
|
|
973
1634
|
}
|
|
974
1635
|
/**
|
|
975
|
-
* Returns
|
|
976
|
-
* "Workspaces" UI list. Legacy 1:1 machines are
|
|
1636
|
+
* Returns local daemon_mode=true machines (non-terminated). Backs the
|
|
1637
|
+
* "Workspaces" UI list. Legacy 1:1 and retired hosted daemon machines are
|
|
1638
|
+
* excluded.
|
|
977
1639
|
*/
|
|
978
1640
|
async getDaemonMachines(orgId) {
|
|
979
1641
|
const all = await this.getMachines(orgId);
|
|
980
|
-
return all.filter((m) => m.daemon_mode && m.status !== "terminated");
|
|
1642
|
+
return all.filter((m) => m.daemon_mode && m.compute_mode === "local" && m.status !== "terminated");
|
|
981
1643
|
}
|
|
982
1644
|
/** Attach an agent to a daemon-mode Machine. */
|
|
983
1645
|
async attachAgent(orgId, machineId, agentId, opts) {
|
|
@@ -997,8 +1659,8 @@ var ParallClient = class _ParallClient {
|
|
|
997
1659
|
async retryAgentWorkspaceSetup(orgId, machineId, agentId) {
|
|
998
1660
|
return this.request("POST", ENDPOINTS.MACHINE_AGENT_WORKSPACE_SETUP(orgId, machineId, agentId));
|
|
999
1661
|
}
|
|
1000
|
-
async patchMachineLLMSource(orgId, machineId,
|
|
1001
|
-
return this.request("PATCH", ENDPOINTS.MACHINE_LLM_SOURCE(orgId, machineId), { llm_source:
|
|
1662
|
+
async patchMachineLLMSource(orgId, machineId, llmSource) {
|
|
1663
|
+
return this.request("PATCH", ENDPOINTS.MACHINE_LLM_SOURCE(orgId, machineId), { llm_source: llmSource });
|
|
1002
1664
|
}
|
|
1003
1665
|
/** Get machine-level runtime auth state. */
|
|
1004
1666
|
async getMachineRuntimeAuth(orgId, machineId) {
|
|
@@ -1039,8 +1701,9 @@ var ParallClient = class _ParallClient {
|
|
|
1039
1701
|
* daemon should call this on a fixed cadence (e.g. every 30s) so an
|
|
1040
1702
|
* external observer can detect a wedged supervisor.
|
|
1041
1703
|
*/
|
|
1042
|
-
async postMachineHeartbeat() {
|
|
1043
|
-
|
|
1704
|
+
async postMachineHeartbeat(daemonVersion) {
|
|
1705
|
+
const body = daemonVersion ? { daemon_version: daemonVersion } : void 0;
|
|
1706
|
+
return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH, body);
|
|
1044
1707
|
}
|
|
1045
1708
|
async reportAgentWorkspaceState(agentId, state) {
|
|
1046
1709
|
const res = await this.request("PUT", ENDPOINTS.MACHINES_ME_AGENT_WORKSPACE_STATE(agentId), state);
|
|
@@ -1068,8 +1731,12 @@ var ParallClient = class _ParallClient {
|
|
|
1068
1731
|
async resizeMachine(orgId, machineId, spec) {
|
|
1069
1732
|
return this.request("PATCH", ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
|
|
1070
1733
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1734
|
+
/** Signal a local daemon-mode Machine to check for and apply an update. */
|
|
1735
|
+
async requestMachineUpdate(orgId, machineId, mandatory = false) {
|
|
1736
|
+
await this.request("POST", ENDPOINTS.MACHINE_REQUEST_UPDATE(orgId, machineId), { mandatory });
|
|
1737
|
+
}
|
|
1738
|
+
async browseMachineFilesystem(orgId, machineId, path8) {
|
|
1739
|
+
return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path8 }, void 0, false, { timeoutMs: 15e3 });
|
|
1073
1740
|
}
|
|
1074
1741
|
/** Create a new machine key. Returns the raw key string (shown once) + metadata. */
|
|
1075
1742
|
async createMachineKey(orgId, machineId, name) {
|
|
@@ -1419,8 +2086,8 @@ var ParallClient = class _ParallClient {
|
|
|
1419
2086
|
async deleteWikiPathScope(orgId, wikiId, scopeId) {
|
|
1420
2087
|
await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
|
|
1421
2088
|
}
|
|
1422
|
-
async getWikiAccessStatus(orgId, wikiId,
|
|
1423
|
-
return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0,
|
|
2089
|
+
async getWikiAccessStatus(orgId, wikiId, path8) {
|
|
2090
|
+
return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path8 ? { path: path8 } : void 0);
|
|
1424
2091
|
}
|
|
1425
2092
|
async createWikiAccessRequest(orgId, wikiId, data) {
|
|
1426
2093
|
await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
|
|
@@ -1429,11 +2096,11 @@ var ParallClient = class _ParallClient {
|
|
|
1429
2096
|
async getWikiCommits(orgId, wikiId, params) {
|
|
1430
2097
|
return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
|
|
1431
2098
|
}
|
|
1432
|
-
async getWikiFileCommits(orgId, wikiId,
|
|
1433
|
-
return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path:
|
|
2099
|
+
async getWikiFileCommits(orgId, wikiId, path8, params) {
|
|
2100
|
+
return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path8, ...params });
|
|
1434
2101
|
}
|
|
1435
|
-
async getWikiBlame(orgId, wikiId,
|
|
1436
|
-
return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path:
|
|
2102
|
+
async getWikiBlame(orgId, wikiId, path8, ref) {
|
|
2103
|
+
return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
|
|
1437
2104
|
}
|
|
1438
2105
|
// ---- Wiki Operations (audit log) ----
|
|
1439
2106
|
async getWikiOperations(orgId, wikiId, params) {
|
|
@@ -1917,121 +2584,12 @@ var ParallWs = class {
|
|
|
1917
2584
|
}
|
|
1918
2585
|
};
|
|
1919
2586
|
|
|
1920
|
-
// ts/daemon/dist/
|
|
1921
|
-
|
|
1922
|
-
import * as os from "node:os";
|
|
1923
|
-
import * as path from "node:path";
|
|
1924
|
-
function resolvePath(value) {
|
|
1925
|
-
return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
|
|
1926
|
-
}
|
|
1927
|
-
function parseMs(value, fallback) {
|
|
1928
|
-
if (!value)
|
|
1929
|
-
return fallback;
|
|
1930
|
-
const n = Number(value);
|
|
1931
|
-
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
1932
|
-
}
|
|
1933
|
-
function parseMsAllowZero(value, fallback) {
|
|
1934
|
-
if (value === void 0)
|
|
1935
|
-
return fallback;
|
|
1936
|
-
const n = Number(value);
|
|
1937
|
-
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
1938
|
-
}
|
|
1939
|
-
function daemonConfigDir(env = process.env) {
|
|
1940
|
-
return path.join(env.HOME || os.homedir(), ".parall-daemon");
|
|
1941
|
-
}
|
|
1942
|
-
function daemonConfigPath(env = process.env) {
|
|
1943
|
-
return path.join(daemonConfigDir(env), "config.json");
|
|
1944
|
-
}
|
|
1945
|
-
function tryLoadConfigFile(env) {
|
|
1946
|
-
const cfgPath = daemonConfigPath(env);
|
|
1947
|
-
let content;
|
|
1948
|
-
try {
|
|
1949
|
-
content = fs.readFileSync(cfgPath, "utf-8");
|
|
1950
|
-
} catch (err) {
|
|
1951
|
-
if (err.code === "ENOENT")
|
|
1952
|
-
return null;
|
|
1953
|
-
console.error(`Failed to read daemon config at ${cfgPath}: ${String(err)}`);
|
|
1954
|
-
return null;
|
|
1955
|
-
}
|
|
1956
|
-
try {
|
|
1957
|
-
return JSON.parse(content);
|
|
1958
|
-
} catch (err) {
|
|
1959
|
-
console.error(`Failed to parse daemon config at ${cfgPath}: ${String(err)}`);
|
|
1960
|
-
return null;
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
function resolveClaudeDaemonConfig(env = process.env) {
|
|
1964
|
-
let apiUrl = env.PRLL_API_URL?.trim() || "";
|
|
1965
|
-
let apiKey = env.PRLL_API_KEY?.trim() || "";
|
|
1966
|
-
if (!apiUrl || !apiKey) {
|
|
1967
|
-
const file = tryLoadConfigFile(env);
|
|
1968
|
-
if (file) {
|
|
1969
|
-
if (!apiUrl && file.api_url)
|
|
1970
|
-
apiUrl = file.api_url.trim();
|
|
1971
|
-
if (!apiKey && file.api_key)
|
|
1972
|
-
apiKey = file.api_key.trim();
|
|
1973
|
-
}
|
|
1974
|
-
}
|
|
1975
|
-
if (!apiUrl)
|
|
1976
|
-
throw new Error("Missing required env var: PRLL_API_URL");
|
|
1977
|
-
if (!apiKey)
|
|
1978
|
-
throw new Error("Missing required env var: PRLL_API_KEY");
|
|
1979
|
-
if (!apiKey.startsWith("mck_")) {
|
|
1980
|
-
throw new Error(`PRLL_API_KEY does not look like a Machine bearer (expected prefix "mck_"). Daemon mode requires a machine-scoped key issued via POST /machines/{id}/keys.`);
|
|
1981
|
-
}
|
|
1982
|
-
const rootClaudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
|
|
1983
|
-
const rootStateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(rootClaudeHome, ".parall-agent"));
|
|
1984
|
-
return {
|
|
1985
|
-
apiUrl,
|
|
1986
|
-
apiKey,
|
|
1987
|
-
agentBin: env.PRLL_CLAUDE_AGENT_BIN?.trim() || "parall-claude-agent",
|
|
1988
|
-
rootStateDir,
|
|
1989
|
-
rootClaudeHome,
|
|
1990
|
-
wsUrl: env.PRLL_WS_URL?.trim() || void 0,
|
|
1991
|
-
swimlaneName: env.PRLL_SWIMLANE_NAME?.trim() || void 0,
|
|
1992
|
-
pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 3e4),
|
|
1993
|
-
heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 3e4),
|
|
1994
|
-
restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5e3),
|
|
1995
|
-
restartBackoffMaxMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MAX_MS, 5 * 6e4),
|
|
1996
|
-
bootstrapBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MS, 2e3),
|
|
1997
|
-
bootstrapBackoffMaxMs: parseMs(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MAX_MS, 6e4),
|
|
1998
|
-
supervisorRestartBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS, 5e3),
|
|
1999
|
-
supervisorRestartBackoffMaxMs: parseMs(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MAX_MS, 5 * 6e4)
|
|
2000
|
-
};
|
|
2001
|
-
}
|
|
2002
|
-
function assertSafeAgentId(agentId) {
|
|
2003
|
-
if (!/^[A-Za-z0-9_-]+$/.test(agentId)) {
|
|
2004
|
-
throw new Error(`Invalid agentId for filesystem path: ${agentId}`);
|
|
2005
|
-
}
|
|
2006
|
-
return agentId;
|
|
2007
|
-
}
|
|
2008
|
-
function agentStateDirFor(rootStateDir, agentId) {
|
|
2009
|
-
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId));
|
|
2010
|
-
}
|
|
2011
|
-
function agentClaudeHomeFor(rootClaudeHome, agentId) {
|
|
2012
|
-
return path.join(rootClaudeHome, "agents", assertSafeAgentId(agentId));
|
|
2013
|
-
}
|
|
2014
|
-
function sharedClaudeCredentialsFileFor(rootClaudeHome) {
|
|
2015
|
-
return path.join(rootClaudeHome, ".claude", ".credentials.json");
|
|
2016
|
-
}
|
|
2017
|
-
function agentClaudeCredentialsFileFor(agentClaudeHome) {
|
|
2018
|
-
return path.join(agentClaudeHome, ".claude", ".credentials.json");
|
|
2019
|
-
}
|
|
2020
|
-
function agentWorkspaceDirFor(rootStateDir, agentId) {
|
|
2021
|
-
return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
|
|
2022
|
-
}
|
|
2023
|
-
function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
|
|
2024
|
-
const base = explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
|
|
2025
|
-
if (!swimlaneName)
|
|
2026
|
-
return base;
|
|
2027
|
-
const url = new URL(base);
|
|
2028
|
-
url.searchParams.set("swimlane", swimlaneName);
|
|
2029
|
-
return url.toString();
|
|
2030
|
-
}
|
|
2587
|
+
// ts/daemon/dist/index.js
|
|
2588
|
+
init_config();
|
|
2031
2589
|
|
|
2032
2590
|
// ts/daemon/dist/supervisor.js
|
|
2033
2591
|
import { spawn as spawn2 } from "node:child_process";
|
|
2034
|
-
import * as
|
|
2592
|
+
import * as fs5 from "node:fs";
|
|
2035
2593
|
import * as path5 from "node:path";
|
|
2036
2594
|
|
|
2037
2595
|
// ts/daemon/dist/filesystem.js
|
|
@@ -2148,106 +2706,52 @@ async function listDirectory(dirPath) {
|
|
|
2148
2706
|
return { entries };
|
|
2149
2707
|
}
|
|
2150
2708
|
|
|
2709
|
+
// ts/daemon/dist/supervisor.js
|
|
2710
|
+
init_config();
|
|
2711
|
+
|
|
2151
2712
|
// ts/daemon/dist/runtimes.js
|
|
2713
|
+
import * as fs3 from "node:fs";
|
|
2152
2714
|
import * as path3 from "node:path";
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
delete env.
|
|
2166
|
-
|
|
2167
|
-
delete env.PRLL_CLAUDE_ALLOW_API_KEY;
|
|
2715
|
+
init_config();
|
|
2716
|
+
function buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
|
|
2717
|
+
const env = { ...baseEnv };
|
|
2718
|
+
clearAllProviderCreds(env);
|
|
2719
|
+
env.PRLL_API_KEY = apiKey;
|
|
2720
|
+
env.PRLL_ORG_ID = orgId;
|
|
2721
|
+
env.AGENT_ID = agentId;
|
|
2722
|
+
env.PRLL_AGENT_ID = agentId;
|
|
2723
|
+
env.PRLL_STATE_DIR = dirs.stateDir;
|
|
2724
|
+
env.PRLL_WORKSPACE_DIR = dirs.workspaceDir;
|
|
2725
|
+
if (pc)
|
|
2726
|
+
env.PRLL_PROVIDER_CONFIG = JSON.stringify(pc);
|
|
2727
|
+
delete env.PRLL_DAEMON_MODE;
|
|
2728
|
+
return env;
|
|
2168
2729
|
}
|
|
2169
2730
|
var claudeCodeAdapter = {
|
|
2170
2731
|
bin: "parall-claude-agent",
|
|
2171
2732
|
buildEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
|
|
2172
|
-
const env =
|
|
2173
|
-
clearAllProviderCreds(env);
|
|
2174
|
-
env.PRLL_API_KEY = apiKey;
|
|
2175
|
-
env.PRLL_ORG_ID = orgId;
|
|
2176
|
-
env.AGENT_ID = agentId;
|
|
2177
|
-
env.PRLL_AGENT_ID = agentId;
|
|
2733
|
+
const env = buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc);
|
|
2178
2734
|
env.PRLL_CLAUDE_HOME = dirs.claudeHome;
|
|
2179
|
-
env.PRLL_CLAUDE_STATE_DIR = dirs.stateDir;
|
|
2180
|
-
env.PRLL_CLAUDE_WORKSPACE_DIR = dirs.workspaceDir;
|
|
2181
|
-
const source = llmSource(pc);
|
|
2182
|
-
if (source === "parall") {
|
|
2183
|
-
env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
2184
|
-
env.ANTHROPIC_BASE_URL = `${baseEnv.PRLL_API_URL}/api/llm`;
|
|
2185
|
-
env.PRLL_CLAUDE_ALLOW_API_KEY = "1";
|
|
2186
|
-
} else if (source === "custom") {
|
|
2187
|
-
if (pc?.anthropic_auth_token) {
|
|
2188
|
-
env.ANTHROPIC_AUTH_TOKEN = pc.anthropic_auth_token;
|
|
2189
|
-
env.PRLL_CLAUDE_ALLOW_API_KEY = "1";
|
|
2190
|
-
}
|
|
2191
|
-
if (pc?.anthropic_base_url)
|
|
2192
|
-
env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
|
|
2193
|
-
}
|
|
2194
|
-
delete env.PRLL_DAEMON_MODE;
|
|
2195
2735
|
return env;
|
|
2196
2736
|
}
|
|
2197
2737
|
};
|
|
2198
2738
|
var codexAdapter = {
|
|
2199
2739
|
bin: "parall-codex-agent",
|
|
2200
2740
|
buildEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
|
|
2201
|
-
const env =
|
|
2202
|
-
clearAllProviderCreds(env);
|
|
2203
|
-
env.PRLL_API_KEY = apiKey;
|
|
2204
|
-
env.PRLL_ORG_ID = orgId;
|
|
2205
|
-
env.AGENT_ID = agentId;
|
|
2206
|
-
env.PRLL_AGENT_ID = agentId;
|
|
2207
|
-
env.PRLL_CODEX_STATE_DIR = dirs.stateDir;
|
|
2208
|
-
env.PRLL_CODEX_WORKSPACE_DIR = dirs.workspaceDir;
|
|
2741
|
+
const env = buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc);
|
|
2209
2742
|
env.PRLL_CODEX_HOME = path3.join(dirs.stateDir, ".codex");
|
|
2210
|
-
const source = llmSource(pc);
|
|
2211
|
-
if (source === "parall") {
|
|
2212
|
-
env.OPENAI_API_KEY = apiKey;
|
|
2213
|
-
env.OPENAI_BASE_URL = `${baseEnv.PRLL_API_URL}/api/llm/v1`;
|
|
2214
|
-
} else if (source === "custom") {
|
|
2215
|
-
if (pc?.openai_api_key)
|
|
2216
|
-
env.OPENAI_API_KEY = pc.openai_api_key;
|
|
2217
|
-
if (pc?.openai_base_url)
|
|
2218
|
-
env.OPENAI_BASE_URL = pc.openai_base_url;
|
|
2219
|
-
}
|
|
2220
|
-
delete env.PRLL_DAEMON_MODE;
|
|
2221
2743
|
return env;
|
|
2222
2744
|
}
|
|
2223
2745
|
};
|
|
2224
2746
|
var defaultAdapter = {
|
|
2225
2747
|
bin: "parall-agent",
|
|
2226
|
-
buildEnv
|
|
2227
|
-
const env = { ...baseEnv };
|
|
2228
|
-
env.PRLL_API_KEY = apiKey;
|
|
2229
|
-
env.PRLL_ORG_ID = orgId;
|
|
2230
|
-
env.AGENT_ID = agentId;
|
|
2231
|
-
env.PRLL_AGENT_ID = agentId;
|
|
2232
|
-
env.PRLL_STATE_DIR = dirs.stateDir;
|
|
2233
|
-
env.PRLL_WORKSPACE_DIR = dirs.workspaceDir;
|
|
2234
|
-
delete env.PRLL_DAEMON_MODE;
|
|
2235
|
-
return env;
|
|
2236
|
-
}
|
|
2748
|
+
buildEnv: buildStandardEnv
|
|
2237
2749
|
};
|
|
2238
2750
|
var openclawAdapter = {
|
|
2239
2751
|
bin: "parall-openclaw-agent",
|
|
2240
|
-
buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
|
|
2241
|
-
const env =
|
|
2242
|
-
clearAllProviderCreds(env);
|
|
2243
|
-
env.PRLL_API_KEY = apiKey;
|
|
2244
|
-
env.PRLL_ORG_ID = orgId;
|
|
2245
|
-
env.AGENT_ID = agentId;
|
|
2246
|
-
env.PRLL_AGENT_ID = agentId;
|
|
2247
|
-
env.PRLL_OPENCLAW_STATE_DIR = dirs.stateDir;
|
|
2248
|
-
env.PRLL_OPENCLAW_WORKSPACE_DIR = dirs.workspaceDir;
|
|
2752
|
+
buildEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
|
|
2753
|
+
const env = buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc);
|
|
2249
2754
|
env.OPENCLAW_GATEWAY_PORT = env.OPENCLAW_GATEWAY_PORT || "0";
|
|
2250
|
-
delete env.PRLL_DAEMON_MODE;
|
|
2251
2755
|
return env;
|
|
2252
2756
|
}
|
|
2253
2757
|
};
|
|
@@ -2256,8 +2760,25 @@ var RUNTIME_ADAPTERS = {
|
|
|
2256
2760
|
"codex": codexAdapter,
|
|
2257
2761
|
"openclaw": openclawAdapter
|
|
2258
2762
|
};
|
|
2763
|
+
var OVERLAY_BIN_NAMES = {
|
|
2764
|
+
"claude-code": "parall-claude-agent.js",
|
|
2765
|
+
"codex": "parall-codex-agent.js",
|
|
2766
|
+
"openclaw": "parall-openclaw-agent.js"
|
|
2767
|
+
};
|
|
2259
2768
|
function getRuntimeAdapter(runtimeType) {
|
|
2260
|
-
|
|
2769
|
+
const base = RUNTIME_ADAPTERS[runtimeType] ?? defaultAdapter;
|
|
2770
|
+
const overlayName = OVERLAY_BIN_NAMES[runtimeType];
|
|
2771
|
+
if (!overlayName)
|
|
2772
|
+
return base;
|
|
2773
|
+
try {
|
|
2774
|
+
const bundleDir = resolveBundleDir();
|
|
2775
|
+
const overlayBin = path3.join(bundleDir, "current", overlayName);
|
|
2776
|
+
if (fs3.existsSync(overlayBin)) {
|
|
2777
|
+
return { ...base, bin: overlayBin };
|
|
2778
|
+
}
|
|
2779
|
+
} catch {
|
|
2780
|
+
}
|
|
2781
|
+
return base;
|
|
2261
2782
|
}
|
|
2262
2783
|
function assertAgentKey(apiKey) {
|
|
2263
2784
|
if (apiKey.startsWith("mck_")) {
|
|
@@ -2268,7 +2789,7 @@ function assertAgentKey(apiKey) {
|
|
|
2268
2789
|
// ts/daemon/dist/workspace.js
|
|
2269
2790
|
import { spawn } from "node:child_process";
|
|
2270
2791
|
import { createHash } from "node:crypto";
|
|
2271
|
-
import * as
|
|
2792
|
+
import * as fs4 from "node:fs";
|
|
2272
2793
|
import * as path4 from "node:path";
|
|
2273
2794
|
var OUTPUT_TAIL_LIMIT = 32 * 1024;
|
|
2274
2795
|
var DEFAULT_SETUP_TIMEOUT_SEC = 600;
|
|
@@ -2386,7 +2907,7 @@ function resolveWorkspaceDir(workspace, defaultWorkspaceDir) {
|
|
|
2386
2907
|
async function ensureWorkspace(plan, log2) {
|
|
2387
2908
|
const ws = plan.workspace;
|
|
2388
2909
|
if (ws.mode === "default") {
|
|
2389
|
-
|
|
2910
|
+
fs4.mkdirSync(plan.workspaceDir, { recursive: true });
|
|
2390
2911
|
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2391
2912
|
return;
|
|
2392
2913
|
}
|
|
@@ -2394,7 +2915,7 @@ async function ensureWorkspace(plan, log2) {
|
|
|
2394
2915
|
assertSafeCustomWorkspacePath(plan);
|
|
2395
2916
|
let st;
|
|
2396
2917
|
try {
|
|
2397
|
-
st =
|
|
2918
|
+
st = fs4.statSync(plan.workspaceDir);
|
|
2398
2919
|
} catch (err) {
|
|
2399
2920
|
if (isNodeError(err) && err.code === "ENOENT") {
|
|
2400
2921
|
throw new Error(`workspace path does not exist: ${plan.workspaceDir}`);
|
|
@@ -2404,7 +2925,7 @@ async function ensureWorkspace(plan, log2) {
|
|
|
2404
2925
|
if (!st.isDirectory()) {
|
|
2405
2926
|
throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
|
|
2406
2927
|
}
|
|
2407
|
-
assertSafeCustomWorkspacePath(plan,
|
|
2928
|
+
assertSafeCustomWorkspacePath(plan, fs4.realpathSync(plan.workspaceDir));
|
|
2408
2929
|
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2409
2930
|
return;
|
|
2410
2931
|
}
|
|
@@ -2415,17 +2936,17 @@ async function ensureWorkspace(plan, log2) {
|
|
|
2415
2936
|
if (plan.customWorkspaceField) {
|
|
2416
2937
|
assertSafeCustomWorkspacePath(plan);
|
|
2417
2938
|
}
|
|
2418
|
-
if (!
|
|
2419
|
-
|
|
2939
|
+
if (!fs4.existsSync(plan.workspaceDir)) {
|
|
2940
|
+
fs4.mkdirSync(path4.dirname(plan.workspaceDir), { recursive: true });
|
|
2420
2941
|
assertWritableWorkspaceDir(path4.dirname(plan.workspaceDir));
|
|
2421
2942
|
await runCommand("git", ["clone", remote, plan.workspaceDir], process.cwd());
|
|
2422
2943
|
} else {
|
|
2423
|
-
const st =
|
|
2944
|
+
const st = fs4.statSync(plan.workspaceDir);
|
|
2424
2945
|
if (!st.isDirectory()) {
|
|
2425
2946
|
throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
|
|
2426
2947
|
}
|
|
2427
2948
|
if (plan.customWorkspaceField) {
|
|
2428
|
-
assertSafeCustomWorkspacePath(plan,
|
|
2949
|
+
assertSafeCustomWorkspacePath(plan, fs4.realpathSync(plan.workspaceDir));
|
|
2429
2950
|
}
|
|
2430
2951
|
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2431
2952
|
await ensureGitWorktree(plan.workspaceDir);
|
|
@@ -2449,13 +2970,13 @@ async function verifyExistingWorkspace(plan, log2) {
|
|
|
2449
2970
|
if (plan.customWorkspaceField) {
|
|
2450
2971
|
assertSafeCustomWorkspacePath(plan);
|
|
2451
2972
|
}
|
|
2452
|
-
const st =
|
|
2973
|
+
const st = fs4.statSync(plan.workspaceDir);
|
|
2453
2974
|
if (!st.isDirectory()) {
|
|
2454
2975
|
log2.warn(`workspace ready state ignored: path is not a directory: ${plan.workspaceDir}`);
|
|
2455
2976
|
return false;
|
|
2456
2977
|
}
|
|
2457
2978
|
if (plan.customWorkspaceField) {
|
|
2458
|
-
assertSafeCustomWorkspacePath(plan,
|
|
2979
|
+
assertSafeCustomWorkspacePath(plan, fs4.realpathSync(plan.workspaceDir));
|
|
2459
2980
|
}
|
|
2460
2981
|
assertWritableWorkspaceDir(plan.workspaceDir);
|
|
2461
2982
|
if (plan.workspace.mode === "git") {
|
|
@@ -2618,11 +3139,11 @@ function assertSafeCustomWorkspacePath(plan, candidate = plan.workspaceDir) {
|
|
|
2618
3139
|
}
|
|
2619
3140
|
}
|
|
2620
3141
|
function assertWritableWorkspaceDir(dir) {
|
|
2621
|
-
|
|
3142
|
+
fs4.accessSync(dir, fs4.constants.R_OK | fs4.constants.W_OK | fs4.constants.X_OK);
|
|
2622
3143
|
const probe = path4.join(dir, `.parall-workspace-check-${process.pid}-${Date.now()}`);
|
|
2623
|
-
const fd =
|
|
2624
|
-
|
|
2625
|
-
|
|
3144
|
+
const fd = fs4.openSync(probe, "wx", 384);
|
|
3145
|
+
fs4.closeSync(fd);
|
|
3146
|
+
fs4.unlinkSync(probe);
|
|
2626
3147
|
}
|
|
2627
3148
|
function workspacePathDenyReason(value) {
|
|
2628
3149
|
if (value === "/")
|
|
@@ -2729,11 +3250,16 @@ var DaemonSupervisor = class {
|
|
|
2729
3250
|
machineOrgId = null;
|
|
2730
3251
|
machineLlmSource = "parall";
|
|
2731
3252
|
stopResolve = null;
|
|
3253
|
+
updater = null;
|
|
3254
|
+
healthConfirmed = false;
|
|
2732
3255
|
constructor(config, client, log2) {
|
|
2733
3256
|
this.config = config;
|
|
2734
3257
|
this.client = client;
|
|
2735
3258
|
this.log = log2;
|
|
2736
3259
|
}
|
|
3260
|
+
setUpdater(updater) {
|
|
3261
|
+
this.updater = updater;
|
|
3262
|
+
}
|
|
2737
3263
|
/** Start the supervisor. Returns a promise that resolves on `stop()`. */
|
|
2738
3264
|
async run(signal) {
|
|
2739
3265
|
if (this.running)
|
|
@@ -2755,6 +3281,10 @@ var DaemonSupervisor = class {
|
|
|
2755
3281
|
throw err;
|
|
2756
3282
|
}
|
|
2757
3283
|
this.migrateFlatLayout();
|
|
3284
|
+
const daemonVersion = this.updater?.getLocalVersion();
|
|
3285
|
+
if (daemonVersion) {
|
|
3286
|
+
this.client.postMachineHeartbeat(daemonVersion).catch((err) => this.log.warn(`daemon version report failed: ${String(err)}`));
|
|
3287
|
+
}
|
|
2758
3288
|
await this.fullReconcile();
|
|
2759
3289
|
this.ws = new ParallWs({
|
|
2760
3290
|
getTicket: () => this.client.getMachineWsTicket(),
|
|
@@ -2763,11 +3293,33 @@ var DaemonSupervisor = class {
|
|
|
2763
3293
|
});
|
|
2764
3294
|
this.ws.on("machine.hello", (_data) => {
|
|
2765
3295
|
this.log.info("machine WS connected (machine.hello)");
|
|
3296
|
+
if (!this.healthConfirmed && this.updater) {
|
|
3297
|
+
try {
|
|
3298
|
+
this.updater.confirmVersion();
|
|
3299
|
+
this.healthConfirmed = true;
|
|
3300
|
+
} catch (err) {
|
|
3301
|
+
this.log.warn(`confirmVersion failed: ${String(err)}`);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
2766
3304
|
void (async () => {
|
|
2767
3305
|
await this.refreshMachineConfig();
|
|
2768
3306
|
await this.fullReconcile();
|
|
2769
3307
|
})();
|
|
2770
3308
|
});
|
|
3309
|
+
this.ws.on("machine.update", (data) => {
|
|
3310
|
+
this.log.info(`WS: daemon update available \u2014 version=${data.new_version} mandatory=${data.mandatory}`);
|
|
3311
|
+
if (this.updater) {
|
|
3312
|
+
void this.updater.triggerUpdate(data.new_version, data.mandatory).then(async (applied) => {
|
|
3313
|
+
if (applied) {
|
|
3314
|
+
this.log.info("daemon update applied \u2014 stopping supervisor before restart");
|
|
3315
|
+
await this.stop();
|
|
3316
|
+
process.exit(42);
|
|
3317
|
+
}
|
|
3318
|
+
}).catch((err) => {
|
|
3319
|
+
this.log.warn(`daemon update failed: ${String(err)}`);
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
});
|
|
2771
3323
|
this.ws.on("machine.agent.attached", (data) => {
|
|
2772
3324
|
this.log.info(`WS: agent ${data.agent_id} attached`);
|
|
2773
3325
|
void this.handleAgentAttached(data.agent_id);
|
|
@@ -2924,13 +3476,13 @@ var DaemonSupervisor = class {
|
|
|
2924
3476
|
const root = this.config.rootStateDir;
|
|
2925
3477
|
const agentsDir = path5.join(root, "agents");
|
|
2926
3478
|
const flatWorkspace = path5.join(root, "workspace");
|
|
2927
|
-
if (!
|
|
3479
|
+
if (!fs5.existsSync(flatWorkspace) || fs5.existsSync(agentsDir))
|
|
2928
3480
|
return;
|
|
2929
3481
|
let ownerAgentId;
|
|
2930
3482
|
const sessionsDir = path5.join(root, "sessions");
|
|
2931
|
-
if (
|
|
3483
|
+
if (fs5.existsSync(sessionsDir)) {
|
|
2932
3484
|
try {
|
|
2933
|
-
for (const file of
|
|
3485
|
+
for (const file of fs5.readdirSync(sessionsDir)) {
|
|
2934
3486
|
if (!file.endsWith(".json"))
|
|
2935
3487
|
continue;
|
|
2936
3488
|
const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
|
|
@@ -2946,11 +3498,11 @@ var DaemonSupervisor = class {
|
|
|
2946
3498
|
const targetId = ownerAgentId ?? "_orphan";
|
|
2947
3499
|
const targetDir = path5.join(agentsDir, targetId);
|
|
2948
3500
|
try {
|
|
2949
|
-
|
|
3501
|
+
fs5.mkdirSync(targetDir, { recursive: true });
|
|
2950
3502
|
for (const sub of ["workspace", "sessions", "dispatch-context"]) {
|
|
2951
3503
|
const src = path5.join(root, sub);
|
|
2952
|
-
if (
|
|
2953
|
-
|
|
3504
|
+
if (fs5.existsSync(src)) {
|
|
3505
|
+
fs5.renameSync(src, path5.join(targetDir, sub));
|
|
2954
3506
|
}
|
|
2955
3507
|
}
|
|
2956
3508
|
this.log.info(`migrated legacy flat state \u2192 agents/${targetId}/`);
|
|
@@ -3157,9 +3709,9 @@ var DaemonSupervisor = class {
|
|
|
3157
3709
|
const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
|
|
3158
3710
|
const claudeHome = isK8s ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId) : this.config.rootClaudeHome;
|
|
3159
3711
|
try {
|
|
3160
|
-
|
|
3712
|
+
fs5.mkdirSync(stateDir, { recursive: true });
|
|
3161
3713
|
if (isK8s) {
|
|
3162
|
-
|
|
3714
|
+
fs5.mkdirSync(claudeHome, { recursive: true });
|
|
3163
3715
|
this.ensureSharedCredentialLink(claudeHome, agentId);
|
|
3164
3716
|
}
|
|
3165
3717
|
} catch (err) {
|
|
@@ -3288,34 +3840,38 @@ var DaemonSupervisor = class {
|
|
|
3288
3840
|
const sharedCredentials = path5.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
|
|
3289
3841
|
const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
|
|
3290
3842
|
const agentCredentialsDir = path5.dirname(agentCredentials);
|
|
3291
|
-
|
|
3292
|
-
|
|
3843
|
+
fs5.mkdirSync(path5.dirname(sharedCredentials), { recursive: true });
|
|
3844
|
+
fs5.mkdirSync(agentCredentialsDir, { recursive: true });
|
|
3293
3845
|
try {
|
|
3294
|
-
const existing =
|
|
3846
|
+
const existing = fs5.lstatSync(agentCredentials);
|
|
3295
3847
|
if (existing.isSymbolicLink()) {
|
|
3296
|
-
const currentTarget =
|
|
3848
|
+
const currentTarget = fs5.readlinkSync(agentCredentials);
|
|
3297
3849
|
if (path5.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
|
|
3298
3850
|
return;
|
|
3299
3851
|
}
|
|
3300
|
-
|
|
3852
|
+
fs5.unlinkSync(agentCredentials);
|
|
3301
3853
|
} else if (existing.isDirectory()) {
|
|
3302
3854
|
this.log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
|
|
3303
3855
|
return;
|
|
3304
3856
|
} else {
|
|
3305
|
-
|
|
3857
|
+
fs5.unlinkSync(agentCredentials);
|
|
3306
3858
|
}
|
|
3307
3859
|
} catch (err) {
|
|
3308
3860
|
if (err.code !== "ENOENT") {
|
|
3309
3861
|
throw err;
|
|
3310
3862
|
}
|
|
3311
3863
|
}
|
|
3312
|
-
|
|
3864
|
+
fs5.symlinkSync(sharedCredentials, agentCredentials);
|
|
3313
3865
|
}
|
|
3314
3866
|
};
|
|
3315
3867
|
|
|
3868
|
+
// ts/daemon/dist/index.js
|
|
3869
|
+
init_updater();
|
|
3870
|
+
|
|
3316
3871
|
// ts/daemon/dist/cli.js
|
|
3317
|
-
|
|
3318
|
-
import * as
|
|
3872
|
+
init_config();
|
|
3873
|
+
import * as fs7 from "node:fs";
|
|
3874
|
+
import * as path7 from "node:path";
|
|
3319
3875
|
import * as os3 from "node:os";
|
|
3320
3876
|
import * as readline from "node:readline";
|
|
3321
3877
|
import { spawn as spawn3, execSync } from "node:child_process";
|
|
@@ -3323,15 +3879,15 @@ var CONFIG_DIR = daemonConfigDir();
|
|
|
3323
3879
|
var CONFIG_PATH = daemonConfigPath();
|
|
3324
3880
|
function readConfig() {
|
|
3325
3881
|
try {
|
|
3326
|
-
return JSON.parse(
|
|
3882
|
+
return JSON.parse(fs7.readFileSync(CONFIG_PATH, "utf-8"));
|
|
3327
3883
|
} catch {
|
|
3328
3884
|
return null;
|
|
3329
3885
|
}
|
|
3330
3886
|
}
|
|
3331
3887
|
function writeConfig(config) {
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3888
|
+
fs7.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
3889
|
+
fs7.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
|
|
3890
|
+
fs7.chmodSync(CONFIG_PATH, 384);
|
|
3335
3891
|
}
|
|
3336
3892
|
function prompt(question) {
|
|
3337
3893
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -3350,10 +3906,10 @@ function isLinux() {
|
|
|
3350
3906
|
}
|
|
3351
3907
|
var PLIST_LABEL = "com.parall.daemon";
|
|
3352
3908
|
function plistPath() {
|
|
3353
|
-
return
|
|
3909
|
+
return path7.join(os3.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
3354
3910
|
}
|
|
3355
3911
|
function systemdUnitPath() {
|
|
3356
|
-
return
|
|
3912
|
+
return path7.join(os3.homedir(), ".config", "systemd", "user", "parall-daemon.service");
|
|
3357
3913
|
}
|
|
3358
3914
|
function getDaemonBin() {
|
|
3359
3915
|
try {
|
|
@@ -3363,7 +3919,7 @@ function getDaemonBin() {
|
|
|
3363
3919
|
}
|
|
3364
3920
|
}
|
|
3365
3921
|
function generatePlist(daemonBin) {
|
|
3366
|
-
const logPath =
|
|
3922
|
+
const logPath = path7.join(os3.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
3367
3923
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3368
3924
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3369
3925
|
<plist version="1.0">
|
|
@@ -3372,7 +3928,9 @@ function generatePlist(daemonBin) {
|
|
|
3372
3928
|
<string>${PLIST_LABEL}</string>
|
|
3373
3929
|
<key>ProgramArguments</key>
|
|
3374
3930
|
<array>
|
|
3375
|
-
<string
|
|
3931
|
+
<string>/bin/sh</string>
|
|
3932
|
+
<string>-c</string>
|
|
3933
|
+
<string>OVERLAY="$HOME/.parall-daemon/bundle/current/parall-daemon.js"; if [ -f "$OVERLAY" ]; then exec node "$OVERLAY"; else exec ${daemonBin}; fi</string>
|
|
3376
3934
|
</array>
|
|
3377
3935
|
<key>RunAtLoad</key>
|
|
3378
3936
|
<true/>
|
|
@@ -3395,7 +3953,7 @@ Wants=network-online.target
|
|
|
3395
3953
|
|
|
3396
3954
|
[Service]
|
|
3397
3955
|
Type=simple
|
|
3398
|
-
ExecStart
|
|
3956
|
+
ExecStart=/bin/sh -c 'OVERLAY="$HOME/.parall-daemon/bundle/current/parall-daemon.js"; if [ -f "$OVERLAY" ]; then exec node "$OVERLAY"; else exec ${daemonBin}; fi'
|
|
3399
3957
|
Restart=always
|
|
3400
3958
|
RestartSec=5
|
|
3401
3959
|
|
|
@@ -3410,16 +3968,16 @@ function installService() {
|
|
|
3410
3968
|
}
|
|
3411
3969
|
const bin = getDaemonBin();
|
|
3412
3970
|
if (isMacOS()) {
|
|
3413
|
-
const dir =
|
|
3414
|
-
|
|
3415
|
-
|
|
3971
|
+
const dir = path7.dirname(plistPath());
|
|
3972
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
3973
|
+
fs7.writeFileSync(plistPath(), generatePlist(bin));
|
|
3416
3974
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
3417
3975
|
execSync(`launchctl bootstrap gui/$(id -u) ${plistPath()}`);
|
|
3418
3976
|
console.log(`launchd agent installed: ${plistPath()}`);
|
|
3419
3977
|
} else if (isLinux()) {
|
|
3420
|
-
const dir =
|
|
3421
|
-
|
|
3422
|
-
|
|
3978
|
+
const dir = path7.dirname(systemdUnitPath());
|
|
3979
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
3980
|
+
fs7.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
|
|
3423
3981
|
execSync("systemctl --user daemon-reload");
|
|
3424
3982
|
execSync("systemctl --user enable --now parall-daemon");
|
|
3425
3983
|
console.log(`systemd service installed: ${systemdUnitPath()}`);
|
|
@@ -3486,8 +4044,8 @@ function cmdLogs(lines) {
|
|
|
3486
4044
|
child2.on("exit", (code) => process.exit(code ?? 0));
|
|
3487
4045
|
return;
|
|
3488
4046
|
}
|
|
3489
|
-
const logPath =
|
|
3490
|
-
if (!
|
|
4047
|
+
const logPath = path7.join(os3.homedir(), "Library", "Logs", "parall-daemon.log");
|
|
4048
|
+
if (!fs7.existsSync(logPath)) {
|
|
3491
4049
|
console.log("No log file found at", logPath);
|
|
3492
4050
|
return;
|
|
3493
4051
|
}
|
|
@@ -3497,20 +4055,56 @@ function cmdLogs(lines) {
|
|
|
3497
4055
|
function cmdServiceUninstall() {
|
|
3498
4056
|
if (isMacOS()) {
|
|
3499
4057
|
execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
|
|
3500
|
-
if (
|
|
3501
|
-
|
|
4058
|
+
if (fs7.existsSync(plistPath()))
|
|
4059
|
+
fs7.unlinkSync(plistPath());
|
|
3502
4060
|
console.log("launchd agent uninstalled.");
|
|
3503
4061
|
} else if (isLinux()) {
|
|
3504
4062
|
execSync("systemctl --user stop parall-daemon 2>/dev/null || true");
|
|
3505
4063
|
execSync("systemctl --user disable parall-daemon 2>/dev/null || true");
|
|
3506
|
-
if (
|
|
3507
|
-
|
|
4064
|
+
if (fs7.existsSync(systemdUnitPath()))
|
|
4065
|
+
fs7.unlinkSync(systemdUnitPath());
|
|
3508
4066
|
execSync("systemctl --user daemon-reload");
|
|
3509
4067
|
console.log("systemd service uninstalled.");
|
|
3510
4068
|
} else {
|
|
3511
4069
|
console.log("Unsupported platform.");
|
|
3512
4070
|
}
|
|
3513
4071
|
}
|
|
4072
|
+
async function cmdUpdate(checkOnly) {
|
|
4073
|
+
const { resolveClaudeDaemonConfig: resolveClaudeDaemonConfig2, resolveBundleDir: resolveBundleDir2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
4074
|
+
const { DaemonUpdater: DaemonUpdater2 } = await Promise.resolve().then(() => (init_updater(), updater_exports));
|
|
4075
|
+
const config = resolveClaudeDaemonConfig2(process.env);
|
|
4076
|
+
const bundleDir = resolveBundleDir2(process.env);
|
|
4077
|
+
const signingEnabled = !!process.env.PRLL_DAEMON_SIGNING_PUBLIC_KEY;
|
|
4078
|
+
const updater = new DaemonUpdater2(bundleDir, config.updateCdnUrl, {
|
|
4079
|
+
info: (msg) => console.log(msg),
|
|
4080
|
+
warn: (msg) => console.warn(msg),
|
|
4081
|
+
error: (msg) => console.error(msg)
|
|
4082
|
+
}, signingEnabled);
|
|
4083
|
+
const local = updater.getLocalVersion();
|
|
4084
|
+
console.log(`Current version: ${local ?? "unknown"}`);
|
|
4085
|
+
console.log(`CDN: ${config.updateCdnUrl}`);
|
|
4086
|
+
console.log(`Bundle dir: ${bundleDir}`);
|
|
4087
|
+
if (checkOnly) {
|
|
4088
|
+
console.log("\nChecking for updates...");
|
|
4089
|
+
const result = await updater.checkAvailable();
|
|
4090
|
+
if (result.available) {
|
|
4091
|
+
console.log(`Update available: ${result.currentVersion ?? "unknown"} \u2192 ${result.remoteVersion}`);
|
|
4092
|
+
} else if (result.remoteVersion) {
|
|
4093
|
+
console.log(`Already up to date (${result.currentVersion}).`);
|
|
4094
|
+
} else {
|
|
4095
|
+
console.log("Could not check for updates.");
|
|
4096
|
+
}
|
|
4097
|
+
return;
|
|
4098
|
+
} else {
|
|
4099
|
+
console.log("\nChecking and applying updates...");
|
|
4100
|
+
}
|
|
4101
|
+
const applied = await updater.checkAndApply();
|
|
4102
|
+
if (applied) {
|
|
4103
|
+
console.log("Update applied. Restart the daemon to use the new version.");
|
|
4104
|
+
} else {
|
|
4105
|
+
console.log("Already up to date.");
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
3514
4108
|
function printUsage() {
|
|
3515
4109
|
console.log(`
|
|
3516
4110
|
parall-daemon \u2014 Parall local agent runtime
|
|
@@ -3520,6 +4114,7 @@ Usage:
|
|
|
3520
4114
|
parall-daemon init Configure the daemon (interactive)
|
|
3521
4115
|
parall-daemon status Show daemon service status
|
|
3522
4116
|
parall-daemon stop Stop the background service
|
|
4117
|
+
parall-daemon update [--check] Check for / apply daemon updates
|
|
3523
4118
|
parall-daemon logs [-n LINES] Tail daemon logs
|
|
3524
4119
|
parall-daemon service install Install as background service (launchd/systemd)
|
|
3525
4120
|
parall-daemon service uninstall Uninstall background service
|
|
@@ -3551,6 +4146,9 @@ async function runCLI(args) {
|
|
|
3551
4146
|
cmdLogs(lines);
|
|
3552
4147
|
return "handled";
|
|
3553
4148
|
}
|
|
4149
|
+
case "update":
|
|
4150
|
+
await cmdUpdate(args.includes("--check"));
|
|
4151
|
+
return "handled";
|
|
3554
4152
|
case "service": {
|
|
3555
4153
|
const sub = args[1];
|
|
3556
4154
|
if (sub === "install") {
|
|
@@ -3580,6 +4178,7 @@ async function runCLI(args) {
|
|
|
3580
4178
|
}
|
|
3581
4179
|
|
|
3582
4180
|
// ts/daemon/dist/index.js
|
|
4181
|
+
var UPDATE_EXIT_CODE = 42;
|
|
3583
4182
|
var log = createLogger("daemon");
|
|
3584
4183
|
function formatError(reason) {
|
|
3585
4184
|
if (reason instanceof Error) {
|
|
@@ -3587,10 +4186,12 @@ function formatError(reason) {
|
|
|
3587
4186
|
}
|
|
3588
4187
|
return String(reason);
|
|
3589
4188
|
}
|
|
3590
|
-
async function runForever(config, client, log2, signal) {
|
|
4189
|
+
async function runForever(config, client, log2, signal, updater) {
|
|
3591
4190
|
let attempt = 0;
|
|
3592
4191
|
while (!signal.aborted) {
|
|
3593
4192
|
const supervisor = new DaemonSupervisor(config, client, log2);
|
|
4193
|
+
if (updater)
|
|
4194
|
+
supervisor.setUpdater(updater);
|
|
3594
4195
|
try {
|
|
3595
4196
|
await supervisor.run(signal);
|
|
3596
4197
|
await supervisor.stop();
|
|
@@ -3619,6 +4220,16 @@ async function main() {
|
|
|
3619
4220
|
const config = resolveClaudeDaemonConfig(process.env);
|
|
3620
4221
|
log.info(`boot: api=${config.apiUrl} pollMs=${config.pollIntervalMs} heartbeatMs=${config.heartbeatIntervalMs} agentBin=${config.agentBin}`);
|
|
3621
4222
|
log.info(`keepalive: bootstrapBackoffMs=${config.bootstrapBackoffMs} supervisorRestartBackoffMs=${config.supervisorRestartBackoffMs}`);
|
|
4223
|
+
let updater = null;
|
|
4224
|
+
if (!config.updateDisabled) {
|
|
4225
|
+
const bundleDir = resolveBundleDir(process.env);
|
|
4226
|
+
updater = new DaemonUpdater(bundleDir, config.updateCdnUrl, log, true);
|
|
4227
|
+
if (updater.checkRollback()) {
|
|
4228
|
+
log.info("rollback applied \u2014 exiting for service manager restart");
|
|
4229
|
+
process.exit(UPDATE_EXIT_CODE);
|
|
4230
|
+
}
|
|
4231
|
+
log.info(`update: bundleDir=${bundleDir} cdn=${config.updateCdnUrl} interval=${config.updateIntervalMs}ms`);
|
|
4232
|
+
}
|
|
3622
4233
|
const client = new ParallClient({
|
|
3623
4234
|
baseUrl: config.apiUrl,
|
|
3624
4235
|
token: config.apiKey,
|
|
@@ -3640,7 +4251,18 @@ async function main() {
|
|
|
3640
4251
|
process.exit(1);
|
|
3641
4252
|
});
|
|
3642
4253
|
config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
|
|
3643
|
-
|
|
4254
|
+
if (updater) {
|
|
4255
|
+
const applied = await updater.checkAndApply().catch((err) => {
|
|
4256
|
+
log.warn(`boot update check failed: ${String(err)}`);
|
|
4257
|
+
return false;
|
|
4258
|
+
});
|
|
4259
|
+
if (applied) {
|
|
4260
|
+
log.info("boot update applied \u2014 exiting for restart");
|
|
4261
|
+
process.exit(UPDATE_EXIT_CODE);
|
|
4262
|
+
}
|
|
4263
|
+
updater.startPeriodicCheck(config.updateIntervalMs);
|
|
4264
|
+
}
|
|
4265
|
+
await runForever(config, client, log, abortController.signal, updater);
|
|
3644
4266
|
}
|
|
3645
4267
|
var cliArgs = process.argv.slice(2);
|
|
3646
4268
|
runCLI(cliArgs).then((result) => {
|