ccanalyzer 1.1.0 → 1.1.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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "ccanalyzer",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Web-based analyzer for Claude Code sessions",
5
5
  "bin": {
6
- "ccanalyzer": "./bin/index.js"
6
+ "ccanalyzer": "bin/index.js"
7
7
  },
8
8
  "main": "./src/server.js",
9
9
  "scripts": {
package/src/parser.js CHANGED
@@ -164,6 +164,7 @@ function parseSubagents(sessionDir) {
164
164
  try { meta = JSON.parse(fs.readFileSync(path.join(subagentsDir, metaFile), 'utf8')); } catch {}
165
165
 
166
166
  const parsed = parseAgentFile(jsonlPath);
167
+ const { skills } = collectToolsUsed(jsonlPath);
167
168
  agents.push({
168
169
  agentId,
169
170
  meta,
@@ -173,6 +174,7 @@ function parseSubagents(sessionDir) {
173
174
  totalUsage: parsed.totalUsage,
174
175
  totalCost: parsed.totalCost,
175
176
  model: parsed.model,
177
+ skillsUsed: [...skills],
176
178
  });
177
179
  }
178
180
 
@@ -278,6 +280,25 @@ function getAllProjects() {
278
280
  return projects;
279
281
  }
280
282
 
283
+ function collectToolsUsed(filePath) {
284
+ const mcps = new Set();
285
+ const skills = new Set();
286
+ for (const entry of parseLines(filePath)) {
287
+ if (entry.type !== 'assistant') continue;
288
+ for (const block of (entry.message?.content || [])) {
289
+ if (!block || block.type !== 'tool_use') continue;
290
+ if (block.name.startsWith('mcp__')) {
291
+ const parts = block.name.split('__');
292
+ const server = (parts[1] || '').replace(/^claude_ai_/, '').replace(/_/g, ' ');
293
+ if (server) mcps.add(server);
294
+ } else if (block.name === 'Skill' && block.input?.skill) {
295
+ skills.add(block.input.skill);
296
+ }
297
+ }
298
+ }
299
+ return { mcps, skills };
300
+ }
301
+
281
302
  function getSessionDetail(dirName, sessionFile) {
282
303
  const filePath = path.join(PROJECTS_DIR, dirName, sessionFile);
283
304
  if (!fs.existsSync(filePath)) throw new Error('Session not found');
@@ -297,7 +318,22 @@ function getSessionDetail(dirName, sessionFile) {
297
318
  }
298
319
  }
299
320
 
300
- return { ...session, agents };
321
+ // Aggregate MCPs and skills from main session + all subagent files
322
+ const allMcps = new Set();
323
+ const allSkills = new Set();
324
+ const filesToScan = [filePath];
325
+ const subagentsDir = path.join(sessionDir, 'subagents');
326
+ if (fs.existsSync(subagentsDir)) {
327
+ fs.readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl'))
328
+ .forEach(f => filesToScan.push(path.join(subagentsDir, f)));
329
+ }
330
+ for (const f of filesToScan) {
331
+ const { mcps, skills } = collectToolsUsed(f);
332
+ mcps.forEach(m => allMcps.add(m));
333
+ skills.forEach(s => allSkills.add(s));
334
+ }
335
+
336
+ return { ...session, agents, sessionMcps: [...allMcps], sessionSkills: [...allSkills] };
301
337
  }
302
338
 
303
339
  function getAgentDetail(dirName, sessionFile, agentId) {
package/src/public/app.js CHANGED
@@ -276,6 +276,20 @@ async function loadSessionDetail(dirNameEncoded, fileEncoded) {
276
276
  state.currentProject = state.projects.find(p => p.dirName === dirName);
277
277
  }
278
278
 
279
+ // Build map: message uuid → skills used by agents spawned from that message
280
+ state.agentSkillsByUuid = {};
281
+ if (session.agents) {
282
+ for (const agent of session.agents) {
283
+ if (agent.spawnedByUuid && agent.skillsUsed?.length) {
284
+ const existing = state.agentSkillsByUuid[agent.spawnedByUuid] || [];
285
+ for (const s of agent.skillsUsed) {
286
+ if (!existing.includes(s)) existing.push(s);
287
+ }
288
+ state.agentSkillsByUuid[agent.spawnedByUuid] = existing;
289
+ }
290
+ }
291
+ }
292
+
279
293
  setBreadcrumb([state.currentProject?.path || dirName, session.title]);
280
294
  renderSessionDetail(session, dirName, file);
281
295
  }
@@ -288,28 +302,12 @@ function renderSessionDetail(session, dirName, file) {
288
302
  ? new Date(lastTimestamp) - new Date(firstTimestamp) : null;
289
303
  const hasAgents = agents && agents.length > 0;
290
304
 
291
- // Aggregate MCPs and skills across all messages
292
- const sessionMcps = new Set();
293
- const sessionSkills = new Set();
294
- for (const msg of session.messages) {
295
- if (msg.type !== 'assistant') continue;
296
- const content = Array.isArray(msg.content) ? msg.content : [];
297
- for (const block of content) {
298
- if (!block || block.type !== 'tool_use') continue;
299
- if (block.name.startsWith('mcp__')) {
300
- const parts = block.name.split('__');
301
- const server = (parts[1] || '').replace(/^claude_ai_/, '').replace(/_/g, ' ');
302
- if (server) sessionMcps.add(server);
303
- } else if (block.name === 'Skill' && block.input?.skill) {
304
- sessionSkills.add(block.input.skill);
305
- }
306
- }
307
- }
308
- const mcpBar = sessionMcps.size > 0
309
- ? `<div class="session-tools-bar">${[...sessionMcps].map(s => `<span class="usage-chip"><span class="tag-mcp">mcp</span>${escHtml(s)}</span>`).join('')}</div>`
305
+ // MCPs and skills pre-aggregated server-side (main + all subagents)
306
+ const mcpBar = session.sessionMcps?.length
307
+ ? `<div class="session-tools-bar">${session.sessionMcps.map(s => `<span class="usage-chip"><span class="tag-mcp">mcp</span>${escHtml(s)}</span>`).join('')}</div>`
310
308
  : '';
311
- const skillBar = sessionSkills.size > 0
312
- ? `<div class="session-tools-bar">${[...sessionSkills].map(s => `<span class="usage-chip"><span class="tag-skill">skill</span>${escHtml(s)}</span>`).join('')}</div>`
309
+ const skillBar = session.sessionSkills?.length
310
+ ? `<div class="session-tools-bar">${session.sessionSkills.map(s => `<span class="usage-chip"><span class="tag-skill">skill</span>${escHtml(s)}</span>`).join('')}</div>`
313
311
  : '';
314
312
 
315
313
  container.innerHTML = `
@@ -1000,6 +998,8 @@ function renderMessage(m, i, ctx, activeAgent) {
1000
998
  ${m.model ? `<span class="usage-chip" style="color:var(--accent)">${escHtml(modelShort(m.model))}</span>` : ''}
1001
999
  ${mcpServers.map(s => `<span class="usage-chip"><span class="tag-mcp">mcp</span>${escHtml(s)}</span>`).join('')}
1002
1000
  ${skillsUsed.map(s => `<span class="usage-chip"><span class="tag-skill">skill</span>${escHtml(s)}</span>`).join('')}
1001
+ ${activeAgent?.kind === 'skill' && !skillsUsed.includes(activeAgent.name) ? `<span class="usage-chip"><span class="tag-skill">skill</span>${escHtml(activeAgent.name)}</span>` : ''}
1002
+ ${(state.agentSkillsByUuid?.[m.uuid] || []).filter(s => !skillsUsed.includes(s)).map(s => `<span class="usage-chip"><span class="tag-skill">skill</span>${escHtml(s)}</span>`).join('')}
1003
1003
  </div>`;
1004
1004
  }
1005
1005
 
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ccanalyzer</title>
7
- <link rel="stylesheet" href="style.css?v=6" />
7
+ <link rel="stylesheet" href="style.css?v=9" />
8
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
9
  </head>
10
10
  <body>
@@ -33,6 +33,6 @@
33
33
  <div class="spinner"></div>
34
34
  </div>
35
35
 
36
- <script src="app.js?v=6"></script>
36
+ <script src="app.js?v=9"></script>
37
37
  </body>
38
38
  </html>