ccakashic 0.2.0 → 0.2.2
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/README.md +11 -4
- package/bin/ccakashic.js +14 -3
- package/lib/discover.js +69 -1
- package/lib/html-generator.js +32 -7
- package/lib/template-assets.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
An Akashic Record of your Claude Code sessions — browse Claude Code session logs (`~/.claude/projects/`) as beautiful HTML in your browser.
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
5
9
|
## Usage
|
|
6
10
|
|
|
7
11
|
### npx
|
|
@@ -23,13 +27,16 @@ A local HTTP server starts and your browser opens automatically.
|
|
|
23
27
|
## Features
|
|
24
28
|
|
|
25
29
|
- **Fully browser-based** — Project list → Session list → Conversation detail
|
|
26
|
-
- **Chat-style layout** — User / assistant messages in bubbles
|
|
27
|
-
- **Collapsible tool calls** — Bash, Read, Edit, and other tool invocations
|
|
30
|
+
- **Chat-style layout** — User / assistant messages in chat bubbles
|
|
31
|
+
- **Collapsible tool calls** — Bash, Read, Edit, and other tool invocations collapsed by default
|
|
28
32
|
- **Diff view** — File edits shown with red/green line highlights
|
|
29
33
|
- **Date navigation** — Side nav and sticky headers to jump between dates
|
|
30
|
-
- **
|
|
34
|
+
- **Cost estimation** — Per-turn and per-message USD cost based on Claude Opus 4 pricing (input / output / cache read / cache write breakdown)
|
|
35
|
+
- **Elapsed time** — Per-turn duration and per-tool execution time derived from timestamps
|
|
36
|
+
- **Local command display** — `!` shell commands rendered with prompt and output
|
|
31
37
|
- **Inline subagent conversations** — Subagent dialogues nested inside the Agent tool_use that spawned them
|
|
32
|
-
- **Permalinks** — Click any message timestamp to get a shareable URL
|
|
38
|
+
- **Permalinks** — Click any message timestamp to get a shareable URL (`#t20260416103045`)
|
|
39
|
+
- **Session-level stats** — Estimated cost, turns, token breakdown, cache hit rate, and duration in the header
|
|
33
40
|
- **Dark mode** — Follows `prefers-color-scheme` automatically
|
|
34
41
|
- **Filter search** — Incremental filtering on list pages
|
|
35
42
|
- **Keyboard navigation** — `j` / `k` to move between messages
|
package/bin/ccakashic.js
CHANGED
|
@@ -5,7 +5,7 @@ const http = require('http');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { exec } = require('child_process');
|
|
8
|
-
const { listProjects, listSessions } = require('../lib/discover');
|
|
8
|
+
const { listProjects, listSessions, findSessionForCwd } = require('../lib/discover');
|
|
9
9
|
const { parseSession } = require('../lib/parser');
|
|
10
10
|
const { generate } = require('../lib/html-generator');
|
|
11
11
|
const { generateIndex, generateSessionList } = require('../lib/pages');
|
|
@@ -85,10 +85,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
server.listen(PORT, '127.0.0.1', () => {
|
|
88
|
+
server.listen(PORT, '127.0.0.1', async () => {
|
|
89
89
|
const addr = server.address();
|
|
90
90
|
const url = `http://127.0.0.1:${addr.port}`;
|
|
91
91
|
console.log(`ccakashic running at ${url}`);
|
|
92
92
|
console.log('Press Ctrl+C to stop');
|
|
93
|
-
|
|
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);
|
|
94
105
|
});
|
package/lib/discover.js
CHANGED
|
@@ -150,4 +150,72 @@ async function listSessions(projectDir) {
|
|
|
150
150
|
return sessions;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
|
|
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 };
|
package/lib/html-generator.js
CHANGED
|
@@ -142,8 +142,10 @@ function renderMessage(msg) {
|
|
|
142
142
|
|
|
143
143
|
function makeItemBadge(m) {
|
|
144
144
|
const parts = [];
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
const c = m.usage
|
|
146
|
+
? calcCost(m.usage.input_tokens || 0, m.usage.output_tokens || 0, m.usage.cache_read_input_tokens || 0, m.usage.cache_creation_input_tokens || 0)
|
|
147
|
+
: null;
|
|
148
|
+
if (c) {
|
|
147
149
|
parts.push(`${c.totalStr}`);
|
|
148
150
|
}
|
|
149
151
|
if (m.execMs) {
|
|
@@ -152,7 +154,7 @@ function renderMessage(msg) {
|
|
|
152
154
|
parts.push(`${formatDuration(m.elapsedMs)}`);
|
|
153
155
|
}
|
|
154
156
|
if (!parts.length) return '';
|
|
155
|
-
const detail =
|
|
157
|
+
const detail = c ? ` <span class="usage-detail">(in:${c.inStr} out:${c.outStr} cache-r:${c.crStr} cache-w:${c.cwStr})</span>` : '';
|
|
156
158
|
return `<span class="item-usage">${parts.join(' | ')}${detail}</span>`;
|
|
157
159
|
}
|
|
158
160
|
|
|
@@ -274,10 +276,10 @@ function calcCost(input, output, cacheRead, cacheWrite) {
|
|
|
274
276
|
const total = inCost + outCost + crCost + cwCost;
|
|
275
277
|
return {
|
|
276
278
|
totalStr: formatCost(total),
|
|
277
|
-
inStr:
|
|
278
|
-
outStr:
|
|
279
|
-
crStr:
|
|
280
|
-
cwStr:
|
|
279
|
+
inStr: formatTokens(input),
|
|
280
|
+
outStr: formatTokens(output),
|
|
281
|
+
crStr: formatTokens(cacheRead),
|
|
282
|
+
cwStr: formatTokens(cacheWrite),
|
|
281
283
|
};
|
|
282
284
|
}
|
|
283
285
|
|
|
@@ -382,6 +384,9 @@ ${detailLayoutCSS()}
|
|
|
382
384
|
</nav>
|
|
383
385
|
<main class="chat-container" id="session-top">
|
|
384
386
|
${groupsHtml}
|
|
387
|
+
<div class="load-more-row">
|
|
388
|
+
<button class="load-more-btn" id="loadMoreBtn" type="button">Load more ↻</button>
|
|
389
|
+
</div>
|
|
385
390
|
<div id="session-bottom"></div>
|
|
386
391
|
</main>
|
|
387
392
|
</div>
|
|
@@ -405,6 +410,26 @@ function detailLayoutCSS() {
|
|
|
405
410
|
min-width: 0;
|
|
406
411
|
}
|
|
407
412
|
|
|
413
|
+
/* Load more (reload) button at the bottom of the thread */
|
|
414
|
+
.load-more-row {
|
|
415
|
+
display: flex;
|
|
416
|
+
justify-content: center;
|
|
417
|
+
margin: 24px 0 12px;
|
|
418
|
+
}
|
|
419
|
+
.load-more-btn {
|
|
420
|
+
padding: 8px 18px;
|
|
421
|
+
font-size: 0.85rem;
|
|
422
|
+
color: var(--text);
|
|
423
|
+
background: var(--bg-secondary);
|
|
424
|
+
border: 1px solid var(--border);
|
|
425
|
+
border-radius: 6px;
|
|
426
|
+
cursor: pointer;
|
|
427
|
+
transition: background 0.1s;
|
|
428
|
+
}
|
|
429
|
+
.load-more-btn:hover {
|
|
430
|
+
background: var(--bg-hover, var(--border));
|
|
431
|
+
}
|
|
432
|
+
|
|
408
433
|
/* Stats bar */
|
|
409
434
|
.stats-bar {
|
|
410
435
|
display: flex;
|
package/lib/template-assets.js
CHANGED
|
@@ -633,6 +633,23 @@ function getAppJS() {
|
|
|
633
633
|
el.innerHTML = renderMarkdown(el.textContent);
|
|
634
634
|
});
|
|
635
635
|
|
|
636
|
+
// Scroll to bottom when hash is #session-bottom. We re-scroll here (after
|
|
637
|
+
// markdown rendering) because the DOM height grows when markdown expands,
|
|
638
|
+
// making the browser's initial anchor scroll land in the wrong place.
|
|
639
|
+
if (window.location.hash === '#session-bottom') {
|
|
640
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Load more: reload the page to pick up any newly appended messages, then
|
|
644
|
+
// scroll back to the bottom.
|
|
645
|
+
var loadMoreBtn = document.getElementById('loadMoreBtn');
|
|
646
|
+
if (loadMoreBtn) {
|
|
647
|
+
loadMoreBtn.addEventListener('click', function() {
|
|
648
|
+
window.location.hash = 'session-bottom';
|
|
649
|
+
window.location.reload();
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
636
653
|
// Keyboard navigation: j/k to move between user messages
|
|
637
654
|
var msgEls = Array.from(document.querySelectorAll('.msg-user, .msg-assistant'));
|
|
638
655
|
var currentIdx = -1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccakashic",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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
6
|
"ccakashic": "bin/ccakashic.js"
|