@joeyshi12/casper 0.11.0 → 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 +6 -1
  2. package/dist/agents/prompt.txt +1 -1
  3. package/dist/mcp.js +1 -1
  4. package/dist/server.js +462 -473
  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"() {
@@ -770,7 +763,6 @@ function invalidateAgents() {
770
763
  async function listAgents() {
771
764
  if (cache && Date.now() - cache.at < CACHE_TTL_MS) return cache.agents;
772
765
  const found = /* @__PURE__ */ new Map();
773
- for (const b of BUILTIN) found.set(b.id, b);
774
766
  try {
775
767
  const { stdout, stderr } = await execFileAsync(config.kiroBin, ["agent", "list"], {
776
768
  cwd: config.defaultCwd,
@@ -779,7 +771,7 @@ async function listAgents() {
779
771
  const text2 = (stderr || "") + "\n" + (stdout || "");
780
772
  for (const rawLine of text2.split("\n")) {
781
773
  const line = rawLine.replace(ANSI, "");
782
- const m = /^\s{0,2}(\*\s)?([A-Za-z0-9_-]+)\s{2,}(Global|Workspace|Local)\b/.exec(
774
+ const m = /^\s{0,2}(\*\s)?([A-Za-z0-9_-]+)\s{2,}(\(Built-in\)|Global|Workspace|Local)(?:\s|$)/.exec(
783
775
  line
784
776
  );
785
777
  if (!m) continue;
@@ -788,19 +780,18 @@ async function listAgents() {
788
780
  }
789
781
  } catch {
790
782
  }
791
- cache = { at: Date.now(), agents: [...found.values()] };
783
+ const agents = found.size > 0 ? [...found.values()] : FALLBACK;
784
+ cache = { at: Date.now(), agents };
792
785
  return cache.agents;
793
786
  }
794
- var execFileAsync, BUILTIN, CACHE_TTL_MS, cache, ANSI;
787
+ var execFileAsync, FALLBACK, CACHE_TTL_MS, cache, ANSI;
795
788
  var init_agents = __esm({
796
789
  "server/src/session/agents.ts"() {
797
790
  "use strict";
798
791
  init_config();
799
792
  execFileAsync = promisify(execFile);
800
- BUILTIN = [
801
- { id: "kiro_default", name: "kiro_default", description: "General-purpose Kiro agent" },
802
- { id: "kiro_planner", name: "kiro_planner", description: "Planning-focused agent" },
803
- { id: "kiro_guide", name: "kiro_guide", description: "Guidance and Q&A agent" }
793
+ FALLBACK = [
794
+ { id: "kiro_default", name: "kiro_default", description: "General-purpose Kiro agent" }
804
795
  ];
805
796
  CACHE_TTL_MS = 15e3;
806
797
  cache = null;
@@ -808,27 +799,32 @@ var init_agents = __esm({
808
799
  }
809
800
  });
810
801
 
811
- // server/src/session/workspaces.ts
812
- import crypto3 from "node:crypto";
802
+ // server/src/session/chats.ts
813
803
  import fs7 from "node:fs";
814
804
  import path8 from "node:path";
815
- function workspacesRoot() {
816
- return path8.join(config.casperDataDir, "workspaces");
805
+ function chatsRoot() {
806
+ return path8.join(config.casperDataDir, "chats");
817
807
  }
818
- function workspaceDir(workspaceId) {
819
- return path8.join(workspacesRoot(), workspaceId);
808
+ function chatDir(chatId) {
809
+ return path8.join(chatsRoot(), chatId);
820
810
  }
821
- function createWorkspace() {
822
- const id = crypto3.randomUUID();
823
- 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);
824
819
  fs7.mkdirSync(dir, { recursive: true, mode: 448 });
825
- return { id, dir };
820
+ return dir;
826
821
  }
827
- function isWorkspacePath(dir) {
828
- 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);
829
825
  }
830
- var init_workspaces = __esm({
831
- "server/src/session/workspaces.ts"() {
826
+ var init_chats = __esm({
827
+ "server/src/session/chats.ts"() {
832
828
  "use strict";
833
829
  init_config();
834
830
  }
@@ -870,6 +866,9 @@ async function realConfineToRoot(root, absPath) {
870
866
  function isValidSessionId(id) {
871
867
  return /^[A-Za-z0-9._-]+$/.test(id) && id !== "." && !id.includes("..");
872
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
+ }
873
872
  var realRootCache;
874
873
  var init_paths2 = __esm({
875
874
  "server/src/util/paths.ts"() {
@@ -1085,9 +1084,9 @@ var init_KiroProcess = __esm({
1085
1084
  this.child.stdout.on("error", () => {
1086
1085
  });
1087
1086
  }
1088
- /** Perform the ACP initialize handshake. Idempotent. */
1087
+ /** Perform the ACP initialize handshake. kiro's reply is not used. */
1089
1088
  async initialize() {
1090
- const result = await this.client.request(ACP_METHODS.initialize, {
1089
+ await this.client.request(ACP_METHODS.initialize, {
1091
1090
  protocolVersion: 1,
1092
1091
  clientCapabilities: {
1093
1092
  fs: { readTextFile: false, writeTextFile: false },
@@ -1097,7 +1096,6 @@ var init_KiroProcess = __esm({
1097
1096
  // worth wiring to package.json.
1098
1097
  clientInfo: { name: "casper", version: "0.5.0" }
1099
1098
  });
1100
- return result;
1101
1099
  }
1102
1100
  newSession(params) {
1103
1101
  return this.client.request(ACP_METHODS.sessionNew, params);
@@ -1122,10 +1120,10 @@ var init_KiroProcess = __esm({
1122
1120
  setModel(sessionId, modelId) {
1123
1121
  return this.client.request(ACP_METHODS.sessionSetModel, { sessionId, modelId });
1124
1122
  }
1125
- execCommand(sessionId, command, args) {
1123
+ execCommand(sessionId, command) {
1126
1124
  return this.client.request(ACP_METHODS.commandsExecute, {
1127
1125
  sessionId,
1128
- command: { command: command.replace(/^\//, ""), args: args ? { input: args } : {} }
1126
+ command: { command: command.replace(/^\//, ""), args: {} }
1129
1127
  });
1130
1128
  }
1131
1129
  /** Cleanly shut down: close stdin (triggers kiro's graceful exit), then kill. */
@@ -1257,83 +1255,36 @@ var init_TurnState = __esm({
1257
1255
  get() {
1258
1256
  return this.snapshot;
1259
1257
  }
1260
- /** Apply one event; returns the updated snapshot. */
1261
1258
  apply(payload) {
1262
1259
  const s = this.snapshot;
1263
1260
  switch (payload.kind) {
1264
- case "turn_started": {
1265
- this.snapshot = { ...s, turnStatus: "running", currentTurnStartedAt: Date.now() };
1261
+ case "turn_started":
1262
+ this.snapshot = { ...s, turnStatus: "running" };
1266
1263
  break;
1267
- }
1268
- case "turn_ended": {
1269
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1264
+ case "turn_ended":
1265
+ case "turn_error":
1266
+ this.snapshot = { ...s, turnStatus: "idle" };
1270
1267
  break;
1271
- }
1272
- case "turn_error": {
1273
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1274
- break;
1275
- }
1276
- case "process_exited": {
1277
- this.snapshot = { ...s, turnStatus: "idle", currentTurnStartedAt: void 0 };
1278
- break;
1279
- }
1280
- case "metadata": {
1281
- const p = payload.params;
1282
- const turnCredits = (p.meteringUsage ?? []).reduce((sum, m) => sum + m.value, 0);
1283
- this.snapshot = {
1284
- ...s,
1285
- contextUsagePercentage: p.contextUsagePercentage ?? s.contextUsagePercentage,
1286
- // meteringUsage is emitted per-turn; accumulate into the session total.
1287
- creditsSpent: turnCredits > 0 ? s.creditsSpent + turnCredits : s.creditsSpent,
1288
- lastTurnCredits: turnCredits > 0 ? turnCredits : s.lastTurnCredits,
1289
- lastTurnDurationMs: p.turnDurationMs ?? s.lastTurnDurationMs
1290
- };
1268
+ case "process_exited":
1269
+ this.snapshot = { ...s, turnStatus: "idle", compacting: false };
1291
1270
  break;
1292
- }
1293
- case "subagent_update": {
1271
+ case "metadata":
1294
1272
  this.snapshot = {
1295
1273
  ...s,
1296
- subagents: payload.params.subagents,
1297
- pendingStages: payload.params.pendingStages
1274
+ contextUsagePercentage: payload.params.contextUsagePercentage ?? s.contextUsagePercentage
1298
1275
  };
1299
1276
  break;
1300
- }
1301
- case "mcp_health": {
1302
- const { serverName, error } = payload.params;
1303
- const status = payload.ok ? "initialized" : "failed";
1304
- const next = s.mcpServers.filter((m) => m.serverName !== serverName);
1305
- next.push({ serverName, status, error, updatedAt: Date.now() });
1306
- this.snapshot = { ...s, mcpServers: next };
1307
- break;
1308
- }
1309
- case "commands_available": {
1310
- this.snapshot = { ...s, availableCommands: payload.params.commands };
1311
- break;
1312
- }
1313
- case "compaction": {
1277
+ case "compaction":
1314
1278
  this.snapshot = { ...s, compacting: payload.params.status.type === "started" };
1315
1279
  break;
1316
- }
1317
- case "oauth_request": {
1318
- const prompts = [
1319
- ...s.oauthPrompts,
1320
- {
1321
- serverName: payload.params.serverName,
1322
- url: payload.params.url,
1323
- createdAt: Date.now()
1324
- }
1325
- ];
1326
- this.snapshot = { ...s, oauthPrompts: prompts };
1327
- break;
1328
- }
1329
1280
  default:
1330
1281
  break;
1331
1282
  }
1332
1283
  return this.snapshot;
1333
1284
  }
1334
- /** Seed cumulative credits/context from kiro's persisted metadata on resume. */
1335
- seed(creditsSpent, contextUsagePercentage) {
1336
- 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 };
1337
1288
  }
1338
1289
  };
1339
1290
  }
@@ -1371,13 +1322,12 @@ function renderableBlocks(content) {
1371
1322
  function summarize(j) {
1372
1323
  const state = j.session_state;
1373
1324
  const turns = state?.conversation_metadata?.user_turn_metadatas ?? [];
1374
- const creditsSpent = turns.reduce(
1375
- (sum, t) => sum + (t.metering_usage ?? []).reduce((a, m) => a + m.value, 0),
1376
- 0
1377
- );
1378
1325
  const contextUsagePercentage = state?.rts_model_state?.context_usage_percentage ?? turns[turns.length - 1]?.context_usage_percentage;
1379
1326
  return {
1380
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,
1381
1331
  // Left empty when kiro has not named it; resolveSessionTitle decides what to show.
1382
1332
  title: j.title?.trim() ?? "",
1383
1333
  cwd: j.cwd,
@@ -1387,7 +1337,6 @@ function summarize(j) {
1387
1337
  agentId: state?.agent_name,
1388
1338
  modelId: state?.rts_model_state?.model_info?.model_id,
1389
1339
  running: false,
1390
- creditsSpent,
1391
1340
  contextUsagePercentage
1392
1341
  };
1393
1342
  }
@@ -1419,6 +1368,7 @@ async function deletePersistedSession(sessionId) {
1419
1368
  path10.join(config.kiroSessionsDir, `${sessionId}.json`),
1420
1369
  path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
1421
1370
  path10.join(config.kiroSessionsDir, `${sessionId}.history`),
1371
+ path10.join(config.kiroSessionsDir, `${sessionId}.lock`),
1422
1372
  path10.join(config.kiroSessionsDir, sessionId)
1423
1373
  ];
1424
1374
  await Promise.all(targets.map((f) => fs9.rm(f, { recursive: true, force: true })));
@@ -1435,48 +1385,54 @@ async function readPersistedSession(sessionId) {
1435
1385
  return null;
1436
1386
  }
1437
1387
  }
1438
- async function hasRecordedTurns(sessionId) {
1439
- if (!isValidSessionId(sessionId)) return false;
1440
- try {
1441
- const st = await fs9.stat(path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`));
1442
- return st.size > 0;
1443
- } catch {
1444
- return false;
1445
- }
1388
+ async function promptCount(sessionId) {
1389
+ const entries = await readJsonlEntries(sessionId);
1390
+ return entries.filter((e) => e.kind === "Prompt").length;
1446
1391
  }
1447
- async function hydrateTranscript(sessionId) {
1392
+ async function readJsonlEntries(sessionId) {
1448
1393
  if (!isValidSessionId(sessionId)) return [];
1449
1394
  let raw;
1450
1395
  try {
1451
- raw = await fs9.readFile(
1452
- path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`),
1453
- "utf8"
1454
- );
1396
+ raw = await fs9.readFile(path10.join(config.kiroSessionsDir, `${sessionId}.jsonl`), "utf8");
1455
1397
  } catch {
1456
1398
  return [];
1457
1399
  }
1458
- const items = [];
1459
- const toolsById = /* @__PURE__ */ new Map();
1400
+ const entries = [];
1460
1401
  for (const line of raw.split("\n")) {
1461
1402
  const trimmed = line.trim();
1462
1403
  if (!trimmed) continue;
1463
- let entry;
1464
1404
  try {
1465
- entry = JSON.parse(trimmed);
1405
+ entries.push(JSON.parse(trimmed));
1466
1406
  } catch {
1467
- continue;
1468
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)) {
1469
1425
  const content = contentBlocks(entry.data.content);
1470
1426
  const textOf = (kind) => content.filter((c) => c.kind === kind).map(blockText).join("");
1471
1427
  const baseId = entry.data.message_id ?? `${items.length}`;
1472
1428
  const ts = entry.data.meta?.timestamp;
1473
1429
  const pushMsg = (msg) => items.push({ type: "message", message: msg });
1474
1430
  if (entry.kind === "Prompt") {
1475
- const raw2 = textOf("text");
1476
- const text2 = stripAttachmentsLine(raw2);
1477
- const imagePaths = imageAttachmentPaths(raw2);
1478
- if (text2.trim() || imagePaths.length)
1479
- 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 });
1480
1436
  } else if (entry.kind === "AssistantMessage") {
1481
1437
  const thinking = textOf("thinking");
1482
1438
  if (thinking.trim())
@@ -1545,9 +1501,52 @@ var init_sessionStore = __esm({
1545
1501
  setCwd(sessionId, cwd) {
1546
1502
  this.write(sessionId, "cwd", cwd);
1547
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
+ }
1548
1546
  /** Forget every override for a session, when it's permanently deleted. */
1549
1547
  remove(sessionId) {
1550
1548
  db().prepare("DELETE FROM sessions WHERE session_id = ?").run(sessionId);
1549
+ db().prepare("DELETE FROM message_attachments WHERE session_id = ?").run(sessionId);
1551
1550
  }
1552
1551
  read(sessionId, column) {
1553
1552
  const row = db().prepare(`SELECT ${column} AS value FROM sessions WHERE session_id = ?`).get(sessionId);
@@ -1592,18 +1591,8 @@ function mapNotification(n) {
1592
1591
  return { kind: "session_update", update: n.params.update };
1593
1592
  case KIRO_NOTIFICATIONS.metadata:
1594
1593
  return { kind: "metadata", params: n.params };
1595
- case KIRO_NOTIFICATIONS.subagentListUpdate:
1596
- return { kind: "subagent_update", params: n.params };
1597
- case KIRO_NOTIFICATIONS.mcpServerInitialized:
1598
- return { kind: "mcp_health", params: n.params, ok: true };
1599
- case KIRO_NOTIFICATIONS.mcpServerInitFailure:
1600
- return { kind: "mcp_health", params: n.params, ok: false };
1601
- case KIRO_NOTIFICATIONS.commandsAvailable:
1602
- return { kind: "commands_available", params: n.params };
1603
1594
  case KIRO_NOTIFICATIONS.compactionStatus:
1604
1595
  return { kind: "compaction", params: n.params };
1605
- case KIRO_NOTIFICATIONS.mcpOauthRequest:
1606
- return { kind: "oauth_request", params: n.params };
1607
1596
  default:
1608
1597
  return null;
1609
1598
  }
@@ -1625,7 +1614,7 @@ var init_SessionManager = __esm({
1625
1614
  init_dist();
1626
1615
  init_config();
1627
1616
  init_agents();
1628
- init_workspaces();
1617
+ init_chats();
1629
1618
  init_paths2();
1630
1619
  init_KiroProcess();
1631
1620
  init_EventStore();
@@ -1637,6 +1626,8 @@ var init_SessionManager = __esm({
1637
1626
  store;
1638
1627
  turnState = new TurnState();
1639
1628
  cwd;
1629
+ /** The chat that owns this session's uploads. See chats.ts. */
1630
+ chatId;
1640
1631
  agentId;
1641
1632
  modelId;
1642
1633
  currentModeId;
@@ -1705,7 +1696,7 @@ var init_SessionManager = __esm({
1705
1696
  override: this.store.getTitle(sessionId),
1706
1697
  kiroTitle: parts.kiroTitle,
1707
1698
  firstPrompt: parts.firstPrompt,
1708
- folder: isWorkspacePath(parts.cwd) ? void 0 : path11.basename(parts.cwd)
1699
+ folder: isManagedWorkspace(parts.cwd) ? void 0 : path11.basename(parts.cwd)
1709
1700
  });
1710
1701
  }
1711
1702
  /** Set a user title override for a session. */
@@ -1716,9 +1707,7 @@ var init_SessionManager = __esm({
1716
1707
  if (s) s.title = clean || s.title;
1717
1708
  }
1718
1709
  /**
1719
- * Re-point a session at a different working directory, for when the original was moved
1720
- * or deleted. Validates and creates the target exactly like a new session's cwd, so a
1721
- * 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
1722
1711
  * old cwd, so it is disposed and the next turn respawns in the new one, transcript intact.
1723
1712
  */
1724
1713
  async setSessionCwd(sessionId, input) {
@@ -1734,28 +1723,22 @@ var init_SessionManager = __esm({
1734
1723
  return resolved;
1735
1724
  }
1736
1725
  /**
1737
- * Restart the session's kiro child, so everything kiro only reads when a child
1738
- * starts is read again: the agent definition, the workspace's `.kiro` directory,
1739
- * 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.
1740
1730
  *
1741
- * No ACP method does this. kiro advertises no reload command, and `session/set_mode`
1742
- * leaves the MCP servers it started alone, so replacing the process is the only
1743
- * way - the transcript comes back with `session/load`.
1744
- *
1745
- * The old child is awaited out before the new one starts, because kiro flushes its
1746
- * session file on shutdown and the replacement reads that file to reload the
1747
- * conversation. Respawned eagerly rather than at the next prompt, so the caller
1748
- * gets a detail that reflects what the fresh process reported.
1749
- *
1750
- * Needs a session that has recorded a turn, so it can be loaded back. Verified
1751
- * against kiro 2.11: loading one with an empty event log fails with "Session not
1752
- * 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.
1753
1733
  */
1754
1734
  async reloadSession(sessionId) {
1755
1735
  const s = await this.ensureOpen(sessionId);
1756
1736
  if (s.running) {
1757
1737
  throw new Error("Cannot reload while a turn is running");
1758
1738
  }
1739
+ if (s.turnState.get().compacting) {
1740
+ throw new Error("Cannot reload while the conversation is being compacted");
1741
+ }
1759
1742
  if (s.reloading) {
1760
1743
  throw new Error("A reload is already running for this session");
1761
1744
  }
@@ -1842,10 +1825,7 @@ var init_SessionManager = __esm({
1842
1825
  s.createdAt = persisted.createdAt;
1843
1826
  s.updatedAt = persisted.updatedAt;
1844
1827
  s.markLive();
1845
- s.turnState.seed(
1846
- persisted.creditsSpent ?? 0,
1847
- persisted.contextUsagePercentage ?? 0
1848
- );
1828
+ s.turnState.seed(persisted.contextUsagePercentage ?? 0);
1849
1829
  this.sessions.set(sessionId, s);
1850
1830
  return s;
1851
1831
  }
@@ -1933,7 +1913,8 @@ var init_SessionManager = __esm({
1933
1913
  // -------------------------------------------------------------------------
1934
1914
  /** Create a new session. Spawns immediately so we get a real kiro sessionId. */
1935
1915
  async createSession(opts) {
1936
- 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);
1937
1918
  const tempId = `pending-${Date.now()}-${Math.floor(this.sessions.size)}`;
1938
1919
  const store = new EventStore(tempId);
1939
1920
  const s = new Session(tempId, store, cwd);
@@ -1947,6 +1928,8 @@ var init_SessionManager = __esm({
1947
1928
  this.evict(s.sessionId);
1948
1929
  throw err;
1949
1930
  }
1931
+ this.store.setChatId(s.sessionId, chatId);
1932
+ s.chatId = chatId;
1950
1933
  const name = sanitizeTitle(opts.title ?? "") || (opts.freshWorkspace ? "" : path11.basename(s.cwd));
1951
1934
  if (name) {
1952
1935
  this.store.setTitle(s.sessionId, name);
@@ -1957,7 +1940,7 @@ var init_SessionManager = __esm({
1957
1940
  // -------------------------------------------------------------------------
1958
1941
  // Actions - these spawn the process lazily.
1959
1942
  // -------------------------------------------------------------------------
1960
- async runPrompt(sessionId, content) {
1943
+ async runPrompt(sessionId, content, attachments) {
1961
1944
  const s = await this.ensureOpen(sessionId);
1962
1945
  await this.settleReload(s);
1963
1946
  if (s.running) throw new Error("A turn is already running for this session");
@@ -1977,7 +1960,13 @@ var init_SessionManager = __esm({
1977
1960
  s.title = title;
1978
1961
  }
1979
1962
  }
1980
- 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 });
1981
1970
  proc.prompt({ sessionId: s.sessionId, prompt: content }).then((res) => s.record({ kind: "turn_ended", stopReason: res.stopReason })).catch((err) => {
1982
1971
  this.log.error({ err, sessionId: s.sessionId }, "prompt turn failed");
1983
1972
  const tail = proc.stderrTail();
@@ -2012,19 +2001,28 @@ ${tail}` : err.message;
2012
2001
  s.modelId = modelId;
2013
2002
  s.lastActivity = Date.now();
2014
2003
  }
2015
- async execCommand(sessionId, command, args) {
2004
+ async execCommand(sessionId, command) {
2016
2005
  const s = await this.ensureOpen(sessionId);
2017
2006
  await this.settleReload(s);
2018
2007
  const proc = await this.ensureProc(s);
2019
- await proc.execCommand(s.sessionId, command, args);
2008
+ await proc.execCommand(s.sessionId, command);
2020
2009
  s.lastActivity = Date.now();
2021
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
+ */
2022
2019
  summaryOf(live, persisted, transcript) {
2023
2020
  const sessionId = live?.sessionId ?? persisted.sessionId;
2024
2021
  const snap = live?.turnState.get();
2025
2022
  const cwd = live?.cwd ?? this.store.getCwd(sessionId) ?? persisted.cwd;
2026
2023
  return {
2027
2024
  sessionId,
2025
+ chatId: live?.chatId ?? this.store.getChatId(sessionId),
2028
2026
  title: this.titleOf(sessionId, {
2029
2027
  // kiro's file is what kiro called it; the live copy is only a cache of it.
2030
2028
  kiroTitle: persisted?.title || live?.title,
@@ -2039,9 +2037,8 @@ ${tail}` : err.message;
2039
2037
  agentId: live?.agentId ?? persisted?.agentId,
2040
2038
  modelId: live?.modelId ?? persisted?.modelId,
2041
2039
  running: live?.running ?? false,
2042
- // A dormant session's totals live in kiro's file; a live one's snapshot
2043
- // starts at zero until the first turn reports, so fall back to the file.
2044
- 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.
2045
2042
  contextUsagePercentage: snap?.contextUsagePercentage || persisted?.contextUsagePercentage
2046
2043
  };
2047
2044
  }
@@ -2063,25 +2060,12 @@ ${tail}` : err.message;
2063
2060
  }
2064
2061
  async getDetail(sessionId) {
2065
2062
  const [transcript, persisted] = await Promise.all([
2066
- hydrateTranscript(sessionId),
2063
+ hydrateTranscript(sessionId, this.store.attachmentsBySession(sessionId)),
2067
2064
  readPersistedSession(sessionId)
2068
2065
  ]);
2069
2066
  const s = this.sessions.get(sessionId);
2070
- if (s) return this.buildDetail(s, transcript, persisted ?? void 0);
2071
- if (!persisted) throw new Error(`Unknown session: ${sessionId}`);
2072
- return {
2073
- summary: this.summaryOf(void 0, persisted, transcript),
2074
- modes: [],
2075
- currentModeId: persisted.agentId,
2076
- transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
2077
- transcriptTotal: transcript.length,
2078
- observability: {
2079
- ...emptyObservabilitySnapshot(),
2080
- creditsSpent: persisted.creditsSpent ?? 0,
2081
- contextUsagePercentage: persisted.contextUsagePercentage ?? 0
2082
- },
2083
- head: 0
2084
- };
2067
+ if (!s && !persisted) throw new Error(`Unknown session: ${sessionId}`);
2068
+ return this.buildDetail(s, transcript, persisted ?? void 0);
2085
2069
  }
2086
2070
  /**
2087
2071
  * A slice of the transcript for lazy "load older on scroll up". Re-hydrates
@@ -2089,29 +2073,34 @@ ${tail}` : err.message;
2089
2073
  * requested window. offset/limit are clamped to the transcript bounds.
2090
2074
  */
2091
2075
  async getTranscriptPage(sessionId, offset, limit) {
2092
- const transcript = await hydrateTranscript(sessionId);
2076
+ const transcript = await hydrateTranscript(
2077
+ sessionId,
2078
+ this.store.attachmentsBySession(sessionId)
2079
+ );
2093
2080
  const start = Math.max(0, Math.min(offset, transcript.length));
2094
2081
  const end = Math.max(start, Math.min(start + limit, transcript.length));
2095
2082
  return transcript.slice(start, end);
2096
2083
  }
2084
+ /** One projection, so a dormant session and a live one cannot disagree. */
2097
2085
  buildDetail(s, transcript, persisted) {
2098
- const snap = s.turnState.get();
2099
2086
  return {
2100
2087
  summary: this.summaryOf(s, persisted, transcript),
2101
- modes: s.availableModes,
2102
- currentModeId: s.currentModeId,
2088
+ modes: s?.availableModes ?? [],
2089
+ currentModeId: s?.currentModeId ?? persisted?.agentId,
2103
2090
  // Only the tail is sent on load; replayHead/title use the full transcript.
2104
2091
  transcript: transcript.slice(-TRANSCRIPT_PAGE_SIZE),
2105
2092
  transcriptTotal: transcript.length,
2106
- observability: snap,
2107
- 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
2108
2098
  };
2109
2099
  }
2110
2100
  /**
2111
- * The cursor a reconnecting client should start from. Normally the store head, but kiro writes
2112
- * a turn to its jsonl only once the turn completes, so a hydrated transcript is missing one in
2113
- * flight. Rewind to just before its turn_started and let the WS replay the whole turn. Guarded
2114
- * 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.
2115
2104
  */
2116
2105
  replayHead(s, transcript) {
2117
2106
  const head = s.store.head();
@@ -2216,13 +2205,17 @@ var init_rateLimit = __esm({
2216
2205
  });
2217
2206
 
2218
2207
  // server/src/routes/auth.ts
2219
- import crypto4 from "node:crypto";
2208
+ import crypto3 from "node:crypto";
2220
2209
  import fastifyCookie from "@fastify/cookie";
2210
+ function logins() {
2211
+ loginStore ??= new LoginStore();
2212
+ return loginStore;
2213
+ }
2221
2214
  function secretMatches(supplied, expected) {
2222
2215
  if (typeof supplied !== "string") return false;
2223
- const a = crypto4.createHash("sha256").update(supplied).digest();
2224
- const b = crypto4.createHash("sha256").update(expected).digest();
2225
- 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);
2226
2219
  }
2227
2220
  function extractToken(req) {
2228
2221
  const auth = req.headers["authorization"];
@@ -2240,7 +2233,7 @@ function sessionToken(req) {
2240
2233
  return req.cookies?.[SESSION_COOKIE];
2241
2234
  }
2242
2235
  function hasValidSession(req) {
2243
- return logins.verify(sessionToken(req)) !== null;
2236
+ return logins().verify(sessionToken(req)) !== null;
2244
2237
  }
2245
2238
  function cookieOptions() {
2246
2239
  return {
@@ -2268,27 +2261,27 @@ async function registerAuth(app) {
2268
2261
  loginLimiter.succeed(req.ip);
2269
2262
  }
2270
2263
  const ua = req.headers["user-agent"];
2271
- const { token } = logins.create(typeof ua === "string" ? ua : void 0);
2264
+ const { token } = logins().create(typeof ua === "string" ? ua : void 0);
2272
2265
  reply.setCookie(SESSION_COOKIE, token, cookieOptions());
2273
2266
  return { ok: true };
2274
2267
  });
2275
2268
  app.post("/api/logout", async (req, reply) => {
2276
- logins.revokeToken(sessionToken(req));
2269
+ logins().revokeToken(sessionToken(req));
2277
2270
  reply.clearCookie(SESSION_COOKIE, { path: "/" });
2278
2271
  return { ok: true };
2279
2272
  });
2280
2273
  app.get("/api/devices", async (req) => {
2281
- return { devices: logins.list(sessionToken(req)) };
2274
+ return { devices: logins().list(sessionToken(req)) };
2282
2275
  });
2283
2276
  app.delete("/api/devices/:id", async (req, reply) => {
2284
2277
  const { id } = req.params;
2285
- const current = logins.list(sessionToken(req)).find((d) => d.current);
2286
- const removed = logins.revokeId(id);
2278
+ const current = logins().list(sessionToken(req)).find((d) => d.current);
2279
+ const removed = logins().revokeId(id);
2287
2280
  if (current?.id === id) reply.clearCookie(SESSION_COOKIE, { path: "/" });
2288
2281
  return { ok: removed };
2289
2282
  });
2290
2283
  app.post("/api/logout-all", async (_req, reply) => {
2291
- logins.revokeAll();
2284
+ logins().revokeAll();
2292
2285
  reply.clearCookie(SESSION_COOKIE, { path: "/" });
2293
2286
  return { ok: true };
2294
2287
  });
@@ -2300,7 +2293,7 @@ async function registerAuth(app) {
2300
2293
  reply.code(401).send({ error: "Unauthorized: log in first" });
2301
2294
  });
2302
2295
  }
2303
- var SESSION_COOKIE, logins, loginLimiter;
2296
+ var SESSION_COOKIE, loginStore, loginLimiter;
2304
2297
  var init_auth = __esm({
2305
2298
  "server/src/routes/auth.ts"() {
2306
2299
  "use strict";
@@ -2308,7 +2301,6 @@ var init_auth = __esm({
2308
2301
  init_logins();
2309
2302
  init_rateLimit();
2310
2303
  SESSION_COOKIE = "casper.sid";
2311
- logins = new LoginStore();
2312
2304
  loginLimiter = new AttemptLimiter(10, 15 * 60 * 1e3);
2313
2305
  }
2314
2306
  });
@@ -2522,15 +2514,15 @@ function looksBinary(buf) {
2522
2514
  }
2523
2515
  function classifyKind(name) {
2524
2516
  const ext = path13.extname(name).toLowerCase();
2525
- if (ext in IMAGE_EXTS2) return "image";
2517
+ if (ext in IMAGE_EXTS) return "image";
2526
2518
  if (TEXT_EXTS.has(ext)) return "text";
2527
2519
  return "binary";
2528
2520
  }
2529
- var IMAGE_EXTS2, TEXT_EXTS, MIME_MAP;
2521
+ var IMAGE_EXTS, TEXT_EXTS, MIME_MAP;
2530
2522
  var init_filekind = __esm({
2531
2523
  "server/src/util/filekind.ts"() {
2532
2524
  "use strict";
2533
- IMAGE_EXTS2 = {
2525
+ IMAGE_EXTS = {
2534
2526
  ".png": "image/png",
2535
2527
  ".jpg": "image/jpeg",
2536
2528
  ".jpeg": "image/jpeg",
@@ -2602,7 +2594,7 @@ var init_filekind = __esm({
2602
2594
  ".tex"
2603
2595
  ]);
2604
2596
  MIME_MAP = {
2605
- ...IMAGE_EXTS2,
2597
+ ...IMAGE_EXTS,
2606
2598
  ".txt": "text/plain",
2607
2599
  ".md": "text/markdown",
2608
2600
  ".csv": "text/csv",
@@ -2630,10 +2622,128 @@ var init_filekind = __esm({
2630
2622
  }
2631
2623
  });
2632
2624
 
2633
- // server/src/routes/fs.ts
2625
+ // server/src/routes/filePreview.ts
2634
2626
  import fs12 from "node:fs/promises";
2635
2627
  import { createReadStream } from "node:fs";
2636
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";
2637
2747
  function registerFsRoutes(app) {
2638
2748
  app.get(
2639
2749
  "/api/fs/dirs",
@@ -2641,10 +2751,10 @@ function registerFsRoutes(app) {
2641
2751
  const input = (req.query.path ?? "").trim();
2642
2752
  const base = config.defaultCwd;
2643
2753
  const endsWithSep = input.endsWith("/");
2644
- const resolved = input ? path14.resolve(base, input) : base;
2645
- const dir = endsWithSep || !input ? resolved : path14.dirname(resolved);
2646
- const prefix = endsWithSep || !input ? "" : path14.basename(resolved);
2647
- 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");
2648
2758
  const listing = await resolveAbsolutePath(dir, "directory");
2649
2759
  if (!listing.ok) {
2650
2760
  if (listing.status === 403) return replyWith(reply, listing);
@@ -2653,7 +2763,7 @@ function registerFsRoutes(app) {
2653
2763
  const realDir = listing.real;
2654
2764
  let entries = [];
2655
2765
  try {
2656
- const dirents = await fs12.readdir(realDir, { withFileTypes: true });
2766
+ const dirents = await fs13.readdir(realDir, { withFileTypes: true });
2657
2767
  const checks2 = await Promise.all(
2658
2768
  dirents.map(async (d) => {
2659
2769
  const target = await classifyDirent(realDir, d, absoluteRoots());
@@ -2664,7 +2774,7 @@ function registerFsRoutes(app) {
2664
2774
  entries = checks2.filter((name) => name !== null).filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())).sort((a, b) => {
2665
2775
  if (isDot(a) !== isDot(b)) return isDot(a) ? 1 : -1;
2666
2776
  return a.localeCompare(b);
2667
- }).slice(0, 500).map((name) => path14.join(dir, name));
2777
+ }).slice(0, 500).map((name) => path15.join(dir, name));
2668
2778
  } catch {
2669
2779
  entries = [];
2670
2780
  }
@@ -2672,51 +2782,38 @@ function registerFsRoutes(app) {
2672
2782
  }
2673
2783
  );
2674
2784
  app.get(
2675
- "/api/fs/image",
2785
+ "/api/fs/file",
2676
2786
  async (req, reply) => {
2677
2787
  const filePath = (req.query.path ?? "").trim();
2678
2788
  if (!filePath) {
2679
2789
  reply.code(400);
2680
2790
  return { error: "path parameter is required" };
2681
2791
  }
2682
- if (!path14.isAbsolute(filePath)) {
2792
+ if (!path15.isAbsolute(filePath)) {
2683
2793
  reply.code(400);
2684
2794
  return { error: "path must be absolute" };
2685
2795
  }
2686
- const ext = path14.extname(filePath).toLowerCase();
2687
- if (classifyKind(filePath) !== "image") {
2688
- reply.code(400);
2689
- return { error: `Not a supported image type: ${ext}` };
2690
- }
2691
- const resolvedImage = await resolveAbsolutePath(filePath, "file");
2692
- if (!resolvedImage.ok) return replyWith(reply, resolvedImage);
2693
- const { real, stat } = resolvedImage;
2694
- if (stat.size > MAX_IMAGE_BYTES) {
2695
- reply.code(413);
2696
- return { error: "Image too large" };
2697
- }
2698
- const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2699
- reply.header("Cache-Control", "private, no-cache");
2700
- reply.header("ETag", etag);
2701
- reply.header("Last-Modified", stat.mtime.toUTCString());
2702
- if (req.headers["if-none-match"] === etag) {
2703
- reply.code(304);
2704
- 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));
2705
2806
  }
2706
- reply.header("Content-Type", mimeForExt(ext));
2707
- reply.header("Content-Length", stat.size);
2708
- return reply.send(createReadStream(real));
2807
+ return sendFilePreview(req, reply, resolved.real, resolved.stat);
2709
2808
  }
2710
2809
  );
2711
2810
  }
2712
- var MAX_IMAGE_BYTES;
2713
2811
  var init_fs = __esm({
2714
2812
  "server/src/routes/fs.ts"() {
2715
2813
  "use strict";
2716
2814
  init_config();
2717
2815
  init_confinedFile();
2718
- init_filekind();
2719
- MAX_IMAGE_BYTES = 20 * 1024 * 1024;
2816
+ init_filePreview();
2720
2817
  }
2721
2818
  });
2722
2819
 
@@ -2733,7 +2830,11 @@ function registerSessionRoutes(app, manager) {
2733
2830
  agentId: body.agentId,
2734
2831
  modelId: body.modelId,
2735
2832
  freshWorkspace: body.freshWorkspace,
2736
- 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
2737
2838
  });
2738
2839
  } catch (err) {
2739
2840
  reply.code(400);
@@ -2769,7 +2870,7 @@ function registerSessionRoutes(app, manager) {
2769
2870
  "/api/sessions/:id/prompt",
2770
2871
  async (req, reply) => {
2771
2872
  try {
2772
- await manager.runPrompt(req.params.id, req.body.prompt);
2873
+ await manager.runPrompt(req.params.id, req.body.prompt, req.body.attachments);
2773
2874
  return { ok: true };
2774
2875
  } catch (err) {
2775
2876
  reply.code(400);
@@ -2838,34 +2939,9 @@ var init_sessions = __esm({
2838
2939
  });
2839
2940
 
2840
2941
  // server/src/routes/workspace.ts
2841
- import fs13 from "node:fs/promises";
2842
- import { createReadStream as createReadStream2 } from "node:fs";
2843
- import path15 from "node:path";
2844
- function tooLargeForPreview(size, maxBytes) {
2845
- return {
2846
- error: `File too large for preview (${(size / 1024 / 1024).toFixed(1)} MB, max ${maxBytes / 1024 / 1024} MB)`
2847
- };
2848
- }
2849
- function hexdump(buf) {
2850
- const lines = [];
2851
- for (let off = 0; off < buf.length; off += 16) {
2852
- const slice = buf.subarray(off, off + 16);
2853
- const hex = [];
2854
- let ascii = "";
2855
- for (let i = 0; i < 16; i++) {
2856
- if (i < slice.length) {
2857
- const b = slice[i];
2858
- hex.push(b.toString(16).padStart(2, "0"));
2859
- ascii += b >= 32 && b <= 126 ? String.fromCharCode(b) : ".";
2860
- } else {
2861
- hex.push(" ");
2862
- }
2863
- if (i === 7) hex.push("");
2864
- }
2865
- lines.push(`${off.toString(16).padStart(8, "0")} ${hex.join(" ")} |${ascii}|`);
2866
- }
2867
- return lines.join("\n");
2868
- }
2942
+ import fs14 from "node:fs/promises";
2943
+ import { createReadStream as createReadStream3 } from "node:fs";
2944
+ import path16 from "node:path";
2869
2945
  function registerWorkspaceRoutes(app, manager) {
2870
2946
  app.get(
2871
2947
  "/api/sessions/:id/tree",
@@ -2880,7 +2956,7 @@ function registerWorkspaceRoutes(app, manager) {
2880
2956
  const { cwd, relative, real: realTarget } = resolved;
2881
2957
  let dirents;
2882
2958
  try {
2883
- dirents = await fs13.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2959
+ dirents = await fs14.readdir(realTarget, { withFileTypes: true, encoding: "utf8" });
2884
2960
  } catch {
2885
2961
  return replyWith(reply, await workspaceNotFound(cwd));
2886
2962
  }
@@ -2894,7 +2970,7 @@ function registerWorkspaceRoutes(app, manager) {
2894
2970
  continue;
2895
2971
  }
2896
2972
  try {
2897
- const stat = await fs13.stat(target.real);
2973
+ const stat = await fs14.stat(target.real);
2898
2974
  entries.push({
2899
2975
  name: d.name,
2900
2976
  path: entryRelative,
@@ -2923,8 +2999,8 @@ function registerWorkspaceRoutes(app, manager) {
2923
2999
  reply.code(413);
2924
3000
  return { error: `File too large (${(stat.size / 1024 / 1024).toFixed(1)} MB, max 100 MB)` };
2925
3001
  }
2926
- const ext = path15.extname(realTarget);
2927
- const filename = path15.basename(realTarget);
3002
+ const ext = path16.extname(realTarget);
3003
+ const filename = path16.basename(realTarget);
2928
3004
  const asciiName = filename.replace(/[^\w.\-]/g, "_");
2929
3005
  reply.header("Content-Type", mimeForExt(ext));
2930
3006
  reply.header(
@@ -2932,7 +3008,7 @@ function registerWorkspaceRoutes(app, manager) {
2932
3008
  `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(filename)}`
2933
3009
  );
2934
3010
  reply.header("Content-Length", stat.size);
2935
- return reply.send(createReadStream2(realTarget));
3011
+ return reply.send(createReadStream3(realTarget));
2936
3012
  }
2937
3013
  );
2938
3014
  app.get(
@@ -2941,132 +3017,52 @@ function registerWorkspaceRoutes(app, manager) {
2941
3017
  const resolved = await resolveSessionPath(manager, req.params.id, req.query.path, "file");
2942
3018
  if (!resolved.ok) return replyWith(reply, resolved);
2943
3019
  const { real: realTarget, stat } = resolved;
2944
- const ext = path15.extname(realTarget).toLowerCase();
2945
- const mime = mimeForExt(ext);
2946
- const isImage = mime.startsWith("image/");
2947
- const isPdf = mime === "application/pdf";
2948
- const isHtml = mime === "text/html" && req.query.raw === "1";
2949
- const kind = classifyKind(realTarget);
2950
- if (isImage || isPdf) {
2951
- if (stat.size > MAX_IMAGE_PREVIEW_BYTES) {
2952
- reply.code(413);
2953
- return tooLargeForPreview(stat.size, MAX_IMAGE_PREVIEW_BYTES);
2954
- }
2955
- } else if (kind !== "binary") {
2956
- if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2957
- reply.code(413);
2958
- return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2959
- }
2960
- }
2961
- if (isHtml) {
2962
- if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2963
- reply.code(413);
2964
- return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2965
- }
2966
- reply.header("Content-Security-Policy", "sandbox allow-scripts allow-forms");
2967
- reply.header("Content-Type", "text/html; charset=utf-8");
2968
- reply.header("Cache-Control", "no-store");
2969
- return reply.send(createReadStream2(realTarget));
2970
- }
2971
- if (isImage || isPdf) {
2972
- const etag = `W/"${stat.size}-${stat.mtimeMs}"`;
2973
- reply.header("Cache-Control", "private, no-cache");
2974
- reply.header("ETag", etag);
2975
- reply.header("Last-Modified", stat.mtime.toUTCString());
2976
- const ifNoneMatch = req.headers["if-none-match"];
2977
- if (ifNoneMatch === etag) {
2978
- reply.code(304);
2979
- return reply.send();
2980
- }
2981
- reply.header("Content-Type", mime);
2982
- reply.header("Content-Disposition", "inline");
2983
- reply.header("Content-Length", stat.size);
2984
- return reply.send(createReadStream2(realTarget));
2985
- }
2986
- if (kind === "binary") {
2987
- let fh;
2988
- try {
2989
- fh = await fs13.open(realTarget, "r");
2990
- const buf = Buffer.alloc(Math.min(HEXDUMP_BYTES, stat.size));
2991
- const { bytesRead } = await fh.read(buf, 0, buf.length, 0);
2992
- const head = buf.subarray(0, bytesRead);
2993
- if (!looksBinary(head)) {
2994
- if (stat.size > MAX_TEXT_PREVIEW_BYTES) {
2995
- reply.code(413);
2996
- return tooLargeForPreview(stat.size, MAX_TEXT_PREVIEW_BYTES);
2997
- }
2998
- reply.header("Content-Type", "text/plain; charset=utf-8");
2999
- reply.header("Content-Disposition", "inline");
3000
- reply.header("Content-Length", stat.size);
3001
- return reply.send(createReadStream2(realTarget));
3002
- }
3003
- const dump = hexdump(head);
3004
- const header = `Binary file - ${stat.size} bytes
3005
- Showing first ${bytesRead} bytes as hexdump:
3006
-
3007
- `;
3008
- const body = header + dump + (stat.size > bytesRead ? "\n\n\u2026 (truncated)" : "");
3009
- reply.header("Content-Type", "text/plain; charset=utf-8");
3010
- reply.header("Content-Disposition", "inline");
3011
- return reply.send(body);
3012
- } finally {
3013
- await fh?.close();
3014
- }
3015
- }
3016
- reply.header("Content-Type", "text/plain; charset=utf-8");
3017
- reply.header("Content-Disposition", "inline");
3018
- reply.header("Content-Length", stat.size);
3019
- return reply.send(createReadStream2(realTarget));
3020
+ return sendFilePreview(req, reply, realTarget, stat);
3020
3021
  }
3021
3022
  );
3022
3023
  }
3023
- var MAX_DOWNLOAD_BYTES, HEXDUMP_BYTES, MAX_TEXT_PREVIEW_BYTES, MAX_IMAGE_PREVIEW_BYTES;
3024
+ var MAX_DOWNLOAD_BYTES;
3024
3025
  var init_workspace = __esm({
3025
3026
  "server/src/routes/workspace.ts"() {
3026
3027
  "use strict";
3027
3028
  init_config();
3028
3029
  init_confinedFile();
3029
3030
  init_filekind();
3031
+ init_filePreview();
3030
3032
  MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024;
3031
- HEXDUMP_BYTES = 4096;
3032
- MAX_TEXT_PREVIEW_BYTES = 1024 * 1024;
3033
- MAX_IMAGE_PREVIEW_BYTES = 20 * 1024 * 1024;
3034
3033
  }
3035
3034
  });
3036
3035
 
3037
3036
  // server/src/routes/uploads.ts
3038
- import { createReadStream as createReadStream3, createWriteStream } from "node:fs";
3039
- import fs14 from "node:fs/promises";
3040
- import path16 from "node:path";
3041
- 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";
3042
3041
  import { execFile as execFile3 } from "node:child_process";
3043
3042
  import { pipeline } from "node:stream/promises";
3044
- function uploadDirFor(sessionId) {
3045
- return path16.join(config.casperDataDir, "sessions", sessionId, "uploads");
3046
- }
3047
3043
  function sanitizeName(raw) {
3048
- const base = path16.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
3044
+ const base = path17.basename(raw).replace(/[^\w.\- ]/g, "_").trim();
3049
3045
  if (!base || base === "." || base === "..") return `upload-${Date.now()}`;
3050
3046
  return base.replace(/^\.+/, "") || `upload-${Date.now()}`;
3051
3047
  }
3052
3048
  async function uniquePath(dir, name) {
3053
- const ext = path16.extname(name);
3049
+ const ext = path17.extname(name);
3054
3050
  const stem = name.slice(0, name.length - ext.length);
3055
3051
  let candidate = name;
3056
3052
  let n = 1;
3057
3053
  while (true) {
3058
3054
  try {
3059
- await fs14.access(path16.join(dir, candidate));
3055
+ await fs15.access(path17.join(dir, candidate));
3060
3056
  candidate = `${stem} (${n++})${ext}`;
3061
3057
  } catch {
3062
- return path16.join(dir, candidate);
3058
+ return path17.join(dir, candidate);
3063
3059
  }
3064
3060
  }
3065
3061
  }
3066
3062
  function sha256File(absPath) {
3067
3063
  return new Promise((resolve, reject) => {
3068
- const hash = crypto5.createHash("sha256");
3069
- 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);
3070
3066
  });
3071
3067
  }
3072
3068
  function fileType(absPath) {
@@ -3079,7 +3075,7 @@ function fileType(absPath) {
3079
3075
  async function sampleStrings(absPath, limit = 40) {
3080
3076
  let fh;
3081
3077
  try {
3082
- fh = await fs14.open(absPath, "r");
3078
+ fh = await fs15.open(absPath, "r");
3083
3079
  const buf = Buffer.alloc(STRINGS_SCAN_BYTES);
3084
3080
  const { bytesRead } = await fh.read(buf, 0, STRINGS_SCAN_BYTES, 0);
3085
3081
  const out = [];
@@ -3102,22 +3098,16 @@ async function sampleStrings(absPath, limit = 40) {
3102
3098
  await fh?.close();
3103
3099
  }
3104
3100
  }
3105
- function registerUploadRoutes(app, manager) {
3101
+ function registerUploadRoutes(app) {
3106
3102
  app.post(
3107
- "/api/sessions/:id/uploads",
3103
+ "/api/chats/:chatId/uploads",
3108
3104
  async (req, reply) => {
3109
- try {
3110
- await manager.getSessionCwd(req.params.id);
3111
- } catch {
3112
- reply.code(404);
3113
- return { error: "Session not found" };
3114
- }
3115
- if (!isValidSessionId(req.params.id)) {
3105
+ if (!isValidChatId(req.params.chatId)) {
3116
3106
  reply.code(400);
3117
- return { error: "Invalid session id" };
3107
+ return { error: "Invalid chat id" };
3118
3108
  }
3119
- const uploadDir = uploadDirFor(req.params.id);
3120
- await fs14.mkdir(uploadDir, { recursive: true, mode: 448 });
3109
+ const uploadDir = chatUploadsDir(req.params.chatId);
3110
+ await fs15.mkdir(uploadDir, { recursive: true, mode: 448 });
3121
3111
  const results = [];
3122
3112
  const parts = req.files();
3123
3113
  for await (const part of parts) {
@@ -3131,17 +3121,17 @@ function registerUploadRoutes(app, manager) {
3131
3121
  return { error: `Failed to store ${name}` };
3132
3122
  }
3133
3123
  if (part.file.truncated) {
3134
- await fs14.rm(dest, { force: true });
3124
+ await fs15.rm(dest, { force: true });
3135
3125
  reply.code(413);
3136
3126
  return {
3137
3127
  error: `${name} exceeds the max upload size (${Math.round(config.maxUploadBytes / 1024 / 1024)} MB)`
3138
3128
  };
3139
3129
  }
3140
- const stat = await fs14.stat(dest);
3141
- const ext = path16.extname(dest);
3130
+ const stat = await fs15.stat(dest);
3131
+ const ext = path17.extname(dest);
3142
3132
  const kind = classifyKind(dest);
3143
3133
  const uploaded = {
3144
- name: path16.basename(dest),
3134
+ name: path17.basename(dest),
3145
3135
  // Absolute, because the file sits outside the session's workspace: a
3146
3136
  // relative path would resolve against the wrong root for both the agent
3147
3137
  // and the image endpoint.
@@ -3151,14 +3141,14 @@ function registerUploadRoutes(app, manager) {
3151
3141
  kind
3152
3142
  };
3153
3143
  if (kind === "binary") {
3154
- const [type, sha2563, strings] = await Promise.all([
3144
+ const [type, sha2562, strings] = await Promise.all([
3155
3145
  fileType(dest),
3156
3146
  sha256File(dest),
3157
3147
  sampleStrings(dest)
3158
3148
  ]);
3159
3149
  uploaded.triage = {
3160
3150
  fileType: type || void 0,
3161
- sha256: sha2563,
3151
+ sha256: sha2562,
3162
3152
  strings: strings.length ? strings : void 0
3163
3153
  };
3164
3154
  }
@@ -3176,6 +3166,7 @@ var init_uploads = __esm({
3176
3166
  init_config();
3177
3167
  init_filekind();
3178
3168
  init_paths2();
3169
+ init_chats();
3179
3170
  STRINGS_SCAN_BYTES = 256 * 1024;
3180
3171
  }
3181
3172
  });
@@ -3199,7 +3190,7 @@ var init_health = __esm({
3199
3190
  });
3200
3191
 
3201
3192
  // server/src/ws/dirWatchers.ts
3202
- import fs15 from "node:fs";
3193
+ import fs16 from "node:fs";
3203
3194
  function diffWatchSet(current, next) {
3204
3195
  const have = new Set(current);
3205
3196
  const want = new Set(next.slice(0, MAX_WATCHES));
@@ -3223,7 +3214,7 @@ function createDirWatchers(opts) {
3223
3214
  const target = await opts.resolve(relative);
3224
3215
  if (!target || closed) return;
3225
3216
  try {
3226
- const watcher = fs15.watch(target, { persistent: false }, () => {
3217
+ const watcher = fs16.watch(target, { persistent: false }, () => {
3227
3218
  const existing = timers.get(relative);
3228
3219
  if (existing) clearTimeout(existing);
3229
3220
  timers.set(
@@ -3281,7 +3272,7 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3281
3272
  return null;
3282
3273
  }
3283
3274
  },
3284
- onChange: (path19) => send(socket, { type: "fs_changed", path: path19 })
3275
+ onChange: (path20) => send(socket, { type: "fs_changed", path: path20 })
3285
3276
  });
3286
3277
  const forward = (event) => {
3287
3278
  if (event.seq <= cursor) return;
@@ -3332,7 +3323,7 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3332
3323
  case "watch_paths":
3333
3324
  return watchers.sync(msg.paths);
3334
3325
  case "prompt":
3335
- return ack("prompt", () => manager.runPrompt(sessionId, msg.content));
3326
+ return ack("prompt", () => manager.runPrompt(sessionId, msg.content, msg.attachments));
3336
3327
  case "cancel":
3337
3328
  return ack("cancel", () => manager.cancel(sessionId));
3338
3329
  case "set_mode":
@@ -3342,7 +3333,7 @@ function handleConnection(socket, manager, sessionId, startCursor) {
3342
3333
  case "exec_command":
3343
3334
  return ack(
3344
3335
  "exec_command",
3345
- () => manager.execCommand(sessionId, msg.command, msg.args)
3336
+ () => manager.execCommand(sessionId, msg.command)
3346
3337
  );
3347
3338
  default:
3348
3339
  return send(socket, { type: "error", message: "Unknown message type" });
@@ -3414,9 +3405,8 @@ var init_gateway = __esm({
3414
3405
  });
3415
3406
 
3416
3407
  // server/src/app.ts
3417
- import fs16 from "node:fs";
3408
+ import fs17 from "node:fs";
3418
3409
  import Fastify from "fastify";
3419
- import cors from "@fastify/cors";
3420
3410
  import websocket from "@fastify/websocket";
3421
3411
  import fastifyStatic from "@fastify/static";
3422
3412
  import multipart from "@fastify/multipart";
@@ -3436,7 +3426,6 @@ async function buildApp() {
3436
3426
  });
3437
3427
  const manager = new SessionManager(logger);
3438
3428
  const startedAt = Date.now();
3439
- await app.register(cors, { origin: true });
3440
3429
  await app.register(websocket, {
3441
3430
  options: { maxPayload: 16 * 1024 * 1024 }
3442
3431
  });
@@ -3450,22 +3439,22 @@ async function buildApp() {
3450
3439
  registerFsRoutes(app);
3451
3440
  registerSessionRoutes(app, manager);
3452
3441
  registerWorkspaceRoutes(app, manager);
3453
- registerUploadRoutes(app, manager);
3442
+ registerUploadRoutes(app);
3454
3443
  registerWsGateway(app, manager);
3455
- if (fs16.existsSync(config.webDist)) {
3444
+ if (fs17.existsSync(config.webDist)) {
3456
3445
  await app.register(fastifyStatic, {
3457
3446
  root: config.webDist
3458
3447
  });
3459
3448
  app.addHook("onSend", async (req, reply) => {
3460
- const path19 = req.url.split("?")[0] ?? "";
3461
- if (path19 === "/manifest.json" || path19.startsWith("/icons/")) {
3449
+ const path20 = req.url.split("?")[0] ?? "";
3450
+ if (path20 === "/manifest.json" || path20.startsWith("/icons/")) {
3462
3451
  reply.header("cache-control", "public, max-age=300");
3463
3452
  }
3464
3453
  });
3465
3454
  app.setNotFoundHandler((req, reply) => {
3466
- const path19 = req.url.split("?")[0] ?? "";
3467
- const looksLikeAsset = path19.startsWith("/assets/") || /\.[a-zA-Z0-9]+$/.test(path19);
3468
- 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) {
3469
3458
  reply.code(404).send({ error: "Not found" });
3470
3459
  return;
3471
3460
  }
@@ -3539,15 +3528,15 @@ __export(doctor_exports, {
3539
3528
  mcpWiring: () => mcpWiring
3540
3529
  });
3541
3530
  import { execFileSync as execFileSync3 } from "node:child_process";
3542
- import fs17 from "node:fs";
3543
- import path17 from "node:path";
3531
+ import fs18 from "node:fs";
3532
+ import path18 from "node:path";
3544
3533
  function which(bin) {
3545
- if (bin.includes("/")) return fs17.existsSync(bin) ? bin : void 0;
3546
- 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)) {
3547
3536
  if (dir === "") continue;
3548
- const candidate = path17.join(dir, bin);
3537
+ const candidate = path18.join(dir, bin);
3549
3538
  try {
3550
- fs17.accessSync(candidate, fs17.constants.X_OK);
3539
+ fs18.accessSync(candidate, fs18.constants.X_OK);
3551
3540
  return candidate;
3552
3541
  } catch {
3553
3542
  }
@@ -3571,7 +3560,7 @@ function mcpWiring(agentJson, resolve) {
3571
3560
  return { ok: false, detail: `${command || "(no command)"} not found - widgets are unavailable` };
3572
3561
  }
3573
3562
  const script = args.find((a) => a.endsWith(".js"));
3574
- if (script && !fs17.existsSync(script)) {
3563
+ if (script && !fs18.existsSync(script)) {
3575
3564
  return { ok: false, detail: `${script} is gone - delete the agent file and restart to rewrite it` };
3576
3565
  }
3577
3566
  return { ok: true, detail: script ?? `${command} ${args.join(" ")}` };
@@ -3603,7 +3592,7 @@ function checks() {
3603
3592
  const settings = readSettings(config.configFile);
3604
3593
  const hasToken = typeof settings.token === "string" && settings.token !== "";
3605
3594
  out.push(
3606
- fs17.existsSync(config.configFile) ? {
3595
+ fs18.existsSync(config.configFile) ? {
3607
3596
  ok: hasToken,
3608
3597
  name: "settings",
3609
3598
  detail: hasToken ? `${config.configFile} (token set)` : `${config.configFile} exists but has no token - auth is DISABLED`
@@ -3613,8 +3602,8 @@ function checks() {
3613
3602
  detail: `${config.configFile} not found; a token is generated on first run`
3614
3603
  }
3615
3604
  );
3616
- if (fs17.existsSync(config.configFile)) {
3617
- const mode = fs17.statSync(config.configFile).mode & 511;
3605
+ if (fs18.existsSync(config.configFile)) {
3606
+ const mode = fs18.statSync(config.configFile).mode & 511;
3618
3607
  out.push({
3619
3608
  ok: mode === 384,
3620
3609
  name: "settings mode",
@@ -3622,10 +3611,10 @@ function checks() {
3622
3611
  });
3623
3612
  }
3624
3613
  try {
3625
- fs17.mkdirSync(config.casperDataDir, { recursive: true });
3626
- fs17.accessSync(config.casperDataDir, fs17.constants.W_OK);
3627
- const db2 = path17.join(config.casperDataDir, "casper.db");
3628
- 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` : "";
3629
3618
  out.push({ ok: true, name: "data dir", detail: `${config.casperDataDir} writable${size}` });
3630
3619
  } catch (err) {
3631
3620
  out.push({
@@ -3636,22 +3625,22 @@ function checks() {
3636
3625
  });
3637
3626
  }
3638
3627
  out.push(
3639
- 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 } : {
3640
3629
  ok: false,
3641
3630
  fatal: true,
3642
3631
  name: "web app",
3643
3632
  detail: `${config.webDist} not found - the package is incomplete or CASPER_WEB_DIST is wrong`
3644
3633
  }
3645
3634
  );
3646
- const agent = path17.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3647
- const hasAgent = fs17.existsSync(agent);
3635
+ const agent = path18.join(config.kiroSessionsDir, "..", "..", "agents", "casper.json");
3636
+ const hasAgent = fs18.existsSync(agent);
3648
3637
  out.push({
3649
3638
  ok: hasAgent,
3650
3639
  name: "casper agent",
3651
3640
  detail: hasAgent ? agent : `${agent} missing - it is installed on first run`
3652
3641
  });
3653
3642
  if (hasAgent) {
3654
- const wiring = mcpWiring(fs17.readFileSync(agent, "utf8"), which);
3643
+ const wiring = mcpWiring(fs18.readFileSync(agent, "utf8"), which);
3655
3644
  out.push({ ok: wiring.ok, name: "casper mcp", detail: wiring.detail });
3656
3645
  }
3657
3646
  return out;
@@ -4115,14 +4104,14 @@ var init_server2 = __esm({
4115
4104
  "server/src/mcp/server.ts"() {
4116
4105
  "use strict";
4117
4106
  init_protocol();
4118
- VERSION = true ? "0.11.0" : "dev";
4107
+ VERSION = true ? "0.12.0" : "dev";
4119
4108
  }
4120
4109
  });
4121
4110
 
4122
4111
  // server/src/cli/program.ts
4123
- import fs18 from "node:fs";
4112
+ import fs19 from "node:fs";
4124
4113
  import os4 from "node:os";
4125
- import path18 from "node:path";
4114
+ import path19 from "node:path";
4126
4115
 
4127
4116
  // server/node_modules/commander/lib/error.js
4128
4117
  var CommanderError = class extends Error {
@@ -7235,9 +7224,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
7235
7224
  * @param {string} [path]
7236
7225
  * @return {(string|null|Command)}
7237
7226
  */
7238
- executableDir(path19) {
7239
- if (path19 === void 0) return this._executableDir;
7240
- this._executableDir = path19;
7227
+ executableDir(path20) {
7228
+ if (path20 === void 0) return this._executableDir;
7229
+ this._executableDir = path20;
7241
7230
  return this;
7242
7231
  }
7243
7232
  /**
@@ -7495,7 +7484,7 @@ var program = new Command();
7495
7484
  // server/src/cli/program.ts
7496
7485
  init_paths();
7497
7486
  init_settings();
7498
- var VERSION2 = true ? "0.11.0" : "dev";
7487
+ var VERSION2 = true ? "0.12.0" : "dev";
7499
7488
  async function bootstrap() {
7500
7489
  const file = configFilePath();
7501
7490
  const settings = readSettings(file);
@@ -7541,8 +7530,8 @@ async function resetToken(value) {
7541
7530
  updateSettings(file, { token });
7542
7531
  process.stdout.write(`wrote a new token to ${file}
7543
7532
  `);
7544
- const dbFile = path18.join(dataDirPath(), "casper.db");
7545
- if (fs18.existsSync(dbFile)) {
7533
+ const dbFile = path19.join(dataDirPath(), "casper.db");
7534
+ if (fs19.existsSync(dbFile)) {
7546
7535
  const { LoginStore: LoginStore2 } = await Promise.resolve().then(() => (init_logins(), logins_exports));
7547
7536
  new LoginStore2().revokeAll();
7548
7537
  process.stdout.write("revoked all device sessions\n");
@@ -7559,26 +7548,26 @@ async function resetToken(value) {
7559
7548
  }
7560
7549
  function buildProgram() {
7561
7550
  const program2 = new Command();
7562
- 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", `
7563
7552
  Settings live in ${configFilePath()}.
7564
7553
  Update with: npm install -g @joeyshi12/casper`);
7565
- 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 () => {
7566
7555
  await bootstrap();
7567
7556
  const { serve: serve2 } = await Promise.resolve().then(() => (init_server(), server_exports));
7568
7557
  await serve2();
7569
7558
  });
7570
- program2.command("token").description("print the access token").allowUnknownOption(false).allowExcessArguments(false).action(printToken);
7571
- 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) => {
7572
7561
  await resetToken(value);
7573
7562
  });
7574
- 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 () => {
7575
7564
  process.exitCode = (await Promise.resolve().then(() => (init_doctor(), doctor_exports))).doctor();
7576
7565
  });
7577
- 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 () => {
7578
7567
  (await Promise.resolve().then(() => (init_server2(), server_exports2))).runMcpServer();
7579
7568
  });
7580
7569
  const service = program2.command("service").description("manage the systemd user service");
7581
- 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 () => {
7582
7571
  process.exitCode = run(await Promise.resolve().then(() => (init_service(), service_exports)));
7583
7572
  });
7584
7573
  svcCommand("install", "run Casper as a systemd user service", (m) => m.serviceInstall());