@uxcontinuum/ccwrapped 1.0.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 +92 -0
- package/index.js +749 -0
- package/package.json +33 -0
- package/wrapped.py +1171 -0
package/index.js
ADDED
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
// ── ANSI ──────────────────────────────────────────────────────────────────────
|
|
9
|
+
let C = {
|
|
10
|
+
bold: '\x1b[1m', dim: '\x1b[2m', reset: '\x1b[0m',
|
|
11
|
+
green: '\x1b[92m', cyan: '\x1b[96m', yellow: '\x1b[93m',
|
|
12
|
+
magenta: '\x1b[95m', red: '\x1b[91m', blue: '\x1b[94m', white: '\x1b[97m',
|
|
13
|
+
};
|
|
14
|
+
function noColor() { Object.keys(C).forEach(k => C[k] = ''); }
|
|
15
|
+
|
|
16
|
+
// ── PATHS ─────────────────────────────────────────────────────────────────────
|
|
17
|
+
function findClaudeDir() {
|
|
18
|
+
const candidates = [path.join(os.homedir(), '.claude', 'projects')];
|
|
19
|
+
if (os.homedir() === '/root') {
|
|
20
|
+
try {
|
|
21
|
+
for (const u of fs.readdirSync('/home')) {
|
|
22
|
+
const alt = `/home/${u}/.claude/projects`;
|
|
23
|
+
if (!candidates.includes(alt)) candidates.push(alt);
|
|
24
|
+
}
|
|
25
|
+
} catch (_) {}
|
|
26
|
+
}
|
|
27
|
+
for (const c of candidates) {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.statSync(c).isDirectory()) return c;
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
}
|
|
32
|
+
return candidates[0];
|
|
33
|
+
}
|
|
34
|
+
const CLAUDE_DIR = findClaudeDir();
|
|
35
|
+
|
|
36
|
+
// ── FILE SCAN ─────────────────────────────────────────────────────────────────
|
|
37
|
+
function findJsonl(dir, cutoffMs, out = []) {
|
|
38
|
+
let entries;
|
|
39
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return out; }
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
if (e.name === 'subagents') continue;
|
|
42
|
+
const full = path.join(dir, e.name);
|
|
43
|
+
if (e.isDirectory()) { findJsonl(full, cutoffMs, out); continue; }
|
|
44
|
+
if (!e.name.endsWith('.jsonl')) continue;
|
|
45
|
+
try {
|
|
46
|
+
if (fs.statSync(full).mtimeMs >= cutoffMs) out.push(full);
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── SESSION PARSE ─────────────────────────────────────────────────────────────
|
|
53
|
+
function projName(filePath) {
|
|
54
|
+
const parts = filePath.split(path.sep);
|
|
55
|
+
const idx = parts.lastIndexOf('projects');
|
|
56
|
+
if (idx < 0 || idx + 1 >= parts.length) return 'unknown';
|
|
57
|
+
let raw = parts[idx + 1];
|
|
58
|
+
const prefix = `-home-${os.userInfo().username}-`;
|
|
59
|
+
if (raw.startsWith(prefix)) raw = raw.slice(prefix.length);
|
|
60
|
+
else if (raw.startsWith('-home-')) raw = raw.replace(/^-+/, '');
|
|
61
|
+
return raw || '(home)';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseSession(filePath, cutoffMs) {
|
|
65
|
+
let lines;
|
|
66
|
+
try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch (_) { return null; }
|
|
67
|
+
|
|
68
|
+
const toolCalls = {};
|
|
69
|
+
const userPrompts = [];
|
|
70
|
+
const timestamps = [];
|
|
71
|
+
let inputTokens = 0, outputTokens = 0, title = null;
|
|
72
|
+
|
|
73
|
+
for (const raw of lines) {
|
|
74
|
+
if (!raw.trim()) continue;
|
|
75
|
+
let msg;
|
|
76
|
+
try { msg = JSON.parse(raw); } catch (_) { continue; }
|
|
77
|
+
|
|
78
|
+
if (msg.type === 'custom-title') { title = msg.title || ''; continue; }
|
|
79
|
+
if (msg.type !== 'user' && msg.type !== 'assistant') continue;
|
|
80
|
+
|
|
81
|
+
const ts = msg.timestamp ? Date.parse(msg.timestamp) : 0;
|
|
82
|
+
if (ts && ts < cutoffMs) continue;
|
|
83
|
+
if (ts) timestamps.push(ts);
|
|
84
|
+
|
|
85
|
+
const content = msg.message?.content ?? [];
|
|
86
|
+
if (msg.type === 'assistant') {
|
|
87
|
+
const u = msg.message?.usage ?? {};
|
|
88
|
+
inputTokens += u.input_tokens ?? 0;
|
|
89
|
+
outputTokens += u.output_tokens ?? 0;
|
|
90
|
+
if (Array.isArray(content)) {
|
|
91
|
+
for (const b of content)
|
|
92
|
+
if (b?.type === 'tool_use') toolCalls[b.name] = (toolCalls[b.name] ?? 0) + 1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (msg.type === 'user' && Array.isArray(content)) {
|
|
96
|
+
for (const b of content)
|
|
97
|
+
if (b?.type === 'text' && b.text?.trim()) userPrompts.push(b.text.trim());
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!timestamps.length) return null;
|
|
102
|
+
|
|
103
|
+
let durationMins = 0;
|
|
104
|
+
if (timestamps.length >= 2) {
|
|
105
|
+
durationMins = Math.min(480, Math.floor((Math.max(...timestamps) - Math.min(...timestamps)) / 60000));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
project: projName(filePath),
|
|
110
|
+
title: title ?? '',
|
|
111
|
+
toolCalls, userPrompts, timestamps,
|
|
112
|
+
durationMins, inputTokens, outputTokens,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── AGGREGATE ─────────────────────────────────────────────────────────────────
|
|
117
|
+
function aggregate(sessions) {
|
|
118
|
+
const totalTools = {}, projects = {}, hours = {};
|
|
119
|
+
let allPrompts = [], allTs = [], longest = null, longestMins = 0;
|
|
120
|
+
let totalOut = 0, totalIn = 0;
|
|
121
|
+
|
|
122
|
+
for (const s of sessions) {
|
|
123
|
+
for (const [t, c] of Object.entries(s.toolCalls)) totalTools[t] = (totalTools[t] ?? 0) + c;
|
|
124
|
+
allPrompts = allPrompts.concat(s.userPrompts);
|
|
125
|
+
allTs = allTs.concat(s.timestamps);
|
|
126
|
+
projects[s.project] = (projects[s.project] ?? 0) + 1;
|
|
127
|
+
totalOut += s.outputTokens;
|
|
128
|
+
totalIn += s.inputTokens;
|
|
129
|
+
if (s.durationMins > longestMins) { longestMins = s.durationMins; longest = s; }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const totalToolCalls = Object.values(totalTools).reduce((a, b) => a + b, 0);
|
|
133
|
+
const toolPct = {};
|
|
134
|
+
if (totalToolCalls) for (const [t, c] of Object.entries(totalTools)) toolPct[t] = Math.round(100 * c / totalToolCalls);
|
|
135
|
+
|
|
136
|
+
for (const ts of allTs) { const h = new Date(ts).getHours(); hours[h] = (hours[h] ?? 0) + 1; }
|
|
137
|
+
const peakHour = Object.entries(hours).sort((a,b) => b[1]-a[1])[0]?.[0] ?? 12;
|
|
138
|
+
const nightCt = [22,23,0,1,2,3,4].reduce((s,h) => s + (hours[h] ?? 0), 0);
|
|
139
|
+
const nightPct = allTs.length ? Math.round(100 * nightCt / allTs.length) : 0;
|
|
140
|
+
|
|
141
|
+
const STOP = new Set(['this','that','with','from','have','will','just','your','what','when',
|
|
142
|
+
'then','also','some','into','make','more','here','they','them','been','were','need','want',
|
|
143
|
+
'code','file','like','very','only','about','would','should','could','there','their','these',
|
|
144
|
+
'those','such','both']);
|
|
145
|
+
const wordCt = {};
|
|
146
|
+
for (const p of allPrompts)
|
|
147
|
+
for (const w of p.toLowerCase().match(/\b[a-z]{4,}\b/g) ?? [])
|
|
148
|
+
if (!STOP.has(w)) wordCt[w] = (wordCt[w] ?? 0) + 1;
|
|
149
|
+
const topWord = Object.entries(wordCt).sort((a,b) => b[1]-a[1])[0] ?? ['', 0];
|
|
150
|
+
|
|
151
|
+
const polite = allPrompts.filter(p => /\b(please|thank|thanks|sorry)\b/i.test(p)).length;
|
|
152
|
+
const justCount = allPrompts.reduce((n, p) => n + (p.toLowerCase().split('just').length - 1), 0);
|
|
153
|
+
const avgLen = allPrompts.length ? Math.floor(allPrompts.reduce((s,p) => s + p.length, 0) / allPrompts.length) : 0;
|
|
154
|
+
const untitled = sessions.filter(s => !s.title).length;
|
|
155
|
+
const topTools = Object.entries(totalTools).sort((a,b) => b[1]-a[1]).slice(0,5);
|
|
156
|
+
const topProj = Object.entries(projects).sort((a,b) => b[1]-a[1])[0] ?? ['none', 0];
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
totalSessions: sessions.length, totalToolCalls, totalOut, totalIn,
|
|
160
|
+
toolPct, topTools, topProj, projectCount: Object.keys(projects).length,
|
|
161
|
+
peakHour: +peakHour, nightPct, longest, longestMins, avgLen,
|
|
162
|
+
politeCount: polite, justCount, untitledCount: untitled, topWord,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── FORMAT HELPERS ────────────────────────────────────────────────────────────
|
|
167
|
+
const fmtHour = h => h === 0 ? 'midnight' : h < 12 ? `${h}am` : h === 12 ? 'noon' : `${h-12}pm`;
|
|
168
|
+
const fmtTok = n => n >= 1e6 ? `${(n/1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.floor(n/1e3)}K` : `${n}`;
|
|
169
|
+
const fmtDur = m => m < 60 ? `${m}m` : `${Math.floor(m/60)}h${m%60 ? ` ${m%60}m` : ''}`;
|
|
170
|
+
|
|
171
|
+
function wrap(text, width) {
|
|
172
|
+
const words = text.split(' ');
|
|
173
|
+
const lines = [];
|
|
174
|
+
let line = '';
|
|
175
|
+
for (const w of words) {
|
|
176
|
+
if (line && line.length + 1 + w.length > width) { lines.push(line); line = w; }
|
|
177
|
+
else line = line ? `${line} ${w}` : w;
|
|
178
|
+
}
|
|
179
|
+
if (line) lines.push(line);
|
|
180
|
+
return lines;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function pr(text = '') { process.stdout.write(text + '\n'); }
|
|
184
|
+
|
|
185
|
+
// ── ARCHETYPES ────────────────────────────────────────────────────────────────
|
|
186
|
+
const ARCHETYPES = {
|
|
187
|
+
'THE ORCHESTRATOR': {
|
|
188
|
+
color: 'magenta',
|
|
189
|
+
short: "Why do one thing when you can spawn five agents in parallel? You delegate like a CEO who just discovered hiring.",
|
|
190
|
+
full: "You've internalized the most important insight in AI-native development: Claude is a fleet, not a tool. While others prompt one thing at a time, you parallelize. Your sessions look like org charts.",
|
|
191
|
+
famousFor: "Spawning subagents to research while you're already writing the implementation",
|
|
192
|
+
strength: "You get compound leverage out of Claude that most users never access.",
|
|
193
|
+
watchOut: "Orchestration overhead can exceed the task cost. Sometimes one good prompt beats five parallel ones.",
|
|
194
|
+
},
|
|
195
|
+
'THE BASH HAMMER': {
|
|
196
|
+
color: 'red',
|
|
197
|
+
short: "You don't read docs. You run things and see what breaks. The terminal is your native tongue.",
|
|
198
|
+
full: "Your debugging strategy is empirical: run it, watch it fail, learn from the output. Documentation is for people who have time. You'd rather have stderr than a man page. The feedback loop between you and a running process is tighter than most engineers get in a full week.",
|
|
199
|
+
famousFor: "Running a command to see if it works before asking why it might not",
|
|
200
|
+
strength: "Speed. Nothing you ship has been theorized to death.",
|
|
201
|
+
watchOut: '"It worked on my machine" is a philosophy, not a workaround.',
|
|
202
|
+
},
|
|
203
|
+
'THE DETECTIVE': {
|
|
204
|
+
color: 'cyan',
|
|
205
|
+
short: "You investigate before you act. The codebase has no secrets from you. You find things first.",
|
|
206
|
+
full: "You Grep before you guess. You Read before you rewrite. Your sessions start with an investigation phase that most developers skip entirely, which is why you break fewer things.",
|
|
207
|
+
famousFor: "Knowing exactly where the bug is before touching a single file",
|
|
208
|
+
strength: "Your changes are targeted. Your blast radius is minimal.",
|
|
209
|
+
watchOut: "Sometimes you read so much the fix is obvious in your head but still not in the code.",
|
|
210
|
+
},
|
|
211
|
+
'THE FILE SURGEON': {
|
|
212
|
+
color: 'green',
|
|
213
|
+
short: "Precise, deliberate, minimal blast radius. You fix what needs fixing and leave the rest alone.",
|
|
214
|
+
full: "Your PRs are small and your diffs are clean. While others rewrite files to fix a function, you cut with precision. Edit + Write dominate your sessions because you're not exploring — you already know what needs to change.",
|
|
215
|
+
famousFor: "The smallest diff that completely solves the problem",
|
|
216
|
+
strength: "Clean git history. Low regression risk. Reviews that take 5 minutes.",
|
|
217
|
+
watchOut: "Sometimes the right fix actually is a full rewrite. Precision can become avoidance.",
|
|
218
|
+
},
|
|
219
|
+
'THE NIGHT OWL': {
|
|
220
|
+
color: 'blue',
|
|
221
|
+
short: "Your best code ships after midnight. Sleep is optional. The dark hours are yours.",
|
|
222
|
+
full: "You've found the only time slot with no meetings, no Slack pings, and no one asking for status updates. The commit timestamps tell the story.",
|
|
223
|
+
famousFor: "Pushing commits at 2am that somehow work in standup",
|
|
224
|
+
strength: "Uninterrupted focus blocks that most developers can only dream about.",
|
|
225
|
+
watchOut: "The merge conflicts waiting in the morning. And the sleep debt.",
|
|
226
|
+
},
|
|
227
|
+
'THE ESSAYIST': {
|
|
228
|
+
color: 'yellow',
|
|
229
|
+
short: "You explain everything. Context, edge cases, constraints. Claude always knows exactly what you want.",
|
|
230
|
+
full: "Your prompts read like engineering specs. You front-load context, enumerate constraints, and explain what you've already tried. Claude's first output hits close to correct more often than average.",
|
|
231
|
+
famousFor: "3-paragraph prompts that get first-pass outputs most people take 5 attempts to reach",
|
|
232
|
+
strength: "High hit rate on first output. Claude is never guessing with you.",
|
|
233
|
+
watchOut: "Occasionally the prompt takes longer to write than the task would have taken to do manually.",
|
|
234
|
+
},
|
|
235
|
+
'THE COMMANDER': {
|
|
236
|
+
color: 'red',
|
|
237
|
+
short: 'Short, direct, no ceremony. "Fix it." "Add auth." "Make it faster." Claude figures out the rest.',
|
|
238
|
+
full: "You prompt like someone who's been doing this long enough to trust the model. Your sessions move fast.",
|
|
239
|
+
famousFor: "Two-word prompts that somehow produce exactly the right thing",
|
|
240
|
+
strength: "Tight iteration loops. High sessions-per-hour. No wasted setup.",
|
|
241
|
+
watchOut: "The context that lives only in your head sometimes doesn't make it into the output.",
|
|
242
|
+
},
|
|
243
|
+
'THE GENERALIST': {
|
|
244
|
+
color: 'white',
|
|
245
|
+
short: "No single tool dominates. You read the problem and pick the right approach. That's rarer than it sounds.",
|
|
246
|
+
full: "Your tool usage is balanced because your problems are varied. You're not locked into a pattern.",
|
|
247
|
+
famousFor: "Knowing which tool to reach for before you open the file",
|
|
248
|
+
strength: "You don't have a hammer, so nothing looks like a nail.",
|
|
249
|
+
watchOut: "Balance can read as indecisiveness. Sometimes a bias toward one approach is a feature.",
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function getArchetypeProfile(stats) {
|
|
254
|
+
const { toolPct, nightPct, avgLen } = stats;
|
|
255
|
+
const bash = toolPct.Bash ?? 0;
|
|
256
|
+
const read = (toolPct.Read ?? 0) + (toolPct.Glob ?? 0) + (toolPct.Grep ?? 0);
|
|
257
|
+
const edit = (toolPct.Edit ?? 0) + (toolPct.Write ?? 0);
|
|
258
|
+
const agent = toolPct.Agent ?? 0;
|
|
259
|
+
|
|
260
|
+
let name;
|
|
261
|
+
if (agent > 10) name = 'THE ORCHESTRATOR';
|
|
262
|
+
else if (bash > 55) name = 'THE BASH HAMMER';
|
|
263
|
+
else if (read > 50) name = 'THE DETECTIVE';
|
|
264
|
+
else if (edit > 40) name = 'THE FILE SURGEON';
|
|
265
|
+
else if (nightPct>35) name = 'THE NIGHT OWL';
|
|
266
|
+
else if (avgLen > 600)name = 'THE ESSAYIST';
|
|
267
|
+
else if (avgLen < 80) name = 'THE COMMANDER';
|
|
268
|
+
else name = 'THE GENERALIST';
|
|
269
|
+
|
|
270
|
+
const profile = { ...ARCHETYPES[name], name };
|
|
271
|
+
|
|
272
|
+
const t5 = stats.topTools;
|
|
273
|
+
if (name === 'THE BASH HAMMER')
|
|
274
|
+
profile.supporting = [`Bash: ${bash}% of all tool calls`, `Sessions: ${stats.totalSessions.toLocaleString()} total`, `Peak hour: ${fmtHour(stats.peakHour)}`];
|
|
275
|
+
else if (name === 'THE DETECTIVE')
|
|
276
|
+
profile.supporting = [`Grep + Read + Glob: ${read}% of tool calls`, `Projects explored: ${stats.projectCount}`, `Avg prompt length: ${avgLen} chars`];
|
|
277
|
+
else if (name === 'THE FILE SURGEON')
|
|
278
|
+
profile.supporting = [`Edit + Write: ${edit}% of tool calls`, `Sessions: ${stats.totalSessions.toLocaleString()} total`, `Tokens generated: ${fmtTok(stats.totalOut)}`];
|
|
279
|
+
else if (name === 'THE ORCHESTRATOR')
|
|
280
|
+
profile.supporting = [`Agent calls: ${agent}% of tool calls`, `Total tool calls: ${stats.totalToolCalls.toLocaleString()}`, `Projects active: ${stats.projectCount}`];
|
|
281
|
+
else if (name === 'THE NIGHT OWL')
|
|
282
|
+
profile.supporting = [`After-10pm sessions: ${nightPct}%`, `Peak hour: ${fmtHour(stats.peakHour)}`, `Total sessions: ${stats.totalSessions.toLocaleString()}`];
|
|
283
|
+
else if (name === 'THE ESSAYIST')
|
|
284
|
+
profile.supporting = [`Avg prompt: ${avgLen} characters`, `Sessions: ${stats.totalSessions.toLocaleString()}`, `Top tool: ${t5[0]?.[0] ?? 'Bash'}`];
|
|
285
|
+
else if (name === 'THE COMMANDER')
|
|
286
|
+
profile.supporting = [`Avg prompt: ${avgLen} characters`, `Sessions: ${stats.totalSessions.toLocaleString()}`, `Peak hour: ${fmtHour(stats.peakHour)}`];
|
|
287
|
+
else
|
|
288
|
+
profile.supporting = t5.slice(0,3).map(([t,c]) => `${t}: ${c} calls`);
|
|
289
|
+
|
|
290
|
+
return profile;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function getSecondaryTrait(stats) {
|
|
294
|
+
const { justCount, politeCount, untitledCount, totalSessions, nightPct, toolPct } = stats;
|
|
295
|
+
if (justCount > 30) return ['The Just-er', `You said "just" ${justCount} times. Just do it. Just fix it. Just make it work.`];
|
|
296
|
+
if (politeCount > 20) return ['The Polite Prompter', `You said please or thank you ${politeCount} times. Claude appreciates it.`];
|
|
297
|
+
if (untitledCount > totalSessions * 0.5) {
|
|
298
|
+
const pct = Math.round(100 * untitledCount / totalSessions);
|
|
299
|
+
return ['The Untitled', `${pct}% of your sessions have no title. Living dangerously.`];
|
|
300
|
+
}
|
|
301
|
+
if (nightPct > 25 && (toolPct.Bash ?? 0) > 40)
|
|
302
|
+
return ['The 2am Deployer', "You know what you're doing. You just don't know when to stop."];
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── SPIRIT MODEL ──────────────────────────────────────────────────────────────
|
|
307
|
+
function getSpiritModel(stats) {
|
|
308
|
+
const { avgLen, toolPct, untitledCount, totalSessions } = stats;
|
|
309
|
+
const read = (toolPct.Read ?? 0) + (toolPct.Grep ?? 0) + (toolPct.Glob ?? 0);
|
|
310
|
+
const bash = toolPct.Bash ?? 0;
|
|
311
|
+
const untitledPct = totalSessions ? untitledCount / totalSessions : 0;
|
|
312
|
+
|
|
313
|
+
if (avgLen > 500 || read > 40) return {
|
|
314
|
+
name: 'Claude Opus',
|
|
315
|
+
color: 'magenta',
|
|
316
|
+
tagline: 'Methodical. First-principles. You think before you type.',
|
|
317
|
+
quip: 'You could write a thesis before writing the code. Sometimes you do.',
|
|
318
|
+
stats: [`Avg prompt: ${avgLen} chars`, `Read/Grep/Glob: ${read}% of tool calls`],
|
|
319
|
+
};
|
|
320
|
+
if (avgLen < 120 || (bash > 60 && untitledPct > 0.5)) return {
|
|
321
|
+
name: 'Claude Haiku',
|
|
322
|
+
color: 'cyan',
|
|
323
|
+
tagline: 'Fast, minimal, no ceremony.',
|
|
324
|
+
quip: 'You prompt like you\'re billed by the token.',
|
|
325
|
+
stats: [`Avg prompt: ${avgLen} chars`, `Top tool: ${stats.topTools[0]?.[0] ?? 'Bash'}`],
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
name: 'Claude Sonnet',
|
|
329
|
+
color: 'green',
|
|
330
|
+
tagline: 'Balanced, pragmatic, gets it done.',
|
|
331
|
+
quip: "You've found the balance and you know it.",
|
|
332
|
+
stats: [`Avg prompt: ${avgLen} chars`, `Top tool: ${stats.topTools[0]?.[0] ?? 'Bash'}`],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── PROMPT DNA ────────────────────────────────────────────────────────────────
|
|
337
|
+
function getPromptDna(stats) {
|
|
338
|
+
const { avgLen, politeCount, totalSessions, toolPct, nightPct, peakHour, justCount } = stats;
|
|
339
|
+
const bash = toolPct.Bash ?? 0;
|
|
340
|
+
|
|
341
|
+
const formality = politeCount > totalSessions * 0.3
|
|
342
|
+
? ['formal', 'You treat Claude like a colleague. Please and thank you included.']
|
|
343
|
+
: avgLen < 80
|
|
344
|
+
? ['blunt', 'No preamble. No ceremony. You get to the point faster than most people think.']
|
|
345
|
+
: ['direct', 'Professional but not stiff. You explain what you need without small talk.'];
|
|
346
|
+
|
|
347
|
+
const verbosity = avgLen > 700
|
|
348
|
+
? ['verbose', `Avg ${avgLen} chars per prompt. You front-load context and explain edge cases. Claude rarely has to ask a follow-up.`]
|
|
349
|
+
: avgLen > 300
|
|
350
|
+
? ['moderate', `Avg ${avgLen} chars. Enough context to be useful, not so much that you're writing a spec.`]
|
|
351
|
+
: avgLen > 100
|
|
352
|
+
? ['terse', `Avg ${avgLen} chars. Short and functional. You've learned what Claude needs.`]
|
|
353
|
+
: ['minimal', `Avg ${avgLen} chars. "Fix it." "Add tests." Claude fills in the rest.`];
|
|
354
|
+
|
|
355
|
+
const style = bash > 55
|
|
356
|
+
? ['executor', 'Your prompts are task orders. You want Claude to run something, not think about running something.']
|
|
357
|
+
: justCount > 40
|
|
358
|
+
? ['minimizer', `You say "just" a lot (${justCount}x). You minimize the ask. It's a habit.`]
|
|
359
|
+
: avgLen > 600
|
|
360
|
+
? ['specifier', 'You write prompts like requirements. Comprehensive. Claude gets it right because you leave nothing out.']
|
|
361
|
+
: ['collaborator', 'You prompt back and forth. Claude is a thinking partner, not an executor.'];
|
|
362
|
+
|
|
363
|
+
const timing = nightPct > 35
|
|
364
|
+
? ['nocturnal', 'Most of your prompts land after 10pm. Your creative output happens in the dark.']
|
|
365
|
+
: peakHour < 9
|
|
366
|
+
? ['early riser', "You're at it before most people open Slack."]
|
|
367
|
+
: peakHour > 17
|
|
368
|
+
? ['after-hours', 'Your peak is late afternoon into evening. You warm up slowly.']
|
|
369
|
+
: ['office hours', `Peak hour: ${fmtHour(peakHour)}. You work when the world expects you to.`];
|
|
370
|
+
|
|
371
|
+
const summaries = {
|
|
372
|
+
'formal-verbose-specifier': 'You prompt like a senior engineer writing an RFC. Thorough, polite, leaves nothing to interpretation.',
|
|
373
|
+
'formal-verbose-collaborator': 'You prompt like a thoughtful manager delegating to a smart report. Context-first, measured tone.',
|
|
374
|
+
'blunt-minimal-executor': 'You prompt like a tired sysadmin at 2am. Fast, functional, zero ceremony. It works.',
|
|
375
|
+
'blunt-terse-executor': "Short commands, high trust. You've calibrated Claude to your shorthand.",
|
|
376
|
+
'direct-moderate-collaborator': "Clear and efficient. You prompt like someone who respects both their time and Claude's ability.",
|
|
377
|
+
'direct-verbose-specifier': 'You prompt like a product manager who is also a developer. Structured, specific, outcome-focused.',
|
|
378
|
+
'direct-terse-executor': 'Functional and fast. You know what you want and you say it.',
|
|
379
|
+
'direct-moderate-executor': "You prompt like a developer who's done this before. Not terse, not long-winded. Just clear.",
|
|
380
|
+
'direct-verbose-executor': 'You write long prompts that end in short commands. The context is the care; the task is the punch.',
|
|
381
|
+
'direct-terse-collaborator': 'Efficient back-and-forth. Short prompts, quick corrections, good signal.',
|
|
382
|
+
'blunt-verbose-executor': "Dense task orders. You write a lot but you're still telling Claude what to do, not asking.",
|
|
383
|
+
'blunt-moderate-executor': 'You front-load just enough context to get the output right, then ship it. Clean loop.',
|
|
384
|
+
'formal-terse-specifier': 'Polite but demanding. You know exactly what you want and you ask for it nicely.',
|
|
385
|
+
'formal-moderate-collaborator': 'You work with Claude like a trusted colleague. Respectful, collaborative, clear.',
|
|
386
|
+
};
|
|
387
|
+
const key = `${formality[0]}-${verbosity[0]}-${style[0]}`;
|
|
388
|
+
const summary = summaries[key] ?? `You prompt ${formality[0]}, ${verbosity[0]}, and with a ${style[0]} style. That combination is distinctly yours.`;
|
|
389
|
+
|
|
390
|
+
return { formality, verbosity, style, timing, summary };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── TUNE-UP ───────────────────────────────────────────────────────────────────
|
|
394
|
+
function generateTuneUpRules(stats) {
|
|
395
|
+
const { toolPct, avgLen, justCount, untitledCount, totalSessions, politeCount, nightPct, peakHour } = stats;
|
|
396
|
+
const bash = toolPct.Bash ?? 0;
|
|
397
|
+
const read = (toolPct.Read ?? 0) + (toolPct.Glob ?? 0) + (toolPct.Grep ?? 0);
|
|
398
|
+
const edit = (toolPct.Edit ?? 0) + (toolPct.Write ?? 0);
|
|
399
|
+
const agent = toolPct.Agent ?? 0;
|
|
400
|
+
const rules = [];
|
|
401
|
+
|
|
402
|
+
if (bash > 55)
|
|
403
|
+
rules.push([`When I ask you to run or check something, use Bash first. Don't suggest reading docs or alternative approaches unless I ask.`, `You use Bash ${bash}% of the time.`]);
|
|
404
|
+
if (avgLen > 500)
|
|
405
|
+
rules.push([`I provide full context in my prompts. Start working with what I give you. Don't ask clarifying questions before attempting the task.`, `Your avg prompt is ${avgLen} chars.`]);
|
|
406
|
+
if (justCount > 25)
|
|
407
|
+
rules.push([`When I use the word 'just' in a prompt, ignore it as a signal of simplicity. The task is rarely as simple as that word implies.`, `You said 'just' ${justCount} times.`]);
|
|
408
|
+
if (edit > 40)
|
|
409
|
+
rules.push([`Prefer targeted edits over full rewrites. Keep diffs minimal unless I explicitly ask for a rewrite.`, `Edit + Write is ${edit}% of your tool calls.`]);
|
|
410
|
+
if (read > 45)
|
|
411
|
+
rules.push([`Before making changes to unfamiliar code, read the relevant files first. Don't assume structure from filenames.`, `You use Read/Grep/Glob ${read}% of the time.`]);
|
|
412
|
+
if (nightPct > 30)
|
|
413
|
+
rules.push([`Keep responses direct and skip lengthy preambles. I often work late and don't need warmup text before the answer.`, `${nightPct}% of your sessions are after 10pm.`]);
|
|
414
|
+
if (peakHour <= 7)
|
|
415
|
+
rules.push([`My peak hour is ${fmtHour(peakHour)}. Get straight to the work. Skip introductory context I already know.`, `Early sessions suggest you prefer efficiency.`]);
|
|
416
|
+
if (agent > 8)
|
|
417
|
+
rules.push([`When a task can be parallelized, suggest spawning subagents rather than doing things sequentially.`, `You use Agent calls ${agent}% of the time.`]);
|
|
418
|
+
if (avgLen < 80)
|
|
419
|
+
rules.push([`My prompts are often short. Use context from recent conversation history to fill in what I haven't explicitly stated.`, `Avg prompt is ${avgLen} chars.`]);
|
|
420
|
+
if (untitledCount > totalSessions * 0.6)
|
|
421
|
+
rules.push([`If we've done something worth remembering, suggest a session title at the end.`, `${Math.round(100*untitledCount/totalSessions)}% of your sessions are untitled.`]);
|
|
422
|
+
|
|
423
|
+
return rules;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── ROAST ─────────────────────────────────────────────────────────────────────
|
|
427
|
+
function buildRoast(stats, days) {
|
|
428
|
+
const { totalSessions, toolPct, nightPct, peakHour, avgLen, justCount, politeCount,
|
|
429
|
+
untitledCount, totalOut, projectCount, longestMins, topWord } = stats;
|
|
430
|
+
const bash = toolPct.Bash ?? 0;
|
|
431
|
+
const grep = toolPct.Grep ?? 0;
|
|
432
|
+
const read = toolPct.Read ?? 0;
|
|
433
|
+
const edit = (toolPct.Edit ?? 0) + (toolPct.Write ?? 0);
|
|
434
|
+
const agent = toolPct.Agent ?? 0;
|
|
435
|
+
const lines = [];
|
|
436
|
+
|
|
437
|
+
if (totalSessions > 10000)
|
|
438
|
+
lines.push(`You opened ${totalSessions.toLocaleString()} Claude sessions in ${days} days. At some point that stops being productivity and starts being a coping mechanism.`);
|
|
439
|
+
else if (totalSessions > 1000)
|
|
440
|
+
lines.push(`${totalSessions.toLocaleString()} sessions in ${days} days. Claude is basically your main relationship at this point.`);
|
|
441
|
+
else if (totalSessions > 100)
|
|
442
|
+
lines.push(`${totalSessions} sessions in ${days} days. Healthy. Worrying about you less now.`);
|
|
443
|
+
else
|
|
444
|
+
lines.push(`Only ${totalSessions} sessions in ${days} days. Either you have a healthy work-life balance or Claude kept crashing.`);
|
|
445
|
+
|
|
446
|
+
if (bash > 60)
|
|
447
|
+
lines.push(`Bash is ${bash}% of your tool calls. You are not using Claude to help you think. You are using Claude to hold your terminal history.`);
|
|
448
|
+
else if (bash > 45)
|
|
449
|
+
lines.push(`You run Bash ${bash}% of the time. The docs exist. They are right there.`);
|
|
450
|
+
|
|
451
|
+
if (agent > 15)
|
|
452
|
+
lines.push(`You spawn subagents ${agent}% of the time. That's not workflow optimization, that's just hiring Claude to manage Claude.`);
|
|
453
|
+
else if (agent > 5)
|
|
454
|
+
lines.push(`You've discovered subagents. Give it two weeks.`);
|
|
455
|
+
|
|
456
|
+
if (grep + read > 50 && edit < 15)
|
|
457
|
+
lines.push(`You spend ${grep+read}% of your time reading the codebase and ${edit}% changing it. At some point reading becomes procrastinating.`);
|
|
458
|
+
|
|
459
|
+
if (nightPct > 40)
|
|
460
|
+
lines.push(`${nightPct}% of your sessions started after 10pm. You have found a way to be consistently online during the hours when judgment is lowest. This is fine.`);
|
|
461
|
+
else if (nightPct > 25)
|
|
462
|
+
lines.push(`${nightPct}% night sessions. Peak hour was ${fmtHour(peakHour)}. You're not a night owl, you're just avoiding something.`);
|
|
463
|
+
|
|
464
|
+
if (avgLen > 800)
|
|
465
|
+
lines.push(`Your average prompt is ${avgLen} characters. You write more context for Claude than you do for your teammates. This is probably correct.`);
|
|
466
|
+
else if (avgLen < 60)
|
|
467
|
+
lines.push(`Average prompt length: ${avgLen} characters. "Fix it" is a complete sentence to you. Claude is doing a lot of inference work on your behalf.`);
|
|
468
|
+
|
|
469
|
+
if (justCount > 50)
|
|
470
|
+
lines.push(`You said "just" ${justCount} times. Just fix it. Just add auth. Just make it work. The word "just" implies simplicity. Nothing you're asking Claude to do is simple.`);
|
|
471
|
+
else if (justCount > 20)
|
|
472
|
+
lines.push(`You said "just" ${justCount} times. It's a tell. You say it when you know the task is bigger than you're admitting.`);
|
|
473
|
+
|
|
474
|
+
if (politeCount > 30)
|
|
475
|
+
lines.push(`You said please or thank you ${politeCount} times. Claude is not going to remember this. But it says something about you.`);
|
|
476
|
+
else if (politeCount === 0 && totalSessions > 50)
|
|
477
|
+
lines.push(`You've never once said please or thank you to Claude. Not once. That's a choice.`);
|
|
478
|
+
|
|
479
|
+
if (untitledCount > totalSessions * 0.7) {
|
|
480
|
+
const pct = Math.round(100 * untitledCount / totalSessions);
|
|
481
|
+
lines.push(`${pct}% of your sessions have no title. You are living in a sea of "New Session" and "New Session (2)" and you have made peace with this.`);
|
|
482
|
+
} else if (untitledCount > 20)
|
|
483
|
+
lines.push(`${untitledCount} sessions with no title. Future you is going to hate current you for this.`);
|
|
484
|
+
|
|
485
|
+
const books = (totalOut / 5 / 90000).toFixed(1);
|
|
486
|
+
if (+books > 100)
|
|
487
|
+
lines.push(`Claude has generated ${fmtTok(totalOut)} output tokens for you. That's ${books}x the word count of Lord of the Rings. You've read all of it and remembered approximately none of it.`);
|
|
488
|
+
else if (+books > 10)
|
|
489
|
+
lines.push(`${fmtTok(totalOut)} output tokens. You've received more words from Claude than exist in most novels. Some of them were probably useful.`);
|
|
490
|
+
|
|
491
|
+
if (projectCount > 500)
|
|
492
|
+
lines.push(`You have ${projectCount} active projects. That's not a portfolio. That's avoidance with version control.`);
|
|
493
|
+
else if (projectCount > 50)
|
|
494
|
+
lines.push(`${projectCount} different projects in ${days} days. You start things.`);
|
|
495
|
+
|
|
496
|
+
if (longestMins >= 480)
|
|
497
|
+
lines.push(`Your longest session hit the 8-hour cap on our tracker. We stopped counting. You didn't stop working.`);
|
|
498
|
+
else if (longestMins > 120)
|
|
499
|
+
lines.push(`Your longest session was ${fmtDur(longestMins)}. You got into it.`);
|
|
500
|
+
|
|
501
|
+
if (topWord[0] && topWord[1] > 20)
|
|
502
|
+
lines.push(`Your most common word in prompts was "${topWord[0]}" at ${topWord[1]} times. This is either your main domain or your main problem.`);
|
|
503
|
+
|
|
504
|
+
lines.push(`None of this is criticism. ${totalSessions.toLocaleString()} sessions in ${days} days means you're actually using the tool. That puts you ahead of most people who installed it and forgot about it.`);
|
|
505
|
+
return lines;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── PRINT FUNCTIONS ───────────────────────────────────────────────────────────
|
|
509
|
+
const W = 46;
|
|
510
|
+
const sep = () => pr(` ${C.dim}${'─'.repeat(W)}${C.reset}`);
|
|
511
|
+
const bar = (ch = '━') => pr(` ${C.bold}${C.cyan}${ch.repeat(W)}${C.reset}`);
|
|
512
|
+
|
|
513
|
+
function slide(emoji, headline, body, color = C.white) {
|
|
514
|
+
pr();
|
|
515
|
+
pr(` ${C.bold}${color}${emoji} ${headline}${C.reset}`);
|
|
516
|
+
pr();
|
|
517
|
+
for (const l of wrap(body.replace(/\x1b\[[0-9;]*m/g,''), W)) pr(` ${l}`);
|
|
518
|
+
pr();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function printWrapped(stats, days) {
|
|
522
|
+
const profile = getArchetypeProfile(stats);
|
|
523
|
+
const secondary = getSecondaryTrait(stats);
|
|
524
|
+
const { totalSessions, totalOut, topProj, projectCount, peakHour, nightPct,
|
|
525
|
+
longest, longestMins, topTools, topWord } = stats;
|
|
526
|
+
const topTool = topTools[0] ?? ['Bash', 0];
|
|
527
|
+
const toolP = stats.toolPct[topTool[0]] ?? 0;
|
|
528
|
+
const books = (totalOut / 5 / 90000).toFixed(1);
|
|
529
|
+
|
|
530
|
+
pr(); bar();
|
|
531
|
+
pr(` ${C.bold}${C.cyan} YOUR CLAUDE CODE WRAPPED${C.reset}`);
|
|
532
|
+
pr(` ${C.dim} last ${days} days${C.reset}`);
|
|
533
|
+
bar();
|
|
534
|
+
|
|
535
|
+
const sl = totalSessions === 1 ? 'session' : 'sessions';
|
|
536
|
+
slide('🔥','YOUR BIGGEST NUMBER', `${totalSessions.toLocaleString()} ${sl} with Claude in the last ${days} days.`, C.yellow);
|
|
537
|
+
sep();
|
|
538
|
+
slide('📍','WHERE YOU LIVED', `Your most active project was ${topProj[0]} — ${topProj[1]} sessions across ${projectCount} total projects.`, C.green);
|
|
539
|
+
sep();
|
|
540
|
+
slide('⚡','YOUR WEAPON OF CHOICE', `${topTool[0]} — you reached for it ${toolP}% of the time. ${topTool[1].toLocaleString()} calls total.`, C.yellow);
|
|
541
|
+
sep();
|
|
542
|
+
slide('🧠','TOKENS BURNED', `Claude generated ${fmtTok(totalOut)} output tokens for you.`, C.cyan);
|
|
543
|
+
if (+books > 0.5) pr(` ${C.dim}That's ${books}x the word count of Lord of the Rings.${C.reset}`);
|
|
544
|
+
sep();
|
|
545
|
+
if (nightPct > 20)
|
|
546
|
+
slide('🌙','YOU\'RE A NIGHT OWL', `${nightPct}% of your sessions started after 10pm. Your peak hour was ${fmtHour(peakHour)}.`, C.blue);
|
|
547
|
+
else
|
|
548
|
+
slide('⏰',`PEAK HOUR: ${fmtHour(peakHour).toUpperCase()}`, `That's when you hit your stride. Night sessions: ${nightPct}% of the total.`, C.blue);
|
|
549
|
+
if (longest) {
|
|
550
|
+
sep();
|
|
551
|
+
const dateStr = longest.timestamps.length ? ` on ${new Date(Math.min(...longest.timestamps)).toLocaleDateString('en-US',{month:'short',day:'numeric'})}` : '';
|
|
552
|
+
const projStr = longest.project ? ` in ${longest.project}` : '';
|
|
553
|
+
slide('⏱','LONGEST GRIND', `${fmtDur(longestMins)}${dateStr}${projStr}.`, C.magenta);
|
|
554
|
+
}
|
|
555
|
+
sep();
|
|
556
|
+
pr(); pr(` ${C.bold}${C[profile.color]}🧬 YOUR ARCHETYPE${C.reset}`);
|
|
557
|
+
pr(); pr(` ${' '.repeat(Math.floor((W - profile.name.length) / 2))}${C.bold}${C[profile.color]}${profile.name}${C.reset}`);
|
|
558
|
+
pr();
|
|
559
|
+
for (const l of wrap(profile.short, W)) pr(` ${l}`);
|
|
560
|
+
pr();
|
|
561
|
+
if (secondary) {
|
|
562
|
+
sep();
|
|
563
|
+
slide('✦', `SECONDARY TRAIT: ${secondary[0].toUpperCase()}`, secondary[1], C.dim);
|
|
564
|
+
}
|
|
565
|
+
if (topWord[0]) { sep(); slide('💬','YOUR MOST USED WORD', `You said "${topWord[0]}" ${topWord[1]} times in your prompts.`, C.dim); }
|
|
566
|
+
bar();
|
|
567
|
+
pr(); pr(` ${C.dim}Try it: github.com/turleydesigns/claude-wrapped${C.reset}`);
|
|
568
|
+
pr(` ${C.dim}What does yours say?${C.reset}`); pr();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function printArchetype(stats) {
|
|
572
|
+
const profile = getArchetypeProfile(stats);
|
|
573
|
+
const secondary = getSecondaryTrait(stats);
|
|
574
|
+
const col = C[profile.color];
|
|
575
|
+
|
|
576
|
+
pr(); pr(` ${C.bold}${col}${'▓'.repeat(W)}${C.reset}`);
|
|
577
|
+
pr(` ${C.bold}${col}▓${' '.repeat(W-2)}▓${C.reset}`);
|
|
578
|
+
const lbl = 'YOUR CLAUDE CODE ARCHETYPE';
|
|
579
|
+
const pad = Math.floor((W-2-lbl.length)/2);
|
|
580
|
+
pr(` ${C.bold}${col}▓${' '.repeat(pad)}${lbl}${' '.repeat(W-2-pad-lbl.length)}▓${C.reset}`);
|
|
581
|
+
pr(` ${C.bold}${col}▓${' '.repeat(W-2)}▓${C.reset}`);
|
|
582
|
+
pr(` ${C.bold}${col}${'▓'.repeat(W)}${C.reset}`);
|
|
583
|
+
pr();
|
|
584
|
+
const np = Math.floor((W - profile.name.length) / 2);
|
|
585
|
+
pr(` ${' '.repeat(np)}${C.bold}${col}${profile.name}${C.reset}`);
|
|
586
|
+
pr();
|
|
587
|
+
for (const l of wrap(profile.full, W)) pr(` ${l}`);
|
|
588
|
+
pr(); sep(); pr();
|
|
589
|
+
for (const s of profile.supporting) pr(` ${C.bold}·${C.reset} ${s}`);
|
|
590
|
+
pr(); sep(); pr();
|
|
591
|
+
pr(` ${C.bold}${col}Famous for:${C.reset}`);
|
|
592
|
+
for (const l of wrap(profile.famousFor, W-4)) pr(` ${l}`);
|
|
593
|
+
pr(); pr(` ${C.bold}${C.green}Strength:${C.reset}`);
|
|
594
|
+
for (const l of wrap(profile.strength, W-4)) pr(` ${l}`);
|
|
595
|
+
pr(); pr(` ${C.bold}${C.yellow}Watch out:${C.reset}`);
|
|
596
|
+
for (const l of wrap(profile.watchOut, W-4)) pr(` ${l}`);
|
|
597
|
+
pr();
|
|
598
|
+
if (secondary) {
|
|
599
|
+
sep(); pr();
|
|
600
|
+
pr(` ${C.bold}✦ Secondary: ${secondary[0]}${C.reset}`);
|
|
601
|
+
for (const l of wrap(secondary[1], W-2)) pr(` ${l}`);
|
|
602
|
+
pr();
|
|
603
|
+
}
|
|
604
|
+
sep(); pr();
|
|
605
|
+
pr(` ${C.dim}Try it: github.com/turleydesigns/claude-wrapped${C.reset}`);
|
|
606
|
+
pr(` ${C.dim}npx claude-wrapped --archetype${C.reset}`);
|
|
607
|
+
pr(` ${C.dim}What's yours?${C.reset}`); pr();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function printRoast(stats, days) {
|
|
611
|
+
const { name: aname, color: acol } = getArchetypeProfile(stats);
|
|
612
|
+
const lines = buildRoast(stats, days);
|
|
613
|
+
pr(); pr(` ${C.bold}${C.yellow}${'━'.repeat(W)}${C.reset}`);
|
|
614
|
+
pr(` ${C.bold}${C.yellow} THE ROAST${C.reset}`);
|
|
615
|
+
pr(` ${C.dim} ${days}-day Claude Code session review${C.reset}`);
|
|
616
|
+
pr(` ${C.bold}${C.yellow}${'━'.repeat(W)}${C.reset}`);
|
|
617
|
+
pr(); pr(` ${C.dim}Archetype: ${C[acol]}${C.bold}${aname}${C.reset}`); pr();
|
|
618
|
+
for (const line of lines) {
|
|
619
|
+
for (const l of wrap(line, W)) pr(` ${l}`);
|
|
620
|
+
pr();
|
|
621
|
+
}
|
|
622
|
+
pr(` ${C.bold}${C.yellow}${'━'.repeat(W)}${C.reset}`);
|
|
623
|
+
pr(); pr(` ${C.dim}Try it: github.com/turleydesigns/claude-wrapped${C.reset}`);
|
|
624
|
+
pr(` ${C.dim}npx claude-wrapped --roast${C.reset}`);
|
|
625
|
+
pr(` ${C.dim}What does yours say?${C.reset}`); pr();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function printDna(stats) {
|
|
629
|
+
const dna = getPromptDna(stats);
|
|
630
|
+
const { name: aname, color: acol } = getArchetypeProfile(stats);
|
|
631
|
+
pr(); pr(` ${C.bold}${C.magenta}${'━'.repeat(W)}${C.reset}`);
|
|
632
|
+
pr(` ${C.bold}${C.magenta} YOUR PROMPT STYLE DNA${C.reset}`);
|
|
633
|
+
pr(` ${C.dim} how you actually talk to Claude${C.reset}`);
|
|
634
|
+
pr(` ${C.bold}${C.magenta}${'━'.repeat(W)}${C.reset}`); pr();
|
|
635
|
+
for (const [label, [val, desc], col] of [
|
|
636
|
+
['Formality', dna.formality, C.cyan], ['Verbosity', dna.verbosity, C.green],
|
|
637
|
+
['Style', dna.style, C.yellow], ['Timing', dna.timing, C.blue],
|
|
638
|
+
]) {
|
|
639
|
+
pr(` ${C.bold}${col}${label}: ${val.toUpperCase()}${C.reset}`);
|
|
640
|
+
for (const l of wrap(desc, W-2)) pr(` ${C.dim}${l}${C.reset}`);
|
|
641
|
+
pr();
|
|
642
|
+
}
|
|
643
|
+
sep(); pr();
|
|
644
|
+
pr(` ${C.bold}The one-liner:${C.reset}`);
|
|
645
|
+
for (const l of wrap(dna.summary, W)) pr(` ${l}`);
|
|
646
|
+
pr(); pr(` ${C.dim}Archetype: ${C[acol]}${C.bold}${aname}${C.reset}`); pr();
|
|
647
|
+
pr(` ${C.bold}${C.magenta}${'━'.repeat(W)}${C.reset}`);
|
|
648
|
+
pr(); pr(` ${C.dim}Try it: github.com/turleydesigns/claude-wrapped${C.reset}`);
|
|
649
|
+
pr(` ${C.dim}npx claude-wrapped --dna${C.reset}`);
|
|
650
|
+
pr(` ${C.dim}What's your prompt style?${C.reset}`); pr();
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function printSpirit(stats) {
|
|
654
|
+
const m = getSpiritModel(stats);
|
|
655
|
+
const col = C[m.color];
|
|
656
|
+
pr(); pr(` ${C.bold}${'━'.repeat(W)}${C.reset}`);
|
|
657
|
+
pr(` ${C.bold} YOUR SPIRIT MODEL${C.reset}`);
|
|
658
|
+
pr(` ${C.bold}${'━'.repeat(W)}${C.reset}`); pr();
|
|
659
|
+
const np = Math.floor((W - m.name.length) / 2);
|
|
660
|
+
pr(` ${' '.repeat(np)}${C.bold}${col}${m.name}${C.reset}`); pr();
|
|
661
|
+
pr(` ${m.tagline}`);
|
|
662
|
+
pr(` ${C.dim}${m.quip}${C.reset}`); pr();
|
|
663
|
+
for (const s of m.stats) pr(` ${C.bold}·${C.reset} ${s}`);
|
|
664
|
+
pr(); pr(` ${C.bold}${'━'.repeat(W)}${C.reset}`);
|
|
665
|
+
pr(); pr(` ${C.dim}npx claude-wrapped --spirit${C.reset}`);
|
|
666
|
+
pr(` ${C.dim}What's your spirit model?${C.reset}`); pr();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function printTuneUp(stats, days, doWrite) {
|
|
670
|
+
const rules = generateTuneUpRules(stats);
|
|
671
|
+
pr(); pr(` ${C.bold}${C.green}${'━'.repeat(W)}${C.reset}`);
|
|
672
|
+
pr(` ${C.bold}${C.green} CLAUDE.md TUNE-UP${C.reset}`);
|
|
673
|
+
pr(` ${C.dim} based on ${days} days of your actual sessions${C.reset}`);
|
|
674
|
+
pr(` ${C.bold}${C.green}${'━'.repeat(W)}${C.reset}`); pr();
|
|
675
|
+
|
|
676
|
+
const cwdFile = path.join(process.cwd(), 'CLAUDE.md');
|
|
677
|
+
const homeFile = path.join(os.homedir(), 'CLAUDE.md');
|
|
678
|
+
const foundPath = fs.existsSync(cwdFile) ? cwdFile : fs.existsSync(homeFile) ? homeFile : null;
|
|
679
|
+
pr(` ${C.dim}${foundPath ? `Found: ${foundPath}` : 'No CLAUDE.md found in current directory or home.'}${C.reset}`); pr();
|
|
680
|
+
|
|
681
|
+
if (!rules.length) { pr(` ${C.dim}No strong patterns detected yet. Try running after 7+ active days.${C.reset}`); pr(); return; }
|
|
682
|
+
|
|
683
|
+
pr(` ${C.bold}${rules.length} suggestions based on your patterns:${C.reset}`); pr();
|
|
684
|
+
const blockLines = ['## How I work (from session patterns)\n', ...rules.map(([rule]) => `- ${rule}`)];
|
|
685
|
+
for (let i = 0; i < rules.length; i++) {
|
|
686
|
+
const [rule, reason] = rules[i];
|
|
687
|
+
pr(` ${C.bold}${C.green}${i+1}.${C.reset} ${C.dim}${reason}${C.reset}`);
|
|
688
|
+
for (const l of wrap(rule, W-4)) pr(` ${l}`);
|
|
689
|
+
pr();
|
|
690
|
+
}
|
|
691
|
+
sep(); pr(); pr(` ${C.bold}Proposed CLAUDE.md block:${C.reset}`); pr();
|
|
692
|
+
pr(` ${C.dim}┌${'─'.repeat(W-2)}┐${C.reset}`);
|
|
693
|
+
for (const line of blockLines) {
|
|
694
|
+
for (const wl of (line.trim() ? wrap(line, W-6) : [''])) {
|
|
695
|
+
pr(` ${C.dim}│${C.reset} ${wl}${' '.repeat(Math.max(0, W-4-wl.length))}${C.dim}│${C.reset}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
pr(` ${C.dim}└${'─'.repeat(W-2)}┘${C.reset}`); pr();
|
|
699
|
+
|
|
700
|
+
if (doWrite) {
|
|
701
|
+
const dest = foundPath ?? cwdFile;
|
|
702
|
+
const existing = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : '';
|
|
703
|
+
if (existing.includes('## How I work (from session patterns)')) {
|
|
704
|
+
pr(` ${C.yellow}CLAUDE.md already has a session-patterns block. Skipping.${C.reset}`);
|
|
705
|
+
} else {
|
|
706
|
+
fs.appendFileSync(dest, '\n' + blockLines.join('\n') + '\n');
|
|
707
|
+
pr(` ${C.bold}${C.green}Written to ${dest}${C.reset}`);
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
pr(` ${C.dim}To write this to CLAUDE.md:${C.reset}`);
|
|
711
|
+
pr(` ${C.dim} npx claude-wrapped --tune-up --write${C.reset}`);
|
|
712
|
+
}
|
|
713
|
+
pr(); pr(` ${C.bold}${C.green}${'━'.repeat(W)}${C.reset}`);
|
|
714
|
+
pr(); pr(` ${C.dim}npx claude-wrapped --tune-up${C.reset}`); pr();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ── MAIN ──────────────────────────────────────────────────────────────────────
|
|
718
|
+
const argv = process.argv.slice(2);
|
|
719
|
+
const hasFlag = f => argv.includes(f);
|
|
720
|
+
const getArg = (f, def) => { const i = argv.indexOf(f); return i >= 0 && argv[i+1] ? +argv[i+1] : def; };
|
|
721
|
+
|
|
722
|
+
if (hasFlag('--no-color') || hasFlag('--no-colour')) noColor();
|
|
723
|
+
|
|
724
|
+
const days = getArg('--days', 30);
|
|
725
|
+
const cutoffMs = Date.now() - days * 86400000;
|
|
726
|
+
|
|
727
|
+
try { fs.statSync(CLAUDE_DIR); } catch (_) {
|
|
728
|
+
pr(`No Claude projects directory found.`);
|
|
729
|
+
pr(`Expected: ${CLAUDE_DIR}`);
|
|
730
|
+
process.exit(0);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const files = findJsonl(CLAUDE_DIR, cutoffMs);
|
|
734
|
+
const sessions = files.map(f => parseSession(f, cutoffMs)).filter(Boolean);
|
|
735
|
+
|
|
736
|
+
if (!sessions.length) {
|
|
737
|
+
pr(`No Claude Code sessions found in the last ${days} days.`);
|
|
738
|
+
pr(`Looking in: ${CLAUDE_DIR}`);
|
|
739
|
+
process.exit(0);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const stats = aggregate(sessions);
|
|
743
|
+
|
|
744
|
+
if (hasFlag('--archetype')) printArchetype(stats);
|
|
745
|
+
else if (hasFlag('--roast')) printRoast(stats, days);
|
|
746
|
+
else if (hasFlag('--dna')) printDna(stats);
|
|
747
|
+
else if (hasFlag('--spirit')) printSpirit(stats);
|
|
748
|
+
else if (hasFlag('--tune-up')) printTuneUp(stats, days, hasFlag('--write'));
|
|
749
|
+
else printWrapped(stats, days);
|