@makimoto/cc-log-viewer 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/src/server.js ADDED
@@ -0,0 +1,227 @@
1
+ const path = require("path");
2
+ const os = require("os");
3
+ const express = require("express");
4
+ const Database = require("better-sqlite3");
5
+
6
+ const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
7
+ const DB_PATH = path.join(CLAUDE_DIR, "cc-log-viewer.db");
8
+ const STATIC_DIR = path.join(__dirname, "static");
9
+
10
+ const app = express();
11
+ app.use(express.json());
12
+ app.use(express.static(STATIC_DIR));
13
+
14
+ function getDb() {
15
+ const db = new Database(DB_PATH);
16
+ db.pragma("journal_mode = WAL");
17
+ return db;
18
+ }
19
+
20
+ function sanitizeFtsQuery(q) {
21
+ const terms = q.trim().split(/\s+/);
22
+ if (!terms.length || (terms.length === 1 && terms[0] === "")) {
23
+ return '""';
24
+ }
25
+ const quoted = [];
26
+ for (const term of terms) {
27
+ const clean = term.replace(/["\x00]/g, "");
28
+ if (clean) {
29
+ quoted.push(`"${clean}"`);
30
+ }
31
+ }
32
+ return quoted.length ? quoted.join(" ") : '""';
33
+ }
34
+
35
+ // SPA routing
36
+ app.get("/", (_req, res) => {
37
+ res.sendFile(path.join(STATIC_DIR, "index.html"));
38
+ });
39
+
40
+ app.get("/sessions/:sessionId", (_req, res) => {
41
+ res.sendFile(path.join(STATIC_DIR, "index.html"));
42
+ });
43
+
44
+ // Search messages
45
+ app.get("/api/search", (req, res) => {
46
+ const q = req.query.q || "";
47
+ const project = req.query.project || null;
48
+ const branch = req.query.branch || null;
49
+ const role = req.query.role || null;
50
+ const page = Math.max(1, parseInt(req.query.page, 10) || 1);
51
+ const perPage = Math.min(500, Math.max(1, parseInt(req.query.per_page, 10) || 20));
52
+ const offset = (page - 1) * perPage;
53
+
54
+ if (!q.trim()) {
55
+ return res.json({ total: 0, page, per_page: perPage, results: [] });
56
+ }
57
+
58
+ const safeQ = sanitizeFtsQuery(q);
59
+
60
+ let filters = "";
61
+ const params = { q: safeQ };
62
+
63
+ if (project) {
64
+ filters += " AND s.project_name = $project";
65
+ params.project = project;
66
+ }
67
+ if (branch) {
68
+ filters += " AND s.git_branch = $branch";
69
+ params.branch = branch;
70
+ }
71
+ if (role) {
72
+ filters += " AND m.role = $role";
73
+ params.role = role;
74
+ }
75
+
76
+ const baseFrom = `
77
+ FROM messages AS m
78
+ JOIN sessions AS s ON m.session_id = s.session_id
79
+ WHERE messages MATCH $q
80
+ `;
81
+
82
+ const db = getDb();
83
+ try {
84
+ const countSql = `SELECT COUNT(*) AS total ${baseFrom}${filters}`;
85
+ const totalRow = db.prepare(countSql).get(params);
86
+ const total = totalRow.total;
87
+
88
+ const resultsSql = `
89
+ SELECT
90
+ snippet(messages, 2, '<mark>', '</mark>', '...', 64) AS snippet,
91
+ m.session_id,
92
+ m.role,
93
+ m.timestamp,
94
+ m.message_type,
95
+ s.summary,
96
+ s.project_name,
97
+ s.git_branch
98
+ ${baseFrom}${filters}
99
+ ORDER BY m.timestamp DESC
100
+ LIMIT $limit OFFSET $offset
101
+ `;
102
+ params.limit = perPage;
103
+ params.offset = offset;
104
+
105
+ const rows = db.prepare(resultsSql).all(params);
106
+
107
+ res.json({ total, page, per_page: perPage, results: rows });
108
+ } finally {
109
+ db.close();
110
+ }
111
+ });
112
+
113
+ // List sessions
114
+ app.get("/api/sessions", (req, res) => {
115
+ const project = req.query.project || null;
116
+ const branch = req.query.branch || null;
117
+ const page = Math.max(1, parseInt(req.query.page, 10) || 1);
118
+ const perPage = Math.min(500, Math.max(1, parseInt(req.query.per_page, 10) || 50));
119
+ const offset = (page - 1) * perPage;
120
+
121
+ let filters = "";
122
+ const params = {};
123
+
124
+ if (project) {
125
+ filters += " AND project_name = $project";
126
+ params.project = project;
127
+ }
128
+ if (branch) {
129
+ filters += " AND git_branch = $branch";
130
+ params.branch = branch;
131
+ }
132
+
133
+ const where = "WHERE 1=1" + filters;
134
+
135
+ const db = getDb();
136
+ try {
137
+ const countSql = `SELECT COUNT(*) AS total FROM sessions ${where}`;
138
+ const totalRow = db.prepare(countSql).get(params);
139
+ const total = totalRow.total;
140
+
141
+ const resultsSql = `
142
+ SELECT *
143
+ FROM sessions
144
+ ${where}
145
+ ORDER BY modified DESC
146
+ LIMIT $limit OFFSET $offset
147
+ `;
148
+ params.limit = perPage;
149
+ params.offset = offset;
150
+
151
+ const rows = db.prepare(resultsSql).all(params);
152
+
153
+ res.json({ total, page, per_page: perPage, sessions: rows });
154
+ } finally {
155
+ db.close();
156
+ }
157
+ });
158
+
159
+ // Get single session with messages
160
+ app.get("/api/sessions/:sessionId", (req, res) => {
161
+ const sessionId = req.params.sessionId;
162
+
163
+ const db = getDb();
164
+ try {
165
+ const session = db.prepare(
166
+ "SELECT * FROM sessions WHERE session_id = $sid"
167
+ ).get({ sid: sessionId });
168
+
169
+ if (!session) {
170
+ return res.status(404).json({ detail: "Session not found" });
171
+ }
172
+
173
+ const messages = db.prepare(`
174
+ SELECT role, content, timestamp, message_type
175
+ FROM messages
176
+ WHERE session_id = $sid
177
+ ORDER BY timestamp ASC
178
+ `).all({ sid: sessionId });
179
+
180
+ res.json({ session, messages });
181
+ } finally {
182
+ db.close();
183
+ }
184
+ });
185
+
186
+ // Stats
187
+ app.get("/api/stats", (_req, res) => {
188
+ const db = getDb();
189
+ try {
190
+ const sessionCount = db.prepare("SELECT COUNT(*) AS c FROM sessions").get().c;
191
+ const messageCount = db.prepare("SELECT COUNT(*) AS c FROM messages").get().c;
192
+ const projects = db.prepare(
193
+ "SELECT DISTINCT project_name FROM sessions ORDER BY project_name"
194
+ ).all().map((r) => r.project_name);
195
+ const lastIndexed = db.prepare(
196
+ "SELECT value FROM index_meta WHERE key = 'last_indexed_at'"
197
+ ).get();
198
+
199
+ res.json({
200
+ session_count: sessionCount,
201
+ message_count: messageCount,
202
+ projects,
203
+ last_indexed_at: lastIndexed ? lastIndexed.value : null,
204
+ });
205
+ } finally {
206
+ db.close();
207
+ }
208
+ });
209
+
210
+ // Reindex
211
+ app.post("/api/reindex", async (_req, res) => {
212
+ try {
213
+ const { buildIndex } = require("./indexer.js");
214
+ await buildIndex();
215
+ res.json({ status: "ok" });
216
+ } catch (err) {
217
+ res.status(500).json({ detail: err.message });
218
+ }
219
+ });
220
+
221
+ function start(host, port) {
222
+ app.listen(port, host, () => {
223
+ console.log(`Starting cc-log-viewer at http://${host}:${port}`);
224
+ });
225
+ }
226
+
227
+ module.exports = { app, start };