offsite 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/apps/server/dist/main.mjs +251 -138
  2. package/apps/server/dist/main.mjs.map +1 -1
  3. package/apps/web/dist/assets/HugeiconsIcon-CXlzk0hX.js +1 -0
  4. package/apps/web/dist/assets/angular-html-CPrDeWzk.js +1 -0
  5. package/apps/web/dist/assets/{angular-ts-DGEt84_g.js → angular-ts-DCisMPVX.js} +1 -1
  6. package/apps/web/dist/assets/{apl-bz_iDiNf.js → apl-BRH60u60.js} +1 -1
  7. package/apps/web/dist/assets/{astro-jXSlLJ7l.js → astro-Dq6p_VTJ.js} +1 -1
  8. package/apps/web/dist/assets/{blade-2sxYYh5A.js → blade-CC3PQRrX.js} +1 -1
  9. package/apps/web/dist/assets/c-iCihek07.js +1 -0
  10. package/apps/web/dist/assets/{chapel-DUoqr6M4.js → chapel-DmtgEXO_.js} +1 -1
  11. package/apps/web/dist/assets/{cobol-Cn0D9nVy.js → cobol-CGlx5WnO.js} +1 -1
  12. package/apps/web/dist/assets/{coffee-C1tCcS8h.js → coffee-CiiSLhB6.js} +1 -1
  13. package/apps/web/dist/assets/cpp-71xkOQct.js +1 -0
  14. package/apps/web/dist/assets/{crystal-DF-BMake.js → crystal-Bn4W9FYe.js} +1 -1
  15. package/apps/web/dist/assets/css-AeYbVOD-.js +1 -0
  16. package/apps/web/dist/assets/{edge-D8r2HbS9.js → edge-D8qeodQq.js} +1 -1
  17. package/apps/web/dist/assets/{elixir-K6buEKBc.js → elixir-BkREp_f2.js} +1 -1
  18. package/apps/web/dist/assets/{elm-B78wD7Un.js → elm-o4NJxfws.js} +1 -1
  19. package/apps/web/dist/assets/{erb-DuhNLWPu.js → erb-CpQ5AKmM.js} +1 -1
  20. package/apps/web/dist/assets/{git-rebase-C6n0imm6.js → git-rebase-BDPpS8w6.js} +1 -1
  21. package/apps/web/dist/assets/{glimmer-js-B65Nng8o.js → glimmer-js-BTqwx0oO.js} +1 -1
  22. package/apps/web/dist/assets/{glimmer-ts-Cme47tem.js → glimmer-ts-N9jKyowB.js} +1 -1
  23. package/apps/web/dist/assets/glsl-D5MA2a_X.js +1 -0
  24. package/apps/web/dist/assets/graphql-Ctjuxyvl.js +1 -0
  25. package/apps/web/dist/assets/{hack-W5aeWZGp.js → hack-ZTcFnVsP.js} +1 -1
  26. package/apps/web/dist/assets/haml-ChtpF0E7.js +1 -0
  27. package/apps/web/dist/assets/{handlebars-CYz9msjm.js → handlebars-CpsBgm7t.js} +1 -1
  28. package/apps/web/dist/assets/{html-derivative-fPnJ7c-5.js → html-derivative-DNPJNw-0.js} +1 -1
  29. package/apps/web/dist/assets/html-hnucWepv.js +1 -0
  30. package/apps/web/dist/assets/{http-DIR9LW29.js → http-kmH8AyfE.js} +1 -1
  31. package/apps/web/dist/assets/{hurl-73h4sQYH.js → hurl-DCZHXKU4.js} +1 -1
  32. package/apps/web/dist/assets/image-viewer-CBM7yEMl.js +1 -0
  33. package/apps/web/dist/assets/index-DkRVBJlp.css +2 -0
  34. package/apps/web/dist/assets/index-h6Rg_ww0.js +1206 -0
  35. package/apps/web/dist/assets/java-DdHNQ9hk.js +1 -0
  36. package/apps/web/dist/assets/javascript-obGGvMPs.js +1 -0
  37. package/apps/web/dist/assets/{jinja-wBZAPGc9.js → jinja-81yqctvv.js} +1 -1
  38. package/apps/web/dist/assets/{jison-bZuttet2.js → jison-CBjhYtSs.js} +1 -1
  39. package/apps/web/dist/assets/json-DAgxKhMr.js +1 -0
  40. package/apps/web/dist/assets/jsx-gTgmArGx.js +1 -0
  41. package/apps/web/dist/assets/jsx-runtime-BNakU3Ej.js +1 -0
  42. package/apps/web/dist/assets/{julia-Bj20nWQB.js → julia-KRkZs2hS.js} +1 -1
  43. package/apps/web/dist/assets/{just-pKKGVoyp.js → just-BuSkJCkO.js} +1 -1
  44. package/apps/web/dist/assets/{latex-DN5Fi2zg.js → latex-CvI9akcJ.js} +1 -1
  45. package/apps/web/dist/assets/{liquid-DtPrOPBs.js → liquid-VY2h0b2O.js} +1 -1
  46. package/apps/web/dist/assets/lua-FONpmTUP.js +1 -0
  47. package/apps/web/dist/assets/{marko-BAsjT454.js → marko-BRIeiu02.js} +1 -1
  48. package/apps/web/dist/assets/{mdc-DwMa0Ya_.js → mdc-CX_1c8hS.js} +1 -1
  49. package/apps/web/dist/assets/{nginx-BlYeGp57.js → nginx-BTMw9COX.js} +1 -1
  50. package/apps/web/dist/assets/{nim-DASYTjR5.js → nim-BrYFxDF2.js} +1 -1
  51. package/apps/web/dist/assets/{org-DSeAzk6D.js → org-Cd0u8vWD.js} +1 -1
  52. package/apps/web/dist/assets/{perl-BGPYGeVd.js → perl-1pN6i0RM.js} +1 -1
  53. package/apps/web/dist/assets/{php-kgT6t38x.js → php-C7NaTRTO.js} +1 -1
  54. package/apps/web/dist/assets/{pug-COTHnC13.js → pug-Co4hciMS.js} +1 -1
  55. package/apps/web/dist/assets/{qml-DhqsM4c7.js → qml-_a-QhfPD.js} +1 -1
  56. package/apps/web/dist/assets/r-BHcO8Vjq.js +1 -0
  57. package/apps/web/dist/assets/{razor-BGH58TzE.js → razor-DVe451rE.js} +1 -1
  58. package/apps/web/dist/assets/regexp-DR20uMnK.js +1 -0
  59. package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  60. package/apps/web/dist/assets/{rst-UweowKJr.js → rst-hVyWO_Fc.js} +1 -1
  61. package/apps/web/dist/assets/{ruby-DtfMo-6m.js → ruby-Zd-8tM08.js} +1 -1
  62. package/apps/web/dist/assets/{sas-DkQGY_o2.js → sas-D3h2pRQf.js} +1 -1
  63. package/apps/web/dist/assets/scss-DDJaLTnA.js +1 -0
  64. package/apps/web/dist/assets/shellscript-Da78Ox-3.js +1 -0
  65. package/apps/web/dist/assets/{shellsession-CT0a4IuI.js → shellsession-CVsQGYfZ.js} +1 -1
  66. package/apps/web/dist/assets/{soy-B3ufTOzS.js → soy-CXX9LSpm.js} +1 -1
  67. package/apps/web/dist/assets/sql-ByhjMBKn.js +1 -0
  68. package/apps/web/dist/assets/{stata-BYhfUPEu.js → stata-CjrVU8-t.js} +1 -1
  69. package/apps/web/dist/assets/{surrealql-UIZa84-V.js → surrealql-BZoq2lXR.js} +1 -1
  70. package/apps/web/dist/assets/{svelte-SkphxuD6.js → svelte-CZ5lJGR6.js} +1 -1
  71. package/apps/web/dist/assets/{templ-C8T3Zgrx.js → templ-EWf5LxrK.js} +1 -1
  72. package/apps/web/dist/assets/tex-BK-H2Wn4.js +1 -0
  73. package/apps/web/dist/assets/{ts-tags-Bk8_Hqg_.js → ts-tags-C4CDubZg.js} +1 -1
  74. package/apps/web/dist/assets/tsx-CUOciP0b.js +1 -0
  75. package/apps/web/dist/assets/{twig-pKLKVHRj.js → twig-D6Rh4Rda.js} +1 -1
  76. package/apps/web/dist/assets/typescript-BydbNFcO.js +1 -0
  77. package/apps/web/dist/assets/{typst-DCcUGqaw.js → typst-Bq_qwoSs.js} +1 -1
  78. package/apps/web/dist/assets/video-player-D73_kRr7.js +1416 -0
  79. package/apps/web/dist/assets/video-viewer-CUqarcH1.js +1 -0
  80. package/apps/web/dist/assets/{vue-B_0IsDtY.js → vue-D5NIjYCb.js} +1 -1
  81. package/apps/web/dist/assets/{vue-html-B42dVyQs.js → vue-html-DkKkQnZL.js} +1 -1
  82. package/apps/web/dist/assets/{vue-vine-CUms6qs_.js → vue-vine-C-CFC0K-.js} +1 -1
  83. package/apps/web/dist/assets/xml-BlnrEAEy.js +1 -0
  84. package/apps/web/dist/assets/{xsl-73EQy__K.js → xsl-BOPS8gLt.js} +1 -1
  85. package/apps/web/dist/assets/yaml-Du_6cdrH.js +1 -0
  86. package/apps/web/dist/assets/zoomable-image-CVx2-A2g.js +41 -0
  87. package/apps/web/dist/index.html +5 -2
  88. package/apps/web/dist/sw.js +1 -1
  89. package/package.json +1 -1
  90. package/apps/web/dist/assets/angular-html-DS1oV12F.js +0 -1
  91. package/apps/web/dist/assets/c-DapwzVu-.js +0 -1
  92. package/apps/web/dist/assets/cpp-DxWN2Okg.js +0 -1
  93. package/apps/web/dist/assets/css-DO_8FP-m.js +0 -1
  94. package/apps/web/dist/assets/glsl-DCAs0c4j.js +0 -1
  95. package/apps/web/dist/assets/graphql-CjoRmsAT.js +0 -1
  96. package/apps/web/dist/assets/haml-C_s1GJd7.js +0 -1
  97. package/apps/web/dist/assets/html-rJzVMoNs.js +0 -1
  98. package/apps/web/dist/assets/index-CbDKWecy.js +0 -1206
  99. package/apps/web/dist/assets/index-aoUjY0qi.css +0 -2
  100. package/apps/web/dist/assets/java-CC9QxJEZ.js +0 -1
  101. package/apps/web/dist/assets/javascript-BhEdsWyv.js +0 -1
  102. package/apps/web/dist/assets/json-BwYVcFZr.js +0 -1
  103. package/apps/web/dist/assets/jsx-DZ0EGwxr.js +0 -1
  104. package/apps/web/dist/assets/lua-C1ueigSQ.js +0 -1
  105. package/apps/web/dist/assets/r-D-KnVREb.js +0 -1
  106. package/apps/web/dist/assets/regexp-BHxEHonG.js +0 -1
  107. package/apps/web/dist/assets/scss-QYV6e_ks.js +0 -1
  108. package/apps/web/dist/assets/shellscript-CBTXJvxj.js +0 -1
  109. package/apps/web/dist/assets/sql-BhGvXJN_.js +0 -1
  110. package/apps/web/dist/assets/tex-Be-e7W9g.js +0 -1
  111. package/apps/web/dist/assets/tsx-B_hC-_lk.js +0 -1
  112. package/apps/web/dist/assets/typescript-D8MumUKS.js +0 -1
  113. package/apps/web/dist/assets/xml-Dh5mAVtG.js +0 -1
  114. package/apps/web/dist/assets/yaml-C5lWNsLB.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
2
+ import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
3
3
  import { createServer } from "node:http";
4
4
  import path from "node:path";
5
5
  import { consola } from "consola";
@@ -823,11 +823,9 @@ const configSchema = z.strictObject({
823
823
  opencode: z.record(z.string(), modelSetting).default({})
824
824
  }).prefault({}),
825
825
  delivery: z.enum(["queue", "steer"]).default("steer"),
826
- autoResolve: z.strictObject({
827
- questions: z.number().positive().nullable().default(3),
828
- permissions: z.number().positive().nullable().default(1),
829
- permissionDecision: z.enum(["allow", "deny"]).default("allow")
830
- }).prefault({}),
826
+ autoSkipQuestionTimeout: z.number().positive().nullable().default(180),
827
+ autoPermissionMode: z.enum(["allow", "deny"]).default("allow"),
828
+ autoPermissionTimeout: z.number().positive().nullable().default(15),
831
829
  terminal: z.strictObject({
832
830
  shell: z.strictObject({
833
831
  macos: z.string().nullable().default(null),
@@ -914,20 +912,27 @@ function broadcastActivity() {
914
912
  });
915
913
  }
916
914
  function scheduleAutoResolve(sessionId, request) {
917
- if (request.kind === "plan") return;
918
- const settings = readConfig().autoResolve;
919
- const minutes = request.kind === "question" ? settings.questions : settings.permissions;
920
- if (minutes === null) return;
915
+ if (request.kind === "plan") return request;
916
+ const config = readConfig();
917
+ const seconds = request.kind === "question" ? config.autoSkipQuestionTimeout : config.autoPermissionTimeout;
918
+ if (seconds === null) return request;
919
+ const decision = request.kind === "question" ? "allow" : config.autoPermissionMode;
921
920
  setTimeout(async () => {
922
921
  const run = runs.get(sessionId);
923
922
  if (run === void 0) return;
924
923
  try {
925
- if (request.kind === "question") await run.respond(request.id, "allow", {});
926
- else await run.respond(request.id, readConfig().autoResolve.permissionDecision, null);
924
+ await run.respond(request.id, decision, request.kind === "question" ? {} : null);
927
925
  } catch (error) {
928
926
  console.warn(`could not auto-resolve ${request.id}: ${error}`);
929
927
  }
930
- }, minutes * 6e4);
928
+ }, seconds * 1e3);
929
+ return {
930
+ ...request,
931
+ autoResolve: {
932
+ at: Date.now() + seconds * 1e3,
933
+ decision
934
+ }
935
+ };
931
936
  }
932
937
  //#endregion
933
938
  //#region src/lib/codex/requests.ts
@@ -1257,11 +1262,12 @@ var CodexRun = class CodexRun {
1257
1262
  });
1258
1263
  return;
1259
1264
  }
1260
- const request = toRequest$1(String(id), method, params, this.items);
1261
- if (request === null) {
1265
+ const created = toRequest$1(String(id), method, params, this.items);
1266
+ if (created === null) {
1262
1267
  this.server.replyError(id, `offsite does not handle ${method}`);
1263
1268
  return;
1264
1269
  }
1270
+ const request = scheduleAutoResolve(this.sessionId, created);
1265
1271
  this.pending.set(request.id, {
1266
1272
  request,
1267
1273
  id,
@@ -1274,7 +1280,6 @@ var CodexRun = class CodexRun {
1274
1280
  request
1275
1281
  });
1276
1282
  broadcastActivity();
1277
- scheduleAutoResolve(this.sessionId, request);
1278
1283
  }
1279
1284
  async startTurn(prompt) {
1280
1285
  clearTimeout(this.idleTimer);
@@ -1746,7 +1751,7 @@ function toolItems(base, tool) {
1746
1751
  case "skill": return [{
1747
1752
  ...base,
1748
1753
  kind: "skill",
1749
- skill: text$2(input.name),
1754
+ skill: text$2(input.id),
1750
1755
  output,
1751
1756
  status
1752
1757
  }];
@@ -2299,14 +2304,17 @@ var OpenCodeRun = class OpenCodeRun {
2299
2304
  });
2300
2305
  }
2301
2306
  addPending(pending) {
2302
- this.pending.set(pending.request.id, pending);
2307
+ const request = scheduleAutoResolve(this.sessionId, pending.request);
2308
+ this.pending.set(request.id, {
2309
+ ...pending,
2310
+ request
2311
+ });
2303
2312
  publish$2(this.sessionId, {
2304
2313
  type: "request",
2305
2314
  sessionId: this.sessionId,
2306
- request: pending.request
2315
+ request
2307
2316
  });
2308
2317
  broadcastActivity();
2309
- scheduleAutoResolve(this.sessionId, pending.request);
2310
2318
  }
2311
2319
  finishTurn() {
2312
2320
  this.partial = "";
@@ -2722,8 +2730,15 @@ async function updateSession(id, patch) {
2722
2730
  break;
2723
2731
  case "opencode": await renameOpenCode(id, patch.title);
2724
2732
  }
2725
- if (patch.status === "inactive") db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
2726
- else if (patch.status !== void 0) {
2733
+ if (patch.status === "inactive") {
2734
+ const run = runs.get(id);
2735
+ if (run !== void 0) {
2736
+ if (run.running) await run.interrupt();
2737
+ run.stop();
2738
+ await run.closed;
2739
+ }
2740
+ db.delete(sessions$1).where(eq(sessions$1.sessionId, id)).run();
2741
+ } else if (patch.status !== void 0) {
2727
2742
  const set = patch.status === "active" ? {
2728
2743
  status: patch.status,
2729
2744
  interactedAt: Date.now()
@@ -4312,13 +4327,13 @@ var Run = class {
4312
4327
  }, IDLE_LIMIT);
4313
4328
  }
4314
4329
  ask(options) {
4315
- const request = toRequest({
4330
+ const request = scheduleAutoResolve(this.sessionId, toRequest({
4316
4331
  id: randomUUID(),
4317
4332
  toolName: options.toolName,
4318
4333
  input: options.input,
4319
4334
  title: options.title,
4320
4335
  canAlwaysAllow: options.suggestions.length > 0
4321
- });
4336
+ }));
4322
4337
  return new Promise((resolve) => {
4323
4338
  this.pending.set(request.id, {
4324
4339
  request,
@@ -4332,7 +4347,6 @@ var Run = class {
4332
4347
  request
4333
4348
  });
4334
4349
  broadcastActivity();
4335
- scheduleAutoResolve(this.sessionId, request);
4336
4350
  options.signal.addEventListener("abort", () => this.settle(request.id, {
4337
4351
  behavior: "deny",
4338
4352
  message: "Cancelled"
@@ -4384,6 +4398,153 @@ var Run = class {
4384
4398
  }
4385
4399
  };
4386
4400
  //#endregion
4401
+ //#region ../core/src/media.ts
4402
+ const mediaTypes = /* @__PURE__ */ new Map([
4403
+ [".png", "image/png"],
4404
+ [".jpg", "image/jpeg"],
4405
+ [".jpeg", "image/jpeg"],
4406
+ [".gif", "image/gif"],
4407
+ [".webp", "image/webp"],
4408
+ [".svg", "image/svg+xml"],
4409
+ [".mp4", "video/mp4"],
4410
+ [".m4v", "video/mp4"],
4411
+ [".webm", "video/webm"],
4412
+ [".mov", "video/quicktime"],
4413
+ [".mp3", "audio/mpeg"],
4414
+ [".wav", "audio/wav"],
4415
+ [".ogg", "audio/ogg"],
4416
+ [".opus", "audio/ogg"],
4417
+ [".m4a", "audio/mp4"],
4418
+ [".aac", "audio/aac"],
4419
+ [".flac", "audio/flac"]
4420
+ ]);
4421
+ //#endregion
4422
+ //#region src/services/files.ts
4423
+ const FILE_LIMIT = 2e4;
4424
+ const OUTPUT_LIMIT$1 = 262144;
4425
+ const READ_LIMIT = 1048576;
4426
+ const MEDIA_LINK_LIFETIME = 36e5;
4427
+ const mediaLinks = /* @__PURE__ */ new Map();
4428
+ const skipped = /* @__PURE__ */ new Set([
4429
+ "node_modules",
4430
+ ".git",
4431
+ ".DS_Store",
4432
+ "dist",
4433
+ "target",
4434
+ ".next",
4435
+ "__pycache__",
4436
+ ".venv"
4437
+ ]);
4438
+ async function listFiles(cwd) {
4439
+ const files = [];
4440
+ const pending = [""];
4441
+ while (pending.length > 0 && files.length < FILE_LIMIT) {
4442
+ const dir = pending.pop() ?? "";
4443
+ let entries;
4444
+ try {
4445
+ entries = await readdir(path.join(cwd, dir), { withFileTypes: true });
4446
+ } catch {
4447
+ continue;
4448
+ }
4449
+ for (const entry of entries) {
4450
+ if (skipped.has(entry.name)) continue;
4451
+ const relative = dir === "" ? entry.name : `${dir}/${entry.name}`;
4452
+ if (entry.isDirectory()) pending.push(relative);
4453
+ if (entry.isFile()) files.push(relative);
4454
+ if (entry.isSymbolicLink()) {
4455
+ if ((await stat(path.join(cwd, relative)).catch(() => null))?.isFile() === true) files.push(relative);
4456
+ }
4457
+ }
4458
+ }
4459
+ return files.slice(0, FILE_LIMIT).sort();
4460
+ }
4461
+ function readProjectFile(cwd, file) {
4462
+ const full = resolveProjectFile(cwd, file);
4463
+ const stats = statSync(full);
4464
+ if (!stats.isFile()) throw new Error(`${file} is not a file`);
4465
+ if (stats.size > READ_LIMIT) return {
4466
+ kind: "large",
4467
+ size: stats.size,
4468
+ limit: READ_LIMIT
4469
+ };
4470
+ const data = readFileSync(full);
4471
+ const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
4472
+ if (mediaType?.startsWith("image/") === true) return {
4473
+ kind: "image",
4474
+ size: stats.size,
4475
+ mediaType,
4476
+ data: data.toString("base64")
4477
+ };
4478
+ if (data.includes(0)) return {
4479
+ kind: "binary",
4480
+ size: stats.size
4481
+ };
4482
+ return {
4483
+ kind: "text",
4484
+ size: stats.size,
4485
+ content: data.toString("utf8")
4486
+ };
4487
+ }
4488
+ function linkProjectMedia(cwd, file) {
4489
+ const full = resolveProjectFile(cwd, file);
4490
+ const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
4491
+ if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
4492
+ for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
4493
+ const token = randomBytes(32).toString("base64url");
4494
+ mediaLinks.set(token, {
4495
+ file: full,
4496
+ mediaType,
4497
+ expiresAt: Date.now() + MEDIA_LINK_LIFETIME
4498
+ });
4499
+ return token;
4500
+ }
4501
+ function findMediaLink(token) {
4502
+ if (token === null) return null;
4503
+ const link = mediaLinks.get(token);
4504
+ if (link === void 0 || link.expiresAt <= Date.now()) return null;
4505
+ return link;
4506
+ }
4507
+ function saveUpload(name, data) {
4508
+ const dir = path.join(home, "uploads", randomUUID());
4509
+ mkdirSync(dir, { recursive: true });
4510
+ const file = path.join(dir, path.basename(name));
4511
+ writeFileSync(file, data);
4512
+ return file;
4513
+ }
4514
+ async function taskOutput(sessionId, file) {
4515
+ const tasks = path.dirname(file);
4516
+ if (!/^[\w-]+$/.test(sessionId) || !path.isAbsolute(file) || path.normalize(file) !== file || !/^\w+\.output$/.test(path.basename(file)) || path.basename(tasks) !== "tasks" || path.basename(path.dirname(tasks)) !== sessionId) throw new Error("Not a background task output file of this thread");
4517
+ let stats;
4518
+ try {
4519
+ stats = await lstat(file);
4520
+ } catch (error) {
4521
+ if (hasCode(error, "ENOENT")) return null;
4522
+ throw error;
4523
+ }
4524
+ if (!stats.isFile()) throw new Error("Background task output is not a regular file");
4525
+ const start = Math.max(0, stats.size - OUTPUT_LIMIT$1);
4526
+ const handle = await open(file, "r");
4527
+ try {
4528
+ const buffer = Buffer.alloc(stats.size - start);
4529
+ const read = await handle.read(buffer, 0, buffer.length, start);
4530
+ return {
4531
+ text: buffer.subarray(0, read.bytesRead).toString("utf8"),
4532
+ truncated: start > 0
4533
+ };
4534
+ } finally {
4535
+ await handle.close();
4536
+ }
4537
+ }
4538
+ function resolveProjectFile(cwd, file) {
4539
+ const root = realpathSync(cwd);
4540
+ const resolved = path.resolve(root, file);
4541
+ if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
4542
+ const full = realpathSync(resolved);
4543
+ const relative = path.relative(root, full);
4544
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`${file} is outside the project`);
4545
+ return full;
4546
+ }
4547
+ //#endregion
4387
4548
  //#region src/services/git.ts
4388
4549
  const DIFF_LIMIT = 8e4;
4389
4550
  const UNTRACKED_LIMIT = 100;
@@ -4642,6 +4803,7 @@ async function isRepo(cwd) {
4642
4803
  }
4643
4804
  async function repoDiff(repo, prefix) {
4644
4805
  const tracked = await git(repo, [
4806
+ "--no-optional-locks",
4645
4807
  "diff",
4646
4808
  await diffBase(repo),
4647
4809
  "--no-color",
@@ -4692,6 +4854,7 @@ function newFilePatch(cwd, file, prefix) {
4692
4854
  const name = `${prefix}${file}`;
4693
4855
  const header = `diff --git a/${name} b/${name}\nnew file\n--- /dev/null\n+++ b/${name}\n`;
4694
4856
  const full = path.join(cwd, file);
4857
+ if (lstatSync(full).isSymbolicLink()) return `${header}@@ -0,0 +1 @@\n+${readlinkSync(full)}\n`;
4695
4858
  if (statSync(full).size > NEW_FILE_LIMIT) return `${header}Binary or large file\n`;
4696
4859
  const content = readFileSync(full, "utf8");
4697
4860
  if (content.includes("\0")) return `${header}Binary or large file\n`;
@@ -4700,119 +4863,6 @@ function newFilePatch(cwd, file, prefix) {
4700
4863
  return `${header}@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}\n`).join("")}`;
4701
4864
  }
4702
4865
  //#endregion
4703
- //#region src/services/files.ts
4704
- const FILE_LIMIT = 2e4;
4705
- const OUTPUT_LIMIT$1 = 262144;
4706
- const READ_LIMIT = 1048576;
4707
- const images = /* @__PURE__ */ new Map([
4708
- [".png", "image/png"],
4709
- [".jpg", "image/jpeg"],
4710
- [".jpeg", "image/jpeg"],
4711
- [".gif", "image/gif"],
4712
- [".webp", "image/webp"],
4713
- [".svg", "image/svg+xml"]
4714
- ]);
4715
- const skipped = /* @__PURE__ */ new Set([
4716
- "node_modules",
4717
- ".git",
4718
- "dist",
4719
- "target",
4720
- ".next"
4721
- ]);
4722
- async function listFiles(cwd) {
4723
- try {
4724
- const { stdout } = await git(cwd, [
4725
- "ls-files",
4726
- "--cached",
4727
- "--others",
4728
- "--exclude-standard"
4729
- ]);
4730
- return stdout.split("\n").filter(Boolean).slice(0, FILE_LIMIT);
4731
- } catch {
4732
- return walk(cwd);
4733
- }
4734
- }
4735
- function readProjectFile(cwd, file) {
4736
- const root = realpathSync(cwd);
4737
- const full = realpathSync(path.resolve(root, file));
4738
- const relative = path.relative(root, full);
4739
- if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`${file} is outside the project`);
4740
- const stats = statSync(full);
4741
- if (!stats.isFile()) throw new Error(`${file} is not a file`);
4742
- if (stats.size > READ_LIMIT) return {
4743
- kind: "large",
4744
- size: stats.size,
4745
- limit: READ_LIMIT
4746
- };
4747
- const data = readFileSync(full);
4748
- const mediaType = images.get(path.extname(full).toLowerCase());
4749
- if (mediaType !== void 0) return {
4750
- kind: "image",
4751
- size: stats.size,
4752
- mediaType,
4753
- data: data.toString("base64")
4754
- };
4755
- if (data.includes(0)) return {
4756
- kind: "binary",
4757
- size: stats.size
4758
- };
4759
- return {
4760
- kind: "text",
4761
- size: stats.size,
4762
- content: data.toString("utf8")
4763
- };
4764
- }
4765
- function saveUpload(name, data) {
4766
- const dir = path.join(home, "uploads", randomUUID());
4767
- mkdirSync(dir, { recursive: true });
4768
- const file = path.join(dir, path.basename(name));
4769
- writeFileSync(file, data);
4770
- return file;
4771
- }
4772
- async function taskOutput(sessionId, file) {
4773
- const tasks = path.dirname(file);
4774
- if (!/^[\w-]+$/.test(sessionId) || !path.isAbsolute(file) || path.normalize(file) !== file || !/^\w+\.output$/.test(path.basename(file)) || path.basename(tasks) !== "tasks" || path.basename(path.dirname(tasks)) !== sessionId) throw new Error("Not a background task output file of this thread");
4775
- let stats;
4776
- try {
4777
- stats = await lstat(file);
4778
- } catch (error) {
4779
- if (hasCode(error, "ENOENT")) return null;
4780
- throw error;
4781
- }
4782
- if (!stats.isFile()) throw new Error("Background task output is not a regular file");
4783
- const start = Math.max(0, stats.size - OUTPUT_LIMIT$1);
4784
- const handle = await open(file, "r");
4785
- try {
4786
- const buffer = Buffer.alloc(stats.size - start);
4787
- const read = await handle.read(buffer, 0, buffer.length, start);
4788
- return {
4789
- text: buffer.subarray(0, read.bytesRead).toString("utf8"),
4790
- truncated: start > 0
4791
- };
4792
- } finally {
4793
- await handle.close();
4794
- }
4795
- }
4796
- function walk(root) {
4797
- const files = [];
4798
- const pending = [""];
4799
- while (pending.length > 0 && files.length < FILE_LIMIT) {
4800
- const dir = pending.pop() ?? "";
4801
- let entries;
4802
- try {
4803
- entries = readdirSync(path.join(root, dir), { withFileTypes: true });
4804
- } catch {
4805
- continue;
4806
- }
4807
- for (const entry of entries) {
4808
- const relative = dir === "" ? entry.name : `${dir}/${entry.name}`;
4809
- if (entry.isDirectory() && !skipped.has(entry.name)) pending.push(relative);
4810
- if (entry.isFile()) files.push(relative);
4811
- }
4812
- }
4813
- return files;
4814
- }
4815
- //#endregion
4816
4866
  //#region src/services/runs.ts
4817
4867
  const locks = /* @__PURE__ */ new Map();
4818
4868
  function stopAll() {
@@ -6026,6 +6076,7 @@ const handlers = {
6026
6076
  capabilities: (params) => capabilities(params.provider, params.accountId, params.projectId === null ? os.homedir() : projectPath(params.projectId)),
6027
6077
  "files.list": (params) => listFiles(projectPath(params.projectId)),
6028
6078
  "files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
6079
+ "files.media": (params) => linkProjectMedia(projectPath(params.projectId), params.path),
6029
6080
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
6030
6081
  "tasks.output": (params) => taskOutput(params.sessionId, params.path),
6031
6082
  "git.status": (params) => gitRepos(projectPath(params.projectId)),
@@ -6204,6 +6255,10 @@ const code = args.auth && args.pair && !hasDevices() ? addCode(null) : null;
6204
6255
  const tickets = /* @__PURE__ */ new Map();
6205
6256
  const server = createServer((request, response) => {
6206
6257
  const url = new URL(request.url ?? "/", "http://localhost");
6258
+ if (url.pathname === "/api/media") {
6259
+ serveMedia(url.searchParams.get("token"), request, response);
6260
+ return;
6261
+ }
6207
6262
  if (url.pathname.startsWith("/api/")) {
6208
6263
  answerApi(url.pathname, request, response);
6209
6264
  return;
@@ -6300,6 +6355,64 @@ function answerApi(pathname, request, response) {
6300
6355
  response.statusCode = 404;
6301
6356
  response.end("not found");
6302
6357
  }
6358
+ function serveMedia(token, request, response) {
6359
+ if (request.method !== "GET") {
6360
+ response.statusCode = 405;
6361
+ response.end("use GET");
6362
+ return;
6363
+ }
6364
+ const link = findMediaLink(token);
6365
+ if (link === null) {
6366
+ response.statusCode = 404;
6367
+ response.end("This media link expired");
6368
+ return;
6369
+ }
6370
+ const stats = statSync(link.file, { throwIfNoEntry: false });
6371
+ if (stats === void 0 || !stats.isFile()) {
6372
+ response.statusCode = 404;
6373
+ response.end("not found");
6374
+ return;
6375
+ }
6376
+ response.setHeader("content-type", link.mediaType);
6377
+ response.setHeader("accept-ranges", "bytes");
6378
+ response.setHeader("cache-control", "private, max-age=3600");
6379
+ response.setHeader("content-security-policy", "sandbox");
6380
+ response.setHeader("x-content-type-options", "nosniff");
6381
+ if (request.headers.range === void 0) {
6382
+ response.setHeader("content-length", stats.size);
6383
+ createReadStream(link.file).on("error", () => response.destroy()).pipe(response);
6384
+ return;
6385
+ }
6386
+ const range = byteRange(request.headers.range, stats.size);
6387
+ if (range === null) {
6388
+ response.statusCode = 416;
6389
+ response.setHeader("content-range", `bytes */${stats.size}`);
6390
+ response.end();
6391
+ return;
6392
+ }
6393
+ response.statusCode = 206;
6394
+ response.setHeader("content-range", `bytes ${range.start}-${range.end}/${stats.size}`);
6395
+ response.setHeader("content-length", range.end - range.start + 1);
6396
+ createReadStream(link.file, range).on("error", () => response.destroy()).pipe(response);
6397
+ }
6398
+ function byteRange(header, size) {
6399
+ const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);
6400
+ if (match === null) return null;
6401
+ if (match[3] !== void 0) {
6402
+ if (size === 0 || Number(match[3]) === 0) return null;
6403
+ return {
6404
+ start: Math.max(size - Number(match[3]), 0),
6405
+ end: size - 1
6406
+ };
6407
+ }
6408
+ const start = Number(match[1]);
6409
+ const end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1);
6410
+ if (start > end) return null;
6411
+ return {
6412
+ start,
6413
+ end
6414
+ };
6415
+ }
6303
6416
  function answerJson(response, body) {
6304
6417
  response.setHeader("content-type", "application/json");
6305
6418
  response.end(JSON.stringify(body));