@parall/daemon 1.29.3 → 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,11 +1,630 @@
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) {
5
623
  return {
6
624
  info: (msg) => console.log(`${(/* @__PURE__ */ new Date()).toISOString()} [${prefix}] ${msg}`),
7
625
  warn: (msg) => console.warn(`${(/* @__PURE__ */ new Date()).toISOString()} [${prefix}] ${msg}`),
8
- error: (msg) => console.error(`${(/* @__PURE__ */ new Date()).toISOString()} [${prefix}] ${msg}`)
626
+ error: (msg) => console.error(`${(/* @__PURE__ */ new Date()).toISOString()} [${prefix}] ${msg}`),
627
+ child: (sub) => createLogger(`${prefix}:${sub}`)
9
628
  };
10
629
  }
11
630
 
@@ -39,6 +658,7 @@ var ENDPOINTS = {
39
658
  ORG_MEMBER: (orgId, userId) => `${API_BASE}/orgs/${orgId}/members/${userId}`,
40
659
  ORG_MEMBER_CHATS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/chats`,
41
660
  ORG_MEMBER_TASKS: (orgId, memberId) => `${API_BASE}/orgs/${orgId}/members/${memberId}/tasks`,
661
+ REF_SEARCH: (orgId) => `${API_BASE}/orgs/${orgId}/refs/search`,
42
662
  // Direct messages (org-scoped, atomic find-or-create + send)
43
663
  DM: (orgId) => `${API_BASE}/orgs/${orgId}/dm`,
44
664
  // Onboarding
@@ -82,6 +702,7 @@ var ENDPOINTS = {
82
702
  AGENT_ACTIVITY: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/activity`,
83
703
  AGENT_MONITOR: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/monitor`,
84
704
  AGENT_ME: (orgId) => `${API_BASE}/orgs/${orgId}/agents/me`,
705
+ AGENT_NEW_SESSION: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/new-session`,
85
706
  AGENT_SESSIONS: (orgId, agentId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/sessions`,
86
707
  AGENT_SESSION: (orgId, agentId, sessionId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/sessions/${sessionId}`,
87
708
  AGENT_SESSION_STEPS: (orgId, agentId, sessionId) => `${API_BASE}/orgs/${orgId}/agents/${agentId}/sessions/${sessionId}/steps`,
@@ -120,6 +741,8 @@ var ENDPOINTS = {
120
741
  MACHINE_KEY: (orgId, machineId, keyId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/keys/${keyId}`,
121
742
  MACHINE_RUNTIME_AUTH_SESSIONS: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/runtime-auth-sessions`,
122
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`,
745
+ MACHINE_BROWSE: (orgId, machineId) => `${API_BASE}/orgs/${orgId}/machines/${machineId}/browse`,
123
746
  // Machine self-control-plane (mck_-scoped). The bearer token implicitly
124
747
  // identifies the Machine, so there is no `:mid` URL parameter — these are
125
748
  // "self" routes called by the daemon for its own host.
@@ -129,6 +752,7 @@ var ENDPOINTS = {
129
752
  MACHINES_ME_AGENT_LAUNCH_CREDENTIAL: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/launch-credential`,
130
753
  MACHINES_ME_AGENT_WORKSPACE_STATE: (agentId) => `${API_BASE}/machines/me/agents/${agentId}/workspace-state`,
131
754
  MACHINES_ME_WS_TICKET: `${API_BASE}/machines/me/ws/ticket`,
755
+ MACHINES_ME_BROWSE_RESPONSE: (requestId) => `${API_BASE}/machines/me/browse-response/${requestId}`,
132
756
  // Tasks (org-scoped)
133
757
  TASKS: (orgId) => `${API_BASE}/orgs/${orgId}/tasks`,
134
758
  TASK: (orgId, taskId) => `${API_BASE}/orgs/${orgId}/tasks/${taskId}`,
@@ -163,6 +787,12 @@ var ENDPOINTS = {
163
787
  INVITATION_ACCEPT: (id) => `${API_BASE}/invitations/${id}/accept`,
164
788
  INVITATION_DECLINE: (id) => `${API_BASE}/invitations/${id}/decline`,
165
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`,
166
796
  // Wikis (org-scoped, served by wiki-service)
167
797
  WIKIS: (orgId) => `${WIKI_BASE}/orgs/${orgId}/wikis`,
168
798
  WIKI: (orgId, wikiId) => `${WIKI_BASE}/orgs/${orgId}/wikis/${wikiId}`,
@@ -282,6 +912,8 @@ var WS_EVENTS = {
282
912
  INVITATION_ACCEPTED: "invitation.accepted",
283
913
  INVITATION_DECLINED: "invitation.declined",
284
914
  INVITATION_REVOKED: "invitation.revoked",
915
+ ORG_JOIN_REQUEST_NEW: "org.join_request.new",
916
+ ORG_INVITE_LINK_JOINED: "org.invite_link.joined",
285
917
  AGENT_CONFIG_UPDATE: "agent_config.update",
286
918
  PRESENCE_UPDATE: "presence.update",
287
919
  WIKI_CHANGESET_CREATED: "wiki.changeset.created",
@@ -308,7 +940,11 @@ var WS_EVENTS = {
308
940
  MACHINE_AGENT_ATTACHED: "machine.agent.attached",
309
941
  MACHINE_AGENT_DETACHED: "machine.agent.detached",
310
942
  MACHINE_STOP: "machine.stop",
311
- MACHINE_WORKSPACE_SETUP_REQUESTED: "machine.workspace.setup.requested"
943
+ MACHINE_WORKSPACE_SETUP_REQUESTED: "machine.workspace.setup.requested",
944
+ MACHINE_FILESYSTEM_BROWSE: "machine.filesystem.browse",
945
+ MACHINE_UPDATE: "machine.update",
946
+ MACHINE_CONFIG_UPDATED: "machine.config.updated",
947
+ AGENT_NEW_SESSION: "agent.new_session"
312
948
  };
313
949
 
314
950
  // ts/sdk/dist/client.js
@@ -391,10 +1027,10 @@ var ParallClient = class _ParallClient {
391
1027
  * REFRESH_THRESHOLD_S, refresh it **before** sending the request.
392
1028
  * No-op when the token is still fresh, missing, or un-parseable.
393
1029
  */
394
- async ensureFreshToken(path6) {
1030
+ async ensureFreshToken(path8) {
395
1031
  if (!this.token || !this.getRefreshToken)
396
1032
  return;
397
- const pathSuffix = path6.replace(/^\/api\/v1/, "");
1033
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
398
1034
  if (_ParallClient.AUTH_PATHS.has(pathSuffix))
399
1035
  return;
400
1036
  const exp = _ParallClient.decodeJwtExp(this.token);
@@ -426,11 +1062,11 @@ var ParallClient = class _ParallClient {
426
1062
  this.refreshPromise = null;
427
1063
  }
428
1064
  }
429
- async request(method, path6, body, query, retried = false, opts) {
1065
+ async request(method, path8, body, query, retried = false, opts) {
430
1066
  if (!retried) {
431
- await this.ensureFreshToken(path6);
1067
+ await this.ensureFreshToken(path8);
432
1068
  }
433
- let url = `${this.baseUrl}${path6}`;
1069
+ let url = `${this.baseUrl}${path8}`;
434
1070
  if (query) {
435
1071
  const params = new URLSearchParams();
436
1072
  for (const [key, value] of Object.entries(query)) {
@@ -455,12 +1091,12 @@ var ParallClient = class _ParallClient {
455
1091
  throw _ParallClient.normalizeFetchError(err);
456
1092
  }
457
1093
  if (res.status === 401) {
458
- const pathSuffix = path6.replace(/^\/api\/v1/, "");
1094
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
459
1095
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
460
1096
  if (!retried && !isAuthPath && this.getRefreshToken) {
461
1097
  const refreshed = await this.tryRefresh();
462
1098
  if (refreshed) {
463
- return this.request(method, path6, body, query, true, opts);
1099
+ return this.request(method, path8, body, query, true, opts);
464
1100
  }
465
1101
  }
466
1102
  if (this.onTokenExpired && !isAuthPath) {
@@ -497,15 +1133,15 @@ var ParallClient = class _ParallClient {
497
1133
  * hit the 100 MiB cap, so a longer 5-minute timeout is used so a
498
1134
  * 50 MiB blob on a slow connection doesn't get chopped at 15 s.
499
1135
  */
500
- async multipartRequest(method, path6, body, retried = false) {
1136
+ async multipartRequest(method, path8, body, retried = false) {
501
1137
  if (!retried) {
502
- await this.ensureFreshToken(path6);
1138
+ await this.ensureFreshToken(path8);
503
1139
  }
504
1140
  const { "Content-Type": _drop, ...headers } = this.buildHeaders();
505
1141
  void _drop;
506
1142
  let res;
507
1143
  try {
508
- res = await fetch(`${this.baseUrl}${path6}`, {
1144
+ res = await fetch(`${this.baseUrl}${path8}`, {
509
1145
  method,
510
1146
  headers,
511
1147
  body,
@@ -515,12 +1151,12 @@ var ParallClient = class _ParallClient {
515
1151
  throw _ParallClient.normalizeFetchError(err);
516
1152
  }
517
1153
  if (res.status === 401) {
518
- const pathSuffix = path6.replace(/^\/api\/v1/, "");
1154
+ const pathSuffix = path8.replace(/^\/api\/v1/, "");
519
1155
  const isAuthPath = _ParallClient.AUTH_PATHS.has(pathSuffix);
520
1156
  if (!retried && !isAuthPath && this.getRefreshToken) {
521
1157
  const refreshed = await this.tryRefresh();
522
1158
  if (refreshed) {
523
- return this.multipartRequest(method, path6, body, true);
1159
+ return this.multipartRequest(method, path8, body, true);
524
1160
  }
525
1161
  }
526
1162
  if (this.onTokenExpired && !isAuthPath) {
@@ -637,6 +1273,9 @@ var ParallClient = class _ParallClient {
637
1273
  const res = await this.request("GET", ENDPOINTS.ORG_MEMBERS_ONLINE(orgId));
638
1274
  return res.user_ids ?? [];
639
1275
  }
1276
+ async searchRefs(orgId, params) {
1277
+ return this.request("GET", ENDPOINTS.REF_SEARCH(orgId), void 0, params);
1278
+ }
640
1279
  async removeOrgMember(orgId, userId) {
641
1280
  return this.request("DELETE", ENDPOINTS.ORG_MEMBER(orgId, userId));
642
1281
  }
@@ -690,6 +1329,34 @@ var ParallClient = class _ParallClient {
690
1329
  async getInvitationByToken(token) {
691
1330
  return this.request("GET", ENDPOINTS.INVITATION_BY_TOKEN(token));
692
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
+ }
693
1360
  // ---- Direct Messages (org-scoped) ----
694
1361
  async sendDirectMessage(orgId, req) {
695
1362
  return this.request("POST", ENDPOINTS.DM(orgId), req);
@@ -854,6 +1521,9 @@ var ParallClient = class _ParallClient {
854
1521
  return this.request("GET", ENDPOINTS.AGENT_ME(orgId));
855
1522
  }
856
1523
  // ---- Agent Sessions (org-scoped) ----
1524
+ async requestNewAgentSession(orgId, agentId) {
1525
+ return this.request("POST", ENDPOINTS.AGENT_NEW_SESSION(orgId, agentId));
1526
+ }
857
1527
  async createAgentSession(orgId, agentId, req) {
858
1528
  return this.request("POST", ENDPOINTS.AGENT_SESSIONS(orgId, agentId), req);
859
1529
  }
@@ -956,19 +1626,20 @@ var ParallClient = class _ParallClient {
956
1626
  }
957
1627
  // ---- Daemon-mode Machine management (org-scoped, user auth) ----
958
1628
  /**
959
- * Create a new daemon-mode Machine. Returns the Machine row + one-shot
960
- * 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.
961
1631
  */
962
1632
  async createMachine(orgId, opts) {
963
1633
  return this.request("POST", ENDPOINTS.MACHINES(orgId), opts);
964
1634
  }
965
1635
  /**
966
- * Returns only daemon_mode=true machines (non-terminated). Backs the
967
- * "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.
968
1639
  */
969
1640
  async getDaemonMachines(orgId) {
970
1641
  const all = await this.getMachines(orgId);
971
- 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");
972
1643
  }
973
1644
  /** Attach an agent to a daemon-mode Machine. */
974
1645
  async attachAgent(orgId, machineId, agentId, opts) {
@@ -988,8 +1659,8 @@ var ParallClient = class _ParallClient {
988
1659
  async retryAgentWorkspaceSetup(orgId, machineId, agentId) {
989
1660
  return this.request("POST", ENDPOINTS.MACHINE_AGENT_WORKSPACE_SETUP(orgId, machineId, agentId));
990
1661
  }
991
- async patchMachineLLMSource(orgId, machineId, llmSource2) {
992
- 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 });
993
1664
  }
994
1665
  /** Get machine-level runtime auth state. */
995
1666
  async getMachineRuntimeAuth(orgId, machineId) {
@@ -1030,8 +1701,9 @@ var ParallClient = class _ParallClient {
1030
1701
  * daemon should call this on a fixed cadence (e.g. every 30s) so an
1031
1702
  * external observer can detect a wedged supervisor.
1032
1703
  */
1033
- async postMachineHeartbeat() {
1034
- 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);
1035
1707
  }
1036
1708
  async reportAgentWorkspaceState(agentId, state) {
1037
1709
  const res = await this.request("PUT", ENDPOINTS.MACHINES_ME_AGENT_WORKSPACE_STATE(agentId), state);
@@ -1053,9 +1725,19 @@ var ParallClient = class _ParallClient {
1053
1725
  async getMachineWsTicket() {
1054
1726
  return this.request("POST", ENDPOINTS.MACHINES_ME_WS_TICKET);
1055
1727
  }
1728
+ async postBrowseResponse(requestId, response) {
1729
+ return this.request("POST", ENDPOINTS.MACHINES_ME_BROWSE_RESPONSE(requestId), response);
1730
+ }
1056
1731
  async resizeMachine(orgId, machineId, spec) {
1057
1732
  return this.request("PATCH", ENDPOINTS.MACHINE_SPEC(orgId, machineId), spec);
1058
1733
  }
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 });
1740
+ }
1059
1741
  /** Create a new machine key. Returns the raw key string (shown once) + metadata. */
1060
1742
  async createMachineKey(orgId, machineId, name) {
1061
1743
  return this.request("POST", ENDPOINTS.MACHINE_KEYS(orgId, machineId), name ? { name } : void 0);
@@ -1404,8 +2086,8 @@ var ParallClient = class _ParallClient {
1404
2086
  async deleteWikiPathScope(orgId, wikiId, scopeId) {
1405
2087
  await this.request("DELETE", ENDPOINTS.WIKI_PATH_SCOPE(orgId, wikiId, scopeId));
1406
2088
  }
1407
- async getWikiAccessStatus(orgId, wikiId, path6) {
1408
- return this.request("GET", ENDPOINTS.WIKI_ACCESS_STATUS(orgId, wikiId), void 0, path6 ? { path: path6 } : 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);
1409
2091
  }
1410
2092
  async createWikiAccessRequest(orgId, wikiId, data) {
1411
2093
  await this.request("POST", ENDPOINTS.WIKI_ACCESS_REQUESTS(orgId, wikiId), data);
@@ -1414,11 +2096,11 @@ var ParallClient = class _ParallClient {
1414
2096
  async getWikiCommits(orgId, wikiId, params) {
1415
2097
  return this.request("GET", ENDPOINTS.WIKI_COMMITS(orgId, wikiId), void 0, params);
1416
2098
  }
1417
- async getWikiFileCommits(orgId, wikiId, path6, params) {
1418
- return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path6, ...params });
2099
+ async getWikiFileCommits(orgId, wikiId, path8, params) {
2100
+ return this.request("GET", ENDPOINTS.WIKI_FILE_COMMITS(orgId, wikiId), void 0, { path: path8, ...params });
1419
2101
  }
1420
- async getWikiBlame(orgId, wikiId, path6, ref) {
1421
- return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path6, ref });
2102
+ async getWikiBlame(orgId, wikiId, path8, ref) {
2103
+ return this.request("GET", ENDPOINTS.WIKI_BLAME(orgId, wikiId), void 0, { path: path8, ref });
1422
2104
  }
1423
2105
  // ---- Wiki Operations (audit log) ----
1424
2106
  async getWikiOperations(orgId, wikiId, params) {
@@ -1902,223 +2584,174 @@ var ParallWs = class {
1902
2584
  }
1903
2585
  };
1904
2586
 
1905
- // ts/daemon/dist/config.js
1906
- import * as fs from "node:fs";
1907
- import * as os from "node:os";
1908
- import * as path from "node:path";
1909
- function resolvePath(value) {
1910
- return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
1911
- }
1912
- function parseMs(value, fallback) {
1913
- if (!value)
1914
- return fallback;
1915
- const n = Number(value);
1916
- return Number.isFinite(n) && n > 0 ? n : fallback;
1917
- }
1918
- function parseMsAllowZero(value, fallback) {
1919
- if (value === void 0)
1920
- return fallback;
1921
- const n = Number(value);
1922
- return Number.isFinite(n) && n >= 0 ? n : fallback;
1923
- }
1924
- function daemonConfigDir(env = process.env) {
1925
- return path.join(env.HOME || os.homedir(), ".parall-daemon");
1926
- }
1927
- function daemonConfigPath(env = process.env) {
1928
- return path.join(daemonConfigDir(env), "config.json");
1929
- }
1930
- function tryLoadConfigFile(env) {
1931
- const cfgPath = daemonConfigPath(env);
1932
- let content;
1933
- try {
1934
- content = fs.readFileSync(cfgPath, "utf-8");
1935
- } catch (err) {
1936
- if (err.code === "ENOENT")
1937
- return null;
1938
- console.error(`Failed to read daemon config at ${cfgPath}: ${String(err)}`);
1939
- return null;
2587
+ // ts/daemon/dist/index.js
2588
+ init_config();
2589
+
2590
+ // ts/daemon/dist/supervisor.js
2591
+ import { spawn as spawn2 } from "node:child_process";
2592
+ import * as fs5 from "node:fs";
2593
+ import * as path5 from "node:path";
2594
+
2595
+ // ts/daemon/dist/filesystem.js
2596
+ import * as fs2 from "fs";
2597
+ import * as path2 from "path";
2598
+ import * as os2 from "os";
2599
+ var MAX_ENTRIES = 200;
2600
+ var SYSTEM_DIR_PREFIXES = [
2601
+ "/Applications",
2602
+ "/bin",
2603
+ "/boot",
2604
+ "/dev",
2605
+ "/etc",
2606
+ "/Library",
2607
+ "/private",
2608
+ "/proc",
2609
+ "/root",
2610
+ "/run",
2611
+ "/sbin",
2612
+ "/System",
2613
+ "/sys",
2614
+ "/usr",
2615
+ "/var"
2616
+ ];
2617
+ var CREDENTIAL_DIR_NAMES = /* @__PURE__ */ new Set([
2618
+ ".aws",
2619
+ ".azure",
2620
+ ".claude",
2621
+ ".codex",
2622
+ ".config",
2623
+ ".docker",
2624
+ ".gnupg",
2625
+ ".kube",
2626
+ ".npm",
2627
+ ".ssh",
2628
+ ".parall-agent",
2629
+ ".parall-daemon"
2630
+ ]);
2631
+ function browseDenyReason(value) {
2632
+ const normalized = path2.resolve(value).split(path2.sep).join("/");
2633
+ if (normalized === "/")
2634
+ return "";
2635
+ for (const prefix of SYSTEM_DIR_PREFIXES) {
2636
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
2637
+ return "a system directory";
2638
+ }
1940
2639
  }
1941
- try {
1942
- return JSON.parse(content);
1943
- } catch (err) {
1944
- console.error(`Failed to parse daemon config at ${cfgPath}: ${String(err)}`);
1945
- return null;
2640
+ const parts = normalized.split("/").filter(Boolean);
2641
+ for (const part of parts) {
2642
+ if (CREDENTIAL_DIR_NAMES.has(part)) {
2643
+ return "a credential or application state directory";
2644
+ }
1946
2645
  }
2646
+ return "";
1947
2647
  }
1948
- function resolveClaudeDaemonConfig(env = process.env) {
1949
- let apiUrl = env.PRLL_API_URL?.trim() || "";
1950
- let apiKey = env.PRLL_API_KEY?.trim() || "";
1951
- if (!apiUrl || !apiKey) {
1952
- const file = tryLoadConfigFile(env);
1953
- if (file) {
1954
- if (!apiUrl && file.api_url)
1955
- apiUrl = file.api_url.trim();
1956
- if (!apiKey && file.api_key)
1957
- apiKey = file.api_key.trim();
2648
+ function syntheticRoots() {
2649
+ const roots = [];
2650
+ const platform2 = os2.platform();
2651
+ const candidates = platform2 === "darwin" ? ["/Users", os2.homedir()] : ["/home", os2.homedir()];
2652
+ for (const dir of [...new Set(candidates)]) {
2653
+ try {
2654
+ fs2.accessSync(dir, fs2.constants.R_OK);
2655
+ roots.push({ name: dir, type: "dir" });
2656
+ } catch {
1958
2657
  }
1959
2658
  }
1960
- if (!apiUrl)
1961
- throw new Error("Missing required env var: PRLL_API_URL");
1962
- if (!apiKey)
1963
- throw new Error("Missing required env var: PRLL_API_KEY");
1964
- if (!apiKey.startsWith("mck_")) {
1965
- 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.`);
1966
- }
1967
- const rootClaudeHome = resolvePath(env.PRLL_CLAUDE_HOME?.trim() || env.HOME || os.homedir());
1968
- const rootStateDir = resolvePath(env.PRLL_CLAUDE_STATE_DIR?.trim() || path.join(rootClaudeHome, ".parall-agent"));
1969
- return {
1970
- apiUrl,
1971
- apiKey,
1972
- agentBin: env.PRLL_CLAUDE_AGENT_BIN?.trim() || "parall-claude-agent",
1973
- rootStateDir,
1974
- rootClaudeHome,
1975
- wsUrl: env.PRLL_WS_URL?.trim() || void 0,
1976
- swimlaneName: env.PRLL_SWIMLANE_NAME?.trim() || void 0,
1977
- pollIntervalMs: parseMs(env.PRLL_DAEMON_POLL_INTERVAL_MS, 3e4),
1978
- heartbeatIntervalMs: parseMs(env.PRLL_DAEMON_HEARTBEAT_INTERVAL_MS, 3e4),
1979
- restartBackoffMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MS, 5e3),
1980
- restartBackoffMaxMs: parseMs(env.PRLL_DAEMON_RESTART_BACKOFF_MAX_MS, 5 * 6e4),
1981
- bootstrapBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MS, 2e3),
1982
- bootstrapBackoffMaxMs: parseMs(env.PRLL_DAEMON_BOOTSTRAP_BACKOFF_MAX_MS, 6e4),
1983
- supervisorRestartBackoffMs: parseMsAllowZero(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MS, 5e3),
1984
- supervisorRestartBackoffMaxMs: parseMs(env.PRLL_DAEMON_SUPERVISOR_RESTART_BACKOFF_MAX_MS, 5 * 6e4)
1985
- };
2659
+ return roots;
1986
2660
  }
1987
- function assertSafeAgentId(agentId) {
1988
- if (!/^[A-Za-z0-9_-]+$/.test(agentId)) {
1989
- throw new Error(`Invalid agentId for filesystem path: ${agentId}`);
2661
+ async function listDirectory(dirPath) {
2662
+ const resolved = path2.resolve(dirPath);
2663
+ const normalized = resolved.split(path2.sep).join("/");
2664
+ if (normalized === "/") {
2665
+ return { entries: syntheticRoots() };
1990
2666
  }
1991
- return agentId;
1992
- }
1993
- function agentStateDirFor(rootStateDir, agentId) {
1994
- return path.join(rootStateDir, "agents", assertSafeAgentId(agentId));
1995
- }
1996
- function agentClaudeHomeFor(rootClaudeHome, agentId) {
1997
- return path.join(rootClaudeHome, "agents", assertSafeAgentId(agentId));
1998
- }
1999
- function sharedClaudeCredentialsFileFor(rootClaudeHome) {
2000
- return path.join(rootClaudeHome, ".claude", ".credentials.json");
2001
- }
2002
- function agentClaudeCredentialsFileFor(agentClaudeHome) {
2003
- return path.join(agentClaudeHome, ".claude", ".credentials.json");
2004
- }
2005
- function agentWorkspaceDirFor(rootStateDir, agentId) {
2006
- return path.join(rootStateDir, "agents", assertSafeAgentId(agentId), "workspace");
2007
- }
2008
- function resolveWsUrl(apiUrl, explicitWsUrl, swimlaneName) {
2009
- const base = explicitWsUrl || `${apiUrl.replace(/\/$/, "").replace(/^http/, "ws")}/ws`;
2010
- if (!swimlaneName)
2011
- return base;
2012
- const url = new URL(base);
2013
- url.searchParams.set("swimlane", swimlaneName);
2014
- return url.toString();
2667
+ const deny = browseDenyReason(normalized);
2668
+ if (deny) {
2669
+ return { entries: [], error: `Access denied: ${deny}` };
2670
+ }
2671
+ let realPath;
2672
+ try {
2673
+ realPath = fs2.realpathSync(resolved);
2674
+ } catch (err) {
2675
+ const code = err.code;
2676
+ if (code === "ENOENT")
2677
+ return { entries: [], error: "Directory not found" };
2678
+ return { entries: [], error: "Permission denied" };
2679
+ }
2680
+ const realDeny = browseDenyReason(realPath.split(path2.sep).join("/"));
2681
+ if (realDeny) {
2682
+ return { entries: [], error: `Access denied: ${realDeny}` };
2683
+ }
2684
+ let dirents;
2685
+ try {
2686
+ dirents = fs2.readdirSync(realPath, { withFileTypes: true });
2687
+ } catch (err) {
2688
+ const code = err.code;
2689
+ if (code === "ENOENT")
2690
+ return { entries: [], error: "Directory not found" };
2691
+ if (code === "EACCES" || code === "EPERM")
2692
+ return { entries: [], error: "Permission denied" };
2693
+ return { entries: [], error: `Failed to read directory: ${code ?? String(err)}` };
2694
+ }
2695
+ const entries = [];
2696
+ for (const d of dirents) {
2697
+ if (!d.isDirectory())
2698
+ continue;
2699
+ if (d.name.startsWith("."))
2700
+ continue;
2701
+ entries.push({ name: d.name, type: "dir" });
2702
+ if (entries.length >= MAX_ENTRIES)
2703
+ break;
2704
+ }
2705
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2706
+ return { entries };
2015
2707
  }
2016
2708
 
2017
2709
  // ts/daemon/dist/supervisor.js
2018
- import { spawn as spawn2 } from "node:child_process";
2019
- import * as fs3 from "node:fs";
2020
- import * as path4 from "node:path";
2710
+ init_config();
2021
2711
 
2022
2712
  // ts/daemon/dist/runtimes.js
2023
- import * as path2 from "node:path";
2024
- function llmSource(pc) {
2025
- if (pc?.llm_source)
2026
- return pc.llm_source;
2027
- if (pc?.openai_api_key || pc?.openai_base_url || pc?.anthropic_auth_token || pc?.anthropic_base_url) {
2028
- return "custom";
2029
- }
2030
- return "parall";
2031
- }
2032
- function clearAllProviderCreds(env) {
2033
- delete env.ANTHROPIC_AUTH_TOKEN;
2034
- delete env.ANTHROPIC_BASE_URL;
2035
- delete env.ANTHROPIC_API_KEY;
2036
- delete env.OPENAI_API_KEY;
2037
- delete env.OPENAI_BASE_URL;
2038
- delete env.PRLL_CLAUDE_ALLOW_API_KEY;
2713
+ import * as fs3 from "node:fs";
2714
+ import * as path3 from "node:path";
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;
2039
2729
  }
2040
2730
  var claudeCodeAdapter = {
2041
2731
  bin: "parall-claude-agent",
2042
2732
  buildEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
2043
- const env = { ...baseEnv };
2044
- clearAllProviderCreds(env);
2045
- env.PRLL_API_KEY = apiKey;
2046
- env.PRLL_ORG_ID = orgId;
2047
- env.AGENT_ID = agentId;
2048
- env.PRLL_AGENT_ID = agentId;
2733
+ const env = buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc);
2049
2734
  env.PRLL_CLAUDE_HOME = dirs.claudeHome;
2050
- env.PRLL_CLAUDE_STATE_DIR = dirs.stateDir;
2051
- env.PRLL_CLAUDE_WORKSPACE_DIR = dirs.workspaceDir;
2052
- const source = llmSource(pc);
2053
- if (source === "parall") {
2054
- env.ANTHROPIC_AUTH_TOKEN = apiKey;
2055
- env.ANTHROPIC_BASE_URL = `${baseEnv.PRLL_API_URL}/api/llm`;
2056
- env.PRLL_CLAUDE_ALLOW_API_KEY = "1";
2057
- } else if (source === "custom") {
2058
- if (pc?.anthropic_auth_token) {
2059
- env.ANTHROPIC_AUTH_TOKEN = pc.anthropic_auth_token;
2060
- env.PRLL_CLAUDE_ALLOW_API_KEY = "1";
2061
- }
2062
- if (pc?.anthropic_base_url)
2063
- env.ANTHROPIC_BASE_URL = pc.anthropic_base_url;
2064
- }
2065
- delete env.PRLL_DAEMON_MODE;
2066
2735
  return env;
2067
2736
  }
2068
2737
  };
2069
2738
  var codexAdapter = {
2070
2739
  bin: "parall-codex-agent",
2071
2740
  buildEnv(baseEnv, agentId, orgId, apiKey, dirs, pc) {
2072
- const env = { ...baseEnv };
2073
- clearAllProviderCreds(env);
2074
- env.PRLL_API_KEY = apiKey;
2075
- env.PRLL_ORG_ID = orgId;
2076
- env.AGENT_ID = agentId;
2077
- env.PRLL_AGENT_ID = agentId;
2078
- env.PRLL_CODEX_STATE_DIR = dirs.stateDir;
2079
- env.PRLL_CODEX_WORKSPACE_DIR = dirs.workspaceDir;
2080
- env.PRLL_CODEX_HOME = path2.join(dirs.stateDir, ".codex");
2081
- const source = llmSource(pc);
2082
- if (source === "parall") {
2083
- env.OPENAI_API_KEY = apiKey;
2084
- env.OPENAI_BASE_URL = `${baseEnv.PRLL_API_URL}/api/llm/v1`;
2085
- } else if (source === "custom") {
2086
- if (pc?.openai_api_key)
2087
- env.OPENAI_API_KEY = pc.openai_api_key;
2088
- if (pc?.openai_base_url)
2089
- env.OPENAI_BASE_URL = pc.openai_base_url;
2090
- }
2091
- delete env.PRLL_DAEMON_MODE;
2741
+ const env = buildStandardEnv(baseEnv, agentId, orgId, apiKey, dirs, pc);
2742
+ env.PRLL_CODEX_HOME = path3.join(dirs.stateDir, ".codex");
2092
2743
  return env;
2093
2744
  }
2094
2745
  };
2095
2746
  var defaultAdapter = {
2096
2747
  bin: "parall-agent",
2097
- buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
2098
- const env = { ...baseEnv };
2099
- env.PRLL_API_KEY = apiKey;
2100
- env.PRLL_ORG_ID = orgId;
2101
- env.AGENT_ID = agentId;
2102
- env.PRLL_AGENT_ID = agentId;
2103
- env.PRLL_STATE_DIR = dirs.stateDir;
2104
- env.PRLL_WORKSPACE_DIR = dirs.workspaceDir;
2105
- delete env.PRLL_DAEMON_MODE;
2106
- return env;
2107
- }
2748
+ buildEnv: buildStandardEnv
2108
2749
  };
2109
2750
  var openclawAdapter = {
2110
2751
  bin: "parall-openclaw-agent",
2111
- buildEnv(baseEnv, agentId, orgId, apiKey, dirs) {
2112
- const env = { ...baseEnv };
2113
- clearAllProviderCreds(env);
2114
- env.PRLL_API_KEY = apiKey;
2115
- env.PRLL_ORG_ID = orgId;
2116
- env.AGENT_ID = agentId;
2117
- env.PRLL_AGENT_ID = agentId;
2118
- env.PRLL_OPENCLAW_STATE_DIR = dirs.stateDir;
2119
- 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);
2120
2754
  env.OPENCLAW_GATEWAY_PORT = env.OPENCLAW_GATEWAY_PORT || "0";
2121
- delete env.PRLL_DAEMON_MODE;
2122
2755
  return env;
2123
2756
  }
2124
2757
  };
@@ -2127,8 +2760,25 @@ var RUNTIME_ADAPTERS = {
2127
2760
  "codex": codexAdapter,
2128
2761
  "openclaw": openclawAdapter
2129
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
+ };
2130
2768
  function getRuntimeAdapter(runtimeType) {
2131
- 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;
2132
2782
  }
2133
2783
  function assertAgentKey(apiKey) {
2134
2784
  if (apiKey.startsWith("mck_")) {
@@ -2139,8 +2789,8 @@ function assertAgentKey(apiKey) {
2139
2789
  // ts/daemon/dist/workspace.js
2140
2790
  import { spawn } from "node:child_process";
2141
2791
  import { createHash } from "node:crypto";
2142
- import * as fs2 from "node:fs";
2143
- import * as path3 from "node:path";
2792
+ import * as fs4 from "node:fs";
2793
+ import * as path4 from "node:path";
2144
2794
  var OUTPUT_TAIL_LIMIT = 32 * 1024;
2145
2795
  var DEFAULT_SETUP_TIMEOUT_SEC = 600;
2146
2796
  async function prepareWorkspace(opts) {
@@ -2257,7 +2907,7 @@ function resolveWorkspaceDir(workspace, defaultWorkspaceDir) {
2257
2907
  async function ensureWorkspace(plan, log2) {
2258
2908
  const ws = plan.workspace;
2259
2909
  if (ws.mode === "default") {
2260
- fs2.mkdirSync(plan.workspaceDir, { recursive: true });
2910
+ fs4.mkdirSync(plan.workspaceDir, { recursive: true });
2261
2911
  assertWritableWorkspaceDir(plan.workspaceDir);
2262
2912
  return;
2263
2913
  }
@@ -2265,7 +2915,7 @@ async function ensureWorkspace(plan, log2) {
2265
2915
  assertSafeCustomWorkspacePath(plan);
2266
2916
  let st;
2267
2917
  try {
2268
- st = fs2.statSync(plan.workspaceDir);
2918
+ st = fs4.statSync(plan.workspaceDir);
2269
2919
  } catch (err) {
2270
2920
  if (isNodeError(err) && err.code === "ENOENT") {
2271
2921
  throw new Error(`workspace path does not exist: ${plan.workspaceDir}`);
@@ -2275,7 +2925,7 @@ async function ensureWorkspace(plan, log2) {
2275
2925
  if (!st.isDirectory()) {
2276
2926
  throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
2277
2927
  }
2278
- assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
2928
+ assertSafeCustomWorkspacePath(plan, fs4.realpathSync(plan.workspaceDir));
2279
2929
  assertWritableWorkspaceDir(plan.workspaceDir);
2280
2930
  return;
2281
2931
  }
@@ -2286,17 +2936,17 @@ async function ensureWorkspace(plan, log2) {
2286
2936
  if (plan.customWorkspaceField) {
2287
2937
  assertSafeCustomWorkspacePath(plan);
2288
2938
  }
2289
- if (!fs2.existsSync(plan.workspaceDir)) {
2290
- fs2.mkdirSync(path3.dirname(plan.workspaceDir), { recursive: true });
2291
- assertWritableWorkspaceDir(path3.dirname(plan.workspaceDir));
2939
+ if (!fs4.existsSync(plan.workspaceDir)) {
2940
+ fs4.mkdirSync(path4.dirname(plan.workspaceDir), { recursive: true });
2941
+ assertWritableWorkspaceDir(path4.dirname(plan.workspaceDir));
2292
2942
  await runCommand("git", ["clone", remote, plan.workspaceDir], process.cwd());
2293
2943
  } else {
2294
- const st = fs2.statSync(plan.workspaceDir);
2944
+ const st = fs4.statSync(plan.workspaceDir);
2295
2945
  if (!st.isDirectory()) {
2296
2946
  throw new Error(`workspace path is not a directory: ${plan.workspaceDir}`);
2297
2947
  }
2298
2948
  if (plan.customWorkspaceField) {
2299
- assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
2949
+ assertSafeCustomWorkspacePath(plan, fs4.realpathSync(plan.workspaceDir));
2300
2950
  }
2301
2951
  assertWritableWorkspaceDir(plan.workspaceDir);
2302
2952
  await ensureGitWorktree(plan.workspaceDir);
@@ -2320,13 +2970,13 @@ async function verifyExistingWorkspace(plan, log2) {
2320
2970
  if (plan.customWorkspaceField) {
2321
2971
  assertSafeCustomWorkspacePath(plan);
2322
2972
  }
2323
- const st = fs2.statSync(plan.workspaceDir);
2973
+ const st = fs4.statSync(plan.workspaceDir);
2324
2974
  if (!st.isDirectory()) {
2325
2975
  log2.warn(`workspace ready state ignored: path is not a directory: ${plan.workspaceDir}`);
2326
2976
  return false;
2327
2977
  }
2328
2978
  if (plan.customWorkspaceField) {
2329
- assertSafeCustomWorkspacePath(plan, fs2.realpathSync(plan.workspaceDir));
2979
+ assertSafeCustomWorkspacePath(plan, fs4.realpathSync(plan.workspaceDir));
2330
2980
  }
2331
2981
  assertWritableWorkspaceDir(plan.workspaceDir);
2332
2982
  if (plan.workspace.mode === "git") {
@@ -2419,7 +3069,7 @@ async function tryGitOutput(cmd, args, cwd) {
2419
3069
  }
2420
3070
  }
2421
3071
  function runCommand(cmd, args, cwd, timeoutMs = 12e4, env = process.env) {
2422
- return new Promise((resolve4, reject) => {
3072
+ return new Promise((resolve5, reject) => {
2423
3073
  let tail = "";
2424
3074
  let timedOut = false;
2425
3075
  let settled = false;
@@ -2461,7 +3111,7 @@ ${tail}`)));
2461
3111
  return;
2462
3112
  }
2463
3113
  if (code === 0) {
2464
- settle(() => resolve4(tail));
3114
+ settle(() => resolve5(tail));
2465
3115
  } else {
2466
3116
  settle(() => reject(new Error(`command failed (${code ?? signal}): ${cmd} ${args.join(" ")}
2467
3117
  ${tail}`)));
@@ -2470,10 +3120,10 @@ ${tail}`)));
2470
3120
  });
2471
3121
  }
2472
3122
  function requireAbsolute(value, field) {
2473
- if (!value || !path3.isAbsolute(value)) {
3123
+ if (!value || !path4.isAbsolute(value)) {
2474
3124
  throw new Error(`${field} must be an absolute path`);
2475
3125
  }
2476
- return path3.resolve(value);
3126
+ return path4.resolve(value);
2477
3127
  }
2478
3128
  function assertSafeCustomWorkspacePath(plan, candidate = plan.workspaceDir) {
2479
3129
  if (!plan.customWorkspaceField)
@@ -2483,17 +3133,17 @@ function assertSafeCustomWorkspacePath(plan, candidate = plan.workspaceDir) {
2483
3133
  if (reason) {
2484
3134
  throw new Error(`${plan.customWorkspaceField} must point to a project folder, not ${reason}: ${normalized}`);
2485
3135
  }
2486
- const defaultWorkspace = path3.resolve(plan.defaultWorkspaceDir);
3136
+ const defaultWorkspace = path4.resolve(plan.defaultWorkspaceDir);
2487
3137
  if (isAncestorPath(normalized, defaultWorkspace) || normalized === defaultWorkspace) {
2488
3138
  throw new Error(`${plan.customWorkspaceField} must not point at daemon state directories: ${normalized}`);
2489
3139
  }
2490
3140
  }
2491
3141
  function assertWritableWorkspaceDir(dir) {
2492
- fs2.accessSync(dir, fs2.constants.R_OK | fs2.constants.W_OK | fs2.constants.X_OK);
2493
- const probe = path3.join(dir, `.parall-workspace-check-${process.pid}-${Date.now()}`);
2494
- const fd = fs2.openSync(probe, "wx", 384);
2495
- fs2.closeSync(fd);
2496
- fs2.unlinkSync(probe);
3142
+ fs4.accessSync(dir, fs4.constants.R_OK | fs4.constants.W_OK | fs4.constants.X_OK);
3143
+ const probe = path4.join(dir, `.parall-workspace-check-${process.pid}-${Date.now()}`);
3144
+ const fd = fs4.openSync(probe, "wx", 384);
3145
+ fs4.closeSync(fd);
3146
+ fs4.unlinkSync(probe);
2497
3147
  }
2498
3148
  function workspacePathDenyReason(value) {
2499
3149
  if (value === "/")
@@ -2554,11 +3204,11 @@ function workspacePathDenyReason(value) {
2554
3204
  return "";
2555
3205
  }
2556
3206
  function isAncestorPath(parent, child) {
2557
- const relative2 = path3.relative(parent, child);
2558
- return relative2 !== "" && !relative2.startsWith("..") && !path3.isAbsolute(relative2);
3207
+ const relative2 = path4.relative(parent, child);
3208
+ return relative2 !== "" && !relative2.startsWith("..") && !path4.isAbsolute(relative2);
2559
3209
  }
2560
3210
  function toPolicyPath(value) {
2561
- return path3.resolve(value).split(path3.sep).join("/");
3211
+ return path4.resolve(value).split(path4.sep).join("/");
2562
3212
  }
2563
3213
  function isNodeError(err) {
2564
3214
  return err instanceof Error && "code" in err;
@@ -2574,14 +3224,14 @@ var WORKSPACE_SETUP_RETRY_DELAY_MS = 5e3;
2574
3224
  function sleepCancellable(ms, signal) {
2575
3225
  if (signal.aborted)
2576
3226
  return Promise.resolve(false);
2577
- return new Promise((resolve4) => {
3227
+ return new Promise((resolve5) => {
2578
3228
  const timer = setTimeout(() => {
2579
3229
  signal.removeEventListener("abort", onAbort);
2580
- resolve4(true);
3230
+ resolve5(true);
2581
3231
  }, ms);
2582
3232
  const onAbort = () => {
2583
3233
  clearTimeout(timer);
2584
- resolve4(false);
3234
+ resolve5(false);
2585
3235
  };
2586
3236
  signal.addEventListener("abort", onAbort, { once: true });
2587
3237
  });
@@ -2600,11 +3250,16 @@ var DaemonSupervisor = class {
2600
3250
  machineOrgId = null;
2601
3251
  machineLlmSource = "parall";
2602
3252
  stopResolve = null;
3253
+ updater = null;
3254
+ healthConfirmed = false;
2603
3255
  constructor(config, client, log2) {
2604
3256
  this.config = config;
2605
3257
  this.client = client;
2606
3258
  this.log = log2;
2607
3259
  }
3260
+ setUpdater(updater) {
3261
+ this.updater = updater;
3262
+ }
2608
3263
  /** Start the supervisor. Returns a promise that resolves on `stop()`. */
2609
3264
  async run(signal) {
2610
3265
  if (this.running)
@@ -2626,6 +3281,10 @@ var DaemonSupervisor = class {
2626
3281
  throw err;
2627
3282
  }
2628
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
+ }
2629
3288
  await this.fullReconcile();
2630
3289
  this.ws = new ParallWs({
2631
3290
  getTicket: () => this.client.getMachineWsTicket(),
@@ -2634,11 +3293,33 @@ var DaemonSupervisor = class {
2634
3293
  });
2635
3294
  this.ws.on("machine.hello", (_data) => {
2636
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
+ }
2637
3304
  void (async () => {
2638
3305
  await this.refreshMachineConfig();
2639
3306
  await this.fullReconcile();
2640
3307
  })();
2641
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
+ });
2642
3323
  this.ws.on("machine.agent.attached", (data) => {
2643
3324
  this.log.info(`WS: agent ${data.agent_id} attached`);
2644
3325
  void this.handleAgentAttached(data.agent_id);
@@ -2659,6 +3340,10 @@ var DaemonSupervisor = class {
2659
3340
  this.log.info(`WS: workspace setup requested for agent ${data.agent_id}`);
2660
3341
  void this.handleWorkspaceSetupRequested(data.agent_id);
2661
3342
  });
3343
+ this.ws.on("machine.filesystem.browse", (data) => {
3344
+ this.log.info(`WS: filesystem browse requested: ${data.path}`);
3345
+ void this.handleFilesystemBrowse(data.request_id, data.path);
3346
+ });
2662
3347
  this.ws.on("machine.stop", (data) => {
2663
3348
  this.log.info(`WS: machine.stop received (reason=${data.reason ?? "none"})`);
2664
3349
  void this.stop();
@@ -2669,8 +3354,8 @@ var DaemonSupervisor = class {
2669
3354
  }
2670
3355
  });
2671
3356
  await this.ws.connect();
2672
- await new Promise((resolve4) => {
2673
- this.stopResolve = resolve4;
3357
+ await new Promise((resolve5) => {
3358
+ this.stopResolve = resolve5;
2674
3359
  });
2675
3360
  signal.removeEventListener("abort", onAbort);
2676
3361
  }
@@ -2789,15 +3474,15 @@ var DaemonSupervisor = class {
2789
3474
  */
2790
3475
  migrateFlatLayout() {
2791
3476
  const root = this.config.rootStateDir;
2792
- const agentsDir = path4.join(root, "agents");
2793
- const flatWorkspace = path4.join(root, "workspace");
2794
- if (!fs3.existsSync(flatWorkspace) || fs3.existsSync(agentsDir))
3477
+ const agentsDir = path5.join(root, "agents");
3478
+ const flatWorkspace = path5.join(root, "workspace");
3479
+ if (!fs5.existsSync(flatWorkspace) || fs5.existsSync(agentsDir))
2795
3480
  return;
2796
3481
  let ownerAgentId;
2797
- const sessionsDir = path4.join(root, "sessions");
2798
- if (fs3.existsSync(sessionsDir)) {
3482
+ const sessionsDir = path5.join(root, "sessions");
3483
+ if (fs5.existsSync(sessionsDir)) {
2799
3484
  try {
2800
- for (const file of fs3.readdirSync(sessionsDir)) {
3485
+ for (const file of fs5.readdirSync(sessionsDir)) {
2801
3486
  if (!file.endsWith(".json"))
2802
3487
  continue;
2803
3488
  const decoded = Buffer.from(file.replace(".json", ""), "base64url").toString();
@@ -2811,13 +3496,13 @@ var DaemonSupervisor = class {
2811
3496
  }
2812
3497
  }
2813
3498
  const targetId = ownerAgentId ?? "_orphan";
2814
- const targetDir = path4.join(agentsDir, targetId);
3499
+ const targetDir = path5.join(agentsDir, targetId);
2815
3500
  try {
2816
- fs3.mkdirSync(targetDir, { recursive: true });
3501
+ fs5.mkdirSync(targetDir, { recursive: true });
2817
3502
  for (const sub of ["workspace", "sessions", "dispatch-context"]) {
2818
- const src = path4.join(root, sub);
2819
- if (fs3.existsSync(src)) {
2820
- fs3.renameSync(src, path4.join(targetDir, sub));
3503
+ const src = path5.join(root, sub);
3504
+ if (fs5.existsSync(src)) {
3505
+ fs5.renameSync(src, path5.join(targetDir, sub));
2821
3506
  }
2822
3507
  }
2823
3508
  this.log.info(`migrated legacy flat state \u2192 agents/${targetId}/`);
@@ -2877,6 +3562,28 @@ var DaemonSupervisor = class {
2877
3562
  await this.terminateChild(state);
2878
3563
  this.children.delete(agentId);
2879
3564
  }
3565
+ async handleFilesystemBrowse(requestId, dirPath) {
3566
+ try {
3567
+ const result = await listDirectory(dirPath);
3568
+ await this.client.postBrowseResponse(requestId, {
3569
+ request_id: requestId,
3570
+ path: dirPath,
3571
+ entries: result.entries,
3572
+ error: result.error
3573
+ });
3574
+ } catch (err) {
3575
+ this.log.warn(`filesystem browse failed: ${String(err)}`);
3576
+ try {
3577
+ await this.client.postBrowseResponse(requestId, {
3578
+ request_id: requestId,
3579
+ path: dirPath,
3580
+ entries: [],
3581
+ error: String(err)
3582
+ });
3583
+ } catch {
3584
+ }
3585
+ }
3586
+ }
2880
3587
  async handleWorkspaceSetupRequested(agentId) {
2881
3588
  if (this.spawningAgents.has(agentId)) {
2882
3589
  this.log.info(`agent ${agentId}: workspace setup already in progress; queueing one restart`);
@@ -3002,9 +3709,9 @@ var DaemonSupervisor = class {
3002
3709
  const isK8s = !!process.env.KUBERNETES_SERVICE_HOST;
3003
3710
  const claudeHome = isK8s ? agentClaudeHomeFor(this.config.rootClaudeHome, agentId) : this.config.rootClaudeHome;
3004
3711
  try {
3005
- fs3.mkdirSync(stateDir, { recursive: true });
3712
+ fs5.mkdirSync(stateDir, { recursive: true });
3006
3713
  if (isK8s) {
3007
- fs3.mkdirSync(claudeHome, { recursive: true });
3714
+ fs5.mkdirSync(claudeHome, { recursive: true });
3008
3715
  this.ensureSharedCredentialLink(claudeHome, agentId);
3009
3716
  }
3010
3717
  } catch (err) {
@@ -3109,15 +3816,15 @@ var DaemonSupervisor = class {
3109
3816
  const child = state.child;
3110
3817
  if (!child)
3111
3818
  return;
3112
- return new Promise((resolve4) => {
3113
- const onExit = () => resolve4();
3819
+ return new Promise((resolve5) => {
3820
+ const onExit = () => resolve5();
3114
3821
  child.once("exit", onExit);
3115
3822
  try {
3116
3823
  child.kill("SIGTERM");
3117
3824
  } catch (err) {
3118
3825
  this.log.warn(`SIGTERM ${state.agentId} threw: ${String(err)}`);
3119
3826
  child.off("exit", onExit);
3120
- resolve4();
3827
+ resolve5();
3121
3828
  return;
3122
3829
  }
3123
3830
  const hardKill = setTimeout(() => {
@@ -3130,60 +3837,64 @@ var DaemonSupervisor = class {
3130
3837
  });
3131
3838
  }
3132
3839
  ensureSharedCredentialLink(agentClaudeHome, agentId) {
3133
- const sharedCredentials = path4.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
3840
+ const sharedCredentials = path5.resolve(sharedClaudeCredentialsFileFor(this.config.rootClaudeHome));
3134
3841
  const agentCredentials = agentClaudeCredentialsFileFor(agentClaudeHome);
3135
- const agentCredentialsDir = path4.dirname(agentCredentials);
3136
- fs3.mkdirSync(path4.dirname(sharedCredentials), { recursive: true });
3137
- fs3.mkdirSync(agentCredentialsDir, { recursive: true });
3842
+ const agentCredentialsDir = path5.dirname(agentCredentials);
3843
+ fs5.mkdirSync(path5.dirname(sharedCredentials), { recursive: true });
3844
+ fs5.mkdirSync(agentCredentialsDir, { recursive: true });
3138
3845
  try {
3139
- const existing = fs3.lstatSync(agentCredentials);
3846
+ const existing = fs5.lstatSync(agentCredentials);
3140
3847
  if (existing.isSymbolicLink()) {
3141
- const currentTarget = fs3.readlinkSync(agentCredentials);
3142
- if (path4.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
3848
+ const currentTarget = fs5.readlinkSync(agentCredentials);
3849
+ if (path5.resolve(agentCredentialsDir, currentTarget) === sharedCredentials) {
3143
3850
  return;
3144
3851
  }
3145
- fs3.unlinkSync(agentCredentials);
3852
+ fs5.unlinkSync(agentCredentials);
3146
3853
  } else if (existing.isDirectory()) {
3147
3854
  this.log.warn(`agent ${agentId}: credential path is a directory, cannot link ${agentCredentials}`);
3148
3855
  return;
3149
3856
  } else {
3150
- fs3.unlinkSync(agentCredentials);
3857
+ fs5.unlinkSync(agentCredentials);
3151
3858
  }
3152
3859
  } catch (err) {
3153
3860
  if (err.code !== "ENOENT") {
3154
3861
  throw err;
3155
3862
  }
3156
3863
  }
3157
- fs3.symlinkSync(sharedCredentials, agentCredentials);
3864
+ fs5.symlinkSync(sharedCredentials, agentCredentials);
3158
3865
  }
3159
3866
  };
3160
3867
 
3868
+ // ts/daemon/dist/index.js
3869
+ init_updater();
3870
+
3161
3871
  // ts/daemon/dist/cli.js
3162
- import * as fs4 from "node:fs";
3163
- import * as path5 from "node:path";
3164
- import * as os2 from "node:os";
3872
+ init_config();
3873
+ import * as fs7 from "node:fs";
3874
+ import * as path7 from "node:path";
3875
+ import * as os3 from "node:os";
3165
3876
  import * as readline from "node:readline";
3166
3877
  import { spawn as spawn3, execSync } from "node:child_process";
3167
3878
  var CONFIG_DIR = daemonConfigDir();
3168
3879
  var CONFIG_PATH = daemonConfigPath();
3169
3880
  function readConfig() {
3170
3881
  try {
3171
- return JSON.parse(fs4.readFileSync(CONFIG_PATH, "utf-8"));
3882
+ return JSON.parse(fs7.readFileSync(CONFIG_PATH, "utf-8"));
3172
3883
  } catch {
3173
3884
  return null;
3174
3885
  }
3175
3886
  }
3176
3887
  function writeConfig(config) {
3177
- fs4.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
3178
- fs4.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 384 });
3179
- fs4.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);
3180
3891
  }
3181
3892
  function prompt(question) {
3182
3893
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3183
- return new Promise((resolve4) => {
3894
+ return new Promise((resolve5) => {
3184
3895
  rl.question(question, (answer) => {
3185
3896
  rl.close();
3186
- resolve4(answer.trim());
3897
+ resolve5(answer.trim());
3187
3898
  });
3188
3899
  });
3189
3900
  }
@@ -3195,10 +3906,10 @@ function isLinux() {
3195
3906
  }
3196
3907
  var PLIST_LABEL = "com.parall.daemon";
3197
3908
  function plistPath() {
3198
- return path5.join(os2.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
3909
+ return path7.join(os3.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
3199
3910
  }
3200
3911
  function systemdUnitPath() {
3201
- return path5.join(os2.homedir(), ".config", "systemd", "user", "parall-daemon.service");
3912
+ return path7.join(os3.homedir(), ".config", "systemd", "user", "parall-daemon.service");
3202
3913
  }
3203
3914
  function getDaemonBin() {
3204
3915
  try {
@@ -3208,7 +3919,7 @@ function getDaemonBin() {
3208
3919
  }
3209
3920
  }
3210
3921
  function generatePlist(daemonBin) {
3211
- const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
3922
+ const logPath = path7.join(os3.homedir(), "Library", "Logs", "parall-daemon.log");
3212
3923
  return `<?xml version="1.0" encoding="UTF-8"?>
3213
3924
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3214
3925
  <plist version="1.0">
@@ -3217,7 +3928,9 @@ function generatePlist(daemonBin) {
3217
3928
  <string>${PLIST_LABEL}</string>
3218
3929
  <key>ProgramArguments</key>
3219
3930
  <array>
3220
- <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>
3221
3934
  </array>
3222
3935
  <key>RunAtLoad</key>
3223
3936
  <true/>
@@ -3240,7 +3953,7 @@ Wants=network-online.target
3240
3953
 
3241
3954
  [Service]
3242
3955
  Type=simple
3243
- 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'
3244
3957
  Restart=always
3245
3958
  RestartSec=5
3246
3959
 
@@ -3255,16 +3968,16 @@ function installService() {
3255
3968
  }
3256
3969
  const bin = getDaemonBin();
3257
3970
  if (isMacOS()) {
3258
- const dir = path5.dirname(plistPath());
3259
- fs4.mkdirSync(dir, { recursive: true });
3260
- fs4.writeFileSync(plistPath(), generatePlist(bin));
3971
+ const dir = path7.dirname(plistPath());
3972
+ fs7.mkdirSync(dir, { recursive: true });
3973
+ fs7.writeFileSync(plistPath(), generatePlist(bin));
3261
3974
  execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
3262
3975
  execSync(`launchctl bootstrap gui/$(id -u) ${plistPath()}`);
3263
3976
  console.log(`launchd agent installed: ${plistPath()}`);
3264
3977
  } else if (isLinux()) {
3265
- const dir = path5.dirname(systemdUnitPath());
3266
- fs4.mkdirSync(dir, { recursive: true });
3267
- fs4.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
3978
+ const dir = path7.dirname(systemdUnitPath());
3979
+ fs7.mkdirSync(dir, { recursive: true });
3980
+ fs7.writeFileSync(systemdUnitPath(), generateSystemdUnit(bin));
3268
3981
  execSync("systemctl --user daemon-reload");
3269
3982
  execSync("systemctl --user enable --now parall-daemon");
3270
3983
  console.log(`systemd service installed: ${systemdUnitPath()}`);
@@ -3331,8 +4044,8 @@ function cmdLogs(lines) {
3331
4044
  child2.on("exit", (code) => process.exit(code ?? 0));
3332
4045
  return;
3333
4046
  }
3334
- const logPath = path5.join(os2.homedir(), "Library", "Logs", "parall-daemon.log");
3335
- if (!fs4.existsSync(logPath)) {
4047
+ const logPath = path7.join(os3.homedir(), "Library", "Logs", "parall-daemon.log");
4048
+ if (!fs7.existsSync(logPath)) {
3336
4049
  console.log("No log file found at", logPath);
3337
4050
  return;
3338
4051
  }
@@ -3342,20 +4055,56 @@ function cmdLogs(lines) {
3342
4055
  function cmdServiceUninstall() {
3343
4056
  if (isMacOS()) {
3344
4057
  execSync(`launchctl bootout gui/$(id -u) ${plistPath()} 2>/dev/null || true`);
3345
- if (fs4.existsSync(plistPath()))
3346
- fs4.unlinkSync(plistPath());
4058
+ if (fs7.existsSync(plistPath()))
4059
+ fs7.unlinkSync(plistPath());
3347
4060
  console.log("launchd agent uninstalled.");
3348
4061
  } else if (isLinux()) {
3349
4062
  execSync("systemctl --user stop parall-daemon 2>/dev/null || true");
3350
4063
  execSync("systemctl --user disable parall-daemon 2>/dev/null || true");
3351
- if (fs4.existsSync(systemdUnitPath()))
3352
- fs4.unlinkSync(systemdUnitPath());
4064
+ if (fs7.existsSync(systemdUnitPath()))
4065
+ fs7.unlinkSync(systemdUnitPath());
3353
4066
  execSync("systemctl --user daemon-reload");
3354
4067
  console.log("systemd service uninstalled.");
3355
4068
  } else {
3356
4069
  console.log("Unsupported platform.");
3357
4070
  }
3358
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
+ }
3359
4108
  function printUsage() {
3360
4109
  console.log(`
3361
4110
  parall-daemon \u2014 Parall local agent runtime
@@ -3365,6 +4114,7 @@ Usage:
3365
4114
  parall-daemon init Configure the daemon (interactive)
3366
4115
  parall-daemon status Show daemon service status
3367
4116
  parall-daemon stop Stop the background service
4117
+ parall-daemon update [--check] Check for / apply daemon updates
3368
4118
  parall-daemon logs [-n LINES] Tail daemon logs
3369
4119
  parall-daemon service install Install as background service (launchd/systemd)
3370
4120
  parall-daemon service uninstall Uninstall background service
@@ -3396,6 +4146,9 @@ async function runCLI(args) {
3396
4146
  cmdLogs(lines);
3397
4147
  return "handled";
3398
4148
  }
4149
+ case "update":
4150
+ await cmdUpdate(args.includes("--check"));
4151
+ return "handled";
3399
4152
  case "service": {
3400
4153
  const sub = args[1];
3401
4154
  if (sub === "install") {
@@ -3425,6 +4178,7 @@ async function runCLI(args) {
3425
4178
  }
3426
4179
 
3427
4180
  // ts/daemon/dist/index.js
4181
+ var UPDATE_EXIT_CODE = 42;
3428
4182
  var log = createLogger("daemon");
3429
4183
  function formatError(reason) {
3430
4184
  if (reason instanceof Error) {
@@ -3432,10 +4186,12 @@ function formatError(reason) {
3432
4186
  }
3433
4187
  return String(reason);
3434
4188
  }
3435
- async function runForever(config, client, log2, signal) {
4189
+ async function runForever(config, client, log2, signal, updater) {
3436
4190
  let attempt = 0;
3437
4191
  while (!signal.aborted) {
3438
4192
  const supervisor = new DaemonSupervisor(config, client, log2);
4193
+ if (updater)
4194
+ supervisor.setUpdater(updater);
3439
4195
  try {
3440
4196
  await supervisor.run(signal);
3441
4197
  await supervisor.stop();
@@ -3464,6 +4220,16 @@ async function main() {
3464
4220
  const config = resolveClaudeDaemonConfig(process.env);
3465
4221
  log.info(`boot: api=${config.apiUrl} pollMs=${config.pollIntervalMs} heartbeatMs=${config.heartbeatIntervalMs} agentBin=${config.agentBin}`);
3466
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
+ }
3467
4233
  const client = new ParallClient({
3468
4234
  baseUrl: config.apiUrl,
3469
4235
  token: config.apiKey,
@@ -3485,7 +4251,18 @@ async function main() {
3485
4251
  process.exit(1);
3486
4252
  });
3487
4253
  config.wsUrl = resolveWsUrl(config.apiUrl, config.wsUrl, config.swimlaneName);
3488
- 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);
3489
4266
  }
3490
4267
  var cliArgs = process.argv.slice(2);
3491
4268
  runCLI(cliArgs).then((result) => {