@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/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/cc-log-viewer.js +52 -0
- package/package.json +39 -0
- package/src/indexer.js +506 -0
- package/src/server.js +227 -0
- package/src/static/index.html +1490 -0
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 };
|