@joeyshi12/casper 0.9.8 → 0.10.0

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 (73) hide show
  1. package/README.md +6 -0
  2. package/dist/mcp.js +1 -1
  3. package/dist/server.js +547 -636
  4. package/dist/web/assets/{abnfDiagram-N423BO3Z-BNWwSSFp.js → abnfDiagram-N423BO3Z-DuqqTYEM.js} +1 -1
  5. package/dist/web/assets/{arc-CcwkM28l.js → arc-BbitKrIO.js} +1 -1
  6. package/dist/web/assets/{architectureDiagram-T3A2C74G-BTs-BMSL.js → architectureDiagram-T3A2C74G-Dc0p1FLK.js} +1 -1
  7. package/dist/web/assets/{blockDiagram-VBNYF7ZC-Cg3AKEAE.js → blockDiagram-VBNYF7ZC-A2tZMhwX.js} +1 -1
  8. package/dist/web/assets/{c4Diagram-5PPSVZJV-BeLOHTLt.js → c4Diagram-5PPSVZJV-BGYnWUNq.js} +1 -1
  9. package/dist/web/assets/channel-oev4WWHD.js +1 -0
  10. package/dist/web/assets/{chunk-2GRJ4B5K-BLdzjU64.js → chunk-2GRJ4B5K-DGcSgggV.js} +1 -1
  11. package/dist/web/assets/{chunk-2Q5K7J3B-OBq9M6-Y.js → chunk-2Q5K7J3B-Cqd5ykHS.js} +1 -1
  12. package/dist/web/assets/{chunk-5RXB4S5H-Bo5TlL5E.js → chunk-5RXB4S5H-4s-V0baK.js} +1 -1
  13. package/dist/web/assets/{chunk-5VM5RSS4-Bu73pV_h.js → chunk-5VM5RSS4-DN_MfTRV.js} +1 -1
  14. package/dist/web/assets/{chunk-6Q2QTUOP-zkF1AB2H.js → chunk-6Q2QTUOP-6-ncud5-.js} +1 -1
  15. package/dist/web/assets/{chunk-GF5L2VYU-1gox46V8.js → chunk-GF5L2VYU-CkCAkajU.js} +1 -1
  16. package/dist/web/assets/{chunk-JWPE2WC7-Co6JHAZe.js → chunk-JWPE2WC7-B2HT0tHT.js} +1 -1
  17. package/dist/web/assets/{chunk-KBJHAD2P-COImJ5cB.js → chunk-KBJHAD2P-DtYmpITE.js} +1 -1
  18. package/dist/web/assets/{chunk-RYQCIY6F-DwZzSkn2.js → chunk-RYQCIY6F-CqU4ZESr.js} +1 -1
  19. package/dist/web/assets/{chunk-XXDRQBXY-D7J4Ua5K.js → chunk-XXDRQBXY-5zlfmd8I.js} +1 -1
  20. package/dist/web/assets/classDiagram-JCYQIIEL-D4FIkEsQ.js +1 -0
  21. package/dist/web/assets/classDiagram-v2-OCEON4UE-D4FIkEsQ.js +1 -0
  22. package/dist/web/assets/{cose-bilkent-JH36ORCC-io_sMArY.js → cose-bilkent-JH36ORCC-BtXxufQe.js} +1 -1
  23. package/dist/web/assets/{cynefin-VYW2F7L2-2kI4W7NK.js → cynefin-VYW2F7L2-DFLsp7Rj.js} +1 -1
  24. package/dist/web/assets/{cynefinDiagram-MW4NZA55-DDOMF8nO.js → cynefinDiagram-MW4NZA55-BiXfZ5zy.js} +1 -1
  25. package/dist/web/assets/{dagre-VZM6K2ZE-D8ePE05C.js → dagre-VZM6K2ZE-MBbBcp4r.js} +1 -1
  26. package/dist/web/assets/{diagram-7IWD3JNH-D7-_PTKo.js → diagram-7IWD3JNH-BBJQPRUY.js} +1 -1
  27. package/dist/web/assets/{diagram-B4RE2ZJO-CotIn3Ty.js → diagram-B4RE2ZJO-D39huys8.js} +1 -1
  28. package/dist/web/assets/{diagram-LBJQPF4R-DofOzTTJ.js → diagram-LBJQPF4R-BEI6m3xw.js} +1 -1
  29. package/dist/web/assets/{diagram-Q27KOJAE-CVkyN7TJ.js → diagram-Q27KOJAE-0mMWjfIY.js} +1 -1
  30. package/dist/web/assets/{diagram-UB23O5K3-BMeuDkaO.js → diagram-UB23O5K3-Cq_AT0df.js} +1 -1
  31. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-DprK65id.js → ebnfDiagram-BXEA7PRR-CspS7m6Z.js} +1 -1
  32. package/dist/web/assets/{erDiagram-JOGREHBK-D-hwXtn8.js → erDiagram-JOGREHBK-B6NxTPkb.js} +1 -1
  33. package/dist/web/assets/{flowDiagram-UKHOOZJN-BanZuk45.js → flowDiagram-UKHOOZJN-D3Pt3-cN.js} +1 -1
  34. package/dist/web/assets/{ganttDiagram-PKOTCBZU-DKdLDEUL.js → ganttDiagram-PKOTCBZU-CV4orYkm.js} +1 -1
  35. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-CadXyKLa.js → gitGraphDiagram-DS77QQ5N-CANHeJZg.js} +1 -1
  36. package/dist/web/assets/index-CK4YbLbU.js +253 -0
  37. package/dist/web/assets/index-Cz-T9NEB.css +1 -0
  38. package/dist/web/assets/index-Dewf4nkF.js +3 -0
  39. package/dist/web/assets/{infoDiagram-6WML65LV-DguQBaFj.js → infoDiagram-6WML65LV-ySSVfb1E.js} +1 -1
  40. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-sCmvl_Ov.js → ishikawaDiagram-WSZJBQD7-C8PCYjFX.js} +1 -1
  41. package/dist/web/assets/{journeyDiagram-NVQOT4AX-Dp3H0qDU.js → journeyDiagram-NVQOT4AX-BS_jBDBe.js} +1 -1
  42. package/dist/web/assets/{kanban-definition-27J2QSJJ-C6mTugp9.js → kanban-definition-27J2QSJJ-H6jhmFjt.js} +1 -1
  43. package/dist/web/assets/katex-BYZTj3zW.css +1 -0
  44. package/dist/web/assets/katex-C5jXJg4s.js +257 -0
  45. package/dist/web/assets/{linear-CyDuoWyx.js → linear-YU9d9rPY.js} +1 -1
  46. package/dist/web/assets/{mermaid.core-BpVrpe6c.js → mermaid.core-DsId9yj5.js} +5 -5
  47. package/dist/web/assets/{mindmap-definition-FAOFIHXS-BPgOZVAx.js → mindmap-definition-FAOFIHXS-DVFyeioX.js} +1 -1
  48. package/dist/web/assets/{pegDiagram-VL7TDLO6-eXPwHM7z.js → pegDiagram-VL7TDLO6-BD4va82R.js} +1 -1
  49. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-CFIMahU1.js → pieDiagram-7S7Q4E2Y-CZAefhVe.js} +1 -1
  50. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-dnDjuXib.js → quadrantDiagram-CIZ2JOQS-D3RB1kgH.js} +1 -1
  51. package/dist/web/assets/{railroadDiagram-AXF67PYL-_3DCSEsR.js → railroadDiagram-AXF67PYL-Cvnf0WjF.js} +1 -1
  52. package/dist/web/assets/{requirementDiagram-LRYGKXZP-DimVO25E.js → requirementDiagram-LRYGKXZP-C4B6vv6V.js} +1 -1
  53. package/dist/web/assets/{sankeyDiagram-W5VNT64P-DtCSNUnd.js → sankeyDiagram-W5VNT64P-C5xg3C0D.js} +1 -1
  54. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-DyZDw9rq.js → sequenceDiagram-SI44F4Z6-DjWE-t7c.js} +1 -1
  55. package/dist/web/assets/{sizeCapture-X5ZJPWSS-DsJVViPa.js → sizeCapture-X5ZJPWSS-Bf7ugfWa.js} +1 -1
  56. package/dist/web/assets/{stateDiagram-OKZ733FA-3W1WK-Lx.js → stateDiagram-OKZ733FA-DxEitXwP.js} +1 -1
  57. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-BV8yVaCo.js +1 -0
  58. package/dist/web/assets/{swimlanes-SLNWSIFB-BdAToTPU.js → swimlanes-SLNWSIFB-DMW3T9iR.js} +2 -2
  59. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-DZu5mMsy.js +8 -0
  60. package/dist/web/assets/{timeline-definition-Z64GVDOM-W9PH8xTz.js → timeline-definition-Z64GVDOM-whI5A_En.js} +1 -1
  61. package/dist/web/assets/{vennDiagram-T6HMQDX7-DztY6X4E.js → vennDiagram-T6HMQDX7-DGdGbcJw.js} +1 -1
  62. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-BVJp-LQ1.js → wardleyDiagram-T6FBY63Y-CTNpUHSX.js} +1 -1
  63. package/dist/web/assets/{xychartDiagram-ELKLHX3M-CUgAA_UM.js → xychartDiagram-ELKLHX3M-C2j32C85.js} +1 -1
  64. package/dist/web/index.html +2 -2
  65. package/dist/web/sw.js +1 -1
  66. package/package.json +1 -1
  67. package/dist/web/assets/channel-DdxwT919.js +0 -1
  68. package/dist/web/assets/classDiagram-JCYQIIEL-CqM6S8tE.js +0 -1
  69. package/dist/web/assets/classDiagram-v2-OCEON4UE-CqM6S8tE.js +0 -1
  70. package/dist/web/assets/index-C0NwqMe_.css +0 -1
  71. package/dist/web/assets/index-Cv7GqnlJ.js +0 -510
  72. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-BIWqQTVE.js +0 -1
  73. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-iIR_M-eG.js +0 -8
package/dist/server.js CHANGED
@@ -292,6 +292,7 @@ var init_config = __esm({
292
292
  "token",
293
293
  "sessionTtlSeconds",
294
294
  "kiroBin",
295
+ "kiroSessionsDir",
295
296
  "defaultCwd",
296
297
  "maxLiveSessions",
297
298
  "defaultAgent",
@@ -325,8 +326,18 @@ var init_config = __esm({
325
326
  * or SSH keys.
326
327
  */
327
328
  fileRoot: path5.resolve(setting("CASPER_FILE_ROOT", "fileRoot", "/")),
328
- /** Directory where kiro-cli persists its own session files. */
329
- kiroSessionsDir: path5.join(home, ".kiro", "sessions", "cli"),
329
+ /**
330
+ * Directory where kiro-cli persists its own session files. Overridable for the
331
+ * same reason as every other path here: a non-default kiro home, a container,
332
+ * or a test that must not write into the real one.
333
+ */
334
+ kiroSessionsDir: path5.resolve(
335
+ setting(
336
+ "CASPER_KIRO_SESSIONS_DIR",
337
+ "kiroSessionsDir",
338
+ path5.join(home, ".kiro", "sessions", "cli")
339
+ )
340
+ ),
330
341
  /** Where casper.db lives. Env-only: it says where data is, and the config
331
342
  * file isn't there. */
332
343
  casperDataDir: dataDirPath(),
@@ -1608,8 +1619,10 @@ var init_SessionManager = __esm({
1608
1619
  sessions = /* @__PURE__ */ new Map();
1609
1620
  log;
1610
1621
  store = new SessionStore();
1611
- constructor(log) {
1622
+ spawnProcess;
1623
+ constructor(log, opts = {}) {
1612
1624
  this.log = log;
1625
+ this.spawnProcess = opts.spawn ?? ((o, l) => new KiroProcess(o, l));
1613
1626
  }
1614
1627
  /**
1615
1628
  * What a session is called, from every read path. The folder is skipped for a workspace of
@@ -1721,7 +1734,7 @@ var init_SessionManager = __esm({
1721
1734
  if (s.spawning) return s.spawning;
1722
1735
  s.spawning = (async () => {
1723
1736
  await this.ensureCapacity();
1724
- const proc = new KiroProcess(
1737
+ const proc = this.spawnProcess(
1725
1738
  { cwd: s.cwd, agent: s.agentId, model: s.modelId },
1726
1739
  this.log
1727
1740
  );
@@ -1858,61 +1871,58 @@ ${tail}` : err.message;
1858
1871
  await proc.execCommand(s.sessionId, command, args);
1859
1872
  s.lastActivity = Date.now();
1860
1873
  }
1861
- // -------------------------------------------------------------------------
1862
- // Listing / detail - never spawns.
1863
- // -------------------------------------------------------------------------
1874
+ summaryOf(live, persisted, transcript) {
1875
+ const sessionId = live?.sessionId ?? persisted.sessionId;
1876
+ const snap = live?.turnState.get();
1877
+ const cwd = live?.cwd ?? this.store.getCwd(sessionId) ?? persisted.cwd;
1878
+ return {
1879
+ sessionId,
1880
+ title: this.titleOf(sessionId, {
1881
+ // kiro's file is what kiro called it; the live copy is only a cache of it.
1882
+ kiroTitle: persisted?.title || live?.title,
1883
+ firstPrompt: transcript && firstPromptText(transcript),
1884
+ cwd
1885
+ }),
1886
+ cwd,
1887
+ createdAt: persisted?.createdAt ?? live.createdAt,
1888
+ // kiro's file and our own activity move separately.
1889
+ updatedAt: live ? newerOf(persisted?.updatedAt, live.updatedAt) : persisted.updatedAt,
1890
+ liveness: live?.proc ? "live" : "dormant",
1891
+ agentId: live?.agentId ?? persisted?.agentId,
1892
+ modelId: live?.modelId ?? persisted?.modelId,
1893
+ running: live?.running ?? false,
1894
+ // A dormant session's totals live in kiro's file; a live one's snapshot
1895
+ // starts at zero until the first turn reports, so fall back to the file.
1896
+ creditsSpent: snap?.creditsSpent || persisted?.creditsSpent,
1897
+ contextUsagePercentage: snap?.contextUsagePercentage || persisted?.contextUsagePercentage
1898
+ };
1899
+ }
1864
1900
  async listSessions() {
1865
1901
  const persisted = await listPersistedSessions(this.log);
1902
+ const files = new Map(persisted.map((p) => [p.sessionId, p]));
1866
1903
  const byId = /* @__PURE__ */ new Map();
1867
1904
  for (const p of persisted) {
1868
- byId.set(p.sessionId, {
1869
- ...p,
1870
- title: this.titleOf(p.sessionId, {
1871
- kiroTitle: p.title,
1872
- cwd: this.store.getCwd(p.sessionId) ?? p.cwd
1873
- }),
1874
- cwd: this.store.getCwd(p.sessionId) ?? p.cwd
1875
- });
1905
+ byId.set(p.sessionId, this.summaryOf(void 0, p));
1876
1906
  }
1877
1907
  for (const s of this.sessions.values()) {
1878
- const snap = s.turnState.get();
1879
- const base = byId.get(s.sessionId);
1880
- if (isGhost(s, base !== void 0)) {
1908
+ if (isGhost(s, files.has(s.sessionId))) {
1881
1909
  this.evict(s.sessionId);
1882
1910
  continue;
1883
1911
  }
1884
- byId.set(s.sessionId, {
1885
- sessionId: s.sessionId,
1886
- title: this.titleOf(s.sessionId, { kiroTitle: base?.title, cwd: s.cwd }),
1887
- cwd: s.cwd,
1888
- createdAt: base?.createdAt ?? s.createdAt,
1889
- updatedAt: newerOf(base?.updatedAt, s.updatedAt),
1890
- liveness: s.proc ? "live" : "dormant",
1891
- agentId: s.agentId ?? base?.agentId,
1892
- modelId: s.modelId ?? base?.modelId,
1893
- running: s.running,
1894
- creditsSpent: snap.creditsSpent || base?.creditsSpent,
1895
- contextUsagePercentage: snap.contextUsagePercentage || base?.contextUsagePercentage
1896
- });
1912
+ byId.set(s.sessionId, this.summaryOf(s, files.get(s.sessionId)));
1897
1913
  }
1898
1914
  return [...byId.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1899
1915
  }
1900
1916
  async getDetail(sessionId) {
1901
- const transcript = await hydrateTranscript(sessionId);
1917
+ const [transcript, persisted] = await Promise.all([
1918
+ hydrateTranscript(sessionId),
1919
+ readPersistedSession(sessionId)
1920
+ ]);
1902
1921
  const s = this.sessions.get(sessionId);
1903
- if (s) return this.buildDetail(s, transcript);
1904
- const persisted = await readPersistedSession(sessionId);
1922
+ if (s) return this.buildDetail(s, transcript, persisted ?? void 0);
1905
1923
  if (!persisted) throw new Error(`Unknown session: ${sessionId}`);
1906
1924
  return {
1907
- summary: {
1908
- ...persisted,
1909
- title: this.titleOf(sessionId, {
1910
- kiroTitle: persisted.title,
1911
- firstPrompt: firstPromptText(transcript),
1912
- cwd: this.store.getCwd(sessionId) ?? persisted.cwd
1913
- }),
1914
- cwd: this.store.getCwd(sessionId) ?? persisted.cwd
1915
- },
1925
+ summary: this.summaryOf(void 0, persisted, transcript),
1916
1926
  modes: [],
1917
1927
  currentModeId: persisted.agentId,
1918
1928
  transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
@@ -1936,26 +1946,10 @@ ${tail}` : err.message;
1936
1946
  const end = Math.max(start, Math.min(start + limit, transcript.length));
1937
1947
  return transcript.slice(start, end);
1938
1948
  }
1939
- buildDetail(s, transcript) {
1949
+ buildDetail(s, transcript, persisted) {
1940
1950
  const snap = s.turnState.get();
1941
1951
  return {
1942
- summary: {
1943
- sessionId: s.sessionId,
1944
- title: this.titleOf(s.sessionId, {
1945
- kiroTitle: s.title,
1946
- firstPrompt: firstPromptText(transcript),
1947
- cwd: s.cwd
1948
- }),
1949
- cwd: s.cwd,
1950
- createdAt: s.createdAt,
1951
- updatedAt: s.updatedAt,
1952
- liveness: s.proc ? "live" : "dormant",
1953
- agentId: s.agentId,
1954
- modelId: s.modelId,
1955
- running: s.running,
1956
- creditsSpent: snap.creditsSpent,
1957
- contextUsagePercentage: snap.contextUsagePercentage
1958
- },
1952
+ summary: this.summaryOf(s, persisted, transcript),
1959
1953
  modes: s.availableModes,
1960
1954
  currentModeId: s.currentModeId,
1961
1955
  // Only the tail is sent on load; replayHead/title use the full transcript.
@@ -2299,13 +2293,242 @@ var init_agents2 = __esm({
2299
2293
  }
2300
2294
  });
2301
2295
 
2302
- // server/src/routes/fs.ts
2296
+ // server/src/util/confinedFile.ts
2303
2297
  import fs11 from "node:fs/promises";
2304
- import { createReadStream } from "node:fs";
2305
2298
  import path12 from "node:path";
2306
- function confinedPath(input) {
2307
- return confineToRoot(config.fileRoot, input) ?? confineToRoot(config.casperDataDir, input);
2299
+ async function resolveConfined(input, spec) {
2300
+ let target = null;
2301
+ for (const root of spec.lexical) {
2302
+ target = confineToRoot(root, input);
2303
+ if (target) break;
2304
+ }
2305
+ if (!target) return { ok: false, ...spec.escaped };
2306
+ let real = null;
2307
+ for (const root of spec.real) {
2308
+ real = await realConfineToRoot(root, target);
2309
+ if (real) break;
2310
+ }
2311
+ if (!real) return { ok: false, ...await spec.notFound() };
2312
+ let stat;
2313
+ try {
2314
+ stat = await fs11.stat(real);
2315
+ } catch {
2316
+ return { ok: false, ...await spec.notFound() };
2317
+ }
2318
+ if (spec.require === "file" && !stat.isFile()) {
2319
+ return { ok: false, status: 400, error: "Path is not a file" };
2320
+ }
2321
+ if (spec.require === "directory" && !stat.isDirectory()) {
2322
+ return { ok: false, ...await spec.notFound() };
2323
+ }
2324
+ return { ok: true, real, stat };
2325
+ }
2326
+ function replyWith(reply, failure) {
2327
+ reply.code(failure.status);
2328
+ return { error: failure.error };
2329
+ }
2330
+ async function resolveSessionPath(sessions, sessionId, requestedPath, require2) {
2331
+ let cwd;
2332
+ try {
2333
+ cwd = await sessions.getSessionCwd(sessionId);
2334
+ } catch {
2335
+ return { ok: false, status: 404, error: "Session not found" };
2336
+ }
2337
+ const relative = (requestedPath ?? "").replace(/^\/+/, "");
2338
+ if (require2 === "file" && !relative) {
2339
+ return { ok: false, status: 400, error: "path parameter is required" };
2340
+ }
2341
+ const resolved = await resolveConfined(relative, {
2342
+ lexical: [cwd],
2343
+ real: [config.fileRoot],
2344
+ require: require2,
2345
+ escaped: { status: 400, error: "Invalid path" },
2346
+ notFound: require2 === "file" ? () => ({ status: 404, error: "File not found" }) : () => workspaceNotFound(cwd)
2347
+ });
2348
+ return resolved.ok ? { ...resolved, cwd, relative } : resolved;
2349
+ }
2350
+ async function workspaceNotFound(cwd) {
2351
+ let missing;
2352
+ try {
2353
+ missing = !(await fs11.stat(cwd)).isDirectory();
2354
+ } catch {
2355
+ missing = true;
2356
+ }
2357
+ return missing ? { status: 404, error: `Workspace folder no longer exists: ${cwd}` } : { status: 404, error: "Directory not found" };
2358
+ }
2359
+ function absoluteRoots() {
2360
+ return [config.fileRoot, config.casperDataDir];
2361
+ }
2362
+ async function resolveAbsolutePath(input, require2) {
2363
+ const roots = absoluteRoots();
2364
+ return resolveConfined(input, {
2365
+ lexical: roots,
2366
+ real: roots,
2367
+ require: require2,
2368
+ escaped: { status: 403, error: "Path outside allowed root" },
2369
+ notFound: () => ({ status: 404, error: "File not found" })
2370
+ });
2371
+ }
2372
+ async function classifyDirent(parentReal, entry, realRoots) {
2373
+ const absolute = path12.join(parentReal, entry.name);
2374
+ if (entry.isDirectory()) return { kind: "directory", real: absolute };
2375
+ if (entry.isFile()) return { kind: "file", real: absolute };
2376
+ if (!entry.isSymbolicLink()) return null;
2377
+ for (const root of realRoots) {
2378
+ const real = await realConfineToRoot(root, absolute);
2379
+ if (!real) continue;
2380
+ try {
2381
+ const stat = await fs11.stat(real);
2382
+ if (stat.isDirectory()) return { kind: "directory", real };
2383
+ if (stat.isFile()) return { kind: "file", real };
2384
+ } catch {
2385
+ }
2386
+ return null;
2387
+ }
2388
+ return null;
2389
+ }
2390
+ var init_confinedFile = __esm({
2391
+ "server/src/util/confinedFile.ts"() {
2392
+ "use strict";
2393
+ init_config();
2394
+ init_paths2();
2395
+ }
2396
+ });
2397
+
2398
+ // server/src/util/filekind.ts
2399
+ import path13 from "node:path";
2400
+ function mimeForExt(ext) {
2401
+ return MIME_MAP[ext.toLowerCase()] ?? "application/octet-stream";
2402
+ }
2403
+ function looksBinary(buf) {
2404
+ if (buf.length === 0) return false;
2405
+ if (buf.length >= 2) {
2406
+ const b0 = buf[0];
2407
+ const b1 = buf[1];
2408
+ if (b0 === 255 && b1 === 254 || b0 === 254 && b1 === 255) return true;
2409
+ }
2410
+ let control = 0;
2411
+ for (let i = 0; i < buf.length; i++) {
2412
+ const b = buf[i];
2413
+ if (b === 0) return true;
2414
+ if (b < 32 && b !== 9 && b !== 10 && b !== 12 && b !== 13) control++;
2415
+ }
2416
+ return control / buf.length > 0.3;
2417
+ }
2418
+ function classifyKind(name) {
2419
+ const ext = path13.extname(name).toLowerCase();
2420
+ if (ext in IMAGE_EXTS2) return "image";
2421
+ if (TEXT_EXTS.has(ext)) return "text";
2422
+ return "binary";
2308
2423
  }
2424
+ var IMAGE_EXTS2, TEXT_EXTS, MIME_MAP;
2425
+ var init_filekind = __esm({
2426
+ "server/src/util/filekind.ts"() {
2427
+ "use strict";
2428
+ IMAGE_EXTS2 = {
2429
+ ".png": "image/png",
2430
+ ".jpg": "image/jpeg",
2431
+ ".jpeg": "image/jpeg",
2432
+ ".gif": "image/gif",
2433
+ ".webp": "image/webp",
2434
+ ".svg": "image/svg+xml",
2435
+ ".bmp": "image/bmp",
2436
+ ".ico": "image/x-icon",
2437
+ ".avif": "image/avif"
2438
+ };
2439
+ TEXT_EXTS = /* @__PURE__ */ new Set([
2440
+ ".txt",
2441
+ ".md",
2442
+ ".markdown",
2443
+ ".rst",
2444
+ ".log",
2445
+ ".csv",
2446
+ ".tsv",
2447
+ ".json",
2448
+ ".jsonl",
2449
+ ".yaml",
2450
+ ".yml",
2451
+ ".toml",
2452
+ ".ini",
2453
+ ".env",
2454
+ ".cfg",
2455
+ ".conf",
2456
+ ".xml",
2457
+ ".html",
2458
+ ".htm",
2459
+ ".css",
2460
+ ".scss",
2461
+ ".less",
2462
+ ".js",
2463
+ ".mjs",
2464
+ ".cjs",
2465
+ ".jsx",
2466
+ ".ts",
2467
+ ".tsx",
2468
+ ".vue",
2469
+ ".svelte",
2470
+ ".py",
2471
+ ".rb",
2472
+ ".go",
2473
+ ".rs",
2474
+ ".java",
2475
+ ".kt",
2476
+ ".c",
2477
+ ".h",
2478
+ ".cpp",
2479
+ ".hpp",
2480
+ ".cc",
2481
+ ".cs",
2482
+ ".php",
2483
+ ".swift",
2484
+ ".scala",
2485
+ ".sh",
2486
+ ".bash",
2487
+ ".zsh",
2488
+ ".fish",
2489
+ ".sql",
2490
+ ".graphql",
2491
+ ".proto",
2492
+ ".dockerfile",
2493
+ ".makefile",
2494
+ ".gradle",
2495
+ ".diff",
2496
+ ".patch",
2497
+ ".tex"
2498
+ ]);
2499
+ MIME_MAP = {
2500
+ ...IMAGE_EXTS2,
2501
+ ".txt": "text/plain",
2502
+ ".md": "text/markdown",
2503
+ ".csv": "text/csv",
2504
+ ".json": "application/json",
2505
+ ".xml": "application/xml",
2506
+ ".yaml": "text/yaml",
2507
+ ".yml": "text/yaml",
2508
+ ".html": "text/html",
2509
+ ".css": "text/css",
2510
+ ".js": "application/javascript",
2511
+ ".mjs": "application/javascript",
2512
+ ".ts": "text/typescript",
2513
+ ".tsx": "text/typescript",
2514
+ ".toml": "text/plain",
2515
+ ".pdf": "application/pdf",
2516
+ ".zip": "application/zip",
2517
+ ".tar": "application/x-tar",
2518
+ ".gz": "application/gzip",
2519
+ ".wasm": "application/wasm",
2520
+ ".exe": "application/vnd.microsoft.portable-executable",
2521
+ ".dll": "application/vnd.microsoft.portable-executable",
2522
+ ".so": "application/x-sharedlib",
2523
+ ".bin": "application/octet-stream"
2524
+ };
2525
+ }
2526
+ });
2527
+
2528
+ // server/src/routes/fs.ts
2529
+ import fs12 from "node:fs/promises";
2530
+ import { createReadStream } from "node:fs";
2531
+ import path14 from "node:path";
2309
2532
  function registerFsRoutes(app) {
2310
2533
  app.get(
2311
2534
  "/api/fs/dirs",
@@ -2313,37 +2536,27 @@ function registerFsRoutes(app) {
2313
2536
  const input = (req.query.path ?? "").trim();
2314
2537
  const base = config.defaultCwd;
2315
2538
  const endsWithSep = input.endsWith("/");
2316
- const resolved = input ? path12.resolve(base, input) : base;
2317
- const dir = endsWithSep || !input ? resolved : path12.dirname(resolved);
2318
- const prefix = endsWithSep || !input ? "" : path12.basename(resolved);
2319
- const targetKind = await fs11.stat(resolved).then((s) => s.isDirectory() ? "directory" : "file").catch(() => "missing");
2320
- if (confinedPath(dir) === null) {
2321
- reply.code(403);
2322
- return { error: "Path outside allowed root" };
2323
- }
2324
- const realDir = await realConfineToRoot(config.fileRoot, dir);
2325
- if (!realDir) {
2539
+ const resolved = input ? path14.resolve(base, input) : base;
2540
+ const dir = endsWithSep || !input ? resolved : path14.dirname(resolved);
2541
+ const prefix = endsWithSep || !input ? "" : path14.basename(resolved);
2542
+ const targetKind = await fs12.stat(resolved).then((s) => s.isDirectory() ? "directory" : "file").catch(() => "missing");
2543
+ const listing = await resolveAbsolutePath(dir, "directory");
2544
+ if (!listing.ok) {
2545
+ if (listing.status === 403) return replyWith(reply, listing);
2326
2546
  return { dir, entries: [], target: resolved, targetKind };
2327
2547
  }
2548
+ const realDir = listing.real;
2328
2549
  let entries = [];
2329
2550
  try {
2330
- const dirents = await fs11.readdir(realDir, { withFileTypes: true });
2551
+ const dirents = await fs12.readdir(realDir, { withFileTypes: true });
2331
2552
  const candidates = dirents.filter((d) => !d.name.startsWith("."));
2332
2553
  const checks2 = await Promise.all(
2333
2554
  candidates.map(async (d) => {
2334
- if (d.isDirectory()) return d.name;
2335
- if (!d.isSymbolicLink()) return null;
2336
- const full = path12.join(realDir, d.name);
2337
- const real = await realConfineToRoot(config.fileRoot, full);
2338
- if (!real) return null;
2339
- try {
2340
- return (await fs11.stat(real)).isDirectory() ? d.name : null;
2341
- } catch {
2342
- return null;
2343
- }
2555
+ const target = await classifyDirent(realDir, d, absoluteRoots());
2556
+ return target?.kind === "directory" ? d.name : null;
2344
2557
  })
2345
2558
  );
2346
- entries = checks2.filter((name) => name !== null).filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())).sort((a, b) => a.localeCompare(b)).slice(0, 20).map((name) => path12.join(dir, name));
2559
+ entries = checks2.filter((name) => name !== null).filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())).sort((a, b) => a.localeCompare(b)).slice(0, 20).map((name) => path14.join(dir, name));
2347
2560
  } catch {
2348
2561
  entries = [];
2349
2562
  }
@@ -2358,72 +2571,43 @@ function registerFsRoutes(app) {
2358
2571
  reply.code(400);
2359
2572
  return { error: "path parameter is required" };
2360
2573
  }
2361
- if (!path12.isAbsolute(filePath)) {
2574
+ if (!path14.isAbsolute(filePath)) {
2362
2575
  reply.code(400);
2363
2576
  return { error: "path must be absolute" };
2364
2577
  }
2365
- const resolved = confinedPath(filePath);
2366
- if (resolved === null) {
2367
- reply.code(403);
2368
- return { error: "Path outside allowed root" };
2369
- }
2370
- const ext = path12.extname(resolved).toLowerCase();
2371
- const mime = IMAGE_MIMES[ext];
2372
- if (!mime) {
2578
+ const ext = path14.extname(filePath).toLowerCase();
2579
+ if (classifyKind(filePath) !== "image") {
2373
2580
  reply.code(400);
2374
2581
  return { error: `Not a supported image type: ${ext}` };
2375
2582
  }
2376
- const real = await realConfineToRoot(config.fileRoot, resolved);
2377
- if (!real) {
2378
- reply.code(404);
2379
- return { error: "File not found" };
2380
- }
2381
- let stat;
2382
- try {
2383
- stat = await fs11.stat(real);
2384
- } catch {
2385
- reply.code(404);
2386
- return { error: "File not found" };
2387
- }
2388
- if (!stat.isFile()) {
2389
- reply.code(400);
2390
- return { error: "Path is not a file" };
2391
- }
2583
+ const resolvedImage = await resolveAbsolutePath(filePath, "file");
2584
+ if (!resolvedImage.ok) return replyWith(reply, resolvedImage);
2585
+ const { real, stat } = resolvedImage;
2392
2586
  if (stat.size > MAX_IMAGE_BYTES) {
2393
2587
  reply.code(413);
2394
2588
  return { error: "Image too large" };
2395
2589
  }
2396
2590
  const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2397
- reply.header("Cache-Control", "private, max-age=3600");
2591
+ reply.header("Cache-Control", "private, no-cache");
2398
2592
  reply.header("ETag", etag);
2399
2593
  reply.header("Last-Modified", stat.mtime.toUTCString());
2400
2594
  if (req.headers["if-none-match"] === etag) {
2401
2595
  reply.code(304);
2402
2596
  return reply.send();
2403
2597
  }
2404
- reply.header("Content-Type", mime);
2598
+ reply.header("Content-Type", mimeForExt(ext));
2405
2599
  reply.header("Content-Length", stat.size);
2406
2600
  return reply.send(createReadStream(real));
2407
2601
  }
2408
2602
  );
2409
2603
  }
2410
- var IMAGE_MIMES, MAX_IMAGE_BYTES;
2604
+ var MAX_IMAGE_BYTES;
2411
2605
  var init_fs = __esm({
2412
2606
  "server/src/routes/fs.ts"() {
2413
2607
  "use strict";
2414
2608
  init_config();
2415
- init_paths2();
2416
- IMAGE_MIMES = {
2417
- ".png": "image/png",
2418
- ".jpg": "image/jpeg",
2419
- ".jpeg": "image/jpeg",
2420
- ".gif": "image/gif",
2421
- ".webp": "image/webp",
2422
- ".svg": "image/svg+xml",
2423
- ".bmp": "image/bmp",
2424
- ".ico": "image/x-icon",
2425
- ".avif": "image/avif"
2426
- };
2609
+ init_confinedFile();
2610
+ init_filekind();
2427
2611
  MAX_IMAGE_BYTES = 20 * 1024 * 1024;
2428
2612
  }
2429
2613
  });
@@ -2537,236 +2721,72 @@ var init_sessions = __esm({
2537
2721
  }
2538
2722
  });
2539
2723
 
2540
- // server/src/util/filekind.ts
2541
- import path13 from "node:path";
2542
- function mimeForExt(ext) {
2543
- return MIME_MAP[ext.toLowerCase()] ?? "application/octet-stream";
2724
+ // server/src/routes/workspace.ts
2725
+ import fs13 from "node:fs/promises";
2726
+ import { createReadStream as createReadStream2 } from "node:fs";
2727
+ import path15 from "node:path";
2728
+ function tooLargeForPreview(size, maxBytes) {
2729
+ return {
2730
+ error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
2731
+ };
2544
2732
  }
2545
- function looksBinary(buf) {
2546
- if (buf.length === 0) return false;
2547
- if (buf.length >= 2) {
2548
- const b0 = buf[0];
2549
- const b1 = buf[1];
2550
- if (b0 === 255 && b1 === 254 || b0 === 254 && b1 === 255) return true;
2551
- }
2552
- let control = 0;
2553
- for (let i = 0; i < buf.length; i++) {
2554
- const b = buf[i];
2555
- if (b === 0) return true;
2556
- if (b < 32 && b !== 9 && b !== 10 && b !== 12 && b !== 13) control++;
2733
+ function hexdump(buf) {
2734
+ const lines = [];
2735
+ for (let off = 0; off < buf.length; off += 16) {
2736
+ const slice = buf.subarray(off, off + 16);
2737
+ const hex = [];
2738
+ let ascii = "";
2739
+ for (let i = 0; i < 16; i++) {
2740
+ if (i < slice.length) {
2741
+ const b = slice[i];
2742
+ hex.push(b.toString(16).padStart(2, "0"));
2743
+ ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
2744
+ } else {
2745
+ hex.push(" ");
2746
+ }
2747
+ if (i === 7) hex.push("");
2748
+ }
2749
+ lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
2557
2750
  }
2558
- return control / buf.length > 0.3;
2559
- }
2560
- function classifyKind(name) {
2561
- const ext = path13.extname(name).toLowerCase();
2562
- if (ext in IMAGE_EXTS2) return "image";
2563
- if (TEXT_EXTS.has(ext)) return "text";
2564
- return "binary";
2565
- }
2566
- var IMAGE_EXTS2, TEXT_EXTS, MIME_MAP;
2567
- var init_filekind = __esm({
2568
- "server/src/util/filekind.ts"() {
2569
- "use strict";
2570
- IMAGE_EXTS2 = {
2571
- ".png": "image/png",
2572
- ".jpg": "image/jpeg",
2573
- ".jpeg": "image/jpeg",
2574
- ".gif": "image/gif",
2575
- ".webp": "image/webp",
2576
- ".svg": "image/svg+xml",
2577
- ".bmp": "image/bmp",
2578
- ".ico": "image/x-icon",
2579
- ".avif": "image/avif"
2580
- };
2581
- TEXT_EXTS = /* @__PURE__ */ new Set([
2582
- ".txt",
2583
- ".md",
2584
- ".markdown",
2585
- ".rst",
2586
- ".log",
2587
- ".csv",
2588
- ".tsv",
2589
- ".json",
2590
- ".jsonl",
2591
- ".yaml",
2592
- ".yml",
2593
- ".toml",
2594
- ".ini",
2595
- ".env",
2596
- ".cfg",
2597
- ".conf",
2598
- ".xml",
2599
- ".html",
2600
- ".htm",
2601
- ".css",
2602
- ".scss",
2603
- ".less",
2604
- ".js",
2605
- ".mjs",
2606
- ".cjs",
2607
- ".jsx",
2608
- ".ts",
2609
- ".tsx",
2610
- ".vue",
2611
- ".svelte",
2612
- ".py",
2613
- ".rb",
2614
- ".go",
2615
- ".rs",
2616
- ".java",
2617
- ".kt",
2618
- ".c",
2619
- ".h",
2620
- ".cpp",
2621
- ".hpp",
2622
- ".cc",
2623
- ".cs",
2624
- ".php",
2625
- ".swift",
2626
- ".scala",
2627
- ".sh",
2628
- ".bash",
2629
- ".zsh",
2630
- ".fish",
2631
- ".sql",
2632
- ".graphql",
2633
- ".proto",
2634
- ".dockerfile",
2635
- ".makefile",
2636
- ".gradle",
2637
- ".diff",
2638
- ".patch",
2639
- ".tex"
2640
- ]);
2641
- MIME_MAP = {
2642
- ...IMAGE_EXTS2,
2643
- ".txt": "text/plain",
2644
- ".md": "text/markdown",
2645
- ".csv": "text/csv",
2646
- ".json": "application/json",
2647
- ".xml": "application/xml",
2648
- ".yaml": "text/yaml",
2649
- ".yml": "text/yaml",
2650
- ".html": "text/html",
2651
- ".css": "text/css",
2652
- ".js": "application/javascript",
2653
- ".mjs": "application/javascript",
2654
- ".ts": "text/typescript",
2655
- ".tsx": "text/typescript",
2656
- ".toml": "text/plain",
2657
- ".pdf": "application/pdf",
2658
- ".zip": "application/zip",
2659
- ".tar": "application/x-tar",
2660
- ".gz": "application/gzip",
2661
- ".wasm": "application/wasm",
2662
- ".exe": "application/vnd.microsoft.portable-executable",
2663
- ".dll": "application/vnd.microsoft.portable-executable",
2664
- ".so": "application/x-sharedlib",
2665
- ".bin": "application/octet-stream"
2666
- };
2667
- }
2668
- });
2669
-
2670
- // server/src/routes/workspace.ts
2671
- import fs12 from "node:fs/promises";
2672
- import { createReadStream as createReadStream2 } from "node:fs";
2673
- import path14 from "node:path";
2674
- function tooLargeForPreview(size, maxBytes) {
2675
- return {
2676
- error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
2677
- };
2678
- }
2679
- function hexdump(buf) {
2680
- const lines = [];
2681
- for (let off = 0; off < buf.length; off += 16) {
2682
- const slice = buf.subarray(off, off + 16);
2683
- const hex = [];
2684
- let ascii = "";
2685
- for (let i = 0; i < 16; i++) {
2686
- if (i < slice.length) {
2687
- const b = slice[i];
2688
- hex.push(b.toString(16).padStart(2, "0"));
2689
- ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
2690
- } else {
2691
- hex.push(" ");
2692
- }
2693
- if (i === 7) hex.push("");
2694
- }
2695
- lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
2696
- }
2697
- return lines.join("\n");
2751
+ return lines.join("\n");
2698
2752
  }
2699
2753
  function registerWorkspaceRoutes(app, manager) {
2700
2754
  app.get(
2701
2755
  "/api/sessions/:id/tree",
2702
2756
  async (req, reply) => {
2703
- let cwd;
2704
- try {
2705
- cwd = await manager.getSessionCwd(req.params.id);
2706
- } catch {
2707
- reply.code(404);
2708
- return { error: "Session not found" };
2709
- }
2710
- const relative = (req.query.path ?? "").replace(/^\/+/, "");
2711
- const target = confineToRoot(cwd, relative);
2712
- if (!target) {
2713
- reply.code(400);
2714
- return { error: "Invalid path" };
2715
- }
2716
- const workspaceMissing = async () => {
2717
- try {
2718
- return !(await fs12.stat(cwd)).isDirectory();
2719
- } catch {
2720
- return true;
2721
- }
2722
- };
2723
- const notFound = async () => {
2724
- reply.code(404);
2725
- return await workspaceMissing() ? { error: `Workspace folder no longer exists: ${cwd}` } : { error: "Directory not found" };
2726
- };
2727
- const realTarget = await realConfineToRoot(config.fileRoot, target);
2728
- if (!realTarget) {
2729
- return notFound();
2730
- }
2757
+ const resolved = await resolveSessionPath(
2758
+ manager,
2759
+ req.params.id,
2760
+ req.query.path,
2761
+ "directory"
2762
+ );
2763
+ if (!resolved.ok) return replyWith(reply, resolved);
2764
+ const { cwd, relative, real: realTarget } = resolved;
2731
2765
  let dirents;
2732
2766
  try {
2733
- dirents = await fs12.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2767
+ dirents = await fs13.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2734
2768
  } catch {
2735
- return notFound();
2769
+ return replyWith(reply, await workspaceNotFound(cwd));
2736
2770
  }
2737
2771
  const entries = [];
2738
2772
  for (const d of dirents) {
2739
- const name = d.name;
2740
- const entryRelative = relative ? `${relative}/${name}` : name;
2741
- const entryAbsolute = path14.join(realTarget, name);
2742
- let kind = d.isDirectory() ? "directory" : d.isFile() ? "file" : null;
2743
- let statTarget = entryAbsolute;
2744
- if (kind === null && d.isSymbolicLink()) {
2745
- const real = await realConfineToRoot(config.fileRoot, entryAbsolute);
2746
- if (!real) continue;
2747
- try {
2748
- const st = await fs12.stat(real);
2749
- kind = st.isDirectory() ? "directory" : st.isFile() ? "file" : null;
2750
- statTarget = real;
2751
- } catch {
2752
- continue;
2753
- }
2773
+ const target = await classifyDirent(realTarget, d, [config.fileRoot]);
2774
+ if (!target) continue;
2775
+ const entryRelative = relative ? `${relative}/${d.name}` : d.name;
2776
+ if (target.kind === "directory") {
2777
+ entries.push({ name: d.name, path: entryRelative, type: "directory" });
2778
+ continue;
2754
2779
  }
2755
- if (kind === null) continue;
2756
- if (kind === "directory") {
2757
- entries.push({ name, path: entryRelative, type: "directory" });
2758
- } else {
2759
- try {
2760
- const stat = await fs12.stat(statTarget);
2761
- entries.push({
2762
- name,
2763
- path: entryRelative,
2764
- type: "file",
2765
- size: stat.size,
2766
- modifiedAt: stat.mtime.toISOString()
2767
- });
2768
- } catch {
2769
- }
2780
+ try {
2781
+ const stat = await fs13.stat(target.real);
2782
+ entries.push({
2783
+ name: d.name,
2784
+ path: entryRelative,
2785
+ type: "file",
2786
+ size: stat.size,
2787
+ modifiedAt: stat.mtime.toISOString()
2788
+ });
2789
+ } catch {
2770
2790
  }
2771
2791
  }
2772
2792
  entries.sort((a, b) => {
@@ -2780,45 +2800,15 @@ function registerWorkspaceRoutes(app, manager) {
2780
2800
  app.get(
2781
2801
  "/api/sessions/:id/download",
2782
2802
  async (req, reply) => {
2783
- let cwd;
2784
- try {
2785
- cwd = await manager.getSessionCwd(req.params.id);
2786
- } catch {
2787
- reply.code(404);
2788
- return { error: "Session not found" };
2789
- }
2790
- const relative = (req.query.path ?? "").replace(/^\/+/, "");
2791
- if (!relative) {
2792
- reply.code(400);
2793
- return { error: "path parameter is required" };
2794
- }
2795
- const target = confineToRoot(cwd, relative);
2796
- if (!target) {
2797
- reply.code(400);
2798
- return { error: "Invalid path" };
2799
- }
2800
- const realTarget = await realConfineToRoot(config.fileRoot, target);
2801
- if (!realTarget) {
2802
- reply.code(404);
2803
- return { error: "File not found" };
2804
- }
2805
- let stat;
2806
- try {
2807
- stat = await fs12.stat(realTarget);
2808
- } catch {
2809
- reply.code(404);
2810
- return { error: "File not found" };
2811
- }
2812
- if (!stat.isFile()) {
2813
- reply.code(400);
2814
- return { error: "Path is not a file" };
2815
- }
2803
+ const resolved = await resolveSessionPath(manager, req.params.id, req.query.path, "file");
2804
+ if (!resolved.ok) return replyWith(reply, resolved);
2805
+ const { real: realTarget, stat } = resolved;
2816
2806
  if (stat.size > MAX_DOWNLOAD_BYTES) {
2817
2807
  reply.code(413);
2818
2808
  return { error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 100 MB)` };
2819
2809
  }
2820
- const ext = path14.extname(realTarget);
2821
- const filename = path14.basename(realTarget);
2810
+ const ext = path15.extname(realTarget);
2811
+ const filename = path15.basename(realTarget);
2822
2812
  const asciiName = filename.replace(/[^\w.\-]/g, "_");
2823
2813
  reply.header("Content-Type", mimeForExt(ext));
2824
2814
  reply.header(
@@ -2832,40 +2822,10 @@ function registerWorkspaceRoutes(app, manager) {
2832
2822
  app.get(
2833
2823
  "/api/sessions/:id/preview",
2834
2824
  async (req, reply) => {
2835
- let cwd;
2836
- try {
2837
- cwd = await manager.getSessionCwd(req.params.id);
2838
- } catch {
2839
- reply.code(404);
2840
- return { error: "Session not found" };
2841
- }
2842
- const relative = (req.query.path ?? "").replace(/^\/+/, "");
2843
- if (!relative) {
2844
- reply.code(400);
2845
- return { error: "path parameter is required" };
2846
- }
2847
- const target = confineToRoot(cwd, relative);
2848
- if (!target) {
2849
- reply.code(400);
2850
- return { error: "Invalid path" };
2851
- }
2852
- const realTarget = await realConfineToRoot(config.fileRoot, target);
2853
- if (!realTarget) {
2854
- reply.code(404);
2855
- return { error: "File not found" };
2856
- }
2857
- let stat;
2858
- try {
2859
- stat = await fs12.stat(realTarget);
2860
- } catch {
2861
- reply.code(404);
2862
- return { error: "File not found" };
2863
- }
2864
- if (!stat.isFile()) {
2865
- reply.code(400);
2866
- return { error: "Path is not a file" };
2867
- }
2868
- const ext = path14.extname(realTarget).toLowerCase();
2825
+ const resolved = await resolveSessionPath(manager, req.params.id, req.query.path, "file");
2826
+ if (!resolved.ok) return replyWith(reply, resolved);
2827
+ const { real: realTarget, stat } = resolved;
2828
+ const ext = path15.extname(realTarget).toLowerCase();
2869
2829
  const mime = mimeForExt(ext);
2870
2830
  const isImage = mime.startsWith("image/");
2871
2831
  const isPdf = mime === "application/pdf";
@@ -2894,7 +2854,7 @@ function registerWorkspaceRoutes(app, manager) {
2894
2854
  }
2895
2855
  if (isImage || isPdf) {
2896
2856
  const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2897
- reply.header("Cache-Control", "private, max-age=3600");
2857
+ reply.header("Cache-Control", "private, no-cache");
2898
2858
  reply.header("ETag", etag);
2899
2859
  reply.header("Last-Modified", stat.mtime.toUTCString());
2900
2860
  const ifNoneMatch = req.headers["if-none-match"];
@@ -2910,7 +2870,7 @@ function registerWorkspaceRoutes(app, manager) {
2910
2870
  if (kind === "binary") {
2911
2871
  let fh;
2912
2872
  try {
2913
- fh = await fs12.open(realTarget, "r");
2873
+ fh = await fs13.open(realTarget, "r");
2914
2874
  const buf = Buffer.alloc(Math.min(HEXDUMP_BYTES, stat.size));
2915
2875
  const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
2916
2876
  const head = buf.subarray(0, bytesRead);
@@ -2949,7 +2909,7 @@ var init_workspace = __esm({
2949
2909
  "server/src/routes/workspace.ts"() {
2950
2910
  "use strict";
2951
2911
  init_config();
2952
- init_paths2();
2912
+ init_confinedFile();
2953
2913
  init_filekind();
2954
2914
  MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
2955
2915
  HEXDUMP_BYTES = 4096;
@@ -2960,30 +2920,30 @@ var init_workspace = __esm({
2960
2920
 
2961
2921
  // server/src/routes/uploads.ts
2962
2922
  import { createReadStream as createReadStream3, createWriteStream } from "node:fs";
2963
- import fs13 from "node:fs/promises";
2964
- import path15 from "node:path";
2923
+ import fs14 from "node:fs/promises";
2924
+ import path16 from "node:path";
2965
2925
  import crypto5 from "node:crypto";
2966
2926
  import { execFile as execFile3 } from "node:child_process";
2967
2927
  import { pipeline } from "node:stream/promises";
2968
2928
  function uploadDirFor(sessionId) {
2969
- return path15.join(config.casperDataDir, "sessions", sessionId, "uploads");
2929
+ return path16.join(config.casperDataDir, "sessions", sessionId, "uploads");
2970
2930
  }
2971
2931
  function sanitizeName(raw) {
2972
- const base = path15.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
2932
+ const base = path16.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
2973
2933
  if (!base || base === "." || base === "..") return `upload-${Date.now()}`;
2974
2934
  return base.replace(/^\.+/, "") || `upload-${Date.now()}`;
2975
2935
  }
2976
2936
  async function uniquePath(dir, name) {
2977
- const ext = path15.extname(name);
2937
+ const ext = path16.extname(name);
2978
2938
  const stem = name.slice(0, name.length - ext.length);
2979
2939
  let candidate = name;
2980
2940
  let n = 1;
2981
2941
  while (true) {
2982
2942
  try {
2983
- await fs13.access(path15.join(dir, candidate));
2943
+ await fs14.access(path16.join(dir, candidate));
2984
2944
  candidate = `${stem} (${n++})${ext}`;
2985
2945
  } catch {
2986
- return path15.join(dir, candidate);
2946
+ return path16.join(dir, candidate);
2987
2947
  }
2988
2948
  }
2989
2949
  }
@@ -3003,7 +2963,7 @@ function fileType(absPath) {
3003
2963
  async function sampleStrings(absPath, limit = 40) {
3004
2964
  let fh;
3005
2965
  try {
3006
- fh = await fs13.open(absPath, "r");
2966
+ fh = await fs14.open(absPath, "r");
3007
2967
  const buf = Buffer.alloc(STRINGS_SCAN_BYTES);
3008
2968
  const { bytesRead } = await fh.read(buf, 0, STRINGS_SCAN_BYTES, 0);
3009
2969
  const out = [];
@@ -3041,7 +3001,7 @@ function registerUploadRoutes(app, manager) {
3041
3001
  return { error: "Invalid session id" };
3042
3002
  }
3043
3003
  const uploadDir = uploadDirFor(req.params.id);
3044
- await fs13.mkdir(uploadDir, { recursive: true, mode: 448 });
3004
+ await fs14.mkdir(uploadDir, { recursive: true, mode: 448 });
3045
3005
  const results = [];
3046
3006
  const parts = req.files();
3047
3007
  for await (const part of parts) {
@@ -3055,17 +3015,17 @@ function registerUploadRoutes(app, manager) {
3055
3015
  return { error: `Failed to store ${name}` };
3056
3016
  }
3057
3017
  if (part.file.truncated) {
3058
- await fs13.rm(dest, { force: true });
3018
+ await fs14.rm(dest, { force: true });
3059
3019
  reply.code(413);
3060
3020
  return {
3061
3021
  error: `${name} exceeds the max upload size (${Math.round(config.maxUploadBytes / 1024 / 1024)} MB)`
3062
3022
  };
3063
3023
  }
3064
- const stat = await fs13.stat(dest);
3065
- const ext = path15.extname(dest);
3024
+ const stat = await fs14.stat(dest);
3025
+ const ext = path16.extname(dest);
3066
3026
  const kind = classifyKind(dest);
3067
3027
  const uploaded = {
3068
- name: path15.basename(dest),
3028
+ name: path16.basename(dest),
3069
3029
  // Absolute, because the file sits outside the session's workspace: a
3070
3030
  // relative path would resolve against the wrong root for both the agent
3071
3031
  // and the image endpoint.
@@ -3122,97 +3082,8 @@ var init_health = __esm({
3122
3082
  }
3123
3083
  });
3124
3084
 
3125
- // server/src/ws/dispatch.ts
3126
- function send(socket, msg) {
3127
- if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(msg));
3128
- }
3129
- async function handleClientMessage(socket, manager, sessionId, msg, watchers) {
3130
- switch (msg.type) {
3131
- case "ping":
3132
- send(socket, { type: "pong" });
3133
- return;
3134
- case "hello":
3135
- return;
3136
- case "watch_paths":
3137
- await watchers.sync(msg.paths);
3138
- return;
3139
- case "prompt":
3140
- try {
3141
- await manager.runPrompt(sessionId, msg.content);
3142
- send(socket, { type: "ack", action: "prompt", ok: true });
3143
- } catch (err) {
3144
- send(socket, {
3145
- type: "ack",
3146
- action: "prompt",
3147
- ok: false,
3148
- error: err.message
3149
- });
3150
- }
3151
- return;
3152
- case "cancel":
3153
- try {
3154
- manager.cancel(sessionId);
3155
- send(socket, { type: "ack", action: "cancel", ok: true });
3156
- } catch (err) {
3157
- send(socket, {
3158
- type: "ack",
3159
- action: "cancel",
3160
- ok: false,
3161
- error: err.message
3162
- });
3163
- }
3164
- return;
3165
- case "set_mode":
3166
- try {
3167
- await manager.setMode(sessionId, msg.modeId);
3168
- send(socket, { type: "ack", action: "set_mode", ok: true });
3169
- } catch (err) {
3170
- send(socket, {
3171
- type: "ack",
3172
- action: "set_mode",
3173
- ok: false,
3174
- error: err.message
3175
- });
3176
- }
3177
- return;
3178
- case "set_model":
3179
- try {
3180
- await manager.setModel(sessionId, msg.modelId);
3181
- send(socket, { type: "ack", action: "set_model", ok: true });
3182
- } catch (err) {
3183
- send(socket, {
3184
- type: "ack",
3185
- action: "set_model",
3186
- ok: false,
3187
- error: err.message
3188
- });
3189
- }
3190
- return;
3191
- case "exec_command":
3192
- try {
3193
- await manager.execCommand(sessionId, msg.command, msg.args);
3194
- send(socket, { type: "ack", action: "exec_command", ok: true });
3195
- } catch (err) {
3196
- send(socket, {
3197
- type: "ack",
3198
- action: "exec_command",
3199
- ok: false,
3200
- error: err.message
3201
- });
3202
- }
3203
- return;
3204
- default:
3205
- send(socket, { type: "error", message: `Unknown message type` });
3206
- }
3207
- }
3208
- var init_dispatch = __esm({
3209
- "server/src/ws/dispatch.ts"() {
3210
- "use strict";
3211
- }
3212
- });
3213
-
3214
3085
  // server/src/ws/dirWatchers.ts
3215
- import fs14 from "node:fs";
3086
+ import fs15 from "node:fs";
3216
3087
  function diffWatchSet(current, next) {
3217
3088
  const have = new Set(current);
3218
3089
  const want = new Set(next.slice(0, MAX_WATCHES));
@@ -3236,7 +3107,7 @@ function createDirWatchers(opts) {
3236
3107
  const target = await opts.resolve(relative);
3237
3108
  if (!target || closed) return;
3238
3109
  try {
3239
- const watcher = fs14.watch(target, { persistent: false }, () => {
3110
+ const watcher = fs15.watch(target, { persistent: false }, () => {
3240
3111
  const existing = timers.get(relative);
3241
3112
  if (existing) clearTimeout(existing);
3242
3113
  timers.set(
@@ -3278,6 +3149,122 @@ var init_dirWatchers = __esm({
3278
3149
  });
3279
3150
 
3280
3151
  // server/src/ws/gateway.ts
3152
+ function send(socket, msg) {
3153
+ if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(msg));
3154
+ }
3155
+ function handleConnection(socket, manager, sessionId, startCursor) {
3156
+ let cursor = startCursor;
3157
+ let unsubscribe = null;
3158
+ let alive = true;
3159
+ let ready = false;
3160
+ const watchers = createDirWatchers({
3161
+ resolve: async (relative) => {
3162
+ try {
3163
+ return confineToRoot(await manager.getSessionCwd(sessionId), relative);
3164
+ } catch {
3165
+ return null;
3166
+ }
3167
+ },
3168
+ onChange: (path19) => send(socket, { type: "fs_changed", path: path19 })
3169
+ });
3170
+ const forward = (event) => {
3171
+ if (event.seq <= cursor) return;
3172
+ cursor = event.seq;
3173
+ send(socket, { type: "event", event });
3174
+ };
3175
+ const attach = async () => {
3176
+ try {
3177
+ await manager.ensureOpen(sessionId);
3178
+ } catch (err) {
3179
+ send(socket, { type: "error", message: err.message });
3180
+ socket.close(1011, "open failed");
3181
+ return;
3182
+ }
3183
+ const store = manager.getStore(sessionId);
3184
+ if (!store) {
3185
+ send(socket, { type: "error", message: "Session store unavailable" });
3186
+ socket.close(1011, "no store");
3187
+ return;
3188
+ }
3189
+ const { events, gap } = store.getSince(cursor);
3190
+ if (gap) {
3191
+ send(socket, {
3192
+ type: "resync",
3193
+ reason: "cursor older than buffer; refetch full transcript"
3194
+ });
3195
+ cursor = store.head();
3196
+ } else {
3197
+ for (const e of events) forward(e);
3198
+ }
3199
+ send(socket, { type: "replay_complete", head: store.head() });
3200
+ unsubscribe = manager.onEvent(sessionId, forward);
3201
+ ready = true;
3202
+ };
3203
+ void attach();
3204
+ const ack = async (action, run) => {
3205
+ try {
3206
+ await run();
3207
+ send(socket, { type: "ack", action, ok: true });
3208
+ } catch (err) {
3209
+ send(socket, { type: "ack", action, ok: false, error: err.message });
3210
+ }
3211
+ };
3212
+ const handle2 = (msg) => {
3213
+ switch (msg.type) {
3214
+ case "ping":
3215
+ return send(socket, { type: "pong" });
3216
+ case "watch_paths":
3217
+ return watchers.sync(msg.paths);
3218
+ case "prompt":
3219
+ return ack("prompt", () => manager.runPrompt(sessionId, msg.content));
3220
+ case "cancel":
3221
+ return ack("cancel", () => manager.cancel(sessionId));
3222
+ case "set_mode":
3223
+ return ack("set_mode", () => manager.setMode(sessionId, msg.modeId));
3224
+ case "set_model":
3225
+ return ack("set_model", () => manager.setModel(sessionId, msg.modelId));
3226
+ case "exec_command":
3227
+ return ack(
3228
+ "exec_command",
3229
+ () => manager.execCommand(sessionId, msg.command, msg.args)
3230
+ );
3231
+ default:
3232
+ return send(socket, { type: "error", message: "Unknown message type" });
3233
+ }
3234
+ };
3235
+ const heartbeat = setInterval(() => {
3236
+ if (!alive) {
3237
+ socket.terminate();
3238
+ return;
3239
+ }
3240
+ alive = false;
3241
+ try {
3242
+ socket.ping();
3243
+ } catch {
3244
+ }
3245
+ }, HEARTBEAT_MS);
3246
+ heartbeat.unref?.();
3247
+ socket.on("pong", () => {
3248
+ alive = true;
3249
+ });
3250
+ socket.on("message", (raw) => {
3251
+ alive = true;
3252
+ if (!ready) return;
3253
+ let msg;
3254
+ try {
3255
+ msg = JSON.parse(raw.toString());
3256
+ } catch {
3257
+ send(socket, { type: "error", message: "Invalid JSON" });
3258
+ return;
3259
+ }
3260
+ void handle2(msg);
3261
+ });
3262
+ socket.on("close", () => {
3263
+ clearInterval(heartbeat);
3264
+ unsubscribe?.();
3265
+ watchers.close();
3266
+ });
3267
+ }
3281
3268
  function registerWsGateway(app, manager) {
3282
3269
  app.get("/ws", { websocket: true }, (socket, req) => {
3283
3270
  const query = req.query;
@@ -3286,92 +3273,17 @@ function registerWsGateway(app, manager) {
3286
3273
  socket.close(1008, "Unauthorized");
3287
3274
  return;
3288
3275
  }
3289
- const sessionId = query.sessionId;
3290
- if (!sessionId) {
3276
+ if (!query.sessionId) {
3291
3277
  send(socket, { type: "error", message: "Missing sessionId" });
3292
3278
  socket.close(1008, "Missing sessionId");
3293
3279
  return;
3294
3280
  }
3295
- let cursor = Number.parseInt(query.cursor ?? "0", 10) || 0;
3296
- let unsubscribe = null;
3297
- const watchers = createDirWatchers({
3298
- resolve: async (relative) => {
3299
- try {
3300
- return confineToRoot(await manager.getSessionCwd(sessionId), relative);
3301
- } catch {
3302
- return null;
3303
- }
3304
- },
3305
- onChange: (path18) => send(socket, { type: "fs_changed", path: path18 })
3306
- });
3307
- let alive = true;
3308
- let ready = false;
3309
- const forward = (event) => {
3310
- if (event.seq <= cursor) return;
3311
- cursor = event.seq;
3312
- send(socket, { type: "event", event });
3313
- };
3314
- const attach = async () => {
3315
- try {
3316
- await manager.ensureOpen(sessionId);
3317
- } catch (err) {
3318
- send(socket, { type: "error", message: err.message });
3319
- socket.close(1011, "open failed");
3320
- return;
3321
- }
3322
- const store = manager.getStore(sessionId);
3323
- if (!store) {
3324
- send(socket, { type: "error", message: "Session store unavailable" });
3325
- socket.close(1011, "no store");
3326
- return;
3327
- }
3328
- const { events, gap } = store.getSince(cursor);
3329
- if (gap) {
3330
- send(socket, {
3331
- type: "resync",
3332
- reason: "cursor older than buffer; refetch full transcript"
3333
- });
3334
- cursor = store.head();
3335
- } else {
3336
- for (const e of events) forward(e);
3337
- }
3338
- send(socket, { type: "replay_complete", head: store.head() });
3339
- unsubscribe = manager.onEvent(sessionId, forward);
3340
- ready = true;
3341
- };
3342
- void attach();
3343
- const heartbeat = setInterval(() => {
3344
- if (!alive) {
3345
- socket.terminate();
3346
- return;
3347
- }
3348
- alive = false;
3349
- try {
3350
- socket.ping();
3351
- } catch {
3352
- }
3353
- }, HEARTBEAT_MS);
3354
- heartbeat.unref?.();
3355
- socket.on("pong", () => {
3356
- alive = true;
3357
- });
3358
- socket.on("message", (raw) => {
3359
- alive = true;
3360
- if (!ready) return;
3361
- let msg;
3362
- try {
3363
- msg = JSON.parse(raw.toString());
3364
- } catch {
3365
- send(socket, { type: "error", message: "Invalid JSON" });
3366
- return;
3367
- }
3368
- void handleClientMessage(socket, manager, sessionId, msg, watchers);
3369
- });
3370
- socket.on("close", () => {
3371
- clearInterval(heartbeat);
3372
- unsubscribe?.();
3373
- watchers.close();
3374
- });
3281
+ handleConnection(
3282
+ socket,
3283
+ manager,
3284
+ query.sessionId,
3285
+ Number.parseInt(query.cursor ?? "0", 10) || 0
3286
+ );
3375
3287
  });
3376
3288
  }
3377
3289
  var HEARTBEAT_MS;
@@ -3379,7 +3291,6 @@ var init_gateway = __esm({
3379
3291
  "server/src/ws/gateway.ts"() {
3380
3292
  "use strict";
3381
3293
  init_auth();
3382
- init_dispatch();
3383
3294
  init_dirWatchers();
3384
3295
  init_paths2();
3385
3296
  HEARTBEAT_MS = 2e4;
@@ -3387,7 +3298,7 @@ var init_gateway = __esm({
3387
3298
  });
3388
3299
 
3389
3300
  // server/src/app.ts
3390
- import fs15 from "node:fs";
3301
+ import fs16 from "node:fs";
3391
3302
  import Fastify from "fastify";
3392
3303
  import cors from "@fastify/cors";
3393
3304
  import websocket from "@fastify/websocket";
@@ -3425,20 +3336,20 @@ async function buildApp() {
3425
3336
  registerWorkspaceRoutes(app, manager);
3426
3337
  registerUploadRoutes(app, manager);
3427
3338
  registerWsGateway(app, manager);
3428
- if (fs15.existsSync(config.webDist)) {
3339
+ if (fs16.existsSync(config.webDist)) {
3429
3340
  await app.register(fastifyStatic, {
3430
3341
  root: config.webDist
3431
3342
  });
3432
3343
  app.addHook("onSend", async (req, reply) => {
3433
- const path18 = req.url.split("?")[0] ?? "";
3434
- if (path18 === "/manifest.json" || path18.startsWith("/icons/")) {
3344
+ const path19 = req.url.split("?")[0] ?? "";
3345
+ if (path19 === "/manifest.json" || path19.startsWith("/icons/")) {
3435
3346
  reply.header("cache-control", "public, max-age=300");
3436
3347
  }
3437
3348
  });
3438
3349
  app.setNotFoundHandler((req, reply) => {
3439
- const path18 = req.url.split("?")[0] ?? "";
3440
- const looksLikeAsset = path18.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path18);
3441
- if (path18.startsWith("/api/") || path18.startsWith("/ws") || looksLikeAsset) {
3350
+ const path19 = req.url.split("?")[0] ?? "";
3351
+ const looksLikeAsset = path19.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path19);
3352
+ if (path19.startsWith("/api/") || path19.startsWith("/ws") || looksLikeAsset) {
3442
3353
  reply.code(404).send({ error: "Not found" });
3443
3354
  return;
3444
3355
  }
@@ -3512,15 +3423,15 @@ __export(doctor_exports, {
3512
3423
  mcpWiring: () => mcpWiring
3513
3424
  });
3514
3425
  import { execFileSync as execFileSync3 } from "node:child_process";
3515
- import fs16 from "node:fs";
3516
- import path16 from "node:path";
3426
+ import fs17 from "node:fs";
3427
+ import path17 from "node:path";
3517
3428
  function which(bin) {
3518
- if (bin.includes("/")) return fs16.existsSync(bin) ? bin : void 0;
3519
- for (const dir of (process.env.PATH ?? "").split(path16.delimiter)) {
3429
+ if (bin.includes("/")) return fs17.existsSync(bin) ? bin : void 0;
3430
+ for (const dir of (process.env.PATH ?? "").split(path17.delimiter)) {
3520
3431
  if (dir === "") continue;
3521
- const candidate = path16.join(dir, bin);
3432
+ const candidate = path17.join(dir, bin);
3522
3433
  try {
3523
- fs16.accessSync(candidate, fs16.constants.X_OK);
3434
+ fs17.accessSync(candidate, fs17.constants.X_OK);
3524
3435
  return candidate;
3525
3436
  } catch {
3526
3437
  }
@@ -3544,7 +3455,7 @@ function mcpWiring(agentJson, resolve) {
3544
3455
  return { ok: false, detail: `${command || "(no command)"} not found - widgets are unavailable` };
3545
3456
  }
3546
3457
  const script = args.find((a) => a.endsWith(".js"));
3547
- if (script && !fs16.existsSync(script)) {
3458
+ if (script && !fs17.existsSync(script)) {
3548
3459
  return { ok: false, detail: `${script} is gone - delete the agent file and restart to rewrite it` };
3549
3460
  }
3550
3461
  return { ok: true, detail: script ?? `${command} ${args.join(" ")}` };
@@ -3576,7 +3487,7 @@ function checks() {
3576
3487
  const settings = readSettings(config.configFile);
3577
3488
  const hasToken = typeof settings.token === "string" && settings.token !== "";
3578
3489
  out.push(
3579
- fs16.existsSync(config.configFile) ? {
3490
+ fs17.existsSync(config.configFile) ? {
3580
3491
  ok: hasToken,
3581
3492
  name: "settings",
3582
3493
  detail: hasToken ? `${config.configFile} (token set)` : `${config.configFile} exists but has no token - auth is DISABLED`
@@ -3586,8 +3497,8 @@ function checks() {
3586
3497
  detail: `${config.configFile} not found; a token is generated on first run`
3587
3498
  }
3588
3499
  );
3589
- if (fs16.existsSync(config.configFile)) {
3590
- const mode = fs16.statSync(config.configFile).mode & 511;
3500
+ if (fs17.existsSync(config.configFile)) {
3501
+ const mode = fs17.statSync(config.configFile).mode & 511;
3591
3502
  out.push({
3592
3503
  ok: mode === 384,
3593
3504
  name: "settings mode",
@@ -3595,10 +3506,10 @@ function checks() {
3595
3506
  });
3596
3507
  }
3597
3508
  try {
3598
- fs16.mkdirSync(config.casperDataDir, { recursive: true });
3599
- fs16.accessSync(config.casperDataDir, fs16.constants.W_OK);
3600
- const db2 = path16.join(config.casperDataDir, "casper.db");
3601
- const size = fs16.existsSync(db2) ? `, casper.db ${(fs16.statSync(db2).size / 1024).toFixed(0)} KB` : "";
3509
+ fs17.mkdirSync(config.casperDataDir, { recursive: true });
3510
+ fs17.accessSync(config.casperDataDir, fs17.constants.W_OK);
3511
+ const db2 = path17.join(config.casperDataDir, "casper.db");
3512
+ const size = fs17.existsSync(db2) ? `, casper.db ${(fs17.statSync(db2).size / 1024).toFixed(0)} KB` : "";
3602
3513
  out.push({ ok: true, name: "data dir", detail: `${config.casperDataDir} writable${size}` });
3603
3514
  } catch (err) {
3604
3515
  out.push({
@@ -3609,22 +3520,22 @@ function checks() {
3609
3520
  });
3610
3521
  }
3611
3522
  out.push(
3612
- fs16.existsSync(config.webDist) ? { ok: true, name: "web app", detail: config.webDist } : {
3523
+ fs17.existsSync(config.webDist) ? { ok: true, name: "web app", detail: config.webDist } : {
3613
3524
  ok: false,
3614
3525
  fatal: true,
3615
3526
  name: "web app",
3616
3527
  detail: `${config.webDist} not found - the package is incomplete or CASPER_WEB_DIST is wrong`
3617
3528
  }
3618
3529
  );
3619
- const agent = path16.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3620
- const hasAgent = fs16.existsSync(agent);
3530
+ const agent = path17.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3531
+ const hasAgent = fs17.existsSync(agent);
3621
3532
  out.push({
3622
3533
  ok: hasAgent,
3623
3534
  name: "casper agent",
3624
3535
  detail: hasAgent ? agent : `${agent} missing - it is installed on first run`
3625
3536
  });
3626
3537
  if (hasAgent) {
3627
- const wiring = mcpWiring(fs16.readFileSync(agent, "utf8"), which);
3538
+ const wiring = mcpWiring(fs17.readFileSync(agent, "utf8"), which);
3628
3539
  out.push({ ok: wiring.ok, name: "casper mcp", detail: wiring.detail });
3629
3540
  }
3630
3541
  return out;
@@ -4088,14 +3999,14 @@ var init_server2 = __esm({
4088
3999
  "server/src/mcp/server.ts"() {
4089
4000
  "use strict";
4090
4001
  init_protocol();
4091
- VERSION = true ? "0.9.8" : "dev";
4002
+ VERSION = true ? "0.10.0" : "dev";
4092
4003
  }
4093
4004
  });
4094
4005
 
4095
4006
  // server/src/cli/program.ts
4096
- import fs17 from "node:fs";
4007
+ import fs18 from "node:fs";
4097
4008
  import os4 from "node:os";
4098
- import path17 from "node:path";
4009
+ import path18 from "node:path";
4099
4010
 
4100
4011
  // server/node_modules/commander/lib/error.js
4101
4012
  var CommanderError = class extends Error {
@@ -7208,9 +7119,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
7208
7119
  * @param {string} [path]
7209
7120
  * @return {(string|null|Command)}
7210
7121
  */
7211
- executableDir(path18) {
7212
- if (path18 === void 0) return this._executableDir;
7213
- this._executableDir = path18;
7122
+ executableDir(path19) {
7123
+ if (path19 === void 0) return this._executableDir;
7124
+ this._executableDir = path19;
7214
7125
  return this;
7215
7126
  }
7216
7127
  /**
@@ -7468,7 +7379,7 @@ var program = new Command();
7468
7379
  // server/src/cli/program.ts
7469
7380
  init_paths();
7470
7381
  init_settings();
7471
- var VERSION2 = true ? "0.9.8" : "dev";
7382
+ var VERSION2 = true ? "0.10.0" : "dev";
7472
7383
  async function bootstrap() {
7473
7384
  const file = configFilePath();
7474
7385
  const settings = readSettings(file);
@@ -7514,8 +7425,8 @@ async function resetToken(value) {
7514
7425
  updateSettings(file, { token });
7515
7426
  process.stdout.write(`wrote a new token to ${file}
7516
7427
  `);
7517
- const dbFile = path17.join(dataDirPath(), "casper.db");
7518
- if (fs17.existsSync(dbFile)) {
7428
+ const dbFile = path18.join(dataDirPath(), "casper.db");
7429
+ if (fs18.existsSync(dbFile)) {
7519
7430
  const { LoginStore: LoginStore2 } = await Promise.resolve().then(() => (init_logins(), logins_exports));
7520
7431
  new LoginStore2().revokeAll();
7521
7432
  process.stdout.write("revoked all device sessions\n");