coderemote 0.3.8 → 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.
Files changed (2) hide show
  1. package/dist/coderemote.js +1884 -361
  2. package/package.json +1 -1
@@ -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";
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
7
  import net3 from "node:net";
8
- import os6 from "node:os";
9
- import path12 from "node:path";
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") {
@@ -601,11 +641,8 @@ var AgentHub = class extends EventEmitter {
601
641
  if (last && !turnEntries.some((e) => e.kind === "assistant" && e.text.trim() === last)) {
602
642
  this.#entry(s, { kind: "assistant", text: last });
603
643
  }
604
- s.streaming.clear();
605
- const duration = s.turnStartedAt ? now - s.turnStartedAt : 0;
606
- s.turnStartedAt = null;
607
- s.state = s.requests.size ? "waiting" : "idle";
608
- 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);
609
646
  break;
610
647
  }
611
648
  case "Interrupt":
@@ -738,13 +775,13 @@ var AgentHub = class extends EventEmitter {
738
775
  pending.resolve(output);
739
776
  }
740
777
  async #backfill(s, file) {
741
- let entries = [];
778
+ let entries2 = [];
742
779
  try {
743
- entries = await this.#transcript(file, MAX_ENTRIES);
780
+ entries2 = await this.#transcript(file, MAX_ENTRIES);
744
781
  } catch {
745
782
  }
746
783
  const tools = /* @__PURE__ */ new Map();
747
- for (const e of entries) {
784
+ for (const e of entries2) {
748
785
  if (e.kind === "tool") {
749
786
  tools.set(e.toolUseId, e.name);
750
787
  e.input = clipInput(e.input);
@@ -760,6 +797,30 @@ var AgentHub = class extends EventEmitter {
760
797
  if (s.entries.length > MAX_ENTRIES) s.entries.splice(0, s.entries.length - MAX_ENTRIES);
761
798
  }
762
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
+ }
763
824
  #fromRollout(s, item) {
764
825
  const fields = codexItem(item);
765
826
  if (!fields) return;
@@ -790,26 +851,856 @@ var AgentHub = class extends EventEmitter {
790
851
  s.turnStartedAt = null;
791
852
  }
792
853
  #entry(s, fields) {
793
- const entry2 = { seq: ++s.seq, time: this.#now(), ...fields };
794
- s.entries.push(entry2);
854
+ const entry3 = { seq: ++s.seq, time: this.#now(), ...fields };
855
+ s.entries.push(entry3);
795
856
  if (s.entries.length > MAX_ENTRIES) s.entries.shift();
796
- this.emit("message", { type: "entry", session: s.id, entry: entry2 });
857
+ this.emit("message", { type: "entry", session: s.id, entry: entry3 });
797
858
  }
798
859
  #emitSession(s) {
799
860
  this.emit("message", { type: "session", session: s.toJSON() });
800
861
  }
801
862
  };
802
863
 
803
- // ../../packages/agent-core/src/git.js
804
- import { execFile } from "node:child_process";
864
+ // ../../packages/agent-core/src/commands.js
805
865
  import fs4 from "node:fs/promises";
806
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";
807
1698
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".build", "build", "dist", "DerivedData", "Pods", "vendor", ".venv", "target"]);
808
1699
  var MAX_SCAN_DEPTH = 4;
809
1700
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
810
1701
  function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 * 1024 } = {}) {
811
1702
  return new Promise((resolve, reject) => {
812
- execFile(
1703
+ execFile2(
813
1704
  "git",
814
1705
  args,
815
1706
  {
@@ -830,30 +1721,36 @@ function git(cwd, args, { timeout = 3e4, okCodes = [0], maxBuffer = 16 * 1024 *
830
1721
  async function findRepos(root2) {
831
1722
  const repos = [];
832
1723
  async function walk(dir, depth) {
833
- let entries;
1724
+ let entries2;
834
1725
  try {
835
- entries = await fs4.readdir(dir, { withFileTypes: true });
1726
+ entries2 = await fs8.readdir(dir, { withFileTypes: true });
836
1727
  } catch {
837
1728
  return;
838
1729
  }
839
- if (entries.some((e) => e.name === ".git")) {
840
- repos.push(dir);
841
- return;
842
- }
1730
+ if (entries2.some((e) => e.name === ".git")) repos.push(dir);
843
1731
  if (depth >= MAX_SCAN_DEPTH) return;
844
- const subdirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name));
845
- 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)));
846
1734
  }
847
1735
  await walk(root2, 0);
848
- 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));
849
1737
  return Promise.all(
850
1738
  repos.map(async (repo) => ({
851
1739
  path: repo,
852
- name: repo === root2 ? path3.basename(root2) : path3.relative(root2, repo),
853
- branch: await currentBranch(repo)
1740
+ name: repo === root2 ? path6.basename(root2) : path6.relative(root2, repo),
1741
+ branch: await currentBranch(repo),
1742
+ ...await summary(repo)
854
1743
  }))
855
1744
  );
856
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
+ }
857
1754
  async function currentBranch(repo) {
858
1755
  try {
859
1756
  const { stdout } = await git(repo, ["symbolic-ref", "--short", "-q", "HEAD"], { okCodes: [0, 1] });
@@ -867,9 +1764,9 @@ async function currentBranch(repo) {
867
1764
  async function status(repo) {
868
1765
  const { stdout } = await git(repo, ["status", "--porcelain=v2", "--branch", "--untracked-files=all", "-z"]);
869
1766
  const result = { branch: null, upstream: null, ahead: 0, behind: 0, files: [] };
870
- const records = stdout.split("\0");
871
- for (let i = 0; i < records.length; i++) {
872
- const r = records[i];
1767
+ const records2 = stdout.split("\0");
1768
+ for (let i = 0; i < records2.length; i++) {
1769
+ const r = records2[i];
873
1770
  if (!r) continue;
874
1771
  if (r.startsWith("# branch.head ")) {
875
1772
  const head = r.slice(14);
@@ -882,20 +1779,20 @@ async function status(repo) {
882
1779
  result.behind = Math.abs(parseInt(b, 10));
883
1780
  } else if (r.startsWith("1 ")) {
884
1781
  const parts = r.split(" ");
885
- result.files.push(entry(parts[1], parts.slice(8).join(" ")));
1782
+ result.files.push(entry2(parts[1], parts.slice(8).join(" ")));
886
1783
  } else if (r.startsWith("2 ")) {
887
1784
  const parts = r.split(" ");
888
- 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] });
889
1786
  } else if (r.startsWith("u ")) {
890
1787
  const parts = r.split(" ");
891
- 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 });
892
1789
  } else if (r.startsWith("? ")) {
893
1790
  result.files.push({ path: r.slice(2), index: ".", worktree: "?" });
894
1791
  }
895
1792
  }
896
1793
  return result;
897
1794
  }
898
- function entry(xy, filePath) {
1795
+ function entry2(xy, filePath) {
899
1796
  return { path: filePath, index: xy[0], worktree: xy[1] };
900
1797
  }
901
1798
  async function diff(repo, file, { staged = false, untracked = false } = {}) {
@@ -1052,33 +1949,33 @@ var LANDING_HTML = `<!doctype html>
1052
1949
  `;
1053
1950
 
1054
1951
  // ../../packages/agent-core/src/ports.js
1055
- import { execFile as execFile3 } from "node:child_process";
1056
- import fs6 from "node:fs/promises";
1952
+ import { execFile as execFile4 } from "node:child_process";
1953
+ import fs10 from "node:fs/promises";
1057
1954
  import net from "node:net";
1058
- import os from "node:os";
1955
+ import os2 from "node:os";
1059
1956
 
1060
1957
  // ../../packages/agent-core/src/platform.js
1061
- import { execFile as execFile2, spawn } from "node:child_process";
1062
- import fs5 from "node:fs";
1063
- 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";
1064
1961
  var isWindows = process.platform === "win32";
1065
1962
  function shellQuote(arg) {
1066
1963
  return `'${String(arg).replace(/'/g, `'\\''`)}'`;
1067
1964
  }
1068
1965
  function runnable(file) {
1069
1966
  try {
1070
- if (!isWindows) fs5.accessSync(file, fs5.constants.X_OK);
1071
- return fs5.statSync(file).isFile();
1967
+ if (!isWindows) fs9.accessSync(file, fs9.constants.X_OK);
1968
+ return fs9.statSync(file).isFile();
1072
1969
  } catch {
1073
1970
  return false;
1074
1971
  }
1075
1972
  }
1076
1973
  function findExecutable(name, env = process.env) {
1077
- 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) : [""];
1078
1975
  const match = (base2) => exts.map((e) => base2 + e).find(runnable) ?? null;
1079
- if (name.includes("/") || name.includes(path4.sep)) return match(path4.resolve(name));
1080
- for (const dir of (env.PATH ?? env.Path ?? "").split(path4.delimiter)) {
1081
- 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));
1082
1979
  if (found) return found;
1083
1980
  }
1084
1981
  return null;
@@ -1107,12 +2004,12 @@ function terminalCommand(argv2) {
1107
2004
  }
1108
2005
  function spawnCommand(argv2, options = {}) {
1109
2006
  const { file, args, verbatim } = resolveCommand(argv2);
1110
- return spawn(file, args, { ...options, windowsVerbatimArguments: verbatim });
2007
+ return spawn2(file, args, { ...options, windowsVerbatimArguments: verbatim });
1111
2008
  }
1112
2009
  function execCommand(argv2, options = {}) {
1113
2010
  return new Promise((resolve, reject) => {
1114
2011
  const { file, args, verbatim } = resolveCommand(argv2);
1115
- execFile2(
2012
+ execFile3(
1116
2013
  file,
1117
2014
  args,
1118
2015
  { ...options, windowsVerbatimArguments: verbatim },
@@ -1122,7 +2019,7 @@ function execCommand(argv2, options = {}) {
1122
2019
  }
1123
2020
  function loginShell(script, timeout) {
1124
2021
  return new Promise((resolve) => {
1125
- execFile2(
2022
+ execFile3(
1126
2023
  interactiveShell().file,
1127
2024
  ["-l", "-c", script],
1128
2025
  { timeout, maxBuffer: 16 * 1024 * 1024 },
@@ -1144,9 +2041,9 @@ async function commandExists(command) {
1144
2041
  }
1145
2042
 
1146
2043
  // ../../packages/agent-core/src/ports.js
1147
- function run(cmd2, args) {
2044
+ function run2(cmd2, args) {
1148
2045
  return new Promise((resolve, reject) => {
1149
- execFile3(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
2046
+ execFile4(cmd2, args, { maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {
1150
2047
  if (err?.code === "ENOENT") reject(err);
1151
2048
  else resolve(stdout || "");
1152
2049
  });
@@ -1160,7 +2057,7 @@ function splitHostPort(name) {
1160
2057
  }
1161
2058
  async function listeningSockets() {
1162
2059
  try {
1163
- 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"]);
1164
2061
  const sockets2 = [];
1165
2062
  let pid;
1166
2063
  let name;
@@ -1175,9 +2072,9 @@ async function listeningSockets() {
1175
2072
  }
1176
2073
  return sockets2;
1177
2074
  } catch (e) {
1178
- if (e.code !== "ENOENT" || os.platform() !== "linux") throw e;
2075
+ if (e.code !== "ENOENT" || os2.platform() !== "linux") throw e;
1179
2076
  }
1180
- const out = await run("ss", ["-Hltnp"]);
2077
+ const out = await run2("ss", ["-Hltnp"]);
1181
2078
  const sockets = [];
1182
2079
  for (const line of out.split("\n")) {
1183
2080
  const cols = line.trim().split(/\s+/);
@@ -1188,7 +2085,7 @@ async function listeningSockets() {
1188
2085
  return sockets;
1189
2086
  }
1190
2087
  async function processTable() {
1191
- 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="]);
1192
2089
  const table = /* @__PURE__ */ new Map();
1193
2090
  for (const line of out.split("\n")) {
1194
2091
  const m = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
@@ -1199,11 +2096,11 @@ async function processTable() {
1199
2096
  async function workingDirs(pids) {
1200
2097
  const dirs = /* @__PURE__ */ new Map();
1201
2098
  if (!pids.length || isWindows) return dirs;
1202
- if (os.platform() === "linux") {
1203
- 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))));
1204
2101
  return dirs;
1205
2102
  }
1206
- 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(() => "");
1207
2104
  let pid;
1208
2105
  for (const line of out.split("\n")) {
1209
2106
  if (line[0] === "p") pid = parseInt(line.slice(1), 10);
@@ -1215,9 +2112,9 @@ var WINDOWS_SHELLS = /* @__PURE__ */ new Set(["cmd.exe", "powershell.exe", "pwsh
1215
2112
  async function windowsListeners() {
1216
2113
  const query = "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine | ConvertTo-Json -Compress";
1217
2114
  const [v4, v6, json] = await Promise.all([
1218
- run("netstat", ["-ano", "-p", "TCP"]),
1219
- run("netstat", ["-ano", "-p", "TCPv6"]),
1220
- 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])
1221
2118
  ]);
1222
2119
  const table = /* @__PURE__ */ new Map();
1223
2120
  for (const p of [JSON.parse(json || "[]")].flat()) {
@@ -1247,9 +2144,9 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
1247
2144
  const byKey = /* @__PURE__ */ new Map();
1248
2145
  for (const s of sockets) {
1249
2146
  const key = `${s.pid}:${s.port}`;
1250
- const entry2 = byKey.get(key);
1251
- if (entry2) {
1252
- 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);
1253
2150
  continue;
1254
2151
  }
1255
2152
  byKey.set(key, {
@@ -1266,10 +2163,10 @@ async function listPorts(shells = /* @__PURE__ */ new Map()) {
1266
2163
  agent: s.pid === process.pid || AGENT_COMMAND.test(table.get(s.pid)?.command ?? "")
1267
2164
  });
1268
2165
  }
1269
- const entries = [...byKey.values()];
1270
- const dirs = await workingDirs([...new Set(entries.map((e) => e.pid))]);
1271
- for (const e of entries) e.cwd = dirs.get(e.pid) ?? null;
1272
- 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);
1273
2170
  }
1274
2171
  function sessionOf(pid, table, shells) {
1275
2172
  for (let p = pid, depth = 0; p > 1 && depth < 64; p = table.get(p)?.ppid, depth++) {
@@ -1304,17 +2201,18 @@ function isListening(port2, timeout = 2e3) {
1304
2201
  }
1305
2202
 
1306
2203
  // ../../packages/agent-core/src/push.js
1307
- import { execFile as execFile4 } from "node:child_process";
1308
- import { createCipheriv, randomBytes } from "node:crypto";
1309
- 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";
1310
2207
  import { promisify } from "node:util";
1311
- var run2 = promisify(execFile4);
2208
+ var run3 = promisify(execFile5);
1312
2209
  var FINISHED_AFTER = 60 * 1e3;
1313
2210
  var ACTIVE_WITHIN = 2 * 60;
2211
+ var AT_SESSION_WITHIN = 30;
1314
2212
  async function macIdleSeconds() {
1315
- if (os2.platform() !== "darwin") return null;
2213
+ if (os3.platform() !== "darwin") return null;
1316
2214
  try {
1317
- 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 });
1318
2216
  const ns = stdout.match(/"HIDIdleTime"\s*=\s*(\d+)/)?.[1];
1319
2217
  return ns ? Number(BigInt(ns) / 1000000000n) : null;
1320
2218
  } catch {
@@ -1325,8 +2223,25 @@ async function userActiveHere() {
1325
2223
  const idle = await macIdleSeconds();
1326
2224
  return idle !== null && idle < ACTIVE_WITHIN;
1327
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
+ }
1328
2243
  function seal(key, data) {
1329
- const nonce = randomBytes(12);
2244
+ const nonce = randomBytes2(12);
1330
2245
  const cipher = createCipheriv("aes-256-gcm", key, nonce);
1331
2246
  const body = Buffer.concat([cipher.update(JSON.stringify(data), "utf8"), cipher.final()]);
1332
2247
  return Buffer.concat([nonce, body, cipher.getAuthTag()]).toString("base64");
@@ -1376,8 +2291,8 @@ var PushSender = class {
1376
2291
  #devices;
1377
2292
  #store;
1378
2293
  #hub;
1379
- #foreground;
1380
- #userActive;
2294
+ #shows;
2295
+ #atSession;
1381
2296
  #fetch;
1382
2297
  // Requests a notification went out for, so their resolution can replace it.
1383
2298
  #notified = /* @__PURE__ */ new Set();
@@ -1385,15 +2300,17 @@ var PushSender = class {
1385
2300
  * @param {object} opts
1386
2301
  * @param {import('./agents.js').AgentHub} opts.hub
1387
2302
  * @param {{ load: () => object[], save: (devices: object[]) => void }} [opts.store]
1388
- * @param {() => boolean} [opts.foreground] whether a client shows this machine right now
1389
- * @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
1390
2307
  */
1391
- 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 }) {
1392
2309
  this.#hub = hub;
1393
2310
  this.#store = store;
1394
2311
  this.#devices = store?.load() ?? [];
1395
- this.#foreground = foreground;
1396
- this.#userActive = userActive ?? userActiveHere;
2312
+ this.#shows = shows;
2313
+ this.#atSession = atSession;
1397
2314
  this.#fetch = fetch2;
1398
2315
  hub.on("message", (msg) => this.#follow(msg).catch(() => {
1399
2316
  }));
@@ -1426,10 +2343,12 @@ var PushSender = class {
1426
2343
  #save() {
1427
2344
  this.#store?.save(this.#devices);
1428
2345
  }
1429
- async #quiet() {
1430
- 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;
1431
2350
  try {
1432
- return await this.#userActive();
2351
+ return await this.#atSession(this.#hub.host(what.session));
1433
2352
  } catch {
1434
2353
  return false;
1435
2354
  }
@@ -1438,7 +2357,7 @@ var PushSender = class {
1438
2357
  if (!this.#devices.length) return;
1439
2358
  if (msg.type === "request") {
1440
2359
  const { request } = msg;
1441
- if (await this.#quiet()) return;
2360
+ if (await this.#quiet({ session: request.session })) return;
1442
2361
  if (!this.#hub.requests().some((r) => r.id === request.id)) return;
1443
2362
  this.#notified.add(request.id);
1444
2363
  const question = request.kind === "question";
@@ -1470,7 +2389,7 @@ var PushSender = class {
1470
2389
  fallback: "Answered on your computer"
1471
2390
  });
1472
2391
  } else if (msg.type === "entry" && msg.entry.kind === "turn_end" && msg.entry.duration >= FINISHED_AFTER) {
1473
- if (await this.#quiet()) return;
2392
+ if (await this.#quiet({ session: msg.session })) return;
1474
2393
  const session = this.#hub.session(msg.session);
1475
2394
  await this.#broadcast("finished", {
1476
2395
  kind: "finished",
@@ -1482,11 +2401,27 @@ var PushSender = class {
1482
2401
  });
1483
2402
  }
1484
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
+ }
1485
2420
  async #broadcast(kind, note) {
1486
2421
  await Promise.all(this.#devices.filter((d) => d.kinds[kind]).map((d) => this.#send(d, note)));
1487
2422
  }
1488
- async #send(device, { kind, title, body, session, request, question, options, category, collapseId, priority, silent, fallback }) {
1489
- 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 };
1490
2425
  const aps = { alert: { title: "Code Remote", body: fallback }, "mutable-content": 1 };
1491
2426
  if (category) aps.category = category;
1492
2427
  if (session) aps["thread-id"] = session;
@@ -1521,7 +2456,7 @@ var PushSender = class {
1521
2456
  };
1522
2457
 
1523
2458
  // ../../packages/agent-core/src/inspector.js
1524
- import http from "node:http";
2459
+ import http2 from "node:http";
1525
2460
  import net2 from "node:net";
1526
2461
  import zlib from "node:zlib";
1527
2462
  var MAX_ENTRIES2 = 200;
@@ -1564,7 +2499,7 @@ var InspectingProxy = class {
1564
2499
  /** @param {number} target the dev server's port on this machine */
1565
2500
  constructor(target) {
1566
2501
  this.target = target;
1567
- this.server = http.createServer((req, res) => this.#forward(req, res));
2502
+ this.server = http2.createServer((req, res) => this.#forward(req, res));
1568
2503
  this.server.on("upgrade", (req, socket, head) => this.#tunnelUpgrade(req, socket, head));
1569
2504
  }
1570
2505
  /** Listens on a free loopback port; resolves to it. */
@@ -1582,7 +2517,7 @@ var InspectingProxy = class {
1582
2517
  }
1583
2518
  /** Summaries, oldest first; `after` skips ids up to and including it. */
1584
2519
  list(after = 0) {
1585
- return this.#entries.filter((e) => e.id > after).map(summary);
2520
+ return this.#entries.filter((e) => e.id > after).map(summary2);
1586
2521
  }
1587
2522
  get(id) {
1588
2523
  return this.#entries.find((e) => e.id === id);
@@ -1591,7 +2526,7 @@ var InspectingProxy = class {
1591
2526
  this.#entries = [];
1592
2527
  }
1593
2528
  #record(req) {
1594
- const entry2 = {
2529
+ const entry3 = {
1595
2530
  id: nextId++,
1596
2531
  time: Date.now(),
1597
2532
  method: req.method,
@@ -1602,21 +2537,21 @@ var InspectingProxy = class {
1602
2537
  request: { headers: req.headers, tap: new BodyTap() },
1603
2538
  response: { headers: {}, tap: new BodyTap() }
1604
2539
  };
1605
- this.#entries.push(entry2);
2540
+ this.#entries.push(entry3);
1606
2541
  if (this.#entries.length > MAX_ENTRIES2) this.#entries.shift();
1607
- return entry2;
2542
+ return entry3;
1608
2543
  }
1609
2544
  /** The dev server sees itself addressed as localhost:<its port>, as without the proxy. */
1610
2545
  #headers(req) {
1611
2546
  return { ...req.headers, host: `localhost:${this.target}` };
1612
2547
  }
1613
2548
  #forward(req, res) {
1614
- const entry2 = this.#record(req);
2549
+ const entry3 = this.#record(req);
1615
2550
  const started = performance.now();
1616
2551
  const finish = () => {
1617
- entry2.duration = Math.round(performance.now() - started);
2552
+ entry3.duration = Math.round(performance.now() - started);
1618
2553
  };
1619
- const upstream = http.request({
2554
+ const upstream = http2.request({
1620
2555
  agent: false,
1621
2556
  host: "localhost",
1622
2557
  port: this.target,
@@ -1625,11 +2560,11 @@ var InspectingProxy = class {
1625
2560
  headers: this.#headers(req)
1626
2561
  });
1627
2562
  upstream.on("response", (up) => {
1628
- entry2.status = up.statusCode;
1629
- entry2.response.headers = up.headers;
2563
+ entry3.status = up.statusCode;
2564
+ entry3.response.headers = up.headers;
1630
2565
  res.writeHead(up.statusCode, up.statusMessage, up.rawHeaders);
1631
2566
  up.on("data", (chunk) => {
1632
- entry2.response.tap.push(chunk);
2567
+ entry3.response.tap.push(chunk);
1633
2568
  res.write(chunk);
1634
2569
  });
1635
2570
  up.on("end", () => {
@@ -1640,14 +2575,14 @@ var InspectingProxy = class {
1640
2575
  });
1641
2576
  upstream.on("error", (e) => {
1642
2577
  finish();
1643
- 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;
1644
2579
  if (res.headersSent) return res.destroy();
1645
- entry2.status = 502;
2580
+ entry3.status = 502;
1646
2581
  res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
1647
- res.end(`coderemote preview: ${entry2.error}
2582
+ res.end(`coderemote preview: ${entry3.error}
1648
2583
  `);
1649
2584
  });
1650
- req.on("data", (chunk) => entry2.request.tap.push(chunk));
2585
+ req.on("data", (chunk) => entry3.request.tap.push(chunk));
1651
2586
  req.pipe(upstream);
1652
2587
  res.on("close", () => {
1653
2588
  if (!res.writableFinished) upstream.destroy();
@@ -1655,7 +2590,7 @@ var InspectingProxy = class {
1655
2590
  }
1656
2591
  /** WebSockets (hot reload) pass through as raw bytes; only the handshake is recorded. */
1657
2592
  #tunnelUpgrade(req, socket, head) {
1658
- const entry2 = this.#record(req);
2593
+ const entry3 = this.#record(req);
1659
2594
  const upstream = net2.connect({ host: "localhost", port: this.target }, () => {
1660
2595
  const headers = this.#headers(req);
1661
2596
  const lines = [`${req.method} ${req.url} HTTP/1.1`];
@@ -1666,18 +2601,18 @@ var InspectingProxy = class {
1666
2601
  \r
1667
2602
  `);
1668
2603
  if (head?.length) upstream.write(head);
1669
- entry2.status = 101;
2604
+ entry3.status = 101;
1670
2605
  upstream.pipe(socket);
1671
2606
  socket.pipe(upstream);
1672
2607
  });
1673
2608
  upstream.on("error", (e) => {
1674
- entry2.error = e.message;
2609
+ entry3.error = e.message;
1675
2610
  socket.destroy();
1676
2611
  });
1677
2612
  socket.on("error", () => upstream.destroy());
1678
2613
  }
1679
2614
  };
1680
- function summary(e) {
2615
+ function summary2(e) {
1681
2616
  return {
1682
2617
  id: e.id,
1683
2618
  time: e.time,
@@ -1693,16 +2628,369 @@ function summary(e) {
1693
2628
  }
1694
2629
  function detail(e) {
1695
2630
  return {
1696
- ...summary(e),
2631
+ ...summary2(e),
1697
2632
  request: { headers: e.request.headers, body: e.request.tap.describe(e.request.headers) },
1698
2633
  response: { headers: e.response.headers, body: e.response.tap.describe(e.response.headers) }
1699
2634
  };
1700
2635
  }
1701
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
+
1702
2990
  // ../../packages/agent-core/src/sessions.js
1703
- import { execFile as execFile5 } from "node:child_process";
1704
- import { randomUUID as randomUUID2 } from "node:crypto";
1705
- 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";
1706
2994
  import pty from "node-pty";
1707
2995
  var SCROLLBACK_LIMIT = 256 * 1024;
1708
2996
  function guardConpty(term) {
@@ -1721,7 +3009,7 @@ var TerminalSession = class {
1721
3009
  * terminalCommand says; the session ends when it exits.
1722
3010
  */
1723
3011
  constructor({ cols, rows, cwd, env = {}, command = null }) {
1724
- this.id = randomUUID2();
3012
+ this.id = randomUUID3();
1725
3013
  this.createdAt = Date.now();
1726
3014
  this.chunks = [];
1727
3015
  this.bufferedBytes = 0;
@@ -1734,7 +3022,7 @@ var TerminalSession = class {
1734
3022
  name: "xterm-256color",
1735
3023
  cols,
1736
3024
  rows,
1737
- cwd: cwd || os3.homedir(),
3025
+ cwd: cwd || os4.homedir(),
1738
3026
  // CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
1739
3027
  env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
1740
3028
  });
@@ -1787,7 +3075,7 @@ var TerminalSession = class {
1787
3075
  /** Ends the session; on Windows, which has no signals, always forcibly (see releaseConpty). */
1788
3076
  kill(signal) {
1789
3077
  if (!isWindows) this.pty.kill(signal);
1790
- 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)], () => {
1791
3079
  });
1792
3080
  }
1793
3081
  /** Types into the session, as a client's keyboard would; nothing once it has exited. */
@@ -1846,21 +3134,21 @@ var SessionManager = class {
1846
3134
  };
1847
3135
 
1848
3136
  // ../../packages/agent-core/src/uploads.js
1849
- import { execFile as execFile6 } from "node:child_process";
1850
- import { randomUUID as randomUUID3 } from "node:crypto";
3137
+ import { execFile as execFile8 } from "node:child_process";
3138
+ import { randomUUID as randomUUID4 } from "node:crypto";
1851
3139
  import { createWriteStream } from "node:fs";
1852
- import fs7 from "node:fs/promises";
1853
- import os4 from "node:os";
1854
- import path5 from "node:path";
3140
+ import fs12 from "node:fs/promises";
3141
+ import os5 from "node:os";
3142
+ import path9 from "node:path";
1855
3143
  import { Transform } from "node:stream";
1856
- import { pipeline } from "node:stream/promises";
3144
+ import { pipeline as pipeline2 } from "node:stream/promises";
1857
3145
  import { promisify as promisify2 } from "node:util";
1858
- var run3 = promisify2(execFile6);
3146
+ var run4 = promisify2(execFile8);
1859
3147
  var UPLOAD_LIMIT = 100 * 1024 * 1024;
1860
3148
  var KEEP = 7 * 24 * 60 * 60 * 1e3;
1861
3149
  var ID = /^[0-9a-f-]{36}$/;
1862
3150
  function safeName(name) {
1863
- const base2 = path5.basename(String(name ?? "").replace(/\\/g, "/"));
3151
+ const base2 = path9.basename(String(name ?? "").replace(/\\/g, "/"));
1864
3152
  const clean = base2.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
1865
3153
  return clean || "file";
1866
3154
  }
@@ -1872,10 +3160,10 @@ var UploadStore = class {
1872
3160
  /** Streams `body` to disk. 413 past `limit`; HEIC photos become JPEG on macOS. */
1873
3161
  async save(body, { name, type, length, limit = UPLOAD_LIMIT }) {
1874
3162
  if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
1875
- const id = randomUUID3();
3163
+ const id = randomUUID4();
1876
3164
  const day = new Date(this.now()).toISOString().slice(0, 10);
1877
- await fs7.mkdir(path5.join(this.dir, day), { recursive: true, mode: 448 });
1878
- 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)}`);
1879
3167
  let size = 0;
1880
3168
  const counter = new Transform({
1881
3169
  transform(chunk, _enc, done) {
@@ -1884,41 +3172,41 @@ var UploadStore = class {
1884
3172
  }
1885
3173
  });
1886
3174
  try {
1887
- await pipeline(body, counter, createWriteStream(file, { mode: 384 }));
3175
+ await pipeline2(body, counter, createWriteStream(file, { mode: 384 }));
1888
3176
  } catch (e) {
1889
- await fs7.rm(file, { force: true });
3177
+ await fs12.rm(file, { force: true });
1890
3178
  throw e;
1891
3179
  }
1892
3180
  let kind = type || "application/octet-stream";
1893
- if ((kind === "image/heic" || /\.heic$/i.test(file)) && os4.platform() === "darwin") {
3181
+ if ((kind === "image/heic" || /\.heic$/i.test(file)) && os5.platform() === "darwin") {
1894
3182
  const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
1895
3183
  try {
1896
- await run3("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
1897
- await fs7.rm(file);
3184
+ await run4("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
3185
+ await fs12.rm(file);
1898
3186
  file = jpeg;
1899
3187
  kind = "image/jpeg";
1900
- size = (await fs7.stat(file)).size;
3188
+ size = (await fs12.stat(file)).size;
1901
3189
  } catch {
1902
3190
  }
1903
3191
  }
1904
- 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 };
1905
3193
  }
1906
3194
  /** The file of an upload; 404 once it's gone (swept, or never existed). */
1907
3195
  async path(id) {
1908
3196
  if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
1909
3197
  let days = [];
1910
3198
  try {
1911
- days = (await fs7.readdir(this.dir)).sort().reverse();
3199
+ days = (await fs12.readdir(this.dir)).sort().reverse();
1912
3200
  } catch {
1913
3201
  }
1914
3202
  for (const day of days) {
1915
3203
  let names = [];
1916
3204
  try {
1917
- names = await fs7.readdir(path5.join(this.dir, day));
3205
+ names = await fs12.readdir(path9.join(this.dir, day));
1918
3206
  } catch {
1919
3207
  }
1920
3208
  const hit = names.find((n) => n.startsWith(`${id}-`));
1921
- if (hit) return path5.join(this.dir, day, hit);
3209
+ if (hit) return path9.join(this.dir, day, hit);
1922
3210
  }
1923
3211
  throw new HttpError(404, "no such upload (they are kept for 7 days)");
1924
3212
  }
@@ -1926,25 +3214,25 @@ var UploadStore = class {
1926
3214
  async sweep() {
1927
3215
  let days = [];
1928
3216
  try {
1929
- days = await fs7.readdir(this.dir);
3217
+ days = await fs12.readdir(this.dir);
1930
3218
  } catch {
1931
3219
  return;
1932
3220
  }
1933
3221
  const cutoff = this.now() - KEEP;
1934
3222
  for (const day of days) {
1935
- const dir = path5.join(this.dir, day);
3223
+ const dir = path9.join(this.dir, day);
1936
3224
  let names = [];
1937
3225
  try {
1938
- names = await fs7.readdir(dir);
3226
+ names = await fs12.readdir(dir);
1939
3227
  } catch {
1940
3228
  continue;
1941
3229
  }
1942
3230
  for (const n of names) {
1943
- const file = path5.join(dir, n);
1944
- const stat = await fs7.stat(file).catch(() => null);
1945
- 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 });
1946
3234
  }
1947
- 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 });
1948
3236
  }
1949
3237
  }
1950
3238
  };
@@ -1957,21 +3245,48 @@ function kindOf(stat) {
1957
3245
  return "other";
1958
3246
  }
1959
3247
  async function describe(fullPath, name) {
1960
- const lst = await fs8.lstat(fullPath);
1961
- 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 };
1962
3250
  if (lst.isSymbolicLink()) {
1963
3251
  try {
1964
- entry2.target = kindOf(await fs8.stat(fullPath));
3252
+ entry3.target = kindOf(await fs13.stat(fullPath));
1965
3253
  } catch {
1966
- entry2.target = "broken";
3254
+ entry3.target = "broken";
1967
3255
  }
1968
3256
  }
1969
- return entry2;
3257
+ return entry3;
1970
3258
  }
1971
3259
  var PASTE_START = "\x1B[200~";
1972
3260
  var PASTE_END = "\x1B[201~";
1973
3261
  var KEYS = { interrupt: "\x1B", cycle_mode: "\x1B[Z" };
1974
- 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
+ }
1975
3290
  function typeable(text) {
1976
3291
  return text.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
1977
3292
  }
@@ -1981,7 +3296,7 @@ function sendJson(res, status2, body) {
1981
3296
  res.writeHead(status2, { "content-type": "application/json", "content-length": Buffer.byteLength(json) });
1982
3297
  res.end(json);
1983
3298
  }
1984
- async function readJson(req) {
3299
+ async function readJson2(req) {
1985
3300
  const chunks = [];
1986
3301
  for await (const c of req) chunks.push(c);
1987
3302
  try {
@@ -1994,19 +3309,24 @@ function createAgentServer({
1994
3309
  token,
1995
3310
  root: root2,
1996
3311
  tunnelToken = async () => null,
1997
- preview = null,
3312
+ preview: preview2 = null,
1998
3313
  tunnelStatus: tunnelStatus2 = null,
3314
+ tunnelDelete: tunnelDelete2 = null,
1999
3315
  hookSecret = null,
2000
3316
  hookBin = null,
2001
3317
  uploadsDir = null,
2002
3318
  claudeCommand = "claude",
2003
3319
  codexCommand = "codex",
2004
- 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 }),
2005
3324
  pluginInstalled = null,
2006
3325
  agents = new AgentHub(),
2007
3326
  pushStore: pushStore2 = null,
2008
3327
  pushOptions = {},
2009
- previewSweep = {}
3328
+ previewSweep = {},
3329
+ version = null
2010
3330
  }) {
2011
3331
  const handoff = new TunnelHandoff({ token, mint: tunnelToken });
2012
3332
  const sandbox = new Sandbox(root2);
@@ -2019,9 +3339,13 @@ function createAgentServer({
2019
3339
  });
2020
3340
  }, 10 * 60 * 1e3);
2021
3341
  sweep.unref();
2022
- let foregroundAt = 0;
2023
- const clientInForeground = () => Date.now() - foregroundAt < 3e4;
2024
- 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
+ }));
2025
3349
  if (!agents.presenceSet) {
2026
3350
  agents.present = async () => !clientInForeground() && await (pushOptions.userActive ?? userActiveHere)();
2027
3351
  }
@@ -2112,12 +3436,16 @@ function createAgentServer({
2112
3436
  }
2113
3437
  async function codexModels() {
2114
3438
  try {
2115
- 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"));
2116
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 ?? "" }));
2117
3441
  } catch {
2118
3442
  return [];
2119
3443
  }
2120
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
+ }
2121
3449
  function hookAuthorized(req) {
2122
3450
  if (!hookSecret) return false;
2123
3451
  const got = Buffer.from(String(req.headers[HOOK_SECRET_HEADER] || ""));
@@ -2125,7 +3453,7 @@ function createAgentServer({
2125
3453
  return got.length === want.length && timingSafeEqual2(got, want);
2126
3454
  }
2127
3455
  function previewPort(value, ownPort) {
2128
- if (!preview) throw new HttpError(404, "previews need a tunnel");
3456
+ if (!preview2) throw new HttpError(404, "previews need a tunnel");
2129
3457
  const port2 = Number(value);
2130
3458
  if (!Number.isInteger(port2) || port2 < 1 || port2 > 65535) throw new HttpError(400, "port must be 1-65535");
2131
3459
  if (port2 === ownPort) throw new HttpError(400, "the agent's own port is already on its tunnel");
@@ -2135,7 +3463,7 @@ function createAgentServer({
2135
3463
  const proxies = /* @__PURE__ */ new Map();
2136
3464
  function previewed(port2) {
2137
3465
  const proxy = proxies.get(port2);
2138
- if (!proxy || !preview.list().some((p) => p.port === proxy.port)) {
3466
+ if (!proxy || !preview2.list().some((p) => p.port === proxy.port)) {
2139
3467
  throw new HttpError(404, "that port is not being previewed");
2140
3468
  }
2141
3469
  return proxy;
@@ -2146,21 +3474,23 @@ function createAgentServer({
2146
3474
  return proxy;
2147
3475
  }
2148
3476
  async function previews() {
2149
- const issued = await preview.token();
2150
- 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]));
2151
3479
  const ports = [];
2152
3480
  for (const [port2, proxy] of proxies) {
2153
3481
  const p = forwarded.get(proxy.port);
2154
3482
  if (p) ports.push({ ...p, port: port2 });
2155
3483
  }
2156
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 });
2157
3487
  return { ports, token: issued?.token ?? null, expiresAt: issued?.expiresAt ?? null };
2158
3488
  }
2159
3489
  const { checkMs = 5e3, graceMs = 3e4, killedMs = 1500 } = previewSweep;
2160
3490
  const silentSince = /* @__PURE__ */ new Map();
2161
3491
  let sweeping = false;
2162
3492
  async function sweepPreviews(ports = [...proxies.keys()], now = false) {
2163
- const forwarded = new Set(preview.list().map((p) => p.port));
3493
+ const forwarded = new Set(preview2.list().map((p) => p.port));
2164
3494
  for (const port2 of ports) {
2165
3495
  const proxy = proxies.get(port2);
2166
3496
  if (!proxy || !forwarded.has(proxy.port) || await isListening(port2)) {
@@ -2171,12 +3501,12 @@ function createAgentServer({
2171
3501
  if (!now && Date.now() - silentSince.get(port2) < graceMs) continue;
2172
3502
  silentSince.delete(port2);
2173
3503
  try {
2174
- await preview.unforward(proxy.port);
3504
+ await preview2.unforward(proxy.port);
2175
3505
  } catch {
2176
3506
  }
2177
3507
  }
2178
3508
  }
2179
- const previewTimer = preview && setInterval(async () => {
3509
+ const previewTimer = preview2 && setInterval(async () => {
2180
3510
  if (sweeping || !proxies.size) return;
2181
3511
  sweeping = true;
2182
3512
  try {
@@ -2192,20 +3522,20 @@ function createAgentServer({
2192
3522
  async function repoOf(p) {
2193
3523
  const repo = await sandbox.resolve(p);
2194
3524
  try {
2195
- await fs8.access(path6.join(repo, ".git"));
3525
+ await fs13.access(path10.join(repo, ".git"));
2196
3526
  } catch {
2197
3527
  throw new HttpError(400, "not a git repository");
2198
3528
  }
2199
3529
  return repo;
2200
3530
  }
2201
3531
  async function filesIn(repo, files) {
2202
- 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))) {
2203
3533
  throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
2204
3534
  }
2205
3535
  return Promise.all(
2206
3536
  files.map(async (f) => {
2207
- const abs = await sandbox.resolve(path6.join(repo, f), { allowMissing: true });
2208
- 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);
2209
3539
  if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
2210
3540
  return rel;
2211
3541
  })
@@ -2214,55 +3544,95 @@ function createAgentServer({
2214
3544
  const routes = {
2215
3545
  [Routes.info]: async () => ({
2216
3546
  protocol: PROTOCOL_VERSION,
2217
- hostname: os5.hostname(),
2218
- platform: os5.platform(),
3547
+ minClientProtocol: MIN_CLIENT_PROTOCOL,
3548
+ version,
3549
+ hostname: os6.hostname(),
3550
+ platform: os6.platform(),
2219
3551
  root: sandbox.root,
2220
- user: os5.userInfo().username,
3552
+ user: os6.userInfo().username,
2221
3553
  // On Windows, terminals run PowerShell whatever $SHELL (Git Bash sets one) says.
2222
3554
  shell: isWindows ? interactiveShell().file : process.env.SHELL || null
2223
3555
  }),
2224
3556
  [Routes.fsList]: async (url) => {
2225
3557
  const dir = await sandbox.resolve(url.searchParams.get("path"));
2226
- const names = await fs8.readdir(dir);
2227
- const entries = (await Promise.all(names.map((n) => describe(path6.join(dir, n), n).catch(() => null)))).filter(Boolean);
2228
- 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 };
2229
3561
  },
2230
3562
  [Routes.fsStat]: async (url) => {
2231
3563
  const p = await sandbox.resolve(url.searchParams.get("path"));
2232
- return { path: p, ...await describe(p, path6.basename(p)) };
3564
+ return { path: p, ...await describe(p, path10.basename(p)) };
2233
3565
  },
2234
3566
  [Routes.fsRead]: async (url, req, res) => {
2235
3567
  const p = await sandbox.resolve(url.searchParams.get("path"));
2236
- const stat = await fs8.stat(p);
3568
+ const stat = await fs13.stat(p);
2237
3569
  if (!stat.isFile()) throw new HttpError(400, "not a file");
2238
- res.writeHead(200, { "content-type": "application/octet-stream", "content-length": stat.size });
2239
- 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);
2240
3589
  },
2241
3590
  [Routes.fsWrite]: async (url, req) => {
2242
3591
  const p = await sandbox.resolveChild(url.searchParams.get("path"), { allowMissing: true });
2243
3592
  const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
2244
3593
  try {
2245
- await pipeline2(req, createWriteStream2(tmp));
2246
- await fs8.rename(tmp, p);
3594
+ await pipeline3(req, createWriteStream2(tmp));
3595
+ await fs13.rename(tmp, p);
2247
3596
  } catch (e) {
2248
- await fs8.rm(tmp, { force: true });
3597
+ await fs13.rm(tmp, { force: true });
2249
3598
  throw e;
2250
3599
  }
2251
- return describe(p, path6.basename(p));
3600
+ return describe(p, path10.basename(p));
2252
3601
  },
2253
3602
  [Routes.fsMkdir]: async (url) => {
2254
- 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 });
2255
3604
  return {};
2256
3605
  },
2257
3606
  [Routes.fsRename]: async (url, req) => {
2258
- const { from, to } = await readJson(req);
2259
- 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 }));
2260
3609
  return {};
2261
3610
  },
2262
3611
  [Routes.fsDelete]: async (url) => {
2263
- 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 });
2264
3613
  return {};
2265
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
+ },
2266
3636
  [Routes.gitRepos]: async () => ({ repos: await findRepos(sandbox.root) }),
2267
3637
  [Routes.gitStatus]: async (url) => status(await repoOf(url.searchParams.get("repo"))),
2268
3638
  [Routes.gitDiff]: async (url) => {
@@ -2274,48 +3644,48 @@ function createAgentServer({
2274
3644
  });
2275
3645
  },
2276
3646
  [Routes.gitStage]: async (url, req) => {
2277
- const { repo: r, files } = await readJson(req);
3647
+ const { repo: r, files } = await readJson2(req);
2278
3648
  const repo = await repoOf(r);
2279
3649
  await stage(repo, await filesIn(repo, files));
2280
3650
  return status(repo);
2281
3651
  },
2282
3652
  [Routes.gitUnstage]: async (url, req) => {
2283
- const { repo: r, files } = await readJson(req);
3653
+ const { repo: r, files } = await readJson2(req);
2284
3654
  const repo = await repoOf(r);
2285
3655
  await unstage(repo, await filesIn(repo, files));
2286
3656
  return status(repo);
2287
3657
  },
2288
3658
  [Routes.gitDiscard]: async (url, req) => {
2289
- const { repo: r, files } = await readJson(req);
3659
+ const { repo: r, files } = await readJson2(req);
2290
3660
  const repo = await repoOf(r);
2291
3661
  await discard(repo, await filesIn(repo, files));
2292
3662
  return status(repo);
2293
3663
  },
2294
3664
  [Routes.gitCommit]: async (url, req) => {
2295
- const { repo: r, message } = await readJson(req);
3665
+ const { repo: r, message } = await readJson2(req);
2296
3666
  return commit(await repoOf(r), message);
2297
3667
  },
2298
3668
  [Routes.gitLog]: async (url) => ({
2299
3669
  commits: await log(await repoOf(url.searchParams.get("repo")), parseInt(url.searchParams.get("limit"), 10) || 50)
2300
3670
  }),
2301
3671
  [Routes.gitShow]: async (url) => show(await repoOf(url.searchParams.get("repo")), url.searchParams.get("commit")),
2302
- [Routes.gitPull]: async (url, req) => ({ output: await pull(await repoOf((await readJson(req)).repo)) }),
2303
- [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)) }),
2304
3674
  [Routes.termList]: async () => ({ sessions: sessions.list() }),
2305
3675
  [Routes.portsList]: async () => ({ ports: await listPorts(shellPids()) }),
2306
3676
  [Routes.portsKill]: async (url, req) => {
2307
- const { pid, force } = await readJson(req);
3677
+ const { pid, force } = await readJson2(req);
2308
3678
  const ports = await killListener(pid, { force: force === true, shells: shellPids() });
2309
- if (preview) setTimeout(() => sweepPreviews(ports, true).catch(() => {
3679
+ if (preview2) setTimeout(() => sweepPreviews(ports, true).catch(() => {
2310
3680
  }), killedMs).unref();
2311
3681
  return {};
2312
3682
  },
2313
3683
  [Routes.previewList]: async () => {
2314
- if (!preview) throw new HttpError(404, "previews need a tunnel");
3684
+ if (!preview2) throw new HttpError(404, "previews need a tunnel");
2315
3685
  return previews();
2316
3686
  },
2317
3687
  [Routes.previewOpen]: async (url, req) => {
2318
- const port2 = previewPort((await readJson(req)).port, server.address()?.port);
3688
+ const port2 = previewPort((await readJson2(req)).port, server.address()?.port);
2319
3689
  let proxy = proxies.get(port2);
2320
3690
  if (!proxy) {
2321
3691
  proxy = new InspectingProxy(port2);
@@ -2323,7 +3693,7 @@ function createAgentServer({
2323
3693
  proxies.set(port2, proxy);
2324
3694
  }
2325
3695
  try {
2326
- await preview.forward(proxy.port);
3696
+ await preview2.forward(proxy.port);
2327
3697
  } catch (e) {
2328
3698
  throw new HttpError(502, e.message);
2329
3699
  }
@@ -2331,25 +3701,34 @@ function createAgentServer({
2331
3701
  },
2332
3702
  [Routes.previewClose]: async (url) => {
2333
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
+ }
2334
3708
  const proxy = proxies.get(port2);
2335
- if (proxy) await preview.unforward(proxy.port);
3709
+ if (proxy) await preview2.unforward(proxy.port);
2336
3710
  return previews();
2337
3711
  },
2338
3712
  [Routes.previewShare]: async (url, req) => {
2339
- const { port: value, hours } = await readJson(req);
3713
+ const { port: value, hours } = await readJson2(req);
2340
3714
  const proxy = previewed(previewPort(value, server.address()?.port));
2341
3715
  if (!Number.isInteger(hours) || hours < 1 || hours > 720) throw new HttpError(400, "hours must be 1-720");
2342
3716
  try {
2343
- await preview.share(proxy.port, hours);
3717
+ await preview2.share(proxy.port, hours);
2344
3718
  } catch (e) {
2345
3719
  throw new HttpError(502, e.message);
2346
3720
  }
2347
3721
  return previews();
2348
3722
  },
2349
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
+ }
2350
3729
  const proxy = proxies.get(previewPort(url.searchParams.get("port"), server.address()?.port));
2351
3730
  try {
2352
- if (proxy) await preview.unshare(proxy.port);
3731
+ if (proxy) await preview2.unshare(proxy.port);
2353
3732
  } catch (e) {
2354
3733
  throw new HttpError(502, e.message);
2355
3734
  }
@@ -2359,9 +3738,9 @@ function createAgentServer({
2359
3738
  requests: inspected(url.searchParams.get("port")).list(parseInt(url.searchParams.get("after"), 10) || 0)
2360
3739
  }),
2361
3740
  [Routes.previewRequest]: async (url) => {
2362
- const entry2 = inspected(url.searchParams.get("port")).get(parseInt(url.searchParams.get("id"), 10));
2363
- if (!entry2) throw new HttpError(404, "no such request (only the latest 200 are kept)");
2364
- 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);
2365
3744
  },
2366
3745
  [Routes.previewRequestsClear]: async (url) => {
2367
3746
  inspected(url.searchParams.get("port")).clear();
@@ -2372,6 +3751,11 @@ function createAgentServer({
2372
3751
  if (!status2) throw new HttpError(404, "this agent runs without a tunnel");
2373
3752
  return status2;
2374
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
+ },
2375
3759
  [Routes.tunnelToken]: async () => {
2376
3760
  const issued = await handoff.issue();
2377
3761
  if (!issued) throw new HttpError(404, "this tunnel is anonymous");
@@ -2384,8 +3768,43 @@ function createAgentServer({
2384
3768
  const [plugin, codexPlugin] = await Promise.all([claude && hasPlugin("claude"), codex && hasPlugin("codex")]);
2385
3769
  return { claude, plugin: Boolean(plugin), codex, codexPlugin: Boolean(codexPlugin), codexModels: codex ? await codexModels() : [] };
2386
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
+ },
2387
3806
  [Routes.agentsLaunch]: async (url, req) => {
2388
- 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);
2389
3808
  if (approvalPolicy !== void 0 && !["on-request", "never"].includes(approvalPolicy)) {
2390
3809
  throw new HttpError(400, "approvalPolicy must be on-request or never");
2391
3810
  }
@@ -2397,7 +3816,7 @@ function createAgentServer({
2397
3816
  } catch (e) {
2398
3817
  throw new HttpError(400, e.message);
2399
3818
  }
2400
- 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");
2401
3820
  if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
2402
3821
  throw new HttpError(400, "invalid model");
2403
3822
  }
@@ -2418,8 +3837,8 @@ function createAgentServer({
2418
3837
  if (!await hasPlugin(agent)) {
2419
3838
  throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
2420
3839
  }
2421
- const id = resume ?? randomUUID4();
2422
- 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 });
2423
3842
  const argv2 = [
2424
3843
  launcher.command,
2425
3844
  ...launcher.args({
@@ -2440,14 +3859,14 @@ function createAgentServer({
2440
3859
  return { terminal: term.id, session: agents.launched({ id, terminal: term.id, cwd: dir, agent }) };
2441
3860
  },
2442
3861
  [Routes.agentsPrompt]: async (url, req) => {
2443
- const { session: id, text, attachments } = await readJson(req);
3862
+ const { session: id, text, attachments } = await readJson2(req);
2444
3863
  if (typeof text !== "string" || !text.trim() || text.length > 1e5) {
2445
3864
  throw new HttpError(400, "text must be a non-empty string");
2446
3865
  }
2447
3866
  const { term } = typingTarget(id, { whileWaiting: false });
2448
3867
  const paths = await attachmentPaths(attachments);
2449
3868
  const typed = typeable([text.trim(), ...paths].join("\n"));
2450
- const prompt = randomUUID4();
3869
+ const prompt = randomUUID5();
2451
3870
  agents.prompted(id, { id: prompt, text: typed, paths });
2452
3871
  await typeInto(term, typed);
2453
3872
  return { prompt };
@@ -2467,23 +3886,30 @@ function createAgentServer({
2467
3886
  return { id, name: saved, type, size };
2468
3887
  },
2469
3888
  [Routes.agentsKeys]: async (url, req) => {
2470
- const { session: id, action, command } = await readJson(req);
3889
+ const { session: id, action, command, args } = await readJson2(req);
2471
3890
  const allowed = agents.session(typeof id === "string" ? id : "")?.capabilities?.keys;
2472
- if (allowed && !allowed.includes(action === "command" ? command : action)) {
3891
+ if (action !== "command" && allowed && !allowed.includes(action)) {
2473
3892
  throw new HttpError(400, `this session takes ${allowed.join(", ")}`);
2474
3893
  }
2475
3894
  if (action === "command") {
2476
- if (!COMMANDS.includes(command)) throw new HttpError(400, `command must be one of ${COMMANDS.join(", ")}`);
2477
- 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));
2478
3903
  } else if (Object.hasOwn(KEYS, action)) {
2479
3904
  typingTarget(id, { whileWaiting: true }).term.write(KEYS[action]);
3905
+ if (action === "interrupt") agents.interrupted(id);
2480
3906
  } else {
2481
3907
  throw new HttpError(400, `action must be one of ${[...Object.keys(KEYS), "command"].join(", ")}`);
2482
3908
  }
2483
3909
  return {};
2484
3910
  },
2485
3911
  [Routes.agentsStop]: async (url, req) => {
2486
- const { session: id } = await readJson(req);
3912
+ const { session: id } = await readJson2(req);
2487
3913
  const { term } = typingTarget(id, { whileWaiting: true });
2488
3914
  term.kill();
2489
3915
  setTimeout(() => {
@@ -2492,7 +3918,7 @@ function createAgentServer({
2492
3918
  return {};
2493
3919
  },
2494
3920
  [Routes.agentsForget]: async (url, req) => {
2495
- const { session: id } = await readJson(req);
3921
+ const { session: id } = await readJson2(req);
2496
3922
  const session = typeof id === "string" ? agents.session(id) : null;
2497
3923
  if (!session) throw new HttpError(404, "no such session");
2498
3924
  if (session.state !== "ended" && session.terminal && sessions.get(session.terminal)) {
@@ -2502,11 +3928,11 @@ function createAgentServer({
2502
3928
  return {};
2503
3929
  },
2504
3930
  [Routes.agentsAnswer]: async (url, req) => {
2505
- agents.answer(await readJson(req));
3931
+ agents.answer(await readJson2(req));
2506
3932
  return {};
2507
3933
  },
2508
3934
  [Routes.pushDeviceSet]: async (url, req) => {
2509
- push2.register(await readJson(req));
3935
+ push2.register(await readJson2(req));
2510
3936
  return {};
2511
3937
  },
2512
3938
  [Routes.pushDeviceDelete]: async (url) => {
@@ -2514,7 +3940,7 @@ function createAgentServer({
2514
3940
  return {};
2515
3941
  },
2516
3942
  [Routes.pushTest]: async (url, req) => {
2517
- await push2.test((await readJson(req)).token);
3943
+ await push2.test((await readJson2(req)).token);
2518
3944
  return {};
2519
3945
  },
2520
3946
  [Routes.termKill]: async (url) => {
@@ -2524,7 +3950,7 @@ function createAgentServer({
2524
3950
  return {};
2525
3951
  }
2526
3952
  };
2527
- const server = http2.createServer(async (req, res) => {
3953
+ const server = http3.createServer(async (req, res) => {
2528
3954
  const url = new URL(req.url, "http://localhost");
2529
3955
  if (req.method === "GET" && url.pathname === "/") {
2530
3956
  res.writeHead(200, {
@@ -2552,14 +3978,16 @@ function createAgentServer({
2552
3978
  if (!hookAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
2553
3979
  plugins[url.searchParams.get("agent") || "claude"] = { installed: true, checkedAt: Date.now() };
2554
3980
  const terminal = url.searchParams.get("terminal");
3981
+ const app = url.searchParams.get("app");
2555
3982
  const closed = new AbortController();
2556
3983
  res.on("close", () => {
2557
3984
  if (!res.writableFinished) closed.abort();
2558
3985
  });
2559
3986
  try {
2560
- const output = await agents.handle(await readJson(req), {
3987
+ const output = await agents.handle(await readJson2(req), {
2561
3988
  agent: url.searchParams.get("agent") || "claude",
2562
3989
  terminal: terminal && sessions.get(terminal) ? terminal : null,
3990
+ app: app && app.length <= 1024 && /\.app$/.test(app) ? app : null,
2563
3991
  signal: closed.signal
2564
3992
  });
2565
3993
  if (res.destroyed) return;
@@ -2638,11 +4066,22 @@ function createAgentServer({
2638
4066
  } catch {
2639
4067
  return;
2640
4068
  }
2641
- 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);
2642
4081
  });
2643
- ws.on("close", () => agents.off("message", send));
2644
4082
  }
2645
4083
  server.on("close", () => {
4084
+ installs?.close();
2646
4085
  clearInterval(sweep);
2647
4086
  clearInterval(previewTimer);
2648
4087
  sessions.killAll();
@@ -2651,18 +4090,19 @@ function createAgentServer({
2651
4090
  server.killAllSessions = () => sessions.killAll();
2652
4091
  server.agents = agents;
2653
4092
  server.clientInForeground = clientInForeground;
4093
+ server.clientShows = clientShows;
2654
4094
  server.push = push2;
2655
4095
  server.agentsUploadPath = (id) => uploadPath(id);
2656
4096
  return server;
2657
4097
  }
2658
4098
 
2659
4099
  // src/devtunnel.js
2660
- import { execFile as execFile7 } from "node:child_process";
4100
+ import { execFile as execFile9 } from "node:child_process";
2661
4101
  import { createHash } from "node:crypto";
2662
- import fs9 from "node:fs";
2663
- import path7 from "node:path";
4102
+ import fs14 from "node:fs";
4103
+ import path11 from "node:path";
2664
4104
  import { promisify as promisify3 } from "node:util";
2665
- var run4 = promisify3(execFile7);
4105
+ var run5 = promisify3(execFile9);
2666
4106
  var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
2667
4107
  var BUILDS = {
2668
4108
  "darwin-arm64": "osx-arm64-devtunnel",
@@ -2691,11 +4131,11 @@ async function ensureDevtunnel({ dir, progress }) {
2691
4131
  `No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
2692
4132
  );
2693
4133
  }
2694
- const bin = path7.join(dir, isWindows ? "devtunnel.exe" : "devtunnel");
2695
- 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");
2696
4136
  let meta = {};
2697
4137
  try {
2698
- meta = JSON.parse(fs9.readFileSync(metaFile, "utf8"));
4138
+ meta = JSON.parse(fs14.readFileSync(metaFile, "utf8"));
2699
4139
  } catch {
2700
4140
  }
2701
4141
  const installed = findExecutable(bin) !== null;
@@ -2706,7 +4146,7 @@ async function ensureDevtunnel({ dir, progress }) {
2706
4146
  const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
2707
4147
  const lastModified = head.headers.get("last-modified");
2708
4148
  if (head.ok && lastModified && lastModified === meta.lastModified) {
2709
- fs9.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
4149
+ fs14.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
2710
4150
  return bin;
2711
4151
  }
2712
4152
  } catch {
@@ -2721,12 +4161,12 @@ async function ensureDevtunnel({ dir, progress }) {
2721
4161
  return bin;
2722
4162
  }
2723
4163
  async function download(url, bin, metaFile, progress) {
2724
- fs9.mkdirSync(path7.dirname(bin), { recursive: true, mode: 448 });
4164
+ fs14.mkdirSync(path11.dirname(bin), { recursive: true, mode: 448 });
2725
4165
  const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
2726
4166
  if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
2727
4167
  const total = Number(res.headers.get("content-length")) || 0;
2728
4168
  const tmp = `${bin}.${process.pid}.download`;
2729
- const out = fs9.createWriteStream(tmp, { mode: 493 });
4169
+ const out = fs14.createWriteStream(tmp, { mode: 493 });
2730
4170
  const md5 = createHash("md5");
2731
4171
  let received = 0;
2732
4172
  try {
@@ -2739,22 +4179,22 @@ async function download(url, bin, metaFile, progress) {
2739
4179
  await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
2740
4180
  if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
2741
4181
  await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
2742
- fs9.chmodSync(tmp, 493);
2743
- fs9.renameSync(tmp, bin);
4182
+ fs14.chmodSync(tmp, 493);
4183
+ fs14.renameSync(tmp, bin);
2744
4184
  } catch (e) {
2745
4185
  out.destroy();
2746
- fs9.rmSync(tmp, { force: true });
4186
+ fs14.rmSync(tmp, { force: true });
2747
4187
  throw e;
2748
4188
  }
2749
- 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() }));
2750
4190
  }
2751
4191
  async function verify(file, md5, expectedMd5) {
2752
4192
  if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
2753
4193
  if (isWindows) return verifyAuthenticode(file);
2754
4194
  if (process.platform !== "darwin") return;
2755
4195
  try {
2756
- await run4("codesign", ["--verify", "--strict", file]);
2757
- const { stderr } = await run4("codesign", ["-dv", file]);
4196
+ await run5("codesign", ["--verify", "--strict", file]);
4197
+ const { stderr } = await run5("codesign", ["-dv", file]);
2758
4198
  if (!stderr.includes(`TeamIdentifier=${MICROSOFT_TEAM_ID}`)) throw new Error("not signed by Microsoft");
2759
4199
  } catch (e) {
2760
4200
  throw new Error(`devtunnel download failed its signature check: ${e.message}`);
@@ -2765,7 +4205,7 @@ async function verifyAuthenticode(file) {
2765
4205
  let status2;
2766
4206
  let signer;
2767
4207
  try {
2768
- const { stdout } = await run4("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
4208
+ const { stdout } = await run5("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
2769
4209
  env: { ...process.env, CODEREMOTE_VERIFY: file }
2770
4210
  });
2771
4211
  [status2, signer] = stdout.trim().split("|");
@@ -2778,51 +4218,52 @@ async function verifyAuthenticode(file) {
2778
4218
  }
2779
4219
 
2780
4220
  // src/hook.js
2781
- import { randomBytes as randomBytes2 } from "node:crypto";
2782
- import fs10 from "node:fs";
2783
- import http3 from "node:http";
2784
- 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";
2785
4226
  var CONNECT_TIMEOUT = 1e3;
2786
4227
  var EVENT_TIMEOUT = 5e3;
2787
4228
  function ensureHookSecret(dir) {
2788
- const file = path8.join(dir, "hook-secret");
4229
+ const file = path12.join(dir, "hook-secret");
2789
4230
  try {
2790
- const secret2 = fs10.readFileSync(file, "utf8").trim();
4231
+ const secret2 = fs15.readFileSync(file, "utf8").trim();
2791
4232
  if (secret2) return secret2;
2792
4233
  } catch {
2793
4234
  }
2794
- fs10.mkdirSync(dir, { recursive: true, mode: 448 });
2795
- const secret = randomBytes2(32).toString("base64url");
2796
- 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 });
2797
4238
  return secret;
2798
4239
  }
2799
4240
  function writeHookLauncher(dir, { node = process.execPath, script }) {
2800
- const bin = path8.join(dir, "bin");
2801
- fs10.mkdirSync(bin, { recursive: true, mode: 448 });
4241
+ const bin = path12.join(dir, "bin");
4242
+ fs15.mkdirSync(bin, { recursive: true, mode: 448 });
2802
4243
  const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
2803
- const file = path8.join(bin, "hook");
2804
- fs10.writeFileSync(file, `#!/bin/sh
4244
+ const file = path12.join(bin, "hook");
4245
+ fs15.writeFileSync(file, `#!/bin/sh
2805
4246
  exec ${quote(node)} ${quote(script)} hook "$@"
2806
4247
  `, { mode: 493 });
2807
- fs10.chmodSync(file, 493);
2808
- 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 });
2809
4250
  return file;
2810
4251
  }
2811
4252
  function readHookSecret(dir) {
2812
4253
  try {
2813
- return fs10.readFileSync(path8.join(dir, "hook-secret"), "utf8").trim() || null;
4254
+ return fs15.readFileSync(path12.join(dir, "hook-secret"), "utf8").trim() || null;
2814
4255
  } catch {
2815
4256
  return null;
2816
4257
  }
2817
4258
  }
2818
4259
  function real(p) {
2819
4260
  try {
2820
- return fs10.realpathSync.native(p);
4261
+ return fs15.realpathSync.native(p);
2821
4262
  } catch {
2822
- return path8.resolve(p);
4263
+ return path12.resolve(p);
2823
4264
  }
2824
4265
  }
2825
- 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);
2826
4267
  function hookTarget(agents, cwd, env = process.env) {
2827
4268
  const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
2828
4269
  if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
@@ -2831,23 +4272,46 @@ function hookTarget(agents, cwd, env = process.env) {
2831
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];
2832
4273
  return match ? { port: match.port, terminal: null } : null;
2833
4274
  }
2834
- 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 }) {
2835
4298
  return new Promise((resolve) => {
2836
4299
  const query = new URLSearchParams({ agent });
2837
4300
  if (terminal) query.set("terminal", terminal);
2838
- const req = http3.request({
4301
+ if (app) query.set("app", app);
4302
+ const req = http4.request({
2839
4303
  host: "127.0.0.1",
2840
4304
  port: port2,
2841
4305
  method: "POST",
2842
4306
  path: `/api/agents/hook?${query}`,
2843
4307
  headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body), [HOOK_SECRET_HEADER]: secret }
2844
4308
  });
2845
- const fail = () => {
4309
+ const fail2 = () => {
2846
4310
  req.destroy();
2847
4311
  resolve("");
2848
4312
  };
2849
- const connect = setTimeout(fail, CONNECT_TIMEOUT);
2850
- const overall = wait ? null : setTimeout(fail, EVENT_TIMEOUT);
4313
+ const connect = setTimeout(fail2, CONNECT_TIMEOUT);
4314
+ const overall = wait ? null : setTimeout(fail2, EVENT_TIMEOUT);
2851
4315
  req.on("socket", (socket) => socket.once("connect", () => clearTimeout(connect)));
2852
4316
  req.on("error", () => resolve(""));
2853
4317
  req.on("response", (res) => {
@@ -2862,7 +4326,7 @@ function deliverHook({ port: port2, terminal, agent, secret, body, wait }) {
2862
4326
  req.end(body);
2863
4327
  });
2864
4328
  }
2865
- 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 }) {
2866
4330
  if (!secret) return "";
2867
4331
  let event;
2868
4332
  try {
@@ -2873,16 +4337,23 @@ async function runHook({ input, agents, secret, agent = "claude", env = process.
2873
4337
  const target = hookTarget(agents, event?.cwd, env);
2874
4338
  if (!target) return "";
2875
4339
  const wait = event.hook_event_name === "PermissionRequest";
2876
- 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 });
2877
4342
  return wait ? out.trim() : "";
2878
4343
  }
2879
4344
 
2880
4345
  // src/preview.js
2881
- import { spawn as spawn2 } from "node:child_process";
4346
+ import { spawn as spawn4 } from "node:child_process";
2882
4347
 
2883
4348
  // src/tunnels.js
2884
4349
  var TUNNEL_LIMIT = 10;
2885
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
+ }
2886
4357
  var base = (id) => String(id).split(".")[0];
2887
4358
  async function listTunnels(devtunnel2) {
2888
4359
  const res = await devtunnel2("list", "--limit", "100", "--json");
@@ -3014,8 +4485,8 @@ var PreviewTunnel = class {
3014
4485
  "--json"
3015
4486
  );
3016
4487
  if (!res.ok) throw new Error(`devtunnel access create failed: ${res.out.trim()}`);
3017
- const entries = JSON.parse(res.out.slice(res.out.indexOf("{"))).accessControlEntries ?? [];
3018
- 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);
3019
4490
  this.#shares.set(port2, Number.isFinite(until) ? until : Date.now() + hours * 36e5);
3020
4491
  });
3021
4492
  }
@@ -3125,7 +4596,7 @@ ${created.out}`);
3125
4596
  }
3126
4597
  #startHost() {
3127
4598
  return new Promise((resolve, reject) => {
3128
- 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"] });
3129
4600
  const urls = /* @__PURE__ */ new Map();
3130
4601
  let log2 = "";
3131
4602
  const timer = setTimeout(() => {
@@ -3160,40 +4631,40 @@ ${log2}`));
3160
4631
  };
3161
4632
 
3162
4633
  // src/push-store.js
3163
- import fs11 from "node:fs";
3164
- import path9 from "node:path";
4634
+ import fs16 from "node:fs";
4635
+ import path13 from "node:path";
3165
4636
  function pushStore(dir, port2) {
3166
- const file = path9.join(dir, `${port2}.json`);
4637
+ const file = path13.join(dir, `${port2}.json`);
3167
4638
  return {
3168
4639
  load() {
3169
4640
  try {
3170
- const devices = JSON.parse(fs11.readFileSync(file, "utf8"));
4641
+ const devices = JSON.parse(fs16.readFileSync(file, "utf8"));
3171
4642
  return Array.isArray(devices) ? devices : [];
3172
4643
  } catch {
3173
4644
  return [];
3174
4645
  }
3175
4646
  },
3176
4647
  save(devices) {
3177
- fs11.mkdirSync(dir, { recursive: true, mode: 448 });
3178
- 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 });
3179
4650
  }
3180
4651
  };
3181
4652
  }
3182
4653
 
3183
4654
  // src/registry.js
3184
- import fs12 from "node:fs";
3185
- import path10 from "node:path";
4655
+ import fs17 from "node:fs";
4656
+ import path14 from "node:path";
3186
4657
  var AgentRegistry = class {
3187
4658
  constructor(dir) {
3188
4659
  this.dir = dir;
3189
4660
  }
3190
4661
  register({ port: port2, root: root2, url }) {
3191
- fs12.mkdirSync(this.dir, { recursive: true, mode: 448 });
4662
+ fs17.mkdirSync(this.dir, { recursive: true, mode: 448 });
3192
4663
  const file = this.#file(port2);
3193
- 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 });
3194
4665
  const cleanup = () => {
3195
4666
  try {
3196
- 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);
3197
4668
  } catch {
3198
4669
  }
3199
4670
  };
@@ -3204,25 +4675,25 @@ var AgentRegistry = class {
3204
4675
  list() {
3205
4676
  let names = [];
3206
4677
  try {
3207
- names = fs12.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
4678
+ names = fs17.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
3208
4679
  } catch {
3209
4680
  return [];
3210
4681
  }
3211
4682
  const agents = [];
3212
4683
  for (const name of names) {
3213
- const file = path10.join(this.dir, name);
4684
+ const file = path14.join(this.dir, name);
3214
4685
  try {
3215
- const agent = JSON.parse(fs12.readFileSync(file, "utf8"));
4686
+ const agent = JSON.parse(fs17.readFileSync(file, "utf8"));
3216
4687
  process.kill(agent.pid, 0);
3217
4688
  agents.push(agent);
3218
4689
  } catch {
3219
- fs12.rmSync(file, { force: true });
4690
+ fs17.rmSync(file, { force: true });
3220
4691
  }
3221
4692
  }
3222
4693
  return agents.sort((a, b) => a.port - b.port);
3223
4694
  }
3224
4695
  #file(port2) {
3225
- return path10.join(this.dir, `${port2}.json`);
4696
+ return path14.join(this.dir, `${port2}.json`);
3226
4697
  }
3227
4698
  };
3228
4699
 
@@ -3236,6 +4707,13 @@ function parse(out) {
3236
4707
  function machineName(description) {
3237
4708
  return (description || "").replace(/^(coderemote|vsremote)\s+/, "").replace(/\.local\b/, "") || null;
3238
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
+ }
3239
4717
  function machine(tunnel, current) {
3240
4718
  return {
3241
4719
  id: tunnel.tunnelId,
@@ -3247,12 +4725,12 @@ function machine(tunnel, current) {
3247
4725
  current
3248
4726
  };
3249
4727
  }
3250
- function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview }) {
4728
+ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: anonymous2, preview: preview2 }) {
3251
4729
  let cached = null;
3252
- return async () => {
4730
+ const status2 = async () => {
3253
4731
  const id = tunnelId();
3254
4732
  if (!id) return null;
3255
- 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 };
3256
4734
  const [own, listed] = await Promise.all([
3257
4735
  devtunnel2("show", id, "--json"),
3258
4736
  // Every tunnel, previews' too: they all count against the account's limit.
@@ -3283,17 +4761,53 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
3283
4761
  downloaded: tunnel.downloadTotal ?? null
3284
4762
  },
3285
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),
3286
4766
  // null when the account couldn't be listed
3287
4767
  quota: all && { used: all.length, limit: TUNNEL_LIMIT }
3288
4768
  };
3289
4769
  cached = { at: Date.now(), value };
3290
- 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 };
3291
4805
  };
3292
4806
  }
3293
4807
 
3294
4808
  // src/update.js
3295
- import fs13 from "node:fs";
3296
- import path11 from "node:path";
4809
+ import fs18 from "node:fs";
4810
+ import path15 from "node:path";
3297
4811
  var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
3298
4812
  var CHECK_INTERVAL_MS = 24 * 36e5;
3299
4813
  var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
@@ -3317,10 +4831,10 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
3317
4831
  }
3318
4832
  }
3319
4833
  async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
3320
- const file = path11.join(dir, "update.json");
4834
+ const file = path15.join(dir, "update.json");
3321
4835
  let cache = {};
3322
4836
  try {
3323
- cache = JSON.parse(fs13.readFileSync(file, "utf8"));
4837
+ cache = JSON.parse(fs18.readFileSync(file, "utf8"));
3324
4838
  } catch {
3325
4839
  }
3326
4840
  let latest = cache.latest;
@@ -3329,8 +4843,8 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
3329
4843
  if (fetched) {
3330
4844
  latest = fetched;
3331
4845
  try {
3332
- fs13.mkdirSync(dir, { recursive: true, mode: 448 });
3333
- 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 }));
3334
4848
  } catch {
3335
4849
  }
3336
4850
  }
@@ -3338,12 +4852,12 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
3338
4852
  return isNewer(latest, current) ? latest : null;
3339
4853
  }
3340
4854
  function installCommand(installedAt) {
3341
- const p = installedAt.split(path11.sep).join("/");
4855
+ const p = installedAt.split(path15.sep).join("/");
3342
4856
  const runtime = p.match(/^(.*\/runtime\/node)\/(lib\/)?node_modules\/coderemote\//);
3343
4857
  if (runtime) {
3344
4858
  const prefix = installedAt.slice(0, runtime[1].length);
3345
- 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")];
3346
- 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"]];
3347
4861
  }
3348
4862
  if (p.includes("/Cellar/coderemote/")) return ["brew", ["upgrade", "coderemote"]];
3349
4863
  if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
@@ -3355,7 +4869,7 @@ function installCommand(installedAt) {
3355
4869
  // package.json
3356
4870
  var package_default = {
3357
4871
  name: "coderemote",
3358
- version: "0.3.8",
4872
+ version: "0.3.9",
3359
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.",
3360
4874
  keywords: [
3361
4875
  "remote",
@@ -3678,28 +5192,28 @@ ${log2}`));
3678
5192
  }
3679
5193
 
3680
5194
  // bin/coderemote.js
3681
- var run5 = promisify4(execFile8);
3682
- var CONFIG_DIR = path12.join(os6.homedir(), ".coderemote");
3683
- var LEGACY_CONFIG_DIR = path12.join(os6.homedir(), ".vsremote");
3684
- if (!fs14.existsSync(CONFIG_DIR) && fs14.existsSync(LEGACY_CONFIG_DIR)) fs14.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
3685
- var CONFIG_FILE = path12.join(CONFIG_DIR, "config.json");
3686
- var PUSH_DIR = path12.join(CONFIG_DIR, "push");
3687
- 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");
3688
5202
  var DEFAULT_PORT = 31580;
3689
5203
  var VERSION = package_default.version;
3690
- var PACKAGED = path12.basename(path12.dirname(fileURLToPath(import.meta.url))) === "dist";
5204
+ var PACKAGED = path16.basename(path16.dirname(fileURLToPath(import.meta.url))) === "dist";
3691
5205
  var spinner = new Spinner();
3692
5206
  function loadConfig() {
3693
5207
  let cfg2 = {};
3694
5208
  try {
3695
- cfg2 = JSON.parse(fs14.readFileSync(CONFIG_FILE, "utf8"));
5209
+ cfg2 = JSON.parse(fs19.readFileSync(CONFIG_FILE, "utf8"));
3696
5210
  } catch {
3697
5211
  }
3698
5212
  if (!cfg2.token) {
3699
- cfg2.token = randomBytes3(32).toString("base64url");
5213
+ cfg2.token = randomBytes4(32).toString("base64url");
3700
5214
  saveConfig(cfg2);
3701
- fs14.rmSync(PUSH_DIR, { recursive: true, force: true });
3702
- 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 });
3703
5217
  }
3704
5218
  if (cfg2.tunnelId && !cfg2.tunnels) {
3705
5219
  cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
@@ -3715,8 +5229,8 @@ function stopAgent(pid) {
3715
5229
  }
3716
5230
  }
3717
5231
  function saveConfig(cfg2) {
3718
- fs14.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
3719
- 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 });
3720
5234
  }
3721
5235
  function portFree(port2) {
3722
5236
  return new Promise((resolve) => {
@@ -3725,20 +5239,22 @@ function portFree(port2) {
3725
5239
  probe.listen(port2, "127.0.0.1", () => probe.close(() => resolve(true)));
3726
5240
  });
3727
5241
  }
3728
- function listen(token, port2, root2, tunnelToken, preview, status2) {
5242
+ function listen(token, port2, root2, tunnelToken, preview2, status2, remover) {
3729
5243
  const hookSecret = ensureHookSecret(CONFIG_DIR);
3730
- writeHookLauncher(CONFIG_DIR, { script: fs14.realpathSync(process.argv[1]) });
5244
+ writeHookLauncher(CONFIG_DIR, { script: fs19.realpathSync(process.argv[1]) });
3731
5245
  const server = createAgentServer({
3732
5246
  token,
3733
5247
  root: root2,
3734
5248
  tunnelToken,
3735
- preview,
5249
+ preview: preview2,
3736
5250
  tunnelStatus: status2,
5251
+ tunnelDelete: remover,
3737
5252
  hookSecret,
3738
5253
  // This very CLI, so coding agents in the app's terminals report through the same version.
3739
- hookBin: fs14.realpathSync(process.argv[1]),
5254
+ hookBin: fs19.realpathSync(process.argv[1]),
3740
5255
  pushStore: pushStore(PUSH_DIR, port2),
3741
- uploadsDir: UPLOADS_DIR
5256
+ uploadsDir: UPLOADS_DIR,
5257
+ version: VERSION
3742
5258
  });
3743
5259
  return new Promise((resolve, reject) => {
3744
5260
  server.once("error", reject);
@@ -3749,7 +5265,7 @@ var DEVTUNNEL = "devtunnel";
3749
5265
  async function resolveDevtunnel() {
3750
5266
  let installing = false;
3751
5267
  DEVTUNNEL = await ensureDevtunnel({
3752
- dir: path12.join(CONFIG_DIR, "bin"),
5268
+ dir: path16.join(CONFIG_DIR, "bin"),
3753
5269
  progress: (detail2) => {
3754
5270
  if (!installing) {
3755
5271
  installing = true;
@@ -3762,7 +5278,7 @@ async function resolveDevtunnel() {
3762
5278
  }
3763
5279
  async function devtunnel(...args) {
3764
5280
  try {
3765
- const { stdout } = await run5(DEVTUNNEL, args);
5281
+ const { stdout } = await run6(DEVTUNNEL, args);
3766
5282
  return { ok: true, out: stdout };
3767
5283
  } catch (e) {
3768
5284
  return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
@@ -3776,10 +5292,9 @@ function loginArgs(opts = rest) {
3776
5292
  }
3777
5293
  function interactiveLogin(args) {
3778
5294
  return new Promise((resolve) => {
3779
- spawn3(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
5295
+ spawn5(DEVTUNNEL, args, { stdio: "inherit" }).on("exit", resolve);
3780
5296
  });
3781
5297
  }
3782
- var LOGIN_NEEDED = /not logged in|login token expired|login required|please log ?in/i;
3783
5298
  async function login(reason) {
3784
5299
  spinner.stop();
3785
5300
  console.error(`Dev tunnels: ${reason.trim()}`);
@@ -3795,12 +5310,11 @@ async function login(reason) {
3795
5310
  }
3796
5311
  async function tunnelUser() {
3797
5312
  let who = await devtunnel("user", "show");
3798
- if (!who.ok || LOGIN_NEEDED.test(who.out)) {
5313
+ if (!signedInUser(who)) {
3799
5314
  await login(who.out || "not logged in");
3800
5315
  who = await devtunnel("user", "show");
3801
5316
  }
3802
- const m = who.out.match(/Logged in as (\S+) using (\S+?)\.?\s*$/m);
3803
- return m ? `${m[1]} (${m[2]})` : who.out.trim();
5317
+ return signedInUser(who) ?? who.out.trim();
3804
5318
  }
3805
5319
  function applyAccess(tunnelId, anonymous2) {
3806
5320
  return anonymous2 ? devtunnel("access", "create", tunnelId, "--anonymous") : devtunnel("access", "reset", tunnelId);
@@ -3820,7 +5334,7 @@ ${access.out}`);
3820
5334
  if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
3821
5335
  ${updated.out}`);
3822
5336
  }
3823
- const name = port2 === DEFAULT_PORT ? os6.hostname() : `${os6.hostname()} :${port2}`;
5337
+ const name = port2 === DEFAULT_PORT ? os7.hostname() : `${os7.hostname()} :${port2}`;
3824
5338
  const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
3825
5339
  if (anonymous2) createArgs.push("--allow-anonymous");
3826
5340
  let created = await devtunnel(...createArgs);
@@ -3862,7 +5376,7 @@ ${res.out}`);
3862
5376
  }
3863
5377
  function hostTunnel(tunnelId, port2) {
3864
5378
  return new Promise((resolve, reject) => {
3865
- const child = spawn3(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
5379
+ const child = spawn5(DEVTUNNEL, ["host", tunnelId], { stdio: ["ignore", "pipe", "pipe"] });
3866
5380
  let log2 = "";
3867
5381
  const onData = (d) => {
3868
5382
  log2 += d;
@@ -3966,9 +5480,9 @@ var flag = (name) => {
3966
5480
  };
3967
5481
  var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
3968
5482
  var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
3969
- var root = path12.resolve(flag("--root") ?? process.cwd());
5483
+ var root = path16.resolve(flag("--root") ?? process.cwd());
3970
5484
  var cfg = loadConfig();
3971
- var registry = new AgentRegistry(path12.join(CONFIG_DIR, "agents"));
5485
+ var registry = new AgentRegistry(path16.join(CONFIG_DIR, "agents"));
3972
5486
  async function start({ tunnel }) {
3973
5487
  const agents = registry.list();
3974
5488
  if (flag("--port")) {
@@ -4000,10 +5514,10 @@ async function start({ tunnel }) {
4000
5514
  let server;
4001
5515
  let tunnelId;
4002
5516
  const tunnelToken = async () => tunnel && !anonymous && tunnelId ? connectToken(tunnelId) : null;
4003
- const preview = tunnel ? new PreviewTunnel({
5517
+ const preview2 = tunnel ? new PreviewTunnel({
4004
5518
  devtunnel,
4005
5519
  command: () => DEVTUNNEL,
4006
- description: `coderemote preview ${os6.hostname()}`,
5520
+ description: `coderemote preview ${os7.hostname()}`,
4007
5521
  leftover: cfg.previewTunnels?.[port],
4008
5522
  remember: (id) => {
4009
5523
  cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
@@ -4013,10 +5527,19 @@ async function start({ tunnel }) {
4013
5527
  mintToken: connectToken
4014
5528
  }) : null;
4015
5529
  let account;
4016
- 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;
4017
5540
  spinner.start("Starting agent");
4018
5541
  try {
4019
- server = await listen(cfg.token, port, root, tunnelToken, preview, status2);
5542
+ server = await listen(cfg.token, port, root, tunnelToken, preview2, status2, remover);
4020
5543
  } catch (e) {
4021
5544
  spinner.fail();
4022
5545
  if (e.code !== "EADDRINUSE") throw e;
@@ -4053,7 +5576,7 @@ async function start({ tunnel }) {
4053
5576
  registry.register({ port, root, url });
4054
5577
  const shutdown = async () => {
4055
5578
  child?.kill();
4056
- await preview?.close();
5579
+ await preview2?.close();
4057
5580
  await server.killAllSessions();
4058
5581
  server.close();
4059
5582
  process.exit(0);
@@ -4063,7 +5586,7 @@ async function start({ tunnel }) {
4063
5586
  process.on("SIGHUP", shutdown);
4064
5587
  process.on("exit", () => {
4065
5588
  child?.kill();
4066
- preview?.stop();
5589
+ preview2?.stop();
4067
5590
  });
4068
5591
  return { url, access };
4069
5592
  }
@@ -4186,7 +5709,7 @@ function currentAgent() {
4186
5709
  const agents = registry.list();
4187
5710
  if (flag("--port")) return agents.find((a) => a.port === port);
4188
5711
  const here = process.cwd();
4189
- 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);
4190
5713
  return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
4191
5714
  }
4192
5715
  async function previewPorts(agent, given) {
@@ -4322,7 +5845,7 @@ Token: ${cfg.token}`);
4322
5845
  break;
4323
5846
  }
4324
5847
  case "rotate-token": {
4325
- cfg.token = randomBytes3(32).toString("base64url");
5848
+ cfg.token = randomBytes4(32).toString("base64url");
4326
5849
  saveConfig(cfg);
4327
5850
  const agents = registry.list();
4328
5851
  for (const agent of agents) {
@@ -4395,7 +5918,7 @@ Token: ${cfg.token}`);
4395
5918
  console.log(`coderemote ${VERSION} is the latest version.`);
4396
5919
  break;
4397
5920
  }
4398
- const [command, args] = installCommand(fs14.realpathSync(process.argv[1]));
5921
+ const [command, args] = installCommand(fs19.realpathSync(process.argv[1]));
4399
5922
  console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
4400
5923
  `);
4401
5924
  const code = await new Promise((resolve) => {