@joeyshi12/casper 0.9.7 → 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 +565 -638
  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.
@@ -2174,6 +2168,20 @@ var init_auth = __esm({
2174
2168
  // server/src/session/models.ts
2175
2169
  import { execFile } from "node:child_process";
2176
2170
  import { promisify } from "node:util";
2171
+ async function configuredDefault() {
2172
+ try {
2173
+ const { stdout } = await execFileAsync(config.kiroBin, [
2174
+ "settings",
2175
+ "chat.defaultModel",
2176
+ "-f",
2177
+ "json"
2178
+ ]);
2179
+ const value = JSON.parse(stdout);
2180
+ return typeof value === "string" && value ? value : void 0;
2181
+ } catch {
2182
+ return void 0;
2183
+ }
2184
+ }
2177
2185
  async function listModels(log) {
2178
2186
  if (cache) return cache;
2179
2187
  const { stdout } = await execFileAsync(
@@ -2182,6 +2190,8 @@ async function listModels(log) {
2182
2190
  { maxBuffer: 4 * 1024 * 1024 }
2183
2191
  );
2184
2192
  const parsed = JSON.parse(stdout);
2193
+ const configured = await configuredDefault();
2194
+ const preferred = parsed.models.some((m) => m.model_id === configured) ? configured : parsed.default_model;
2185
2195
  cache = parsed.models.map((m, i) => ({
2186
2196
  modelId: m.model_id,
2187
2197
  modelName: m.model_name,
@@ -2189,9 +2199,9 @@ async function listModels(log) {
2189
2199
  contextWindowTokens: m.context_window_tokens,
2190
2200
  rateMultiplier: m.rate_multiplier,
2191
2201
  rateUnit: m.rate_unit,
2192
- isDefault: m.default_model ?? i === 0
2202
+ isDefault: preferred ? m.model_id === preferred : m.default_model ?? i === 0
2193
2203
  }));
2194
- log.info({ count: cache.length }, "loaded model list");
2204
+ log.info({ count: cache.length, default: preferred }, "loaded model list");
2195
2205
  return cache;
2196
2206
  }
2197
2207
  async function kiroVersion() {
@@ -2283,13 +2293,242 @@ var init_agents2 = __esm({
2283
2293
  }
2284
2294
  });
2285
2295
 
2286
- // server/src/routes/fs.ts
2296
+ // server/src/util/confinedFile.ts
2287
2297
  import fs11 from "node:fs/promises";
2288
- import { createReadStream } from "node:fs";
2289
2298
  import path12 from "node:path";
2290
- function confinedPath(input) {
2291
- 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";
2292
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";
2293
2532
  function registerFsRoutes(app) {
2294
2533
  app.get(
2295
2534
  "/api/fs/dirs",
@@ -2297,37 +2536,27 @@ function registerFsRoutes(app) {
2297
2536
  const input = (req.query.path ?? "").trim();
2298
2537
  const base = config.defaultCwd;
2299
2538
  const endsWithSep = input.endsWith("/");
2300
- const resolved = input ? path12.resolve(base, input) : base;
2301
- const dir = endsWithSep || !input ? resolved : path12.dirname(resolved);
2302
- const prefix = endsWithSep || !input ? "" : path12.basename(resolved);
2303
- const targetKind = await fs11.stat(resolved).then((s) => s.isDirectory() ? "directory" : "file").catch(() => "missing");
2304
- if (confinedPath(dir) === null) {
2305
- reply.code(403);
2306
- return { error: "Path outside allowed root" };
2307
- }
2308
- const realDir = await realConfineToRoot(config.fileRoot, dir);
2309
- 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);
2310
2546
  return { dir, entries: [], target: resolved, targetKind };
2311
2547
  }
2548
+ const realDir = listing.real;
2312
2549
  let entries = [];
2313
2550
  try {
2314
- const dirents = await fs11.readdir(realDir, { withFileTypes: true });
2551
+ const dirents = await fs12.readdir(realDir, { withFileTypes: true });
2315
2552
  const candidates = dirents.filter((d) => !d.name.startsWith("."));
2316
2553
  const checks2 = await Promise.all(
2317
2554
  candidates.map(async (d) => {
2318
- if (d.isDirectory()) return d.name;
2319
- if (!d.isSymbolicLink()) return null;
2320
- const full = path12.join(realDir, d.name);
2321
- const real = await realConfineToRoot(config.fileRoot, full);
2322
- if (!real) return null;
2323
- try {
2324
- return (await fs11.stat(real)).isDirectory() ? d.name : null;
2325
- } catch {
2326
- return null;
2327
- }
2555
+ const target = await classifyDirent(realDir, d, absoluteRoots());
2556
+ return target?.kind === "directory" ? d.name : null;
2328
2557
  })
2329
2558
  );
2330
- 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));
2331
2560
  } catch {
2332
2561
  entries = [];
2333
2562
  }
@@ -2342,72 +2571,43 @@ function registerFsRoutes(app) {
2342
2571
  reply.code(400);
2343
2572
  return { error: "path parameter is required" };
2344
2573
  }
2345
- if (!path12.isAbsolute(filePath)) {
2574
+ if (!path14.isAbsolute(filePath)) {
2346
2575
  reply.code(400);
2347
2576
  return { error: "path must be absolute" };
2348
2577
  }
2349
- const resolved = confinedPath(filePath);
2350
- if (resolved === null) {
2351
- reply.code(403);
2352
- return { error: "Path outside allowed root" };
2353
- }
2354
- const ext = path12.extname(resolved).toLowerCase();
2355
- const mime = IMAGE_MIMES[ext];
2356
- if (!mime) {
2578
+ const ext = path14.extname(filePath).toLowerCase();
2579
+ if (classifyKind(filePath) !== "image") {
2357
2580
  reply.code(400);
2358
2581
  return { error: `Not a supported image type: ${ext}` };
2359
2582
  }
2360
- const real = await realConfineToRoot(config.fileRoot, resolved);
2361
- if (!real) {
2362
- reply.code(404);
2363
- return { error: "File not found" };
2364
- }
2365
- let stat;
2366
- try {
2367
- stat = await fs11.stat(real);
2368
- } catch {
2369
- reply.code(404);
2370
- return { error: "File not found" };
2371
- }
2372
- if (!stat.isFile()) {
2373
- reply.code(400);
2374
- return { error: "Path is not a file" };
2375
- }
2583
+ const resolvedImage = await resolveAbsolutePath(filePath, "file");
2584
+ if (!resolvedImage.ok) return replyWith(reply, resolvedImage);
2585
+ const { real, stat } = resolvedImage;
2376
2586
  if (stat.size > MAX_IMAGE_BYTES) {
2377
2587
  reply.code(413);
2378
2588
  return { error: "Image too large" };
2379
2589
  }
2380
2590
  const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2381
- reply.header("Cache-Control", "private, max-age=3600");
2591
+ reply.header("Cache-Control", "private, no-cache");
2382
2592
  reply.header("ETag", etag);
2383
2593
  reply.header("Last-Modified", stat.mtime.toUTCString());
2384
2594
  if (req.headers["if-none-match"] === etag) {
2385
2595
  reply.code(304);
2386
2596
  return reply.send();
2387
2597
  }
2388
- reply.header("Content-Type", mime);
2598
+ reply.header("Content-Type", mimeForExt(ext));
2389
2599
  reply.header("Content-Length", stat.size);
2390
2600
  return reply.send(createReadStream(real));
2391
2601
  }
2392
2602
  );
2393
2603
  }
2394
- var IMAGE_MIMES, MAX_IMAGE_BYTES;
2604
+ var MAX_IMAGE_BYTES;
2395
2605
  var init_fs = __esm({
2396
2606
  "server/src/routes/fs.ts"() {
2397
2607
  "use strict";
2398
2608
  init_config();
2399
- init_paths2();
2400
- IMAGE_MIMES = {
2401
- ".png": "image/png",
2402
- ".jpg": "image/jpeg",
2403
- ".jpeg": "image/jpeg",
2404
- ".gif": "image/gif",
2405
- ".webp": "image/webp",
2406
- ".svg": "image/svg+xml",
2407
- ".bmp": "image/bmp",
2408
- ".ico": "image/x-icon",
2409
- ".avif": "image/avif"
2410
- };
2609
+ init_confinedFile();
2610
+ init_filekind();
2411
2611
  MAX_IMAGE_BYTES = 20 * 1024 * 1024;
2412
2612
  }
2413
2613
  });
@@ -2521,236 +2721,72 @@ var init_sessions = __esm({
2521
2721
  }
2522
2722
  });
2523
2723
 
2524
- // server/src/util/filekind.ts
2525
- import path13 from "node:path";
2526
- function mimeForExt(ext) {
2527
- 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
+ };
2528
2732
  }
2529
- function looksBinary(buf) {
2530
- if (buf.length === 0) return false;
2531
- if (buf.length >= 2) {
2532
- const b0 = buf[0];
2533
- const b1 = buf[1];
2534
- if (b0 === 255 && b1 === 254 || b0 === 254 && b1 === 255) return true;
2535
- }
2536
- let control = 0;
2537
- for (let i = 0; i < buf.length; i++) {
2538
- const b = buf[i];
2539
- if (b === 0) return true;
2540
- 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}|`);
2541
2750
  }
2542
- return control / buf.length > 0.3;
2543
- }
2544
- function classifyKind(name) {
2545
- const ext = path13.extname(name).toLowerCase();
2546
- if (ext in IMAGE_EXTS2) return "image";
2547
- if (TEXT_EXTS.has(ext)) return "text";
2548
- return "binary";
2549
- }
2550
- var IMAGE_EXTS2, TEXT_EXTS, MIME_MAP;
2551
- var init_filekind = __esm({
2552
- "server/src/util/filekind.ts"() {
2553
- "use strict";
2554
- IMAGE_EXTS2 = {
2555
- ".png": "image/png",
2556
- ".jpg": "image/jpeg",
2557
- ".jpeg": "image/jpeg",
2558
- ".gif": "image/gif",
2559
- ".webp": "image/webp",
2560
- ".svg": "image/svg+xml",
2561
- ".bmp": "image/bmp",
2562
- ".ico": "image/x-icon",
2563
- ".avif": "image/avif"
2564
- };
2565
- TEXT_EXTS = /* @__PURE__ */ new Set([
2566
- ".txt",
2567
- ".md",
2568
- ".markdown",
2569
- ".rst",
2570
- ".log",
2571
- ".csv",
2572
- ".tsv",
2573
- ".json",
2574
- ".jsonl",
2575
- ".yaml",
2576
- ".yml",
2577
- ".toml",
2578
- ".ini",
2579
- ".env",
2580
- ".cfg",
2581
- ".conf",
2582
- ".xml",
2583
- ".html",
2584
- ".htm",
2585
- ".css",
2586
- ".scss",
2587
- ".less",
2588
- ".js",
2589
- ".mjs",
2590
- ".cjs",
2591
- ".jsx",
2592
- ".ts",
2593
- ".tsx",
2594
- ".vue",
2595
- ".svelte",
2596
- ".py",
2597
- ".rb",
2598
- ".go",
2599
- ".rs",
2600
- ".java",
2601
- ".kt",
2602
- ".c",
2603
- ".h",
2604
- ".cpp",
2605
- ".hpp",
2606
- ".cc",
2607
- ".cs",
2608
- ".php",
2609
- ".swift",
2610
- ".scala",
2611
- ".sh",
2612
- ".bash",
2613
- ".zsh",
2614
- ".fish",
2615
- ".sql",
2616
- ".graphql",
2617
- ".proto",
2618
- ".dockerfile",
2619
- ".makefile",
2620
- ".gradle",
2621
- ".diff",
2622
- ".patch",
2623
- ".tex"
2624
- ]);
2625
- MIME_MAP = {
2626
- ...IMAGE_EXTS2,
2627
- ".txt": "text/plain",
2628
- ".md": "text/markdown",
2629
- ".csv": "text/csv",
2630
- ".json": "application/json",
2631
- ".xml": "application/xml",
2632
- ".yaml": "text/yaml",
2633
- ".yml": "text/yaml",
2634
- ".html": "text/html",
2635
- ".css": "text/css",
2636
- ".js": "application/javascript",
2637
- ".mjs": "application/javascript",
2638
- ".ts": "text/typescript",
2639
- ".tsx": "text/typescript",
2640
- ".toml": "text/plain",
2641
- ".pdf": "application/pdf",
2642
- ".zip": "application/zip",
2643
- ".tar": "application/x-tar",
2644
- ".gz": "application/gzip",
2645
- ".wasm": "application/wasm",
2646
- ".exe": "application/vnd.microsoft.portable-executable",
2647
- ".dll": "application/vnd.microsoft.portable-executable",
2648
- ".so": "application/x-sharedlib",
2649
- ".bin": "application/octet-stream"
2650
- };
2651
- }
2652
- });
2653
-
2654
- // server/src/routes/workspace.ts
2655
- import fs12 from "node:fs/promises";
2656
- import { createReadStream as createReadStream2 } from "node:fs";
2657
- import path14 from "node:path";
2658
- function tooLargeForPreview(size, maxBytes) {
2659
- return {
2660
- error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
2661
- };
2662
- }
2663
- function hexdump(buf) {
2664
- const lines = [];
2665
- for (let off = 0; off < buf.length; off += 16) {
2666
- const slice = buf.subarray(off, off + 16);
2667
- const hex = [];
2668
- let ascii = "";
2669
- for (let i = 0; i < 16; i++) {
2670
- if (i < slice.length) {
2671
- const b = slice[i];
2672
- hex.push(b.toString(16).padStart(2, "0"));
2673
- ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
2674
- } else {
2675
- hex.push(" ");
2676
- }
2677
- if (i === 7) hex.push("");
2678
- }
2679
- lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
2680
- }
2681
- return lines.join("\n");
2751
+ return lines.join("\n");
2682
2752
  }
2683
2753
  function registerWorkspaceRoutes(app, manager) {
2684
2754
  app.get(
2685
2755
  "/api/sessions/:id/tree",
2686
2756
  async (req, reply) => {
2687
- let cwd;
2688
- try {
2689
- cwd = await manager.getSessionCwd(req.params.id);
2690
- } catch {
2691
- reply.code(404);
2692
- return { error: "Session not found" };
2693
- }
2694
- const relative = (req.query.path ?? "").replace(/^\/+/, "");
2695
- const target = confineToRoot(cwd, relative);
2696
- if (!target) {
2697
- reply.code(400);
2698
- return { error: "Invalid path" };
2699
- }
2700
- const workspaceMissing = async () => {
2701
- try {
2702
- return !(await fs12.stat(cwd)).isDirectory();
2703
- } catch {
2704
- return true;
2705
- }
2706
- };
2707
- const notFound = async () => {
2708
- reply.code(404);
2709
- return await workspaceMissing() ? { error: `Workspace folder no longer exists: ${cwd}` } : { error: "Directory not found" };
2710
- };
2711
- const realTarget = await realConfineToRoot(config.fileRoot, target);
2712
- if (!realTarget) {
2713
- return notFound();
2714
- }
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;
2715
2765
  let dirents;
2716
2766
  try {
2717
- dirents = await fs12.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2767
+ dirents = await fs13.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2718
2768
  } catch {
2719
- return notFound();
2769
+ return replyWith(reply, await workspaceNotFound(cwd));
2720
2770
  }
2721
2771
  const entries = [];
2722
2772
  for (const d of dirents) {
2723
- const name = d.name;
2724
- const entryRelative = relative ? `${relative}/${name}` : name;
2725
- const entryAbsolute = path14.join(realTarget, name);
2726
- let kind = d.isDirectory() ? "directory" : d.isFile() ? "file" : null;
2727
- let statTarget = entryAbsolute;
2728
- if (kind === null && d.isSymbolicLink()) {
2729
- const real = await realConfineToRoot(config.fileRoot, entryAbsolute);
2730
- if (!real) continue;
2731
- try {
2732
- const st = await fs12.stat(real);
2733
- kind = st.isDirectory() ? "directory" : st.isFile() ? "file" : null;
2734
- statTarget = real;
2735
- } catch {
2736
- continue;
2737
- }
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;
2738
2779
  }
2739
- if (kind === null) continue;
2740
- if (kind === "directory") {
2741
- entries.push({ name, path: entryRelative, type: "directory" });
2742
- } else {
2743
- try {
2744
- const stat = await fs12.stat(statTarget);
2745
- entries.push({
2746
- name,
2747
- path: entryRelative,
2748
- type: "file",
2749
- size: stat.size,
2750
- modifiedAt: stat.mtime.toISOString()
2751
- });
2752
- } catch {
2753
- }
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 {
2754
2790
  }
2755
2791
  }
2756
2792
  entries.sort((a, b) => {
@@ -2764,45 +2800,15 @@ function registerWorkspaceRoutes(app, manager) {
2764
2800
  app.get(
2765
2801
  "/api/sessions/:id/download",
2766
2802
  async (req, reply) => {
2767
- let cwd;
2768
- try {
2769
- cwd = await manager.getSessionCwd(req.params.id);
2770
- } catch {
2771
- reply.code(404);
2772
- return { error: "Session not found" };
2773
- }
2774
- const relative = (req.query.path ?? "").replace(/^\/+/, "");
2775
- if (!relative) {
2776
- reply.code(400);
2777
- return { error: "path parameter is required" };
2778
- }
2779
- const target = confineToRoot(cwd, relative);
2780
- if (!target) {
2781
- reply.code(400);
2782
- return { error: "Invalid path" };
2783
- }
2784
- const realTarget = await realConfineToRoot(config.fileRoot, target);
2785
- if (!realTarget) {
2786
- reply.code(404);
2787
- return { error: "File not found" };
2788
- }
2789
- let stat;
2790
- try {
2791
- stat = await fs12.stat(realTarget);
2792
- } catch {
2793
- reply.code(404);
2794
- return { error: "File not found" };
2795
- }
2796
- if (!stat.isFile()) {
2797
- reply.code(400);
2798
- return { error: "Path is not a file" };
2799
- }
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;
2800
2806
  if (stat.size > MAX_DOWNLOAD_BYTES) {
2801
2807
  reply.code(413);
2802
2808
  return { error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 100 MB)` };
2803
2809
  }
2804
- const ext = path14.extname(realTarget);
2805
- const filename = path14.basename(realTarget);
2810
+ const ext = path15.extname(realTarget);
2811
+ const filename = path15.basename(realTarget);
2806
2812
  const asciiName = filename.replace(/[^\w.\-]/g, "_");
2807
2813
  reply.header("Content-Type", mimeForExt(ext));
2808
2814
  reply.header(
@@ -2816,40 +2822,10 @@ function registerWorkspaceRoutes(app, manager) {
2816
2822
  app.get(
2817
2823
  "/api/sessions/:id/preview",
2818
2824
  async (req, reply) => {
2819
- let cwd;
2820
- try {
2821
- cwd = await manager.getSessionCwd(req.params.id);
2822
- } catch {
2823
- reply.code(404);
2824
- return { error: "Session not found" };
2825
- }
2826
- const relative = (req.query.path ?? "").replace(/^\/+/, "");
2827
- if (!relative) {
2828
- reply.code(400);
2829
- return { error: "path parameter is required" };
2830
- }
2831
- const target = confineToRoot(cwd, relative);
2832
- if (!target) {
2833
- reply.code(400);
2834
- return { error: "Invalid path" };
2835
- }
2836
- const realTarget = await realConfineToRoot(config.fileRoot, target);
2837
- if (!realTarget) {
2838
- reply.code(404);
2839
- return { error: "File not found" };
2840
- }
2841
- let stat;
2842
- try {
2843
- stat = await fs12.stat(realTarget);
2844
- } catch {
2845
- reply.code(404);
2846
- return { error: "File not found" };
2847
- }
2848
- if (!stat.isFile()) {
2849
- reply.code(400);
2850
- return { error: "Path is not a file" };
2851
- }
2852
- 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();
2853
2829
  const mime = mimeForExt(ext);
2854
2830
  const isImage = mime.startsWith("image/");
2855
2831
  const isPdf = mime === "application/pdf";
@@ -2878,7 +2854,7 @@ function registerWorkspaceRoutes(app, manager) {
2878
2854
  }
2879
2855
  if (isImage || isPdf) {
2880
2856
  const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2881
- reply.header("Cache-Control", "private, max-age=3600");
2857
+ reply.header("Cache-Control", "private, no-cache");
2882
2858
  reply.header("ETag", etag);
2883
2859
  reply.header("Last-Modified", stat.mtime.toUTCString());
2884
2860
  const ifNoneMatch = req.headers["if-none-match"];
@@ -2894,7 +2870,7 @@ function registerWorkspaceRoutes(app, manager) {
2894
2870
  if (kind === "binary") {
2895
2871
  let fh;
2896
2872
  try {
2897
- fh = await fs12.open(realTarget, "r");
2873
+ fh = await fs13.open(realTarget, "r");
2898
2874
  const buf = Buffer.alloc(Math.min(HEXDUMP_BYTES, stat.size));
2899
2875
  const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
2900
2876
  const head = buf.subarray(0, bytesRead);
@@ -2933,7 +2909,7 @@ var init_workspace = __esm({
2933
2909
  "server/src/routes/workspace.ts"() {
2934
2910
  "use strict";
2935
2911
  init_config();
2936
- init_paths2();
2912
+ init_confinedFile();
2937
2913
  init_filekind();
2938
2914
  MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
2939
2915
  HEXDUMP_BYTES = 4096;
@@ -2944,30 +2920,30 @@ var init_workspace = __esm({
2944
2920
 
2945
2921
  // server/src/routes/uploads.ts
2946
2922
  import { createReadStream as createReadStream3, createWriteStream } from "node:fs";
2947
- import fs13 from "node:fs/promises";
2948
- import path15 from "node:path";
2923
+ import fs14 from "node:fs/promises";
2924
+ import path16 from "node:path";
2949
2925
  import crypto5 from "node:crypto";
2950
2926
  import { execFile as execFile3 } from "node:child_process";
2951
2927
  import { pipeline } from "node:stream/promises";
2952
2928
  function uploadDirFor(sessionId) {
2953
- return path15.join(config.casperDataDir, "sessions", sessionId, "uploads");
2929
+ return path16.join(config.casperDataDir, "sessions", sessionId, "uploads");
2954
2930
  }
2955
2931
  function sanitizeName(raw) {
2956
- const base = path15.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
2932
+ const base = path16.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
2957
2933
  if (!base || base === "." || base === "..") return `upload-${Date.now()}`;
2958
2934
  return base.replace(/^\.+/, "") || `upload-${Date.now()}`;
2959
2935
  }
2960
2936
  async function uniquePath(dir, name) {
2961
- const ext = path15.extname(name);
2937
+ const ext = path16.extname(name);
2962
2938
  const stem = name.slice(0, name.length - ext.length);
2963
2939
  let candidate = name;
2964
2940
  let n = 1;
2965
2941
  while (true) {
2966
2942
  try {
2967
- await fs13.access(path15.join(dir, candidate));
2943
+ await fs14.access(path16.join(dir, candidate));
2968
2944
  candidate = `${stem} (${n++})${ext}`;
2969
2945
  } catch {
2970
- return path15.join(dir, candidate);
2946
+ return path16.join(dir, candidate);
2971
2947
  }
2972
2948
  }
2973
2949
  }
@@ -2987,7 +2963,7 @@ function fileType(absPath) {
2987
2963
  async function sampleStrings(absPath, limit = 40) {
2988
2964
  let fh;
2989
2965
  try {
2990
- fh = await fs13.open(absPath, "r");
2966
+ fh = await fs14.open(absPath, "r");
2991
2967
  const buf = Buffer.alloc(STRINGS_SCAN_BYTES);
2992
2968
  const { bytesRead } = await fh.read(buf, 0, STRINGS_SCAN_BYTES, 0);
2993
2969
  const out = [];
@@ -3025,7 +3001,7 @@ function registerUploadRoutes(app, manager) {
3025
3001
  return { error: "Invalid session id" };
3026
3002
  }
3027
3003
  const uploadDir = uploadDirFor(req.params.id);
3028
- await fs13.mkdir(uploadDir, { recursive: true, mode: 448 });
3004
+ await fs14.mkdir(uploadDir, { recursive: true, mode: 448 });
3029
3005
  const results = [];
3030
3006
  const parts = req.files();
3031
3007
  for await (const part of parts) {
@@ -3039,17 +3015,17 @@ function registerUploadRoutes(app, manager) {
3039
3015
  return { error: `Failed to store ${name}` };
3040
3016
  }
3041
3017
  if (part.file.truncated) {
3042
- await fs13.rm(dest, { force: true });
3018
+ await fs14.rm(dest, { force: true });
3043
3019
  reply.code(413);
3044
3020
  return {
3045
3021
  error: `${name} exceeds the max upload size (${Math.round(config.maxUploadBytes / 1024 / 1024)} MB)`
3046
3022
  };
3047
3023
  }
3048
- const stat = await fs13.stat(dest);
3049
- const ext = path15.extname(dest);
3024
+ const stat = await fs14.stat(dest);
3025
+ const ext = path16.extname(dest);
3050
3026
  const kind = classifyKind(dest);
3051
3027
  const uploaded = {
3052
- name: path15.basename(dest),
3028
+ name: path16.basename(dest),
3053
3029
  // Absolute, because the file sits outside the session's workspace: a
3054
3030
  // relative path would resolve against the wrong root for both the agent
3055
3031
  // and the image endpoint.
@@ -3106,97 +3082,8 @@ var init_health = __esm({
3106
3082
  }
3107
3083
  });
3108
3084
 
3109
- // server/src/ws/dispatch.ts
3110
- function send(socket, msg) {
3111
- if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(msg));
3112
- }
3113
- async function handleClientMessage(socket, manager, sessionId, msg, watchers) {
3114
- switch (msg.type) {
3115
- case "ping":
3116
- send(socket, { type: "pong" });
3117
- return;
3118
- case "hello":
3119
- return;
3120
- case "watch_paths":
3121
- await watchers.sync(msg.paths);
3122
- return;
3123
- case "prompt":
3124
- try {
3125
- await manager.runPrompt(sessionId, msg.content);
3126
- send(socket, { type: "ack", action: "prompt", ok: true });
3127
- } catch (err) {
3128
- send(socket, {
3129
- type: "ack",
3130
- action: "prompt",
3131
- ok: false,
3132
- error: err.message
3133
- });
3134
- }
3135
- return;
3136
- case "cancel":
3137
- try {
3138
- manager.cancel(sessionId);
3139
- send(socket, { type: "ack", action: "cancel", ok: true });
3140
- } catch (err) {
3141
- send(socket, {
3142
- type: "ack",
3143
- action: "cancel",
3144
- ok: false,
3145
- error: err.message
3146
- });
3147
- }
3148
- return;
3149
- case "set_mode":
3150
- try {
3151
- await manager.setMode(sessionId, msg.modeId);
3152
- send(socket, { type: "ack", action: "set_mode", ok: true });
3153
- } catch (err) {
3154
- send(socket, {
3155
- type: "ack",
3156
- action: "set_mode",
3157
- ok: false,
3158
- error: err.message
3159
- });
3160
- }
3161
- return;
3162
- case "set_model":
3163
- try {
3164
- await manager.setModel(sessionId, msg.modelId);
3165
- send(socket, { type: "ack", action: "set_model", ok: true });
3166
- } catch (err) {
3167
- send(socket, {
3168
- type: "ack",
3169
- action: "set_model",
3170
- ok: false,
3171
- error: err.message
3172
- });
3173
- }
3174
- return;
3175
- case "exec_command":
3176
- try {
3177
- await manager.execCommand(sessionId, msg.command, msg.args);
3178
- send(socket, { type: "ack", action: "exec_command", ok: true });
3179
- } catch (err) {
3180
- send(socket, {
3181
- type: "ack",
3182
- action: "exec_command",
3183
- ok: false,
3184
- error: err.message
3185
- });
3186
- }
3187
- return;
3188
- default:
3189
- send(socket, { type: "error", message: `Unknown message type` });
3190
- }
3191
- }
3192
- var init_dispatch = __esm({
3193
- "server/src/ws/dispatch.ts"() {
3194
- "use strict";
3195
- }
3196
- });
3197
-
3198
3085
  // server/src/ws/dirWatchers.ts
3199
- import fs14 from "node:fs";
3086
+ import fs15 from "node:fs";
3200
3087
  function diffWatchSet(current, next) {
3201
3088
  const have = new Set(current);
3202
3089
  const want = new Set(next.slice(0, MAX_WATCHES));
@@ -3220,7 +3107,7 @@ function createDirWatchers(opts) {
3220
3107
  const target = await opts.resolve(relative);
3221
3108
  if (!target || closed) return;
3222
3109
  try {
3223
- const watcher = fs14.watch(target, { persistent: false }, () => {
3110
+ const watcher = fs15.watch(target, { persistent: false }, () => {
3224
3111
  const existing = timers.get(relative);
3225
3112
  if (existing) clearTimeout(existing);
3226
3113
  timers.set(
@@ -3262,6 +3149,122 @@ var init_dirWatchers = __esm({
3262
3149
  });
3263
3150
 
3264
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
+ }
3265
3268
  function registerWsGateway(app, manager) {
3266
3269
  app.get("/ws", { websocket: true }, (socket, req) => {
3267
3270
  const query = req.query;
@@ -3270,92 +3273,17 @@ function registerWsGateway(app, manager) {
3270
3273
  socket.close(1008, "Unauthorized");
3271
3274
  return;
3272
3275
  }
3273
- const sessionId = query.sessionId;
3274
- if (!sessionId) {
3276
+ if (!query.sessionId) {
3275
3277
  send(socket, { type: "error", message: "Missing sessionId" });
3276
3278
  socket.close(1008, "Missing sessionId");
3277
3279
  return;
3278
3280
  }
3279
- let cursor = Number.parseInt(query.cursor ?? "0", 10) || 0;
3280
- let unsubscribe = null;
3281
- const watchers = createDirWatchers({
3282
- resolve: async (relative) => {
3283
- try {
3284
- return confineToRoot(await manager.getSessionCwd(sessionId), relative);
3285
- } catch {
3286
- return null;
3287
- }
3288
- },
3289
- onChange: (path18) => send(socket, { type: "fs_changed", path: path18 })
3290
- });
3291
- let alive = true;
3292
- let ready = false;
3293
- const forward = (event) => {
3294
- if (event.seq <= cursor) return;
3295
- cursor = event.seq;
3296
- send(socket, { type: "event", event });
3297
- };
3298
- const attach = async () => {
3299
- try {
3300
- await manager.ensureOpen(sessionId);
3301
- } catch (err) {
3302
- send(socket, { type: "error", message: err.message });
3303
- socket.close(1011, "open failed");
3304
- return;
3305
- }
3306
- const store = manager.getStore(sessionId);
3307
- if (!store) {
3308
- send(socket, { type: "error", message: "Session store unavailable" });
3309
- socket.close(1011, "no store");
3310
- return;
3311
- }
3312
- const { events, gap } = store.getSince(cursor);
3313
- if (gap) {
3314
- send(socket, {
3315
- type: "resync",
3316
- reason: "cursor older than buffer; refetch full transcript"
3317
- });
3318
- cursor = store.head();
3319
- } else {
3320
- for (const e of events) forward(e);
3321
- }
3322
- send(socket, { type: "replay_complete", head: store.head() });
3323
- unsubscribe = manager.onEvent(sessionId, forward);
3324
- ready = true;
3325
- };
3326
- void attach();
3327
- const heartbeat = setInterval(() => {
3328
- if (!alive) {
3329
- socket.terminate();
3330
- return;
3331
- }
3332
- alive = false;
3333
- try {
3334
- socket.ping();
3335
- } catch {
3336
- }
3337
- }, HEARTBEAT_MS);
3338
- heartbeat.unref?.();
3339
- socket.on("pong", () => {
3340
- alive = true;
3341
- });
3342
- socket.on("message", (raw) => {
3343
- alive = true;
3344
- if (!ready) return;
3345
- let msg;
3346
- try {
3347
- msg = JSON.parse(raw.toString());
3348
- } catch {
3349
- send(socket, { type: "error", message: "Invalid JSON" });
3350
- return;
3351
- }
3352
- void handleClientMessage(socket, manager, sessionId, msg, watchers);
3353
- });
3354
- socket.on("close", () => {
3355
- clearInterval(heartbeat);
3356
- unsubscribe?.();
3357
- watchers.close();
3358
- });
3281
+ handleConnection(
3282
+ socket,
3283
+ manager,
3284
+ query.sessionId,
3285
+ Number.parseInt(query.cursor ?? "0", 10) || 0
3286
+ );
3359
3287
  });
3360
3288
  }
3361
3289
  var HEARTBEAT_MS;
@@ -3363,7 +3291,6 @@ var init_gateway = __esm({
3363
3291
  "server/src/ws/gateway.ts"() {
3364
3292
  "use strict";
3365
3293
  init_auth();
3366
- init_dispatch();
3367
3294
  init_dirWatchers();
3368
3295
  init_paths2();
3369
3296
  HEARTBEAT_MS = 2e4;
@@ -3371,7 +3298,7 @@ var init_gateway = __esm({
3371
3298
  });
3372
3299
 
3373
3300
  // server/src/app.ts
3374
- import fs15 from "node:fs";
3301
+ import fs16 from "node:fs";
3375
3302
  import Fastify from "fastify";
3376
3303
  import cors from "@fastify/cors";
3377
3304
  import websocket from "@fastify/websocket";
@@ -3409,20 +3336,20 @@ async function buildApp() {
3409
3336
  registerWorkspaceRoutes(app, manager);
3410
3337
  registerUploadRoutes(app, manager);
3411
3338
  registerWsGateway(app, manager);
3412
- if (fs15.existsSync(config.webDist)) {
3339
+ if (fs16.existsSync(config.webDist)) {
3413
3340
  await app.register(fastifyStatic, {
3414
3341
  root: config.webDist
3415
3342
  });
3416
3343
  app.addHook("onSend", async (req, reply) => {
3417
- const path18 = req.url.split("?")[0] ?? "";
3418
- if (path18 === "/manifest.json" || path18.startsWith("/icons/")) {
3344
+ const path19 = req.url.split("?")[0] ?? "";
3345
+ if (path19 === "/manifest.json" || path19.startsWith("/icons/")) {
3419
3346
  reply.header("cache-control", "public, max-age=300");
3420
3347
  }
3421
3348
  });
3422
3349
  app.setNotFoundHandler((req, reply) => {
3423
- const path18 = req.url.split("?")[0] ?? "";
3424
- const looksLikeAsset = path18.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path18);
3425
- 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) {
3426
3353
  reply.code(404).send({ error: "Not found" });
3427
3354
  return;
3428
3355
  }
@@ -3496,15 +3423,15 @@ __export(doctor_exports, {
3496
3423
  mcpWiring: () => mcpWiring
3497
3424
  });
3498
3425
  import { execFileSync as execFileSync3 } from "node:child_process";
3499
- import fs16 from "node:fs";
3500
- import path16 from "node:path";
3426
+ import fs17 from "node:fs";
3427
+ import path17 from "node:path";
3501
3428
  function which(bin) {
3502
- if (bin.includes("/")) return fs16.existsSync(bin) ? bin : void 0;
3503
- 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)) {
3504
3431
  if (dir === "") continue;
3505
- const candidate = path16.join(dir, bin);
3432
+ const candidate = path17.join(dir, bin);
3506
3433
  try {
3507
- fs16.accessSync(candidate, fs16.constants.X_OK);
3434
+ fs17.accessSync(candidate, fs17.constants.X_OK);
3508
3435
  return candidate;
3509
3436
  } catch {
3510
3437
  }
@@ -3528,7 +3455,7 @@ function mcpWiring(agentJson, resolve) {
3528
3455
  return { ok: false, detail: `${command || "(no command)"} not found - widgets are unavailable` };
3529
3456
  }
3530
3457
  const script = args.find((a) => a.endsWith(".js"));
3531
- if (script && !fs16.existsSync(script)) {
3458
+ if (script && !fs17.existsSync(script)) {
3532
3459
  return { ok: false, detail: `${script} is gone - delete the agent file and restart to rewrite it` };
3533
3460
  }
3534
3461
  return { ok: true, detail: script ?? `${command} ${args.join(" ")}` };
@@ -3560,7 +3487,7 @@ function checks() {
3560
3487
  const settings = readSettings(config.configFile);
3561
3488
  const hasToken = typeof settings.token === "string" && settings.token !== "";
3562
3489
  out.push(
3563
- fs16.existsSync(config.configFile) ? {
3490
+ fs17.existsSync(config.configFile) ? {
3564
3491
  ok: hasToken,
3565
3492
  name: "settings",
3566
3493
  detail: hasToken ? `${config.configFile} (token set)` : `${config.configFile} exists but has no token - auth is DISABLED`
@@ -3570,8 +3497,8 @@ function checks() {
3570
3497
  detail: `${config.configFile} not found; a token is generated on first run`
3571
3498
  }
3572
3499
  );
3573
- if (fs16.existsSync(config.configFile)) {
3574
- const mode = fs16.statSync(config.configFile).mode & 511;
3500
+ if (fs17.existsSync(config.configFile)) {
3501
+ const mode = fs17.statSync(config.configFile).mode & 511;
3575
3502
  out.push({
3576
3503
  ok: mode === 384,
3577
3504
  name: "settings mode",
@@ -3579,10 +3506,10 @@ function checks() {
3579
3506
  });
3580
3507
  }
3581
3508
  try {
3582
- fs16.mkdirSync(config.casperDataDir, { recursive: true });
3583
- fs16.accessSync(config.casperDataDir, fs16.constants.W_OK);
3584
- const db2 = path16.join(config.casperDataDir, "casper.db");
3585
- 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` : "";
3586
3513
  out.push({ ok: true, name: "data dir", detail: `${config.casperDataDir} writable${size}` });
3587
3514
  } catch (err) {
3588
3515
  out.push({
@@ -3593,22 +3520,22 @@ function checks() {
3593
3520
  });
3594
3521
  }
3595
3522
  out.push(
3596
- 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 } : {
3597
3524
  ok: false,
3598
3525
  fatal: true,
3599
3526
  name: "web app",
3600
3527
  detail: `${config.webDist} not found - the package is incomplete or CASPER_WEB_DIST is wrong`
3601
3528
  }
3602
3529
  );
3603
- const agent = path16.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3604
- const hasAgent = fs16.existsSync(agent);
3530
+ const agent = path17.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3531
+ const hasAgent = fs17.existsSync(agent);
3605
3532
  out.push({
3606
3533
  ok: hasAgent,
3607
3534
  name: "casper agent",
3608
3535
  detail: hasAgent ? agent : `${agent} missing - it is installed on first run`
3609
3536
  });
3610
3537
  if (hasAgent) {
3611
- const wiring = mcpWiring(fs16.readFileSync(agent, "utf8"), which);
3538
+ const wiring = mcpWiring(fs17.readFileSync(agent, "utf8"), which);
3612
3539
  out.push({ ok: wiring.ok, name: "casper mcp", detail: wiring.detail });
3613
3540
  }
3614
3541
  return out;
@@ -4072,14 +3999,14 @@ var init_server2 = __esm({
4072
3999
  "server/src/mcp/server.ts"() {
4073
4000
  "use strict";
4074
4001
  init_protocol();
4075
- VERSION = true ? "0.9.7" : "dev";
4002
+ VERSION = true ? "0.10.0" : "dev";
4076
4003
  }
4077
4004
  });
4078
4005
 
4079
4006
  // server/src/cli/program.ts
4080
- import fs17 from "node:fs";
4007
+ import fs18 from "node:fs";
4081
4008
  import os4 from "node:os";
4082
- import path17 from "node:path";
4009
+ import path18 from "node:path";
4083
4010
 
4084
4011
  // server/node_modules/commander/lib/error.js
4085
4012
  var CommanderError = class extends Error {
@@ -7192,9 +7119,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
7192
7119
  * @param {string} [path]
7193
7120
  * @return {(string|null|Command)}
7194
7121
  */
7195
- executableDir(path18) {
7196
- if (path18 === void 0) return this._executableDir;
7197
- this._executableDir = path18;
7122
+ executableDir(path19) {
7123
+ if (path19 === void 0) return this._executableDir;
7124
+ this._executableDir = path19;
7198
7125
  return this;
7199
7126
  }
7200
7127
  /**
@@ -7452,7 +7379,7 @@ var program = new Command();
7452
7379
  // server/src/cli/program.ts
7453
7380
  init_paths();
7454
7381
  init_settings();
7455
- var VERSION2 = true ? "0.9.7" : "dev";
7382
+ var VERSION2 = true ? "0.10.0" : "dev";
7456
7383
  async function bootstrap() {
7457
7384
  const file = configFilePath();
7458
7385
  const settings = readSettings(file);
@@ -7498,8 +7425,8 @@ async function resetToken(value) {
7498
7425
  updateSettings(file, { token });
7499
7426
  process.stdout.write(`wrote a new token to ${file}
7500
7427
  `);
7501
- const dbFile = path17.join(dataDirPath(), "casper.db");
7502
- if (fs17.existsSync(dbFile)) {
7428
+ const dbFile = path18.join(dataDirPath(), "casper.db");
7429
+ if (fs18.existsSync(dbFile)) {
7503
7430
  const { LoginStore: LoginStore2 } = await Promise.resolve().then(() => (init_logins(), logins_exports));
7504
7431
  new LoginStore2().revokeAll();
7505
7432
  process.stdout.write("revoked all device sessions\n");