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