ccakashic 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,20 +1,20 @@
1
- # cctape
1
+ # ccakashic
2
2
 
3
- A CLI tool to browse Claude Code session logs (`~/.claude/projects/`) as beautiful HTML in your browser.
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
5
  ## Usage
6
6
 
7
7
  ### npx
8
8
 
9
9
  ```bash
10
- npx cctape
10
+ npx ccakashic
11
11
  ```
12
12
 
13
13
  ### Run from source
14
14
 
15
15
  ```bash
16
- git clone git@github.com:ashimon83/cctape.git
17
- cd cctape
16
+ git clone git@github.com:ashimon83/ccakashic.git
17
+ cd ccakashic
18
18
  npm start
19
19
  ```
20
20
 
@@ -39,7 +39,7 @@ A local HTTP server starts and your browser opens automatically.
39
39
 
40
40
  ```bash
41
41
  # Custom port (default: 3333)
42
- CCTAPE_PORT=3000 npx cctape
42
+ CCAKASHIC_PORT=3000 npx ccakashic
43
43
  ```
44
44
 
45
45
  ## Requirements
@@ -17,7 +17,7 @@ function openInBrowser(url) {
17
17
  exec(`${cmd} "${url}"`);
18
18
  }
19
19
 
20
- const PORT = parseInt(process.env.CCTAPE_PORT) || 3333;
20
+ const PORT = parseInt(process.env.CCAKASHIC_PORT) || 3333;
21
21
 
22
22
  const server = http.createServer(async (req, res) => {
23
23
  try {
@@ -88,7 +88,7 @@ const server = http.createServer(async (req, res) => {
88
88
  server.listen(PORT, '127.0.0.1', () => {
89
89
  const addr = server.address();
90
90
  const url = `http://127.0.0.1:${addr.port}`;
91
- console.log(`cctape running at ${url}`);
91
+ console.log(`ccakashic running at ${url}`);
92
92
  console.log('Press Ctrl+C to stop');
93
93
  openInBrowser(url);
94
94
  });
@@ -132,12 +132,38 @@ function renderMessage(msg) {
132
132
  const id = msgId(msg.timestamp);
133
133
  const time = `<a class="timestamp" href="#${id}">${formatTime(msg.timestamp)}</a>`;
134
134
 
135
+ const turnBadge = msg.turnUsage
136
+ ? (() => {
137
+ const c = calcCost(msg.turnUsage.input, msg.turnUsage.output, msg.turnUsage.cacheRead, msg.turnUsage.cacheCreate);
138
+ const dur = msg.turnUsage.durationMs ? ` | ${formatDuration(msg.turnUsage.durationMs)}` : '';
139
+ return `<span class="turn-usage">&#9889; Turn: ${c.totalStr}${dur} <span class="usage-detail">(in:${c.inStr} out:${c.outStr} cache-r:${c.crStr} cache-w:${c.cwStr})</span></span>`;
140
+ })()
141
+ : '';
142
+
143
+ function makeItemBadge(m) {
144
+ const parts = [];
145
+ if (m.usage) {
146
+ const c = 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
+ parts.push(`${c.totalStr}`);
148
+ }
149
+ if (m.execMs) {
150
+ parts.push(`${formatDuration(m.execMs)}`);
151
+ } else if (m.elapsedMs && m.elapsedMs >= 1000) {
152
+ parts.push(`${formatDuration(m.elapsedMs)}`);
153
+ }
154
+ if (!parts.length) return '';
155
+ const detail = m.usage ? ` <span class="usage-detail">(in:${formatCost(tokenCostUsd(m.usage.input_tokens||0, COST_PER_M.input))} out:${formatCost(tokenCostUsd(m.usage.output_tokens||0, COST_PER_M.output))} cache-r:${formatCost(tokenCostUsd(m.usage.cache_read_input_tokens||0, COST_PER_M.cacheRead))} cache-w:${formatCost(tokenCostUsd(m.usage.cache_creation_input_tokens||0, COST_PER_M.cacheWrite))})</span>` : '';
156
+ return `<span class="item-usage">${parts.join(' | ')}${detail}</span>`;
157
+ }
158
+
159
+ const itemBadge = makeItemBadge(msg);
160
+
135
161
  switch (msg.type) {
136
162
  case 'user':
137
- return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div></div>`;
163
+ return `<div class="msg msg-user" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div>${turnBadge}</div>`;
138
164
 
139
165
  case 'assistant':
140
- return `<div class="msg msg-assistant" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div></div>`;
166
+ return `<div class="msg msg-assistant" id="${id}">${time}<div class="msg-content" data-markdown>${escapeHtml(msg.text)}</div>${itemBadge ? `<div>${itemBadge}</div>` : ''}</div>`;
141
167
 
142
168
  case 'thinking':
143
169
  return `<div class="msg msg-thinking" id="${id}">${time}<span class="thinking-indicator">Thinking...</span></div>`;
@@ -148,16 +174,23 @@ function renderMessage(msg) {
148
174
  const subagentHtml = msg.subagentMessages
149
175
  ? `<div class="subagent-inline"><details><summary><span class="tool-summary">Subagent conversation</span></summary><div class="subagent-content">${msg.subagentMessages.map(renderMessage).join('\n')}</div></details></div>`
150
176
  : '';
151
- return `<div class="msg msg-tool" id="${id}"><details><summary>${time}<span class="tool-summary">${summary}</span></summary><div class="tool-details">${renderToolInput(msg)}${resultHtml}${subagentHtml}</div></details></div>`;
177
+ return `<div class="msg msg-tool" id="${id}"><details><summary>${time}<span class="tool-summary">${summary}</span></summary><div class="tool-details">${renderToolInput(msg)}${resultHtml}${subagentHtml}</div></details>${itemBadge ? `<div class="tool-usage-row">${itemBadge}</div>` : ''}</div>`;
152
178
  }
153
179
 
154
180
  case 'tool_result':
155
181
  // Unpaired tool result (shouldn't happen often)
156
182
  return `<div class="msg msg-tool" id="${id}"><div class="tool-output"><pre><code>${escapeHtml((msg.content || '').slice(0, 2000))}</code></pre></div></div>`;
157
183
 
184
+ case 'local_command': {
185
+ const cmd = msg.command ? `<div class="local-cmd-input"><span class="local-cmd-prompt">$</span> ${escapeHtml(msg.command)}</div>` : '';
186
+ const stdout = msg.stdout && msg.stdout.trim() ? `<pre class="local-cmd-output"><code>${escapeHtml(msg.stdout)}</code></pre>` : '';
187
+ const stderr = msg.stderr && msg.stderr.trim() ? `<pre class="local-cmd-output local-cmd-stderr"><code>${escapeHtml(msg.stderr)}</code></pre>` : '';
188
+ return `<div class="msg msg-local-cmd" id="${id}">${time}${cmd}${stdout}${stderr}${turnBadge}</div>`;
189
+ }
190
+
158
191
  case 'system':
159
192
  if (msg.subtype === 'turn_duration') {
160
- return `<div class="msg msg-system" id="${id}"><span class="duration">Turn: ${formatDuration(msg.durationMs)}</span></div>`;
193
+ return ''; // Now shown in turn usage badge
161
194
  }
162
195
  return `<div class="msg msg-system" id="${id}">${escapeHtml(msg.content || '')}</div>`;
163
196
 
@@ -214,6 +247,40 @@ function groupMessagesByDate(messages) {
214
247
  return groups;
215
248
  }
216
249
 
250
+ // Cost per 1M tokens (USD) - Claude Opus 4 pricing
251
+ const COST_PER_M = {
252
+ input: 15,
253
+ output: 75,
254
+ cacheRead: 1.5,
255
+ cacheWrite: 18.75,
256
+ };
257
+
258
+ function tokenCostUsd(tokens, rate) {
259
+ return (tokens / 1_000_000) * rate;
260
+ }
261
+
262
+ function formatCost(usd) {
263
+ if (usd < 0.001) return '<$0.01';
264
+ if (usd < 0.01) return `$${usd.toFixed(3)}`;
265
+ if (usd < 1) return `$${usd.toFixed(2)}`;
266
+ return `$${usd.toFixed(2)}`;
267
+ }
268
+
269
+ function calcCost(input, output, cacheRead, cacheWrite) {
270
+ const inCost = tokenCostUsd(input, COST_PER_M.input);
271
+ const outCost = tokenCostUsd(output, COST_PER_M.output);
272
+ const crCost = tokenCostUsd(cacheRead, COST_PER_M.cacheRead);
273
+ const cwCost = tokenCostUsd(cacheWrite, COST_PER_M.cacheWrite);
274
+ const total = inCost + outCost + crCost + cwCost;
275
+ return {
276
+ totalStr: formatCost(total),
277
+ inStr: formatCost(inCost),
278
+ outStr: formatCost(outCost),
279
+ crStr: formatCost(crCost),
280
+ cwStr: formatCost(cwCost),
281
+ };
282
+ }
283
+
217
284
  function formatTokens(n) {
218
285
  if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
219
286
  if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
@@ -233,20 +300,18 @@ function formatDurationLong(ms) {
233
300
  function renderStats(stats) {
234
301
  if (!stats || !stats.turns) return '';
235
302
 
303
+ const totalCost = calcCost(stats.inputTokens, stats.outputTokens, stats.cacheRead, stats.cacheCreation);
304
+
236
305
  const items = [];
306
+ items.push(`<span class="stat-item"><span class="stat-label">Est. Cost</span><span class="stat-value">${totalCost.totalStr}</span></span>`);
237
307
  items.push(`<span class="stat-item"><span class="stat-label">Turns</span><span class="stat-value">${stats.turns}</span></span>`);
238
308
  items.push(`<span class="stat-item"><span class="stat-label">Input</span><span class="stat-value">${formatTokens(stats.inputTokens)}</span></span>`);
239
309
  items.push(`<span class="stat-item"><span class="stat-label">Output</span><span class="stat-value">${formatTokens(stats.outputTokens)}</span></span>`);
240
- items.push(`<span class="stat-item"><span class="stat-label">Cache Create</span><span class="stat-value">${formatTokens(stats.cacheCreation)}</span></span>`);
241
310
  items.push(`<span class="stat-item"><span class="stat-label">Cache Read</span><span class="stat-value">${formatTokens(stats.cacheRead)}</span></span>`);
242
- items.push(`<span class="stat-item"><span class="stat-label">Total</span><span class="stat-value">${formatTokens(stats.totalTokens)}</span></span>`);
311
+ items.push(`<span class="stat-item"><span class="stat-label">Cache Write</span><span class="stat-value">${formatTokens(stats.cacheCreation)}</span></span>`);
243
312
  items.push(`<span class="stat-item"><span class="stat-label">Cache Hit</span><span class="stat-value">${(stats.cacheHitRate * 100).toFixed(0)}%</span></span>`);
244
313
  if (stats.durationMs) {
245
314
  items.push(`<span class="stat-item"><span class="stat-label">Duration</span><span class="stat-value">${formatDurationLong(stats.durationMs)}</span></span>`);
246
- if (stats.outputTokens && stats.durationMs > 0) {
247
- const tokPerMin = Math.round(stats.outputTokens / (stats.durationMs / 60000));
248
- items.push(`<span class="stat-item"><span class="stat-label">Output/min</span><span class="stat-value">${formatTokens(tokPerMin)}</span></span>`);
249
- }
250
315
  }
251
316
 
252
317
  return `<div class="stats-bar">${items.join('')}</div>`;
@@ -293,7 +358,7 @@ ${detailLayoutCSS()}
293
358
  </style>
294
359
  </head>
295
360
  <body>
296
- <a href="https://github.com/ashimon83/cctape" class="github-corner" aria-label="View source on GitHub" target="_blank" rel="noopener"><svg width="70" height="70" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
361
+ <a href="https://github.com/ashimon83/ccakashic" class="github-corner" aria-label="View source on GitHub" target="_blank" rel="noopener"><svg width="70" height="70" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
297
362
  <div class="detail-sticky-bar" id="detailStickyBar"></div>
298
363
  <header class="session-header">
299
364
  ${backLink}
package/lib/pages.js CHANGED
@@ -22,7 +22,7 @@ function formatTokens(n) {
22
22
  return String(n);
23
23
  }
24
24
 
25
- const GITHUB_URL = 'https://github.com/ashimon83/cctape';
25
+ const GITHUB_URL = 'https://github.com/ashimon83/ccakashic';
26
26
 
27
27
  function githubCorner() {
28
28
  return `<a href="${GITHUB_URL}" class="github-corner" aria-label="View source on GitHub" target="_blank" rel="noopener"><svg width="70" height="70" viewBox="0 0 250 250" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>`;
@@ -251,9 +251,9 @@ function generateIndex(projects) {
251
251
  </a>`;
252
252
  }).join('\n');
253
253
 
254
- return pageShell('cctape', `
254
+ return pageShell('ccakashic', `
255
255
  <div class="page-header">
256
- <h1>cctape</h1>
256
+ <h1>ccakashic</h1>
257
257
  <div class="subtitle">Claude Code Session Logs</div>
258
258
  </div>
259
259
  <div class="list-container">
@@ -327,10 +327,10 @@ function generateSessionList(project, sessions) {
327
327
  </div>`;
328
328
  }).join('\n');
329
329
 
330
- return pageShell(`${project.name} — cctape`, `
330
+ return pageShell(`${project.name} — ccakashic`, `
331
331
  <div class="sticky-date-bar" id="stickyDateBar"></div>
332
332
  <div class="page-header">
333
- <div class="breadcrumb"><a href="/">cctape</a> / ${escapeHtml(project.name)}</div>
333
+ <div class="breadcrumb"><a href="/">ccakashic</a> / ${escapeHtml(project.name)}</div>
334
334
  <h1>${escapeHtml(project.name)}</h1>
335
335
  <div class="subtitle">${sessions.length} sessions</div>
336
336
  </div>
package/lib/parser.js CHANGED
@@ -142,21 +142,97 @@ function buildConversation(lines) {
142
142
  // Pair tool_use with tool_result
143
143
  pairToolMessages(messages);
144
144
 
145
+ // Calculate per-turn token usage (tokens between each user input)
146
+ calculateTurnUsage(messages);
147
+
148
+ // Calculate elapsed time for each message (time since previous message)
149
+ calculateElapsed(messages);
150
+
145
151
  return messages;
146
152
  }
147
153
 
154
+ function parseLocalCommand(text) {
155
+ // Parse <bash-input>, <bash-stdout>, <bash-stderr> tags
156
+ const inputMatch = text.match(/<bash-input>([\s\S]*?)<\/bash-input>/);
157
+ const stdoutMatch = text.match(/<bash-stdout>([\s\S]*?)<\/bash-stdout>/);
158
+ const stderrMatch = text.match(/<bash-stderr>([\s\S]*?)<\/bash-stderr>/);
159
+
160
+ if (inputMatch) {
161
+ return { type: 'local_command', subtype: 'input', command: inputMatch[1] };
162
+ }
163
+ if (stdoutMatch || stderrMatch) {
164
+ return {
165
+ type: 'local_command',
166
+ subtype: 'output',
167
+ stdout: stdoutMatch ? stdoutMatch[1] : '',
168
+ stderr: stderrMatch ? stderrMatch[1] : '',
169
+ };
170
+ }
171
+ return null;
172
+ }
173
+
174
+ function processUserText(text, line, messages) {
175
+ // Skip local-command-caveat (system instruction, not user input)
176
+ if (text.match(/^<local-command-caveat>/)) {
177
+ return;
178
+ }
179
+
180
+ // Parse local command input/output
181
+ const localCmd = parseLocalCommand(text);
182
+ if (localCmd) {
183
+ if (localCmd.subtype === 'input') {
184
+ messages.push({
185
+ type: 'local_command',
186
+ command: localCmd.command,
187
+ timestamp: line.timestamp,
188
+ uuid: line.uuid,
189
+ });
190
+ } else if (localCmd.subtype === 'output') {
191
+ // Attach to previous local_command if exists
192
+ const prev = messages.length > 0 ? messages[messages.length - 1] : null;
193
+ if (prev && prev.type === 'local_command' && !prev.stdout) {
194
+ prev.stdout = localCmd.stdout;
195
+ prev.stderr = localCmd.stderr;
196
+ } else {
197
+ messages.push({
198
+ type: 'local_command',
199
+ command: '',
200
+ stdout: localCmd.stdout,
201
+ stderr: localCmd.stderr,
202
+ timestamp: line.timestamp,
203
+ uuid: line.uuid,
204
+ });
205
+ }
206
+ }
207
+ return;
208
+ }
209
+
210
+ // Strip system-reminder tags but keep surrounding user text
211
+ const stripped = text
212
+ .replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
213
+ .replace(/<command-name>[\s\S]*?<\/command-name>/g, '')
214
+ .replace(/<command-message>[\s\S]*?<\/command-message>/g, '')
215
+ .replace(/<command-args>[\s\S]*?<\/command-args>/g, '')
216
+ .replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '')
217
+ .trim();
218
+
219
+ if (stripped) {
220
+ messages.push({
221
+ type: 'user',
222
+ text: stripped,
223
+ timestamp: line.timestamp,
224
+ uuid: line.uuid,
225
+ });
226
+ }
227
+ }
228
+
148
229
  function processUserMessage(line, messages) {
149
230
  const content = line.message?.content;
150
231
  if (!content) return;
151
232
 
152
233
  if (typeof content === 'string') {
153
234
  if (content.trim()) {
154
- messages.push({
155
- type: 'user',
156
- text: content,
157
- timestamp: line.timestamp,
158
- uuid: line.uuid,
159
- });
235
+ processUserText(content, line, messages);
160
236
  }
161
237
  return;
162
238
  }
@@ -164,12 +240,7 @@ function processUserMessage(line, messages) {
164
240
  if (Array.isArray(content)) {
165
241
  for (const block of content) {
166
242
  if (block.type === 'text' && block.text?.trim()) {
167
- messages.push({
168
- type: 'user',
169
- text: block.text,
170
- timestamp: line.timestamp,
171
- uuid: line.uuid,
172
- });
243
+ processUserText(block.text, line, messages);
173
244
  } else if (block.type === 'tool_result') {
174
245
  const toolResult = {
175
246
  type: 'tool_result',
@@ -218,6 +289,9 @@ function processAssistantMessage(line, messages) {
218
289
  const content = line.message?.content;
219
290
  if (!content || !Array.isArray(content)) return;
220
291
 
292
+ const usage = line.message?.usage || null;
293
+ let usageAssigned = false;
294
+
221
295
  for (const block of content) {
222
296
  if (block.type === 'thinking') {
223
297
  messages.push({
@@ -232,7 +306,9 @@ function processAssistantMessage(line, messages) {
232
306
  model: line.message.model,
233
307
  timestamp: line.timestamp,
234
308
  uuid: line.uuid,
309
+ usage: !usageAssigned ? usage : null,
235
310
  });
311
+ usageAssigned = true;
236
312
  } else if (block.type === 'tool_use') {
237
313
  messages.push({
238
314
  type: 'tool_use',
@@ -241,7 +317,79 @@ function processAssistantMessage(line, messages) {
241
317
  input: block.input,
242
318
  timestamp: line.timestamp,
243
319
  uuid: line.uuid,
320
+ usage: !usageAssigned ? usage : null,
244
321
  });
322
+ usageAssigned = true;
323
+ }
324
+ }
325
+ }
326
+
327
+ function calculateTurnUsage(messages) {
328
+ // Walk backwards from each user message, summing usage of preceding assistant messages
329
+ // A "turn" = user input → all assistant responses until next user input
330
+ for (let i = 0; i < messages.length; i++) {
331
+ if (messages[i].type !== 'user' && messages[i].type !== 'local_command') continue;
332
+
333
+ // Sum usage of all following assistant/tool messages until next user message
334
+ let totalInput = 0;
335
+ let totalOutput = 0;
336
+ let totalCacheRead = 0;
337
+ let totalCacheCreate = 0;
338
+ const seenUsageIds = new Set(); // Dedupe (multiple blocks from same API response)
339
+
340
+ for (let j = i + 1; j < messages.length; j++) {
341
+ const m = messages[j];
342
+ if (m.type === 'user' || m.type === 'local_command') break;
343
+ if (m.usage && !seenUsageIds.has(m.uuid + (m.usage.input_tokens || 0))) {
344
+ seenUsageIds.add(m.uuid + (m.usage.input_tokens || 0));
345
+ totalInput += m.usage.input_tokens || 0;
346
+ totalOutput += m.usage.output_tokens || 0;
347
+ totalCacheRead += m.usage.cache_read_input_tokens || 0;
348
+ totalCacheCreate += m.usage.cache_creation_input_tokens || 0;
349
+ }
350
+ }
351
+
352
+ // Find turn_duration in the following messages
353
+ let turnDurationMs = 0;
354
+ for (let j = i + 1; j < messages.length; j++) {
355
+ const m = messages[j];
356
+ if (m.type === 'user' || m.type === 'local_command') break;
357
+ if (m.type === 'system' && m.subtype === 'turn_duration') {
358
+ turnDurationMs = m.durationMs || 0;
359
+ break;
360
+ }
361
+ }
362
+
363
+ const total = totalInput + totalOutput + totalCacheRead + totalCacheCreate;
364
+ if (total > 0 || turnDurationMs > 0) {
365
+ messages[i].turnUsage = {
366
+ input: totalInput,
367
+ output: totalOutput,
368
+ cacheRead: totalCacheRead,
369
+ cacheCreate: totalCacheCreate,
370
+ total,
371
+ durationMs: turnDurationMs,
372
+ };
373
+ }
374
+ }
375
+ }
376
+
377
+ function calculateElapsed(messages) {
378
+ for (let i = 1; i < messages.length; i++) {
379
+ const prev = messages[i - 1];
380
+ const curr = messages[i];
381
+ if (prev.timestamp && curr.timestamp) {
382
+ const elapsed = new Date(curr.timestamp) - new Date(prev.timestamp);
383
+ if (elapsed > 0) {
384
+ curr.elapsedMs = elapsed;
385
+ }
386
+ }
387
+ // For tool_use with paired result, calculate execution time
388
+ if (curr.type === 'tool_use' && curr.result?.timestamp && curr.timestamp) {
389
+ const execTime = new Date(curr.result.timestamp) - new Date(curr.timestamp);
390
+ if (execTime > 0) {
391
+ curr.execMs = execTime;
392
+ }
245
393
  }
246
394
  }
247
395
  }
@@ -131,6 +131,71 @@ body {
131
131
  position: relative;
132
132
  }
133
133
 
134
+ /* Local command (user ! commands) */
135
+ .msg-local-cmd {
136
+ align-self: flex-end;
137
+ margin-left: 20%;
138
+ background: var(--bg-secondary);
139
+ border: 1px solid var(--border);
140
+ border-radius: 8px;
141
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
142
+ font-size: 0.82rem;
143
+ }
144
+ .local-cmd-input {
145
+ padding: 2px 0;
146
+ font-weight: 600;
147
+ }
148
+ .local-cmd-prompt {
149
+ color: var(--text-muted);
150
+ margin-right: 4px;
151
+ }
152
+ .local-cmd-output {
153
+ margin-top: 4px;
154
+ padding: 6px 8px;
155
+ background: var(--code-block-bg);
156
+ color: var(--code-block-text);
157
+ border-radius: 4px;
158
+ overflow-x: auto;
159
+ font-size: 0.78rem;
160
+ max-height: 200px;
161
+ overflow-y: auto;
162
+ }
163
+ .local-cmd-output code {
164
+ background: none;
165
+ padding: 0;
166
+ color: inherit;
167
+ }
168
+ .local-cmd-stderr {
169
+ border-left: 3px solid #ef4444;
170
+ }
171
+
172
+ /* Token usage badges */
173
+ .turn-usage, .item-usage {
174
+ display: inline-block;
175
+ margin-top: 6px;
176
+ padding: 2px 8px;
177
+ font-size: 0.68rem;
178
+ font-variant-numeric: tabular-nums;
179
+ color: var(--text-muted);
180
+ background: var(--bg-secondary);
181
+ border: 1px solid var(--border);
182
+ border-radius: 4px;
183
+ }
184
+ .turn-usage {
185
+ font-weight: 600;
186
+ }
187
+ .item-usage {
188
+ font-weight: 400;
189
+ opacity: 0.8;
190
+ }
191
+ .tool-usage-row {
192
+ padding: 4px 14px 6px;
193
+ }
194
+ .usage-detail {
195
+ font-size: 0.6rem;
196
+ opacity: 0.7;
197
+ }
198
+
134
199
  .msg .timestamp {
135
200
  display: block;
136
201
  font-size: 0.7rem;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "ccakashic",
3
- "version": "0.1.0",
4
- "description": "Browse Claude Code session logs (~/.claude/projects/) as beautiful HTML in your browser",
3
+ "version": "0.2.0",
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
- "cctape": "./bin/cctape.js"
6
+ "ccakashic": "bin/ccakashic.js"
7
7
  },
8
8
  "files": [
9
9
  "bin",
@@ -15,7 +15,7 @@
15
15
  "node": ">=18"
16
16
  },
17
17
  "scripts": {
18
- "start": "node bin/cctape.js"
18
+ "start": "node bin/ccakashic.js"
19
19
  },
20
20
  "dependencies": {},
21
21
  "keywords": [
@@ -26,16 +26,17 @@
26
26
  "viewer",
27
27
  "log",
28
28
  "jsonl",
29
- "html"
29
+ "html",
30
+ "akashic"
30
31
  ],
31
32
  "author": "ashimon83",
32
33
  "license": "MIT",
33
34
  "repository": {
34
35
  "type": "git",
35
- "url": "git+https://github.com/ashimon83/cctape.git"
36
+ "url": "git+https://github.com/ashimon83/ccakashic.git"
36
37
  },
37
- "homepage": "https://github.com/ashimon83/cctape#readme",
38
+ "homepage": "https://github.com/ashimon83/ccakashic#readme",
38
39
  "bugs": {
39
- "url": "https://github.com/ashimon83/cctape/issues"
40
+ "url": "https://github.com/ashimon83/ccakashic/issues"
40
41
  }
41
42
  }