claude-mission-control 1.21.0 → 1.22.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/lib/collector.js +10 -4
- package/lib/transcripts.js +40 -4
- package/package.json +1 -1
package/lib/collector.js
CHANGED
|
@@ -621,6 +621,7 @@ class Collector {
|
|
|
621
621
|
const sessionDays = [];
|
|
622
622
|
let sessionTotal = 0;
|
|
623
623
|
let costTotal = 0;
|
|
624
|
+
const liveIds = new Set(this.raw.live.map((s) => s.sessionId));
|
|
624
625
|
const dayCut = Date.now() - 24 * 3600000;
|
|
625
626
|
const timeline = [];
|
|
626
627
|
const weekDayKey = dayKey(weekCut);
|
|
@@ -634,12 +635,14 @@ class Collector {
|
|
|
634
635
|
let weekCacheAll = 0;
|
|
635
636
|
for (const g of this.raw.transcriptGroups.values()) {
|
|
636
637
|
if (isIgnored(g.path)) continue;
|
|
637
|
-
sessionTotal += g.sessions.length;
|
|
638
638
|
const projModels = {};
|
|
639
639
|
for (const m of g.sessions) {
|
|
640
640
|
const mDays = combinedDays(m);
|
|
641
641
|
sessionDays.push(mDays);
|
|
642
|
-
|
|
642
|
+
// Automated and empty sessions count toward spend, not toward sessions.
|
|
643
|
+
const real = !sessionKind(m, liveIds.has(m.sessionId));
|
|
644
|
+
if (real) sessionTotal++;
|
|
645
|
+
if (real && m.lastActivityAt >= dayCut) {
|
|
643
646
|
timeline.push({
|
|
644
647
|
project: friendlyName(g.path),
|
|
645
648
|
sessionId: m.sessionId,
|
|
@@ -649,7 +652,7 @@ class Collector {
|
|
|
649
652
|
});
|
|
650
653
|
}
|
|
651
654
|
if (m.lastActivityAt >= weekCut) {
|
|
652
|
-
weekSessions++;
|
|
655
|
+
if (real) weekSessions++;
|
|
653
656
|
if (m.waits) for (let i = 0; i < 4; i++) weekWaits[i] += m.waits[i];
|
|
654
657
|
let mWeekCost = 0;
|
|
655
658
|
for (const [day, byM] of Object.entries(mDays)) {
|
|
@@ -675,7 +678,7 @@ class Collector {
|
|
|
675
678
|
mCost += cost;
|
|
676
679
|
projModels[model] = (projModels[model] || 0) + cost;
|
|
677
680
|
}
|
|
678
|
-
topCandidates.push({
|
|
681
|
+
if (real) topCandidates.push({
|
|
679
682
|
sessionId: m.sessionId,
|
|
680
683
|
title: this.titleOf(m).title,
|
|
681
684
|
projectName: friendlyName(g.path),
|
|
@@ -726,9 +729,12 @@ class Collector {
|
|
|
726
729
|
fameSessions() {
|
|
727
730
|
const pricing = readPricing();
|
|
728
731
|
const out = [];
|
|
732
|
+
const liveIds = new Set(this.raw.live.map((s) => s.sessionId));
|
|
729
733
|
for (const g of this.raw.transcriptGroups.values()) {
|
|
730
734
|
if (isIgnored(g.path)) continue;
|
|
731
735
|
for (const m of g.sessions) {
|
|
736
|
+
// A one-shot claude -p run that starts as a session ends would claim its commits.
|
|
737
|
+
if (sessionKind(m, liveIds.has(m.sessionId))) continue;
|
|
732
738
|
const ts = toolSummary(m.tools);
|
|
733
739
|
out.push({
|
|
734
740
|
sessionId: m.sessionId,
|
package/lib/transcripts.js
CHANGED
|
@@ -154,7 +154,7 @@ async function scanFile(abs, sessionIdFromName) {
|
|
|
154
154
|
}
|
|
155
155
|
if (meta.firstUserPrompt === null) {
|
|
156
156
|
const text = extractUserText(rec);
|
|
157
|
-
if (text) meta.firstUserPrompt = text.slice(0,
|
|
157
|
+
if (text) meta.firstUserPrompt = text.slice(0, 300); // room for tidyPromptTitle to drop a preamble
|
|
158
158
|
}
|
|
159
159
|
if (meta.cwd && meta.startedAt && meta.firstUserPrompt) break;
|
|
160
160
|
}
|
|
@@ -655,12 +655,48 @@ function dailyCostSeries(daysList, numDays, today = Date.now(), pricing = null)
|
|
|
655
655
|
// Best display title for a session, with its provenance.
|
|
656
656
|
// `custom` is a user-set name from config.json (sessionNames); it wins over
|
|
657
657
|
// anything derived from the transcript.
|
|
658
|
+
// A prompt reads badly as a title when it opens with where the work is:
|
|
659
|
+
// "In /Users/x/site/deploy.sh, the rsync flag…" or "In the Foo monorepo at
|
|
660
|
+
// `path` (branch y), the…". The card already names the project, so drop that
|
|
661
|
+
// clause, keep a file name it pointed at, and shorten home paths to ~/.
|
|
662
|
+
const LOCATION_WORDS = /\b(repo|repository|monorepo|project|folder|directory|codebase|theme|plugin|package|workspace)\b/i;
|
|
663
|
+
const homePaths = (t) => t.replace(/\/(?:Users|home)\/[^/\s]+\//g, '~/');
|
|
664
|
+
|
|
665
|
+
function leadingClauseEnd(t) {
|
|
666
|
+
let depth = 0, quote = null;
|
|
667
|
+
for (let i = 3; i < t.length; i++) {
|
|
668
|
+
const c = t[i];
|
|
669
|
+
if (quote) { if (c === quote) quote = null; continue; }
|
|
670
|
+
if (c === '`' || c === '"') quote = c;
|
|
671
|
+
else if ('([{'.includes(c)) depth++;
|
|
672
|
+
else if (')]}'.includes(c)) depth = Math.max(0, depth - 1);
|
|
673
|
+
else if ((c === ',' || c === ':') && depth === 0 && (i + 1 === t.length || t[i + 1] === ' ')) return i;
|
|
674
|
+
}
|
|
675
|
+
return -1;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function tidyPromptTitle(text) {
|
|
679
|
+
const t = String(text || '');
|
|
680
|
+
if (/^In /.test(t)) {
|
|
681
|
+
const end = leadingClauseEnd(t);
|
|
682
|
+
const clause = end > 0 ? t.slice(3, end) : '';
|
|
683
|
+
if (clause && (clause.includes('/') || LOCATION_WORDS.test(clause))) {
|
|
684
|
+
const rest = t.slice(end + 1).trim();
|
|
685
|
+
if (rest.length >= 8) {
|
|
686
|
+
const file = /\/([\w.-]+\.[a-z][a-z0-9]{0,5})(?=[\s`"'),]|$)/i.exec(clause);
|
|
687
|
+
return homePaths(file ? `${file[1]}: ${rest}` : rest);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return homePaths(t);
|
|
692
|
+
}
|
|
693
|
+
|
|
658
694
|
function sessionTitle(meta, customNames) {
|
|
659
695
|
const custom = customNames && customNames[meta.sessionId];
|
|
660
696
|
if (custom) return { title: custom, source: 'custom' };
|
|
661
697
|
if (meta.aiTitle) return { title: meta.aiTitle, source: 'ai-title' };
|
|
662
|
-
if (meta.firstUserPrompt) return { title: meta.firstUserPrompt, source: 'first-prompt' };
|
|
663
|
-
if (meta.lastPrompt) return { title: meta.lastPrompt, source: 'last-prompt' };
|
|
698
|
+
if (meta.firstUserPrompt) return { title: tidyPromptTitle(meta.firstUserPrompt).slice(0, 120), source: 'first-prompt' };
|
|
699
|
+
if (meta.lastPrompt) return { title: tidyPromptTitle(meta.lastPrompt).slice(0, 120), source: 'last-prompt' };
|
|
664
700
|
return { title: meta.sessionId.slice(0, 8), source: 'session-id' };
|
|
665
701
|
}
|
|
666
702
|
|
|
@@ -685,7 +721,7 @@ function groupByProject(sessions) {
|
|
|
685
721
|
}
|
|
686
722
|
|
|
687
723
|
module.exports = {
|
|
688
|
-
scanAllTranscripts, scanFile, groupByProject, sessionTitle, sessionKind, combinedUsage, mergeUsage,
|
|
724
|
+
scanAllTranscripts, scanFile, groupByProject, sessionTitle, tidyPromptTitle, sessionKind, combinedUsage, mergeUsage,
|
|
689
725
|
scanLine, mergeDays, combinedDays, dailyCostSeries, subagentSummary, usageBreakdown, dayKey, contextSummary, toolSummary,
|
|
690
726
|
waitingReason, reasonText, shouldReadMeta, TOOL_RING,
|
|
691
727
|
};
|
package/package.json
CHANGED