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.
Files changed (87) hide show
  1. package/LICENSE +201 -0
  2. package/dashboard/assets/index-Cux_zpcb.js +244 -0
  3. package/dashboard/assets/index-Dg_mccmz.css +1 -0
  4. package/dashboard/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  5. package/dashboard/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  6. package/dashboard/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  7. package/dashboard/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  8. package/dashboard/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  9. package/dashboard/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  10. package/dashboard/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  11. package/dashboard/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  12. package/dashboard/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  13. package/dashboard/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  14. package/dashboard/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  15. package/dashboard/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  16. package/dashboard/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  17. package/dashboard/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  18. package/dashboard/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  19. package/dashboard/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  20. package/dashboard/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  21. package/dashboard/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  22. package/dashboard/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  23. package/dashboard/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  24. package/dashboard/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  25. package/dashboard/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  26. package/dashboard/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  27. package/dashboard/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  28. package/dashboard/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  29. package/dashboard/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  30. package/dashboard/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  31. package/dashboard/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  32. package/dashboard/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  33. package/dashboard/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  34. package/dashboard/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  35. package/dashboard/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  36. package/dashboard/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  37. package/dashboard/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  38. package/dashboard/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  39. package/dashboard/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  40. package/dashboard/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  41. package/dashboard/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  42. package/dashboard/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  43. package/dashboard/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  44. package/dashboard/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  45. package/dashboard/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  46. package/dashboard/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  47. package/dashboard/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  48. package/dashboard/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  49. package/dashboard/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  50. package/dashboard/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  51. package/dashboard/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  52. package/dashboard/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  53. package/dashboard/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  54. package/dashboard/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  55. package/dashboard/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  56. package/dashboard/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  57. package/dashboard/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  58. package/dashboard/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  59. package/dashboard/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  60. package/dashboard/index.html +13 -0
  61. package/dist/cli.d.ts +2 -0
  62. package/dist/cli.js +101 -0
  63. package/dist/db/index.d.ts +3 -0
  64. package/dist/db/index.js +24 -0
  65. package/dist/db/queries.d.ts +29 -0
  66. package/dist/db/queries.js +388 -0
  67. package/dist/db/schema.d.ts +1 -0
  68. package/dist/db/schema.js +90 -0
  69. package/dist/parser/cost.d.ts +3 -0
  70. package/dist/parser/cost.js +88 -0
  71. package/dist/parser/index.d.ts +9 -0
  72. package/dist/parser/index.js +89 -0
  73. package/dist/parser/session-parser.d.ts +2 -0
  74. package/dist/parser/session-parser.js +229 -0
  75. package/dist/parser/subagent-parser.d.ts +5 -0
  76. package/dist/parser/subagent-parser.js +150 -0
  77. package/dist/server/index.d.ts +2 -0
  78. package/dist/server/index.js +52 -0
  79. package/dist/server/routes/sessions.d.ts +3 -0
  80. package/dist/server/routes/sessions.js +31 -0
  81. package/dist/server/routes/stats.d.ts +3 -0
  82. package/dist/server/routes/stats.js +11 -0
  83. package/dist/server/routes/sync.d.ts +3 -0
  84. package/dist/server/routes/sync.js +11 -0
  85. package/dist/types.d.ts +292 -0
  86. package/dist/types.js +2 -0
  87. 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,5 @@
1
+ import type { ParsedSubagent, ParsedToolCall } from "../types.js";
2
+ export declare function parseSubagentJsonl(jsonlPath: string, agentId: string, sessionId: string): Promise<{
3
+ subagent: ParsedSubagent;
4
+ toolCalls: ParsedToolCall[];
5
+ }>;
@@ -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,2 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare function startServer(db: Database.Database, claudeDir: string, port: number): Promise<string>;
@@ -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,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type Database from "better-sqlite3";
3
+ export declare function registerSessionRoutes(app: FastifyInstance, db: Database.Database): void;
@@ -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,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type Database from "better-sqlite3";
3
+ export declare function registerStatsRoutes(app: FastifyInstance, db: Database.Database): void;
@@ -0,0 +1,11 @@
1
+ import { getStats } from "../../db/queries.js";
2
+ export function registerStatsRoutes(app, db) {
3
+ app.get("/api/stats", async (req) => {
4
+ const q = req.query;
5
+ return getStats(db, {
6
+ after: q.after,
7
+ before: q.before,
8
+ model: q.model,
9
+ });
10
+ });
11
+ }
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from "fastify";
2
+ import type Database from "better-sqlite3";
3
+ export declare function registerSyncRoutes(app: FastifyInstance, db: Database.Database, claudeDir: string): void;
@@ -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
+ }