claude-deck 0.1.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/LICENSE +201 -0
- package/dashboard/assets/index-Cux_zpcb.js +244 -0
- package/dashboard/assets/index-Dg_mccmz.css +1 -0
- package/dashboard/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/dashboard/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/dashboard/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/dashboard/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/dashboard/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/dashboard/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/dashboard/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/dashboard/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/dashboard/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/dashboard/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/dashboard/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/dashboard/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/dashboard/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/dashboard/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/dashboard/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/dashboard/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/dashboard/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/dashboard/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/dashboard/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/dashboard/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/dashboard/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/dashboard/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/dashboard/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/dashboard/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/dashboard/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/dashboard/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dashboard/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dashboard/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dashboard/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dashboard/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dashboard/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dashboard/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dashboard/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dashboard/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/dashboard/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/dashboard/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/dashboard/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/dashboard/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/dashboard/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/dashboard/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/dashboard/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/dashboard/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/dashboard/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/dashboard/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/dashboard/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/dashboard/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/dashboard/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/dashboard/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/dashboard/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/dashboard/index.html +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +101 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.js +24 -0
- package/dist/db/queries.d.ts +29 -0
- package/dist/db/queries.js +388 -0
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.js +90 -0
- package/dist/parser/cost.d.ts +3 -0
- package/dist/parser/cost.js +88 -0
- package/dist/parser/index.d.ts +9 -0
- package/dist/parser/index.js +89 -0
- package/dist/parser/session-parser.d.ts +2 -0
- package/dist/parser/session-parser.js +229 -0
- package/dist/parser/subagent-parser.d.ts +5 -0
- package/dist/parser/subagent-parser.js +150 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +52 -0
- package/dist/server/routes/sessions.d.ts +3 -0
- package/dist/server/routes/sessions.js +31 -0
- package/dist/server/routes/stats.d.ts +3 -0
- package/dist/server/routes/stats.js +11 -0
- package/dist/server/routes/sync.d.ts +3 -0
- package/dist/server/routes/sync.js +11 -0
- package/dist/types.d.ts +292 -0
- package/dist/types.js +2 -0
- package/package.json +56 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createReadStream } from "fs";
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { estimateCost } from "./cost.js";
|
|
4
|
+
const MAX_TOOL_INPUT_LENGTH = 2000;
|
|
5
|
+
const MAX_TOOL_RESPONSE_LENGTH = 2000;
|
|
6
|
+
function truncate(s, max) {
|
|
7
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
8
|
+
}
|
|
9
|
+
function extractText(content) {
|
|
10
|
+
if (typeof content === "string")
|
|
11
|
+
return content;
|
|
12
|
+
return content
|
|
13
|
+
.filter((b) => b.type === "text")
|
|
14
|
+
.map((b) => b.text)
|
|
15
|
+
.join("\n");
|
|
16
|
+
}
|
|
17
|
+
/** Strip XML system tags that get injected into user messages */
|
|
18
|
+
function cleanPrompt(text) {
|
|
19
|
+
// Remove common system-injected XML blocks
|
|
20
|
+
return text
|
|
21
|
+
.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, "")
|
|
22
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
|
|
23
|
+
.replace(/<command-name>[\s\S]*?<\/command-name>/g, "")
|
|
24
|
+
.replace(/<command-message>[\s\S]*?<\/command-message>/g, "")
|
|
25
|
+
.replace(/<command-args>[\s\S]*?<\/command-args>/g, "")
|
|
26
|
+
.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, "")
|
|
27
|
+
.trim();
|
|
28
|
+
}
|
|
29
|
+
export async function parseSessionJsonl(jsonlPath, sessionId, project, projectHash, jsonlMtime) {
|
|
30
|
+
const toolCalls = [];
|
|
31
|
+
const messages = [];
|
|
32
|
+
const compactions = [];
|
|
33
|
+
const modelCounts = new Map();
|
|
34
|
+
let inputTokens = 0;
|
|
35
|
+
let outputTokens = 0;
|
|
36
|
+
let cacheReadTokens = 0;
|
|
37
|
+
let cacheCreateTokens = 0;
|
|
38
|
+
let totalCost = 0;
|
|
39
|
+
let firstPrompt = null;
|
|
40
|
+
let startedAt = null;
|
|
41
|
+
let endedAt = null;
|
|
42
|
+
let turnCount = 0;
|
|
43
|
+
let lastRole = null;
|
|
44
|
+
let peakContextTokens = 0;
|
|
45
|
+
// Map tool_use_id → index in toolCalls for linking results
|
|
46
|
+
const toolUseIdMap = new Map();
|
|
47
|
+
// Track seen message IDs to avoid duplicate content extraction (tool_use blocks, text, turns)
|
|
48
|
+
// but NOT usage — usage is summed from every record to match claude /stats accounting.
|
|
49
|
+
const seenMessageIds = new Set();
|
|
50
|
+
const rl = createInterface({
|
|
51
|
+
input: createReadStream(jsonlPath),
|
|
52
|
+
crlfDelay: Infinity,
|
|
53
|
+
});
|
|
54
|
+
for await (const line of rl) {
|
|
55
|
+
if (!line.trim())
|
|
56
|
+
continue;
|
|
57
|
+
let record;
|
|
58
|
+
try {
|
|
59
|
+
record = JSON.parse(line);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
continue; // skip malformed lines
|
|
63
|
+
}
|
|
64
|
+
const ts = record.timestamp ?? "";
|
|
65
|
+
if (ts && !startedAt)
|
|
66
|
+
startedAt = ts;
|
|
67
|
+
if (ts)
|
|
68
|
+
endedAt = ts;
|
|
69
|
+
if (record.type === "assistant") {
|
|
70
|
+
const rec = record;
|
|
71
|
+
const msg = rec.message;
|
|
72
|
+
const msgId = msg.id;
|
|
73
|
+
const alreadySeen = msgId ? seenMessageIds.has(msgId) : false;
|
|
74
|
+
if (msgId)
|
|
75
|
+
seenMessageIds.add(msgId);
|
|
76
|
+
// Track model (only once per message)
|
|
77
|
+
if (msg.model && !alreadySeen) {
|
|
78
|
+
modelCounts.set(msg.model, (modelCounts.get(msg.model) ?? 0) + 1);
|
|
79
|
+
}
|
|
80
|
+
// Accumulate usage from every record (matches claude /stats accounting)
|
|
81
|
+
let turnCost = 0;
|
|
82
|
+
if (msg.usage) {
|
|
83
|
+
inputTokens += msg.usage.input_tokens ?? 0;
|
|
84
|
+
outputTokens += msg.usage.output_tokens ?? 0;
|
|
85
|
+
cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
|
|
86
|
+
cacheCreateTokens += msg.usage.cache_creation_input_tokens ?? 0;
|
|
87
|
+
turnCost = estimateCost(msg.model ?? "claude-sonnet-4-6", msg.usage);
|
|
88
|
+
totalCost += turnCost;
|
|
89
|
+
// Track peak context fill (total tokens sent in a single API call)
|
|
90
|
+
const turnContext = (msg.usage.input_tokens ?? 0) +
|
|
91
|
+
(msg.usage.cache_read_input_tokens ?? 0) +
|
|
92
|
+
(msg.usage.cache_creation_input_tokens ?? 0);
|
|
93
|
+
if (turnContext > peakContextTokens)
|
|
94
|
+
peakContextTokens = turnContext;
|
|
95
|
+
}
|
|
96
|
+
// Extract tool use blocks (deduplicate by tool_use_id to avoid duplicates)
|
|
97
|
+
if (Array.isArray(msg.content)) {
|
|
98
|
+
for (const block of msg.content) {
|
|
99
|
+
if (block.type === "tool_use") {
|
|
100
|
+
const tb = block;
|
|
101
|
+
if (!toolUseIdMap.has(tb.id)) {
|
|
102
|
+
const tc = {
|
|
103
|
+
toolUseId: tb.id,
|
|
104
|
+
toolName: tb.name,
|
|
105
|
+
toolInput: truncate(JSON.stringify(tb.input), MAX_TOOL_INPUT_LENGTH),
|
|
106
|
+
toolResponse: null,
|
|
107
|
+
status: "pending",
|
|
108
|
+
timestamp: ts,
|
|
109
|
+
subagentId: null,
|
|
110
|
+
};
|
|
111
|
+
toolUseIdMap.set(tb.id, toolCalls.length);
|
|
112
|
+
toolCalls.push(tc);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Extract text for messages (only once per message)
|
|
117
|
+
if (!alreadySeen) {
|
|
118
|
+
const text = extractText(msg.content);
|
|
119
|
+
if (text.trim()) {
|
|
120
|
+
messages.push({
|
|
121
|
+
role: "assistant",
|
|
122
|
+
content: text,
|
|
123
|
+
timestamp: ts,
|
|
124
|
+
model: msg.model ?? null,
|
|
125
|
+
costUsd: turnCost,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Track turns (only once per message ID)
|
|
131
|
+
if (!alreadySeen && lastRole !== "assistant")
|
|
132
|
+
turnCount++;
|
|
133
|
+
if (!alreadySeen)
|
|
134
|
+
lastRole = "assistant";
|
|
135
|
+
}
|
|
136
|
+
else if (record.type === "user") {
|
|
137
|
+
const rec = record;
|
|
138
|
+
// External user message
|
|
139
|
+
if (rec.userType === "external") {
|
|
140
|
+
const rawText = typeof rec.message?.content === "string"
|
|
141
|
+
? rec.message.content
|
|
142
|
+
: extractText(rec.message?.content ?? []);
|
|
143
|
+
const text = cleanPrompt(rawText);
|
|
144
|
+
if (text) {
|
|
145
|
+
messages.push({ role: "user", content: text, timestamp: ts, model: null, costUsd: null });
|
|
146
|
+
if (!firstPrompt)
|
|
147
|
+
firstPrompt = text;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Tool result — match via top-level toolUseID or message.content tool_result blocks
|
|
151
|
+
if (rec.toolUseResult && rec.toolUseID) {
|
|
152
|
+
const idx = toolUseIdMap.get(rec.toolUseID);
|
|
153
|
+
if (idx !== undefined) {
|
|
154
|
+
toolCalls[idx].status = "success";
|
|
155
|
+
toolCalls[idx].toolResponse = truncate(JSON.stringify(rec.toolUseResult), MAX_TOOL_RESPONSE_LENGTH);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Also check message.content for tool_result blocks (primary path for most tools)
|
|
159
|
+
const userContent = rec.message?.content;
|
|
160
|
+
if (Array.isArray(userContent)) {
|
|
161
|
+
for (const block of userContent) {
|
|
162
|
+
if (block.type === "tool_result") {
|
|
163
|
+
const idx = toolUseIdMap.get(block.tool_use_id);
|
|
164
|
+
if (idx !== undefined && !toolCalls[idx].toolResponse) {
|
|
165
|
+
toolCalls[idx].status = "success";
|
|
166
|
+
toolCalls[idx].toolResponse = truncate(typeof block.content === "string"
|
|
167
|
+
? block.content
|
|
168
|
+
: JSON.stringify(block.content ?? ""), MAX_TOOL_RESPONSE_LENGTH);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (rec.userType === "external") {
|
|
174
|
+
lastRole = "user";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (record.type === "system" &&
|
|
178
|
+
record.subtype === "compact_boundary") {
|
|
179
|
+
const rec = record;
|
|
180
|
+
if (rec.compactMetadata) {
|
|
181
|
+
compactions.push({
|
|
182
|
+
timestamp: ts,
|
|
183
|
+
trigger: rec.compactMetadata.trigger ?? "auto",
|
|
184
|
+
preTokens: rec.compactMetadata.preTokens ?? 0,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Determine primary model
|
|
190
|
+
let primaryModel = null;
|
|
191
|
+
let maxCount = 0;
|
|
192
|
+
for (const [model, count] of modelCounts) {
|
|
193
|
+
if (count > maxCount) {
|
|
194
|
+
primaryModel = model;
|
|
195
|
+
maxCount = count;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Calculate duration
|
|
199
|
+
let durationMs = null;
|
|
200
|
+
if (startedAt && endedAt) {
|
|
201
|
+
durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
id: sessionId,
|
|
205
|
+
project,
|
|
206
|
+
projectHash,
|
|
207
|
+
firstPrompt,
|
|
208
|
+
model: primaryModel,
|
|
209
|
+
inputTokens,
|
|
210
|
+
outputTokens,
|
|
211
|
+
cacheReadTokens,
|
|
212
|
+
cacheCreateTokens,
|
|
213
|
+
estimatedCostUsd: totalCost,
|
|
214
|
+
messageCount: messages.length,
|
|
215
|
+
toolCallCount: toolCalls.length,
|
|
216
|
+
subagentCount: 0, // filled in by index.ts after subagent parsing
|
|
217
|
+
turnCount,
|
|
218
|
+
peakContextTokens,
|
|
219
|
+
startedAt,
|
|
220
|
+
endedAt,
|
|
221
|
+
durationMs,
|
|
222
|
+
jsonlPath,
|
|
223
|
+
jsonlMtime,
|
|
224
|
+
toolCalls,
|
|
225
|
+
messages,
|
|
226
|
+
subagents: [],
|
|
227
|
+
compactions,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { createReadStream } from "fs";
|
|
2
|
+
import { createInterface } from "readline";
|
|
3
|
+
import { estimateCost } from "./cost.js";
|
|
4
|
+
const MAX_TOOL_INPUT_LENGTH = 2000;
|
|
5
|
+
const MAX_TOOL_RESPONSE_LENGTH = 2000;
|
|
6
|
+
function truncate(s, max) {
|
|
7
|
+
return s.length > max ? s.slice(0, max) + "..." : s;
|
|
8
|
+
}
|
|
9
|
+
export async function parseSubagentJsonl(jsonlPath, agentId, sessionId) {
|
|
10
|
+
const toolCalls = [];
|
|
11
|
+
const toolUseIdMap = new Map();
|
|
12
|
+
let inputTokens = 0;
|
|
13
|
+
let outputTokens = 0;
|
|
14
|
+
let cacheReadTokens = 0;
|
|
15
|
+
let cacheCreateTokens = 0;
|
|
16
|
+
let totalCost = 0;
|
|
17
|
+
let agentType = null;
|
|
18
|
+
let prompt = null;
|
|
19
|
+
let lastAssistantText = null;
|
|
20
|
+
let startedAt = null;
|
|
21
|
+
let endedAt = null;
|
|
22
|
+
const modelCounts = new Map();
|
|
23
|
+
const seenMessageIds = new Set();
|
|
24
|
+
const rl = createInterface({
|
|
25
|
+
input: createReadStream(jsonlPath),
|
|
26
|
+
crlfDelay: Infinity,
|
|
27
|
+
});
|
|
28
|
+
for await (const line of rl) {
|
|
29
|
+
if (!line.trim())
|
|
30
|
+
continue;
|
|
31
|
+
let record;
|
|
32
|
+
try {
|
|
33
|
+
record = JSON.parse(line);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const ts = record.timestamp ?? "";
|
|
39
|
+
if (ts && !startedAt)
|
|
40
|
+
startedAt = ts;
|
|
41
|
+
if (ts)
|
|
42
|
+
endedAt = ts;
|
|
43
|
+
if (record.type === "assistant") {
|
|
44
|
+
const rec = record;
|
|
45
|
+
const msg = rec.message;
|
|
46
|
+
const msgId = msg.id;
|
|
47
|
+
const alreadySeen = msgId ? seenMessageIds.has(msgId) : false;
|
|
48
|
+
if (msgId)
|
|
49
|
+
seenMessageIds.add(msgId);
|
|
50
|
+
if (msg.model && !alreadySeen) {
|
|
51
|
+
modelCounts.set(msg.model, (modelCounts.get(msg.model) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
// Accumulate usage from every record (matches claude /stats accounting)
|
|
54
|
+
if (msg.usage) {
|
|
55
|
+
inputTokens += msg.usage.input_tokens ?? 0;
|
|
56
|
+
outputTokens += msg.usage.output_tokens ?? 0;
|
|
57
|
+
cacheReadTokens += msg.usage.cache_read_input_tokens ?? 0;
|
|
58
|
+
cacheCreateTokens += msg.usage.cache_creation_input_tokens ?? 0;
|
|
59
|
+
totalCost += estimateCost(msg.model ?? "claude-sonnet-4-6", msg.usage);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(msg.content)) {
|
|
62
|
+
for (const block of msg.content) {
|
|
63
|
+
if (block.type === "tool_use") {
|
|
64
|
+
const tb = block;
|
|
65
|
+
if (!toolUseIdMap.has(tb.id)) {
|
|
66
|
+
const tc = {
|
|
67
|
+
toolUseId: tb.id,
|
|
68
|
+
toolName: tb.name,
|
|
69
|
+
toolInput: truncate(JSON.stringify(tb.input), MAX_TOOL_INPUT_LENGTH),
|
|
70
|
+
toolResponse: null,
|
|
71
|
+
status: "pending",
|
|
72
|
+
timestamp: ts,
|
|
73
|
+
subagentId: agentId,
|
|
74
|
+
};
|
|
75
|
+
toolUseIdMap.set(tb.id, toolCalls.length);
|
|
76
|
+
toolCalls.push(tc);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (block.type === "text" && "text" in block) {
|
|
80
|
+
lastAssistantText = block.text;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (record.type === "user") {
|
|
86
|
+
const rec = record;
|
|
87
|
+
// Try to extract agent type and prompt from the first user message
|
|
88
|
+
if (!prompt && rec.userType === "external") {
|
|
89
|
+
const content = typeof rec.message?.content === "string"
|
|
90
|
+
? rec.message.content
|
|
91
|
+
: Array.isArray(rec.message?.content)
|
|
92
|
+
? rec.message.content
|
|
93
|
+
.filter((b) => b.type === "text")
|
|
94
|
+
.map((b) => b.text)
|
|
95
|
+
.join("\n")
|
|
96
|
+
: "";
|
|
97
|
+
if (content)
|
|
98
|
+
prompt = content.slice(0, 500);
|
|
99
|
+
}
|
|
100
|
+
// Tool result
|
|
101
|
+
if (rec.toolUseResult && rec.toolUseID) {
|
|
102
|
+
const idx = toolUseIdMap.get(rec.toolUseID);
|
|
103
|
+
if (idx !== undefined) {
|
|
104
|
+
toolCalls[idx].status = "success";
|
|
105
|
+
toolCalls[idx].toolResponse = truncate(JSON.stringify(rec.toolUseResult), MAX_TOOL_RESPONSE_LENGTH);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Try to infer agent type from the agent ID path or prompt
|
|
111
|
+
if (!agentType && prompt) {
|
|
112
|
+
const lower = prompt.toLowerCase();
|
|
113
|
+
if (lower.includes("explore") || lower.includes("search"))
|
|
114
|
+
agentType = "Explore";
|
|
115
|
+
else if (lower.includes("plan"))
|
|
116
|
+
agentType = "Plan";
|
|
117
|
+
else
|
|
118
|
+
agentType = "general-purpose";
|
|
119
|
+
}
|
|
120
|
+
// Determine primary model
|
|
121
|
+
let model = null;
|
|
122
|
+
let maxCount = 0;
|
|
123
|
+
for (const [m, count] of modelCounts) {
|
|
124
|
+
if (count > maxCount) {
|
|
125
|
+
model = m;
|
|
126
|
+
maxCount = count;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
let durationMs = null;
|
|
130
|
+
if (startedAt && endedAt) {
|
|
131
|
+
durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
subagent: {
|
|
135
|
+
id: agentId,
|
|
136
|
+
agentType,
|
|
137
|
+
model,
|
|
138
|
+
prompt,
|
|
139
|
+
inputTokens,
|
|
140
|
+
outputTokens,
|
|
141
|
+
cacheReadTokens,
|
|
142
|
+
cacheCreateTokens,
|
|
143
|
+
estimatedCostUsd: totalCost,
|
|
144
|
+
toolCallCount: toolCalls.length,
|
|
145
|
+
durationMs,
|
|
146
|
+
resultSummary: lastAssistantText?.slice(0, 500) ?? null,
|
|
147
|
+
},
|
|
148
|
+
toolCalls,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import fastifyStatic from "@fastify/static";
|
|
3
|
+
import fastifyCors from "@fastify/cors";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { join, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { registerSessionRoutes } from "./routes/sessions.js";
|
|
8
|
+
import { registerStatsRoutes } from "./routes/stats.js";
|
|
9
|
+
import { registerSyncRoutes } from "./routes/sync.js";
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
export async function startServer(db, claudeDir, port) {
|
|
12
|
+
const app = Fastify({ logger: false });
|
|
13
|
+
await app.register(fastifyCors, { origin: true });
|
|
14
|
+
// API routes
|
|
15
|
+
registerSessionRoutes(app, db);
|
|
16
|
+
registerStatsRoutes(app, db);
|
|
17
|
+
registerSyncRoutes(app, db, claudeDir);
|
|
18
|
+
// Serve dashboard static files
|
|
19
|
+
// In dev: dashboard files are in ../dashboard relative to dist/server/
|
|
20
|
+
// When published: dashboard/ is at package root
|
|
21
|
+
const dashboardPaths = [
|
|
22
|
+
join(__dirname, "..", "..", "dashboard"), // from dist/server/
|
|
23
|
+
join(__dirname, "..", "dashboard"), // fallback
|
|
24
|
+
];
|
|
25
|
+
let dashboardDir = null;
|
|
26
|
+
for (const p of dashboardPaths) {
|
|
27
|
+
if (existsSync(p)) {
|
|
28
|
+
dashboardDir = p;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (dashboardDir) {
|
|
33
|
+
await app.register(fastifyStatic, {
|
|
34
|
+
root: dashboardDir,
|
|
35
|
+
prefix: "/",
|
|
36
|
+
wildcard: false,
|
|
37
|
+
});
|
|
38
|
+
// SPA fallback — serve index.html for non-API routes
|
|
39
|
+
app.setNotFoundHandler(async (_req, reply) => {
|
|
40
|
+
return reply.sendFile("index.html", dashboardDir);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
app.get("/", async () => {
|
|
45
|
+
return {
|
|
46
|
+
message: "Claude Deck API is running. Dashboard not found — run 'npm run build:ui' first.",
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const address = await app.listen({ port, host: "0.0.0.0" });
|
|
51
|
+
return address;
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { listSessions, getSession, getTimeline, getSubagents, getSessionInsights, } from "../../db/queries.js";
|
|
2
|
+
export function registerSessionRoutes(app, db) {
|
|
3
|
+
app.get("/api/sessions", async (req) => {
|
|
4
|
+
const q = req.query;
|
|
5
|
+
return listSessions(db, {
|
|
6
|
+
project: q.project,
|
|
7
|
+
model: q.model,
|
|
8
|
+
after: q.after,
|
|
9
|
+
before: q.before,
|
|
10
|
+
sort: q.sort,
|
|
11
|
+
limit: q.limit ? parseInt(q.limit) : undefined,
|
|
12
|
+
offset: q.offset ? parseInt(q.offset) : undefined,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
app.get("/api/sessions/:id", async (req) => {
|
|
16
|
+
const session = getSession(db, req.params.id);
|
|
17
|
+
if (!session) {
|
|
18
|
+
return { error: "Session not found" };
|
|
19
|
+
}
|
|
20
|
+
return session;
|
|
21
|
+
});
|
|
22
|
+
app.get("/api/sessions/:id/timeline", async (req) => {
|
|
23
|
+
return getTimeline(db, req.params.id);
|
|
24
|
+
});
|
|
25
|
+
app.get("/api/sessions/:id/subagents", async (req) => {
|
|
26
|
+
return getSubagents(db, req.params.id);
|
|
27
|
+
});
|
|
28
|
+
app.get("/api/sessions/:id/insights", async (req) => {
|
|
29
|
+
return getSessionInsights(db, req.params.id);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { syncAll } from "../../parser/index.js";
|
|
2
|
+
import { getSyncStatus } from "../../db/queries.js";
|
|
3
|
+
export function registerSyncRoutes(app, db, claudeDir) {
|
|
4
|
+
app.post("/api/sync", async () => {
|
|
5
|
+
const result = await syncAll(db, claudeDir);
|
|
6
|
+
return result;
|
|
7
|
+
});
|
|
8
|
+
app.get("/api/sync/status", async () => {
|
|
9
|
+
return getSyncStatus(db);
|
|
10
|
+
});
|
|
11
|
+
}
|