@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.
- package/README.md +6 -1
- package/dist/agents/prompt.txt +1 -1
- package/dist/mcp.js +1 -1
- package/dist/server.js +462 -473
- package/dist/web/assets/{abnfDiagram-N423BO3Z-B88fK1c8.js → abnfDiagram-N423BO3Z-4Ko3ozfJ.js} +1 -1
- package/dist/web/assets/{arc-DMIevERD.js → arc-Ch7Ehjbn.js} +1 -1
- package/dist/web/assets/{architectureDiagram-T3A2C74G-CJ5mXlmv.js → architectureDiagram-T3A2C74G-DcWU5yRD.js} +1 -1
- package/dist/web/assets/{blockDiagram-VBNYF7ZC-BssSdi9Q.js → blockDiagram-VBNYF7ZC-BUzWG1_k.js} +1 -1
- package/dist/web/assets/{c4Diagram-5PPSVZJV-BJHrRcC9.js → c4Diagram-5PPSVZJV-Cbk3RgdU.js} +1 -1
- package/dist/web/assets/channel-DSuKNwf1.js +1 -0
- package/dist/web/assets/{chunk-2GRJ4B5K-Bmq_IVc0.js → chunk-2GRJ4B5K-B0WXdW3m.js} +1 -1
- package/dist/web/assets/{chunk-2Q5K7J3B-B2erbhym.js → chunk-2Q5K7J3B-Dr5AcgBH.js} +1 -1
- package/dist/web/assets/{chunk-5RXB4S5H-B_DuY9SH.js → chunk-5RXB4S5H-NRZvLElM.js} +1 -1
- package/dist/web/assets/{chunk-5VM5RSS4-BgiHQCU1.js → chunk-5VM5RSS4-B2ezMICZ.js} +1 -1
- package/dist/web/assets/{chunk-6Q2QTUOP-pmOmsjWC.js → chunk-6Q2QTUOP-0_kTL1pm.js} +1 -1
- package/dist/web/assets/{chunk-GF5L2VYU-B-PjTMWL.js → chunk-GF5L2VYU-ChJJ-3FT.js} +1 -1
- package/dist/web/assets/{chunk-JWPE2WC7-Be9UnGmk.js → chunk-JWPE2WC7-B15-wRYl.js} +1 -1
- package/dist/web/assets/{chunk-KBJHAD2P-DPxdXs9r.js → chunk-KBJHAD2P-DsR7v9qB.js} +1 -1
- package/dist/web/assets/{chunk-RYQCIY6F-BKc5gOjQ.js → chunk-RYQCIY6F-BkT8T354.js} +1 -1
- package/dist/web/assets/{chunk-XXDRQBXY-CoOnbv-S.js → chunk-XXDRQBXY-CRB4wwou.js} +1 -1
- package/dist/web/assets/classDiagram-JCYQIIEL-DY_Arw9F.js +1 -0
- package/dist/web/assets/classDiagram-v2-OCEON4UE-DY_Arw9F.js +1 -0
- package/dist/web/assets/{cose-bilkent-JH36ORCC-BZ5Oon1z.js → cose-bilkent-JH36ORCC-BESlNRq3.js} +1 -1
- package/dist/web/assets/{cynefin-VYW2F7L2-DlXmf5Bs.js → cynefin-VYW2F7L2-BPfoy8I9.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-MW4NZA55-ClZQ1oTx.js → cynefinDiagram-MW4NZA55-Byn6JJg0.js} +1 -1
- package/dist/web/assets/{dagre-VZM6K2ZE-CX3pDtYQ.js → dagre-VZM6K2ZE-CF6UO64P.js} +1 -1
- package/dist/web/assets/{diagram-7IWD3JNH-DBelWFkJ.js → diagram-7IWD3JNH-DgIzI-sU.js} +1 -1
- package/dist/web/assets/{diagram-B4RE2ZJO-CDqvztqK.js → diagram-B4RE2ZJO-DTXfP5om.js} +1 -1
- package/dist/web/assets/{diagram-LBJQPF4R-DWL1uAJS.js → diagram-LBJQPF4R-CTLNzser.js} +1 -1
- package/dist/web/assets/{diagram-Q27KOJAE-BRsHqM1Z.js → diagram-Q27KOJAE-M38db-G2.js} +1 -1
- package/dist/web/assets/{diagram-UB23O5K3-Ci1U9Hdl.js → diagram-UB23O5K3-CG-vZyFB.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-BXEA7PRR-CBPphElN.js → ebnfDiagram-BXEA7PRR-CsJ9VNb2.js} +1 -1
- package/dist/web/assets/{erDiagram-JOGREHBK-FI4byJC2.js → erDiagram-JOGREHBK-3dK0h9cf.js} +1 -1
- package/dist/web/assets/{flowDiagram-UKHOOZJN-BCKR_o2t.js → flowDiagram-UKHOOZJN-DHTGEHfr.js} +1 -1
- package/dist/web/assets/{ganttDiagram-PKOTCBZU-BYp35VGe.js → ganttDiagram-PKOTCBZU-DiLysF1Q.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-DS77QQ5N--AySchaL.js → gitGraphDiagram-DS77QQ5N-2uthJlH6.js} +1 -1
- package/dist/web/assets/index-D3zBm-7r.js +249 -0
- package/dist/web/assets/index-D_ANHT9G.css +1 -0
- package/dist/web/assets/{index-D9ooBx1d.js → index-DbaSy_a8.js} +1 -1
- package/dist/web/assets/{infoDiagram-6WML65LV-CbKYvHyN.js → infoDiagram-6WML65LV-B35ot4_O.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-dLs0tgi6.js → ishikawaDiagram-WSZJBQD7-C6nTJ8A6.js} +1 -1
- package/dist/web/assets/{journeyDiagram-NVQOT4AX-CXUsDb3x.js → journeyDiagram-NVQOT4AX-Bmt5Dt3Z.js} +1 -1
- package/dist/web/assets/{kanban-definition-27J2QSJJ-r33P8KpU.js → kanban-definition-27J2QSJJ-DkGk9WrO.js} +1 -1
- package/dist/web/assets/{linear-s7N3jDHb.js → linear-D9RsKviS.js} +1 -1
- package/dist/web/assets/{mermaid.core-Cw282Xmk.js → mermaid.core-BkEkrWDL.js} +4 -4
- package/dist/web/assets/{mindmap-definition-FAOFIHXS-BXPAvM6U.js → mindmap-definition-FAOFIHXS-7XkMFJ0C.js} +1 -1
- package/dist/web/assets/{pegDiagram-VL7TDLO6-D-H5hjvM.js → pegDiagram-VL7TDLO6-KpO4Zp9v.js} +1 -1
- package/dist/web/assets/{pieDiagram-7S7Q4E2Y-D3W5_BlG.js → pieDiagram-7S7Q4E2Y-CnZkxeeK.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-BNSHhxkB.js → quadrantDiagram-CIZ2JOQS-Jej_KG8V.js} +1 -1
- package/dist/web/assets/{railroadDiagram-AXF67PYL-BvLjuIqc.js → railroadDiagram-AXF67PYL-BfteVM0C.js} +1 -1
- package/dist/web/assets/{requirementDiagram-LRYGKXZP-CZTpqoEk.js → requirementDiagram-LRYGKXZP-BTSeDpTM.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-W5VNT64P-xvU-3Mf_.js → sankeyDiagram-W5VNT64P-CeB7UW0v.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-SI44F4Z6-Bpugvm4V.js → sequenceDiagram-SI44F4Z6-BeOLM9UY.js} +1 -1
- package/dist/web/assets/{sizeCapture-X5ZJPWSS-B87TxLF7.js → sizeCapture-X5ZJPWSS-7aezJBJp.js} +1 -1
- package/dist/web/assets/{stateDiagram-OKZ733FA-BEQB_m5r.js → stateDiagram-OKZ733FA-DmJkX_vQ.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-CkEEUkE9.js +1 -0
- package/dist/web/assets/{swimlanes-SLNWSIFB-CVMCMeYR.js → swimlanes-SLNWSIFB-CmnhwW2u.js} +2 -2
- package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-DaOmpb-r.js +8 -0
- package/dist/web/assets/{timeline-definition-Z64GVDOM-Dylz4pg-.js → timeline-definition-Z64GVDOM-DtxMhUPD.js} +1 -1
- package/dist/web/assets/{vennDiagram-T6HMQDX7-BmS_JQjJ.js → vennDiagram-T6HMQDX7-DO34MvMM.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-T6FBY63Y-C0qCz7i-.js → wardleyDiagram-T6FBY63Y-s2zhjdd1.js} +1 -1
- package/dist/web/assets/{xychartDiagram-ELKLHX3M-aOG1xxkv.js → xychartDiagram-ELKLHX3M-CnCQs0NK.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -2
- package/dist/web/assets/channel-CFhLIKKC.js +0 -1
- package/dist/web/assets/classDiagram-JCYQIIEL-CyTqHDI_.js +0 -1
- package/dist/web/assets/classDiagram-v2-OCEON4UE-CyTqHDI_.js +0 -1
- package/dist/web/assets/index-Dd6MG4c6.css +0 -1
- package/dist/web/assets/index-DigNQUdR.js +0 -253
- package/dist/web/assets/stateDiagram-v2-UEYNNEHI-OtsvcH8I.js +0 -1
- 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
|
|
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
|
|
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/
|
|
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 {
|
|
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:
|
|
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(
|
|
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 ?
|
|
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(
|
|
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
|
|
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)
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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/
|
|
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
|
|
816
|
-
return path8.join(config.casperDataDir, "
|
|
805
|
+
function chatsRoot() {
|
|
806
|
+
return path8.join(config.casperDataDir, "chats");
|
|
817
807
|
}
|
|
818
|
-
function
|
|
819
|
-
return path8.join(
|
|
808
|
+
function chatDir(chatId) {
|
|
809
|
+
return path8.join(chatsRoot(), chatId);
|
|
820
810
|
}
|
|
821
|
-
function
|
|
822
|
-
|
|
823
|
-
|
|
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
|
|
820
|
+
return dir;
|
|
826
821
|
}
|
|
827
|
-
function
|
|
828
|
-
|
|
822
|
+
function isManagedWorkspace(dir) {
|
|
823
|
+
const root = chatsRoot();
|
|
824
|
+
return dir === root || dir.startsWith(root + path8.sep);
|
|
829
825
|
}
|
|
830
|
-
var
|
|
831
|
-
"server/src/session/
|
|
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.
|
|
1087
|
+
/** Perform the ACP initialize handshake. kiro's reply is not used. */
|
|
1089
1088
|
async initialize() {
|
|
1090
|
-
|
|
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
|
|
1123
|
+
execCommand(sessionId, command) {
|
|
1126
1124
|
return this.client.request(ACP_METHODS.commandsExecute, {
|
|
1127
1125
|
sessionId,
|
|
1128
|
-
command: { command: command.replace(/^\//, ""), 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"
|
|
1261
|
+
case "turn_started":
|
|
1262
|
+
this.snapshot = { ...s, turnStatus: "running" };
|
|
1266
1263
|
break;
|
|
1267
|
-
|
|
1268
|
-
case "
|
|
1269
|
-
this.snapshot = { ...s, turnStatus: "idle"
|
|
1264
|
+
case "turn_ended":
|
|
1265
|
+
case "turn_error":
|
|
1266
|
+
this.snapshot = { ...s, turnStatus: "idle" };
|
|
1270
1267
|
break;
|
|
1271
|
-
|
|
1272
|
-
|
|
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
|
-
|
|
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
|
|
1335
|
-
seed(
|
|
1336
|
-
this.snapshot = { ...this.snapshot,
|
|
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
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
if (text2.trim() ||
|
|
1479
|
-
pushMsg({ id: `u-${baseId}`, role: "user", text: text2, timestamp: ts,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
1738
|
-
*
|
|
1739
|
-
*
|
|
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
|
-
*
|
|
1742
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2043
|
-
//
|
|
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
|
|
2071
|
-
|
|
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(
|
|
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
|
|
2102
|
-
currentModeId: s
|
|
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:
|
|
2107
|
-
|
|
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
|
|
2112
|
-
*
|
|
2113
|
-
*
|
|
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
|
|
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 =
|
|
2224
|
-
const b =
|
|
2225
|
-
return
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
...
|
|
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/
|
|
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 ?
|
|
2645
|
-
const dir = endsWithSep || !input ? resolved :
|
|
2646
|
-
const prefix = endsWithSep || !input ? "" :
|
|
2647
|
-
const targetKind = await
|
|
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
|
|
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) =>
|
|
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/
|
|
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 (!
|
|
2792
|
+
if (!path15.isAbsolute(filePath)) {
|
|
2683
2793
|
reply.code(400);
|
|
2684
2794
|
return { error: "path must be absolute" };
|
|
2685
2795
|
}
|
|
2686
|
-
const
|
|
2687
|
-
if (
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
reply.
|
|
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.
|
|
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
|
-
|
|
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
|
|
2842
|
-
import { createReadStream as
|
|
2843
|
-
import
|
|
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
|
|
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
|
|
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 =
|
|
2927
|
-
const filename =
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
3039
|
-
import
|
|
3040
|
-
import
|
|
3041
|
-
import
|
|
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 =
|
|
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 =
|
|
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
|
|
3055
|
+
await fs15.access(path17.join(dir, candidate));
|
|
3060
3056
|
candidate = `${stem} (${n++})${ext}`;
|
|
3061
3057
|
} catch {
|
|
3062
|
-
return
|
|
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 =
|
|
3069
|
-
|
|
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
|
|
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
|
|
3101
|
+
function registerUploadRoutes(app) {
|
|
3106
3102
|
app.post(
|
|
3107
|
-
"/api/
|
|
3103
|
+
"/api/chats/:chatId/uploads",
|
|
3108
3104
|
async (req, reply) => {
|
|
3109
|
-
|
|
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
|
|
3107
|
+
return { error: "Invalid chat id" };
|
|
3118
3108
|
}
|
|
3119
|
-
const uploadDir =
|
|
3120
|
-
await
|
|
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
|
|
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
|
|
3141
|
-
const ext =
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
|
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 =
|
|
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: (
|
|
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
|
|
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
|
|
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
|
|
3442
|
+
registerUploadRoutes(app);
|
|
3454
3443
|
registerWsGateway(app, manager);
|
|
3455
|
-
if (
|
|
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
|
|
3461
|
-
if (
|
|
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
|
|
3467
|
-
const looksLikeAsset =
|
|
3468
|
-
if (
|
|
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
|
|
3543
|
-
import
|
|
3531
|
+
import fs18 from "node:fs";
|
|
3532
|
+
import path18 from "node:path";
|
|
3544
3533
|
function which(bin) {
|
|
3545
|
-
if (bin.includes("/")) return
|
|
3546
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
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 =
|
|
3537
|
+
const candidate = path18.join(dir, bin);
|
|
3549
3538
|
try {
|
|
3550
|
-
|
|
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 && !
|
|
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
|
-
|
|
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 (
|
|
3617
|
-
const mode =
|
|
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
|
-
|
|
3626
|
-
|
|
3627
|
-
const db2 =
|
|
3628
|
-
const size =
|
|
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
|
-
|
|
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 =
|
|
3647
|
-
const hasAgent =
|
|
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(
|
|
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.
|
|
4107
|
+
VERSION = true ? "0.12.0" : "dev";
|
|
4119
4108
|
}
|
|
4120
4109
|
});
|
|
4121
4110
|
|
|
4122
4111
|
// server/src/cli/program.ts
|
|
4123
|
-
import
|
|
4112
|
+
import fs19 from "node:fs";
|
|
4124
4113
|
import os4 from "node:os";
|
|
4125
|
-
import
|
|
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(
|
|
7239
|
-
if (
|
|
7240
|
-
this._executableDir =
|
|
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.
|
|
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 =
|
|
7545
|
-
if (
|
|
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().
|
|
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").
|
|
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").
|
|
7571
|
-
program2.command("reset-token").argument("[value]", "token to set; generated when omitted").description("set a new token and sign every device out").
|
|
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").
|
|
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)").
|
|
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).
|
|
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());
|