@joeyshi12/casper 0.11.1 → 0.13.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 (72) hide show
  1. package/README.md +4 -1
  2. package/dist/agents/prompt.txt +1 -1
  3. package/dist/mcp.js +1 -1
  4. package/dist/server.js +617 -634
  5. package/dist/web/assets/{abnfDiagram-N423BO3Z-B88fK1c8.js → abnfDiagram-N423BO3Z-Ckdr5R00.js} +1 -1
  6. package/dist/web/assets/{arc-DMIevERD.js → arc-RD0bGtxd.js} +1 -1
  7. package/dist/web/assets/{architectureDiagram-T3A2C74G-CJ5mXlmv.js → architectureDiagram-T3A2C74G-CHkgCulK.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-VBNYF7ZC-BssSdi9Q.js → blockDiagram-VBNYF7ZC-bs7XxVhZ.js} +1 -1
  9. package/dist/web/assets/{c4Diagram-5PPSVZJV-BJHrRcC9.js → c4Diagram-5PPSVZJV-sipmg1A6.js} +1 -1
  10. package/dist/web/assets/channel-D8Rxtl4g.js +1 -0
  11. package/dist/web/assets/{chunk-2GRJ4B5K-Bmq_IVc0.js → chunk-2GRJ4B5K-Dk5uhiWq.js} +1 -1
  12. package/dist/web/assets/{chunk-2Q5K7J3B-B2erbhym.js → chunk-2Q5K7J3B-RLD9D6CE.js} +1 -1
  13. package/dist/web/assets/{chunk-5RXB4S5H-B_DuY9SH.js → chunk-5RXB4S5H-Bisltr8Y.js} +1 -1
  14. package/dist/web/assets/{chunk-5VM5RSS4-BgiHQCU1.js → chunk-5VM5RSS4-BFuA55Yw.js} +1 -1
  15. package/dist/web/assets/{chunk-6Q2QTUOP-pmOmsjWC.js → chunk-6Q2QTUOP-Dgi0Ckwy.js} +1 -1
  16. package/dist/web/assets/{chunk-GF5L2VYU-B-PjTMWL.js → chunk-GF5L2VYU-sLBx1TNb.js} +1 -1
  17. package/dist/web/assets/{chunk-JWPE2WC7-Be9UnGmk.js → chunk-JWPE2WC7-BoIB6XgD.js} +1 -1
  18. package/dist/web/assets/{chunk-KBJHAD2P-DPxdXs9r.js → chunk-KBJHAD2P-DE2z1pFd.js} +1 -1
  19. package/dist/web/assets/{chunk-RYQCIY6F-BKc5gOjQ.js → chunk-RYQCIY6F-S_juj3_n.js} +1 -1
  20. package/dist/web/assets/{chunk-XXDRQBXY-CoOnbv-S.js → chunk-XXDRQBXY-gCHzWtzi.js} +1 -1
  21. package/dist/web/assets/classDiagram-JCYQIIEL-Ch31vHtu.js +1 -0
  22. package/dist/web/assets/classDiagram-v2-OCEON4UE-Ch31vHtu.js +1 -0
  23. package/dist/web/assets/{cose-bilkent-JH36ORCC-BZ5Oon1z.js → cose-bilkent-JH36ORCC-Dp9lkOsx.js} +1 -1
  24. package/dist/web/assets/{cynefin-VYW2F7L2-DlXmf5Bs.js → cynefin-VYW2F7L2-OwRF9q4d.js} +1 -1
  25. package/dist/web/assets/{cynefinDiagram-MW4NZA55-ClZQ1oTx.js → cynefinDiagram-MW4NZA55-DQsh1pP_.js} +1 -1
  26. package/dist/web/assets/{dagre-VZM6K2ZE-CX3pDtYQ.js → dagre-VZM6K2ZE-BOwA_-xr.js} +1 -1
  27. package/dist/web/assets/{diagram-7IWD3JNH-DBelWFkJ.js → diagram-7IWD3JNH-DCcvWrT9.js} +1 -1
  28. package/dist/web/assets/{diagram-B4RE2ZJO-CDqvztqK.js → diagram-B4RE2ZJO-1TUDDf93.js} +1 -1
  29. package/dist/web/assets/{diagram-LBJQPF4R-DWL1uAJS.js → diagram-LBJQPF4R-CQ9CHasC.js} +1 -1
  30. package/dist/web/assets/{diagram-Q27KOJAE-BRsHqM1Z.js → diagram-Q27KOJAE-CnzspMbf.js} +1 -1
  31. package/dist/web/assets/{diagram-UB23O5K3-Ci1U9Hdl.js → diagram-UB23O5K3-DwSm7ENR.js} +1 -1
  32. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-CBPphElN.js → ebnfDiagram-BXEA7PRR-B88O4qYG.js} +1 -1
  33. package/dist/web/assets/{erDiagram-JOGREHBK-FI4byJC2.js → erDiagram-JOGREHBK-RVTjJMdw.js} +1 -1
  34. package/dist/web/assets/{flowDiagram-UKHOOZJN-BCKR_o2t.js → flowDiagram-UKHOOZJN-CAGMFzJU.js} +1 -1
  35. package/dist/web/assets/{ganttDiagram-PKOTCBZU-BYp35VGe.js → ganttDiagram-PKOTCBZU-CEvl6Pim.js} +1 -1
  36. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N--AySchaL.js → gitGraphDiagram-DS77QQ5N-CH6kGWAl.js} +1 -1
  37. package/dist/web/assets/{index-D9ooBx1d.js → index-B1jqxtEG.js} +1 -1
  38. package/dist/web/assets/index-Bm1zLB-5.js +249 -0
  39. package/dist/web/assets/index-bZk3Q_Yv.css +1 -0
  40. package/dist/web/assets/{infoDiagram-6WML65LV-CbKYvHyN.js → infoDiagram-6WML65LV-DAcfWiSl.js} +1 -1
  41. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-dLs0tgi6.js → ishikawaDiagram-WSZJBQD7-C0CNHY8o.js} +1 -1
  42. package/dist/web/assets/{journeyDiagram-NVQOT4AX-CXUsDb3x.js → journeyDiagram-NVQOT4AX-Dkf1b7P5.js} +1 -1
  43. package/dist/web/assets/{kanban-definition-27J2QSJJ-r33P8KpU.js → kanban-definition-27J2QSJJ-DEyWowZE.js} +1 -1
  44. package/dist/web/assets/{linear-s7N3jDHb.js → linear-BIoTAhlf.js} +1 -1
  45. package/dist/web/assets/{mermaid.core-Cw282Xmk.js → mermaid.core-DTmrl8Wn.js} +4 -4
  46. package/dist/web/assets/{mindmap-definition-FAOFIHXS-BXPAvM6U.js → mindmap-definition-FAOFIHXS-mw_tNbPO.js} +1 -1
  47. package/dist/web/assets/{pegDiagram-VL7TDLO6-D-H5hjvM.js → pegDiagram-VL7TDLO6-BhX4qWsU.js} +1 -1
  48. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-D3W5_BlG.js → pieDiagram-7S7Q4E2Y-D8y4mlrR.js} +1 -1
  49. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-BNSHhxkB.js → quadrantDiagram-CIZ2JOQS-eRi0JVMP.js} +1 -1
  50. package/dist/web/assets/{railroadDiagram-AXF67PYL-BvLjuIqc.js → railroadDiagram-AXF67PYL-BtWC75OM.js} +1 -1
  51. package/dist/web/assets/{requirementDiagram-LRYGKXZP-CZTpqoEk.js → requirementDiagram-LRYGKXZP-QqWK5Q2W.js} +1 -1
  52. package/dist/web/assets/{sankeyDiagram-W5VNT64P-xvU-3Mf_.js → sankeyDiagram-W5VNT64P-DWPZuz_o.js} +1 -1
  53. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-Bpugvm4V.js → sequenceDiagram-SI44F4Z6-BjtTZ0wA.js} +1 -1
  54. package/dist/web/assets/{sizeCapture-X5ZJPWSS-B87TxLF7.js → sizeCapture-X5ZJPWSS-CHpioWdU.js} +1 -1
  55. package/dist/web/assets/{stateDiagram-OKZ733FA-BEQB_m5r.js → stateDiagram-OKZ733FA-CUyQMj6M.js} +1 -1
  56. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-BStIRqFz.js +1 -0
  57. package/dist/web/assets/{swimlanes-SLNWSIFB-CVMCMeYR.js → swimlanes-SLNWSIFB-DDbnLkK8.js} +2 -2
  58. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-D-e3uCkk.js +8 -0
  59. package/dist/web/assets/{timeline-definition-Z64GVDOM-Dylz4pg-.js → timeline-definition-Z64GVDOM-Cxc91WrS.js} +1 -1
  60. package/dist/web/assets/{vennDiagram-T6HMQDX7-BmS_JQjJ.js → vennDiagram-T6HMQDX7-CjOhqkxb.js} +1 -1
  61. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-C0qCz7i-.js → wardleyDiagram-T6FBY63Y-4Ag9jAUE.js} +1 -1
  62. package/dist/web/assets/{xychartDiagram-ELKLHX3M-aOG1xxkv.js → xychartDiagram-ELKLHX3M-BbQy0Bji.js} +1 -1
  63. package/dist/web/index.html +2 -2
  64. package/dist/web/sw.js +1 -1
  65. package/package.json +1 -2
  66. package/dist/web/assets/channel-CFhLIKKC.js +0 -1
  67. package/dist/web/assets/classDiagram-JCYQIIEL-CyTqHDI_.js +0 -1
  68. package/dist/web/assets/classDiagram-v2-OCEON4UE-CyTqHDI_.js +0 -1
  69. package/dist/web/assets/index-Dd6MG4c6.css +0 -1
  70. package/dist/web/assets/index-DigNQUdR.js +0 -253
  71. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-OtsvcH8I.js +0 -1
  72. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-TYfbhlBN.js +0 -8
package/dist/server.js CHANGED
@@ -29,7 +29,7 @@ var init_paths = __esm({
29
29
  });
30
30
 
31
31
  // server/src/cli/settings.ts
32
- import crypto from "node:crypto";
32
+ import crypto2 from "node:crypto";
33
33
  import fs2 from "node:fs";
34
34
  import path3 from "node:path";
35
35
  function readSettings(file) {
@@ -50,7 +50,7 @@ function updateSettings(file, changes) {
50
50
  fs2.renameSync(tmp, file);
51
51
  }
52
52
  function generateToken() {
53
- return crypto.randomBytes(24).toString("hex");
53
+ return crypto2.randomBytes(24).toString("hex");
54
54
  }
55
55
  function printTokenBlock(token, heading) {
56
56
  const line = "\u2500".repeat(Math.max(heading.length, token.length) + 2);
@@ -71,6 +71,17 @@ var init_settings = __esm({
71
71
  }
72
72
  });
73
73
 
74
+ // server/src/util/hash.ts
75
+ import { createHash } from "node:crypto";
76
+ function sha256(text2) {
77
+ return createHash("sha256").update(text2).digest("hex");
78
+ }
79
+ var init_hash = __esm({
80
+ "server/src/util/hash.ts"() {
81
+ "use strict";
82
+ }
83
+ });
84
+
74
85
  // server/src/cli/agentFile.ts
75
86
  var agentFile_exports = {};
76
87
  __export(agentFile_exports, {
@@ -78,7 +89,6 @@ __export(agentFile_exports, {
78
89
  agentPrompt: () => agentPrompt,
79
90
  installAgentFile: () => installAgentFile
80
91
  });
81
- import crypto2 from "node:crypto";
82
92
  import fs3 from "node:fs";
83
93
  import path4 from "node:path";
84
94
  import url from "node:url";
@@ -121,9 +131,6 @@ function agentConfig(prompt, mcp) {
121
131
  model: null
122
132
  };
123
133
  }
124
- function sha256(text2) {
125
- return crypto2.createHash("sha256").update(text2).digest("hex");
126
- }
127
134
  function installAgentFile(home2, dataDir) {
128
135
  const target = path4.join(home2, ".kiro", "agents", "casper.json");
129
136
  const prompt = agentPrompt();
@@ -165,6 +172,7 @@ function installAgentFile(home2, dataDir) {
165
172
  var init_agentFile = __esm({
166
173
  "server/src/cli/agentFile.ts"() {
167
174
  "use strict";
175
+ init_hash();
168
176
  }
169
177
  });
170
178
 
@@ -320,7 +328,7 @@ var init_config = __esm({
320
328
  */
321
329
  defaultAgent: setting("DEFAULT_AGENT", "defaultAgent", "casper"),
322
330
  /**
323
- * Filesystem root the file endpoints (/api/fs/dirs, /api/fs/image) are confined to; anything
331
+ * Filesystem root the file endpoints (/api/fs/dirs, /api/fs/file) are confined to; anything
324
332
  * resolving outside it is rejected. Defaults to /, so browsing spans everything the process
325
333
  * can read - set a narrower path to keep authenticated users out of system files such as /etc
326
334
  * or SSH keys.
@@ -383,8 +391,9 @@ var init_db = __esm({
383
391
  "use strict";
384
392
  init_config();
385
393
  SCHEMA = `
386
- CREATE TABLE IF NOT EXISTS sessions (
387
- session_id TEXT PRIMARY KEY,
394
+ CREATE TABLE IF NOT EXISTS chats (
395
+ chat_id TEXT PRIMARY KEY,
396
+ session_id TEXT UNIQUE,
388
397
  title TEXT,
389
398
  cwd TEXT
390
399
  );
@@ -396,6 +405,15 @@ CREATE TABLE IF NOT EXISTS logins (
396
405
  user_agent TEXT
397
406
  );
398
407
  CREATE INDEX IF NOT EXISTS logins_hash ON logins (hash);
408
+ CREATE TABLE IF NOT EXISTS message_attachments (
409
+ chat_id TEXT NOT NULL,
410
+ ordinal INTEGER NOT NULL,
411
+ path TEXT NOT NULL,
412
+ name TEXT NOT NULL,
413
+ size INTEGER NOT NULL,
414
+ kind TEXT NOT NULL,
415
+ PRIMARY KEY (chat_id, ordinal, path)
416
+ );
399
417
  `;
400
418
  }
401
419
  });
@@ -405,10 +423,7 @@ var logins_exports = {};
405
423
  __export(logins_exports, {
406
424
  LoginStore: () => LoginStore
407
425
  });
408
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
409
- function sha2562(s) {
410
- return createHash("sha256").update(s).digest("hex");
411
- }
426
+ import { randomBytes, timingSafeEqual } from "node:crypto";
412
427
  function sameHash(a, b) {
413
428
  if (a.length !== b.length) return false;
414
429
  return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
@@ -419,6 +434,7 @@ var init_logins = __esm({
419
434
  "use strict";
420
435
  init_config();
421
436
  init_db();
437
+ init_hash();
422
438
  LAST_SEEN_WRITE_INTERVAL_MS = 6e4;
423
439
  str = (v) => typeof v === "string" ? v : "";
424
440
  toRecord = (r) => ({
@@ -446,7 +462,7 @@ var init_logins = __esm({
446
462
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
447
463
  const record = {
448
464
  id: randomBytes(9).toString("base64url"),
449
- hash: sha2562(token),
465
+ hash: sha256(token),
450
466
  createdAt: nowIso,
451
467
  lastSeenAt: nowIso,
452
468
  userAgent
@@ -464,7 +480,7 @@ var init_logins = __esm({
464
480
  verify(token) {
465
481
  if (!token) return null;
466
482
  const now = Date.now();
467
- const row = db().prepare("SELECT * FROM logins WHERE hash = ?").get(sha2562(token));
483
+ const row = db().prepare("SELECT * FROM logins WHERE hash = ?").get(sha256(token));
468
484
  if (!row) return null;
469
485
  const record = toRecord(row);
470
486
  if (now - Date.parse(record.lastSeenAt) > this.ttlMs()) {
@@ -480,7 +496,7 @@ var init_logins = __esm({
480
496
  /** List all active devices, marking the one owning `currentToken`. */
481
497
  list(currentToken) {
482
498
  this.pruneExpired();
483
- const currentHash = currentToken ? sha2562(currentToken) : void 0;
499
+ const currentHash = currentToken ? sha256(currentToken) : void 0;
484
500
  const rows = db().prepare("SELECT * FROM logins ORDER BY last_seen_at DESC").all();
485
501
  return rows.map(toRecord).map((r) => ({
486
502
  id: r.id,
@@ -493,7 +509,7 @@ var init_logins = __esm({
493
509
  /** Revoke the device holding this token (used on logout). */
494
510
  revokeToken(token) {
495
511
  if (!token) return;
496
- db().prepare("DELETE FROM logins WHERE hash = ?").run(sha2562(token));
512
+ db().prepare("DELETE FROM logins WHERE hash = ?").run(sha256(token));
497
513
  }
498
514
  /** Revoke a device by its public id. Returns false if there was no such device. */
499
515
  revokeId(id) {
@@ -660,45 +676,20 @@ var init_acp = __esm({
660
676
  });
661
677
 
662
678
  // shared/dist/attachments.js
663
- function attachmentPaths(text2) {
664
- const line = text2.split("\n").find((l) => l.startsWith(ATTACHMENTS_PREFIX));
665
- if (!line)
666
- return [];
667
- return line.slice(ATTACHMENTS_PREFIX.length).split(", ").map((s) => s.trim()).filter(Boolean);
668
- }
669
- function imageAttachmentPaths(text2) {
670
- return attachmentPaths(text2).filter((p) => {
671
- const lower = p.toLowerCase();
672
- return IMAGE_EXTS.some((ext) => lower.endsWith(ext));
673
- });
674
- }
675
679
  function stripAttachmentsLine(text2) {
676
680
  return text2.split("\n").filter((l) => !l.startsWith(ATTACHMENTS_PREFIX)).join("\n").trim();
677
681
  }
678
- var ATTACHMENTS_PREFIX, IMAGE_EXTS;
682
+ var ATTACHMENTS_PREFIX;
679
683
  var init_attachments = __esm({
680
684
  "shared/dist/attachments.js"() {
681
685
  "use strict";
682
686
  ATTACHMENTS_PREFIX = "Attached files: ";
683
- IMAGE_EXTS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico", ".avif"];
684
687
  }
685
688
  });
686
689
 
687
690
  // shared/dist/observability.js
688
691
  function emptyObservabilitySnapshot() {
689
- return {
690
- turnStatus: "idle",
691
- creditsSpent: 0,
692
- lastTurnCredits: 0,
693
- contextUsagePercentage: 0,
694
- lastTurnDurationMs: 0,
695
- subagents: [],
696
- pendingStages: [],
697
- mcpServers: [],
698
- availableCommands: [],
699
- oauthPrompts: [],
700
- compacting: false
701
- };
692
+ return { turnStatus: "idle", contextUsagePercentage: 0, compacting: false };
702
693
  }
703
694
  var init_observability = __esm({
704
695
  "shared/dist/observability.js"() {
@@ -806,27 +797,32 @@ var init_agents = __esm({
806
797
  }
807
798
  });
808
799
 
809
- // server/src/session/workspaces.ts
810
- import crypto3 from "node:crypto";
800
+ // server/src/session/chats.ts
811
801
  import fs7 from "node:fs";
812
802
  import path8 from "node:path";
813
- function workspacesRoot() {
814
- return path8.join(config.casperDataDir, "workspaces");
803
+ function chatsRoot() {
804
+ return path8.join(config.casperDataDir, "chats");
805
+ }
806
+ function chatDir(chatId) {
807
+ return path8.join(chatsRoot(), chatId);
815
808
  }
816
- function workspaceDir(workspaceId) {
817
- return path8.join(workspacesRoot(), workspaceId);
809
+ function chatUploadsDir(chatId) {
810
+ return path8.join(chatDir(chatId), "uploads");
818
811
  }
819
- function createWorkspace() {
820
- const id = crypto3.randomUUID();
821
- const dir = workspaceDir(id);
812
+ function chatWorkspaceDir(chatId) {
813
+ return path8.join(chatDir(chatId), "workspace");
814
+ }
815
+ function createChatWorkspace(chatId) {
816
+ const dir = chatWorkspaceDir(chatId);
822
817
  fs7.mkdirSync(dir, { recursive: true, mode: 448 });
823
- return { id, dir };
818
+ return dir;
824
819
  }
825
- function isWorkspacePath(dir) {
826
- return dir === workspacesRoot() || dir.startsWith(workspacesRoot() + path8.sep);
820
+ function isManagedWorkspace(dir) {
821
+ const root = chatsRoot();
822
+ return dir === root || dir.startsWith(root + path8.sep);
827
823
  }
828
- var init_workspaces = __esm({
829
- "server/src/session/workspaces.ts"() {
824
+ var init_chats = __esm({
825
+ "server/src/session/chats.ts"() {
830
826
  "use strict";
831
827
  init_config();
832
828
  }
@@ -868,6 +864,9 @@ async function realConfineToRoot(root, absPath) {
868
864
  function isValidSessionId(id) {
869
865
  return /^[A-Za-z0-9._-]+$/.test(id) && id !== "." && !id.includes("..");
870
866
  }
867
+ function isValidChatId(id) {
868
+ return !!id && isValidSessionId(id);
869
+ }
871
870
  var realRootCache;
872
871
  var init_paths2 = __esm({
873
872
  "server/src/util/paths.ts"() {
@@ -1083,9 +1082,9 @@ var init_KiroProcess = __esm({
1083
1082
  this.child.stdout.on("error", () => {
1084
1083
  });
1085
1084
  }
1086
- /** Perform the ACP initialize handshake. Idempotent. */
1085
+ /** Perform the ACP initialize handshake. kiro's reply is not used. */
1087
1086
  async initialize() {
1088
- const result = await this.client.request(ACP_METHODS.initialize, {
1087
+ await this.client.request(ACP_METHODS.initialize, {
1089
1088
  protocolVersion: 1,
1090
1089
  clientCapabilities: {
1091
1090
  fs: { readTextFile: false, writeTextFile: false },
@@ -1095,7 +1094,6 @@ var init_KiroProcess = __esm({
1095
1094
  // worth wiring to package.json.
1096
1095
  clientInfo: { name: "casper", version: "0.5.0" }
1097
1096
  });
1098
- return result;
1099
1097
  }
1100
1098
  newSession(params) {
1101
1099
  return this.client.request(ACP_METHODS.sessionNew, params);
@@ -1120,10 +1118,10 @@ var init_KiroProcess = __esm({
1120
1118
  setModel(sessionId, modelId) {
1121
1119
  return this.client.request(ACP_METHODS.sessionSetModel, { sessionId, modelId });
1122
1120
  }
1123
- execCommand(sessionId, command, args) {
1121
+ execCommand(sessionId, command) {
1124
1122
  return this.client.request(ACP_METHODS.commandsExecute, {
1125
1123
  sessionId,
1126
- command: { command: command.replace(/^\//, ""), args: args ? { input: args } : {} }
1124
+ command: { command: command.replace(/^\//, ""), args: {} }
1127
1125
  });
1128
1126
  }
1129
1127
  /** Cleanly shut down: close stdin (triggers kiro's graceful exit), then kill. */
@@ -1192,13 +1190,13 @@ var init_EventStore = __esm({
1192
1190
  "use strict";
1193
1191
  init_config();
1194
1192
  EventStore = class extends EventEmitter4 {
1195
- sessionId;
1193
+ chatId;
1196
1194
  buffer = [];
1197
1195
  capacity;
1198
1196
  seq = 0;
1199
- constructor(sessionId) {
1197
+ constructor(chatId) {
1200
1198
  super();
1201
- this.sessionId = sessionId;
1199
+ this.chatId = chatId;
1202
1200
  this.capacity = config.eventBufferSize;
1203
1201
  }
1204
1202
  /** Append an event, assign it the next seq, and fan it out. */
@@ -1207,7 +1205,7 @@ var init_EventStore = __esm({
1207
1205
  const event = {
1208
1206
  seq: this.seq,
1209
1207
  ts: Date.now(),
1210
- sessionId: this.sessionId,
1208
+ chatId: this.chatId,
1211
1209
  payload
1212
1210
  };
1213
1211
  this.buffer.push(event);
@@ -1255,83 +1253,36 @@ var init_TurnState = __esm({
1255
1253
  get() {
1256
1254
  return this.snapshot;
1257
1255
  }
1258
- /** Apply one event; returns the updated snapshot. */
1259
1256
  apply(payload) {
1260
1257
  const s = this.snapshot;
1261
1258
  switch (payload.kind) {
1262
- case "turn_started": {
1263
- this.snapshot = { ...s, turnStatus: "running", currentTurnStartedAt: Date.now() };
1259
+ case "turn_started":
1260
+ this.snapshot = { ...s, turnStatus: "running" };
1264
1261
  break;
1265
- }
1266
- case "turn_ended": {
1267
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1262
+ case "turn_ended":
1263
+ case "turn_error":
1264
+ this.snapshot = { ...s, turnStatus: "idle" };
1268
1265
  break;
1269
- }
1270
- case "turn_error": {
1271
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1266
+ case "process_exited":
1267
+ this.snapshot = { ...s, turnStatus: "idle", compacting: false };
1272
1268
  break;
1273
- }
1274
- case "process_exited": {
1275
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1276
- break;
1277
- }
1278
- case "metadata": {
1279
- const p = payload.params;
1280
- const turnCredits = (p.meteringUsage ?? []).reduce((sum, m) => sum + m.value, 0);
1281
- this.snapshot = {
1282
- ...s,
1283
- contextUsagePercentage: p.contextUsagePercentage ?? s.contextUsagePercentage,
1284
- // meteringUsage is emitted per-turn; accumulate into the session total.
1285
- creditsSpent: turnCredits > 0 ? s.creditsSpent + turnCredits : s.creditsSpent,
1286
- lastTurnCredits: turnCredits > 0 ? turnCredits : s.lastTurnCredits,
1287
- lastTurnDurationMs: p.turnDurationMs ?? s.lastTurnDurationMs
1288
- };
1289
- break;
1290
- }
1291
- case "subagent_update": {
1269
+ case "metadata":
1292
1270
  this.snapshot = {
1293
1271
  ...s,
1294
- subagents: payload.params.subagents,
1295
- pendingStages: payload.params.pendingStages
1272
+ contextUsagePercentage: payload.params.contextUsagePercentage ?? s.contextUsagePercentage
1296
1273
  };
1297
1274
  break;
1298
- }
1299
- case "mcp_health": {
1300
- const { serverName, error } = payload.params;
1301
- const status = payload.ok ? "initialized" : "failed";
1302
- const next = s.mcpServers.filter((m) => m.serverName !== serverName);
1303
- next.push({ serverName, status, error, updatedAt: Date.now() });
1304
- this.snapshot = { ...s, mcpServers: next };
1305
- break;
1306
- }
1307
- case "commands_available": {
1308
- this.snapshot = { ...s, availableCommands: payload.params.commands };
1309
- break;
1310
- }
1311
- case "compaction": {
1275
+ case "compaction":
1312
1276
  this.snapshot = { ...s, compacting: payload.params.status.type === "started" };
1313
1277
  break;
1314
- }
1315
- case "oauth_request": {
1316
- const prompts = [
1317
- ...s.oauthPrompts,
1318
- {
1319
- serverName: payload.params.serverName,
1320
- url: payload.params.url,
1321
- createdAt: Date.now()
1322
- }
1323
- ];
1324
- this.snapshot = { ...s, oauthPrompts: prompts };
1325
- break;
1326
- }
1327
1278
  default:
1328
1279
  break;
1329
1280
  }
1330
1281
  return this.snapshot;
1331
1282
  }
1332
- /** Seed cumulative credits/context from kiro's persisted metadata on resume. */
1333
- seed(creditsSpent, contextUsagePercentage) {
1334
- this.snapshot = { ...this.snapshot, creditsSpent, contextUsagePercentage };
1283
+ /** Seed context usage from kiro's persisted metadata on resume. */
1284
+ seed(contextUsagePercentage) {
1285
+ this.snapshot = { ...this.snapshot, contextUsagePercentage };
1335
1286
  }
1336
1287
  };
1337
1288
  }
@@ -1369,10 +1320,6 @@ function renderableBlocks(content) {
1369
1320
  function summarize(j) {
1370
1321
  const state = j.session_state;
1371
1322
  const turns = state?.conversation_metadata?.user_turn_metadatas ?? [];
1372
- const creditsSpent = turns.reduce(
1373
- (sum, t) => sum + (t.metering_usage ?? []).reduce((a, m) => a + m.value, 0),
1374
- 0
1375
- );
1376
1323
  const contextUsagePercentage = state?.rts_model_state?.context_usage_percentage ?? turns[turns.length - 1]?.context_usage_percentage;
1377
1324
  return {
1378
1325
  sessionId: j.session_id,
@@ -1381,42 +1328,18 @@ function summarize(j) {
1381
1328
  cwd: j.cwd,
1382
1329
  createdAt: j.created_at,
1383
1330
  updatedAt: j.updated_at,
1384
- liveness: "dormant",
1385
1331
  agentId: state?.agent_name,
1386
1332
  modelId: state?.rts_model_state?.model_info?.model_id,
1387
- running: false,
1388
- creditsSpent,
1389
1333
  contextUsagePercentage
1390
1334
  };
1391
1335
  }
1392
- async function listPersistedSessions(log) {
1393
- let files;
1394
- try {
1395
- files = await fs9.readdir(config.kiroSessionsDir);
1396
- } catch {
1397
- return [];
1398
- }
1399
- const jsonFiles = files.filter((f) => f.endsWith(".json"));
1400
- const summaries = [];
1401
- await Promise.all(
1402
- jsonFiles.map(async (f) => {
1403
- try {
1404
- const raw = await fs9.readFile(path10.join(config.kiroSessionsDir, f), "utf8");
1405
- summaries.push(summarize(JSON.parse(raw)));
1406
- } catch (err) {
1407
- log.debug({ err, f }, "kiroFiles: skipping unreadable session file");
1408
- }
1409
- })
1410
- );
1411
- summaries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1412
- return summaries;
1413
- }
1414
1336
  async function deletePersistedSession(sessionId) {
1415
1337
  if (!isValidSessionId(sessionId)) return;
1416
1338
  const targets = [
1417
1339
  path10.join(config.kiroSessionsDir, `${sessionId}.json`),
1418
1340
  path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
1419
1341
  path10.join(config.kiroSessionsDir, `${sessionId}.history`),
1342
+ path10.join(config.kiroSessionsDir, `${sessionId}.lock`),
1420
1343
  path10.join(config.kiroSessionsDir, sessionId)
1421
1344
  ];
1422
1345
  await Promise.all(targets.map((f) => fs9.rm(f, { recursive: true, force: true })));
@@ -1433,48 +1356,54 @@ async function readPersistedSession(sessionId) {
1433
1356
  return null;
1434
1357
  }
1435
1358
  }
1436
- async function hasRecordedTurns(sessionId) {
1437
- if (!isValidSessionId(sessionId)) return false;
1438
- try {
1439
- const st = await fs9.stat(path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`));
1440
- return st.size > 0;
1441
- } catch {
1442
- return false;
1443
- }
1359
+ async function promptCount(sessionId) {
1360
+ const entries = await readJsonlEntries(sessionId);
1361
+ return entries.filter((e) => e.kind === "Prompt").length;
1444
1362
  }
1445
- async function hydrateTranscript(sessionId) {
1363
+ async function readJsonlEntries(sessionId) {
1446
1364
  if (!isValidSessionId(sessionId)) return [];
1447
1365
  let raw;
1448
1366
  try {
1449
- raw = await fs9.readFile(
1450
- path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
1451
- "utf8"
1452
- );
1367
+ raw = await fs9.readFile(path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`), "utf8");
1453
1368
  } catch {
1454
1369
  return [];
1455
1370
  }
1456
- const items = [];
1457
- const toolsById = /* @__PURE__ */ new Map();
1371
+ const entries = [];
1458
1372
  for (const line of raw.split("\n")) {
1459
1373
  const trimmed = line.trim();
1460
1374
  if (!trimmed) continue;
1461
- let entry;
1462
1375
  try {
1463
- entry = JSON.parse(trimmed);
1376
+ entries.push(JSON.parse(trimmed));
1464
1377
  } catch {
1465
- continue;
1466
1378
  }
1379
+ }
1380
+ return entries;
1381
+ }
1382
+ async function hasRecordedTurns(sessionId) {
1383
+ if (!isValidSessionId(sessionId)) return false;
1384
+ try {
1385
+ const st = await fs9.stat(path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`));
1386
+ return st.size > 0;
1387
+ } catch {
1388
+ return false;
1389
+ }
1390
+ }
1391
+ async function hydrateTranscript(sessionId, attachments) {
1392
+ const items = [];
1393
+ let userOrdinal = 0;
1394
+ const toolsById = /* @__PURE__ */ new Map();
1395
+ for (const entry of await readJsonlEntries(sessionId)) {
1467
1396
  const content = contentBlocks(entry.data.content);
1468
1397
  const textOf = (kind) => content.filter((c) => c.kind === kind).map(blockText).join("");
1469
1398
  const baseId = entry.data.message_id ?? `${items.length}`;
1470
1399
  const ts = entry.data.meta?.timestamp;
1471
1400
  const pushMsg = (msg) => items.push({ type: "message", message: msg });
1472
1401
  if (entry.kind === "Prompt") {
1473
- const raw2 = textOf("text");
1474
- const text2 = stripAttachmentsLine(raw2);
1475
- const imagePaths = imageAttachmentPaths(raw2);
1476
- if (text2.trim() || imagePaths.length)
1477
- pushMsg({ id: `u-${baseId}`, role: "user", text: text2, timestamp: ts, imagePaths });
1402
+ const text2 = stripAttachmentsLine(textOf("text"));
1403
+ const attached = attachments?.get(userOrdinal);
1404
+ userOrdinal++;
1405
+ if (text2.trim() || attached?.length)
1406
+ pushMsg({ id: `u-${baseId}`, role: "user", text: text2, timestamp: ts, attachments: attached });
1478
1407
  } else if (entry.kind === "AssistantMessage") {
1479
1408
  const thinking = textOf("thinking");
1480
1409
  if (thinking.trim())
@@ -1524,38 +1453,92 @@ var init_kiroFiles = __esm({
1524
1453
  }
1525
1454
  });
1526
1455
 
1527
- // server/src/session/sessionStore.ts
1528
- var SessionStore;
1529
- var init_sessionStore = __esm({
1530
- "server/src/session/sessionStore.ts"() {
1456
+ // server/src/session/chatStore.ts
1457
+ var ChatStore;
1458
+ var init_chatStore = __esm({
1459
+ "server/src/session/chatStore.ts"() {
1531
1460
  "use strict";
1532
1461
  init_db();
1533
- SessionStore = class {
1534
- getTitle(sessionId) {
1535
- return this.read(sessionId, "title");
1462
+ ChatStore = class {
1463
+ /** Every chat. Ordering is kiro's business, applied once the file is joined on. */
1464
+ all() {
1465
+ return db().prepare("SELECT chat_id, session_id, title, cwd FROM chats").all().map((r) => ({ chatId: r.chat_id, sessionId: r.session_id, title: r.title, cwd: r.cwd }));
1466
+ }
1467
+ get(chatId) {
1468
+ const r = db().prepare("SELECT chat_id, session_id, title, cwd FROM chats WHERE chat_id = ?").get(chatId);
1469
+ return r ? { chatId: r.chat_id, sessionId: r.session_id, title: r.title, cwd: r.cwd } : void 0;
1470
+ }
1471
+ create(chatId) {
1472
+ db().prepare("INSERT INTO chats (chat_id) VALUES (?) ON CONFLICT(chat_id) DO NOTHING").run(chatId);
1473
+ }
1474
+ /**
1475
+ * kiro names the session once it starts one; bind it to the chat that owns it. A session
1476
+ * belongs to exactly one chat, so any earlier claim on it is released.
1477
+ */
1478
+ bindSession(chatId, sessionId) {
1479
+ db().prepare("UPDATE chats SET session_id = NULL WHERE session_id = ? AND chat_id <> ?").run(sessionId, chatId);
1480
+ this.write(chatId, "session_id", sessionId);
1481
+ }
1482
+ sessionIdForChat(chatId) {
1483
+ const row = db().prepare("SELECT session_id FROM chats WHERE chat_id = ?").get(chatId);
1484
+ return row?.session_id ?? void 0;
1536
1485
  }
1537
- setTitle(sessionId, title) {
1538
- this.write(sessionId, "title", title);
1486
+ getTitle(chatId) {
1487
+ return this.read(chatId, "title");
1539
1488
  }
1540
- getCwd(sessionId) {
1541
- return this.read(sessionId, "cwd");
1489
+ setTitle(chatId, title) {
1490
+ this.write(chatId, "title", title);
1542
1491
  }
1543
- setCwd(sessionId, cwd) {
1544
- this.write(sessionId, "cwd", cwd);
1492
+ getCwd(chatId) {
1493
+ return this.read(chatId, "cwd");
1545
1494
  }
1546
- /** Forget every override for a session, when it's permanently deleted. */
1547
- remove(sessionId) {
1548
- db().prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
1495
+ setCwd(chatId, cwd) {
1496
+ this.write(chatId, "cwd", cwd);
1549
1497
  }
1550
- read(sessionId, column) {
1551
- const row = db().prepare(`SELECT ${column} AS value FROM sessions WHERE session_id = ?`).get(sessionId);
1498
+ /** Record what was attached to one prompt. See db.ts for why the key is an ordinal. */
1499
+ setAttachments(chatId, ordinal, files) {
1500
+ if (files.length === 0) return;
1501
+ const stmt = db().prepare(
1502
+ `INSERT INTO message_attachments (chat_id, ordinal, path, name, size, kind)
1503
+ VALUES (?, ?, ?, ?, ?, ?)
1504
+ ON CONFLICT(chat_id, ordinal, path) DO NOTHING`
1505
+ );
1506
+ for (const f of files) {
1507
+ stmt.run(chatId, ordinal, f.path, f.name, f.size, f.kind);
1508
+ }
1509
+ }
1510
+ /** Every attachment in a chat, grouped by the message it belongs to. */
1511
+ attachmentsByChat(chatId) {
1512
+ const rows = db().prepare(
1513
+ `SELECT ordinal, path, name, size, kind FROM message_attachments
1514
+ WHERE chat_id = ? ORDER BY ordinal, path`
1515
+ ).all(chatId);
1516
+ const out = /* @__PURE__ */ new Map();
1517
+ for (const r of rows) {
1518
+ const list = out.get(r.ordinal) ?? [];
1519
+ list.push({
1520
+ path: r.path,
1521
+ name: r.name,
1522
+ size: r.size,
1523
+ kind: r.kind
1524
+ });
1525
+ out.set(r.ordinal, list);
1526
+ }
1527
+ return out;
1528
+ }
1529
+ remove(chatId) {
1530
+ db().prepare("DELETE FROM chats WHERE chat_id = ?").run(chatId);
1531
+ db().prepare("DELETE FROM message_attachments WHERE chat_id = ?").run(chatId);
1532
+ }
1533
+ read(chatId, column) {
1534
+ const row = db().prepare(`SELECT ${column} AS value FROM chats WHERE chat_id = ?`).get(chatId);
1552
1535
  return row?.value ?? void 0;
1553
1536
  }
1554
- write(sessionId, column, value) {
1537
+ write(chatId, column, value) {
1555
1538
  db().prepare(
1556
- `INSERT INTO sessions (session_id, ${column}) VALUES (?, ?)
1557
- ON CONFLICT(session_id) DO UPDATE SET ${column} = excluded.${column}`
1558
- ).run(sessionId, value);
1539
+ `INSERT INTO chats (chat_id, ${column}) VALUES (?, ?)
1540
+ ON CONFLICT(chat_id) DO UPDATE SET ${column} = excluded.${column}`
1541
+ ).run(chatId, value);
1559
1542
  }
1560
1543
  };
1561
1544
  }
@@ -1590,18 +1573,8 @@ function mapNotification(n) {
1590
1573
  return { kind: "session_update", update: n.params.update };
1591
1574
  case KIRO_NOTIFICATIONS.metadata:
1592
1575
  return { kind: "metadata", params: n.params };
1593
- case KIRO_NOTIFICATIONS.subagentListUpdate:
1594
- return { kind: "subagent_update", params: n.params };
1595
- case KIRO_NOTIFICATIONS.mcpServerInitialized:
1596
- return { kind: "mcp_health", params: n.params, ok: true };
1597
- case KIRO_NOTIFICATIONS.mcpServerInitFailure:
1598
- return { kind: "mcp_health", params: n.params, ok: false };
1599
- case KIRO_NOTIFICATIONS.commandsAvailable:
1600
- return { kind: "commands_available", params: n.params };
1601
1576
  case KIRO_NOTIFICATIONS.compactionStatus:
1602
1577
  return { kind: "compaction", params: n.params };
1603
- case KIRO_NOTIFICATIONS.mcpOauthRequest:
1604
- return { kind: "oauth_request", params: n.params };
1605
1578
  default:
1606
1579
  return null;
1607
1580
  }
@@ -1623,13 +1596,13 @@ var init_SessionManager = __esm({
1623
1596
  init_dist();
1624
1597
  init_config();
1625
1598
  init_agents();
1626
- init_workspaces();
1599
+ init_chats();
1627
1600
  init_paths2();
1628
1601
  init_KiroProcess();
1629
1602
  init_EventStore();
1630
1603
  init_TurnState();
1631
1604
  init_kiroFiles();
1632
- init_sessionStore();
1605
+ init_chatStore();
1633
1606
  Session = class {
1634
1607
  sessionId;
1635
1608
  store;
@@ -1688,7 +1661,7 @@ var init_SessionManager = __esm({
1688
1661
  SessionManager = class {
1689
1662
  sessions = /* @__PURE__ */ new Map();
1690
1663
  log;
1691
- store = new SessionStore();
1664
+ store = new ChatStore();
1692
1665
  spawnProcess;
1693
1666
  constructor(log, opts = {}) {
1694
1667
  this.log = log;
@@ -1698,62 +1671,53 @@ var init_SessionManager = __esm({
1698
1671
  * What a session is called, from every read path. The folder is skipped for a workspace of
1699
1672
  * ours, whose name is a uuid.
1700
1673
  */
1701
- titleOf(sessionId, parts) {
1674
+ titleOf(chat, parts) {
1702
1675
  return resolveSessionTitle({
1703
- override: this.store.getTitle(sessionId),
1676
+ override: chat.title ?? void 0,
1704
1677
  kiroTitle: parts.kiroTitle,
1705
1678
  firstPrompt: parts.firstPrompt,
1706
- folder: isWorkspacePath(parts.cwd) ? void 0 : path11.basename(parts.cwd)
1679
+ folder: isManagedWorkspace(parts.cwd) ? void 0 : path11.basename(parts.cwd)
1707
1680
  });
1708
1681
  }
1709
- /** Set a user title override for a session. */
1710
- renameSession(sessionId, title) {
1682
+ renameChat(chatId, title) {
1711
1683
  const clean = sanitizeTitle(title);
1712
- this.store.setTitle(sessionId, clean);
1713
- const s = this.sessions.get(sessionId);
1684
+ this.store.setTitle(chatId, clean);
1685
+ const s = this.sessions.get(this.store.sessionIdForChat(chatId) ?? "");
1714
1686
  if (s) s.title = clean || s.title;
1715
1687
  }
1716
1688
  /**
1717
- * Re-point a session at a different working directory, for when the original was moved
1718
- * or deleted. Validates and creates the target exactly like a new session's cwd, so a
1719
- * folder is only created on this explicit action. Any live process was spawned with the
1689
+ * Re-point a session at a different working directory. Any live process was spawned with the
1720
1690
  * old cwd, so it is disposed and the next turn respawns in the new one, transcript intact.
1721
1691
  */
1722
- async setSessionCwd(sessionId, input) {
1692
+ async setChatCwd(chatId, input) {
1723
1693
  const resolved = resolveCwd(input);
1724
- const s = await this.ensureOpen(sessionId);
1725
- this.store.setCwd(sessionId, resolved);
1694
+ const s = await this.ensureOpen(chatId);
1695
+ this.store.setCwd(chatId, resolved);
1726
1696
  if (s.cwd !== resolved) {
1727
1697
  s.cwd = resolved;
1728
1698
  s.proc?.dispose();
1729
1699
  s.proc = void 0;
1730
- this.log.info({ sessionId, cwd: resolved }, "session working directory changed");
1700
+ this.log.info({ chatId, cwd: resolved }, "chat working directory changed");
1731
1701
  }
1732
1702
  return resolved;
1733
1703
  }
1734
1704
  /**
1735
- * Restart the session's kiro child, so everything kiro only reads when a child
1736
- * starts is read again: the agent definition, the workspace's `.kiro` directory,
1737
- * and the MCP servers kiro launches for itself.
1738
- *
1739
- * No ACP method does this. kiro advertises no reload command, and `session/set_mode`
1740
- * leaves the MCP servers it started alone, so replacing the process is the only
1741
- * way - the transcript comes back with `session/load`.
1705
+ * Restart the session's kiro child, so what kiro reads only at startup is read again: the
1706
+ * agent definition, the workspace's `.kiro`, and the MCP servers kiro launches itself.
1707
+ * Replacing the process is the only way - no ACP method does it, and `session/set_mode`
1708
+ * leaves those MCP servers alone.
1742
1709
  *
1743
- * The old child is awaited out before the new one starts, because kiro flushes its
1744
- * session file on shutdown and the replacement reads that file to reload the
1745
- * conversation. Respawned eagerly rather than at the next prompt, so the caller
1746
- * gets a detail that reflects what the fresh process reported.
1747
- *
1748
- * Needs a session that has recorded a turn, so it can be loaded back. Verified
1749
- * against kiro 2.11: loading one with an empty event log fails with "Session not
1750
- * found", and kiro deletes both its files when that process exits.
1710
+ * The old child must be awaited out first: kiro flushes its session file on shutdown and
1711
+ * the replacement reads that file to reload the conversation.
1751
1712
  */
1752
- async reloadSession(sessionId) {
1753
- const s = await this.ensureOpen(sessionId);
1713
+ async reloadChat(chatId) {
1714
+ const s = await this.ensureOpen(chatId);
1754
1715
  if (s.running) {
1755
1716
  throw new Error("Cannot reload while a turn is running");
1756
1717
  }
1718
+ if (s.turnState.get().compacting) {
1719
+ throw new Error("Cannot reload while the conversation is being compacted");
1720
+ }
1757
1721
  if (s.reloading) {
1758
1722
  throw new Error("A reload is already running for this session");
1759
1723
  }
@@ -1762,14 +1726,14 @@ var init_SessionManager = __esm({
1762
1726
  ready = resolve;
1763
1727
  });
1764
1728
  try {
1765
- return await this.replaceProcess(s);
1729
+ return await this.replaceProcess(chatId, s);
1766
1730
  } finally {
1767
1731
  s.reloading = void 0;
1768
1732
  ready();
1769
1733
  }
1770
1734
  }
1771
1735
  /** The reload itself. Only ever called with the session's reload claim held. */
1772
- async replaceProcess(s) {
1736
+ async replaceProcess(chatId, s) {
1773
1737
  if (!await hasRecordedTurns(s.sessionId)) {
1774
1738
  throw new Error(
1775
1739
  "kiro has not saved this session yet. Send a message first, then reload."
@@ -1788,7 +1752,7 @@ var init_SessionManager = __esm({
1788
1752
  s.lastActivity = Date.now();
1789
1753
  invalidateAgents();
1790
1754
  this.log.info({ sessionId: s.sessionId, cwd: s.cwd }, "session reloaded");
1791
- return this.getDetail(s.sessionId);
1755
+ return this.getDetail(chatId);
1792
1756
  }
1793
1757
  /**
1794
1758
  * Wait out a reload before touching the session's process. Without this an action
@@ -1805,33 +1769,34 @@ var init_SessionManager = __esm({
1805
1769
  // -------------------------------------------------------------------------
1806
1770
  // Event subscription - works for any opened session, spawned or not.
1807
1771
  // -------------------------------------------------------------------------
1808
- onEvent(sessionId, cb) {
1809
- const s = this.sessions.get(sessionId);
1772
+ onEvent(chatId, cb) {
1773
+ const s = this.sessions.get(this.store.sessionIdForChat(chatId) ?? "");
1810
1774
  if (!s) return null;
1811
1775
  s.store.on("event", cb);
1812
1776
  return () => s.store.off("event", cb);
1813
1777
  }
1814
- getStore(sessionId) {
1815
- return this.sessions.get(sessionId)?.store;
1778
+ getStore(chatId) {
1779
+ return this.sessions.get(this.store.sessionIdForChat(chatId) ?? "")?.store;
1816
1780
  }
1817
- /** Get a session's working directory. Opens the session in memory if needed. */
1818
- async getSessionCwd(sessionId) {
1819
- const s = await this.ensureOpen(sessionId);
1781
+ /** A chat's working directory. Opens it in memory if needed. */
1782
+ async getChatCwd(chatId) {
1783
+ const s = await this.ensureOpen(chatId);
1820
1784
  return s.cwd;
1821
1785
  }
1822
- /** Open a session in memory (store + metadata) WITHOUT spawning a process. */
1823
- async ensureOpen(sessionId) {
1786
+ /** Open a chat's session in memory (store + metadata) WITHOUT spawning a process. */
1787
+ async ensureOpen(chatId) {
1788
+ const sessionId = this.sessionIdOf(chatId);
1824
1789
  const existing = this.sessions.get(sessionId);
1825
1790
  if (existing) return existing;
1826
1791
  const persisted = await readPersistedSession(sessionId);
1827
1792
  if (!persisted) throw new Error(`Unknown session: ${sessionId}`);
1828
- const effectiveCwd = this.store.getCwd(sessionId) ?? persisted.cwd;
1793
+ const effectiveCwd = this.store.getCwd(chatId) ?? persisted.cwd;
1829
1794
  if (!isWithinRoot(config.fileRoot, effectiveCwd)) {
1830
1795
  throw new Error(
1831
1796
  `Session working directory is outside the allowed root: ${effectiveCwd}`
1832
1797
  );
1833
1798
  }
1834
- const store = new EventStore(sessionId);
1799
+ const store = new EventStore(chatId);
1835
1800
  const s = new Session(sessionId, store, effectiveCwd);
1836
1801
  s.title = persisted.title;
1837
1802
  s.agentId = persisted.agentId;
@@ -1840,10 +1805,7 @@ var init_SessionManager = __esm({
1840
1805
  s.createdAt = persisted.createdAt;
1841
1806
  s.updatedAt = persisted.updatedAt;
1842
1807
  s.markLive();
1843
- s.turnState.seed(
1844
- persisted.creditsSpent ?? 0,
1845
- persisted.contextUsagePercentage ?? 0
1846
- );
1808
+ s.turnState.seed(persisted.contextUsagePercentage ?? 0);
1847
1809
  this.sessions.set(sessionId, s);
1848
1810
  return s;
1849
1811
  }
@@ -1930,10 +1892,11 @@ var init_SessionManager = __esm({
1930
1892
  // Creating / opening
1931
1893
  // -------------------------------------------------------------------------
1932
1894
  /** Create a new session. Spawns immediately so we get a real kiro sessionId. */
1933
- async createSession(opts) {
1934
- const cwd = opts.freshWorkspace ? createWorkspace().dir : resolveCwd(opts.cwd);
1895
+ async createChat(opts) {
1896
+ const chatId = isValidChatId(opts.chatId) ? opts.chatId : crypto.randomUUID();
1897
+ const cwd = opts.freshWorkspace ? createChatWorkspace(chatId) : resolveCwd(opts.cwd);
1935
1898
  const tempId = `pending-${Date.now()}-${Math.floor(this.sessions.size)}`;
1936
- const store = new EventStore(tempId);
1899
+ const store = new EventStore(chatId);
1937
1900
  const s = new Session(tempId, store, cwd);
1938
1901
  s.agentId = opts.agentId ?? config.defaultAgent;
1939
1902
  s.currentModeId = s.agentId;
@@ -1945,18 +1908,20 @@ var init_SessionManager = __esm({
1945
1908
  this.evict(s.sessionId);
1946
1909
  throw err;
1947
1910
  }
1911
+ this.store.create(chatId);
1912
+ this.store.bindSession(chatId, s.sessionId);
1948
1913
  const name = sanitizeTitle(opts.title ?? "") || (opts.freshWorkspace ? "" : path11.basename(s.cwd));
1949
1914
  if (name) {
1950
- this.store.setTitle(s.sessionId, name);
1915
+ this.store.setTitle(chatId, name);
1951
1916
  s.title = name;
1952
1917
  }
1953
- return this.buildDetail(s, []);
1918
+ return this.buildDetail(this.store.get(chatId), s, []);
1954
1919
  }
1955
1920
  // -------------------------------------------------------------------------
1956
1921
  // Actions - these spawn the process lazily.
1957
1922
  // -------------------------------------------------------------------------
1958
- async runPrompt(sessionId, content) {
1959
- const s = await this.ensureOpen(sessionId);
1923
+ async runPrompt(chatId, content, attachments) {
1924
+ const s = await this.ensureOpen(chatId);
1960
1925
  await this.settleReload(s);
1961
1926
  if (s.running) throw new Error("A turn is already running for this session");
1962
1927
  s.running = true;
@@ -1968,14 +1933,20 @@ var init_SessionManager = __esm({
1968
1933
  s.running = false;
1969
1934
  throw err;
1970
1935
  }
1971
- if (!this.store.getTitle(s.sessionId)) {
1936
+ if (!this.store.getTitle(chatId)) {
1972
1937
  const title = titleFromPrompt(content);
1973
1938
  if (title) {
1974
- this.store.setTitle(s.sessionId, title);
1939
+ this.store.setTitle(chatId, title);
1975
1940
  s.title = title;
1976
1941
  }
1977
1942
  }
1978
- s.record({ kind: "turn_started", prompt: content });
1943
+ let recorded;
1944
+ if (attachments?.length) {
1945
+ const ordinal = await promptCount(s.sessionId);
1946
+ this.store.setAttachments(chatId, ordinal, attachments);
1947
+ recorded = attachments;
1948
+ }
1949
+ s.record({ kind: "turn_started", prompt: content, attachments: recorded });
1979
1950
  proc.prompt({ sessionId: s.sessionId, prompt: content }).then((res) => s.record({ kind: "turn_ended", stopReason: res.stopReason })).catch((err) => {
1980
1951
  this.log.error({ err, sessionId: s.sessionId }, "prompt turn failed");
1981
1952
  const tail = proc.stderrTail();
@@ -1989,12 +1960,12 @@ ${tail}` : err.message;
1989
1960
  s.lastActivity = Date.now();
1990
1961
  });
1991
1962
  }
1992
- cancel(sessionId) {
1993
- const s = this.sessions.get(sessionId);
1963
+ cancel(chatId) {
1964
+ const s = this.sessions.get(this.store.sessionIdForChat(chatId) ?? "");
1994
1965
  s?.proc?.cancel(s.sessionId);
1995
1966
  }
1996
- async setMode(sessionId, modeId) {
1997
- const s = await this.ensureOpen(sessionId);
1967
+ async setMode(chatId, modeId) {
1968
+ const s = await this.ensureOpen(chatId);
1998
1969
  await this.settleReload(s);
1999
1970
  const proc = await this.ensureProc(s);
2000
1971
  await proc.setMode(s.sessionId, modeId);
@@ -2002,114 +1973,124 @@ ${tail}` : err.message;
2002
1973
  s.agentId = modeId;
2003
1974
  s.lastActivity = Date.now();
2004
1975
  }
2005
- async setModel(sessionId, modelId) {
2006
- const s = await this.ensureOpen(sessionId);
1976
+ async setModel(chatId, modelId) {
1977
+ const s = await this.ensureOpen(chatId);
2007
1978
  await this.settleReload(s);
2008
1979
  const proc = await this.ensureProc(s);
2009
1980
  await proc.setModel(s.sessionId, modelId);
2010
1981
  s.modelId = modelId;
2011
1982
  s.lastActivity = Date.now();
2012
1983
  }
2013
- async execCommand(sessionId, command, args) {
2014
- const s = await this.ensureOpen(sessionId);
1984
+ async execCommand(chatId, command) {
1985
+ const s = await this.ensureOpen(chatId);
2015
1986
  await this.settleReload(s);
2016
1987
  const proc = await this.ensureProc(s);
2017
- await proc.execCommand(s.sessionId, command, args);
1988
+ await proc.execCommand(s.sessionId, command);
2018
1989
  s.lastActivity = Date.now();
2019
1990
  }
2020
- summaryOf(live, persisted, transcript) {
2021
- const sessionId = live?.sessionId ?? persisted.sessionId;
1991
+ // -------------------------------------------------------------------------
1992
+ // Listing / detail - never spawns.
1993
+ // -------------------------------------------------------------------------
1994
+ /**
1995
+ * The one place a ChatSummary is assembled: kiro's file and Casper's live state each hold
1996
+ * part of the truth, so every read path projects them through here. `persisted` is absent for
1997
+ * a session with no file yet, `live` for a dormant one, and callers guarantee one of them.
1998
+ */
1999
+ summaryOf(chat, live, persisted, transcript) {
2022
2000
  const snap = live?.turnState.get();
2023
- const cwd = live?.cwd ?? this.store.getCwd(sessionId) ?? persisted.cwd;
2001
+ const cwd = live?.cwd ?? chat.cwd ?? persisted?.cwd ?? config.defaultCwd;
2024
2002
  return {
2025
- sessionId,
2026
- title: this.titleOf(sessionId, {
2003
+ chatId: chat.chatId,
2004
+ sessionId: live?.sessionId ?? chat.sessionId ?? void 0,
2005
+ title: this.titleOf(chat, {
2027
2006
  // kiro's file is what kiro called it; the live copy is only a cache of it.
2028
2007
  kiroTitle: persisted?.title || live?.title,
2029
2008
  firstPrompt: transcript && firstPromptText(transcript),
2030
2009
  cwd
2031
2010
  }),
2032
2011
  cwd,
2033
- createdAt: persisted?.createdAt ?? live.createdAt,
2012
+ createdAt: persisted?.createdAt ?? live?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2034
2013
  // kiro's file and our own activity move separately.
2035
- updatedAt: live ? newerOf(persisted?.updatedAt, live.updatedAt) : persisted.updatedAt,
2014
+ updatedAt: live ? newerOf(persisted?.updatedAt, live.updatedAt) : persisted?.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2036
2015
  liveness: live?.proc ? "live" : "dormant",
2037
2016
  agentId: live?.agentId ?? persisted?.agentId,
2038
2017
  modelId: live?.modelId ?? persisted?.modelId,
2039
2018
  running: live?.running ?? false,
2040
- // A dormant session's totals live in kiro's file; a live one's snapshot
2041
- // starts at zero until the first turn reports, so fall back to the file.
2042
- creditsSpent: snap?.creditsSpent || persisted?.creditsSpent,
2019
+ // A live snapshot starts at zero until the first turn reports, so a dormant
2020
+ // session's value comes from kiro's file.
2043
2021
  contextUsagePercentage: snap?.contextUsagePercentage || persisted?.contextUsagePercentage
2044
2022
  };
2045
2023
  }
2046
- async listSessions() {
2047
- const persisted = await listPersistedSessions(this.log);
2048
- const files = new Map(persisted.map((p) => [p.sessionId, p]));
2049
- const byId = /* @__PURE__ */ new Map();
2050
- for (const p of persisted) {
2051
- byId.set(p.sessionId, this.summaryOf(void 0, p));
2052
- }
2053
- for (const s of this.sessions.values()) {
2054
- if (isGhost(s, files.has(s.sessionId))) {
2055
- this.evict(s.sessionId);
2056
- continue;
2024
+ async listChats() {
2025
+ const rows = this.store.all();
2026
+ const files = await Promise.all(
2027
+ rows.map((r) => r.sessionId ? readPersistedSession(r.sessionId) : null)
2028
+ );
2029
+ const out = [];
2030
+ rows.forEach((row, i) => {
2031
+ const live = row.sessionId ? this.sessions.get(row.sessionId) : void 0;
2032
+ const persisted = files[i] ?? void 0;
2033
+ if (live && isGhost(live, !!persisted)) {
2034
+ this.evict(live.sessionId);
2035
+ return;
2057
2036
  }
2058
- byId.set(s.sessionId, this.summaryOf(s, files.get(s.sessionId)));
2059
- }
2060
- return [...byId.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2061
- }
2062
- async getDetail(sessionId) {
2037
+ if (!persisted && !live) return;
2038
+ out.push(this.summaryOf(row, live, persisted));
2039
+ });
2040
+ return out.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
2041
+ }
2042
+ /** kiro's session id for a chat, or a throw if the chat is unknown. */
2043
+ sessionIdOf(chatId) {
2044
+ const sessionId = this.store.sessionIdForChat(chatId);
2045
+ if (!sessionId) throw new Error(`Unknown chat: ${chatId}`);
2046
+ return sessionId;
2047
+ }
2048
+ async getDetail(chatId) {
2049
+ const chat = this.store.get(chatId);
2050
+ if (!chat) throw new Error(`Unknown chat: ${chatId}`);
2051
+ const sessionId = chat.sessionId;
2063
2052
  const [transcript, persisted] = await Promise.all([
2064
- hydrateTranscript(sessionId),
2065
- readPersistedSession(sessionId)
2053
+ sessionId ? hydrateTranscript(sessionId, this.store.attachmentsByChat(chatId)) : Promise.resolve([]),
2054
+ sessionId ? readPersistedSession(sessionId) : Promise.resolve(null)
2066
2055
  ]);
2067
- const s = this.sessions.get(sessionId);
2068
- if (s) return this.buildDetail(s, transcript, persisted ?? void 0);
2069
- if (!persisted) throw new Error(`Unknown session: ${sessionId}`);
2070
- return {
2071
- summary: this.summaryOf(void 0, persisted, transcript),
2072
- modes: [],
2073
- currentModeId: persisted.agentId,
2074
- transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
2075
- transcriptTotal: transcript.length,
2076
- observability: {
2077
- ...emptyObservabilitySnapshot(),
2078
- creditsSpent: persisted.creditsSpent ?? 0,
2079
- contextUsagePercentage: persisted.contextUsagePercentage ?? 0
2080
- },
2081
- head: 0
2082
- };
2056
+ const s = sessionId ? this.sessions.get(sessionId) : void 0;
2057
+ if (!s && !persisted) throw new Error(`Unknown chat: ${chatId}`);
2058
+ return this.buildDetail(chat, s, transcript, persisted ?? void 0);
2083
2059
  }
2084
2060
  /**
2085
2061
  * A slice of the transcript for lazy "load older on scroll up". Re-hydrates
2086
2062
  * from disk (fast: ~150ms even for a multi-MB session) and returns the
2087
2063
  * requested window. offset/limit are clamped to the transcript bounds.
2088
2064
  */
2089
- async getTranscriptPage(sessionId, offset, limit) {
2090
- const transcript = await hydrateTranscript(sessionId);
2065
+ async getTranscriptPage(chatId, offset, limit) {
2066
+ const transcript = await hydrateTranscript(
2067
+ this.sessionIdOf(chatId),
2068
+ this.store.attachmentsByChat(chatId)
2069
+ );
2091
2070
  const start = Math.max(0, Math.min(offset, transcript.length));
2092
2071
  const end = Math.max(start, Math.min(start + limit, transcript.length));
2093
2072
  return transcript.slice(start, end);
2094
2073
  }
2095
- buildDetail(s, transcript, persisted) {
2096
- const snap = s.turnState.get();
2074
+ /** One projection, so a dormant session and a live one cannot disagree. */
2075
+ buildDetail(chat, s, transcript, persisted) {
2097
2076
  return {
2098
- summary: this.summaryOf(s, persisted, transcript),
2099
- modes: s.availableModes,
2100
- currentModeId: s.currentModeId,
2077
+ summary: this.summaryOf(chat, s, persisted, transcript),
2078
+ modes: s?.availableModes ?? [],
2079
+ currentModeId: s?.currentModeId ?? persisted?.agentId,
2101
2080
  // Only the tail is sent on load; replayHead/title use the full transcript.
2102
2081
  transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
2103
2082
  transcriptTotal: transcript.length,
2104
- observability: snap,
2105
- head: this.replayHead(s, transcript)
2083
+ observability: s?.turnState.get() ?? {
2084
+ ...emptyObservabilitySnapshot(),
2085
+ contextUsagePercentage: persisted?.contextUsagePercentage ?? 0
2086
+ },
2087
+ head: s ? this.replayHead(s, transcript) : 0
2106
2088
  };
2107
2089
  }
2108
2090
  /**
2109
- * The cursor a reconnecting client should start from. Normally the store head, but kiro writes
2110
- * a turn to its jsonl only once the turn completes, so a hydrated transcript is missing one in
2111
- * flight. Rewind to just before its turn_started and let the WS replay the whole turn. Guarded
2112
- * against duplication in case kiro ever persists the prompt at turn start.
2091
+ * The cursor a reconnecting client starts from. Normally the store head, but kiro writes a turn
2092
+ * to its jsonl only once it completes, so a hydrated transcript is missing one in flight:
2093
+ * rewind to just before its turn_started and let the socket replay the whole turn.
2113
2094
  */
2114
2095
  replayHead(s, transcript) {
2115
2096
  const head = s.store.head();
@@ -2147,16 +2128,18 @@ ${tail}` : err.message;
2147
2128
  }
2148
2129
  // Permanently delete a session: evict it from memory, remove its on-disk
2149
2130
  // files, and drop any title override.
2150
- async deleteSession(sessionId) {
2151
- const s = this.sessions.get(sessionId);
2131
+ async deleteChat(chatId) {
2132
+ const sessionId = this.store.sessionIdForChat(chatId);
2133
+ const s = sessionId ? this.sessions.get(sessionId) : void 0;
2152
2134
  if (s?.proc) {
2153
2135
  await s.proc.disposeAndWait().catch(() => {
2154
2136
  });
2155
2137
  s.proc = void 0;
2156
2138
  s.running = false;
2157
2139
  }
2140
+ this.store.remove(chatId);
2141
+ if (!sessionId) return;
2158
2142
  this.evict(sessionId);
2159
- this.store.remove(sessionId);
2160
2143
  await deletePersistedSession(sessionId);
2161
2144
  setTimeout(() => void deletePersistedSession(sessionId).catch(() => {
2162
2145
  }), 2500).unref?.();
@@ -2214,13 +2197,17 @@ var init_rateLimit = __esm({
2214
2197
  });
2215
2198
 
2216
2199
  // server/src/routes/auth.ts
2217
- import crypto4 from "node:crypto";
2200
+ import crypto3 from "node:crypto";
2218
2201
  import fastifyCookie from "@fastify/cookie";
2202
+ function logins() {
2203
+ loginStore ??= new LoginStore();
2204
+ return loginStore;
2205
+ }
2219
2206
  function secretMatches(supplied, expected) {
2220
2207
  if (typeof supplied !== "string") return false;
2221
- const a = crypto4.createHash("sha256").update(supplied).digest();
2222
- const b = crypto4.createHash("sha256").update(expected).digest();
2223
- return crypto4.timingSafeEqual(a, b);
2208
+ const a = crypto3.createHash("sha256").update(supplied).digest();
2209
+ const b = crypto3.createHash("sha256").update(expected).digest();
2210
+ return crypto3.timingSafeEqual(a, b);
2224
2211
  }
2225
2212
  function extractToken(req) {
2226
2213
  const auth = req.headers["authorization"];
@@ -2238,7 +2225,7 @@ function sessionToken(req) {
2238
2225
  return req.cookies?.[SESSION_COOKIE];
2239
2226
  }
2240
2227
  function hasValidSession(req) {
2241
- return logins.verify(sessionToken(req)) !== null;
2228
+ return logins().verify(sessionToken(req)) !== null;
2242
2229
  }
2243
2230
  function cookieOptions() {
2244
2231
  return {
@@ -2266,27 +2253,27 @@ async function registerAuth(app) {
2266
2253
  loginLimiter.succeed(req.ip);
2267
2254
  }
2268
2255
  const ua = req.headers["user-agent"];
2269
- const { token } = logins.create(typeof ua === "string" ? ua : void 0);
2256
+ const { token } = logins().create(typeof ua === "string" ? ua : void 0);
2270
2257
  reply.setCookie(SESSION_COOKIE, token, cookieOptions());
2271
2258
  return { ok: true };
2272
2259
  });
2273
2260
  app.post("/api/logout", async (req, reply) => {
2274
- logins.revokeToken(sessionToken(req));
2261
+ logins().revokeToken(sessionToken(req));
2275
2262
  reply.clearCookie(SESSION_COOKIE, { path: "/" });
2276
2263
  return { ok: true };
2277
2264
  });
2278
2265
  app.get("/api/devices", async (req) => {
2279
- return { devices: logins.list(sessionToken(req)) };
2266
+ return { devices: logins().list(sessionToken(req)) };
2280
2267
  });
2281
2268
  app.delete("/api/devices/:id", async (req, reply) => {
2282
2269
  const { id } = req.params;
2283
- const current = logins.list(sessionToken(req)).find((d) => d.current);
2284
- const removed = logins.revokeId(id);
2270
+ const current = logins().list(sessionToken(req)).find((d) => d.current);
2271
+ const removed = logins().revokeId(id);
2285
2272
  if (current?.id === id) reply.clearCookie(SESSION_COOKIE, { path: "/" });
2286
2273
  return { ok: removed };
2287
2274
  });
2288
2275
  app.post("/api/logout-all", async (_req, reply) => {
2289
- logins.revokeAll();
2276
+ logins().revokeAll();
2290
2277
  reply.clearCookie(SESSION_COOKIE, { path: "/" });
2291
2278
  return { ok: true };
2292
2279
  });
@@ -2298,7 +2285,7 @@ async function registerAuth(app) {
2298
2285
  reply.code(401).send({ error: "Unauthorized: log in first" });
2299
2286
  });
2300
2287
  }
2301
- var SESSION_COOKIE, logins, loginLimiter;
2288
+ var SESSION_COOKIE, loginStore, loginLimiter;
2302
2289
  var init_auth = __esm({
2303
2290
  "server/src/routes/auth.ts"() {
2304
2291
  "use strict";
@@ -2306,7 +2293,6 @@ var init_auth = __esm({
2306
2293
  init_logins();
2307
2294
  init_rateLimit();
2308
2295
  SESSION_COOKIE = "casper.sid";
2309
- logins = new LoginStore();
2310
2296
  loginLimiter = new AttemptLimiter(10, 15 * 60 * 1e3);
2311
2297
  }
2312
2298
  });
@@ -2430,12 +2416,12 @@ function replyWith(reply, failure) {
2430
2416
  reply.code(failure.status);
2431
2417
  return { error: failure.error };
2432
2418
  }
2433
- async function resolveSessionPath(sessions, sessionId, requestedPath, require2) {
2419
+ async function resolveChatPath(sessions, sessionId, requestedPath, require2) {
2434
2420
  let cwd;
2435
2421
  try {
2436
- cwd = await sessions.getSessionCwd(sessionId);
2422
+ cwd = await sessions.getChatCwd(sessionId);
2437
2423
  } catch {
2438
- return { ok: false, status: 404, error: "Session not found" };
2424
+ return { ok: false, status: 404, error: "Chat not found" };
2439
2425
  }
2440
2426
  const relative = (requestedPath ?? "").replace(/^\/+/, "");
2441
2427
  if (require2 === "file" && !relative) {
@@ -2520,15 +2506,15 @@ function looksBinary(buf) {
2520
2506
  }
2521
2507
  function classifyKind(name) {
2522
2508
  const ext = path13.extname(name).toLowerCase();
2523
- if (ext in IMAGE_EXTS2) return "image";
2509
+ if (ext in IMAGE_EXTS) return "image";
2524
2510
  if (TEXT_EXTS.has(ext)) return "text";
2525
2511
  return "binary";
2526
2512
  }
2527
- var IMAGE_EXTS2, TEXT_EXTS, MIME_MAP;
2513
+ var IMAGE_EXTS, TEXT_EXTS, MIME_MAP;
2528
2514
  var init_filekind = __esm({
2529
2515
  "server/src/util/filekind.ts"() {
2530
2516
  "use strict";
2531
- IMAGE_EXTS2 = {
2517
+ IMAGE_EXTS = {
2532
2518
  ".png": "image/png",
2533
2519
  ".jpg": "image/jpeg",
2534
2520
  ".jpeg": "image/jpeg",
@@ -2600,7 +2586,7 @@ var init_filekind = __esm({
2600
2586
  ".tex"
2601
2587
  ]);
2602
2588
  MIME_MAP = {
2603
- ...IMAGE_EXTS2,
2589
+ ...IMAGE_EXTS,
2604
2590
  ".txt": "text/plain",
2605
2591
  ".md": "text/markdown",
2606
2592
  ".csv": "text/csv",
@@ -2628,10 +2614,128 @@ var init_filekind = __esm({
2628
2614
  }
2629
2615
  });
2630
2616
 
2631
- // server/src/routes/fs.ts
2617
+ // server/src/routes/filePreview.ts
2632
2618
  import fs12 from "node:fs/promises";
2633
2619
  import { createReadStream } from "node:fs";
2634
2620
  import path14 from "node:path";
2621
+ function tooLargeForPreview(size, maxBytes) {
2622
+ return {
2623
+ error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
2624
+ };
2625
+ }
2626
+ function hexdump(buf) {
2627
+ const lines = [];
2628
+ for (let off = 0; off < buf.length; off += 16) {
2629
+ const slice = buf.subarray(off, off + 16);
2630
+ const hex = [];
2631
+ let ascii = "";
2632
+ for (let i = 0; i < 16; i++) {
2633
+ if (i < slice.length) {
2634
+ const b = slice[i];
2635
+ hex.push(b.toString(16).padStart(2, "0"));
2636
+ ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
2637
+ } else {
2638
+ hex.push(" ");
2639
+ }
2640
+ if (i === 7) hex.push("");
2641
+ }
2642
+ lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
2643
+ }
2644
+ return lines.join("\n");
2645
+ }
2646
+ async function sendFilePreview(req, reply, realTarget, stat) {
2647
+ const ext = path14.extname(realTarget).toLowerCase();
2648
+ const mime = mimeForExt(ext);
2649
+ const isImage = mime.startsWith("image/");
2650
+ const isPdf = mime === "application/pdf";
2651
+ const isHtml = mime === "text/html" && req.query.raw === "1";
2652
+ const kind = classifyKind(realTarget);
2653
+ if (isImage || isPdf) {
2654
+ if (stat.size > MAX_IMAGE_PREVIEW_BYTES) {
2655
+ reply.code(413);
2656
+ return tooLargeForPreview(stat.size, MAX_IMAGE_PREVIEW_BYTES);
2657
+ }
2658
+ } else if (kind !== "binary") {
2659
+ if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2660
+ reply.code(413);
2661
+ return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2662
+ }
2663
+ }
2664
+ if (isHtml) {
2665
+ if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2666
+ reply.code(413);
2667
+ return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2668
+ }
2669
+ reply.header("Content-Security-Policy", "sandbox allow-scripts allow-forms");
2670
+ reply.header("Content-Type", "text/html; charset=utf-8");
2671
+ reply.header("Cache-Control", "no-store");
2672
+ return reply.send(createReadStream(realTarget));
2673
+ }
2674
+ if (isImage || isPdf) {
2675
+ const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2676
+ reply.header("Cache-Control", "private, no-cache");
2677
+ reply.header("ETag", etag);
2678
+ reply.header("Last-Modified", stat.mtime.toUTCString());
2679
+ const ifNoneMatch = req.headers["if-none-match"];
2680
+ if (ifNoneMatch === etag) {
2681
+ reply.code(304);
2682
+ return reply.send();
2683
+ }
2684
+ reply.header("Content-Type", mime);
2685
+ reply.header("Content-Disposition", "inline");
2686
+ reply.header("Content-Length", stat.size);
2687
+ return reply.send(createReadStream(realTarget));
2688
+ }
2689
+ if (kind === "binary") {
2690
+ let fh;
2691
+ try {
2692
+ fh = await fs12.open(realTarget, "r");
2693
+ const buf = Buffer.alloc(Math.min(HEXDUMP_BYTES, stat.size));
2694
+ const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
2695
+ const head = buf.subarray(0, bytesRead);
2696
+ if (!looksBinary(head)) {
2697
+ if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2698
+ reply.code(413);
2699
+ return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2700
+ }
2701
+ reply.header("Content-Type", "text/plain; charset=utf-8");
2702
+ reply.header("Content-Disposition", "inline");
2703
+ reply.header("Content-Length", stat.size);
2704
+ return reply.send(createReadStream(realTarget));
2705
+ }
2706
+ const dump = hexdump(head);
2707
+ const header = `Binary file - ${stat.size} bytes
2708
+ Showing first ${bytesRead} bytes as hexdump:
2709
+
2710
+ `;
2711
+ const body = header + dump + (stat.size > bytesRead ? "\n\n\u2026 (truncated)" : "");
2712
+ reply.header("Content-Type", "text/plain; charset=utf-8");
2713
+ reply.header("Content-Disposition", "inline");
2714
+ return reply.send(body);
2715
+ } finally {
2716
+ await fh?.close();
2717
+ }
2718
+ }
2719
+ reply.header("Content-Type", "text/plain; charset=utf-8");
2720
+ reply.header("Content-Disposition", "inline");
2721
+ reply.header("Content-Length", stat.size);
2722
+ return reply.send(createReadStream(realTarget));
2723
+ }
2724
+ var HEXDUMP_BYTES, MAX_TEXT_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_BYTES;
2725
+ var init_filePreview = __esm({
2726
+ "server/src/routes/filePreview.ts"() {
2727
+ "use strict";
2728
+ init_filekind();
2729
+ HEXDUMP_BYTES = 4096;
2730
+ MAX_TEXT_PREVIEW_BYTES = 1024 * 1024;
2731
+ MAX_IMAGE_PREVIEW_BYTES = 20 * 1024 * 1024;
2732
+ }
2733
+ });
2734
+
2735
+ // server/src/routes/fs.ts
2736
+ import fs13 from "node:fs/promises";
2737
+ import { createReadStream as createReadStream2 } from "node:fs";
2738
+ import path15 from "node:path";
2635
2739
  function registerFsRoutes(app) {
2636
2740
  app.get(
2637
2741
  "/api/fs/dirs",
@@ -2639,10 +2743,10 @@ function registerFsRoutes(app) {
2639
2743
  const input = (req.query.path ?? "").trim();
2640
2744
  const base = config.defaultCwd;
2641
2745
  const endsWithSep = input.endsWith("/");
2642
- const resolved = input ? path14.resolve(base, input) : base;
2643
- const dir = endsWithSep || !input ? resolved : path14.dirname(resolved);
2644
- const prefix = endsWithSep || !input ? "" : path14.basename(resolved);
2645
- const targetKind = await fs12.stat(resolved).then((s) => s.isDirectory() ? "directory" : "file").catch(() => "missing");
2746
+ const resolved = input ? path15.resolve(base, input) : base;
2747
+ const dir = endsWithSep || !input ? resolved : path15.dirname(resolved);
2748
+ const prefix = endsWithSep || !input ? "" : path15.basename(resolved);
2749
+ const targetKind = await fs13.stat(resolved).then((s) => s.isDirectory() ? "directory" : "file").catch(() => "missing");
2646
2750
  const listing = await resolveAbsolutePath(dir, "directory");
2647
2751
  if (!listing.ok) {
2648
2752
  if (listing.status === 403) return replyWith(reply, listing);
@@ -2651,7 +2755,7 @@ function registerFsRoutes(app) {
2651
2755
  const realDir = listing.real;
2652
2756
  let entries = [];
2653
2757
  try {
2654
- const dirents = await fs12.readdir(realDir, { withFileTypes: true });
2758
+ const dirents = await fs13.readdir(realDir, { withFileTypes: true });
2655
2759
  const checks2 = await Promise.all(
2656
2760
  dirents.map(async (d) => {
2657
2761
  const target = await classifyDirent(realDir, d, absoluteRoots());
@@ -2662,7 +2766,7 @@ function registerFsRoutes(app) {
2662
2766
  entries = checks2.filter((name) => name !== null).filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())).sort((a, b) => {
2663
2767
  if (isDot(a) !== isDot(b)) return isDot(a) ? 1 : -1;
2664
2768
  return a.localeCompare(b);
2665
- }).slice(0, 500).map((name) => path14.join(dir, name));
2769
+ }).slice(0, 500).map((name) => path15.join(dir, name));
2666
2770
  } catch {
2667
2771
  entries = [];
2668
2772
  }
@@ -2670,75 +2774,66 @@ function registerFsRoutes(app) {
2670
2774
  }
2671
2775
  );
2672
2776
  app.get(
2673
- "/api/fs/image",
2777
+ "/api/fs/file",
2674
2778
  async (req, reply) => {
2675
2779
  const filePath = (req.query.path ?? "").trim();
2676
2780
  if (!filePath) {
2677
2781
  reply.code(400);
2678
2782
  return { error: "path parameter is required" };
2679
2783
  }
2680
- if (!path14.isAbsolute(filePath)) {
2784
+ if (!path15.isAbsolute(filePath)) {
2681
2785
  reply.code(400);
2682
2786
  return { error: "path must be absolute" };
2683
2787
  }
2684
- const ext = path14.extname(filePath).toLowerCase();
2685
- if (classifyKind(filePath) !== "image") {
2686
- reply.code(400);
2687
- return { error: `Not a supported image type: ${ext}` };
2688
- }
2689
- const resolvedImage = await resolveAbsolutePath(filePath, "file");
2690
- if (!resolvedImage.ok) return replyWith(reply, resolvedImage);
2691
- const { real, stat } = resolvedImage;
2692
- if (stat.size > MAX_IMAGE_BYTES) {
2693
- reply.code(413);
2694
- return { error: "Image too large" };
2695
- }
2696
- const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2697
- reply.header("Cache-Control", "private, no-cache");
2698
- reply.header("ETag", etag);
2699
- reply.header("Last-Modified", stat.mtime.toUTCString());
2700
- if (req.headers["if-none-match"] === etag) {
2701
- reply.code(304);
2702
- return reply.send();
2788
+ const resolved = await resolveAbsolutePath(filePath, "file");
2789
+ if (!resolved.ok) return replyWith(reply, resolved);
2790
+ if (req.query.download === "1") {
2791
+ reply.header("Content-Type", "application/octet-stream");
2792
+ reply.header(
2793
+ "Content-Disposition",
2794
+ `attachment; filename="${path15.basename(resolved.real).replace(/"/g, "")}"`
2795
+ );
2796
+ reply.header("Content-Length", resolved.stat.size);
2797
+ return reply.send(createReadStream2(resolved.real));
2703
2798
  }
2704
- reply.header("Content-Type", mimeForExt(ext));
2705
- reply.header("Content-Length", stat.size);
2706
- return reply.send(createReadStream(real));
2799
+ return sendFilePreview(req, reply, resolved.real, resolved.stat);
2707
2800
  }
2708
2801
  );
2709
2802
  }
2710
- var MAX_IMAGE_BYTES;
2711
2803
  var init_fs = __esm({
2712
2804
  "server/src/routes/fs.ts"() {
2713
2805
  "use strict";
2714
2806
  init_config();
2715
2807
  init_confinedFile();
2716
- init_filekind();
2717
- MAX_IMAGE_BYTES = 20 * 1024 * 1024;
2808
+ init_filePreview();
2718
2809
  }
2719
2810
  });
2720
2811
 
2721
- // server/src/routes/sessions.ts
2722
- function registerSessionRoutes(app, manager) {
2723
- app.get("/api/sessions", async () => {
2724
- return { sessions: await manager.listSessions() };
2812
+ // server/src/routes/chats.ts
2813
+ function registerChatRoutes(app, manager) {
2814
+ app.get("/api/chats", async () => {
2815
+ return { chats: await manager.listChats() };
2725
2816
  });
2726
- app.post("/api/sessions", async (req, reply) => {
2817
+ app.post("/api/chats", async (req, reply) => {
2727
2818
  const body = req.body ?? {};
2728
2819
  try {
2729
- return await manager.createSession({
2820
+ return await manager.createChat({
2730
2821
  cwd: body.cwd,
2731
2822
  agentId: body.agentId,
2732
2823
  modelId: body.modelId,
2733
2824
  freshWorkspace: body.freshWorkspace,
2734
- title: body.title
2825
+ title: body.title,
2826
+ // Not optional in practice: the client's chat already owns any file uploaded before
2827
+ // the session existed, so dropping it here strands those files under an id nothing
2828
+ // refers to again.
2829
+ chatId: body.chatId
2735
2830
  });
2736
2831
  } catch (err) {
2737
2832
  reply.code(400);
2738
2833
  return { error: err.message };
2739
2834
  }
2740
2835
  });
2741
- app.get("/api/sessions/:id", async (req, reply) => {
2836
+ app.get("/api/chats/:id", async (req, reply) => {
2742
2837
  try {
2743
2838
  return await manager.getDetail(req.params.id);
2744
2839
  } catch (err) {
@@ -2747,7 +2842,7 @@ function registerSessionRoutes(app, manager) {
2747
2842
  }
2748
2843
  });
2749
2844
  app.get(
2750
- "/api/sessions/:id/transcript",
2845
+ "/api/chats/:id/transcript",
2751
2846
  async (req, reply) => {
2752
2847
  const offset = Number.parseInt(req.query.offset ?? "", 10);
2753
2848
  const limit = Number.parseInt(req.query.limit ?? "", 10);
@@ -2764,10 +2859,10 @@ function registerSessionRoutes(app, manager) {
2764
2859
  }
2765
2860
  );
2766
2861
  app.post(
2767
- "/api/sessions/:id/prompt",
2862
+ "/api/chats/:id/prompt",
2768
2863
  async (req, reply) => {
2769
2864
  try {
2770
- await manager.runPrompt(req.params.id, req.body.prompt);
2865
+ await manager.runPrompt(req.params.id, req.body.prompt, req.body.attachments);
2771
2866
  return { ok: true };
2772
2867
  } catch (err) {
2773
2868
  reply.code(400);
@@ -2775,33 +2870,33 @@ function registerSessionRoutes(app, manager) {
2775
2870
  }
2776
2871
  }
2777
2872
  );
2778
- app.post("/api/sessions/:id/cancel", async (req) => {
2873
+ app.post("/api/chats/:id/cancel", async (req) => {
2779
2874
  manager.cancel(req.params.id);
2780
2875
  return { ok: true };
2781
2876
  });
2782
2877
  app.post(
2783
- "/api/sessions/:id/model",
2878
+ "/api/chats/:id/model",
2784
2879
  async (req) => {
2785
2880
  await manager.setModel(req.params.id, req.body.modelId);
2786
2881
  return { ok: true };
2787
2882
  }
2788
2883
  );
2789
2884
  app.post(
2790
- "/api/sessions/:id/mode",
2885
+ "/api/chats/:id/mode",
2791
2886
  async (req) => {
2792
2887
  await manager.setMode(req.params.id, req.body.modeId);
2793
2888
  return { ok: true };
2794
2889
  }
2795
2890
  );
2796
2891
  app.post(
2797
- "/api/sessions/:id/rename",
2892
+ "/api/chats/:id/rename",
2798
2893
  async (req) => {
2799
- manager.renameSession(req.params.id, req.body.title);
2894
+ manager.renameChat(req.params.id, req.body.title);
2800
2895
  return { ok: true };
2801
2896
  }
2802
2897
  );
2803
2898
  app.post(
2804
- "/api/sessions/:id/cwd",
2899
+ "/api/chats/:id/cwd",
2805
2900
  async (req, reply) => {
2806
2901
  const cwd = (req.body?.cwd ?? "").trim();
2807
2902
  if (!cwd) {
@@ -2809,66 +2904,41 @@ function registerSessionRoutes(app, manager) {
2809
2904
  return { error: "cwd is required" };
2810
2905
  }
2811
2906
  try {
2812
- return { ok: true, cwd: await manager.setSessionCwd(req.params.id, cwd) };
2907
+ return { ok: true, cwd: await manager.setChatCwd(req.params.id, cwd) };
2813
2908
  } catch (err) {
2814
2909
  reply.code(400);
2815
2910
  return { error: err.message };
2816
2911
  }
2817
2912
  }
2818
2913
  );
2819
- app.post("/api/sessions/:id/reload", async (req, reply) => {
2914
+ app.post("/api/chats/:id/reload", async (req, reply) => {
2820
2915
  try {
2821
- return await manager.reloadSession(req.params.id);
2916
+ return await manager.reloadChat(req.params.id);
2822
2917
  } catch (err) {
2823
2918
  reply.code(400);
2824
2919
  return { error: err.message };
2825
2920
  }
2826
2921
  });
2827
- app.delete("/api/sessions/:id", async (req) => {
2828
- await manager.deleteSession(req.params.id);
2922
+ app.delete("/api/chats/:id", async (req) => {
2923
+ await manager.deleteChat(req.params.id);
2829
2924
  return { ok: true };
2830
2925
  });
2831
2926
  }
2832
- var init_sessions = __esm({
2833
- "server/src/routes/sessions.ts"() {
2927
+ var init_chats2 = __esm({
2928
+ "server/src/routes/chats.ts"() {
2834
2929
  "use strict";
2835
2930
  }
2836
2931
  });
2837
2932
 
2838
2933
  // server/src/routes/workspace.ts
2839
- import fs13 from "node:fs/promises";
2840
- import { createReadStream as createReadStream2 } from "node:fs";
2841
- import path15 from "node:path";
2842
- function tooLargeForPreview(size, maxBytes) {
2843
- return {
2844
- error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
2845
- };
2846
- }
2847
- function hexdump(buf) {
2848
- const lines = [];
2849
- for (let off = 0; off < buf.length; off += 16) {
2850
- const slice = buf.subarray(off, off + 16);
2851
- const hex = [];
2852
- let ascii = "";
2853
- for (let i = 0; i < 16; i++) {
2854
- if (i < slice.length) {
2855
- const b = slice[i];
2856
- hex.push(b.toString(16).padStart(2, "0"));
2857
- ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
2858
- } else {
2859
- hex.push(" ");
2860
- }
2861
- if (i === 7) hex.push("");
2862
- }
2863
- lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
2864
- }
2865
- return lines.join("\n");
2866
- }
2934
+ import fs14 from "node:fs/promises";
2935
+ import { createReadStream as createReadStream3 } from "node:fs";
2936
+ import path16 from "node:path";
2867
2937
  function registerWorkspaceRoutes(app, manager) {
2868
2938
  app.get(
2869
- "/api/sessions/:id/tree",
2939
+ "/api/chats/:id/tree",
2870
2940
  async (req, reply) => {
2871
- const resolved = await resolveSessionPath(
2941
+ const resolved = await resolveChatPath(
2872
2942
  manager,
2873
2943
  req.params.id,
2874
2944
  req.query.path,
@@ -2878,7 +2948,7 @@ function registerWorkspaceRoutes(app, manager) {
2878
2948
  const { cwd, relative, real: realTarget } = resolved;
2879
2949
  let dirents;
2880
2950
  try {
2881
- dirents = await fs13.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2951
+ dirents = await fs14.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2882
2952
  } catch {
2883
2953
  return replyWith(reply, await workspaceNotFound(cwd));
2884
2954
  }
@@ -2892,7 +2962,7 @@ function registerWorkspaceRoutes(app, manager) {
2892
2962
  continue;
2893
2963
  }
2894
2964
  try {
2895
- const stat = await fs13.stat(target.real);
2965
+ const stat = await fs14.stat(target.real);
2896
2966
  entries.push({
2897
2967
  name: d.name,
2898
2968
  path: entryRelative,
@@ -2912,17 +2982,17 @@ function registerWorkspaceRoutes(app, manager) {
2912
2982
  }
2913
2983
  );
2914
2984
  app.get(
2915
- "/api/sessions/:id/download",
2985
+ "/api/chats/:id/download",
2916
2986
  async (req, reply) => {
2917
- const resolved = await resolveSessionPath(manager, req.params.id, req.query.path, "file");
2987
+ const resolved = await resolveChatPath(manager, req.params.id, req.query.path, "file");
2918
2988
  if (!resolved.ok) return replyWith(reply, resolved);
2919
2989
  const { real: realTarget, stat } = resolved;
2920
2990
  if (stat.size > MAX_DOWNLOAD_BYTES) {
2921
2991
  reply.code(413);
2922
2992
  return { error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 100 MB)` };
2923
2993
  }
2924
- const ext = path15.extname(realTarget);
2925
- const filename = path15.basename(realTarget);
2994
+ const ext = path16.extname(realTarget);
2995
+ const filename = path16.basename(realTarget);
2926
2996
  const asciiName = filename.replace(/[^\w.\-]/g, "_");
2927
2997
  reply.header("Content-Type", mimeForExt(ext));
2928
2998
  reply.header(
@@ -2930,141 +3000,61 @@ function registerWorkspaceRoutes(app, manager) {
2930
3000
  `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(filename)}`
2931
3001
  );
2932
3002
  reply.header("Content-Length", stat.size);
2933
- return reply.send(createReadStream2(realTarget));
3003
+ return reply.send(createReadStream3(realTarget));
2934
3004
  }
2935
3005
  );
2936
3006
  app.get(
2937
- "/api/sessions/:id/preview",
3007
+ "/api/chats/:id/preview",
2938
3008
  async (req, reply) => {
2939
- const resolved = await resolveSessionPath(manager, req.params.id, req.query.path, "file");
3009
+ const resolved = await resolveChatPath(manager, req.params.id, req.query.path, "file");
2940
3010
  if (!resolved.ok) return replyWith(reply, resolved);
2941
3011
  const { real: realTarget, stat } = resolved;
2942
- const ext = path15.extname(realTarget).toLowerCase();
2943
- const mime = mimeForExt(ext);
2944
- const isImage = mime.startsWith("image/");
2945
- const isPdf = mime === "application/pdf";
2946
- const isHtml = mime === "text/html" && req.query.raw === "1";
2947
- const kind = classifyKind(realTarget);
2948
- if (isImage || isPdf) {
2949
- if (stat.size > MAX_IMAGE_PREVIEW_BYTES) {
2950
- reply.code(413);
2951
- return tooLargeForPreview(stat.size, MAX_IMAGE_PREVIEW_BYTES);
2952
- }
2953
- } else if (kind !== "binary") {
2954
- if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2955
- reply.code(413);
2956
- return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2957
- }
2958
- }
2959
- if (isHtml) {
2960
- if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2961
- reply.code(413);
2962
- return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2963
- }
2964
- reply.header("Content-Security-Policy", "sandbox allow-scripts allow-forms");
2965
- reply.header("Content-Type", "text/html; charset=utf-8");
2966
- reply.header("Cache-Control", "no-store");
2967
- return reply.send(createReadStream2(realTarget));
2968
- }
2969
- if (isImage || isPdf) {
2970
- const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2971
- reply.header("Cache-Control", "private, no-cache");
2972
- reply.header("ETag", etag);
2973
- reply.header("Last-Modified", stat.mtime.toUTCString());
2974
- const ifNoneMatch = req.headers["if-none-match"];
2975
- if (ifNoneMatch === etag) {
2976
- reply.code(304);
2977
- return reply.send();
2978
- }
2979
- reply.header("Content-Type", mime);
2980
- reply.header("Content-Disposition", "inline");
2981
- reply.header("Content-Length", stat.size);
2982
- return reply.send(createReadStream2(realTarget));
2983
- }
2984
- if (kind === "binary") {
2985
- let fh;
2986
- try {
2987
- fh = await fs13.open(realTarget, "r");
2988
- const buf = Buffer.alloc(Math.min(HEXDUMP_BYTES, stat.size));
2989
- const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
2990
- const head = buf.subarray(0, bytesRead);
2991
- if (!looksBinary(head)) {
2992
- if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2993
- reply.code(413);
2994
- return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2995
- }
2996
- reply.header("Content-Type", "text/plain; charset=utf-8");
2997
- reply.header("Content-Disposition", "inline");
2998
- reply.header("Content-Length", stat.size);
2999
- return reply.send(createReadStream2(realTarget));
3000
- }
3001
- const dump = hexdump(head);
3002
- const header = `Binary file - ${stat.size} bytes
3003
- Showing first ${bytesRead} bytes as hexdump:
3004
-
3005
- `;
3006
- const body = header + dump + (stat.size > bytesRead ? "\n\n\u2026 (truncated)" : "");
3007
- reply.header("Content-Type", "text/plain; charset=utf-8");
3008
- reply.header("Content-Disposition", "inline");
3009
- return reply.send(body);
3010
- } finally {
3011
- await fh?.close();
3012
- }
3013
- }
3014
- reply.header("Content-Type", "text/plain; charset=utf-8");
3015
- reply.header("Content-Disposition", "inline");
3016
- reply.header("Content-Length", stat.size);
3017
- return reply.send(createReadStream2(realTarget));
3012
+ return sendFilePreview(req, reply, realTarget, stat);
3018
3013
  }
3019
3014
  );
3020
3015
  }
3021
- var MAX_DOWNLOAD_BYTES, HEXDUMP_BYTES, MAX_TEXT_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_BYTES;
3016
+ var MAX_DOWNLOAD_BYTES;
3022
3017
  var init_workspace = __esm({
3023
3018
  "server/src/routes/workspace.ts"() {
3024
3019
  "use strict";
3025
3020
  init_config();
3026
3021
  init_confinedFile();
3027
3022
  init_filekind();
3023
+ init_filePreview();
3028
3024
  MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
3029
- HEXDUMP_BYTES = 4096;
3030
- MAX_TEXT_PREVIEW_BYTES = 1024 * 1024;
3031
- MAX_IMAGE_PREVIEW_BYTES = 20 * 1024 * 1024;
3032
3025
  }
3033
3026
  });
3034
3027
 
3035
3028
  // server/src/routes/uploads.ts
3036
- import { createReadStream as createReadStream3, createWriteStream } from "node:fs";
3037
- import fs14 from "node:fs/promises";
3038
- import path16 from "node:path";
3039
- import crypto5 from "node:crypto";
3029
+ import { createReadStream as createReadStream4, createWriteStream } from "node:fs";
3030
+ import fs15 from "node:fs/promises";
3031
+ import path17 from "node:path";
3032
+ import crypto4 from "node:crypto";
3040
3033
  import { execFile as execFile3 } from "node:child_process";
3041
3034
  import { pipeline } from "node:stream/promises";
3042
- function uploadDirFor(sessionId) {
3043
- return path16.join(config.casperDataDir, "sessions", sessionId, "uploads");
3044
- }
3045
3035
  function sanitizeName(raw) {
3046
- const base = path16.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
3036
+ const base = path17.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
3047
3037
  if (!base || base === "." || base === "..") return `upload-${Date.now()}`;
3048
3038
  return base.replace(/^\.+/, "") || `upload-${Date.now()}`;
3049
3039
  }
3050
3040
  async function uniquePath(dir, name) {
3051
- const ext = path16.extname(name);
3041
+ const ext = path17.extname(name);
3052
3042
  const stem = name.slice(0, name.length - ext.length);
3053
3043
  let candidate = name;
3054
3044
  let n = 1;
3055
3045
  while (true) {
3056
3046
  try {
3057
- await fs14.access(path16.join(dir, candidate));
3047
+ await fs15.access(path17.join(dir, candidate));
3058
3048
  candidate = `${stem} (${n++})${ext}`;
3059
3049
  } catch {
3060
- return path16.join(dir, candidate);
3050
+ return path17.join(dir, candidate);
3061
3051
  }
3062
3052
  }
3063
3053
  }
3064
3054
  function sha256File(absPath) {
3065
3055
  return new Promise((resolve, reject) => {
3066
- const hash = crypto5.createHash("sha256");
3067
- createReadStream3(absPath).on("data", (d) => hash.update(d)).on("end", () => resolve(hash.digest("hex"))).on("error", reject);
3056
+ const hash = crypto4.createHash("sha256");
3057
+ createReadStream4(absPath).on("data", (d) => hash.update(d)).on("end", () => resolve(hash.digest("hex"))).on("error", reject);
3068
3058
  });
3069
3059
  }
3070
3060
  function fileType(absPath) {
@@ -3077,7 +3067,7 @@ function fileType(absPath) {
3077
3067
  async function sampleStrings(absPath, limit = 40) {
3078
3068
  let fh;
3079
3069
  try {
3080
- fh = await fs14.open(absPath, "r");
3070
+ fh = await fs15.open(absPath, "r");
3081
3071
  const buf = Buffer.alloc(STRINGS_SCAN_BYTES);
3082
3072
  const { bytesRead } = await fh.read(buf, 0, STRINGS_SCAN_BYTES, 0);
3083
3073
  const out = [];
@@ -3100,22 +3090,16 @@ async function sampleStrings(absPath, limit = 40) {
3100
3090
  await fh?.close();
3101
3091
  }
3102
3092
  }
3103
- function registerUploadRoutes(app, manager) {
3093
+ function registerUploadRoutes(app) {
3104
3094
  app.post(
3105
- "/api/sessions/:id/uploads",
3095
+ "/api/chats/:chatId/uploads",
3106
3096
  async (req, reply) => {
3107
- try {
3108
- await manager.getSessionCwd(req.params.id);
3109
- } catch {
3110
- reply.code(404);
3111
- return { error: "Session not found" };
3112
- }
3113
- if (!isValidSessionId(req.params.id)) {
3097
+ if (!isValidChatId(req.params.chatId)) {
3114
3098
  reply.code(400);
3115
- return { error: "Invalid session id" };
3099
+ return { error: "Invalid chat id" };
3116
3100
  }
3117
- const uploadDir = uploadDirFor(req.params.id);
3118
- await fs14.mkdir(uploadDir, { recursive: true, mode: 448 });
3101
+ const uploadDir = chatUploadsDir(req.params.chatId);
3102
+ await fs15.mkdir(uploadDir, { recursive: true, mode: 448 });
3119
3103
  const results = [];
3120
3104
  const parts = req.files();
3121
3105
  for await (const part of parts) {
@@ -3129,17 +3113,17 @@ function registerUploadRoutes(app, manager) {
3129
3113
  return { error: `Failed to store ${name}` };
3130
3114
  }
3131
3115
  if (part.file.truncated) {
3132
- await fs14.rm(dest, { force: true });
3116
+ await fs15.rm(dest, { force: true });
3133
3117
  reply.code(413);
3134
3118
  return {
3135
3119
  error: `${name} exceeds the max upload size (${Math.round(config.maxUploadBytes / 1024 / 1024)} MB)`
3136
3120
  };
3137
3121
  }
3138
- const stat = await fs14.stat(dest);
3139
- const ext = path16.extname(dest);
3122
+ const stat = await fs15.stat(dest);
3123
+ const ext = path17.extname(dest);
3140
3124
  const kind = classifyKind(dest);
3141
3125
  const uploaded = {
3142
- name: path16.basename(dest),
3126
+ name: path17.basename(dest),
3143
3127
  // Absolute, because the file sits outside the session's workspace: a
3144
3128
  // relative path would resolve against the wrong root for both the agent
3145
3129
  // and the image endpoint.
@@ -3149,14 +3133,14 @@ function registerUploadRoutes(app, manager) {
3149
3133
  kind
3150
3134
  };
3151
3135
  if (kind === "binary") {
3152
- const [type, sha2563, strings] = await Promise.all([
3136
+ const [type, sha2562, strings] = await Promise.all([
3153
3137
  fileType(dest),
3154
3138
  sha256File(dest),
3155
3139
  sampleStrings(dest)
3156
3140
  ]);
3157
3141
  uploaded.triage = {
3158
3142
  fileType: type || void 0,
3159
- sha256: sha2563,
3143
+ sha256: sha2562,
3160
3144
  strings: strings.length ? strings : void 0
3161
3145
  };
3162
3146
  }
@@ -3174,6 +3158,7 @@ var init_uploads = __esm({
3174
3158
  init_config();
3175
3159
  init_filekind();
3176
3160
  init_paths2();
3161
+ init_chats();
3177
3162
  STRINGS_SCAN_BYTES = 256 * 1024;
3178
3163
  }
3179
3164
  });
@@ -3197,7 +3182,7 @@ var init_health = __esm({
3197
3182
  });
3198
3183
 
3199
3184
  // server/src/ws/dirWatchers.ts
3200
- import fs15 from "node:fs";
3185
+ import fs16 from "node:fs";
3201
3186
  function diffWatchSet(current, next) {
3202
3187
  const have = new Set(current);
3203
3188
  const want = new Set(next.slice(0, MAX_WATCHES));
@@ -3221,7 +3206,7 @@ function createDirWatchers(opts) {
3221
3206
  const target = await opts.resolve(relative);
3222
3207
  if (!target || closed) return;
3223
3208
  try {
3224
- const watcher = fs15.watch(target, { persistent: false }, () => {
3209
+ const watcher = fs16.watch(target, { persistent: false }, () => {
3225
3210
  const existing = timers.get(relative);
3226
3211
  if (existing) clearTimeout(existing);
3227
3212
  timers.set(
@@ -3266,7 +3251,7 @@ var init_dirWatchers = __esm({
3266
3251
  function send(socket, msg) {
3267
3252
  if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(msg));
3268
3253
  }
3269
- function handleConnection(socket, manager, sessionId, startCursor) {
3254
+ function handleConnection(socket, manager, chatId, startCursor) {
3270
3255
  let cursor = startCursor;
3271
3256
  let unsubscribe = null;
3272
3257
  let alive = true;
@@ -3274,12 +3259,12 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3274
3259
  const watchers = createDirWatchers({
3275
3260
  resolve: async (relative) => {
3276
3261
  try {
3277
- return confineToRoot(await manager.getSessionCwd(sessionId), relative);
3262
+ return confineToRoot(await manager.getChatCwd(chatId), relative);
3278
3263
  } catch {
3279
3264
  return null;
3280
3265
  }
3281
3266
  },
3282
- onChange: (path19) => send(socket, { type: "fs_changed", path: path19 })
3267
+ onChange: (path20) => send(socket, { type: "fs_changed", path: path20 })
3283
3268
  });
3284
3269
  const forward = (event) => {
3285
3270
  if (event.seq <= cursor) return;
@@ -3288,13 +3273,13 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3288
3273
  };
3289
3274
  const attach = async () => {
3290
3275
  try {
3291
- await manager.ensureOpen(sessionId);
3276
+ await manager.ensureOpen(chatId);
3292
3277
  } catch (err) {
3293
3278
  send(socket, { type: "error", message: err.message });
3294
3279
  socket.close(1011, "open failed");
3295
3280
  return;
3296
3281
  }
3297
- const store = manager.getStore(sessionId);
3282
+ const store = manager.getStore(chatId);
3298
3283
  if (!store) {
3299
3284
  send(socket, { type: "error", message: "Session store unavailable" });
3300
3285
  socket.close(1011, "no store");
@@ -3311,7 +3296,7 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3311
3296
  for (const e of events) forward(e);
3312
3297
  }
3313
3298
  send(socket, { type: "replay_complete", head: store.head() });
3314
- unsubscribe = manager.onEvent(sessionId, forward);
3299
+ unsubscribe = manager.onEvent(chatId, forward);
3315
3300
  ready = true;
3316
3301
  };
3317
3302
  void attach();
@@ -3330,17 +3315,17 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3330
3315
  case "watch_paths":
3331
3316
  return watchers.sync(msg.paths);
3332
3317
  case "prompt":
3333
- return ack("prompt", () => manager.runPrompt(sessionId, msg.content));
3318
+ return ack("prompt", () => manager.runPrompt(chatId, msg.content, msg.attachments));
3334
3319
  case "cancel":
3335
- return ack("cancel", () => manager.cancel(sessionId));
3320
+ return ack("cancel", () => manager.cancel(chatId));
3336
3321
  case "set_mode":
3337
- return ack("set_mode", () => manager.setMode(sessionId, msg.modeId));
3322
+ return ack("set_mode", () => manager.setMode(chatId, msg.modeId));
3338
3323
  case "set_model":
3339
- return ack("set_model", () => manager.setModel(sessionId, msg.modelId));
3324
+ return ack("set_model", () => manager.setModel(chatId, msg.modelId));
3340
3325
  case "exec_command":
3341
3326
  return ack(
3342
3327
  "exec_command",
3343
- () => manager.execCommand(sessionId, msg.command, msg.args)
3328
+ () => manager.execCommand(chatId, msg.command)
3344
3329
  );
3345
3330
  default:
3346
3331
  return send(socket, { type: "error", message: "Unknown message type" });
@@ -3387,15 +3372,15 @@ function registerWsGateway(app, manager) {
3387
3372
  socket.close(1008, "Unauthorized");
3388
3373
  return;
3389
3374
  }
3390
- if (!query.sessionId) {
3391
- send(socket, { type: "error", message: "Missing sessionId" });
3392
- socket.close(1008, "Missing sessionId");
3375
+ if (!query.chatId) {
3376
+ send(socket, { type: "error", message: "Missing chatId" });
3377
+ socket.close(1008, "Missing chatId");
3393
3378
  return;
3394
3379
  }
3395
3380
  handleConnection(
3396
3381
  socket,
3397
3382
  manager,
3398
- query.sessionId,
3383
+ query.chatId,
3399
3384
  Number.parseInt(query.cursor ?? "0", 10) || 0
3400
3385
  );
3401
3386
  });
@@ -3412,9 +3397,8 @@ var init_gateway = __esm({
3412
3397
  });
3413
3398
 
3414
3399
  // server/src/app.ts
3415
- import fs16 from "node:fs";
3400
+ import fs17 from "node:fs";
3416
3401
  import Fastify from "fastify";
3417
- import cors from "@fastify/cors";
3418
3402
  import websocket from "@fastify/websocket";
3419
3403
  import fastifyStatic from "@fastify/static";
3420
3404
  import multipart from "@fastify/multipart";
@@ -3434,7 +3418,6 @@ async function buildApp() {
3434
3418
  });
3435
3419
  const manager = new SessionManager(logger);
3436
3420
  const startedAt = Date.now();
3437
- await app.register(cors, { origin: true });
3438
3421
  await app.register(websocket, {
3439
3422
  options: { maxPayload: 16 * 1024 * 1024 }
3440
3423
  });
@@ -3446,24 +3429,24 @@ async function buildApp() {
3446
3429
  registerModelRoutes(app);
3447
3430
  registerAgentRoutes(app);
3448
3431
  registerFsRoutes(app);
3449
- registerSessionRoutes(app, manager);
3432
+ registerChatRoutes(app, manager);
3450
3433
  registerWorkspaceRoutes(app, manager);
3451
- registerUploadRoutes(app, manager);
3434
+ registerUploadRoutes(app);
3452
3435
  registerWsGateway(app, manager);
3453
- if (fs16.existsSync(config.webDist)) {
3436
+ if (fs17.existsSync(config.webDist)) {
3454
3437
  await app.register(fastifyStatic, {
3455
3438
  root: config.webDist
3456
3439
  });
3457
3440
  app.addHook("onSend", async (req, reply) => {
3458
- const path19 = req.url.split("?")[0] ?? "";
3459
- if (path19 === "/manifest.json" || path19.startsWith("/icons/")) {
3441
+ const path20 = req.url.split("?")[0] ?? "";
3442
+ if (path20 === "/manifest.json" || path20.startsWith("/icons/")) {
3460
3443
  reply.header("cache-control", "public, max-age=300");
3461
3444
  }
3462
3445
  });
3463
3446
  app.setNotFoundHandler((req, reply) => {
3464
- const path19 = req.url.split("?")[0] ?? "";
3465
- const looksLikeAsset = path19.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path19);
3466
- if (path19.startsWith("/api/") || path19.startsWith("/ws") || looksLikeAsset) {
3447
+ const path20 = req.url.split("?")[0] ?? "";
3448
+ const looksLikeAsset = path20.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path20);
3449
+ if (path20.startsWith("/api/") || path20.startsWith("/ws") || looksLikeAsset) {
3467
3450
  reply.code(404).send({ error: "Not found" });
3468
3451
  return;
3469
3452
  }
@@ -3482,7 +3465,7 @@ var init_app = __esm({
3482
3465
  init_models2();
3483
3466
  init_agents2();
3484
3467
  init_fs();
3485
- init_sessions();
3468
+ init_chats2();
3486
3469
  init_workspace();
3487
3470
  init_uploads();
3488
3471
  init_health();
@@ -3537,15 +3520,15 @@ __export(doctor_exports, {
3537
3520
  mcpWiring: () => mcpWiring
3538
3521
  });
3539
3522
  import { execFileSync as execFileSync3 } from "node:child_process";
3540
- import fs17 from "node:fs";
3541
- import path17 from "node:path";
3523
+ import fs18 from "node:fs";
3524
+ import path18 from "node:path";
3542
3525
  function which(bin) {
3543
- if (bin.includes("/")) return fs17.existsSync(bin) ? bin : void 0;
3544
- for (const dir of (process.env.PATH ?? "").split(path17.delimiter)) {
3526
+ if (bin.includes("/")) return fs18.existsSync(bin) ? bin : void 0;
3527
+ for (const dir of (process.env.PATH ?? "").split(path18.delimiter)) {
3545
3528
  if (dir === "") continue;
3546
- const candidate = path17.join(dir, bin);
3529
+ const candidate = path18.join(dir, bin);
3547
3530
  try {
3548
- fs17.accessSync(candidate, fs17.constants.X_OK);
3531
+ fs18.accessSync(candidate, fs18.constants.X_OK);
3549
3532
  return candidate;
3550
3533
  } catch {
3551
3534
  }
@@ -3569,7 +3552,7 @@ function mcpWiring(agentJson, resolve) {
3569
3552
  return { ok: false, detail: `${command || "(no command)"} not found - widgets are unavailable` };
3570
3553
  }
3571
3554
  const script = args.find((a) => a.endsWith(".js"));
3572
- if (script && !fs17.existsSync(script)) {
3555
+ if (script && !fs18.existsSync(script)) {
3573
3556
  return { ok: false, detail: `${script} is gone - delete the agent file and restart to rewrite it` };
3574
3557
  }
3575
3558
  return { ok: true, detail: script ?? `${command} ${args.join(" ")}` };
@@ -3601,7 +3584,7 @@ function checks() {
3601
3584
  const settings = readSettings(config.configFile);
3602
3585
  const hasToken = typeof settings.token === "string" && settings.token !== "";
3603
3586
  out.push(
3604
- fs17.existsSync(config.configFile) ? {
3587
+ fs18.existsSync(config.configFile) ? {
3605
3588
  ok: hasToken,
3606
3589
  name: "settings",
3607
3590
  detail: hasToken ? `${config.configFile} (token set)` : `${config.configFile} exists but has no token - auth is DISABLED`
@@ -3611,8 +3594,8 @@ function checks() {
3611
3594
  detail: `${config.configFile} not found; a token is generated on first run`
3612
3595
  }
3613
3596
  );
3614
- if (fs17.existsSync(config.configFile)) {
3615
- const mode = fs17.statSync(config.configFile).mode & 511;
3597
+ if (fs18.existsSync(config.configFile)) {
3598
+ const mode = fs18.statSync(config.configFile).mode & 511;
3616
3599
  out.push({
3617
3600
  ok: mode === 384,
3618
3601
  name: "settings mode",
@@ -3620,10 +3603,10 @@ function checks() {
3620
3603
  });
3621
3604
  }
3622
3605
  try {
3623
- fs17.mkdirSync(config.casperDataDir, { recursive: true });
3624
- fs17.accessSync(config.casperDataDir, fs17.constants.W_OK);
3625
- const db2 = path17.join(config.casperDataDir, "casper.db");
3626
- const size = fs17.existsSync(db2) ? `, casper.db ${(fs17.statSync(db2).size / 1024).toFixed(0)} KB` : "";
3606
+ fs18.mkdirSync(config.casperDataDir, { recursive: true });
3607
+ fs18.accessSync(config.casperDataDir, fs18.constants.W_OK);
3608
+ const db2 = path18.join(config.casperDataDir, "casper.db");
3609
+ const size = fs18.existsSync(db2) ? `, casper.db ${(fs18.statSync(db2).size / 1024).toFixed(0)} KB` : "";
3627
3610
  out.push({ ok: true, name: "data dir", detail: `${config.casperDataDir} writable${size}` });
3628
3611
  } catch (err) {
3629
3612
  out.push({
@@ -3634,22 +3617,22 @@ function checks() {
3634
3617
  });
3635
3618
  }
3636
3619
  out.push(
3637
- fs17.existsSync(config.webDist) ? { ok: true, name: "web app", detail: config.webDist } : {
3620
+ fs18.existsSync(config.webDist) ? { ok: true, name: "web app", detail: config.webDist } : {
3638
3621
  ok: false,
3639
3622
  fatal: true,
3640
3623
  name: "web app",
3641
3624
  detail: `${config.webDist} not found - the package is incomplete or CASPER_WEB_DIST is wrong`
3642
3625
  }
3643
3626
  );
3644
- const agent = path17.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3645
- const hasAgent = fs17.existsSync(agent);
3627
+ const agent = path18.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3628
+ const hasAgent = fs18.existsSync(agent);
3646
3629
  out.push({
3647
3630
  ok: hasAgent,
3648
3631
  name: "casper agent",
3649
3632
  detail: hasAgent ? agent : `${agent} missing - it is installed on first run`
3650
3633
  });
3651
3634
  if (hasAgent) {
3652
- const wiring = mcpWiring(fs17.readFileSync(agent, "utf8"), which);
3635
+ const wiring = mcpWiring(fs18.readFileSync(agent, "utf8"), which);
3653
3636
  out.push({ ok: wiring.ok, name: "casper mcp", detail: wiring.detail });
3654
3637
  }
3655
3638
  return out;
@@ -4113,14 +4096,14 @@ var init_server2 = __esm({
4113
4096
  "server/src/mcp/server.ts"() {
4114
4097
  "use strict";
4115
4098
  init_protocol();
4116
- VERSION = true ? "0.11.1" : "dev";
4099
+ VERSION = true ? "0.13.0" : "dev";
4117
4100
  }
4118
4101
  });
4119
4102
 
4120
4103
  // server/src/cli/program.ts
4121
- import fs18 from "node:fs";
4104
+ import fs19 from "node:fs";
4122
4105
  import os4 from "node:os";
4123
- import path18 from "node:path";
4106
+ import path19 from "node:path";
4124
4107
 
4125
4108
  // server/node_modules/commander/lib/error.js
4126
4109
  var CommanderError = class extends Error {
@@ -7233,9 +7216,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
7233
7216
  * @param {string} [path]
7234
7217
  * @return {(string|null|Command)}
7235
7218
  */
7236
- executableDir(path19) {
7237
- if (path19 === void 0) return this._executableDir;
7238
- this._executableDir = path19;
7219
+ executableDir(path20) {
7220
+ if (path20 === void 0) return this._executableDir;
7221
+ this._executableDir = path20;
7239
7222
  return this;
7240
7223
  }
7241
7224
  /**
@@ -7493,7 +7476,7 @@ var program = new Command();
7493
7476
  // server/src/cli/program.ts
7494
7477
  init_paths();
7495
7478
  init_settings();
7496
- var VERSION2 = true ? "0.11.1" : "dev";
7479
+ var VERSION2 = true ? "0.13.0" : "dev";
7497
7480
  async function bootstrap() {
7498
7481
  const file = configFilePath();
7499
7482
  const settings = readSettings(file);
@@ -7539,8 +7522,8 @@ async function resetToken(value) {
7539
7522
  updateSettings(file, { token });
7540
7523
  process.stdout.write(`wrote a new token to ${file}
7541
7524
  `);
7542
- const dbFile = path18.join(dataDirPath(), "casper.db");
7543
- if (fs18.existsSync(dbFile)) {
7525
+ const dbFile = path19.join(dataDirPath(), "casper.db");
7526
+ if (fs19.existsSync(dbFile)) {
7544
7527
  const { LoginStore: LoginStore2 } = await Promise.resolve().then(() => (init_logins(), logins_exports));
7545
7528
  new LoginStore2().revokeAll();
7546
7529
  process.stdout.write("revoked all device sessions\n");
@@ -7557,26 +7540,26 @@ async function resetToken(value) {
7557
7540
  }
7558
7541
  function buildProgram() {
7559
7542
  const program2 = new Command();
7560
- program2.name("casper").description("Web client for kiro-cli over the Agent Client Protocol").version(VERSION2, "-v, --version").showHelpAfterError().allowUnknownOption(false).allowExcessArguments(false).addHelpText("after", `
7543
+ program2.name("casper").description("Web client for kiro-cli over the Agent Client Protocol").version(VERSION2, "-v, --version").showHelpAfterError().addHelpText("after", `
7561
7544
  Settings live in ${configFilePath()}.
7562
7545
  Update with: npm install -g @joeyshi12/casper`);
7563
- program2.command("start").description("run the server in the foreground").allowUnknownOption(false).allowExcessArguments(false).action(async () => {
7546
+ program2.command("start").description("run the server in the foreground").action(async () => {
7564
7547
  await bootstrap();
7565
7548
  const { serve: serve2 } = await Promise.resolve().then(() => (init_server(), server_exports));
7566
7549
  await serve2();
7567
7550
  });
7568
- program2.command("token").description("print the access token").allowUnknownOption(false).allowExcessArguments(false).action(printToken);
7569
- program2.command("reset-token").argument("[value]", "token to set; generated when omitted").description("set a new token and sign every device out").allowUnknownOption(false).allowExcessArguments(false).action(async (value) => {
7551
+ program2.command("token").description("print the access token").action(printToken);
7552
+ program2.command("reset-token").argument("[value]", "token to set; generated when omitted").description("set a new token and sign every device out").action(async (value) => {
7570
7553
  await resetToken(value);
7571
7554
  });
7572
- program2.command("doctor").description("check kiro-cli, settings, data directory and web app").allowUnknownOption(false).allowExcessArguments(false).action(async () => {
7555
+ program2.command("doctor").description("check kiro-cli, settings, data directory and web app").action(async () => {
7573
7556
  process.exitCode = (await Promise.resolve().then(() => (init_doctor(), doctor_exports))).doctor();
7574
7557
  });
7575
- program2.command("mcp").description("run the generative-UI MCP server on stdio (kiro spawns this itself)").allowUnknownOption(false).allowExcessArguments(false).action(async () => {
7558
+ program2.command("mcp").description("run the generative-UI MCP server on stdio (kiro spawns this itself)").action(async () => {
7576
7559
  (await Promise.resolve().then(() => (init_server2(), server_exports2))).runMcpServer();
7577
7560
  });
7578
7561
  const service = program2.command("service").description("manage the systemd user service");
7579
- const svcCommand = (name, describe, run) => service.command(name).description(describe).allowUnknownOption(false).allowExcessArguments(false).action(async () => {
7562
+ const svcCommand = (name, describe, run) => service.command(name).description(describe).action(async () => {
7580
7563
  process.exitCode = run(await Promise.resolve().then(() => (init_service(), service_exports)));
7581
7564
  });
7582
7565
  svcCommand("install", "run Casper as a systemd user service", (m) => m.serviceInstall());