@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.
@@ -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(path7) {
1030
+ async ensureFreshToken(path8) {
401
1031
  if (!this.token || !this.getRefreshToken)
402
1032
  return;
403
- const pathSuffix = path7.replace(/^\/api\/v1/, "");
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, path7, body, query, retried = false, opts) {
1065
+ async request(method, path8, body, query, retried = false, opts) {
436
1066
  if (!retried) {
437
- await this.ensureFreshToken(path7);
1067
+ await this.ensureFreshToken(path8);
438
1068
  }
439
- let url = `${this.baseUrl}${path7}`;
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 = path7.replace(/^\/api\/v1/, "");
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, path7, body, query, true, opts);
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, path7, body, retried = false) {
1136
+ async multipartRequest(method, path8, body, retried = false) {
507
1137
  if (!retried) {
508
- await this.ensureFreshToken(path7);
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}${path7}`, {
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 = path7.replace(/^\/api\/v1/, "");
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, path7, body, true);
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 + one-shot
969
- * mck_ token. The UI uses this when the user clicks "Create Workspace".
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 only daemon_mode=true machines (non-terminated). Backs the
976
- * "Workspaces" UI list. Legacy 1:1 machines are excluded.
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, llmSource2) {
1001
- return this.request("PATCH", ENDPOINTS.MACHINE_LLM_SOURCE(orgId, machineId), { llm_source: llmSource2 });
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
- return this.request("POST", ENDPOINTS.MACHINES_ME_HEALTH);
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
- async browseMachineFilesystem(orgId, machineId, path7) {
1072
- return this.request("POST", ENDPOINTS.MACHINE_BROWSE(orgId, machineId), { path: path7 }, void 0, false, { timeoutMs: 15e3 });
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, path7) {
1423
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path7 ? { path: path7 } : 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, path7, params) {
1433
- return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path7, ...params });
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, path7, ref) {
1436
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path7, ref });
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/config.js
1921
- import * as fs from "node:fs";
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 fs4 from "node:fs";
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
- function llmSource(pc) {
2154
- if (pc?.llm_source)
2155
- return pc.llm_source;
2156
- if (pc?.openai_api_key || pc?.openai_base_url || pc?.anthropic_auth_token || pc?.anthropic_base_url) {
2157
- return "custom";
2158
- }
2159
- return "parall";
2160
- }
2161
- function clearAllProviderCreds(env) {
2162
- delete env.ANTHROPIC_AUTH_TOKEN;
2163
- delete env.ANTHROPIC_BASE_URL;
2164
- delete env.ANTHROPIC_API_KEY;
2165
- delete env.OPENAI_API_KEY;
2166
- delete env.OPENAI_BASE_URL;
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 = { ...baseEnv };
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 = { ...baseEnv };
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(baseEnv, agentId, orgId, apiKey, dirs) {
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 = { ...baseEnv };
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
- return RUNTIME_ADAPTERS[runtimeType] ?? defaultAdapter;
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 fs3 from "node:fs";
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
- fs3.mkdirSync(plan.workspaceDir, { recursive: true });
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 = fs3.statSync(plan.workspaceDir);
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, fs3.realpathSync(plan.workspaceDir));
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 (!fs3.existsSync(plan.workspaceDir)) {
2419
- fs3.mkdirSync(path4.dirname(plan.workspaceDir), { recursive: true });
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 = fs3.statSync(plan.workspaceDir);
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, fs3.realpathSync(plan.workspaceDir));
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 = fs3.statSync(plan.workspaceDir);
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, fs3.realpathSync(plan.workspaceDir));
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
- fs3.accessSync(dir, fs3.constants.R_OK | fs3.constants.W_OK | fs3.constants.X_OK);
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 = fs3.openSync(probe, "wx", 384);
2624
- fs3.closeSync(fd);
2625
- fs3.unlinkSync(probe);
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 (!fs4.existsSync(flatWorkspace) || fs4.existsSync(agentsDir))
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 (fs4.existsSync(sessionsDir)) {
3483
+ if (fs5.existsSync(sessionsDir)) {
2932
3484
  try {
2933
- for (const file of fs4.readdirSync(sessionsDir)) {
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
- fs4.mkdirSync(targetDir, { recursive: true });
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 (fs4.existsSync(src)) {
2953
- fs4.renameSync(src, path5.join(targetDir, sub));
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
- fs4.mkdirSync(stateDir, { recursive: true });
3712
+ fs5.mkdirSync(stateDir, { recursive: true });
3161
3713
  if (isK8s) {
3162
- fs4.mkdirSync(claudeHome, { recursive: true });
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
- fs4.mkdirSync(path5.dirname(sharedCredentials), { recursive: true });
3292
- fs4.mkdirSync(agentCredentialsDir, { recursive: true });
3843
+ fs5.mkdirSync(path5.dirname(sharedCredentials), { recursive: true });
3844
+ fs5.mkdirSync(agentCredentialsDir, { recursive: true });
3293
3845
  try {
3294
- const existing = fs4.lstatSync(agentCredentials);
3846
+ const existing = fs5.lstatSync(agentCredentials);
3295
3847
  if (existing.isSymbolicLink()) {
3296
- const currentTarget = fs4.readlinkSync(agentCredentials);
3848
+ const currentTarget = fs5.readlinkSync(agentCredentials);
3297
3849
  if (path5.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
3298
3850
  return;
3299
3851
  }
3300
- fs4.unlinkSync(agentCredentials);
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
- fs4.unlinkSync(agentCredentials);
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
- fs4.symlinkSync(sharedCredentials, agentCredentials);
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
- import * as fs5 from "node:fs";
3318
- import * as path6 from "node:path";
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(fs5.readFileSync(CONFIG_PATH, "utf-8"));
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
- fs5.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
3333
- fs5.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
3334
- fs5.chmodSync(CONFIG_PATH, 384);
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 path6.join(os3.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
3909
+ return path7.join(os3.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
3354
3910
  }
3355
3911
  function systemdUnitPath() {
3356
- return path6.join(os3.homedir(), ".config", "systemd", "user", "parall-daemon.service");
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 = path6.join(os3.homedir(), "Library", "Logs", "parall-daemon.log");
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>${daemonBin}</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=${daemonBin}
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 = path6.dirname(plistPath());
3414
- fs5.mkdirSync(dir, { recursive: true });
3415
- fs5.writeFileSync(plistPath(), generatePlist(bin));
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 = path6.dirname(systemdUnitPath());
3421
- fs5.mkdirSync(dir, { recursive: true });
3422
- fs5.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
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 = path6.join(os3.homedir(), "Library", "Logs", "parall-daemon.log");
3490
- if (!fs5.existsSync(logPath)) {
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 (fs5.existsSync(plistPath()))
3501
- fs5.unlinkSync(plistPath());
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 (fs5.existsSync(systemdUnitPath()))
3507
- fs5.unlinkSync(systemdUnitPath());
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
- await runForever(config, client, log, abortController.signal);
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) => {