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