@alook/daemon 0.0.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4773 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/cli/index.ts
6
+ import { Command, CommanderError } from "commander";
7
+
8
+ // src/cli/proxyServerApi.ts
9
+ import * as fs from "fs";
10
+ function proxyServerApiFromEnv(prefix = "ALOOK", env = process.env) {
11
+ const proxyUrl = env[`${prefix}_PROXY_URL`];
12
+ const tokenFile = env[`${prefix}_PROXY_TOKEN_FILE`];
13
+ if (!proxyUrl || !tokenFile)
14
+ return null;
15
+ const voucher = fs.readFileSync(tokenFile, "utf8").trim();
16
+ return createProxyServerApi({ proxyUrl, voucher });
17
+ }
18
+ function createProxyServerApi(config) {
19
+ const fetchImpl = config.fetchImpl ?? fetch;
20
+ const base = config.proxyUrl.replace(/\/+$/, "");
21
+ async function call(method, body) {
22
+ const { agentId: _omit, ...wire } = body ?? {};
23
+ const res = await fetchImpl(`${base}/api/${method}`, {
24
+ method: "POST",
25
+ headers: {
26
+ "content-type": "application/json",
27
+ authorization: `Bearer ${config.voucher}`
28
+ },
29
+ body: JSON.stringify(wire)
30
+ });
31
+ const json = await res.json();
32
+ if (!res.ok) {
33
+ const e = new Error(json?.error ?? `proxy api/${method} failed (${res.status})`);
34
+ e.code = json?.code;
35
+ e.hint = json?.hint;
36
+ throw e;
37
+ }
38
+ return json;
39
+ }
40
+ return {
41
+ listServers: (r) => call("listServers", r),
42
+ listChannels: (r) => call("listChannels", r),
43
+ inboxPull: (r) => call("inboxPull", r),
44
+ inboxSnapshot: (r) => call("inboxSnapshot", r),
45
+ ack: (r) => call("ack", r),
46
+ send: (r) => call("send", r),
47
+ read: (r) => call("read", r),
48
+ resolve: (r) => call("resolve", r),
49
+ listMembers: (r) => call("listMembers", r),
50
+ joinServer: (r) => call("joinServer", r)
51
+ };
52
+ }
53
+
54
+ // src/cli/daemonStart.ts
55
+ import * as fs9 from "fs";
56
+ import * as path10 from "path";
57
+ import * as crypto2 from "crypto";
58
+ import * as os3 from "os";
59
+ import { homedir as homedir3 } from "os";
60
+ import { WebSocket } from "ws";
61
+ import { createRequire as createRequire3 } from "module";
62
+
63
+ // src/daemon/createDaemon.ts
64
+ import { homedir as homedir2 } from "os";
65
+
66
+ // src/logger.ts
67
+ var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
68
+ var DEFAULT_HEADER = "@alook/daemon";
69
+ var VALID_LEVELS = ["debug", "info", "warn", "error"];
70
+ function envLevel() {
71
+ const raw = process.env.ALOOK_LOG_LEVEL?.toLowerCase();
72
+ return VALID_LEVELS.includes(raw) ? raw : undefined;
73
+ }
74
+ function formatData(data) {
75
+ if (data.length === 0)
76
+ return "";
77
+ const parts = [];
78
+ for (const d of data) {
79
+ if (d instanceof Error) {
80
+ parts.push(`err=${d.message}`);
81
+ } else if (d !== null && typeof d === "object" && !Array.isArray(d)) {
82
+ const entries = Object.entries(d);
83
+ for (const [k, v] of entries) {
84
+ parts.push(`${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
85
+ }
86
+ } else {
87
+ parts.push(String(d));
88
+ }
89
+ }
90
+ return " " + parts.join(" ");
91
+ }
92
+ function createLogger(options = {}) {
93
+ const header = options.header ?? DEFAULT_HEADER;
94
+ const minRank = LEVEL_RANK[options.level ?? envLevel() ?? "info"];
95
+ const now = options.now ?? (() => new Date().toISOString());
96
+ const out = options.out ?? ((line) => process.stdout.write(line + `
97
+ `));
98
+ const err = options.err ?? ((line) => process.stderr.write(line + `
99
+ `));
100
+ const emit = (level, message, data) => {
101
+ if (LEVEL_RANK[level] < minRank)
102
+ return;
103
+ const line = `${now()} ${header} ${level.toUpperCase().padEnd(5)} ${message}${formatData(data)}`;
104
+ (level === "warn" || level === "error" ? err : out)(line);
105
+ };
106
+ return {
107
+ debug: (m, ...d) => emit("debug", m, d),
108
+ info: (m, ...d) => emit("info", m, d),
109
+ warn: (m, ...d) => emit("warn", m, d),
110
+ error: (m, ...d) => emit("error", m, d),
111
+ child: (tag) => createLogger({ ...options, header: `${header}:${tag}` })
112
+ };
113
+ }
114
+
115
+ // src/server/wsControlChannel.ts
116
+ function describeErr(err) {
117
+ return err instanceof Error ? err.message : String(err);
118
+ }
119
+
120
+ class WsControlChannel {
121
+ opts;
122
+ statusValue = "idle";
123
+ commandCbs = [];
124
+ resyncHooks = [];
125
+ ws = null;
126
+ attempt = 0;
127
+ closedByUser = false;
128
+ authRejected = false;
129
+ pingTimer = null;
130
+ pongDeadline = 0;
131
+ resyncProvider = null;
132
+ log;
133
+ constructor(opts) {
134
+ this.opts = opts;
135
+ this.log = opts.logger ?? createLogger({ header: "@alook/daemon:ws" });
136
+ }
137
+ get status() {
138
+ return this.statusValue;
139
+ }
140
+ connect() {
141
+ this.closedByUser = false;
142
+ this.authRejected = false;
143
+ this.openSocket();
144
+ }
145
+ close() {
146
+ this.closedByUser = true;
147
+ this.clearHeartbeat();
148
+ this.ws?.close();
149
+ this.ws = null;
150
+ this.statusValue = "closed";
151
+ }
152
+ onCommand(cb) {
153
+ this.commandCbs.push(cb);
154
+ }
155
+ onResync(provider) {
156
+ this.resyncProvider = provider;
157
+ }
158
+ onOpen(hook) {
159
+ this.resyncHooks.push(hook);
160
+ }
161
+ async reportReady(ready) {
162
+ this.sendFrame({ type: "ready", ...ready });
163
+ }
164
+ sendReady(ready) {
165
+ this.sendFrame({ type: "ready", ...ready });
166
+ }
167
+ async reportAgentSession(info) {
168
+ this.sendFrame({ type: "agent_session", ...info });
169
+ }
170
+ async reportAgentActivity(info) {
171
+ this.sendFrame({ type: "agent_activity", ...info });
172
+ }
173
+ async reportBotAuditEvent(frame) {
174
+ this.sendFrame(frame);
175
+ }
176
+ async reportWakeAck(info) {
177
+ this.sendFrame({ type: "agent_wake_ack", ...info });
178
+ }
179
+ async reportStoppedAck(info) {
180
+ this.sendFrame({ type: "agent_stopped_ack", ...info });
181
+ }
182
+ async reportSessionError(frame) {
183
+ this.sendFrame(frame);
184
+ }
185
+ sendFrame(frame) {
186
+ if (this.statusValue !== "open" || !this.ws) {
187
+ this.log.debug("frame dropped — socket not open", { type: frame.type });
188
+ return;
189
+ }
190
+ this.ws.send(JSON.stringify(frame));
191
+ }
192
+ resyncOnConnect() {
193
+ if (this.resyncProvider) {
194
+ const { ready, sessions } = this.resyncProvider();
195
+ this.sendFrame({ type: "ready", ...ready });
196
+ for (const s of sessions)
197
+ this.sendFrame({ type: "agent_session", ...s });
198
+ this.log.info("resync sent", { ready: ready.runtimeReport.length, sessions: sessions.length });
199
+ }
200
+ for (const hook of this.resyncHooks) {
201
+ try {
202
+ hook();
203
+ } catch {}
204
+ }
205
+ }
206
+ openSocket() {
207
+ this.statusValue = this.attempt === 0 ? "connecting" : "reconnecting";
208
+ const ws = this.opts.webSocketFactory(this.opts.url, this.opts.headers ?? {});
209
+ this.ws = ws;
210
+ ws.on("open", () => {
211
+ this.statusValue = "open";
212
+ this.log.info("control channel open", { attempt: this.attempt });
213
+ this.startHeartbeat();
214
+ this.resyncOnConnect();
215
+ });
216
+ ws.on("message", (data) => this.onMessage(data));
217
+ ws.on("pong", () => {
218
+ this.attempt = 0;
219
+ this.pongDeadline = this.now() + (this.opts.heartbeat?.pongTimeoutMs ?? 30000);
220
+ this.log.debug("heartbeat pong");
221
+ });
222
+ ws.on("close", (code, reason) => this.onSocketClosed(code, reason));
223
+ ws.on("error", () => {});
224
+ }
225
+ onMessage(data) {
226
+ let frame = null;
227
+ try {
228
+ frame = JSON.parse(String(data));
229
+ } catch {
230
+ return;
231
+ }
232
+ if (!frame || typeof frame.type !== "string")
233
+ return;
234
+ if (frame.type === "error" && frame.code === "AUTH_REJECTED") {
235
+ this.authRejected = true;
236
+ this.log.error("AUTH_REJECTED received — machine key rejected, not reconnecting");
237
+ this.opts.onAuthRejected?.();
238
+ return;
239
+ }
240
+ this.attempt = 0;
241
+ const cmd = frame;
242
+ for (const cb of this.commandCbs) {
243
+ try {
244
+ Promise.resolve(cb(cmd)).catch((err) => {
245
+ this.log.warn("command listener threw", { type: cmd.type, err: describeErr(err) });
246
+ });
247
+ } catch (err) {
248
+ this.log.warn("command listener threw synchronously", { type: cmd.type, err: describeErr(err) });
249
+ }
250
+ }
251
+ }
252
+ onSocketClosed(code, reason) {
253
+ this.log.warn("control channel closed", { code, reason: reason ? String(reason) : "" });
254
+ this.clearHeartbeat();
255
+ this.ws = null;
256
+ if (this.closedByUser)
257
+ return;
258
+ if (this.authRejected) {
259
+ this.statusValue = "closed";
260
+ return;
261
+ }
262
+ this.scheduleReconnect();
263
+ }
264
+ scheduleReconnect() {
265
+ const base = this.opts.reconnect?.baseMs ?? 500;
266
+ const max = this.opts.reconnect?.maxMs ?? 30000;
267
+ const maxAttempts = this.opts.reconnect?.maxAttempts ?? Infinity;
268
+ if (this.attempt >= maxAttempts) {
269
+ this.statusValue = "closed";
270
+ return;
271
+ }
272
+ this.attempt += 1;
273
+ const delayMs = Math.min(max, base * 2 ** (this.attempt - 1));
274
+ this.statusValue = "reconnecting";
275
+ this.log.info("reconnecting", { attempt: this.attempt, delayMs });
276
+ setTimeout(() => this.openSocket(), delayMs);
277
+ }
278
+ startHeartbeat() {
279
+ const interval = this.opts.heartbeat?.pingIntervalMs ?? 15000;
280
+ const timeout = this.opts.heartbeat?.pongTimeoutMs ?? 30000;
281
+ this.pongDeadline = this.now() + timeout;
282
+ this.pingTimer = setInterval(() => {
283
+ if (this.now() > this.pongDeadline) {
284
+ this.log.warn("heartbeat pong timeout — forcing reconnect");
285
+ this.ws?.close();
286
+ return;
287
+ }
288
+ this.log.debug("heartbeat ping");
289
+ this.ws?.ping?.();
290
+ }, interval);
291
+ this.pingTimer.unref?.();
292
+ }
293
+ clearHeartbeat() {
294
+ if (this.pingTimer) {
295
+ clearInterval(this.pingTimer);
296
+ this.pingTimer = null;
297
+ }
298
+ }
299
+ now() {
300
+ return this.opts.now ? this.opts.now() : Date.now();
301
+ }
302
+ }
303
+
304
+ // src/credentials/credentialProxy.ts
305
+ import * as crypto from "crypto";
306
+ import * as fs2 from "fs";
307
+ import * as http from "http";
308
+ import * as https from "https";
309
+ import * as os from "os";
310
+ import * as path from "path";
311
+ import { URL } from "url";
312
+ var DEFAULT_HEADER_NAMES = {
313
+ agentId: "X-Agent-Id",
314
+ client: "X-Client",
315
+ capabilities: "X-Agent-Active-Capabilities"
316
+ };
317
+ function randomVoucher(prefix) {
318
+ return prefix + crypto.randomBytes(32).toString("base64url");
319
+ }
320
+ function sanitizeIdSegment(id) {
321
+ return id.replace(/[^A-Za-z0-9._-]/g, "_") || "_";
322
+ }
323
+
324
+ class CredentialBroker {
325
+ registrations = new Map;
326
+ voucherPrefix;
327
+ voucherDir;
328
+ upstreamBaseUrl;
329
+ clientLabel;
330
+ headerNames;
331
+ constructor(config) {
332
+ if (!config.upstreamBaseUrl)
333
+ throw new Error("CredentialBroker: upstreamBaseUrl is required");
334
+ this.upstreamBaseUrl = config.upstreamBaseUrl.replace(/\/+$/, "");
335
+ this.voucherPrefix = config.voucherPrefix ?? "vch_";
336
+ this.clientLabel = config.clientLabel ?? "cli";
337
+ this.headerNames = config.headerNames ?? DEFAULT_HEADER_NAMES;
338
+ this.voucherDir = config.voucherDir ?? path.join(os.tmpdir(), "agent-vouchers");
339
+ }
340
+ mint(agentId, launchId, capabilities, runnerKey) {
341
+ if (!runnerKey)
342
+ throw new Error("CredentialBroker.mint: runnerKey is required (per-agent tier-2 credential)");
343
+ const voucher = randomVoucher(this.voucherPrefix);
344
+ const dir = path.join(this.voucherDir, sanitizeIdSegment(agentId));
345
+ fs2.mkdirSync(dir, { recursive: true });
346
+ const voucherFile = path.join(dir, `${sanitizeIdSegment(launchId)}.token`);
347
+ fs2.writeFileSync(voucherFile, voucher, { mode: 384 });
348
+ this.registrations.set(voucher, {
349
+ agentId,
350
+ launchId,
351
+ capabilities: new Set(capabilities),
352
+ voucherFile,
353
+ runnerKey
354
+ });
355
+ return { voucher, agentId, launchId, capabilities: [...capabilities], voucherFile };
356
+ }
357
+ revoke(voucher) {
358
+ const reg = this.registrations.get(voucher);
359
+ if (!reg)
360
+ return false;
361
+ this.registrations.delete(voucher);
362
+ try {
363
+ fs2.rmSync(reg.voucherFile, { force: true });
364
+ } catch {}
365
+ return true;
366
+ }
367
+ revokeAgent(agentId) {
368
+ let n = 0;
369
+ for (const [voucher, reg] of this.registrations) {
370
+ if (reg.agentId === agentId && this.revoke(voucher))
371
+ n++;
372
+ }
373
+ return n;
374
+ }
375
+ get size() {
376
+ return this.registrations.size;
377
+ }
378
+ check(authHeader, requiredCapability) {
379
+ const voucher = parseBearer(authHeader);
380
+ if (!voucher) {
381
+ return { ok: false, status: 401, code: "missing_voucher", error: "missing bearer voucher" };
382
+ }
383
+ const reg = this.registrations.get(voucher);
384
+ if (!reg) {
385
+ return { ok: false, status: 401, code: "invalid_proxy_token", error: "invalid local agent proxy token" };
386
+ }
387
+ if (requiredCapability && !reg.capabilities.has(requiredCapability)) {
388
+ return {
389
+ ok: false,
390
+ status: 403,
391
+ code: "capability_denied",
392
+ error: `capability '${requiredCapability}' not granted to this voucher`
393
+ };
394
+ }
395
+ return { ok: true, reg };
396
+ }
397
+ }
398
+ function parseBearer(authHeader) {
399
+ if (!authHeader)
400
+ return null;
401
+ const m = /^Bearer\s+(.+)$/i.exec(authHeader.trim());
402
+ return m ? m[1].trim() : null;
403
+ }
404
+ var DEFAULT_CAPABILITY_RESOLVER = (_method, pathname) => {
405
+ if (pathname.includes("/send"))
406
+ return "send";
407
+ if (pathname.includes("/history") || pathname.includes("/search") || pathname.includes("/inbox"))
408
+ return "read";
409
+ if (pathname.includes("/server") || pathname.includes("/channel"))
410
+ return "server";
411
+ return;
412
+ };
413
+ var DEFAULT_UPSTREAM_TIMEOUT_MS = 20000;
414
+ async function startCredentialProxy(broker, options = {}) {
415
+ const host = options.host ?? "127.0.0.1";
416
+ const resolveCap = options.capabilityResolver ?? DEFAULT_CAPABILITY_RESOLVER;
417
+ const upstream = new URL(broker.upstreamBaseUrl);
418
+ const upstreamClient = upstream.protocol === "https:" ? https : http;
419
+ const onPull = options.onInboxPullResponse;
420
+ const onProxyRequest = options.onProxyRequest;
421
+ const server = http.createServer((req, res) => {
422
+ const pathname = new URL(req.url ?? "/", "http://placeholder").pathname;
423
+ const requiredCap = resolveCap(req.method ?? "GET", pathname);
424
+ const verdict = broker.check(req.headers["authorization"], requiredCap);
425
+ if (!verdict.ok) {
426
+ res.writeHead(verdict.status, { "content-type": "application/json" });
427
+ res.end(JSON.stringify({ error: verdict.error, code: verdict.code }));
428
+ req.resume();
429
+ return;
430
+ }
431
+ const reg = verdict.reg;
432
+ const isInboxPull = onPull && pathname.endsWith("/inboxPull");
433
+ if (onProxyRequest) {
434
+ try {
435
+ onProxyRequest(reg.agentId, req.method ?? "GET", pathname);
436
+ } catch {}
437
+ }
438
+ const outHeaders = { ...req.headers };
439
+ delete outHeaders["authorization"];
440
+ delete outHeaders["host"];
441
+ delete outHeaders["content-length"];
442
+ outHeaders["authorization"] = `Bearer ${reg.runnerKey}`;
443
+ outHeaders[broker.headerNames.agentId.toLowerCase()] = reg.agentId;
444
+ outHeaders[broker.headerNames.client.toLowerCase()] = broker.clientLabel;
445
+ outHeaders[broker.headerNames.capabilities.toLowerCase()] = [...reg.capabilities].join(",");
446
+ let responded = false;
447
+ let upstreamRes;
448
+ const upstreamReq = upstreamClient.request({
449
+ protocol: upstream.protocol,
450
+ hostname: upstream.hostname,
451
+ port: upstream.port || (upstream.protocol === "https:" ? 443 : 80),
452
+ method: req.method,
453
+ path: joinPath(upstream.pathname, rewriteAgentPath(req.url ?? "/")),
454
+ headers: outHeaders
455
+ }, (res_) => {
456
+ responded = true;
457
+ upstreamRes = res_;
458
+ const destroyResIfIncomplete = () => {
459
+ if (!res_.complete)
460
+ res.destroy();
461
+ };
462
+ res_.on("error", destroyResIfIncomplete);
463
+ res_.on("close", destroyResIfIncomplete);
464
+ if (isInboxPull && res_.statusCode && res_.statusCode < 300) {
465
+ const chunks = [];
466
+ res_.on("data", (chunk) => chunks.push(chunk));
467
+ res_.on("end", () => {
468
+ const body = Buffer.concat(chunks);
469
+ res.writeHead(res_.statusCode, res_.headers);
470
+ res.end(body);
471
+ try {
472
+ const parsed = JSON.parse(body.toString());
473
+ if (parsed.messages)
474
+ onPull(reg.agentId, parsed.messages);
475
+ } catch {}
476
+ });
477
+ } else {
478
+ res.writeHead(res_.statusCode ?? 502, res_.headers);
479
+ res_.pipe(res);
480
+ }
481
+ });
482
+ upstreamReq.on("error", (err) => {
483
+ if (responded)
484
+ return;
485
+ responded = true;
486
+ res.writeHead(502, { "content-type": "application/json" });
487
+ res.end(JSON.stringify({ error: `upstream error: ${err.message}`, code: "upstream_error" }));
488
+ });
489
+ const upstreamTimeoutMs = options.upstreamTimeoutMs ?? DEFAULT_UPSTREAM_TIMEOUT_MS;
490
+ upstreamReq.setTimeout(upstreamTimeoutMs, () => {
491
+ upstreamReq.destroy();
492
+ upstreamRes?.destroy();
493
+ if (responded) {
494
+ res.destroy();
495
+ return;
496
+ }
497
+ responded = true;
498
+ res.writeHead(504, { "content-type": "application/json" });
499
+ res.end(JSON.stringify({ error: `upstream request timed out after ${upstreamTimeoutMs}ms`, code: "upstream_timeout" }));
500
+ });
501
+ res.on("close", () => {
502
+ upstreamReq.destroy();
503
+ upstreamRes?.destroy();
504
+ });
505
+ req.pipe(upstreamReq);
506
+ });
507
+ await new Promise((resolve, reject) => {
508
+ server.once("error", reject);
509
+ server.listen(options.port ?? 0, host, () => {
510
+ server.removeListener("error", reject);
511
+ resolve();
512
+ });
513
+ });
514
+ const addr = server.address();
515
+ const port = typeof addr === "object" && addr ? addr.port : options.port ?? 0;
516
+ const url = `http://${host}:${port}`;
517
+ return {
518
+ url,
519
+ port,
520
+ close: () => new Promise((resolve) => {
521
+ server.close(() => resolve());
522
+ })
523
+ };
524
+ }
525
+ function joinPath(basePath, reqUrl) {
526
+ const base = basePath.replace(/\/+$/, "");
527
+ const reqPath = reqUrl.startsWith("/") ? reqUrl : `/${reqUrl}`;
528
+ return base + reqPath || "/";
529
+ }
530
+ function rewriteAgentPath(reqUrl) {
531
+ const url = new URL(reqUrl, "http://placeholder");
532
+ if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
533
+ url.pathname = `/api/community/agent${url.pathname.slice("/api".length)}`;
534
+ }
535
+ return url.pathname + url.search;
536
+ }
537
+ // src/runtime/apmStateMachine.ts
538
+ var MAX_APM_GATED_STEERING_EVENTS = 12;
539
+ function createInitialApmGatedSteeringState() {
540
+ return {
541
+ isIdle: false,
542
+ expectedTerminationReason: null,
543
+ phase: "idle",
544
+ outstandingToolUses: 0,
545
+ compacting: false,
546
+ toolBoundaryFlushDisabled: false,
547
+ lastFlushReason: null,
548
+ recentEvents: []
549
+ };
550
+ }
551
+ function reduceApmGatedToolUse(state, input) {
552
+ if (input.kind === "tool_call") {
553
+ return {
554
+ nextState: {
555
+ ...state,
556
+ isIdle: false,
557
+ phase: "tool_wait",
558
+ outstandingToolUses: state.outstandingToolUses + 1
559
+ },
560
+ hadOutstandingToolUse: state.outstandingToolUses > 0,
561
+ shouldFlushToolBatch: false
562
+ };
563
+ }
564
+ const hadOutstandingToolUse = state.outstandingToolUses > 0;
565
+ const outstandingToolUses = Math.max(0, state.outstandingToolUses - 1);
566
+ return {
567
+ nextState: {
568
+ ...state,
569
+ isIdle: false,
570
+ phase: "tool_boundary",
571
+ outstandingToolUses
572
+ },
573
+ hadOutstandingToolUse,
574
+ shouldFlushToolBatch: hadOutstandingToolUse && outstandingToolUses === 0
575
+ };
576
+ }
577
+ function reduceApmGatedCompaction(state, input) {
578
+ if (input.kind === "compaction_started") {
579
+ return { nextState: { ...state, isIdle: false, phase: "compacting", compacting: true } };
580
+ }
581
+ if (input.kind === "compaction_interrupted") {
582
+ return { nextState: { ...state, isIdle: false, compacting: false } };
583
+ }
584
+ return {
585
+ nextState: { ...state, isIdle: false, phase: "assistant_continuation", compacting: false }
586
+ };
587
+ }
588
+ function reduceApmGatedReview(state, input) {
589
+ if (input.kind === "review_started") {
590
+ return { nextState: { ...state, isIdle: false, phase: "reviewing", reviewing: true } };
591
+ }
592
+ return {
593
+ nextState: { ...state, isIdle: false, phase: "assistant_continuation", reviewing: false }
594
+ };
595
+ }
596
+ function reduceApmGatedError(state, input = {}) {
597
+ const shouldDisableToolBoundaryFlush = input.disableToolBoundaryFlush === true;
598
+ return {
599
+ nextState: {
600
+ ...state,
601
+ isIdle: input.terminalWakeable === true,
602
+ phase: "error",
603
+ compacting: false,
604
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled || shouldDisableToolBoundaryFlush
605
+ },
606
+ shouldDisableToolBoundaryFlush
607
+ };
608
+ }
609
+ function reduceApmGatedFlushReadiness(state, input) {
610
+ if (!input.isGated)
611
+ return { shouldNotify: false, blockedReason: "non_gated", effects: [] };
612
+ if (!input.hasSession)
613
+ return { shouldNotify: false, blockedReason: "missing_session", effects: [] };
614
+ if (input.inboxLength === 0)
615
+ return { shouldNotify: false, blockedReason: "empty_inbox", effects: [] };
616
+ if (state.toolBoundaryFlushDisabled) {
617
+ return { shouldNotify: false, blockedReason: "tool_boundary_flush_disabled", effects: [] };
618
+ }
619
+ if (state.compacting)
620
+ return { shouldNotify: false, blockedReason: "compacting", effects: [] };
621
+ if (state.reviewing)
622
+ return { shouldNotify: false, blockedReason: "reviewing", effects: [] };
623
+ if (state.outstandingToolUses > 0) {
624
+ return { shouldNotify: false, blockedReason: "outstanding_tool_uses", effects: [] };
625
+ }
626
+ return {
627
+ shouldNotify: true,
628
+ blockedReason: null,
629
+ effects: [{ kind: "notify_stdin", reason: input.reason, stdinMode: "busy", clauseId: "SMR-002" }]
630
+ };
631
+ }
632
+ function reduceApmGatedRecentEvent(state, input) {
633
+ const summary = `${input.event}:${state.phase}:tools=${state.outstandingToolUses}:compact=${state.compacting}`;
634
+ return {
635
+ nextState: {
636
+ ...state,
637
+ recentEvents: [...state.recentEvents, summary].slice(-MAX_APM_GATED_STEERING_EVENTS)
638
+ }
639
+ };
640
+ }
641
+
642
+ // src/manager/managerPolicy.ts
643
+ var DEFAULT_STALE_THRESHOLD_MS = 120000;
644
+ var DEFAULT_IDLE_TIMEOUT_MS = 300000;
645
+ function createInitialManagerState(staleThresholdMs = DEFAULT_STALE_THRESHOLD_MS, idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS) {
646
+ return { agents: {}, staleThresholdMs, idleTimeoutMs };
647
+ }
648
+ function reduceManager(state, event) {
649
+ switch (event.type) {
650
+ case "register":
651
+ return withAgent(state, event.agentId, (a) => a ?? freshAgent(event.agentId, event.caps), []);
652
+ case "wake":
653
+ return onWake(state, event.agentId, event.message);
654
+ case "spawned":
655
+ return mutate(state, event.agentId, (a) => {
656
+ a.status = "running";
657
+ a.turnActive = true;
658
+ a.lastProgressAt = event.nowMs;
659
+ a.idleSince = null;
660
+ });
661
+ case "session":
662
+ return mutate(state, event.agentId, (a) => {
663
+ a.sessionId = event.sessionId;
664
+ });
665
+ case "progress":
666
+ return mutate(state, event.agentId, (a) => {
667
+ a.lastProgressAt = event.nowMs;
668
+ });
669
+ case "turn_end":
670
+ return onTurnEnd(state, event.agentId, event.nowMs);
671
+ case "exit":
672
+ return onExit(state, event.agentId);
673
+ case "tick":
674
+ return onTick(state, event.nowMs);
675
+ case "runtime_signal":
676
+ return onRuntimeSignal(state, event.agentId, event.kind);
677
+ }
678
+ }
679
+ function onWake(state, agentId, message) {
680
+ const existing = state.agents[agentId];
681
+ const agent = existing ? clone(existing) : null;
682
+ if (!agent) {
683
+ return { state, effects: [] };
684
+ }
685
+ agent.inbox = [...agent.inbox, message];
686
+ agent.idleSince = null;
687
+ if (agent.status === "idle") {
688
+ agent.status = "starting";
689
+ const prompt = drainInboxToPrompt(agent);
690
+ return commit(state, agent, [
691
+ { type: "spawn", agentId, prompt, resumeSessionId: agent.sessionId }
692
+ ]);
693
+ }
694
+ if (agent.status === "running") {
695
+ if (agent.caps.lifecycleKind === "persistent" && agent.caps.supportsStdinNotification) {
696
+ const isGatedMidTurn = agent.caps.busyDeliveryMode === "gated" && agent.turnActive;
697
+ if (isGatedMidTurn) {
698
+ return commit(state, agent, [
699
+ {
700
+ type: "gated_hold",
701
+ agentId,
702
+ reason: "mid_turn_wake",
703
+ blockedReason: agent.apm.phase,
704
+ recentEvents: agent.apm.recentEvents
705
+ }
706
+ ]);
707
+ }
708
+ const text = drainInboxToPrompt(agent);
709
+ const mode = agent.turnActive ? "busy" : "idle";
710
+ if (mode === "idle")
711
+ agent.turnActive = true;
712
+ return commit(state, agent, [{ type: "send", agentId, text, mode }]);
713
+ }
714
+ return commit(state, agent, []);
715
+ }
716
+ return commit(state, agent, []);
717
+ }
718
+ function onTurnEnd(state, agentId, nowMs) {
719
+ const existing = state.agents[agentId];
720
+ if (!existing)
721
+ return { state, effects: [] };
722
+ const agent = clone(existing);
723
+ agent.turnActive = false;
724
+ agent.lastProgressAt = nowMs;
725
+ agent.apm = createInitialApmGatedSteeringState();
726
+ if (agent.caps.lifecycleKind === "per_turn") {
727
+ return commit(state, agent, []);
728
+ }
729
+ if (agent.inbox.length > 0 && agent.caps.supportsStdinNotification) {
730
+ const text = drainInboxToPrompt(agent);
731
+ agent.turnActive = true;
732
+ return commit(state, agent, [{ type: "send", agentId, text, mode: "idle" }]);
733
+ }
734
+ agent.idleSince = nowMs;
735
+ return commit(state, agent, []);
736
+ }
737
+ function onRuntimeSignal(state, agentId, kind) {
738
+ const existing = state.agents[agentId];
739
+ if (!existing)
740
+ return { state, effects: [] };
741
+ const agent = clone(existing);
742
+ const isGatedActive = agent.status === "running" && agent.turnActive && agent.caps.busyDeliveryMode === "gated";
743
+ if (!isGatedActive) {
744
+ agent.apm = reduceApmGatedRecentEvent(agent.apm, { event: kind }).nextState;
745
+ return commit(state, agent, []);
746
+ }
747
+ let attemptFlushReason = null;
748
+ switch (kind) {
749
+ case "tool_call":
750
+ agent.apm = reduceApmGatedToolUse(agent.apm, { kind }).nextState;
751
+ break;
752
+ case "tool_output":
753
+ agent.apm = reduceApmGatedToolUse(agent.apm, { kind }).nextState;
754
+ attemptFlushReason = "tool_batch_complete";
755
+ break;
756
+ case "compaction_started":
757
+ case "compaction_finished":
758
+ agent.apm = reduceApmGatedCompaction(agent.apm, { kind }).nextState;
759
+ if (kind === "compaction_finished")
760
+ attemptFlushReason = "compaction_finished";
761
+ break;
762
+ case "review_started":
763
+ case "review_finished":
764
+ agent.apm = reduceApmGatedReview(agent.apm, { kind }).nextState;
765
+ if (kind === "review_finished")
766
+ attemptFlushReason = "review_finished";
767
+ break;
768
+ case "error":
769
+ agent.apm = reduceApmGatedError(agent.apm, { disableToolBoundaryFlush: true }).nextState;
770
+ break;
771
+ default:
772
+ break;
773
+ }
774
+ agent.apm = reduceApmGatedRecentEvent(agent.apm, { event: kind }).nextState;
775
+ if (attemptFlushReason === null)
776
+ return commit(state, agent, []);
777
+ if (agent.inbox.length === 0)
778
+ return commit(state, agent, []);
779
+ const readiness = reduceApmGatedFlushReadiness(agent.apm, {
780
+ isGated: true,
781
+ hasSession: agent.sessionId != null,
782
+ inboxLength: agent.inbox.length,
783
+ reason: attemptFlushReason
784
+ });
785
+ if (readiness.shouldNotify) {
786
+ const text = drainInboxToPrompt(agent);
787
+ return commit(state, agent, [{ type: "send", agentId, text, mode: "busy" }]);
788
+ }
789
+ return commit(state, agent, [
790
+ {
791
+ type: "gated_hold",
792
+ agentId,
793
+ reason: attemptFlushReason,
794
+ blockedReason: readiness.blockedReason,
795
+ recentEvents: agent.apm.recentEvents
796
+ }
797
+ ]);
798
+ }
799
+ function onExit(state, agentId) {
800
+ const existing = state.agents[agentId];
801
+ if (!existing)
802
+ return { state, effects: [] };
803
+ const agent = clone(existing);
804
+ agent.turnActive = false;
805
+ if (agent.inbox.length > 0) {
806
+ agent.status = "starting";
807
+ const prompt = drainInboxToPrompt(agent);
808
+ return commit(state, agent, [
809
+ { type: "spawn", agentId, prompt, resumeSessionId: agent.sessionId }
810
+ ]);
811
+ }
812
+ agent.status = "idle";
813
+ return commit(state, agent, []);
814
+ }
815
+ function onTick(state, nowMs) {
816
+ const effects = [];
817
+ const agents = { ...state.agents };
818
+ for (const id of Object.keys(agents)) {
819
+ const a = agents[id];
820
+ const stalled = a.status === "running" && a.turnActive && nowMs - a.lastProgressAt >= state.staleThresholdMs && (a.caps.lifecycleKind === "per_turn" || a.caps.supportsStdinNotification && a.caps.busyDeliveryMode === "direct");
821
+ if (stalled) {
822
+ agents[id] = { ...a, status: "stopping", idleSince: null };
823
+ effects.push({ type: "terminate_stalled", agentId: id });
824
+ continue;
825
+ }
826
+ const idleEligible = a.status === "running" && !a.turnActive && a.inbox.length === 0 && a.caps.lifecycleKind === "persistent" && state.idleTimeoutMs > 0 && Number.isFinite(state.idleTimeoutMs);
827
+ if (idleEligible && a.idleSince !== null && nowMs - a.idleSince >= state.idleTimeoutMs) {
828
+ agents[id] = { ...a, status: "stopping", idleSince: null };
829
+ effects.push({ type: "stop", agentId: id, reason: "idle_timeout" });
830
+ }
831
+ }
832
+ return { state: { ...state, agents }, effects };
833
+ }
834
+ function freshAgent(agentId, caps) {
835
+ return {
836
+ agentId,
837
+ status: "idle",
838
+ caps,
839
+ inbox: [],
840
+ sessionId: null,
841
+ turnActive: false,
842
+ lastProgressAt: 0,
843
+ idleSince: null,
844
+ apm: createInitialApmGatedSteeringState()
845
+ };
846
+ }
847
+ function drainInboxToPrompt(agent) {
848
+ const seen = new Set;
849
+ const unique = [];
850
+ for (const m of agent.inbox) {
851
+ if (!seen.has(m.text)) {
852
+ seen.add(m.text);
853
+ unique.push(m.text);
854
+ }
855
+ }
856
+ agent.inbox = [];
857
+ return unique.join(`
858
+ `);
859
+ }
860
+ function clone(a) {
861
+ return {
862
+ ...a,
863
+ inbox: [...a.inbox],
864
+ caps: { ...a.caps },
865
+ apm: { ...a.apm, recentEvents: [...a.apm.recentEvents] }
866
+ };
867
+ }
868
+ function commit(state, agent, effects) {
869
+ return { state: { ...state, agents: { ...state.agents, [agent.agentId]: agent } }, effects };
870
+ }
871
+ function mutate(state, agentId, fn) {
872
+ const existing = state.agents[agentId];
873
+ if (!existing)
874
+ return { state, effects: [] };
875
+ const agent = clone(existing);
876
+ fn(agent);
877
+ return commit(state, agent, []);
878
+ }
879
+ function withAgent(state, agentId, make, effects) {
880
+ const agent = make(state.agents[agentId]);
881
+ return { state: { ...state, agents: { ...state.agents, [agentId]: agent } }, effects };
882
+ }
883
+ // src/runtime/runtimeSession.ts
884
+ import { EventEmitter } from "events";
885
+
886
+ // src/runtime/killTree.ts
887
+ import { spawn } from "child_process";
888
+ var POLL_MS = 100;
889
+ var DEFAULT_GRACE_MS = 2000;
890
+ var isPosix = process.platform !== "win32";
891
+ function spawnAgentProcess(command, args, opts) {
892
+ return spawn(command, args, {
893
+ cwd: opts.cwd,
894
+ stdio: ["pipe", "pipe", "pipe"],
895
+ env: opts.env,
896
+ shell: opts.shell ?? false,
897
+ detached: isPosix
898
+ });
899
+ }
900
+ function isAlive(pid) {
901
+ try {
902
+ process.kill(pid, 0);
903
+ return true;
904
+ } catch (e) {
905
+ return e?.code === "EPERM";
906
+ }
907
+ }
908
+ function signalTree(pid, signal) {
909
+ if (isPosix) {
910
+ try {
911
+ process.kill(-pid, signal);
912
+ } catch {}
913
+ }
914
+ try {
915
+ process.kill(pid, signal);
916
+ } catch {}
917
+ }
918
+ async function killProcessTree(pid, opts) {
919
+ if (!pid || pid < 1)
920
+ return;
921
+ if (!isAlive(pid))
922
+ return;
923
+ const graceMs = opts?.graceMs ?? DEFAULT_GRACE_MS;
924
+ signalTree(pid, "SIGTERM");
925
+ const deadline = Date.now() + graceMs;
926
+ while (Date.now() < deadline) {
927
+ if (!isAlive(pid))
928
+ return;
929
+ await new Promise((r) => setTimeout(r, POLL_MS));
930
+ }
931
+ if (isAlive(pid)) {
932
+ signalTree(pid, "SIGKILL");
933
+ }
934
+ }
935
+
936
+ // src/runtime/runtimeSession.ts
937
+ function descriptorFromDriver(driver) {
938
+ const lifecycle = driver.lifecycle.kind === "per_turn" ? "turn_based" : "persistent_stream";
939
+ const idle = driver.supportsStdinNotification ? "stdin" : "unsupported";
940
+ const busy = driver.supportsStdinNotification ? "stdin_steer" : "unsupported";
941
+ return {
942
+ transport: "child_process",
943
+ lifecycle,
944
+ input: { initial: "start", idle, busy },
945
+ readiness: "spawned",
946
+ turnBoundary: driver.lifecycle.kind === "per_turn" ? "process_exit" : "parsed_event",
947
+ startPolicy: driver.lifecycle.kind === "per_turn" ? driver.lifecycle.start : "immediate",
948
+ inFlightWake: driver.lifecycle.inFlightWake,
949
+ busyDelivery: driver.busyDeliveryMode,
950
+ postTurn: driver.terminateProcessOnTurnEnd ? "terminate_process" : driver.endStdinOnTurnEnd ? "close_stdin" : "keep_alive"
951
+ };
952
+ }
953
+
954
+ class ChildProcessRuntimeSession {
955
+ driver;
956
+ ctx;
957
+ descriptor;
958
+ events = new EventEmitter;
959
+ process = null;
960
+ started = false;
961
+ stdoutBuffer = "";
962
+ requestedStopReason;
963
+ constructor(driver, ctx) {
964
+ this.driver = driver;
965
+ this.ctx = ctx;
966
+ this.descriptor = descriptorFromDriver(driver);
967
+ }
968
+ get pid() {
969
+ return this.process?.pid;
970
+ }
971
+ get currentSessionId() {
972
+ return this.driver.currentSessionId;
973
+ }
974
+ get exitCode() {
975
+ return this.process?.exitCode ?? null;
976
+ }
977
+ get signalCode() {
978
+ return this.process?.signalCode ?? null;
979
+ }
980
+ get closed() {
981
+ return this.process ? this.process.exitCode != null || this.process.signalCode != null : false;
982
+ }
983
+ on(event, cb) {
984
+ this.events.on(event, cb);
985
+ }
986
+ async start(input) {
987
+ if (this.started) {
988
+ return { ok: false, reason: "runtime_error", error: "runtime session already started" };
989
+ }
990
+ this.started = true;
991
+ const launchCtx = {
992
+ ...this.ctx,
993
+ prompt: input.text,
994
+ config: { ...this.ctx.config, sessionId: input.sessionId ?? this.ctx.config.sessionId }
995
+ };
996
+ const { process: proc } = await this.driver.spawn(launchCtx);
997
+ this.process = proc;
998
+ this.attachProcess(proc);
999
+ return { ok: true, acceptedAs: "prompt" };
1000
+ }
1001
+ send(input) {
1002
+ const proc = this.process;
1003
+ if (!proc || this.closed)
1004
+ return { ok: false, reason: "closed" };
1005
+ const encoded = this.driver.encodeStdinMessage(input.text, input.sessionId ?? null, { mode: input.mode });
1006
+ if (!encoded)
1007
+ return { ok: false, reason: "unsupported" };
1008
+ proc.stdin?.write(encoded + `
1009
+ `);
1010
+ return { ok: true, acceptedAs: input.mode === "busy" ? "steer" : "prompt" };
1011
+ }
1012
+ async stop(opts) {
1013
+ const proc = this.process;
1014
+ if (!proc || this.closed)
1015
+ return;
1016
+ this.requestedStopReason = opts?.reason;
1017
+ const pid = proc.pid;
1018
+ if (pid) {
1019
+ await killProcessTree(pid, { graceMs: opts?.forceAfterMs ?? 2000 });
1020
+ } else {
1021
+ proc.kill(opts?.signal ?? "SIGTERM");
1022
+ }
1023
+ }
1024
+ attachProcess(proc) {
1025
+ proc.stdout?.on("data", (chunk) => {
1026
+ const chunkText = chunk.toString();
1027
+ this.events.emit("stdout", chunkText);
1028
+ this.stdoutBuffer += chunkText;
1029
+ const lines = this.stdoutBuffer.split(`
1030
+ `);
1031
+ this.stdoutBuffer = lines.pop() || "";
1032
+ for (const line of lines) {
1033
+ if (!line.trim())
1034
+ continue;
1035
+ for (const event of this.driver.parseLine(line)) {
1036
+ this.events.emit("runtime_event", event);
1037
+ }
1038
+ }
1039
+ });
1040
+ proc.stderr?.on("data", (chunk) => {
1041
+ const text = chunk.toString().trim();
1042
+ if (text)
1043
+ this.events.emit("stderr", text);
1044
+ });
1045
+ proc.on("error", (err) => this.events.emit("error", err));
1046
+ proc.on("exit", (code, signal) => this.events.emit("exit", { code, signal, reason: this.requestedStopReason ? "requested" : "runtime_exit" }));
1047
+ proc.on("close", (code, signal) => this.events.emit("close", { code, signal, reason: this.requestedStopReason ? "requested" : "runtime_exit" }));
1048
+ }
1049
+ }
1050
+ function createChildProcessRuntimeSession(driver, ctx) {
1051
+ return new ChildProcessRuntimeSession(driver, ctx);
1052
+ }
1053
+
1054
+ // src/runtime/sdkManagedSession.ts
1055
+ import { EventEmitter as EventEmitter2 } from "events";
1056
+
1057
+ class SdkManagedSession {
1058
+ driver;
1059
+ ctx;
1060
+ deps;
1061
+ events = new EventEmitter2;
1062
+ inner = null;
1063
+ startedSessionId = null;
1064
+ exited = false;
1065
+ starting = null;
1066
+ stopRequested = false;
1067
+ stopping = null;
1068
+ constructor(driver, ctx, deps) {
1069
+ this.driver = driver;
1070
+ this.ctx = ctx;
1071
+ this.deps = deps;
1072
+ }
1073
+ on(event, cb) {
1074
+ this.events.on(event, cb);
1075
+ }
1076
+ async start(input) {
1077
+ const launchCtx = {
1078
+ ...this.ctx,
1079
+ prompt: input.text,
1080
+ config: { ...this.ctx.config, sessionId: input.sessionId ?? this.ctx.config.sessionId }
1081
+ };
1082
+ this.starting = (async () => {
1083
+ const inner = await this.driver.createSession(launchCtx, this.deps);
1084
+ this.inner = inner;
1085
+ this.startedSessionId = inner.currentSessionId;
1086
+ inner.on("runtime_event", (...args) => this.events.emit("runtime_event", ...args));
1087
+ })();
1088
+ await this.starting;
1089
+ if (this.stopRequested) {
1090
+ if (this.stopping)
1091
+ await this.stopping.catch(() => {});
1092
+ return { ok: true };
1093
+ }
1094
+ this.inner.send(input.text, "idle").catch((err) => this.events.emit("error", err));
1095
+ return { ok: true };
1096
+ }
1097
+ send(input) {
1098
+ if (!this.inner)
1099
+ return { ok: false, reason: "not_started" };
1100
+ this.inner.send(input.text, input.mode).catch((err) => this.events.emit("error", err));
1101
+ return { ok: true };
1102
+ }
1103
+ async stop() {
1104
+ this.stopRequested = true;
1105
+ this.stopping = (async () => {
1106
+ try {
1107
+ if (this.starting)
1108
+ await this.starting.catch(() => {});
1109
+ await this.inner?.stop();
1110
+ } finally {
1111
+ this.emitExit();
1112
+ }
1113
+ })();
1114
+ await this.stopping;
1115
+ }
1116
+ emitExit() {
1117
+ if (this.exited)
1118
+ return;
1119
+ this.exited = true;
1120
+ this.events.emit("exit");
1121
+ }
1122
+ get currentSessionId() {
1123
+ return this.inner?.currentSessionId ?? this.startedSessionId;
1124
+ }
1125
+ }
1126
+
1127
+ // src/manager/managerRuntime.ts
1128
+ var THINKING_MAX_BYTES = 4096;
1129
+ function truncateThinking(text) {
1130
+ const chars = [...text].length;
1131
+ const buf = Buffer.from(text, "utf8");
1132
+ if (buf.byteLength <= THINKING_MAX_BYTES) {
1133
+ return { text, truncated: false, chars };
1134
+ }
1135
+ let end = THINKING_MAX_BYTES;
1136
+ while (end > 0 && (buf[end] & 192) === 128)
1137
+ end--;
1138
+ const truncatedText = buf.subarray(0, end).toString("utf8");
1139
+ return { text: truncatedText, truncated: true, chars };
1140
+ }
1141
+
1142
+ class AgentProcessManager {
1143
+ state;
1144
+ sessions = new Map;
1145
+ runtimeConfigs = new Map;
1146
+ resumeSessions = new Map;
1147
+ launchIds = new Map;
1148
+ liveSessions = new Map;
1149
+ thinkingBuffers = new Map;
1150
+ activeSpawnState = new Map;
1151
+ opts;
1152
+ tickTimer = null;
1153
+ now;
1154
+ log;
1155
+ constructor(opts) {
1156
+ this.opts = {
1157
+ tickIntervalMs: 5000,
1158
+ staleThresholdMs: 120000,
1159
+ idleTimeoutMs: 300000,
1160
+ ...opts
1161
+ };
1162
+ this.now = opts.now ?? (() => Date.now());
1163
+ this.log = opts.logger ?? createLogger({ header: "@alook/daemon:manager" });
1164
+ this.state = createInitialManagerState(this.opts.staleThresholdMs, this.opts.idleTimeoutMs);
1165
+ }
1166
+ register(agentId, launch) {
1167
+ if (launch?.runtimeConfig)
1168
+ this.runtimeConfigs.set(agentId, launch.runtimeConfig);
1169
+ if (launch?.sessionId)
1170
+ this.resumeSessions.set(agentId, launch.sessionId);
1171
+ if (launch?.launchId)
1172
+ this.launchIds.set(agentId, launch.launchId);
1173
+ const driver = this.opts.driverFor(agentId, this.runtimeConfigs.get(agentId));
1174
+ const caps = {
1175
+ lifecycleKind: driver.lifecycle.kind,
1176
+ supportsStdinNotification: driver.supportsStdinNotification,
1177
+ busyDeliveryMode: driver.busyDeliveryMode
1178
+ };
1179
+ this.dispatch({ type: "register", agentId, caps });
1180
+ }
1181
+ deliver(agentId, message) {
1182
+ this.dispatch({ type: "wake", agentId, message, nowMs: this.now() });
1183
+ }
1184
+ start() {
1185
+ if (this.tickTimer)
1186
+ return;
1187
+ this.tickTimer = setInterval(() => this.dispatch({ type: "tick", nowMs: this.now() }), this.opts.tickIntervalMs);
1188
+ this.tickTimer.unref?.();
1189
+ }
1190
+ async stop(agentId) {
1191
+ const session = this.sessions.get(agentId);
1192
+ if (!session)
1193
+ return;
1194
+ await Promise.resolve(session.stop({ reason: "requested", forceAfterMs: 5000 }));
1195
+ this.sessions.delete(agentId);
1196
+ }
1197
+ async stopAll() {
1198
+ if (this.tickTimer) {
1199
+ clearInterval(this.tickTimer);
1200
+ this.tickTimer = null;
1201
+ }
1202
+ await Promise.all([...this.sessions.values()].map((s) => Promise.resolve(s.stop({ reason: "shutdown" }))));
1203
+ this.sessions.clear();
1204
+ }
1205
+ snapshot() {
1206
+ return this.state;
1207
+ }
1208
+ auditContext(agentId) {
1209
+ return {
1210
+ sessionId: this.liveSessions.get(agentId) ?? null,
1211
+ launchId: this.launchIds.get(agentId) ?? null
1212
+ };
1213
+ }
1214
+ liveSessionReports() {
1215
+ return [...this.liveSessions.entries()].map(([agentId, sessionId]) => ({
1216
+ agentId,
1217
+ sessionId,
1218
+ launchId: this.launchIds.get(agentId) ?? ""
1219
+ }));
1220
+ }
1221
+ dispatch(event) {
1222
+ const before = this.deriveActivitySnapshot(this.state);
1223
+ const { state, effects } = reduceManager(this.state, event);
1224
+ this.state = state;
1225
+ for (const effect of effects)
1226
+ this.applyEffect(effect);
1227
+ if (this.opts.onAgentActivity) {
1228
+ const after = this.deriveActivitySnapshot(this.state);
1229
+ for (const [agentId, activity] of Object.entries(after)) {
1230
+ if (agentId in before && before[agentId] !== activity) {
1231
+ this.opts.onAgentActivity({ agentId, state: activity });
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ deriveActivitySnapshot(state) {
1237
+ const snapshot = {};
1238
+ for (const [agentId, agent] of Object.entries(state.agents))
1239
+ snapshot[agentId] = this.deriveActivity(agent);
1240
+ return snapshot;
1241
+ }
1242
+ deriveActivity(agent) {
1243
+ if (agent.status === "running" && !agent.turnActive)
1244
+ return "idle";
1245
+ return agent.status;
1246
+ }
1247
+ withFooter(text) {
1248
+ return this.opts.wakePromptFooter ? `${text}
1249
+
1250
+ ${this.opts.wakePromptFooter}` : text;
1251
+ }
1252
+ applyEffect(effect) {
1253
+ switch (effect.type) {
1254
+ case "spawn":
1255
+ this.doSpawn(effect.agentId, this.withFooter(effect.prompt), effect.resumeSessionId);
1256
+ break;
1257
+ case "send": {
1258
+ const session = this.sessions.get(effect.agentId);
1259
+ session?.send({ text: this.withFooter(effect.text), mode: effect.mode });
1260
+ this.log.info("steering message sent to running agent", { agentId: effect.agentId, mode: effect.mode });
1261
+ break;
1262
+ }
1263
+ case "stop":
1264
+ case "terminate_stalled": {
1265
+ const session = this.sessions.get(effect.agentId);
1266
+ Promise.resolve(session?.stop({ reason: effect.type, forceAfterMs: 5000 }));
1267
+ const spawnState = this.activeSpawnState.get(effect.agentId);
1268
+ if (spawnState)
1269
+ spawnState.suppressExitLog = true;
1270
+ this.logSessionEnded(effect.agentId, effect.type === "stop" ? "stopped" : "terminate_stalled");
1271
+ this.opts.onAgentLocallyStopped?.({ agentId: effect.agentId, reason: effect.type });
1272
+ break;
1273
+ }
1274
+ case "gated_hold":
1275
+ this.log.info("gated busy message held", {
1276
+ agentId: effect.agentId,
1277
+ reason: effect.reason,
1278
+ blockedReason: effect.blockedReason,
1279
+ recentEvents: effect.recentEvents
1280
+ });
1281
+ break;
1282
+ }
1283
+ }
1284
+ logSessionEnded(agentId, reason) {
1285
+ this.log.info("agent session ended", { agentId, sessionId: this.liveSessions.get(agentId) ?? "", reason });
1286
+ }
1287
+ doSpawn(agentId, prompt, resumeSessionId) {
1288
+ const driver = this.opts.driverFor(agentId, this.runtimeConfigs.get(agentId));
1289
+ this.log.info("spawning agent", { agentId, runtime: driver.id });
1290
+ const base = this.opts.baseContextFor(agentId);
1291
+ const runtimeConfig = this.runtimeConfigs.get(agentId) ?? base.config?.runtimeConfig;
1292
+ const provider = runtimeConfig?.runtime ?? null;
1293
+ const sessionId = resumeSessionId ?? this.resumeSessions.get(agentId) ?? this.opts.timeline?.resumeSessionId(agentId, provider) ?? base.config?.sessionId;
1294
+ const description = runtimeConfig?.instruction ?? base.config?.description ?? runtimeConfig?.agentName;
1295
+ const agentName = runtimeConfig?.agentName ?? base.config?.agentName;
1296
+ const agentHandle = runtimeConfig?.agentHandle ?? base.config?.agentHandle;
1297
+ const config = { ...base.config ?? {}, runtimeConfig, sessionId, description, agentName, agentHandle };
1298
+ const standingPrompt = base.standingPrompt || driver.buildSystemPrompt?.(config, agentId) || "";
1299
+ const ctx = {
1300
+ ...base,
1301
+ prompt,
1302
+ standingPrompt,
1303
+ credentialProxy: base.credentialProxy ?? this.opts.credentialProxy,
1304
+ launchId: this.launchIds.get(agentId) ?? base.launchId,
1305
+ config
1306
+ };
1307
+ if (!this.opts.sessionFactory && driver.createSession && !this.opts.sdkDriverDepsFor) {
1308
+ throw new Error(`AgentProcessManager: real spawn of "${agentId}" on in-process SDK runtime "${driver.id}" needs ` + "sdkDriverDepsFor — set ManagerRuntimeOpts.sdkDriverDepsFor, or pass a sessionFactory for tests.");
1309
+ }
1310
+ if (!this.opts.sessionFactory && !driver.createSession && !ctx.credentialProxy) {
1311
+ throw new Error(`AgentProcessManager: real spawn of "${agentId}" needs a credentialProxy — ` + "set ManagerRuntimeOpts.credentialProxy (or baseContextFor's), or pass a sessionFactory for tests.");
1312
+ }
1313
+ const session = this.opts.sessionFactory ? this.opts.sessionFactory({ agentId, driver, ctx }) : driver.createSession ? new SdkManagedSession(driver, ctx, this.opts.sdkDriverDepsFor(ctx)) : createChildProcessRuntimeSession(driver, ctx);
1314
+ this.sessions.set(agentId, session);
1315
+ const state = { hasEstablished: false, hasReportedSpawnFailure: false, suppressExitLog: false };
1316
+ this.activeSpawnState.set(agentId, state);
1317
+ const reportSpawnFailure = (reason) => {
1318
+ if (state.hasEstablished || state.hasReportedSpawnFailure)
1319
+ return;
1320
+ state.hasReportedSpawnFailure = true;
1321
+ this.log.warn("spawn failed", { agentId, runtime: driver.id, reason });
1322
+ this.opts.onRuntimeSpawnFailed?.(driver.id, reason);
1323
+ };
1324
+ session.on("runtime_event", (e) => {
1325
+ if (!state.hasEstablished) {
1326
+ state.hasEstablished = true;
1327
+ }
1328
+ this.opts.onRuntimeSessionEstablished?.(driver.id);
1329
+ if (e?.kind === "turn_end" && driver.lifecycle.kind === "per_turn") {
1330
+ state.suppressExitLog = true;
1331
+ }
1332
+ this.onRuntimeEvent(agentId, e, driver.id);
1333
+ });
1334
+ session.on("error", (...args) => {
1335
+ const err = args[0];
1336
+ const code = err?.code ?? "spawn_error";
1337
+ reportSpawnFailure(String(code));
1338
+ });
1339
+ session.on("exit", () => {
1340
+ reportSpawnFailure("pre_handshake_exit");
1341
+ if (state.hasEstablished && !state.suppressExitLog)
1342
+ this.logSessionEnded(agentId, "exit");
1343
+ this.flushThinkingAudit(agentId);
1344
+ this.sessions.delete(agentId);
1345
+ this.liveSessions.delete(agentId);
1346
+ if (this.activeSpawnState.get(agentId) === state)
1347
+ this.activeSpawnState.delete(agentId);
1348
+ this.dispatch({ type: "exit", agentId });
1349
+ });
1350
+ Promise.resolve(session.start({ text: prompt, sessionId: ctx.config.sessionId })).then(() => {
1351
+ if (this.sessions.get(agentId) !== session)
1352
+ return;
1353
+ this.dispatch({ type: "spawned", agentId, nowMs: this.now() });
1354
+ }).catch((err) => {
1355
+ const code = err?.code ?? "spawn_threw";
1356
+ reportSpawnFailure(String(code));
1357
+ if (this.sessions.get(agentId) === session)
1358
+ this.sessions.delete(agentId);
1359
+ this.dispatch({ type: "exit", agentId });
1360
+ });
1361
+ }
1362
+ flushThinkingAudit(agentId) {
1363
+ const buffered = this.thinkingBuffers.get(agentId);
1364
+ if (!buffered)
1365
+ return;
1366
+ this.thinkingBuffers.delete(agentId);
1367
+ if (!this.opts.onBotAuditEvent)
1368
+ return;
1369
+ const { text, truncated, chars } = truncateThinking(buffered);
1370
+ try {
1371
+ this.opts.onBotAuditEvent(agentId, {
1372
+ kind: "thinking",
1373
+ payload: { text, truncated, chars }
1374
+ }, {
1375
+ sessionId: this.liveSessions.get(agentId) ?? null,
1376
+ launchId: this.launchIds.get(agentId) ?? null
1377
+ });
1378
+ } catch {}
1379
+ }
1380
+ onRuntimeEvent(agentId, e, runtimeId) {
1381
+ const ev = e;
1382
+ if (!ev?.kind)
1383
+ return;
1384
+ if (this.opts.onBotAuditEvent) {
1385
+ if (ev.kind === "thinking" && typeof ev.text === "string") {
1386
+ if (ev.text.length > 0) {
1387
+ this.thinkingBuffers.set(agentId, (this.thinkingBuffers.get(agentId) ?? "") + ev.text);
1388
+ }
1389
+ } else {
1390
+ this.flushThinkingAudit(agentId);
1391
+ if (ev.kind === "tool_call" && typeof ev.name === "string") {
1392
+ if (ev.name !== "Bash") {
1393
+ try {
1394
+ this.opts.onBotAuditEvent(agentId, {
1395
+ kind: "tool_call",
1396
+ payload: { name: ev.name }
1397
+ }, {
1398
+ sessionId: this.liveSessions.get(agentId) ?? null,
1399
+ launchId: this.launchIds.get(agentId) ?? null
1400
+ });
1401
+ } catch {}
1402
+ }
1403
+ }
1404
+ }
1405
+ }
1406
+ if (ev.kind === "session_init" && ev.sessionId) {
1407
+ this.dispatch({ type: "session", agentId, sessionId: ev.sessionId });
1408
+ this.liveSessions.set(agentId, ev.sessionId);
1409
+ this.opts.timeline?.setSession(agentId, ev.sessionId);
1410
+ this.opts.onAgentSession?.({
1411
+ agentId,
1412
+ sessionId: ev.sessionId,
1413
+ launchId: this.launchIds.get(agentId) ?? ""
1414
+ });
1415
+ this.log.info("agent session established", { agentId, sessionId: ev.sessionId, runtime: runtimeId });
1416
+ }
1417
+ if (ev.kind === "text" && typeof ev.text === "string" && ev.text.length > 0) {
1418
+ this.opts.timeline?.appendResponseToLatest(agentId, ev.text);
1419
+ }
1420
+ this.dispatch({ type: "progress", agentId, nowMs: this.now() });
1421
+ this.dispatch({ type: "runtime_signal", agentId, kind: ev.kind, nowMs: this.now() });
1422
+ if (ev.kind === "turn_end") {
1423
+ this.logSessionEnded(agentId, "turn_end");
1424
+ this.dispatch({ type: "turn_end", agentId, nowMs: this.now() });
1425
+ }
1426
+ }
1427
+ }
1428
+ // src/manager/agentRouter.ts
1429
+ class UnknownBotError extends Error {
1430
+ botId;
1431
+ constructor(botId) {
1432
+ super(`Bot not in this daemon's cache: ${botId}`);
1433
+ this.botId = botId;
1434
+ this.name = "UnknownBotError";
1435
+ }
1436
+ }
1437
+
1438
+ class BotEnrollFailedError extends Error {
1439
+ botId;
1440
+ constructor(botId, cause) {
1441
+ super(`Failed to enroll bot ${botId}: ${cause instanceof Error ? cause.message : String(cause)}`);
1442
+ this.botId = botId;
1443
+ this.name = "BotEnrollFailedError";
1444
+ }
1445
+ }
1446
+ function classifyErrorCode(err) {
1447
+ if (err instanceof UnknownBotError)
1448
+ return "bot_unknown";
1449
+ if (err instanceof BotEnrollFailedError)
1450
+ return "bot_enroll_failed";
1451
+ if (err instanceof UnknownRuntimeError)
1452
+ return "bot_runtime_missing";
1453
+ return "internal_error";
1454
+ }
1455
+
1456
+ class UnknownRuntimeError extends Error {
1457
+ requested;
1458
+ available;
1459
+ constructor(requested, available) {
1460
+ super(`Runtime not available on this host: ${requested ?? "<unspecified>"} — installed: ${available.join(", ") || "(none)"}`);
1461
+ this.requested = requested;
1462
+ this.available = available;
1463
+ this.name = "UnknownRuntimeError";
1464
+ }
1465
+ }
1466
+ function defaultFormatUnreadNoticeText(notice) {
1467
+ return `You have unread messages in channel ${notice.channel}.`;
1468
+ }
1469
+
1470
+ class AgentRouter {
1471
+ opts;
1472
+ running = new Set;
1473
+ runtimes = new Map;
1474
+ pendingResend = false;
1475
+ scheduleResend;
1476
+ log;
1477
+ constructor(opts) {
1478
+ this.opts = opts;
1479
+ this.log = opts.logger ?? createLogger({ header: "@alook/daemon:router" });
1480
+ this.scheduleResend = opts.scheduleReadyResend ?? queueMicrotask.bind(globalThis);
1481
+ for (const r of opts.runtimeReport) {
1482
+ this.runtimes.set(r.id, {
1483
+ id: r.id,
1484
+ version: r.version,
1485
+ status: r.status ?? "healthy",
1486
+ lastError: r.lastError,
1487
+ lastErrorAt: r.lastErrorAt
1488
+ });
1489
+ }
1490
+ }
1491
+ async start() {
1492
+ this.opts.channel.onCommand((cmd) => this.onCommand(cmd));
1493
+ this.opts.channel.onResync?.(() => ({
1494
+ ready: this.buildReady(),
1495
+ sessions: this.opts.manager.liveSessionReports()
1496
+ }));
1497
+ await this.opts.channel.reportReady(this.buildReady());
1498
+ }
1499
+ buildReady() {
1500
+ return {
1501
+ runtimeReport: [...this.runtimes.values()],
1502
+ runningAgents: [...this.running],
1503
+ hostname: this.opts.hostname,
1504
+ platform: this.opts.platform,
1505
+ arch: this.opts.arch,
1506
+ osRelease: this.opts.osRelease,
1507
+ daemonVersion: this.opts.daemonVersion
1508
+ };
1509
+ }
1510
+ healthyRuntimeIds() {
1511
+ const out = [];
1512
+ for (const r of this.runtimes.values()) {
1513
+ if (r.status === "healthy")
1514
+ out.push(r.id);
1515
+ }
1516
+ return out;
1517
+ }
1518
+ isRuntimeHealthy(id) {
1519
+ return this.runtimes.get(id)?.status === "healthy";
1520
+ }
1521
+ markRuntimeUnhealthy(id, reason) {
1522
+ const existing = this.runtimes.get(id);
1523
+ if (!existing)
1524
+ return;
1525
+ const nowIso = new Date().toISOString();
1526
+ if (existing.status === "unhealthy" && existing.lastError === reason)
1527
+ return;
1528
+ this.runtimes.set(id, {
1529
+ ...existing,
1530
+ status: "unhealthy",
1531
+ lastError: reason,
1532
+ lastErrorAt: nowIso
1533
+ });
1534
+ this.log.warn("runtime marked unhealthy", { runtimeId: id, reason });
1535
+ this.scheduleReadyFrameResend();
1536
+ }
1537
+ markRuntimeHealthy(id) {
1538
+ const existing = this.runtimes.get(id);
1539
+ if (!existing)
1540
+ return;
1541
+ if (existing.status === "healthy" && !existing.lastError && !existing.lastErrorAt)
1542
+ return;
1543
+ this.runtimes.set(id, {
1544
+ id: existing.id,
1545
+ version: existing.version,
1546
+ status: "healthy"
1547
+ });
1548
+ this.log.info("runtime marked healthy again", { runtimeId: id });
1549
+ this.scheduleReadyFrameResend();
1550
+ }
1551
+ markLocallyStopped(agentId) {
1552
+ if (!this.running.delete(agentId))
1553
+ return;
1554
+ this.log.info("agent removed from running set (local stop)", { agentId });
1555
+ this.scheduleReadyFrameResend();
1556
+ }
1557
+ scheduleReadyFrameResend() {
1558
+ if (this.pendingResend)
1559
+ return;
1560
+ this.pendingResend = true;
1561
+ this.scheduleResend(() => {
1562
+ this.pendingResend = false;
1563
+ try {
1564
+ this.opts.channel.sendReady?.(this.buildReady());
1565
+ } catch {}
1566
+ });
1567
+ }
1568
+ async onCommand(cmd) {
1569
+ switch (cmd.type) {
1570
+ case "agent:wake":
1571
+ this.log.info("agent:wake received", {
1572
+ agentId: cmd.agentId,
1573
+ channel: cmd.unreadNotice.channel,
1574
+ latestSeq: cmd.unreadNotice.latestSeq
1575
+ });
1576
+ try {
1577
+ await this.opts.onBeforeAgent?.(cmd.agentId);
1578
+ this.opts.manager.register(cmd.agentId, {
1579
+ runtimeConfig: cmd.config,
1580
+ sessionId: cmd.sessionId,
1581
+ launchId: cmd.launchId
1582
+ });
1583
+ this.running.add(cmd.agentId);
1584
+ const text = (this.opts.formatUnreadNoticeText ?? defaultFormatUnreadNoticeText)(cmd.unreadNotice);
1585
+ this.opts.manager.deliver(cmd.agentId, { seq: cmd.unreadNotice.latestSeq, text });
1586
+ await this.opts.channel.reportWakeAck?.({
1587
+ agentId: cmd.agentId,
1588
+ launchId: cmd.launchId,
1589
+ status: "ok"
1590
+ });
1591
+ this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "ok" });
1592
+ } catch (err) {
1593
+ if (err instanceof UnknownRuntimeError) {
1594
+ const frame = {
1595
+ type: "session.error",
1596
+ code: "runtime_not_available",
1597
+ agentId: cmd.agentId,
1598
+ payload: {
1599
+ requested: err.requested ?? null,
1600
+ available: err.available
1601
+ }
1602
+ };
1603
+ await this.opts.channel.reportSessionError?.(frame);
1604
+ await this.opts.channel.reportWakeAck?.({
1605
+ agentId: cmd.agentId,
1606
+ launchId: cmd.launchId,
1607
+ status: "error",
1608
+ error: {
1609
+ code: "bot_runtime_missing",
1610
+ message: err.message
1611
+ }
1612
+ });
1613
+ this.log.info("agent:wake ack", {
1614
+ agentId: cmd.agentId,
1615
+ status: "error",
1616
+ "error.code": "bot_runtime_missing"
1617
+ });
1618
+ return;
1619
+ }
1620
+ {
1621
+ const code = classifyErrorCode(err);
1622
+ await this.opts.channel.reportWakeAck?.({
1623
+ agentId: cmd.agentId,
1624
+ launchId: cmd.launchId,
1625
+ status: "error",
1626
+ error: {
1627
+ code,
1628
+ message: err instanceof Error ? err.message : String(err)
1629
+ }
1630
+ });
1631
+ this.log.info("agent:wake ack", { agentId: cmd.agentId, status: "error", "error.code": code });
1632
+ }
1633
+ return;
1634
+ }
1635
+ break;
1636
+ case "agent:stop":
1637
+ this.log.info("agent:stop received", { agentId: cmd.agentId });
1638
+ try {
1639
+ this.running.delete(cmd.agentId);
1640
+ this.opts.manager.stop(cmd.agentId);
1641
+ await this.opts.channel.reportStoppedAck?.({
1642
+ agentId: cmd.agentId,
1643
+ status: "ok"
1644
+ });
1645
+ this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "ok" });
1646
+ } catch (err) {
1647
+ const code = classifyErrorCode(err);
1648
+ await this.opts.channel.reportStoppedAck?.({
1649
+ agentId: cmd.agentId,
1650
+ status: "error",
1651
+ error: {
1652
+ code,
1653
+ message: err instanceof Error ? err.message : String(err)
1654
+ }
1655
+ });
1656
+ this.log.info("agent:stop ack", { agentId: cmd.agentId, status: "error", "error.code": code });
1657
+ }
1658
+ break;
1659
+ case "bot:added":
1660
+ case "bot:updated":
1661
+ case "bot:removed":
1662
+ break;
1663
+ }
1664
+ }
1665
+ }
1666
+ // src/timeline/timeline.ts
1667
+ import { appendFileSync, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync, existsSync } from "fs";
1668
+ import { join as join2 } from "path";
1669
+
1670
+ // src/timeline/filelock.ts
1671
+ import * as fs3 from "fs";
1672
+ var DEFAULT_STALE_MS = 30000;
1673
+ var META = "meta.json";
1674
+ function acquireLock(lockPath, staleMs = DEFAULT_STALE_MS) {
1675
+ if (tryMkdir(lockPath)) {
1676
+ writeMeta(lockPath);
1677
+ return true;
1678
+ }
1679
+ if (isStale(lockPath, staleMs)) {
1680
+ reclaim(lockPath);
1681
+ if (tryMkdir(lockPath)) {
1682
+ writeMeta(lockPath);
1683
+ return true;
1684
+ }
1685
+ }
1686
+ return false;
1687
+ }
1688
+ function releaseLock(lockPath) {
1689
+ try {
1690
+ fs3.rmSync(lockPath, { recursive: true, force: true });
1691
+ } catch {}
1692
+ }
1693
+ function lockPathFor(dir, filename) {
1694
+ return `${dir}/.${filename}.lock`;
1695
+ }
1696
+ function tryMkdir(lockPath) {
1697
+ try {
1698
+ fs3.mkdirSync(lockPath);
1699
+ return true;
1700
+ } catch (err) {
1701
+ if (err.code === "EEXIST")
1702
+ return false;
1703
+ throw err;
1704
+ }
1705
+ }
1706
+ function writeMeta(lockPath) {
1707
+ try {
1708
+ fs3.writeFileSync(`${lockPath}/${META}`, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() }));
1709
+ } catch {}
1710
+ }
1711
+ function isStale(lockPath, staleMs) {
1712
+ try {
1713
+ const raw = fs3.readFileSync(`${lockPath}/${META}`, "utf8");
1714
+ const acquiredAt = JSON.parse(raw).acquiredAt;
1715
+ if (typeof acquiredAt === "number")
1716
+ return Date.now() - acquiredAt > staleMs;
1717
+ } catch {}
1718
+ try {
1719
+ return Date.now() - fs3.statSync(lockPath).mtimeMs > staleMs;
1720
+ } catch {
1721
+ return false;
1722
+ }
1723
+ }
1724
+ function reclaim(lockPath) {
1725
+ try {
1726
+ fs3.rmSync(lockPath, { recursive: true, force: true });
1727
+ } catch {}
1728
+ }
1729
+
1730
+ // src/timeline/timeline.ts
1731
+ function filenameForDate(date) {
1732
+ const y = date.getFullYear();
1733
+ const m = String(date.getMonth() + 1).padStart(2, "0");
1734
+ const d = String(date.getDate()).padStart(2, "0");
1735
+ return `${y}-${m}-${d}.jsonl`;
1736
+ }
1737
+ function recentFilenames(maxDays, now) {
1738
+ const out = [];
1739
+ for (let i = 0;i < maxDays; i++) {
1740
+ const d = new Date(now);
1741
+ d.setDate(d.getDate() - i);
1742
+ out.push(filenameForDate(d));
1743
+ }
1744
+ return out;
1745
+ }
1746
+ function readJsonl(filePath) {
1747
+ let content;
1748
+ try {
1749
+ content = readFileSync3(filePath, "utf-8");
1750
+ } catch {
1751
+ return [];
1752
+ }
1753
+ const entries = [];
1754
+ for (const line of content.trimEnd().split(`
1755
+ `)) {
1756
+ if (!line)
1757
+ continue;
1758
+ try {
1759
+ entries.push(JSON.parse(line));
1760
+ } catch {}
1761
+ }
1762
+ return entries;
1763
+ }
1764
+ function readRecentEntries(timelineDir, opts = {}) {
1765
+ const now = opts.now ?? new Date;
1766
+ const maxDays = opts.maxDays ?? 7;
1767
+ const filenames = recentFilenames(maxDays, now).reverse();
1768
+ const entries = [];
1769
+ for (const filename of filenames) {
1770
+ entries.push(...readJsonl(join2(timelineDir, filename)));
1771
+ }
1772
+ return entries;
1773
+ }
1774
+ function appendOrMergeEntry(timelineDir, entry, now = new Date) {
1775
+ const filename = filenameForDate(now);
1776
+ const filePath = join2(timelineDir, filename);
1777
+ const lockPath = lockPathFor(timelineDir, filename);
1778
+ if (!acquireLock(lockPath))
1779
+ return false;
1780
+ try {
1781
+ let lines = [];
1782
+ if (existsSync(filePath)) {
1783
+ lines = readFileSync3(filePath, "utf-8").trimEnd().split(`
1784
+ `).filter(Boolean);
1785
+ }
1786
+ if (lines.length > 0) {
1787
+ const latest = JSON.parse(lines[lines.length - 1]);
1788
+ const mergeable = latest.session_id === entry.session_id && latest.provider === entry.provider && latest.agent_responses.length === 0;
1789
+ if (mergeable) {
1790
+ latest.messages = [...latest.messages, ...entry.messages];
1791
+ lines[lines.length - 1] = JSON.stringify(latest);
1792
+ const tmpPath = join2(timelineDir, `.${filename}.tmp`);
1793
+ writeFileSync3(tmpPath, lines.join(`
1794
+ `) + `
1795
+ `);
1796
+ renameSync(tmpPath, filePath);
1797
+ return true;
1798
+ }
1799
+ }
1800
+ appendFileSync(filePath, JSON.stringify(entry) + `
1801
+ `);
1802
+ return true;
1803
+ } catch {
1804
+ return false;
1805
+ } finally {
1806
+ releaseLock(lockPath);
1807
+ }
1808
+ }
1809
+ function updateLatestEntry(timelineDir, updater, opts = {}) {
1810
+ const now = opts.now ?? new Date;
1811
+ const maxDays = opts.maxDays ?? 7;
1812
+ for (const filename of recentFilenames(maxDays, now)) {
1813
+ const filePath = join2(timelineDir, filename);
1814
+ if (!existsSync(filePath))
1815
+ continue;
1816
+ const lockPath = lockPathFor(timelineDir, filename);
1817
+ if (!acquireLock(lockPath))
1818
+ continue;
1819
+ try {
1820
+ let content;
1821
+ try {
1822
+ content = readFileSync3(filePath, "utf-8");
1823
+ } catch {
1824
+ continue;
1825
+ }
1826
+ const lines = content.trimEnd().split(`
1827
+ `).filter(Boolean);
1828
+ if (lines.length === 0)
1829
+ continue;
1830
+ const entries = lines.map((l) => JSON.parse(l));
1831
+ updater(entries[entries.length - 1]);
1832
+ const tmpPath = join2(timelineDir, `.${filename}.tmp`);
1833
+ writeFileSync3(tmpPath, entries.map((e) => JSON.stringify(e)).join(`
1834
+ `) + `
1835
+ `);
1836
+ renameSync(tmpPath, filePath);
1837
+ return true;
1838
+ } catch {} finally {
1839
+ releaseLock(lockPath);
1840
+ }
1841
+ }
1842
+ return false;
1843
+ }
1844
+ function createTimelineEntry(fields) {
1845
+ return {
1846
+ session_id: fields.sessionId ?? null,
1847
+ messages: fields.messages,
1848
+ agent_responses: [],
1849
+ provider: fields.provider ?? null
1850
+ };
1851
+ }
1852
+ function findResumableSession(rows, provider) {
1853
+ for (let i = rows.length - 1;i >= 0; i--) {
1854
+ const e = rows[i];
1855
+ if (!e.session_id)
1856
+ continue;
1857
+ if (provider && e.provider !== provider)
1858
+ continue;
1859
+ return e.session_id;
1860
+ }
1861
+ return null;
1862
+ }
1863
+ // src/timeline/recorder.ts
1864
+ import { mkdirSync as mkdirSync3 } from "fs";
1865
+ function createTimelineRecorder(opts) {
1866
+ const now = opts.now ?? (() => new Date);
1867
+ const dirFor = (agentId) => opts.timelineDirFor(agentId);
1868
+ const sessionByAgent = new Map;
1869
+ return {
1870
+ setSession(agentId, sessionId) {
1871
+ sessionByAgent.set(agentId, sessionId);
1872
+ },
1873
+ appendEntryForAgent(agentId, messages) {
1874
+ const dir = dirFor(agentId);
1875
+ try {
1876
+ mkdirSync3(dir, { recursive: true });
1877
+ } catch {}
1878
+ appendOrMergeEntry(dir, createTimelineEntry({
1879
+ messages,
1880
+ sessionId: sessionByAgent.get(agentId) ?? null,
1881
+ provider: opts.providerFor?.(agentId) ?? null
1882
+ }), now());
1883
+ },
1884
+ appendResponseToLatest(agentId, text) {
1885
+ updateLatestEntry(dirFor(agentId), (e) => e.agent_responses.push(text), { now: now() });
1886
+ },
1887
+ resumeSessionId(agentId, provider) {
1888
+ const rows = readRecentEntries(dirFor(agentId), { now: now() });
1889
+ return findResumableSession(rows, provider ?? undefined);
1890
+ }
1891
+ };
1892
+ }
1893
+ // src/discovery.ts
1894
+ import * as path8 from "path";
1895
+ import * as fs8 from "fs";
1896
+ import { fileURLToPath } from "url";
1897
+
1898
+ // src/drivers/cliTransport.ts
1899
+ import * as fs5 from "fs";
1900
+ import * as path3 from "path";
1901
+
1902
+ // src/drivers/systemPrompt.ts
1903
+ var CLI = "alook";
1904
+ function identitySection(config) {
1905
+ const parts = ["## Identity", ""];
1906
+ const introParts = ["You are a user operating in Alook."];
1907
+ if (config.agentName)
1908
+ introParts.push(`Your name is ${config.agentName}.`);
1909
+ parts.push(introParts.join(" "));
1910
+ if (config.agentHandle) {
1911
+ parts.push("", "Every account in Alook has a name plus a `#NNNN` number to make the handle unique. " + `Your handle is \`${config.agentHandle}\`. ` + "Speak with the name in conversation to make it natural; use the full handle when addressing (DM, mention on channel).");
1912
+ }
1913
+ if (config.ownerHandle) {
1914
+ parts.push("", `You are owned by \`${config.ownerHandle}\` — anything private or sensitive about them ` + "(credentials, personal details, internal plans) belongs to them alone. Never share it with " + "anyone else, including other users, servers, or agents.");
1915
+ }
1916
+ if (config.description) {
1917
+ parts.push("", "### Role", "", config.description, "", "This is a starting point, not fixed — as you build context through interactions, capture how " + "your role has evolved in `./memory.md` (the Role text above isn't something you can edit directly).");
1918
+ }
1919
+ return parts.join(`
1920
+ `);
1921
+ }
1922
+ function cliCommandsSection() {
1923
+ return [
1924
+ "## CLI commands",
1925
+ "",
1926
+ `\`${CLI}\` is your command-line interface. Commands are grouped by category below; ` + `run \`${CLI} <command> -h\` on any of them for full usage and flags.`,
1927
+ "",
1928
+ "### Messaging",
1929
+ "",
1930
+ `1. \`${CLI} inbox pull\` — fetch unread messages.`,
1931
+ `2. \`${CLI} message send\` — send a message to a channel, DM, or thread.`,
1932
+ "",
1933
+ "### Servers",
1934
+ "",
1935
+ `1. \`${CLI} server list\` — list servers you're a member of.`,
1936
+ `2. \`${CLI} server member --server <id-or-name>\` — list members of a server.`,
1937
+ `3. \`${CLI} server join --invite <link>\` — join a server via an invite link or token.`,
1938
+ "",
1939
+ "### Channels",
1940
+ "",
1941
+ `1. \`${CLI} channel list --server <id-or-name>\` — list top-level channels in a server.`,
1942
+ `2. \`${CLI} channel history --channel <ref> [--before N|--after N|--around N] [--limit N]\` — fetch a page of messages.`,
1943
+ "",
1944
+ "### Output format",
1945
+ "",
1946
+ `Every \`${CLI}\` command outputs a single JSON line (envelope):`,
1947
+ '- Success: `{"success": { ... }}`',
1948
+ '- Error: `{"error": "message", "hint": "optional recovery hint"}`'
1949
+ ].join(`
1950
+ `);
1951
+ }
1952
+ function messagingSection() {
1953
+ return [
1954
+ "## Messaging",
1955
+ "",
1956
+ "### Sending & receiving",
1957
+ "",
1958
+ "- Send a reply — two options depending on length:",
1959
+ ` - Short: \`${CLI} message send --target <ref> --text "brief reply"\``,
1960
+ ` - Long&Complicated: write body to a tmp file, then \`${CLI} message send --target <ref> --file ./temp_msg.md\``,
1961
+ "- Address your reply to where the message came from.",
1962
+ "",
1963
+ "### Channel refs & addressing",
1964
+ "",
1965
+ "Channels and messages are addressed with path-style refs:",
1966
+ "",
1967
+ "| Channel Ref | Meaning |",
1968
+ "|---|---|",
1969
+ "| `/<server>/<channel>` | A channel in a server |",
1970
+ "| `/<server>/<channel>/#N` | Thread rooted at message #N |",
1971
+ "| `/<server>` | A server, with no specific channel |",
1972
+ "| `/.dm/<peer>` | A DM with another user/agent (peer = handle, `name#0042`) |",
1973
+ "| `/.dm/<peer>#N` | Message #N in a DM |",
1974
+ "| `/.dm/<peer>/#N` | Thread in a DM |",
1975
+ "",
1976
+ "Use the `channel` field from received messages as the `--target` when replying.",
1977
+ "To reply in a thread, use the thread ref (`/<server>/<channel>/#N`).",
1978
+ "These same refs also work inline, inside a message's `--text`/`--file` body — not just as `--target`. " + "Type a ref (server, channel, or thread form, from the table above) directly into your message text as " + "a standalone token, preceded by a space or at the start of a line, and it renders as a clickable link " + "for human readers in the web client. **Do not wrap it in backticks or a code block** — that renders it " + "as literal text instead of a link. Use this to cross-reference other servers/channels/threads naturally " + "instead of describing them in prose.",
1979
+ "",
1980
+ "### Message shape",
1981
+ "",
1982
+ `When you call \`${CLI} inbox pull\`, you receive messages as JSON objects:`,
1983
+ "",
1984
+ "```json",
1985
+ '{"seq": "#3", "channel": "/demo/general", "sender": "@gustavo#4821", "content": {"text": "hello"}, "time": "2026-06-01T12:00:00Z"}',
1986
+ "```",
1987
+ "",
1988
+ "Fields:",
1989
+ "- `seq` — per-channel sequence number (`#N`). Identifies a message within its channel.",
1990
+ "- `channel` — the path ref of the channel/DM. Reuse as `--target` when replying.",
1991
+ "- `sender` — handle (`@name#0042`) of who sent it.",
1992
+ "- `content.text` — the message body.",
1993
+ "- `time` — ISO-8601 timestamp."
1994
+ ].join(`
1995
+ `);
1996
+ }
1997
+ function serversSection() {
1998
+ return [
1999
+ "## Servers",
2000
+ "",
2001
+ `If a message contains a \`/community/invite/...\` link, just run \`${CLI} server join --invite <link>\`. ` + "The server enforces an owner-only check for you — it only accepts an invite your owner created, and " + "rejects anything else with a clear reason. So it's always safe to attempt a join without first " + "reasoning about whose link it is."
2002
+ ].join(`
2003
+ `);
2004
+ }
2005
+ function channelsSection() {
2006
+ return [
2007
+ "## Channels",
2008
+ "",
2009
+ `\`${CLI} channel list\`'s items are \`{ref, name, type}\` — \`ref\` is directly reusable as ` + "`--channel`/`--target` on every other command, no separate id lookup needed. `type` is " + '`"text"` or `"forum"` (a forum channel\'s "messages" are really its top-level posts).'
2010
+ ].join(`
2011
+ `);
2012
+ }
2013
+ function criticalRulesSection() {
2014
+ return [
2015
+ "## Critical rules",
2016
+ "",
2017
+ "- Do not expose tokens, keys, or secrets in any message or channel; redact " + "credential-like strings from tool output before sharing.",
2018
+ "- You never handle credentials directly — every `alook` command is already " + "authenticated for you. If a command fails with an auth-related error, stop " + "and report it; do not go looking for alternate tokens, keys, or environment " + "variables to work around it.",
2019
+ "- **Channel alignment**: you cannot send to a channel with unread messages. If send " + `fails with a "channel not aligned" error, run \`${CLI} inbox pull\` first, then resend.`,
2020
+ "- Finish the work a message asks for before you stop; don't leave a request half-handled."
2021
+ ].join(`
2022
+ `);
2023
+ }
2024
+ function startupSequenceSection() {
2025
+ return [
2026
+ "## On wake",
2027
+ "",
2028
+ "Each time you're woken up:",
2029
+ "1. Acknowledge any message already in front of you.",
2030
+ "2. Read `./memory.md` + latest context timeline to restore state.",
2031
+ `3. If notified of unread messages, run \`${CLI} inbox pull\` to fetch them.`,
2032
+ "4. Do the work, reply, finish completely before stopping."
2033
+ ].join(`
2034
+ `);
2035
+ }
2036
+ function communicationStyleSection() {
2037
+ return [
2038
+ "## Communication style",
2039
+ "",
2040
+ "Your reasoning is invisible to others — keep them in the loop:",
2041
+ "- Acknowledge tasks before starting; give a one-line plan.",
2042
+ "- Post brief updates at milestones (one sentence each).",
2043
+ "- Summarize outcomes when done.",
2044
+ "",
2045
+ "### Etiquette",
2046
+ "",
2047
+ "- Don't jump into a conversation unless @mentioned or directly addressed.",
2048
+ "- Let the person who did the work report on it.",
2049
+ "- Before going idle, unblock anyone waiting on you.",
2050
+ "- Don't narrate inactivity — only speak when you have something actionable.",
2051
+ "- Talk in the same language as the sender."
2052
+ ].join(`
2053
+ `);
2054
+ }
2055
+ function channelAwarenessSection() {
2056
+ return [
2057
+ "## Channel awareness",
2058
+ "",
2059
+ "- Reply where the message came from — same channel or thread.",
2060
+ "- Post results in the channel that owns the topic.",
2061
+ "- When uncertain, check the channel's history or just DM the relevant friends."
2062
+ ].join(`
2063
+ `);
2064
+ }
2065
+ function workspaceMemorySection() {
2066
+ return [
2067
+ "## Workspace & memory",
2068
+ "",
2069
+ "Your cwd is a persistent workspace that survives across sessions.",
2070
+ "",
2071
+ "### memory.md",
2072
+ "",
2073
+ "Read `./memory.md` first on every wake. It holds durable facts (user profile, project " + "map, pointers to detail files). Keep each entry short (one sentence, <140 chars).",
2074
+ "",
2075
+ "### experiences/",
2076
+ "",
2077
+ "For longer rules, workflows, or conditional procedures, write to `experiences/[NAME].md` " + 'and add a one-line index pointer in `./memory.md` (e.g. "read experiences/deploy.md ' + 'when deploying"). Use this for anything too specific or long for memory.md itself.',
2078
+ "",
2079
+ "Do NOT put ephemeral state (current task, in-progress status) in memory.md — the " + "context timeline handles that.",
2080
+ "",
2081
+ "### Context timeline",
2082
+ "",
2083
+ "`./.context_timeline/YYYY-MM-DD.jsonl` — ordered log of everything you did, by day. " + "This is your authoritative history. After compaction, read here to resume."
2084
+ ].join(`
2085
+ `);
2086
+ }
2087
+ function messageNotificationSection(lifecycleKind) {
2088
+ if (lifecycleKind === "per_turn") {
2089
+ return [
2090
+ "## Message notifications",
2091
+ "",
2092
+ "You run once per wake, then your process exits — there is nothing to poll for mid-turn. " + "Finish the current wake's work, then stop. The host spawns a brand-new process for the " + "next message; it re-checks the inbox at the start of that new wake."
2093
+ ].join(`
2094
+ `);
2095
+ }
2096
+ return [
2097
+ "## Message notifications",
2098
+ "",
2099
+ "Your process stays alive across turns. Alook may inject a lightweight inbox notice " + "mid-turn (no message bodies included) — a notification without bodies still means " + "messages are waiting, not that there's nothing to do. " + "Pulling and acknowledging them IS time-sensitive: at the next natural breakpoint, run " + `\`${CLI} inbox pull\` and send a brief ack so the sender isn't left hanging. Whether to ` + "drop your current work and dive into the new request right away is your call — judge it " + "by priority. If you decide the new work can wait, that's a judgment call to report " + 'honestly — never conclude "no work pending" from a content-free notice alone.'
2100
+ ].join(`
2101
+ `);
2102
+ }
2103
+ function buildCliSystemPrompt(config, opts) {
2104
+ const sections = [
2105
+ identitySection(config),
2106
+ cliCommandsSection(),
2107
+ messagingSection(),
2108
+ serversSection(),
2109
+ channelsSection(),
2110
+ criticalRulesSection(),
2111
+ startupSequenceSection(),
2112
+ communicationStyleSection(),
2113
+ channelAwarenessSection(),
2114
+ workspaceMemorySection(),
2115
+ messageNotificationSection(opts.lifecycleKind)
2116
+ ];
2117
+ return sections.filter((s) => s && s.length > 0).join(`
2118
+
2119
+ `);
2120
+ }
2121
+
2122
+ // src/runtimeConfig.ts
2123
+ var PI_BUILTIN_PROVIDER_ENV_KEYS = {
2124
+ google: "GEMINI_API_KEY",
2125
+ openai: "OPENAI_API_KEY",
2126
+ openrouter: "OPENROUTER_API_KEY"
2127
+ };
2128
+ var CONTROLLED_ENV_KEYS = new Set([
2129
+ "ANTHROPIC_BASE_URL",
2130
+ "ANTHROPIC_API_KEY",
2131
+ "ANTHROPIC_CUSTOM_MODEL_OPTION",
2132
+ ...Object.values(PI_BUILTIN_PROVIDER_ENV_KEYS)
2133
+ ]);
2134
+ function resolveLaunchFieldsOrDefault(config) {
2135
+ if (!config)
2136
+ return { fastMode: false, envVars: {}, providerEnv: {} };
2137
+ return resolveLaunchFields(config);
2138
+ }
2139
+ function resolveLaunchFields(config) {
2140
+ const envVars = {};
2141
+ const providerEnv = {};
2142
+ for (const [k, v] of Object.entries(config.envVars ?? {})) {
2143
+ if (!CONTROLLED_ENV_KEYS.has(k))
2144
+ envVars[k] = v;
2145
+ }
2146
+ let model;
2147
+ if (config.model.kind === "named")
2148
+ model = config.model.name;
2149
+ else if (config.model.kind === "custom") {
2150
+ model = config.model.name;
2151
+ if (config.runtime === "claude")
2152
+ providerEnv.ANTHROPIC_CUSTOM_MODEL_OPTION = config.model.name;
2153
+ }
2154
+ const p = config.provider;
2155
+ if (p?.kind === "custom" && config.runtime === "claude") {
2156
+ providerEnv.ANTHROPIC_BASE_URL = p.apiUrl;
2157
+ providerEnv.ANTHROPIC_API_KEY = p.apiKey;
2158
+ } else if (p?.kind === "pi-builtin") {
2159
+ const key = PI_BUILTIN_PROVIDER_ENV_KEYS[p.providerId];
2160
+ if (key)
2161
+ providerEnv[key] = p.apiKey;
2162
+ }
2163
+ return {
2164
+ model,
2165
+ reasoningEffort: config.reasoningEffort,
2166
+ fastMode: config.mode.kind === "fast",
2167
+ command: config.command,
2168
+ disallowedTools: config.disallowedTools,
2169
+ envVars,
2170
+ providerEnv
2171
+ };
2172
+ }
2173
+
2174
+ // src/drivers/cliLink.ts
2175
+ import * as fs4 from "fs";
2176
+ import * as path2 from "path";
2177
+ function writeCliLink(stateDir, cliName, hostCliPath, platform = process.platform) {
2178
+ const binDir = path2.join(stateDir, "bin");
2179
+ fs4.mkdirSync(binDir, { recursive: true });
2180
+ if (!hostCliPath)
2181
+ return binDir;
2182
+ if (platform === "win32") {
2183
+ const cmdFile = path2.join(binDir, `${cliName}.cmd`);
2184
+ const body = `@echo off\r
2185
+ "${hostCliPath}" %*\r
2186
+ `;
2187
+ fs4.writeFileSync(cmdFile, body);
2188
+ return binDir;
2189
+ }
2190
+ const linkPath = path2.join(binDir, cliName);
2191
+ try {
2192
+ fs4.unlinkSync(linkPath);
2193
+ } catch (err) {
2194
+ if (err.code !== "ENOENT")
2195
+ throw err;
2196
+ }
2197
+ try {
2198
+ fs4.symlinkSync(hostCliPath, linkPath);
2199
+ } catch (err) {
2200
+ if (err.code !== "EEXIST")
2201
+ throw err;
2202
+ }
2203
+ return binDir;
2204
+ }
2205
+
2206
+ // src/drivers/spawnEnv.ts
2207
+ function mergeEnvLayers(base, layers) {
2208
+ const env = { ...base };
2209
+ const provenance = {};
2210
+ const ordered = [
2211
+ ...layers.filter((l) => !l.sensitive).sort((a, b) => a.precedence - b.precedence),
2212
+ ...layers.filter((l) => l.sensitive).sort((a, b) => a.precedence - b.precedence)
2213
+ ];
2214
+ for (const layer of ordered) {
2215
+ for (const [k, v] of Object.entries(layer.vars)) {
2216
+ if (v === undefined)
2217
+ continue;
2218
+ env[k] = v;
2219
+ provenance[k] = layer.name;
2220
+ }
2221
+ }
2222
+ return { env, provenance };
2223
+ }
2224
+ function platformEnv(prefix, f) {
2225
+ const E = prefix;
2226
+ return {
2227
+ [`${E}_HOME`]: f.stateHome,
2228
+ [`${E}_ID`]: f.agentId,
2229
+ [`${E}_CLI`]: f.cliName,
2230
+ [`${E}_SERVER_URL`]: f.serverUrl,
2231
+ [`${E}_ACTIVE_CAPABILITIES`]: f.capabilities.join(","),
2232
+ [`${E}_LAUNCH_ID`]: f.launchId,
2233
+ [`${E}_CLI_TRANSPORT_TRACE_DIR`]: f.traceDir
2234
+ };
2235
+ }
2236
+ function runtimeContextEnv(prefix, rc) {
2237
+ if (!rc)
2238
+ return {};
2239
+ const E = prefix;
2240
+ return {
2241
+ [`${E}_CURRENT_AGENT_ID`]: rc.agentId,
2242
+ [`${E}_CURRENT_SERVER_ID`]: rc.serverId,
2243
+ [`${E}_CURRENT_COMPUTER_ID`]: rc.computerId,
2244
+ [`${E}_CURRENT_COMPUTER_NAME`]: rc.computerName,
2245
+ [`${E}_CURRENT_COMPUTER_HOSTNAME`]: rc.hostname,
2246
+ [`${E}_CURRENT_COMPUTER_OS`]: rc.os,
2247
+ [`${E}_CURRENT_DAEMON_VERSION`]: rc.daemonVersion,
2248
+ [`${E}_CURRENT_WORKSPACE_PATH`]: rc.workspacePath
2249
+ };
2250
+ }
2251
+
2252
+ // src/drivers/agentFile.ts
2253
+ import {
2254
+ writeFileSync as writeFileSync5,
2255
+ readFileSync as readFileSync4,
2256
+ lstatSync,
2257
+ symlinkSync as symlinkSync2,
2258
+ unlinkSync as unlinkSync2,
2259
+ existsSync as existsSync2,
2260
+ readlinkSync,
2261
+ copyFileSync
2262
+ } from "fs";
2263
+ import { join as join4 } from "path";
2264
+ import { createHash } from "crypto";
2265
+ var CANONICAL_FILE = "AGENTS.md";
2266
+ var SYMLINK_ALIASES = ["CLAUDE.md"];
2267
+ function contentHash(content) {
2268
+ return createHash("sha256").update(content, "utf-8").digest("hex");
2269
+ }
2270
+ function hasContentChanged(filePath, newContent) {
2271
+ try {
2272
+ const existing = readFileSync4(filePath, "utf-8");
2273
+ return contentHash(existing) !== contentHash(newContent);
2274
+ } catch (err) {
2275
+ if (err?.code === "ENOENT")
2276
+ return true;
2277
+ throw err;
2278
+ }
2279
+ }
2280
+ function ensureSymlinks(workDir) {
2281
+ const canonicalPath = join4(workDir, CANONICAL_FILE);
2282
+ if (!existsSync2(canonicalPath))
2283
+ return;
2284
+ for (const alias of SYMLINK_ALIASES) {
2285
+ if (alias === CANONICAL_FILE)
2286
+ continue;
2287
+ const aliasPath = join4(workDir, alias);
2288
+ try {
2289
+ const stat = lstatSync(aliasPath);
2290
+ if (stat.isSymbolicLink()) {
2291
+ const target = readlinkSync(aliasPath);
2292
+ if (target === CANONICAL_FILE)
2293
+ continue;
2294
+ unlinkSync2(aliasPath);
2295
+ } else {
2296
+ const aliasContent = readFileSync4(aliasPath, "utf-8");
2297
+ const canonicalContent = readFileSync4(canonicalPath, "utf-8");
2298
+ if (aliasContent === canonicalContent)
2299
+ continue;
2300
+ unlinkSync2(aliasPath);
2301
+ }
2302
+ } catch (err) {
2303
+ if (err?.code !== "ENOENT")
2304
+ throw err;
2305
+ }
2306
+ try {
2307
+ symlinkSync2(CANONICAL_FILE, aliasPath);
2308
+ } catch (err) {
2309
+ const code = err?.code;
2310
+ if (code === "EEXIST") {} else if (code === "EPERM" || code === "EACCES") {
2311
+ copyFileSync(canonicalPath, aliasPath);
2312
+ } else {
2313
+ throw err;
2314
+ }
2315
+ }
2316
+ }
2317
+ }
2318
+ function writeAgentFile(workDir, systemPromptContent) {
2319
+ const filePath = join4(workDir, CANONICAL_FILE);
2320
+ const changed = hasContentChanged(filePath, systemPromptContent);
2321
+ if (changed) {
2322
+ writeFileSync5(filePath, systemPromptContent, "utf-8");
2323
+ }
2324
+ ensureSymlinks(workDir);
2325
+ return changed;
2326
+ }
2327
+
2328
+ // src/drivers/cliTransport.ts
2329
+ var DEFAULT_ACTIVE_CAPABILITIES = [
2330
+ "send",
2331
+ "read",
2332
+ "mentions",
2333
+ "tasks",
2334
+ "reactions",
2335
+ "server",
2336
+ "channels",
2337
+ "knowledge"
2338
+ ];
2339
+ var DEFAULT_CLI_CONFIG = {
2340
+ cliName: "alook",
2341
+ envPrefix: "ALOOK",
2342
+ stateDirName: ".alook"
2343
+ };
2344
+ function resolveStateHome(envPrefix) {
2345
+ return process.env[`${envPrefix}_HOME`] || path3.join(process.env.HOME || process.env.USERPROFILE || ".", `.${envPrefix.toLowerCase()}`);
2346
+ }
2347
+ async function prepareCliTransport(ctx, extraEnv = {}, cli = DEFAULT_CLI_CONFIG, platform = process.platform) {
2348
+ const E = cli.envPrefix;
2349
+ const stateHome = resolveStateHome(E);
2350
+ const capabilities = cli.activeCapabilities ?? DEFAULT_ACTIVE_CAPABILITIES;
2351
+ const stateDir = path3.join(ctx.workingDirectory, cli.stateDirName);
2352
+ await fs5.promises.mkdir(stateDir, { recursive: true });
2353
+ if (ctx.standingPrompt)
2354
+ writeAgentFile(ctx.workingDirectory, ctx.standingPrompt);
2355
+ const binDir = writeCliLink(stateDir, cli.cliName, cli.hostCliPath, platform);
2356
+ if (!ctx.credentialProxy) {
2357
+ throw new Error("prepareCliTransport: ctx.credentialProxy is required — start a credential proxy " + "(see src/credentials) and pass { broker, proxyUrl }. There is no plaintext mode.");
2358
+ }
2359
+ ctx.credentialProxy.broker.revokeAgent(ctx.agentId);
2360
+ const reg = ctx.credentialProxy.broker.mint(ctx.agentId, ctx.launchId ?? "default", capabilities, ctx.credentialProxy.runnerKey);
2361
+ const tokenFile = reg.voucherFile;
2362
+ const resolved = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
2363
+ const pathValue = [binDir, process.env.PATH ?? ""].filter(Boolean).join(path3.delimiter);
2364
+ const layers = [
2365
+ { name: "hostStatic", precedence: 10, vars: cli.extraEnv ?? {} },
2366
+ { name: "userEnv", precedence: 20, vars: resolved.envVars },
2367
+ { name: "driver", precedence: 30, vars: extraEnv },
2368
+ {
2369
+ name: "platformContract",
2370
+ precedence: 40,
2371
+ vars: {
2372
+ ...platformEnv(E, {
2373
+ stateHome,
2374
+ agentId: ctx.agentId,
2375
+ cliName: cli.cliName,
2376
+ serverUrl: ctx.config.serverUrl,
2377
+ capabilities,
2378
+ launchId: ctx.launchId,
2379
+ traceDir: ctx.cliTransportTraceDir
2380
+ }),
2381
+ FORCE_COLOR: "0"
2382
+ }
2383
+ },
2384
+ { name: "runtimeContext", precedence: 50, vars: runtimeContextEnv(E, ctx.config.runtimeContext) },
2385
+ {
2386
+ name: "network",
2387
+ precedence: 60,
2388
+ vars: { NO_PROXY: ["127.0.0.1", "localhost", process.env.NO_PROXY].filter(Boolean).join(","), PATH: pathValue }
2389
+ },
2390
+ { name: "providerProtected", precedence: 70, vars: resolved.providerEnv },
2391
+ {
2392
+ name: "credential",
2393
+ precedence: 100,
2394
+ sensitive: true,
2395
+ vars: { [`${E}_PROXY_URL`]: ctx.credentialProxy.proxyUrl, [`${E}_PROXY_TOKEN_FILE`]: tokenFile }
2396
+ }
2397
+ ];
2398
+ const { env: spawnEnv } = mergeEnvLayers(process.env, layers);
2399
+ return { stateDir, tokenFile, spawnEnv };
2400
+ }
2401
+ function buildCliTransportSystemPrompt(config, opts) {
2402
+ return buildCliSystemPrompt(config, opts);
2403
+ }
2404
+
2405
+ // src/drivers/claudeProviderIsolation.ts
2406
+ import * as fs6 from "fs";
2407
+ import * as path4 from "path";
2408
+ function buildClaudeProviderIsolationEnv(ctx) {
2409
+ const hasCustomProvider = Boolean(process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_API_KEY);
2410
+ if (!hasCustomProvider)
2411
+ return {};
2412
+ const root = path4.join(ctx.workingDirectory, ".alook", "claude-provider");
2413
+ const home = path4.join(root, "home");
2414
+ const configDir = path4.join(home, ".claude");
2415
+ fs6.mkdirSync(configDir, { recursive: true });
2416
+ const hostClaude = path4.join(process.env.HOME || ".", ".claude");
2417
+ for (const sub of ["skills", "commands"]) {
2418
+ const target = path4.join(hostClaude, sub);
2419
+ const link = path4.join(configDir, sub);
2420
+ try {
2421
+ if (fs6.existsSync(target) && !fs6.existsSync(link))
2422
+ fs6.symlinkSync(target, link);
2423
+ } catch {}
2424
+ }
2425
+ return {
2426
+ HOME: home,
2427
+ USERPROFILE: home,
2428
+ CLAUDE_CONFIG_DIR: configDir,
2429
+ CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1"
2430
+ };
2431
+ }
2432
+
2433
+ // src/drivers/probe.ts
2434
+ import { execFileSync } from "child_process";
2435
+ import * as fs7 from "fs";
2436
+ import * as path5 from "path";
2437
+ function resolveCommandOnPath(command, deps = {}) {
2438
+ if (deps.which)
2439
+ return deps.which(command);
2440
+ try {
2441
+ if (process.platform === "win32") {
2442
+ const out2 = execFileSync("where", [command], { encoding: "utf8", timeout: 5000 });
2443
+ const first = out2.split(/\r?\n/).find((line) => line.trim().length > 0);
2444
+ return first?.trim() || null;
2445
+ }
2446
+ const out = execFileSync("which", [command], { encoding: "utf8", timeout: 5000 });
2447
+ return out.trim() || null;
2448
+ } catch {
2449
+ return null;
2450
+ }
2451
+ }
2452
+ function firstExistingPath(candidates) {
2453
+ for (const c of candidates) {
2454
+ if (c && fs7.existsSync(c))
2455
+ return c;
2456
+ }
2457
+ return null;
2458
+ }
2459
+ function needsWindowsShimShell(command, platform) {
2460
+ return platform === "win32" && /\.(cmd|bat)$/i.test(command);
2461
+ }
2462
+ function probeCommandVersion(command, args = [], deps = {}, platform = process.platform) {
2463
+ try {
2464
+ const shell = needsWindowsShimShell(command, platform);
2465
+ const out = execFileSync(command, [...args, "--version"], { encoding: "utf8", timeout: 5000, shell });
2466
+ const line = out.split(`
2467
+ `)[0]?.trim();
2468
+ if (!line)
2469
+ return { ok: false, error: "empty_version_output" };
2470
+ return { ok: true, version: line };
2471
+ } catch (err) {
2472
+ const code = err?.code ?? err?.code ?? "version_probe_failed";
2473
+ return { ok: false, error: String(code) };
2474
+ }
2475
+ }
2476
+ function resolveHomePath(relativePath, deps = {}) {
2477
+ return path5.join(deps.homeDir || process.env.HOME || ".", relativePath);
2478
+ }
2479
+ function resolveSpawnSpec(command, args, deps = {}, platform = process.platform) {
2480
+ const resolved = resolveCommandOnPath(command, deps) ?? command;
2481
+ return { command: resolved, args, shell: needsWindowsShimShell(resolved, platform) };
2482
+ }
2483
+ function resolveClaudeCommand(deps = {}) {
2484
+ const onPath = resolveCommandOnPath("claude", deps);
2485
+ if (onPath)
2486
+ return onPath;
2487
+ if (process.platform === "darwin") {
2488
+ return firstExistingPath([
2489
+ resolveHomePath("Applications/Claude Code URL Handler.app/Contents/MacOS/claude", deps),
2490
+ "/Applications/Claude Code URL Handler.app/Contents/MacOS/claude"
2491
+ ]);
2492
+ }
2493
+ return null;
2494
+ }
2495
+ function probeClaude(deps = {}) {
2496
+ const command = resolveClaudeCommand(deps);
2497
+ if (!command)
2498
+ return { status: "unhealthy", lastError: "not_on_path" };
2499
+ const r = probeCommandVersion(command, [], deps);
2500
+ if (!r.ok)
2501
+ return { status: "unhealthy", lastError: r.error };
2502
+ return { status: "healthy", version: r.version };
2503
+ }
2504
+ function probeCliRuntime(binary, deps = {}) {
2505
+ const command = resolveCommandOnPath(binary, deps);
2506
+ if (!command)
2507
+ return { status: "unhealthy", lastError: "not_on_path" };
2508
+ const r = probeCommandVersion(command, [], deps);
2509
+ if (!r.ok)
2510
+ return { status: "unhealthy", lastError: r.error };
2511
+ return { status: "healthy", version: r.version };
2512
+ }
2513
+
2514
+ // src/drivers/claudeLaunch.ts
2515
+ var DEFAULT_CLAUDE_MODEL = "sonnet";
2516
+ var CLAUDE_DISALLOWED_TOOLS = "EnterPlanMode,ExitPlanMode,ScheduleWakeup,CronCreate,CronList,CronDelete";
2517
+ function buildClaudeArgs(config) {
2518
+ const f = resolveLaunchFieldsOrDefault(config.runtimeConfig);
2519
+ const args = [
2520
+ "--allow-dangerously-skip-permissions",
2521
+ "--dangerously-skip-permissions",
2522
+ "--verbose",
2523
+ "--permission-mode",
2524
+ "bypassPermissions",
2525
+ "--output-format",
2526
+ "stream-json",
2527
+ "--input-format",
2528
+ "stream-json",
2529
+ "--include-partial-messages",
2530
+ "--model",
2531
+ f.model || DEFAULT_CLAUDE_MODEL,
2532
+ "--disallowed-tools",
2533
+ f.disallowedTools || CLAUDE_DISALLOWED_TOOLS
2534
+ ];
2535
+ if (f.reasoningEffort)
2536
+ args.push("--effort", f.reasoningEffort);
2537
+ if (f.fastMode)
2538
+ args.push("--settings", '{"fastMode":true}');
2539
+ if (config.sessionId)
2540
+ args.push("--resume", config.sessionId);
2541
+ return args;
2542
+ }
2543
+ function resolveClaudeLaunchCommand(config) {
2544
+ const override = resolveLaunchFieldsOrDefault(config.runtimeConfig).command?.trim();
2545
+ return override || resolveClaudeCommand() || "claude";
2546
+ }
2547
+ function buildClaudeSpawnSpec(claudeCommand, platform = process.platform) {
2548
+ const command = claudeCommand ?? "claude";
2549
+ const shell = platform === "win32" && (!command || /\.(cmd|bat)$/i.test(command));
2550
+ return { command, shell };
2551
+ }
2552
+
2553
+ // src/drivers/claudeEventNormalizer.ts
2554
+ var API_ERROR_RE = /API Error:.*(?:Connection error|\b[45]\d{2}\b)/i;
2555
+
2556
+ class ClaudeEventNormalizer {
2557
+ currentSession = null;
2558
+ get currentSessionId() {
2559
+ return this.currentSession;
2560
+ }
2561
+ normalizeLine(line) {
2562
+ let event;
2563
+ try {
2564
+ event = JSON.parse(line);
2565
+ } catch {
2566
+ return [];
2567
+ }
2568
+ if (event?.session_id)
2569
+ this.currentSession = event.session_id;
2570
+ const out = [];
2571
+ switch (event?.type) {
2572
+ case "system":
2573
+ this.handleSystem(event, out);
2574
+ break;
2575
+ case "assistant":
2576
+ this.handleAssistant(event, out);
2577
+ break;
2578
+ case "user":
2579
+ this.handleUser(event, out);
2580
+ break;
2581
+ case "result":
2582
+ this.handleResult(event, out);
2583
+ break;
2584
+ }
2585
+ return out;
2586
+ }
2587
+ handleSystem(event, out) {
2588
+ if (event.subtype === "init") {
2589
+ out.push({ kind: "session_init", sessionId: event.session_id ?? this.currentSession ?? "" });
2590
+ return;
2591
+ }
2592
+ if (event.subtype === "status" && event.status === "compacting") {
2593
+ out.push({ kind: "compaction_started" });
2594
+ return;
2595
+ }
2596
+ if (event.subtype === "compact_boundary") {
2597
+ out.push({ kind: "compaction_finished" });
2598
+ return;
2599
+ }
2600
+ if (event.subtype === "status" || event.subtype === "stream_event") {
2601
+ out.push({
2602
+ kind: "internal_progress",
2603
+ source: "claude_system",
2604
+ itemType: event.subtype,
2605
+ payloadBytes: JSON.stringify(event).length
2606
+ });
2607
+ }
2608
+ }
2609
+ handleAssistant(event, out) {
2610
+ const content = event?.message?.content;
2611
+ if (!Array.isArray(content))
2612
+ return;
2613
+ for (const block of content) {
2614
+ if (block?.type === "thinking") {
2615
+ out.push({ kind: "thinking", text: block.thinking ?? "" });
2616
+ } else if (block?.type === "text") {
2617
+ const text = block.text ?? "";
2618
+ if (API_ERROR_RE.test(text))
2619
+ out.push({ kind: "error", message: text });
2620
+ else
2621
+ out.push({ kind: "text", text });
2622
+ } else if (block?.type === "tool_use") {
2623
+ out.push({ kind: "tool_call", name: block.name ?? "unknown_tool", input: block.input });
2624
+ }
2625
+ }
2626
+ }
2627
+ handleUser(event, out) {
2628
+ const content = event?.message?.content;
2629
+ if (!Array.isArray(content))
2630
+ return;
2631
+ for (const block of content) {
2632
+ if (block?.type === "tool_result")
2633
+ out.push({ kind: "tool_output", name: "" });
2634
+ }
2635
+ }
2636
+ handleResult(event, out) {
2637
+ const usage = this.buildUsageTelemetry(event);
2638
+ if (usage)
2639
+ out.push(usage);
2640
+ if (event.is_error || event.subtype === "error_during_execution") {
2641
+ out.push({ kind: "error", message: String(event.result ?? "Claude runtime error") });
2642
+ }
2643
+ out.push({ kind: "turn_end", sessionId: event.session_id ?? this.currentSession ?? undefined });
2644
+ }
2645
+ buildUsageTelemetry(event) {
2646
+ const u = event?.usage;
2647
+ if (!u && event?.total_cost_usd == null)
2648
+ return null;
2649
+ return {
2650
+ kind: "telemetry",
2651
+ name: "token_usage",
2652
+ source: "claude_result_usage",
2653
+ usageKind: "per_turn",
2654
+ attrs: {
2655
+ inputTokens: u?.input_tokens,
2656
+ outputTokens: u?.output_tokens,
2657
+ cachedInputTokens: u?.cache_read_input_tokens,
2658
+ cacheCreationInputTokens: u?.cache_creation_input_tokens,
2659
+ totalCostUsd: event?.total_cost_usd,
2660
+ durationMs: event?.duration_ms,
2661
+ durationApiMs: event?.duration_api_ms,
2662
+ numTurns: event?.num_turns,
2663
+ resultSubtype: event?.subtype,
2664
+ resultIsError: event?.is_error,
2665
+ serviceTier: u?.service_tier
2666
+ }
2667
+ };
2668
+ }
2669
+ }
2670
+
2671
+ // src/drivers/claude.ts
2672
+ class ClaudeDriver {
2673
+ id = "claude";
2674
+ lifecycle = { kind: "persistent", stdin: "gated", inFlightWake: "queue" };
2675
+ session = { recovery: "resume_or_fresh" };
2676
+ model = {
2677
+ detectedModelsVerifiedAs: "launchable",
2678
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
2679
+ };
2680
+ supportsStdinNotification = true;
2681
+ busyDeliveryMode = "gated";
2682
+ supportsNativeStandingPrompt = true;
2683
+ eventNormalizer = new ClaudeEventNormalizer;
2684
+ probe() {
2685
+ return probeClaude();
2686
+ }
2687
+ async spawn(ctx) {
2688
+ const cliConfig = ctx.agentCliPath ? { ...DEFAULT_CLI_CONFIG, hostCliPath: ctx.agentCliPath } : undefined;
2689
+ const { spawnEnv } = await prepareCliTransport(ctx, buildClaudeProviderIsolationEnv(ctx), cliConfig);
2690
+ const args = buildClaudeArgs(ctx.config);
2691
+ delete spawnEnv.CLAUDECODE;
2692
+ const claudeCommand = resolveClaudeLaunchCommand(ctx.config);
2693
+ const spawnSpec = buildClaudeSpawnSpec(claudeCommand);
2694
+ const proc = spawnAgentProcess(spawnSpec.command, args, {
2695
+ cwd: ctx.workingDirectory,
2696
+ env: spawnEnv,
2697
+ shell: spawnSpec.shell
2698
+ });
2699
+ const stdinMsg = JSON.stringify({
2700
+ type: "user",
2701
+ message: { role: "user", content: [{ type: "text", text: ctx.prompt }] },
2702
+ ...ctx.config.sessionId ? { session_id: ctx.config.sessionId } : {}
2703
+ });
2704
+ proc.stdin?.write(stdinMsg + `
2705
+ `);
2706
+ return { process: proc };
2707
+ }
2708
+ parseLine(line) {
2709
+ return this.eventNormalizer.normalizeLine(line);
2710
+ }
2711
+ get currentSessionId() {
2712
+ return this.eventNormalizer.currentSessionId;
2713
+ }
2714
+ encodeStdinMessage(text, sessionId, _opts) {
2715
+ return JSON.stringify({
2716
+ type: "user",
2717
+ message: { role: "user", content: [{ type: "text", text }] },
2718
+ ...sessionId ? { session_id: sessionId } : {}
2719
+ });
2720
+ }
2721
+ buildSystemPrompt(config) {
2722
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
2723
+ }
2724
+ }
2725
+
2726
+ // src/drivers/codexTelemetrySidecar.ts
2727
+ function mapCodexTelemetry(method, params) {
2728
+ if (method === "thread/tokenUsage/updated") {
2729
+ const u = params?.usage ?? params ?? {};
2730
+ return [
2731
+ {
2732
+ kind: "telemetry",
2733
+ name: "token_usage",
2734
+ source: "codex_thread_token_usage_updated",
2735
+ usageKind: "cumulative_session",
2736
+ attrs: {
2737
+ totalTokens: u.totalTokens ?? u.total_tokens,
2738
+ inputTokens: u.inputTokens ?? u.input_tokens,
2739
+ cachedInputTokens: u.cachedInputTokens ?? u.cached_input_tokens,
2740
+ outputTokens: u.outputTokens ?? u.output_tokens,
2741
+ reasoningOutputTokens: u.reasoningOutputTokens ?? u.reasoning_output_tokens,
2742
+ modelContextWindow: u.modelContextWindow ?? u.model_context_window,
2743
+ cachedInputRatio: u.cachedInputRatio,
2744
+ contextUtilization: u.contextUtilization
2745
+ }
2746
+ }
2747
+ ];
2748
+ }
2749
+ if (method === "account/rateLimits/updated") {
2750
+ const r = params ?? {};
2751
+ return [
2752
+ {
2753
+ kind: "telemetry",
2754
+ name: "rate_limits",
2755
+ source: "codex_account_rate_limits_updated",
2756
+ attrs: {
2757
+ limitId: r.limitId,
2758
+ planType: r.planType,
2759
+ usedPercent: r.usedPercent,
2760
+ windowDurationMins: r.windowDurationMins,
2761
+ resetsAt: r.resetsAt
2762
+ }
2763
+ }
2764
+ ];
2765
+ }
2766
+ return [];
2767
+ }
2768
+
2769
+ // src/drivers/codexEventNormalizer.ts
2770
+ class CodexEventNormalizer {
2771
+ threadId = null;
2772
+ get currentSessionId() {
2773
+ return this.threadId;
2774
+ }
2775
+ adoptThreadId(threadId) {
2776
+ this.threadId = threadId;
2777
+ }
2778
+ normalizeLine(line) {
2779
+ let msg;
2780
+ try {
2781
+ msg = JSON.parse(line);
2782
+ } catch {
2783
+ return [];
2784
+ }
2785
+ if (msg?.error && msg.id !== undefined) {
2786
+ return [{ kind: "error", message: msg.error?.message ?? "Codex RPC error" }];
2787
+ }
2788
+ if (msg?.result?.thread?.id) {
2789
+ this.threadId = msg.result.thread.id;
2790
+ return [{ kind: "session_init", sessionId: this.threadId }];
2791
+ }
2792
+ if (msg?.method)
2793
+ return this.handleNotification(msg.method, msg.params ?? {});
2794
+ return [];
2795
+ }
2796
+ handleNotification(method, params) {
2797
+ switch (method) {
2798
+ case "thread/started":
2799
+ if (params?.thread?.id)
2800
+ this.threadId = params.thread.id;
2801
+ return this.threadId ? [{ kind: "session_init", sessionId: this.threadId }] : [];
2802
+ case "turn/started":
2803
+ return [{ kind: "thinking", text: "" }];
2804
+ case "item/reasoning/textDelta":
2805
+ case "item/reasoning/summaryTextDelta":
2806
+ return [{ kind: "thinking", text: params?.delta ?? "" }];
2807
+ case "item/agentMessage/delta":
2808
+ return [{ kind: "text", text: params?.delta ?? "" }];
2809
+ case "item/started":
2810
+ return this.handleItemStarted(params);
2811
+ case "item/completed":
2812
+ return this.handleItemCompleted(params);
2813
+ case "rawResponseItem/completed":
2814
+ return [{ kind: "internal_progress", source: "codex_raw_item", itemType: "rawResponseItem" }];
2815
+ case "configWarning":
2816
+ case "warning":
2817
+ case "guardianWarning":
2818
+ case "deprecationNotice":
2819
+ return [
2820
+ { kind: "runtime_diagnostic", severity: "warning", source: method, message: params?.message ?? method }
2821
+ ];
2822
+ case "turn/completed":
2823
+ if (params?.status === "failed")
2824
+ return [{ kind: "error", message: "Codex turn failed" }];
2825
+ if (params?.status === "interrupted") {
2826
+ return [{ kind: "error", message: "Codex turn interrupted" }, { kind: "turn_end", sessionId: this.threadId ?? undefined }];
2827
+ }
2828
+ return [{ kind: "turn_end", sessionId: this.threadId ?? undefined }];
2829
+ case "error":
2830
+ return [{ kind: "error", message: params?.message ?? "Codex error" }];
2831
+ case "thread/tokenUsage/updated":
2832
+ case "account/rateLimits/updated":
2833
+ return mapCodexTelemetry(method, params);
2834
+ default:
2835
+ return [];
2836
+ }
2837
+ }
2838
+ handleItemStarted(params) {
2839
+ const t = params?.item?.type ?? params?.type;
2840
+ switch (t) {
2841
+ case "commandExecution":
2842
+ return [{ kind: "tool_call", name: "shell", input: params?.item }];
2843
+ case "contextCompaction":
2844
+ return [{ kind: "compaction_started" }];
2845
+ case "enteredReviewMode":
2846
+ return [{ kind: "review_started" }];
2847
+ case "fileChange":
2848
+ return [{ kind: "tool_call", name: "file_change", input: params?.item }];
2849
+ case "mcpToolCall":
2850
+ return [{ kind: "tool_call", name: `mcp_${params?.item?.name ?? "tool"}`, input: params?.item }];
2851
+ case "webSearch":
2852
+ return [{ kind: "tool_call", name: "web_search", input: params?.item }];
2853
+ case "collabAgentToolCall":
2854
+ return [{ kind: "tool_call", name: "collab_tool_call", input: params?.item }];
2855
+ default:
2856
+ return [];
2857
+ }
2858
+ }
2859
+ handleItemCompleted(params) {
2860
+ const t = params?.item?.type ?? params?.type;
2861
+ switch (t) {
2862
+ case "commandExecution":
2863
+ return [{ kind: "tool_output", name: "shell" }];
2864
+ case "contextCompaction":
2865
+ return [{ kind: "compaction_finished" }];
2866
+ case "exitedReviewMode":
2867
+ return [{ kind: "review_finished" }];
2868
+ case "fileChange":
2869
+ return [{ kind: "tool_output", name: "file_change" }];
2870
+ case "mcpToolCall":
2871
+ return [{ kind: "tool_output", name: `mcp_${params?.item?.name ?? "tool"}` }];
2872
+ case "webSearch":
2873
+ return [{ kind: "tool_output", name: "web_search" }];
2874
+ case "collabAgentToolCall":
2875
+ return [{ kind: "tool_output", name: "collab_tool_call" }];
2876
+ case "agentMessage":
2877
+ return [{ kind: "text", text: params?.item?.text ?? "" }];
2878
+ case "reasoning":
2879
+ return [{ kind: "thinking", text: params?.item?.text ?? "" }];
2880
+ default:
2881
+ return [];
2882
+ }
2883
+ }
2884
+ }
2885
+
2886
+ // src/drivers/codexHome.ts
2887
+ import * as os2 from "os";
2888
+ import * as path6 from "path";
2889
+ function readConfiguredCodexHome(env) {
2890
+ const raw = env.CODEX_HOME;
2891
+ return typeof raw === "string" && raw.trim().length > 0 ? raw : null;
2892
+ }
2893
+ function resolveCodexHomeRootFromEnv(env = process.env, opts = {}) {
2894
+ const raw = readConfiguredCodexHome(env);
2895
+ if (raw)
2896
+ return path6.resolve(opts.cwd ?? process.cwd(), raw);
2897
+ return path6.join(opts.defaultHomeDir ?? os2.homedir(), ".codex");
2898
+ }
2899
+
2900
+ // src/drivers/codex.ts
2901
+ class CodexDriver {
2902
+ id = "codex";
2903
+ lifecycle = { kind: "persistent", stdin: "gated", inFlightWake: "queue" };
2904
+ session = { recovery: "resume_or_fresh" };
2905
+ model = {
2906
+ detectedModelsVerifiedAs: "launchable",
2907
+ toLaunchSpec: (modelId) => ({ params: { model: modelId } })
2908
+ };
2909
+ supportsStdinNotification = true;
2910
+ busyDeliveryMode = "gated";
2911
+ supportsNativeStandingPrompt = true;
2912
+ eventNormalizer = new CodexEventNormalizer;
2913
+ requestId = 0;
2914
+ codexHomeRoot = null;
2915
+ nextRequestId() {
2916
+ return ++this.requestId;
2917
+ }
2918
+ get codexHome() {
2919
+ return this.codexHomeRoot;
2920
+ }
2921
+ probe() {
2922
+ return probeCliRuntime("codex");
2923
+ }
2924
+ async spawn(ctx) {
2925
+ const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
2926
+ this.codexHomeRoot = resolveCodexHomeRootFromEnv(spawnEnv, { cwd: ctx.workingDirectory });
2927
+ const spec = resolveSpawnSpec("codex", ["app-server", "--listen", "stdio://"]);
2928
+ const proc = spawnAgentProcess(spec.command, spec.args, {
2929
+ cwd: ctx.workingDirectory,
2930
+ env: spawnEnv,
2931
+ shell: spec.shell
2932
+ });
2933
+ queueMicrotask(() => {
2934
+ proc.stdin?.write(JSON.stringify({
2935
+ jsonrpc: "2.0",
2936
+ id: this.nextRequestId(),
2937
+ method: "initialize",
2938
+ params: {
2939
+ clientInfo: { name: "agent-backend", version: "1.0.0" },
2940
+ capabilities: { experimentalApi: true }
2941
+ }
2942
+ }) + `
2943
+ `);
2944
+ const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
2945
+ const resuming = Boolean(ctx.config.sessionId);
2946
+ const params = {
2947
+ cwd: ctx.workingDirectory,
2948
+ approvalPolicy: "never",
2949
+ sandbox: "danger-full-access",
2950
+ sandbox_mode: "danger-full-access",
2951
+ experimentalRawEvents: true
2952
+ };
2953
+ if (resuming)
2954
+ params.threadId = ctx.config.sessionId;
2955
+ if (f.model)
2956
+ params.model = f.model;
2957
+ if (f.reasoningEffort)
2958
+ params.config = { model_reasoning_effort: f.reasoningEffort };
2959
+ if (f.fastMode)
2960
+ params.serviceTier = "fast";
2961
+ proc.stdin?.write(JSON.stringify({
2962
+ jsonrpc: "2.0",
2963
+ id: this.nextRequestId(),
2964
+ method: resuming ? "thread/resume" : "thread/start",
2965
+ params
2966
+ }) + `
2967
+ `);
2968
+ });
2969
+ return { process: proc };
2970
+ }
2971
+ parseLine(line) {
2972
+ return this.eventNormalizer.normalizeLine(line);
2973
+ }
2974
+ get currentSessionId() {
2975
+ return this.eventNormalizer.currentSessionId;
2976
+ }
2977
+ encodeStdinMessage(text, sessionId, opts) {
2978
+ const threadId = sessionId ?? this.eventNormalizer.currentSessionId;
2979
+ if (!threadId)
2980
+ return null;
2981
+ const input = [{ type: "text", text }];
2982
+ if (opts?.mode === "idle") {
2983
+ return JSON.stringify({
2984
+ jsonrpc: "2.0",
2985
+ id: this.nextRequestId(),
2986
+ method: "turn/start",
2987
+ params: { threadId, input }
2988
+ });
2989
+ }
2990
+ return JSON.stringify({
2991
+ jsonrpc: "2.0",
2992
+ id: this.nextRequestId(),
2993
+ method: "turn/steer",
2994
+ params: { threadId, input }
2995
+ });
2996
+ }
2997
+ buildSystemPrompt(config) {
2998
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
2999
+ }
3000
+ }
3001
+
3002
+ // src/drivers/gemini.ts
3003
+ function buildGeminiArgs(config) {
3004
+ const f = resolveLaunchFieldsOrDefault(config.runtimeConfig);
3005
+ return [
3006
+ "--output-format",
3007
+ "stream-json",
3008
+ "--yolo",
3009
+ "-p",
3010
+ "",
3011
+ ...f.model ? ["--model", f.model] : [],
3012
+ ...config.sessionId ? ["--resume", config.sessionId] : []
3013
+ ];
3014
+ }
3015
+
3016
+ class GeminiDriver {
3017
+ id = "gemini";
3018
+ lifecycle = {
3019
+ kind: "per_turn",
3020
+ start: "immediate",
3021
+ exit: "natural",
3022
+ inFlightWake: "spawn_new"
3023
+ };
3024
+ session = { recovery: "resume_or_fresh" };
3025
+ model = {
3026
+ detectedModelsVerifiedAs: "suggestion_only",
3027
+ toLaunchSpec: (modelId) => modelId && modelId !== "default" ? { args: ["--model", modelId] } : { args: [] }
3028
+ };
3029
+ supportsStdinNotification = false;
3030
+ busyDeliveryMode = "none";
3031
+ sessionId = null;
3032
+ probe() {
3033
+ return probeCliRuntime("gemini");
3034
+ }
3035
+ async spawn(ctx) {
3036
+ this.sessionId = ctx.config.sessionId ?? null;
3037
+ const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
3038
+ spawnEnv.GEMINI_CLI_TRUST_WORKSPACE ??= "true";
3039
+ if (process.platform === "win32")
3040
+ spawnEnv.GEMINI_PTY_INFO ??= "child_process";
3041
+ const spec = resolveSpawnSpec("gemini", buildGeminiArgs(ctx.config));
3042
+ const proc = spawnAgentProcess(spec.command, spec.args, {
3043
+ cwd: ctx.workingDirectory,
3044
+ env: spawnEnv,
3045
+ shell: spec.shell
3046
+ });
3047
+ proc.stdin?.end(ctx.prompt);
3048
+ return { process: proc };
3049
+ }
3050
+ parseLine(line) {
3051
+ let event;
3052
+ try {
3053
+ event = JSON.parse(line);
3054
+ } catch {
3055
+ return [];
3056
+ }
3057
+ switch (event?.type) {
3058
+ case "init":
3059
+ this.sessionId = event.session_id ?? this.sessionId;
3060
+ return this.sessionId ? [{ kind: "session_init", sessionId: this.sessionId }] : [];
3061
+ case "message":
3062
+ if (event.role === "assistant" && event.content)
3063
+ return [{ kind: "text", text: event.content }];
3064
+ return [];
3065
+ case "tool_use":
3066
+ return [{ kind: "tool_call", name: event.tool_name ?? "unknown_tool", input: event.parameters }];
3067
+ case "error":
3068
+ return [{ kind: "error", message: event.message ?? "Gemini error" }];
3069
+ case "result":
3070
+ return event.status && event.status !== "success" ? [{ kind: "error", message: String(event.status) }, { kind: "turn_end", sessionId: this.sessionId ?? undefined }] : [{ kind: "turn_end", sessionId: this.sessionId ?? undefined }];
3071
+ default:
3072
+ return [];
3073
+ }
3074
+ }
3075
+ get currentSessionId() {
3076
+ return this.sessionId;
3077
+ }
3078
+ encodeStdinMessage() {
3079
+ return null;
3080
+ }
3081
+ buildSystemPrompt(config) {
3082
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
3083
+ }
3084
+ }
3085
+
3086
+ // src/drivers/copilot.ts
3087
+ class CopilotDriver {
3088
+ id = "copilot";
3089
+ lifecycle = {
3090
+ kind: "per_turn",
3091
+ start: "immediate",
3092
+ exit: "natural",
3093
+ inFlightWake: "spawn_new"
3094
+ };
3095
+ session = { recovery: "resume_or_fresh" };
3096
+ model = {
3097
+ detectedModelsVerifiedAs: "launchable",
3098
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
3099
+ };
3100
+ supportsStdinNotification = false;
3101
+ busyDeliveryMode = "none";
3102
+ sessionId = null;
3103
+ probe() {
3104
+ return probeCliRuntime("copilot");
3105
+ }
3106
+ async spawn(ctx) {
3107
+ this.sessionId = ctx.config.sessionId ?? null;
3108
+ const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
3109
+ const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
3110
+ const args = ["--output-format", "json", "--allow-all-tools", "--allow-all-paths", "-p", ctx.prompt];
3111
+ if (f.model)
3112
+ args.push("--model", f.model);
3113
+ if (f.reasoningEffort)
3114
+ args.push("--effort", f.reasoningEffort);
3115
+ if (ctx.config.sessionId)
3116
+ args.push(`--resume=${ctx.config.sessionId}`);
3117
+ const spec = resolveSpawnSpec("copilot", args);
3118
+ const proc = spawnAgentProcess(spec.command, spec.args, {
3119
+ cwd: ctx.workingDirectory,
3120
+ env: spawnEnv,
3121
+ shell: spec.shell
3122
+ });
3123
+ return { process: proc };
3124
+ }
3125
+ parseLine(line) {
3126
+ let event;
3127
+ try {
3128
+ event = JSON.parse(line);
3129
+ } catch {
3130
+ return [];
3131
+ }
3132
+ switch (event?.type) {
3133
+ case "assistant.turn_start":
3134
+ if (event.sessionId)
3135
+ this.sessionId = event.sessionId;
3136
+ return this.sessionId ? [{ kind: "session_init", sessionId: this.sessionId }] : [];
3137
+ case "assistant.reasoning":
3138
+ return [{ kind: "thinking", text: event.content ?? "" }];
3139
+ case "assistant.message_delta":
3140
+ return [{ kind: "text", text: event.deltaContent ?? "" }];
3141
+ case "assistant.message": {
3142
+ const reqs = event.message?.toolRequests ?? [];
3143
+ return reqs.map((req) => ({
3144
+ kind: "tool_call",
3145
+ name: req.name ?? req.toolName ?? "unknown_tool",
3146
+ input: req.arguments ?? req.parameters ?? req.input ?? {}
3147
+ }));
3148
+ }
3149
+ case "assistant.turn_end":
3150
+ return [{ kind: "turn_end", sessionId: this.sessionId ?? undefined }];
3151
+ case "result":
3152
+ if (event.sessionId)
3153
+ this.sessionId = event.sessionId;
3154
+ return event.exitCode && event.exitCode !== 0 ? [{ kind: "error", message: `Copilot exited with code ${event.exitCode}` }, { kind: "turn_end" }] : [{ kind: "turn_end", sessionId: this.sessionId ?? undefined }];
3155
+ default:
3156
+ return [];
3157
+ }
3158
+ }
3159
+ get currentSessionId() {
3160
+ return this.sessionId;
3161
+ }
3162
+ encodeStdinMessage() {
3163
+ return null;
3164
+ }
3165
+ buildSystemPrompt(config) {
3166
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
3167
+ }
3168
+ }
3169
+
3170
+ // src/drivers/cursor.ts
3171
+ class CursorDriver {
3172
+ id = "cursor";
3173
+ lifecycle = {
3174
+ kind: "per_turn",
3175
+ start: "immediate",
3176
+ exit: "natural",
3177
+ inFlightWake: "spawn_new"
3178
+ };
3179
+ session = { recovery: "resume_or_fresh" };
3180
+ model = {
3181
+ detectedModelsVerifiedAs: "launchable",
3182
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
3183
+ };
3184
+ supportsStdinNotification = false;
3185
+ busyDeliveryMode = "none";
3186
+ sessionId = null;
3187
+ probe() {
3188
+ return probeCliRuntime("cursor-agent");
3189
+ }
3190
+ async spawn(ctx) {
3191
+ this.sessionId = ctx.config.sessionId ?? null;
3192
+ const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
3193
+ const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
3194
+ const args = ["--print", "--output-format", "stream-json", "--yolo", "--approve-mcps", "--trust"];
3195
+ if (f.model)
3196
+ args.push("--model", f.model);
3197
+ if (ctx.config.sessionId)
3198
+ args.push("--resume", ctx.config.sessionId);
3199
+ args.push(ctx.prompt);
3200
+ const spec = resolveSpawnSpec("cursor-agent", args);
3201
+ const proc = spawnAgentProcess(spec.command, spec.args, {
3202
+ cwd: ctx.workingDirectory,
3203
+ env: spawnEnv,
3204
+ shell: spec.shell
3205
+ });
3206
+ return { process: proc };
3207
+ }
3208
+ parseLine(line) {
3209
+ let event;
3210
+ try {
3211
+ event = JSON.parse(line);
3212
+ } catch {
3213
+ return [];
3214
+ }
3215
+ if (event?.type === "system") {
3216
+ if (event.subtype === "init") {
3217
+ this.sessionId = event.session_id ?? this.sessionId;
3218
+ return this.sessionId ? [{ kind: "session_init", sessionId: this.sessionId }] : [];
3219
+ }
3220
+ if (event.subtype === "status" && event.status === "compacting")
3221
+ return [{ kind: "compaction_started" }];
3222
+ if (event.subtype === "compact_boundary")
3223
+ return [{ kind: "compaction_finished" }];
3224
+ return [];
3225
+ }
3226
+ if (event?.type === "assistant") {
3227
+ const content = event.message?.content ?? [];
3228
+ const out = [];
3229
+ for (const block of content) {
3230
+ if (block?.type === "thinking")
3231
+ out.push({ kind: "thinking", text: block.thinking ?? "" });
3232
+ else if (block?.type === "text")
3233
+ out.push({ kind: "text", text: block.text ?? "" });
3234
+ else if (block?.type === "tool_use")
3235
+ out.push({ kind: "tool_call", name: block.name ?? "unknown_tool", input: block.input });
3236
+ }
3237
+ return out;
3238
+ }
3239
+ if (event?.type === "result") {
3240
+ const out = [];
3241
+ if (event.subtype !== "success" || event.is_error) {
3242
+ const detail = (event.errors ?? []).map((e) => e?.message).filter(Boolean).join("; ");
3243
+ out.push({ kind: "error", message: detail || String(event.result ?? "Cursor error") });
3244
+ }
3245
+ out.push({ kind: "turn_end", sessionId: event.session_id ?? this.sessionId ?? undefined });
3246
+ return out;
3247
+ }
3248
+ return [];
3249
+ }
3250
+ get currentSessionId() {
3251
+ return this.sessionId;
3252
+ }
3253
+ encodeStdinMessage() {
3254
+ return null;
3255
+ }
3256
+ buildSystemPrompt(config) {
3257
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
3258
+ }
3259
+ }
3260
+
3261
+ // src/drivers/opencode.ts
3262
+ class OpenCodeDriver {
3263
+ id = "opencode";
3264
+ lifecycle = {
3265
+ kind: "per_turn",
3266
+ start: "defer_until_concrete_message",
3267
+ exit: "terminate_on_turn_end",
3268
+ inFlightWake: "coalesce_into_pending"
3269
+ };
3270
+ session = { recovery: "resume_or_fresh" };
3271
+ model = {
3272
+ detectedModelsVerifiedAs: "launchable",
3273
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
3274
+ };
3275
+ supportsStdinNotification = false;
3276
+ busyDeliveryMode = "none";
3277
+ terminateProcessOnTurnEnd = true;
3278
+ deferSpawnUntilMessage = true;
3279
+ sessionId = null;
3280
+ shouldDeferWakeMessage(message) {
3281
+ return message?.type === "system";
3282
+ }
3283
+ probe() {
3284
+ return probeCliRuntime("opencode");
3285
+ }
3286
+ async spawn(ctx) {
3287
+ this.sessionId = ctx.config.sessionId ?? null;
3288
+ const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
3289
+ const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
3290
+ const args = ["run", "--format", "json", "--dangerously-skip-permissions", "--pure", "--dir", ctx.workingDirectory];
3291
+ if (f.model)
3292
+ args.push("--model", f.model);
3293
+ if (ctx.config.sessionId)
3294
+ args.push("--session", ctx.config.sessionId);
3295
+ const promptArg = ctx.prompt === ctx.standingPrompt ? "No new messages are pending. Stop now." : ctx.prompt;
3296
+ args.push("--", promptArg);
3297
+ const spec = resolveSpawnSpec("opencode", args);
3298
+ const proc = spawnAgentProcess(spec.command, spec.args, {
3299
+ cwd: ctx.workingDirectory,
3300
+ env: spawnEnv,
3301
+ shell: spec.shell
3302
+ });
3303
+ proc.stdin?.end();
3304
+ return { process: proc };
3305
+ }
3306
+ parseLine(line) {
3307
+ let event;
3308
+ try {
3309
+ event = JSON.parse(line);
3310
+ } catch {
3311
+ return [];
3312
+ }
3313
+ const out = [];
3314
+ if (event?.sessionID && this.sessionId !== event.sessionID) {
3315
+ this.sessionId = event.sessionID;
3316
+ out.push({ kind: "session_init", sessionId: this.sessionId });
3317
+ }
3318
+ switch (event?.type) {
3319
+ case "step_start":
3320
+ out.push({ kind: "thinking", text: "" });
3321
+ break;
3322
+ case "text":
3323
+ if (typeof event.part?.text === "string" && event.part.text.length > 0)
3324
+ out.push({ kind: "text", text: event.part.text });
3325
+ break;
3326
+ case "tool_use":
3327
+ out.push({ kind: "tool_call", name: event.part?.tool ?? "unknown_tool", input: event.part?.state?.input });
3328
+ break;
3329
+ case "step_finish":
3330
+ if (event.part?.reason !== "tool-calls")
3331
+ out.push({ kind: "turn_end", sessionId: this.sessionId ?? undefined });
3332
+ break;
3333
+ case "error":
3334
+ out.push({
3335
+ kind: "error",
3336
+ message: event.error?.data?.message ?? event.error?.message ?? "OpenCode error"
3337
+ });
3338
+ out.push({ kind: "turn_end", sessionId: this.sessionId ?? undefined });
3339
+ break;
3340
+ }
3341
+ return out;
3342
+ }
3343
+ get currentSessionId() {
3344
+ return this.sessionId;
3345
+ }
3346
+ encodeStdinMessage() {
3347
+ return null;
3348
+ }
3349
+ buildSystemPrompt(config) {
3350
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
3351
+ }
3352
+ }
3353
+
3354
+ // src/drivers/antigravity.ts
3355
+ import { randomUUID } from "crypto";
3356
+ var ERROR_LINE_PATTERNS = [/^error[:\s]/i, /\bfatal\b/i, /\bpanic\b/i, /unable to/i];
3357
+ var ANTIGRAVITY_PRINT_TIMEOUT = "30m";
3358
+ function buildAntigravityArgs(ctx) {
3359
+ const args = ["--print", "--print-timeout", ANTIGRAVITY_PRINT_TIMEOUT, "--dangerously-skip-permissions"];
3360
+ if (ctx.config.sessionId)
3361
+ args.push("--continue");
3362
+ return args;
3363
+ }
3364
+
3365
+ class AntigravityDriver {
3366
+ id = "antigravity";
3367
+ lifecycle = {
3368
+ kind: "per_turn",
3369
+ start: "immediate",
3370
+ exit: "natural",
3371
+ inFlightWake: "spawn_new"
3372
+ };
3373
+ session = { recovery: "resume_or_fresh" };
3374
+ model = {
3375
+ detectedModelsVerifiedAs: "suggestion_only",
3376
+ toLaunchSpec: (_modelId) => ({ args: [] })
3377
+ };
3378
+ supportsStdinNotification = false;
3379
+ busyDeliveryMode = "none";
3380
+ sessionId = null;
3381
+ sentInit = false;
3382
+ probe() {
3383
+ return probeCliRuntime("agy");
3384
+ }
3385
+ async spawn(ctx) {
3386
+ this.sessionId = ctx.config.sessionId ?? randomUUID();
3387
+ this.sentInit = false;
3388
+ const { spawnEnv } = await prepareCliTransport(ctx, {
3389
+ NO_COLOR: "1",
3390
+ SSH_CLIENT: "",
3391
+ SSH_CONNECTION: "",
3392
+ SSH_TTY: ""
3393
+ });
3394
+ const spec = resolveSpawnSpec("agy", buildAntigravityArgs(ctx));
3395
+ const proc = spawnAgentProcess(spec.command, spec.args, {
3396
+ cwd: ctx.workingDirectory,
3397
+ env: spawnEnv,
3398
+ shell: spec.shell
3399
+ });
3400
+ proc.stdin?.end(ctx.prompt);
3401
+ return { process: proc };
3402
+ }
3403
+ parseLine(line) {
3404
+ const trimmed = line.trim();
3405
+ if (!trimmed)
3406
+ return [];
3407
+ const out = [];
3408
+ if (!this.sentInit) {
3409
+ this.sentInit = true;
3410
+ out.push({ kind: "session_init", sessionId: this.sessionId });
3411
+ }
3412
+ if (ERROR_LINE_PATTERNS.some((re) => re.test(trimmed)))
3413
+ out.push({ kind: "error", message: trimmed });
3414
+ else
3415
+ out.push({ kind: "text", text: line });
3416
+ return out;
3417
+ }
3418
+ get currentSessionId() {
3419
+ return this.sessionId;
3420
+ }
3421
+ encodeStdinMessage() {
3422
+ return null;
3423
+ }
3424
+ buildSystemPrompt(config) {
3425
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
3426
+ }
3427
+ }
3428
+
3429
+ // src/drivers/kimi.ts
3430
+ import { randomUUID as randomUUID2 } from "crypto";
3431
+ function parseToolArguments(args) {
3432
+ if (typeof args !== "string")
3433
+ return args ?? {};
3434
+ try {
3435
+ return JSON.parse(args);
3436
+ } catch {
3437
+ return { raw: args };
3438
+ }
3439
+ }
3440
+
3441
+ class KimiDriver {
3442
+ id = "kimi";
3443
+ lifecycle = { kind: "persistent", stdin: "direct", inFlightWake: "steer" };
3444
+ session = { recovery: "resume_or_fresh" };
3445
+ model = {
3446
+ detectedModelsVerifiedAs: "launchable",
3447
+ toLaunchSpec: (modelId) => ({ args: ["--model", modelId] })
3448
+ };
3449
+ supportsStdinNotification = true;
3450
+ busyDeliveryMode = "direct";
3451
+ sessionId = "";
3452
+ sentInit = false;
3453
+ promptRequestId = randomUUID2();
3454
+ probe() {
3455
+ return probeCliRuntime("kimi");
3456
+ }
3457
+ async spawn(ctx) {
3458
+ this.sessionId = ctx.config.sessionId || randomUUID2();
3459
+ const isResume = Boolean(ctx.config.sessionId);
3460
+ const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
3461
+ const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
3462
+ const args = ["--wire", "--yolo", "--session", this.sessionId];
3463
+ if (f.model)
3464
+ args.push("--model", f.model);
3465
+ const spec = resolveSpawnSpec("kimi", args);
3466
+ const proc = spawnAgentProcess(spec.command, spec.args, {
3467
+ cwd: ctx.workingDirectory,
3468
+ env: spawnEnv,
3469
+ shell: spec.shell
3470
+ });
3471
+ proc.stdin?.write(JSON.stringify({
3472
+ jsonrpc: "2.0",
3473
+ id: randomUUID2(),
3474
+ method: "initialize",
3475
+ params: {
3476
+ protocol_version: "1.3",
3477
+ client: { name: "agent-backend", version: "1.0.0" },
3478
+ capabilities: { supports_question: false, supports_plan_mode: false }
3479
+ }
3480
+ }) + `
3481
+ `);
3482
+ proc.stdin?.write(JSON.stringify({
3483
+ jsonrpc: "2.0",
3484
+ id: this.promptRequestId,
3485
+ method: "prompt",
3486
+ params: {
3487
+ user_input: isResume ? ctx.prompt : "Your system prompt contains your standing instructions. Follow it now and begin listening for messages."
3488
+ }
3489
+ }) + `
3490
+ `);
3491
+ return { process: proc };
3492
+ }
3493
+ parseLine(line) {
3494
+ let msg;
3495
+ try {
3496
+ msg = JSON.parse(line);
3497
+ } catch {
3498
+ return [];
3499
+ }
3500
+ const out = [];
3501
+ if (!this.sentInit) {
3502
+ this.sentInit = true;
3503
+ out.push({ kind: "session_init", sessionId: this.sessionId });
3504
+ }
3505
+ if (msg?.error) {
3506
+ out.push({ kind: "error", message: msg.error?.message ?? "Unknown Kimi error" });
3507
+ out.push({ kind: "turn_end", sessionId: this.sessionId });
3508
+ return out;
3509
+ }
3510
+ if (msg?.method !== "event")
3511
+ return out;
3512
+ const payload = msg.params ?? {};
3513
+ switch (payload.event) {
3514
+ case "StepBegin":
3515
+ out.push({ kind: "thinking", text: "" });
3516
+ break;
3517
+ case "CompactionBegin":
3518
+ out.push({ kind: "compaction_started" });
3519
+ break;
3520
+ case "CompactionEnd":
3521
+ out.push({ kind: "compaction_finished" });
3522
+ break;
3523
+ case "ContentPart":
3524
+ if (payload.type === "think")
3525
+ out.push({ kind: "thinking", text: payload.think ?? "" });
3526
+ else if (payload.type === "text")
3527
+ out.push({ kind: "text", text: payload.text ?? "" });
3528
+ break;
3529
+ case "ToolCall":
3530
+ out.push({
3531
+ kind: "tool_call",
3532
+ name: payload.function?.name ?? "unknown_tool",
3533
+ input: parseToolArguments(payload.function?.arguments)
3534
+ });
3535
+ break;
3536
+ case "TurnEnd":
3537
+ out.push({ kind: "turn_end", sessionId: this.sessionId });
3538
+ break;
3539
+ case "StepInterrupted":
3540
+ out.push({ kind: "error", message: "Turn interrupted" });
3541
+ out.push({ kind: "turn_end", sessionId: this.sessionId });
3542
+ break;
3543
+ }
3544
+ return out;
3545
+ }
3546
+ get currentSessionId() {
3547
+ return this.sessionId || null;
3548
+ }
3549
+ encodeStdinMessage(text, _sessionId, opts) {
3550
+ const method = opts?.mode === "idle" ? "prompt" : "steer";
3551
+ return JSON.stringify({ jsonrpc: "2.0", id: randomUUID2(), method, params: { user_input: text } });
3552
+ }
3553
+ buildSystemPrompt(config) {
3554
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
3555
+ }
3556
+ }
3557
+
3558
+ // src/drivers/pi.ts
3559
+ import { createRequire as createRequire2 } from "module";
3560
+ import { mkdirSync as mkdirSync6, existsSync as existsSync5, readFileSync as readFileSync5, realpathSync } from "fs";
3561
+ import * as path7 from "path";
3562
+
3563
+ // src/runtime/sdkRuntimeSession.ts
3564
+ import { EventEmitter as EventEmitter3 } from "events";
3565
+ var IDLE_PROMPT_RETRY_MS = 25;
3566
+ var IDLE_PROMPT_MAX_WAIT_MS = 1000;
3567
+ function delay(ms) {
3568
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
3569
+ }
3570
+ function errorMessage(err) {
3571
+ return err instanceof Error ? err.message : String(err);
3572
+ }
3573
+
3574
+ class SdkRuntimeSession {
3575
+ handle;
3576
+ sessionId;
3577
+ events = new EventEmitter3;
3578
+ sentInit = false;
3579
+ constructor(handle, sessionId) {
3580
+ this.handle = handle;
3581
+ this.sessionId = sessionId;
3582
+ }
3583
+ on(event, cb) {
3584
+ this.events.on(event, cb);
3585
+ }
3586
+ emitEvents(events) {
3587
+ if (!this.sentInit && events.length > 0) {
3588
+ this.sentInit = true;
3589
+ this.events.emit("runtime_event", { kind: "session_init", sessionId: this.sessionId });
3590
+ }
3591
+ for (const e of events)
3592
+ this.events.emit("runtime_event", e);
3593
+ }
3594
+ async send(text, mode) {
3595
+ try {
3596
+ if (mode === "busy") {
3597
+ await this.handle.steer(text);
3598
+ return { ok: true };
3599
+ }
3600
+ const stillStreaming = this.handle.isStreaming && !await this.waitForStreamingToClear();
3601
+ if (stillStreaming) {
3602
+ await this.handle.steer(text);
3603
+ return { ok: true };
3604
+ }
3605
+ try {
3606
+ await this.handle.prompt(text);
3607
+ } catch (err) {
3608
+ this.emitEvents([
3609
+ { kind: "error", message: errorMessage(err) },
3610
+ { kind: "turn_end", sessionId: this.sessionId }
3611
+ ]);
3612
+ }
3613
+ } catch (err) {
3614
+ this.emitEvents([{ kind: "error", message: errorMessage(err) }]);
3615
+ }
3616
+ return { ok: true };
3617
+ }
3618
+ async waitForStreamingToClear() {
3619
+ const deadline = Date.now() + IDLE_PROMPT_MAX_WAIT_MS;
3620
+ while (this.handle.isStreaming) {
3621
+ if (Date.now() >= deadline)
3622
+ return false;
3623
+ await delay(IDLE_PROMPT_RETRY_MS);
3624
+ }
3625
+ return true;
3626
+ }
3627
+ async stop() {
3628
+ if (this.handle.isStreaming && this.handle.abort)
3629
+ await this.handle.abort();
3630
+ await this.handle.dispose?.();
3631
+ }
3632
+ get currentSessionId() {
3633
+ return this.sessionId;
3634
+ }
3635
+ }
3636
+
3637
+ // src/drivers/pi.ts
3638
+ var PI_SDK_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
3639
+ function isPiSdkPackageJson(pkgJsonPath) {
3640
+ if (!existsSync5(pkgJsonPath))
3641
+ return false;
3642
+ try {
3643
+ const pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf-8"));
3644
+ return pkg.name === PI_SDK_PACKAGE_NAME;
3645
+ } catch {
3646
+ return false;
3647
+ }
3648
+ }
3649
+ function resolvePiSdkPackageDir(deps = {}) {
3650
+ const binPath = resolveCommandOnPath("pi", deps);
3651
+ if (!binPath)
3652
+ return;
3653
+ try {
3654
+ let dir = path7.dirname(realpathSync(binPath));
3655
+ const MAX_DEPTH = 8;
3656
+ for (let i = 0;i < MAX_DEPTH; i++) {
3657
+ if (isPiSdkPackageJson(path7.join(dir, "package.json")))
3658
+ return dir;
3659
+ const siblingDir = path7.join(dir, "node_modules", PI_SDK_PACKAGE_NAME);
3660
+ if (isPiSdkPackageJson(path7.join(siblingDir, "package.json")))
3661
+ return siblingDir;
3662
+ const parent = path7.dirname(dir);
3663
+ if (parent === dir)
3664
+ break;
3665
+ dir = parent;
3666
+ }
3667
+ } catch {}
3668
+ return;
3669
+ }
3670
+ function resolvePiSdkVersionFromPath(deps = {}) {
3671
+ const dir = resolvePiSdkPackageDir(deps);
3672
+ if (!dir)
3673
+ return;
3674
+ try {
3675
+ const pkg = JSON.parse(readFileSync5(path7.join(dir, "package.json"), "utf-8"));
3676
+ return pkg.version;
3677
+ } catch {
3678
+ return;
3679
+ }
3680
+ }
3681
+ function readPiSdkVersion() {
3682
+ try {
3683
+ const req = createRequire2(import.meta.url);
3684
+ const pkg = req("@earendil-works/pi-coding-agent/package.json");
3685
+ if (pkg.version)
3686
+ return pkg.version;
3687
+ } catch {}
3688
+ return resolvePiSdkVersionFromPath();
3689
+ }
3690
+ function mapPiSdkEventToParsedEvents(event, sessionId, state) {
3691
+ if (event?.type === "message_update") {
3692
+ const d = event.delta ?? {};
3693
+ switch (d.type) {
3694
+ case "thinking_delta":
3695
+ return [{ kind: "thinking", text: d.delta ?? "" }];
3696
+ case "text_delta":
3697
+ state.sawTextDelta = true;
3698
+ return [{ kind: "text", text: d.delta ?? "" }];
3699
+ case "text_end":
3700
+ return state.sawTextDelta ? [] : [{ kind: "text", text: d.content ?? "" }];
3701
+ case "error":
3702
+ return [{ kind: "error", message: d.message ?? "Pi error" }];
3703
+ default:
3704
+ return [];
3705
+ }
3706
+ }
3707
+ switch (event?.type) {
3708
+ case "tool_execution_start":
3709
+ return [{ kind: "tool_call", name: event.toolName ?? "unknown_tool", input: event.args ?? {} }];
3710
+ case "tool_execution_end":
3711
+ return [{ kind: "tool_output", name: event.toolName ?? "unknown_tool" }];
3712
+ case "compaction_start":
3713
+ return [{ kind: "compaction_started" }];
3714
+ case "compaction_end":
3715
+ return [{ kind: "compaction_finished" }];
3716
+ case "agent_end":
3717
+ return [{ kind: "turn_end", sessionId }];
3718
+ default:
3719
+ return [];
3720
+ }
3721
+ }
3722
+
3723
+ class PiDriver {
3724
+ id = "pi";
3725
+ lifecycle = { kind: "persistent", stdin: "direct", inFlightWake: "steer" };
3726
+ session = { recovery: "resume_or_fresh" };
3727
+ model = {
3728
+ detectedModelsVerifiedAs: "launchable",
3729
+ toLaunchSpec: (modelId) => ({ params: { model: modelId } })
3730
+ };
3731
+ supportsStdinNotification = true;
3732
+ busyDeliveryMode = "direct";
3733
+ supportsNativeStandingPrompt = true;
3734
+ sessionId = null;
3735
+ probe() {
3736
+ const version = readPiSdkVersion();
3737
+ if (!version) {
3738
+ return { status: "unhealthy", lastError: "sdk_not_installed" };
3739
+ }
3740
+ return { status: "healthy", version };
3741
+ }
3742
+ spawn() {
3743
+ throw new Error("PiDriver uses a native RuntimeSession; child-process spawn is unsupported");
3744
+ }
3745
+ async createSession(ctx, deps) {
3746
+ const spawnEnv = await deps.buildSpawnEnv();
3747
+ if (ctx.standingPrompt) {
3748
+ mkdirSync6(ctx.workingDirectory, { recursive: true });
3749
+ writeAgentFile(ctx.workingDirectory, ctx.standingPrompt);
3750
+ }
3751
+ const f = resolveLaunchFieldsOrDefault(ctx.config.runtimeConfig);
3752
+ const { session, sessionId } = await deps.createAgentSession({
3753
+ cwd: ctx.workingDirectory,
3754
+ sessionId: ctx.config.sessionId,
3755
+ model: f.model,
3756
+ thinkingLevel: f.reasoningEffort,
3757
+ spawnEnv
3758
+ });
3759
+ this.sessionId = sessionId;
3760
+ const state = { sawTextDelta: false };
3761
+ const handle = {
3762
+ prompt: (t) => session.prompt(t, session.isStreaming ? { streamingBehavior: "followUp" } : undefined),
3763
+ steer: (t) => session.steer(t),
3764
+ abort: () => session.abort(),
3765
+ dispose: () => session.dispose(),
3766
+ get isStreaming() {
3767
+ return session.isStreaming;
3768
+ }
3769
+ };
3770
+ const runtimeSession = new SdkRuntimeSession(handle, this.sessionId);
3771
+ session.subscribe((event) => runtimeSession.emitEvents(mapPiSdkEventToParsedEvents(event, this.sessionId, state)));
3772
+ return runtimeSession;
3773
+ }
3774
+ parseLine() {
3775
+ return [];
3776
+ }
3777
+ get currentSessionId() {
3778
+ return this.sessionId;
3779
+ }
3780
+ encodeStdinMessage() {
3781
+ return null;
3782
+ }
3783
+ buildSystemPrompt(config) {
3784
+ return buildCliTransportSystemPrompt(config, { lifecycleKind: this.lifecycle.kind });
3785
+ }
3786
+ }
3787
+
3788
+ // src/drivers/index.ts
3789
+ var driverFactories = {
3790
+ claude: () => new ClaudeDriver,
3791
+ codex: () => new CodexDriver,
3792
+ antigravity: () => new AntigravityDriver,
3793
+ copilot: () => new CopilotDriver,
3794
+ cursor: () => new CursorDriver,
3795
+ gemini: () => new GeminiDriver,
3796
+ kimi: () => new KimiDriver,
3797
+ opencode: () => new OpenCodeDriver,
3798
+ pi: () => new PiDriver
3799
+ };
3800
+ function getDriver(runtimeId) {
3801
+ const createDriver = driverFactories[runtimeId];
3802
+ const driver = createDriver?.();
3803
+ if (!driver) {
3804
+ throw new Error(`Unknown runtime: ${runtimeId}. Available: ${Object.keys(driverFactories).join(", ")}`);
3805
+ }
3806
+ return driver;
3807
+ }
3808
+ function listRuntimeIds() {
3809
+ return Object.keys(driverFactories);
3810
+ }
3811
+
3812
+ // src/discovery.ts
3813
+ function resolveAlookCliPath(moduleDir) {
3814
+ const thisDir = moduleDir ?? path8.dirname(fileURLToPath(import.meta.url));
3815
+ const target = path8.basename(thisDir) === "dist" ? path8.resolve(thisDir, "cli", "index.js") : path8.resolve(thisDir, "..", "scripts", "alook-shim.mjs");
3816
+ return fs8.existsSync(target) ? target : null;
3817
+ }
3818
+ function deriveCliFallbackCandidates(cliPath) {
3819
+ if (!cliPath)
3820
+ return [];
3821
+ const normalized = cliPath.split(path8.sep).join("/");
3822
+ const marker = "/node_modules/";
3823
+ const idx = normalized.indexOf(marker);
3824
+ if (idx === -1)
3825
+ return [];
3826
+ const globalRoot = cliPath.slice(0, idx + marker.length - 1);
3827
+ const tail = path8.join("dist", "cli", "index.js");
3828
+ return [
3829
+ path8.join(globalRoot, "@alook", "daemon", tail)
3830
+ ].filter((candidate) => candidate !== cliPath);
3831
+ }
3832
+ function resolveAlookCliPathWithFallback(primary) {
3833
+ const resolved = primary ?? resolveAlookCliPath();
3834
+ if (resolved && fs8.existsSync(resolved))
3835
+ return resolved;
3836
+ if (resolved) {
3837
+ const fallbacks = deriveCliFallbackCandidates(resolved);
3838
+ for (const fallback of fallbacks) {
3839
+ if (fs8.existsSync(fallback))
3840
+ return fallback;
3841
+ }
3842
+ }
3843
+ return resolved;
3844
+ }
3845
+ async function detectRuntimes() {
3846
+ const ids = listRuntimeIds();
3847
+ const results = [];
3848
+ const nowIso = new Date().toISOString();
3849
+ for (const id of ids) {
3850
+ try {
3851
+ const driver = getDriver(id);
3852
+ const probe = await driver.probe();
3853
+ const healthy = probe.status === "healthy";
3854
+ results.push({
3855
+ id,
3856
+ status: healthy ? "healthy" : "unhealthy",
3857
+ version: probe.version,
3858
+ lastError: healthy ? undefined : probe.lastError ?? "probe_failed",
3859
+ lastErrorAt: healthy ? undefined : nowIso
3860
+ });
3861
+ } catch (err) {
3862
+ results.push({
3863
+ id,
3864
+ status: "unhealthy",
3865
+ lastError: err?.code ?? "probe_threw",
3866
+ lastErrorAt: nowIso
3867
+ });
3868
+ }
3869
+ }
3870
+ return results;
3871
+ }
3872
+
3873
+ // src/drivers/piSdkDeps.ts
3874
+ import { readFileSync as readFileSync6 } from "fs";
3875
+ import * as path9 from "path";
3876
+ import { pathToFileURL } from "url";
3877
+ var PI_SDK_PACKAGE_NAME2 = "@earendil-works/pi-coding-agent";
3878
+ var cachedSdkPromise = null;
3879
+ async function importPiSdkFromGlobalInstall() {
3880
+ const dir = resolvePiSdkPackageDir();
3881
+ if (!dir) {
3882
+ throw new Error(`${PI_SDK_PACKAGE_NAME2} not found — install it (e.g. \`npm install -g ${PI_SDK_PACKAGE_NAME2}\`) before launching a pi agent`);
3883
+ }
3884
+ const pkg = JSON.parse(readFileSync6(path9.join(dir, "package.json"), "utf-8"));
3885
+ const entry = pkg.exports?.["."]?.import ?? pkg.main ?? "./dist/index.js";
3886
+ const entryPath = path9.join(dir, entry);
3887
+ return import(pathToFileURL(entryPath).href);
3888
+ }
3889
+ function loadPiSdkModule() {
3890
+ if (!cachedSdkPromise) {
3891
+ cachedSdkPromise = (async () => {
3892
+ try {
3893
+ return await import(PI_SDK_PACKAGE_NAME2);
3894
+ } catch {
3895
+ return importPiSdkFromGlobalInstall();
3896
+ }
3897
+ })().catch((err) => {
3898
+ cachedSdkPromise = null;
3899
+ throw err;
3900
+ });
3901
+ }
3902
+ return cachedSdkPromise;
3903
+ }
3904
+ function parseModelString(model) {
3905
+ if (!model)
3906
+ return;
3907
+ const idx = model.indexOf("/");
3908
+ if (idx <= 0 || idx === model.length - 1)
3909
+ return;
3910
+ return { provider: model.slice(0, idx), id: model.slice(idx + 1) };
3911
+ }
3912
+ function createPiSdkDriverDeps(ctx, loadSdk = loadPiSdkModule) {
3913
+ return {
3914
+ async buildSpawnEnv() {
3915
+ const cliConfig = ctx.agentCliPath ? { ...DEFAULT_CLI_CONFIG, hostCliPath: ctx.agentCliPath } : DEFAULT_CLI_CONFIG;
3916
+ const { spawnEnv } = await prepareCliTransport(ctx, {}, cliConfig);
3917
+ return spawnEnv;
3918
+ },
3919
+ async createAgentSession(opts) {
3920
+ const sdk = await loadSdk();
3921
+ const authStorage = sdk.AuthStorage.create();
3922
+ const provider = ctx.config.runtimeConfig?.provider;
3923
+ if (provider?.kind === "pi-builtin") {
3924
+ authStorage.setRuntimeApiKey(provider.providerId, provider.apiKey);
3925
+ }
3926
+ const modelRegistry = sdk.ModelRegistry.create(authStorage);
3927
+ const parsed = parseModelString(opts.model);
3928
+ const model = parsed ? modelRegistry.find(parsed.provider, parsed.id) : undefined;
3929
+ const cwd = opts.cwd;
3930
+ const sessionManager = opts.sessionId ? sdk.SessionManager.continueRecent(cwd) : sdk.SessionManager.create(cwd);
3931
+ const spawnEnv = opts.spawnEnv;
3932
+ const bashTool = sdk.createBashToolDefinition(cwd, {
3933
+ spawnHook: (spawnCtx) => ({ ...spawnCtx, env: { ...spawnCtx.env, ...spawnEnv } })
3934
+ });
3935
+ const { session, sessionId } = await sdk.createAgentSession({
3936
+ cwd,
3937
+ model,
3938
+ thinkingLevel: opts.thinkingLevel,
3939
+ authStorage,
3940
+ modelRegistry,
3941
+ sessionManager,
3942
+ customTools: [bashTool]
3943
+ });
3944
+ const resolvedSessionId = sessionId ?? session.sessionId;
3945
+ if (!resolvedSessionId)
3946
+ throw new Error("pi SDK createAgentSession did not produce a sessionId");
3947
+ return { session, sessionId: resolvedSessionId };
3948
+ }
3949
+ };
3950
+ }
3951
+
3952
+ // ../shared/src/lib/discriminator.ts
3953
+ function formatHandle(name, discriminator) {
3954
+ return `${name}#${discriminator}`;
3955
+ }
3956
+
3957
+ // src/daemon/createDaemon.ts
3958
+ var WARMUP_BACKOFF_MS = [250, 500, 1000, 2000, 4000];
3959
+ var WARMUP_CEILING_MS = 30000;
3960
+ function deriveAuditLogSubcommand(pathname) {
3961
+ const stripped = pathname.replace(/^\/api\/community\/agent\//, "/api/");
3962
+ if (!stripped.startsWith("/api/"))
3963
+ return null;
3964
+ const sub = stripped.slice("/api/".length).split("/")[0]?.split("?")[0] ?? "";
3965
+ if (!sub)
3966
+ return null;
3967
+ if (sub === "ack")
3968
+ return null;
3969
+ return sub;
3970
+ }
3971
+ async function createDaemon(opts) {
3972
+ const log = opts.logger ?? createLogger({ header: "@alook/daemon" });
3973
+ const fallbackBase = (process.env.ALOOK_PROJECT_ROOT || `${homedir2()}/.alook`) + "/daemon";
3974
+ const workdirFor = (agentId) => `${opts.workingDirectoryBase ?? fallbackBase}/${agentId}`;
3975
+ const resolvedCliPath = resolveAlookCliPathWithFallback(opts.agentCliPath);
3976
+ const timeline2 = createTimelineRecorder({
3977
+ timelineDirFor: (agentId) => `${workdirFor(agentId)}/.context_timeline`,
3978
+ providerFor: () => opts.runtimeReport[0]?.id ?? null
3979
+ });
3980
+ let channelRef = null;
3981
+ let managerRef = null;
3982
+ const emitBotAuditEvent = (agentId, event, context) => {
3983
+ channelRef?.reportBotAuditEvent?.({
3984
+ type: "bot_audit_event",
3985
+ agentId,
3986
+ sessionId: context?.sessionId ?? null,
3987
+ launchId: context?.launchId ?? null,
3988
+ event
3989
+ });
3990
+ };
3991
+ const broker = new CredentialBroker({ upstreamBaseUrl: opts.serverUrl });
3992
+ const proxy = await startCredentialProxy(broker, {
3993
+ onInboxPullResponse: (agentId, messages) => timeline2.appendEntryForAgent(agentId, messages),
3994
+ onProxyRequest: (agentId, _method, pathname) => {
3995
+ const subcommand = deriveAuditLogSubcommand(pathname);
3996
+ if (!subcommand)
3997
+ return;
3998
+ const context = managerRef?.auditContext(agentId);
3999
+ emitBotAuditEvent(agentId, {
4000
+ kind: "cli_invocation",
4001
+ payload: { subcommand }
4002
+ }, context);
4003
+ }
4004
+ });
4005
+ const enrolledKeys = new Map;
4006
+ const botsById = new Map;
4007
+ async function listMyBotsHttp() {
4008
+ const res = await fetch(`${opts.serverUrl}/api/community/daemon/bots`, {
4009
+ method: "GET",
4010
+ headers: { authorization: `Bearer ${opts.machineKey}` }
4011
+ });
4012
+ if (!res.ok)
4013
+ throw new Error(`listMyBots ${res.status}`);
4014
+ const json = await res.json();
4015
+ return json.bots ?? [];
4016
+ }
4017
+ async function coldStartWarmup() {
4018
+ const start = Date.now();
4019
+ let attempt = 0;
4020
+ while (Date.now() - start < WARMUP_CEILING_MS) {
4021
+ try {
4022
+ const bots = await listMyBotsHttp();
4023
+ botsById.clear();
4024
+ for (const b of bots) {
4025
+ botsById.set(b.id, {
4026
+ name: b.name,
4027
+ discriminator: b.discriminator,
4028
+ description: b.description,
4029
+ ownerName: b.ownerName,
4030
+ ownerDiscriminator: b.ownerDiscriminator
4031
+ });
4032
+ }
4033
+ log.info("cold-start bot-cache warmup succeeded", { bots: bots.length, attempt });
4034
+ return;
4035
+ } catch {
4036
+ const delay2 = WARMUP_BACKOFF_MS[Math.min(attempt, WARMUP_BACKOFF_MS.length - 1)];
4037
+ await new Promise((r) => setTimeout(r, delay2));
4038
+ attempt++;
4039
+ }
4040
+ }
4041
+ log.warn("cold-start bot-cache warmup exhausted its ceiling", { ceilingMs: WARMUP_CEILING_MS, attempts: attempt });
4042
+ }
4043
+ async function resyncPendingWakes() {
4044
+ try {
4045
+ const res = await fetch(`${opts.serverUrl}/api/community/daemon/resync-wakes`, {
4046
+ method: "POST",
4047
+ headers: { authorization: `Bearer ${opts.machineKey}` }
4048
+ });
4049
+ if (!res.ok)
4050
+ throw new Error(`resync-wakes ${res.status}`);
4051
+ const json = await res.json();
4052
+ log.info("wake resync completed", { woken: json.woken ?? 0 });
4053
+ } catch (err) {
4054
+ log.warn("wake resync failed", { err: err instanceof Error ? err.message : String(err) });
4055
+ }
4056
+ }
4057
+ const enrollAgent = async (agentId) => {
4058
+ const existing = enrolledKeys.get(agentId);
4059
+ if (existing)
4060
+ return existing;
4061
+ try {
4062
+ const res = await fetch(`${opts.serverUrl}/api/community/daemon/enroll-agent`, {
4063
+ method: "POST",
4064
+ headers: { "content-type": "application/json", authorization: `Bearer ${opts.machineKey}` },
4065
+ body: JSON.stringify({ agentId })
4066
+ });
4067
+ const json = await res.json();
4068
+ if (!res.ok || !json.runnerKey) {
4069
+ if (res.status === 404) {
4070
+ throw new UnknownBotError(agentId);
4071
+ }
4072
+ throw new BotEnrollFailedError(agentId, new Error(json.error ?? `enroll failed (${res.status})`));
4073
+ }
4074
+ enrolledKeys.set(agentId, json.runnerKey);
4075
+ return json.runnerKey;
4076
+ } catch (err) {
4077
+ if (err instanceof UnknownBotError || err instanceof BotEnrollFailedError) {
4078
+ log.warn("agent enroll failed", { agentId, err: err.message });
4079
+ throw err;
4080
+ }
4081
+ const wrapped = new BotEnrollFailedError(agentId, err);
4082
+ log.warn("agent enroll failed", { agentId, err: wrapped.message });
4083
+ throw wrapped;
4084
+ }
4085
+ };
4086
+ const channel = new WsControlChannel({
4087
+ url: opts.serverWsUrl,
4088
+ headers: { Authorization: `Bearer ${opts.machineKey}` },
4089
+ webSocketFactory: opts.webSocketFactory,
4090
+ onAuthRejected: opts.onAuthRejected,
4091
+ logger: log.child("ws")
4092
+ });
4093
+ channelRef = channel;
4094
+ function handleBotFrame(cmd) {
4095
+ switch (cmd.type) {
4096
+ case "bot:added":
4097
+ botsById.set(cmd.botId, {
4098
+ name: cmd.name,
4099
+ discriminator: cmd.discriminator,
4100
+ description: cmd.description,
4101
+ ownerName: cmd.ownerName,
4102
+ ownerDiscriminator: cmd.ownerDiscriminator
4103
+ });
4104
+ log.debug("bot:added", { botId: cmd.botId, name: cmd.name });
4105
+ break;
4106
+ case "bot:updated": {
4107
+ const prev = botsById.get(cmd.botId);
4108
+ botsById.set(cmd.botId, {
4109
+ name: cmd.name,
4110
+ discriminator: cmd.discriminator,
4111
+ description: cmd.description,
4112
+ ownerName: cmd.ownerName,
4113
+ ownerDiscriminator: cmd.ownerDiscriminator
4114
+ });
4115
+ const nameChanged = prev && prev.name !== cmd.name;
4116
+ const descChanged = prev && (prev.description ?? "") !== (cmd.description ?? "");
4117
+ log.debug("bot:updated", { botId: cmd.botId, name: cmd.name });
4118
+ if (nameChanged || descChanged) {
4119
+ manager.stop(cmd.botId);
4120
+ }
4121
+ break;
4122
+ }
4123
+ case "bot:removed":
4124
+ botsById.delete(cmd.botId);
4125
+ enrolledKeys.delete(cmd.botId);
4126
+ log.debug("bot:removed", { botId: cmd.botId });
4127
+ manager.stop(cmd.botId);
4128
+ break;
4129
+ default:
4130
+ break;
4131
+ }
4132
+ }
4133
+ let router = null;
4134
+ const manager = new AgentProcessManager({
4135
+ driverFor: (agentId, runtimeConfig) => {
4136
+ const requested = runtimeConfig?.runtime;
4137
+ if (requested && router && !router.isRuntimeHealthy(requested)) {
4138
+ throw new UnknownRuntimeError(requested, router.healthyRuntimeIds());
4139
+ }
4140
+ return opts.driverFor(agentId, runtimeConfig);
4141
+ },
4142
+ onRuntimeSpawnFailed: (runtimeId, reason) => {
4143
+ router?.markRuntimeUnhealthy(runtimeId, reason);
4144
+ },
4145
+ onRuntimeSessionEstablished: (runtimeId) => {
4146
+ router?.markRuntimeHealthy(runtimeId);
4147
+ },
4148
+ baseContextFor: (agentId) => {
4149
+ const runnerKey = enrolledKeys.get(agentId);
4150
+ if (!runnerKey)
4151
+ throw new Error(`agent ${agentId} not enrolled yet — enroll before deliver`);
4152
+ const botMeta = botsById.get(agentId);
4153
+ return {
4154
+ agentId,
4155
+ workingDirectory: workdirFor(agentId),
4156
+ credentialProxy: { broker, proxyUrl: proxy.url, runnerKey },
4157
+ agentCliPath: resolvedCliPath ?? opts.agentCliPath,
4158
+ config: {
4159
+ ...botMeta?.name ? { agentName: botMeta.name } : {},
4160
+ ...botMeta?.name && botMeta?.discriminator ? { agentHandle: `@${formatHandle(botMeta.name, botMeta.discriminator)}` } : {},
4161
+ ...botMeta?.description ? { description: botMeta.description } : {},
4162
+ ...botMeta?.ownerName && botMeta?.ownerDiscriminator ? { ownerHandle: `@${formatHandle(botMeta.ownerName, botMeta.ownerDiscriminator)}` } : {}
4163
+ }
4164
+ };
4165
+ },
4166
+ tickIntervalMs: opts.tickIntervalMs ?? 2000,
4167
+ onAgentSession: (info) => void channel.reportAgentSession(info),
4168
+ onAgentActivity: (info) => void channel.reportAgentActivity?.(info),
4169
+ onBotAuditEvent: (agentId, event, context) => emitBotAuditEvent(agentId, event, context),
4170
+ onAgentLocallyStopped: (info) => router?.markLocallyStopped(info.agentId),
4171
+ sdkDriverDepsFor: (ctx) => createPiSdkDriverDeps(ctx),
4172
+ timeline: timeline2,
4173
+ wakePromptFooter: "Use `alook inbox pull` to read your messages, then reply with `alook message send`.",
4174
+ logger: log.child("manager")
4175
+ });
4176
+ managerRef = manager;
4177
+ manager.start();
4178
+ router = new AgentRouter({
4179
+ manager,
4180
+ channel,
4181
+ runtimeReport: opts.runtimeReport,
4182
+ hostname: opts.hostname,
4183
+ platform: opts.platform,
4184
+ arch: opts.arch,
4185
+ osRelease: opts.osRelease,
4186
+ daemonVersion: opts.daemonVersion,
4187
+ logger: log.child("router"),
4188
+ onBeforeAgent: async (agentId) => {
4189
+ if (!botsById.has(agentId)) {
4190
+ try {
4191
+ const bots = await listMyBotsHttp();
4192
+ for (const b of bots) {
4193
+ botsById.set(b.id, {
4194
+ name: b.name,
4195
+ discriminator: b.discriminator,
4196
+ description: b.description,
4197
+ ownerName: b.ownerName,
4198
+ ownerDiscriminator: b.ownerDiscriminator
4199
+ });
4200
+ }
4201
+ } catch {}
4202
+ }
4203
+ if (!botsById.has(agentId)) {
4204
+ throw new UnknownBotError(agentId);
4205
+ }
4206
+ await enrollAgent(agentId);
4207
+ },
4208
+ formatUnreadNoticeText: (notice) => `You have unread messages in channel ${notice.channel}.`
4209
+ });
4210
+ channel.onCommand((cmd) => {
4211
+ handleBotFrame(cmd);
4212
+ });
4213
+ channel.onOpen(() => {
4214
+ coldStartWarmup();
4215
+ resyncPendingWakes();
4216
+ });
4217
+ channel.connect();
4218
+ await router.start();
4219
+ return {
4220
+ isOpen: () => channel.status === "open",
4221
+ proxyUrl: proxy.url,
4222
+ stop: async () => {
4223
+ channel.close();
4224
+ await proxy.close();
4225
+ await manager.stopAll();
4226
+ }
4227
+ };
4228
+ }
4229
+
4230
+ // src/cli/daemonStart.ts
4231
+ var requireFromHere = createRequire3(import.meta.url);
4232
+ function readDaemonVersion() {
4233
+ try {
4234
+ const pkg = requireFromHere("../../package.json");
4235
+ return pkg.version ?? "";
4236
+ } catch {
4237
+ return "";
4238
+ }
4239
+ }
4240
+ var CAPABILITIES = ["send", "read", "mentions", "tasks", "reactions", "server", "channels", "knowledge"];
4241
+ function resolveDefaultBaseDir() {
4242
+ const root = process.env.ALOOK_PROJECT_ROOT || path10.join(homedir3(), ".alook");
4243
+ return path10.join(root, "daemon");
4244
+ }
4245
+ var DEFAULT_BASE_DIR = resolveDefaultBaseDir();
4246
+ var log = createLogger({ header: "@alook/daemon" });
4247
+ function keyHash(machineKey) {
4248
+ return crypto2.createHash("sha256").update(machineKey).digest("hex").slice(0, 12);
4249
+ }
4250
+ function daemonsDir(baseDir) {
4251
+ return path10.join(baseDir, "daemons");
4252
+ }
4253
+ function pidfilePath(baseDir, machineKey) {
4254
+ return path10.join(daemonsDir(baseDir), `${keyHash(machineKey)}.pid`);
4255
+ }
4256
+ function isProcessAlive(pid) {
4257
+ try {
4258
+ process.kill(pid, 0);
4259
+ return true;
4260
+ } catch {
4261
+ return false;
4262
+ }
4263
+ }
4264
+ function readPidFile(filePath) {
4265
+ if (!fs9.existsSync(filePath))
4266
+ return null;
4267
+ try {
4268
+ const content = JSON.parse(fs9.readFileSync(filePath, "utf8"));
4269
+ if (typeof content.pid === "number" && typeof content.key === "string")
4270
+ return content;
4271
+ } catch {}
4272
+ return null;
4273
+ }
4274
+ function writePidFile(filePath, pid, machineKey) {
4275
+ fs9.mkdirSync(path10.dirname(filePath), { recursive: true });
4276
+ fs9.writeFileSync(filePath, JSON.stringify({ pid, key: machineKey }));
4277
+ }
4278
+ function acquireLock2(baseDir, machineKey) {
4279
+ const pf = pidfilePath(baseDir, machineKey);
4280
+ const existing = readPidFile(pf);
4281
+ if (existing && isProcessAlive(existing.pid)) {
4282
+ log.error(`daemon for this machine key already running (pid ${existing.pid}). Stop it first or remove ${pf}`);
4283
+ process.exit(1);
4284
+ }
4285
+ writePidFile(pf, process.pid, machineKey);
4286
+ return pf;
4287
+ }
4288
+ function releaseLock2(pf) {
4289
+ try {
4290
+ const content = readPidFile(pf);
4291
+ if (content && content.pid === process.pid) {
4292
+ fs9.unlinkSync(pf);
4293
+ }
4294
+ } catch {}
4295
+ }
4296
+ function daemonList(opts) {
4297
+ const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
4298
+ const dir = daemonsDir(baseDir);
4299
+ if (!fs9.existsSync(dir))
4300
+ return [];
4301
+ const files = fs9.readdirSync(dir).filter((f) => f.endsWith(".pid"));
4302
+ const results = [];
4303
+ for (const file of files) {
4304
+ const filePath = path10.join(dir, file);
4305
+ const data = readPidFile(filePath);
4306
+ if (!data)
4307
+ continue;
4308
+ const alive = isProcessAlive(data.pid);
4309
+ if (!alive) {
4310
+ try {
4311
+ fs9.unlinkSync(filePath);
4312
+ } catch {}
4313
+ }
4314
+ results.push({
4315
+ keyHash: file.replace(".pid", ""),
4316
+ keyPrefix: data.key.slice(0, 20) + "…",
4317
+ pid: data.pid,
4318
+ alive
4319
+ });
4320
+ }
4321
+ return results;
4322
+ }
4323
+ function daemonStop(opts) {
4324
+ const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
4325
+ const pf = pidfilePath(baseDir, opts.machineKey);
4326
+ const data = readPidFile(pf);
4327
+ if (!data) {
4328
+ log.info("no daemon running for this machine key (pidfile not found)");
4329
+ return;
4330
+ }
4331
+ if (!isProcessAlive(data.pid)) {
4332
+ log.info(`stale pidfile (pid ${data.pid} is not running) — removing`);
4333
+ try {
4334
+ fs9.unlinkSync(pf);
4335
+ } catch {}
4336
+ return;
4337
+ }
4338
+ log.info(`sending SIGTERM to daemon (pid ${data.pid})…`);
4339
+ process.kill(data.pid, "SIGTERM");
4340
+ const deadline = Date.now() + 5000;
4341
+ while (Date.now() < deadline && isProcessAlive(data.pid)) {
4342
+ const start = Date.now();
4343
+ while (Date.now() - start < 100) {}
4344
+ }
4345
+ if (isProcessAlive(data.pid)) {
4346
+ log.error(`daemon (pid ${data.pid}) did not exit in 5s — sending SIGKILL`);
4347
+ process.kill(data.pid, "SIGKILL");
4348
+ } else {
4349
+ log.info("daemon stopped");
4350
+ }
4351
+ try {
4352
+ fs9.unlinkSync(pf);
4353
+ } catch {}
4354
+ }
4355
+ function credentialFilePathByMachineId(baseDir, machineId) {
4356
+ return path10.join(daemonsDir(baseDir), `${machineId}.credential.json`);
4357
+ }
4358
+ function credentialFilesDir(baseDir) {
4359
+ return daemonsDir(baseDir);
4360
+ }
4361
+ function readCredentialFile(filePath) {
4362
+ if (!fs9.existsSync(filePath))
4363
+ return null;
4364
+ try {
4365
+ const content = JSON.parse(fs9.readFileSync(filePath, "utf8"));
4366
+ if (typeof content.credential === "string" && content.credential.startsWith("cmk_") && typeof content.machineId === "string") {
4367
+ return { credential: content.credential, machineId: content.machineId };
4368
+ }
4369
+ } catch {}
4370
+ return null;
4371
+ }
4372
+ function writeCredentialFile(filePath, credential, machineId) {
4373
+ fs9.mkdirSync(path10.dirname(filePath), { recursive: true });
4374
+ fs9.writeFileSync(filePath, JSON.stringify({ credential, machineId }), { mode: 384 });
4375
+ }
4376
+ function findExistingCredentialForBearer(baseDir, bearer) {
4377
+ const dir = credentialFilesDir(baseDir);
4378
+ if (!fs9.existsSync(dir))
4379
+ return null;
4380
+ for (const file of fs9.readdirSync(dir)) {
4381
+ if (!file.endsWith(".credential.json"))
4382
+ continue;
4383
+ const parsed = readCredentialFile(path10.join(dir, file));
4384
+ if (parsed && parsed.credential === bearer)
4385
+ return parsed;
4386
+ }
4387
+ return null;
4388
+ }
4389
+ async function activatePairingToken(serverUrl, tokenId, hostname2, platform, arch, osRelease, daemonVersion, runtimeReport) {
4390
+ const res = await fetch(`${serverUrl}/api/community/daemon/activate`, {
4391
+ method: "POST",
4392
+ headers: {
4393
+ "content-type": "application/json",
4394
+ authorization: `Bearer ${tokenId}`
4395
+ },
4396
+ body: JSON.stringify({ hostname: hostname2, platform, arch, osRelease, daemonVersion, runtimeReport })
4397
+ });
4398
+ const json = await res.json().catch(() => ({}));
4399
+ if (!res.ok || !json.credential || !json.machineId) {
4400
+ throw new Error(json.error ?? `activate failed (${res.status})`);
4401
+ }
4402
+ return { credential: json.credential, machineId: json.machineId };
4403
+ }
4404
+ async function daemonStart(opts) {
4405
+ const serverUrl = opts.serverUrl || process.env.ALOOK_SERVER_URL;
4406
+ const wsUrl = opts.wsUrl || process.env.ALOOK_SERVER_WS_URL;
4407
+ if (!serverUrl) {
4408
+ log.error("Server URL required — pass --server-url or set ALOOK_SERVER_URL");
4409
+ process.exit(2);
4410
+ }
4411
+ if (!wsUrl) {
4412
+ log.error("WebSocket URL required — pass --ws-url or set ALOOK_SERVER_WS_URL");
4413
+ process.exit(2);
4414
+ }
4415
+ const baseDir = opts.baseDir || process.env.ALOOK_DATA_DIR || DEFAULT_BASE_DIR;
4416
+ const pf = acquireLock2(baseDir, opts.machineKey);
4417
+ const agentCliPath = resolveAlookCliPathWithFallback() ?? process.argv[1];
4418
+ const runtimeDetections = await detectRuntimes();
4419
+ const healthyRuntimeIds = runtimeDetections.filter((r) => r.status === "healthy").map((r) => r.id);
4420
+ log.info(healthyRuntimeIds.length === 0 ? "no agent CLIs detected" : `detected agent CLIs: ${healthyRuntimeIds.join(", ")}`);
4421
+ const unhealthyIds = runtimeDetections.filter((r) => r.status === "unhealthy").map((r) => `${r.id}(${r.lastError ?? "unknown"})`);
4422
+ if (unhealthyIds.length > 0)
4423
+ log.info(`unhealthy runtimes: ${unhealthyIds.join(", ")}`);
4424
+ const runtimeReport = runtimeDetections.map((r) => ({
4425
+ id: r.id,
4426
+ version: r.version,
4427
+ status: r.status,
4428
+ lastError: r.lastError,
4429
+ lastErrorAt: r.lastErrorAt
4430
+ }));
4431
+ let dialingCredential;
4432
+ if (opts.machineKey.startsWith("cmt_")) {
4433
+ log.info("activating pairing token…");
4434
+ try {
4435
+ const activated = await activatePairingToken(serverUrl, opts.machineKey, os3.hostname(), process.platform, process.arch, os3.release(), readDaemonVersion(), runtimeReport);
4436
+ dialingCredential = activated.credential;
4437
+ writeCredentialFile(credentialFilePathByMachineId(baseDir, activated.machineId), dialingCredential, activated.machineId);
4438
+ log.info("pairing token activated — credential persisted");
4439
+ } catch (err) {
4440
+ log.error(`activation failed: ${err instanceof Error ? err.message : String(err)}`);
4441
+ releaseLock2(pf);
4442
+ process.exit(1);
4443
+ }
4444
+ } else if (opts.machineKey.startsWith("cmk_")) {
4445
+ const match = findExistingCredentialForBearer(baseDir, opts.machineKey);
4446
+ if (match) {
4447
+ dialingCredential = match.credential;
4448
+ log.info("using persisted daemon credential");
4449
+ } else {
4450
+ dialingCredential = opts.machineKey;
4451
+ log.info("dialing with provided cmk_ (no on-disk record)");
4452
+ }
4453
+ } else {
4454
+ log.error("invalid machine key format — expected `cmt_` (pairing token) or `cmk_` (credential)");
4455
+ releaseLock2(pf);
4456
+ process.exit(2);
4457
+ }
4458
+ const daemon = await createDaemon({
4459
+ machineKey: dialingCredential,
4460
+ serverUrl,
4461
+ serverWsUrl: wsUrl,
4462
+ webSocketFactory: (url, headers) => new WebSocket(url, { headers }),
4463
+ runtimeReport,
4464
+ driverFor: (_agentId, runtimeConfig) => {
4465
+ const requested = runtimeConfig?.runtime;
4466
+ const known = runtimeReport.map((r) => r.id);
4467
+ if (!requested || !known.includes(requested)) {
4468
+ throw new UnknownRuntimeError(requested, healthyRuntimeIds);
4469
+ }
4470
+ return getDriver(requested);
4471
+ },
4472
+ capabilities: CAPABILITIES,
4473
+ agentCliPath,
4474
+ workingDirectoryBase: baseDir,
4475
+ hostname: os3.hostname(),
4476
+ platform: process.platform,
4477
+ arch: process.arch,
4478
+ osRelease: os3.release(),
4479
+ daemonVersion: readDaemonVersion(),
4480
+ logger: log,
4481
+ onAuthRejected: () => {
4482
+ log.error("machine key rejected by server — is it correct / has it expired?");
4483
+ releaseLock2(pf);
4484
+ process.exit(1);
4485
+ }
4486
+ });
4487
+ log.info(`daemon up — proxy at ${daemon.proxyUrl}, dialing ${wsUrl}`);
4488
+ const readyTimer = setInterval(() => {
4489
+ if (daemon.isOpen()) {
4490
+ clearInterval(readyTimer);
4491
+ log.info("control plane OPEN");
4492
+ }
4493
+ }, 200);
4494
+ readyTimer.unref?.();
4495
+ const shutdown = async () => {
4496
+ log.info("shutting down…");
4497
+ clearInterval(readyTimer);
4498
+ releaseLock2(pf);
4499
+ await daemon.stop();
4500
+ process.exit(0);
4501
+ };
4502
+ process.on("SIGINT", shutdown);
4503
+ process.on("SIGTERM", shutdown);
4504
+ await new Promise(() => {});
4505
+ }
4506
+
4507
+ // ../shared/src/lib/invite-link.ts
4508
+ var INVITE_URL_RE = /(?:https?:\/\/[^\s/]+)?\/community\/invite\/([A-Za-z0-9_-]{6,64})/;
4509
+ var BARE_TOKEN_RE = /^[A-Za-z0-9_-]{6,64}$/;
4510
+ function parseInviteToken(input) {
4511
+ const trimmed = input.trim();
4512
+ if (!trimmed)
4513
+ return null;
4514
+ const urlMatch = INVITE_URL_RE.exec(trimmed);
4515
+ if (urlMatch)
4516
+ return urlMatch[1];
4517
+ return BARE_TOKEN_RE.test(trimmed) ? trimmed : null;
4518
+ }
4519
+
4520
+ // src/cli/index.ts
4521
+ class CliError extends Error {
4522
+ }
4523
+ function printEnvelope(env) {
4524
+ const out = {};
4525
+ if (env.success !== undefined && env.success !== null)
4526
+ out.success = env.success;
4527
+ if (env.error !== undefined && env.error !== null)
4528
+ out.error = env.error;
4529
+ if (env.hint !== undefined && env.hint !== null)
4530
+ out.hint = env.hint;
4531
+ process.stdout.write(JSON.stringify(out) + `
4532
+ `);
4533
+ }
4534
+ var injectedApi = null;
4535
+ function setApiForTesting(api) {
4536
+ injectedApi = api;
4537
+ }
4538
+ function getApi() {
4539
+ if (injectedApi)
4540
+ return injectedApi;
4541
+ const fromEnv = proxyServerApiFromEnv();
4542
+ if (fromEnv)
4543
+ return fromEnv;
4544
+ throw new CliError("no ServerApi available — ALOOK_PROXY_URL + ALOOK_PROXY_TOKEN_FILE must be set");
4545
+ }
4546
+ function agentId(opts) {
4547
+ const id = opts.agent || process.env.ALOOK_AGENT_ID || process.env.ALOOK_ID;
4548
+ if (!id)
4549
+ throw new CliError("agent identity required — pass --agent <id> or set ALOOK_AGENT_ID");
4550
+ return id;
4551
+ }
4552
+ async function cmdMessageSend(opts) {
4553
+ const api = getApi();
4554
+ const agent = agentId(opts);
4555
+ const channel = opts.target;
4556
+ if (!channel)
4557
+ throw new CliError("message send: --target <ref> is required (e.g. /demo-workspace/general)");
4558
+ let text;
4559
+ const fileFlag = opts.file;
4560
+ const textFlag = opts.text;
4561
+ if (fileFlag) {
4562
+ const fs10 = await import("fs");
4563
+ if (!fs10.existsSync(fileFlag))
4564
+ throw new CliError(`message send: file not found: ${fileFlag}`);
4565
+ text = fs10.readFileSync(fileFlag, "utf8").trim();
4566
+ } else if (typeof textFlag === "string") {
4567
+ text = textFlag;
4568
+ }
4569
+ if (!text) {
4570
+ throw new CliError("message send: --text <text> or --file <path> is required");
4571
+ }
4572
+ const res = await api.send({ agentId: agent, channel, content: { text } });
4573
+ if (res.state === "blocked") {
4574
+ throw new CliError(`channel not aligned: ${res.unreadCount} unread message(s) in ${channel} (latest #${res.latestSeq}). Run \`alook inbox pull\` to align, then resend.`);
4575
+ }
4576
+ return { sent: `${res.message.channel}${res.message.seq}` };
4577
+ }
4578
+ async function cmdInboxPull(opts) {
4579
+ const api = getApi();
4580
+ const agent = agentId(opts);
4581
+ const max = opts.max ? Number(opts.max) : undefined;
4582
+ const { messages, hasMore } = await api.inboxPull({ agentId: agent, max });
4583
+ let acked = 0;
4584
+ if (opts.ack !== false && messages.length > 0) {
4585
+ const latest = new Map;
4586
+ for (const m of messages) {
4587
+ const seqN = Number(m.seq.replace("#", ""));
4588
+ const cur = latest.get(m.channel);
4589
+ if (!cur || seqN > cur.seq)
4590
+ latest.set(m.channel, { channel: m.channel, seq: seqN });
4591
+ }
4592
+ await api.ack({ agentId: agent, cursors: [...latest.values()] });
4593
+ acked = latest.size;
4594
+ }
4595
+ return { messages, hasMore, acked };
4596
+ }
4597
+ async function cmdServerList(opts) {
4598
+ const api = getApi();
4599
+ const agent = agentId(opts);
4600
+ const { servers } = await api.listServers({ agentId: agent });
4601
+ return { servers };
4602
+ }
4603
+ async function cmdServerMember(opts) {
4604
+ const api = getApi();
4605
+ const agent = agentId(opts);
4606
+ const server = opts.server;
4607
+ if (!server)
4608
+ throw new CliError("server member: --server <name> is required");
4609
+ const { members } = await api.listMembers({ agentId: agent, server });
4610
+ return { members };
4611
+ }
4612
+ async function cmdServerJoin(opts) {
4613
+ const api = getApi();
4614
+ const agent = agentId(opts);
4615
+ const raw = opts.invite;
4616
+ if (!raw)
4617
+ throw new CliError("server join: --invite <link> is required");
4618
+ const token = parseInviteToken(raw);
4619
+ if (!token)
4620
+ throw new CliError(`server join: could not find an invite token in "${raw}"`);
4621
+ const { server } = await api.joinServer({ agentId: agent, invite: token });
4622
+ return { server };
4623
+ }
4624
+ async function cmdChannelList(opts) {
4625
+ const api = getApi();
4626
+ const agent = agentId(opts);
4627
+ const server = opts.server;
4628
+ if (!server)
4629
+ throw new CliError("channel list: --server <id-or-name> is required");
4630
+ const { channels } = await api.listChannels({ agentId: agent, server });
4631
+ return { channels };
4632
+ }
4633
+ async function cmdChannelHistory(opts) {
4634
+ const api = getApi();
4635
+ const agent = agentId(opts);
4636
+ const channel = opts.channel;
4637
+ if (!channel)
4638
+ throw new CliError("channel history: --channel <ref> is required");
4639
+ const toSeq = (v) => v === undefined ? undefined : Number(v);
4640
+ const { items, hasMore, latestSeq } = await api.read({
4641
+ agentId: agent,
4642
+ channel,
4643
+ before: toSeq(opts.before),
4644
+ after: toSeq(opts.after),
4645
+ around: toSeq(opts.around),
4646
+ limit: toSeq(opts.limit)
4647
+ });
4648
+ return { items, hasMore, ...latestSeq !== undefined ? { latestSeq } : {} };
4649
+ }
4650
+ function buildProgram() {
4651
+ const program = new Command("alook").description("agent CLI").exitOverride().configureOutput({
4652
+ writeOut: () => {},
4653
+ writeErr: () => {}
4654
+ }).option("--agent <id>", "agent identity (or ALOOK_AGENT_ID env)");
4655
+ const message = program.command("message").description("message operations").exitOverride();
4656
+ message.configureOutput({ writeOut: () => {}, writeErr: () => {} });
4657
+ message.command("send").description("send a message to a channel, DM, or thread").option("--target <ref>", "destination (path-style ref, e.g. /demo-workspace/general)").option("--text <text>", "inline message body (short messages)").option("--file <path>", "read message body from a file (long messages)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4658
+ const localOpts = this.opts();
4659
+ const globalOpts = program.opts();
4660
+ const result = await cmdMessageSend({ ...globalOpts, ...localOpts });
4661
+ printEnvelope({ success: result });
4662
+ });
4663
+ const inbox = program.command("inbox").description("inbox operations").exitOverride();
4664
+ inbox.configureOutput({ writeOut: () => {}, writeErr: () => {} });
4665
+ inbox.command("pull").description("fetch unread messages from all channels").option("--max <n>", "max messages to return").option("--no-ack", "do not advance read waterlines (peek only)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4666
+ const localOpts = this.opts();
4667
+ const globalOpts = program.opts();
4668
+ const result = await cmdInboxPull({ ...globalOpts, ...localOpts });
4669
+ printEnvelope({ success: result });
4670
+ });
4671
+ const server = program.command("server").description("server operations").exitOverride();
4672
+ server.configureOutput({ writeOut: () => {}, writeErr: () => {} });
4673
+ server.command("list").description("list servers this agent is a member of").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4674
+ const localOpts = this.opts();
4675
+ const globalOpts = program.opts();
4676
+ const result = await cmdServerList({ ...globalOpts, ...localOpts });
4677
+ printEnvelope({ success: result });
4678
+ });
4679
+ server.command("member").description("list members of a server").option("--server <id-or-name>", "server id or name (from `server list`)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4680
+ const localOpts = this.opts();
4681
+ const globalOpts = program.opts();
4682
+ const result = await cmdServerMember({ ...globalOpts, ...localOpts });
4683
+ printEnvelope({ success: result });
4684
+ });
4685
+ server.command("join").description("join a server via an invite link or token").option("--invite <link>", "invite URL or bare token").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4686
+ const localOpts = this.opts();
4687
+ const globalOpts = program.opts();
4688
+ const result = await cmdServerJoin({ ...globalOpts, ...localOpts });
4689
+ printEnvelope({ success: result });
4690
+ });
4691
+ const channel = program.command("channel").description("channel operations").exitOverride();
4692
+ channel.configureOutput({ writeOut: () => {}, writeErr: () => {} });
4693
+ channel.command("list").description("list top-level channels visible to this agent in one server").option("--server <id-or-name>", "server id or name (from `server list`)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4694
+ const localOpts = this.opts();
4695
+ const globalOpts = program.opts();
4696
+ const result = await cmdChannelList({ ...globalOpts, ...localOpts });
4697
+ printEnvelope({ success: result });
4698
+ });
4699
+ channel.command("history").description("fetch a page of messages from a channel, thread, or DM").option("--channel <ref>", "channel/thread/DM ref (path-style)").option("--before <seq>", "messages before this seq").option("--after <seq>", "messages after this seq").option("--around <seq>", "messages around this seq").option("--limit <n>", "max messages to return").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4700
+ const localOpts = this.opts();
4701
+ const globalOpts = program.opts();
4702
+ const result = await cmdChannelHistory({ ...globalOpts, ...localOpts });
4703
+ printEnvelope({ success: result });
4704
+ });
4705
+ const daemon = program.command("daemon").description("daemon operations").exitOverride();
4706
+ daemon.configureOutput({ writeOut: () => {}, writeErr: () => {} });
4707
+ daemon.command("start").description("start the daemon (connects to server, manages agent lifecycles)").requiredOption("--machine-key <key>", "machine key for server authentication").option("--server-url <url>", "server HTTP URL (or ALOOK_SERVER_URL env)").option("--ws-url <url>", "server WebSocket URL (or ALOOK_SERVER_WS_URL env)").option("--base-dir <path>", "data directory for agent workspaces and pidfile (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(async function() {
4708
+ const localOpts = this.opts();
4709
+ await daemonStart({
4710
+ machineKey: localOpts.machineKey,
4711
+ serverUrl: localOpts.serverUrl,
4712
+ wsUrl: localOpts.wsUrl,
4713
+ baseDir: localOpts.baseDir
4714
+ });
4715
+ });
4716
+ daemon.command("stop").description("stop the daemon for a specific machine key").requiredOption("--machine-key <key>", "machine key identifying which daemon to stop").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(function() {
4717
+ const localOpts = this.opts();
4718
+ daemonStop({
4719
+ machineKey: localOpts.machineKey,
4720
+ baseDir: localOpts.baseDir
4721
+ });
4722
+ });
4723
+ daemon.command("list").description("list running daemons on this machine").option("--base-dir <path>", "data directory (or ALOOK_DATA_DIR env)").exitOverride().configureOutput({ writeOut: () => {}, writeErr: () => {} }).action(function() {
4724
+ const localOpts = this.opts();
4725
+ const daemons = daemonList({ baseDir: localOpts.baseDir });
4726
+ printEnvelope({ success: { daemons } });
4727
+ });
4728
+ return program;
4729
+ }
4730
+ async function main(argv = process.argv.slice(2)) {
4731
+ const program = buildProgram();
4732
+ try {
4733
+ await program.parseAsync(argv, { from: "user" });
4734
+ } catch (err) {
4735
+ if (err instanceof CommanderError) {
4736
+ if (err.code === "commander.helpDisplayed" || err.code === "commander.help") {
4737
+ const helpText = getHelpText(program, argv);
4738
+ printEnvelope({ success: { usage: helpText } });
4739
+ } else if (err.code === "commander.unknownCommand") {
4740
+ printEnvelope({ error: `unknown command: ${argv.join(" ") || "(none)"}. Run \`alook help\`.` });
4741
+ } else {
4742
+ printEnvelope({ error: err.message });
4743
+ }
4744
+ } else if (err instanceof CliError) {
4745
+ printEnvelope({ error: err.message, hint: err.hint });
4746
+ } else {
4747
+ printEnvelope({ error: err.message, hint: err.hint });
4748
+ }
4749
+ }
4750
+ return 0;
4751
+ }
4752
+ function getHelpText(program, argv) {
4753
+ const args = argv.filter((a) => a !== "-h" && a !== "--help");
4754
+ let cmd = program;
4755
+ for (const arg of args) {
4756
+ if (arg.startsWith("-"))
4757
+ continue;
4758
+ const sub = cmd.commands.find((c) => c.name() === arg);
4759
+ if (sub)
4760
+ cmd = sub;
4761
+ else
4762
+ break;
4763
+ }
4764
+ return cmd.helpInformation();
4765
+ }
4766
+ var invokedDirectly = typeof process !== "undefined" && process.argv[1] && /(?:^|[\\/])(?:cli[\\/]index\.[jt]s|alook)$/.test(process.argv[1]) && !process.argv[1].includes("vitest") && !process.argv[1].includes("node_modules");
4767
+ if (invokedDirectly) {
4768
+ main().then((code) => process.exit(code));
4769
+ }
4770
+ export {
4771
+ setApiForTesting,
4772
+ main
4773
+ };