coderemote 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/coderemote.js
4
- import { execFile as execFile8, execFileSync, spawn as spawn3 } from "node:child_process";
5
- import { randomBytes as randomBytes3 } from "node:crypto";
6
- import fs14 from "node:fs";
7
- import net2 from "node:net";
8
- import os6 from "node:os";
9
- import path12 from "node:path";
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";
7
+ import net3 from "node:net";
8
+ import os7 from "node:os";
9
+ import path16 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
- import { createReadStream, createWriteStream as createWriteStream2 } from "node:fs";
16
- import fs8 from "node:fs/promises";
17
- import http2 from "node:http";
18
- import os5 from "node:os";
19
- import path6 from "node:path";
20
- import { pipeline as pipeline2 } from "node:stream/promises";
21
- import { randomUUID as randomUUID4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
15
+ import { createReadStream as createReadStream2, createWriteStream as createWriteStream2 } from "node:fs";
16
+ import fs13 from "node:fs/promises";
17
+ import http3 from "node:http";
18
+ import os6 from "node:os";
19
+ import path10 from "node:path";
20
+ import { pipeline as pipeline3 } from "node:stream/promises";
21
+ import { randomUUID as randomUUID5, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
22
22
 
23
23
  // ../../packages/protocol/index.js
24
24
  var PROTOCOL_VERSION = 4;
25
+ var MIN_CLIENT_PROTOCOL = 4;
25
26
  var PAIRING_SCHEME = "vsremote";
26
27
  var Routes = Object.freeze({
27
28
  info: "GET /api/info",
@@ -32,6 +33,8 @@ var Routes = Object.freeze({
32
33
  fsMkdir: "POST /api/fs/mkdir",
33
34
  fsRename: "POST /api/fs/rename",
34
35
  fsDelete: "DELETE /api/fs",
36
+ // Added within v4: older agents answer 404.
37
+ fsSearch: "GET /api/fs/search",
35
38
  termList: "GET /api/term",
36
39
  termKill: "DELETE /api/term",
37
40
  gitRepos: "GET /api/git/repos",
@@ -57,12 +60,21 @@ var Routes = Object.freeze({
57
60
  previewRequestsClear: "DELETE /api/preview/requests",
58
61
  tunnelToken: "POST /api/tunnel/token",
59
62
  tunnelStatus: "GET /api/tunnel",
63
+ // Added within v4: older agents answer 404.
64
+ tunnelDelete: "DELETE /api/tunnel",
60
65
  agentsHook: "POST /api/agents/hook",
61
66
  agentsSessions: "GET /api/agents/sessions",
62
67
  agentsTimeline: "GET /api/agents/timeline",
63
68
  agentsAnswer: "POST /api/agents/answer",
64
69
  agentsLaunch: "POST /api/agents/sessions",
65
70
  agentsSetup: "GET /api/agents/setup",
71
+ agentsCommands: "GET /api/agents/commands",
72
+ agentsHistory: "GET /api/agents/history",
73
+ buildsSchemes: "GET /api/builds/schemes",
74
+ buildsStart: "POST /api/builds",
75
+ buildsCurrent: "GET /api/builds/current",
76
+ buildsCancel: "POST /api/builds/cancel",
77
+ buildsInstall: "POST /api/builds/install",
66
78
  agentsPrompt: "POST /api/agents/prompt",
67
79
  agentsKeys: "POST /api/agents/keys",
68
80
  agentsStop: "POST /api/agents/stop",
@@ -192,9 +204,12 @@ var RolloutTail = class {
192
204
  #partial = "";
193
205
  #timer = null;
194
206
  #onItem;
195
- constructor(file, onItem) {
207
+ #onTurnEnd;
208
+ constructor(file, onItem, onTurnEnd = () => {
209
+ }) {
196
210
  this.#file = file;
197
211
  this.#onItem = onItem;
212
+ this.#onTurnEnd = onTurnEnd;
198
213
  this.#timer = setInterval(() => this.poll().catch(() => {
199
214
  }), POLL);
200
215
  this.#timer.unref?.();
@@ -220,7 +235,12 @@ var RolloutTail = class {
220
235
  } catch {
221
236
  continue;
222
237
  }
223
- if (record?.type === "event_msg" && record.payload?.type === "item_completed") this.#onItem(record.payload.item);
238
+ if (record?.type !== "event_msg") continue;
239
+ if (record.payload?.type === "item_completed") this.#onItem(record.payload.item);
240
+ else if (record.payload?.type === "task_complete") {
241
+ const message = record.payload.error?.message;
242
+ this.#onTurnEnd(typeof message === "string" && message.trim() ? message.trim() : null);
243
+ }
224
244
  }
225
245
  } finally {
226
246
  await handle.close();
@@ -284,15 +304,15 @@ async function readTranscript(file, limit = 500) {
284
304
  } catch {
285
305
  return [];
286
306
  }
287
- const entries = [];
307
+ const entries2 = [];
288
308
  for (const line of text.split("\n")) {
289
309
  if (!line.trim()) continue;
290
310
  try {
291
- entries.push(...entriesOf(JSON.parse(line)));
311
+ entries2.push(...entriesOf(JSON.parse(line)));
292
312
  } catch {
293
313
  }
294
314
  }
295
- return entries.slice(-limit);
315
+ return entries2.slice(-limit);
296
316
  }
297
317
 
298
318
  // ../../packages/agent-core/src/agents.js
@@ -302,6 +322,7 @@ var MAX_TITLE = 80;
302
322
  var ENDED_TTL = 24 * 60 * 60 * 1e3;
303
323
  var REQUEST_TTL = 60 * 60 * 1e3;
304
324
  var START_TIMEOUT = 15 * 1e3;
325
+ var STOP_GRACE = 3 * 1e3;
305
326
  var SEQUENTIAL_REQUEST_TTL = 10 * 60 * 1e3;
306
327
  var ADAPTERS = {
307
328
  claude: {
@@ -360,6 +381,7 @@ var AgentSession = class {
360
381
  this.titled = false;
361
382
  this.state = "idle";
362
383
  this.terminal = terminal;
384
+ this.app = null;
363
385
  this.startedAt = now;
364
386
  this.lastActivityAt = now;
365
387
  this.endedAt = null;
@@ -411,7 +433,7 @@ var AgentHub = class extends EventEmitter {
411
433
  transcript = readTranscript,
412
434
  startTimeout = START_TIMEOUT,
413
435
  present = async () => false,
414
- rolloutTail = (file, onItem) => new RolloutTail(file, onItem)
436
+ rolloutTail = (file, onItem, onTurnEnd) => new RolloutTail(file, onItem, onTurnEnd)
415
437
  } = {}) {
416
438
  super();
417
439
  this.#now = now;
@@ -433,6 +455,17 @@ var AgentHub = class extends EventEmitter {
433
455
  s.prompts.push({ id, text: comparable(text, paths), at: this.#now() });
434
456
  s.prompts = s.prompts.filter((p) => this.#now() - p.at < 6e4).slice(-20);
435
457
  }
458
+ /**
459
+ * A client pressed Esc in a running session. Claude Code reports no hook for an interrupted
460
+ * turn (only an idle notification a minute later), so the turn ends here; a hook that shows
461
+ * it still going starts a turn again.
462
+ */
463
+ interrupted(sessionId) {
464
+ const s = this.#sessions.get(sessionId);
465
+ if (!s || s.state !== "running" || s.starting) return;
466
+ this.#endTurn(s, this.#now());
467
+ this.#emitSession(s);
468
+ }
436
469
  /** Whether `resume` may reopen a session: only one no process runs any more. */
437
470
  canResume(id) {
438
471
  const s = this.#sessions.get(id);
@@ -477,18 +510,23 @@ var AgentHub = class extends EventEmitter {
477
510
  session(id) {
478
511
  return this.#sessions.get(id)?.toJSON() ?? null;
479
512
  }
513
+ /** Where a session runs: one of the agent's terminals, or an app on the computer (or neither known). */
514
+ host(id) {
515
+ const s = this.#sessions.get(id);
516
+ return s ? { terminal: s.terminal, app: s.app } : null;
517
+ }
480
518
  timeline(id, after = 0) {
481
519
  const s = this.#sessions.get(id);
482
520
  if (!s) throw new HttpError(404, "no such session");
483
- const entries = s.entries.filter((e) => e.seq > after);
484
- return { entries, next: s.seq, requests: [...s.requests.values()].map((r) => r.request) };
521
+ const entries2 = s.entries.filter((e) => e.seq > after);
522
+ return { entries: entries2, next: s.seq, requests: [...s.requests.values()].map((r) => r.request) };
485
523
  }
486
524
  /**
487
525
  * Takes one hook event. Resolves with the hook's output: for `PermissionRequest` once the
488
526
  * request is answered (null when the computer should decide), `{}` for everything else.
489
527
  * Aborting `signal` (the hook was killed because the computer answered) withdraws the request.
490
528
  */
491
- async handle(raw, { agent = "claude", terminal = null, signal } = {}) {
529
+ async handle(raw, { agent = "claude", terminal = null, app = null, signal } = {}) {
492
530
  if (!raw || typeof raw.session_id !== "string" || !raw.session_id) {
493
531
  throw new HttpError(400, "hook event without session_id");
494
532
  }
@@ -497,8 +535,9 @@ var AgentHub = class extends EventEmitter {
497
535
  const now = this.#now();
498
536
  let replaced = null;
499
537
  if (!this.#sessions.has(event.session_id)) {
538
+ const cleared = event.hook_event_name === "SessionStart" && event.source === "clear" && terminal;
500
539
  const candidates = [...this.#sessions.values()].filter(
501
- (p) => p.starting && (terminal ? p.terminal === terminal : p.agent === agent && p.cwd === event.cwd)
540
+ (p) => cleared ? p.terminal === terminal && p.agent === agent : p.starting && (terminal ? p.terminal === terminal : p.agent === agent && p.cwd === event.cwd)
502
541
  );
503
542
  if (candidates.length === 1) {
504
543
  replaced = candidates[0];
@@ -515,7 +554,7 @@ var AgentHub = class extends EventEmitter {
515
554
  }
516
555
  await s.ready;
517
556
  if (s.agent === "codex" && !s.tail && typeof event.transcript_path === "string") {
518
- s.tail = this.#rolloutTail(event.transcript_path, (item) => this.#fromRollout(s, item));
557
+ s.tail = this.#rolloutTail(event.transcript_path, (item) => this.#fromRollout(s, item), (error) => this.#rolloutTurnEnd(s, error));
519
558
  s.tail.poll?.().catch(() => {
520
559
  });
521
560
  }
@@ -524,6 +563,7 @@ var AgentHub = class extends EventEmitter {
524
563
  this.emit("message", { type: "session_removed", session: replaced.id, replacedBy: s.id });
525
564
  }
526
565
  if (terminal && !s.terminal) s.terminal = terminal;
566
+ if (app) s.app = app;
527
567
  s.starting = false;
528
568
  s.lastActivityAt = now;
529
569
  if (s.state === "ended" && event.hook_event_name !== "SessionEnd") {
@@ -532,6 +572,7 @@ var AgentHub = class extends EventEmitter {
532
572
  }
533
573
  switch (event.hook_event_name) {
534
574
  case "SessionStart":
575
+ if (s.state === "running" && s.turnStartedAt == null) s.state = "idle";
535
576
  break;
536
577
  case "UserPromptSubmit": {
537
578
  const text = typeof event.prompt === "string" ? event.prompt : "";
@@ -600,11 +641,8 @@ var AgentHub = class extends EventEmitter {
600
641
  if (last && !turnEntries.some((e) => e.kind === "assistant" && e.text.trim() === last)) {
601
642
  this.#entry(s, { kind: "assistant", text: last });
602
643
  }
603
- s.streaming.clear();
604
- const duration = s.turnStartedAt ? now - s.turnStartedAt : 0;
605
- s.turnStartedAt = null;
606
- s.state = s.requests.size ? "waiting" : "idle";
607
- this.#entry(s, { kind: "turn_end", duration });
644
+ if (s.turnStartedAt == null && s.entries.at(-1)?.kind === "turn_end") break;
645
+ this.#endTurn(s, now);
608
646
  break;
609
647
  }
610
648
  case "Interrupt":
@@ -737,13 +775,13 @@ var AgentHub = class extends EventEmitter {
737
775
  pending.resolve(output);
738
776
  }
739
777
  async #backfill(s, file) {
740
- let entries = [];
778
+ let entries2 = [];
741
779
  try {
742
- entries = await this.#transcript(file, MAX_ENTRIES);
780
+ entries2 = await this.#transcript(file, MAX_ENTRIES);
743
781
  } catch {
744
782
  }
745
783
  const tools = /* @__PURE__ */ new Map();
746
- for (const e of entries) {
784
+ for (const e of entries2) {
747
785
  if (e.kind === "tool") {
748
786
  tools.set(e.toolUseId, e.name);
749
787
  e.input = clipInput(e.input);
@@ -759,6 +797,30 @@ var AgentHub = class extends EventEmitter {
759
797
  if (s.entries.length > MAX_ENTRIES) s.entries.splice(0, s.entries.length - MAX_ENTRIES);
760
798
  }
761
799
  /** Something Codex said or a tool's result, from its rollout, unless the hooks told it already. */
800
+ #endTurn(s, now, error = null) {
801
+ s.streaming.clear();
802
+ const duration = s.turnStartedAt ? now - s.turnStartedAt : 0;
803
+ s.turnStartedAt = null;
804
+ s.state = s.requests.size ? "waiting" : "idle";
805
+ this.#entry(s, { kind: "turn_end", duration, ...error ? { error: true, text: error } : {} });
806
+ }
807
+ /**
808
+ * Codex's rollout says a turn ended. Stop doesn't run when it failed (an unsupported model, a
809
+ * lost connection), so a failed turn ends here, with its error. One that ended normally is
810
+ * left to Stop, which carries the last reply, unless Stop never comes (its hook failed).
811
+ */
812
+ #rolloutTurnEnd(s, error) {
813
+ if (s.turnStartedAt == null) return;
814
+ const started = s.turnStartedAt;
815
+ const end = () => {
816
+ if (s.turnStartedAt !== started) return;
817
+ this.#endTurn(s, this.#now(), error);
818
+ s.lastActivityAt = this.#now();
819
+ this.#emitSession(s);
820
+ };
821
+ if (error) end();
822
+ else setTimeout(end, STOP_GRACE).unref?.();
823
+ }
762
824
  #fromRollout(s, item) {
763
825
  const fields = codexItem(item);
764
826
  if (!fields) return;
@@ -789,26 +851,856 @@ var AgentHub = class extends EventEmitter {
789
851
  s.turnStartedAt = null;
790
852
  }
791
853
  #entry(s, fields) {
792
- const entry2 = { seq: ++s.seq, time: this.#now(), ...fields };
793
- s.entries.push(entry2);
854
+ const entry3 = { seq: ++s.seq, time: this.#now(), ...fields };
855
+ s.entries.push(entry3);
794
856
  if (s.entries.length > MAX_ENTRIES) s.entries.shift();
795
- this.emit("message", { type: "entry", session: s.id, entry: entry2 });
857
+ this.emit("message", { type: "entry", session: s.id, entry: entry3 });
796
858
  }
797
859
  #emitSession(s) {
798
860
  this.emit("message", { type: "session", session: s.toJSON() });
799
861
  }
800
862
  };
801
863
 
802
- // ../../packages/agent-core/src/git.js
803
- import { execFile } from "node:child_process";
864
+ // ../../packages/agent-core/src/commands.js
804
865
  import fs4 from "node:fs/promises";
805
866
  import path3 from "node:path";
867
+ var HEAD_BYTES = 4096;
868
+ var MAX_DESCRIPTION = 300;
869
+ var MAX_DEPTH = 3;
870
+ var CLAUDE = [
871
+ ["/add-dir", "Add a working directory", false, "<path>"],
872
+ ["/agents", "Manage agent configurations", true],
873
+ ["/batch", "Run a task across many files in parallel", false, "<task>"],
874
+ ["/claude-api", "Build with the Claude API and Anthropic SDK", false],
875
+ ["/clear", "Start a new session with empty context", false],
876
+ ["/code-review", "Review the current diff for bugs", false, "[level] [target]"],
877
+ ["/color", "Set the prompt bar color for this session", true],
878
+ ["/compact", "Clear history but keep a summary in context", false, "[instructions]"],
879
+ ["/config", "Open settings", true],
880
+ ["/context", "Show context usage", false],
881
+ ["/debug", "Debug this session", false, "[issue]"],
882
+ ["/doctor", "Check the installation and settings", true],
883
+ ["/effort", "Set the reasoning effort", true, "[level]"],
884
+ ["/exit", "Exit Claude Code", false],
885
+ ["/export", "Export the conversation", true],
886
+ ["/fast", "Toggle fast mode", false],
887
+ ["/feedback", "Send feedback about Claude Code", true],
888
+ ["/fewer-permission-prompts", "Allowlist common read-only commands", false],
889
+ ["/goal", "Set or view the goal for this session", false, "[goal]"],
890
+ ["/help", "Show help and available commands", true],
891
+ ["/hooks", "Manage hook configurations", true],
892
+ ["/ide", "Manage IDE integrations", true],
893
+ ["/init", "Create a CLAUDE.md with codebase documentation", false],
894
+ ["/insights", "Report on your Claude Code sessions", false],
895
+ ["/login", "Sign in to your Anthropic account", true],
896
+ ["/logout", "Sign out of your Anthropic account", false],
897
+ ["/loop", "Run a prompt on a recurring interval", false, "[interval] <prompt>"],
898
+ ["/mcp", "Manage MCP servers", true],
899
+ ["/memory", "Edit memory files", true],
900
+ ["/model", "Set the model", true, "[model]"],
901
+ ["/output-style", "Set the output style", true],
902
+ ["/permissions", "Manage tool permission rules", true],
903
+ ["/plugin", "Manage plugins", true],
904
+ ["/recap", "Summarize this session so far", false],
905
+ ["/release-notes", "Show release notes", false],
906
+ ["/reload-plugins", "Reload plugins", false],
907
+ ["/reload-skills", "Reload skills", false],
908
+ ["/rename", "Rename this session", false, "<name>"],
909
+ ["/resume", "Resume a conversation", true],
910
+ ["/rewind", "Restore code or conversation to an earlier point", true],
911
+ ["/schedule", "Create or manage scheduled cloud agents", false, "[task]"],
912
+ ["/security-review", "Security review of the pending changes", false],
913
+ ["/simplify", "Review changed code for cleanups and apply them", false],
914
+ ["/status", "Show version, model and account status", true],
915
+ ["/statusline", "Set up the status line", false],
916
+ ["/terminal-setup", "Set up terminal key bindings", false],
917
+ ["/theme", "Change the color theme", true],
918
+ ["/ultrareview", "Deep multi-agent review in the cloud", false],
919
+ ["/update-config", "Configure Claude Code settings", false, "<change>"],
920
+ ["/usage", "Show plan usage limits", true]
921
+ ];
922
+ var CODEX = [
923
+ ["/agents", "open the agent command center", true],
924
+ ["/app", "continue this session in the Desktop app", false],
925
+ ["/approve", "approve one retry of a recent auto-review denial", false],
926
+ ["/archive", "archive this session", false],
927
+ ["/cd", "change the current working directory", false, "<path>"],
928
+ ["/clear", "clear the terminal and start a new chat", false],
929
+ ["/compact", "summarize conversation to prevent hitting the context limit", false],
930
+ ["/config", "configure Codex preferences", true],
931
+ ["/copy", "copy the last response or part of it", true],
932
+ ["/debug-config", "show config layers and requirement sources for debugging", false],
933
+ ["/delete", "permanently delete this session", true],
934
+ ["/diff", "show git diff (including untracked files)", false],
935
+ ["/exit", "exit Codex", false],
936
+ ["/experimental", "toggle experimental features", true],
937
+ ["/feedback", "send logs to maintainers", true],
938
+ ["/fork", "fork the current chat", false],
939
+ ["/goal", "set or view the goal for a long-running task", false, "[goal]"],
940
+ ["/hooks", "view and manage lifecycle hooks", true],
941
+ ["/ide", "include current selection, open files, and other context from your IDE", false],
942
+ ["/import", "import setup, this project, and recent chats from Claude Code", true],
943
+ ["/init", "create an AGENTS.md file with instructions for Codex", false],
944
+ ["/keymap", "remap TUI shortcuts", true],
945
+ ["/logout", "log out of Codex", false],
946
+ ["/mcp", "list MCP tools; use /mcp verbose or /mcp login <name>", false, "[verbose | login <name>]"],
947
+ ["/mention", "mention a file", true],
948
+ ["/model", "choose what model and reasoning effort to use", true],
949
+ ["/new", "start a new chat during a conversation", false],
950
+ ["/permissions", "choose what Codex is allowed to do", true],
951
+ ["/pets", "choose or hide the terminal pet", true],
952
+ ["/plan", "switch to Plan mode", false],
953
+ ["/plugins", "browse plugins", true],
954
+ ["/presets", "run or save reusable prompts and slash commands", true],
955
+ ["/ps", "list background terminals", false],
956
+ ["/pwd", "show the current working directory", false],
957
+ ["/raw", "toggle raw scrollback mode for copy-friendly terminal selection", false],
958
+ ["/recap", "summarize the current conversation now", false],
959
+ ["/rename", "rename the current thread", false, "<name>"],
960
+ ["/resume", "resume a saved chat", true],
961
+ ["/review", "review my current changes and find issues", true],
962
+ ["/side", "start a side conversation in an ephemeral fork", false],
963
+ ["/skills", "use skills to improve how Codex performs specific tasks", true],
964
+ ["/status", "show current session configuration and token usage", false],
965
+ ["/statusline", "configure which items appear in the status line", true],
966
+ ["/stop", "stop all background terminals", false],
967
+ ["/subagents", "switch between this session's subagents", true],
968
+ ["/theme", "choose a syntax highlighting theme", true],
969
+ ["/title", "configure which items appear in the terminal title", true],
970
+ ["/tui", "choose the TUI mode for the next launch", true],
971
+ ["/usage", "view account usage or use a usage limit reset", true],
972
+ ["/vim", "toggle Vim mode for the composer", false],
973
+ ["/voice", "start or stop voice; use /voice settings to choose a voice", false],
974
+ ["/warnings", "view retained warnings and diagnostic details", true],
975
+ ["/worktree", "start or continue a conversation in a new worktree", true]
976
+ ];
977
+ var BUILTINS = { claude: CLAUDE, codex: CODEX };
978
+ function builtin([name, description, interactive, argumentHint = ""]) {
979
+ return { name, kind: "command", description, argumentHint, source: "builtin", interactive };
980
+ }
981
+ function unquote(v) {
982
+ const t = v.trim();
983
+ if (/^".*"$/.test(t)) return t.slice(1, -1).replace(/\\(["\\])/g, "$1");
984
+ if (/^'.*'$/.test(t)) return t.slice(1, -1).replace(/''/g, "'");
985
+ return t;
986
+ }
987
+ function frontmatter(text) {
988
+ const lines = text.replace(/^/, "").split(/\r?\n/);
989
+ if (lines[0].trim() !== "---") return null;
990
+ const end = lines.indexOf("---", 1);
991
+ if (end < 0) return null;
992
+ const out = {};
993
+ let block = null;
994
+ for (const line of lines.slice(1, end)) {
995
+ const m = /^([A-Za-z][\w-]*):\s*(.*)$/.exec(line);
996
+ if (m) {
997
+ block = [">", "|", ">-", "|-", ""].includes(m[2].trim()) ? m[1] : null;
998
+ out[m[1]] = block ? "" : unquote(m[2]);
999
+ } else if (block && /^\s+\S/.test(line)) {
1000
+ out[block] = `${out[block]} ${line.trim()}`.trim();
1001
+ }
1002
+ }
1003
+ return out;
1004
+ }
1005
+ async function readHead(file) {
1006
+ let handle;
1007
+ try {
1008
+ handle = await fs4.open(file, "r");
1009
+ const { buffer, bytesRead } = await handle.read(Buffer.alloc(HEAD_BYTES), 0, HEAD_BYTES, 0);
1010
+ return frontmatter(buffer.subarray(0, bytesRead).toString("utf8")) ?? {};
1011
+ } catch {
1012
+ return {};
1013
+ } finally {
1014
+ await handle?.close();
1015
+ }
1016
+ }
1017
+ async function entries(dir) {
1018
+ try {
1019
+ return await fs4.readdir(dir, { withFileTypes: true });
1020
+ } catch {
1021
+ return [];
1022
+ }
1023
+ }
1024
+ async function readJson(file) {
1025
+ try {
1026
+ return JSON.parse(await fs4.readFile(file, "utf8"));
1027
+ } catch {
1028
+ return null;
1029
+ }
1030
+ }
1031
+ function entry(name, kind, head, source) {
1032
+ return {
1033
+ name,
1034
+ kind,
1035
+ description: String(head.description ?? "").slice(0, MAX_DESCRIPTION),
1036
+ argumentHint: kind === "command" ? String(head["argument-hint"] ?? "") : "",
1037
+ source,
1038
+ interactive: false
1039
+ };
1040
+ }
1041
+ async function commandFiles(dir, prefix, source, depth = 0, parts = []) {
1042
+ const out = [];
1043
+ for (const e of await entries(dir)) {
1044
+ if (e.name.startsWith(".")) continue;
1045
+ const file = path3.join(dir, e.name);
1046
+ if (e.isDirectory() && depth < MAX_DEPTH) {
1047
+ out.push(...await commandFiles(file, prefix, source, depth + 1, [...parts, e.name]));
1048
+ } else if (e.isFile() && e.name.endsWith(".md")) {
1049
+ const name = `/${prefix}${[...parts, e.name.slice(0, -3)].join(":")}`;
1050
+ out.push(entry(name, "command", await readHead(file), source));
1051
+ }
1052
+ }
1053
+ return out;
1054
+ }
1055
+ async function skillFiles(dir, { sigil = "/", prefix = "", source, kind = "command" }) {
1056
+ const out = [];
1057
+ for (const e of await entries(dir)) {
1058
+ if (!e.isDirectory() || e.name.startsWith(".")) continue;
1059
+ const file = path3.join(dir, e.name, "SKILL.md");
1060
+ try {
1061
+ await fs4.access(file);
1062
+ } catch {
1063
+ continue;
1064
+ }
1065
+ const head = await readHead(file);
1066
+ if (String(head["user-invocable"]).toLowerCase() === "false") continue;
1067
+ const name = /^[\w.:-]+$/.test(head.name ?? "") ? head.name : e.name;
1068
+ out.push(entry(`${sigil}${prefix}${name}`, kind, head, source));
1069
+ }
1070
+ return out;
1071
+ }
1072
+ async function claudePlugins(home, cwd) {
1073
+ const installed = (await readJson(path3.join(home, ".claude", "plugins", "installed_plugins.json")))?.plugins ?? {};
1074
+ const enabled = {};
1075
+ 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")
1079
+ ]) {
1080
+ Object.assign(enabled, (await readJson(file))?.enabledPlugins ?? {});
1081
+ }
1082
+ const out = [];
1083
+ for (const [key, installs] of Object.entries(installed)) {
1084
+ if (enabled[key] !== true || !Array.isArray(installs)) continue;
1085
+ const install = installs.find((i) => !i.projectPath || path3.resolve(i.projectPath) === path3.resolve(cwd));
1086
+ if (install?.installPath) out.push({ name: key.split("@")[0], root: install.installPath });
1087
+ }
1088
+ return out;
1089
+ }
1090
+ async function claudeCommands(home, cwd) {
1091
+ const user = path3.join(home, ".claude");
1092
+ const project = path3.join(cwd, ".claude");
1093
+ const found = [
1094
+ ...await commandFiles(path3.join(user, "commands"), "", "user"),
1095
+ ...await skillFiles(path3.join(user, "skills"), { source: "user" })
1096
+ ];
1097
+ for (const e of await entries(path3.join(user, "skills", "synced"))) {
1098
+ if (e.isDirectory() && !e.name.startsWith(".")) {
1099
+ found.push(...await skillFiles(path3.join(user, "skills", "synced", e.name), { prefix: "anthropic-skills:", source: "user" }));
1100
+ }
1101
+ }
1102
+ if (path3.resolve(project) !== path3.resolve(user)) {
1103
+ found.push(
1104
+ ...await commandFiles(path3.join(project, "commands"), "", "project"),
1105
+ ...await skillFiles(path3.join(project, "skills"), { source: "project" })
1106
+ );
1107
+ }
1108
+ for (const plugin of await claudePlugins(home, cwd)) {
1109
+ 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" })
1112
+ );
1113
+ }
1114
+ return found;
1115
+ }
1116
+ async function codexDisabled(codexHome) {
1117
+ const off = /* @__PURE__ */ new Set();
1118
+ let text = "";
1119
+ try {
1120
+ text = await fs4.readFile(path3.join(codexHome, "config.toml"), "utf8");
1121
+ } catch {
1122
+ }
1123
+ let section = null;
1124
+ for (const line of text.split(/\r?\n/)) {
1125
+ const header = /^\s*\[plugins\."([^"]+)"\]\s*$/.exec(line);
1126
+ if (header) section = header[1];
1127
+ else if (/^\s*\[/.test(line)) section = null;
1128
+ else if (section && /^\s*enabled\s*=\s*false\b/.test(line)) off.add(section);
1129
+ }
1130
+ return off;
1131
+ }
1132
+ async function latestVersion(dir) {
1133
+ let best = null;
1134
+ for (const e of await entries(dir)) {
1135
+ 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 };
1138
+ }
1139
+ return best?.dir ?? null;
1140
+ }
1141
+ async function projectDirs(cwd, home) {
1142
+ const dirs = [];
1143
+ for (let dir = path3.resolve(cwd); dir !== path3.resolve(home) && dir !== path3.dirname(dir); dir = path3.dirname(dir)) {
1144
+ dirs.push(dir);
1145
+ try {
1146
+ await fs4.access(path3.join(dir, ".git"));
1147
+ break;
1148
+ } catch {
1149
+ }
1150
+ }
1151
+ return dirs;
1152
+ }
1153
+ async function codexMentions(home, codexHome, cwd) {
1154
+ const mention = { sigil: "$", kind: "mention" };
1155
+ const skills = path3.join(codexHome, "skills");
1156
+ const found = [
1157
+ ...await skillFiles(skills, { ...mention, source: "user" }),
1158
+ ...await skillFiles(path3.join(skills, ".system"), { ...mention, source: "user" })
1159
+ ];
1160
+ for (const dir of await projectDirs(cwd, home)) {
1161
+ found.push(...await skillFiles(path3.join(dir, ".agents", "skills"), { ...mention, source: "project" }));
1162
+ }
1163
+ const off = await codexDisabled(codexHome);
1164
+ const cache = path3.join(codexHome, "plugins", "cache");
1165
+ for (const market of await entries(cache)) {
1166
+ if (!market.isDirectory()) continue;
1167
+ for (const plugin of await entries(path3.join(cache, market.name))) {
1168
+ 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" }));
1171
+ }
1172
+ }
1173
+ return found;
1174
+ }
1175
+ async function listCommands({ agent, cwd, home, codexHome = path3.join(home, ".codex") }) {
1176
+ const builtins = (BUILTINS[agent] ?? BUILTINS.claude).map(builtin);
1177
+ const found = agent === "codex" ? await codexMentions(home, codexHome, cwd) : await claudeCommands(home, cwd);
1178
+ const byName = new Map(builtins.map((c) => [c.name, c]));
1179
+ for (const c of found) byName.set(c.name, c);
1180
+ const rest2 = [...byName.values()].filter((c) => c.source !== "builtin").sort((a, b) => a.name.localeCompare(b.name));
1181
+ return [...byName.values()].filter((c) => c.source === "builtin").concat(rest2);
1182
+ }
1183
+
1184
+ // ../../packages/agent-core/src/builds.js
1185
+ import { execFile, spawn } from "node:child_process";
1186
+ import { randomUUID as randomUUID2 } from "node:crypto";
1187
+ 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";
1191
+ var MAX_LINES = 5e3;
1192
+ var MAX_LINE = 2048;
1193
+ var MAX_ERRORS = 20;
1194
+ var TAIL_LINES = 15;
1195
+ var KILL_AFTER = 5e3;
1196
+ var SIGNING_HINTS = [
1197
+ [/requires a development team/i, "Set a team in Xcode: select the app target, then Signing & Capabilities \u203A Team."],
1198
+ [/No Account for Team|No accounts? (?:with|for|in)/i, "Sign in to Xcode with an Apple Account of the project's team (Xcode \u203A Settings \u203A Accounts)."],
1199
+ [
1200
+ /No profiles for|requires a provisioning profile|doesn't include (?:the )?(?:currently selected )?device|not registered|no devices/i,
1201
+ "Run the app on this iPhone from Xcode once, so it is registered for development builds."
1202
+ ]
1203
+ ];
1204
+ async function findContainer(folder) {
1205
+ let names;
1206
+ try {
1207
+ names = await fs5.readdir(folder);
1208
+ } catch {
1209
+ return null;
1210
+ }
1211
+ for (const [ext, kind] of [[".xcworkspace", "workspace"], [".xcodeproj", "project"]]) {
1212
+ const name = names.filter((n) => n.endsWith(ext)).sort()[0];
1213
+ if (name) return { container: path4.join(folder, name), kind };
1214
+ }
1215
+ return null;
1216
+ }
1217
+ function run(command, args, { cwd, timeout = 12e4 } = {}) {
1218
+ return new Promise((resolve, reject) => {
1219
+ execFile(command, args, { cwd, timeout, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
1220
+ if (error) reject(Object.assign(new HttpError(409, (stderr || error.message).trim().split("\n").slice(-3).join("\n")), { code: "EXCODE" }));
1221
+ else resolve(stdout);
1222
+ });
1223
+ });
1224
+ }
1225
+ async function listSchemes(folder, { xcodebuild = "xcodebuild" } = {}) {
1226
+ const found = await findContainer(folder);
1227
+ 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 }));
1229
+ const schemes = out[found.kind]?.schemes ?? [];
1230
+ return { container: path4.basename(found.container), kind: found.kind, schemes };
1231
+ }
1232
+ function failureOf(lines) {
1233
+ const compile = [...new Set(lines.filter((l) => /^\S.*?:\d+:(?:\d+:)? error: /.test(l)))];
1234
+ if (compile.length) return compile.slice(0, MAX_ERRORS).join("\n");
1235
+ const errors = [...new Set(lines.filter((l) => /^(?:xcodebuild: )?error: /.test(l.trim())).map((l) => l.trim()))];
1236
+ const text = (errors.length ? errors.slice(0, MAX_ERRORS) : lines.slice(-TAIL_LINES)).join("\n");
1237
+ const hint = SIGNING_HINTS.find(([re]) => re.test(text))?.[1];
1238
+ return hint ? `${hint}
1239
+
1240
+ ${text}` : text;
1241
+ }
1242
+ function exportOptions() {
1243
+ return `<?xml version="1.0" encoding="UTF-8"?>
1244
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1245
+ <plist version="1.0"><dict>
1246
+ <key>method</key><string>debugging</string>
1247
+ <key>signingStyle</key><string>automatic</string>
1248
+ <key>compileBitcode</key><false/>
1249
+ <key>thinning</key><string>&lt;none&gt;</string>
1250
+ </dict></plist>
1251
+ `;
1252
+ }
1253
+ async function readInfo(appPath) {
1254
+ const info = JSON.parse(await run("plutil", ["-convert", "json", "-o", "-", path4.join(appPath, "Info.plist")]));
1255
+ return {
1256
+ bundleId: info.CFBundleIdentifier,
1257
+ version: info.CFBundleShortVersionString ?? info.CFBundleVersion ?? "1.0",
1258
+ name: info.CFBundleDisplayName ?? info.CFBundleName ?? path4.basename(appPath, ".app")
1259
+ };
1260
+ }
1261
+ var BuildRunner = class extends EventEmitter2 {
1262
+ #xcodebuild;
1263
+ #readInfo;
1264
+ #dir;
1265
+ #now;
1266
+ #killAfter;
1267
+ #build = null;
1268
+ #lines = [];
1269
+ #child = null;
1270
+ constructor({ xcodebuild = "xcodebuild", readInfo: info = readInfo, dir = path4.join(os.homedir(), ".coderemote", "builds"), now = Date.now, killAfter = KILL_AFTER } = {}) {
1271
+ super();
1272
+ this.#xcodebuild = xcodebuild;
1273
+ this.#readInfo = info;
1274
+ this.#dir = dir;
1275
+ this.#now = now;
1276
+ this.#killAfter = killAfter;
1277
+ }
1278
+ get running() {
1279
+ return this.#build?.state === "running";
1280
+ }
1281
+ /** The latest build with its log lines after `after` (a line count), or null before any. */
1282
+ current(after = 0) {
1283
+ if (!this.#build) return null;
1284
+ const { first } = this.#build;
1285
+ const from = Math.max(after, first);
1286
+ return { ...this.#public(), log: this.#lines.slice(from - first), next: first + this.#lines.length };
1287
+ }
1288
+ /** The `.ipa` and app fields of the latest build, once it succeeded. */
1289
+ artifact() {
1290
+ return this.#build?.state === "succeeded" ? this.#build.artifact : null;
1291
+ }
1292
+ /** Starts building `scheme` of the project in `folder`; answers the build. */
1293
+ async start({ folder, scheme, configuration = "Debug" }) {
1294
+ if (this.running) throw new HttpError(409, "a build is already running");
1295
+ if (typeof scheme !== "string" || !scheme) throw new HttpError(400, "scheme is required");
1296
+ if (!/^[\w .-]+$/.test(configuration)) throw new HttpError(400, "invalid configuration");
1297
+ const found = await findContainer(folder);
1298
+ 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 });
1303
+ this.#lines = [];
1304
+ this.#build = { id, path: folder, scheme, configuration, state: "running", step: "archiving", startedAt: this.#now(), endedAt: null, error: null, first: 0, artifact: null };
1305
+ const build = this.#build;
1306
+ this.#run(build, found, out).catch(() => {
1307
+ });
1308
+ return this.#public();
1309
+ }
1310
+ /** Stops the running build. */
1311
+ cancel() {
1312
+ if (!this.running) throw new HttpError(409, "no build is running");
1313
+ this.#build.cancelled = true;
1314
+ this.#stop();
1315
+ }
1316
+ #public() {
1317
+ const { id, path: folder, scheme, configuration, state, step, startedAt, endedAt, error, artifact } = this.#build;
1318
+ return { id, path: folder, scheme, configuration, state, step, startedAt, endedAt, error, app: artifact?.app ?? null };
1319
+ }
1320
+ #log(text) {
1321
+ for (const raw of text.split(/\r?\n/)) {
1322
+ if (!raw) continue;
1323
+ this.#lines.push(raw.length > MAX_LINE ? `${raw.slice(0, MAX_LINE)}\u2026` : raw);
1324
+ }
1325
+ const extra = this.#lines.length - MAX_LINES;
1326
+ if (extra > 0) {
1327
+ this.#lines.splice(0, extra);
1328
+ this.#build.first += extra;
1329
+ }
1330
+ }
1331
+ async #run(build, { container, kind }, out) {
1332
+ const archive = path4.join(out, "App.xcarchive");
1333
+ const exported = path4.join(out, "export");
1334
+ try {
1335
+ await this.#xcode(build, [
1336
+ `-${kind}`,
1337
+ container,
1338
+ "-scheme",
1339
+ build.scheme,
1340
+ "-configuration",
1341
+ build.configuration,
1342
+ "-destination",
1343
+ "generic/platform=iOS",
1344
+ "-archivePath",
1345
+ archive,
1346
+ "-allowProvisioningUpdates",
1347
+ "-skipPackagePluginValidation",
1348
+ "archive"
1349
+ ], build.path);
1350
+ build.step = "exporting";
1351
+ const options = path4.join(out, "ExportOptions.plist");
1352
+ await fs5.writeFile(options, exportOptions());
1353
+ await this.#xcode(build, [
1354
+ "-exportArchive",
1355
+ "-archivePath",
1356
+ archive,
1357
+ "-exportPath",
1358
+ exported,
1359
+ "-exportOptionsPlist",
1360
+ options,
1361
+ "-allowProvisioningUpdates"
1362
+ ], build.path);
1363
+ const ipaName = (await fs5.readdir(exported)).find((n) => n.endsWith(".ipa"));
1364
+ 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"));
1367
+ 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 } };
1371
+ build.state = "succeeded";
1372
+ } catch (e) {
1373
+ if (build.cancelled) {
1374
+ build.state = "cancelled";
1375
+ } else {
1376
+ build.state = "failed";
1377
+ build.error = e.xcodeFailed ? failureOf(this.#lines) : e.message;
1378
+ }
1379
+ } finally {
1380
+ build.endedAt = this.#now();
1381
+ this.#child = null;
1382
+ if (build === this.#build) this.emit("ended", this.#public());
1383
+ }
1384
+ }
1385
+ #xcode(build, args, cwd) {
1386
+ return new Promise((resolve, reject) => {
1387
+ if (build.cancelled) return reject(new Error("cancelled"));
1388
+ this.#log(`$ xcodebuild ${args.join(" ")}`);
1389
+ const child = spawn(this.#xcodebuild, args, { cwd, detached: true, stdio: ["ignore", "pipe", "pipe"] });
1390
+ this.#child = child;
1391
+ let partial = "";
1392
+ const take = (chunk) => {
1393
+ const text = partial + chunk.toString("utf8");
1394
+ const cut = text.lastIndexOf("\n");
1395
+ partial = cut < 0 ? text : text.slice(cut + 1);
1396
+ if (cut >= 0) this.#log(text.slice(0, cut));
1397
+ };
1398
+ child.stdout.on("data", take);
1399
+ child.stderr.on("data", take);
1400
+ child.on("error", (e) => reject(e.code === "ENOENT" ? new Error("xcodebuild was not found: install Xcode on the computer") : e));
1401
+ child.on("close", (code) => {
1402
+ if (partial) this.#log(partial);
1403
+ if (code === 0) resolve();
1404
+ else reject(Object.assign(new Error(`xcodebuild exited with ${code}`), { xcodeFailed: true }));
1405
+ });
1406
+ });
1407
+ }
1408
+ #stop() {
1409
+ const child = this.#child;
1410
+ if (!child?.pid) return;
1411
+ const signal = (s) => {
1412
+ try {
1413
+ process.kill(-child.pid, s);
1414
+ } catch {
1415
+ }
1416
+ };
1417
+ signal("SIGTERM");
1418
+ setTimeout(() => {
1419
+ if (child.exitCode === null && child.signalCode === null) signal("SIGKILL");
1420
+ }, this.#killAfter).unref?.();
1421
+ }
1422
+ };
1423
+
1424
+ // ../../packages/agent-core/src/history.js
1425
+ import fs6 from "node:fs/promises";
1426
+ import path5 from "node:path";
1427
+ var LIMIT = 50;
1428
+ var HEAD_BYTES2 = 128 * 1024;
1429
+ var TAIL_BYTES2 = 64 * 1024;
1430
+ var MAX_TITLE2 = 80;
1431
+ async function slice(file, position, length) {
1432
+ let handle;
1433
+ try {
1434
+ handle = await fs6.open(file, "r");
1435
+ const { size } = await handle.stat();
1436
+ const start2 = position < 0 ? Math.max(0, size + position) : position;
1437
+ const { buffer, bytesRead } = await handle.read(Buffer.alloc(Math.min(length, size)), 0, Math.min(length, size), start2);
1438
+ return buffer.subarray(0, bytesRead).toString("utf8");
1439
+ } catch {
1440
+ return "";
1441
+ } finally {
1442
+ await handle?.close();
1443
+ }
1444
+ }
1445
+ function records(text) {
1446
+ const out = [];
1447
+ for (const line of text.split("\n")) {
1448
+ try {
1449
+ out.push(JSON.parse(line));
1450
+ } catch {
1451
+ }
1452
+ }
1453
+ return out;
1454
+ }
1455
+ function titleOf2(text) {
1456
+ const line = String(text).trim().split("\n")[0].trim();
1457
+ return line.length > MAX_TITLE2 ? `${line.slice(0, MAX_TITLE2 - 1)}\u2026` : line;
1458
+ }
1459
+ function typedPrompt(record) {
1460
+ if (record.type !== "user" || record.isMeta || record.isSidechain) return null;
1461
+ const content = record.message?.content;
1462
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((c) => c?.type === "text")?.text : null;
1463
+ if (typeof text !== "string") return null;
1464
+ const t = text.trim();
1465
+ return t && !t.startsWith("<") && !t.startsWith("[Request interrupted") ? t : null;
1466
+ }
1467
+ async function claudeHistory(home, cwd) {
1468
+ const dir = path5.join(home, ".claude", "projects", path5.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-"));
1469
+ let names = [];
1470
+ try {
1471
+ names = (await fs6.readdir(dir)).filter((n) => n.endsWith(".jsonl"));
1472
+ } catch {
1473
+ return [];
1474
+ }
1475
+ const files = [];
1476
+ for (const name of names) {
1477
+ 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 });
1480
+ } catch {
1481
+ }
1482
+ }
1483
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs);
1484
+ const out = [];
1485
+ for (const f of files) {
1486
+ if (out.length >= LIMIT) break;
1487
+ const tail = records(await slice(f.file, -TAIL_BYTES2, TAIL_BYTES2));
1488
+ const named = tail.findLast((r) => r.type === "custom-title" && r.customTitle)?.customTitle ?? tail.findLast((r) => r.type === "ai-title" && r.aiTitle)?.aiTitle;
1489
+ const head = named ? [] : records(await slice(f.file, 0, HEAD_BYTES2));
1490
+ const prompt = named ? null : head.map(typedPrompt).find(Boolean);
1491
+ if (!named && !prompt) continue;
1492
+ out.push({
1493
+ id: f.id,
1494
+ title: titleOf2(named ?? prompt),
1495
+ updatedAt: Math.round(f.mtimeMs),
1496
+ branch: tail.findLast((r) => typeof r.gitBranch === "string")?.gitBranch ?? null,
1497
+ size: f.size
1498
+ });
1499
+ }
1500
+ return out;
1501
+ }
1502
+ async function codexHistory(codexHome, cwd) {
1503
+ const names = /* @__PURE__ */ new Map();
1504
+ try {
1505
+ for (const r of records(await fs6.readFile(path5.join(codexHome, "session_index.jsonl"), "utf8"))) {
1506
+ if (typeof r.id === "string" && typeof r.thread_name === "string") names.set(r.id, r.thread_name);
1507
+ }
1508
+ } catch {
1509
+ }
1510
+ const files = [];
1511
+ const walk = async (dir, depth) => {
1512
+ let entries2 = [];
1513
+ try {
1514
+ entries2 = await fs6.readdir(dir, { withFileTypes: true });
1515
+ } catch {
1516
+ return;
1517
+ }
1518
+ for (const e of entries2) {
1519
+ const p = path5.join(dir, e.name);
1520
+ if (e.isDirectory() && depth < 3) await walk(p, depth + 1);
1521
+ else if (e.isFile() && /^rollout-.*\.jsonl$/.test(e.name)) {
1522
+ try {
1523
+ const { mtimeMs, size } = await fs6.stat(p);
1524
+ files.push({ file: p, mtimeMs, size });
1525
+ } catch {
1526
+ }
1527
+ }
1528
+ }
1529
+ };
1530
+ await walk(path5.join(codexHome, "sessions"), 0);
1531
+ files.sort((a, b) => b.mtimeMs - a.mtimeMs);
1532
+ const want = path5.resolve(cwd);
1533
+ const out = [];
1534
+ for (const f of files) {
1535
+ if (out.length >= LIMIT) break;
1536
+ const head = await slice(f.file, 0, HEAD_BYTES2);
1537
+ 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;
1539
+ 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
+ const title = names.get(meta.id) ?? prompt;
1541
+ if (!title) continue;
1542
+ out.push({
1543
+ id: meta.id,
1544
+ title: titleOf2(title),
1545
+ updatedAt: Math.round(f.mtimeMs),
1546
+ branch: typeof meta.git?.branch === "string" ? meta.git.branch : null,
1547
+ size: f.size
1548
+ });
1549
+ }
1550
+ return out;
1551
+ }
1552
+ async function listHistory({ agent, cwd, home, codexHome = path5.join(home, ".codex") }) {
1553
+ return agent === "codex" ? codexHistory(codexHome, cwd) : claudeHistory(home, cwd);
1554
+ }
1555
+
1556
+ // ../../packages/agent-core/src/install-links.js
1557
+ import { randomBytes } from "node:crypto";
1558
+ import { createReadStream } from "node:fs";
1559
+ import fs7 from "node:fs/promises";
1560
+ import http from "node:http";
1561
+ import { pipeline } from "node:stream/promises";
1562
+ var TTL = 10 * 60 * 1e3;
1563
+ var SHARE_HOURS = 1;
1564
+ var xml = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1565
+ function manifest({ ipaUrl, bundleId, version, title }) {
1566
+ return `<?xml version="1.0" encoding="UTF-8"?>
1567
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1568
+ <plist version="1.0"><dict>
1569
+ <key>items</key><array><dict>
1570
+ <key>assets</key><array><dict>
1571
+ <key>kind</key><string>software-package</string>
1572
+ <key>url</key><string>${xml(ipaUrl)}</string>
1573
+ </dict></array>
1574
+ <key>metadata</key><dict>
1575
+ <key>bundle-identifier</key><string>${xml(bundleId)}</string>
1576
+ <key>bundle-version</key><string>${xml(version)}</string>
1577
+ <key>kind</key><string>software</string>
1578
+ <key>title</key><string>${xml(title)}</string>
1579
+ </dict>
1580
+ </dict></array>
1581
+ </dict></plist>
1582
+ `;
1583
+ }
1584
+ var InstallLinks = class {
1585
+ #preview;
1586
+ #now;
1587
+ #ttl;
1588
+ #setTimer;
1589
+ #server = null;
1590
+ #port = null;
1591
+ #base = null;
1592
+ #tokens = /* @__PURE__ */ new Map();
1593
+ #serial = Promise.resolve();
1594
+ constructor({ preview: preview2, now = Date.now, ttl = TTL, setTimer = (fn, ms) => setTimeout(fn, ms).unref?.() } = {}) {
1595
+ this.#preview = preview2;
1596
+ this.#now = now;
1597
+ this.#ttl = ttl;
1598
+ this.#setTimer = setTimer;
1599
+ }
1600
+ /** A link installing `ipa` (an app `{ bundleId, version, name }`): `{ url, expiresAt }`. */
1601
+ create({ ipa, app }) {
1602
+ return this.#queue(async () => {
1603
+ await this.#listen();
1604
+ if (!this.#base) {
1605
+ const { url } = await this.#preview.forward(this.#port);
1606
+ await this.#preview.share(this.#port, SHARE_HOURS);
1607
+ this.#base = url.replace(/\/+$/, "");
1608
+ }
1609
+ const token = randomBytes(24).toString("hex");
1610
+ const expiresAt = this.#now() + this.#ttl;
1611
+ const manifestUrl = `${this.#base}/${token}/manifest.plist`;
1612
+ this.#tokens.set(token, {
1613
+ ipa,
1614
+ manifest: manifest({ ipaUrl: `${this.#base}/${token}/app.ipa`, bundleId: app.bundleId, version: app.version, title: app.name })
1615
+ });
1616
+ this.#setTimer(() => this.#expire(token), this.#ttl);
1617
+ return { url: `itms-services://?action=download-manifest&url=${encodeURIComponent(manifestUrl)}`, expiresAt };
1618
+ });
1619
+ }
1620
+ /** Whether links are being served through the preview tunnel right now. */
1621
+ get sharing() {
1622
+ return this.#base !== null;
1623
+ }
1624
+ /** Ends every link now and closes the public window (the Ports tab's Stop). */
1625
+ stop() {
1626
+ const tokens = [...this.#tokens.keys()];
1627
+ if (!tokens.length) return this.#queue(async () => {
1628
+ });
1629
+ for (const token of tokens.slice(0, -1)) this.#tokens.delete(token);
1630
+ this.#expire(tokens.at(-1));
1631
+ return this.#queue(async () => {
1632
+ });
1633
+ }
1634
+ /** Live tokens, for tests. */
1635
+ get size() {
1636
+ return this.#tokens.size;
1637
+ }
1638
+ /** The local port the files are served on (null until first used). */
1639
+ get port() {
1640
+ return this.#port;
1641
+ }
1642
+ close() {
1643
+ this.#tokens.clear();
1644
+ this.#server?.close();
1645
+ }
1646
+ #queue(fn) {
1647
+ const next = this.#serial.then(fn, fn);
1648
+ this.#serial = next.catch(() => {
1649
+ });
1650
+ return next;
1651
+ }
1652
+ #expire(token) {
1653
+ if (!this.#tokens.delete(token) || this.#tokens.size) return;
1654
+ this.#queue(async () => {
1655
+ if (this.#tokens.size || !this.#base) return;
1656
+ this.#base = null;
1657
+ try {
1658
+ await this.#preview.unshare(this.#port);
1659
+ await this.#preview.unforward(this.#port);
1660
+ } catch {
1661
+ }
1662
+ });
1663
+ }
1664
+ async #listen() {
1665
+ if (this.#server) return;
1666
+ this.#server = http.createServer((req, res) => this.#serve(req, res).catch(() => res.destroy()));
1667
+ await new Promise((resolve, reject) => {
1668
+ this.#server.once("error", reject);
1669
+ this.#server.listen(0, "127.0.0.1", resolve);
1670
+ });
1671
+ this.#port = this.#server.address().port;
1672
+ }
1673
+ async #serve(req, res) {
1674
+ const m = /^\/([0-9a-f]{48})\/(manifest\.plist|app\.ipa)$/.exec(req.url.split("?")[0]);
1675
+ const link = m && this.#tokens.get(m[1]);
1676
+ if (!link || req.method !== "GET" && req.method !== "HEAD") {
1677
+ res.writeHead(404).end();
1678
+ return;
1679
+ }
1680
+ if (m[2] === "manifest.plist") {
1681
+ const body = Buffer.from(link.manifest);
1682
+ res.writeHead(200, { "content-type": "text/xml", "content-length": body.length, "cache-control": "no-store" });
1683
+ res.end(req.method === "HEAD" ? void 0 : body);
1684
+ return;
1685
+ }
1686
+ const { size } = await fs7.stat(link.ipa);
1687
+ res.writeHead(200, { "content-type": "application/octet-stream", "content-length": size, "cache-control": "no-store" });
1688
+ if (req.method === "HEAD") return res.end();
1689
+ await pipeline(createReadStream(link.ipa), res);
1690
+ this.#expire(m[1]);
1691
+ }
1692
+ };
1693
+
1694
+ // ../../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";
806
1698
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".build", "build", "dist", "DerivedData", "Pods", "vendor", ".venv", "target"]);
807
1699
  var MAX_SCAN_DEPTH = 4;
808
1700
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
809
1701
  function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 * 1024 } = {}) {
810
1702
  return new Promise((resolve, reject) => {
811
- execFile(
1703
+ execFile2(
812
1704
  "git",
813
1705
  args,
814
1706
  {
@@ -829,30 +1721,36 @@ function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 *
829
1721
  async function findRepos(root2) {
830
1722
  const repos = [];
831
1723
  async function walk(dir, depth) {
832
- let entries;
1724
+ let entries2;
833
1725
  try {
834
- entries = await fs4.readdir(dir, { withFileTypes: true });
1726
+ entries2 = await fs8.readdir(dir, { withFileTypes: true });
835
1727
  } catch {
836
1728
  return;
837
1729
  }
838
- if (entries.some((e) => e.name === ".git")) {
839
- repos.push(dir);
840
- return;
841
- }
1730
+ if (entries2.some((e) => e.name === ".git")) repos.push(dir);
842
1731
  if (depth >= MAX_SCAN_DEPTH) return;
843
- const subdirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name));
844
- await Promise.all(subdirs.map((e) => walk(path3.join(dir, e.name), depth + 1)));
1732
+ 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)));
845
1734
  }
846
1735
  await walk(root2, 0);
847
- repos.sort((a, b) => a.split(path3.sep).length - b.split(path3.sep).length || a.localeCompare(b));
1736
+ repos.sort((a, b) => a.split(path6.sep).length - b.split(path6.sep).length || a.localeCompare(b));
848
1737
  return Promise.all(
849
1738
  repos.map(async (repo) => ({
850
1739
  path: repo,
851
- name: repo === root2 ? path3.basename(root2) : path3.relative(root2, repo),
852
- branch: await currentBranch(repo)
1740
+ name: repo === root2 ? path6.basename(root2) : path6.relative(root2, repo),
1741
+ branch: await currentBranch(repo),
1742
+ ...await summary(repo)
853
1743
  }))
854
1744
  );
855
1745
  }
1746
+ async function summary(repo) {
1747
+ try {
1748
+ const { files, ahead, behind } = await status(repo);
1749
+ return { changes: files.length, ahead, behind };
1750
+ } catch {
1751
+ return { changes: null, ahead: null, behind: null };
1752
+ }
1753
+ }
856
1754
  async function currentBranch(repo) {
857
1755
  try {
858
1756
  const { stdout } = await git(repo, ["symbolic-ref", "--short", "-q", "HEAD"], { okCodes: [0, 1] });
@@ -866,9 +1764,9 @@ async function currentBranch(repo) {
866
1764
  async function status(repo) {
867
1765
  const { stdout } = await git(repo, ["status", "--porcelain=v2", "--branch", "--untracked-files=all", "-z"]);
868
1766
  const result = { branch: null, upstream: null, ahead: 0, behind: 0, files: [] };
869
- const records = stdout.split("\0");
870
- for (let i = 0; i < records.length; i++) {
871
- const r = records[i];
1767
+ const records2 = stdout.split("\0");
1768
+ for (let i = 0; i < records2.length; i++) {
1769
+ const r = records2[i];
872
1770
  if (!r) continue;
873
1771
  if (r.startsWith("# branch.head ")) {
874
1772
  const head = r.slice(14);
@@ -881,20 +1779,20 @@ async function status(repo) {
881
1779
  result.behind = Math.abs(parseInt(b, 10));
882
1780
  } else if (r.startsWith("1 ")) {
883
1781
  const parts = r.split(" ");
884
- result.files.push(entry(parts[1], parts.slice(8).join(" ")));
1782
+ result.files.push(entry2(parts[1], parts.slice(8).join(" ")));
885
1783
  } else if (r.startsWith("2 ")) {
886
1784
  const parts = r.split(" ");
887
- result.files.push({ ...entry(parts[1], parts.slice(9).join(" ")), origPath: records[++i] });
1785
+ result.files.push({ ...entry2(parts[1], parts.slice(9).join(" ")), origPath: records2[++i] });
888
1786
  } else if (r.startsWith("u ")) {
889
1787
  const parts = r.split(" ");
890
- result.files.push({ ...entry(parts[1], parts.slice(10).join(" ")), conflicted: true });
1788
+ result.files.push({ ...entry2(parts[1], parts.slice(10).join(" ")), conflicted: true });
891
1789
  } else if (r.startsWith("? ")) {
892
1790
  result.files.push({ path: r.slice(2), index: ".", worktree: "?" });
893
1791
  }
894
1792
  }
895
1793
  return result;
896
1794
  }
897
- function entry(xy, filePath) {
1795
+ function entry2(xy, filePath) {
898
1796
  return { path: filePath, index: xy[0], worktree: xy[1] };
899
1797
  }
900
1798
  async function diff(repo, file, { staged = false, untracked = false } = {}) {
@@ -1051,32 +1949,33 @@ var LANDING_HTML = `<!doctype html>
1051
1949
  `;
1052
1950
 
1053
1951
  // ../../packages/agent-core/src/ports.js
1054
- import { execFile as execFile3 } from "node:child_process";
1055
- import fs6 from "node:fs/promises";
1056
- import os from "node:os";
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";
1057
1956
 
1058
1957
  // ../../packages/agent-core/src/platform.js
1059
- import { execFile as execFile2, spawn } from "node:child_process";
1060
- import fs5 from "node:fs";
1061
- import path4 from "node:path";
1958
+ import { execFile as execFile3, spawn as spawn2 } from "node:child_process";
1959
+ import fs9 from "node:fs";
1960
+ import path7 from "node:path";
1062
1961
  var isWindows = process.platform === "win32";
1063
1962
  function shellQuote(arg) {
1064
1963
  return `'${String(arg).replace(/'/g, `'\\''`)}'`;
1065
1964
  }
1066
1965
  function runnable(file) {
1067
1966
  try {
1068
- if (!isWindows) fs5.accessSync(file, fs5.constants.X_OK);
1069
- return fs5.statSync(file).isFile();
1967
+ if (!isWindows) fs9.accessSync(file, fs9.constants.X_OK);
1968
+ return fs9.statSync(file).isFile();
1070
1969
  } catch {
1071
1970
  return false;
1072
1971
  }
1073
1972
  }
1074
1973
  function findExecutable(name, env = process.env) {
1075
- const exts = isWindows && !path4.extname(name) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
1974
+ const exts = isWindows && !path7.extname(name) ? (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
1076
1975
  const match = (base2) => exts.map((e) => base2 + e).find(runnable) ?? null;
1077
- if (name.includes("/") || name.includes(path4.sep)) return match(path4.resolve(name));
1078
- for (const dir of (env.PATH ?? env.Path ?? "").split(path4.delimiter)) {
1079
- const found = dir && match(path4.join(dir.replace(/^"(.*)"$/, "$1"), name));
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));
1080
1979
  if (found) return found;
1081
1980
  }
1082
1981
  return null;
@@ -1105,12 +2004,12 @@ function terminalCommand(argv2) {
1105
2004
  }
1106
2005
  function spawnCommand(argv2, options = {}) {
1107
2006
  const { file, args, verbatim } = resolveCommand(argv2);
1108
- return spawn(file, args, { ...options, windowsVerbatimArguments: verbatim });
2007
+ return spawn2(file, args, { ...options, windowsVerbatimArguments: verbatim });
1109
2008
  }
1110
2009
  function execCommand(argv2, options = {}) {
1111
2010
  return new Promise((resolve, reject) => {
1112
2011
  const { file, args, verbatim } = resolveCommand(argv2);
1113
- execFile2(
2012
+ execFile3(
1114
2013
  file,
1115
2014
  args,
1116
2015
  { ...options, windowsVerbatimArguments: verbatim },
@@ -1120,7 +2019,7 @@ function execCommand(argv2, options = {}) {
1120
2019
  }
1121
2020
  function loginShell(script, timeout) {
1122
2021
  return new Promise((resolve) => {
1123
- execFile2(
2022
+ execFile3(
1124
2023
  interactiveShell().file,
1125
2024
  ["-l", "-c", script],
1126
2025
  { timeout, maxBuffer: 16 * 1024 * 1024 },
@@ -1142,9 +2041,9 @@ async function commandExists(command) {
1142
2041
  }
1143
2042
 
1144
2043
  // ../../packages/agent-core/src/ports.js
1145
- function run(cmd2, args) {
2044
+ function run2(cmd2, args) {
1146
2045
  return new Promise((resolve, reject) => {
1147
- execFile3(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
2046
+ execFile4(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
1148
2047
  if (err?.code === "ENOENT") reject(err);
1149
2048
  else resolve(stdout || "");
1150
2049
  });
@@ -1158,7 +2057,7 @@ function splitHostPort(name) {
1158
2057
  }
1159
2058
  async function listeningSockets() {
1160
2059
  try {
1161
- const out2 = await run("lsof", ["-nP", "-a", "-u", String(os.userInfo().uid), "-iTCP", "-sTCP:LISTEN", "-F", "pcn"]);
2060
+ const out2 = await run2("lsof", ["-nP", "-a", "-u", String(os2.userInfo().uid), "-iTCP", "-sTCP:LISTEN", "-F", "pcn"]);
1162
2061
  const sockets2 = [];
1163
2062
  let pid;
1164
2063
  let name;
@@ -1173,9 +2072,9 @@ async function listeningSockets() {
1173
2072
  }
1174
2073
  return sockets2;
1175
2074
  } catch (e) {
1176
- if (e.code !== "ENOENT" || os.platform() !== "linux") throw e;
2075
+ if (e.code !== "ENOENT" || os2.platform() !== "linux") throw e;
1177
2076
  }
1178
- const out = await run("ss", ["-Hltnp"]);
2077
+ const out = await run2("ss", ["-Hltnp"]);
1179
2078
  const sockets = [];
1180
2079
  for (const line of out.split("\n")) {
1181
2080
  const cols = line.trim().split(/\s+/);
@@ -1186,7 +2085,7 @@ async function listeningSockets() {
1186
2085
  return sockets;
1187
2086
  }
1188
2087
  async function processTable() {
1189
- const out = await run("ps", ["-A", "-ww", "-o", "pid=,ppid=,tty=,command="]);
2088
+ const out = await run2("ps", ["-A", "-ww", "-o", "pid=,ppid=,tty=,command="]);
1190
2089
  const table = /* @__PURE__ */ new Map();
1191
2090
  for (const line of out.split("\n")) {
1192
2091
  const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
@@ -1197,11 +2096,11 @@ async function processTable() {
1197
2096
  async function workingDirs(pids) {
1198
2097
  const dirs = /* @__PURE__ */ new Map();
1199
2098
  if (!pids.length || isWindows) return dirs;
1200
- if (os.platform() === "linux") {
1201
- await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs6.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
2099
+ if (os2.platform() === "linux") {
2100
+ await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs10.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
1202
2101
  return dirs;
1203
2102
  }
1204
- const out = await run("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
2103
+ const out = await run2("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
1205
2104
  let pid;
1206
2105
  for (const line of out.split("\n")) {
1207
2106
  if (line[0] === "p") pid = parseInt(line.slice(1), 10);
@@ -1213,9 +2112,9 @@ var WINDOWS_SHELLS = /* @__PURE__ */ new Set(["cmd.exe", "powershell.exe", "pwsh
1213
2112
  async function windowsListeners() {
1214
2113
  const query = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
1215
2114
  const [v4, v6, json] = await Promise.all([
1216
- run("netstat", ["-ano", "-p", "TCP"]),
1217
- run("netstat", ["-ano", "-p", "TCPv6"]),
1218
- run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query])
2115
+ run2("netstat", ["-ano", "-p", "TCP"]),
2116
+ run2("netstat", ["-ano", "-p", "TCPv6"]),
2117
+ run2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", query])
1219
2118
  ]);
1220
2119
  const table = /* @__PURE__ */ new Map();
1221
2120
  for (const p of [JSON.parse(json || "[]")].flat()) {
@@ -1245,9 +2144,9 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
1245
2144
  const byKey = /* @__PURE__ */ new Map();
1246
2145
  for (const s of sockets) {
1247
2146
  const key = `${s.pid}:${s.port}`;
1248
- const entry2 = byKey.get(key);
1249
- if (entry2) {
1250
- if (!entry2.addresses.includes(s.address)) entry2.addresses.push(s.address);
2147
+ const entry3 = byKey.get(key);
2148
+ if (entry3) {
2149
+ if (!entry3.addresses.includes(s.address)) entry3.addresses.push(s.address);
1251
2150
  continue;
1252
2151
  }
1253
2152
  byKey.set(key, {
@@ -1264,10 +2163,10 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
1264
2163
  agent: s.pid === process.pid || AGENT_COMMAND.test(table.get(s.pid)?.command ?? "")
1265
2164
  });
1266
2165
  }
1267
- const entries = [...byKey.values()];
1268
- const dirs = await workingDirs([...new Set(entries.map((e) => e.pid))]);
1269
- for (const e of entries) e.cwd = dirs.get(e.pid) ?? null;
1270
- return entries.sort((a, b) => a.port - b.port || a.pid - b.pid);
2166
+ const entries2 = [...byKey.values()];
2167
+ const dirs = await workingDirs([...new Set(entries2.map((e) => e.pid))]);
2168
+ for (const e of entries2) e.cwd = dirs.get(e.pid) ?? null;
2169
+ return entries2.sort((a, b) => a.port - b.port || a.pid - b.pid);
1271
2170
  }
1272
2171
  function sessionOf(pid, table, shells) {
1273
2172
  for (let p = pid, depth = 0; p > 1 && depth < 64; p = table.get(p)?.ppid, depth++) {
@@ -1278,28 +2177,42 @@ function sessionOf(pid, table, shells) {
1278
2177
  async function killListener(pid, { force = false, shells } = {}) {
1279
2178
  if (!Number.isInteger(pid) || pid <= 1) throw new HttpError(400, "pid must be a process id");
1280
2179
  if (pid === process.pid) throw new HttpError(403, "the agent cannot stop itself");
1281
- const ports = await listPorts(shells);
1282
- if (!ports.some((p) => p.pid === pid)) throw new HttpError(404, "no listening process with that pid");
2180
+ const ports = (await listPorts(shells)).filter((p) => p.pid === pid).map((p) => p.port);
2181
+ if (!ports.length) throw new HttpError(404, "no listening process with that pid");
1283
2182
  try {
1284
2183
  process.kill(pid, force ? "SIGKILL" : "SIGTERM");
1285
2184
  } catch (e) {
1286
2185
  if (e.code === "ESRCH") throw new HttpError(404, "no listening process with that pid");
1287
2186
  throw e;
1288
2187
  }
2188
+ return ports;
2189
+ }
2190
+ function isListening(port2, timeout = 2e3) {
2191
+ return new Promise((resolve) => {
2192
+ const socket = net.connect({ host: "localhost", port: port2, timeout });
2193
+ const done = (up) => {
2194
+ socket.destroy();
2195
+ resolve(up);
2196
+ };
2197
+ socket.once("connect", () => done(true));
2198
+ socket.once("timeout", () => done(true));
2199
+ socket.once("error", () => done(false));
2200
+ });
1289
2201
  }
1290
2202
 
1291
2203
  // ../../packages/agent-core/src/push.js
1292
- import { execFile as execFile4 } from "node:child_process";
1293
- import { createCipheriv, randomBytes } from "node:crypto";
1294
- import os2 from "node:os";
2204
+ import { execFile as execFile5 } from "node:child_process";
2205
+ import { createCipheriv, randomBytes as randomBytes2 } from "node:crypto";
2206
+ import os3 from "node:os";
1295
2207
  import { promisify } from "node:util";
1296
- var run2 = promisify(execFile4);
2208
+ var run3 = promisify(execFile5);
1297
2209
  var FINISHED_AFTER = 60 * 1e3;
1298
2210
  var ACTIVE_WITHIN = 2 * 60;
2211
+ var AT_SESSION_WITHIN = 30;
1299
2212
  async function macIdleSeconds() {
1300
- if (os2.platform() !== "darwin") return null;
2213
+ if (os3.platform() !== "darwin") return null;
1301
2214
  try {
1302
- const { stdout } = await run2("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-k", "HIDIdleTime"], { timeout: 2e3 });
2215
+ const { stdout } = await run3("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-k", "HIDIdleTime"], { timeout: 2e3 });
1303
2216
  const ns = stdout.match(/"HIDIdleTime"\s*=\s*(\d+)/)?.[1];
1304
2217
  return ns ? Number(BigInt(ns) / 1000000000n) : null;
1305
2218
  } catch {
@@ -1310,8 +2223,25 @@ async function userActiveHere() {
1310
2223
  const idle = await macIdleSeconds();
1311
2224
  return idle !== null && idle < ACTIVE_WITHIN;
1312
2225
  }
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
+ }
1313
2243
  function seal(key, data) {
1314
- const nonce = randomBytes(12);
2244
+ const nonce = randomBytes2(12);
1315
2245
  const cipher = createCipheriv("aes-256-gcm", key, nonce);
1316
2246
  const body = Buffer.concat([cipher.update(JSON.stringify(data), "utf8"), cipher.final()]);
1317
2247
  return Buffer.concat([nonce, body, cipher.getAuthTag()]).toString("base64");
@@ -1361,8 +2291,8 @@ var PushSender = class {
1361
2291
  #devices;
1362
2292
  #store;
1363
2293
  #hub;
1364
- #foreground;
1365
- #userActive;
2294
+ #shows;
2295
+ #atSession;
1366
2296
  #fetch;
1367
2297
  // Requests a notification went out for, so their resolution can replace it.
1368
2298
  #notified = /* @__PURE__ */ new Set();
@@ -1370,15 +2300,17 @@ var PushSender = class {
1370
2300
  * @param {object} opts
1371
2301
  * @param {import('./agents.js').AgentHub} opts.hub
1372
2302
  * @param {{ load: () => object[], save: (devices: object[]) => void }} [opts.store]
1373
- * @param {() => boolean} [opts.foreground] whether a client shows this machine right now
1374
- * @param {() => Promise<boolean>} [opts.userActive] whether someone is using the computer
2303
+ * @param {(what: { session?: string, builds?: boolean }) => boolean} [opts.shows] whether a
2304
+ * 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
1375
2307
  */
1376
- constructor({ hub, store = null, foreground = () => false, userActive = null, fetch: fetch2 = globalThis.fetch }) {
2308
+ constructor({ hub, store = null, shows = () => false, atSession = atSessionHere, fetch: fetch2 = globalThis.fetch }) {
1377
2309
  this.#hub = hub;
1378
2310
  this.#store = store;
1379
2311
  this.#devices = store?.load() ?? [];
1380
- this.#foreground = foreground;
1381
- this.#userActive = userActive ?? userActiveHere;
2312
+ this.#shows = shows;
2313
+ this.#atSession = atSession;
1382
2314
  this.#fetch = fetch2;
1383
2315
  hub.on("message", (msg) => this.#follow(msg).catch(() => {
1384
2316
  }));
@@ -1411,10 +2343,12 @@ var PushSender = class {
1411
2343
  #save() {
1412
2344
  this.#store?.save(this.#devices);
1413
2345
  }
1414
- async #quiet() {
1415
- if (this.#foreground()) return true;
2346
+ /** No push about a session someone is looking at, on a client or at the computer. */
2347
+ async #quiet(what) {
2348
+ if (this.#shows(what)) return true;
2349
+ if (!what.session) return false;
1416
2350
  try {
1417
- return await this.#userActive();
2351
+ return await this.#atSession(this.#hub.host(what.session));
1418
2352
  } catch {
1419
2353
  return false;
1420
2354
  }
@@ -1423,7 +2357,7 @@ var PushSender = class {
1423
2357
  if (!this.#devices.length) return;
1424
2358
  if (msg.type === "request") {
1425
2359
  const { request } = msg;
1426
- if (await this.#quiet()) return;
2360
+ if (await this.#quiet({ session: request.session })) return;
1427
2361
  if (!this.#hub.requests().some((r) => r.id === request.id)) return;
1428
2362
  this.#notified.add(request.id);
1429
2363
  const question = request.kind === "question";
@@ -1455,7 +2389,7 @@ var PushSender = class {
1455
2389
  fallback: "Answered on your computer"
1456
2390
  });
1457
2391
  } else if (msg.type === "entry" && msg.entry.kind === "turn_end" && msg.entry.duration >= FINISHED_AFTER) {
1458
- if (await this.#quiet()) return;
2392
+ if (await this.#quiet({ session: msg.session })) return;
1459
2393
  const session = this.#hub.session(msg.session);
1460
2394
  await this.#broadcast("finished", {
1461
2395
  kind: "finished",
@@ -1467,11 +2401,27 @@ var PushSender = class {
1467
2401
  });
1468
2402
  }
1469
2403
  }
2404
+ /** A build ended (see builds.js): ready to install, or failed. Cancelled builds say nothing. */
2405
+ async buildEnded(build) {
2406
+ if (!this.#devices.length || build.state === "cancelled") return;
2407
+ if (await this.#quiet({ builds: true })) return;
2408
+ const ok = build.state === "succeeded";
2409
+ const firstError = String(build.error ?? "").split("\n").find((l) => l.trim()) ?? "";
2410
+ await this.#broadcast("finished", {
2411
+ kind: "build",
2412
+ title: ok ? "Build ready to install" : "Build failed",
2413
+ body: ok ? `${build.app?.name ?? build.scheme} ${build.app?.version ?? ""}`.trim() : `${build.scheme}: ${firstError}`,
2414
+ build: build.id,
2415
+ category: "BUILD",
2416
+ collapseId: `build-${build.id}`,
2417
+ fallback: ok ? "A build is ready to install" : "A build failed"
2418
+ });
2419
+ }
1470
2420
  async #broadcast(kind, note) {
1471
2421
  await Promise.all(this.#devices.filter((d) => d.kinds[kind]).map((d) => this.#send(d, note)));
1472
2422
  }
1473
- async #send(device, { kind, title, body, session, request, question, options, category, collapseId, priority, silent, fallback }) {
1474
- const secret = { kind, title, body, session, request, question, options };
2423
+ async #send(device, { kind, title, body, session, request, build, question, options, category, collapseId, priority, silent, fallback }) {
2424
+ const secret = { kind, title, body, session, request, build, question, options };
1475
2425
  const aps = { alert: { title: "Code Remote", body: fallback }, "mutable-content": 1 };
1476
2426
  if (category) aps.category = category;
1477
2427
  if (session) aps["thread-id"] = session;
@@ -1506,8 +2456,8 @@ var PushSender = class {
1506
2456
  };
1507
2457
 
1508
2458
  // ../../packages/agent-core/src/inspector.js
1509
- import http from "node:http";
1510
- import net from "node:net";
2459
+ import http2 from "node:http";
2460
+ import net2 from "node:net";
1511
2461
  import zlib from "node:zlib";
1512
2462
  var MAX_ENTRIES2 = 200;
1513
2463
  var MAX_BODY = 64 * 1024;
@@ -1549,7 +2499,7 @@ var InspectingProxy = class {
1549
2499
  /** @param {number} target the dev server's port on this machine */
1550
2500
  constructor(target) {
1551
2501
  this.target = target;
1552
- this.server = http.createServer((req, res) => this.#forward(req, res));
2502
+ this.server = http2.createServer((req, res) => this.#forward(req, res));
1553
2503
  this.server.on("upgrade", (req, socket, head) => this.#tunnelUpgrade(req, socket, head));
1554
2504
  }
1555
2505
  /** Listens on a free loopback port; resolves to it. */
@@ -1567,7 +2517,7 @@ var InspectingProxy = class {
1567
2517
  }
1568
2518
  /** Summaries, oldest first; `after` skips ids up to and including it. */
1569
2519
  list(after = 0) {
1570
- return this.#entries.filter((e) => e.id > after).map(summary);
2520
+ return this.#entries.filter((e) => e.id > after).map(summary2);
1571
2521
  }
1572
2522
  get(id) {
1573
2523
  return this.#entries.find((e) => e.id === id);
@@ -1576,7 +2526,7 @@ var InspectingProxy = class {
1576
2526
  this.#entries = [];
1577
2527
  }
1578
2528
  #record(req) {
1579
- const entry2 = {
2529
+ const entry3 = {
1580
2530
  id: nextId++,
1581
2531
  time: Date.now(),
1582
2532
  method: req.method,
@@ -1587,21 +2537,21 @@ var InspectingProxy = class {
1587
2537
  request: { headers: req.headers, tap: new BodyTap() },
1588
2538
  response: { headers: {}, tap: new BodyTap() }
1589
2539
  };
1590
- this.#entries.push(entry2);
2540
+ this.#entries.push(entry3);
1591
2541
  if (this.#entries.length > MAX_ENTRIES2) this.#entries.shift();
1592
- return entry2;
2542
+ return entry3;
1593
2543
  }
1594
2544
  /** The dev server sees itself addressed as localhost:<its port>, as without the proxy. */
1595
2545
  #headers(req) {
1596
2546
  return { ...req.headers, host: `localhost:${this.target}` };
1597
2547
  }
1598
2548
  #forward(req, res) {
1599
- const entry2 = this.#record(req);
2549
+ const entry3 = this.#record(req);
1600
2550
  const started = performance.now();
1601
2551
  const finish = () => {
1602
- entry2.duration = Math.round(performance.now() - started);
2552
+ entry3.duration = Math.round(performance.now() - started);
1603
2553
  };
1604
- const upstream = http.request({
2554
+ const upstream = http2.request({
1605
2555
  agent: false,
1606
2556
  host: "localhost",
1607
2557
  port: this.target,
@@ -1610,11 +2560,11 @@ var InspectingProxy = class {
1610
2560
  headers: this.#headers(req)
1611
2561
  });
1612
2562
  upstream.on("response", (up) => {
1613
- entry2.status = up.statusCode;
1614
- entry2.response.headers = up.headers;
2563
+ entry3.status = up.statusCode;
2564
+ entry3.response.headers = up.headers;
1615
2565
  res.writeHead(up.statusCode, up.statusMessage, up.rawHeaders);
1616
2566
  up.on("data", (chunk) => {
1617
- entry2.response.tap.push(chunk);
2567
+ entry3.response.tap.push(chunk);
1618
2568
  res.write(chunk);
1619
2569
  });
1620
2570
  up.on("end", () => {
@@ -1625,14 +2575,14 @@ var InspectingProxy = class {
1625
2575
  });
1626
2576
  upstream.on("error", (e) => {
1627
2577
  finish();
1628
- entry2.error = e.code === "ECONNREFUSED" ? `nothing is listening on port ${this.target}` : e.message;
2578
+ entry3.error = e.code === "ECONNREFUSED" ? `nothing is listening on port ${this.target}` : e.message;
1629
2579
  if (res.headersSent) return res.destroy();
1630
- entry2.status = 502;
2580
+ entry3.status = 502;
1631
2581
  res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
1632
- res.end(`coderemote preview: ${entry2.error}
2582
+ res.end(`coderemote preview: ${entry3.error}
1633
2583
  `);
1634
2584
  });
1635
- req.on("data", (chunk) => entry2.request.tap.push(chunk));
2585
+ req.on("data", (chunk) => entry3.request.tap.push(chunk));
1636
2586
  req.pipe(upstream);
1637
2587
  res.on("close", () => {
1638
2588
  if (!res.writableFinished) upstream.destroy();
@@ -1640,8 +2590,8 @@ var InspectingProxy = class {
1640
2590
  }
1641
2591
  /** WebSockets (hot reload) pass through as raw bytes; only the handshake is recorded. */
1642
2592
  #tunnelUpgrade(req, socket, head) {
1643
- const entry2 = this.#record(req);
1644
- const upstream = net.connect({ host: "localhost", port: this.target }, () => {
2593
+ const entry3 = this.#record(req);
2594
+ const upstream = net2.connect({ host: "localhost", port: this.target }, () => {
1645
2595
  const headers = this.#headers(req);
1646
2596
  const lines = [`${req.method} ${req.url} HTTP/1.1`];
1647
2597
  for (const [k, v] of Object.entries(headers)) {
@@ -1651,18 +2601,18 @@ var InspectingProxy = class {
1651
2601
  \r
1652
2602
  `);
1653
2603
  if (head?.length) upstream.write(head);
1654
- entry2.status = 101;
2604
+ entry3.status = 101;
1655
2605
  upstream.pipe(socket);
1656
2606
  socket.pipe(upstream);
1657
2607
  });
1658
2608
  upstream.on("error", (e) => {
1659
- entry2.error = e.message;
2609
+ entry3.error = e.message;
1660
2610
  socket.destroy();
1661
2611
  });
1662
2612
  socket.on("error", () => upstream.destroy());
1663
2613
  }
1664
2614
  };
1665
- function summary(e) {
2615
+ function summary2(e) {
1666
2616
  return {
1667
2617
  id: e.id,
1668
2618
  time: e.time,
@@ -1678,16 +2628,369 @@ function summary(e) {
1678
2628
  }
1679
2629
  function detail(e) {
1680
2630
  return {
1681
- ...summary(e),
2631
+ ...summary2(e),
1682
2632
  request: { headers: e.request.headers, body: e.request.tap.describe(e.request.headers) },
1683
2633
  response: { headers: e.response.headers, body: e.response.tap.describe(e.response.headers) }
1684
2634
  };
1685
2635
  }
1686
2636
 
2637
+ // ../../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";
2641
+ import readline from "node:readline";
2642
+ var ALWAYS_SKIPPED = /* @__PURE__ */ new Set([".git", "node_modules"]);
2643
+ var MAX_FILE_BYTES = 2 * 1024 * 1024;
2644
+ var MAX_QUERY = 1e3;
2645
+ var MAX_LINE2 = 240;
2646
+ var LEAD = 40;
2647
+ var MAX_RANGES = 20;
2648
+ var NAME_LIMIT = 100;
2649
+ var DEFAULT_LIMIT = 500;
2650
+ var MAX_LIMIT = 2e3;
2651
+ var PER_FILE = 100;
2652
+ var TIMEOUT_MS = 15e3;
2653
+ var rgPath;
2654
+ function ripgrep() {
2655
+ if (rgPath === void 0) rgPath = findExecutable("rg");
2656
+ return rgPath;
2657
+ }
2658
+ function queryRegex(query, { caseSensitive = false, wholeWord = false, regex = false } = {}, global = false) {
2659
+ let source = regex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2660
+ if (wholeWord) source = `\\b(?:${source})\\b`;
2661
+ try {
2662
+ return new RegExp(source, `${global ? "g" : ""}${caseSensitive ? "" : "i"}`);
2663
+ } catch (e) {
2664
+ throw new HttpError(400, `invalid regular expression: ${e.message}`);
2665
+ }
2666
+ }
2667
+ function globSource(glob) {
2668
+ let out = "";
2669
+ for (let i = 0; i < glob.length; i++) {
2670
+ const c = glob[i];
2671
+ if (c === "*" && glob[i + 1] === "*") {
2672
+ if (glob[i + 2] === "/") {
2673
+ out += "(?:.*/)?";
2674
+ i += 2;
2675
+ } else {
2676
+ out += ".*";
2677
+ i += 1;
2678
+ }
2679
+ } else if (c === "*") out += "[^/]*";
2680
+ else if (c === "?") out += "[^/]";
2681
+ else if (c === "{") out += "(?:";
2682
+ else if (c === "}") out += ")";
2683
+ else if (c === ",") out += "|";
2684
+ else out += c.replace(/[.+^$()|[\]\\]/g, "\\$&");
2685
+ }
2686
+ return out;
2687
+ }
2688
+ function pathFilter(include = "", exclude = "") {
2689
+ const matchers = (list) => splitPatterns(list).map((raw) => {
2690
+ let p = raw.replace(/\\/g, "/").replace(/^\.\//, "");
2691
+ if (!/[*?{]/.test(p)) {
2692
+ if (p.startsWith("/")) {
2693
+ const head = p.slice(1).replace(/\/$/, "").toLowerCase();
2694
+ return (rel) => {
2695
+ const r = rel.toLowerCase();
2696
+ return r === head || r.startsWith(`${head}/`);
2697
+ };
2698
+ }
2699
+ const needle = p.toLowerCase();
2700
+ return (rel) => rel.toLowerCase().includes(needle);
2701
+ }
2702
+ if (p.startsWith("/")) p = p.slice(1);
2703
+ else if (!p.startsWith("**/")) p = `**/${p}`;
2704
+ const re = new RegExp(`^${globSource(p.replace(/\/$/, ""))}(?:/.*)?$`, "i");
2705
+ return (rel) => re.test(rel);
2706
+ });
2707
+ const inc = matchers(include);
2708
+ const exc = matchers(exclude);
2709
+ return (rel) => (!inc.length || inc.some((m) => m(rel))) && !exc.some((m) => m(rel));
2710
+ }
2711
+ function splitPatterns(list) {
2712
+ const out = [];
2713
+ let depth = 0;
2714
+ let cur = "";
2715
+ for (const c of list) {
2716
+ if (c === "{") depth++;
2717
+ if (c === "}") depth = Math.max(0, depth - 1);
2718
+ if (c === "," && depth === 0) {
2719
+ out.push(cur);
2720
+ cur = "";
2721
+ } else cur += c;
2722
+ }
2723
+ out.push(cur);
2724
+ return out.map((s) => s.trim()).filter(Boolean);
2725
+ }
2726
+ function preview(line, re) {
2727
+ const all = [];
2728
+ re.lastIndex = 0;
2729
+ for (let m; all.length < MAX_RANGES && (m = re.exec(line)); ) {
2730
+ if (m[0].length === 0) {
2731
+ re.lastIndex++;
2732
+ continue;
2733
+ }
2734
+ all.push([m.index, m.index + m[0].length]);
2735
+ }
2736
+ const indent2 = line.length - line.trimStart().length;
2737
+ let start2 = indent2;
2738
+ if (all.length && line.length - start2 > MAX_LINE2) start2 = Math.max(indent2, all[0][0] - LEAD);
2739
+ const end = Math.min(line.length, start2 + MAX_LINE2);
2740
+ const ranges = all.filter(([s, e]) => e > start2 && s < end).map(([s, e]) => [Math.max(s, start2) - start2, Math.min(e, end) - start2]);
2741
+ return { text: line.slice(start2, end).trimEnd(), ranges, clipped: start2 > indent2 || end < line.length };
2742
+ }
2743
+ function runGit(cwd, args) {
2744
+ return new Promise((resolve) => {
2745
+ execFile6(
2746
+ "git",
2747
+ args,
2748
+ { cwd, maxBuffer: 256 * 1024 * 1024, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } },
2749
+ (err, out) => resolve(err ? null : out)
2750
+ );
2751
+ });
2752
+ }
2753
+ async function exists(p) {
2754
+ try {
2755
+ await fs11.lstat(p);
2756
+ return true;
2757
+ } catch {
2758
+ return false;
2759
+ }
2760
+ }
2761
+ async function listFiles(root2, signal) {
2762
+ const files = [];
2763
+ const skipped = (rel) => rel.split("/").some((part) => ALWAYS_SKIPPED.has(part));
2764
+ const join = (prefix, name) => prefix ? `${prefix}/${name}` : name;
2765
+ async function fromGit(dir, prefix) {
2766
+ const [tracked, untracked] = await Promise.all([
2767
+ runGit(dir, ["ls-files", "-z", "-s"]),
2768
+ runGit(dir, ["ls-files", "-z", "-o", "--exclude-standard"])
2769
+ ]);
2770
+ if (tracked === null || untracked === null) return false;
2771
+ const subdirs = [];
2772
+ const seen = /* @__PURE__ */ new Set();
2773
+ for (const entry3 of tracked.split("\0")) {
2774
+ if (!entry3) continue;
2775
+ const tab = entry3.indexOf(" ");
2776
+ const mode = entry3.slice(0, entry3.indexOf(" "));
2777
+ const rel = entry3.slice(tab + 1);
2778
+ if (seen.has(rel)) continue;
2779
+ seen.add(rel);
2780
+ if (skipped(rel)) continue;
2781
+ if (mode === "160000") subdirs.push(rel);
2782
+ else if (mode !== "120000") files.push(join(prefix, rel));
2783
+ }
2784
+ for (const rel of untracked.split("\0")) {
2785
+ if (!rel || skipped(rel)) continue;
2786
+ if (rel.endsWith("/")) subdirs.push(rel.slice(0, -1));
2787
+ else files.push(join(prefix, rel));
2788
+ }
2789
+ for (const rel of subdirs) await visit(path8.join(dir, rel), join(prefix, rel));
2790
+ return true;
2791
+ }
2792
+ async function visit(dir, prefix) {
2793
+ if (signal?.aborted) return;
2794
+ if (await exists(path8.join(dir, ".git")) && await fromGit(dir, prefix)) return;
2795
+ let entries2;
2796
+ try {
2797
+ entries2 = await fs11.readdir(dir, { withFileTypes: true });
2798
+ } catch {
2799
+ return;
2800
+ }
2801
+ for (const e of entries2) {
2802
+ if (ALWAYS_SKIPPED.has(e.name)) continue;
2803
+ if (e.isDirectory()) await visit(path8.join(dir, e.name), join(prefix, e.name));
2804
+ else if (e.isFile()) files.push(join(prefix, e.name));
2805
+ }
2806
+ }
2807
+ await visit(root2, "");
2808
+ return files;
2809
+ }
2810
+ var Results = class {
2811
+ constructor(limit) {
2812
+ this.limit = limit;
2813
+ this.count = 0;
2814
+ this.byFile = /* @__PURE__ */ new Map();
2815
+ this.truncated = false;
2816
+ }
2817
+ get full() {
2818
+ return this.count >= this.limit;
2819
+ }
2820
+ /** Whether a match came past the limit: there are more than it, so searching can stop. */
2821
+ get done() {
2822
+ return this.full && this.truncated;
2823
+ }
2824
+ add(rel, line, match) {
2825
+ if (this.full) {
2826
+ this.truncated = true;
2827
+ return;
2828
+ }
2829
+ let file = this.byFile.get(rel);
2830
+ if (!file) this.byFile.set(rel, file = []);
2831
+ if (file.length >= PER_FILE) {
2832
+ this.truncated = true;
2833
+ return;
2834
+ }
2835
+ file.push({ line, ...match });
2836
+ this.count++;
2837
+ }
2838
+ };
2839
+ async function searchFile(root2, rel, test, re, results) {
2840
+ const abs = path8.join(root2, ...rel.split("/"));
2841
+ let stat;
2842
+ try {
2843
+ stat = await fs11.lstat(abs);
2844
+ } catch {
2845
+ return;
2846
+ }
2847
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.size === 0) return;
2848
+ let buf;
2849
+ try {
2850
+ buf = await fs11.readFile(abs);
2851
+ } catch {
2852
+ return;
2853
+ }
2854
+ if (buf.subarray(0, 8e3).includes(0)) return;
2855
+ const text = buf.toString("utf8");
2856
+ if (!new RegExp(test.source, `${test.flags}m`).test(text)) return;
2857
+ const lines = text.split("\n");
2858
+ for (let i = 0; i < lines.length && !results.done; i++) {
2859
+ const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
2860
+ if (test.test(line)) results.add(rel, i + 1, preview(line, re));
2861
+ }
2862
+ }
2863
+ async function searchHere(root2, files, test, re, results, signal) {
2864
+ let next = 0;
2865
+ const worker = async () => {
2866
+ while (next < files.length && !results.done && !signal?.aborted) {
2867
+ await searchFile(root2, files[next++], test, re, results);
2868
+ }
2869
+ };
2870
+ await Promise.all(Array.from({ length: 16 }, worker));
2871
+ }
2872
+ function runRipgrep(rg, root2, args, onLine, signal) {
2873
+ return new Promise((resolve, reject) => {
2874
+ const child = spawn3(rg, ["--no-config", "--no-messages", "--hidden", "--glob", "!.git", "--glob", "!node_modules", ...args], {
2875
+ cwd: root2,
2876
+ stdio: ["ignore", "pipe", "pipe"]
2877
+ });
2878
+ let stderr = "";
2879
+ let stopped = false;
2880
+ const stop = () => {
2881
+ stopped = true;
2882
+ child.kill();
2883
+ };
2884
+ signal?.addEventListener("abort", stop, { once: true });
2885
+ child.stderr.on("data", (d) => stderr += d);
2886
+ const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
2887
+ lines.on("line", (line) => {
2888
+ if (!stopped && onLine(line) === false) stop();
2889
+ });
2890
+ child.on("error", reject);
2891
+ child.on("close", (code) => {
2892
+ signal?.removeEventListener("abort", stop);
2893
+ if (code === 2 && /regex/i.test(stderr)) return reject(new HttpError(400, stderr.trim().split("\n").pop()));
2894
+ resolve();
2895
+ });
2896
+ });
2897
+ }
2898
+ var relOf = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
2899
+ async function search({
2900
+ root: root2,
2901
+ query = "",
2902
+ caseSensitive = false,
2903
+ wholeWord = false,
2904
+ regex = false,
2905
+ include = "",
2906
+ exclude = "",
2907
+ limit = DEFAULT_LIMIT,
2908
+ engine = "auto",
2909
+ signal
2910
+ }) {
2911
+ if (query.length > MAX_QUERY) throw new HttpError(400, "query is too long");
2912
+ if (!query && !include.trim()) throw new HttpError(400, "missing query");
2913
+ limit = Math.min(Math.max(1, Math.floor(limit) || DEFAULT_LIMIT), MAX_LIMIT);
2914
+ const options = { caseSensitive, wholeWord, regex };
2915
+ const re = query ? queryRegex(query, options, true) : null;
2916
+ const test = query ? queryRegex(query, options) : null;
2917
+ const keep = pathFilter(include, exclude);
2918
+ const rg = engine === "node" ? null : ripgrep();
2919
+ if (engine === "rg" && !rg) throw new HttpError(500, "ripgrep is not installed");
2920
+ const timeout = new AbortController();
2921
+ const timer = setTimeout(() => timeout.abort(), TIMEOUT_MS);
2922
+ const stop = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
2923
+ const results = new Results(limit);
2924
+ const names = [];
2925
+ let namesTruncated = false;
2926
+ const nameLimit = query ? NAME_LIMIT : limit;
2927
+ const addName = (rel) => {
2928
+ if (!keep(rel)) return true;
2929
+ const base2 = rel.slice(rel.lastIndexOf("/") + 1);
2930
+ if (test && !test.test(base2)) return true;
2931
+ if (names.length >= nameLimit) {
2932
+ namesTruncated = true;
2933
+ return false;
2934
+ }
2935
+ names.push(rel);
2936
+ return true;
2937
+ };
2938
+ try {
2939
+ if (rg) {
2940
+ const listing = runRipgrep(rg, root2, ["--files", "./"], (line) => addName(relOf(line)), stop);
2941
+ const flags = [
2942
+ "--json",
2943
+ "--max-filesize",
2944
+ String(MAX_FILE_BYTES),
2945
+ "--max-count",
2946
+ String(PER_FILE),
2947
+ ...regex ? [] : ["--fixed-strings"],
2948
+ ...caseSensitive ? ["--case-sensitive"] : ["--ignore-case"],
2949
+ ...wholeWord ? ["--word-regexp"] : []
2950
+ ];
2951
+ const content = query ? runRipgrep(
2952
+ rg,
2953
+ root2,
2954
+ [...flags, "--regexp", query, "--", "./"],
2955
+ (line) => {
2956
+ let msg;
2957
+ try {
2958
+ msg = JSON.parse(line);
2959
+ } catch {
2960
+ return true;
2961
+ }
2962
+ if (msg.type !== "match" || msg.data.path.text === void 0 || msg.data.lines.text === void 0) return true;
2963
+ const rel = relOf(msg.data.path.text);
2964
+ if (!keep(rel)) return true;
2965
+ const text = msg.data.lines.text.replace(/\r?\n$/, "");
2966
+ results.add(rel, msg.data.line_number, preview(text, re));
2967
+ return !results.done;
2968
+ },
2969
+ stop
2970
+ ) : null;
2971
+ await Promise.all([listing, content]);
2972
+ if (stop.aborted && !signal?.aborted) results.truncated = true;
2973
+ } else {
2974
+ const files = (await listFiles(root2, stop)).filter(keep);
2975
+ for (const rel of files) if (!addName(rel)) break;
2976
+ if (query) await searchHere(root2, files, test, re, results, stop);
2977
+ }
2978
+ } finally {
2979
+ clearTimeout(timer);
2980
+ }
2981
+ if (timeout.signal.aborted) results.truncated = true;
2982
+ const byPath = (a, b) => a < b ? -1 : a > b ? 1 : 0;
2983
+ return {
2984
+ names: names.sort(byPath).map((p) => ({ path: p })),
2985
+ files: [...results.byFile.entries()].sort(([a], [b]) => byPath(a, b)).map(([p, matches]) => ({ path: p, matches })),
2986
+ truncated: results.truncated || namesTruncated
2987
+ };
2988
+ }
2989
+
1687
2990
  // ../../packages/agent-core/src/sessions.js
1688
- import { execFile as execFile5 } from "node:child_process";
1689
- import { randomUUID as randomUUID2 } from "node:crypto";
1690
- import os3 from "node:os";
2991
+ import { execFile as execFile7 } from "node:child_process";
2992
+ import { randomUUID as randomUUID3 } from "node:crypto";
2993
+ import os4 from "node:os";
1691
2994
  import pty from "node-pty";
1692
2995
  var SCROLLBACK_LIMIT = 256 * 1024;
1693
2996
  function guardConpty(term) {
@@ -1706,7 +3009,7 @@ var TerminalSession = class {
1706
3009
  * terminalCommand says; the session ends when it exits.
1707
3010
  */
1708
3011
  constructor({ cols, rows, cwd, env = {}, command = null }) {
1709
- this.id = randomUUID2();
3012
+ this.id = randomUUID3();
1710
3013
  this.createdAt = Date.now();
1711
3014
  this.chunks = [];
1712
3015
  this.bufferedBytes = 0;
@@ -1719,7 +3022,7 @@ var TerminalSession = class {
1719
3022
  name: "xterm-256color",
1720
3023
  cols,
1721
3024
  rows,
1722
- cwd: cwd || os3.homedir(),
3025
+ cwd: cwd || os4.homedir(),
1723
3026
  // CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
1724
3027
  env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
1725
3028
  });
@@ -1772,7 +3075,7 @@ var TerminalSession = class {
1772
3075
  /** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
1773
3076
  kill(signal) {
1774
3077
  if (!isWindows) this.pty.kill(signal);
1775
- else if (this.exitCode === null) execFile5("taskkill", ["/T", "/F", "/PID", String(this.pty.pid)], () => {
3078
+ else if (this.exitCode === null) execFile7("taskkill", ["/T", "/F", "/PID", String(this.pty.pid)], () => {
1776
3079
  });
1777
3080
  }
1778
3081
  /** Types into the session, as a client's keyboard would; nothing once it has exited. */
@@ -1831,21 +3134,21 @@ var SessionManager = class {
1831
3134
  };
1832
3135
 
1833
3136
  // ../../packages/agent-core/src/uploads.js
1834
- import { execFile as execFile6 } from "node:child_process";
1835
- import { randomUUID as randomUUID3 } from "node:crypto";
3137
+ import { execFile as execFile8 } from "node:child_process";
3138
+ import { randomUUID as randomUUID4 } from "node:crypto";
1836
3139
  import { createWriteStream } from "node:fs";
1837
- import fs7 from "node:fs/promises";
1838
- import os4 from "node:os";
1839
- import path5 from "node:path";
3140
+ import fs12 from "node:fs/promises";
3141
+ import os5 from "node:os";
3142
+ import path9 from "node:path";
1840
3143
  import { Transform } from "node:stream";
1841
- import { pipeline } from "node:stream/promises";
3144
+ import { pipeline as pipeline2 } from "node:stream/promises";
1842
3145
  import { promisify as promisify2 } from "node:util";
1843
- var run3 = promisify2(execFile6);
3146
+ var run4 = promisify2(execFile8);
1844
3147
  var UPLOAD_LIMIT = 100 * 1024 * 1024;
1845
3148
  var KEEP = 7 * 24 * 60 * 60 * 1e3;
1846
3149
  var ID = /^[0-9a-f-]{36}$/;
1847
3150
  function safeName(name) {
1848
- const base2 = path5.basename(String(name ?? "").replace(/\\/g, "/"));
3151
+ const base2 = path9.basename(String(name ?? "").replace(/\\/g, "/"));
1849
3152
  const clean = base2.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
1850
3153
  return clean || "file";
1851
3154
  }
@@ -1857,10 +3160,10 @@ var UploadStore = class {
1857
3160
  /** Streams `body` to disk. 413 past `limit`; HEIC photos become JPEG on macOS. */
1858
3161
  async save(body, { name, type, length, limit = UPLOAD_LIMIT }) {
1859
3162
  if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
1860
- const id = randomUUID3();
3163
+ const id = randomUUID4();
1861
3164
  const day = new Date(this.now()).toISOString().slice(0, 10);
1862
- await fs7.mkdir(path5.join(this.dir, day), { recursive: true, mode: 448 });
1863
- let file = path5.join(this.dir, day, `${id}-${safeName(name)}`);
3165
+ await fs12.mkdir(path9.join(this.dir, day), { recursive: true, mode: 448 });
3166
+ let file = path9.join(this.dir, day, `${id}-${safeName(name)}`);
1864
3167
  let size = 0;
1865
3168
  const counter = new Transform({
1866
3169
  transform(chunk, _enc, done) {
@@ -1869,41 +3172,41 @@ var UploadStore = class {
1869
3172
  }
1870
3173
  });
1871
3174
  try {
1872
- await pipeline(body, counter, createWriteStream(file, { mode: 384 }));
3175
+ await pipeline2(body, counter, createWriteStream(file, { mode: 384 }));
1873
3176
  } catch (e) {
1874
- await fs7.rm(file, { force: true });
3177
+ await fs12.rm(file, { force: true });
1875
3178
  throw e;
1876
3179
  }
1877
3180
  let kind = type || "application/octet-stream";
1878
- if ((kind === "image/heic" || /\.heic$/i.test(file)) && os4.platform() === "darwin") {
3181
+ if ((kind === "image/heic" || /\.heic$/i.test(file)) && os5.platform() === "darwin") {
1879
3182
  const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
1880
3183
  try {
1881
- await run3("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
1882
- await fs7.rm(file);
3184
+ await run4("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
3185
+ await fs12.rm(file);
1883
3186
  file = jpeg;
1884
3187
  kind = "image/jpeg";
1885
- size = (await fs7.stat(file)).size;
3188
+ size = (await fs12.stat(file)).size;
1886
3189
  } catch {
1887
3190
  }
1888
3191
  }
1889
- return { id, name: path5.basename(file).slice(id.length + 1), type: kind, size, path: file };
3192
+ return { id, name: path9.basename(file).slice(id.length + 1), type: kind, size, path: file };
1890
3193
  }
1891
3194
  /** The file of an upload; 404 once it's gone (swept, or never existed). */
1892
3195
  async path(id) {
1893
3196
  if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
1894
3197
  let days = [];
1895
3198
  try {
1896
- days = (await fs7.readdir(this.dir)).sort().reverse();
3199
+ days = (await fs12.readdir(this.dir)).sort().reverse();
1897
3200
  } catch {
1898
3201
  }
1899
3202
  for (const day of days) {
1900
3203
  let names = [];
1901
3204
  try {
1902
- names = await fs7.readdir(path5.join(this.dir, day));
3205
+ names = await fs12.readdir(path9.join(this.dir, day));
1903
3206
  } catch {
1904
3207
  }
1905
3208
  const hit = names.find((n) => n.startsWith(`${id}-`));
1906
- if (hit) return path5.join(this.dir, day, hit);
3209
+ if (hit) return path9.join(this.dir, day, hit);
1907
3210
  }
1908
3211
  throw new HttpError(404, "no such upload (they are kept for 7 days)");
1909
3212
  }
@@ -1911,25 +3214,25 @@ var UploadStore = class {
1911
3214
  async sweep() {
1912
3215
  let days = [];
1913
3216
  try {
1914
- days = await fs7.readdir(this.dir);
3217
+ days = await fs12.readdir(this.dir);
1915
3218
  } catch {
1916
3219
  return;
1917
3220
  }
1918
3221
  const cutoff = this.now() - KEEP;
1919
3222
  for (const day of days) {
1920
- const dir = path5.join(this.dir, day);
3223
+ const dir = path9.join(this.dir, day);
1921
3224
  let names = [];
1922
3225
  try {
1923
- names = await fs7.readdir(dir);
3226
+ names = await fs12.readdir(dir);
1924
3227
  } catch {
1925
3228
  continue;
1926
3229
  }
1927
3230
  for (const n of names) {
1928
- const file = path5.join(dir, n);
1929
- const stat = await fs7.stat(file).catch(() => null);
1930
- if (stat && stat.mtimeMs < cutoff) await fs7.rm(file, { force: true });
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 });
1931
3234
  }
1932
- if (!(await fs7.readdir(dir).catch(() => [])).length) await fs7.rm(dir, { recursive: true, force: true });
3235
+ if (!(await fs12.readdir(dir).catch(() => [])).length) await fs12.rm(dir, { recursive: true, force: true });
1933
3236
  }
1934
3237
  }
1935
3238
  };
@@ -1942,21 +3245,48 @@ function kindOf(stat) {
1942
3245
  return "other";
1943
3246
  }
1944
3247
  async function describe(fullPath, name) {
1945
- const lst = await fs8.lstat(fullPath);
1946
- const entry2 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
3248
+ const lst = await fs13.lstat(fullPath);
3249
+ const entry3 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
1947
3250
  if (lst.isSymbolicLink()) {
1948
3251
  try {
1949
- entry2.target = kindOf(await fs8.stat(fullPath));
3252
+ entry3.target = kindOf(await fs13.stat(fullPath));
1950
3253
  } catch {
1951
- entry2.target = "broken";
3254
+ entry3.target = "broken";
1952
3255
  }
1953
3256
  }
1954
- return entry2;
3257
+ return entry3;
1955
3258
  }
1956
3259
  var PASTE_START = "\x1B[200~";
1957
3260
  var PASTE_END = "\x1B[201~";
1958
3261
  var KEYS = { interrupt: "\x1B", cycle_mode: "\x1B[Z" };
1959
- var COMMANDS = ["/compact", "/clear", "/model"];
3262
+ var MEDIA_TYPES = {
3263
+ ".mp4": "video/mp4",
3264
+ ".m4v": "video/x-m4v",
3265
+ ".mov": "video/quicktime",
3266
+ ".webm": "video/webm",
3267
+ ".mp3": "audio/mpeg",
3268
+ ".m4a": "audio/mp4",
3269
+ ".aac": "audio/aac",
3270
+ ".wav": "audio/wav",
3271
+ ".caf": "audio/x-caf"
3272
+ };
3273
+ function byteRange(header, size) {
3274
+ const m = /^bytes=(\d*)-(\d*)$/.exec(String(header ?? "").trim());
3275
+ if (!m || m[1] === "" && m[2] === "") return null;
3276
+ let start2;
3277
+ let end;
3278
+ if (m[1] === "") {
3279
+ const n = Number(m[2]);
3280
+ if (n === 0) return "unsatisfiable";
3281
+ start2 = Math.max(0, size - n);
3282
+ end = size - 1;
3283
+ } else {
3284
+ start2 = Number(m[1]);
3285
+ end = m[2] === "" ? size - 1 : Math.min(Number(m[2]), size - 1);
3286
+ }
3287
+ if (start2 >= size || start2 > end) return "unsatisfiable";
3288
+ return { start: start2, end };
3289
+ }
1960
3290
  function typeable(text) {
1961
3291
  return text.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
1962
3292
  }
@@ -1966,7 +3296,7 @@ function sendJson(res, status2, body) {
1966
3296
  res.writeHead(status2, { "content-type": "application/json", "content-length": Buffer.byteLength(json) });
1967
3297
  res.end(json);
1968
3298
  }
1969
- async function readJson(req) {
3299
+ async function readJson2(req) {
1970
3300
  const chunks = [];
1971
3301
  for await (const c of req) chunks.push(c);
1972
3302
  try {
@@ -1979,18 +3309,24 @@ function createAgentServer({
1979
3309
  token,
1980
3310
  root: root2,
1981
3311
  tunnelToken = async () => null,
1982
- preview = null,
3312
+ preview: preview2 = null,
1983
3313
  tunnelStatus: tunnelStatus2 = null,
3314
+ tunnelDelete: tunnelDelete2 = null,
1984
3315
  hookSecret = null,
1985
3316
  hookBin = null,
1986
3317
  uploadsDir = null,
1987
3318
  claudeCommand = "claude",
1988
3319
  codexCommand = "codex",
1989
- codexHome = process.env.CODEX_HOME || path6.join(os5.homedir(), ".codex"),
3320
+ codexHome = process.env.CODEX_HOME || path10.join(os6.homedir(), ".codex"),
3321
+ home = os6.homedir(),
3322
+ xcodebuild = "xcodebuild",
3323
+ builds = new BuildRunner({ xcodebuild }),
1990
3324
  pluginInstalled = null,
1991
3325
  agents = new AgentHub(),
1992
3326
  pushStore: pushStore2 = null,
1993
- pushOptions = {}
3327
+ pushOptions = {},
3328
+ previewSweep = {},
3329
+ version = null
1994
3330
  }) {
1995
3331
  const handoff = new TunnelHandoff({ token, mint: tunnelToken });
1996
3332
  const sandbox = new Sandbox(root2);
@@ -2003,9 +3339,13 @@ function createAgentServer({
2003
3339
  });
2004
3340
  }, 10 * 60 * 1e3);
2005
3341
  sweep.unref();
2006
- let foregroundAt = 0;
2007
- const clientInForeground = () => Date.now() - foregroundAt < 3e4;
2008
- const push2 = new PushSender({ hub: agents, store: pushStore2, foreground: clientInForeground, ...pushOptions });
3342
+ const presences = /* @__PURE__ */ new Map();
3343
+ const freshPresences = () => [...presences.values()].filter((p) => Date.now() - p.at < 3e4);
3344
+ const clientInForeground = () => freshPresences().length > 0;
3345
+ const clientShows = ({ session = null, builds: builds2 = false }) => freshPresences().some((p) => p.all || (session ? p.session === session : builds2 && p.builds));
3346
+ const push2 = new PushSender({ hub: agents, store: pushStore2, shows: clientShows, ...pushOptions });
3347
+ builds.on("ended", (build) => push2.buildEnded(build).catch(() => {
3348
+ }));
2009
3349
  if (!agents.presenceSet) {
2010
3350
  agents.present = async () => !clientInForeground() && await (pushOptions.userActive ?? userActiveHere)();
2011
3351
  }
@@ -2096,12 +3436,16 @@ function createAgentServer({
2096
3436
  }
2097
3437
  async function codexModels() {
2098
3438
  try {
2099
- const { models } = JSON.parse(await fs8.readFile(path6.join(codexHome, "models_cache.json"), "utf8"));
3439
+ const { models } = JSON.parse(await fs13.readFile(path10.join(codexHome, "models_cache.json"), "utf8"));
2100
3440
  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 ?? "" }));
2101
3441
  } catch {
2102
3442
  return [];
2103
3443
  }
2104
3444
  }
3445
+ const installs = preview2 ? new InstallLinks({ preview: preview2 }) : null;
3446
+ function sessionCommands(session) {
3447
+ return listCommands({ agent: session.agent, cwd: session.cwd || sandbox.root, home, codexHome });
3448
+ }
2105
3449
  function hookAuthorized(req) {
2106
3450
  if (!hookSecret) return false;
2107
3451
  const got = Buffer.from(String(req.headers[HOOK_SECRET_HEADER] || ""));
@@ -2109,7 +3453,7 @@ function createAgentServer({
2109
3453
  return got.length === want.length && timingSafeEqual2(got, want);
2110
3454
  }
2111
3455
  function previewPort(value, ownPort) {
2112
- if (!preview) throw new HttpError(404, "previews need a tunnel");
3456
+ if (!preview2) throw new HttpError(404, "previews need a tunnel");
2113
3457
  const port2 = Number(value);
2114
3458
  if (!Number.isInteger(port2) || port2 < 1 || port2 > 65535) throw new HttpError(400, "port must be 1-65535");
2115
3459
  if (port2 === ownPort) throw new HttpError(400, "the agent's own port is already on its tunnel");
@@ -2119,7 +3463,7 @@ function createAgentServer({
2119
3463
  const proxies = /* @__PURE__ */ new Map();
2120
3464
  function previewed(port2) {
2121
3465
  const proxy = proxies.get(port2);
2122
- if (!proxy || !preview.list().some((p) => p.port === proxy.port)) {
3466
+ if (!proxy || !preview2.list().some((p) => p.port === proxy.port)) {
2123
3467
  throw new HttpError(404, "that port is not being previewed");
2124
3468
  }
2125
3469
  return proxy;
@@ -2130,36 +3474,68 @@ function createAgentServer({
2130
3474
  return proxy;
2131
3475
  }
2132
3476
  async function previews() {
2133
- const issued = await preview.token();
2134
- const forwarded = new Map(preview.list().map((p) => [p.port, p]));
3477
+ const issued = await preview2.token();
3478
+ const forwarded = new Map(preview2.list().map((p) => [p.port, p]));
2135
3479
  const ports = [];
2136
3480
  for (const [port2, proxy] of proxies) {
2137
3481
  const p = forwarded.get(proxy.port);
2138
3482
  if (p) ports.push({ ...p, port: port2 });
2139
3483
  }
2140
3484
  ports.sort((a, b) => a.port - b.port);
3485
+ const install = installs?.sharing ? forwarded.get(installs.port) : null;
3486
+ if (install) ports.push({ ...install, install: true });
2141
3487
  return { ports, token: issued?.token ?? null, expiresAt: issued?.expiresAt ?? null };
2142
3488
  }
3489
+ const { checkMs = 5e3, graceMs = 3e4, killedMs = 1500 } = previewSweep;
3490
+ const silentSince = /* @__PURE__ */ new Map();
3491
+ let sweeping = false;
3492
+ async function sweepPreviews(ports = [...proxies.keys()], now = false) {
3493
+ const forwarded = new Set(preview2.list().map((p) => p.port));
3494
+ for (const port2 of ports) {
3495
+ const proxy = proxies.get(port2);
3496
+ if (!proxy || !forwarded.has(proxy.port) || await isListening(port2)) {
3497
+ silentSince.delete(port2);
3498
+ continue;
3499
+ }
3500
+ if (!silentSince.has(port2)) silentSince.set(port2, Date.now());
3501
+ if (!now && Date.now() - silentSince.get(port2) < graceMs) continue;
3502
+ silentSince.delete(port2);
3503
+ try {
3504
+ await preview2.unforward(proxy.port);
3505
+ } catch {
3506
+ }
3507
+ }
3508
+ }
3509
+ const previewTimer = preview2 && setInterval(async () => {
3510
+ if (sweeping || !proxies.size) return;
3511
+ sweeping = true;
3512
+ try {
3513
+ await sweepPreviews();
3514
+ } finally {
3515
+ sweeping = false;
3516
+ }
3517
+ }, checkMs);
3518
+ previewTimer?.unref();
2143
3519
  function shellPids() {
2144
3520
  return new Map(sessions.list().map((s) => [s.pid, s.id]));
2145
3521
  }
2146
3522
  async function repoOf(p) {
2147
3523
  const repo = await sandbox.resolve(p);
2148
3524
  try {
2149
- await fs8.access(path6.join(repo, ".git"));
3525
+ await fs13.access(path10.join(repo, ".git"));
2150
3526
  } catch {
2151
3527
  throw new HttpError(400, "not a git repository");
2152
3528
  }
2153
3529
  return repo;
2154
3530
  }
2155
3531
  async function filesIn(repo, files) {
2156
- if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path6.isAbsolute(f))) {
3532
+ if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path10.isAbsolute(f))) {
2157
3533
  throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
2158
3534
  }
2159
3535
  return Promise.all(
2160
3536
  files.map(async (f) => {
2161
- const abs = await sandbox.resolve(path6.join(repo, f), { allowMissing: true });
2162
- const rel = path6.relative(repo, abs);
3537
+ const abs = await sandbox.resolve(path10.join(repo, f), { allowMissing: true });
3538
+ const rel = path10.relative(repo, abs);
2163
3539
  if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
2164
3540
  return rel;
2165
3541
  })
@@ -2168,55 +3544,95 @@ function createAgentServer({
2168
3544
  const routes = {
2169
3545
  [Routes.info]: async () => ({
2170
3546
  protocol: PROTOCOL_VERSION,
2171
- hostname: os5.hostname(),
2172
- platform: os5.platform(),
3547
+ minClientProtocol: MIN_CLIENT_PROTOCOL,
3548
+ version,
3549
+ hostname: os6.hostname(),
3550
+ platform: os6.platform(),
2173
3551
  root: sandbox.root,
2174
- user: os5.userInfo().username,
3552
+ user: os6.userInfo().username,
2175
3553
  // On Windows, terminals run PowerShell whatever $SHELL (Git Bash sets one) says.
2176
3554
  shell: isWindows ? interactiveShell().file : process.env.SHELL || null
2177
3555
  }),
2178
3556
  [Routes.fsList]: async (url) => {
2179
3557
  const dir = await sandbox.resolve(url.searchParams.get("path"));
2180
- const names = await fs8.readdir(dir);
2181
- const entries = (await Promise.all(names.map((n) => describe(path6.join(dir, n), n).catch(() => null)))).filter(Boolean);
2182
- return { path: dir, entries };
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);
3560
+ return { path: dir, entries: entries2 };
2183
3561
  },
2184
3562
  [Routes.fsStat]: async (url) => {
2185
3563
  const p = await sandbox.resolve(url.searchParams.get("path"));
2186
- return { path: p, ...await describe(p, path6.basename(p)) };
3564
+ return { path: p, ...await describe(p, path10.basename(p)) };
2187
3565
  },
2188
3566
  [Routes.fsRead]: async (url, req, res) => {
2189
3567
  const p = await sandbox.resolve(url.searchParams.get("path"));
2190
- const stat = await fs8.stat(p);
3568
+ const stat = await fs13.stat(p);
2191
3569
  if (!stat.isFile()) throw new HttpError(400, "not a file");
2192
- res.writeHead(200, { "content-type": "application/octet-stream", "content-length": stat.size });
2193
- await pipeline2(createReadStream(p), res);
3570
+ const type = MEDIA_TYPES[path10.extname(p).toLowerCase()] ?? "application/octet-stream";
3571
+ const range = byteRange(req.headers.range, stat.size);
3572
+ if (range === "unsatisfiable") {
3573
+ res.writeHead(416, { "content-range": `bytes */${stat.size}`, "accept-ranges": "bytes" });
3574
+ res.end();
3575
+ return;
3576
+ }
3577
+ if (range) {
3578
+ res.writeHead(206, {
3579
+ "content-type": type,
3580
+ "content-length": range.end - range.start + 1,
3581
+ "content-range": `bytes ${range.start}-${range.end}/${stat.size}`,
3582
+ "accept-ranges": "bytes"
3583
+ });
3584
+ await pipeline3(createReadStream2(p, range), res);
3585
+ return;
3586
+ }
3587
+ res.writeHead(200, { "content-type": type, "content-length": stat.size, "accept-ranges": "bytes" });
3588
+ await pipeline3(createReadStream2(p), res);
2194
3589
  },
2195
3590
  [Routes.fsWrite]: async (url, req) => {
2196
3591
  const p = await sandbox.resolveChild(url.searchParams.get("path"), { allowMissing: true });
2197
3592
  const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
2198
3593
  try {
2199
- await pipeline2(req, createWriteStream2(tmp));
2200
- await fs8.rename(tmp, p);
3594
+ await pipeline3(req, createWriteStream2(tmp));
3595
+ await fs13.rename(tmp, p);
2201
3596
  } catch (e) {
2202
- await fs8.rm(tmp, { force: true });
3597
+ await fs13.rm(tmp, { force: true });
2203
3598
  throw e;
2204
3599
  }
2205
- return describe(p, path6.basename(p));
3600
+ return describe(p, path10.basename(p));
2206
3601
  },
2207
3602
  [Routes.fsMkdir]: async (url) => {
2208
- await fs8.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
3603
+ await fs13.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
2209
3604
  return {};
2210
3605
  },
2211
3606
  [Routes.fsRename]: async (url, req) => {
2212
- const { from, to } = await readJson(req);
2213
- await fs8.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
3607
+ const { from, to } = await readJson2(req);
3608
+ await fs13.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
2214
3609
  return {};
2215
3610
  },
2216
3611
  [Routes.fsDelete]: async (url) => {
2217
- await fs8.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
3612
+ await fs13.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
2218
3613
  return {};
2219
3614
  },
3615
+ [Routes.fsSearch]: async (url, req, res) => {
3616
+ const q = url.searchParams;
3617
+ const flag2 = (name) => q.get(name) === "1";
3618
+ const gone = new AbortController();
3619
+ res.on("close", () => {
3620
+ if (!res.writableFinished) gone.abort();
3621
+ });
3622
+ const found = await search({
3623
+ root: sandbox.root,
3624
+ query: q.get("q") ?? "",
3625
+ caseSensitive: flag2("case"),
3626
+ wholeWord: flag2("word"),
3627
+ regex: flag2("regex"),
3628
+ include: q.get("include") ?? "",
3629
+ exclude: q.get("exclude") ?? "",
3630
+ limit: Number(q.get("limit")) || void 0,
3631
+ signal: gone.signal
3632
+ });
3633
+ if (gone.signal.aborted) return void 0;
3634
+ return { root: sandbox.root, ...found };
3635
+ },
2220
3636
  [Routes.gitRepos]: async () => ({ repos: await findRepos(sandbox.root) }),
2221
3637
  [Routes.gitStatus]: async (url) => status(await repoOf(url.searchParams.get("repo"))),
2222
3638
  [Routes.gitDiff]: async (url) => {
@@ -2228,46 +3644,48 @@ function createAgentServer({
2228
3644
  });
2229
3645
  },
2230
3646
  [Routes.gitStage]: async (url, req) => {
2231
- const { repo: r, files } = await readJson(req);
3647
+ const { repo: r, files } = await readJson2(req);
2232
3648
  const repo = await repoOf(r);
2233
3649
  await stage(repo, await filesIn(repo, files));
2234
3650
  return status(repo);
2235
3651
  },
2236
3652
  [Routes.gitUnstage]: async (url, req) => {
2237
- const { repo: r, files } = await readJson(req);
3653
+ const { repo: r, files } = await readJson2(req);
2238
3654
  const repo = await repoOf(r);
2239
3655
  await unstage(repo, await filesIn(repo, files));
2240
3656
  return status(repo);
2241
3657
  },
2242
3658
  [Routes.gitDiscard]: async (url, req) => {
2243
- const { repo: r, files } = await readJson(req);
3659
+ const { repo: r, files } = await readJson2(req);
2244
3660
  const repo = await repoOf(r);
2245
3661
  await discard(repo, await filesIn(repo, files));
2246
3662
  return status(repo);
2247
3663
  },
2248
3664
  [Routes.gitCommit]: async (url, req) => {
2249
- const { repo: r, message } = await readJson(req);
3665
+ const { repo: r, message } = await readJson2(req);
2250
3666
  return commit(await repoOf(r), message);
2251
3667
  },
2252
3668
  [Routes.gitLog]: async (url) => ({
2253
3669
  commits: await log(await repoOf(url.searchParams.get("repo")), parseInt(url.searchParams.get("limit"), 10) || 50)
2254
3670
  }),
2255
3671
  [Routes.gitShow]: async (url) => show(await repoOf(url.searchParams.get("repo")), url.searchParams.get("commit")),
2256
- [Routes.gitPull]: async (url, req) => ({ output: await pull(await repoOf((await readJson(req)).repo)) }),
2257
- [Routes.gitPush]: async (url, req) => ({ output: await push(await repoOf((await readJson(req)).repo)) }),
3672
+ [Routes.gitPull]: async (url, req) => ({ output: await pull(await repoOf((await readJson2(req)).repo)) }),
3673
+ [Routes.gitPush]: async (url, req) => ({ output: await push(await repoOf((await readJson2(req)).repo)) }),
2258
3674
  [Routes.termList]: async () => ({ sessions: sessions.list() }),
2259
3675
  [Routes.portsList]: async () => ({ ports: await listPorts(shellPids()) }),
2260
3676
  [Routes.portsKill]: async (url, req) => {
2261
- const { pid, force } = await readJson(req);
2262
- await killListener(pid, { force: force === true, shells: shellPids() });
3677
+ const { pid, force } = await readJson2(req);
3678
+ const ports = await killListener(pid, { force: force === true, shells: shellPids() });
3679
+ if (preview2) setTimeout(() => sweepPreviews(ports, true).catch(() => {
3680
+ }), killedMs).unref();
2263
3681
  return {};
2264
3682
  },
2265
3683
  [Routes.previewList]: async () => {
2266
- if (!preview) throw new HttpError(404, "previews need a tunnel");
3684
+ if (!preview2) throw new HttpError(404, "previews need a tunnel");
2267
3685
  return previews();
2268
3686
  },
2269
3687
  [Routes.previewOpen]: async (url, req) => {
2270
- const port2 = previewPort((await readJson(req)).port, server.address()?.port);
3688
+ const port2 = previewPort((await readJson2(req)).port, server.address()?.port);
2271
3689
  let proxy = proxies.get(port2);
2272
3690
  if (!proxy) {
2273
3691
  proxy = new InspectingProxy(port2);
@@ -2275,7 +3693,7 @@ function createAgentServer({
2275
3693
  proxies.set(port2, proxy);
2276
3694
  }
2277
3695
  try {
2278
- await preview.forward(proxy.port);
3696
+ await preview2.forward(proxy.port);
2279
3697
  } catch (e) {
2280
3698
  throw new HttpError(502, e.message);
2281
3699
  }
@@ -2283,25 +3701,34 @@ function createAgentServer({
2283
3701
  },
2284
3702
  [Routes.previewClose]: async (url) => {
2285
3703
  const port2 = previewPort(url.searchParams.get("port"), server.address()?.port);
3704
+ if (installs && port2 === installs.port) {
3705
+ await installs.stop();
3706
+ return previews();
3707
+ }
2286
3708
  const proxy = proxies.get(port2);
2287
- if (proxy) await preview.unforward(proxy.port);
3709
+ if (proxy) await preview2.unforward(proxy.port);
2288
3710
  return previews();
2289
3711
  },
2290
3712
  [Routes.previewShare]: async (url, req) => {
2291
- const { port: value, hours } = await readJson(req);
3713
+ const { port: value, hours } = await readJson2(req);
2292
3714
  const proxy = previewed(previewPort(value, server.address()?.port));
2293
3715
  if (!Number.isInteger(hours) || hours < 1 || hours > 720) throw new HttpError(400, "hours must be 1-720");
2294
3716
  try {
2295
- await preview.share(proxy.port, hours);
3717
+ await preview2.share(proxy.port, hours);
2296
3718
  } catch (e) {
2297
3719
  throw new HttpError(502, e.message);
2298
3720
  }
2299
3721
  return previews();
2300
3722
  },
2301
3723
  [Routes.previewUnshare]: async (url) => {
3724
+ const asked = previewPort(url.searchParams.get("port"), server.address()?.port);
3725
+ if (installs && asked === installs.port) {
3726
+ await installs.stop();
3727
+ return previews();
3728
+ }
2302
3729
  const proxy = proxies.get(previewPort(url.searchParams.get("port"), server.address()?.port));
2303
3730
  try {
2304
- if (proxy) await preview.unshare(proxy.port);
3731
+ if (proxy) await preview2.unshare(proxy.port);
2305
3732
  } catch (e) {
2306
3733
  throw new HttpError(502, e.message);
2307
3734
  }
@@ -2311,9 +3738,9 @@ function createAgentServer({
2311
3738
  requests: inspected(url.searchParams.get("port")).list(parseInt(url.searchParams.get("after"), 10) || 0)
2312
3739
  }),
2313
3740
  [Routes.previewRequest]: async (url) => {
2314
- const entry2 = inspected(url.searchParams.get("port")).get(parseInt(url.searchParams.get("id"), 10));
2315
- if (!entry2) throw new HttpError(404, "no such request (only the latest 200 are kept)");
2316
- return detail(entry2);
3741
+ const entry3 = inspected(url.searchParams.get("port")).get(parseInt(url.searchParams.get("id"), 10));
3742
+ if (!entry3) throw new HttpError(404, "no such request (only the latest 200 are kept)");
3743
+ return detail(entry3);
2317
3744
  },
2318
3745
  [Routes.previewRequestsClear]: async (url) => {
2319
3746
  inspected(url.searchParams.get("port")).clear();
@@ -2324,6 +3751,11 @@ function createAgentServer({
2324
3751
  if (!status2) throw new HttpError(404, "this agent runs without a tunnel");
2325
3752
  return status2;
2326
3753
  },
3754
+ [Routes.tunnelDelete]: async (url, req) => {
3755
+ if (!tunnelDelete2) throw new HttpError(404, "this agent runs without a tunnel");
3756
+ const { id, account, force } = await readJson2(req);
3757
+ return tunnelDelete2({ id, account, force: force === true });
3758
+ },
2327
3759
  [Routes.tunnelToken]: async () => {
2328
3760
  const issued = await handoff.issue();
2329
3761
  if (!issued) throw new HttpError(404, "this tunnel is anonymous");
@@ -2336,8 +3768,43 @@ function createAgentServer({
2336
3768
  const [plugin, codexPlugin] = await Promise.all([claude && hasPlugin("claude"), codex && hasPlugin("codex")]);
2337
3769
  return { claude, plugin: Boolean(plugin), codex, codexPlugin: Boolean(codexPlugin), codexModels: codex ? await codexModels() : [] };
2338
3770
  },
3771
+ [Routes.agentsCommands]: async (url) => {
3772
+ const session = agents.session(url.searchParams.get("session") ?? "");
3773
+ if (!session) throw new HttpError(404, "no such session");
3774
+ return { commands: await sessionCommands(session) };
3775
+ },
3776
+ [Routes.agentsHistory]: async (url) => {
3777
+ const session = agents.session(url.searchParams.get("session") ?? "");
3778
+ if (!session) throw new HttpError(404, "no such session");
3779
+ const found = await listHistory({ agent: session.agent, cwd: session.cwd || sandbox.root, home, codexHome });
3780
+ return { sessions: found.filter((h) => h.id !== session.id) };
3781
+ },
3782
+ [Routes.buildsSchemes]: async (url) => listSchemes(await sandbox.resolve(url.searchParams.get("path")), { xcodebuild }),
3783
+ [Routes.buildsStart]: async (url, req) => {
3784
+ const { path: folder, scheme, configuration } = await readJson2(req);
3785
+ return builds.start({ folder: await sandbox.resolve(folder), scheme, configuration });
3786
+ },
3787
+ [Routes.buildsCurrent]: async (url) => {
3788
+ const build = builds.current(parseInt(url.searchParams.get("after"), 10) || 0);
3789
+ if (!build) throw new HttpError(404, "no build yet");
3790
+ return build;
3791
+ },
3792
+ [Routes.buildsCancel]: async () => {
3793
+ builds.cancel();
3794
+ return {};
3795
+ },
3796
+ [Routes.buildsInstall]: async () => {
3797
+ if (!installs) throw new HttpError(409, "installing needs the tunnel: start the agent with `coderemote up`");
3798
+ const artifact = builds.artifact();
3799
+ if (!artifact) throw new HttpError(409, "there is no successful build to install");
3800
+ try {
3801
+ return await installs.create(artifact);
3802
+ } catch (e) {
3803
+ throw e instanceof HttpError ? e : new HttpError(502, e.message);
3804
+ }
3805
+ },
2339
3806
  [Routes.agentsLaunch]: async (url, req) => {
2340
- const { agent = "claude", cwd, model, permissionMode, approvalPolicy, resume, cols, rows } = await readJson(req);
3807
+ const { agent = "claude", cwd, model, permissionMode, approvalPolicy, resume, cols, rows } = await readJson2(req);
2341
3808
  if (approvalPolicy !== void 0 && !["on-request", "never"].includes(approvalPolicy)) {
2342
3809
  throw new HttpError(400, "approvalPolicy must be on-request or never");
2343
3810
  }
@@ -2349,7 +3816,7 @@ function createAgentServer({
2349
3816
  } catch (e) {
2350
3817
  throw new HttpError(400, e.message);
2351
3818
  }
2352
- if (!(await fs8.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
3819
+ if (!(await fs13.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
2353
3820
  if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
2354
3821
  throw new HttpError(400, "invalid model");
2355
3822
  }
@@ -2370,8 +3837,8 @@ function createAgentServer({
2370
3837
  if (!await hasPlugin(agent)) {
2371
3838
  throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
2372
3839
  }
2373
- const id = resume ?? randomUUID4();
2374
- if (uploadsDir) await fs8.mkdir(uploadsDir, { recursive: true, mode: 448 });
3840
+ const id = resume ?? randomUUID5();
3841
+ if (uploadsDir) await fs13.mkdir(uploadsDir, { recursive: true, mode: 448 });
2375
3842
  const argv2 = [
2376
3843
  launcher.command,
2377
3844
  ...launcher.args({
@@ -2392,14 +3859,14 @@ function createAgentServer({
2392
3859
  return { terminal: term.id, session: agents.launched({ id, terminal: term.id, cwd: dir, agent }) };
2393
3860
  },
2394
3861
  [Routes.agentsPrompt]: async (url, req) => {
2395
- const { session: id, text, attachments } = await readJson(req);
3862
+ const { session: id, text, attachments } = await readJson2(req);
2396
3863
  if (typeof text !== "string" || !text.trim() || text.length > 1e5) {
2397
3864
  throw new HttpError(400, "text must be a non-empty string");
2398
3865
  }
2399
3866
  const { term } = typingTarget(id, { whileWaiting: false });
2400
3867
  const paths = await attachmentPaths(attachments);
2401
3868
  const typed = typeable([text.trim(), ...paths].join("\n"));
2402
- const prompt = randomUUID4();
3869
+ const prompt = randomUUID5();
2403
3870
  agents.prompted(id, { id: prompt, text: typed, paths });
2404
3871
  await typeInto(term, typed);
2405
3872
  return { prompt };
@@ -2419,23 +3886,30 @@ function createAgentServer({
2419
3886
  return { id, name: saved, type, size };
2420
3887
  },
2421
3888
  [Routes.agentsKeys]: async (url, req) => {
2422
- const { session: id, action, command } = await readJson(req);
3889
+ const { session: id, action, command, args } = await readJson2(req);
2423
3890
  const allowed = agents.session(typeof id === "string" ? id : "")?.capabilities?.keys;
2424
- if (allowed && !allowed.includes(action === "command" ? command : action)) {
3891
+ if (action !== "command" && allowed && !allowed.includes(action)) {
2425
3892
  throw new HttpError(400, `this session takes ${allowed.join(", ")}`);
2426
3893
  }
2427
3894
  if (action === "command") {
2428
- if (!COMMANDS.includes(command)) throw new HttpError(400, `command must be one of ${COMMANDS.join(", ")}`);
2429
- await typeInto(typingTarget(id, { whileWaiting: false }).term, command);
3895
+ if (typeof command !== "string") throw new HttpError(400, "command must be a string");
3896
+ if (args !== void 0 && (typeof args !== "string" || args.length > 1e5)) throw new HttpError(400, "args must be a string");
3897
+ const { session, term } = typingTarget(id, { whileWaiting: false });
3898
+ const listed = (await sessionCommands(session)).some((c) => c.kind === "command" && c.name === command);
3899
+ if (!listed && !session.capabilities?.keys?.includes(command)) {
3900
+ throw new HttpError(400, `${command} is not a command this session takes`);
3901
+ }
3902
+ await typeInto(term, typeable(args?.trim() ? `${command} ${args.trim()}` : command));
2430
3903
  } else if (Object.hasOwn(KEYS, action)) {
2431
3904
  typingTarget(id, { whileWaiting: true }).term.write(KEYS[action]);
3905
+ if (action === "interrupt") agents.interrupted(id);
2432
3906
  } else {
2433
3907
  throw new HttpError(400, `action must be one of ${[...Object.keys(KEYS), "command"].join(", ")}`);
2434
3908
  }
2435
3909
  return {};
2436
3910
  },
2437
3911
  [Routes.agentsStop]: async (url, req) => {
2438
- const { session: id } = await readJson(req);
3912
+ const { session: id } = await readJson2(req);
2439
3913
  const { term } = typingTarget(id, { whileWaiting: true });
2440
3914
  term.kill();
2441
3915
  setTimeout(() => {
@@ -2444,7 +3918,7 @@ function createAgentServer({
2444
3918
  return {};
2445
3919
  },
2446
3920
  [Routes.agentsForget]: async (url, req) => {
2447
- const { session: id } = await readJson(req);
3921
+ const { session: id } = await readJson2(req);
2448
3922
  const session = typeof id === "string" ? agents.session(id) : null;
2449
3923
  if (!session) throw new HttpError(404, "no such session");
2450
3924
  if (session.state !== "ended" && session.terminal && sessions.get(session.terminal)) {
@@ -2454,11 +3928,11 @@ function createAgentServer({
2454
3928
  return {};
2455
3929
  },
2456
3930
  [Routes.agentsAnswer]: async (url, req) => {
2457
- agents.answer(await readJson(req));
3931
+ agents.answer(await readJson2(req));
2458
3932
  return {};
2459
3933
  },
2460
3934
  [Routes.pushDeviceSet]: async (url, req) => {
2461
- push2.register(await readJson(req));
3935
+ push2.register(await readJson2(req));
2462
3936
  return {};
2463
3937
  },
2464
3938
  [Routes.pushDeviceDelete]: async (url) => {
@@ -2466,7 +3940,7 @@ function createAgentServer({
2466
3940
  return {};
2467
3941
  },
2468
3942
  [Routes.pushTest]: async (url, req) => {
2469
- await push2.test((await readJson(req)).token);
3943
+ await push2.test((await readJson2(req)).token);
2470
3944
  return {};
2471
3945
  },
2472
3946
  [Routes.termKill]: async (url) => {
@@ -2476,7 +3950,7 @@ function createAgentServer({
2476
3950
  return {};
2477
3951
  }
2478
3952
  };
2479
- const server = http2.createServer(async (req, res) => {
3953
+ const server = http3.createServer(async (req, res) => {
2480
3954
  const url = new URL(req.url, "http://localhost");
2481
3955
  if (req.method === "GET" && url.pathname === "/") {
2482
3956
  res.writeHead(200, {
@@ -2504,14 +3978,16 @@ function createAgentServer({
2504
3978
  if (!hookAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
2505
3979
  plugins[url.searchParams.get("agent") || "claude"] = { installed: true, checkedAt: Date.now() };
2506
3980
  const terminal = url.searchParams.get("terminal");
3981
+ const app = url.searchParams.get("app");
2507
3982
  const closed = new AbortController();
2508
3983
  res.on("close", () => {
2509
3984
  if (!res.writableFinished) closed.abort();
2510
3985
  });
2511
3986
  try {
2512
- const output = await agents.handle(await readJson(req), {
3987
+ const output = await agents.handle(await readJson2(req), {
2513
3988
  agent: url.searchParams.get("agent") || "claude",
2514
3989
  terminal: terminal && sessions.get(terminal) ? terminal : null,
3990
+ app: app && app.length <= 1024 && /\.app$/.test(app) ? app : null,
2515
3991
  signal: closed.signal
2516
3992
  });
2517
3993
  if (res.destroyed) return;
@@ -2590,30 +4066,43 @@ function createAgentServer({
2590
4066
  } catch {
2591
4067
  return;
2592
4068
  }
2593
- if (msg.type === AgentsClientMessage.presence) foregroundAt = msg.foreground === true ? Date.now() : 0;
4069
+ if (msg.type !== AgentsClientMessage.presence) return;
4070
+ if (msg.foreground !== true) return presences.delete(ws);
4071
+ presences.set(ws, {
4072
+ at: Date.now(),
4073
+ all: msg.session === void 0 && msg.builds === void 0,
4074
+ session: typeof msg.session === "string" ? msg.session : null,
4075
+ builds: msg.builds === true
4076
+ });
4077
+ });
4078
+ ws.on("close", () => {
4079
+ agents.off("message", send);
4080
+ presences.delete(ws);
2594
4081
  });
2595
- ws.on("close", () => agents.off("message", send));
2596
4082
  }
2597
4083
  server.on("close", () => {
4084
+ installs?.close();
2598
4085
  clearInterval(sweep);
4086
+ clearInterval(previewTimer);
2599
4087
  sessions.killAll();
2600
4088
  for (const proxy of proxies.values()) proxy.close();
2601
4089
  });
2602
4090
  server.killAllSessions = () => sessions.killAll();
2603
4091
  server.agents = agents;
2604
4092
  server.clientInForeground = clientInForeground;
4093
+ server.clientShows = clientShows;
2605
4094
  server.push = push2;
2606
4095
  server.agentsUploadPath = (id) => uploadPath(id);
2607
4096
  return server;
2608
4097
  }
2609
4098
 
2610
4099
  // src/devtunnel.js
2611
- import { execFile as execFile7 } from "node:child_process";
4100
+ import { execFile as execFile9 } from "node:child_process";
2612
4101
  import { createHash } from "node:crypto";
2613
- import fs9 from "node:fs";
2614
- import path7 from "node:path";
4102
+ import fs14 from "node:fs";
4103
+ import path11 from "node:path";
2615
4104
  import { promisify as promisify3 } from "node:util";
2616
- var run4 = promisify3(execFile7);
4105
+ var run5 = promisify3(execFile9);
2617
4106
  var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
2618
4107
  var BUILDS = {
2619
4108
  "darwin-arm64": "osx-arm64-devtunnel",
@@ -2642,11 +4131,11 @@ async function ensureDevtunnel({ dir, progress }) {
2642
4131
  `No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
2643
4132
  );
2644
4133
  }
2645
- const bin = path7.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
2646
- const metaFile = path7.join(dir, "devtunnel.json");
4134
+ const bin = path11.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
4135
+ const metaFile = path11.join(dir, "devtunnel.json");
2647
4136
  let meta = {};
2648
4137
  try {
2649
- meta = JSON.parse(fs9.readFileSync(metaFile, "utf8"));
4138
+ meta = JSON.parse(fs14.readFileSync(metaFile, "utf8"));
2650
4139
  } catch {
2651
4140
  }
2652
4141
  const installed = findExecutable(bin) !== null;
@@ -2657,7 +4146,7 @@ async function ensureDevtunnel({ dir, progress }) {
2657
4146
  const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
2658
4147
  const lastModified = head.headers.get("last-modified");
2659
4148
  if (head.ok && lastModified && lastModified === meta.lastModified) {
2660
- fs9.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
4149
+ fs14.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
2661
4150
  return bin;
2662
4151
  }
2663
4152
  } catch {
@@ -2672,12 +4161,12 @@ async function ensureDevtunnel({ dir, progress }) {
2672
4161
  return bin;
2673
4162
  }
2674
4163
  async function download(url, bin, metaFile, progress) {
2675
- fs9.mkdirSync(path7.dirname(bin), { recursive: true, mode: 448 });
4164
+ fs14.mkdirSync(path11.dirname(bin), { recursive: true, mode: 448 });
2676
4165
  const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
2677
4166
  if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
2678
4167
  const total = Number(res.headers.get("content-length")) || 0;
2679
4168
  const tmp = `${bin}.${process.pid}.download`;
2680
- const out = fs9.createWriteStream(tmp, { mode: 493 });
4169
+ const out = fs14.createWriteStream(tmp, { mode: 493 });
2681
4170
  const md5 = createHash("md5");
2682
4171
  let received = 0;
2683
4172
  try {
@@ -2690,22 +4179,22 @@ async function download(url, bin, metaFile, progress) {
2690
4179
  await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
2691
4180
  if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
2692
4181
  await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
2693
- fs9.chmodSync(tmp, 493);
2694
- fs9.renameSync(tmp, bin);
4182
+ fs14.chmodSync(tmp, 493);
4183
+ fs14.renameSync(tmp, bin);
2695
4184
  } catch (e) {
2696
4185
  out.destroy();
2697
- fs9.rmSync(tmp, { force: true });
4186
+ fs14.rmSync(tmp, { force: true });
2698
4187
  throw e;
2699
4188
  }
2700
- fs9.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
4189
+ fs14.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
2701
4190
  }
2702
4191
  async function verify(file, md5, expectedMd5) {
2703
4192
  if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
2704
4193
  if (isWindows) return verifyAuthenticode(file);
2705
4194
  if (process.platform !== "darwin") return;
2706
4195
  try {
2707
- await run4("codesign", ["--verify", "--strict", file]);
2708
- const { stderr } = await run4("codesign", ["-dv", file]);
4196
+ await run5("codesign", ["--verify", "--strict", file]);
4197
+ const { stderr } = await run5("codesign", ["-dv", file]);
2709
4198
  if (!stderr.includes(`TeamIdentifier=${MICROSOFT_TEAM_ID}`)) throw new Error("not signed by Microsoft");
2710
4199
  } catch (e) {
2711
4200
  throw new Error(`devtunnel download failed its signature check: ${e.message}`);
@@ -2716,7 +4205,7 @@ async function verifyAuthenticode(file) {
2716
4205
  let status2;
2717
4206
  let signer;
2718
4207
  try {
2719
- const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
4208
+ const { stdout } = await run5("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
2720
4209
  env: { ...process.env, CODEREMOTE_VERIFY: file }
2721
4210
  });
2722
4211
  [status2, signer] = stdout.trim().split("|");
@@ -2729,51 +4218,52 @@ async function verifyAuthenticode(file) {
2729
4218
  }
2730
4219
 
2731
4220
  // src/hook.js
2732
- import { randomBytes as randomBytes2 } from "node:crypto";
2733
- import fs10 from "node:fs";
2734
- import http3 from "node:http";
2735
- import path8 from "node:path";
4221
+ import { execFile as execFile10 } from "node:child_process";
4222
+ import { randomBytes as randomBytes3 } from "node:crypto";
4223
+ import fs15 from "node:fs";
4224
+ import http4 from "node:http";
4225
+ import path12 from "node:path";
2736
4226
  var CONNECT_TIMEOUT = 1e3;
2737
4227
  var EVENT_TIMEOUT = 5e3;
2738
4228
  function ensureHookSecret(dir) {
2739
- const file = path8.join(dir, "hook-secret");
4229
+ const file = path12.join(dir, "hook-secret");
2740
4230
  try {
2741
- const secret2 = fs10.readFileSync(file, "utf8").trim();
4231
+ const secret2 = fs15.readFileSync(file, "utf8").trim();
2742
4232
  if (secret2) return secret2;
2743
4233
  } catch {
2744
4234
  }
2745
- fs10.mkdirSync(dir, { recursive: true, mode: 448 });
2746
- const secret = randomBytes2(32).toString("base64url");
2747
- fs10.writeFileSync(file, secret, { mode: 384 });
4235
+ fs15.mkdirSync(dir, { recursive: true, mode: 448 });
4236
+ const secret = randomBytes3(32).toString("base64url");
4237
+ fs15.writeFileSync(file, secret, { mode: 384 });
2748
4238
  return secret;
2749
4239
  }
2750
4240
  function writeHookLauncher(dir, { node = process.execPath, script }) {
2751
- const bin = path8.join(dir, "bin");
2752
- fs10.mkdirSync(bin, { recursive: true, mode: 448 });
4241
+ const bin = path12.join(dir, "bin");
4242
+ fs15.mkdirSync(bin, { recursive: true, mode: 448 });
2753
4243
  const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
2754
- const file = path8.join(bin, "hook");
2755
- fs10.writeFileSync(file, `#!/bin/sh
4244
+ const file = path12.join(bin, "hook");
4245
+ fs15.writeFileSync(file, `#!/bin/sh
2756
4246
  exec ${quote(node)} ${quote(script)} hook "$@"
2757
4247
  `, { mode: 493 });
2758
- fs10.chmodSync(file, 493);
2759
- fs10.writeFileSync(path8.join(bin, "hook.json"), JSON.stringify({ node, script }), { mode: 384 });
4248
+ fs15.chmodSync(file, 493);
4249
+ fs15.writeFileSync(path12.join(bin, "hook.json"), JSON.stringify({ node, script }), { mode: 384 });
2760
4250
  return file;
2761
4251
  }
2762
4252
  function readHookSecret(dir) {
2763
4253
  try {
2764
- return fs10.readFileSync(path8.join(dir, "hook-secret"), "utf8").trim() || null;
4254
+ return fs15.readFileSync(path12.join(dir, "hook-secret"), "utf8").trim() || null;
2765
4255
  } catch {
2766
4256
  return null;
2767
4257
  }
2768
4258
  }
2769
4259
  function real(p) {
2770
4260
  try {
2771
- return fs10.realpathSync.native(p);
4261
+ return fs15.realpathSync.native(p);
2772
4262
  } catch {
2773
- return path8.resolve(p);
4263
+ return path12.resolve(p);
2774
4264
  }
2775
4265
  }
2776
- var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(path8.sep) ? root2 : root2 + path8.sep);
4266
+ var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(path12.sep) ? root2 : root2 + path12.sep);
2777
4267
  function hookTarget(agents, cwd, env = process.env) {
2778
4268
  const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
2779
4269
  if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
@@ -2782,23 +4272,46 @@ function hookTarget(agents, cwd, env = process.env) {
2782
4272
  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];
2783
4273
  return match ? { port: match.port, terminal: null } : null;
2784
4274
  }
2785
- function deliverHook({ port: port2, terminal, agent, secret, body, wait }) {
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 }) {
2786
4298
  return new Promise((resolve) => {
2787
4299
  const query = new URLSearchParams({ agent });
2788
4300
  if (terminal) query.set("terminal", terminal);
2789
- const req = http3.request({
4301
+ if (app) query.set("app", app);
4302
+ const req = http4.request({
2790
4303
  host: "127.0.0.1",
2791
4304
  port: port2,
2792
4305
  method: "POST",
2793
4306
  path: `/api/agents/hook?${query}`,
2794
4307
  headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body), [HOOK_SECRET_HEADER]: secret }
2795
4308
  });
2796
- const fail = () => {
4309
+ const fail2 = () => {
2797
4310
  req.destroy();
2798
4311
  resolve("");
2799
4312
  };
2800
- const connect = setTimeout(fail, CONNECT_TIMEOUT);
2801
- const overall = wait ? null : setTimeout(fail, EVENT_TIMEOUT);
4313
+ const connect = setTimeout(fail2, CONNECT_TIMEOUT);
4314
+ const overall = wait ? null : setTimeout(fail2, EVENT_TIMEOUT);
2802
4315
  req.on("socket", (socket) => socket.once("connect", () => clearTimeout(connect)));
2803
4316
  req.on("error", () => resolve(""));
2804
4317
  req.on("response", (res) => {
@@ -2813,7 +4326,7 @@ function deliverHook({ port: port2, terminal, agent, secret, body, wait }) {
2813
4326
  req.end(body);
2814
4327
  });
2815
4328
  }
2816
- async function runHook({ input, agents, secret, agent = "claude", env = process.env }) {
4329
+ async function runHook({ input, agents, secret, agent = "claude", env = process.env, findApp = hostApp }) {
2817
4330
  if (!secret) return "";
2818
4331
  let event;
2819
4332
  try {
@@ -2824,16 +4337,23 @@ async function runHook({ input, agents, secret, agent = "claude", env = process.
2824
4337
  const target = hookTarget(agents, event?.cwd, env);
2825
4338
  if (!target) return "";
2826
4339
  const wait = event.hook_event_name === "PermissionRequest";
2827
- const out = await deliverHook({ ...target, agent, secret, body: input, wait });
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 });
2828
4342
  return wait ? out.trim() : "";
2829
4343
  }
2830
4344
 
2831
4345
  // src/preview.js
2832
- import { spawn as spawn2 } from "node:child_process";
4346
+ import { spawn as spawn4 } from "node:child_process";
2833
4347
 
2834
4348
  // src/tunnels.js
2835
4349
  var TUNNEL_LIMIT = 10;
2836
4350
  var PREVIEW_LABEL = "coderemote-preview";
4351
+ var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
4352
+ function signedInUser(res) {
4353
+ if (!res.ok || LOGIN_NEEDED.test(res.out)) return null;
4354
+ const m = res.out.match(/Logged in as (\S+) using (\S+?)\.?\s*$/m);
4355
+ return m ? `${m[1]} (${m[2]})` : res.out.trim() || null;
4356
+ }
2837
4357
  var base = (id) => String(id).split(".")[0];
2838
4358
  async function listTunnels(devtunnel2) {
2839
4359
  const res = await devtunnel2("list", "--limit", "100", "--json");
@@ -2965,8 +4485,8 @@ var PreviewTunnel = class {
2965
4485
  "--json"
2966
4486
  );
2967
4487
  if (!res.ok) throw new Error(`devtunnel access create failed: ${res.out.trim()}`);
2968
- const entries = JSON.parse(res.out.slice(res.out.indexOf("{"))).accessControlEntries ?? [];
2969
- const until = Date.parse(entries.find((e) => e.type === "Anonymous")?.expiration);
4488
+ const entries2 = JSON.parse(res.out.slice(res.out.indexOf("{"))).accessControlEntries ?? [];
4489
+ const until = Date.parse(entries2.find((e) => e.type === "Anonymous")?.expiration);
2970
4490
  this.#shares.set(port2, Number.isFinite(until) ? until : Date.now() + hours * 36e5);
2971
4491
  });
2972
4492
  }
@@ -3076,7 +4596,7 @@ ${created.out}`);
3076
4596
  }
3077
4597
  #startHost() {
3078
4598
  return new Promise((resolve, reject) => {
3079
- const child = spawn2(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
4599
+ const child = spawn4(this.command(), ["host", this.#tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
3080
4600
  const urls = /* @__PURE__ */ new Map();
3081
4601
  let log2 = "";
3082
4602
  const timer = setTimeout(() => {
@@ -3111,40 +4631,40 @@ ${log2}`));
3111
4631
  };
3112
4632
 
3113
4633
  // src/push-store.js
3114
- import fs11 from "node:fs";
3115
- import path9 from "node:path";
4634
+ import fs16 from "node:fs";
4635
+ import path13 from "node:path";
3116
4636
  function pushStore(dir, port2) {
3117
- const file = path9.join(dir, `${port2}.json`);
4637
+ const file = path13.join(dir, `${port2}.json`);
3118
4638
  return {
3119
4639
  load() {
3120
4640
  try {
3121
- const devices = JSON.parse(fs11.readFileSync(file, "utf8"));
4641
+ const devices = JSON.parse(fs16.readFileSync(file, "utf8"));
3122
4642
  return Array.isArray(devices) ? devices : [];
3123
4643
  } catch {
3124
4644
  return [];
3125
4645
  }
3126
4646
  },
3127
4647
  save(devices) {
3128
- fs11.mkdirSync(dir, { recursive: true, mode: 448 });
3129
- fs11.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
4648
+ fs16.mkdirSync(dir, { recursive: true, mode: 448 });
4649
+ fs16.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
3130
4650
  }
3131
4651
  };
3132
4652
  }
3133
4653
 
3134
4654
  // src/registry.js
3135
- import fs12 from "node:fs";
3136
- import path10 from "node:path";
4655
+ import fs17 from "node:fs";
4656
+ import path14 from "node:path";
3137
4657
  var AgentRegistry = class {
3138
4658
  constructor(dir) {
3139
4659
  this.dir = dir;
3140
4660
  }
3141
4661
  register({ port: port2, root: root2, url }) {
3142
- fs12.mkdirSync(this.dir, { recursive: true, mode: 448 });
4662
+ fs17.mkdirSync(this.dir, { recursive: true, mode: 448 });
3143
4663
  const file = this.#file(port2);
3144
- fs12.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
4664
+ fs17.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
3145
4665
  const cleanup = () => {
3146
4666
  try {
3147
- if (JSON.parse(fs12.readFileSync(file, "utf8")).pid === process.pid) fs12.rmSync(file);
4667
+ if (JSON.parse(fs17.readFileSync(file, "utf8")).pid === process.pid) fs17.rmSync(file);
3148
4668
  } catch {
3149
4669
  }
3150
4670
  };
@@ -3155,30 +4675,31 @@ var AgentRegistry = class {
3155
4675
  list() {
3156
4676
  let names = [];
3157
4677
  try {
3158
- names = fs12.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
4678
+ names = fs17.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
3159
4679
  } catch {
3160
4680
  return [];
3161
4681
  }
3162
4682
  const agents = [];
3163
4683
  for (const name of names) {
3164
- const file = path10.join(this.dir, name);
4684
+ const file = path14.join(this.dir, name);
3165
4685
  try {
3166
- const agent = JSON.parse(fs12.readFileSync(file, "utf8"));
4686
+ const agent = JSON.parse(fs17.readFileSync(file, "utf8"));
3167
4687
  process.kill(agent.pid, 0);
3168
4688
  agents.push(agent);
3169
4689
  } catch {
3170
- fs12.rmSync(file, { force: true });
4690
+ fs17.rmSync(file, { force: true });
3171
4691
  }
3172
4692
  }
3173
4693
  return agents.sort((a, b) => a.port - b.port);
3174
4694
  }
3175
4695
  #file(port2) {
3176
- return path10.join(this.dir, `${port2}.json`);
4696
+ return path14.join(this.dir, `${port2}.json`);
3177
4697
  }
3178
4698
  };
3179
4699
 
3180
4700
  // src/tunnel-status.js
3181
4701
  var MACHINE_LABEL = "coderemote";
4702
+ var PROVIDER = "devtunnels";
3182
4703
  var CACHE_MS = 15e3;
3183
4704
  function parse(out) {
3184
4705
  return JSON.parse(out.slice(out.indexOf("{")));
@@ -3186,6 +4707,13 @@ function parse(out) {
3186
4707
  function machineName(description) {
3187
4708
  return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local\b/, "") || null;
3188
4709
  }
4710
+ var VSCODE_LABEL = "vscode-server-launcher";
4711
+ function otherTunnel(tunnel) {
4712
+ const labels = tunnel.labels ?? [];
4713
+ const kind = labels.includes(PREVIEW_LABEL) ? "preview" : labels.includes(VSCODE_LABEL) ? "vscode" : "other";
4714
+ const name = kind === "preview" ? machineName((tunnel.description || "").replace(/^coderemote preview\s+/, "")) : tunnel.description || null;
4715
+ return { id: tunnel.tunnelId, kind, name, online: tunnel.hostConnections > 0 };
4716
+ }
3189
4717
  function machine(tunnel, current) {
3190
4718
  return {
3191
4719
  id: tunnel.tunnelId,
@@ -3197,19 +4725,21 @@ function machine(tunnel, current) {
3197
4725
  current
3198
4726
  };
3199
4727
  }
3200
- function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview }) {
4728
+ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview: preview2 }) {
3201
4729
  let cached = null;
3202
- return async () => {
4730
+ const status2 = async () => {
3203
4731
  const id = tunnelId();
3204
4732
  if (!id) return null;
3205
- if (cached && Date.now() - cached.at < CACHE_MS) return { ...cached.value, previewPorts: preview?.list().length ?? 0 };
4733
+ if (cached && Date.now() - cached.at < CACHE_MS) return { ...cached.value, previewPorts: preview2?.list().length ?? 0 };
3206
4734
  const [own, listed] = await Promise.all([
3207
4735
  devtunnel2("show", id, "--json"),
3208
- devtunnel2("list", "--labels", MACHINE_LABEL, "--json")
4736
+ // Every tunnel, previews' too: they all count against the account's limit.
4737
+ devtunnel2("list", "--limit", "100", "--json")
3209
4738
  ]);
3210
4739
  if (!own.ok) throw new Error(`devtunnel show failed: ${own.out.trim()}`);
3211
4740
  const tunnel = parse(own.out).tunnel;
3212
- const others = listed.ok ? (parse(listed.out).tunnels ?? []).filter((t) => t.tunnelId !== id) : [];
4741
+ const all = listed.ok ? parse(listed.out).tunnels ?? [] : null;
4742
+ const others = (all ?? []).filter((t) => t.tunnelId !== id && t.labels?.includes(MACHINE_LABEL));
3213
4743
  const shown = await Promise.all(
3214
4744
  others.map(async (t) => {
3215
4745
  const res = await devtunnel2("show", t.tunnelId, "--json");
@@ -3218,6 +4748,7 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
3218
4748
  );
3219
4749
  const machines = [machine(tunnel, true), ...shown.filter(Boolean).map((t) => machine(t, false))];
3220
4750
  const value = {
4751
+ provider: PROVIDER,
3221
4752
  account: account() ?? null,
3222
4753
  tunnel: {
3223
4754
  id,
@@ -3229,16 +4760,54 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
3229
4760
  uploaded: tunnel.uploadTotal ?? null,
3230
4761
  downloaded: tunnel.downloadTotal ?? null
3231
4762
  },
3232
- machines
4763
+ machines,
4764
+ // The account's tunnels that aren't machines; empty when it couldn't be listed.
4765
+ others: (all ?? []).filter((t) => t.tunnelId !== id && !t.labels?.includes(MACHINE_LABEL)).map(otherTunnel),
4766
+ // null when the account couldn't be listed
4767
+ quota: all && { used: all.length, limit: TUNNEL_LIMIT }
3233
4768
  };
3234
4769
  cached = { at: Date.now(), value };
3235
- return { ...value, previewPorts: preview?.list().length ?? 0 };
4770
+ return { ...value, previewPorts: preview2?.list().length ?? 0 };
4771
+ };
4772
+ status2.invalidate = () => {
4773
+ cached = null;
4774
+ };
4775
+ return status2;
4776
+ }
4777
+ var fail = (status2, code, message) => Object.assign(new Error(message), { status: status2, code });
4778
+ function tunnelDelete({ devtunnel: devtunnel2, tunnelId, deleted = () => {
4779
+ } }) {
4780
+ return async ({ id, account, force = false }) => {
4781
+ if (typeof id !== "string" || !id) throw fail(400, "EINVAL", "id is required");
4782
+ if (typeof account !== "string" || !account) throw fail(400, "EINVAL", "account is required");
4783
+ const user = signedInUser(await devtunnel2("user", "show"));
4784
+ if (!user) {
4785
+ throw fail(403, "ELOGIN", "The computer\u2019s dev tunnels sign-in has expired. Run `coderemote login` on it, then try again.");
4786
+ }
4787
+ if (user !== account) {
4788
+ throw fail(409, "EACCOUNT", `The computer is now signed in as ${user}, not ${account}. Refresh, then try again.`);
4789
+ }
4790
+ const { tunnels, error } = await listTunnels(devtunnel2);
4791
+ if (error) throw fail(502, "EDEVTUNNEL", error);
4792
+ const tunnel = findTunnel(tunnels, id);
4793
+ if (!tunnel) throw fail(404, "ENOTUNNEL", `No tunnel ${id} on ${account}.`);
4794
+ const own = tunnelId();
4795
+ if (own && findTunnel([tunnel], own)) {
4796
+ throw fail(409, "EOWN", "This is the tunnel this connection runs through. Delete it on the computer: `coderemote tunnels delete <id> --force`.");
4797
+ }
4798
+ if (tunnel.hostConnections > 0 && !force) {
4799
+ throw fail(409, "EONLINE", "An agent is serving this tunnel right now. Deleting it unpairs its phones; send force to delete it anyway.");
4800
+ }
4801
+ const res = await devtunnel2("delete", tunnel.tunnelId, "-f");
4802
+ if (!res.ok) throw fail(502, "EDEVTUNNEL", res.out.trim());
4803
+ deleted(tunnel.tunnelId);
4804
+ return { deleted: tunnel.tunnelId };
3236
4805
  };
3237
4806
  }
3238
4807
 
3239
4808
  // src/update.js
3240
- import fs13 from "node:fs";
3241
- import path11 from "node:path";
4809
+ import fs18 from "node:fs";
4810
+ import path15 from "node:path";
3242
4811
  var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
3243
4812
  var CHECK_INTERVAL_MS = 24 * 36e5;
3244
4813
  var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
@@ -3262,10 +4831,10 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
3262
4831
  }
3263
4832
  }
3264
4833
  async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
3265
- const file = path11.join(dir, "update.json");
4834
+ const file = path15.join(dir, "update.json");
3266
4835
  let cache = {};
3267
4836
  try {
3268
- cache = JSON.parse(fs13.readFileSync(file, "utf8"));
4837
+ cache = JSON.parse(fs18.readFileSync(file, "utf8"));
3269
4838
  } catch {
3270
4839
  }
3271
4840
  let latest = cache.latest;
@@ -3274,8 +4843,8 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
3274
4843
  if (fetched) {
3275
4844
  latest = fetched;
3276
4845
  try {
3277
- fs13.mkdirSync(dir, { recursive: true, mode: 448 });
3278
- fs13.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
4846
+ fs18.mkdirSync(dir, { recursive: true, mode: 448 });
4847
+ fs18.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
3279
4848
  } catch {
3280
4849
  }
3281
4850
  }
@@ -3283,12 +4852,12 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
3283
4852
  return isNewer(latest, current) ? latest : null;
3284
4853
  }
3285
4854
  function installCommand(installedAt) {
3286
- const p = installedAt.split(path11.sep).join("/");
4855
+ const p = installedAt.split(path15.sep).join("/");
3287
4856
  const runtime = p.match(/^(.*\/runtime\/node)\/(lib\/)?node_modules\/coderemote\//);
3288
4857
  if (runtime) {
3289
4858
  const prefix = installedAt.slice(0, runtime[1].length);
3290
- const [node, modules] = runtime[2] ? [path11.join(prefix, "bin", "node"), path11.join(prefix, "lib", "node_modules")] : [path11.join(prefix, "node.exe"), path11.join(prefix, "node_modules")];
3291
- return [node, [path11.join(modules, "npm", "bin", "npm-cli.js"), "install", "-g", "--prefix", prefix, "coderemote@latest"]];
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"]];
3292
4861
  }
3293
4862
  if (p.includes("/Cellar/coderemote/")) return ["brew", ["upgrade", "coderemote"]];
3294
4863
  if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
@@ -3300,7 +4869,7 @@ function installCommand(installedAt) {
3300
4869
  // package.json
3301
4870
  var package_default = {
3302
4871
  name: "coderemote",
3303
- version: "0.3.7",
4872
+ version: "0.3.9",
3304
4873
  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.",
3305
4874
  keywords: [
3306
4875
  "remote",
@@ -3623,28 +5192,28 @@ ${log2}`));
3623
5192
  }
3624
5193
 
3625
5194
  // bin/coderemote.js
3626
- var run5 = promisify4(execFile8);
3627
- var CONFIG_DIR = path12.join(os6.homedir(), ".coderemote");
3628
- var LEGACY_CONFIG_DIR = path12.join(os6.homedir(), ".vsremote");
3629
- if (!fs14.existsSync(CONFIG_DIR) && fs14.existsSync(LEGACY_CONFIG_DIR)) fs14.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
3630
- var CONFIG_FILE = path12.join(CONFIG_DIR, "config.json");
3631
- var PUSH_DIR = path12.join(CONFIG_DIR, "push");
3632
- var UPLOADS_DIR = path12.join(CONFIG_DIR, "uploads");
5195
+ 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");
3633
5202
  var DEFAULT_PORT = 31580;
3634
5203
  var VERSION = package_default.version;
3635
- var PACKAGED = path12.basename(path12.dirname(fileURLToPath(import.meta.url))) === "dist";
5204
+ var PACKAGED = path16.basename(path16.dirname(fileURLToPath(import.meta.url))) === "dist";
3636
5205
  var spinner = new Spinner();
3637
5206
  function loadConfig() {
3638
5207
  let cfg2 = {};
3639
5208
  try {
3640
- cfg2 = JSON.parse(fs14.readFileSync(CONFIG_FILE, "utf8"));
5209
+ cfg2 = JSON.parse(fs19.readFileSync(CONFIG_FILE, "utf8"));
3641
5210
  } catch {
3642
5211
  }
3643
5212
  if (!cfg2.token) {
3644
- cfg2.token = randomBytes3(32).toString("base64url");
5213
+ cfg2.token = randomBytes4(32).toString("base64url");
3645
5214
  saveConfig(cfg2);
3646
- fs14.rmSync(PUSH_DIR, { recursive: true, force: true });
3647
- fs14.rmSync(UPLOADS_DIR, { recursive: true, force: true });
5215
+ fs19.rmSync(PUSH_DIR, { recursive: true, force: true });
5216
+ fs19.rmSync(UPLOADS_DIR, { recursive: true, force: true });
3648
5217
  }
3649
5218
  if (cfg2.tunnelId && !cfg2.tunnels) {
3650
5219
  cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
@@ -3660,30 +5229,32 @@ function stopAgent(pid) {
3660
5229
  }
3661
5230
  }
3662
5231
  function saveConfig(cfg2) {
3663
- fs14.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
3664
- fs14.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
5232
+ fs19.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
5233
+ fs19.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
3665
5234
  }
3666
5235
  function portFree(port2) {
3667
5236
  return new Promise((resolve) => {
3668
- const probe = net2.createServer();
5237
+ const probe = net3.createServer();
3669
5238
  probe.once("error", () => resolve(false));
3670
5239
  probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
3671
5240
  });
3672
5241
  }
3673
- function listen(token, port2, root2, tunnelToken, preview, status2) {
5242
+ function listen(token, port2, root2, tunnelToken, preview2, status2, remover) {
3674
5243
  const hookSecret = ensureHookSecret(CONFIG_DIR);
3675
- writeHookLauncher(CONFIG_DIR, { script: fs14.realpathSync(process.argv[1]) });
5244
+ writeHookLauncher(CONFIG_DIR, { script: fs19.realpathSync(process.argv[1]) });
3676
5245
  const server = createAgentServer({
3677
5246
  token,
3678
5247
  root: root2,
3679
5248
  tunnelToken,
3680
- preview,
5249
+ preview: preview2,
3681
5250
  tunnelStatus: status2,
5251
+ tunnelDelete: remover,
3682
5252
  hookSecret,
3683
5253
  // This very CLI, so coding agents in the app's terminals report through the same version.
3684
- hookBin: fs14.realpathSync(process.argv[1]),
5254
+ hookBin: fs19.realpathSync(process.argv[1]),
3685
5255
  pushStore: pushStore(PUSH_DIR, port2),
3686
- uploadsDir: UPLOADS_DIR
5256
+ uploadsDir: UPLOADS_DIR,
5257
+ version: VERSION
3687
5258
  });
3688
5259
  return new Promise((resolve, reject) => {
3689
5260
  server.once("error", reject);
@@ -3694,7 +5265,7 @@ var DEVTUNNEL = "devtunnel";
3694
5265
  async function resolveDevtunnel() {
3695
5266
  let installing = false;
3696
5267
  DEVTUNNEL = await ensureDevtunnel({
3697
- dir: path12.join(CONFIG_DIR, "bin"),
5268
+ dir: path16.join(CONFIG_DIR, "bin"),
3698
5269
  progress: (detail2) => {
3699
5270
  if (!installing) {
3700
5271
  installing = true;
@@ -3707,7 +5278,7 @@ async function resolveDevtunnel() {
3707
5278
  }
3708
5279
  async function devtunnel(...args) {
3709
5280
  try {
3710
- const { stdout } = await run5(DEVTUNNEL, args);
5281
+ const { stdout } = await run6(DEVTUNNEL, args);
3711
5282
  return { ok: true, out: stdout };
3712
5283
  } catch (e) {
3713
5284
  return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
@@ -3721,10 +5292,9 @@ function loginArgs(opts = rest) {
3721
5292
  }
3722
5293
  function interactiveLogin(args) {
3723
5294
  return new Promise((resolve) => {
3724
- spawn3(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
5295
+ spawn5(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
3725
5296
  });
3726
5297
  }
3727
- var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
3728
5298
  async function login(reason) {
3729
5299
  spinner.stop();
3730
5300
  console.error(`Dev tunnels: ${reason.trim()}`);
@@ -3740,12 +5310,11 @@ async function login(reason) {
3740
5310
  }
3741
5311
  async function tunnelUser() {
3742
5312
  let who = await devtunnel("user", "show");
3743
- if (!who.ok || LOGIN_NEEDED.test(who.out)) {
5313
+ if (!signedInUser(who)) {
3744
5314
  await login(who.out || "not logged in");
3745
5315
  who = await devtunnel("user", "show");
3746
5316
  }
3747
- const m = who.out.match(/Logged in as (\S+) using (\S+?)\.?\s*$/m);
3748
- return m ? `${m[1]} (${m[2]})` : who.out.trim();
5317
+ return signedInUser(who) ?? who.out.trim();
3749
5318
  }
3750
5319
  function applyAccess(tunnelId, anonymous2) {
3751
5320
  return anonymous2 ? devtunnel("access", "create", tunnelId, "--anonymous") : devtunnel("access", "reset", tunnelId);
@@ -3765,7 +5334,7 @@ ${access.out}`);
3765
5334
  if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
3766
5335
  ${updated.out}`);
3767
5336
  }
3768
- const name = port2 === DEFAULT_PORT ? os6.hostname() : `${os6.hostname()} :${port2}`;
5337
+ const name = port2 === DEFAULT_PORT ? os7.hostname() : `${os7.hostname()} :${port2}`;
3769
5338
  const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
3770
5339
  if (anonymous2) createArgs.push("--allow-anonymous");
3771
5340
  let created = await devtunnel(...createArgs);
@@ -3807,7 +5376,7 @@ ${res.out}`);
3807
5376
  }
3808
5377
  function hostTunnel(tunnelId, port2) {
3809
5378
  return new Promise((resolve, reject) => {
3810
- const child = spawn3(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
5379
+ const child = spawn5(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
3811
5380
  let log2 = "";
3812
5381
  const onData = (d) => {
3813
5382
  log2 += d;
@@ -3911,9 +5480,9 @@ var flag = (name) => {
3911
5480
  };
3912
5481
  var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
3913
5482
  var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
3914
- var root = path12.resolve(flag("--root") ?? process.cwd());
5483
+ var root = path16.resolve(flag("--root") ?? process.cwd());
3915
5484
  var cfg = loadConfig();
3916
- var registry = new AgentRegistry(path12.join(CONFIG_DIR, "agents"));
5485
+ var registry = new AgentRegistry(path16.join(CONFIG_DIR, "agents"));
3917
5486
  async function start({ tunnel }) {
3918
5487
  const agents = registry.list();
3919
5488
  if (flag("--port")) {
@@ -3945,10 +5514,10 @@ async function start({ tunnel }) {
3945
5514
  let server;
3946
5515
  let tunnelId;
3947
5516
  const tunnelToken = async () => tunnel && !anonymous && tunnelId ? connectToken(tunnelId) : null;
3948
- const preview = tunnel ? new PreviewTunnel({
5517
+ const preview2 = tunnel ? new PreviewTunnel({
3949
5518
  devtunnel,
3950
5519
  command: () => DEVTUNNEL,
3951
- description: `coderemote preview ${os6.hostname()}`,
5520
+ description: `coderemote preview ${os7.hostname()}`,
3952
5521
  leftover: cfg.previewTunnels?.[port],
3953
5522
  remember: (id) => {
3954
5523
  cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
@@ -3958,10 +5527,19 @@ async function start({ tunnel }) {
3958
5527
  mintToken: connectToken
3959
5528
  }) : null;
3960
5529
  let account;
3961
- const status2 = tunnel ? tunnelStatus({ devtunnel, tunnelId: () => tunnelId, account: () => account, anonymous, preview }) : null;
5530
+ const status2 = tunnel ? tunnelStatus({ devtunnel, tunnelId: () => tunnelId, account: () => account, anonymous, preview: preview2 }) : null;
5531
+ const remover = tunnel ? tunnelDelete({
5532
+ devtunnel,
5533
+ tunnelId: () => tunnelId,
5534
+ deleted: (id) => {
5535
+ status2.invalidate();
5536
+ const saved = loadConfig();
5537
+ if (forgetTunnel(saved, id)) saveConfig(saved);
5538
+ }
5539
+ }) : null;
3962
5540
  spinner.start("Starting agent");
3963
5541
  try {
3964
- server = await listen(cfg.token, port, root, tunnelToken, preview, status2);
5542
+ server = await listen(cfg.token, port, root, tunnelToken, preview2, status2, remover);
3965
5543
  } catch (e) {
3966
5544
  spinner.fail();
3967
5545
  if (e.code !== "EADDRINUSE") throw e;
@@ -3998,7 +5576,7 @@ async function start({ tunnel }) {
3998
5576
  registry.register({ port, root, url });
3999
5577
  const shutdown = async () => {
4000
5578
  child?.kill();
4001
- await preview?.close();
5579
+ await preview2?.close();
4002
5580
  await server.killAllSessions();
4003
5581
  server.close();
4004
5582
  process.exit(0);
@@ -4008,7 +5586,7 @@ async function start({ tunnel }) {
4008
5586
  process.on("SIGHUP", shutdown);
4009
5587
  process.on("exit", () => {
4010
5588
  child?.kill();
4011
- preview?.stop();
5589
+ preview2?.stop();
4012
5590
  });
4013
5591
  return { url, access };
4014
5592
  }
@@ -4131,7 +5709,7 @@ function currentAgent() {
4131
5709
  const agents = registry.list();
4132
5710
  if (flag("--port")) return agents.find((a) => a.port === port);
4133
5711
  const here = process.cwd();
4134
- const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path12.sep)).sort((a, b) => b.root.length - a.root.length);
5712
+ const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path16.sep)).sort((a, b) => b.root.length - a.root.length);
4135
5713
  return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
4136
5714
  }
4137
5715
  async function previewPorts(agent, given) {
@@ -4267,7 +5845,7 @@ Token: ${cfg.token}`);
4267
5845
  break;
4268
5846
  }
4269
5847
  case "rotate-token": {
4270
- cfg.token = randomBytes3(32).toString("base64url");
5848
+ cfg.token = randomBytes4(32).toString("base64url");
4271
5849
  saveConfig(cfg);
4272
5850
  const agents = registry.list();
4273
5851
  for (const agent of agents) {
@@ -4340,7 +5918,7 @@ Token: ${cfg.token}`);
4340
5918
  console.log(`coderemote ${VERSION} is the latest version.`);
4341
5919
  break;
4342
5920
  }
4343
- const [command, args] = installCommand(fs14.realpathSync(process.argv[1]));
5921
+ const [command, args] = installCommand(fs19.realpathSync(process.argv[1]));
4344
5922
  console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
4345
5923
  `);
4346
5924
  const code = await new Promise((resolve) => {