@joeyshi12/casper 0.11.1 → 0.12.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 +456 -465
  5. package/dist/web/assets/{abnfDiagram-N423BO3Z-B88fK1c8.js → abnfDiagram-N423BO3Z-4Ko3ozfJ.js} +1 -1
  6. package/dist/web/assets/{arc-DMIevERD.js → arc-Ch7Ehjbn.js} +1 -1
  7. package/dist/web/assets/{architectureDiagram-T3A2C74G-CJ5mXlmv.js → architectureDiagram-T3A2C74G-DcWU5yRD.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-VBNYF7ZC-BssSdi9Q.js → blockDiagram-VBNYF7ZC-BUzWG1_k.js} +1 -1
  9. package/dist/web/assets/{c4Diagram-5PPSVZJV-BJHrRcC9.js → c4Diagram-5PPSVZJV-Cbk3RgdU.js} +1 -1
  10. package/dist/web/assets/channel-DSuKNwf1.js +1 -0
  11. package/dist/web/assets/{chunk-2GRJ4B5K-Bmq_IVc0.js → chunk-2GRJ4B5K-B0WXdW3m.js} +1 -1
  12. package/dist/web/assets/{chunk-2Q5K7J3B-B2erbhym.js → chunk-2Q5K7J3B-Dr5AcgBH.js} +1 -1
  13. package/dist/web/assets/{chunk-5RXB4S5H-B_DuY9SH.js → chunk-5RXB4S5H-NRZvLElM.js} +1 -1
  14. package/dist/web/assets/{chunk-5VM5RSS4-BgiHQCU1.js → chunk-5VM5RSS4-B2ezMICZ.js} +1 -1
  15. package/dist/web/assets/{chunk-6Q2QTUOP-pmOmsjWC.js → chunk-6Q2QTUOP-0_kTL1pm.js} +1 -1
  16. package/dist/web/assets/{chunk-GF5L2VYU-B-PjTMWL.js → chunk-GF5L2VYU-ChJJ-3FT.js} +1 -1
  17. package/dist/web/assets/{chunk-JWPE2WC7-Be9UnGmk.js → chunk-JWPE2WC7-B15-wRYl.js} +1 -1
  18. package/dist/web/assets/{chunk-KBJHAD2P-DPxdXs9r.js → chunk-KBJHAD2P-DsR7v9qB.js} +1 -1
  19. package/dist/web/assets/{chunk-RYQCIY6F-BKc5gOjQ.js → chunk-RYQCIY6F-BkT8T354.js} +1 -1
  20. package/dist/web/assets/{chunk-XXDRQBXY-CoOnbv-S.js → chunk-XXDRQBXY-CRB4wwou.js} +1 -1
  21. package/dist/web/assets/classDiagram-JCYQIIEL-DY_Arw9F.js +1 -0
  22. package/dist/web/assets/classDiagram-v2-OCEON4UE-DY_Arw9F.js +1 -0
  23. package/dist/web/assets/{cose-bilkent-JH36ORCC-BZ5Oon1z.js → cose-bilkent-JH36ORCC-BESlNRq3.js} +1 -1
  24. package/dist/web/assets/{cynefin-VYW2F7L2-DlXmf5Bs.js → cynefin-VYW2F7L2-BPfoy8I9.js} +1 -1
  25. package/dist/web/assets/{cynefinDiagram-MW4NZA55-ClZQ1oTx.js → cynefinDiagram-MW4NZA55-Byn6JJg0.js} +1 -1
  26. package/dist/web/assets/{dagre-VZM6K2ZE-CX3pDtYQ.js → dagre-VZM6K2ZE-CF6UO64P.js} +1 -1
  27. package/dist/web/assets/{diagram-7IWD3JNH-DBelWFkJ.js → diagram-7IWD3JNH-DgIzI-sU.js} +1 -1
  28. package/dist/web/assets/{diagram-B4RE2ZJO-CDqvztqK.js → diagram-B4RE2ZJO-DTXfP5om.js} +1 -1
  29. package/dist/web/assets/{diagram-LBJQPF4R-DWL1uAJS.js → diagram-LBJQPF4R-CTLNzser.js} +1 -1
  30. package/dist/web/assets/{diagram-Q27KOJAE-BRsHqM1Z.js → diagram-Q27KOJAE-M38db-G2.js} +1 -1
  31. package/dist/web/assets/{diagram-UB23O5K3-Ci1U9Hdl.js → diagram-UB23O5K3-CG-vZyFB.js} +1 -1
  32. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-CBPphElN.js → ebnfDiagram-BXEA7PRR-CsJ9VNb2.js} +1 -1
  33. package/dist/web/assets/{erDiagram-JOGREHBK-FI4byJC2.js → erDiagram-JOGREHBK-3dK0h9cf.js} +1 -1
  34. package/dist/web/assets/{flowDiagram-UKHOOZJN-BCKR_o2t.js → flowDiagram-UKHOOZJN-DHTGEHfr.js} +1 -1
  35. package/dist/web/assets/{ganttDiagram-PKOTCBZU-BYp35VGe.js → ganttDiagram-PKOTCBZU-DiLysF1Q.js} +1 -1
  36. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N--AySchaL.js → gitGraphDiagram-DS77QQ5N-2uthJlH6.js} +1 -1
  37. package/dist/web/assets/index-D3zBm-7r.js +249 -0
  38. package/dist/web/assets/index-D_ANHT9G.css +1 -0
  39. package/dist/web/assets/{index-D9ooBx1d.js → index-DbaSy_a8.js} +1 -1
  40. package/dist/web/assets/{infoDiagram-6WML65LV-CbKYvHyN.js → infoDiagram-6WML65LV-B35ot4_O.js} +1 -1
  41. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-dLs0tgi6.js → ishikawaDiagram-WSZJBQD7-C6nTJ8A6.js} +1 -1
  42. package/dist/web/assets/{journeyDiagram-NVQOT4AX-CXUsDb3x.js → journeyDiagram-NVQOT4AX-Bmt5Dt3Z.js} +1 -1
  43. package/dist/web/assets/{kanban-definition-27J2QSJJ-r33P8KpU.js → kanban-definition-27J2QSJJ-DkGk9WrO.js} +1 -1
  44. package/dist/web/assets/{linear-s7N3jDHb.js → linear-D9RsKviS.js} +1 -1
  45. package/dist/web/assets/{mermaid.core-Cw282Xmk.js → mermaid.core-BkEkrWDL.js} +4 -4
  46. package/dist/web/assets/{mindmap-definition-FAOFIHXS-BXPAvM6U.js → mindmap-definition-FAOFIHXS-7XkMFJ0C.js} +1 -1
  47. package/dist/web/assets/{pegDiagram-VL7TDLO6-D-H5hjvM.js → pegDiagram-VL7TDLO6-KpO4Zp9v.js} +1 -1
  48. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-D3W5_BlG.js → pieDiagram-7S7Q4E2Y-CnZkxeeK.js} +1 -1
  49. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-BNSHhxkB.js → quadrantDiagram-CIZ2JOQS-Jej_KG8V.js} +1 -1
  50. package/dist/web/assets/{railroadDiagram-AXF67PYL-BvLjuIqc.js → railroadDiagram-AXF67PYL-BfteVM0C.js} +1 -1
  51. package/dist/web/assets/{requirementDiagram-LRYGKXZP-CZTpqoEk.js → requirementDiagram-LRYGKXZP-BTSeDpTM.js} +1 -1
  52. package/dist/web/assets/{sankeyDiagram-W5VNT64P-xvU-3Mf_.js → sankeyDiagram-W5VNT64P-CeB7UW0v.js} +1 -1
  53. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-Bpugvm4V.js → sequenceDiagram-SI44F4Z6-BeOLM9UY.js} +1 -1
  54. package/dist/web/assets/{sizeCapture-X5ZJPWSS-B87TxLF7.js → sizeCapture-X5ZJPWSS-7aezJBJp.js} +1 -1
  55. package/dist/web/assets/{stateDiagram-OKZ733FA-BEQB_m5r.js → stateDiagram-OKZ733FA-DmJkX_vQ.js} +1 -1
  56. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-CkEEUkE9.js +1 -0
  57. package/dist/web/assets/{swimlanes-SLNWSIFB-CVMCMeYR.js → swimlanes-SLNWSIFB-CmnhwW2u.js} +2 -2
  58. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-DaOmpb-r.js +8 -0
  59. package/dist/web/assets/{timeline-definition-Z64GVDOM-Dylz4pg-.js → timeline-definition-Z64GVDOM-DtxMhUPD.js} +1 -1
  60. package/dist/web/assets/{vennDiagram-T6HMQDX7-BmS_JQjJ.js → vennDiagram-T6HMQDX7-DO34MvMM.js} +1 -1
  61. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-C0qCz7i-.js → wardleyDiagram-T6FBY63Y-s2zhjdd1.js} +1 -1
  62. package/dist/web/assets/{xychartDiagram-ELKLHX3M-aOG1xxkv.js → xychartDiagram-ELKLHX3M-CnCQs0NK.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.
@@ -386,7 +394,10 @@ var init_db = __esm({
386
394
  CREATE TABLE IF NOT EXISTS sessions (
387
395
  session_id TEXT PRIMARY KEY,
388
396
  title TEXT,
389
- cwd TEXT
397
+ cwd TEXT,
398
+ -- The chat that owns this session's uploads (see chats.ts). NULL for a session Casper did
399
+ -- not create; getChatId names those after the session.
400
+ chat_id TEXT
390
401
  );
391
402
  CREATE TABLE IF NOT EXISTS logins (
392
403
  id TEXT PRIMARY KEY,
@@ -396,6 +407,15 @@ CREATE TABLE IF NOT EXISTS logins (
396
407
  user_agent TEXT
397
408
  );
398
409
  CREATE INDEX IF NOT EXISTS logins_hash ON logins (hash);
410
+ CREATE TABLE IF NOT EXISTS message_attachments (
411
+ session_id TEXT NOT NULL,
412
+ ordinal INTEGER NOT NULL,
413
+ path TEXT NOT NULL,
414
+ name TEXT NOT NULL,
415
+ size INTEGER NOT NULL,
416
+ kind TEXT NOT NULL,
417
+ PRIMARY KEY (session_id, ordinal, path)
418
+ );
399
419
  `;
400
420
  }
401
421
  });
@@ -405,10 +425,7 @@ var logins_exports = {};
405
425
  __export(logins_exports, {
406
426
  LoginStore: () => LoginStore
407
427
  });
408
- import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
409
- function sha2562(s) {
410
- return createHash("sha256").update(s).digest("hex");
411
- }
428
+ import { randomBytes, timingSafeEqual } from "node:crypto";
412
429
  function sameHash(a, b) {
413
430
  if (a.length !== b.length) return false;
414
431
  return timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
@@ -419,6 +436,7 @@ var init_logins = __esm({
419
436
  "use strict";
420
437
  init_config();
421
438
  init_db();
439
+ init_hash();
422
440
  LAST_SEEN_WRITE_INTERVAL_MS = 6e4;
423
441
  str = (v) => typeof v === "string" ? v : "";
424
442
  toRecord = (r) => ({
@@ -446,7 +464,7 @@ var init_logins = __esm({
446
464
  const nowIso = (/* @__PURE__ */ new Date()).toISOString();
447
465
  const record = {
448
466
  id: randomBytes(9).toString("base64url"),
449
- hash: sha2562(token),
467
+ hash: sha256(token),
450
468
  createdAt: nowIso,
451
469
  lastSeenAt: nowIso,
452
470
  userAgent
@@ -464,7 +482,7 @@ var init_logins = __esm({
464
482
  verify(token) {
465
483
  if (!token) return null;
466
484
  const now = Date.now();
467
- const row = db().prepare("SELECT * FROM logins WHERE hash = ?").get(sha2562(token));
485
+ const row = db().prepare("SELECT * FROM logins WHERE hash = ?").get(sha256(token));
468
486
  if (!row) return null;
469
487
  const record = toRecord(row);
470
488
  if (now - Date.parse(record.lastSeenAt) > this.ttlMs()) {
@@ -480,7 +498,7 @@ var init_logins = __esm({
480
498
  /** List all active devices, marking the one owning `currentToken`. */
481
499
  list(currentToken) {
482
500
  this.pruneExpired();
483
- const currentHash = currentToken ? sha2562(currentToken) : void 0;
501
+ const currentHash = currentToken ? sha256(currentToken) : void 0;
484
502
  const rows = db().prepare("SELECT * FROM logins ORDER BY last_seen_at DESC").all();
485
503
  return rows.map(toRecord).map((r) => ({
486
504
  id: r.id,
@@ -493,7 +511,7 @@ var init_logins = __esm({
493
511
  /** Revoke the device holding this token (used on logout). */
494
512
  revokeToken(token) {
495
513
  if (!token) return;
496
- db().prepare("DELETE FROM logins WHERE hash = ?").run(sha2562(token));
514
+ db().prepare("DELETE FROM logins WHERE hash = ?").run(sha256(token));
497
515
  }
498
516
  /** Revoke a device by its public id. Returns false if there was no such device. */
499
517
  revokeId(id) {
@@ -660,45 +678,20 @@ var init_acp = __esm({
660
678
  });
661
679
 
662
680
  // 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
681
  function stripAttachmentsLine(text2) {
676
682
  return text2.split("\n").filter((l) => !l.startsWith(ATTACHMENTS_PREFIX)).join("\n").trim();
677
683
  }
678
- var ATTACHMENTS_PREFIX, IMAGE_EXTS;
684
+ var ATTACHMENTS_PREFIX;
679
685
  var init_attachments = __esm({
680
686
  "shared/dist/attachments.js"() {
681
687
  "use strict";
682
688
  ATTACHMENTS_PREFIX = "Attached files: ";
683
- IMAGE_EXTS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico", ".avif"];
684
689
  }
685
690
  });
686
691
 
687
692
  // shared/dist/observability.js
688
693
  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
- };
694
+ return { turnStatus: "idle", contextUsagePercentage: 0, compacting: false };
702
695
  }
703
696
  var init_observability = __esm({
704
697
  "shared/dist/observability.js"() {
@@ -806,27 +799,32 @@ var init_agents = __esm({
806
799
  }
807
800
  });
808
801
 
809
- // server/src/session/workspaces.ts
810
- import crypto3 from "node:crypto";
802
+ // server/src/session/chats.ts
811
803
  import fs7 from "node:fs";
812
804
  import path8 from "node:path";
813
- function workspacesRoot() {
814
- return path8.join(config.casperDataDir, "workspaces");
805
+ function chatsRoot() {
806
+ return path8.join(config.casperDataDir, "chats");
815
807
  }
816
- function workspaceDir(workspaceId) {
817
- return path8.join(workspacesRoot(), workspaceId);
808
+ function chatDir(chatId) {
809
+ return path8.join(chatsRoot(), chatId);
818
810
  }
819
- function createWorkspace() {
820
- const id = crypto3.randomUUID();
821
- const dir = workspaceDir(id);
811
+ function chatUploadsDir(chatId) {
812
+ return path8.join(chatDir(chatId), "uploads");
813
+ }
814
+ function chatWorkspaceDir(chatId) {
815
+ return path8.join(chatDir(chatId), "workspace");
816
+ }
817
+ function createChatWorkspace(chatId) {
818
+ const dir = chatWorkspaceDir(chatId);
822
819
  fs7.mkdirSync(dir, { recursive: true, mode: 448 });
823
- return { id, dir };
820
+ return dir;
824
821
  }
825
- function isWorkspacePath(dir) {
826
- return dir === workspacesRoot() || dir.startsWith(workspacesRoot() + path8.sep);
822
+ function isManagedWorkspace(dir) {
823
+ const root = chatsRoot();
824
+ return dir === root || dir.startsWith(root + path8.sep);
827
825
  }
828
- var init_workspaces = __esm({
829
- "server/src/session/workspaces.ts"() {
826
+ var init_chats = __esm({
827
+ "server/src/session/chats.ts"() {
830
828
  "use strict";
831
829
  init_config();
832
830
  }
@@ -868,6 +866,9 @@ async function realConfineToRoot(root, absPath) {
868
866
  function isValidSessionId(id) {
869
867
  return /^[A-Za-z0-9._-]+$/.test(id) && id !== "." && !id.includes("..");
870
868
  }
869
+ function isValidChatId(id) {
870
+ return !!id && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id);
871
+ }
871
872
  var realRootCache;
872
873
  var init_paths2 = __esm({
873
874
  "server/src/util/paths.ts"() {
@@ -1083,9 +1084,9 @@ var init_KiroProcess = __esm({
1083
1084
  this.child.stdout.on("error", () => {
1084
1085
  });
1085
1086
  }
1086
- /** Perform the ACP initialize handshake. Idempotent. */
1087
+ /** Perform the ACP initialize handshake. kiro's reply is not used. */
1087
1088
  async initialize() {
1088
- const result = await this.client.request(ACP_METHODS.initialize, {
1089
+ await this.client.request(ACP_METHODS.initialize, {
1089
1090
  protocolVersion: 1,
1090
1091
  clientCapabilities: {
1091
1092
  fs: { readTextFile: false, writeTextFile: false },
@@ -1095,7 +1096,6 @@ var init_KiroProcess = __esm({
1095
1096
  // worth wiring to package.json.
1096
1097
  clientInfo: { name: "casper", version: "0.5.0" }
1097
1098
  });
1098
- return result;
1099
1099
  }
1100
1100
  newSession(params) {
1101
1101
  return this.client.request(ACP_METHODS.sessionNew, params);
@@ -1120,10 +1120,10 @@ var init_KiroProcess = __esm({
1120
1120
  setModel(sessionId, modelId) {
1121
1121
  return this.client.request(ACP_METHODS.sessionSetModel, { sessionId, modelId });
1122
1122
  }
1123
- execCommand(sessionId, command, args) {
1123
+ execCommand(sessionId, command) {
1124
1124
  return this.client.request(ACP_METHODS.commandsExecute, {
1125
1125
  sessionId,
1126
- command: { command: command.replace(/^\//, ""), args: args ? { input: args } : {} }
1126
+ command: { command: command.replace(/^\//, ""), args: {} }
1127
1127
  });
1128
1128
  }
1129
1129
  /** Cleanly shut down: close stdin (triggers kiro's graceful exit), then kill. */
@@ -1255,83 +1255,36 @@ var init_TurnState = __esm({
1255
1255
  get() {
1256
1256
  return this.snapshot;
1257
1257
  }
1258
- /** Apply one event; returns the updated snapshot. */
1259
1258
  apply(payload) {
1260
1259
  const s = this.snapshot;
1261
1260
  switch (payload.kind) {
1262
- case "turn_started": {
1263
- this.snapshot = { ...s, turnStatus: "running", currentTurnStartedAt: Date.now() };
1261
+ case "turn_started":
1262
+ this.snapshot = { ...s, turnStatus: "running" };
1264
1263
  break;
1265
- }
1266
- case "turn_ended": {
1267
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1264
+ case "turn_ended":
1265
+ case "turn_error":
1266
+ this.snapshot = { ...s, turnStatus: "idle" };
1268
1267
  break;
1269
- }
1270
- case "turn_error": {
1271
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1272
- 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
- };
1268
+ case "process_exited":
1269
+ this.snapshot = { ...s, turnStatus: "idle", compacting: false };
1289
1270
  break;
1290
- }
1291
- case "subagent_update": {
1271
+ case "metadata":
1292
1272
  this.snapshot = {
1293
1273
  ...s,
1294
- subagents: payload.params.subagents,
1295
- pendingStages: payload.params.pendingStages
1274
+ contextUsagePercentage: payload.params.contextUsagePercentage ?? s.contextUsagePercentage
1296
1275
  };
1297
1276
  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": {
1277
+ case "compaction":
1312
1278
  this.snapshot = { ...s, compacting: payload.params.status.type === "started" };
1313
1279
  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
1280
  default:
1328
1281
  break;
1329
1282
  }
1330
1283
  return this.snapshot;
1331
1284
  }
1332
- /** Seed cumulative credits/context from kiro's persisted metadata on resume. */
1333
- seed(creditsSpent, contextUsagePercentage) {
1334
- this.snapshot = { ...this.snapshot, creditsSpent, contextUsagePercentage };
1285
+ /** Seed context usage from kiro's persisted metadata on resume. */
1286
+ seed(contextUsagePercentage) {
1287
+ this.snapshot = { ...this.snapshot, contextUsagePercentage };
1335
1288
  }
1336
1289
  };
1337
1290
  }
@@ -1369,13 +1322,12 @@ function renderableBlocks(content) {
1369
1322
  function summarize(j) {
1370
1323
  const state = j.session_state;
1371
1324
  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
1325
  const contextUsagePercentage = state?.rts_model_state?.context_usage_percentage ?? turns[turns.length - 1]?.context_usage_percentage;
1377
1326
  return {
1378
1327
  sessionId: j.session_id,
1328
+ // kiro's file knows nothing about chats; summaryOf replaces this with the recorded id
1329
+ // when there is one, and this matches what getChatId names a session without a row.
1330
+ chatId: j.session_id,
1379
1331
  // Left empty when kiro has not named it; resolveSessionTitle decides what to show.
1380
1332
  title: j.title?.trim() ?? "",
1381
1333
  cwd: j.cwd,
@@ -1385,7 +1337,6 @@ function summarize(j) {
1385
1337
  agentId: state?.agent_name,
1386
1338
  modelId: state?.rts_model_state?.model_info?.model_id,
1387
1339
  running: false,
1388
- creditsSpent,
1389
1340
  contextUsagePercentage
1390
1341
  };
1391
1342
  }
@@ -1417,6 +1368,7 @@ async function deletePersistedSession(sessionId) {
1417
1368
  path10.join(config.kiroSessionsDir, `${sessionId}.json`),
1418
1369
  path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
1419
1370
  path10.join(config.kiroSessionsDir, `${sessionId}.history`),
1371
+ path10.join(config.kiroSessionsDir, `${sessionId}.lock`),
1420
1372
  path10.join(config.kiroSessionsDir, sessionId)
1421
1373
  ];
1422
1374
  await Promise.all(targets.map((f) => fs9.rm(f, { recursive: true, force: true })));
@@ -1433,48 +1385,54 @@ async function readPersistedSession(sessionId) {
1433
1385
  return null;
1434
1386
  }
1435
1387
  }
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
- }
1388
+ async function promptCount(sessionId) {
1389
+ const entries = await readJsonlEntries(sessionId);
1390
+ return entries.filter((e) => e.kind === "Prompt").length;
1444
1391
  }
1445
- async function hydrateTranscript(sessionId) {
1392
+ async function readJsonlEntries(sessionId) {
1446
1393
  if (!isValidSessionId(sessionId)) return [];
1447
1394
  let raw;
1448
1395
  try {
1449
- raw = await fs9.readFile(
1450
- path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
1451
- "utf8"
1452
- );
1396
+ raw = await fs9.readFile(path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`), "utf8");
1453
1397
  } catch {
1454
1398
  return [];
1455
1399
  }
1456
- const items = [];
1457
- const toolsById = /* @__PURE__ */ new Map();
1400
+ const entries = [];
1458
1401
  for (const line of raw.split("\n")) {
1459
1402
  const trimmed = line.trim();
1460
1403
  if (!trimmed) continue;
1461
- let entry;
1462
1404
  try {
1463
- entry = JSON.parse(trimmed);
1405
+ entries.push(JSON.parse(trimmed));
1464
1406
  } catch {
1465
- continue;
1466
1407
  }
1408
+ }
1409
+ return entries;
1410
+ }
1411
+ async function hasRecordedTurns(sessionId) {
1412
+ if (!isValidSessionId(sessionId)) return false;
1413
+ try {
1414
+ const st = await fs9.stat(path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`));
1415
+ return st.size > 0;
1416
+ } catch {
1417
+ return false;
1418
+ }
1419
+ }
1420
+ async function hydrateTranscript(sessionId, attachments) {
1421
+ const items = [];
1422
+ let userOrdinal = 0;
1423
+ const toolsById = /* @__PURE__ */ new Map();
1424
+ for (const entry of await readJsonlEntries(sessionId)) {
1467
1425
  const content = contentBlocks(entry.data.content);
1468
1426
  const textOf = (kind) => content.filter((c) => c.kind === kind).map(blockText).join("");
1469
1427
  const baseId = entry.data.message_id ?? `${items.length}`;
1470
1428
  const ts = entry.data.meta?.timestamp;
1471
1429
  const pushMsg = (msg) => items.push({ type: "message", message: msg });
1472
1430
  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 });
1431
+ const text2 = stripAttachmentsLine(textOf("text"));
1432
+ const attached = attachments?.get(userOrdinal);
1433
+ userOrdinal++;
1434
+ if (text2.trim() || attached?.length)
1435
+ pushMsg({ id: `u-${baseId}`, role: "user", text: text2, timestamp: ts, attachments: attached });
1478
1436
  } else if (entry.kind === "AssistantMessage") {
1479
1437
  const thinking = textOf("thinking");
1480
1438
  if (thinking.trim())
@@ -1543,9 +1501,52 @@ var init_sessionStore = __esm({
1543
1501
  setCwd(sessionId, cwd) {
1544
1502
  this.write(sessionId, "cwd", cwd);
1545
1503
  }
1504
+ /**
1505
+ * The chat that owns this session's uploads. A session Casper did not create has no row -
1506
+ * one started with kiro-cli directly still shows up in the list - so it is named after the
1507
+ * session, which gives it one stable directory rather than a new one per call.
1508
+ */
1509
+ getChatId(sessionId) {
1510
+ return this.read(sessionId, "chat_id") ?? sessionId;
1511
+ }
1512
+ setChatId(sessionId, chatId) {
1513
+ this.write(sessionId, "chat_id", chatId);
1514
+ }
1515
+ /** Record what was attached to one prompt. See db.ts for why the key is an ordinal. */
1516
+ setAttachments(sessionId, ordinal, files) {
1517
+ if (files.length === 0) return;
1518
+ const stmt = db().prepare(
1519
+ `INSERT INTO message_attachments (session_id, ordinal, path, name, size, kind)
1520
+ VALUES (?, ?, ?, ?, ?, ?)
1521
+ ON CONFLICT(session_id, ordinal, path) DO NOTHING`
1522
+ );
1523
+ for (const f of files) {
1524
+ stmt.run(sessionId, ordinal, f.path, f.name, f.size, f.kind);
1525
+ }
1526
+ }
1527
+ /** Every attachment in a session, grouped by the message it belongs to. */
1528
+ attachmentsBySession(sessionId) {
1529
+ const rows = db().prepare(
1530
+ `SELECT ordinal, path, name, size, kind FROM message_attachments
1531
+ WHERE session_id = ? ORDER BY ordinal, path`
1532
+ ).all(sessionId);
1533
+ const out = /* @__PURE__ */ new Map();
1534
+ for (const r of rows) {
1535
+ const list = out.get(r.ordinal) ?? [];
1536
+ list.push({
1537
+ path: r.path,
1538
+ name: r.name,
1539
+ size: r.size,
1540
+ kind: r.kind
1541
+ });
1542
+ out.set(r.ordinal, list);
1543
+ }
1544
+ return out;
1545
+ }
1546
1546
  /** Forget every override for a session, when it's permanently deleted. */
1547
1547
  remove(sessionId) {
1548
1548
  db().prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
1549
+ db().prepare("DELETE FROM message_attachments WHERE session_id = ?").run(sessionId);
1549
1550
  }
1550
1551
  read(sessionId, column) {
1551
1552
  const row = db().prepare(`SELECT ${column} AS value FROM sessions WHERE session_id = ?`).get(sessionId);
@@ -1590,18 +1591,8 @@ function mapNotification(n) {
1590
1591
  return { kind: "session_update", update: n.params.update };
1591
1592
  case KIRO_NOTIFICATIONS.metadata:
1592
1593
  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
1594
  case KIRO_NOTIFICATIONS.compactionStatus:
1602
1595
  return { kind: "compaction", params: n.params };
1603
- case KIRO_NOTIFICATIONS.mcpOauthRequest:
1604
- return { kind: "oauth_request", params: n.params };
1605
1596
  default:
1606
1597
  return null;
1607
1598
  }
@@ -1623,7 +1614,7 @@ var init_SessionManager = __esm({
1623
1614
  init_dist();
1624
1615
  init_config();
1625
1616
  init_agents();
1626
- init_workspaces();
1617
+ init_chats();
1627
1618
  init_paths2();
1628
1619
  init_KiroProcess();
1629
1620
  init_EventStore();
@@ -1635,6 +1626,8 @@ var init_SessionManager = __esm({
1635
1626
  store;
1636
1627
  turnState = new TurnState();
1637
1628
  cwd;
1629
+ /** The chat that owns this session's uploads. See chats.ts. */
1630
+ chatId;
1638
1631
  agentId;
1639
1632
  modelId;
1640
1633
  currentModeId;
@@ -1703,7 +1696,7 @@ var init_SessionManager = __esm({
1703
1696
  override: this.store.getTitle(sessionId),
1704
1697
  kiroTitle: parts.kiroTitle,
1705
1698
  firstPrompt: parts.firstPrompt,
1706
- folder: isWorkspacePath(parts.cwd) ? void 0 : path11.basename(parts.cwd)
1699
+ folder: isManagedWorkspace(parts.cwd) ? void 0 : path11.basename(parts.cwd)
1707
1700
  });
1708
1701
  }
1709
1702
  /** Set a user title override for a session. */
@@ -1714,9 +1707,7 @@ var init_SessionManager = __esm({
1714
1707
  if (s) s.title = clean || s.title;
1715
1708
  }
1716
1709
  /**
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
1710
+ * Re-point a session at a different working directory. Any live process was spawned with the
1720
1711
  * old cwd, so it is disposed and the next turn respawns in the new one, transcript intact.
1721
1712
  */
1722
1713
  async setSessionCwd(sessionId, input) {
@@ -1732,28 +1723,22 @@ var init_SessionManager = __esm({
1732
1723
  return resolved;
1733
1724
  }
1734
1725
  /**
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.
1726
+ * Restart the session's kiro child, so what kiro reads only at startup is read again: the
1727
+ * agent definition, the workspace's `.kiro`, and the MCP servers kiro launches itself.
1728
+ * Replacing the process is the only way - no ACP method does it, and `session/set_mode`
1729
+ * leaves those MCP servers alone.
1738
1730
  *
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`.
1742
- *
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.
1731
+ * The old child must be awaited out first: kiro flushes its session file on shutdown and
1732
+ * the replacement reads that file to reload the conversation.
1751
1733
  */
1752
1734
  async reloadSession(sessionId) {
1753
1735
  const s = await this.ensureOpen(sessionId);
1754
1736
  if (s.running) {
1755
1737
  throw new Error("Cannot reload while a turn is running");
1756
1738
  }
1739
+ if (s.turnState.get().compacting) {
1740
+ throw new Error("Cannot reload while the conversation is being compacted");
1741
+ }
1757
1742
  if (s.reloading) {
1758
1743
  throw new Error("A reload is already running for this session");
1759
1744
  }
@@ -1840,10 +1825,7 @@ var init_SessionManager = __esm({
1840
1825
  s.createdAt = persisted.createdAt;
1841
1826
  s.updatedAt = persisted.updatedAt;
1842
1827
  s.markLive();
1843
- s.turnState.seed(
1844
- persisted.creditsSpent ?? 0,
1845
- persisted.contextUsagePercentage ?? 0
1846
- );
1828
+ s.turnState.seed(persisted.contextUsagePercentage ?? 0);
1847
1829
  this.sessions.set(sessionId, s);
1848
1830
  return s;
1849
1831
  }
@@ -1931,7 +1913,8 @@ var init_SessionManager = __esm({
1931
1913
  // -------------------------------------------------------------------------
1932
1914
  /** Create a new session. Spawns immediately so we get a real kiro sessionId. */
1933
1915
  async createSession(opts) {
1934
- const cwd = opts.freshWorkspace ? createWorkspace().dir : resolveCwd(opts.cwd);
1916
+ const chatId = isValidChatId(opts.chatId) ? opts.chatId : crypto.randomUUID();
1917
+ const cwd = opts.freshWorkspace ? createChatWorkspace(chatId) : resolveCwd(opts.cwd);
1935
1918
  const tempId = `pending-${Date.now()}-${Math.floor(this.sessions.size)}`;
1936
1919
  const store = new EventStore(tempId);
1937
1920
  const s = new Session(tempId, store, cwd);
@@ -1945,6 +1928,8 @@ var init_SessionManager = __esm({
1945
1928
  this.evict(s.sessionId);
1946
1929
  throw err;
1947
1930
  }
1931
+ this.store.setChatId(s.sessionId, chatId);
1932
+ s.chatId = chatId;
1948
1933
  const name = sanitizeTitle(opts.title ?? "") || (opts.freshWorkspace ? "" : path11.basename(s.cwd));
1949
1934
  if (name) {
1950
1935
  this.store.setTitle(s.sessionId, name);
@@ -1955,7 +1940,7 @@ var init_SessionManager = __esm({
1955
1940
  // -------------------------------------------------------------------------
1956
1941
  // Actions - these spawn the process lazily.
1957
1942
  // -------------------------------------------------------------------------
1958
- async runPrompt(sessionId, content) {
1943
+ async runPrompt(sessionId, content, attachments) {
1959
1944
  const s = await this.ensureOpen(sessionId);
1960
1945
  await this.settleReload(s);
1961
1946
  if (s.running) throw new Error("A turn is already running for this session");
@@ -1975,7 +1960,13 @@ var init_SessionManager = __esm({
1975
1960
  s.title = title;
1976
1961
  }
1977
1962
  }
1978
- s.record({ kind: "turn_started", prompt: content });
1963
+ let recorded;
1964
+ if (attachments?.length) {
1965
+ const ordinal = await promptCount(s.sessionId);
1966
+ this.store.setAttachments(s.sessionId, ordinal, attachments);
1967
+ recorded = attachments;
1968
+ }
1969
+ s.record({ kind: "turn_started", prompt: content, attachments: recorded });
1979
1970
  proc.prompt({ sessionId: s.sessionId, prompt: content }).then((res) => s.record({ kind: "turn_ended", stopReason: res.stopReason })).catch((err) => {
1980
1971
  this.log.error({ err, sessionId: s.sessionId }, "prompt turn failed");
1981
1972
  const tail = proc.stderrTail();
@@ -2010,19 +2001,28 @@ ${tail}` : err.message;
2010
2001
  s.modelId = modelId;
2011
2002
  s.lastActivity = Date.now();
2012
2003
  }
2013
- async execCommand(sessionId, command, args) {
2004
+ async execCommand(sessionId, command) {
2014
2005
  const s = await this.ensureOpen(sessionId);
2015
2006
  await this.settleReload(s);
2016
2007
  const proc = await this.ensureProc(s);
2017
- await proc.execCommand(s.sessionId, command, args);
2008
+ await proc.execCommand(s.sessionId, command);
2018
2009
  s.lastActivity = Date.now();
2019
2010
  }
2011
+ // -------------------------------------------------------------------------
2012
+ // Listing / detail - never spawns.
2013
+ // -------------------------------------------------------------------------
2014
+ /**
2015
+ * The one place a SessionSummary is assembled: kiro's file and Casper's live state each hold
2016
+ * part of the truth, so every read path projects them through here. `persisted` is absent for
2017
+ * a session with no file yet, `live` for a dormant one, and callers guarantee one of them.
2018
+ */
2020
2019
  summaryOf(live, persisted, transcript) {
2021
2020
  const sessionId = live?.sessionId ?? persisted.sessionId;
2022
2021
  const snap = live?.turnState.get();
2023
2022
  const cwd = live?.cwd ?? this.store.getCwd(sessionId) ?? persisted.cwd;
2024
2023
  return {
2025
2024
  sessionId,
2025
+ chatId: live?.chatId ?? this.store.getChatId(sessionId),
2026
2026
  title: this.titleOf(sessionId, {
2027
2027
  // kiro's file is what kiro called it; the live copy is only a cache of it.
2028
2028
  kiroTitle: persisted?.title || live?.title,
@@ -2037,9 +2037,8 @@ ${tail}` : err.message;
2037
2037
  agentId: live?.agentId ?? persisted?.agentId,
2038
2038
  modelId: live?.modelId ?? persisted?.modelId,
2039
2039
  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,
2040
+ // A live snapshot starts at zero until the first turn reports, so a dormant
2041
+ // session's value comes from kiro's file.
2043
2042
  contextUsagePercentage: snap?.contextUsagePercentage || persisted?.contextUsagePercentage
2044
2043
  };
2045
2044
  }
@@ -2061,25 +2060,12 @@ ${tail}` : err.message;
2061
2060
  }
2062
2061
  async getDetail(sessionId) {
2063
2062
  const [transcript, persisted] = await Promise.all([
2064
- hydrateTranscript(sessionId),
2063
+ hydrateTranscript(sessionId, this.store.attachmentsBySession(sessionId)),
2065
2064
  readPersistedSession(sessionId)
2066
2065
  ]);
2067
2066
  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
- };
2067
+ if (!s && !persisted) throw new Error(`Unknown session: ${sessionId}`);
2068
+ return this.buildDetail(s, transcript, persisted ?? void 0);
2083
2069
  }
2084
2070
  /**
2085
2071
  * A slice of the transcript for lazy "load older on scroll up". Re-hydrates
@@ -2087,29 +2073,34 @@ ${tail}` : err.message;
2087
2073
  * requested window. offset/limit are clamped to the transcript bounds.
2088
2074
  */
2089
2075
  async getTranscriptPage(sessionId, offset, limit) {
2090
- const transcript = await hydrateTranscript(sessionId);
2076
+ const transcript = await hydrateTranscript(
2077
+ sessionId,
2078
+ this.store.attachmentsBySession(sessionId)
2079
+ );
2091
2080
  const start = Math.max(0, Math.min(offset, transcript.length));
2092
2081
  const end = Math.max(start, Math.min(start + limit, transcript.length));
2093
2082
  return transcript.slice(start, end);
2094
2083
  }
2084
+ /** One projection, so a dormant session and a live one cannot disagree. */
2095
2085
  buildDetail(s, transcript, persisted) {
2096
- const snap = s.turnState.get();
2097
2086
  return {
2098
2087
  summary: this.summaryOf(s, persisted, transcript),
2099
- modes: s.availableModes,
2100
- currentModeId: s.currentModeId,
2088
+ modes: s?.availableModes ?? [],
2089
+ currentModeId: s?.currentModeId ?? persisted?.agentId,
2101
2090
  // Only the tail is sent on load; replayHead/title use the full transcript.
2102
2091
  transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
2103
2092
  transcriptTotal: transcript.length,
2104
- observability: snap,
2105
- head: this.replayHead(s, transcript)
2093
+ observability: s?.turnState.get() ?? {
2094
+ ...emptyObservabilitySnapshot(),
2095
+ contextUsagePercentage: persisted?.contextUsagePercentage ?? 0
2096
+ },
2097
+ head: s ? this.replayHead(s, transcript) : 0
2106
2098
  };
2107
2099
  }
2108
2100
  /**
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.
2101
+ * The cursor a reconnecting client starts from. Normally the store head, but kiro writes a turn
2102
+ * to its jsonl only once it completes, so a hydrated transcript is missing one in flight:
2103
+ * rewind to just before its turn_started and let the socket replay the whole turn.
2113
2104
  */
2114
2105
  replayHead(s, transcript) {
2115
2106
  const head = s.store.head();
@@ -2214,13 +2205,17 @@ var init_rateLimit = __esm({
2214
2205
  });
2215
2206
 
2216
2207
  // server/src/routes/auth.ts
2217
- import crypto4 from "node:crypto";
2208
+ import crypto3 from "node:crypto";
2218
2209
  import fastifyCookie from "@fastify/cookie";
2210
+ function logins() {
2211
+ loginStore ??= new LoginStore();
2212
+ return loginStore;
2213
+ }
2219
2214
  function secretMatches(supplied, expected) {
2220
2215
  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);
2216
+ const a = crypto3.createHash("sha256").update(supplied).digest();
2217
+ const b = crypto3.createHash("sha256").update(expected).digest();
2218
+ return crypto3.timingSafeEqual(a, b);
2224
2219
  }
2225
2220
  function extractToken(req) {
2226
2221
  const auth = req.headers["authorization"];
@@ -2238,7 +2233,7 @@ function sessionToken(req) {
2238
2233
  return req.cookies?.[SESSION_COOKIE];
2239
2234
  }
2240
2235
  function hasValidSession(req) {
2241
- return logins.verify(sessionToken(req)) !== null;
2236
+ return logins().verify(sessionToken(req)) !== null;
2242
2237
  }
2243
2238
  function cookieOptions() {
2244
2239
  return {
@@ -2266,27 +2261,27 @@ async function registerAuth(app) {
2266
2261
  loginLimiter.succeed(req.ip);
2267
2262
  }
2268
2263
  const ua = req.headers["user-agent"];
2269
- const { token } = logins.create(typeof ua === "string" ? ua : void 0);
2264
+ const { token } = logins().create(typeof ua === "string" ? ua : void 0);
2270
2265
  reply.setCookie(SESSION_COOKIE, token, cookieOptions());
2271
2266
  return { ok: true };
2272
2267
  });
2273
2268
  app.post("/api/logout", async (req, reply) => {
2274
- logins.revokeToken(sessionToken(req));
2269
+ logins().revokeToken(sessionToken(req));
2275
2270
  reply.clearCookie(SESSION_COOKIE, { path: "/" });
2276
2271
  return { ok: true };
2277
2272
  });
2278
2273
  app.get("/api/devices", async (req) => {
2279
- return { devices: logins.list(sessionToken(req)) };
2274
+ return { devices: logins().list(sessionToken(req)) };
2280
2275
  });
2281
2276
  app.delete("/api/devices/:id", async (req, reply) => {
2282
2277
  const { id } = req.params;
2283
- const current = logins.list(sessionToken(req)).find((d) => d.current);
2284
- const removed = logins.revokeId(id);
2278
+ const current = logins().list(sessionToken(req)).find((d) => d.current);
2279
+ const removed = logins().revokeId(id);
2285
2280
  if (current?.id === id) reply.clearCookie(SESSION_COOKIE, { path: "/" });
2286
2281
  return { ok: removed };
2287
2282
  });
2288
2283
  app.post("/api/logout-all", async (_req, reply) => {
2289
- logins.revokeAll();
2284
+ logins().revokeAll();
2290
2285
  reply.clearCookie(SESSION_COOKIE, { path: "/" });
2291
2286
  return { ok: true };
2292
2287
  });
@@ -2298,7 +2293,7 @@ async function registerAuth(app) {
2298
2293
  reply.code(401).send({ error: "Unauthorized: log in first" });
2299
2294
  });
2300
2295
  }
2301
- var SESSION_COOKIE, logins, loginLimiter;
2296
+ var SESSION_COOKIE, loginStore, loginLimiter;
2302
2297
  var init_auth = __esm({
2303
2298
  "server/src/routes/auth.ts"() {
2304
2299
  "use strict";
@@ -2306,7 +2301,6 @@ var init_auth = __esm({
2306
2301
  init_logins();
2307
2302
  init_rateLimit();
2308
2303
  SESSION_COOKIE = "casper.sid";
2309
- logins = new LoginStore();
2310
2304
  loginLimiter = new AttemptLimiter(10, 15 * 60 * 1e3);
2311
2305
  }
2312
2306
  });
@@ -2520,15 +2514,15 @@ function looksBinary(buf) {
2520
2514
  }
2521
2515
  function classifyKind(name) {
2522
2516
  const ext = path13.extname(name).toLowerCase();
2523
- if (ext in IMAGE_EXTS2) return "image";
2517
+ if (ext in IMAGE_EXTS) return "image";
2524
2518
  if (TEXT_EXTS.has(ext)) return "text";
2525
2519
  return "binary";
2526
2520
  }
2527
- var IMAGE_EXTS2, TEXT_EXTS, MIME_MAP;
2521
+ var IMAGE_EXTS, TEXT_EXTS, MIME_MAP;
2528
2522
  var init_filekind = __esm({
2529
2523
  "server/src/util/filekind.ts"() {
2530
2524
  "use strict";
2531
- IMAGE_EXTS2 = {
2525
+ IMAGE_EXTS = {
2532
2526
  ".png": "image/png",
2533
2527
  ".jpg": "image/jpeg",
2534
2528
  ".jpeg": "image/jpeg",
@@ -2600,7 +2594,7 @@ var init_filekind = __esm({
2600
2594
  ".tex"
2601
2595
  ]);
2602
2596
  MIME_MAP = {
2603
- ...IMAGE_EXTS2,
2597
+ ...IMAGE_EXTS,
2604
2598
  ".txt": "text/plain",
2605
2599
  ".md": "text/markdown",
2606
2600
  ".csv": "text/csv",
@@ -2628,10 +2622,128 @@ var init_filekind = __esm({
2628
2622
  }
2629
2623
  });
2630
2624
 
2631
- // server/src/routes/fs.ts
2625
+ // server/src/routes/filePreview.ts
2632
2626
  import fs12 from "node:fs/promises";
2633
2627
  import { createReadStream } from "node:fs";
2634
2628
  import path14 from "node:path";
2629
+ function tooLargeForPreview(size, maxBytes) {
2630
+ return {
2631
+ error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
2632
+ };
2633
+ }
2634
+ function hexdump(buf) {
2635
+ const lines = [];
2636
+ for (let off = 0; off < buf.length; off += 16) {
2637
+ const slice = buf.subarray(off, off + 16);
2638
+ const hex = [];
2639
+ let ascii = "";
2640
+ for (let i = 0; i < 16; i++) {
2641
+ if (i < slice.length) {
2642
+ const b = slice[i];
2643
+ hex.push(b.toString(16).padStart(2, "0"));
2644
+ ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
2645
+ } else {
2646
+ hex.push(" ");
2647
+ }
2648
+ if (i === 7) hex.push("");
2649
+ }
2650
+ lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
2651
+ }
2652
+ return lines.join("\n");
2653
+ }
2654
+ async function sendFilePreview(req, reply, realTarget, stat) {
2655
+ const ext = path14.extname(realTarget).toLowerCase();
2656
+ const mime = mimeForExt(ext);
2657
+ const isImage = mime.startsWith("image/");
2658
+ const isPdf = mime === "application/pdf";
2659
+ const isHtml = mime === "text/html" && req.query.raw === "1";
2660
+ const kind = classifyKind(realTarget);
2661
+ if (isImage || isPdf) {
2662
+ if (stat.size > MAX_IMAGE_PREVIEW_BYTES) {
2663
+ reply.code(413);
2664
+ return tooLargeForPreview(stat.size, MAX_IMAGE_PREVIEW_BYTES);
2665
+ }
2666
+ } else if (kind !== "binary") {
2667
+ if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2668
+ reply.code(413);
2669
+ return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2670
+ }
2671
+ }
2672
+ if (isHtml) {
2673
+ if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2674
+ reply.code(413);
2675
+ return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2676
+ }
2677
+ reply.header("Content-Security-Policy", "sandbox allow-scripts allow-forms");
2678
+ reply.header("Content-Type", "text/html; charset=utf-8");
2679
+ reply.header("Cache-Control", "no-store");
2680
+ return reply.send(createReadStream(realTarget));
2681
+ }
2682
+ if (isImage || isPdf) {
2683
+ const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2684
+ reply.header("Cache-Control", "private, no-cache");
2685
+ reply.header("ETag", etag);
2686
+ reply.header("Last-Modified", stat.mtime.toUTCString());
2687
+ const ifNoneMatch = req.headers["if-none-match"];
2688
+ if (ifNoneMatch === etag) {
2689
+ reply.code(304);
2690
+ return reply.send();
2691
+ }
2692
+ reply.header("Content-Type", mime);
2693
+ reply.header("Content-Disposition", "inline");
2694
+ reply.header("Content-Length", stat.size);
2695
+ return reply.send(createReadStream(realTarget));
2696
+ }
2697
+ if (kind === "binary") {
2698
+ let fh;
2699
+ try {
2700
+ fh = await fs12.open(realTarget, "r");
2701
+ const buf = Buffer.alloc(Math.min(HEXDUMP_BYTES, stat.size));
2702
+ const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
2703
+ const head = buf.subarray(0, bytesRead);
2704
+ if (!looksBinary(head)) {
2705
+ if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2706
+ reply.code(413);
2707
+ return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2708
+ }
2709
+ reply.header("Content-Type", "text/plain; charset=utf-8");
2710
+ reply.header("Content-Disposition", "inline");
2711
+ reply.header("Content-Length", stat.size);
2712
+ return reply.send(createReadStream(realTarget));
2713
+ }
2714
+ const dump = hexdump(head);
2715
+ const header = `Binary file - ${stat.size} bytes
2716
+ Showing first ${bytesRead} bytes as hexdump:
2717
+
2718
+ `;
2719
+ const body = header + dump + (stat.size > bytesRead ? "\n\n\u2026 (truncated)" : "");
2720
+ reply.header("Content-Type", "text/plain; charset=utf-8");
2721
+ reply.header("Content-Disposition", "inline");
2722
+ return reply.send(body);
2723
+ } finally {
2724
+ await fh?.close();
2725
+ }
2726
+ }
2727
+ reply.header("Content-Type", "text/plain; charset=utf-8");
2728
+ reply.header("Content-Disposition", "inline");
2729
+ reply.header("Content-Length", stat.size);
2730
+ return reply.send(createReadStream(realTarget));
2731
+ }
2732
+ var HEXDUMP_BYTES, MAX_TEXT_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_BYTES;
2733
+ var init_filePreview = __esm({
2734
+ "server/src/routes/filePreview.ts"() {
2735
+ "use strict";
2736
+ init_filekind();
2737
+ HEXDUMP_BYTES = 4096;
2738
+ MAX_TEXT_PREVIEW_BYTES = 1024 * 1024;
2739
+ MAX_IMAGE_PREVIEW_BYTES = 20 * 1024 * 1024;
2740
+ }
2741
+ });
2742
+
2743
+ // server/src/routes/fs.ts
2744
+ import fs13 from "node:fs/promises";
2745
+ import { createReadStream as createReadStream2 } from "node:fs";
2746
+ import path15 from "node:path";
2635
2747
  function registerFsRoutes(app) {
2636
2748
  app.get(
2637
2749
  "/api/fs/dirs",
@@ -2639,10 +2751,10 @@ function registerFsRoutes(app) {
2639
2751
  const input = (req.query.path ?? "").trim();
2640
2752
  const base = config.defaultCwd;
2641
2753
  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");
2754
+ const resolved = input ? path15.resolve(base, input) : base;
2755
+ const dir = endsWithSep || !input ? resolved : path15.dirname(resolved);
2756
+ const prefix = endsWithSep || !input ? "" : path15.basename(resolved);
2757
+ const targetKind = await fs13.stat(resolved).then((s) => s.isDirectory() ? "directory" : "file").catch(() => "missing");
2646
2758
  const listing = await resolveAbsolutePath(dir, "directory");
2647
2759
  if (!listing.ok) {
2648
2760
  if (listing.status === 403) return replyWith(reply, listing);
@@ -2651,7 +2763,7 @@ function registerFsRoutes(app) {
2651
2763
  const realDir = listing.real;
2652
2764
  let entries = [];
2653
2765
  try {
2654
- const dirents = await fs12.readdir(realDir, { withFileTypes: true });
2766
+ const dirents = await fs13.readdir(realDir, { withFileTypes: true });
2655
2767
  const checks2 = await Promise.all(
2656
2768
  dirents.map(async (d) => {
2657
2769
  const target = await classifyDirent(realDir, d, absoluteRoots());
@@ -2662,7 +2774,7 @@ function registerFsRoutes(app) {
2662
2774
  entries = checks2.filter((name) => name !== null).filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())).sort((a, b) => {
2663
2775
  if (isDot(a) !== isDot(b)) return isDot(a) ? 1 : -1;
2664
2776
  return a.localeCompare(b);
2665
- }).slice(0, 500).map((name) => path14.join(dir, name));
2777
+ }).slice(0, 500).map((name) => path15.join(dir, name));
2666
2778
  } catch {
2667
2779
  entries = [];
2668
2780
  }
@@ -2670,51 +2782,38 @@ function registerFsRoutes(app) {
2670
2782
  }
2671
2783
  );
2672
2784
  app.get(
2673
- "/api/fs/image",
2785
+ "/api/fs/file",
2674
2786
  async (req, reply) => {
2675
2787
  const filePath = (req.query.path ?? "").trim();
2676
2788
  if (!filePath) {
2677
2789
  reply.code(400);
2678
2790
  return { error: "path parameter is required" };
2679
2791
  }
2680
- if (!path14.isAbsolute(filePath)) {
2792
+ if (!path15.isAbsolute(filePath)) {
2681
2793
  reply.code(400);
2682
2794
  return { error: "path must be absolute" };
2683
2795
  }
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();
2796
+ const resolved = await resolveAbsolutePath(filePath, "file");
2797
+ if (!resolved.ok) return replyWith(reply, resolved);
2798
+ if (req.query.download === "1") {
2799
+ reply.header("Content-Type", "application/octet-stream");
2800
+ reply.header(
2801
+ "Content-Disposition",
2802
+ `attachment; filename="${path15.basename(resolved.real).replace(/"/g, "")}"`
2803
+ );
2804
+ reply.header("Content-Length", resolved.stat.size);
2805
+ return reply.send(createReadStream2(resolved.real));
2703
2806
  }
2704
- reply.header("Content-Type", mimeForExt(ext));
2705
- reply.header("Content-Length", stat.size);
2706
- return reply.send(createReadStream(real));
2807
+ return sendFilePreview(req, reply, resolved.real, resolved.stat);
2707
2808
  }
2708
2809
  );
2709
2810
  }
2710
- var MAX_IMAGE_BYTES;
2711
2811
  var init_fs = __esm({
2712
2812
  "server/src/routes/fs.ts"() {
2713
2813
  "use strict";
2714
2814
  init_config();
2715
2815
  init_confinedFile();
2716
- init_filekind();
2717
- MAX_IMAGE_BYTES = 20 * 1024 * 1024;
2816
+ init_filePreview();
2718
2817
  }
2719
2818
  });
2720
2819
 
@@ -2731,7 +2830,11 @@ function registerSessionRoutes(app, manager) {
2731
2830
  agentId: body.agentId,
2732
2831
  modelId: body.modelId,
2733
2832
  freshWorkspace: body.freshWorkspace,
2734
- title: body.title
2833
+ title: body.title,
2834
+ // Not optional in practice: the client's chat already owns any file uploaded before
2835
+ // the session existed, so dropping it here strands those files under an id nothing
2836
+ // refers to again.
2837
+ chatId: body.chatId
2735
2838
  });
2736
2839
  } catch (err) {
2737
2840
  reply.code(400);
@@ -2767,7 +2870,7 @@ function registerSessionRoutes(app, manager) {
2767
2870
  "/api/sessions/:id/prompt",
2768
2871
  async (req, reply) => {
2769
2872
  try {
2770
- await manager.runPrompt(req.params.id, req.body.prompt);
2873
+ await manager.runPrompt(req.params.id, req.body.prompt, req.body.attachments);
2771
2874
  return { ok: true };
2772
2875
  } catch (err) {
2773
2876
  reply.code(400);
@@ -2836,34 +2939,9 @@ var init_sessions = __esm({
2836
2939
  });
2837
2940
 
2838
2941
  // 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
- }
2942
+ import fs14 from "node:fs/promises";
2943
+ import { createReadStream as createReadStream3 } from "node:fs";
2944
+ import path16 from "node:path";
2867
2945
  function registerWorkspaceRoutes(app, manager) {
2868
2946
  app.get(
2869
2947
  "/api/sessions/:id/tree",
@@ -2878,7 +2956,7 @@ function registerWorkspaceRoutes(app, manager) {
2878
2956
  const { cwd, relative, real: realTarget } = resolved;
2879
2957
  let dirents;
2880
2958
  try {
2881
- dirents = await fs13.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2959
+ dirents = await fs14.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2882
2960
  } catch {
2883
2961
  return replyWith(reply, await workspaceNotFound(cwd));
2884
2962
  }
@@ -2892,7 +2970,7 @@ function registerWorkspaceRoutes(app, manager) {
2892
2970
  continue;
2893
2971
  }
2894
2972
  try {
2895
- const stat = await fs13.stat(target.real);
2973
+ const stat = await fs14.stat(target.real);
2896
2974
  entries.push({
2897
2975
  name: d.name,
2898
2976
  path: entryRelative,
@@ -2921,8 +2999,8 @@ function registerWorkspaceRoutes(app, manager) {
2921
2999
  reply.code(413);
2922
3000
  return { error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 100 MB)` };
2923
3001
  }
2924
- const ext = path15.extname(realTarget);
2925
- const filename = path15.basename(realTarget);
3002
+ const ext = path16.extname(realTarget);
3003
+ const filename = path16.basename(realTarget);
2926
3004
  const asciiName = filename.replace(/[^\w.\-]/g, "_");
2927
3005
  reply.header("Content-Type", mimeForExt(ext));
2928
3006
  reply.header(
@@ -2930,7 +3008,7 @@ function registerWorkspaceRoutes(app, manager) {
2930
3008
  `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(filename)}`
2931
3009
  );
2932
3010
  reply.header("Content-Length", stat.size);
2933
- return reply.send(createReadStream2(realTarget));
3011
+ return reply.send(createReadStream3(realTarget));
2934
3012
  }
2935
3013
  );
2936
3014
  app.get(
@@ -2939,132 +3017,52 @@ function registerWorkspaceRoutes(app, manager) {
2939
3017
  const resolved = await resolveSessionPath(manager, req.params.id, req.query.path, "file");
2940
3018
  if (!resolved.ok) return replyWith(reply, resolved);
2941
3019
  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));
3020
+ return sendFilePreview(req, reply, realTarget, stat);
3018
3021
  }
3019
3022
  );
3020
3023
  }
3021
- var MAX_DOWNLOAD_BYTES, HEXDUMP_BYTES, MAX_TEXT_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_BYTES;
3024
+ var MAX_DOWNLOAD_BYTES;
3022
3025
  var init_workspace = __esm({
3023
3026
  "server/src/routes/workspace.ts"() {
3024
3027
  "use strict";
3025
3028
  init_config();
3026
3029
  init_confinedFile();
3027
3030
  init_filekind();
3031
+ init_filePreview();
3028
3032
  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
3033
  }
3033
3034
  });
3034
3035
 
3035
3036
  // 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";
3037
+ import { createReadStream as createReadStream4, createWriteStream } from "node:fs";
3038
+ import fs15 from "node:fs/promises";
3039
+ import path17 from "node:path";
3040
+ import crypto4 from "node:crypto";
3040
3041
  import { execFile as execFile3 } from "node:child_process";
3041
3042
  import { pipeline } from "node:stream/promises";
3042
- function uploadDirFor(sessionId) {
3043
- return path16.join(config.casperDataDir, "sessions", sessionId, "uploads");
3044
- }
3045
3043
  function sanitizeName(raw) {
3046
- const base = path16.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
3044
+ const base = path17.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
3047
3045
  if (!base || base === "." || base === "..") return `upload-${Date.now()}`;
3048
3046
  return base.replace(/^\.+/, "") || `upload-${Date.now()}`;
3049
3047
  }
3050
3048
  async function uniquePath(dir, name) {
3051
- const ext = path16.extname(name);
3049
+ const ext = path17.extname(name);
3052
3050
  const stem = name.slice(0, name.length - ext.length);
3053
3051
  let candidate = name;
3054
3052
  let n = 1;
3055
3053
  while (true) {
3056
3054
  try {
3057
- await fs14.access(path16.join(dir, candidate));
3055
+ await fs15.access(path17.join(dir, candidate));
3058
3056
  candidate = `${stem} (${n++})${ext}`;
3059
3057
  } catch {
3060
- return path16.join(dir, candidate);
3058
+ return path17.join(dir, candidate);
3061
3059
  }
3062
3060
  }
3063
3061
  }
3064
3062
  function sha256File(absPath) {
3065
3063
  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);
3064
+ const hash = crypto4.createHash("sha256");
3065
+ createReadStream4(absPath).on("data", (d) => hash.update(d)).on("end", () => resolve(hash.digest("hex"))).on("error", reject);
3068
3066
  });
3069
3067
  }
3070
3068
  function fileType(absPath) {
@@ -3077,7 +3075,7 @@ function fileType(absPath) {
3077
3075
  async function sampleStrings(absPath, limit = 40) {
3078
3076
  let fh;
3079
3077
  try {
3080
- fh = await fs14.open(absPath, "r");
3078
+ fh = await fs15.open(absPath, "r");
3081
3079
  const buf = Buffer.alloc(STRINGS_SCAN_BYTES);
3082
3080
  const { bytesRead } = await fh.read(buf, 0, STRINGS_SCAN_BYTES, 0);
3083
3081
  const out = [];
@@ -3100,22 +3098,16 @@ async function sampleStrings(absPath, limit = 40) {
3100
3098
  await fh?.close();
3101
3099
  }
3102
3100
  }
3103
- function registerUploadRoutes(app, manager) {
3101
+ function registerUploadRoutes(app) {
3104
3102
  app.post(
3105
- "/api/sessions/:id/uploads",
3103
+ "/api/chats/:chatId/uploads",
3106
3104
  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)) {
3105
+ if (!isValidChatId(req.params.chatId)) {
3114
3106
  reply.code(400);
3115
- return { error: "Invalid session id" };
3107
+ return { error: "Invalid chat id" };
3116
3108
  }
3117
- const uploadDir = uploadDirFor(req.params.id);
3118
- await fs14.mkdir(uploadDir, { recursive: true, mode: 448 });
3109
+ const uploadDir = chatUploadsDir(req.params.chatId);
3110
+ await fs15.mkdir(uploadDir, { recursive: true, mode: 448 });
3119
3111
  const results = [];
3120
3112
  const parts = req.files();
3121
3113
  for await (const part of parts) {
@@ -3129,17 +3121,17 @@ function registerUploadRoutes(app, manager) {
3129
3121
  return { error: `Failed to store ${name}` };
3130
3122
  }
3131
3123
  if (part.file.truncated) {
3132
- await fs14.rm(dest, { force: true });
3124
+ await fs15.rm(dest, { force: true });
3133
3125
  reply.code(413);
3134
3126
  return {
3135
3127
  error: `${name} exceeds the max upload size (${Math.round(config.maxUploadBytes / 1024 / 1024)} MB)`
3136
3128
  };
3137
3129
  }
3138
- const stat = await fs14.stat(dest);
3139
- const ext = path16.extname(dest);
3130
+ const stat = await fs15.stat(dest);
3131
+ const ext = path17.extname(dest);
3140
3132
  const kind = classifyKind(dest);
3141
3133
  const uploaded = {
3142
- name: path16.basename(dest),
3134
+ name: path17.basename(dest),
3143
3135
  // Absolute, because the file sits outside the session's workspace: a
3144
3136
  // relative path would resolve against the wrong root for both the agent
3145
3137
  // and the image endpoint.
@@ -3149,14 +3141,14 @@ function registerUploadRoutes(app, manager) {
3149
3141
  kind
3150
3142
  };
3151
3143
  if (kind === "binary") {
3152
- const [type, sha2563, strings] = await Promise.all([
3144
+ const [type, sha2562, strings] = await Promise.all([
3153
3145
  fileType(dest),
3154
3146
  sha256File(dest),
3155
3147
  sampleStrings(dest)
3156
3148
  ]);
3157
3149
  uploaded.triage = {
3158
3150
  fileType: type || void 0,
3159
- sha256: sha2563,
3151
+ sha256: sha2562,
3160
3152
  strings: strings.length ? strings : void 0
3161
3153
  };
3162
3154
  }
@@ -3174,6 +3166,7 @@ var init_uploads = __esm({
3174
3166
  init_config();
3175
3167
  init_filekind();
3176
3168
  init_paths2();
3169
+ init_chats();
3177
3170
  STRINGS_SCAN_BYTES = 256 * 1024;
3178
3171
  }
3179
3172
  });
@@ -3197,7 +3190,7 @@ var init_health = __esm({
3197
3190
  });
3198
3191
 
3199
3192
  // server/src/ws/dirWatchers.ts
3200
- import fs15 from "node:fs";
3193
+ import fs16 from "node:fs";
3201
3194
  function diffWatchSet(current, next) {
3202
3195
  const have = new Set(current);
3203
3196
  const want = new Set(next.slice(0, MAX_WATCHES));
@@ -3221,7 +3214,7 @@ function createDirWatchers(opts) {
3221
3214
  const target = await opts.resolve(relative);
3222
3215
  if (!target || closed) return;
3223
3216
  try {
3224
- const watcher = fs15.watch(target, { persistent: false }, () => {
3217
+ const watcher = fs16.watch(target, { persistent: false }, () => {
3225
3218
  const existing = timers.get(relative);
3226
3219
  if (existing) clearTimeout(existing);
3227
3220
  timers.set(
@@ -3279,7 +3272,7 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3279
3272
  return null;
3280
3273
  }
3281
3274
  },
3282
- onChange: (path19) => send(socket, { type: "fs_changed", path: path19 })
3275
+ onChange: (path20) => send(socket, { type: "fs_changed", path: path20 })
3283
3276
  });
3284
3277
  const forward = (event) => {
3285
3278
  if (event.seq <= cursor) return;
@@ -3330,7 +3323,7 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3330
3323
  case "watch_paths":
3331
3324
  return watchers.sync(msg.paths);
3332
3325
  case "prompt":
3333
- return ack("prompt", () => manager.runPrompt(sessionId, msg.content));
3326
+ return ack("prompt", () => manager.runPrompt(sessionId, msg.content, msg.attachments));
3334
3327
  case "cancel":
3335
3328
  return ack("cancel", () => manager.cancel(sessionId));
3336
3329
  case "set_mode":
@@ -3340,7 +3333,7 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3340
3333
  case "exec_command":
3341
3334
  return ack(
3342
3335
  "exec_command",
3343
- () => manager.execCommand(sessionId, msg.command, msg.args)
3336
+ () => manager.execCommand(sessionId, msg.command)
3344
3337
  );
3345
3338
  default:
3346
3339
  return send(socket, { type: "error", message: "Unknown message type" });
@@ -3412,9 +3405,8 @@ var init_gateway = __esm({
3412
3405
  });
3413
3406
 
3414
3407
  // server/src/app.ts
3415
- import fs16 from "node:fs";
3408
+ import fs17 from "node:fs";
3416
3409
  import Fastify from "fastify";
3417
- import cors from "@fastify/cors";
3418
3410
  import websocket from "@fastify/websocket";
3419
3411
  import fastifyStatic from "@fastify/static";
3420
3412
  import multipart from "@fastify/multipart";
@@ -3434,7 +3426,6 @@ async function buildApp() {
3434
3426
  });
3435
3427
  const manager = new SessionManager(logger);
3436
3428
  const startedAt = Date.now();
3437
- await app.register(cors, { origin: true });
3438
3429
  await app.register(websocket, {
3439
3430
  options: { maxPayload: 16 * 1024 * 1024 }
3440
3431
  });
@@ -3448,22 +3439,22 @@ async function buildApp() {
3448
3439
  registerFsRoutes(app);
3449
3440
  registerSessionRoutes(app, manager);
3450
3441
  registerWorkspaceRoutes(app, manager);
3451
- registerUploadRoutes(app, manager);
3442
+ registerUploadRoutes(app);
3452
3443
  registerWsGateway(app, manager);
3453
- if (fs16.existsSync(config.webDist)) {
3444
+ if (fs17.existsSync(config.webDist)) {
3454
3445
  await app.register(fastifyStatic, {
3455
3446
  root: config.webDist
3456
3447
  });
3457
3448
  app.addHook("onSend", async (req, reply) => {
3458
- const path19 = req.url.split("?")[0] ?? "";
3459
- if (path19 === "/manifest.json" || path19.startsWith("/icons/")) {
3449
+ const path20 = req.url.split("?")[0] ?? "";
3450
+ if (path20 === "/manifest.json" || path20.startsWith("/icons/")) {
3460
3451
  reply.header("cache-control", "public, max-age=300");
3461
3452
  }
3462
3453
  });
3463
3454
  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) {
3455
+ const path20 = req.url.split("?")[0] ?? "";
3456
+ const looksLikeAsset = path20.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path20);
3457
+ if (path20.startsWith("/api/") || path20.startsWith("/ws") || looksLikeAsset) {
3467
3458
  reply.code(404).send({ error: "Not found" });
3468
3459
  return;
3469
3460
  }
@@ -3537,15 +3528,15 @@ __export(doctor_exports, {
3537
3528
  mcpWiring: () => mcpWiring
3538
3529
  });
3539
3530
  import { execFileSync as execFileSync3 } from "node:child_process";
3540
- import fs17 from "node:fs";
3541
- import path17 from "node:path";
3531
+ import fs18 from "node:fs";
3532
+ import path18 from "node:path";
3542
3533
  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)) {
3534
+ if (bin.includes("/")) return fs18.existsSync(bin) ? bin : void 0;
3535
+ for (const dir of (process.env.PATH ?? "").split(path18.delimiter)) {
3545
3536
  if (dir === "") continue;
3546
- const candidate = path17.join(dir, bin);
3537
+ const candidate = path18.join(dir, bin);
3547
3538
  try {
3548
- fs17.accessSync(candidate, fs17.constants.X_OK);
3539
+ fs18.accessSync(candidate, fs18.constants.X_OK);
3549
3540
  return candidate;
3550
3541
  } catch {
3551
3542
  }
@@ -3569,7 +3560,7 @@ function mcpWiring(agentJson, resolve) {
3569
3560
  return { ok: false, detail: `${command || "(no command)"} not found - widgets are unavailable` };
3570
3561
  }
3571
3562
  const script = args.find((a) => a.endsWith(".js"));
3572
- if (script && !fs17.existsSync(script)) {
3563
+ if (script && !fs18.existsSync(script)) {
3573
3564
  return { ok: false, detail: `${script} is gone - delete the agent file and restart to rewrite it` };
3574
3565
  }
3575
3566
  return { ok: true, detail: script ?? `${command} ${args.join(" ")}` };
@@ -3601,7 +3592,7 @@ function checks() {
3601
3592
  const settings = readSettings(config.configFile);
3602
3593
  const hasToken = typeof settings.token === "string" && settings.token !== "";
3603
3594
  out.push(
3604
- fs17.existsSync(config.configFile) ? {
3595
+ fs18.existsSync(config.configFile) ? {
3605
3596
  ok: hasToken,
3606
3597
  name: "settings",
3607
3598
  detail: hasToken ? `${config.configFile} (token set)` : `${config.configFile} exists but has no token - auth is DISABLED`
@@ -3611,8 +3602,8 @@ function checks() {
3611
3602
  detail: `${config.configFile} not found; a token is generated on first run`
3612
3603
  }
3613
3604
  );
3614
- if (fs17.existsSync(config.configFile)) {
3615
- const mode = fs17.statSync(config.configFile).mode & 511;
3605
+ if (fs18.existsSync(config.configFile)) {
3606
+ const mode = fs18.statSync(config.configFile).mode & 511;
3616
3607
  out.push({
3617
3608
  ok: mode === 384,
3618
3609
  name: "settings mode",
@@ -3620,10 +3611,10 @@ function checks() {
3620
3611
  });
3621
3612
  }
3622
3613
  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` : "";
3614
+ fs18.mkdirSync(config.casperDataDir, { recursive: true });
3615
+ fs18.accessSync(config.casperDataDir, fs18.constants.W_OK);
3616
+ const db2 = path18.join(config.casperDataDir, "casper.db");
3617
+ const size = fs18.existsSync(db2) ? `, casper.db ${(fs18.statSync(db2).size / 1024).toFixed(0)} KB` : "";
3627
3618
  out.push({ ok: true, name: "data dir", detail: `${config.casperDataDir} writable${size}` });
3628
3619
  } catch (err) {
3629
3620
  out.push({
@@ -3634,22 +3625,22 @@ function checks() {
3634
3625
  });
3635
3626
  }
3636
3627
  out.push(
3637
- fs17.existsSync(config.webDist) ? { ok: true, name: "web app", detail: config.webDist } : {
3628
+ fs18.existsSync(config.webDist) ? { ok: true, name: "web app", detail: config.webDist } : {
3638
3629
  ok: false,
3639
3630
  fatal: true,
3640
3631
  name: "web app",
3641
3632
  detail: `${config.webDist} not found - the package is incomplete or CASPER_WEB_DIST is wrong`
3642
3633
  }
3643
3634
  );
3644
- const agent = path17.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3645
- const hasAgent = fs17.existsSync(agent);
3635
+ const agent = path18.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3636
+ const hasAgent = fs18.existsSync(agent);
3646
3637
  out.push({
3647
3638
  ok: hasAgent,
3648
3639
  name: "casper agent",
3649
3640
  detail: hasAgent ? agent : `${agent} missing - it is installed on first run`
3650
3641
  });
3651
3642
  if (hasAgent) {
3652
- const wiring = mcpWiring(fs17.readFileSync(agent, "utf8"), which);
3643
+ const wiring = mcpWiring(fs18.readFileSync(agent, "utf8"), which);
3653
3644
  out.push({ ok: wiring.ok, name: "casper mcp", detail: wiring.detail });
3654
3645
  }
3655
3646
  return out;
@@ -4113,14 +4104,14 @@ var init_server2 = __esm({
4113
4104
  "server/src/mcp/server.ts"() {
4114
4105
  "use strict";
4115
4106
  init_protocol();
4116
- VERSION = true ? "0.11.1" : "dev";
4107
+ VERSION = true ? "0.12.0" : "dev";
4117
4108
  }
4118
4109
  });
4119
4110
 
4120
4111
  // server/src/cli/program.ts
4121
- import fs18 from "node:fs";
4112
+ import fs19 from "node:fs";
4122
4113
  import os4 from "node:os";
4123
- import path18 from "node:path";
4114
+ import path19 from "node:path";
4124
4115
 
4125
4116
  // server/node_modules/commander/lib/error.js
4126
4117
  var CommanderError = class extends Error {
@@ -7233,9 +7224,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
7233
7224
  * @param {string} [path]
7234
7225
  * @return {(string|null|Command)}
7235
7226
  */
7236
- executableDir(path19) {
7237
- if (path19 === void 0) return this._executableDir;
7238
- this._executableDir = path19;
7227
+ executableDir(path20) {
7228
+ if (path20 === void 0) return this._executableDir;
7229
+ this._executableDir = path20;
7239
7230
  return this;
7240
7231
  }
7241
7232
  /**
@@ -7493,7 +7484,7 @@ var program = new Command();
7493
7484
  // server/src/cli/program.ts
7494
7485
  init_paths();
7495
7486
  init_settings();
7496
- var VERSION2 = true ? "0.11.1" : "dev";
7487
+ var VERSION2 = true ? "0.12.0" : "dev";
7497
7488
  async function bootstrap() {
7498
7489
  const file = configFilePath();
7499
7490
  const settings = readSettings(file);
@@ -7539,8 +7530,8 @@ async function resetToken(value) {
7539
7530
  updateSettings(file, { token });
7540
7531
  process.stdout.write(`wrote a new token to ${file}
7541
7532
  `);
7542
- const dbFile = path18.join(dataDirPath(), "casper.db");
7543
- if (fs18.existsSync(dbFile)) {
7533
+ const dbFile = path19.join(dataDirPath(), "casper.db");
7534
+ if (fs19.existsSync(dbFile)) {
7544
7535
  const { LoginStore: LoginStore2 } = await Promise.resolve().then(() => (init_logins(), logins_exports));
7545
7536
  new LoginStore2().revokeAll();
7546
7537
  process.stdout.write("revoked all device sessions\n");
@@ -7557,26 +7548,26 @@ async function resetToken(value) {
7557
7548
  }
7558
7549
  function buildProgram() {
7559
7550
  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", `
7551
+ program2.name("casper").description("Web client for kiro-cli over the Agent Client Protocol").version(VERSION2, "-v, --version").showHelpAfterError().addHelpText("after", `
7561
7552
  Settings live in ${configFilePath()}.
7562
7553
  Update with: npm install -g @joeyshi12/casper`);
7563
- program2.command("start").description("run the server in the foreground").allowUnknownOption(false).allowExcessArguments(false).action(async () => {
7554
+ program2.command("start").description("run the server in the foreground").action(async () => {
7564
7555
  await bootstrap();
7565
7556
  const { serve: serve2 } = await Promise.resolve().then(() => (init_server(), server_exports));
7566
7557
  await serve2();
7567
7558
  });
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) => {
7559
+ program2.command("token").description("print the access token").action(printToken);
7560
+ 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
7561
  await resetToken(value);
7571
7562
  });
7572
- program2.command("doctor").description("check kiro-cli, settings, data directory and web app").allowUnknownOption(false).allowExcessArguments(false).action(async () => {
7563
+ program2.command("doctor").description("check kiro-cli, settings, data directory and web app").action(async () => {
7573
7564
  process.exitCode = (await Promise.resolve().then(() => (init_doctor(), doctor_exports))).doctor();
7574
7565
  });
7575
- program2.command("mcp").description("run the generative-UI MCP server on stdio (kiro spawns this itself)").allowUnknownOption(false).allowExcessArguments(false).action(async () => {
7566
+ program2.command("mcp").description("run the generative-UI MCP server on stdio (kiro spawns this itself)").action(async () => {
7576
7567
  (await Promise.resolve().then(() => (init_server2(), server_exports2))).runMcpServer();
7577
7568
  });
7578
7569
  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 () => {
7570
+ const svcCommand = (name, describe, run) => service.command(name).description(describe).action(async () => {
7580
7571
  process.exitCode = run(await Promise.resolve().then(() => (init_service(), service_exports)));
7581
7572
  });
7582
7573
  svcCommand("install", "run Casper as a systemd user service", (m) => m.serviceInstall());