coderemote 0.3.9 → 0.3.10

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.
Files changed (3) hide show
  1. package/README.md +38 -3
  2. package/dist/coderemote.js +1894 -687
  3. package/package.json +1 -1
@@ -1,22 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/coderemote.js
4
- import { execFile as execFile11, execFileSync, spawn as spawn5 } from "node:child_process";
5
- import { randomBytes as randomBytes4 } from "node:crypto";
6
- import fs19 from "node:fs";
4
+ import { execFile as execFile11, execFileSync, spawn as spawn6 } from "node:child_process";
5
+ import { randomBytes as randomBytes6 } from "node:crypto";
6
+ import fs22 from "node:fs";
7
7
  import net3 from "node:net";
8
- import os7 from "node:os";
9
- import path16 from "node:path";
8
+ import os9 from "node:os";
9
+ import path19 from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { promisify as promisify4 } from "node:util";
12
12
  import qrcode from "qrcode-terminal";
13
13
 
14
14
  // ../../packages/agent-core/src/server.js
15
15
  import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "node:fs";
16
- import fs13 from "node:fs/promises";
16
+ import fs15 from "node:fs/promises";
17
17
  import http3 from "node:http";
18
- import os6 from "node:os";
19
- import path10 from "node:path";
18
+ import os8 from "node:os";
19
+ import path12 from "node:path";
20
+ import tls from "node:tls";
20
21
  import { pipeline as pipeline3 } from "node:stream/promises";
21
22
  import { randomUUID as randomUUID5, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
22
23
 
@@ -44,10 +45,20 @@ var Routes = Object.freeze({
44
45
  gitUnstage: "POST /api/git/unstage",
45
46
  gitDiscard: "POST /api/git/discard",
46
47
  gitCommit: "POST /api/git/commit",
48
+ // Added within v4: older agents answer 404.
49
+ gitCommitMessage: "POST /api/git/commit-message",
47
50
  gitLog: "GET /api/git/log",
48
51
  gitShow: "GET /api/git/show",
49
52
  gitPull: "POST /api/git/pull",
50
53
  gitPush: "POST /api/git/push",
54
+ // Added within v4 after the commit-message route: older agents answer 404.
55
+ gitFetch: "POST /api/git/fetch",
56
+ gitBranches: "GET /api/git/branches",
57
+ gitCheckout: "POST /api/git/checkout",
58
+ gitStashes: "GET /api/git/stashes",
59
+ gitStash: "POST /api/git/stash",
60
+ gitStashApply: "POST /api/git/stash/apply",
61
+ gitStashDrop: "POST /api/git/stash/drop",
51
62
  portsList: "GET /api/ports",
52
63
  portsKill: "POST /api/ports/kill",
53
64
  previewList: "GET /api/preview",
@@ -80,6 +91,8 @@ var Routes = Object.freeze({
80
91
  agentsStop: "POST /api/agents/stop",
81
92
  agentsForget: "POST /api/agents/forget",
82
93
  agentsUpload: "POST /api/agents/uploads",
94
+ // Added within v4: older agents answer 404.
95
+ agentsUploadFile: "GET /api/agents/uploads",
83
96
  pushDeviceSet: "PUT /api/push/device",
84
97
  pushDeviceDelete: "DELETE /api/push/device",
85
98
  pushTest: "POST /api/push/test"
@@ -87,6 +100,7 @@ var Routes = Object.freeze({
87
100
  var TUNNEL_HANDOFF_PATH = "/vsremote/handoff";
88
101
  var TUNNEL_AUTH_CALLBACK_HOST = "tunnel-auth";
89
102
  var TERMINAL_WS_PATH = "/api/term/ws";
103
+ var LAN_SERVICE_TYPE = "_coderemote._tcp";
90
104
  var TerminalServerMessage = Object.freeze({ session: "session", exit: "exit" });
91
105
  var TerminalClientMessage = Object.freeze({ input: "input", resize: "resize" });
92
106
  var AGENTS_WS_PATH = "/api/agents/ws";
@@ -104,17 +118,28 @@ var AgentsClientMessage = Object.freeze({ presence: "presence" });
104
118
  function webUrl({ url, token }) {
105
119
  return `${url}/#token=${encodeURIComponent(token)}`;
106
120
  }
107
- function pairingUrl({ url, token }) {
108
- return `${PAIRING_SCHEME}://pair?url=${encodeURIComponent(url)}&token=${encodeURIComponent(token)}`;
121
+ function pairingUrl({ url = null, token, agentId = null, lan = null }) {
122
+ if (!url && !lan) throw new Error("a pairing link needs a tunnel URL or the LAN listener");
123
+ if (lan && !agentId) throw new Error("a pairing link with the LAN listener needs the agent id");
124
+ const q = new URLSearchParams();
125
+ if (url) q.set("url", url);
126
+ q.set("token", token);
127
+ if (agentId) q.set("id", agentId);
128
+ if (lan) {
129
+ q.set("lan", String(lan.port));
130
+ q.set("addr", lan.addresses.join(","));
131
+ q.set("fp", lan.fingerprint);
132
+ }
133
+ return `${PAIRING_SCHEME}://pair?${q.toString().replace(/\+/g, "%20")}`;
109
134
  }
110
135
 
111
136
  // ../../packages/agent-core/src/server.js
112
137
  import { WebSocketServer } from "ws";
113
138
 
114
139
  // ../../packages/agent-core/src/agents.js
115
- import { randomUUID } from "node:crypto";
140
+ import { randomUUID as randomUUID2 } from "node:crypto";
116
141
  import { EventEmitter } from "node:events";
117
- import path2 from "node:path";
142
+ import path3 from "node:path";
118
143
 
119
144
  // ../../packages/agent-core/src/sandbox.js
120
145
  import fsSync from "node:fs";
@@ -264,6 +289,10 @@ function isTyped(text) {
264
289
  return t && !t.startsWith("<command-") && !t.startsWith("<local-command") && !t.startsWith("[Request interrupted");
265
290
  }
266
291
  function* entriesOf(record) {
292
+ if (record?.type === "ai-title" && typeof record.aiTitle === "string" && record.aiTitle.trim()) {
293
+ yield { time: 0, kind: "title", text: record.aiTitle.trim() };
294
+ return;
295
+ }
267
296
  if (!record || record.isSidechain || record.isMeta) return;
268
297
  const time2 = Date.parse(record.timestamp) || 0;
269
298
  const content = record.message?.content;
@@ -279,20 +308,38 @@ function* entriesOf(record) {
279
308
  }
280
309
  }
281
310
  const text = textOf(content);
282
- if (isTyped(text) && content.every((c) => c?.type !== "tool_result")) yield { time: time2, kind: "prompt", text };
311
+ if (content.some((c) => c?.type === "tool_result")) return;
312
+ const images = content.filter((c) => c?.type === "image" && c.source?.type === "base64" && typeof c.source.data === "string").map((c) => ({ type: c.source.media_type ?? "image/png", data: c.source.data }));
313
+ if (isTyped(text) || images.length && !text.trim()) yield { time: time2, kind: "prompt", text, ...images.length ? { images } : {} };
283
314
  } else if (record.type === "assistant" && Array.isArray(content)) {
315
+ const model = record.message?.model;
316
+ if (typeof model === "string" && model && model !== "<synthetic>") yield { time: time2, kind: "model", model };
284
317
  for (const c of content) {
285
318
  if (c?.type === "text" && c.text?.trim()) yield { time: time2, kind: "assistant", text: c.text.trimEnd() };
319
+ else if (c?.type === "thinking" && c.thinking?.trim()) yield { time: time2, kind: "assistant", text: c.thinking.trimEnd(), thinking: true };
286
320
  else if (c?.type === "tool_use") yield { time: time2, kind: "tool", toolUseId: c.id ?? null, name: c.name ?? null, input: c.input ?? null };
287
321
  }
288
322
  }
289
323
  }
324
+ function parse(text) {
325
+ const entries2 = [];
326
+ for (const line of text.split("\n")) {
327
+ if (!line.trim()) continue;
328
+ try {
329
+ entries2.push(...entriesOf(JSON.parse(line)));
330
+ } catch {
331
+ }
332
+ }
333
+ return entries2;
334
+ }
290
335
  async function readTranscript(file, limit = 500) {
291
336
  let text;
337
+ let end = 0;
292
338
  try {
293
339
  const handle = await fs3.open(file, "r");
294
340
  try {
295
341
  const { size } = await handle.stat();
342
+ end = size;
296
343
  const start2 = Math.max(0, size - TAIL_BYTES);
297
344
  const buf = Buffer.alloc(size - start2);
298
345
  await handle.read(buf, 0, buf.length, start2);
@@ -304,19 +351,179 @@ async function readTranscript(file, limit = 500) {
304
351
  } catch {
305
352
  return [];
306
353
  }
307
- const entries2 = [];
354
+ const all = parse(text);
355
+ const title = all.findLast((e) => e.kind === "title");
356
+ const model = all.findLast((e) => e.kind === "model");
357
+ const entries2 = all.filter((e) => e.kind !== "title" && e.kind !== "model").slice(-limit);
358
+ if (model) entries2.unshift(model);
359
+ if (title) entries2.unshift(title);
360
+ entries2.end = end;
361
+ return entries2;
362
+ }
363
+ async function readTranscriptSince(file, offset) {
364
+ try {
365
+ const handle = await fs3.open(file, "r");
366
+ try {
367
+ const { size } = await handle.stat();
368
+ if (size < offset) return { entries: [], end: size };
369
+ const buf = Buffer.alloc(Math.min(size - offset, TAIL_BYTES));
370
+ await handle.read(buf, 0, buf.length, offset);
371
+ const last = buf.lastIndexOf(10);
372
+ if (last < 0) return { entries: [], end: offset };
373
+ return { entries: parse(buf.subarray(0, last + 1).toString("utf8")), end: offset + last + 1 };
374
+ } finally {
375
+ await handle.close();
376
+ }
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+
382
+ // ../../packages/agent-core/src/uploads.js
383
+ import { execFile } from "node:child_process";
384
+ import { createHash, randomUUID } from "node:crypto";
385
+ import { createWriteStream } from "node:fs";
386
+ import fs4 from "node:fs/promises";
387
+ import os from "node:os";
388
+ import path2 from "node:path";
389
+ import { Transform } from "node:stream";
390
+ import { pipeline } from "node:stream/promises";
391
+ import { promisify } from "node:util";
392
+ var run = promisify(execFile);
393
+ var UPLOAD_LIMIT = 100 * 1024 * 1024;
394
+ var KEEP = 7 * 24 * 60 * 60 * 1e3;
395
+ var ID = /^[0-9a-f-]{36}$/;
396
+ var IMAGE_EXTENSIONS = { "image/png": ".png", "image/jpeg": ".jpg", "image/gif": ".gif", "image/webp": ".webp" };
397
+ function imageType(file) {
398
+ const ext = path2.extname(String(file)).toLowerCase();
399
+ if (ext === ".jpeg") return "image/jpeg";
400
+ return Object.keys(IMAGE_EXTENSIONS).find((t) => IMAGE_EXTENSIONS[t] === ext) ?? null;
401
+ }
402
+ function attachedFiles(text, dir, typeOf = () => "application/octet-stream") {
403
+ if (!dir || !text.includes(dir)) return { text, files: [] };
404
+ const root2 = dir.endsWith(path2.sep) ? dir : dir + path2.sep;
405
+ const files = [];
406
+ const kept = [];
308
407
  for (const line of text.split("\n")) {
309
- if (!line.trim()) continue;
408
+ const rest2 = line.trim().startsWith(root2) ? line.trim().slice(root2.length) : null;
409
+ const m = rest2 && /^\d{4}-\d{2}-\d{2}[\\/]([0-9a-f-]{36})-(.+)$/.exec(rest2);
410
+ if (m) files.push({ id: m[1], name: m[2], type: typeOf(m[2]) });
411
+ else kept.push(line);
412
+ }
413
+ return files.length ? { text: kept.join("\n").trim(), files } : { text, files };
414
+ }
415
+ function safeName(name) {
416
+ const base2 = path2.basename(String(name ?? "").replace(/\\/g, "/"));
417
+ const clean = base2.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
418
+ return clean || "file";
419
+ }
420
+ var UploadStore = class {
421
+ constructor(dir, { now = Date.now } = {}) {
422
+ this.dir = dir;
423
+ this.now = now;
424
+ }
425
+ /** Streams `body` to disk. 413 past `limit`; HEIC photos become JPEG on macOS. */
426
+ async save(body, { name, type, length, limit = UPLOAD_LIMIT }) {
427
+ if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
428
+ const id = randomUUID();
429
+ const day = new Date(this.now()).toISOString().slice(0, 10);
430
+ await fs4.mkdir(path2.join(this.dir, day), { recursive: true, mode: 448 });
431
+ let file = path2.join(this.dir, day, `${id}-${safeName(name)}`);
432
+ let size = 0;
433
+ const counter = new Transform({
434
+ transform(chunk, _enc, done) {
435
+ size += chunk.length;
436
+ done(size > limit ? new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`) : null, chunk);
437
+ }
438
+ });
310
439
  try {
311
- entries2.push(...entriesOf(JSON.parse(line)));
440
+ await pipeline(body, counter, createWriteStream(file, { mode: 384 }));
441
+ } catch (e) {
442
+ await fs4.rm(file, { force: true });
443
+ throw e;
444
+ }
445
+ let kind = type || "application/octet-stream";
446
+ if ((kind === "image/heic" || /\.heic$/i.test(file)) && os.platform() === "darwin") {
447
+ const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
448
+ try {
449
+ await run("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
450
+ await fs4.rm(file);
451
+ file = jpeg;
452
+ kind = "image/jpeg";
453
+ size = (await fs4.stat(file)).size;
454
+ } catch {
455
+ }
456
+ }
457
+ return { id, name: path2.basename(file).slice(id.length + 1), type: kind, size, path: file };
458
+ }
459
+ /** The file of an upload; 404 once it's gone (swept, or never existed). */
460
+ async path(id) {
461
+ if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
462
+ let days = [];
463
+ try {
464
+ days = (await fs4.readdir(this.dir)).sort().reverse();
465
+ } catch {
466
+ }
467
+ for (const day of days) {
468
+ let names = [];
469
+ try {
470
+ names = await fs4.readdir(path2.join(this.dir, day));
471
+ } catch {
472
+ }
473
+ const hit = names.find((n) => n.startsWith(`${id}-`));
474
+ if (hit) return path2.join(this.dir, day, hit);
475
+ }
476
+ throw new HttpError(404, "no such upload (they are kept for 7 days)");
477
+ }
478
+ /**
479
+ * Keeps bytes the agent found itself (an image in a transcript) as an upload, named by their
480
+ * content so the same image read again is the same upload. Resolves `{ id, type }`.
481
+ */
482
+ async keep(bytes2, type) {
483
+ const hash = createHash("sha256").update(bytes2).digest("hex");
484
+ const id = `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
485
+ const existing = await this.path(id).catch(() => null);
486
+ if (existing) {
487
+ const now = new Date(this.now());
488
+ await fs4.utimes(existing, now, now).catch(() => {
489
+ });
490
+ return { id, type };
491
+ }
492
+ const day = new Date(this.now()).toISOString().slice(0, 10);
493
+ await fs4.mkdir(path2.join(this.dir, day), { recursive: true, mode: 448 });
494
+ await fs4.writeFile(path2.join(this.dir, day, `${id}-image${IMAGE_EXTENSIONS[type] ?? ""}`), bytes2, { mode: 384 });
495
+ return { id, type };
496
+ }
497
+ /** Deletes uploads older than a week. */
498
+ async sweep() {
499
+ let days = [];
500
+ try {
501
+ days = await fs4.readdir(this.dir);
312
502
  } catch {
503
+ return;
504
+ }
505
+ const cutoff = this.now() - KEEP;
506
+ for (const day of days) {
507
+ const dir = path2.join(this.dir, day);
508
+ let names = [];
509
+ try {
510
+ names = await fs4.readdir(dir);
511
+ } catch {
512
+ continue;
513
+ }
514
+ for (const n of names) {
515
+ const file = path2.join(dir, n);
516
+ const stat = await fs4.stat(file).catch(() => null);
517
+ if (stat && stat.mtimeMs < cutoff) await fs4.rm(file, { force: true });
518
+ }
519
+ if (!(await fs4.readdir(dir).catch(() => [])).length) await fs4.rm(dir, { recursive: true, force: true });
313
520
  }
314
521
  }
315
- return entries2.slice(-limit);
316
- }
522
+ };
317
523
 
318
524
  // ../../packages/agent-core/src/agents.js
319
525
  var MAX_ENTRIES = 500;
526
+ var SAVED_ENTRIES = 200;
320
527
  var MAX_TEXT = 16 * 1024;
321
528
  var MAX_TITLE = 80;
322
529
  var ENDED_TTL = 24 * 60 * 60 * 1e3;
@@ -327,7 +534,7 @@ var SEQUENTIAL_REQUEST_TTL = 10 * 60 * 1e3;
327
534
  var ADAPTERS = {
328
535
  claude: {
329
536
  name: "Claude Code",
330
- capabilities: { approvals: "parallel", alwaysAllow: true, keys: ["interrupt", "cycle_mode", "/compact", "/clear", "/model"] },
537
+ capabilities: { approvals: "parallel", alwaysAllow: true, keys: ["interrupt", "cycle_mode", "set_mode", "/compact", "/clear", "/model"] },
331
538
  normalize: (e) => e
332
539
  },
333
540
  codex: {
@@ -367,6 +574,33 @@ function resultText(response) {
367
574
  }
368
575
  return JSON.stringify(response, null, 2);
369
576
  }
577
+ function isNotice(prompt) {
578
+ return prompt.trimStart().startsWith("<task-notification>");
579
+ }
580
+ var PERMISSION_MODES = ["manual", "acceptEdits", "plan", "auto", "bypassPermissions", "dontAsk"];
581
+ function modeOf(value) {
582
+ if (value === "default") return "manual";
583
+ return PERMISSION_MODES.includes(value) ? value : null;
584
+ }
585
+ var FOOTER_MODES = {
586
+ "manual mode": "manual",
587
+ "accept edits": "acceptEdits",
588
+ "plan mode": "plan",
589
+ "auto mode": "auto",
590
+ "bypass permissions": "bypassPermissions",
591
+ "don't ask": "dontAsk"
592
+ };
593
+ var FOOTER = /(?:⏸|⏵⏵) *(manual mode|accept edits|plan mode|auto mode|bypass permissions|don't ask) on/g;
594
+ var CURSOR_RIGHT = /\x1b\[(\d*)C/g;
595
+ var ANSI = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\[[0-9;?<>=]*[ -/]*[@-~]|\x1b[()][0-9A-B]|\x1b./g;
596
+ function plain(text) {
597
+ return text.replace(CURSOR_RIGHT, (_, n) => " ".repeat(Math.min(Number(n) || 1, 200))).replace(ANSI, "");
598
+ }
599
+ function footerMode(text) {
600
+ let mode = null;
601
+ for (const m of plain(text).matchAll(FOOTER)) mode = FOOTER_MODES[m[1]];
602
+ return mode;
603
+ }
370
604
  function titleOf(prompt) {
371
605
  const line = prompt.trim().split("\n")[0].trim();
372
606
  return line.length > MAX_TITLE ? `${line.slice(0, MAX_TITLE - 1)}\u2026` : line;
@@ -376,17 +610,17 @@ var AgentSession = class {
376
610
  this.id = id;
377
611
  this.agent = agent;
378
612
  this.cwd = cwd;
379
- this.title = path2.basename(cwd || "") || "Session";
613
+ this.title = path3.basename(cwd || "") || "Session";
380
614
  this.capabilities = (ADAPTERS[agent] ?? ADAPTERS.claude).capabilities;
381
615
  this.titled = false;
382
616
  this.state = "idle";
383
617
  this.terminal = terminal;
384
- this.app = null;
385
618
  this.startedAt = now;
386
619
  this.lastActivityAt = now;
387
620
  this.endedAt = null;
388
621
  this.starting = false;
389
622
  this.error = null;
623
+ this.cleared = false;
390
624
  this.turnStartedAt = null;
391
625
  this.entries = [];
392
626
  this.seq = 0;
@@ -395,6 +629,8 @@ var AgentSession = class {
395
629
  this.prompts = [];
396
630
  this.tail = null;
397
631
  this.ready = Promise.resolve();
632
+ this.permissionMode = null;
633
+ this.model = null;
398
634
  }
399
635
  toJSON() {
400
636
  return {
@@ -409,7 +645,9 @@ var AgentSession = class {
409
645
  pending: this.requests.size,
410
646
  starting: this.starting,
411
647
  error: this.error,
412
- capabilities: this.capabilities
648
+ capabilities: this.capabilities,
649
+ permissionMode: this.permissionMode,
650
+ model: this.model
413
651
  };
414
652
  }
415
653
  };
@@ -419,9 +657,14 @@ var AgentHub = class extends EventEmitter {
419
657
  #now;
420
658
  #requestTtl;
421
659
  #transcript;
660
+ #transcriptSince;
422
661
  #startTimeout;
423
662
  #sequentialTtl;
424
663
  #rolloutTail;
664
+ // Terminal id -> the tail of its output, for a footer split across chunks.
665
+ #footers = /* @__PURE__ */ new Map();
666
+ // Session id -> callbacks waiting for the footer to show a mode.
667
+ #modeWaiters = /* @__PURE__ */ new Map();
425
668
  /**
426
669
  * `present` says whether someone is at the computer or looking at it in a client; agents whose
427
670
  * approvals are sequential ask there instead of holding the request for the phone.
@@ -431,28 +674,39 @@ var AgentHub = class extends EventEmitter {
431
674
  requestTtl = REQUEST_TTL,
432
675
  sequentialTtl = SEQUENTIAL_REQUEST_TTL,
433
676
  transcript = readTranscript,
677
+ transcriptSince = readTranscriptSince,
434
678
  startTimeout = START_TIMEOUT,
435
679
  present = async () => false,
436
- rolloutTail = (file, onItem, onTurnEnd) => new RolloutTail(file, onItem, onTurnEnd)
680
+ rolloutTail = (file, onItem, onTurnEnd) => new RolloutTail(file, onItem, onTurnEnd),
681
+ // Keeps an image from a transcript where clients can fetch it: (bytes, type) => { id, type }.
682
+ keepImage = null,
683
+ // Where files sent from clients are kept, and the media type of one by its name: prompts
684
+ // show the files they name there as attachments instead of paths.
685
+ uploadsDir = null,
686
+ fileType = void 0
437
687
  } = {}) {
438
688
  super();
439
689
  this.#now = now;
440
690
  this.#requestTtl = requestTtl;
441
691
  this.#sequentialTtl = sequentialTtl;
442
692
  this.#transcript = transcript;
693
+ this.#transcriptSince = transcriptSince;
443
694
  this.#startTimeout = startTimeout;
444
695
  this.present = present;
445
696
  this.#rolloutTail = rolloutTail;
697
+ this.keepImage = keepImage;
698
+ this.uploadsDir = uploadsDir;
699
+ this.fileType = fileType;
446
700
  this.presenceSet = arguments[0]?.present !== void 0;
447
701
  }
448
702
  /**
449
703
  * A prompt a client typed into a session's terminal. Once Claude Code reports a matching
450
704
  * `UserPromptSubmit`, the socket gets `prompt_confirmed` with this id.
451
705
  */
452
- prompted(sessionId, { id, text, paths = [] }) {
706
+ prompted(sessionId, { id, text, paths = [], images = [] }) {
453
707
  const s = this.#sessions.get(sessionId);
454
708
  if (!s) return;
455
- s.prompts.push({ id, text: comparable(text, paths), at: this.#now() });
709
+ s.prompts.push({ id, text: comparable(text, paths), images, at: this.#now() });
456
710
  s.prompts = s.prompts.filter((p) => this.#now() - p.at < 6e4).slice(-20);
457
711
  }
458
712
  /**
@@ -466,6 +720,59 @@ var AgentHub = class extends EventEmitter {
466
720
  this.#endTurn(s, this.#now());
467
721
  this.#emitSession(s);
468
722
  }
723
+ /**
724
+ * Output of one of the agent's terminals. Shift+Tab reports no hook, so a Claude Code session's
725
+ * permission mode follows its footer.
726
+ */
727
+ terminalOutput(terminal, buf) {
728
+ const s = [...this.#sessions.values()].find((x) => x.terminal === terminal && x.agent === "claude" && x.state !== "ended");
729
+ if (!s) {
730
+ this.#footers.delete(terminal);
731
+ return;
732
+ }
733
+ const text = (this.#footers.get(terminal) ?? "") + buf.toString("utf8");
734
+ this.#footers.set(terminal, text.slice(-256));
735
+ const mode = footerMode(text);
736
+ if (!mode) return;
737
+ const waiters = this.#modeWaiters.get(s.id) ?? [];
738
+ const left = waiters.filter((w) => w.from === mode);
739
+ if (left.length) this.#modeWaiters.set(s.id, left);
740
+ else this.#modeWaiters.delete(s.id);
741
+ for (const w of waiters) if (w.from !== mode) w.done(mode);
742
+ if (mode !== s.permissionMode) {
743
+ s.permissionMode = mode;
744
+ this.#emitSession(s);
745
+ }
746
+ }
747
+ /**
748
+ * The mode the footer shows next that isn't `from` (a running session redraws its footer
749
+ * unchanged all the time), or null if none shows within `ms`.
750
+ */
751
+ waitForMode(sessionId, ms, from = this.#sessions.get(sessionId)?.permissionMode ?? null) {
752
+ return new Promise((resolve) => {
753
+ const waiter = {
754
+ from,
755
+ done: (mode) => {
756
+ clearTimeout(timer);
757
+ resolve(mode);
758
+ }
759
+ };
760
+ const timer = setTimeout(() => {
761
+ const left = (this.#modeWaiters.get(sessionId) ?? []).filter((w) => w !== waiter);
762
+ if (left.length) this.#modeWaiters.set(sessionId, left);
763
+ else this.#modeWaiters.delete(sessionId);
764
+ resolve(null);
765
+ }, ms);
766
+ this.#modeWaiters.set(sessionId, [...this.#modeWaiters.get(sessionId) ?? [], waiter]);
767
+ });
768
+ }
769
+ /** The model a client switched a session to with `/model`, until its transcript names it. */
770
+ modelChosen(sessionId, model) {
771
+ const s = this.#sessions.get(sessionId);
772
+ if (!s || !model || model === s.model) return;
773
+ s.model = model;
774
+ this.#emitSession(s);
775
+ }
469
776
  /** Whether `resume` may reopen a session: only one no process runs any more. */
470
777
  canResume(id) {
471
778
  const s = this.#sessions.get(id);
@@ -510,10 +817,10 @@ var AgentHub = class extends EventEmitter {
510
817
  session(id) {
511
818
  return this.#sessions.get(id)?.toJSON() ?? null;
512
819
  }
513
- /** Where a session runs: one of the agent's terminals, or an app on the computer (or neither known). */
820
+ /** Where a session runs: `terminal` is one of the agent's terminals, null for one started outside Code Remote. */
514
821
  host(id) {
515
822
  const s = this.#sessions.get(id);
516
- return s ? { terminal: s.terminal, app: s.app } : null;
823
+ return s ? { terminal: s.terminal } : null;
517
824
  }
518
825
  timeline(id, after = 0) {
519
826
  const s = this.#sessions.get(id);
@@ -521,12 +828,70 @@ var AgentHub = class extends EventEmitter {
521
828
  const entries2 = s.entries.filter((e) => e.seq > after);
522
829
  return { entries: entries2, next: s.seq, requests: [...s.requests.values()].map((r) => r.request) };
523
830
  }
831
+ /** Settles once a session's history (read from its transcript) is in. */
832
+ async ready(id) {
833
+ await this.#sessions.get(id)?.ready;
834
+ }
835
+ /**
836
+ * The sessions as `restore` takes them back after the agent restarts. Claude Code's transcript
837
+ * holds a session's whole timeline, so its entries are read again from there (with whatever
838
+ * happened while the agent was down); others keep their latest entries.
839
+ */
840
+ snapshot() {
841
+ return [...this.#sessions.values()].filter((s) => !s.starting).map((s) => ({
842
+ id: s.id,
843
+ agent: s.agent,
844
+ cwd: s.cwd,
845
+ title: s.title,
846
+ titled: s.titled,
847
+ state: s.state,
848
+ terminal: s.terminal,
849
+ startedAt: s.startedAt,
850
+ lastActivityAt: s.lastActivityAt,
851
+ endedAt: s.endedAt,
852
+ error: s.error,
853
+ cleared: s.cleared,
854
+ transcriptPath: s.transcriptPath ?? null,
855
+ permissionMode: s.permissionMode,
856
+ model: s.model,
857
+ seq: s.seq,
858
+ entries: s.agent === "claude" && s.transcriptPath ? [] : s.entries.slice(-SAVED_ENTRIES)
859
+ }));
860
+ }
861
+ /**
862
+ * Brings back the sessions of `snapshot`. Those that ran in the agent's terminals ended with
863
+ * them; others may still run, and their next hook event says how they are doing.
864
+ */
865
+ restore(saved) {
866
+ const now = this.#now();
867
+ for (const r of Array.isArray(saved) ? saved : []) {
868
+ if (!r || typeof r.id !== "string" || this.#sessions.has(r.id) || !Object.hasOwn(ADAPTERS, r.agent)) continue;
869
+ if (r.state === "ended" && !(r.endedAt >= now - ENDED_TTL)) continue;
870
+ const s = new AgentSession({ id: r.id, agent: r.agent, cwd: r.cwd ?? null, terminal: null, now: r.startedAt ?? now });
871
+ s.title = typeof r.title === "string" ? r.title : s.title;
872
+ s.titled = r.titled === true;
873
+ s.lastActivityAt = r.lastActivityAt ?? now;
874
+ s.error = r.error ?? null;
875
+ s.cleared = r.cleared === true;
876
+ s.permissionMode = modeOf(r.permissionMode);
877
+ s.model = typeof r.model === "string" ? r.model : null;
878
+ s.seq = Number.isInteger(r.seq) ? r.seq : 0;
879
+ if (r.state === "ended" || r.terminal) {
880
+ s.state = "ended";
881
+ s.endedAt = r.state === "ended" ? r.endedAt ?? now : now;
882
+ }
883
+ if (typeof r.transcriptPath === "string") s.transcriptPath = r.transcriptPath;
884
+ if (s.agent === "claude" && s.transcriptPath) s.ready = this.#backfill(s, s.transcriptPath);
885
+ else s.entries = Array.isArray(r.entries) ? r.entries.filter((e) => Number.isInteger(e?.seq) && e.seq <= s.seq) : [];
886
+ this.#sessions.set(s.id, s);
887
+ }
888
+ }
524
889
  /**
525
890
  * Takes one hook event. Resolves with the hook's output: for `PermissionRequest` once the
526
891
  * request is answered (null when the computer should decide), `{}` for everything else.
527
892
  * Aborting `signal` (the hook was killed because the computer answered) withdraws the request.
528
893
  */
529
- async handle(raw, { agent = "claude", terminal = null, app = null, signal } = {}) {
894
+ async handle(raw, { agent = "claude", terminal = null, signal } = {}) {
530
895
  if (!raw || typeof raw.session_id !== "string" || !raw.session_id) {
531
896
  throw new HttpError(400, "hook event without session_id");
532
897
  }
@@ -536,8 +901,8 @@ var AgentHub = class extends EventEmitter {
536
901
  let replaced = null;
537
902
  if (!this.#sessions.has(event.session_id)) {
538
903
  const cleared = event.hook_event_name === "SessionStart" && event.source === "clear" && terminal;
539
- const candidates = [...this.#sessions.values()].filter(
540
- (p) => cleared ? p.terminal === terminal && p.agent === agent : p.starting && (terminal ? p.terminal === terminal : p.agent === agent && p.cwd === event.cwd)
904
+ const candidates = cleared ? [...this.#sessions.values()].filter((p) => p.terminal === terminal && p.agent === agent && (p.state !== "ended" || p.cleared)).sort((a, b) => b.lastActivityAt - a.lastActivityAt).slice(0, 1) : [...this.#sessions.values()].filter(
905
+ (p) => p.starting && (terminal ? p.terminal === terminal : p.agent === agent && p.cwd === event.cwd)
541
906
  );
542
907
  if (candidates.length === 1) {
543
908
  replaced = candidates[0];
@@ -550,9 +915,14 @@ var AgentHub = class extends EventEmitter {
550
915
  s = new AgentSession({ id: event.session_id, agent, cwd: event.cwd ?? null, terminal, now });
551
916
  this.#sessions.set(s.id, s);
552
917
  const fresh = event.hook_event_name === "SessionStart" && event.source !== "resume";
553
- if (!fresh && typeof event.transcript_path === "string") s.ready = this.#backfill(s, event.transcript_path);
918
+ if (typeof event.transcript_path === "string") {
919
+ s.transcriptPath = event.transcript_path;
920
+ if (fresh) s.transcriptOffset = 0;
921
+ else s.ready = this.#backfill(s, event.transcript_path);
922
+ }
554
923
  }
555
924
  await s.ready;
925
+ if (["PreToolUse", "Stop", "StopFailure"].includes(event.hook_event_name)) await this.#catchUp(s);
556
926
  if (s.agent === "codex" && !s.tail && typeof event.transcript_path === "string") {
557
927
  s.tail = this.#rolloutTail(event.transcript_path, (item) => this.#fromRollout(s, item), (error) => this.#rolloutTurnEnd(s, error));
558
928
  s.tail.poll?.().catch(() => {
@@ -563,31 +933,38 @@ var AgentHub = class extends EventEmitter {
563
933
  this.emit("message", { type: "session_removed", session: replaced.id, replacedBy: s.id });
564
934
  }
565
935
  if (terminal && !s.terminal) s.terminal = terminal;
566
- if (app) s.app = app;
567
936
  s.starting = false;
568
937
  s.lastActivityAt = now;
569
938
  if (s.state === "ended" && event.hook_event_name !== "SessionEnd") {
570
939
  s.state = "idle";
571
940
  s.endedAt = null;
941
+ s.cleared = false;
572
942
  }
943
+ s.permissionMode = modeOf(event.permission_mode) ?? s.permissionMode;
944
+ if (typeof event.model === "string" && event.model) s.model = event.model;
945
+ this.#withdrawAnsweredElsewhere(s, event);
573
946
  switch (event.hook_event_name) {
574
947
  case "SessionStart":
575
948
  if (s.state === "running" && s.turnStartedAt == null) s.state = "idle";
576
949
  break;
577
950
  case "UserPromptSubmit": {
578
- const text = typeof event.prompt === "string" ? event.prompt : "";
579
- if (!s.titled && text.trim()) {
951
+ const said = typeof event.prompt === "string" ? event.prompt : "";
952
+ const { text, files } = this.#withFiles(said);
953
+ if (!s.titled && text.trim() && !isNotice(text)) {
580
954
  s.title = titleOf(text);
581
955
  s.titled = true;
582
956
  }
583
957
  this.#startTurn(s, now);
584
- this.#entry(s, { kind: "prompt", text });
585
- const said = comparable(text);
586
- const i = s.prompts.findIndex((p) => p.text === said || p.text && said.startsWith(p.text));
587
- if (i >= 0) {
588
- const [prompt] = s.prompts.splice(i, 1);
589
- this.emit("message", { type: "prompt_confirmed", session: s.id, prompt: prompt.id });
590
- }
958
+ const typed = comparable(said);
959
+ const i = s.prompts.findIndex((p) => p.text === typed || p.text && typed.startsWith(p.text));
960
+ const [prompt] = i >= 0 ? s.prompts.splice(i, 1) : [];
961
+ this.#entry(s, {
962
+ kind: "prompt",
963
+ text,
964
+ ...prompt?.images?.length ? { images: prompt.images } : {},
965
+ ...files.length ? { files } : {}
966
+ });
967
+ if (prompt) this.emit("message", { type: "prompt_confirmed", session: s.id, prompt: prompt.id });
591
968
  break;
592
969
  }
593
970
  case "PreToolUse":
@@ -653,6 +1030,7 @@ var AgentHub = class extends EventEmitter {
653
1030
  }
654
1031
  break;
655
1032
  case "SessionEnd":
1033
+ if (event.reason === "clear") s.cleared = true;
656
1034
  this.#end(s);
657
1035
  break;
658
1036
  default:
@@ -661,8 +1039,27 @@ var AgentHub = class extends EventEmitter {
661
1039
  this.#emitSession(s);
662
1040
  return {};
663
1041
  }
1042
+ /**
1043
+ * Claude Code kills a waiting PermissionRequest hook when its own dialog is answered, but not
1044
+ * always when the answer comes from somewhere else (Claude's remote control on another device),
1045
+ * so the request would stay open. The session going on proves it was answered: the asking tool
1046
+ * ran (PostToolUse), or the turn moved on or ended.
1047
+ */
1048
+ #withdrawAnsweredElsewhere(s, event) {
1049
+ if (!s.requests.size) return;
1050
+ const name = event.hook_event_name;
1051
+ let stale = [];
1052
+ if (name === "PostToolUse" || name === "PostToolUseFailure") {
1053
+ const oldest = [...s.requests.values()].find((p) => p.request.tool === (event.tool_name ?? null));
1054
+ if (oldest) stale = [oldest];
1055
+ } else if (["UserPromptSubmit", "Stop", "StopFailure", "Interrupt"].includes(name) || name === "Notification" && event.notification_type === "idle_prompt") {
1056
+ stale = [...s.requests.values()];
1057
+ }
1058
+ for (const pending of stale) this.#resolve(pending, null, "withdrawn", "computer");
1059
+ }
664
1060
  /** The terminal a session ran in exited: whatever ran in it is over. */
665
1061
  terminalExited(terminal) {
1062
+ this.#footers.delete(terminal);
666
1063
  for (const s of this.#sessions.values()) {
667
1064
  if (s.terminal === terminal && s.state !== "ended") {
668
1065
  if (s.starting) s.error = `${(ADAPTERS[s.agent] ?? ADAPTERS.claude).name} exited before it started. Open the terminal to see why.`;
@@ -734,7 +1131,7 @@ var AgentHub = class extends EventEmitter {
734
1131
  const isQuestion = event.tool_name === "AskUserQuestion";
735
1132
  const input = event.tool_input ?? {};
736
1133
  const request = {
737
- id: randomUUID(),
1134
+ id: randomUUID2(),
738
1135
  session: s.id,
739
1136
  kind: isQuestion ? "question" : "permission",
740
1137
  tool: event.tool_name ?? null,
@@ -788,14 +1185,68 @@ var AgentHub = class extends EventEmitter {
788
1185
  } else if (e.kind === "tool_result") {
789
1186
  e.name ??= tools.get(e.toolUseId) ?? null;
790
1187
  ({ text: e.output, truncated: e.truncated } = clip(e.output));
791
- } else if (e.kind === "prompt" && !s.titled && e.text.trim()) {
1188
+ } else if (e.kind === "title") {
792
1189
  s.title = titleOf(e.text);
793
1190
  s.titled = true;
1191
+ continue;
1192
+ } else if (e.kind === "model") {
1193
+ s.model = e.model;
1194
+ continue;
1195
+ } else if (e.kind === "prompt") {
1196
+ const { text, files } = this.#withFiles(e.text);
1197
+ e.text = text;
1198
+ if (files.length) e.files = files;
1199
+ if (!s.titled && e.text.trim() && !isNotice(e.text)) {
1200
+ s.title = titleOf(e.text);
1201
+ s.titled = true;
1202
+ }
1203
+ if (e.images) {
1204
+ const kept = await Promise.all(
1205
+ e.images.map((i) => Promise.resolve(this.keepImage?.(Buffer.from(i.data, "base64"), i.type)).catch(() => null))
1206
+ );
1207
+ e.images = kept.filter(Boolean);
1208
+ if (!e.images.length) delete e.images;
1209
+ }
794
1210
  }
795
1211
  s.entries.push({ seq: ++s.seq, ...e });
796
1212
  }
1213
+ if (s.agent === "claude" && Number.isInteger(entries2.end)) s.transcriptOffset = entries2.end;
797
1214
  if (s.entries.length > MAX_ENTRIES) s.entries.splice(0, s.entries.length - MAX_ENTRIES);
798
1215
  }
1216
+ /**
1217
+ * Claude Code's shown thinking, its title for the conversation and the model answering, from the part of the
1218
+ * transcript written since the last look. Text, tools and prompts come from the hooks.
1219
+ */
1220
+ #catchUp(s) {
1221
+ s.catchingUp = (s.catchingUp ?? Promise.resolve()).then(() => this.#readSince(s)).catch(() => {
1222
+ });
1223
+ return s.catchingUp;
1224
+ }
1225
+ async #readSince(s) {
1226
+ if (s.agent !== "claude" || !s.transcriptPath || !Number.isInteger(s.transcriptOffset)) return;
1227
+ const read = await this.#transcriptSince(s.transcriptPath, s.transcriptOffset);
1228
+ if (!read) return;
1229
+ s.transcriptOffset = read.end;
1230
+ let changed = false;
1231
+ for (const e of read.entries) {
1232
+ if (e.kind === "model") {
1233
+ if (e.model !== s.model) {
1234
+ s.model = e.model;
1235
+ changed = true;
1236
+ }
1237
+ } else if (e.kind === "title") {
1238
+ const title = titleOf(e.text);
1239
+ if (title !== s.title) {
1240
+ s.title = title;
1241
+ changed = true;
1242
+ }
1243
+ s.titled = true;
1244
+ } else if (e.kind === "assistant" && e.thinking) {
1245
+ this.#entry(s, { kind: "assistant", text: e.text, thinking: true });
1246
+ }
1247
+ }
1248
+ if (changed) this.#emitSession(s);
1249
+ }
799
1250
  /** Something Codex said or a tool's result, from its rollout, unless the hooks told it already. */
800
1251
  #endTurn(s, now, error = null) {
801
1252
  s.streaming.clear();
@@ -856,14 +1307,18 @@ var AgentHub = class extends EventEmitter {
856
1307
  if (s.entries.length > MAX_ENTRIES) s.entries.shift();
857
1308
  this.emit("message", { type: "entry", session: s.id, entry: entry3 });
858
1309
  }
1310
+ /** A prompt's text without the paths of files a client attached, and those files. */
1311
+ #withFiles(text) {
1312
+ return attachedFiles(text, this.uploadsDir, this.fileType);
1313
+ }
859
1314
  #emitSession(s) {
860
1315
  this.emit("message", { type: "session", session: s.toJSON() });
861
1316
  }
862
1317
  };
863
1318
 
864
1319
  // ../../packages/agent-core/src/commands.js
865
- import fs4 from "node:fs/promises";
866
- import path3 from "node:path";
1320
+ import fs5 from "node:fs/promises";
1321
+ import path4 from "node:path";
867
1322
  var HEAD_BYTES = 4096;
868
1323
  var MAX_DESCRIPTION = 300;
869
1324
  var MAX_DEPTH = 3;
@@ -1005,7 +1460,7 @@ function frontmatter(text) {
1005
1460
  async function readHead(file) {
1006
1461
  let handle;
1007
1462
  try {
1008
- handle = await fs4.open(file, "r");
1463
+ handle = await fs5.open(file, "r");
1009
1464
  const { buffer, bytesRead } = await handle.read(Buffer.alloc(HEAD_BYTES), 0, HEAD_BYTES, 0);
1010
1465
  return frontmatter(buffer.subarray(0, bytesRead).toString("utf8")) ?? {};
1011
1466
  } catch {
@@ -1016,14 +1471,14 @@ async function readHead(file) {
1016
1471
  }
1017
1472
  async function entries(dir) {
1018
1473
  try {
1019
- return await fs4.readdir(dir, { withFileTypes: true });
1474
+ return await fs5.readdir(dir, { withFileTypes: true });
1020
1475
  } catch {
1021
1476
  return [];
1022
1477
  }
1023
1478
  }
1024
1479
  async function readJson(file) {
1025
1480
  try {
1026
- return JSON.parse(await fs4.readFile(file, "utf8"));
1481
+ return JSON.parse(await fs5.readFile(file, "utf8"));
1027
1482
  } catch {
1028
1483
  return null;
1029
1484
  }
@@ -1042,7 +1497,7 @@ async function commandFiles(dir, prefix, source, depth = 0, parts = []) {
1042
1497
  const out = [];
1043
1498
  for (const e of await entries(dir)) {
1044
1499
  if (e.name.startsWith(".")) continue;
1045
- const file = path3.join(dir, e.name);
1500
+ const file = path4.join(dir, e.name);
1046
1501
  if (e.isDirectory() && depth < MAX_DEPTH) {
1047
1502
  out.push(...await commandFiles(file, prefix, source, depth + 1, [...parts, e.name]));
1048
1503
  } else if (e.isFile() && e.name.endsWith(".md")) {
@@ -1056,9 +1511,9 @@ async function skillFiles(dir, { sigil = "/", prefix = "", source, kind = "comma
1056
1511
  const out = [];
1057
1512
  for (const e of await entries(dir)) {
1058
1513
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
1059
- const file = path3.join(dir, e.name, "SKILL.md");
1514
+ const file = path4.join(dir, e.name, "SKILL.md");
1060
1515
  try {
1061
- await fs4.access(file);
1516
+ await fs5.access(file);
1062
1517
  } catch {
1063
1518
  continue;
1064
1519
  }
@@ -1070,45 +1525,45 @@ async function skillFiles(dir, { sigil = "/", prefix = "", source, kind = "comma
1070
1525
  return out;
1071
1526
  }
1072
1527
  async function claudePlugins(home, cwd) {
1073
- const installed = (await readJson(path3.join(home, ".claude", "plugins", "installed_plugins.json")))?.plugins ?? {};
1528
+ const installed = (await readJson(path4.join(home, ".claude", "plugins", "installed_plugins.json")))?.plugins ?? {};
1074
1529
  const enabled = {};
1075
1530
  for (const file of [
1076
- path3.join(home, ".claude", "settings.json"),
1077
- path3.join(cwd, ".claude", "settings.json"),
1078
- path3.join(cwd, ".claude", "settings.local.json")
1531
+ path4.join(home, ".claude", "settings.json"),
1532
+ path4.join(cwd, ".claude", "settings.json"),
1533
+ path4.join(cwd, ".claude", "settings.local.json")
1079
1534
  ]) {
1080
1535
  Object.assign(enabled, (await readJson(file))?.enabledPlugins ?? {});
1081
1536
  }
1082
1537
  const out = [];
1083
1538
  for (const [key, installs] of Object.entries(installed)) {
1084
1539
  if (enabled[key] !== true || !Array.isArray(installs)) continue;
1085
- const install = installs.find((i) => !i.projectPath || path3.resolve(i.projectPath) === path3.resolve(cwd));
1540
+ const install = installs.find((i) => !i.projectPath || path4.resolve(i.projectPath) === path4.resolve(cwd));
1086
1541
  if (install?.installPath) out.push({ name: key.split("@")[0], root: install.installPath });
1087
1542
  }
1088
1543
  return out;
1089
1544
  }
1090
1545
  async function claudeCommands(home, cwd) {
1091
- const user = path3.join(home, ".claude");
1092
- const project = path3.join(cwd, ".claude");
1546
+ const user = path4.join(home, ".claude");
1547
+ const project = path4.join(cwd, ".claude");
1093
1548
  const found = [
1094
- ...await commandFiles(path3.join(user, "commands"), "", "user"),
1095
- ...await skillFiles(path3.join(user, "skills"), { source: "user" })
1549
+ ...await commandFiles(path4.join(user, "commands"), "", "user"),
1550
+ ...await skillFiles(path4.join(user, "skills"), { source: "user" })
1096
1551
  ];
1097
- for (const e of await entries(path3.join(user, "skills", "synced"))) {
1552
+ for (const e of await entries(path4.join(user, "skills", "synced"))) {
1098
1553
  if (e.isDirectory() && !e.name.startsWith(".")) {
1099
- found.push(...await skillFiles(path3.join(user, "skills", "synced", e.name), { prefix: "anthropic-skills:", source: "user" }));
1554
+ found.push(...await skillFiles(path4.join(user, "skills", "synced", e.name), { prefix: "anthropic-skills:", source: "user" }));
1100
1555
  }
1101
1556
  }
1102
- if (path3.resolve(project) !== path3.resolve(user)) {
1557
+ if (path4.resolve(project) !== path4.resolve(user)) {
1103
1558
  found.push(
1104
- ...await commandFiles(path3.join(project, "commands"), "", "project"),
1105
- ...await skillFiles(path3.join(project, "skills"), { source: "project" })
1559
+ ...await commandFiles(path4.join(project, "commands"), "", "project"),
1560
+ ...await skillFiles(path4.join(project, "skills"), { source: "project" })
1106
1561
  );
1107
1562
  }
1108
1563
  for (const plugin of await claudePlugins(home, cwd)) {
1109
1564
  found.push(
1110
- ...await commandFiles(path3.join(plugin.root, "commands"), `${plugin.name}:`, "plugin"),
1111
- ...await skillFiles(path3.join(plugin.root, "skills"), { prefix: `${plugin.name}:`, source: "plugin" })
1565
+ ...await commandFiles(path4.join(plugin.root, "commands"), `${plugin.name}:`, "plugin"),
1566
+ ...await skillFiles(path4.join(plugin.root, "skills"), { prefix: `${plugin.name}:`, source: "plugin" })
1112
1567
  );
1113
1568
  }
1114
1569
  return found;
@@ -1117,7 +1572,7 @@ async function codexDisabled(codexHome) {
1117
1572
  const off = /* @__PURE__ */ new Set();
1118
1573
  let text = "";
1119
1574
  try {
1120
- text = await fs4.readFile(path3.join(codexHome, "config.toml"), "utf8");
1575
+ text = await fs5.readFile(path4.join(codexHome, "config.toml"), "utf8");
1121
1576
  } catch {
1122
1577
  }
1123
1578
  let section = null;
@@ -1133,17 +1588,17 @@ async function latestVersion(dir) {
1133
1588
  let best = null;
1134
1589
  for (const e of await entries(dir)) {
1135
1590
  if (!e.isDirectory() || e.name.startsWith(".")) continue;
1136
- const { mtimeMs } = await fs4.stat(path3.join(dir, e.name));
1137
- if (!best || mtimeMs > best.mtimeMs) best = { dir: path3.join(dir, e.name), mtimeMs };
1591
+ const { mtimeMs } = await fs5.stat(path4.join(dir, e.name));
1592
+ if (!best || mtimeMs > best.mtimeMs) best = { dir: path4.join(dir, e.name), mtimeMs };
1138
1593
  }
1139
1594
  return best?.dir ?? null;
1140
1595
  }
1141
1596
  async function projectDirs(cwd, home) {
1142
1597
  const dirs = [];
1143
- for (let dir = path3.resolve(cwd); dir !== path3.resolve(home) && dir !== path3.dirname(dir); dir = path3.dirname(dir)) {
1598
+ for (let dir = path4.resolve(cwd); dir !== path4.resolve(home) && dir !== path4.dirname(dir); dir = path4.dirname(dir)) {
1144
1599
  dirs.push(dir);
1145
1600
  try {
1146
- await fs4.access(path3.join(dir, ".git"));
1601
+ await fs5.access(path4.join(dir, ".git"));
1147
1602
  break;
1148
1603
  } catch {
1149
1604
  }
@@ -1152,27 +1607,27 @@ async function projectDirs(cwd, home) {
1152
1607
  }
1153
1608
  async function codexMentions(home, codexHome, cwd) {
1154
1609
  const mention = { sigil: "$", kind: "mention" };
1155
- const skills = path3.join(codexHome, "skills");
1610
+ const skills = path4.join(codexHome, "skills");
1156
1611
  const found = [
1157
1612
  ...await skillFiles(skills, { ...mention, source: "user" }),
1158
- ...await skillFiles(path3.join(skills, ".system"), { ...mention, source: "user" })
1613
+ ...await skillFiles(path4.join(skills, ".system"), { ...mention, source: "user" })
1159
1614
  ];
1160
1615
  for (const dir of await projectDirs(cwd, home)) {
1161
- found.push(...await skillFiles(path3.join(dir, ".agents", "skills"), { ...mention, source: "project" }));
1616
+ found.push(...await skillFiles(path4.join(dir, ".agents", "skills"), { ...mention, source: "project" }));
1162
1617
  }
1163
1618
  const off = await codexDisabled(codexHome);
1164
- const cache = path3.join(codexHome, "plugins", "cache");
1619
+ const cache = path4.join(codexHome, "plugins", "cache");
1165
1620
  for (const market of await entries(cache)) {
1166
1621
  if (!market.isDirectory()) continue;
1167
- for (const plugin of await entries(path3.join(cache, market.name))) {
1622
+ for (const plugin of await entries(path4.join(cache, market.name))) {
1168
1623
  if (!plugin.isDirectory() || off.has(`${plugin.name}@${market.name}`)) continue;
1169
- const root2 = await latestVersion(path3.join(cache, market.name, plugin.name));
1170
- if (root2) found.push(...await skillFiles(path3.join(root2, "skills"), { ...mention, prefix: `${plugin.name}:`, source: "plugin" }));
1624
+ const root2 = await latestVersion(path4.join(cache, market.name, plugin.name));
1625
+ if (root2) found.push(...await skillFiles(path4.join(root2, "skills"), { ...mention, prefix: `${plugin.name}:`, source: "plugin" }));
1171
1626
  }
1172
1627
  }
1173
1628
  return found;
1174
1629
  }
1175
- async function listCommands({ agent, cwd, home, codexHome = path3.join(home, ".codex") }) {
1630
+ async function listCommands({ agent, cwd, home, codexHome = path4.join(home, ".codex") }) {
1176
1631
  const builtins = (BUILTINS[agent] ?? BUILTINS.claude).map(builtin);
1177
1632
  const found = agent === "codex" ? await codexMentions(home, codexHome, cwd) : await claudeCommands(home, cwd);
1178
1633
  const byName = new Map(builtins.map((c) => [c.name, c]));
@@ -1182,12 +1637,12 @@ async function listCommands({ agent, cwd, home, codexHome = path3.join(home, ".c
1182
1637
  }
1183
1638
 
1184
1639
  // ../../packages/agent-core/src/builds.js
1185
- import { execFile, spawn } from "node:child_process";
1186
- import { randomUUID as randomUUID2 } from "node:crypto";
1640
+ import { execFile as execFile2, spawn } from "node:child_process";
1641
+ import { randomUUID as randomUUID3 } from "node:crypto";
1187
1642
  import { EventEmitter as EventEmitter2 } from "node:events";
1188
- import fs5 from "node:fs/promises";
1189
- import os from "node:os";
1190
- import path4 from "node:path";
1643
+ import fs6 from "node:fs/promises";
1644
+ import os2 from "node:os";
1645
+ import path5 from "node:path";
1191
1646
  var MAX_LINES = 5e3;
1192
1647
  var MAX_LINE = 2048;
1193
1648
  var MAX_ERRORS = 20;
@@ -1204,19 +1659,19 @@ var SIGNING_HINTS = [
1204
1659
  async function findContainer(folder) {
1205
1660
  let names;
1206
1661
  try {
1207
- names = await fs5.readdir(folder);
1662
+ names = await fs6.readdir(folder);
1208
1663
  } catch {
1209
1664
  return null;
1210
1665
  }
1211
1666
  for (const [ext, kind] of [[".xcworkspace", "workspace"], [".xcodeproj", "project"]]) {
1212
1667
  const name = names.filter((n) => n.endsWith(ext)).sort()[0];
1213
- if (name) return { container: path4.join(folder, name), kind };
1668
+ if (name) return { container: path5.join(folder, name), kind };
1214
1669
  }
1215
1670
  return null;
1216
1671
  }
1217
- function run(command, args, { cwd, timeout = 12e4 } = {}) {
1672
+ function run2(command, args, { cwd, timeout = 12e4 } = {}) {
1218
1673
  return new Promise((resolve, reject) => {
1219
- execFile(command, args, { cwd, timeout, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
1674
+ execFile2(command, args, { cwd, timeout, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
1220
1675
  if (error) reject(Object.assign(new HttpError(409, (stderr || error.message).trim().split("\n").slice(-3).join("\n")), { code: "EXCODE" }));
1221
1676
  else resolve(stdout);
1222
1677
  });
@@ -1225,9 +1680,9 @@ function run(command, args, { cwd, timeout = 12e4 } = {}) {
1225
1680
  async function listSchemes(folder, { xcodebuild = "xcodebuild" } = {}) {
1226
1681
  const found = await findContainer(folder);
1227
1682
  if (!found) throw new HttpError(404, "no Xcode project or workspace in this folder");
1228
- const out = JSON.parse(await run(xcodebuild, ["-list", "-json", `-${found.kind}`, found.container], { cwd: folder }));
1683
+ const out = JSON.parse(await run2(xcodebuild, ["-list", "-json", `-${found.kind}`, found.container], { cwd: folder }));
1229
1684
  const schemes = out[found.kind]?.schemes ?? [];
1230
- return { container: path4.basename(found.container), kind: found.kind, schemes };
1685
+ return { container: path5.basename(found.container), kind: found.kind, schemes };
1231
1686
  }
1232
1687
  function failureOf(lines) {
1233
1688
  const compile = [...new Set(lines.filter((l) => /^\S.*?:\d+:(?:\d+:)? error: /.test(l)))];
@@ -1251,11 +1706,11 @@ function exportOptions() {
1251
1706
  `;
1252
1707
  }
1253
1708
  async function readInfo(appPath) {
1254
- const info = JSON.parse(await run("plutil", ["-convert", "json", "-o", "-", path4.join(appPath, "Info.plist")]));
1709
+ const info = JSON.parse(await run2("plutil", ["-convert", "json", "-o", "-", path5.join(appPath, "Info.plist")]));
1255
1710
  return {
1256
1711
  bundleId: info.CFBundleIdentifier,
1257
1712
  version: info.CFBundleShortVersionString ?? info.CFBundleVersion ?? "1.0",
1258
- name: info.CFBundleDisplayName ?? info.CFBundleName ?? path4.basename(appPath, ".app")
1713
+ name: info.CFBundleDisplayName ?? info.CFBundleName ?? path5.basename(appPath, ".app")
1259
1714
  };
1260
1715
  }
1261
1716
  var BuildRunner = class extends EventEmitter2 {
@@ -1267,7 +1722,7 @@ var BuildRunner = class extends EventEmitter2 {
1267
1722
  #build = null;
1268
1723
  #lines = [];
1269
1724
  #child = null;
1270
- constructor({ xcodebuild = "xcodebuild", readInfo: info = readInfo, dir = path4.join(os.homedir(), ".coderemote", "builds"), now = Date.now, killAfter = KILL_AFTER } = {}) {
1725
+ constructor({ xcodebuild = "xcodebuild", readInfo: info = readInfo, dir = path5.join(os2.homedir(), ".coderemote", "builds"), now = Date.now, killAfter = KILL_AFTER } = {}) {
1271
1726
  super();
1272
1727
  this.#xcodebuild = xcodebuild;
1273
1728
  this.#readInfo = info;
@@ -1296,10 +1751,10 @@ var BuildRunner = class extends EventEmitter2 {
1296
1751
  if (!/^[\w .-]+$/.test(configuration)) throw new HttpError(400, "invalid configuration");
1297
1752
  const found = await findContainer(folder);
1298
1753
  if (!found) throw new HttpError(404, "no Xcode project or workspace in this folder");
1299
- const id = randomUUID2();
1300
- const out = path4.join(this.#dir, id);
1301
- await fs5.rm(this.#dir, { recursive: true, force: true });
1302
- await fs5.mkdir(out, { recursive: true });
1754
+ const id = randomUUID3();
1755
+ const out = path5.join(this.#dir, id);
1756
+ await fs6.rm(this.#dir, { recursive: true, force: true });
1757
+ await fs6.mkdir(out, { recursive: true });
1303
1758
  this.#lines = [];
1304
1759
  this.#build = { id, path: folder, scheme, configuration, state: "running", step: "archiving", startedAt: this.#now(), endedAt: null, error: null, first: 0, artifact: null };
1305
1760
  const build = this.#build;
@@ -1329,8 +1784,8 @@ var BuildRunner = class extends EventEmitter2 {
1329
1784
  }
1330
1785
  }
1331
1786
  async #run(build, { container, kind }, out) {
1332
- const archive = path4.join(out, "App.xcarchive");
1333
- const exported = path4.join(out, "export");
1787
+ const archive = path5.join(out, "App.xcarchive");
1788
+ const exported = path5.join(out, "export");
1334
1789
  try {
1335
1790
  await this.#xcode(build, [
1336
1791
  `-${kind}`,
@@ -1348,8 +1803,8 @@ var BuildRunner = class extends EventEmitter2 {
1348
1803
  "archive"
1349
1804
  ], build.path);
1350
1805
  build.step = "exporting";
1351
- const options = path4.join(out, "ExportOptions.plist");
1352
- await fs5.writeFile(options, exportOptions());
1806
+ const options = path5.join(out, "ExportOptions.plist");
1807
+ await fs6.writeFile(options, exportOptions());
1353
1808
  await this.#xcode(build, [
1354
1809
  "-exportArchive",
1355
1810
  "-archivePath",
@@ -1360,14 +1815,14 @@ var BuildRunner = class extends EventEmitter2 {
1360
1815
  options,
1361
1816
  "-allowProvisioningUpdates"
1362
1817
  ], build.path);
1363
- const ipaName = (await fs5.readdir(exported)).find((n) => n.endsWith(".ipa"));
1818
+ const ipaName = (await fs6.readdir(exported)).find((n) => n.endsWith(".ipa"));
1364
1819
  if (!ipaName) throw new Error("the export made no .ipa");
1365
- const apps = path4.join(archive, "Products", "Applications");
1366
- const appName = (await fs5.readdir(apps)).find((n) => n.endsWith(".app"));
1820
+ const apps = path5.join(archive, "Products", "Applications");
1821
+ const appName = (await fs6.readdir(apps)).find((n) => n.endsWith(".app"));
1367
1822
  if (!appName) throw new Error("the archive holds no app");
1368
- const ipa = path4.join(exported, ipaName);
1369
- const { size } = await fs5.stat(ipa);
1370
- build.artifact = { ipa, app: { ...await this.#readInfo(path4.join(apps, appName)), size } };
1823
+ const ipa = path5.join(exported, ipaName);
1824
+ const { size } = await fs6.stat(ipa);
1825
+ build.artifact = { ipa, app: { ...await this.#readInfo(path5.join(apps, appName)), size } };
1371
1826
  build.state = "succeeded";
1372
1827
  } catch (e) {
1373
1828
  if (build.cancelled) {
@@ -1422,8 +1877,8 @@ var BuildRunner = class extends EventEmitter2 {
1422
1877
  };
1423
1878
 
1424
1879
  // ../../packages/agent-core/src/history.js
1425
- import fs6 from "node:fs/promises";
1426
- import path5 from "node:path";
1880
+ import fs7 from "node:fs/promises";
1881
+ import path6 from "node:path";
1427
1882
  var LIMIT = 50;
1428
1883
  var HEAD_BYTES2 = 128 * 1024;
1429
1884
  var TAIL_BYTES2 = 64 * 1024;
@@ -1431,7 +1886,7 @@ var MAX_TITLE2 = 80;
1431
1886
  async function slice(file, position, length) {
1432
1887
  let handle;
1433
1888
  try {
1434
- handle = await fs6.open(file, "r");
1889
+ handle = await fs7.open(file, "r");
1435
1890
  const { size } = await handle.stat();
1436
1891
  const start2 = position < 0 ? Math.max(0, size + position) : position;
1437
1892
  const { buffer, bytesRead } = await handle.read(Buffer.alloc(Math.min(length, size)), 0, Math.min(length, size), start2);
@@ -1465,18 +1920,18 @@ function typedPrompt(record) {
1465
1920
  return t && !t.startsWith("<") && !t.startsWith("[Request interrupted") ? t : null;
1466
1921
  }
1467
1922
  async function claudeHistory(home, cwd) {
1468
- const dir = path5.join(home, ".claude", "projects", path5.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-"));
1923
+ const dir = path6.join(home, ".claude", "projects", path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-"));
1469
1924
  let names = [];
1470
1925
  try {
1471
- names = (await fs6.readdir(dir)).filter((n) => n.endsWith(".jsonl"));
1926
+ names = (await fs7.readdir(dir)).filter((n) => n.endsWith(".jsonl"));
1472
1927
  } catch {
1473
1928
  return [];
1474
1929
  }
1475
1930
  const files = [];
1476
1931
  for (const name of names) {
1477
1932
  try {
1478
- const { mtimeMs, size } = await fs6.stat(path5.join(dir, name));
1479
- files.push({ id: name.slice(0, -6), file: path5.join(dir, name), mtimeMs, size });
1933
+ const { mtimeMs, size } = await fs7.stat(path6.join(dir, name));
1934
+ files.push({ id: name.slice(0, -6), file: path6.join(dir, name), mtimeMs, size });
1480
1935
  } catch {
1481
1936
  }
1482
1937
  }
@@ -1502,7 +1957,7 @@ async function claudeHistory(home, cwd) {
1502
1957
  async function codexHistory(codexHome, cwd) {
1503
1958
  const names = /* @__PURE__ */ new Map();
1504
1959
  try {
1505
- for (const r of records(await fs6.readFile(path5.join(codexHome, "session_index.jsonl"), "utf8"))) {
1960
+ for (const r of records(await fs7.readFile(path6.join(codexHome, "session_index.jsonl"), "utf8"))) {
1506
1961
  if (typeof r.id === "string" && typeof r.thread_name === "string") names.set(r.id, r.thread_name);
1507
1962
  }
1508
1963
  } catch {
@@ -1511,31 +1966,31 @@ async function codexHistory(codexHome, cwd) {
1511
1966
  const walk = async (dir, depth) => {
1512
1967
  let entries2 = [];
1513
1968
  try {
1514
- entries2 = await fs6.readdir(dir, { withFileTypes: true });
1969
+ entries2 = await fs7.readdir(dir, { withFileTypes: true });
1515
1970
  } catch {
1516
1971
  return;
1517
1972
  }
1518
1973
  for (const e of entries2) {
1519
- const p = path5.join(dir, e.name);
1974
+ const p = path6.join(dir, e.name);
1520
1975
  if (e.isDirectory() && depth < 3) await walk(p, depth + 1);
1521
1976
  else if (e.isFile() && /^rollout-.*\.jsonl$/.test(e.name)) {
1522
1977
  try {
1523
- const { mtimeMs, size } = await fs6.stat(p);
1978
+ const { mtimeMs, size } = await fs7.stat(p);
1524
1979
  files.push({ file: p, mtimeMs, size });
1525
1980
  } catch {
1526
1981
  }
1527
1982
  }
1528
1983
  }
1529
1984
  };
1530
- await walk(path5.join(codexHome, "sessions"), 0);
1985
+ await walk(path6.join(codexHome, "sessions"), 0);
1531
1986
  files.sort((a, b) => b.mtimeMs - a.mtimeMs);
1532
- const want = path5.resolve(cwd);
1987
+ const want = path6.resolve(cwd);
1533
1988
  const out = [];
1534
1989
  for (const f of files) {
1535
1990
  if (out.length >= LIMIT) break;
1536
1991
  const head = await slice(f.file, 0, HEAD_BYTES2);
1537
1992
  const meta = records(head.split("\n")[0])[0]?.payload;
1538
- if (!meta || typeof meta.id !== "string" || typeof meta.cwd !== "string" || path5.resolve(meta.cwd) !== want) continue;
1993
+ if (!meta || typeof meta.id !== "string" || typeof meta.cwd !== "string" || path6.resolve(meta.cwd) !== want) continue;
1539
1994
  const prompt = records(head).filter((r) => r.type === "response_item" && r.payload?.type === "message" && r.payload.role === "user").flatMap((r) => Array.isArray(r.payload.content) ? r.payload.content : []).map((c) => c?.type === "input_text" && typeof c.text === "string" ? c.text.trim() : "").find((t) => t && !t.startsWith("<") && !t.startsWith("# AGENTS.md"));
1540
1995
  const title = names.get(meta.id) ?? prompt;
1541
1996
  if (!title) continue;
@@ -1549,16 +2004,16 @@ async function codexHistory(codexHome, cwd) {
1549
2004
  }
1550
2005
  return out;
1551
2006
  }
1552
- async function listHistory({ agent, cwd, home, codexHome = path5.join(home, ".codex") }) {
2007
+ async function listHistory({ agent, cwd, home, codexHome = path6.join(home, ".codex") }) {
1553
2008
  return agent === "codex" ? codexHistory(codexHome, cwd) : claudeHistory(home, cwd);
1554
2009
  }
1555
2010
 
1556
2011
  // ../../packages/agent-core/src/install-links.js
1557
2012
  import { randomBytes } from "node:crypto";
1558
2013
  import { createReadStream } from "node:fs";
1559
- import fs7 from "node:fs/promises";
2014
+ import fs8 from "node:fs/promises";
1560
2015
  import http from "node:http";
1561
- import { pipeline } from "node:stream/promises";
2016
+ import { pipeline as pipeline2 } from "node:stream/promises";
1562
2017
  var TTL = 10 * 60 * 1e3;
1563
2018
  var SHARE_HOURS = 1;
1564
2019
  var xml = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -1683,24 +2138,24 @@ var InstallLinks = class {
1683
2138
  res.end(req.method === "HEAD" ? void 0 : body);
1684
2139
  return;
1685
2140
  }
1686
- const { size } = await fs7.stat(link.ipa);
2141
+ const { size } = await fs8.stat(link.ipa);
1687
2142
  res.writeHead(200, { "content-type": "application/octet-stream", "content-length": size, "cache-control": "no-store" });
1688
2143
  if (req.method === "HEAD") return res.end();
1689
- await pipeline(createReadStream(link.ipa), res);
2144
+ await pipeline2(createReadStream(link.ipa), res);
1690
2145
  this.#expire(m[1]);
1691
2146
  }
1692
2147
  };
1693
2148
 
1694
2149
  // ../../packages/agent-core/src/git.js
1695
- import { execFile as execFile2 } from "node:child_process";
1696
- import fs8 from "node:fs/promises";
1697
- import path6 from "node:path";
2150
+ import { execFile as execFile3 } from "node:child_process";
2151
+ import fs9 from "node:fs/promises";
2152
+ import path7 from "node:path";
1698
2153
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".build", "build", "dist", "DerivedData", "Pods", "vendor", ".venv", "target"]);
1699
2154
  var MAX_SCAN_DEPTH = 4;
1700
2155
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
1701
2156
  function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 * 1024 } = {}) {
1702
2157
  return new Promise((resolve, reject) => {
1703
- execFile2(
2158
+ execFile3(
1704
2159
  "git",
1705
2160
  args,
1706
2161
  {
@@ -1723,21 +2178,21 @@ async function findRepos(root2) {
1723
2178
  async function walk(dir, depth) {
1724
2179
  let entries2;
1725
2180
  try {
1726
- entries2 = await fs8.readdir(dir, { withFileTypes: true });
2181
+ entries2 = await fs9.readdir(dir, { withFileTypes: true });
1727
2182
  } catch {
1728
2183
  return;
1729
2184
  }
1730
2185
  if (entries2.some((e) => e.name === ".git")) repos.push(dir);
1731
2186
  if (depth >= MAX_SCAN_DEPTH) return;
1732
2187
  const subdirs = entries2.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name));
1733
- await Promise.all(subdirs.map((e) => walk(path6.join(dir, e.name), depth + 1)));
2188
+ await Promise.all(subdirs.map((e) => walk(path7.join(dir, e.name), depth + 1)));
1734
2189
  }
1735
2190
  await walk(root2, 0);
1736
- repos.sort((a, b) => a.split(path6.sep).length - b.split(path6.sep).length || a.localeCompare(b));
2191
+ repos.sort((a, b) => a.split(path7.sep).length - b.split(path7.sep).length || a.localeCompare(b));
1737
2192
  return Promise.all(
1738
2193
  repos.map(async (repo) => ({
1739
2194
  path: repo,
1740
- name: repo === root2 ? path6.basename(root2) : path6.relative(root2, repo),
2195
+ name: repo === root2 ? path7.basename(root2) : path7.relative(root2, repo),
1741
2196
  branch: await currentBranch(repo),
1742
2197
  ...await summary(repo)
1743
2198
  }))
@@ -1847,27 +2302,335 @@ async function push(repo) {
1847
2302
  const { stdout, stderr } = await git(repo, ["push"], { timeout: 12e4 });
1848
2303
  return (stdout + stderr).trim();
1849
2304
  }
2305
+ async function fetch2(repo) {
2306
+ const { stdout, stderr } = await git(repo, ["fetch", "--all", "--prune"], { timeout: 12e4 });
2307
+ return (stdout + stderr).trim();
2308
+ }
2309
+ async function branchName(repo, name) {
2310
+ if (typeof name !== "string" || !name.trim() || name.startsWith("-")) throw new HttpError(400, "invalid branch");
2311
+ try {
2312
+ await git(repo, ["check-ref-format", "--branch", name]);
2313
+ } catch {
2314
+ throw new HttpError(400, "invalid branch");
2315
+ }
2316
+ return name;
2317
+ }
2318
+ async function branches(repo) {
2319
+ const { stdout } = await git(repo, [
2320
+ "for-each-ref",
2321
+ "--sort=-committerdate",
2322
+ "--format=%(refname)%1f%(refname:short)%1f%(HEAD)%1f%(upstream:short)%1f%(committerdate:unix)%1f%(subject)",
2323
+ "refs/heads",
2324
+ "refs/remotes"
2325
+ ]);
2326
+ const all = stdout.split("\n").filter(Boolean).map((line) => {
2327
+ const [ref, name, head, upstream, at, subject] = line.split("");
2328
+ return {
2329
+ name,
2330
+ remote: ref.startsWith("refs/remotes/"),
2331
+ current: head === "*",
2332
+ upstream: upstream || null,
2333
+ date: at ? parseInt(at, 10) * 1e3 : null,
2334
+ subject: subject ?? "",
2335
+ ref
2336
+ };
2337
+ }).filter((b) => !(b.remote && b.ref.endsWith("/HEAD")));
2338
+ const tracked = new Set(all.filter((b) => !b.remote && b.upstream).map((b) => b.upstream));
2339
+ return all.filter((b) => !b.remote || !tracked.has(b.name)).map(({ ref, ...b }) => b);
2340
+ }
2341
+ async function checkout(repo, branch, { create = false } = {}) {
2342
+ const name = await branchName(repo, branch);
2343
+ const has = async (ref) => (await git(repo, ["show-ref", "--verify", "-q", ref], { okCodes: [0, 1] })).code === 0;
2344
+ if (create) {
2345
+ await git(repo, ["switch", "-q", "-c", name]);
2346
+ } else if (!await has(`refs/heads/${name}`) && name.includes("/") && await has(`refs/remotes/${name}`)) {
2347
+ const local = name.slice(name.indexOf("/") + 1);
2348
+ await git(repo, await has(`refs/heads/${local}`) ? ["switch", "-q", local] : ["switch", "-q", "-c", local, "--track", name]);
2349
+ } else {
2350
+ await git(repo, ["switch", "-q", name]);
2351
+ }
2352
+ return status(repo);
2353
+ }
2354
+ async function stashes(repo) {
2355
+ const { stdout } = await git(repo, ["stash", "list", "-z", "--format=%gd%x1f%gs%x1f%ct"]);
2356
+ return stdout.split("\0").filter(Boolean).map((record) => {
2357
+ const [ref, message, at] = record.split("");
2358
+ return { index: parseInt(ref.match(/\{(\d+)\}/)?.[1] ?? "0", 10), message, date: parseInt(at, 10) * 1e3 };
2359
+ });
2360
+ }
2361
+ async function stash(repo, { message, untracked = true } = {}) {
2362
+ if (message !== void 0 && message !== null && typeof message !== "string") throw new HttpError(400, "invalid message");
2363
+ const { files } = await status(repo);
2364
+ if (!files.length) throw new HttpError(409, "nothing to stash");
2365
+ const args = ["stash", "push", "-q", ...untracked ? ["--include-untracked"] : []];
2366
+ if (message?.trim()) args.push("-m", message.trim());
2367
+ await git(repo, args);
2368
+ return status(repo);
2369
+ }
2370
+ function stashRef(index) {
2371
+ if (!Number.isInteger(index) || index < 0) throw new HttpError(400, "invalid stash");
2372
+ return `stash@{${index}}`;
2373
+ }
2374
+ async function stashApply(repo, index, { pop = false } = {}) {
2375
+ await git(repo, ["stash", pop ? "pop" : "apply", "-q", stashRef(index)]);
2376
+ return status(repo);
2377
+ }
2378
+ async function stashDrop(repo, index) {
2379
+ await git(repo, ["stash", "drop", "-q", stashRef(index)]);
2380
+ return status(repo);
2381
+ }
1850
2382
  function truncate(text) {
1851
2383
  if (Buffer.byteLength(text) <= MAX_DIFF_BYTES) return { text, truncated: false };
1852
2384
  return { text: Buffer.from(text).subarray(0, MAX_DIFF_BYTES).toString("utf8"), truncated: true };
1853
2385
  }
1854
2386
 
1855
- // ../../packages/agent-core/src/handoff.js
1856
- import { createHmac, timingSafeEqual } from "node:crypto";
1857
- var NONCE_WINDOW_MS = 5 * 6e4;
1858
- var REUSE_MIN_LIFETIME_MS = 12 * 36e5;
1859
- var TunnelHandoff = class {
1860
- /**
1861
- * @param {object} opts
1862
- * @param {string} opts.token the agent's bearer token (the HMAC key)
1863
- * @param {() => Promise<{token: string, expiresAt: number} | null>} opts.mint
1864
- */
1865
- constructor({ token, mint, now = Date.now }) {
1866
- this.key = token;
1867
- this.mint = mint;
1868
- this.now = now;
1869
- this.used = /* @__PURE__ */ new Map();
1870
- this.cached = null;
2387
+ // ../../packages/agent-core/src/commit-message.js
2388
+ import { spawn as spawn3 } from "node:child_process";
2389
+ import fs11 from "node:fs/promises";
2390
+ import os3 from "node:os";
2391
+ import path9 from "node:path";
2392
+
2393
+ // ../../packages/agent-core/src/platform.js
2394
+ import { execFile as execFile4, spawn as spawn2 } from "node:child_process";
2395
+ import fs10 from "node:fs";
2396
+ import path8 from "node:path";
2397
+ var isWindows = process.platform === "win32";
2398
+ function shellQuote(arg) {
2399
+ return `'${String(arg).replace(/'/g, `'\\''`)}'`;
2400
+ }
2401
+ function runnable(file) {
2402
+ try {
2403
+ if (!isWindows) fs10.accessSync(file, fs10.constants.X_OK);
2404
+ return fs10.statSync(file).isFile();
2405
+ } catch {
2406
+ return false;
2407
+ }
2408
+ }
2409
+ function findExecutable(name, env = process.env) {
2410
+ const exts = isWindows && !path8.extname(name) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
2411
+ const match = (base2) => exts.map((e) => base2 + e).find(runnable) ?? null;
2412
+ if (name.includes("/") || name.includes(path8.sep)) return match(path8.resolve(name));
2413
+ for (const dir of (env.PATH ?? env.Path ?? "").split(path8.delimiter)) {
2414
+ const found = dir && match(path8.join(dir.replace(/^"(.*)"$/, "$1"), name));
2415
+ if (found) return found;
2416
+ }
2417
+ return null;
2418
+ }
2419
+ function interactiveShell() {
2420
+ if (!isWindows) return { file: process.env.SHELL || "/bin/sh", args: ["-l"] };
2421
+ return { file: findExecutable("pwsh") ?? "powershell.exe", args: ["-NoLogo"] };
2422
+ }
2423
+ function cmdQuote(arg) {
2424
+ const s = String(arg);
2425
+ if (/["%\r\n]/.test(s)) throw new Error(`${JSON.stringify(s)} can't be passed through cmd.exe`);
2426
+ return `"${s.replace(/(\\+)$/, "$1$1")}"`;
2427
+ }
2428
+ function resolveCommand(argv2) {
2429
+ const [name, ...rest2] = argv2;
2430
+ if (!isWindows) return { file: name, args: rest2, verbatim: false };
2431
+ const file = findExecutable(name) ?? name;
2432
+ if (!/\.(bat|cmd)$/i.test(file)) return { file, args: rest2, verbatim: false };
2433
+ const line = [file, ...rest2].map(cmdQuote).join(" ");
2434
+ return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/v:off", "/s", "/c", `"${line}"`], verbatim: true };
2435
+ }
2436
+ function terminalCommand(argv2) {
2437
+ if (!isWindows) return { file: interactiveShell().file, args: ["-l", "-c", `exec ${argv2.map(shellQuote).join(" ")}`] };
2438
+ const { file, args, verbatim } = resolveCommand(argv2);
2439
+ return { file, args: verbatim ? args.join(" ") : args };
2440
+ }
2441
+ function spawnCommand(argv2, options = {}) {
2442
+ const { file, args, verbatim } = resolveCommand(argv2);
2443
+ return spawn2(file, args, { ...options, windowsVerbatimArguments: verbatim });
2444
+ }
2445
+ function execCommand(argv2, options = {}) {
2446
+ return new Promise((resolve, reject) => {
2447
+ const { file, args, verbatim } = resolveCommand(argv2);
2448
+ execFile4(
2449
+ file,
2450
+ args,
2451
+ { ...options, windowsVerbatimArguments: verbatim },
2452
+ (err, stdout, stderr) => err ? reject(Object.assign(err, { stdout, stderr })) : resolve({ stdout, stderr })
2453
+ );
2454
+ });
2455
+ }
2456
+ function loginShell(script2, timeout) {
2457
+ return new Promise((resolve) => {
2458
+ execFile4(
2459
+ interactiveShell().file,
2460
+ ["-l", "-c", script2],
2461
+ { timeout, maxBuffer: 16 * 1024 * 1024 },
2462
+ (err, out) => resolve(err ? null : out)
2463
+ );
2464
+ });
2465
+ }
2466
+ async function capture(argv2, { timeout = 2e4 } = {}) {
2467
+ if (!isWindows) return loginShell(argv2.map(shellQuote).join(" "), timeout);
2468
+ try {
2469
+ return (await execCommand(argv2, { timeout, maxBuffer: 16 * 1024 * 1024 })).stdout;
2470
+ } catch {
2471
+ return null;
2472
+ }
2473
+ }
2474
+ async function commandExists(command) {
2475
+ if (isWindows) return findExecutable(command) !== null;
2476
+ return Boolean((await loginShell(`command -v ${shellQuote(command)}`, 2e4))?.trim());
2477
+ }
2478
+
2479
+ // ../../packages/agent-core/src/commit-message.js
2480
+ var MAX_DIFF_CHARS = 24e3;
2481
+ var TIMEOUT_MS = 12e4;
2482
+ var EXCLUDE = [
2483
+ "*.lock",
2484
+ "package-lock.json",
2485
+ "npm-shrinkwrap.json",
2486
+ "pnpm-lock.yaml",
2487
+ "yarn.lock",
2488
+ "Cargo.lock",
2489
+ "Gemfile.lock",
2490
+ "poetry.lock",
2491
+ "composer.lock",
2492
+ "go.sum",
2493
+ "Package.resolved",
2494
+ "*.min.js",
2495
+ "*.min.css",
2496
+ "*.map",
2497
+ "*.snap",
2498
+ "*.pbxproj"
2499
+ ].map((p) => `:(exclude,glob)**/${p}`);
2500
+ async function pendingChanges(repo) {
2501
+ const staged = (await git(repo, ["diff", "--cached", "--quiet"], { okCodes: [0, 1] })).code === 1;
2502
+ const base2 = staged ? ["--cached"] : await hasHead(repo) ? ["HEAD"] : ["--cached"];
2503
+ const [stat, patch, untracked] = await Promise.all([
2504
+ git(repo, ["diff", "--no-color", "--stat=120", ...base2]).then((r) => r.stdout),
2505
+ git(repo, ["diff", "--no-color", "--no-ext-diff", ...base2, "--", ".", ...EXCLUDE]).then((r) => r.stdout),
2506
+ staged ? "" : git(repo, ["ls-files", "--others", "--exclude-standard"]).then((r) => r.stdout)
2507
+ ]);
2508
+ if (!stat.trim() && !untracked.trim()) throw new HttpError(409, "nothing to commit");
2509
+ const diff2 = patch.length > MAX_DIFF_CHARS ? `${patch.slice(0, MAX_DIFF_CHARS)}
2510
+ [diff truncated]
2511
+ ` : patch;
2512
+ return { staged, stat, diff: diff2, untracked: untracked.split("\n").filter(Boolean) };
2513
+ }
2514
+ async function hasHead(repo) {
2515
+ return (await git(repo, ["rev-parse", "--verify", "-q", "HEAD"], { okCodes: [0, 1, 128] })).code === 0;
2516
+ }
2517
+ function buildPrompt({ stat, diff: diff2, untracked }, recentSubjects) {
2518
+ return [
2519
+ "Write a git commit message for the changes below.",
2520
+ "Match the style of this repository's recent commits (language, tense, prefixes, capitalisation).",
2521
+ "First line: a summary of at most 72 characters.",
2522
+ 'Then a blank line and a body of 1 to 4 short lines saying what changed and why, as "- " bullets',
2523
+ "when there is more than one point. Do not list file names or restate the diff line by line.",
2524
+ "Reply with the commit message only: no preamble, no quotes, no code fences.",
2525
+ "",
2526
+ "Recent commits:",
2527
+ ...recentSubjects.length ? recentSubjects.map((s) => `- ${s}`) : ["(none yet)"],
2528
+ "",
2529
+ "Files changed:",
2530
+ stat.trim(),
2531
+ ...untracked.length ? ["", "New files:", ...untracked.slice(0, 100)] : [],
2532
+ "",
2533
+ "Diff:",
2534
+ diff2.trim()
2535
+ ].join("\n");
2536
+ }
2537
+ function cleanMessage(text) {
2538
+ let s = String(text).replace(/\r\n/g, "\n").trim();
2539
+ s = s.replace(/^```[\w-]*\n([\s\S]*?)\n```$/, "$1").trim();
2540
+ s = s.replace(/^(?:commit message|message)\s*:\s*\n?/i, "").trim();
2541
+ if (/^(["'`]).*\1$/s.test(s)) s = s.slice(1, -1).trim();
2542
+ return s;
2543
+ }
2544
+ var SYSTEM_PROMPT = "You write git commit messages. Reply with the commit message only: no preamble, no quotes, no code fences.";
2545
+ function draftCommand(agent, { claudeCommand = "claude", codexCommand = "codex", outFile, model = null } = {}) {
2546
+ if (agent === "claude") {
2547
+ const quoted = isWindows ? [] : ["--settings", '{"disableAllHooks":true}', "--system-prompt", SYSTEM_PROMPT];
2548
+ return [claudeCommand, "-p", "--model", model || "haiku", "--tools", "", "--no-session-persistence", "--strict-mcp-config", ...quoted];
2549
+ }
2550
+ return [
2551
+ codexCommand,
2552
+ "exec",
2553
+ "--ephemeral",
2554
+ "--skip-git-repo-check",
2555
+ "-s",
2556
+ "read-only",
2557
+ ...model ? ["-m", model] : [],
2558
+ "-c",
2559
+ 'model_reasoning_effort="low"',
2560
+ "-c",
2561
+ "mcp_servers={}",
2562
+ "-o",
2563
+ outFile,
2564
+ "-"
2565
+ ];
2566
+ }
2567
+ function runWithInput(argv2, { cwd, input, timeout = TIMEOUT_MS, env = {} }) {
2568
+ return new Promise((resolve, reject) => {
2569
+ const options = { cwd, env: { ...process.env, CODEREMOTE_NO_HOOK: "1", ...env }, stdio: ["pipe", "pipe", "pipe"] };
2570
+ let child;
2571
+ if (isWindows) child = spawnCommand(argv2, options);
2572
+ else {
2573
+ const { file, args } = terminalCommand(argv2);
2574
+ child = spawn3(file, args, options);
2575
+ }
2576
+ let out = "";
2577
+ let err = "";
2578
+ const timer = setTimeout(() => child.kill(), timeout);
2579
+ child.stdout.on("data", (d) => out += d);
2580
+ child.stderr.on("data", (d) => err += d);
2581
+ child.on("error", (e) => {
2582
+ clearTimeout(timer);
2583
+ reject(e);
2584
+ });
2585
+ child.on("close", (code, signal) => {
2586
+ clearTimeout(timer);
2587
+ if (code === 0) return resolve(out);
2588
+ const why = signal ? "timed out" : (err || out).trim().split("\n").slice(-3).join("\n") || `exited with ${code}`;
2589
+ reject(new HttpError(signal ? 504 : 502, why));
2590
+ });
2591
+ child.stdin.on("error", () => {
2592
+ });
2593
+ child.stdin.end(input);
2594
+ });
2595
+ }
2596
+ async function draftCommitMessage(repo, agent, { claudeCommand, codexCommand, model = null, run: run7 = runWithInput } = {}) {
2597
+ const changes = await pendingChanges(repo);
2598
+ const recent = await hasHead(repo) ? (await git(repo, ["log", "-n12", "--no-merges", "--format=%s"])).stdout.split("\n").filter(Boolean) : [];
2599
+ const input = buildPrompt(changes, recent);
2600
+ let text;
2601
+ if (agent === "codex") {
2602
+ const dir = await fs11.mkdtemp(path9.join(os3.tmpdir(), "coderemote-commit-"));
2603
+ const outFile = path9.join(dir, "message.txt");
2604
+ try {
2605
+ await run7(draftCommand("codex", { codexCommand, outFile, model }), { cwd: repo, input });
2606
+ text = await fs11.readFile(outFile, "utf8");
2607
+ } finally {
2608
+ await fs11.rm(dir, { recursive: true, force: true });
2609
+ }
2610
+ } else {
2611
+ text = await run7(draftCommand("claude", { claudeCommand, model }), { cwd: os3.tmpdir(), input });
2612
+ }
2613
+ const message = cleanMessage(text);
2614
+ if (!message) throw new HttpError(502, "the agent returned no message");
2615
+ return { message, agent, staged: changes.staged };
2616
+ }
2617
+
2618
+ // ../../packages/agent-core/src/handoff.js
2619
+ import { createHmac, timingSafeEqual } from "node:crypto";
2620
+ var NONCE_WINDOW_MS = 5 * 6e4;
2621
+ var REUSE_MIN_LIFETIME_MS = 12 * 36e5;
2622
+ var TunnelHandoff = class {
2623
+ /**
2624
+ * @param {object} opts
2625
+ * @param {string} opts.token the agent's bearer token (the HMAC key)
2626
+ * @param {() => Promise<{token: string, expiresAt: number} | null>} opts.mint
2627
+ */
2628
+ constructor({ token, mint, now = Date.now }) {
2629
+ this.key = token;
2630
+ this.mint = mint;
2631
+ this.now = now;
2632
+ this.used = /* @__PURE__ */ new Map();
2633
+ this.cached = null;
1871
2634
  }
1872
2635
  /** `nonce` is `<unix seconds>.<random>`; each one works once, within five minutes. */
1873
2636
  verify(nonce, sig) {
@@ -1948,102 +2711,150 @@ var LANDING_HTML = `<!doctype html>
1948
2711
  </html>
1949
2712
  `;
1950
2713
 
1951
- // ../../packages/agent-core/src/ports.js
1952
- import { execFile as execFile4 } from "node:child_process";
1953
- import fs10 from "node:fs/promises";
1954
- import net from "node:net";
1955
- import os2 from "node:os";
2714
+ // ../../packages/agent-core/src/lan.js
2715
+ import { X509Certificate } from "node:crypto";
2716
+ import fs12 from "node:fs";
2717
+ import os4 from "node:os";
2718
+ import path10 from "node:path";
1956
2719
 
1957
- // ../../packages/agent-core/src/platform.js
1958
- import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
1959
- import fs9 from "node:fs";
1960
- import path7 from "node:path";
1961
- var isWindows = process.platform === "win32";
1962
- function shellQuote(arg) {
1963
- return `'${String(arg).replace(/'/g, `'\\''`)}'`;
2720
+ // ../../packages/agent-core/src/x509.js
2721
+ import { generateKeyPairSync, randomBytes as randomBytes2, sign } from "node:crypto";
2722
+ function tlv(tag, ...parts) {
2723
+ const body = Buffer.concat(parts);
2724
+ const n = body.length;
2725
+ let len;
2726
+ if (n < 128) len = Buffer.from([n]);
2727
+ else {
2728
+ const bytes2 = [];
2729
+ for (let v = n; v > 0; v >>= 8) bytes2.unshift(v & 255);
2730
+ len = Buffer.from([128 | bytes2.length, ...bytes2]);
2731
+ }
2732
+ return Buffer.concat([Buffer.from([tag]), len, body]);
2733
+ }
2734
+ var seq = (...parts) => tlv(48, ...parts);
2735
+ var set = (...parts) => tlv(49, ...parts);
2736
+ function oid(dotted) {
2737
+ const [a, b, ...rest2] = dotted.split(".").map(Number);
2738
+ const out = [40 * a + b];
2739
+ for (const n of rest2) {
2740
+ const chunk = [n & 127];
2741
+ for (let v = n >> 7; v > 0; v >>= 7) chunk.unshift(128 | v & 127);
2742
+ out.push(...chunk);
2743
+ }
2744
+ return tlv(6, Buffer.from(out));
2745
+ }
2746
+ function utcTime(date) {
2747
+ const s = date.toISOString();
2748
+ return tlv(23, Buffer.from(`${s.slice(2, 4)}${s.slice(5, 7)}${s.slice(8, 10)}${s.slice(11, 13)}${s.slice(14, 16)}${s.slice(17, 19)}Z`));
2749
+ }
2750
+ var ECDSA_SHA256 = seq(oid("1.2.840.10045.4.3.2"));
2751
+ function extension(id, critical, value) {
2752
+ return seq(oid(id), ...critical ? [tlv(1, Buffer.from([255]))] : [], tlv(4, value));
2753
+ }
2754
+ var pem = (label, der) => `-----BEGIN ${label}-----
2755
+ ${der.toString("base64").match(/.{1,64}/g).join("\n")}
2756
+ -----END ${label}-----
2757
+ `;
2758
+ function selfSignedCertificate({ commonName = "coderemote", days = 3650, now = /* @__PURE__ */ new Date() } = {}) {
2759
+ const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
2760
+ const serial = randomBytes2(16);
2761
+ serial[0] = serial[0] & 127 | 64;
2762
+ const name = seq(set(seq(oid("2.5.4.3"), tlv(12, Buffer.from(commonName, "utf8")))));
2763
+ const day = 24 * 60 * 60 * 1e3;
2764
+ const tbs = seq(
2765
+ tlv(160, tlv(2, Buffer.from([2]))),
2766
+ // v3
2767
+ tlv(2, serial),
2768
+ ECDSA_SHA256,
2769
+ name,
2770
+ seq(utcTime(new Date(now.getTime() - day)), utcTime(new Date(now.getTime() + days * day))),
2771
+ name,
2772
+ publicKey.export({ type: "spki", format: "der" }),
2773
+ tlv(
2774
+ 163,
2775
+ seq(
2776
+ extension("2.5.29.19", true, seq()),
2777
+ // basicConstraints: not a CA
2778
+ extension("2.5.29.15", true, tlv(3, Buffer.from([7, 128]))),
2779
+ // keyUsage: digitalSignature
2780
+ extension("2.5.29.37", false, seq(oid("1.3.6.1.5.5.7.3.1")))
2781
+ // extKeyUsage: serverAuth
2782
+ )
2783
+ )
2784
+ );
2785
+ const signature = sign("sha256", tbs, privateKey);
2786
+ const cert = seq(tbs, ECDSA_SHA256, tlv(3, Buffer.from([0]), signature));
2787
+ return {
2788
+ key: privateKey.export({ type: "pkcs8", format: "pem" }),
2789
+ cert: pem("CERTIFICATE", cert)
2790
+ };
1964
2791
  }
1965
- function runnable(file) {
2792
+
2793
+ // ../../packages/agent-core/src/lan.js
2794
+ function certFingerprint(pem2) {
2795
+ return new X509Certificate(pem2).fingerprint256.replace(/:/g, "").toLowerCase();
2796
+ }
2797
+ async function lanCertificate(dir) {
2798
+ const keyFile = path10.join(dir, "key.pem");
2799
+ const certFile = path10.join(dir, "cert.pem");
2800
+ const load = () => {
2801
+ const key = fs12.readFileSync(keyFile, "utf8");
2802
+ const cert = fs12.readFileSync(certFile, "utf8");
2803
+ return { key, cert, fingerprint: certFingerprint(cert) };
2804
+ };
1966
2805
  try {
1967
- if (!isWindows) fs9.accessSync(file, fs9.constants.X_OK);
1968
- return fs9.statSync(file).isFile();
2806
+ return load();
1969
2807
  } catch {
1970
- return false;
1971
2808
  }
1972
- }
1973
- function findExecutable(name, env = process.env) {
1974
- const exts = isWindows && !path7.extname(name) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
1975
- const match = (base2) => exts.map((e) => base2 + e).find(runnable) ?? null;
1976
- if (name.includes("/") || name.includes(path7.sep)) return match(path7.resolve(name));
1977
- for (const dir of (env.PATH ?? env.Path ?? "").split(path7.delimiter)) {
1978
- const found = dir && match(path7.join(dir.replace(/^"(.*)"$/, "$1"), name));
1979
- if (found) return found;
2809
+ try {
2810
+ const { key, cert } = selfSignedCertificate();
2811
+ fs12.mkdirSync(dir, { recursive: true, mode: 448 });
2812
+ fs12.writeFileSync(keyFile, key, { mode: 384 });
2813
+ fs12.chmodSync(keyFile, 384);
2814
+ fs12.writeFileSync(certFile, cert);
2815
+ return load();
2816
+ } catch (e) {
2817
+ for (const f of [keyFile, certFile]) {
2818
+ try {
2819
+ fs12.rmSync(f, { force: true });
2820
+ } catch {
2821
+ }
2822
+ }
2823
+ return { error: `could not create a certificate: ${e.message}` };
1980
2824
  }
1981
- return null;
1982
2825
  }
1983
- function interactiveShell() {
1984
- if (!isWindows) return { file: process.env.SHELL || "/bin/sh", args: ["-l"] };
1985
- return { file: findExecutable("pwsh") ?? "powershell.exe", args: ["-NoLogo"] };
2826
+ var VIRTUAL = /^(utun|ipsec|ppp|bridge|docker|br-|veth|vboxnet|vmnet|vEthernet|awdl|llw|anpi|ap\d|gif|stf|tailscale|zt|wg|VirtualBox|VMware|ZeroTier|Hyper-V|Bluetooth|Local Area Connection\*)/i;
2827
+ function privateV4(address) {
2828
+ const [a, b] = address.split(".").map(Number);
2829
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
1986
2830
  }
1987
- function cmdQuote(arg) {
1988
- const s = String(arg);
1989
- if (/["%\r\n]/.test(s)) throw new Error(`${JSON.stringify(s)} can't be passed through cmd.exe`);
1990
- return `"${s.replace(/(\\+)$/, "$1$1")}"`;
1991
- }
1992
- function resolveCommand(argv2) {
1993
- const [name, ...rest2] = argv2;
1994
- if (!isWindows) return { file: name, args: rest2, verbatim: false };
1995
- const file = findExecutable(name) ?? name;
1996
- if (!/\.(bat|cmd)$/i.test(file)) return { file, args: rest2, verbatim: false };
1997
- const line = [file, ...rest2].map(cmdQuote).join(" ");
1998
- return { file: process.env.ComSpec || "cmd.exe", args: ["/d", "/v:off", "/s", "/c", `"${line}"`], verbatim: true };
1999
- }
2000
- function terminalCommand(argv2) {
2001
- if (!isWindows) return { file: interactiveShell().file, args: ["-l", "-c", `exec ${argv2.map(shellQuote).join(" ")}`] };
2002
- const { file, args, verbatim } = resolveCommand(argv2);
2003
- return { file, args: verbatim ? args.join(" ") : args };
2004
- }
2005
- function spawnCommand(argv2, options = {}) {
2006
- const { file, args, verbatim } = resolveCommand(argv2);
2007
- return spawn2(file, args, { ...options, windowsVerbatimArguments: verbatim });
2008
- }
2009
- function execCommand(argv2, options = {}) {
2010
- return new Promise((resolve, reject) => {
2011
- const { file, args, verbatim } = resolveCommand(argv2);
2012
- execFile3(
2013
- file,
2014
- args,
2015
- { ...options, windowsVerbatimArguments: verbatim },
2016
- (err, stdout, stderr) => err ? reject(Object.assign(err, { stdout, stderr })) : resolve({ stdout, stderr })
2017
- );
2018
- });
2019
- }
2020
- function loginShell(script, timeout) {
2021
- return new Promise((resolve) => {
2022
- execFile3(
2023
- interactiveShell().file,
2024
- ["-l", "-c", script],
2025
- { timeout, maxBuffer: 16 * 1024 * 1024 },
2026
- (err, out) => resolve(err ? null : out)
2027
- );
2028
- });
2029
- }
2030
- async function capture(argv2, { timeout = 2e4 } = {}) {
2031
- if (!isWindows) return loginShell(argv2.map(shellQuote).join(" "), timeout);
2032
- try {
2033
- return (await execCommand(argv2, { timeout, maxBuffer: 16 * 1024 * 1024 })).stdout;
2034
- } catch {
2035
- return null;
2831
+ function lanAddresses(interfaces = os4.networkInterfaces()) {
2832
+ const v4 = [];
2833
+ const v6 = [];
2834
+ for (const [name, addrs] of Object.entries(interfaces)) {
2835
+ if (VIRTUAL.test(name)) continue;
2836
+ for (const a of addrs ?? []) {
2837
+ if (a.internal) continue;
2838
+ const family = a.family === 4 ? "IPv4" : a.family === 6 ? "IPv6" : a.family;
2839
+ if (family === "IPv4" && !a.address.startsWith("169.254.")) v4.push(a.address);
2840
+ if (family === "IPv6" && !/^fe[89ab]/i.test(a.address)) v6.push(a.address);
2841
+ }
2036
2842
  }
2843
+ return [.../* @__PURE__ */ new Set([...v4.filter(privateV4), ...v4.filter((a) => !privateV4(a)), ...v6])];
2037
2844
  }
2038
- async function commandExists(command) {
2039
- if (isWindows) return findExecutable(command) !== null;
2040
- return Boolean((await loginShell(`command -v ${shellQuote(command)}`, 2e4))?.trim());
2845
+ function qrAddresses(addresses) {
2846
+ const firstV6 = addresses.find((a) => a.includes(":"));
2847
+ return addresses.filter((a) => !a.includes(":") || a === firstV6).slice(0, 3);
2041
2848
  }
2042
2849
 
2043
2850
  // ../../packages/agent-core/src/ports.js
2044
- function run2(cmd2, args) {
2851
+ import { execFile as execFile5 } from "node:child_process";
2852
+ import fs13 from "node:fs/promises";
2853
+ import net from "node:net";
2854
+ import os5 from "node:os";
2855
+ function run3(cmd2, args) {
2045
2856
  return new Promise((resolve, reject) => {
2046
- execFile4(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
2857
+ execFile5(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
2047
2858
  if (err?.code === "ENOENT") reject(err);
2048
2859
  else resolve(stdout || "");
2049
2860
  });
@@ -2057,7 +2868,7 @@ function splitHostPort(name) {
2057
2868
  }
2058
2869
  async function listeningSockets() {
2059
2870
  try {
2060
- const out2 = await run2("lsof", ["-nP", "-a", "-u", String(os2.userInfo().uid), "-iTCP", "-sTCP:LISTEN", "-F", "pcn"]);
2871
+ const out2 = await run3("lsof", ["-nP", "-a", "-u", String(os5.userInfo().uid), "-iTCP", "-sTCP:LISTEN", "-F", "pcn"]);
2061
2872
  const sockets2 = [];
2062
2873
  let pid;
2063
2874
  let name;
@@ -2072,9 +2883,9 @@ async function listeningSockets() {
2072
2883
  }
2073
2884
  return sockets2;
2074
2885
  } catch (e) {
2075
- if (e.code !== "ENOENT" || os2.platform() !== "linux") throw e;
2886
+ if (e.code !== "ENOENT" || os5.platform() !== "linux") throw e;
2076
2887
  }
2077
- const out = await run2("ss", ["-Hltnp"]);
2888
+ const out = await run3("ss", ["-Hltnp"]);
2078
2889
  const sockets = [];
2079
2890
  for (const line of out.split("\n")) {
2080
2891
  const cols = line.trim().split(/\s+/);
@@ -2085,7 +2896,7 @@ async function listeningSockets() {
2085
2896
  return sockets;
2086
2897
  }
2087
2898
  async function processTable() {
2088
- const out = await run2("ps", ["-A", "-ww", "-o", "pid=,ppid=,tty=,command="]);
2899
+ const out = await run3("ps", ["-A", "-ww", "-o", "pid=,ppid=,tty=,command="]);
2089
2900
  const table = /* @__PURE__ */ new Map();
2090
2901
  for (const line of out.split("\n")) {
2091
2902
  const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
@@ -2096,11 +2907,11 @@ async function processTable() {
2096
2907
  async function workingDirs(pids) {
2097
2908
  const dirs = /* @__PURE__ */ new Map();
2098
2909
  if (!pids.length || isWindows) return dirs;
2099
- if (os2.platform() === "linux") {
2100
- await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs10.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
2910
+ if (os5.platform() === "linux") {
2911
+ await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs13.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
2101
2912
  return dirs;
2102
2913
  }
2103
- const out = await run2("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
2914
+ const out = await run3("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
2104
2915
  let pid;
2105
2916
  for (const line of out.split("\n")) {
2106
2917
  if (line[0] === "p") pid = parseInt(line.slice(1), 10);
@@ -2112,9 +2923,9 @@ var WINDOWS_SHELLS = /* @__PURE__ */ new Set(["cmd.exe", "powershell.exe", "pwsh
2112
2923
  async function windowsListeners() {
2113
2924
  const query = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
2114
2925
  const [v4, v6, json] = await Promise.all([
2115
- run2("netstat", ["-ano", "-p", "TCP"]),
2116
- run2("netstat", ["-ano", "-p", "TCPv6"]),
2117
- run2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query])
2926
+ run3("netstat", ["-ano", "-p", "TCP"]),
2927
+ run3("netstat", ["-ano", "-p", "TCPv6"]),
2928
+ run3("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query])
2118
2929
  ]);
2119
2930
  const table = /* @__PURE__ */ new Map();
2120
2931
  for (const p of [JSON.parse(json || "[]")].flat()) {
@@ -2201,18 +3012,18 @@ function isListening(port2, timeout = 2e3) {
2201
3012
  }
2202
3013
 
2203
3014
  // ../../packages/agent-core/src/push.js
2204
- import { execFile as execFile5 } from "node:child_process";
2205
- import { createCipheriv, randomBytes as randomBytes2 } from "node:crypto";
2206
- import os3 from "node:os";
2207
- import { promisify } from "node:util";
2208
- var run3 = promisify(execFile5);
2209
- var FINISHED_AFTER = 60 * 1e3;
3015
+ import { execFile as execFile6 } from "node:child_process";
3016
+ import { createCipheriv, randomBytes as randomBytes3 } from "node:crypto";
3017
+ import os6 from "node:os";
3018
+ import { promisify as promisify2 } from "node:util";
3019
+ var run4 = promisify2(execFile6);
3020
+ var FINISHED_AFTER = 30 * 1e3;
3021
+ var REPLY_CHARS = 180;
2210
3022
  var ACTIVE_WITHIN = 2 * 60;
2211
- var AT_SESSION_WITHIN = 30;
2212
3023
  async function macIdleSeconds() {
2213
- if (os3.platform() !== "darwin") return null;
3024
+ if (os6.platform() !== "darwin") return null;
2214
3025
  try {
2215
- const { stdout } = await run3("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-k", "HIDIdleTime"], { timeout: 2e3 });
3026
+ const { stdout } = await run4("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-k", "HIDIdleTime"], { timeout: 2e3 });
2216
3027
  const ns = stdout.match(/"HIDIdleTime"\s*=\s*(\d+)/)?.[1];
2217
3028
  return ns ? Number(BigInt(ns) / 1000000000n) : null;
2218
3029
  } catch {
@@ -2223,25 +3034,8 @@ async function userActiveHere() {
2223
3034
  const idle = await macIdleSeconds();
2224
3035
  return idle !== null && idle < ACTIVE_WITHIN;
2225
3036
  }
2226
- async function frontAppHere() {
2227
- if (os3.platform() !== "darwin") return null;
2228
- try {
2229
- const { stdout: asn } = await run3("lsappinfo", ["front"], { timeout: 2e3 });
2230
- if (!asn.trim()) return null;
2231
- const { stdout } = await run3("lsappinfo", ["info", asn.trim()], { timeout: 2e3 });
2232
- return stdout.match(/bundle path="([^"]+)"/)?.[1] ?? null;
2233
- } catch {
2234
- return null;
2235
- }
2236
- }
2237
- async function atSessionHere(session, { idle = macIdleSeconds, front = frontAppHere } = {}) {
2238
- if (!session || session.terminal) return false;
2239
- const seconds = await idle();
2240
- if (seconds === null || seconds >= AT_SESSION_WITHIN) return false;
2241
- return !session.app || await front() === session.app;
2242
- }
2243
3037
  function seal(key, data) {
2244
- const nonce = randomBytes2(12);
3038
+ const nonce = randomBytes3(12);
2245
3039
  const cipher = createCipheriv("aes-256-gcm", key, nonce);
2246
3040
  const body = Buffer.concat([cipher.update(JSON.stringify(data), "utf8"), cipher.final()]);
2247
3041
  return Buffer.concat([nonce, body, cipher.getAuthTag()]).toString("base64");
@@ -2284,15 +3078,26 @@ function describeRequest(request) {
2284
3078
  return detail2 ? `${request.tool}: ${String(detail2).split("\n")[0].slice(0, 200)}` : `Allow ${request.tool}?`;
2285
3079
  }
2286
3080
  function minutes(ms) {
3081
+ if (ms < 6e4) return `${Math.round(ms / 1e3)} s`;
2287
3082
  const m = Math.round(ms / 6e4);
2288
3083
  return m < 60 ? `${m} min` : `${Math.floor(m / 60)} h ${m % 60} min`;
2289
3084
  }
3085
+ function lastReply(entries2) {
3086
+ for (let i = entries2.length - 1; i >= 0; i--) {
3087
+ const e = entries2[i];
3088
+ if (e.kind === "prompt" || e.kind === "turn_end" && i < entries2.length - 1) return null;
3089
+ if (e.kind !== "assistant" || !e.text?.trim()) continue;
3090
+ const text = e.text.replace(/```[\s\S]*?(```|$)/g, " ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/^\s{0,3}(#{1,6}\s+|>\s?|[-*+]\s+|\d+\.\s+)/gm, "").replace(/[*_`~|]/g, "").replace(/\s+/g, " ").trim();
3091
+ if (!text) continue;
3092
+ return text.length > REPLY_CHARS ? `${text.slice(0, REPLY_CHARS - 1).trimEnd()}\u2026` : text;
3093
+ }
3094
+ return null;
3095
+ }
2290
3096
  var PushSender = class {
2291
3097
  #devices;
2292
3098
  #store;
2293
3099
  #hub;
2294
3100
  #shows;
2295
- #atSession;
2296
3101
  #fetch;
2297
3102
  // Requests a notification went out for, so their resolution can replace it.
2298
3103
  #notified = /* @__PURE__ */ new Set();
@@ -2302,16 +3107,13 @@ var PushSender = class {
2302
3107
  * @param {{ load: () => object[], save: (devices: object[]) => void }} [opts.store]
2303
3108
  * @param {(what: { session?: string, builds?: boolean }) => boolean} [opts.shows] whether a
2304
3109
  * client shows that session (or the builds) right now
2305
- * @param {(host: { terminal: string | null, app: string | null } | null) => Promise<boolean>} [opts.atSession]
2306
- * whether someone at the computer is working in the session running there
2307
3110
  */
2308
- constructor({ hub, store = null, shows = () => false, atSession = atSessionHere, fetch: fetch2 = globalThis.fetch }) {
3111
+ constructor({ hub, store = null, shows = () => false, fetch: fetch3 = globalThis.fetch }) {
2309
3112
  this.#hub = hub;
2310
3113
  this.#store = store;
2311
3114
  this.#devices = store?.load() ?? [];
2312
3115
  this.#shows = shows;
2313
- this.#atSession = atSession;
2314
- this.#fetch = fetch2;
3116
+ this.#fetch = fetch3;
2315
3117
  hub.on("message", (msg) => this.#follow(msg).catch(() => {
2316
3118
  }));
2317
3119
  }
@@ -2343,21 +3145,22 @@ var PushSender = class {
2343
3145
  #save() {
2344
3146
  this.#store?.save(this.#devices);
2345
3147
  }
2346
- /** No push about a session someone is looking at, on a client or at the computer. */
2347
- async #quiet(what) {
3148
+ /**
3149
+ * No push about a session a client shows, nor about one started outside Code Remote: that one
3150
+ * runs in a terminal of the computer's own, so it is followed there (or wherever else it is
3151
+ * driven from, like Claude's own remote control), not from the phone.
3152
+ */
3153
+ #quiet(what) {
2348
3154
  if (this.#shows(what)) return true;
2349
3155
  if (!what.session) return false;
2350
- try {
2351
- return await this.#atSession(this.#hub.host(what.session));
2352
- } catch {
2353
- return false;
2354
- }
3156
+ const host = this.#hub.host(what.session);
3157
+ return !host || !host.terminal;
2355
3158
  }
2356
3159
  async #follow(msg) {
2357
3160
  if (!this.#devices.length) return;
2358
3161
  if (msg.type === "request") {
2359
3162
  const { request } = msg;
2360
- if (await this.#quiet({ session: request.session })) return;
3163
+ if (this.#quiet({ session: request.session })) return;
2361
3164
  if (!this.#hub.requests().some((r) => r.id === request.id)) return;
2362
3165
  this.#notified.add(request.id);
2363
3166
  const question = request.kind === "question";
@@ -2389,12 +3192,19 @@ var PushSender = class {
2389
3192
  fallback: "Answered on your computer"
2390
3193
  });
2391
3194
  } else if (msg.type === "entry" && msg.entry.kind === "turn_end" && msg.entry.duration >= FINISHED_AFTER) {
2392
- if (await this.#quiet({ session: msg.session })) return;
3195
+ if (this.#quiet({ session: msg.session })) return;
2393
3196
  const session = this.#hub.session(msg.session);
3197
+ let reply = null;
3198
+ try {
3199
+ reply = lastReply(this.#hub.timeline(msg.session).entries);
3200
+ } catch {
3201
+ }
3202
+ const took = `Finished after ${minutes(msg.entry.duration)}`;
2394
3203
  await this.#broadcast("finished", {
2395
3204
  kind: "finished",
2396
3205
  title: session?.title ?? "Claude Code",
2397
- body: `Finished after ${minutes(msg.entry.duration)}`,
3206
+ body: reply ? `${reply}
3207
+ ${took}` : took,
2398
3208
  session: msg.session,
2399
3209
  category: "DONE",
2400
3210
  fallback: "A session finished"
@@ -2404,7 +3214,7 @@ var PushSender = class {
2404
3214
  /** A build ended (see builds.js): ready to install, or failed. Cancelled builds say nothing. */
2405
3215
  async buildEnded(build) {
2406
3216
  if (!this.#devices.length || build.state === "cancelled") return;
2407
- if (await this.#quiet({ builds: true })) return;
3217
+ if (this.#quiet({ builds: true })) return;
2408
3218
  const ok = build.state === "succeeded";
2409
3219
  const firstError = String(build.error ?? "").split("\n").find((l) => l.trim()) ?? "";
2410
3220
  await this.#broadcast("finished", {
@@ -2635,9 +3445,9 @@ function detail(e) {
2635
3445
  }
2636
3446
 
2637
3447
  // ../../packages/agent-core/src/search.js
2638
- import { execFile as execFile6, spawn as spawn3 } from "node:child_process";
2639
- import fs11 from "node:fs/promises";
2640
- import path8 from "node:path";
3448
+ import { execFile as execFile7, spawn as spawn4 } from "node:child_process";
3449
+ import fs14 from "node:fs/promises";
3450
+ import path11 from "node:path";
2641
3451
  import readline from "node:readline";
2642
3452
  var ALWAYS_SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules"]);
2643
3453
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
@@ -2649,7 +3459,7 @@ var NAME_LIMIT = 100;
2649
3459
  var DEFAULT_LIMIT = 500;
2650
3460
  var MAX_LIMIT = 2e3;
2651
3461
  var PER_FILE = 100;
2652
- var TIMEOUT_MS = 15e3;
3462
+ var TIMEOUT_MS2 = 15e3;
2653
3463
  var rgPath;
2654
3464
  function ripgrep() {
2655
3465
  if (rgPath === void 0) rgPath = findExecutable("rg");
@@ -2742,7 +3552,7 @@ function preview(line, re) {
2742
3552
  }
2743
3553
  function runGit(cwd, args) {
2744
3554
  return new Promise((resolve) => {
2745
- execFile6(
3555
+ execFile7(
2746
3556
  "git",
2747
3557
  args,
2748
3558
  { cwd, maxBuffer: 256 * 1024 * 1024, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } },
@@ -2752,7 +3562,7 @@ function runGit(cwd, args) {
2752
3562
  }
2753
3563
  async function exists(p) {
2754
3564
  try {
2755
- await fs11.lstat(p);
3565
+ await fs14.lstat(p);
2756
3566
  return true;
2757
3567
  } catch {
2758
3568
  return false;
@@ -2786,21 +3596,21 @@ async function listFiles(root2, signal) {
2786
3596
  if (rel.endsWith("/")) subdirs.push(rel.slice(0, -1));
2787
3597
  else files.push(join(prefix, rel));
2788
3598
  }
2789
- for (const rel of subdirs) await visit(path8.join(dir, rel), join(prefix, rel));
3599
+ for (const rel of subdirs) await visit(path11.join(dir, rel), join(prefix, rel));
2790
3600
  return true;
2791
3601
  }
2792
3602
  async function visit(dir, prefix) {
2793
3603
  if (signal?.aborted) return;
2794
- if (await exists(path8.join(dir, ".git")) && await fromGit(dir, prefix)) return;
3604
+ if (await exists(path11.join(dir, ".git")) && await fromGit(dir, prefix)) return;
2795
3605
  let entries2;
2796
3606
  try {
2797
- entries2 = await fs11.readdir(dir, { withFileTypes: true });
3607
+ entries2 = await fs14.readdir(dir, { withFileTypes: true });
2798
3608
  } catch {
2799
3609
  return;
2800
3610
  }
2801
3611
  for (const e of entries2) {
2802
3612
  if (ALWAYS_SKIPPED.has(e.name)) continue;
2803
- if (e.isDirectory()) await visit(path8.join(dir, e.name), join(prefix, e.name));
3613
+ if (e.isDirectory()) await visit(path11.join(dir, e.name), join(prefix, e.name));
2804
3614
  else if (e.isFile()) files.push(join(prefix, e.name));
2805
3615
  }
2806
3616
  }
@@ -2837,17 +3647,17 @@ var Results = class {
2837
3647
  }
2838
3648
  };
2839
3649
  async function searchFile(root2, rel, test, re, results) {
2840
- const abs = path8.join(root2, ...rel.split("/"));
3650
+ const abs = path11.join(root2, ...rel.split("/"));
2841
3651
  let stat;
2842
3652
  try {
2843
- stat = await fs11.lstat(abs);
3653
+ stat = await fs14.lstat(abs);
2844
3654
  } catch {
2845
3655
  return;
2846
3656
  }
2847
3657
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.size === 0) return;
2848
3658
  let buf;
2849
3659
  try {
2850
- buf = await fs11.readFile(abs);
3660
+ buf = await fs14.readFile(abs);
2851
3661
  } catch {
2852
3662
  return;
2853
3663
  }
@@ -2871,7 +3681,7 @@ async function searchHere(root2, files, test, re, results, signal) {
2871
3681
  }
2872
3682
  function runRipgrep(rg, root2, args, onLine, signal) {
2873
3683
  return new Promise((resolve, reject) => {
2874
- const child = spawn3(rg, ["--no-config", "--no-messages", "--hidden", "--glob", "!.git", "--glob", "!node_modules", ...args], {
3684
+ const child = spawn4(rg, ["--no-config", "--no-messages", "--hidden", "--glob", "!.git", "--glob", "!node_modules", ...args], {
2875
3685
  cwd: root2,
2876
3686
  stdio: ["ignore", "pipe", "pipe"]
2877
3687
  });
@@ -2918,7 +3728,7 @@ async function search({
2918
3728
  const rg = engine === "node" ? null : ripgrep();
2919
3729
  if (engine === "rg" && !rg) throw new HttpError(500, "ripgrep is not installed");
2920
3730
  const timeout = new AbortController();
2921
- const timer = setTimeout(() => timeout.abort(), TIMEOUT_MS);
3731
+ const timer = setTimeout(() => timeout.abort(), TIMEOUT_MS2);
2922
3732
  const stop = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
2923
3733
  const results = new Results(limit);
2924
3734
  const names = [];
@@ -2988,9 +3798,9 @@ async function search({
2988
3798
  }
2989
3799
 
2990
3800
  // ../../packages/agent-core/src/sessions.js
2991
- import { execFile as execFile7 } from "node:child_process";
2992
- import { randomUUID as randomUUID3 } from "node:crypto";
2993
- import os4 from "node:os";
3801
+ import { execFile as execFile8 } from "node:child_process";
3802
+ import { randomUUID as randomUUID4 } from "node:crypto";
3803
+ import os7 from "node:os";
2994
3804
  import pty from "node-pty";
2995
3805
  var SCROLLBACK_LIMIT = 256 * 1024;
2996
3806
  function guardConpty(term) {
@@ -3009,7 +3819,7 @@ var TerminalSession = class {
3009
3819
  * terminalCommand says; the session ends when it exits.
3010
3820
  */
3011
3821
  constructor({ cols, rows, cwd, env = {}, command = null }) {
3012
- this.id = randomUUID3();
3822
+ this.id = randomUUID4();
3013
3823
  this.createdAt = Date.now();
3014
3824
  this.chunks = [];
3015
3825
  this.bufferedBytes = 0;
@@ -3022,7 +3832,7 @@ var TerminalSession = class {
3022
3832
  name: "xterm-256color",
3023
3833
  cols,
3024
3834
  rows,
3025
- cwd: cwd || os4.homedir(),
3835
+ cwd: cwd || os7.homedir(),
3026
3836
  // CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
3027
3837
  env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
3028
3838
  });
@@ -3045,199 +3855,102 @@ var TerminalSession = class {
3045
3855
  this.bufferedBytes -= this.chunks.shift().length;
3046
3856
  }
3047
3857
  for (const ws of this.clients) ws.send(buf, { binary: true });
3858
+ this.onOutput?.(buf);
3048
3859
  }
3049
- attach(ws) {
3050
- this.clients.add(ws);
3051
- const replay = Buffer.concat(this.chunks);
3052
- ws.send(JSON.stringify({ type: "session", id: this.id, shell: this.shell, replay: replay.length }));
3053
- if (replay.length) ws.send(replay, { binary: true });
3054
- ws.on("message", (data, isBinary) => {
3055
- if (isBinary) {
3056
- this.write(data.toString("utf8"));
3057
- return;
3058
- }
3059
- let msg;
3060
- try {
3061
- msg = JSON.parse(data.toString("utf8"));
3062
- } catch {
3063
- return;
3064
- }
3065
- if (msg.type === "input" && typeof msg.data === "string") this.write(msg.data);
3066
- else if (msg.type === "resize") this.resize(msg.cols, msg.rows);
3067
- });
3068
- ws.on("close", () => this.clients.delete(ws));
3069
- }
3070
- resize(cols, rows) {
3071
- if (this.exitCode === null && Number.isInteger(cols) && Number.isInteger(rows) && cols > 0 && rows > 0) {
3072
- this.pty.resize(cols, rows);
3073
- }
3074
- }
3075
- /** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
3076
- kill(signal) {
3077
- if (!isWindows) this.pty.kill(signal);
3078
- else if (this.exitCode === null) execFile7("taskkill", ["/T", "/F", "/PID", String(this.pty.pid)], () => {
3079
- });
3080
- }
3081
- /** Types into the session, as a client's keyboard would; nothing once it has exited. */
3082
- write(data) {
3083
- if (this.exitCode === null) this.pty.write(data);
3084
- }
3085
- toJSON() {
3086
- return {
3087
- id: this.id,
3088
- shell: this.shell,
3089
- pid: this.pty.pid,
3090
- title: this.pty.process,
3091
- createdAt: this.createdAt,
3092
- attached: this.clients.size
3093
- };
3094
- }
3095
- };
3096
- var SessionManager = class {
3097
- #sessions = /* @__PURE__ */ new Map();
3098
- /** @param {{ onExit?: (id: string) => void }} [opts] */
3099
- constructor({ onExit } = {}) {
3100
- this.onExit = onExit;
3101
- }
3102
- create(opts) {
3103
- const session = new TerminalSession(opts);
3104
- session.onExit = () => {
3105
- this.#sessions.delete(session.id);
3106
- this.onExit?.(session.id);
3107
- };
3108
- this.#sessions.set(session.id, session);
3109
- return session;
3110
- }
3111
- get(id) {
3112
- return this.#sessions.get(id);
3113
- }
3114
- list() {
3115
- return [...this.#sessions.values()].map((s) => s.toJSON());
3116
- }
3117
- /**
3118
- * Ends every session; resolves once they have all exited, or after `timeout` ms. Wait for it
3119
- * before exiting: on Windows terminals end asynchronously (see releaseConpty), and exiting
3120
- * while node-pty still holds a console open can crash the process.
3121
- */
3122
- killAll(timeout = 3e3) {
3123
- const exited = [...this.#sessions.values()].map((s) => {
3124
- const done = new Promise((resolve) => s.pty.onExit(resolve));
3125
- s.kill();
3126
- return done;
3127
- });
3128
- let timer;
3129
- const late = new Promise((resolve) => {
3130
- timer = setTimeout(resolve, timeout);
3131
- });
3132
- return Promise.race([Promise.all(exited), late]).finally(() => clearTimeout(timer));
3133
- }
3134
- };
3135
-
3136
- // ../../packages/agent-core/src/uploads.js
3137
- import { execFile as execFile8 } from "node:child_process";
3138
- import { randomUUID as randomUUID4 } from "node:crypto";
3139
- import { createWriteStream } from "node:fs";
3140
- import fs12 from "node:fs/promises";
3141
- import os5 from "node:os";
3142
- import path9 from "node:path";
3143
- import { Transform } from "node:stream";
3144
- import { pipeline as pipeline2 } from "node:stream/promises";
3145
- import { promisify as promisify2 } from "node:util";
3146
- var run4 = promisify2(execFile8);
3147
- var UPLOAD_LIMIT = 100 * 1024 * 1024;
3148
- var KEEP = 7 * 24 * 60 * 60 * 1e3;
3149
- var ID = /^[0-9a-f-]{36}$/;
3150
- function safeName(name) {
3151
- const base2 = path9.basename(String(name ?? "").replace(/\\/g, "/"));
3152
- const clean = base2.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
3153
- return clean || "file";
3154
- }
3155
- var UploadStore = class {
3156
- constructor(dir, { now = Date.now } = {}) {
3157
- this.dir = dir;
3158
- this.now = now;
3159
- }
3160
- /** Streams `body` to disk. 413 past `limit`; HEIC photos become JPEG on macOS. */
3161
- async save(body, { name, type, length, limit = UPLOAD_LIMIT }) {
3162
- if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
3163
- const id = randomUUID4();
3164
- const day = new Date(this.now()).toISOString().slice(0, 10);
3165
- await fs12.mkdir(path9.join(this.dir, day), { recursive: true, mode: 448 });
3166
- let file = path9.join(this.dir, day, `${id}-${safeName(name)}`);
3167
- let size = 0;
3168
- const counter = new Transform({
3169
- transform(chunk, _enc, done) {
3170
- size += chunk.length;
3171
- done(size > limit ? new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`) : null, chunk);
3172
- }
3173
- });
3174
- try {
3175
- await pipeline2(body, counter, createWriteStream(file, { mode: 384 }));
3176
- } catch (e) {
3177
- await fs12.rm(file, { force: true });
3178
- throw e;
3179
- }
3180
- let kind = type || "application/octet-stream";
3181
- if ((kind === "image/heic" || /\.heic$/i.test(file)) && os5.platform() === "darwin") {
3182
- const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
3183
- try {
3184
- await run4("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
3185
- await fs12.rm(file);
3186
- file = jpeg;
3187
- kind = "image/jpeg";
3188
- size = (await fs12.stat(file)).size;
3189
- } catch {
3190
- }
3191
- }
3192
- return { id, name: path9.basename(file).slice(id.length + 1), type: kind, size, path: file };
3193
- }
3194
- /** The file of an upload; 404 once it's gone (swept, or never existed). */
3195
- async path(id) {
3196
- if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
3197
- let days = [];
3198
- try {
3199
- days = (await fs12.readdir(this.dir)).sort().reverse();
3200
- } catch {
3201
- }
3202
- for (const day of days) {
3203
- let names = [];
3204
- try {
3205
- names = await fs12.readdir(path9.join(this.dir, day));
3206
- } catch {
3207
- }
3208
- const hit = names.find((n) => n.startsWith(`${id}-`));
3209
- if (hit) return path9.join(this.dir, day, hit);
3210
- }
3211
- throw new HttpError(404, "no such upload (they are kept for 7 days)");
3212
- }
3213
- /** Deletes uploads older than a week. */
3214
- async sweep() {
3215
- let days = [];
3216
- try {
3217
- days = await fs12.readdir(this.dir);
3218
- } catch {
3219
- return;
3220
- }
3221
- const cutoff = this.now() - KEEP;
3222
- for (const day of days) {
3223
- const dir = path9.join(this.dir, day);
3224
- let names = [];
3860
+ attach(ws) {
3861
+ this.clients.add(ws);
3862
+ const replay = Buffer.concat(this.chunks);
3863
+ ws.send(JSON.stringify({ type: "session", id: this.id, shell: this.shell, replay: replay.length }));
3864
+ if (replay.length) ws.send(replay, { binary: true });
3865
+ ws.on("message", (data, isBinary) => {
3866
+ if (isBinary) {
3867
+ this.write(data.toString("utf8"));
3868
+ return;
3869
+ }
3870
+ let msg;
3225
3871
  try {
3226
- names = await fs12.readdir(dir);
3872
+ msg = JSON.parse(data.toString("utf8"));
3227
3873
  } catch {
3228
- continue;
3229
- }
3230
- for (const n of names) {
3231
- const file = path9.join(dir, n);
3232
- const stat = await fs12.stat(file).catch(() => null);
3233
- if (stat && stat.mtimeMs < cutoff) await fs12.rm(file, { force: true });
3874
+ return;
3234
3875
  }
3235
- if (!(await fs12.readdir(dir).catch(() => [])).length) await fs12.rm(dir, { recursive: true, force: true });
3876
+ if (msg.type === "input" && typeof msg.data === "string") this.write(msg.data);
3877
+ else if (msg.type === "resize") this.resize(msg.cols, msg.rows);
3878
+ });
3879
+ ws.on("close", () => this.clients.delete(ws));
3880
+ }
3881
+ resize(cols, rows) {
3882
+ if (this.exitCode === null && Number.isInteger(cols) && Number.isInteger(rows) && cols > 0 && rows > 0) {
3883
+ this.pty.resize(cols, rows);
3236
3884
  }
3237
3885
  }
3886
+ /** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
3887
+ kill(signal) {
3888
+ if (!isWindows) this.pty.kill(signal);
3889
+ else if (this.exitCode === null) execFile8("taskkill", ["/T", "/F", "/PID", String(this.pty.pid)], () => {
3890
+ });
3891
+ }
3892
+ /** Types into the session, as a client's keyboard would; nothing once it has exited. */
3893
+ write(data) {
3894
+ if (this.exitCode === null) this.pty.write(data);
3895
+ }
3896
+ toJSON() {
3897
+ return {
3898
+ id: this.id,
3899
+ shell: this.shell,
3900
+ pid: this.pty.pid,
3901
+ title: this.pty.process,
3902
+ createdAt: this.createdAt,
3903
+ attached: this.clients.size
3904
+ };
3905
+ }
3906
+ };
3907
+ var SessionManager = class {
3908
+ #sessions = /* @__PURE__ */ new Map();
3909
+ /** @param {{ onExit?: (id: string) => void, onOutput?: (id: string, buf: Buffer) => void }} [opts] */
3910
+ constructor({ onExit, onOutput } = {}) {
3911
+ this.onExit = onExit;
3912
+ this.onOutput = onOutput;
3913
+ }
3914
+ create(opts) {
3915
+ const session = new TerminalSession(opts);
3916
+ session.onExit = () => {
3917
+ this.#sessions.delete(session.id);
3918
+ this.onExit?.(session.id);
3919
+ };
3920
+ session.onOutput = (buf) => this.onOutput?.(session.id, buf);
3921
+ this.#sessions.set(session.id, session);
3922
+ return session;
3923
+ }
3924
+ get(id) {
3925
+ return this.#sessions.get(id);
3926
+ }
3927
+ list() {
3928
+ return [...this.#sessions.values()].map((s) => s.toJSON());
3929
+ }
3930
+ /**
3931
+ * Ends every session; resolves once they have all exited, or after `timeout` ms. Wait for it
3932
+ * before exiting: on Windows terminals end asynchronously (see releaseConpty), and exiting
3933
+ * while node-pty still holds a console open can crash the process.
3934
+ */
3935
+ killAll(timeout = 3e3) {
3936
+ const exited = [...this.#sessions.values()].map((s) => {
3937
+ const done = new Promise((resolve) => s.pty.onExit(resolve));
3938
+ s.kill();
3939
+ return done;
3940
+ });
3941
+ let timer;
3942
+ const late = new Promise((resolve) => {
3943
+ timer = setTimeout(resolve, timeout);
3944
+ });
3945
+ return Promise.race([Promise.all(exited), late]).finally(() => clearTimeout(timer));
3946
+ }
3238
3947
  };
3239
3948
 
3240
3949
  // ../../packages/agent-core/src/server.js
3950
+ var OVER_LAN = Symbol("overLan");
3951
+ function tunnelOnly(method, pathname) {
3952
+ return pathname === TUNNEL_HANDOFF_PATH || pathname === "/api/tunnel" || pathname.startsWith("/api/tunnel/") || pathname === "/api/preview" || pathname.startsWith("/api/preview/") || `${method} ${pathname}` === Routes.buildsInstall || `${method} ${pathname}` === Routes.agentsHook;
3953
+ }
3241
3954
  function kindOf(stat) {
3242
3955
  if (stat.isDirectory()) return "dir";
3243
3956
  if (stat.isFile()) return "file";
@@ -3245,11 +3958,11 @@ function kindOf(stat) {
3245
3958
  return "other";
3246
3959
  }
3247
3960
  async function describe(fullPath, name) {
3248
- const lst = await fs13.lstat(fullPath);
3961
+ const lst = await fs15.lstat(fullPath);
3249
3962
  const entry3 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
3250
3963
  if (lst.isSymbolicLink()) {
3251
3964
  try {
3252
- entry3.target = kindOf(await fs13.stat(fullPath));
3965
+ entry3.target = kindOf(await fs15.stat(fullPath));
3253
3966
  } catch {
3254
3967
  entry3.target = "broken";
3255
3968
  }
@@ -3291,6 +4004,15 @@ function typeable(text) {
3291
4004
  return text.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
3292
4005
  }
3293
4006
  var LAUNCH_MODES = ["manual", "acceptEdits", "plan", "auto"];
4007
+ var MODE_TIMEOUT = 2e3;
4008
+ var MODE_NAMES = {
4009
+ manual: "Manual mode",
4010
+ acceptEdits: "Accept edits",
4011
+ plan: "Plan mode",
4012
+ auto: "Auto mode",
4013
+ bypassPermissions: "Bypass permissions",
4014
+ dontAsk: "Don't ask"
4015
+ };
3294
4016
  function sendJson(res, status2, body) {
3295
4017
  const json = JSON.stringify(body);
3296
4018
  res.writeHead(status2, { "content-type": "application/json", "content-length": Buffer.byteLength(json) });
@@ -3317,24 +4039,52 @@ function createAgentServer({
3317
4039
  uploadsDir = null,
3318
4040
  claudeCommand = "claude",
3319
4041
  codexCommand = "codex",
3320
- codexHome = process.env.CODEX_HOME || path10.join(os6.homedir(), ".codex"),
3321
- home = os6.homedir(),
4042
+ codexHome = process.env.CODEX_HOME || path12.join(os8.homedir(), ".codex"),
4043
+ home = os8.homedir(),
3322
4044
  xcodebuild = "xcodebuild",
3323
4045
  builds = new BuildRunner({ xcodebuild }),
3324
4046
  pluginInstalled = null,
3325
4047
  agents = new AgentHub(),
3326
4048
  pushStore: pushStore2 = null,
4049
+ sessionStore: sessionStore2 = null,
3327
4050
  pushOptions = {},
3328
4051
  previewSweep = {},
3329
- version = null
4052
+ version = null,
4053
+ lan = null,
4054
+ lanAddresses: lanAddresses2 = lanAddresses,
4055
+ agentId = null,
4056
+ tunnelUrl = () => null
3330
4057
  }) {
3331
4058
  const handoff = new TunnelHandoff({ token, mint: tunnelToken });
3332
4059
  const sandbox = new Sandbox(root2);
3333
- const sessions = new SessionManager({ onExit: (id) => agents.terminalExited(id) });
4060
+ const sessions = new SessionManager({
4061
+ onExit: (id) => agents.terminalExited(id),
4062
+ onOutput: (id, buf) => agents.terminalOutput(id, buf)
4063
+ });
3334
4064
  const expected = Buffer.from(`Bearer ${token}`);
3335
4065
  const uploads = uploadsDir ? new UploadStore(uploadsDir) : null;
4066
+ if (uploads && !agents.keepImage) agents.keepImage = (bytes2, type) => uploads.keep(bytes2, type);
4067
+ agents.uploadsDir ??= uploadsDir;
4068
+ agents.fileType ??= (name) => imageType(name) ?? MEDIA_TYPES[path12.extname(name).toLowerCase()] ?? "application/octet-stream";
4069
+ let saveTimer = null;
4070
+ const saveSessions = () => {
4071
+ clearTimeout(saveTimer);
4072
+ saveTimer = null;
4073
+ try {
4074
+ sessionStore2?.save(agents.snapshot());
4075
+ } catch {
4076
+ }
4077
+ };
4078
+ if (sessionStore2) {
4079
+ agents.restore(sessionStore2.load());
4080
+ agents.on("message", () => {
4081
+ saveTimer ??= setTimeout(saveSessions, 1e3);
4082
+ saveTimer.unref?.();
4083
+ });
4084
+ }
3336
4085
  const sweep = setInterval(() => {
3337
4086
  agents.sweep();
4087
+ if (sessionStore2) saveSessions();
3338
4088
  uploads?.sweep().catch(() => {
3339
4089
  });
3340
4090
  }, 10 * 60 * 1e3);
@@ -3367,6 +4117,17 @@ function createAgentServer({
3367
4117
  if (!whileWaiting && session.state === "waiting") throw new HttpError(409, "answer the pending request first");
3368
4118
  return { session, term };
3369
4119
  }
4120
+ async function setMode(id, mode) {
4121
+ if (!PERMISSION_MODES.includes(mode)) throw new HttpError(400, `mode must be one of ${PERMISSION_MODES.join(", ")}`);
4122
+ const { term } = typingTarget(id, { whileWaiting: false });
4123
+ for (let i = 0; i <= PERMISSION_MODES.length; i++) {
4124
+ if (agents.session(id)?.permissionMode === mode) return agents.session(id);
4125
+ const shown = agents.waitForMode(id, MODE_TIMEOUT);
4126
+ term.write(KEYS.cycle_mode);
4127
+ if (!await shown) break;
4128
+ }
4129
+ throw new HttpError(409, `${MODE_NAMES[mode]} isn't available in this session`);
4130
+ }
3370
4131
  async function typeInto(term, text) {
3371
4132
  term.write(`${PASTE_START}${text}${PASTE_END}`);
3372
4133
  await new Promise((r) => setTimeout(r, 300));
@@ -3436,7 +4197,7 @@ function createAgentServer({
3436
4197
  }
3437
4198
  async function codexModels() {
3438
4199
  try {
3439
- const { models } = JSON.parse(await fs13.readFile(path10.join(codexHome, "models_cache.json"), "utf8"));
4200
+ const { models } = JSON.parse(await fs15.readFile(path12.join(codexHome, "models_cache.json"), "utf8"));
3440
4201
  return models.filter((m) => m.visibility === "list" && typeof m.slug === "string").sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)).map((m) => ({ id: m.slug, name: m.display_name || m.slug, description: m.description ?? "" }));
3441
4202
  } catch {
3442
4203
  return [];
@@ -3522,20 +4283,20 @@ function createAgentServer({
3522
4283
  async function repoOf(p) {
3523
4284
  const repo = await sandbox.resolve(p);
3524
4285
  try {
3525
- await fs13.access(path10.join(repo, ".git"));
4286
+ await fs15.access(path12.join(repo, ".git"));
3526
4287
  } catch {
3527
4288
  throw new HttpError(400, "not a git repository");
3528
4289
  }
3529
4290
  return repo;
3530
4291
  }
3531
4292
  async function filesIn(repo, files) {
3532
- if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path10.isAbsolute(f))) {
4293
+ if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path12.isAbsolute(f))) {
3533
4294
  throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
3534
4295
  }
3535
4296
  return Promise.all(
3536
4297
  files.map(async (f) => {
3537
- const abs = await sandbox.resolve(path10.join(repo, f), { allowMissing: true });
3538
- const rel = path10.relative(repo, abs);
4298
+ const abs = await sandbox.resolve(path12.join(repo, f), { allowMissing: true });
4299
+ const rel = path12.relative(repo, abs);
3539
4300
  if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
3540
4301
  return rel;
3541
4302
  })
@@ -3543,31 +4304,35 @@ function createAgentServer({
3543
4304
  }
3544
4305
  const routes = {
3545
4306
  [Routes.info]: async () => ({
4307
+ ...lanInfo(),
3546
4308
  protocol: PROTOCOL_VERSION,
3547
4309
  minClientProtocol: MIN_CLIENT_PROTOCOL,
3548
4310
  version,
3549
- hostname: os6.hostname(),
3550
- platform: os6.platform(),
4311
+ hostname: os8.hostname(),
4312
+ platform: os8.platform(),
3551
4313
  root: sandbox.root,
3552
- user: os6.userInfo().username,
4314
+ user: os8.userInfo().username,
3553
4315
  // On Windows, terminals run PowerShell whatever $SHELL (Git Bash sets one) says.
3554
- shell: isWindows ? interactiveShell().file : process.env.SHELL || null
4316
+ shell: isWindows ? interactiveShell().file : process.env.SHELL || null,
4317
+ agentId,
4318
+ // Null until the tunnel is hosted (an agent started offline connects it later).
4319
+ tunnel: tunnelUrl() ? { url: tunnelUrl() } : null
3555
4320
  }),
3556
4321
  [Routes.fsList]: async (url) => {
3557
4322
  const dir = await sandbox.resolve(url.searchParams.get("path"));
3558
- const names = await fs13.readdir(dir);
3559
- const entries2 = (await Promise.all(names.map((n) => describe(path10.join(dir, n), n).catch(() => null)))).filter(Boolean);
4323
+ const names = await fs15.readdir(dir);
4324
+ const entries2 = (await Promise.all(names.map((n) => describe(path12.join(dir, n), n).catch(() => null)))).filter(Boolean);
3560
4325
  return { path: dir, entries: entries2 };
3561
4326
  },
3562
4327
  [Routes.fsStat]: async (url) => {
3563
4328
  const p = await sandbox.resolve(url.searchParams.get("path"));
3564
- return { path: p, ...await describe(p, path10.basename(p)) };
4329
+ return { path: p, ...await describe(p, path12.basename(p)) };
3565
4330
  },
3566
4331
  [Routes.fsRead]: async (url, req, res) => {
3567
4332
  const p = await sandbox.resolve(url.searchParams.get("path"));
3568
- const stat = await fs13.stat(p);
4333
+ const stat = await fs15.stat(p);
3569
4334
  if (!stat.isFile()) throw new HttpError(400, "not a file");
3570
- const type = MEDIA_TYPES[path10.extname(p).toLowerCase()] ?? "application/octet-stream";
4335
+ const type = MEDIA_TYPES[path12.extname(p).toLowerCase()] ?? "application/octet-stream";
3571
4336
  const range = byteRange(req.headers.range, stat.size);
3572
4337
  if (range === "unsatisfiable") {
3573
4338
  res.writeHead(416, { "content-range": `bytes */${stat.size}`, "accept-ranges": "bytes" });
@@ -3592,24 +4357,24 @@ function createAgentServer({
3592
4357
  const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
3593
4358
  try {
3594
4359
  await pipeline3(req, createWriteStream2(tmp));
3595
- await fs13.rename(tmp, p);
4360
+ await fs15.rename(tmp, p);
3596
4361
  } catch (e) {
3597
- await fs13.rm(tmp, { force: true });
4362
+ await fs15.rm(tmp, { force: true });
3598
4363
  throw e;
3599
4364
  }
3600
- return describe(p, path10.basename(p));
4365
+ return describe(p, path12.basename(p));
3601
4366
  },
3602
4367
  [Routes.fsMkdir]: async (url) => {
3603
- await fs13.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
4368
+ await fs15.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
3604
4369
  return {};
3605
4370
  },
3606
4371
  [Routes.fsRename]: async (url, req) => {
3607
4372
  const { from, to } = await readJson2(req);
3608
- await fs13.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
4373
+ await fs15.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
3609
4374
  return {};
3610
4375
  },
3611
4376
  [Routes.fsDelete]: async (url) => {
3612
- await fs13.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
4377
+ await fs15.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
3613
4378
  return {};
3614
4379
  },
3615
4380
  [Routes.fsSearch]: async (url, req, res) => {
@@ -3665,12 +4430,54 @@ function createAgentServer({
3665
4430
  const { repo: r, message } = await readJson2(req);
3666
4431
  return commit(await repoOf(r), message);
3667
4432
  },
4433
+ [Routes.gitCommitMessage]: async (url, req) => {
4434
+ const { repo: r, agent: wanted, model: chosen } = await readJson2(req);
4435
+ const repo = await repoOf(r);
4436
+ if (wanted !== void 0 && wanted !== null && !(wanted in launchers)) throw new HttpError(400, "unknown agent");
4437
+ if (chosen !== void 0 && chosen !== null && (typeof chosen !== "string" || !/^[\w.:\[\]-]{1,100}$/.test(chosen))) {
4438
+ throw new HttpError(400, "invalid model");
4439
+ }
4440
+ let agent = null;
4441
+ for (const a of wanted ? [wanted] : ["claude", "codex"]) {
4442
+ if (await installed(a)) {
4443
+ agent = a;
4444
+ break;
4445
+ }
4446
+ }
4447
+ if (!agent) {
4448
+ const name = wanted ? launchers[wanted].name : "Claude Code or Codex";
4449
+ throw Object.assign(new HttpError(409, `Drafting a commit message needs ${name} installed on the computer.`), {
4450
+ code: "ENOAGENT"
4451
+ });
4452
+ }
4453
+ const model = wanted && chosen || (agent === "codex" ? (await codexModels())[0]?.id ?? null : null);
4454
+ return draftCommitMessage(repo, agent, { claudeCommand, codexCommand, model });
4455
+ },
3668
4456
  [Routes.gitLog]: async (url) => ({
3669
4457
  commits: await log(await repoOf(url.searchParams.get("repo")), parseInt(url.searchParams.get("limit"), 10) || 50)
3670
4458
  }),
3671
4459
  [Routes.gitShow]: async (url) => show(await repoOf(url.searchParams.get("repo")), url.searchParams.get("commit")),
3672
4460
  [Routes.gitPull]: async (url, req) => ({ output: await pull(await repoOf((await readJson2(req)).repo)) }),
3673
4461
  [Routes.gitPush]: async (url, req) => ({ output: await push(await repoOf((await readJson2(req)).repo)) }),
4462
+ [Routes.gitFetch]: async (url, req) => ({ output: await fetch2(await repoOf((await readJson2(req)).repo)) }),
4463
+ [Routes.gitBranches]: async (url) => ({ branches: await branches(await repoOf(url.searchParams.get("repo"))) }),
4464
+ [Routes.gitCheckout]: async (url, req) => {
4465
+ const { repo, branch, create } = await readJson2(req);
4466
+ return checkout(await repoOf(repo), branch, { create: create === true });
4467
+ },
4468
+ [Routes.gitStashes]: async (url) => ({ stashes: await stashes(await repoOf(url.searchParams.get("repo"))) }),
4469
+ [Routes.gitStash]: async (url, req) => {
4470
+ const { repo, message, untracked } = await readJson2(req);
4471
+ return stash(await repoOf(repo), { message, untracked: untracked !== false });
4472
+ },
4473
+ [Routes.gitStashApply]: async (url, req) => {
4474
+ const { repo, index, pop } = await readJson2(req);
4475
+ return stashApply(await repoOf(repo), index, { pop: pop === true });
4476
+ },
4477
+ [Routes.gitStashDrop]: async (url, req) => {
4478
+ const { repo, index } = await readJson2(req);
4479
+ return stashDrop(await repoOf(repo), index);
4480
+ },
3674
4481
  [Routes.termList]: async () => ({ sessions: sessions.list() }),
3675
4482
  [Routes.portsList]: async () => ({ ports: await listPorts(shellPids()) }),
3676
4483
  [Routes.portsKill]: async (url, req) => {
@@ -3762,7 +4569,10 @@ function createAgentServer({
3762
4569
  return issued;
3763
4570
  },
3764
4571
  [Routes.agentsSessions]: async () => ({ sessions: agents.sessions() }),
3765
- [Routes.agentsTimeline]: async (url) => agents.timeline(url.searchParams.get("session"), parseInt(url.searchParams.get("after"), 10) || 0),
4572
+ [Routes.agentsTimeline]: async (url) => {
4573
+ await agents.ready(url.searchParams.get("session"));
4574
+ return agents.timeline(url.searchParams.get("session"), parseInt(url.searchParams.get("after"), 10) || 0);
4575
+ },
3766
4576
  [Routes.agentsSetup]: async () => {
3767
4577
  const [claude, codex] = await Promise.all([installed("claude"), installed("codex")]);
3768
4578
  const [plugin, codexPlugin] = await Promise.all([claude && hasPlugin("claude"), codex && hasPlugin("codex")]);
@@ -3816,7 +4626,7 @@ function createAgentServer({
3816
4626
  } catch (e) {
3817
4627
  throw new HttpError(400, e.message);
3818
4628
  }
3819
- if (!(await fs13.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
4629
+ if (!(await fs15.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
3820
4630
  if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
3821
4631
  throw new HttpError(400, "invalid model");
3822
4632
  }
@@ -3838,7 +4648,7 @@ function createAgentServer({
3838
4648
  throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
3839
4649
  }
3840
4650
  const id = resume ?? randomUUID5();
3841
- if (uploadsDir) await fs13.mkdir(uploadsDir, { recursive: true, mode: 448 });
4651
+ if (uploadsDir) await fs15.mkdir(uploadsDir, { recursive: true, mode: 448 });
3842
4652
  const argv2 = [
3843
4653
  launcher.command,
3844
4654
  ...launcher.args({
@@ -3867,7 +4677,8 @@ function createAgentServer({
3867
4677
  const paths = await attachmentPaths(attachments);
3868
4678
  const typed = typeable([text.trim(), ...paths].join("\n"));
3869
4679
  const prompt = randomUUID5();
3870
- agents.prompted(id, { id: prompt, text: typed, paths });
4680
+ const images = paths.map((p, i) => ({ id: attachments[i], type: imageType(p) })).filter((i) => i.type);
4681
+ agents.prompted(id, { id: prompt, text: typed, paths, images });
3871
4682
  await typeInto(term, typed);
3872
4683
  return { prompt };
3873
4684
  },
@@ -3885,8 +4696,16 @@ function createAgentServer({
3885
4696
  });
3886
4697
  return { id, name: saved, type, size };
3887
4698
  },
4699
+ [Routes.agentsUploadFile]: async (url, req, res) => {
4700
+ const file = await uploadPath(url.searchParams.get("id"));
4701
+ const stat = await fs15.stat(file).catch(() => null);
4702
+ if (!stat?.isFile()) throw new HttpError(404, "no such upload (they are kept for 7 days)");
4703
+ const type = imageType(file) ?? MEDIA_TYPES[path12.extname(file).toLowerCase()] ?? "application/octet-stream";
4704
+ res.writeHead(200, { "content-type": type, "content-length": stat.size, "cache-control": "private, max-age=604800, immutable" });
4705
+ await pipeline3(createReadStream2(file), res);
4706
+ },
3888
4707
  [Routes.agentsKeys]: async (url, req) => {
3889
- const { session: id, action, command, args } = await readJson2(req);
4708
+ const { session: id, action, command, args, mode } = await readJson2(req);
3890
4709
  const allowed = agents.session(typeof id === "string" ? id : "")?.capabilities?.keys;
3891
4710
  if (action !== "command" && allowed && !allowed.includes(action)) {
3892
4711
  throw new HttpError(400, `this session takes ${allowed.join(", ")}`);
@@ -3900,11 +4719,14 @@ function createAgentServer({
3900
4719
  throw new HttpError(400, `${command} is not a command this session takes`);
3901
4720
  }
3902
4721
  await typeInto(term, typeable(args?.trim() ? `${command} ${args.trim()}` : command));
4722
+ if (command === "/model" && args?.trim()) agents.modelChosen(id, args.trim());
4723
+ } else if (action === "set_mode") {
4724
+ return { session: await setMode(id, mode) };
3903
4725
  } else if (Object.hasOwn(KEYS, action)) {
3904
4726
  typingTarget(id, { whileWaiting: true }).term.write(KEYS[action]);
3905
4727
  if (action === "interrupt") agents.interrupted(id);
3906
4728
  } else {
3907
- throw new HttpError(400, `action must be one of ${[...Object.keys(KEYS), "command"].join(", ")}`);
4729
+ throw new HttpError(400, `action must be one of ${[...Object.keys(KEYS), "set_mode", "command"].join(", ")}`);
3908
4730
  }
3909
4731
  return {};
3910
4732
  },
@@ -3960,6 +4782,9 @@ function createAgentServer({
3960
4782
  });
3961
4783
  return res.end(LANDING_HTML);
3962
4784
  }
4785
+ if (req.socket[OVER_LAN] === true && tunnelOnly(req.method, url.pathname)) {
4786
+ return sendJson(res, 404, { error: "not found" });
4787
+ }
3963
4788
  if (req.method === "GET" && url.pathname === TUNNEL_HANDOFF_PATH) return handoffRedirect(url, res);
3964
4789
  if (`${req.method} ${url.pathname}` === Routes.agentsHook) return hookDelivery(url, req, res);
3965
4790
  if (!authorized(req)) return sendJson(res, 401, { error: "unauthorized" });
@@ -3978,7 +4803,6 @@ function createAgentServer({
3978
4803
  if (!hookAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
3979
4804
  plugins[url.searchParams.get("agent") || "claude"] = { installed: true, checkedAt: Date.now() };
3980
4805
  const terminal = url.searchParams.get("terminal");
3981
- const app = url.searchParams.get("app");
3982
4806
  const closed = new AbortController();
3983
4807
  res.on("close", () => {
3984
4808
  if (!res.writableFinished) closed.abort();
@@ -3987,7 +4811,6 @@ function createAgentServer({
3987
4811
  const output = await agents.handle(await readJson2(req), {
3988
4812
  agent: url.searchParams.get("agent") || "claude",
3989
4813
  terminal: terminal && sessions.get(terminal) ? terminal : null,
3990
- app: app && app.length <= 1024 && /\.app$/.test(app) ? app : null,
3991
4814
  signal: closed.signal
3992
4815
  });
3993
4816
  if (res.destroyed) return;
@@ -4080,10 +4903,41 @@ function createAgentServer({
4080
4903
  presences.delete(ws);
4081
4904
  });
4082
4905
  }
4906
+ let lanServer = null;
4907
+ function lanInfo() {
4908
+ const port2 = lanServer?.listening ? lanServer.address().port : null;
4909
+ return { lan: port2 ? { port: port2, addresses: lanAddresses2(), fingerprint: lan.fingerprint } : null };
4910
+ }
4911
+ server.listenLan = (port2, host = "::") => {
4912
+ if (!lan) return Promise.reject(new Error("no LAN certificate"));
4913
+ const tlsServer = tls.createServer({ key: lan.key, cert: lan.cert, minVersion: "TLSv1.2" }, (socket) => {
4914
+ socket[OVER_LAN] = true;
4915
+ server.emit("connection", socket);
4916
+ });
4917
+ tlsServer.on("tlsClientError", () => {
4918
+ });
4919
+ return new Promise((resolve, reject) => {
4920
+ tlsServer.once("error", (e) => {
4921
+ if (host === "::" && (e.code === "EAFNOSUPPORT" || e.code === "EADDRNOTAVAIL")) {
4922
+ return resolve(server.listenLan(port2, "0.0.0.0"));
4923
+ }
4924
+ reject(e);
4925
+ });
4926
+ tlsServer.listen(port2, host, () => {
4927
+ tlsServer.removeAllListeners("error");
4928
+ tlsServer.on("error", () => {
4929
+ });
4930
+ lanServer = tlsServer;
4931
+ resolve(tlsServer);
4932
+ });
4933
+ });
4934
+ };
4083
4935
  server.on("close", () => {
4936
+ lanServer?.close();
4084
4937
  installs?.close();
4085
4938
  clearInterval(sweep);
4086
4939
  clearInterval(previewTimer);
4940
+ if (saveTimer) saveSessions();
4087
4941
  sessions.killAll();
4088
4942
  for (const proxy of proxies.values()) proxy.close();
4089
4943
  });
@@ -4093,14 +4947,15 @@ function createAgentServer({
4093
4947
  server.clientShows = clientShows;
4094
4948
  server.push = push2;
4095
4949
  server.agentsUploadPath = (id) => uploadPath(id);
4950
+ server.saveAgentSessions = () => sessionStore2 && saveSessions();
4096
4951
  return server;
4097
4952
  }
4098
4953
 
4099
4954
  // src/devtunnel.js
4100
4955
  import { execFile as execFile9 } from "node:child_process";
4101
- import { createHash } from "node:crypto";
4102
- import fs14 from "node:fs";
4103
- import path11 from "node:path";
4956
+ import { createHash as createHash2 } from "node:crypto";
4957
+ import fs16 from "node:fs";
4958
+ import path13 from "node:path";
4104
4959
  import { promisify as promisify3 } from "node:util";
4105
4960
  var run5 = promisify3(execFile9);
4106
4961
  var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
@@ -4131,11 +4986,11 @@ async function ensureDevtunnel({ dir, progress }) {
4131
4986
  `No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
4132
4987
  );
4133
4988
  }
4134
- const bin = path11.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
4135
- const metaFile = path11.join(dir, "devtunnel.json");
4989
+ const bin = path13.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
4990
+ const metaFile = path13.join(dir, "devtunnel.json");
4136
4991
  let meta = {};
4137
4992
  try {
4138
- meta = JSON.parse(fs14.readFileSync(metaFile, "utf8"));
4993
+ meta = JSON.parse(fs16.readFileSync(metaFile, "utf8"));
4139
4994
  } catch {
4140
4995
  }
4141
4996
  const installed = findExecutable(bin) !== null;
@@ -4146,7 +5001,7 @@ async function ensureDevtunnel({ dir, progress }) {
4146
5001
  const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
4147
5002
  const lastModified = head.headers.get("last-modified");
4148
5003
  if (head.ok && lastModified && lastModified === meta.lastModified) {
4149
- fs14.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
5004
+ fs16.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
4150
5005
  return bin;
4151
5006
  }
4152
5007
  } catch {
@@ -4161,13 +5016,13 @@ async function ensureDevtunnel({ dir, progress }) {
4161
5016
  return bin;
4162
5017
  }
4163
5018
  async function download(url, bin, metaFile, progress) {
4164
- fs14.mkdirSync(path11.dirname(bin), { recursive: true, mode: 448 });
5019
+ fs16.mkdirSync(path13.dirname(bin), { recursive: true, mode: 448 });
4165
5020
  const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
4166
5021
  if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
4167
5022
  const total = Number(res.headers.get("content-length")) || 0;
4168
5023
  const tmp = `${bin}.${process.pid}.download`;
4169
- const out = fs14.createWriteStream(tmp, { mode: 493 });
4170
- const md5 = createHash("md5");
5024
+ const out = fs16.createWriteStream(tmp, { mode: 493 });
5025
+ const md5 = createHash2("md5");
4171
5026
  let received = 0;
4172
5027
  try {
4173
5028
  for await (const chunk of res.body) {
@@ -4179,14 +5034,14 @@ async function download(url, bin, metaFile, progress) {
4179
5034
  await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
4180
5035
  if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
4181
5036
  await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
4182
- fs14.chmodSync(tmp, 493);
4183
- fs14.renameSync(tmp, bin);
5037
+ fs16.chmodSync(tmp, 493);
5038
+ fs16.renameSync(tmp, bin);
4184
5039
  } catch (e) {
4185
5040
  out.destroy();
4186
- fs14.rmSync(tmp, { force: true });
5041
+ fs16.rmSync(tmp, { force: true });
4187
5042
  throw e;
4188
5043
  }
4189
- fs14.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
5044
+ fs16.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
4190
5045
  }
4191
5046
  async function verify(file, md5, expectedMd5) {
4192
5047
  if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
@@ -4201,11 +5056,11 @@ async function verify(file, md5, expectedMd5) {
4201
5056
  }
4202
5057
  }
4203
5058
  async function verifyAuthenticode(file) {
4204
- const script = '$s = Get-AuthenticodeSignature -LiteralPath $env:CODEREMOTE_VERIFY; "$($s.Status)|$($s.SignerCertificate.Subject)"';
5059
+ const script2 = '$s = Get-AuthenticodeSignature -LiteralPath $env:CODEREMOTE_VERIFY; "$($s.Status)|$($s.SignerCertificate.Subject)"';
4205
5060
  let status2;
4206
5061
  let signer;
4207
5062
  try {
4208
- const { stdout } = await run5("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
5063
+ const { stdout } = await run5("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script2], {
4209
5064
  env: { ...process.env, CODEREMOTE_VERIFY: file }
4210
5065
  });
4211
5066
  [status2, signer] = stdout.trim().split("|");
@@ -4218,52 +5073,51 @@ async function verifyAuthenticode(file) {
4218
5073
  }
4219
5074
 
4220
5075
  // src/hook.js
4221
- import { execFile as execFile10 } from "node:child_process";
4222
- import { randomBytes as randomBytes3 } from "node:crypto";
4223
- import fs15 from "node:fs";
5076
+ import { randomBytes as randomBytes4 } from "node:crypto";
5077
+ import fs17 from "node:fs";
4224
5078
  import http4 from "node:http";
4225
- import path12 from "node:path";
5079
+ import path14 from "node:path";
4226
5080
  var CONNECT_TIMEOUT = 1e3;
4227
5081
  var EVENT_TIMEOUT = 5e3;
4228
5082
  function ensureHookSecret(dir) {
4229
- const file = path12.join(dir, "hook-secret");
5083
+ const file = path14.join(dir, "hook-secret");
4230
5084
  try {
4231
- const secret2 = fs15.readFileSync(file, "utf8").trim();
5085
+ const secret2 = fs17.readFileSync(file, "utf8").trim();
4232
5086
  if (secret2) return secret2;
4233
5087
  } catch {
4234
5088
  }
4235
- fs15.mkdirSync(dir, { recursive: true, mode: 448 });
4236
- const secret = randomBytes3(32).toString("base64url");
4237
- fs15.writeFileSync(file, secret, { mode: 384 });
5089
+ fs17.mkdirSync(dir, { recursive: true, mode: 448 });
5090
+ const secret = randomBytes4(32).toString("base64url");
5091
+ fs17.writeFileSync(file, secret, { mode: 384 });
4238
5092
  return secret;
4239
5093
  }
4240
- function writeHookLauncher(dir, { node = process.execPath, script }) {
4241
- const bin = path12.join(dir, "bin");
4242
- fs15.mkdirSync(bin, { recursive: true, mode: 448 });
5094
+ function writeHookLauncher(dir, { node = process.execPath, script: script2 }) {
5095
+ const bin = path14.join(dir, "bin");
5096
+ fs17.mkdirSync(bin, { recursive: true, mode: 448 });
4243
5097
  const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
4244
- const file = path12.join(bin, "hook");
4245
- fs15.writeFileSync(file, `#!/bin/sh
4246
- exec ${quote(node)} ${quote(script)} hook "$@"
5098
+ const file = path14.join(bin, "hook");
5099
+ fs17.writeFileSync(file, `#!/bin/sh
5100
+ exec ${quote(node)} ${quote(script2)} hook "$@"
4247
5101
  `, { mode: 493 });
4248
- fs15.chmodSync(file, 493);
4249
- fs15.writeFileSync(path12.join(bin, "hook.json"), JSON.stringify({ node, script }), { mode: 384 });
5102
+ fs17.chmodSync(file, 493);
5103
+ fs17.writeFileSync(path14.join(bin, "hook.json"), JSON.stringify({ node, script: script2 }), { mode: 384 });
4250
5104
  return file;
4251
5105
  }
4252
5106
  function readHookSecret(dir) {
4253
5107
  try {
4254
- return fs15.readFileSync(path12.join(dir, "hook-secret"), "utf8").trim() || null;
5108
+ return fs17.readFileSync(path14.join(dir, "hook-secret"), "utf8").trim() || null;
4255
5109
  } catch {
4256
5110
  return null;
4257
5111
  }
4258
5112
  }
4259
5113
  function real(p) {
4260
5114
  try {
4261
- return fs15.realpathSync.native(p);
5115
+ return fs17.realpathSync.native(p);
4262
5116
  } catch {
4263
- return path12.resolve(p);
5117
+ return path14.resolve(p);
4264
5118
  }
4265
5119
  }
4266
- var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(path12.sep) ? root2 : root2 + path12.sep);
5120
+ var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(path14.sep) ? root2 : root2 + path14.sep);
4267
5121
  function hookTarget(agents, cwd, env = process.env) {
4268
5122
  const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
4269
5123
  if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
@@ -4272,33 +5126,10 @@ function hookTarget(agents, cwd, env = process.env) {
4272
5126
  const match = agents.map((a) => ({ ...a, real: real(a.root) })).filter((a) => within(dir, a.real)).sort((a, b) => b.real.length - a.real.length)[0];
4273
5127
  return match ? { port: match.port, terminal: null } : null;
4274
5128
  }
4275
- var HOST_EVENTS = /* @__PURE__ */ new Set(["SessionStart", "UserPromptSubmit", "PermissionRequest"]);
4276
- function hostAppFrom(psOutput, pid) {
4277
- const parents = /* @__PURE__ */ new Map();
4278
- for (const line of psOutput.split("\n")) {
4279
- const m = line.match(/^\s*(\d+)\s+(\d+)\s+(.+)$/);
4280
- if (m) parents.set(Number(m[1]), { ppid: Number(m[2]), comm: m[3] });
4281
- }
4282
- for (let seen = 0, p = parents.get(pid); p && seen < 64; seen++, p = parents.get(p.ppid)) {
4283
- const at = p.comm.indexOf(".app/");
4284
- if (at > 0) return p.comm.slice(0, at + 4);
4285
- if (p.ppid <= 1) break;
4286
- }
4287
- return null;
4288
- }
4289
- function hostApp() {
4290
- if (process.platform !== "darwin") return Promise.resolve(null);
4291
- return new Promise((resolve) => {
4292
- execFile10("ps", ["-A", "-o", "pid=,ppid=,comm="], { timeout: 1e3, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
4293
- resolve(err ? null : hostAppFrom(stdout, process.ppid));
4294
- });
4295
- });
4296
- }
4297
- function deliverHook({ port: port2, terminal, app = null, agent, secret, body, wait }) {
5129
+ function deliverHook({ port: port2, terminal, agent, secret, body, wait }) {
4298
5130
  return new Promise((resolve) => {
4299
5131
  const query = new URLSearchParams({ agent });
4300
5132
  if (terminal) query.set("terminal", terminal);
4301
- if (app) query.set("app", app);
4302
5133
  const req = http4.request({
4303
5134
  host: "127.0.0.1",
4304
5135
  port: port2,
@@ -4326,8 +5157,8 @@ function deliverHook({ port: port2, terminal, app = null, agent, secret, body, w
4326
5157
  req.end(body);
4327
5158
  });
4328
5159
  }
4329
- async function runHook({ input, agents, secret, agent = "claude", env = process.env, findApp = hostApp }) {
4330
- if (!secret) return "";
5160
+ async function runHook({ input, agents, secret, agent = "claude", env = process.env }) {
5161
+ if (!secret || env.CODEREMOTE_NO_HOOK) return "";
4331
5162
  let event;
4332
5163
  try {
4333
5164
  event = JSON.parse(input);
@@ -4337,15 +5168,15 @@ async function runHook({ input, agents, secret, agent = "claude", env = process.
4337
5168
  const target = hookTarget(agents, event?.cwd, env);
4338
5169
  if (!target) return "";
4339
5170
  const wait = event.hook_event_name === "PermissionRequest";
4340
- const app = !target.terminal && HOST_EVENTS.has(event.hook_event_name) ? await findApp() : null;
4341
- const out = await deliverHook({ ...target, app, agent, secret, body: input, wait });
5171
+ const out = await deliverHook({ ...target, agent, secret, body: input, wait });
4342
5172
  return wait ? out.trim() : "";
4343
5173
  }
4344
5174
 
4345
5175
  // src/preview.js
4346
- import { spawn as spawn4 } from "node:child_process";
5176
+ import { spawn as spawn5 } from "node:child_process";
4347
5177
 
4348
5178
  // src/tunnels.js
5179
+ import { randomBytes as randomBytes5 } from "node:crypto";
4349
5180
  var TUNNEL_LIMIT = 10;
4350
5181
  var PREVIEW_LABEL = "coderemote-preview";
4351
5182
  var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
@@ -4390,6 +5221,7 @@ function forgetTunnel(cfg2, id) {
4390
5221
  for (const [port2, t] of Object.entries(cfg2[key] ?? {})) {
4391
5222
  if (base(t) !== base(id)) continue;
4392
5223
  delete cfg2[key][port2];
5224
+ if (key === "tunnels" && cfg2.tunnelUrls) delete cfg2.tunnelUrls[port2];
4393
5225
  changed = true;
4394
5226
  }
4395
5227
  }
@@ -4419,6 +5251,18 @@ function tunnelLimitHelp(tunnels, cfg2 = {}) {
4419
5251
  " coderemote tunnels delete <tunnel id>..."
4420
5252
  ].join("\n");
4421
5253
  }
5254
+ function ensureAgentId(cfg2, port2) {
5255
+ const known = cfg2.agentIds?.[port2];
5256
+ if (typeof known === "string" && /^[0-9a-f]{32}$/.test(known)) return known;
5257
+ const id = randomBytes5(16).toString("hex");
5258
+ cfg2.agentIds = { ...cfg2.agentIds, [port2]: id };
5259
+ return id;
5260
+ }
5261
+ function rememberTunnelUrl(cfg2, port2, url) {
5262
+ if (!url || cfg2.tunnelUrls?.[port2] === url) return false;
5263
+ cfg2.tunnelUrls = { ...cfg2.tunnelUrls, [port2]: url };
5264
+ return true;
5265
+ }
4422
5266
 
4423
5267
  // src/preview.js
4424
5268
  var HOST_READY_TIMEOUT_MS = 3e4;
@@ -4596,7 +5440,7 @@ ${created.out}`);
4596
5440
  }
4597
5441
  #startHost() {
4598
5442
  return new Promise((resolve, reject) => {
4599
- const child = spawn4(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
5443
+ const child = spawn5(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
4600
5444
  const urls = /* @__PURE__ */ new Map();
4601
5445
  let log2 = "";
4602
5446
  const timer = setTimeout(() => {
@@ -4631,69 +5475,102 @@ ${log2}`));
4631
5475
  };
4632
5476
 
4633
5477
  // src/push-store.js
4634
- import fs16 from "node:fs";
4635
- import path13 from "node:path";
5478
+ import fs18 from "node:fs";
5479
+ import path15 from "node:path";
4636
5480
  function pushStore(dir, port2) {
4637
- const file = path13.join(dir, `${port2}.json`);
5481
+ const file = path15.join(dir, `${port2}.json`);
4638
5482
  return {
4639
5483
  load() {
4640
5484
  try {
4641
- const devices = JSON.parse(fs16.readFileSync(file, "utf8"));
5485
+ const devices = JSON.parse(fs18.readFileSync(file, "utf8"));
4642
5486
  return Array.isArray(devices) ? devices : [];
4643
5487
  } catch {
4644
5488
  return [];
4645
5489
  }
4646
5490
  },
4647
5491
  save(devices) {
4648
- fs16.mkdirSync(dir, { recursive: true, mode: 448 });
4649
- fs16.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
5492
+ fs18.mkdirSync(dir, { recursive: true, mode: 448 });
5493
+ fs18.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
5494
+ }
5495
+ };
5496
+ }
5497
+
5498
+ // src/session-store.js
5499
+ import fs19 from "node:fs";
5500
+ import path16 from "node:path";
5501
+ function sessionStore(dir, port2) {
5502
+ const file = path16.join(dir, `${port2}.json`);
5503
+ return {
5504
+ load() {
5505
+ try {
5506
+ const sessions = JSON.parse(fs19.readFileSync(file, "utf8"));
5507
+ return Array.isArray(sessions) ? sessions : [];
5508
+ } catch {
5509
+ return [];
5510
+ }
5511
+ },
5512
+ save(sessions) {
5513
+ fs19.mkdirSync(dir, { recursive: true, mode: 448 });
5514
+ const tmp = `${file}.${process.pid}.tmp`;
5515
+ fs19.writeFileSync(tmp, JSON.stringify(sessions), { mode: 384 });
5516
+ fs19.renameSync(tmp, file);
4650
5517
  }
4651
5518
  };
4652
5519
  }
4653
5520
 
4654
5521
  // src/registry.js
4655
- import fs17 from "node:fs";
4656
- import path14 from "node:path";
5522
+ import fs20 from "node:fs";
5523
+ import path17 from "node:path";
4657
5524
  var AgentRegistry = class {
4658
5525
  constructor(dir) {
4659
5526
  this.dir = dir;
4660
5527
  }
4661
5528
  register({ port: port2, root: root2, url }) {
4662
- fs17.mkdirSync(this.dir, { recursive: true, mode: 448 });
5529
+ fs20.mkdirSync(this.dir, { recursive: true, mode: 448 });
4663
5530
  const file = this.#file(port2);
4664
- fs17.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
5531
+ let startedAt = Date.now();
5532
+ try {
5533
+ const previous = JSON.parse(fs20.readFileSync(file, "utf8"));
5534
+ if (previous.pid === process.pid) startedAt = previous.startedAt;
5535
+ } catch {
5536
+ }
5537
+ fs20.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt }), { mode: 384 });
4665
5538
  const cleanup = () => {
4666
5539
  try {
4667
- if (JSON.parse(fs17.readFileSync(file, "utf8")).pid === process.pid) fs17.rmSync(file);
5540
+ if (JSON.parse(fs20.readFileSync(file, "utf8")).pid === process.pid) fs20.rmSync(file);
4668
5541
  } catch {
4669
5542
  }
4670
5543
  };
4671
- process.on("exit", cleanup);
5544
+ if (!this.cleanups) this.cleanups = /* @__PURE__ */ new Set();
5545
+ if (!this.cleanups.has(file)) {
5546
+ this.cleanups.add(file);
5547
+ process.on("exit", cleanup);
5548
+ }
4672
5549
  return cleanup;
4673
5550
  }
4674
5551
  /** Live agents; entries left behind by crashed processes are removed. */
4675
5552
  list() {
4676
5553
  let names = [];
4677
5554
  try {
4678
- names = fs17.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
5555
+ names = fs20.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
4679
5556
  } catch {
4680
5557
  return [];
4681
5558
  }
4682
5559
  const agents = [];
4683
5560
  for (const name of names) {
4684
- const file = path14.join(this.dir, name);
5561
+ const file = path17.join(this.dir, name);
4685
5562
  try {
4686
- const agent = JSON.parse(fs17.readFileSync(file, "utf8"));
5563
+ const agent = JSON.parse(fs20.readFileSync(file, "utf8"));
4687
5564
  process.kill(agent.pid, 0);
4688
5565
  agents.push(agent);
4689
5566
  } catch {
4690
- fs17.rmSync(file, { force: true });
5567
+ fs20.rmSync(file, { force: true });
4691
5568
  }
4692
5569
  }
4693
5570
  return agents.sort((a, b) => a.port - b.port);
4694
5571
  }
4695
5572
  #file(port2) {
4696
- return path14.join(this.dir, `${port2}.json`);
5573
+ return path17.join(this.dir, `${port2}.json`);
4697
5574
  }
4698
5575
  };
4699
5576
 
@@ -4701,7 +5578,7 @@ var AgentRegistry = class {
4701
5578
  var MACHINE_LABEL = "coderemote";
4702
5579
  var PROVIDER = "devtunnels";
4703
5580
  var CACHE_MS = 15e3;
4704
- function parse(out) {
5581
+ function parse2(out) {
4705
5582
  return JSON.parse(out.slice(out.indexOf("{")));
4706
5583
  }
4707
5584
  function machineName(description) {
@@ -4737,13 +5614,13 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
4737
5614
  devtunnel2("list", "--limit", "100", "--json")
4738
5615
  ]);
4739
5616
  if (!own.ok) throw new Error(`devtunnel show failed: ${own.out.trim()}`);
4740
- const tunnel = parse(own.out).tunnel;
4741
- const all = listed.ok ? parse(listed.out).tunnels ?? [] : null;
5617
+ const tunnel = parse2(own.out).tunnel;
5618
+ const all = listed.ok ? parse2(listed.out).tunnels ?? [] : null;
4742
5619
  const others = (all ?? []).filter((t) => t.tunnelId !== id && t.labels?.includes(MACHINE_LABEL));
4743
5620
  const shown = await Promise.all(
4744
5621
  others.map(async (t) => {
4745
5622
  const res = await devtunnel2("show", t.tunnelId, "--json");
4746
- return res.ok ? parse(res.out).tunnel : null;
5623
+ return res.ok ? parse2(res.out).tunnel : null;
4747
5624
  })
4748
5625
  );
4749
5626
  const machines = [machine(tunnel, true), ...shown.filter(Boolean).map((t) => machine(t, false))];
@@ -4805,9 +5682,95 @@ function tunnelDelete({ devtunnel: devtunnel2, tunnelId, deleted = () => {
4805
5682
  };
4806
5683
  }
4807
5684
 
5685
+ // src/tunnel-retry.js
5686
+ import http5 from "node:http";
5687
+ import https from "node:https";
5688
+ var RELAY_CHECK_URL = process.env.CODEREMOTE_RELAY_CHECK_URL || "https://global.rel.tunnels.api.visualstudio.com/";
5689
+ function relayReachable({ url = RELAY_CHECK_URL, timeoutMs = 4e3, request = url.startsWith("http:") ? http5.request : https.request } = {}) {
5690
+ return new Promise((resolve) => {
5691
+ let done = false;
5692
+ const finish = (ok) => {
5693
+ if (done) return;
5694
+ done = true;
5695
+ resolve(ok);
5696
+ };
5697
+ try {
5698
+ const req = request(url, { method: "HEAD", timeout: timeoutMs }, (res) => {
5699
+ res.resume();
5700
+ finish(true);
5701
+ });
5702
+ req.on("timeout", () => {
5703
+ req.destroy();
5704
+ finish(false);
5705
+ });
5706
+ req.on("error", () => finish(false));
5707
+ req.end();
5708
+ } catch {
5709
+ finish(false);
5710
+ }
5711
+ });
5712
+ }
5713
+ var NotSignedIn = class extends Error {
5714
+ constructor() {
5715
+ super("not signed in to dev tunnels");
5716
+ this.code = "ENOTSIGNEDIN";
5717
+ }
5718
+ };
5719
+ function retryTunnel({
5720
+ reachable,
5721
+ connect,
5722
+ onSignedOut = () => {
5723
+ },
5724
+ onError = () => {
5725
+ },
5726
+ firstMs = 15e3,
5727
+ maxMs = 3e5,
5728
+ setTimer = setTimeout,
5729
+ clearTimer = clearTimeout
5730
+ }) {
5731
+ let delay = firstMs;
5732
+ let timer = null;
5733
+ let stopped = false;
5734
+ let toldSignedOut = false;
5735
+ let resolveDone;
5736
+ const done = new Promise((r) => resolveDone = r);
5737
+ const schedule = () => {
5738
+ if (stopped) return;
5739
+ timer = setTimer(attempt, delay);
5740
+ timer?.unref?.();
5741
+ delay = Math.min(delay * 2, maxMs);
5742
+ };
5743
+ async function attempt() {
5744
+ timer = null;
5745
+ if (stopped) return;
5746
+ if (!await reachable()) return schedule();
5747
+ try {
5748
+ const result = await connect();
5749
+ if (stopped) return;
5750
+ stopped = true;
5751
+ resolveDone(result);
5752
+ } catch (e) {
5753
+ if (stopped) return;
5754
+ if (e?.code === "ENOTSIGNEDIN") {
5755
+ if (!toldSignedOut) onSignedOut();
5756
+ toldSignedOut = true;
5757
+ } else onError(e);
5758
+ schedule();
5759
+ }
5760
+ }
5761
+ schedule();
5762
+ return {
5763
+ done,
5764
+ stop() {
5765
+ stopped = true;
5766
+ if (timer) clearTimer(timer);
5767
+ }
5768
+ };
5769
+ }
5770
+
4808
5771
  // src/update.js
4809
- import fs18 from "node:fs";
4810
- import path15 from "node:path";
5772
+ import fs21 from "node:fs";
5773
+ import path18 from "node:path";
4811
5774
  var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
4812
5775
  var CHECK_INTERVAL_MS = 24 * 36e5;
4813
5776
  var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
@@ -4820,9 +5783,9 @@ function isNewer(latest, current) {
4820
5783
  }
4821
5784
  return !a[4] && !!b[4];
4822
5785
  }
4823
- async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 } = {}) {
5786
+ async function fetchLatest({ fetch: fetch3 = globalThis.fetch, timeoutMs = 3e3 } = {}) {
4824
5787
  try {
4825
- const res = await fetch2(LATEST_URL, { signal: AbortSignal.timeout(timeoutMs) });
5788
+ const res = await fetch3(LATEST_URL, { signal: AbortSignal.timeout(timeoutMs) });
4826
5789
  if (!res.ok) return null;
4827
5790
  const { version } = await res.json();
4828
5791
  return SEMVER.test(version) ? version : null;
@@ -4830,21 +5793,21 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
4830
5793
  return null;
4831
5794
  }
4832
5795
  }
4833
- async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
4834
- const file = path15.join(dir, "update.json");
5796
+ async function availableUpdate({ current, dir, fetch: fetch3, now = Date.now() }) {
5797
+ const file = path18.join(dir, "update.json");
4835
5798
  let cache = {};
4836
5799
  try {
4837
- cache = JSON.parse(fs18.readFileSync(file, "utf8"));
5800
+ cache = JSON.parse(fs21.readFileSync(file, "utf8"));
4838
5801
  } catch {
4839
5802
  }
4840
5803
  let latest = cache.latest;
4841
5804
  if (!latest || !(now - (cache.checkedAt ?? 0) < CHECK_INTERVAL_MS)) {
4842
- const fetched = await fetchLatest({ fetch: fetch2 });
5805
+ const fetched = await fetchLatest({ fetch: fetch3 });
4843
5806
  if (fetched) {
4844
5807
  latest = fetched;
4845
5808
  try {
4846
- fs18.mkdirSync(dir, { recursive: true, mode: 448 });
4847
- fs18.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
5809
+ fs21.mkdirSync(dir, { recursive: true, mode: 448 });
5810
+ fs21.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
4848
5811
  } catch {
4849
5812
  }
4850
5813
  }
@@ -4852,12 +5815,12 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
4852
5815
  return isNewer(latest, current) ? latest : null;
4853
5816
  }
4854
5817
  function installCommand(installedAt) {
4855
- const p = installedAt.split(path15.sep).join("/");
5818
+ const p = installedAt.split(path18.sep).join("/");
4856
5819
  const runtime = p.match(/^(.*\/runtime\/node)\/(lib\/)?node_modules\/coderemote\//);
4857
5820
  if (runtime) {
4858
5821
  const prefix = installedAt.slice(0, runtime[1].length);
4859
- const [node, modules] = runtime[2] ? [path15.join(prefix, "bin", "node"), path15.join(prefix, "lib", "node_modules")] : [path15.join(prefix, "node.exe"), path15.join(prefix, "node_modules")];
4860
- return [node, [path15.join(modules, "npm", "bin", "npm-cli.js"), "install", "-g", "--prefix", prefix, "coderemote@latest"]];
5822
+ const [node, modules] = runtime[2] ? [path18.join(prefix, "bin", "node"), path18.join(prefix, "lib", "node_modules")] : [path18.join(prefix, "node.exe"), path18.join(prefix, "node_modules")];
5823
+ return [node, [path18.join(modules, "npm", "bin", "npm-cli.js"), "install", "-g", "--prefix", prefix, "coderemote@latest"]];
4861
5824
  }
4862
5825
  if (p.includes("/Cellar/coderemote/")) return ["brew", ["upgrade", "coderemote"]];
4863
5826
  if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
@@ -4869,7 +5832,7 @@ function installCommand(installedAt) {
4869
5832
  // package.json
4870
5833
  var package_default = {
4871
5834
  name: "coderemote",
4872
- version: "0.3.9",
5835
+ version: "0.3.10",
4873
5836
  description: "Use your computer from your iPhone: files, git and a real terminal over a secure Microsoft Dev Tunnel. No SSH, no port forwarding, no VPN.",
4874
5837
  keywords: [
4875
5838
  "remote",
@@ -5191,29 +6154,124 @@ ${log2}`));
5191
6154
  });
5192
6155
  }
5193
6156
 
6157
+ // src/windows-firewall.js
6158
+ import { execFile as execFile10 } from "node:child_process";
6159
+ var psQuote = (s) => `'${String(s).replace(/'/g, "''")}'`;
6160
+ function script(execPath) {
6161
+ return `
6162
+ $ErrorActionPreference = 'SilentlyContinue'
6163
+ $exe = ${psQuote(execPath)}
6164
+ $profiles = @(Get-NetConnectionProfile | ForEach-Object {
6165
+ @{ alias = $_.InterfaceAlias; category = [string]$_.NetworkCategory }
6166
+ })
6167
+ $blocks = @(Get-NetFirewallRule -Direction Inbound -Action Block -Enabled True | ForEach-Object {
6168
+ $rule = $_
6169
+ $program = ($rule | Get-NetFirewallApplicationFilter).Program
6170
+ if ($program -and [Environment]::ExpandEnvironmentVariables($program) -ieq $exe) {
6171
+ @{ name = $rule.Name; displayName = $rule.DisplayName; profile = [string]$rule.Profile }
6172
+ }
6173
+ })
6174
+ # The effective settings, Group Policy included: AllowLocalFirewallRules False ignores local rules.
6175
+ $firewall = @(Get-NetFirewallProfile -PolicyStore ActiveStore | ForEach-Object {
6176
+ @{ name = [string]$_.Name; enabled = [string]$_.Enabled; allowLocalRules = [string]$_.AllowLocalFirewallRules }
6177
+ })
6178
+ ConvertTo-Json -Compress -Depth 4 @{ profiles = $profiles; blockRules = $blocks; firewall = $firewall }
6179
+ `;
6180
+ }
6181
+ function firewallReport({ execPath = process.execPath, timeoutMs = 1e4, run: run7 = execFile10, platform = process.platform } = {}) {
6182
+ if (platform !== "win32") return Promise.resolve(null);
6183
+ return new Promise((resolve) => {
6184
+ try {
6185
+ run7(
6186
+ "powershell.exe",
6187
+ ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script(execPath), "utf16le").toString("base64")],
6188
+ { timeout: timeoutMs, windowsHide: true },
6189
+ (err, stdout) => {
6190
+ if (err) return resolve(null);
6191
+ try {
6192
+ const got = JSON.parse(stdout);
6193
+ resolve({
6194
+ profiles: [got.profiles ?? []].flat(),
6195
+ blockRules: [got.blockRules ?? []].flat(),
6196
+ firewall: [got.firewall ?? []].flat()
6197
+ });
6198
+ } catch {
6199
+ resolve(null);
6200
+ }
6201
+ }
6202
+ );
6203
+ } catch {
6204
+ resolve(null);
6205
+ }
6206
+ });
6207
+ }
6208
+ function lanAdapters(interfaces) {
6209
+ return Object.entries(interfaces).filter(([name, addrs]) => lanAddresses({ [name]: addrs }).length > 0).map(([name]) => name);
6210
+ }
6211
+ var PROFILE_OF = { Public: "Public", Private: "Private", DomainAuthenticated: "Domain" };
6212
+ var covers = (ruleProfile, category) => ruleProfile === "Any" || ruleProfile.split(/,\s*/).includes(PROFILE_OF[category]);
6213
+ function firewallAdvice(report, { lanPort, adapters }) {
6214
+ if (!report) return [];
6215
+ const lines = [];
6216
+ const settings = (p) => (report.firewall ?? []).find((f) => f.name === PROFILE_OF[p.category]);
6217
+ const lan = report.profiles.filter((p) => adapters.includes(p.alias) && settings(p)?.enabled !== "False");
6218
+ if (lan.some((p) => settings(p)?.allowLocalRules === "False")) {
6219
+ return [
6220
+ "Your organization manages this computer's firewall, and rules set on it are ignored:",
6221
+ "phones on this network may not reach it directly. The app then goes through the tunnel; your IT",
6222
+ `can allow inbound TCP port ${lanPort} from the local network.`
6223
+ ];
6224
+ }
6225
+ const publicOnes = lan.filter((p) => p.category === "Public");
6226
+ const blocking = report.blockRules.filter((r) => lan.some((p) => p.category !== "Public" && covers(r.profile, p.category)));
6227
+ const blockingPublic = report.blockRules.some((r) => covers(r.profile, "Public"));
6228
+ if (publicOnes.length) {
6229
+ const names = publicOnes.map((p) => `"${p.alias}"`).join(", ");
6230
+ lines.push(`Windows treats ${names} as a Public network, and its firewall keeps phones on it out.`);
6231
+ lines.push("On a network you trust, make it Private (in PowerShell run as administrator):");
6232
+ for (const p of publicOnes) lines.push(` Set-NetConnectionProfile -InterfaceAlias "${p.alias}" -NetworkCategory Private`);
6233
+ if (!blockingPublic) {
6234
+ lines.push("Or let in only this port, from the local network (Command Prompt or PowerShell as administrator):");
6235
+ lines.push(
6236
+ ` netsh advfirewall firewall add rule name="Code Remote LAN ${lanPort}" dir=in action=allow protocol=TCP localport=${lanPort} remoteip=localsubnet`
6237
+ );
6238
+ }
6239
+ }
6240
+ if (blocking.length) {
6241
+ lines.push(`The firewall blocks node ("${blocking[0].displayName}", left by choosing Cancel when Windows asked), so phones can't connect.`);
6242
+ lines.push("Remove it (in PowerShell run as administrator), then start coderemote again and choose Allow:");
6243
+ lines.push(` Remove-NetFirewallRule -Name ${blocking.map((r) => `"${r.name}"`).join(",")}`);
6244
+ }
6245
+ return lines;
6246
+ }
6247
+
5194
6248
  // bin/coderemote.js
5195
6249
  var run6 = promisify4(execFile11);
5196
- var CONFIG_DIR = path16.join(os7.homedir(), ".coderemote");
5197
- var LEGACY_CONFIG_DIR = path16.join(os7.homedir(), ".vsremote");
5198
- if (!fs19.existsSync(CONFIG_DIR) && fs19.existsSync(LEGACY_CONFIG_DIR)) fs19.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
5199
- var CONFIG_FILE = path16.join(CONFIG_DIR, "config.json");
5200
- var PUSH_DIR = path16.join(CONFIG_DIR, "push");
5201
- var UPLOADS_DIR = path16.join(CONFIG_DIR, "uploads");
6250
+ var CONFIG_DIR = path19.join(os9.homedir(), ".coderemote");
6251
+ var LEGACY_CONFIG_DIR = path19.join(os9.homedir(), ".vsremote");
6252
+ if (!fs22.existsSync(CONFIG_DIR) && fs22.existsSync(LEGACY_CONFIG_DIR)) fs22.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
6253
+ var CONFIG_FILE = path19.join(CONFIG_DIR, "config.json");
6254
+ var PUSH_DIR = path19.join(CONFIG_DIR, "push");
6255
+ var AGENT_SESSIONS_DIR = path19.join(CONFIG_DIR, "agent-sessions");
6256
+ var UPLOADS_DIR = path19.join(CONFIG_DIR, "uploads");
6257
+ var LAN_DIR = path19.join(CONFIG_DIR, "lan");
6258
+ var LAN_PORT_OFFSET = 1e3;
5202
6259
  var DEFAULT_PORT = 31580;
5203
6260
  var VERSION = package_default.version;
5204
- var PACKAGED = path16.basename(path16.dirname(fileURLToPath(import.meta.url))) === "dist";
6261
+ var PACKAGED = path19.basename(path19.dirname(fileURLToPath(import.meta.url))) === "dist";
5205
6262
  var spinner = new Spinner();
5206
6263
  function loadConfig() {
5207
6264
  let cfg2 = {};
5208
6265
  try {
5209
- cfg2 = JSON.parse(fs19.readFileSync(CONFIG_FILE, "utf8"));
6266
+ cfg2 = JSON.parse(fs22.readFileSync(CONFIG_FILE, "utf8"));
5210
6267
  } catch {
5211
6268
  }
5212
6269
  if (!cfg2.token) {
5213
- cfg2.token = randomBytes4(32).toString("base64url");
6270
+ cfg2.token = randomBytes6(32).toString("base64url");
5214
6271
  saveConfig(cfg2);
5215
- fs19.rmSync(PUSH_DIR, { recursive: true, force: true });
5216
- fs19.rmSync(UPLOADS_DIR, { recursive: true, force: true });
6272
+ fs22.rmSync(PUSH_DIR, { recursive: true, force: true });
6273
+ fs22.rmSync(UPLOADS_DIR, { recursive: true, force: true });
6274
+ fs22.rmSync(LAN_DIR, { recursive: true, force: true });
5217
6275
  }
5218
6276
  if (cfg2.tunnelId && !cfg2.tunnels) {
5219
6277
  cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
@@ -5229,8 +6287,8 @@ function stopAgent(pid) {
5229
6287
  }
5230
6288
  }
5231
6289
  function saveConfig(cfg2) {
5232
- fs19.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
5233
- fs19.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
6290
+ fs22.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
6291
+ fs22.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
5234
6292
  }
5235
6293
  function portFree(port2) {
5236
6294
  return new Promise((resolve) => {
@@ -5239,9 +6297,9 @@ function portFree(port2) {
5239
6297
  probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
5240
6298
  });
5241
6299
  }
5242
- function listen(token, port2, root2, tunnelToken, preview2, status2, remover) {
6300
+ function listen(token, port2, root2, tunnelToken, preview2, status2, remover, lan, agentId, tunnelUrl) {
5243
6301
  const hookSecret = ensureHookSecret(CONFIG_DIR);
5244
- writeHookLauncher(CONFIG_DIR, { script: fs19.realpathSync(process.argv[1]) });
6302
+ writeHookLauncher(CONFIG_DIR, { script: fs22.realpathSync(process.argv[1]) });
5245
6303
  const server = createAgentServer({
5246
6304
  token,
5247
6305
  root: root2,
@@ -5251,21 +6309,62 @@ function listen(token, port2, root2, tunnelToken, preview2, status2, remover) {
5251
6309
  tunnelDelete: remover,
5252
6310
  hookSecret,
5253
6311
  // This very CLI, so coding agents in the app's terminals report through the same version.
5254
- hookBin: fs19.realpathSync(process.argv[1]),
6312
+ hookBin: fs22.realpathSync(process.argv[1]),
5255
6313
  pushStore: pushStore(PUSH_DIR, port2),
6314
+ sessionStore: sessionStore(AGENT_SESSIONS_DIR, port2),
5256
6315
  uploadsDir: UPLOADS_DIR,
5257
- version: VERSION
6316
+ version: VERSION,
6317
+ lan,
6318
+ agentId,
6319
+ tunnelUrl
5258
6320
  });
5259
6321
  return new Promise((resolve, reject) => {
5260
6322
  server.once("error", reject);
5261
6323
  server.listen(port2, "127.0.0.1", () => resolve(server));
5262
6324
  });
5263
6325
  }
6326
+ async function openLan(server, agentPort) {
6327
+ const remembered = cfg.lanPorts?.[root];
6328
+ const claimed = new Set(Object.entries(cfg.lanPorts ?? {}).filter(([r]) => r !== root).map(([, p]) => p));
6329
+ const busy = new Set(registry.list().map((a) => a.port));
6330
+ let candidate = remembered ?? agentPort + LAN_PORT_OFFSET;
6331
+ for (let tries = 0; tries < 50; tries++, candidate++) {
6332
+ if (candidate !== remembered && (claimed.has(candidate) || busy.has(candidate))) continue;
6333
+ try {
6334
+ const lanPort = (await server.listenLan(candidate)).address().port;
6335
+ if (lanPort !== remembered) {
6336
+ cfg.lanPorts = { ...cfg.lanPorts, [root]: lanPort };
6337
+ saveConfig(cfg);
6338
+ }
6339
+ return { port: lanPort };
6340
+ } catch (e) {
6341
+ if (e.code !== "EADDRINUSE") return { error: e.message };
6342
+ }
6343
+ }
6344
+ return { error: "no free port" };
6345
+ }
6346
+ function advertiseLan(lanPort, fingerprint, agentPort) {
6347
+ if (process.platform !== "darwin") return null;
6348
+ const host = os9.hostname().replace(/\.local$/, "");
6349
+ const name = agentPort === DEFAULT_PORT ? host : `${host} :${agentPort}`;
6350
+ try {
6351
+ const child = spawn6(
6352
+ "dns-sd",
6353
+ ["-R", name, LAN_SERVICE_TYPE, "local", String(lanPort), `fp=${fingerprint.slice(0, 16)}`, `proto=${PROTOCOL_VERSION}`],
6354
+ { stdio: "ignore" }
6355
+ );
6356
+ child.on("error", () => {
6357
+ });
6358
+ return child;
6359
+ } catch {
6360
+ return null;
6361
+ }
6362
+ }
5264
6363
  var DEVTUNNEL = "devtunnel";
5265
6364
  async function resolveDevtunnel() {
5266
6365
  let installing = false;
5267
6366
  DEVTUNNEL = await ensureDevtunnel({
5268
- dir: path16.join(CONFIG_DIR, "bin"),
6367
+ dir: path19.join(CONFIG_DIR, "bin"),
5269
6368
  progress: (detail2) => {
5270
6369
  if (!installing) {
5271
6370
  installing = true;
@@ -5292,7 +6391,7 @@ function loginArgs(opts = rest) {
5292
6391
  }
5293
6392
  function interactiveLogin(args) {
5294
6393
  return new Promise((resolve) => {
5295
- spawn5(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
6394
+ spawn6(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
5296
6395
  });
5297
6396
  }
5298
6397
  async function login(reason) {
@@ -5322,10 +6421,11 @@ function applyAccess(tunnelId, anonymous2) {
5322
6421
  async function ensureTunnel(cfg2, port2, anonymous2) {
5323
6422
  const known = cfg2.tunnels?.[port2];
5324
6423
  if (known) {
5325
- const [updated, access] = await Promise.all([
5326
- devtunnel("update", known, "--expiration", "30d", "--add-labels", MACHINE_LABEL),
5327
- applyAccess(known, anonymous2)
5328
- ]);
6424
+ const update = () => devtunnel("update", known, "--expiration", "30d", "--add-labels", MACHINE_LABEL);
6425
+ let [updated, access] = await Promise.all([update(), applyAccess(known, anonymous2)]);
6426
+ const conflict = (res) => !res.ok && /conflicting request/i.test(res.out);
6427
+ if (conflict(updated)) updated = await update();
6428
+ if (conflict(access)) access = await applyAccess(known, anonymous2);
5329
6429
  if (updated.ok) {
5330
6430
  if (!access.ok) throw new Error(`devtunnel access failed:
5331
6431
  ${access.out}`);
@@ -5334,18 +6434,13 @@ ${access.out}`);
5334
6434
  if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
5335
6435
  ${updated.out}`);
5336
6436
  }
5337
- const name = port2 === DEFAULT_PORT ? os7.hostname() : `${os7.hostname()} :${port2}`;
6437
+ const name = port2 === DEFAULT_PORT ? os9.hostname() : `${os9.hostname()} :${port2}`;
5338
6438
  const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
5339
6439
  if (anonymous2) createArgs.push("--allow-anonymous");
5340
6440
  let created = await devtunnel(...createArgs);
5341
6441
  if (!created.ok) {
5342
6442
  const room = await makeRoom(devtunnel);
5343
- if (room?.full) {
5344
- spinner.fail();
5345
- console.error(`
5346
- ${tunnelLimitHelp(room.full, cfg2)}`);
5347
- process.exit(1);
5348
- }
6443
+ if (room?.full) throw Object.assign(new Error(tunnelLimitHelp(room.full, cfg2)), { code: "ETUNNELLIMIT" });
5349
6444
  if (room?.freed) {
5350
6445
  spinner.update(`(deleted ${room.freed.length} idle preview tunnel${room.freed.length > 1 ? "s" : ""} to make room)`);
5351
6446
  if (room.freed.filter((id) => forgetTunnel(cfg2, id)).length) saveConfig(cfg2);
@@ -5376,7 +6471,7 @@ ${res.out}`);
5376
6471
  }
5377
6472
  function hostTunnel(tunnelId, port2) {
5378
6473
  return new Promise((resolve, reject) => {
5379
- const child = spawn5(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
6474
+ const child = spawn6(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
5380
6475
  let log2 = "";
5381
6476
  const onData = (d) => {
5382
6477
  log2 += d;
@@ -5389,20 +6484,22 @@ function hostTunnel(tunnelId, port2) {
5389
6484
  ${log2}`)));
5390
6485
  });
5391
6486
  }
5392
- function printPairing(url, token, access, vscodeUrl) {
5393
- const pair = pairingUrl({ url, token });
6487
+ function printPairing(url, token, access, vscodeUrl, link) {
6488
+ const pair = pairingUrl({ url, token, agentId: link?.agentId, lan: link?.lan });
5394
6489
  if (access) console.log(` Access: ${access}`);
5395
6490
  const label = (name) => `${name}:`.padEnd(vscodeUrl ? 9 : 4);
5396
6491
  console.log("");
5397
6492
  if (vscodeUrl) console.log(` ${label("VS Code")} ${vscodeUrl}`);
5398
- console.log(` ${label("Web")} ${webUrl({ url, token })}`);
6493
+ if (url) console.log(` ${label("Web")} ${webUrl({ url, token })}`);
5399
6494
  console.log(` ${label("App")} ${pair}
5400
6495
  `);
6496
+ if (!url) console.log(" No tunnel yet: pair the Code Remote app on this Wi-Fi (an app too old for that calls this link invalid).\n");
5401
6497
  if (vscodeUrl) console.log(" VS Code opens in the browser after signing in with the account of `code tunnel`.");
5402
- console.log(" Scan with the iPhone Camera, or open either link on the phone:\n");
6498
+ console.log(` Scan with the iPhone Camera, or open ${url ? "either link" : "the link"} on the phone:
6499
+ `);
5403
6500
  qrcode.generate(pair, { small: true });
5404
6501
  console.log(
5405
- anonymous ? "\n The links contain your access token. Anyone with them can use this machine." : "\n The links contain your access token; the phone or browser must also sign in as you."
6502
+ anonymous ? "\n The links contain your access token. Anyone with them can use this machine." : url ? "\n The links contain your access token; the phone or browser must also sign in as you." : "\n The link contains your access token; away from this network the phone must also sign in as you."
5406
6503
  );
5407
6504
  console.log(" Run `coderemote rotate-token` to revoke every paired device.\n");
5408
6505
  }
@@ -5440,7 +6537,9 @@ async function startVSCodeTunnel() {
5440
6537
  var USAGE = `Usage: coderemote <command> [options]
5441
6538
 
5442
6539
  up share this folder through a dev tunnel and print the pairing QR;
5443
- the phone or browser must sign in with your dev tunnels account
6540
+ the phone or browser must sign in with your dev tunnels account.
6541
+ Without internet it serves the local network only and connects
6542
+ the tunnel once it can
5444
6543
  serve share this folder on 127.0.0.1 only (no tunnel)
5445
6544
  status list running agents and their terminal sessions
5446
6545
  kill <id>... | --all kill terminal sessions (an id prefix is enough)
@@ -5470,7 +6569,10 @@ Options:
5470
6569
  --allow-anonymous up: skip the sign-in, the pairing token alone grants access
5471
6570
  (alias --no-auth)
5472
6571
  --web up: also run \`code tunnel\` and print a vscode.dev link that opens
5473
- this folder in VS Code in the browser (needs VS Code's \`code\`)`;
6572
+ this folder in VS Code in the browser (needs VS Code's \`code\`)
6573
+ --no-lan up: don't let phones on the same network connect directly (over TLS,
6574
+ still with the token); they use the tunnel. "lan": false in
6575
+ ~/.coderemote/config.json makes it the default, --lan overrides that`;
5474
6576
  var HELP_FLAGS = ["help", "--help", "-help", "-h"];
5475
6577
  var argv = process.argv.slice(2);
5476
6578
  var [cmd, ...rest] = argv.length === 0 || argv.some((a) => HELP_FLAGS.includes(a)) ? ["help"] : argv.some((a) => a === "--version" || a === "-v") ? ["version"] : argv;
@@ -5480,9 +6582,9 @@ var flag = (name) => {
5480
6582
  };
5481
6583
  var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
5482
6584
  var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
5483
- var root = path16.resolve(flag("--root") ?? process.cwd());
6585
+ var root = path19.resolve(flag("--root") ?? process.cwd());
5484
6586
  var cfg = loadConfig();
5485
- var registry = new AgentRegistry(path16.join(CONFIG_DIR, "agents"));
6587
+ var registry = new AgentRegistry(path19.join(CONFIG_DIR, "agents"));
5486
6588
  async function start({ tunnel }) {
5487
6589
  const agents = registry.list();
5488
6590
  if (flag("--port")) {
@@ -5517,7 +6619,7 @@ async function start({ tunnel }) {
5517
6619
  const preview2 = tunnel ? new PreviewTunnel({
5518
6620
  devtunnel,
5519
6621
  command: () => DEVTUNNEL,
5520
- description: `coderemote preview ${os7.hostname()}`,
6622
+ description: `coderemote preview ${os9.hostname()}`,
5521
6623
  leftover: cfg.previewTunnels?.[port],
5522
6624
  remember: (id) => {
5523
6625
  cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
@@ -5537,9 +6639,21 @@ async function start({ tunnel }) {
5537
6639
  if (forgetTunnel(saved, id)) saveConfig(saved);
5538
6640
  }
5539
6641
  }) : null;
6642
+ const lanWanted = tunnel && (rest.includes("--no-lan") ? false : rest.includes("--lan") || cfg.lan !== false);
6643
+ let lanCert = null;
6644
+ let lan = { off: true };
6645
+ if (lanWanted) {
6646
+ const got = await lanCertificate(LAN_DIR);
6647
+ if (got.error) lan = { error: got.error };
6648
+ else lanCert = got;
6649
+ }
6650
+ const hadAgentId = cfg.agentIds?.[port];
6651
+ const agentId = ensureAgentId(cfg, port);
6652
+ if (agentId !== hadAgentId) saveConfig(cfg);
6653
+ let hostedUrl = null;
5540
6654
  spinner.start("Starting agent");
5541
6655
  try {
5542
- server = await listen(cfg.token, port, root, tunnelToken, preview2, status2, remover);
6656
+ server = await listen(cfg.token, port, root, tunnelToken, preview2, status2, remover, lanCert, agentId, () => hostedUrl);
5543
6657
  } catch (e) {
5544
6658
  spinner.fail();
5545
6659
  if (e.code !== "EADDRINUSE") throw e;
@@ -5548,35 +6662,105 @@ async function start({ tunnel }) {
5548
6662
  console.error("Stop that process, or pick another port with --port.");
5549
6663
  process.exit(1);
5550
6664
  }
6665
+ let advertiser = null;
6666
+ let firewall = null;
6667
+ if (lanCert) {
6668
+ lan = await openLan(server, port);
6669
+ if (lan.port) advertiser = advertiseLan(lan.port, lanCert.fingerprint, port);
6670
+ if (lan.port && isWindows) {
6671
+ const lanPort = lan.port;
6672
+ firewall = firewallReport().then((report) => firewallAdvice(report, { lanPort, adapters: lanAdapters(os9.networkInterfaces()) }));
6673
+ }
6674
+ }
5551
6675
  let child;
5552
6676
  let access;
5553
6677
  let url = `http://127.0.0.1:${port}`;
5554
- if (tunnel) {
5555
- spinner.succeed();
6678
+ let later = null;
6679
+ async function connectTunnel({ interactive }) {
5556
6680
  await resolveDevtunnel();
5557
- spinner.start("Checking dev tunnels sign-in");
5558
- const user = await tunnelUser();
6681
+ if (interactive) spinner.start("Checking dev tunnels sign-in");
6682
+ let user;
6683
+ if (interactive) user = await tunnelUser();
6684
+ else {
6685
+ user = signedInUser(await devtunnel("user", "show"));
6686
+ if (!user) throw new NotSignedIn();
6687
+ }
5559
6688
  account = user;
5560
- spinner.succeed();
5561
- spinner.start("Connecting to the relay");
6689
+ if (interactive) {
6690
+ spinner.succeed();
6691
+ spinner.start("Connecting to the relay");
6692
+ }
5562
6693
  const known = cfg.tunnels?.[port];
5563
6694
  let hosting = known ? hostTunnel(known, port) : null;
5564
6695
  hosting?.catch(() => {
5565
6696
  });
5566
- tunnelId = await ensureTunnel(cfg, port, anonymous);
5567
- if (tunnelId !== known) {
6697
+ let id;
6698
+ try {
6699
+ id = await ensureTunnel(cfg, port, anonymous);
6700
+ } catch (e) {
5568
6701
  hosting?.then((h) => h.child.kill(), () => {
5569
6702
  });
5570
- hosting = hostTunnel(tunnelId, port);
6703
+ throw e;
5571
6704
  }
5572
- ({ child, url } = await hosting);
6705
+ if (id !== known) {
6706
+ hosting?.then((h) => h.child.kill(), () => {
6707
+ });
6708
+ hosting = hostTunnel(id, port);
6709
+ }
6710
+ const hosted = await hosting;
6711
+ tunnelId = id;
6712
+ child = hosted.child;
6713
+ url = hosted.url;
6714
+ hostedUrl = url;
6715
+ status2?.invalidate?.();
6716
+ if (rememberTunnelUrl(cfg, port, url)) saveConfig(cfg);
5573
6717
  access = anonymous ? "anyone with the link (no sign-in, --allow-anonymous)" : `sign-in required as ${user}`;
6718
+ return url;
6719
+ }
6720
+ let offline = false;
6721
+ if (tunnel) {
6722
+ spinner.succeed();
6723
+ if (lan.port) {
6724
+ spinner.start("Checking the internet");
6725
+ offline = !await relayReachable();
6726
+ }
6727
+ if (!offline) {
6728
+ try {
6729
+ await connectTunnel({ interactive: true });
6730
+ } catch (e) {
6731
+ spinner.fail();
6732
+ if (e.code !== "ETUNNELLIMIT") throw e;
6733
+ console.error(`
6734
+ ${e.message}`);
6735
+ process.exit(1);
6736
+ }
6737
+ } else {
6738
+ spinner.fail();
6739
+ url = cfg.tunnelUrls?.[port] ?? null;
6740
+ later = retryTunnel({
6741
+ reachable: () => relayReachable(),
6742
+ connect: () => connectTunnel({ interactive: false }),
6743
+ onSignedOut: () => console.log("\n Internet is back, but no one is signed in to dev tunnels: run `coderemote login`."),
6744
+ onError: (e) => console.log(`
6745
+ Couldn't connect the tunnel yet (${e.message.split("\n")[0]}); retrying.`)
6746
+ });
6747
+ later.done.then((connected) => {
6748
+ registry.register({ port, root, url: connected });
6749
+ console.log(`
6750
+ Tunnel connected: ${connected}`);
6751
+ console.log(` Web: ${webUrl({ url: connected, token: cfg.token })}`);
6752
+ console.log(" Phones paired on this network pick it up on their own; sign in on them to use it away from here.\n");
6753
+ });
6754
+ }
5574
6755
  }
5575
6756
  spinner.succeed();
5576
- registry.register({ port, root, url });
6757
+ registry.register({ port, root, url: url ?? `http://127.0.0.1:${port}` });
5577
6758
  const shutdown = async () => {
6759
+ later?.stop();
5578
6760
  child?.kill();
6761
+ advertiser?.kill();
5579
6762
  await preview2?.close();
6763
+ server.saveAgentSessions();
5580
6764
  await server.killAllSessions();
5581
6765
  server.close();
5582
6766
  process.exit(0);
@@ -5585,10 +6769,16 @@ async function start({ tunnel }) {
5585
6769
  process.on("SIGTERM", shutdown);
5586
6770
  process.on("SIGHUP", shutdown);
5587
6771
  process.on("exit", () => {
6772
+ later?.stop();
5588
6773
  child?.kill();
6774
+ advertiser?.kill();
5589
6775
  preview2?.stop();
5590
6776
  });
5591
- return { url, access };
6777
+ const lanLink = lan.port ? {
6778
+ agentId,
6779
+ lan: offline ? { port: lan.port, addresses: qrAddresses(lanAddresses()), fingerprint: lanCert.fingerprint } : null
6780
+ } : null;
6781
+ return { url, access, lan, offline, lanLink, firewall };
5592
6782
  }
5593
6783
  var PLUGIN = "coderemote@coderemote";
5594
6784
  async function claudeCli(...args) {
@@ -5709,7 +6899,7 @@ function currentAgent() {
5709
6899
  const agents = registry.list();
5710
6900
  if (flag("--port")) return agents.find((a) => a.port === port);
5711
6901
  const here = process.cwd();
5712
- const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path16.sep)).sort((a, b) => b.root.length - a.root.length);
6902
+ const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path19.sep)).sort((a, b) => b.root.length - a.root.length);
5713
6903
  return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
5714
6904
  }
5715
6905
  async function previewPorts(agent, given) {
@@ -5775,11 +6965,27 @@ Token: ${cfg.token}`);
5775
6965
  spinner.fail();
5776
6966
  throw e;
5777
6967
  }
5778
- const { url, access } = started;
5779
- const vscodeUrl = rest.includes("--web") ? await startVSCodeTunnel() : null;
6968
+ const { url, access, lan, offline, lanLink, firewall } = started;
6969
+ const vscodeUrl = rest.includes("--web") && !offline ? await startVSCodeTunnel() : null;
5780
6970
  console.log(`
5781
6971
  Sharing: ${root}`);
5782
- printPairing(url, cfg.token, access, vscodeUrl);
6972
+ if (offline) console.log(" Tunnel: not connected (no internet); phones on this network can pair and connect. It connects on its own once the internet is back.");
6973
+ if (lan.port) {
6974
+ console.log(` LAN: on (port ${lan.port}); phones on this network connect directly`);
6975
+ if (process.platform === "darwin") console.log(" If macOS asks whether node may accept incoming connections, choose Allow.");
6976
+ if (isWindows) console.log(" If Windows asks whether node may accept incoming connections, choose Allow (Private networks).");
6977
+ } else if (lan.error) console.log(` LAN: off (${lan.error}); phones connect through the tunnel`);
6978
+ const showFirewall = (lines) => {
6979
+ if (lines.length) console.log(`
6980
+ ${lines.map((l) => ` ${l}`).join("\n")}
6981
+ `);
6982
+ };
6983
+ let firewallLines = null;
6984
+ firewall?.then((lines) => firewallLines = lines);
6985
+ await new Promise((r) => setImmediate(r));
6986
+ if (firewallLines) showFirewall(firewallLines);
6987
+ else firewall?.then(showFirewall);
6988
+ printPairing(url, cfg.token, access, vscodeUrl, lanLink);
5783
6989
  const newer = await update;
5784
6990
  if (newer) {
5785
6991
  const c = (code, s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
@@ -5845,8 +7051,9 @@ Token: ${cfg.token}`);
5845
7051
  break;
5846
7052
  }
5847
7053
  case "rotate-token": {
5848
- cfg.token = randomBytes4(32).toString("base64url");
7054
+ cfg.token = randomBytes6(32).toString("base64url");
5849
7055
  saveConfig(cfg);
7056
+ fs22.rmSync(LAN_DIR, { recursive: true, force: true });
5850
7057
  const agents = registry.list();
5851
7058
  for (const agent of agents) {
5852
7059
  stopAgent(agent.pid);
@@ -5918,7 +7125,7 @@ Token: ${cfg.token}`);
5918
7125
  console.log(`coderemote ${VERSION} is the latest version.`);
5919
7126
  break;
5920
7127
  }
5921
- const [command, args] = installCommand(fs19.realpathSync(process.argv[1]));
7128
+ const [command, args] = installCommand(fs22.realpathSync(process.argv[1]));
5922
7129
  console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
5923
7130
  `);
5924
7131
  const code = await new Promise((resolve) => {