ccakashic 0.2.2 → 0.2.5
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/dist/bin/ccakashic.js +248 -0
- package/dist/discover.js +230 -0
- package/dist/html-generator.js +554 -0
- package/{lib → dist}/pages.js +65 -80
- package/dist/parser.js +465 -0
- package/{lib → dist}/template-assets.js +6 -7
- package/package.json +14 -5
- package/bin/ccakashic.js +0 -105
- package/lib/discover.js +0 -221
- package/lib/html-generator.js +0 -586
- package/lib/parser.js +0 -496
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCSS = getCSS;
|
|
4
|
+
exports.getAppJS = getAppJS;
|
|
3
5
|
function getCSS() {
|
|
4
|
-
|
|
6
|
+
return `
|
|
5
7
|
:root {
|
|
6
8
|
--bg: #fafaf9;
|
|
7
9
|
--bg-secondary: #f5f5f4;
|
|
@@ -568,9 +570,8 @@ body {
|
|
|
568
570
|
}
|
|
569
571
|
`;
|
|
570
572
|
}
|
|
571
|
-
|
|
572
573
|
function getAppJS() {
|
|
573
|
-
|
|
574
|
+
return `
|
|
574
575
|
(function() {
|
|
575
576
|
// Simple markdown renderer (no external dependency)
|
|
576
577
|
function renderMarkdown(text) {
|
|
@@ -671,5 +672,3 @@ function getAppJS() {
|
|
|
671
672
|
})();
|
|
672
673
|
`;
|
|
673
674
|
}
|
|
674
|
-
|
|
675
|
-
module.exports = { getCSS, getAppJS };
|
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccakashic",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Browse Claude Code session logs (~/.claude/projects/) as beautiful HTML in your browser — an Akashic Record of your Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
|
-
"ccakashic": "bin/ccakashic.js"
|
|
6
|
+
"ccakashic": "dist/bin/ccakashic.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
|
-
"
|
|
10
|
-
"lib",
|
|
9
|
+
"dist",
|
|
11
10
|
"vendor",
|
|
12
11
|
"README.md"
|
|
13
12
|
],
|
|
@@ -15,9 +14,19 @@
|
|
|
15
14
|
"node": ">=18"
|
|
16
15
|
},
|
|
17
16
|
"scripts": {
|
|
18
|
-
"
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "npm run build && node dist/bin/ccakashic.js",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"test:watch": "vitest",
|
|
22
|
+
"prepack": "npm run build"
|
|
19
23
|
},
|
|
20
24
|
"dependencies": {},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"typescript": "^5.6.0",
|
|
28
|
+
"vitest": "^2.1.0"
|
|
29
|
+
},
|
|
21
30
|
"keywords": [
|
|
22
31
|
"claude",
|
|
23
32
|
"claude-code",
|
package/bin/ccakashic.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const http = require('http');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { exec } = require('child_process');
|
|
8
|
-
const { listProjects, listSessions, findSessionForCwd } = require('../lib/discover');
|
|
9
|
-
const { parseSession } = require('../lib/parser');
|
|
10
|
-
const { generate } = require('../lib/html-generator');
|
|
11
|
-
const { generateIndex, generateSessionList } = require('../lib/pages');
|
|
12
|
-
|
|
13
|
-
function openInBrowser(url) {
|
|
14
|
-
const cmd = process.platform === 'darwin' ? 'open'
|
|
15
|
-
: process.platform === 'win32' ? 'start'
|
|
16
|
-
: 'xdg-open';
|
|
17
|
-
exec(`${cmd} "${url}"`);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const PORT = parseInt(process.env.CCAKASHIC_PORT) || 3333;
|
|
21
|
-
|
|
22
|
-
const server = http.createServer(async (req, res) => {
|
|
23
|
-
try {
|
|
24
|
-
const url = new URL(req.url, `http://localhost`);
|
|
25
|
-
const pathname = url.pathname;
|
|
26
|
-
|
|
27
|
-
if (pathname === '/' || pathname === '') {
|
|
28
|
-
// Project index
|
|
29
|
-
const projects = listProjects();
|
|
30
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
31
|
-
res.end(generateIndex(projects));
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const projectMatch = pathname.match(/^\/project\/(.+)$/);
|
|
36
|
-
if (projectMatch && !pathname.includes('/session/')) {
|
|
37
|
-
// Session list for a project
|
|
38
|
-
const rawName = decodeURIComponent(projectMatch[1]);
|
|
39
|
-
const projects = listProjects();
|
|
40
|
-
const project = projects.find(p => p.rawName === rawName);
|
|
41
|
-
if (!project) {
|
|
42
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
43
|
-
res.end('Project not found');
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
const sessions = await listSessions(project.dir);
|
|
47
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
48
|
-
res.end(generateSessionList(project, sessions));
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const sessionMatch = pathname.match(/^\/project\/(.+)\/session\/(.+)$/);
|
|
53
|
-
if (sessionMatch) {
|
|
54
|
-
// Render a specific session
|
|
55
|
-
const rawName = decodeURIComponent(sessionMatch[1]);
|
|
56
|
-
const sessionId = decodeURIComponent(sessionMatch[2]);
|
|
57
|
-
const projects = listProjects();
|
|
58
|
-
const project = projects.find(p => p.rawName === rawName);
|
|
59
|
-
if (!project) {
|
|
60
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
61
|
-
res.end('Project not found');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const sessionPath = path.join(project.dir, `${sessionId}.jsonl`);
|
|
65
|
-
if (!fs.existsSync(sessionPath)) {
|
|
66
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
67
|
-
res.end('Session not found');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const sessions = await listSessions(project.dir);
|
|
71
|
-
const session = sessions.find(s => s.id === sessionId) || { id: sessionId, path: sessionPath };
|
|
72
|
-
const parsed = await parseSession(sessionPath);
|
|
73
|
-
const html = generate(parsed, { projectName: project.name, session, backUrl: `/project/${encodeURIComponent(rawName)}` });
|
|
74
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
75
|
-
res.end(html);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
80
|
-
res.end('Not found');
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.error(err);
|
|
83
|
-
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
84
|
-
res.end('Internal server error');
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
server.listen(PORT, '127.0.0.1', async () => {
|
|
89
|
-
const addr = server.address();
|
|
90
|
-
const url = `http://127.0.0.1:${addr.port}`;
|
|
91
|
-
console.log(`ccakashic running at ${url}`);
|
|
92
|
-
console.log('Press Ctrl+C to stop');
|
|
93
|
-
|
|
94
|
-
let openUrl = url;
|
|
95
|
-
try {
|
|
96
|
-
const match = await findSessionForCwd(process.cwd());
|
|
97
|
-
if (match) {
|
|
98
|
-
openUrl = `${url}/project/${encodeURIComponent(match.projectRawName)}/session/${encodeURIComponent(match.sessionId)}#session-bottom`;
|
|
99
|
-
console.log(`Detected session for ${process.cwd()} → opening at bottom`);
|
|
100
|
-
}
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.error('Failed to auto-detect session:', err.message);
|
|
103
|
-
}
|
|
104
|
-
openInBrowser(openUrl);
|
|
105
|
-
});
|
package/lib/discover.js
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const readline = require('readline');
|
|
7
|
-
|
|
8
|
-
const CLAUDE_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
9
|
-
|
|
10
|
-
function decodeDirName(dirName) {
|
|
11
|
-
// Directory names encode paths: /Users/foo/bar → -Users-foo-bar
|
|
12
|
-
// This is lossy (dots become dashes too), but good enough for display
|
|
13
|
-
if (dirName.startsWith('-')) {
|
|
14
|
-
return '/' + dirName.slice(1).replace(/-/g, '/');
|
|
15
|
-
}
|
|
16
|
-
return dirName;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function listProjects() {
|
|
20
|
-
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const entries = fs.readdirSync(CLAUDE_DIR, { withFileTypes: true });
|
|
25
|
-
const projects = [];
|
|
26
|
-
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
if (!entry.isDirectory()) continue;
|
|
29
|
-
|
|
30
|
-
const projectDir = path.join(CLAUDE_DIR, entry.name);
|
|
31
|
-
const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
32
|
-
|
|
33
|
-
if (jsonlFiles.length === 0) continue;
|
|
34
|
-
|
|
35
|
-
// Get most recent modification time
|
|
36
|
-
let lastModified = 0;
|
|
37
|
-
for (const f of jsonlFiles) {
|
|
38
|
-
const stat = fs.statSync(path.join(projectDir, f));
|
|
39
|
-
if (stat.mtimeMs > lastModified) lastModified = stat.mtimeMs;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
projects.push({
|
|
43
|
-
name: decodeDirName(entry.name),
|
|
44
|
-
rawName: entry.name,
|
|
45
|
-
dir: projectDir,
|
|
46
|
-
sessionCount: jsonlFiles.length,
|
|
47
|
-
lastModified: new Date(lastModified),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
projects.sort((a, b) => b.lastModified - a.lastModified);
|
|
52
|
-
return projects;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function getSessionPreview(filePath) {
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
const result = {
|
|
58
|
-
id: path.basename(filePath, '.jsonl'),
|
|
59
|
-
path: filePath,
|
|
60
|
-
timestamp: null,
|
|
61
|
-
lastModified: fs.statSync(filePath).mtimeMs,
|
|
62
|
-
preview: '',
|
|
63
|
-
gitBranch: null,
|
|
64
|
-
slug: null,
|
|
65
|
-
model: null,
|
|
66
|
-
hasSubagents: false,
|
|
67
|
-
totalTokens: 0,
|
|
68
|
-
outputTokens: 0,
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Check for subagents directory
|
|
72
|
-
const sessionDir = path.join(path.dirname(filePath), result.id);
|
|
73
|
-
if (fs.existsSync(path.join(sessionDir, 'subagents'))) {
|
|
74
|
-
result.hasSubagents = true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const rl = readline.createInterface({
|
|
78
|
-
input: fs.createReadStream(filePath, { encoding: 'utf-8' }),
|
|
79
|
-
crlfDelay: Infinity,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
let foundPreview = false;
|
|
83
|
-
|
|
84
|
-
rl.on('line', (line) => {
|
|
85
|
-
try {
|
|
86
|
-
const obj = JSON.parse(line);
|
|
87
|
-
|
|
88
|
-
if (!result.timestamp && obj.timestamp) {
|
|
89
|
-
result.timestamp = obj.timestamp;
|
|
90
|
-
}
|
|
91
|
-
if (!result.gitBranch && obj.gitBranch) {
|
|
92
|
-
result.gitBranch = obj.gitBranch;
|
|
93
|
-
}
|
|
94
|
-
if (!result.slug && obj.slug) {
|
|
95
|
-
result.slug = obj.slug;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Extract model from first assistant message
|
|
99
|
-
if (!result.model && obj.type === 'assistant' && obj.message?.model) {
|
|
100
|
-
result.model = obj.message.model;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Aggregate token usage
|
|
104
|
-
if (obj.type === 'assistant' && obj.message?.usage) {
|
|
105
|
-
const u = obj.message.usage;
|
|
106
|
-
result.totalTokens += (u.input_tokens || 0) + (u.output_tokens || 0)
|
|
107
|
-
+ (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
108
|
-
result.outputTokens += u.output_tokens || 0;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Extract first user message as preview
|
|
112
|
-
if (!foundPreview && obj.type === 'user' && obj.message) {
|
|
113
|
-
const content = obj.message.content;
|
|
114
|
-
let text = '';
|
|
115
|
-
if (typeof content === 'string') {
|
|
116
|
-
text = content;
|
|
117
|
-
} else if (Array.isArray(content)) {
|
|
118
|
-
for (const block of content) {
|
|
119
|
-
if (block.type === 'text' && block.text) {
|
|
120
|
-
text = block.text;
|
|
121
|
-
break;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (text) {
|
|
126
|
-
result.preview = text.replace(/\n/g, ' ').slice(0, 100);
|
|
127
|
-
foundPreview = true;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
} catch {
|
|
131
|
-
// skip malformed lines
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
rl.on('close', () => resolve(result));
|
|
136
|
-
rl.on('error', () => resolve(result));
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function listSessions(projectDir) {
|
|
141
|
-
const jsonlFiles = fs.readdirSync(projectDir)
|
|
142
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
143
|
-
.map(f => path.join(projectDir, f));
|
|
144
|
-
|
|
145
|
-
const sessions = await Promise.all(jsonlFiles.map(getSessionPreview));
|
|
146
|
-
|
|
147
|
-
// Sort by file modification time descending (most recently active first)
|
|
148
|
-
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
149
|
-
|
|
150
|
-
return sessions;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function readCwdFromSession(filePath) {
|
|
154
|
-
return new Promise((resolve) => {
|
|
155
|
-
const rl = readline.createInterface({
|
|
156
|
-
input: fs.createReadStream(filePath, { encoding: 'utf-8' }),
|
|
157
|
-
crlfDelay: Infinity,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
let found = null;
|
|
161
|
-
rl.on('line', (line) => {
|
|
162
|
-
if (found) return;
|
|
163
|
-
try {
|
|
164
|
-
const obj = JSON.parse(line);
|
|
165
|
-
if (obj.cwd) {
|
|
166
|
-
found = obj.cwd;
|
|
167
|
-
rl.close();
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// skip
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
rl.on('close', () => resolve(found));
|
|
175
|
-
rl.on('error', () => resolve(found));
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function latestSessionFile(projectDir) {
|
|
180
|
-
const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
181
|
-
if (!files.length) return null;
|
|
182
|
-
let best = null;
|
|
183
|
-
let bestMtime = 0;
|
|
184
|
-
for (const f of files) {
|
|
185
|
-
const mtime = fs.statSync(path.join(projectDir, f)).mtimeMs;
|
|
186
|
-
if (mtime > bestMtime) {
|
|
187
|
-
bestMtime = mtime;
|
|
188
|
-
best = f;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return best ? { file: best, mtimeMs: bestMtime } : null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async function findSessionForCwd(cwd) {
|
|
195
|
-
// Pick the project whose cwd is the most specific (longest) match for the
|
|
196
|
-
// given cwd. A brand-new session in a specific subdir should win over an
|
|
197
|
-
// older, more-active ancestor project.
|
|
198
|
-
const projects = listProjects();
|
|
199
|
-
let bestMatch = null;
|
|
200
|
-
let bestLen = -1;
|
|
201
|
-
|
|
202
|
-
for (const project of projects) {
|
|
203
|
-
const latest = latestSessionFile(project.dir);
|
|
204
|
-
if (!latest) continue;
|
|
205
|
-
const projectCwd = await readCwdFromSession(path.join(project.dir, latest.file));
|
|
206
|
-
if (!projectCwd) continue;
|
|
207
|
-
|
|
208
|
-
const isMatch = cwd === projectCwd || cwd.startsWith(projectCwd + path.sep);
|
|
209
|
-
if (isMatch && projectCwd.length > bestLen) {
|
|
210
|
-
bestMatch = {
|
|
211
|
-
projectRawName: project.rawName,
|
|
212
|
-
sessionId: path.basename(latest.file, '.jsonl'),
|
|
213
|
-
};
|
|
214
|
-
bestLen = projectCwd.length;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return bestMatch;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
module.exports = { listProjects, listSessions, decodeDirName, findSessionForCwd, CLAUDE_DIR };
|