coderemote 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/dist/coderemote.js +1696 -125
  3. package/package.json +1 -1
@@ -1,24 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/coderemote.js
4
- import { execFile as execFile5, spawn as spawn3 } from "node:child_process";
5
- import { randomBytes } from "node:crypto";
6
- import fs8 from "node:fs";
4
+ import { execFile as execFile8, spawn as spawn3 } from "node:child_process";
5
+ import { randomBytes as randomBytes3 } from "node:crypto";
6
+ import fs13 from "node:fs";
7
7
  import net2 from "node:net";
8
- import os4 from "node:os";
9
- import path7 from "node:path";
8
+ import os6 from "node:os";
9
+ import path11 from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
- import { promisify as promisify3 } from "node:util";
11
+ import { promisify as promisify5 } from "node:util";
12
12
  import qrcode from "qrcode-terminal";
13
13
 
14
14
  // ../../packages/agent-core/src/server.js
15
- import { createReadStream, createWriteStream } from "node:fs";
16
- import fs4 from "node:fs/promises";
15
+ import { createReadStream, createWriteStream as createWriteStream2 } from "node:fs";
16
+ import fs7 from "node:fs/promises";
17
17
  import http2 from "node:http";
18
- import os3 from "node:os";
19
- import path3 from "node:path";
20
- import { pipeline } from "node:stream/promises";
21
- import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
18
+ import os5 from "node:os";
19
+ import path5 from "node:path";
20
+ import { pipeline as pipeline2 } from "node:stream/promises";
21
+ import { randomUUID as randomUUID4, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
22
+ import { execFile as execFile5 } from "node:child_process";
22
23
 
23
24
  // ../../packages/protocol/index.js
24
25
  var PROTOCOL_VERSION = 4;
@@ -56,13 +57,37 @@ var Routes = Object.freeze({
56
57
  previewRequest: "GET /api/preview/request",
57
58
  previewRequestsClear: "DELETE /api/preview/requests",
58
59
  tunnelToken: "POST /api/tunnel/token",
59
- tunnelStatus: "GET /api/tunnel"
60
+ tunnelStatus: "GET /api/tunnel",
61
+ agentsHook: "POST /api/agents/hook",
62
+ agentsSessions: "GET /api/agents/sessions",
63
+ agentsTimeline: "GET /api/agents/timeline",
64
+ agentsAnswer: "POST /api/agents/answer",
65
+ agentsLaunch: "POST /api/agents/sessions",
66
+ agentsSetup: "GET /api/agents/setup",
67
+ agentsPrompt: "POST /api/agents/prompt",
68
+ agentsKeys: "POST /api/agents/keys",
69
+ agentsUpload: "POST /api/agents/uploads",
70
+ pushDeviceSet: "PUT /api/push/device",
71
+ pushDeviceDelete: "DELETE /api/push/device",
72
+ pushTest: "POST /api/push/test"
60
73
  });
61
74
  var TUNNEL_HANDOFF_PATH = "/vsremote/handoff";
62
75
  var TUNNEL_AUTH_CALLBACK_HOST = "tunnel-auth";
63
76
  var TERMINAL_WS_PATH = "/api/term/ws";
64
77
  var TerminalServerMessage = Object.freeze({ session: "session", exit: "exit" });
65
78
  var TerminalClientMessage = Object.freeze({ input: "input", resize: "resize" });
79
+ var AGENTS_WS_PATH = "/api/agents/ws";
80
+ var HOOK_SECRET_HEADER = "x-coderemote-hook";
81
+ var AgentsServerMessage = Object.freeze({
82
+ snapshot: "snapshot",
83
+ session: "session",
84
+ entry: "entry",
85
+ request: "request",
86
+ requestResolved: "request_resolved",
87
+ sessionRemoved: "session_removed",
88
+ promptConfirmed: "prompt_confirmed"
89
+ });
90
+ var AgentsClientMessage = Object.freeze({ presence: "presence" });
66
91
  function webUrl({ url, token }) {
67
92
  return `${url}/#token=${encodeURIComponent(token)}`;
68
93
  }
@@ -73,9 +98,9 @@ function pairingUrl({ url, token }) {
73
98
  // ../../packages/agent-core/src/server.js
74
99
  import { WebSocketServer } from "ws";
75
100
 
76
- // ../../packages/agent-core/src/git.js
77
- import { execFile } from "node:child_process";
78
- import fs2 from "node:fs/promises";
101
+ // ../../packages/agent-core/src/agents.js
102
+ import { randomUUID } from "node:crypto";
103
+ import { EventEmitter } from "node:events";
79
104
  import path2 from "node:path";
80
105
 
81
106
  // ../../packages/agent-core/src/sandbox.js
@@ -109,8 +134,8 @@ var Sandbox = class {
109
134
  }
110
135
  /** Like `resolve`, but refuses the root itself (deleting or renaming the shared folder). */
111
136
  async resolveChild(p, { allowMissing = false } = {}) {
112
- const { abs, real } = await this.#check(p, allowMissing);
113
- if (real === this.realRoot) throw new HttpError(403, "cannot modify the shared folder itself");
137
+ const { abs, real: real2 } = await this.#check(p, allowMissing);
138
+ if (real2 === this.realRoot) throw new HttpError(403, "cannot modify the shared folder itself");
114
139
  return abs;
115
140
  }
116
141
  async #check(p, allowMissing) {
@@ -120,9 +145,9 @@ var Sandbox = class {
120
145
  let tail = "";
121
146
  while (true) {
122
147
  try {
123
- const real = path.join(await fs.realpath(existing), tail);
124
- if (!this.#containsReal(real)) throw outside();
125
- return { abs, real };
148
+ const real2 = path.join(await fs.realpath(existing), tail);
149
+ if (!this.#containsReal(real2)) throw outside();
150
+ return { abs, real: real2 };
126
151
  } catch (e) {
127
152
  if (e instanceof HttpError) throw e;
128
153
  if (!this.#containsLexically(abs)) throw outside();
@@ -139,7 +164,633 @@ function outside() {
139
164
  return new HttpError(403, "path is outside the shared folder");
140
165
  }
141
166
 
167
+ // ../../packages/agent-core/src/codex-rollout.js
168
+ import fs2 from "node:fs/promises";
169
+ var POLL = 1e3;
170
+ function codexItem(item) {
171
+ if (!item || typeof item !== "object") return null;
172
+ if (item.type === "AgentMessage") {
173
+ const text = (item.content ?? []).filter((c) => typeof c?.text === "string").map((c) => c.text).join("\n").trim();
174
+ return text ? { kind: "assistant", text } : null;
175
+ }
176
+ if (item.type === "FileChange" && item.changes && typeof item.changes === "object") {
177
+ const verbs = { add: "Added", update: "Updated", delete: "Deleted" };
178
+ const lines = Object.entries(item.changes).map(([file, change]) => `${verbs[change?.type] ?? "Changed"} ${file}`);
179
+ return { kind: "tool_result", toolUseId: item.id ?? null, output: lines.join("\n"), error: false };
180
+ }
181
+ const output = item.aggregated_output ?? item.aggregatedOutput ?? item.output;
182
+ if (typeof item.id === "string" && typeof output === "string") {
183
+ const code = item.exit_code ?? item.exitCode;
184
+ return { kind: "tool_result", toolUseId: item.id, output, error: typeof code === "number" && code !== 0 };
185
+ }
186
+ return null;
187
+ }
188
+ var RolloutTail = class {
189
+ #file;
190
+ #offset = null;
191
+ #partial = "";
192
+ #timer = null;
193
+ #onItem;
194
+ constructor(file, onItem) {
195
+ this.#file = file;
196
+ this.#onItem = onItem;
197
+ this.#timer = setInterval(() => this.poll().catch(() => {
198
+ }), POLL);
199
+ this.#timer.unref?.();
200
+ }
201
+ async poll() {
202
+ const handle = await fs2.open(this.#file, "r");
203
+ try {
204
+ const { size } = await handle.stat();
205
+ if (this.#offset === null || size < this.#offset) {
206
+ this.#offset = size;
207
+ return;
208
+ }
209
+ if (size === this.#offset) return;
210
+ const buf = Buffer.alloc(size - this.#offset);
211
+ await handle.read(buf, 0, buf.length, this.#offset);
212
+ this.#offset = size;
213
+ const lines = (this.#partial + buf.toString("utf8")).split("\n");
214
+ this.#partial = lines.pop();
215
+ for (const line of lines) {
216
+ let record;
217
+ try {
218
+ record = JSON.parse(line);
219
+ } catch {
220
+ continue;
221
+ }
222
+ if (record?.type === "event_msg" && record.payload?.type === "item_completed") this.#onItem(record.payload.item);
223
+ }
224
+ } finally {
225
+ await handle.close();
226
+ }
227
+ }
228
+ stop() {
229
+ clearInterval(this.#timer);
230
+ }
231
+ };
232
+
233
+ // ../../packages/agent-core/src/transcript.js
234
+ import fs3 from "node:fs/promises";
235
+ var TAIL_BYTES = 2 * 1024 * 1024;
236
+ function textOf(content) {
237
+ if (typeof content === "string") return content;
238
+ if (!Array.isArray(content)) return "";
239
+ return content.filter((c) => c?.type === "text" && typeof c.text === "string").map((c) => c.text).join("\n");
240
+ }
241
+ function isTyped(text) {
242
+ const t = text.trim();
243
+ return t && !t.startsWith("<command-") && !t.startsWith("<local-command") && !t.startsWith("[Request interrupted");
244
+ }
245
+ function* entriesOf(record) {
246
+ if (!record || record.isSidechain || record.isMeta) return;
247
+ const time2 = Date.parse(record.timestamp) || 0;
248
+ const content = record.message?.content;
249
+ if (record.type === "user") {
250
+ if (typeof content === "string") {
251
+ if (isTyped(content)) yield { time: time2, kind: "prompt", text: content };
252
+ return;
253
+ }
254
+ if (!Array.isArray(content)) return;
255
+ for (const c of content) {
256
+ if (c?.type === "tool_result") {
257
+ yield { time: time2, kind: "tool_result", toolUseId: c.tool_use_id ?? null, name: null, output: textOf(c.content), error: c.is_error === true, truncated: false, duration: null };
258
+ }
259
+ }
260
+ const text = textOf(content);
261
+ if (isTyped(text) && content.every((c) => c?.type !== "tool_result")) yield { time: time2, kind: "prompt", text };
262
+ } else if (record.type === "assistant" && Array.isArray(content)) {
263
+ for (const c of content) {
264
+ if (c?.type === "text" && c.text?.trim()) yield { time: time2, kind: "assistant", text: c.text.trimEnd() };
265
+ else if (c?.type === "tool_use") yield { time: time2, kind: "tool", toolUseId: c.id ?? null, name: c.name ?? null, input: c.input ?? null };
266
+ }
267
+ }
268
+ }
269
+ async function readTranscript(file, limit = 500) {
270
+ let text;
271
+ try {
272
+ const handle = await fs3.open(file, "r");
273
+ try {
274
+ const { size } = await handle.stat();
275
+ const start2 = Math.max(0, size - TAIL_BYTES);
276
+ const buf = Buffer.alloc(size - start2);
277
+ await handle.read(buf, 0, buf.length, start2);
278
+ text = buf.toString("utf8");
279
+ if (start2 > 0) text = text.slice(text.indexOf("\n") + 1);
280
+ } finally {
281
+ await handle.close();
282
+ }
283
+ } catch {
284
+ return [];
285
+ }
286
+ const entries = [];
287
+ for (const line of text.split("\n")) {
288
+ if (!line.trim()) continue;
289
+ try {
290
+ entries.push(...entriesOf(JSON.parse(line)));
291
+ } catch {
292
+ }
293
+ }
294
+ return entries.slice(-limit);
295
+ }
296
+
297
+ // ../../packages/agent-core/src/agents.js
298
+ var MAX_ENTRIES = 500;
299
+ var MAX_TEXT = 16 * 1024;
300
+ var MAX_TITLE = 80;
301
+ var ENDED_TTL = 24 * 60 * 60 * 1e3;
302
+ var REQUEST_TTL = 60 * 60 * 1e3;
303
+ var START_TIMEOUT = 15 * 1e3;
304
+ var SEQUENTIAL_REQUEST_TTL = 10 * 60 * 1e3;
305
+ var ADAPTERS = {
306
+ claude: {
307
+ name: "Claude Code",
308
+ capabilities: { approvals: "parallel", alwaysAllow: true, keys: ["interrupt", "cycle_mode", "/compact", "/clear", "/model"] },
309
+ normalize: (e) => e
310
+ },
311
+ codex: {
312
+ name: "Codex",
313
+ // Codex reports a session only once it gets its first prompt.
314
+ startsOnPrompt: true,
315
+ setupHint: ", then trust its hooks with `/hooks` in Codex",
316
+ capabilities: { approvals: "presence", alwaysAllow: false, keys: ["interrupt", "/compact", "/model"] },
317
+ normalize: (e) => ({
318
+ ...e,
319
+ // Codex's patch tool carries the patch where Bash has its command.
320
+ tool_input: e.tool_name === "apply_patch" && typeof e.tool_input === "string" ? { command: e.tool_input } : e.tool_input
321
+ })
322
+ }
323
+ };
324
+ function comparable(text, paths = []) {
325
+ let t = String(text);
326
+ for (const p of paths) t = t.split(p).join(" ");
327
+ return t.replace(/\[Image #\d+\]/g, " ").replace(/\s+/g, " ").trim();
328
+ }
329
+ function clip(text) {
330
+ const buf = Buffer.from(text, "utf8");
331
+ if (buf.length <= MAX_TEXT) return { text, truncated: false };
332
+ return { text: buf.subarray(0, MAX_TEXT).toString("utf8").replace(/�+$/, ""), truncated: true };
333
+ }
334
+ function clipInput(input) {
335
+ if (!input || typeof input !== "object") return input ?? null;
336
+ const out = Array.isArray(input) ? [] : {};
337
+ for (const [k, v] of Object.entries(input)) out[k] = typeof v === "string" ? clip(v).text : v;
338
+ return out;
339
+ }
340
+ function resultText(response) {
341
+ if (response == null) return "";
342
+ if (typeof response === "string") return response;
343
+ if (typeof response.stdout === "string" || typeof response.stderr === "string") {
344
+ return [response.stdout, response.stderr].filter(Boolean).join("\n");
345
+ }
346
+ return JSON.stringify(response, null, 2);
347
+ }
348
+ function titleOf(prompt) {
349
+ const line = prompt.trim().split("\n")[0].trim();
350
+ return line.length > MAX_TITLE ? `${line.slice(0, MAX_TITLE - 1)}\u2026` : line;
351
+ }
352
+ var AgentSession = class {
353
+ constructor({ id, agent, cwd, terminal, now }) {
354
+ this.id = id;
355
+ this.agent = agent;
356
+ this.cwd = cwd;
357
+ this.title = path2.basename(cwd || "") || "Session";
358
+ this.capabilities = (ADAPTERS[agent] ?? ADAPTERS.claude).capabilities;
359
+ this.titled = false;
360
+ this.state = "idle";
361
+ this.terminal = terminal;
362
+ this.startedAt = now;
363
+ this.lastActivityAt = now;
364
+ this.endedAt = null;
365
+ this.starting = false;
366
+ this.error = null;
367
+ this.turnStartedAt = null;
368
+ this.entries = [];
369
+ this.seq = 0;
370
+ this.streaming = /* @__PURE__ */ new Map();
371
+ this.requests = /* @__PURE__ */ new Map();
372
+ this.prompts = [];
373
+ this.tail = null;
374
+ this.ready = Promise.resolve();
375
+ }
376
+ toJSON() {
377
+ return {
378
+ id: this.id,
379
+ agent: this.agent,
380
+ cwd: this.cwd,
381
+ title: this.title,
382
+ state: this.state,
383
+ terminal: this.terminal,
384
+ startedAt: this.startedAt,
385
+ lastActivityAt: this.lastActivityAt,
386
+ pending: this.requests.size,
387
+ starting: this.starting,
388
+ error: this.error,
389
+ capabilities: this.capabilities
390
+ };
391
+ }
392
+ };
393
+ var AgentHub = class extends EventEmitter {
394
+ #sessions = /* @__PURE__ */ new Map();
395
+ #requests = /* @__PURE__ */ new Map();
396
+ #now;
397
+ #requestTtl;
398
+ #transcript;
399
+ #startTimeout;
400
+ #sequentialTtl;
401
+ #rolloutTail;
402
+ /**
403
+ * `present` says whether someone is at the computer or looking at it in a client; agents whose
404
+ * approvals are sequential ask there instead of holding the request for the phone.
405
+ */
406
+ constructor({
407
+ now = Date.now,
408
+ requestTtl = REQUEST_TTL,
409
+ sequentialTtl = SEQUENTIAL_REQUEST_TTL,
410
+ transcript = readTranscript,
411
+ startTimeout = START_TIMEOUT,
412
+ present = async () => false,
413
+ rolloutTail = (file, onItem) => new RolloutTail(file, onItem)
414
+ } = {}) {
415
+ super();
416
+ this.#now = now;
417
+ this.#requestTtl = requestTtl;
418
+ this.#sequentialTtl = sequentialTtl;
419
+ this.#transcript = transcript;
420
+ this.#startTimeout = startTimeout;
421
+ this.present = present;
422
+ this.#rolloutTail = rolloutTail;
423
+ this.presenceSet = arguments[0]?.present !== void 0;
424
+ }
425
+ /**
426
+ * A prompt a client typed into a session's terminal. Once Claude Code reports a matching
427
+ * `UserPromptSubmit`, the socket gets `prompt_confirmed` with this id.
428
+ */
429
+ prompted(sessionId, { id, text, paths = [] }) {
430
+ const s = this.#sessions.get(sessionId);
431
+ if (!s) return;
432
+ s.prompts.push({ id, text: comparable(text, paths), at: this.#now() });
433
+ s.prompts = s.prompts.filter((p) => this.#now() - p.at < 6e4).slice(-20);
434
+ }
435
+ /** Whether `resume` may reopen a session: only one no process runs any more. */
436
+ canResume(id) {
437
+ const s = this.#sessions.get(id);
438
+ return !s || s.state === "ended";
439
+ }
440
+ /**
441
+ * A session the agent just started in `terminal` (`id`: the id it passed with --session-id,
442
+ * or the one it resumes). It shows right away; if Claude Code reports nothing within the
443
+ * start timeout, it ends with an error.
444
+ */
445
+ launched({ id, terminal, cwd, agent = "claude" }) {
446
+ const now = this.#now();
447
+ let s = this.#sessions.get(id);
448
+ if (!s) {
449
+ s = new AgentSession({ id, agent, cwd, terminal, now });
450
+ this.#sessions.set(id, s);
451
+ }
452
+ const adapter = ADAPTERS[agent] ?? ADAPTERS.claude;
453
+ const state = adapter.startsOnPrompt ? "idle" : "running";
454
+ Object.assign(s, { terminal, state, starting: true, error: null, endedAt: null, lastActivityAt: now });
455
+ if (adapter.startsOnPrompt) {
456
+ this.#emitSession(s);
457
+ return s.toJSON();
458
+ }
459
+ const timer = setTimeout(() => {
460
+ if (!s.starting || this.#sessions.get(id) !== s) return;
461
+ s.starting = false;
462
+ s.error = `${adapter.name} didn't report back. If it runs in the terminal, run \`coderemote setup ${agent}\` once on the computer${adapter.setupHint ?? ""}.`;
463
+ this.#end(s);
464
+ this.#emitSession(s);
465
+ }, this.#startTimeout);
466
+ timer.unref?.();
467
+ this.#emitSession(s);
468
+ return s.toJSON();
469
+ }
470
+ sessions() {
471
+ return [...this.#sessions.values()].map((s) => s.toJSON()).sort((a, b) => b.lastActivityAt - a.lastActivityAt);
472
+ }
473
+ requests() {
474
+ return [...this.#requests.values()].map((r) => r.request);
475
+ }
476
+ session(id) {
477
+ return this.#sessions.get(id)?.toJSON() ?? null;
478
+ }
479
+ timeline(id, after = 0) {
480
+ const s = this.#sessions.get(id);
481
+ if (!s) throw new HttpError(404, "no such session");
482
+ const entries = s.entries.filter((e) => e.seq > after);
483
+ return { entries, next: s.seq, requests: [...s.requests.values()].map((r) => r.request) };
484
+ }
485
+ /**
486
+ * Takes one hook event. Resolves with the hook's output: for `PermissionRequest` once the
487
+ * request is answered (null when the computer should decide), `{}` for everything else.
488
+ * Aborting `signal` (the hook was killed because the computer answered) withdraws the request.
489
+ */
490
+ async handle(raw, { agent = "claude", terminal = null, signal } = {}) {
491
+ if (!raw || typeof raw.session_id !== "string" || !raw.session_id) {
492
+ throw new HttpError(400, "hook event without session_id");
493
+ }
494
+ if (!Object.hasOwn(ADAPTERS, agent)) throw new HttpError(400, `unknown agent ${agent}`);
495
+ const event = ADAPTERS[agent].normalize(raw);
496
+ const now = this.#now();
497
+ let replaced = null;
498
+ if (!this.#sessions.has(event.session_id)) {
499
+ const candidates = [...this.#sessions.values()].filter(
500
+ (p) => p.starting && (terminal ? p.terminal === terminal : p.agent === agent && p.cwd === event.cwd)
501
+ );
502
+ if (candidates.length === 1) {
503
+ replaced = candidates[0];
504
+ this.#sessions.delete(replaced.id);
505
+ terminal ??= replaced.terminal;
506
+ }
507
+ }
508
+ let s = this.#sessions.get(event.session_id);
509
+ if (!s) {
510
+ s = new AgentSession({ id: event.session_id, agent, cwd: event.cwd ?? null, terminal, now });
511
+ this.#sessions.set(s.id, s);
512
+ const fresh = event.hook_event_name === "SessionStart" && event.source !== "resume";
513
+ if (!fresh && typeof event.transcript_path === "string") s.ready = this.#backfill(s, event.transcript_path);
514
+ }
515
+ await s.ready;
516
+ if (s.agent === "codex" && !s.tail && typeof event.transcript_path === "string") {
517
+ s.tail = this.#rolloutTail(event.transcript_path, (item) => this.#fromRollout(s, item));
518
+ s.tail.poll?.().catch(() => {
519
+ });
520
+ }
521
+ if (replaced) {
522
+ s.prompts.push(...replaced.prompts);
523
+ this.emit("message", { type: "session_removed", session: replaced.id, replacedBy: s.id });
524
+ }
525
+ if (terminal && !s.terminal) s.terminal = terminal;
526
+ s.starting = false;
527
+ s.lastActivityAt = now;
528
+ if (s.state === "ended" && event.hook_event_name !== "SessionEnd") {
529
+ s.state = "idle";
530
+ s.endedAt = null;
531
+ }
532
+ switch (event.hook_event_name) {
533
+ case "SessionStart":
534
+ break;
535
+ case "UserPromptSubmit": {
536
+ const text = typeof event.prompt === "string" ? event.prompt : "";
537
+ if (!s.titled && text.trim()) {
538
+ s.title = titleOf(text);
539
+ s.titled = true;
540
+ }
541
+ this.#startTurn(s, now);
542
+ this.#entry(s, { kind: "prompt", text });
543
+ const said = comparable(text);
544
+ const i = s.prompts.findIndex((p) => p.text === said || p.text && said.startsWith(p.text));
545
+ if (i >= 0) {
546
+ const [prompt] = s.prompts.splice(i, 1);
547
+ this.emit("message", { type: "prompt_confirmed", session: s.id, prompt: prompt.id });
548
+ }
549
+ break;
550
+ }
551
+ case "PreToolUse":
552
+ this.#startTurn(s, now);
553
+ this.#entry(s, {
554
+ kind: "tool",
555
+ toolUseId: event.tool_use_id ?? null,
556
+ name: event.tool_name,
557
+ input: clipInput(event.tool_input)
558
+ });
559
+ break;
560
+ case "PostToolUse":
561
+ case "PostToolUseFailure": {
562
+ const failed = event.hook_event_name === "PostToolUseFailure";
563
+ const { text, truncated } = clip(failed ? String(event.error ?? "") : resultText(event.tool_response));
564
+ this.#entry(s, {
565
+ kind: "tool_result",
566
+ toolUseId: event.tool_use_id ?? null,
567
+ name: event.tool_name,
568
+ output: text,
569
+ error: failed,
570
+ truncated,
571
+ duration: event.duration_ms ?? null
572
+ });
573
+ break;
574
+ }
575
+ case "MessageDisplay": {
576
+ const message = s.streaming.get(event.message_id) ?? { parts: /* @__PURE__ */ new Map(), last: null };
577
+ s.streaming.set(event.message_id, message);
578
+ message.parts.set(event.index ?? message.parts.size, event.delta ?? "");
579
+ if (event.final) message.last = event.index ?? message.parts.size - 1;
580
+ if (message.last === null || message.parts.size <= message.last) break;
581
+ s.streaming.delete(event.message_id);
582
+ const text = [...message.parts.entries()].sort((a, b) => a[0] - b[0]).map(([, d]) => d).join("").trimEnd();
583
+ const previous = s.entries.at(-1)?.kind === "turn_end" ? s.entries.at(-2) : s.entries.at(-1);
584
+ if (text.trim() && !(previous?.kind === "assistant" && previous.text.trim() === text.trim())) {
585
+ this.#entry(s, { kind: "assistant", text });
586
+ }
587
+ break;
588
+ }
589
+ case "PermissionRequest": {
590
+ this.#emitSession(s);
591
+ const sequential = s.capabilities.approvals === "presence";
592
+ const deferNow = sequential && await this.present().catch(() => false);
593
+ return this.#openRequest(s, event, signal, { deferNow, ttl: sequential ? this.#sequentialTtl : this.#requestTtl });
594
+ }
595
+ case "Stop":
596
+ case "StopFailure": {
597
+ const last = typeof event.last_assistant_message === "string" ? event.last_assistant_message.trim() : "";
598
+ const turnEntries = s.entries.filter((e) => e.time >= (s.turnStartedAt ?? 0));
599
+ if (last && !turnEntries.some((e) => e.kind === "assistant" && e.text.trim() === last)) {
600
+ this.#entry(s, { kind: "assistant", text: last });
601
+ }
602
+ s.streaming.clear();
603
+ const duration = s.turnStartedAt ? now - s.turnStartedAt : 0;
604
+ s.turnStartedAt = null;
605
+ s.state = s.requests.size ? "waiting" : "idle";
606
+ this.#entry(s, { kind: "turn_end", duration });
607
+ break;
608
+ }
609
+ case "Interrupt":
610
+ case "Notification":
611
+ if ((event.hook_event_name === "Interrupt" || event.notification_type === "idle_prompt") && s.state === "running") {
612
+ s.state = "idle";
613
+ s.turnStartedAt = null;
614
+ }
615
+ break;
616
+ case "SessionEnd":
617
+ this.#end(s);
618
+ break;
619
+ default:
620
+ break;
621
+ }
622
+ this.#emitSession(s);
623
+ return {};
624
+ }
625
+ /** The terminal a session ran in exited: whatever ran in it is over. */
626
+ terminalExited(terminal) {
627
+ for (const s of this.#sessions.values()) {
628
+ if (s.terminal === terminal && s.state !== "ended") {
629
+ if (s.starting) s.error = `${(ADAPTERS[s.agent] ?? ADAPTERS.claude).name} exited before it started. Open the terminal to see why.`;
630
+ s.starting = false;
631
+ this.#end(s);
632
+ this.#emitSession(s);
633
+ }
634
+ }
635
+ }
636
+ /** Forgets sessions that ended more than a day ago. */
637
+ sweep() {
638
+ const cutoff = this.#now() - ENDED_TTL;
639
+ for (const s of this.#sessions.values()) {
640
+ if (s.state === "ended" && s.endedAt < cutoff) {
641
+ s.tail?.stop();
642
+ this.#sessions.delete(s.id);
643
+ }
644
+ }
645
+ }
646
+ /**
647
+ * Answers an open request from a client. Throws 404 for an unknown request and 409 for one
648
+ * already resolved (by another device, the computer, or the timeout).
649
+ */
650
+ answer({ request: id, behavior, message, suggestion, answers } = {}) {
651
+ const pending = this.#requests.get(id);
652
+ if (!pending) {
653
+ throw new HttpError(this.#resolved.has(id) ? 409 : 404, this.#resolved.has(id) ? "already answered" : "no such request");
654
+ }
655
+ const { request } = pending;
656
+ if (behavior !== "allow" && behavior !== "deny") throw new HttpError(400, "behavior must be allow or deny");
657
+ let decision;
658
+ let outcome;
659
+ if (behavior === "deny") {
660
+ decision = { behavior: "deny", ...typeof message === "string" && message ? { message } : {} };
661
+ outcome = "denied";
662
+ } else if (request.kind === "question") {
663
+ if (!answers || typeof answers !== "object" || Object.values(answers).some((a) => typeof a !== "string")) {
664
+ throw new HttpError(400, "answers must map each question to a string");
665
+ }
666
+ decision = { behavior: "allow", updatedInput: { ...pending.input, answers } };
667
+ outcome = "answered";
668
+ } else {
669
+ decision = { behavior: "allow" };
670
+ if (suggestion !== void 0) {
671
+ const rule = Number.isInteger(suggestion) ? request.suggestions[suggestion] : void 0;
672
+ if (!rule || !pending.session.capabilities.alwaysAllow) {
673
+ throw new HttpError(400, "suggestion must index the request\u2019s suggestions");
674
+ }
675
+ decision.updatedPermissions = [rule];
676
+ }
677
+ outcome = "allowed";
678
+ }
679
+ this.#resolve(pending, { hookSpecificOutput: { hookEventName: "PermissionRequest", decision } }, outcome, "phone");
680
+ }
681
+ // Ids of requests resolved recently, to tell "already answered" (409) from unknown (404).
682
+ #resolved = /* @__PURE__ */ new Set();
683
+ #openRequest(s, event, signal, { deferNow = false, ttl = this.#requestTtl } = {}) {
684
+ const isQuestion = event.tool_name === "AskUserQuestion";
685
+ const input = event.tool_input ?? {};
686
+ const request = {
687
+ id: randomUUID(),
688
+ session: s.id,
689
+ kind: isQuestion ? "question" : "permission",
690
+ tool: event.tool_name ?? null,
691
+ input: clipInput(input),
692
+ suggestions: Array.isArray(event.permission_suggestions) ? event.permission_suggestions : [],
693
+ questions: isQuestion && Array.isArray(input.questions) ? input.questions : [],
694
+ createdAt: this.#now()
695
+ };
696
+ return new Promise((resolve) => {
697
+ const pending = { request, input, session: s, resolve, timer: null, signal, onAbort: null };
698
+ pending.timer = setTimeout(() => this.#resolve(pending, null, "expired", "agent"), ttl);
699
+ pending.timer.unref?.();
700
+ pending.onAbort = () => this.#resolve(pending, null, "withdrawn", "computer");
701
+ signal?.addEventListener("abort", pending.onAbort, { once: true });
702
+ s.requests.set(request.id, pending);
703
+ this.#requests.set(request.id, pending);
704
+ s.state = "waiting";
705
+ this.#entry(s, { kind: "request", request });
706
+ this.emit("message", { type: "request", request });
707
+ this.#emitSession(s);
708
+ if (signal?.aborted) pending.onAbort();
709
+ else if (deferNow) this.#resolve(pending, null, "deferred", "agent");
710
+ });
711
+ }
712
+ #resolve(pending, output, outcome, by) {
713
+ const { request, session: s } = pending;
714
+ if (!this.#requests.delete(request.id)) return;
715
+ s.requests.delete(request.id);
716
+ clearTimeout(pending.timer);
717
+ pending.signal?.removeEventListener("abort", pending.onAbort);
718
+ this.#resolved.add(request.id);
719
+ if (this.#resolved.size > 1e3) this.#resolved.delete(this.#resolved.values().next().value);
720
+ if (s.state === "waiting" && !s.requests.size) s.state = "running";
721
+ s.lastActivityAt = this.#now();
722
+ this.#entry(s, { kind: "request_resolved", request: request.id, outcome, by });
723
+ this.emit("message", { type: "request_resolved", session: s.id, request: request.id, outcome, by });
724
+ this.#emitSession(s);
725
+ pending.resolve(output);
726
+ }
727
+ async #backfill(s, file) {
728
+ let entries = [];
729
+ try {
730
+ entries = await this.#transcript(file, MAX_ENTRIES);
731
+ } catch {
732
+ }
733
+ const tools = /* @__PURE__ */ new Map();
734
+ for (const e of entries) {
735
+ if (e.kind === "tool") {
736
+ tools.set(e.toolUseId, e.name);
737
+ e.input = clipInput(e.input);
738
+ } else if (e.kind === "tool_result") {
739
+ e.name ??= tools.get(e.toolUseId) ?? null;
740
+ ({ text: e.output, truncated: e.truncated } = clip(e.output));
741
+ } else if (e.kind === "prompt" && !s.titled && e.text.trim()) {
742
+ s.title = titleOf(e.text);
743
+ s.titled = true;
744
+ }
745
+ s.entries.push({ seq: ++s.seq, ...e });
746
+ }
747
+ if (s.entries.length > MAX_ENTRIES) s.entries.splice(0, s.entries.length - MAX_ENTRIES);
748
+ }
749
+ /** Something Codex said or a tool's result, from its rollout, unless the hooks told it already. */
750
+ #fromRollout(s, item) {
751
+ const fields = codexItem(item);
752
+ if (!fields) return;
753
+ const recent = s.entries.slice(-8);
754
+ if (fields.kind === "assistant") {
755
+ if (recent.some((e) => e.kind === "assistant" && e.text.trim() === fields.text)) return;
756
+ this.#entry(s, fields);
757
+ } else {
758
+ if (s.entries.some((e) => e.kind === "tool_result" && e.toolUseId === fields.toolUseId)) return;
759
+ const call = [...s.entries].reverse().find((e) => e.kind === "tool" && e.toolUseId === fields.toolUseId);
760
+ if (!call) return;
761
+ const { text, truncated } = clip(fields.output);
762
+ this.#entry(s, { kind: "tool_result", toolUseId: fields.toolUseId, name: call.name, output: text, error: fields.error, truncated, duration: null });
763
+ }
764
+ s.lastActivityAt = this.#now();
765
+ this.#emitSession(s);
766
+ }
767
+ #startTurn(s, now) {
768
+ if (s.state !== "waiting") s.state = "running";
769
+ s.turnStartedAt ??= now;
770
+ }
771
+ #end(s) {
772
+ s.tail?.stop();
773
+ s.tail = null;
774
+ for (const pending of [...s.requests.values()]) this.#resolve(pending, null, "withdrawn", "computer");
775
+ s.state = "ended";
776
+ s.endedAt = this.#now();
777
+ s.turnStartedAt = null;
778
+ }
779
+ #entry(s, fields) {
780
+ const entry2 = { seq: ++s.seq, time: this.#now(), ...fields };
781
+ s.entries.push(entry2);
782
+ if (s.entries.length > MAX_ENTRIES) s.entries.shift();
783
+ this.emit("message", { type: "entry", session: s.id, entry: entry2 });
784
+ }
785
+ #emitSession(s) {
786
+ this.emit("message", { type: "session", session: s.toJSON() });
787
+ }
788
+ };
789
+
142
790
  // ../../packages/agent-core/src/git.js
791
+ import { execFile } from "node:child_process";
792
+ import fs4 from "node:fs/promises";
793
+ import path3 from "node:path";
143
794
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".build", "build", "dist", "DerivedData", "Pods", "vendor", ".venv", "target"]);
144
795
  var MAX_SCAN_DEPTH = 4;
145
796
  var MAX_DIFF_BYTES = 2 * 1024 * 1024;
@@ -168,7 +819,7 @@ async function findRepos(root2) {
168
819
  async function walk(dir, depth) {
169
820
  let entries;
170
821
  try {
171
- entries = await fs2.readdir(dir, { withFileTypes: true });
822
+ entries = await fs4.readdir(dir, { withFileTypes: true });
172
823
  } catch {
173
824
  return;
174
825
  }
@@ -178,14 +829,14 @@ async function findRepos(root2) {
178
829
  }
179
830
  if (depth >= MAX_SCAN_DEPTH) return;
180
831
  const subdirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIRS.has(e.name));
181
- await Promise.all(subdirs.map((e) => walk(path2.join(dir, e.name), depth + 1)));
832
+ await Promise.all(subdirs.map((e) => walk(path3.join(dir, e.name), depth + 1)));
182
833
  }
183
834
  await walk(root2, 0);
184
- repos.sort((a, b) => a.split(path2.sep).length - b.split(path2.sep).length || a.localeCompare(b));
835
+ repos.sort((a, b) => a.split(path3.sep).length - b.split(path3.sep).length || a.localeCompare(b));
185
836
  return Promise.all(
186
837
  repos.map(async (repo) => ({
187
838
  path: repo,
188
- name: repo === root2 ? path2.basename(root2) : path2.relative(root2, repo),
839
+ name: repo === root2 ? path3.basename(root2) : path3.relative(root2, repo),
189
840
  branch: await currentBranch(repo)
190
841
  }))
191
842
  );
@@ -389,7 +1040,7 @@ var LANDING_HTML = `<!doctype html>
389
1040
 
390
1041
  // ../../packages/agent-core/src/ports.js
391
1042
  import { execFile as execFile2 } from "node:child_process";
392
- import fs3 from "node:fs/promises";
1043
+ import fs5 from "node:fs/promises";
393
1044
  import os from "node:os";
394
1045
  function run(cmd2, args) {
395
1046
  return new Promise((resolve, reject) => {
@@ -447,7 +1098,7 @@ async function workingDirs(pids) {
447
1098
  const dirs = /* @__PURE__ */ new Map();
448
1099
  if (!pids.length) return dirs;
449
1100
  if (os.platform() === "linux") {
450
- await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs3.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
1101
+ await Promise.all(pids.map(async (pid2) => dirs.set(pid2, await fs5.readlink(`/proc/${pid2}/cwd`).catch(() => null))));
451
1102
  return dirs;
452
1103
  }
453
1104
  const out = await run("lsof", ["-a", "-p", pids.join(","), "-d", "cwd", "-F", "pn"]).catch(() => "");
@@ -507,11 +1158,228 @@ async function killListener(pid, { force = false, shells } = {}) {
507
1158
  }
508
1159
  }
509
1160
 
1161
+ // ../../packages/agent-core/src/push.js
1162
+ import { execFile as execFile3 } from "node:child_process";
1163
+ import { createCipheriv, randomBytes } from "node:crypto";
1164
+ import os2 from "node:os";
1165
+ import { promisify } from "node:util";
1166
+ var run2 = promisify(execFile3);
1167
+ var FINISHED_AFTER = 60 * 1e3;
1168
+ var ACTIVE_WITHIN = 2 * 60;
1169
+ async function macIdleSeconds() {
1170
+ if (os2.platform() !== "darwin") return null;
1171
+ try {
1172
+ const { stdout } = await run2("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-k", "HIDIdleTime"], { timeout: 2e3 });
1173
+ const ns = stdout.match(/"HIDIdleTime"\s*=\s*(\d+)/)?.[1];
1174
+ return ns ? Number(BigInt(ns) / 1000000000n) : null;
1175
+ } catch {
1176
+ return null;
1177
+ }
1178
+ }
1179
+ async function userActiveHere() {
1180
+ const idle = await macIdleSeconds();
1181
+ return idle !== null && idle < ACTIVE_WITHIN;
1182
+ }
1183
+ function seal(key, data) {
1184
+ const nonce = randomBytes(12);
1185
+ const cipher = createCipheriv("aes-256-gcm", key, nonce);
1186
+ const body = Buffer.concat([cipher.update(JSON.stringify(data), "utf8"), cipher.final()]);
1187
+ return Buffer.concat([nonce, body, cipher.getAuthTag()]).toString("base64");
1188
+ }
1189
+ function relayUrl(value) {
1190
+ let url;
1191
+ try {
1192
+ url = new URL(value);
1193
+ } catch {
1194
+ return null;
1195
+ }
1196
+ const local = url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost");
1197
+ return url.protocol === "https:" || local ? url.href.replace(/\/$/, "") : null;
1198
+ }
1199
+ function deviceFrom(body) {
1200
+ const relay = relayUrl(body?.relay);
1201
+ if (!relay) throw new HttpError(400, "relay must be an https URL");
1202
+ if (typeof body.token !== "string" || !/^[0-9a-f]{64,200}$/i.test(body.token)) throw new HttpError(400, "token must be an APNs device token (hex)");
1203
+ if (body.environment !== "production" && body.environment !== "development") {
1204
+ throw new HttpError(400, "environment must be production or development");
1205
+ }
1206
+ if (typeof body.ticket !== "string" || !body.ticket || body.ticket.length > 128) throw new HttpError(400, "ticket required");
1207
+ if (typeof body.key !== "string" || Buffer.from(body.key, "base64").length !== 32) throw new HttpError(400, "key must be 32 bytes, base64");
1208
+ if (typeof body.keyId !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(body.keyId)) throw new HttpError(400, "keyId required");
1209
+ const kinds = body.kinds ?? {};
1210
+ return {
1211
+ relay,
1212
+ token: body.token.toLowerCase(),
1213
+ environment: body.environment,
1214
+ ticket: body.ticket,
1215
+ key: body.key,
1216
+ keyId: body.keyId,
1217
+ kinds: { requests: kinds.requests !== false, finished: kinds.finished !== false }
1218
+ };
1219
+ }
1220
+ function describeRequest(request) {
1221
+ if (request.kind === "question") return request.questions[0]?.question ?? "Claude has a question";
1222
+ const input = request.input ?? {};
1223
+ const detail2 = input.command ?? input.file_path ?? input.notebook_path ?? input.url ?? input.pattern ?? null;
1224
+ return detail2 ? `${request.tool}: ${String(detail2).split("\n")[0].slice(0, 200)}` : `Allow ${request.tool}?`;
1225
+ }
1226
+ function minutes(ms) {
1227
+ const m = Math.round(ms / 6e4);
1228
+ return m < 60 ? `${m} min` : `${Math.floor(m / 60)} h ${m % 60} min`;
1229
+ }
1230
+ var PushSender = class {
1231
+ #devices;
1232
+ #store;
1233
+ #hub;
1234
+ #foreground;
1235
+ #userActive;
1236
+ #fetch;
1237
+ // Requests a notification went out for, so their resolution can replace it.
1238
+ #notified = /* @__PURE__ */ new Set();
1239
+ /**
1240
+ * @param {object} opts
1241
+ * @param {import('./agents.js').AgentHub} opts.hub
1242
+ * @param {{ load: () => object[], save: (devices: object[]) => void }} [opts.store]
1243
+ * @param {() => boolean} [opts.foreground] whether a client shows this machine right now
1244
+ * @param {() => Promise<boolean>} [opts.userActive] whether someone is using the computer
1245
+ */
1246
+ constructor({ hub, store = null, foreground = () => false, userActive = null, fetch: fetch2 = globalThis.fetch }) {
1247
+ this.#hub = hub;
1248
+ this.#store = store;
1249
+ this.#devices = store?.load() ?? [];
1250
+ this.#foreground = foreground;
1251
+ this.#userActive = userActive ?? userActiveHere;
1252
+ this.#fetch = fetch2;
1253
+ hub.on("message", (msg) => this.#follow(msg).catch(() => {
1254
+ }));
1255
+ }
1256
+ devices() {
1257
+ return this.#devices.map(({ key, ticket, ...rest2 }) => rest2);
1258
+ }
1259
+ register(body) {
1260
+ const device = deviceFrom(body);
1261
+ this.#devices = [...this.#devices.filter((d) => d.token !== device.token), device];
1262
+ this.#save();
1263
+ }
1264
+ unregister(token) {
1265
+ this.#devices = this.#devices.filter((d) => d.token !== String(token).toLowerCase());
1266
+ this.#save();
1267
+ }
1268
+ /** Sends a test notification; throws 404 for an unknown token, 502 when it doesn't go out. */
1269
+ async test(token) {
1270
+ const device = this.#devices.find((d) => d.token === String(token ?? "").toLowerCase());
1271
+ if (!device) throw new HttpError(404, "that device is not registered");
1272
+ const result = await this.#send(device, {
1273
+ kind: "test",
1274
+ title: "Code Remote",
1275
+ body: "Notifications from this computer work.",
1276
+ category: null,
1277
+ fallback: "Notifications work"
1278
+ });
1279
+ if (!result.ok) throw new HttpError(502, `push failed: ${result.reason ?? result.status}`);
1280
+ }
1281
+ #save() {
1282
+ this.#store?.save(this.#devices);
1283
+ }
1284
+ async #quiet() {
1285
+ if (this.#foreground()) return true;
1286
+ try {
1287
+ return await this.#userActive();
1288
+ } catch {
1289
+ return false;
1290
+ }
1291
+ }
1292
+ async #follow(msg) {
1293
+ if (!this.#devices.length) return;
1294
+ if (msg.type === "request") {
1295
+ const { request } = msg;
1296
+ if (await this.#quiet()) return;
1297
+ if (!this.#hub.requests().some((r) => r.id === request.id)) return;
1298
+ this.#notified.add(request.id);
1299
+ const question = request.kind === "question";
1300
+ const answerable = question && request.questions.length === 1 && !request.questions[0].multiSelect;
1301
+ await this.#broadcast("requests", {
1302
+ kind: "request",
1303
+ title: this.#hub.session(request.session)?.title ?? "Claude Code",
1304
+ body: describeRequest(request),
1305
+ session: request.session,
1306
+ request: request.id,
1307
+ question: answerable ? request.questions[0].question : void 0,
1308
+ options: answerable ? request.questions[0].options.map((o) => o.label) : void 0,
1309
+ category: question ? "QUESTION" : "PERMISSION",
1310
+ collapseId: request.id,
1311
+ fallback: "A session needs your attention"
1312
+ });
1313
+ } else if (msg.type === "request_resolved") {
1314
+ if (!this.#notified.delete(msg.request) || msg.by === "phone") return;
1315
+ await this.#broadcast("requests", {
1316
+ kind: "resolved",
1317
+ title: this.#hub.session(msg.session)?.title ?? "Claude Code",
1318
+ body: msg.outcome === "expired" ? "No longer waiting for an answer" : "Answered on your computer",
1319
+ session: msg.session,
1320
+ request: msg.request,
1321
+ category: null,
1322
+ collapseId: msg.request,
1323
+ priority: 5,
1324
+ silent: true,
1325
+ fallback: "Answered on your computer"
1326
+ });
1327
+ } else if (msg.type === "entry" && msg.entry.kind === "turn_end" && msg.entry.duration >= FINISHED_AFTER) {
1328
+ if (await this.#quiet()) return;
1329
+ const session = this.#hub.session(msg.session);
1330
+ await this.#broadcast("finished", {
1331
+ kind: "finished",
1332
+ title: session?.title ?? "Claude Code",
1333
+ body: `Finished after ${minutes(msg.entry.duration)}`,
1334
+ session: msg.session,
1335
+ category: "DONE",
1336
+ fallback: "A session finished"
1337
+ });
1338
+ }
1339
+ }
1340
+ async #broadcast(kind, note) {
1341
+ await Promise.all(this.#devices.filter((d) => d.kinds[kind]).map((d) => this.#send(d, note)));
1342
+ }
1343
+ async #send(device, { kind, title, body, session, request, question, options, category, collapseId, priority, silent, fallback }) {
1344
+ const secret = { kind, title, body, session, request, question, options };
1345
+ const aps = { alert: { title: "Code Remote", body: fallback }, "mutable-content": 1 };
1346
+ if (category) aps.category = category;
1347
+ if (session) aps["thread-id"] = session;
1348
+ if (!silent) aps.sound = "default";
1349
+ const payload = { aps, k: device.keyId, e: seal(Buffer.from(device.key, "base64"), secret) };
1350
+ let res;
1351
+ try {
1352
+ res = await this.#fetch(`${device.relay}/v1/send`, {
1353
+ method: "POST",
1354
+ headers: { "content-type": "application/json" },
1355
+ body: JSON.stringify({
1356
+ token: device.token,
1357
+ environment: device.environment,
1358
+ ticket: device.ticket,
1359
+ payload,
1360
+ ...collapseId ? { collapseId } : {},
1361
+ ...priority ? { priority } : {}
1362
+ }),
1363
+ signal: AbortSignal.timeout(1e4)
1364
+ });
1365
+ } catch (e) {
1366
+ return { ok: false, status: 0, reason: e.message };
1367
+ }
1368
+ let reason = null;
1369
+ try {
1370
+ reason = (await res.json()).reason ?? null;
1371
+ } catch {
1372
+ }
1373
+ if (res.status === 410 || res.status === 400 && reason === "BadDeviceToken") this.unregister(device.token);
1374
+ return { ok: res.ok, status: res.status, reason };
1375
+ }
1376
+ };
1377
+
510
1378
  // ../../packages/agent-core/src/inspector.js
511
1379
  import http from "node:http";
512
1380
  import net from "node:net";
513
1381
  import zlib from "node:zlib";
514
- var MAX_ENTRIES = 200;
1382
+ var MAX_ENTRIES2 = 200;
515
1383
  var MAX_BODY = 64 * 1024;
516
1384
  var TEXTUAL = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql|[\w.+-]+\+(json|xml)))/i;
517
1385
  var nextId = 1;
@@ -590,7 +1458,7 @@ var InspectingProxy = class {
590
1458
  response: { headers: {}, tap: new BodyTap() }
591
1459
  };
592
1460
  this.#entries.push(entry2);
593
- if (this.#entries.length > MAX_ENTRIES) this.#entries.shift();
1461
+ if (this.#entries.length > MAX_ENTRIES2) this.#entries.shift();
594
1462
  return entry2;
595
1463
  }
596
1464
  /** The dev server sees itself addressed as localhost:<its port>, as without the proxy. */
@@ -687,26 +1555,36 @@ function detail(e) {
687
1555
  }
688
1556
 
689
1557
  // ../../packages/agent-core/src/sessions.js
690
- import { randomUUID } from "node:crypto";
691
- import os2 from "node:os";
1558
+ import { randomUUID as randomUUID2 } from "node:crypto";
1559
+ import os3 from "node:os";
692
1560
  import pty from "node-pty";
693
1561
  var SCROLLBACK_LIMIT = 256 * 1024;
1562
+ function shellQuote(arg) {
1563
+ return `'${String(arg).replace(/'/g, `'\\''`)}'`;
1564
+ }
694
1565
  var TerminalSession = class {
695
- constructor({ cols, rows, cwd }) {
696
- this.id = randomUUID();
1566
+ /**
1567
+ * `command` (an argv array) runs that program through a login shell (for the user's PATH)
1568
+ * instead of an interactive shell; the session ends when it exits.
1569
+ */
1570
+ constructor({ cols, rows, cwd, env = {}, command = null }) {
1571
+ this.id = randomUUID2();
697
1572
  this.createdAt = Date.now();
698
1573
  this.chunks = [];
699
1574
  this.bufferedBytes = 0;
700
1575
  this.clients = /* @__PURE__ */ new Set();
701
1576
  this.exitCode = null;
702
- const shell = process.env.SHELL || (os2.platform() === "win32" ? "powershell.exe" : "/bin/sh");
1577
+ const shell = process.env.SHELL || (os3.platform() === "win32" ? "powershell.exe" : "/bin/sh");
703
1578
  this.shell = shell;
704
- this.pty = pty.spawn(shell, ["-l"], {
1579
+ this.command = command;
1580
+ const args = command ? ["-l", "-c", `exec ${command.map(shellQuote).join(" ")}`] : ["-l"];
1581
+ this.pty = pty.spawn(shell, args, {
705
1582
  name: "xterm-256color",
706
1583
  cols,
707
1584
  rows,
708
- cwd: cwd || os2.homedir(),
709
- env: { ...process.env, TERM: "xterm-256color", COLORTERM: "truecolor" }
1585
+ cwd: cwd || os3.homedir(),
1586
+ // CODEREMOTE_TERM_ID lets `coderemote hook` tell which terminal a coding agent runs in.
1587
+ env: { ...process.env, ...env, CODEREMOTE_TERM_ID: this.id, TERM: "xterm-256color", COLORTERM: "truecolor" }
710
1588
  });
711
1589
  this.pty.onData((data) => this.#output(Buffer.from(data, "utf8")));
712
1590
  this.pty.onExit(({ exitCode }) => {
@@ -755,6 +1633,10 @@ var TerminalSession = class {
755
1633
  kill() {
756
1634
  this.pty.kill();
757
1635
  }
1636
+ /** Types into the session, as a client's keyboard would. */
1637
+ write(data) {
1638
+ this.pty.write(data);
1639
+ }
758
1640
  toJSON() {
759
1641
  return {
760
1642
  id: this.id,
@@ -768,9 +1650,16 @@ var TerminalSession = class {
768
1650
  };
769
1651
  var SessionManager = class {
770
1652
  #sessions = /* @__PURE__ */ new Map();
1653
+ /** @param {{ onExit?: (id: string) => void }} [opts] */
1654
+ constructor({ onExit } = {}) {
1655
+ this.onExit = onExit;
1656
+ }
771
1657
  create(opts) {
772
1658
  const session = new TerminalSession(opts);
773
- session.onExit = () => this.#sessions.delete(session.id);
1659
+ session.onExit = () => {
1660
+ this.#sessions.delete(session.id);
1661
+ this.onExit?.(session.id);
1662
+ };
774
1663
  this.#sessions.set(session.id, session);
775
1664
  return session;
776
1665
  }
@@ -785,6 +1674,110 @@ var SessionManager = class {
785
1674
  }
786
1675
  };
787
1676
 
1677
+ // ../../packages/agent-core/src/uploads.js
1678
+ import { execFile as execFile4 } from "node:child_process";
1679
+ import { randomUUID as randomUUID3 } from "node:crypto";
1680
+ import { createWriteStream } from "node:fs";
1681
+ import fs6 from "node:fs/promises";
1682
+ import os4 from "node:os";
1683
+ import path4 from "node:path";
1684
+ import { Transform } from "node:stream";
1685
+ import { pipeline } from "node:stream/promises";
1686
+ import { promisify as promisify2 } from "node:util";
1687
+ var run3 = promisify2(execFile4);
1688
+ var UPLOAD_LIMIT = 100 * 1024 * 1024;
1689
+ var KEEP = 7 * 24 * 60 * 60 * 1e3;
1690
+ var ID = /^[0-9a-f-]{36}$/;
1691
+ function safeName(name) {
1692
+ const base = path4.basename(String(name ?? "").replace(/\\/g, "/"));
1693
+ const clean = base.replace(/[^\p{L}\p{N}._ -]/gu, "_").replace(/^[.\s]+/, "").slice(-80).trim();
1694
+ return clean || "file";
1695
+ }
1696
+ var UploadStore = class {
1697
+ constructor(dir, { now = Date.now } = {}) {
1698
+ this.dir = dir;
1699
+ this.now = now;
1700
+ }
1701
+ /** Streams `body` to disk. 413 past `limit`; HEIC photos become JPEG on macOS. */
1702
+ async save(body, { name, type, length, limit = UPLOAD_LIMIT }) {
1703
+ if (Number(length) > limit) throw new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`);
1704
+ const id = randomUUID3();
1705
+ const day = new Date(this.now()).toISOString().slice(0, 10);
1706
+ await fs6.mkdir(path4.join(this.dir, day), { recursive: true, mode: 448 });
1707
+ let file = path4.join(this.dir, day, `${id}-${safeName(name)}`);
1708
+ let size = 0;
1709
+ const counter = new Transform({
1710
+ transform(chunk, _enc, done) {
1711
+ size += chunk.length;
1712
+ done(size > limit ? new HttpError(413, `files are limited to ${limit / 1024 / 1024} MB`) : null, chunk);
1713
+ }
1714
+ });
1715
+ try {
1716
+ await pipeline(body, counter, createWriteStream(file, { mode: 384 }));
1717
+ } catch (e) {
1718
+ await fs6.rm(file, { force: true });
1719
+ throw e;
1720
+ }
1721
+ let kind = type || "application/octet-stream";
1722
+ if ((kind === "image/heic" || /\.heic$/i.test(file)) && os4.platform() === "darwin") {
1723
+ const jpeg = file.replace(/(\.heic)?$/i, ".jpg");
1724
+ try {
1725
+ await run3("sips", ["-s", "format", "jpeg", file, "--out", jpeg], { timeout: 3e4 });
1726
+ await fs6.rm(file);
1727
+ file = jpeg;
1728
+ kind = "image/jpeg";
1729
+ size = (await fs6.stat(file)).size;
1730
+ } catch {
1731
+ }
1732
+ }
1733
+ return { id, name: path4.basename(file).slice(id.length + 1), type: kind, size, path: file };
1734
+ }
1735
+ /** The file of an upload; 404 once it's gone (swept, or never existed). */
1736
+ async path(id) {
1737
+ if (typeof id !== "string" || !ID.test(id)) throw new HttpError(400, "invalid upload id");
1738
+ let days = [];
1739
+ try {
1740
+ days = (await fs6.readdir(this.dir)).sort().reverse();
1741
+ } catch {
1742
+ }
1743
+ for (const day of days) {
1744
+ let names = [];
1745
+ try {
1746
+ names = await fs6.readdir(path4.join(this.dir, day));
1747
+ } catch {
1748
+ }
1749
+ const hit = names.find((n) => n.startsWith(`${id}-`));
1750
+ if (hit) return path4.join(this.dir, day, hit);
1751
+ }
1752
+ throw new HttpError(404, "no such upload (they are kept for 7 days)");
1753
+ }
1754
+ /** Deletes uploads older than a week. */
1755
+ async sweep() {
1756
+ let days = [];
1757
+ try {
1758
+ days = await fs6.readdir(this.dir);
1759
+ } catch {
1760
+ return;
1761
+ }
1762
+ const cutoff = this.now() - KEEP;
1763
+ for (const day of days) {
1764
+ const dir = path4.join(this.dir, day);
1765
+ let names = [];
1766
+ try {
1767
+ names = await fs6.readdir(dir);
1768
+ } catch {
1769
+ continue;
1770
+ }
1771
+ for (const n of names) {
1772
+ const file = path4.join(dir, n);
1773
+ const stat = await fs6.stat(file).catch(() => null);
1774
+ if (stat && stat.mtimeMs < cutoff) await fs6.rm(file, { force: true });
1775
+ }
1776
+ if (!(await fs6.readdir(dir).catch(() => [])).length) await fs6.rm(dir, { recursive: true, force: true });
1777
+ }
1778
+ }
1779
+ };
1780
+
788
1781
  // ../../packages/agent-core/src/server.js
789
1782
  function kindOf(stat) {
790
1783
  if (stat.isDirectory()) return "dir";
@@ -793,17 +1786,25 @@ function kindOf(stat) {
793
1786
  return "other";
794
1787
  }
795
1788
  async function describe(fullPath, name) {
796
- const lst = await fs4.lstat(fullPath);
1789
+ const lst = await fs7.lstat(fullPath);
797
1790
  const entry2 = { name, type: kindOf(lst), size: lst.size, mtime: lst.mtimeMs };
798
1791
  if (lst.isSymbolicLink()) {
799
1792
  try {
800
- entry2.target = kindOf(await fs4.stat(fullPath));
1793
+ entry2.target = kindOf(await fs7.stat(fullPath));
801
1794
  } catch {
802
1795
  entry2.target = "broken";
803
1796
  }
804
1797
  }
805
1798
  return entry2;
806
1799
  }
1800
+ var PASTE_START = "\x1B[200~";
1801
+ var PASTE_END = "\x1B[201~";
1802
+ var KEYS = { interrupt: "\x1B", cycle_mode: "\x1B[Z" };
1803
+ var COMMANDS = ["/compact", "/clear", "/model"];
1804
+ function typeable(text) {
1805
+ return text.replace(/\r\n?/g, "\n").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
1806
+ }
1807
+ var LAUNCH_MODES = ["manual", "acceptEdits", "plan", "auto"];
807
1808
  function sendJson(res, status2, body) {
808
1809
  const json = JSON.stringify(body);
809
1810
  res.writeHead(status2, { "content-type": "application/json", "content-length": Buffer.byteLength(json) });
@@ -818,15 +1819,150 @@ async function readJson(req) {
818
1819
  throw new HttpError(400, "invalid json");
819
1820
  }
820
1821
  }
821
- function createAgentServer({ token, root: root2, tunnelToken = async () => null, preview = null, tunnelStatus: tunnelStatus2 = null }) {
1822
+ function createAgentServer({
1823
+ token,
1824
+ root: root2,
1825
+ tunnelToken = async () => null,
1826
+ preview = null,
1827
+ tunnelStatus: tunnelStatus2 = null,
1828
+ hookSecret = null,
1829
+ hookBin = null,
1830
+ uploadsDir = null,
1831
+ claudeCommand = "claude",
1832
+ codexCommand = "codex",
1833
+ codexHome = process.env.CODEX_HOME || path5.join(os5.homedir(), ".codex"),
1834
+ pluginInstalled = null,
1835
+ agents = new AgentHub(),
1836
+ pushStore: pushStore2 = null,
1837
+ pushOptions = {}
1838
+ }) {
822
1839
  const handoff = new TunnelHandoff({ token, mint: tunnelToken });
823
1840
  const sandbox = new Sandbox(root2);
824
- const sessions = new SessionManager();
1841
+ const sessions = new SessionManager({ onExit: (id) => agents.terminalExited(id) });
825
1842
  const expected = Buffer.from(`Bearer ${token}`);
1843
+ const uploads = uploadsDir ? new UploadStore(uploadsDir) : null;
1844
+ const sweep = setInterval(() => {
1845
+ agents.sweep();
1846
+ uploads?.sweep().catch(() => {
1847
+ });
1848
+ }, 10 * 60 * 1e3);
1849
+ sweep.unref();
1850
+ let foregroundAt = 0;
1851
+ const clientInForeground = () => Date.now() - foregroundAt < 3e4;
1852
+ const push2 = new PushSender({ hub: agents, store: pushStore2, foreground: clientInForeground, ...pushOptions });
1853
+ if (!agents.presenceSet) {
1854
+ agents.present = async () => !clientInForeground() && await (pushOptions.userActive ?? userActiveHere)();
1855
+ }
826
1856
  function authorized(req) {
827
1857
  const got = Buffer.from(req.headers.authorization || "");
828
1858
  return got.length === expected.length && timingSafeEqual2(got, expected);
829
1859
  }
1860
+ function terminalEnv() {
1861
+ return {
1862
+ CODEREMOTE_AGENT_PORT: String(server.address()?.port ?? ""),
1863
+ ...hookBin ? { CODEREMOTE_HOOK_BIN: hookBin } : {}
1864
+ };
1865
+ }
1866
+ function typingTarget(id, { whileWaiting }) {
1867
+ const session = typeof id === "string" ? agents.session(id) : null;
1868
+ if (!session) throw new HttpError(404, "no such session");
1869
+ const term = session.terminal && session.state !== "ended" ? sessions.get(session.terminal) : null;
1870
+ if (!term) throw new HttpError(409, "this session runs outside Code Remote; it can be followed but not typed into");
1871
+ if (!whileWaiting && session.state === "waiting") throw new HttpError(409, "answer the pending request first");
1872
+ return { session, term };
1873
+ }
1874
+ async function typeInto(term, text) {
1875
+ term.write(`${PASTE_START}${text}${PASTE_END}`);
1876
+ await new Promise((r) => setTimeout(r, 300));
1877
+ term.write("\r");
1878
+ }
1879
+ function uploadPath(id) {
1880
+ if (!uploads) throw new HttpError(400, "this agent takes no uploads");
1881
+ return uploads.path(id);
1882
+ }
1883
+ async function attachmentPaths(ids) {
1884
+ if (ids === void 0) return [];
1885
+ if (!Array.isArray(ids) || ids.some((a) => typeof a !== "string")) throw new HttpError(400, "attachments must be upload ids");
1886
+ return Promise.all(ids.map((a) => uploadPath(a)));
1887
+ }
1888
+ function loginShell(script) {
1889
+ const shell = process.env.SHELL || "/bin/sh";
1890
+ return new Promise((resolve) => {
1891
+ execFile5(
1892
+ shell,
1893
+ ["-l", "-c", script],
1894
+ { timeout: 2e4, maxBuffer: 16 * 1024 * 1024 },
1895
+ (err, out) => resolve(err ? null : out)
1896
+ );
1897
+ });
1898
+ }
1899
+ const launchers = {
1900
+ claude: {
1901
+ command: claudeCommand,
1902
+ name: "Claude Code",
1903
+ args: ({ id, resume, model, permissionMode }) => [
1904
+ ...resume ? ["--resume", resume] : ["--session-id", id],
1905
+ ...model ? ["--model", model] : [],
1906
+ ...permissionMode ? ["--permission-mode", permissionMode] : [],
1907
+ ...uploadsDir ? ["--add-dir", uploadsDir] : []
1908
+ ],
1909
+ // Fullscreen rendering can fail to start while no client is attached to answer the
1910
+ // terminal's queries; the classic renderer doesn't need them.
1911
+ env: { CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN: "1" },
1912
+ async plugin() {
1913
+ const list = JSON.parse(await loginShell(`${shellQuote(claudeCommand)} plugin list --json`) ?? "[]");
1914
+ return list.some((p) => p.id === "coderemote@coderemote" && p.enabled !== false);
1915
+ },
1916
+ missingPlugin: "Claude Code needs the Code Remote plugin first: run `coderemote setup claude` once on the computer."
1917
+ },
1918
+ codex: {
1919
+ command: codexCommand,
1920
+ name: "Codex",
1921
+ // Codex's sandbox reads anywhere already; --add-dir would make the uploads writable.
1922
+ // Without -a, recent Codex rejects every escalation outright ("granular" policy), so a
1923
+ // session started here asks, and its requests reach the phone or the terminal.
1924
+ args: ({ resume, model, approvalPolicy }) => [
1925
+ ...resume ? ["resume", resume] : [],
1926
+ ...model ? ["-m", model] : [],
1927
+ ...approvalPolicy ? ["-a", approvalPolicy] : []
1928
+ ],
1929
+ env: {},
1930
+ async plugin() {
1931
+ const list = JSON.parse(await loginShell(`${shellQuote(codexCommand)} plugin list --marketplace coderemote --json`) ?? "{}");
1932
+ return (list.installed ?? []).some((p) => p.pluginId === "coderemote@coderemote" && p.enabled !== false);
1933
+ },
1934
+ missingPlugin: "Codex needs the Code Remote plugin first: run `coderemote setup codex` once on the computer, then trust its hooks with `/hooks` in Codex."
1935
+ }
1936
+ };
1937
+ async function installed(agent) {
1938
+ return Boolean((await loginShell(`command -v ${shellQuote(launchers[agent].command)}`))?.trim());
1939
+ }
1940
+ const plugins = {};
1941
+ async function hasPlugin(agent) {
1942
+ const known = plugins[agent];
1943
+ if (known && Date.now() - known.checkedAt < 3e4) return known.installed;
1944
+ let found = false;
1945
+ try {
1946
+ found = pluginInstalled ? await pluginInstalled(agent) : await launchers[agent].plugin();
1947
+ } catch {
1948
+ }
1949
+ plugins[agent] = { installed: found, checkedAt: Date.now() };
1950
+ return found;
1951
+ }
1952
+ async function codexModels() {
1953
+ try {
1954
+ const { models } = JSON.parse(await fs7.readFile(path5.join(codexHome, "models_cache.json"), "utf8"));
1955
+ 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 ?? "" }));
1956
+ } catch {
1957
+ return [];
1958
+ }
1959
+ }
1960
+ function hookAuthorized(req) {
1961
+ if (!hookSecret) return false;
1962
+ const got = Buffer.from(String(req.headers[HOOK_SECRET_HEADER] || ""));
1963
+ const want = Buffer.from(hookSecret);
1964
+ return got.length === want.length && timingSafeEqual2(got, want);
1965
+ }
830
1966
  function previewPort(value, ownPort) {
831
1967
  if (!preview) throw new HttpError(404, "previews need a tunnel");
832
1968
  const port2 = Number(value);
@@ -865,20 +2001,20 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
865
2001
  async function repoOf(p) {
866
2002
  const repo = await sandbox.resolve(p);
867
2003
  try {
868
- await fs4.access(path3.join(repo, ".git"));
2004
+ await fs7.access(path5.join(repo, ".git"));
869
2005
  } catch {
870
2006
  throw new HttpError(400, "not a git repository");
871
2007
  }
872
2008
  return repo;
873
2009
  }
874
2010
  async function filesIn(repo, files) {
875
- if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path3.isAbsolute(f))) {
2011
+ if (!Array.isArray(files) || !files.length || files.some((f) => typeof f !== "string" || !f || path5.isAbsolute(f))) {
876
2012
  throw new HttpError(400, "files must be a non-empty list of paths relative to the repository");
877
2013
  }
878
2014
  return Promise.all(
879
2015
  files.map(async (f) => {
880
- const abs = await sandbox.resolve(path3.join(repo, f), { allowMissing: true });
881
- const rel = path3.relative(repo, abs);
2016
+ const abs = await sandbox.resolve(path5.join(repo, f), { allowMissing: true });
2017
+ const rel = path5.relative(repo, abs);
882
2018
  if (!rel || rel.startsWith("..")) throw new HttpError(403, "path is outside the repository");
883
2019
  return rel;
884
2020
  })
@@ -887,52 +2023,52 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
887
2023
  const routes = {
888
2024
  [Routes.info]: async () => ({
889
2025
  protocol: PROTOCOL_VERSION,
890
- hostname: os3.hostname(),
891
- platform: os3.platform(),
2026
+ hostname: os5.hostname(),
2027
+ platform: os5.platform(),
892
2028
  root: sandbox.root,
893
- user: os3.userInfo().username,
2029
+ user: os5.userInfo().username,
894
2030
  shell: process.env.SHELL || null
895
2031
  }),
896
2032
  [Routes.fsList]: async (url) => {
897
2033
  const dir = await sandbox.resolve(url.searchParams.get("path"));
898
- const names = await fs4.readdir(dir);
899
- const entries = (await Promise.all(names.map((n) => describe(path3.join(dir, n), n).catch(() => null)))).filter(Boolean);
2034
+ const names = await fs7.readdir(dir);
2035
+ const entries = (await Promise.all(names.map((n) => describe(path5.join(dir, n), n).catch(() => null)))).filter(Boolean);
900
2036
  return { path: dir, entries };
901
2037
  },
902
2038
  [Routes.fsStat]: async (url) => {
903
2039
  const p = await sandbox.resolve(url.searchParams.get("path"));
904
- return { path: p, ...await describe(p, path3.basename(p)) };
2040
+ return { path: p, ...await describe(p, path5.basename(p)) };
905
2041
  },
906
2042
  [Routes.fsRead]: async (url, req, res) => {
907
2043
  const p = await sandbox.resolve(url.searchParams.get("path"));
908
- const stat = await fs4.stat(p);
2044
+ const stat = await fs7.stat(p);
909
2045
  if (!stat.isFile()) throw new HttpError(400, "not a file");
910
2046
  res.writeHead(200, { "content-type": "application/octet-stream", "content-length": stat.size });
911
- await pipeline(createReadStream(p), res);
2047
+ await pipeline2(createReadStream(p), res);
912
2048
  },
913
2049
  [Routes.fsWrite]: async (url, req) => {
914
2050
  const p = await sandbox.resolveChild(url.searchParams.get("path"), { allowMissing: true });
915
2051
  const tmp = `${p}.vsremote-${process.pid}-${Date.now()}.tmp`;
916
2052
  try {
917
- await pipeline(req, createWriteStream(tmp));
918
- await fs4.rename(tmp, p);
2053
+ await pipeline2(req, createWriteStream2(tmp));
2054
+ await fs7.rename(tmp, p);
919
2055
  } catch (e) {
920
- await fs4.rm(tmp, { force: true });
2056
+ await fs7.rm(tmp, { force: true });
921
2057
  throw e;
922
2058
  }
923
- return describe(p, path3.basename(p));
2059
+ return describe(p, path5.basename(p));
924
2060
  },
925
2061
  [Routes.fsMkdir]: async (url) => {
926
- await fs4.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
2062
+ await fs7.mkdir(await sandbox.resolve(url.searchParams.get("path"), { allowMissing: true }), { recursive: true });
927
2063
  return {};
928
2064
  },
929
2065
  [Routes.fsRename]: async (url, req) => {
930
2066
  const { from, to } = await readJson(req);
931
- await fs4.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
2067
+ await fs7.rename(await sandbox.resolveChild(from), await sandbox.resolveChild(to, { allowMissing: true }));
932
2068
  return {};
933
2069
  },
934
2070
  [Routes.fsDelete]: async (url) => {
935
- await fs4.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
2071
+ await fs7.rm(await sandbox.resolveChild(url.searchParams.get("path")), { recursive: true });
936
2072
  return {};
937
2073
  },
938
2074
  [Routes.gitRepos]: async () => ({ repos: await findRepos(sandbox.root) }),
@@ -1047,6 +2183,127 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
1047
2183
  if (!issued) throw new HttpError(404, "this tunnel is anonymous");
1048
2184
  return issued;
1049
2185
  },
2186
+ [Routes.agentsSessions]: async () => ({ sessions: agents.sessions() }),
2187
+ [Routes.agentsTimeline]: async (url) => agents.timeline(url.searchParams.get("session"), parseInt(url.searchParams.get("after"), 10) || 0),
2188
+ [Routes.agentsSetup]: async () => {
2189
+ const [claude, codex] = await Promise.all([installed("claude"), installed("codex")]);
2190
+ const [plugin, codexPlugin] = await Promise.all([claude && hasPlugin("claude"), codex && hasPlugin("codex")]);
2191
+ return { claude, plugin: Boolean(plugin), codex, codexPlugin: Boolean(codexPlugin), codexModels: codex ? await codexModels() : [] };
2192
+ },
2193
+ [Routes.agentsLaunch]: async (url, req) => {
2194
+ const { agent = "claude", cwd, model, permissionMode, approvalPolicy, resume, cols, rows } = await readJson(req);
2195
+ if (approvalPolicy !== void 0 && !["on-request", "never"].includes(approvalPolicy)) {
2196
+ throw new HttpError(400, "approvalPolicy must be on-request or never");
2197
+ }
2198
+ if (!Object.hasOwn(launchers, agent)) throw new HttpError(400, `agent must be one of ${Object.keys(launchers).join(", ")}`);
2199
+ const launcher = launchers[agent];
2200
+ let dir;
2201
+ try {
2202
+ dir = await sandbox.resolve(cwd || sandbox.root);
2203
+ } catch (e) {
2204
+ throw new HttpError(400, e.message);
2205
+ }
2206
+ if (!(await fs7.stat(dir)).isDirectory()) throw new HttpError(400, "cwd must be a folder");
2207
+ if (model !== void 0 && (typeof model !== "string" || !/^[A-Za-z0-9._[\]-]{1,80}$/.test(model))) {
2208
+ throw new HttpError(400, "invalid model");
2209
+ }
2210
+ if (permissionMode !== void 0 && !LAUNCH_MODES.includes(permissionMode)) {
2211
+ throw new HttpError(400, `permissionMode must be one of ${LAUNCH_MODES.join(", ")}`);
2212
+ }
2213
+ if (resume !== void 0 && (typeof resume !== "string" || !/^[0-9a-f-]{8,64}$/i.test(resume))) {
2214
+ throw new HttpError(400, "resume must be a session id");
2215
+ }
2216
+ if (resume && !agents.canResume(resume)) {
2217
+ throw new HttpError(409, "that session is still open; continue it where it runs, or end it first");
2218
+ }
2219
+ if (!await installed(agent)) {
2220
+ throw Object.assign(new HttpError(400, `${launcher.name} is not installed on this computer (no \`${launcher.command}\` command).`), {
2221
+ code: agent === "claude" ? "ENOCLAUDE" : "ENOAGENT"
2222
+ });
2223
+ }
2224
+ if (!await hasPlugin(agent)) {
2225
+ throw Object.assign(new HttpError(400, launcher.missingPlugin), { code: "ENOPLUGIN" });
2226
+ }
2227
+ const id = resume ?? randomUUID4();
2228
+ if (uploadsDir) await fs7.mkdir(uploadsDir, { recursive: true, mode: 448 });
2229
+ const argv2 = [
2230
+ launcher.command,
2231
+ ...launcher.args({
2232
+ id,
2233
+ resume,
2234
+ model,
2235
+ permissionMode: agent === "claude" ? permissionMode : void 0,
2236
+ approvalPolicy: agent === "codex" ? approvalPolicy ?? "on-request" : void 0
2237
+ })
2238
+ ];
2239
+ const term = sessions.create({
2240
+ cols: Number.isInteger(cols) && cols > 0 ? cols : 80,
2241
+ rows: Number.isInteger(rows) && rows > 0 ? rows : 24,
2242
+ cwd: dir,
2243
+ env: { ...terminalEnv(), ...launcher.env },
2244
+ command: argv2
2245
+ });
2246
+ return { terminal: term.id, session: agents.launched({ id, terminal: term.id, cwd: dir, agent }) };
2247
+ },
2248
+ [Routes.agentsPrompt]: async (url, req) => {
2249
+ const { session: id, text, attachments } = await readJson(req);
2250
+ if (typeof text !== "string" || !text.trim() || text.length > 1e5) {
2251
+ throw new HttpError(400, "text must be a non-empty string");
2252
+ }
2253
+ const { term } = typingTarget(id, { whileWaiting: false });
2254
+ const paths = await attachmentPaths(attachments);
2255
+ const typed = typeable([text.trim(), ...paths].join("\n"));
2256
+ const prompt = randomUUID4();
2257
+ agents.prompted(id, { id: prompt, text: typed, paths });
2258
+ await typeInto(term, typed);
2259
+ return { prompt };
2260
+ },
2261
+ [Routes.agentsUpload]: async (url, req) => {
2262
+ if (!uploads) throw new HttpError(404, "this agent takes no uploads");
2263
+ let name = req.headers["x-filename"];
2264
+ try {
2265
+ name = decodeURIComponent(name ?? "");
2266
+ } catch {
2267
+ }
2268
+ const { id, name: saved, type, size } = await uploads.save(req, {
2269
+ name,
2270
+ type: req.headers["content-type"],
2271
+ length: req.headers["content-length"]
2272
+ });
2273
+ return { id, name: saved, type, size };
2274
+ },
2275
+ [Routes.agentsKeys]: async (url, req) => {
2276
+ const { session: id, action, command } = await readJson(req);
2277
+ const allowed = agents.session(typeof id === "string" ? id : "")?.capabilities?.keys;
2278
+ if (allowed && !allowed.includes(action === "command" ? command : action)) {
2279
+ throw new HttpError(400, `this session takes ${allowed.join(", ")}`);
2280
+ }
2281
+ if (action === "command") {
2282
+ if (!COMMANDS.includes(command)) throw new HttpError(400, `command must be one of ${COMMANDS.join(", ")}`);
2283
+ await typeInto(typingTarget(id, { whileWaiting: false }).term, command);
2284
+ } else if (Object.hasOwn(KEYS, action)) {
2285
+ typingTarget(id, { whileWaiting: true }).term.write(KEYS[action]);
2286
+ } else {
2287
+ throw new HttpError(400, `action must be one of ${[...Object.keys(KEYS), "command"].join(", ")}`);
2288
+ }
2289
+ return {};
2290
+ },
2291
+ [Routes.agentsAnswer]: async (url, req) => {
2292
+ agents.answer(await readJson(req));
2293
+ return {};
2294
+ },
2295
+ [Routes.pushDeviceSet]: async (url, req) => {
2296
+ push2.register(await readJson(req));
2297
+ return {};
2298
+ },
2299
+ [Routes.pushDeviceDelete]: async (url) => {
2300
+ push2.unregister(url.searchParams.get("token") ?? "");
2301
+ return {};
2302
+ },
2303
+ [Routes.pushTest]: async (url, req) => {
2304
+ await push2.test((await readJson(req)).token);
2305
+ return {};
2306
+ },
1050
2307
  [Routes.termKill]: async (url) => {
1051
2308
  const s = sessions.get(url.searchParams.get("id"));
1052
2309
  if (!s) throw new HttpError(404, "no such session");
@@ -1065,6 +2322,7 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
1065
2322
  return res.end(LANDING_HTML);
1066
2323
  }
1067
2324
  if (req.method === "GET" && url.pathname === TUNNEL_HANDOFF_PATH) return handoffRedirect(url, res);
2325
+ if (`${req.method} ${url.pathname}` === Routes.agentsHook) return hookDelivery(url, req, res);
1068
2326
  if (!authorized(req)) return sendJson(res, 401, { error: "unauthorized" });
1069
2327
  const handler = routes[`${req.method} ${url.pathname}`];
1070
2328
  if (!handler) return sendJson(res, 404, { error: "not found" });
@@ -1077,6 +2335,28 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
1077
2335
  else res.destroy(e);
1078
2336
  }
1079
2337
  });
2338
+ async function hookDelivery(url, req, res) {
2339
+ if (!hookAuthorized(req)) return sendJson(res, 401, { error: "unauthorized" });
2340
+ plugins[url.searchParams.get("agent") || "claude"] = { installed: true, checkedAt: Date.now() };
2341
+ const terminal = url.searchParams.get("terminal");
2342
+ const closed = new AbortController();
2343
+ res.on("close", () => {
2344
+ if (!res.writableFinished) closed.abort();
2345
+ });
2346
+ try {
2347
+ const output = await agents.handle(await readJson(req), {
2348
+ agent: url.searchParams.get("agent") || "claude",
2349
+ terminal: terminal && sessions.get(terminal) ? terminal : null,
2350
+ signal: closed.signal
2351
+ });
2352
+ if (res.destroyed) return;
2353
+ if (output) return sendJson(res, 200, output);
2354
+ res.writeHead(200, { "content-length": 0 });
2355
+ res.end();
2356
+ } catch (e) {
2357
+ if (!res.headersSent) sendJson(res, e.status || 500, { error: e.message });
2358
+ }
2359
+ }
1080
2360
  async function handoffRedirect(url, res) {
1081
2361
  const state = url.searchParams.get("state") || "";
1082
2362
  if (!/^[A-Za-z0-9_-]{8,128}$/.test(state) || !handoff.verify(url.searchParams.get("nonce"), url.searchParams.get("sig"))) {
@@ -1099,6 +2379,10 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
1099
2379
  const wss = new WebSocketServer({ noServer: true });
1100
2380
  server.on("upgrade", async (req, socket, head) => {
1101
2381
  const url = new URL(req.url, "http://localhost");
2382
+ if (url.pathname === AGENTS_WS_PATH && authorized(req)) {
2383
+ wss.handleUpgrade(req, socket, head, watchAgents);
2384
+ return;
2385
+ }
1102
2386
  if (url.pathname !== TERMINAL_WS_PATH || !authorized(req)) {
1103
2387
  socket.end("HTTP/1.1 401 Unauthorized\r\n\r\n");
1104
2388
  return;
@@ -1117,7 +2401,8 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
1117
2401
  cols: parseInt(url.searchParams.get("cols"), 10) || 80,
1118
2402
  rows: parseInt(url.searchParams.get("rows"), 10) || 24,
1119
2403
  // New shells start in the shared folder unless a subfolder is asked for.
1120
- cwd: await sandbox.resolve(url.searchParams.get("cwd") || sandbox.root)
2404
+ cwd: await sandbox.resolve(url.searchParams.get("cwd") || sandbox.root),
2405
+ env: terminalEnv()
1121
2406
  });
1122
2407
  } catch (e) {
1123
2408
  socket.end(`HTTP/1.1 ${e.status || (e.code === "ENOENT" ? 404 : 500)} Error\r
@@ -1128,21 +2413,42 @@ function createAgentServer({ token, root: root2, tunnelToken = async () => null,
1128
2413
  }
1129
2414
  wss.handleUpgrade(req, socket, head, (ws) => session.attach(ws));
1130
2415
  });
2416
+ function watchAgents(ws) {
2417
+ const send = (msg) => ws.send(JSON.stringify(msg));
2418
+ send({ type: "snapshot", sessions: agents.sessions(), requests: agents.requests() });
2419
+ agents.on("message", send);
2420
+ ws.on("message", (data, isBinary) => {
2421
+ if (isBinary) return;
2422
+ let msg;
2423
+ try {
2424
+ msg = JSON.parse(data.toString("utf8"));
2425
+ } catch {
2426
+ return;
2427
+ }
2428
+ if (msg.type === AgentsClientMessage.presence) foregroundAt = msg.foreground === true ? Date.now() : 0;
2429
+ });
2430
+ ws.on("close", () => agents.off("message", send));
2431
+ }
1131
2432
  server.on("close", () => {
2433
+ clearInterval(sweep);
1132
2434
  sessions.killAll();
1133
2435
  for (const proxy of proxies.values()) proxy.close();
1134
2436
  });
1135
2437
  server.killAllSessions = () => sessions.killAll();
2438
+ server.agents = agents;
2439
+ server.clientInForeground = clientInForeground;
2440
+ server.push = push2;
2441
+ server.agentsUploadPath = (id) => uploadPath(id);
1136
2442
  return server;
1137
2443
  }
1138
2444
 
1139
2445
  // src/devtunnel.js
1140
- import { execFile as execFile3 } from "node:child_process";
2446
+ import { execFile as execFile6 } from "node:child_process";
1141
2447
  import { createHash } from "node:crypto";
1142
- import fs5 from "node:fs";
1143
- import path4 from "node:path";
1144
- import { promisify } from "node:util";
1145
- var run2 = promisify(execFile3);
2448
+ import fs8 from "node:fs";
2449
+ import path6 from "node:path";
2450
+ import { promisify as promisify3 } from "node:util";
2451
+ var run4 = promisify3(execFile6);
1146
2452
  var DOWNLOAD_BASE = "https://tunnelsassetsprod.blob.core.windows.net/cli";
1147
2453
  var BUILDS = {
1148
2454
  "darwin-arm64": "osx-arm64-devtunnel",
@@ -1154,15 +2460,15 @@ var MICROSOFT_TEAM_ID = "UBF8T346G9";
1154
2460
  var REFRESH_INTERVAL_MS = 24 * 36e5;
1155
2461
  function executable(file) {
1156
2462
  try {
1157
- fs5.accessSync(file, fs5.constants.X_OK);
1158
- return fs5.statSync(file).isFile();
2463
+ fs8.accessSync(file, fs8.constants.X_OK);
2464
+ return fs8.statSync(file).isFile();
1159
2465
  } catch {
1160
2466
  return false;
1161
2467
  }
1162
2468
  }
1163
2469
  function onPath(name) {
1164
- for (const dir of (process.env.PATH || "").split(path4.delimiter)) {
1165
- if (dir && executable(path4.join(dir, name))) return path4.join(dir, name);
2470
+ for (const dir of (process.env.PATH || "").split(path6.delimiter)) {
2471
+ if (dir && executable(path6.join(dir, name))) return path6.join(dir, name);
1166
2472
  }
1167
2473
  return null;
1168
2474
  }
@@ -1180,11 +2486,11 @@ async function ensureDevtunnel({ dir, progress }) {
1180
2486
  `No devtunnel build for ${process.platform}-${process.arch}. Install it yourself: https://learn.microsoft.com/azure/developer/dev-tunnels/get-started`
1181
2487
  );
1182
2488
  }
1183
- const bin = path4.join(dir, "devtunnel");
1184
- const metaFile = path4.join(dir, "devtunnel.json");
2489
+ const bin = path6.join(dir, "devtunnel");
2490
+ const metaFile = path6.join(dir, "devtunnel.json");
1185
2491
  let meta = {};
1186
2492
  try {
1187
- meta = JSON.parse(fs5.readFileSync(metaFile, "utf8"));
2493
+ meta = JSON.parse(fs8.readFileSync(metaFile, "utf8"));
1188
2494
  } catch {
1189
2495
  }
1190
2496
  const installed = executable(bin);
@@ -1195,7 +2501,7 @@ async function ensureDevtunnel({ dir, progress }) {
1195
2501
  const head = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(3e3) });
1196
2502
  const lastModified = head.headers.get("last-modified");
1197
2503
  if (head.ok && lastModified && lastModified === meta.lastModified) {
1198
- fs5.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
2504
+ fs8.writeFileSync(metaFile, JSON.stringify({ ...meta, checkedAt: Date.now() }));
1199
2505
  return bin;
1200
2506
  }
1201
2507
  } catch {
@@ -1206,12 +2512,12 @@ async function ensureDevtunnel({ dir, progress }) {
1206
2512
  return bin;
1207
2513
  }
1208
2514
  async function download(url, bin, metaFile, progress) {
1209
- fs5.mkdirSync(path4.dirname(bin), { recursive: true, mode: 448 });
2515
+ fs8.mkdirSync(path6.dirname(bin), { recursive: true, mode: 448 });
1210
2516
  const res = await fetch(url, { signal: AbortSignal.timeout(12e4) });
1211
2517
  if (!res.ok || !res.body) throw new Error(`Downloading devtunnel failed: HTTP ${res.status} from ${url}`);
1212
2518
  const total = Number(res.headers.get("content-length")) || 0;
1213
2519
  const tmp = `${bin}.${process.pid}.download`;
1214
- const out = fs5.createWriteStream(tmp, { mode: 493 });
2520
+ const out = fs8.createWriteStream(tmp, { mode: 493 });
1215
2521
  const md5 = createHash("md5");
1216
2522
  let received = 0;
1217
2523
  try {
@@ -1224,27 +2530,126 @@ async function download(url, bin, metaFile, progress) {
1224
2530
  await new Promise((resolve, reject) => out.end((e) => e ? reject(e) : resolve()));
1225
2531
  if (total && received !== total) throw new Error(`devtunnel download was cut short (${received} of ${total} bytes)`);
1226
2532
  await verify(tmp, md5.digest("base64"), res.headers.get("content-md5"));
1227
- fs5.chmodSync(tmp, 493);
1228
- fs5.renameSync(tmp, bin);
2533
+ fs8.chmodSync(tmp, 493);
2534
+ fs8.renameSync(tmp, bin);
1229
2535
  } catch (e) {
1230
2536
  out.destroy();
1231
- fs5.rmSync(tmp, { force: true });
2537
+ fs8.rmSync(tmp, { force: true });
1232
2538
  throw e;
1233
2539
  }
1234
- fs5.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
2540
+ fs8.writeFileSync(metaFile, JSON.stringify({ lastModified: res.headers.get("last-modified"), checkedAt: Date.now() }));
1235
2541
  }
1236
2542
  async function verify(file, md5, expectedMd5) {
1237
2543
  if (expectedMd5 && md5 !== expectedMd5) throw new Error("devtunnel download is corrupt (MD5 mismatch)");
1238
2544
  if (process.platform !== "darwin") return;
1239
2545
  try {
1240
- await run2("codesign", ["--verify", "--strict", file]);
1241
- const { stderr } = await run2("codesign", ["-dv", file]);
2546
+ await run4("codesign", ["--verify", "--strict", file]);
2547
+ const { stderr } = await run4("codesign", ["-dv", file]);
1242
2548
  if (!stderr.includes(`TeamIdentifier=${MICROSOFT_TEAM_ID}`)) throw new Error("not signed by Microsoft");
1243
2549
  } catch (e) {
1244
2550
  throw new Error(`devtunnel download failed its signature check: ${e.message}`);
1245
2551
  }
1246
2552
  }
1247
2553
 
2554
+ // src/hook.js
2555
+ import { randomBytes as randomBytes2 } from "node:crypto";
2556
+ import fs9 from "node:fs";
2557
+ import http3 from "node:http";
2558
+ import path7 from "node:path";
2559
+ var CONNECT_TIMEOUT = 1e3;
2560
+ var EVENT_TIMEOUT = 5e3;
2561
+ function ensureHookSecret(dir) {
2562
+ const file = path7.join(dir, "hook-secret");
2563
+ try {
2564
+ const secret2 = fs9.readFileSync(file, "utf8").trim();
2565
+ if (secret2) return secret2;
2566
+ } catch {
2567
+ }
2568
+ fs9.mkdirSync(dir, { recursive: true, mode: 448 });
2569
+ const secret = randomBytes2(32).toString("base64url");
2570
+ fs9.writeFileSync(file, secret, { mode: 384 });
2571
+ return secret;
2572
+ }
2573
+ function writeHookLauncher(dir, { node = process.execPath, script }) {
2574
+ const bin = path7.join(dir, "bin");
2575
+ fs9.mkdirSync(bin, { recursive: true, mode: 448 });
2576
+ const quote = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
2577
+ const file = path7.join(bin, "hook");
2578
+ fs9.writeFileSync(file, `#!/bin/sh
2579
+ exec ${quote(node)} ${quote(script)} hook "$@"
2580
+ `, { mode: 493 });
2581
+ fs9.chmodSync(file, 493);
2582
+ return file;
2583
+ }
2584
+ function readHookSecret(dir) {
2585
+ try {
2586
+ return fs9.readFileSync(path7.join(dir, "hook-secret"), "utf8").trim() || null;
2587
+ } catch {
2588
+ return null;
2589
+ }
2590
+ }
2591
+ function real(p) {
2592
+ try {
2593
+ return fs9.realpathSync(p);
2594
+ } catch {
2595
+ return path7.resolve(p);
2596
+ }
2597
+ }
2598
+ var within = (dir, root2) => dir === root2 || dir.startsWith(root2.endsWith(path7.sep) ? root2 : root2 + path7.sep);
2599
+ function hookTarget(agents, cwd, env = process.env) {
2600
+ const port2 = parseInt(env.CODEREMOTE_AGENT_PORT, 10);
2601
+ if (port2 && agents.some((a) => a.port === port2)) return { port: port2, terminal: env.CODEREMOTE_TERM_ID || null };
2602
+ if (typeof cwd !== "string" || !cwd) return null;
2603
+ const dir = real(cwd);
2604
+ 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];
2605
+ return match ? { port: match.port, terminal: null } : null;
2606
+ }
2607
+ function deliverHook({ port: port2, terminal, agent, secret, body, wait }) {
2608
+ return new Promise((resolve) => {
2609
+ const query = new URLSearchParams({ agent });
2610
+ if (terminal) query.set("terminal", terminal);
2611
+ const req = http3.request({
2612
+ host: "127.0.0.1",
2613
+ port: port2,
2614
+ method: "POST",
2615
+ path: `/api/agents/hook?${query}`,
2616
+ headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body), [HOOK_SECRET_HEADER]: secret }
2617
+ });
2618
+ const fail = () => {
2619
+ req.destroy();
2620
+ resolve("");
2621
+ };
2622
+ const connect = setTimeout(fail, CONNECT_TIMEOUT);
2623
+ const overall = wait ? null : setTimeout(fail, EVENT_TIMEOUT);
2624
+ req.on("socket", (socket) => socket.once("connect", () => clearTimeout(connect)));
2625
+ req.on("error", () => resolve(""));
2626
+ req.on("response", (res) => {
2627
+ const chunks = [];
2628
+ res.on("data", (c) => chunks.push(c));
2629
+ res.on("end", () => {
2630
+ clearTimeout(overall);
2631
+ resolve(res.statusCode === 200 ? Buffer.concat(chunks).toString("utf8") : "");
2632
+ });
2633
+ res.on("error", () => resolve(""));
2634
+ });
2635
+ req.end(body);
2636
+ });
2637
+ }
2638
+ async function runHook({ input, agents, secret, agent = "claude", env = process.env }) {
2639
+ if (!secret) return "";
2640
+ let event;
2641
+ try {
2642
+ event = JSON.parse(input);
2643
+ } catch {
2644
+ return "";
2645
+ }
2646
+ const target = hookTarget(agents, event?.cwd, env);
2647
+ if (!target) return "";
2648
+ const wait = event.hook_event_name === "PermissionRequest";
2649
+ const out = await deliverHook({ ...target, agent, secret, body: input, wait });
2650
+ return wait ? out.trim() : "";
2651
+ }
2652
+
1248
2653
  // src/preview.js
1249
2654
  import { spawn } from "node:child_process";
1250
2655
  var HOST_READY_TIMEOUT_MS = 3e4;
@@ -1428,20 +2833,41 @@ ${log2}`));
1428
2833
  }
1429
2834
  };
1430
2835
 
2836
+ // src/push-store.js
2837
+ import fs10 from "node:fs";
2838
+ import path8 from "node:path";
2839
+ function pushStore(dir, port2) {
2840
+ const file = path8.join(dir, `${port2}.json`);
2841
+ return {
2842
+ load() {
2843
+ try {
2844
+ const devices = JSON.parse(fs10.readFileSync(file, "utf8"));
2845
+ return Array.isArray(devices) ? devices : [];
2846
+ } catch {
2847
+ return [];
2848
+ }
2849
+ },
2850
+ save(devices) {
2851
+ fs10.mkdirSync(dir, { recursive: true, mode: 448 });
2852
+ fs10.writeFileSync(file, JSON.stringify(devices, null, 2), { mode: 384 });
2853
+ }
2854
+ };
2855
+ }
2856
+
1431
2857
  // src/registry.js
1432
- import fs6 from "node:fs";
1433
- import path5 from "node:path";
2858
+ import fs11 from "node:fs";
2859
+ import path9 from "node:path";
1434
2860
  var AgentRegistry = class {
1435
2861
  constructor(dir) {
1436
2862
  this.dir = dir;
1437
2863
  }
1438
2864
  register({ port: port2, root: root2, url }) {
1439
- fs6.mkdirSync(this.dir, { recursive: true, mode: 448 });
2865
+ fs11.mkdirSync(this.dir, { recursive: true, mode: 448 });
1440
2866
  const file = this.#file(port2);
1441
- fs6.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
2867
+ fs11.writeFileSync(file, JSON.stringify({ pid: process.pid, port: port2, root: root2, url, startedAt: Date.now() }), { mode: 384 });
1442
2868
  const cleanup = () => {
1443
2869
  try {
1444
- if (JSON.parse(fs6.readFileSync(file, "utf8")).pid === process.pid) fs6.rmSync(file);
2870
+ if (JSON.parse(fs11.readFileSync(file, "utf8")).pid === process.pid) fs11.rmSync(file);
1445
2871
  } catch {
1446
2872
  }
1447
2873
  };
@@ -1452,25 +2878,25 @@ var AgentRegistry = class {
1452
2878
  list() {
1453
2879
  let names = [];
1454
2880
  try {
1455
- names = fs6.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
2881
+ names = fs11.readdirSync(this.dir).filter((n) => n.endsWith(".json"));
1456
2882
  } catch {
1457
2883
  return [];
1458
2884
  }
1459
2885
  const agents = [];
1460
2886
  for (const name of names) {
1461
- const file = path5.join(this.dir, name);
2887
+ const file = path9.join(this.dir, name);
1462
2888
  try {
1463
- const agent = JSON.parse(fs6.readFileSync(file, "utf8"));
2889
+ const agent = JSON.parse(fs11.readFileSync(file, "utf8"));
1464
2890
  process.kill(agent.pid, 0);
1465
2891
  agents.push(agent);
1466
2892
  } catch {
1467
- fs6.rmSync(file, { force: true });
2893
+ fs11.rmSync(file, { force: true });
1468
2894
  }
1469
2895
  }
1470
2896
  return agents.sort((a, b) => a.port - b.port);
1471
2897
  }
1472
2898
  #file(port2) {
1473
- return path5.join(this.dir, `${port2}.json`);
2899
+ return path9.join(this.dir, `${port2}.json`);
1474
2900
  }
1475
2901
  };
1476
2902
 
@@ -1534,8 +2960,8 @@ function tunnelStatus({ devtunnel: devtunnel2, tunnelId, account, anonymous: ano
1534
2960
  }
1535
2961
 
1536
2962
  // src/update.js
1537
- import fs7 from "node:fs";
1538
- import path6 from "node:path";
2963
+ import fs12 from "node:fs";
2964
+ import path10 from "node:path";
1539
2965
  var LATEST_URL = "https://registry.npmjs.org/coderemote/latest";
1540
2966
  var CHECK_INTERVAL_MS = 24 * 36e5;
1541
2967
  var SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
@@ -1559,10 +2985,10 @@ async function fetchLatest({ fetch: fetch2 = globalThis.fetch, timeoutMs = 3e3 }
1559
2985
  }
1560
2986
  }
1561
2987
  async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }) {
1562
- const file = path6.join(dir, "update.json");
2988
+ const file = path10.join(dir, "update.json");
1563
2989
  let cache = {};
1564
2990
  try {
1565
- cache = JSON.parse(fs7.readFileSync(file, "utf8"));
2991
+ cache = JSON.parse(fs12.readFileSync(file, "utf8"));
1566
2992
  } catch {
1567
2993
  }
1568
2994
  let latest = cache.latest;
@@ -1571,8 +2997,8 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
1571
2997
  if (fetched) {
1572
2998
  latest = fetched;
1573
2999
  try {
1574
- fs7.mkdirSync(dir, { recursive: true, mode: 448 });
1575
- fs7.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
3000
+ fs12.mkdirSync(dir, { recursive: true, mode: 448 });
3001
+ fs12.writeFileSync(file, JSON.stringify({ latest, checkedAt: now }));
1576
3002
  } catch {
1577
3003
  }
1578
3004
  }
@@ -1580,7 +3006,7 @@ async function availableUpdate({ current, dir, fetch: fetch2, now = Date.now() }
1580
3006
  return isNewer(latest, current) ? latest : null;
1581
3007
  }
1582
3008
  function installCommand(installedAt) {
1583
- const p = installedAt.split(path6.sep).join("/");
3009
+ const p = installedAt.split(path10.sep).join("/");
1584
3010
  if (p.includes("/pnpm/") || p.includes("/.pnpm/")) return ["pnpm", ["add", "-g", "coderemote@latest"]];
1585
3011
  if (p.includes("/.bun/")) return ["bun", ["add", "-g", "coderemote@latest"]];
1586
3012
  if (p.includes("/.yarn/") || p.includes("/yarn/global/")) return ["yarn", ["global", "add", "coderemote@latest"]];
@@ -1590,7 +3016,7 @@ function installCommand(installedAt) {
1590
3016
  // package.json
1591
3017
  var package_default = {
1592
3018
  name: "coderemote",
1593
- version: "0.3.2",
3019
+ version: "0.3.4",
1594
3020
  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.",
1595
3021
  keywords: [
1596
3022
  "remote",
@@ -1869,15 +3295,15 @@ var Spinner = class {
1869
3295
  };
1870
3296
 
1871
3297
  // src/vscode-tunnel.js
1872
- import { execFile as execFile4, spawn as spawn2 } from "node:child_process";
1873
- import { promisify as promisify2 } from "node:util";
1874
- var run3 = promisify2(execFile4);
3298
+ import { execFile as execFile7, spawn as spawn2 } from "node:child_process";
3299
+ import { promisify as promisify4 } from "node:util";
3300
+ var run5 = promisify4(execFile7);
1875
3301
  function findCode() {
1876
3302
  return process.env.CODEREMOTE_CODE || onPath("code");
1877
3303
  }
1878
3304
  async function codeTunnelSignedIn(code) {
1879
3305
  try {
1880
- const { stdout } = await run3(code, ["tunnel", "user", "show"]);
3306
+ const { stdout } = await run5(code, ["tunnel", "user", "show"]);
1881
3307
  return /^logged in/im.test(stdout);
1882
3308
  } catch {
1883
3309
  return false;
@@ -1919,24 +3345,28 @@ ${log2}`));
1919
3345
  }
1920
3346
 
1921
3347
  // bin/coderemote.js
1922
- var run4 = promisify3(execFile5);
1923
- var CONFIG_DIR = path7.join(os4.homedir(), ".coderemote");
1924
- var LEGACY_CONFIG_DIR = path7.join(os4.homedir(), ".vsremote");
1925
- if (!fs8.existsSync(CONFIG_DIR) && fs8.existsSync(LEGACY_CONFIG_DIR)) fs8.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
1926
- var CONFIG_FILE = path7.join(CONFIG_DIR, "config.json");
3348
+ var run6 = promisify5(execFile8);
3349
+ var CONFIG_DIR = path11.join(os6.homedir(), ".coderemote");
3350
+ var LEGACY_CONFIG_DIR = path11.join(os6.homedir(), ".vsremote");
3351
+ if (!fs13.existsSync(CONFIG_DIR) && fs13.existsSync(LEGACY_CONFIG_DIR)) fs13.renameSync(LEGACY_CONFIG_DIR, CONFIG_DIR);
3352
+ var CONFIG_FILE = path11.join(CONFIG_DIR, "config.json");
3353
+ var PUSH_DIR = path11.join(CONFIG_DIR, "push");
3354
+ var UPLOADS_DIR = path11.join(CONFIG_DIR, "uploads");
1927
3355
  var DEFAULT_PORT = 31580;
1928
3356
  var VERSION = package_default.version;
1929
- var PACKAGED = path7.basename(path7.dirname(fileURLToPath(import.meta.url))) === "dist";
3357
+ var PACKAGED = path11.basename(path11.dirname(fileURLToPath(import.meta.url))) === "dist";
1930
3358
  var spinner = new Spinner();
1931
3359
  function loadConfig() {
1932
3360
  let cfg2 = {};
1933
3361
  try {
1934
- cfg2 = JSON.parse(fs8.readFileSync(CONFIG_FILE, "utf8"));
3362
+ cfg2 = JSON.parse(fs13.readFileSync(CONFIG_FILE, "utf8"));
1935
3363
  } catch {
1936
3364
  }
1937
3365
  if (!cfg2.token) {
1938
- cfg2.token = randomBytes(32).toString("base64url");
3366
+ cfg2.token = randomBytes3(32).toString("base64url");
1939
3367
  saveConfig(cfg2);
3368
+ fs13.rmSync(PUSH_DIR, { recursive: true, force: true });
3369
+ fs13.rmSync(UPLOADS_DIR, { recursive: true, force: true });
1940
3370
  }
1941
3371
  if (cfg2.tunnelId && !cfg2.tunnels) {
1942
3372
  cfg2.tunnels = { [DEFAULT_PORT]: cfg2.tunnelId };
@@ -1945,8 +3375,8 @@ function loadConfig() {
1945
3375
  return cfg2;
1946
3376
  }
1947
3377
  function saveConfig(cfg2) {
1948
- fs8.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1949
- fs8.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
3378
+ fs13.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
3379
+ fs13.writeFileSync(CONFIG_FILE, JSON.stringify(cfg2, null, 2), { mode: 384 });
1950
3380
  }
1951
3381
  function portFree(port2) {
1952
3382
  return new Promise((resolve) => {
@@ -1956,7 +3386,20 @@ function portFree(port2) {
1956
3386
  });
1957
3387
  }
1958
3388
  function listen(token, port2, root2, tunnelToken, preview, status2) {
1959
- const server = createAgentServer({ token, root: root2, tunnelToken, preview, tunnelStatus: status2 });
3389
+ const hookSecret = ensureHookSecret(CONFIG_DIR);
3390
+ writeHookLauncher(CONFIG_DIR, { script: fs13.realpathSync(process.argv[1]) });
3391
+ const server = createAgentServer({
3392
+ token,
3393
+ root: root2,
3394
+ tunnelToken,
3395
+ preview,
3396
+ tunnelStatus: status2,
3397
+ hookSecret,
3398
+ // This very CLI, so coding agents in the app's terminals report through the same version.
3399
+ hookBin: fs13.realpathSync(process.argv[1]),
3400
+ pushStore: pushStore(PUSH_DIR, port2),
3401
+ uploadsDir: UPLOADS_DIR
3402
+ });
1960
3403
  return new Promise((resolve, reject) => {
1961
3404
  server.once("error", reject);
1962
3405
  server.listen(port2, "127.0.0.1", () => resolve(server));
@@ -1966,7 +3409,7 @@ var DEVTUNNEL = "devtunnel";
1966
3409
  async function resolveDevtunnel() {
1967
3410
  let installing = false;
1968
3411
  DEVTUNNEL = await ensureDevtunnel({
1969
- dir: path7.join(CONFIG_DIR, "bin"),
3412
+ dir: path11.join(CONFIG_DIR, "bin"),
1970
3413
  progress: (detail2) => {
1971
3414
  if (!installing) {
1972
3415
  installing = true;
@@ -1979,7 +3422,7 @@ async function resolveDevtunnel() {
1979
3422
  }
1980
3423
  async function devtunnel(...args) {
1981
3424
  try {
1982
- const { stdout } = await run4(DEVTUNNEL, args);
3425
+ const { stdout } = await run6(DEVTUNNEL, args);
1983
3426
  return { ok: true, out: stdout };
1984
3427
  } catch (e) {
1985
3428
  return { ok: false, out: `${e.stdout || ""}${e.stderr || ""}` || e.message };
@@ -2037,7 +3480,7 @@ ${access.out}`);
2037
3480
  if (!/not found/i.test(updated.out)) throw new Error(`devtunnel update failed:
2038
3481
  ${updated.out}`);
2039
3482
  }
2040
- const name = port2 === DEFAULT_PORT ? os4.hostname() : `${os4.hostname()} :${port2}`;
3483
+ const name = port2 === DEFAULT_PORT ? os6.hostname() : `${os6.hostname()} :${port2}`;
2041
3484
  const createArgs = ["create", "--expiration", "30d", "-d", `coderemote ${name}`, "--labels", MACHINE_LABEL, "--json"];
2042
3485
  if (anonymous2) createArgs.push("--allow-anonymous");
2043
3486
  const created = await devtunnel(...createArgs);
@@ -2142,6 +3585,9 @@ var USAGE = `Usage: coderemote <command> [options]
2142
3585
  request <ID> [PORT] headers and bodies of one request (bodies up to 64 KB are kept);
2143
3586
  auth headers, cookies and token-like fields are masked unless
2144
3587
  --show-secrets is given; --json on either for machine-readable output
3588
+ setup claude let the app follow Claude Code sessions and answer their prompts
3589
+ (installs the Code Remote plugin into Claude Code); --remove undoes it
3590
+ setup codex the same for Codex (then trust its hooks once with /hooks in Codex)
2145
3591
  version print this coderemote's version (also --version, -v)
2146
3592
  login sign in to Microsoft Dev Tunnels with GitHub (up does this when needed);
2147
3593
  --microsoft for a Microsoft account, --device-code without a browser
@@ -2163,9 +3609,9 @@ var flag = (name) => {
2163
3609
  };
2164
3610
  var anonymous = rest.includes("--allow-anonymous") || rest.includes("--no-auth");
2165
3611
  var port = flag("--port") ? parseInt(flag("--port"), 10) : DEFAULT_PORT;
2166
- var root = path7.resolve(flag("--root") ?? process.cwd());
3612
+ var root = path11.resolve(flag("--root") ?? process.cwd());
2167
3613
  var cfg = loadConfig();
2168
- var registry = new AgentRegistry(path7.join(CONFIG_DIR, "agents"));
3614
+ var registry = new AgentRegistry(path11.join(CONFIG_DIR, "agents"));
2169
3615
  async function start({ tunnel }) {
2170
3616
  const agents = registry.list();
2171
3617
  if (flag("--port")) {
@@ -2200,7 +3646,7 @@ async function start({ tunnel }) {
2200
3646
  const preview = tunnel ? new PreviewTunnel({
2201
3647
  devtunnel,
2202
3648
  command: () => DEVTUNNEL,
2203
- description: `coderemote preview ${os4.hostname()}`,
3649
+ description: `coderemote preview ${os6.hostname()}`,
2204
3650
  leftover: cfg.previewTunnels?.[port],
2205
3651
  remember: (id) => {
2206
3652
  cfg.previewTunnels = { ...cfg.previewTunnels, [port]: id };
@@ -2263,6 +3709,105 @@ async function start({ tunnel }) {
2263
3709
  });
2264
3710
  return { url, access };
2265
3711
  }
3712
+ var PLUGIN = "coderemote@coderemote";
3713
+ async function claudeCli(...args) {
3714
+ try {
3715
+ return (await run6("claude", args, { timeout: 12e4 })).stdout.trim();
3716
+ } catch (e) {
3717
+ if (e.code === "ENOENT") return null;
3718
+ throw new Error((e.stderr || e.stdout || e.message).trim());
3719
+ }
3720
+ }
3721
+ async function setupClaude() {
3722
+ const version = await claudeCli("--version");
3723
+ if (version === null) {
3724
+ console.error("Claude Code is required: install it from https://claude.com/claude-code, then run this again.");
3725
+ process.exit(1);
3726
+ }
3727
+ if (rest.includes("--remove")) {
3728
+ await claudeCli("plugin", "uninstall", PLUGIN).catch((e) => console.error(e.message));
3729
+ await claudeCli("plugin", "marketplace", "remove", "coderemote").catch(() => {
3730
+ });
3731
+ console.log("Removed the Code Remote plugin. New Claude Code sessions no longer reach the app.");
3732
+ return;
3733
+ }
3734
+ const source = flag("--marketplace") ?? "CodeRemoteApp/coderemote";
3735
+ spinner.start(`Installing the Code Remote plugin into ${version}`);
3736
+ try {
3737
+ await claudeCli("plugin", "marketplace", "add", source);
3738
+ await claudeCli("plugin", "marketplace", "update", "coderemote").catch(() => {
3739
+ });
3740
+ await claudeCli("plugin", "install", PLUGIN);
3741
+ await claudeCli("plugin", "update", PLUGIN).catch(() => {
3742
+ });
3743
+ } catch (e) {
3744
+ spinner.fail();
3745
+ console.error(e.message);
3746
+ process.exit(1);
3747
+ }
3748
+ spinner.succeed();
3749
+ console.log("\n New Claude Code sessions in a folder shared with `coderemote up` now show in the");
3750
+ console.log(" app\u2019s Agents tab, where you can answer their permission prompts and questions.");
3751
+ console.log(" Sessions already running pick it up after a restart. Undo: coderemote setup claude --remove\n");
3752
+ }
3753
+ var CODEX_PLUGIN = "coderemote@coderemote";
3754
+ async function codexCli(...args) {
3755
+ try {
3756
+ return (await run6("codex", args, { timeout: 12e4 })).stdout.trim();
3757
+ } catch (e) {
3758
+ if (e.code === "ENOENT") return null;
3759
+ throw new Error((e.stderr || e.stdout || e.message).trim());
3760
+ }
3761
+ }
3762
+ async function setupCodex() {
3763
+ const version = await codexCli("--version");
3764
+ if (version === null) {
3765
+ console.error("Codex is required: install it (npm install -g @openai/codex), then run this again.");
3766
+ process.exit(1);
3767
+ }
3768
+ const hooks = (await codexCli("features", "list")).split("\n").find((line) => /^hooks\s/.test(line));
3769
+ if (!hooks) {
3770
+ console.error(`${version} has no hooks. Update Codex to 0.139 or later (codex update), then run this again.`);
3771
+ process.exit(1);
3772
+ }
3773
+ if (rest.includes("--remove")) {
3774
+ await codexCli("plugin", "remove", CODEX_PLUGIN).catch((e) => console.error(e.message));
3775
+ await codexCli("plugin", "marketplace", "remove", "coderemote").catch(() => {
3776
+ });
3777
+ console.log("Removed the Code Remote plugin. New Codex sessions no longer reach the app.");
3778
+ return;
3779
+ }
3780
+ const given = flag("--marketplace");
3781
+ const source = given ?? "CodeRemoteApp/coderemote";
3782
+ spinner.start(`Installing the Code Remote plugin into ${version}`);
3783
+ try {
3784
+ if (/\bfalse\s*$/.test(hooks)) await codexCli("features", "enable", "hooks");
3785
+ const known = /^coderemote\s/m.test(await codexCli("plugin", "marketplace", "list"));
3786
+ if (known && given) {
3787
+ await codexCli("plugin", "marketplace", "remove", "coderemote").catch(() => {
3788
+ });
3789
+ await codexCli("plugin", "marketplace", "add", source);
3790
+ } else if (known) {
3791
+ await codexCli("plugin", "marketplace", "upgrade", "coderemote").catch(() => {
3792
+ });
3793
+ } else {
3794
+ await codexCli("plugin", "marketplace", "add", source);
3795
+ }
3796
+ await codexCli("plugin", "add", CODEX_PLUGIN);
3797
+ } catch (e) {
3798
+ spinner.fail();
3799
+ console.error(e.message);
3800
+ if (/not found in marketplace/.test(e.message) && !given) {
3801
+ console.error("Your coderemote may be newer than the published plugin; point at a checkout with --marketplace <dir>.");
3802
+ }
3803
+ process.exit(1);
3804
+ }
3805
+ spinner.succeed();
3806
+ console.log("\n One more step, once: open Codex, type /hooks and trust the Code Remote hooks.");
3807
+ console.log(" Codex runs a plugin's hooks only after you have reviewed them.");
3808
+ console.log(" Then new Codex sessions in a folder shared with `coderemote up` show in the app.");
3809
+ console.log(" Undo: coderemote setup codex --remove\n");
3810
+ }
2266
3811
  async function agentApi(agent, method, route, query = {}) {
2267
3812
  const url = new URL(route, `http://127.0.0.1:${agent.port}`);
2268
3813
  for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
@@ -2283,7 +3828,7 @@ function currentAgent() {
2283
3828
  const agents = registry.list();
2284
3829
  if (flag("--port")) return agents.find((a) => a.port === port);
2285
3830
  const here = process.cwd();
2286
- const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path7.sep)).sort((a, b) => b.root.length - a.root.length);
3831
+ const containing = agents.filter((a) => here === a.root || here.startsWith(a.root + path11.sep)).sort((a, b) => b.root.length - a.root.length);
2287
3832
  return containing[0] ?? (agents.length === 1 ? agents[0] : void 0);
2288
3833
  }
2289
3834
  async function previewPorts(agent, given) {
@@ -2307,6 +3852,32 @@ function ago(ms) {
2307
3852
  return `${Math.round(s / 86400)}d`;
2308
3853
  }
2309
3854
  switch (cmd) {
3855
+ case "hook": {
3856
+ let out = "";
3857
+ try {
3858
+ const chunks = [];
3859
+ for await (const c of process.stdin) chunks.push(c);
3860
+ out = await runHook({
3861
+ input: Buffer.concat(chunks).toString("utf8"),
3862
+ agents: registry.list(),
3863
+ secret: readHookSecret(CONFIG_DIR),
3864
+ agent: flag("--agent") ?? "claude"
3865
+ });
3866
+ } catch {
3867
+ }
3868
+ if (out) process.stdout.write(`${out}
3869
+ `);
3870
+ process.exit(0);
3871
+ }
3872
+ case "setup": {
3873
+ if (rest[0] === "claude") await setupClaude();
3874
+ else if (rest[0] === "codex") await setupCodex();
3875
+ else {
3876
+ console.error("Usage: coderemote setup claude|codex [--remove]");
3877
+ process.exit(1);
3878
+ }
3879
+ break;
3880
+ }
2310
3881
  case "serve": {
2311
3882
  await start({ tunnel: false });
2312
3883
  console.log(`coderemote agent on http://127.0.0.1:${port}
@@ -2393,7 +3964,7 @@ Token: ${cfg.token}`);
2393
3964
  break;
2394
3965
  }
2395
3966
  case "rotate-token": {
2396
- cfg.token = randomBytes(32).toString("base64url");
3967
+ cfg.token = randomBytes3(32).toString("base64url");
2397
3968
  saveConfig(cfg);
2398
3969
  const agents = registry.list();
2399
3970
  for (const agent of agents) {
@@ -2422,7 +3993,7 @@ Token: ${cfg.token}`);
2422
3993
  console.log(`coderemote ${VERSION} is the latest version.`);
2423
3994
  break;
2424
3995
  }
2425
- const [command, args] = installCommand(fs8.realpathSync(process.argv[1]));
3996
+ const [command, args] = installCommand(fs13.realpathSync(process.argv[1]));
2426
3997
  console.log(`Updating coderemote ${VERSION} \u2192 ${latest}: ${command} ${args.join(" ")}
2427
3998
  `);
2428
3999
  const code = await new Promise((resolve) => {