@pkprosol/coach 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 ADDED
@@ -0,0 +1,79 @@
1
+ # Coach — Daily AI Work Coach
2
+
3
+ Analyzes your **Claude Code** and **Claude App** sessions to deliver one lesson + one tip daily. Built around the [Hooked model](https://www.nirandfar.com/hooked/) to make self-improvement habitual.
4
+
5
+ ```
6
+ ┌──────────────────────────────────────────────────┐
7
+ │ COACH Day 12 🔥 │
8
+ ├──────────────────────────────────────────────────┤
9
+ │ │
10
+ │ TODAY'S LENS: Prompting Craft │
11
+ │ │
12
+ │ 📖 LESSON │
13
+ │ Your prompts today started broad and required │
14
+ │ 3-4 clarification rounds... │
15
+ │ │
16
+ │ 💡 TIP │
17
+ │ Try the 'Context-Task-Format' frame... │
18
+ │ │
19
+ │ 🌱 Your afternoon sessions showed much tighter │
20
+ │ prompting — you're already improving. │
21
+ │ │
22
+ ├──────────────────────────────────────────────────┤
23
+ │ Was this helpful? [y] Yes [n] No │
24
+ └──────────────────────────────────────────────────┘
25
+ ```
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install -g @pkprosol/coach
31
+ ```
32
+
33
+ This installs the `coach` CLI and auto-registers `/coach` as a slash command in Claude Code.
34
+
35
+ ## Setup
36
+
37
+ ```bash
38
+ coach setup # paste your Anthropic API key
39
+ ```
40
+
41
+ Or set `ANTHROPIC_API_KEY` in your environment.
42
+
43
+ ## Usage
44
+
45
+ ### Terminal
46
+ ```bash
47
+ coach # today's lesson + tip
48
+ coach streak # current streak + stats
49
+ coach history # browse past insights
50
+ ```
51
+
52
+ ### Claude Code
53
+ ```
54
+ /coach # same thing, right inside Claude
55
+ /coach streak
56
+ /coach history
57
+ ```
58
+
59
+ ## How it works
60
+
61
+ 1. **Collects** today's session data from `~/.claude/` (Claude Code) and `~/Library/Application Support/Claude/` (Claude App)
62
+ 2. **Analyzes** your prompts, workflow patterns, and tool usage through a rotating lens (8 dimensions: prompting craft, workflow efficiency, architecture thinking, etc.)
63
+ 3. **Delivers** one specific lesson + one actionable tip, with examples from your actual sessions
64
+ 4. **Learns** from your ratings to improve future insights
65
+
66
+ ## Data sources
67
+
68
+ | Source | Location | What's collected |
69
+ |--------|----------|-----------------|
70
+ | Claude Code | `~/.claude/` | Prompts, session transcripts, tool usage, tokens |
71
+ | Claude App | `~/Library/Application Support/Claude/` | Cowork/agent mode audit logs |
72
+
73
+ All data stays local. Only a summary is sent to the Claude API for analysis.
74
+
75
+ ## Requirements
76
+
77
+ - Node.js 18+
78
+ - An Anthropic API key
79
+ - Claude Code and/or Claude Desktop App
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from "node:readline";
3
+ import ora from "ora";
4
+ import chalk from "chalk";
5
+ import { collectToday } from "../src/collector.js";
6
+ import { analyze } from "../src/analyzer.js";
7
+ import { renderInsight, renderStreak, renderHistory, renderSetupSuccess, renderNoData, renderError, } from "../src/display.js";
8
+ import { loadState, saveState, getApiKey, setApiKey, loadInsights, appendInsight, updateLastInsightRating, updateStreak, recordDimension, } from "../src/storage.js";
9
+ function askQuestion(prompt) {
10
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
11
+ return new Promise((resolve) => {
12
+ rl.question(prompt, (answer) => {
13
+ rl.close();
14
+ resolve(answer.trim().toLowerCase());
15
+ });
16
+ });
17
+ }
18
+ async function handleSetup() {
19
+ const existing = getApiKey();
20
+ if (existing) {
21
+ console.log(chalk.dim("API key already configured."));
22
+ const answer = await askQuestion("Replace it? [y/N] ");
23
+ if (answer !== "y")
24
+ return;
25
+ }
26
+ const key = await askQuestion("Enter your Anthropic API key: ");
27
+ if (!key) {
28
+ console.log(renderError("No key provided."));
29
+ return;
30
+ }
31
+ setApiKey(key);
32
+ console.log(renderSetupSuccess());
33
+ }
34
+ async function handleDefault() {
35
+ const apiKey = getApiKey();
36
+ if (!apiKey) {
37
+ console.log(renderError("No API key found. Run `coach setup` first, or set ANTHROPIC_API_KEY."));
38
+ return;
39
+ }
40
+ const spinner = ora({ text: "Collecting today's sessions...", color: "cyan" }).start();
41
+ // Collect data
42
+ const data = collectToday();
43
+ if (data.prompts.length === 0) {
44
+ spinner.stop();
45
+ console.log(renderNoData());
46
+ return;
47
+ }
48
+ spinner.text = `Found ${data.prompts.length} prompts across ${data.sessions.length} sessions. Analyzing...`;
49
+ // Load state and past insights
50
+ let state = loadState();
51
+ const pastInsights = loadInsights();
52
+ // Analyze
53
+ let insight;
54
+ try {
55
+ insight = await analyze(data, state.recentDimensions, pastInsights, apiKey);
56
+ }
57
+ catch (err) {
58
+ spinner.stop();
59
+ if (err.status === 401) {
60
+ console.log(renderError("Invalid API key. Run `coach setup` to update it."));
61
+ }
62
+ else {
63
+ console.log(renderError(err.message ?? "Analysis failed."));
64
+ }
65
+ return;
66
+ }
67
+ spinner.stop();
68
+ // Update state
69
+ const now = new Date();
70
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
71
+ state = updateStreak(state, today);
72
+ state = recordDimension(state, insight.dimension);
73
+ // Display
74
+ console.log("");
75
+ console.log(renderInsight(insight, state));
76
+ console.log("");
77
+ // Save insight (without rating yet)
78
+ const storedInsight = {
79
+ ...insight,
80
+ date: today,
81
+ rating: null,
82
+ };
83
+ appendInsight(storedInsight);
84
+ // Ask for rating (only in interactive terminal)
85
+ if (process.stdin.isTTY) {
86
+ const answer = await askQuestion(" ");
87
+ if (answer === "y" || answer === "yes") {
88
+ state.helpfulCount++;
89
+ updateLastInsightRating("helpful");
90
+ console.log(chalk.green("\n Thanks! Noted for tomorrow. 🙌\n"));
91
+ }
92
+ else if (answer === "n" || answer === "no") {
93
+ state.notHelpfulCount++;
94
+ updateLastInsightRating("not_helpful");
95
+ console.log(chalk.dim("\n Got it — will adjust. Thanks for the feedback.\n"));
96
+ }
97
+ else {
98
+ console.log(chalk.dim("\n Skipped. See you tomorrow!\n"));
99
+ }
100
+ }
101
+ saveState(state);
102
+ }
103
+ function handleStreakCmd() {
104
+ const state = loadState();
105
+ console.log("");
106
+ console.log(renderStreak(state));
107
+ console.log("");
108
+ }
109
+ function handleHistoryCmd() {
110
+ const insights = loadInsights();
111
+ console.log("");
112
+ console.log(renderHistory(insights));
113
+ console.log("");
114
+ }
115
+ function handleRate(value) {
116
+ const state = loadState();
117
+ if (value === "y" || value === "yes") {
118
+ state.helpfulCount++;
119
+ updateLastInsightRating("helpful");
120
+ saveState(state);
121
+ console.log(chalk.green(" Thanks! Noted for tomorrow. 🙌"));
122
+ }
123
+ else if (value === "n" || value === "no") {
124
+ state.notHelpfulCount++;
125
+ updateLastInsightRating("not_helpful");
126
+ saveState(state);
127
+ console.log(chalk.dim(" Got it — will adjust. Thanks for the feedback."));
128
+ }
129
+ else {
130
+ console.log(renderError("Usage: coach rate y|n"));
131
+ }
132
+ }
133
+ function handleHelp() {
134
+ console.log(`
135
+ ${chalk.bold("coach")} — Daily AI Work Coach
136
+
137
+ ${chalk.bold("Usage:")}
138
+ coach Today's lesson + tip (default)
139
+ coach setup Set your Anthropic API key
140
+ coach history Browse past insights
141
+ coach streak Show current streak + stats
142
+ coach help Show this help message
143
+ `);
144
+ }
145
+ // --- Main ---
146
+ const command = process.argv[2];
147
+ switch (command) {
148
+ case "setup":
149
+ handleSetup();
150
+ break;
151
+ case "history":
152
+ handleHistoryCmd();
153
+ break;
154
+ case "streak":
155
+ handleStreakCmd();
156
+ break;
157
+ case "rate":
158
+ handleRate(process.argv[3] ?? "");
159
+ break;
160
+ case "help":
161
+ case "--help":
162
+ case "-h":
163
+ handleHelp();
164
+ break;
165
+ default:
166
+ handleDefault();
167
+ break;
168
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { mkdirSync, writeFileSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const skillDir = join(homedir(), ".claude", "skills", "coach");
5
+ const skillFile = join(skillDir, "SKILL.md");
6
+ const SKILL_CONTENT = `---
7
+ name: coach
8
+ description: Daily AI work coach - analyzes your Claude Code and Claude App sessions to deliver one lesson and one tip
9
+ disable-model-invocation: true
10
+ user-invocable: true
11
+ ---
12
+
13
+ Run the Coach CLI tool to analyze today's Claude usage and deliver a personalized insight.
14
+
15
+ Execute this command and display the output exactly as-is to the user (it contains formatted terminal UI):
16
+
17
+ \`\`\`bash
18
+ coach
19
+ \`\`\`
20
+
21
+ After showing the output, ask the user: "Was this helpful? (y/n)" and based on their answer, run:
22
+
23
+ - If yes: \`coach rate y\`
24
+ - If no: \`coach rate n\`
25
+
26
+ If the user passes an argument, route it as a subcommand:
27
+ - \`/coach streak\` → \`coach streak\`
28
+ - \`/coach history\` → \`coach history\`
29
+ - \`/coach setup\` → \`coach setup\`
30
+ `;
31
+ try {
32
+ if (!existsSync(join(homedir(), ".claude"))) {
33
+ // Claude Code not installed — skip skill setup silently
34
+ process.exit(0);
35
+ }
36
+ mkdirSync(skillDir, { recursive: true });
37
+ writeFileSync(skillFile, SKILL_CONTENT);
38
+ console.log("✓ Installed /coach command for Claude Code");
39
+ }
40
+ catch {
41
+ // Non-fatal — coach CLI still works without the skill
42
+ console.log("Note: Could not install /coach skill (coach CLI still works via `coach` command)");
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectedData, Dimension, Insight, StoredInsight } from "./types.js";
2
+ export declare function analyze(data: CollectedData, recentDimensions: Dimension[], pastInsights: StoredInsight[], apiKey: string): Promise<Insight>;
@@ -0,0 +1,137 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { DIMENSIONS } from "./types.js";
3
+ function pickDimension(recentDimensions, data) {
4
+ // Filter out recently used dimensions
5
+ const available = DIMENSIONS.filter((d) => !recentDimensions.includes(d));
6
+ const pool = available.length > 0 ? available : [...DIMENSIONS];
7
+ // Heuristic: pick the most relevant dimension for today's data
8
+ // Weight based on data signals
9
+ const scores = new Map();
10
+ for (const d of pool) {
11
+ scores.set(d, Math.random()); // Base randomness
12
+ }
13
+ const avgPromptsPerSession = data.sessions.length > 0
14
+ ? data.prompts.length / data.sessions.length
15
+ : data.prompts.length;
16
+ // Boost relevant dimensions based on data patterns
17
+ if (avgPromptsPerSession > 8) {
18
+ scores.set("Prompting Craft", (scores.get("Prompting Craft") ?? 0) + 2);
19
+ scores.set("Workflow Efficiency", (scores.get("Workflow Efficiency") ?? 0) + 1);
20
+ }
21
+ if (data.projectsWorkedOn.length > 2) {
22
+ scores.set("Focus & Deep Work", (scores.get("Focus & Deep Work") ?? 0) + 2);
23
+ }
24
+ if (data.totalToolCalls > 20) {
25
+ scores.set("Tool Leverage", (scores.get("Tool Leverage") ?? 0) + 1.5);
26
+ }
27
+ if (data.sessions.length === 1 && data.prompts.length > 5) {
28
+ scores.set("Problem Decomposition", (scores.get("Problem Decomposition") ?? 0) + 1.5);
29
+ }
30
+ // Pick highest scoring available dimension
31
+ let best = pool[0];
32
+ let bestScore = -1;
33
+ for (const [dim, score] of scores) {
34
+ if (pool.includes(dim) && score > bestScore) {
35
+ best = dim;
36
+ bestScore = score;
37
+ }
38
+ }
39
+ return best;
40
+ }
41
+ function buildPrompt(data, dimension, pastInsights) {
42
+ // Truncate prompts to avoid overwhelming context
43
+ const samplePrompts = data.prompts.slice(0, 40).map((p) => ({
44
+ text: p.text.slice(0, 500),
45
+ project: p.project,
46
+ sessionId: p.sessionId.slice(0, 8),
47
+ }));
48
+ const sessionSummaries = data.sessions.map((s) => ({
49
+ project: s.project,
50
+ messages: s.messageCount,
51
+ userMessages: s.userMessageCount,
52
+ toolCalls: s.toolCallCount,
53
+ tools: s.toolNames,
54
+ tokens: s.inputTokens + s.outputTokens,
55
+ duration: s.startTime && s.endTime
56
+ ? `${Math.round((new Date(s.endTime).getTime() - new Date(s.startTime).getTime()) / 60000)}min`
57
+ : "unknown",
58
+ branch: s.gitBranch,
59
+ }));
60
+ // Include recent past insights for context
61
+ const recentRated = pastInsights
62
+ .filter((i) => i.rating !== null)
63
+ .slice(-5)
64
+ .map((i) => ({
65
+ dimension: i.dimension,
66
+ rating: i.rating,
67
+ date: i.date,
68
+ }));
69
+ return `You are Coach, a personal AI work coach that analyzes a developer's Claude Code usage patterns to deliver one actionable lesson and one practical tip.
70
+
71
+ ## Today's Analysis Dimension: ${dimension}
72
+
73
+ Dimension descriptions:
74
+ - Prompting Craft: clarity, specificity, effectiveness of the user's prompts
75
+ - Workflow Efficiency: circular patterns, redundant requests, wasted effort
76
+ - Architecture Thinking: over/under-engineering signals in what the user asks for
77
+ - Learning Patterns: building knowledge vs re-learning the same things
78
+ - Focus & Deep Work: context switching vs sustained depth
79
+ - Communication Style: how problems are described to Claude
80
+ - Tool Leverage: effective use of Claude's capabilities (tools, features)
81
+ - Problem Decomposition: breaking down vs monolithic asks
82
+
83
+ ## Today's Session Data
84
+
85
+ Date: ${data.date}
86
+ Projects worked on: ${data.projectsWorkedOn.join(", ")}
87
+ Total sessions: ${data.sessions.length}
88
+ Total messages: ${data.totalMessages}
89
+ Total tool calls: ${data.totalToolCalls}
90
+ Total tokens: ${data.totalTokens.toLocaleString()}
91
+
92
+ ### User Prompts (chronological)
93
+ ${JSON.stringify(samplePrompts, null, 2)}
94
+
95
+ ### Session Summaries
96
+ ${JSON.stringify(sessionSummaries, null, 2)}
97
+
98
+ ${recentRated.length > 0 ? `### Past Insight Ratings (for context on what the user finds helpful)
99
+ ${JSON.stringify(recentRated, null, 2)}` : ""}
100
+
101
+ ## Your Task
102
+
103
+ Analyze the data through the lens of "${dimension}" and return a JSON object with exactly these fields:
104
+
105
+ {
106
+ "dimension": "${dimension}",
107
+ "lesson": "A specific, data-backed observation about today's work (2-3 sentences). Reference actual prompts or patterns you see.",
108
+ "tip": "One concrete, actionable technique they can try tomorrow (2-3 sentences). Be specific with a method or framework.",
109
+ "specificExample": { "before": "An actual prompt from today (or close paraphrase)", "after": "A rewritten version applying your tip" } or null if not applicable,
110
+ "encouragement": "One sentence noting something they did well today. Be genuine — find something real."
111
+ }
112
+
113
+ Guidelines:
114
+ - Be specific. Reference actual data from the session — prompt text, project names, patterns.
115
+ - The lesson should feel like a personal discovery, not generic advice.
116
+ - The tip should be immediately actionable tomorrow.
117
+ - If the specificExample doesn't make sense for this dimension, set it to null.
118
+ - Keep the encouragement genuine and grounded in their actual work.
119
+ - Total response should feel insightful but concise.
120
+
121
+ Respond with ONLY the JSON object, no markdown fences or other text.`;
122
+ }
123
+ export async function analyze(data, recentDimensions, pastInsights, apiKey) {
124
+ const dimension = pickDimension(recentDimensions, data);
125
+ const prompt = buildPrompt(data, dimension, pastInsights);
126
+ const client = new Anthropic({ apiKey });
127
+ const response = await client.messages.create({
128
+ model: "claude-sonnet-4-5-20250929",
129
+ max_tokens: 1024,
130
+ messages: [{ role: "user", content: prompt }],
131
+ });
132
+ const text = response.content[0].type === "text" ? response.content[0].text : "";
133
+ // Parse JSON from response — handle possible markdown fences
134
+ const cleaned = text.replace(/^```json?\s*/, "").replace(/\s*```$/, "").trim();
135
+ const result = JSON.parse(cleaned);
136
+ return result;
137
+ }
@@ -0,0 +1,2 @@
1
+ import type { CollectedData } from "./types.js";
2
+ export declare function collectToday(dateOverride?: string): CollectedData;
@@ -0,0 +1,315 @@
1
+ import { readFileSync, readdirSync, existsSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join, basename } from "node:path";
4
+ const CLAUDE_DIR = join(homedir(), ".claude");
5
+ const HISTORY_FILE = join(CLAUDE_DIR, "history.jsonl");
6
+ const PROJECTS_DIR = join(CLAUDE_DIR, "projects");
7
+ const DESKTOP_SESSIONS_DIR = join(homedir(), "Library", "Application Support", "Claude", "local-agent-mode-sessions");
8
+ function readJsonl(path) {
9
+ if (!existsSync(path))
10
+ return [];
11
+ const raw = readFileSync(path, "utf-8").trim();
12
+ if (!raw)
13
+ return [];
14
+ const items = [];
15
+ for (const line of raw.split("\n")) {
16
+ try {
17
+ items.push(JSON.parse(line));
18
+ }
19
+ catch {
20
+ // Skip malformed lines
21
+ }
22
+ }
23
+ return items;
24
+ }
25
+ function toLocalDateStr(date) {
26
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
27
+ }
28
+ function localToday() {
29
+ return toLocalDateStr(new Date());
30
+ }
31
+ function isSameDay(timestamp, targetDate) {
32
+ const date = new Date(timestamp);
33
+ return toLocalDateStr(date) === targetDate;
34
+ }
35
+ function extractProjectName(projectPath) {
36
+ return basename(projectPath);
37
+ }
38
+ // =============================================
39
+ // Claude Code collection
40
+ // =============================================
41
+ function collectCodePrompts(today) {
42
+ const entries = readJsonl(HISTORY_FILE);
43
+ const prompts = [];
44
+ for (const entry of entries) {
45
+ if (isSameDay(entry.timestamp, today)) {
46
+ prompts.push({
47
+ text: entry.display,
48
+ timestamp: new Date(entry.timestamp).toISOString(),
49
+ sessionId: entry.sessionId,
50
+ project: extractProjectName(entry.project),
51
+ });
52
+ }
53
+ }
54
+ return prompts;
55
+ }
56
+ function findCodeSessionFiles(_today, sessionIds) {
57
+ const files = [];
58
+ if (!existsSync(PROJECTS_DIR))
59
+ return files;
60
+ for (const projectDir of readdirSync(PROJECTS_DIR)) {
61
+ const projectPath = join(PROJECTS_DIR, projectDir);
62
+ let entries;
63
+ try {
64
+ entries = readdirSync(projectPath);
65
+ }
66
+ catch {
67
+ continue;
68
+ }
69
+ for (const entry of entries) {
70
+ if (!entry.endsWith(".jsonl"))
71
+ continue;
72
+ const sessionId = entry.replace(".jsonl", "");
73
+ if (sessionIds.has(sessionId)) {
74
+ files.push(join(projectPath, entry));
75
+ }
76
+ }
77
+ }
78
+ return files;
79
+ }
80
+ function parseCodeSessionFile(filePath, today) {
81
+ const messages = readJsonl(filePath);
82
+ if (messages.length === 0)
83
+ return null;
84
+ let userCount = 0;
85
+ let assistantCount = 0;
86
+ let toolCallCount = 0;
87
+ const toolNames = new Set();
88
+ let inputTokens = 0;
89
+ let outputTokens = 0;
90
+ let startTime = "";
91
+ let endTime = "";
92
+ let sessionId = "";
93
+ let project = "";
94
+ let gitBranch;
95
+ for (const msg of messages) {
96
+ if (msg.type === "file-history-snapshot")
97
+ continue;
98
+ if (!msg.timestamp || !isSameDay(msg.timestamp, today))
99
+ continue;
100
+ if (!startTime || msg.timestamp < startTime)
101
+ startTime = msg.timestamp;
102
+ if (!endTime || msg.timestamp > endTime)
103
+ endTime = msg.timestamp;
104
+ if (msg.sessionId)
105
+ sessionId = msg.sessionId;
106
+ if (msg.gitBranch)
107
+ gitBranch = msg.gitBranch;
108
+ if (msg.cwd)
109
+ project = extractProjectName(msg.cwd);
110
+ if (msg.type === "user") {
111
+ userCount++;
112
+ }
113
+ else if (msg.type === "assistant") {
114
+ assistantCount++;
115
+ if (msg.message?.usage) {
116
+ inputTokens += msg.message.usage.input_tokens || 0;
117
+ outputTokens += msg.message.usage.output_tokens || 0;
118
+ }
119
+ if (Array.isArray(msg.message?.content)) {
120
+ for (const block of msg.message.content) {
121
+ if (block.type === "tool_use" && block.name) {
122
+ toolCallCount++;
123
+ toolNames.add(block.name);
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ if (userCount === 0 && assistantCount === 0)
130
+ return null;
131
+ return {
132
+ sessionId,
133
+ project,
134
+ source: "claude-code",
135
+ messageCount: userCount + assistantCount,
136
+ userMessageCount: userCount,
137
+ assistantMessageCount: assistantCount,
138
+ toolCallCount,
139
+ toolNames: [...toolNames],
140
+ inputTokens,
141
+ outputTokens,
142
+ startTime,
143
+ endTime,
144
+ gitBranch,
145
+ };
146
+ }
147
+ function collectCodeSessions(today) {
148
+ const prompts = collectCodePrompts(today);
149
+ const sessionIds = new Set(prompts.map((p) => p.sessionId));
150
+ const sessionFiles = findCodeSessionFiles(today, sessionIds);
151
+ const sessions = [];
152
+ for (const file of sessionFiles) {
153
+ const summary = parseCodeSessionFile(file, today);
154
+ if (summary)
155
+ sessions.push(summary);
156
+ }
157
+ return { prompts, sessions };
158
+ }
159
+ // =============================================
160
+ // Claude Desktop App collection
161
+ // =============================================
162
+ function findDesktopAuditFiles() {
163
+ const results = [];
164
+ if (!existsSync(DESKTOP_SESSIONS_DIR))
165
+ return results;
166
+ try {
167
+ // Traverse: workspace-id / user-id / local_session-id/audit.jsonl
168
+ for (const workspace of readdirSync(DESKTOP_SESSIONS_DIR)) {
169
+ const wsPath = join(DESKTOP_SESSIONS_DIR, workspace);
170
+ if (!statSync(wsPath).isDirectory())
171
+ continue;
172
+ for (const userId of readdirSync(wsPath)) {
173
+ const userPath = join(wsPath, userId);
174
+ if (!statSync(userPath).isDirectory())
175
+ continue;
176
+ for (const entry of readdirSync(userPath)) {
177
+ // Session directories are named like local_<uuid>
178
+ const sessionDir = join(userPath, entry);
179
+ if (!entry.startsWith("local_") || !statSync(sessionDir).isDirectory())
180
+ continue;
181
+ const auditPath = join(sessionDir, "audit.jsonl");
182
+ const metaPath = join(userPath, entry + ".json");
183
+ if (existsSync(auditPath)) {
184
+ results.push({ auditPath, metaPath });
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ catch {
191
+ // Gracefully handle permission errors etc.
192
+ }
193
+ return results;
194
+ }
195
+ function parseDesktopSession(auditPath, metaPath, today) {
196
+ const entries = readJsonl(auditPath);
197
+ const prompts = [];
198
+ // Read session metadata for title
199
+ let title = "Claude App";
200
+ let sessionId = "";
201
+ if (existsSync(metaPath)) {
202
+ try {
203
+ const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
204
+ title = meta.title || "Claude App";
205
+ sessionId = meta.sessionId || "";
206
+ }
207
+ catch {
208
+ // Ignore parse errors
209
+ }
210
+ }
211
+ let userCount = 0;
212
+ let assistantCount = 0;
213
+ let toolCallCount = 0;
214
+ const toolNames = new Set();
215
+ let startTime = "";
216
+ let endTime = "";
217
+ let hasTodayMessages = false;
218
+ for (const entry of entries) {
219
+ if (!entry._audit_timestamp || !isSameDay(entry._audit_timestamp, today))
220
+ continue;
221
+ hasTodayMessages = true;
222
+ if (!startTime || entry._audit_timestamp < startTime)
223
+ startTime = entry._audit_timestamp;
224
+ if (!endTime || entry._audit_timestamp > endTime)
225
+ endTime = entry._audit_timestamp;
226
+ if (!sessionId && entry.session_id)
227
+ sessionId = entry.session_id;
228
+ if (entry.type === "user") {
229
+ userCount++;
230
+ const text = typeof entry.message?.content === "string"
231
+ ? entry.message.content
232
+ : Array.isArray(entry.message?.content)
233
+ ? entry.message.content
234
+ .filter((b) => b.type === "text" && b.text)
235
+ .map((b) => b.text)
236
+ .join(" ")
237
+ : "";
238
+ if (text) {
239
+ prompts.push({
240
+ text,
241
+ timestamp: entry._audit_timestamp,
242
+ sessionId: sessionId || entry.session_id,
243
+ project: `[App] ${title}`,
244
+ });
245
+ }
246
+ }
247
+ else if (entry.type === "assistant") {
248
+ assistantCount++;
249
+ // Count tool_use blocks in assistant messages
250
+ if (Array.isArray(entry.message?.content)) {
251
+ for (const block of entry.message.content) {
252
+ if (block.type === "tool_use" && block.name) {
253
+ toolCallCount++;
254
+ toolNames.add(block.name);
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ if (!hasTodayMessages) {
261
+ return { prompts: [], session: null };
262
+ }
263
+ const session = {
264
+ sessionId,
265
+ project: `[App] ${title}`,
266
+ source: "claude-app",
267
+ messageCount: userCount + assistantCount,
268
+ userMessageCount: userCount,
269
+ assistantMessageCount: assistantCount,
270
+ toolCallCount,
271
+ toolNames: [...toolNames],
272
+ inputTokens: 0, // Not available in audit logs
273
+ outputTokens: 0,
274
+ startTime,
275
+ endTime,
276
+ };
277
+ return { prompts, session };
278
+ }
279
+ function collectDesktopSessions(today) {
280
+ const auditFiles = findDesktopAuditFiles();
281
+ const prompts = [];
282
+ const sessions = [];
283
+ for (const { auditPath, metaPath } of auditFiles) {
284
+ const result = parseDesktopSession(auditPath, metaPath, today);
285
+ prompts.push(...result.prompts);
286
+ if (result.session && result.session.messageCount > 0) {
287
+ sessions.push(result.session);
288
+ }
289
+ }
290
+ return { prompts, sessions };
291
+ }
292
+ // =============================================
293
+ // Combined collector
294
+ // =============================================
295
+ export function collectToday(dateOverride) {
296
+ const today = dateOverride ?? localToday();
297
+ // Collect from both sources
298
+ const code = collectCodeSessions(today);
299
+ const desktop = collectDesktopSessions(today);
300
+ const prompts = [...code.prompts, ...desktop.prompts].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
301
+ const sessions = [...code.sessions, ...desktop.sessions];
302
+ const totalTokens = sessions.reduce((s, sess) => s + sess.inputTokens + sess.outputTokens, 0);
303
+ const totalMessages = sessions.reduce((s, sess) => s + sess.messageCount, 0);
304
+ const totalToolCalls = sessions.reduce((s, sess) => s + sess.toolCallCount, 0);
305
+ const projectsWorkedOn = [...new Set(prompts.map((p) => p.project))];
306
+ return {
307
+ date: today,
308
+ prompts,
309
+ sessions,
310
+ totalTokens,
311
+ totalMessages,
312
+ totalToolCalls,
313
+ projectsWorkedOn,
314
+ };
315
+ }
@@ -0,0 +1,7 @@
1
+ import type { Insight, CoachState, StoredInsight } from "./types.js";
2
+ export declare function renderInsight(insight: Insight, state: CoachState): string;
3
+ export declare function renderStreak(state: CoachState): string;
4
+ export declare function renderHistory(insights: StoredInsight[]): string;
5
+ export declare function renderSetupSuccess(): string;
6
+ export declare function renderNoData(): string;
7
+ export declare function renderError(msg: string): string;
@@ -0,0 +1,139 @@
1
+ import chalk from "chalk";
2
+ const WIDTH = 50;
3
+ function hr() {
4
+ return chalk.dim("─".repeat(WIDTH));
5
+ }
6
+ function boxTop() {
7
+ return chalk.dim("┌" + "─".repeat(WIDTH) + "┐");
8
+ }
9
+ function boxBot() {
10
+ return chalk.dim("└" + "─".repeat(WIDTH) + "┘");
11
+ }
12
+ function boxMid() {
13
+ return chalk.dim("├" + "─".repeat(WIDTH) + "┤");
14
+ }
15
+ function padLine(text) {
16
+ return chalk.dim("│") + " " + text;
17
+ }
18
+ function wrapText(text, maxWidth) {
19
+ const words = text.split(" ");
20
+ const lines = [];
21
+ let current = "";
22
+ for (const word of words) {
23
+ if (current.length + word.length + 1 > maxWidth) {
24
+ lines.push(current);
25
+ current = word;
26
+ }
27
+ else {
28
+ current = current ? current + " " + word : word;
29
+ }
30
+ }
31
+ if (current)
32
+ lines.push(current);
33
+ return lines;
34
+ }
35
+ function renderSection(label, body) {
36
+ const lines = [];
37
+ lines.push(padLine(""));
38
+ lines.push(padLine(chalk.bold(label)));
39
+ for (const line of wrapText(body, WIDTH - 4)) {
40
+ lines.push(padLine(" " + line));
41
+ }
42
+ return lines;
43
+ }
44
+ export function renderInsight(insight, state) {
45
+ const out = [];
46
+ // Header
47
+ out.push(boxTop());
48
+ const streakStr = state.streak > 0 ? `Day ${state.streak} ${state.streak >= 3 ? "🔥" : ""}` : "Day 1";
49
+ const header = ` COACH`;
50
+ const headerRight = streakStr;
51
+ const padding = WIDTH - header.length - headerRight.length - 1;
52
+ out.push(padLine(chalk.bold.white(header) + " ".repeat(Math.max(1, padding)) + chalk.yellow(headerRight)));
53
+ out.push(boxMid());
54
+ // Dimension
55
+ out.push(padLine(""));
56
+ out.push(padLine(chalk.cyan.bold(` TODAY'S LENS: ${insight.dimension}`)));
57
+ // Lesson
58
+ out.push(...renderSection(" 📖 LESSON", insight.lesson));
59
+ // Tip
60
+ out.push(...renderSection(" 💡 TIP", insight.tip));
61
+ // Specific example
62
+ if (insight.specificExample) {
63
+ out.push(padLine(""));
64
+ out.push(padLine(chalk.bold(" ✦ BEFORE (your prompt):")));
65
+ for (const line of wrapText(insight.specificExample.before, WIDTH - 6)) {
66
+ out.push(padLine(chalk.dim(" " + line)));
67
+ }
68
+ out.push(padLine(""));
69
+ out.push(padLine(chalk.bold(" ✦ AFTER (try this):")));
70
+ for (const line of wrapText(insight.specificExample.after, WIDTH - 6)) {
71
+ out.push(padLine(chalk.green(" " + line)));
72
+ }
73
+ }
74
+ // Encouragement
75
+ out.push(padLine(""));
76
+ out.push(padLine(" 🌱 " + chalk.italic(insight.encouragement)));
77
+ // Footer
78
+ out.push(padLine(""));
79
+ out.push(boxMid());
80
+ out.push(padLine(chalk.dim(" Was this helpful? ") + chalk.bold("[y]") + " Yes " + chalk.bold("[n]") + " No"));
81
+ out.push(boxBot());
82
+ return out.join("\n");
83
+ }
84
+ export function renderStreak(state) {
85
+ const out = [];
86
+ out.push(boxTop());
87
+ out.push(padLine(chalk.bold.white(" COACH STATS")));
88
+ out.push(boxMid());
89
+ out.push(padLine(""));
90
+ out.push(padLine(` 🔥 Current streak: ${chalk.bold.yellow(String(state.streak))} days`));
91
+ out.push(padLine(` 📊 Total insights: ${chalk.bold(String(state.totalInsights))}`));
92
+ if (state.totalInsights > 0) {
93
+ const helpfulPct = Math.round((state.helpfulCount / state.totalInsights) * 100);
94
+ out.push(padLine(` 👍 Helpful rate: ${chalk.bold.green(helpfulPct + "%")} (${state.helpfulCount}/${state.totalInsights})`));
95
+ }
96
+ out.push(padLine(` 📅 Last run: ${state.lastRunDate ?? "never"}`));
97
+ out.push(padLine(""));
98
+ out.push(boxBot());
99
+ return out.join("\n");
100
+ }
101
+ export function renderHistory(insights) {
102
+ if (insights.length === 0) {
103
+ return chalk.dim("No insights yet. Run `coach` to get your first one!");
104
+ }
105
+ const out = [];
106
+ out.push(boxTop());
107
+ out.push(padLine(chalk.bold.white(" PAST INSIGHTS")));
108
+ out.push(boxMid());
109
+ // Show last 10
110
+ const recent = insights.slice(-10).reverse();
111
+ for (const insight of recent) {
112
+ const ratingIcon = insight.rating === "helpful"
113
+ ? chalk.green("👍")
114
+ : insight.rating === "not_helpful"
115
+ ? chalk.red("👎")
116
+ : chalk.dim("--");
117
+ out.push(padLine(""));
118
+ out.push(padLine(` ${chalk.dim(insight.date)} ${chalk.cyan(insight.dimension)} ${ratingIcon}`));
119
+ const preview = insight.lesson.slice(0, WIDTH - 8);
120
+ out.push(padLine(chalk.dim(` ${preview}...`)));
121
+ }
122
+ out.push(padLine(""));
123
+ out.push(boxBot());
124
+ if (insights.length > 10) {
125
+ out.push(chalk.dim(` Showing last 10 of ${insights.length} insights`));
126
+ }
127
+ return out.join("\n");
128
+ }
129
+ export function renderSetupSuccess() {
130
+ return chalk.green("✓") + " API key saved. Run " + chalk.bold("coach") + " to get your first insight!";
131
+ }
132
+ export function renderNoData() {
133
+ return chalk.yellow("No Claude Code sessions found for today.") +
134
+ "\n" +
135
+ chalk.dim("Use Claude Code throughout the day, then run `coach` in the evening for your daily insight.");
136
+ }
137
+ export function renderError(msg) {
138
+ return chalk.red("Error: ") + msg;
139
+ }
@@ -0,0 +1,10 @@
1
+ import type { CoachState, StoredInsight } from "./types.js";
2
+ export declare function loadState(): CoachState;
3
+ export declare function saveState(state: CoachState): void;
4
+ export declare function getApiKey(): string | undefined;
5
+ export declare function setApiKey(key: string): void;
6
+ export declare function updateStreak(state: CoachState, today: string): CoachState;
7
+ export declare function recordDimension(state: CoachState, dimension: string): CoachState;
8
+ export declare function appendInsight(insight: StoredInsight): void;
9
+ export declare function loadInsights(): StoredInsight[];
10
+ export declare function updateLastInsightRating(rating: "helpful" | "not_helpful"): void;
@@ -0,0 +1,91 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync, appendFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ const COACH_DIR = join(homedir(), ".coach");
5
+ const STATE_FILE = join(COACH_DIR, "state.json");
6
+ const INSIGHTS_FILE = join(COACH_DIR, "insights.jsonl");
7
+ function ensureDir() {
8
+ if (!existsSync(COACH_DIR)) {
9
+ mkdirSync(COACH_DIR, { recursive: true });
10
+ }
11
+ }
12
+ const DEFAULT_STATE = {
13
+ streak: 0,
14
+ lastRunDate: null,
15
+ recentDimensions: [],
16
+ totalInsights: 0,
17
+ helpfulCount: 0,
18
+ notHelpfulCount: 0,
19
+ };
20
+ export function loadState() {
21
+ ensureDir();
22
+ if (!existsSync(STATE_FILE)) {
23
+ return { ...DEFAULT_STATE };
24
+ }
25
+ const raw = readFileSync(STATE_FILE, "utf-8");
26
+ return JSON.parse(raw);
27
+ }
28
+ export function saveState(state) {
29
+ ensureDir();
30
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
31
+ }
32
+ export function getApiKey() {
33
+ const state = loadState();
34
+ return state.apiKey ?? process.env.ANTHROPIC_API_KEY;
35
+ }
36
+ export function setApiKey(key) {
37
+ const state = loadState();
38
+ state.apiKey = key;
39
+ saveState(state);
40
+ }
41
+ export function updateStreak(state, today) {
42
+ if (state.lastRunDate === today) {
43
+ return state; // Already ran today
44
+ }
45
+ const yesterday = new Date(today);
46
+ yesterday.setDate(yesterday.getDate() - 1);
47
+ const yesterdayStr = yesterday.toISOString().slice(0, 10);
48
+ if (state.lastRunDate === yesterdayStr) {
49
+ state.streak += 1;
50
+ }
51
+ else if (state.lastRunDate === null) {
52
+ state.streak = 1;
53
+ }
54
+ else {
55
+ state.streak = 1; // Streak broken, restart
56
+ }
57
+ state.lastRunDate = today;
58
+ return state;
59
+ }
60
+ export function recordDimension(state, dimension) {
61
+ state.recentDimensions.push(dimension);
62
+ // Keep only last 4 to ensure rotation
63
+ if (state.recentDimensions.length > 4) {
64
+ state.recentDimensions = state.recentDimensions.slice(-4);
65
+ }
66
+ state.totalInsights += 1;
67
+ return state;
68
+ }
69
+ export function appendInsight(insight) {
70
+ ensureDir();
71
+ appendFileSync(INSIGHTS_FILE, JSON.stringify(insight) + "\n");
72
+ }
73
+ export function loadInsights() {
74
+ ensureDir();
75
+ if (!existsSync(INSIGHTS_FILE)) {
76
+ return [];
77
+ }
78
+ const raw = readFileSync(INSIGHTS_FILE, "utf-8").trim();
79
+ if (!raw)
80
+ return [];
81
+ return raw.split("\n").map((line) => JSON.parse(line));
82
+ }
83
+ export function updateLastInsightRating(rating) {
84
+ const insights = loadInsights();
85
+ if (insights.length === 0)
86
+ return;
87
+ insights[insights.length - 1].rating = rating;
88
+ // Rewrite entire file
89
+ ensureDir();
90
+ writeFileSync(INSIGHTS_FILE, insights.map((i) => JSON.stringify(i)).join("\n") + "\n");
91
+ }
@@ -0,0 +1,109 @@
1
+ export declare const DIMENSIONS: readonly ["Prompting Craft", "Workflow Efficiency", "Architecture Thinking", "Learning Patterns", "Focus & Deep Work", "Communication Style", "Tool Leverage", "Problem Decomposition"];
2
+ export type Dimension = (typeof DIMENSIONS)[number];
3
+ export interface UserPrompt {
4
+ text: string;
5
+ timestamp: string;
6
+ sessionId: string;
7
+ project: string;
8
+ }
9
+ export type SessionSource = "claude-code" | "claude-app";
10
+ export interface SessionSummary {
11
+ sessionId: string;
12
+ project: string;
13
+ source: SessionSource;
14
+ messageCount: number;
15
+ userMessageCount: number;
16
+ assistantMessageCount: number;
17
+ toolCallCount: number;
18
+ toolNames: string[];
19
+ inputTokens: number;
20
+ outputTokens: number;
21
+ startTime: string;
22
+ endTime: string;
23
+ gitBranch?: string;
24
+ }
25
+ export interface CollectedData {
26
+ date: string;
27
+ prompts: UserPrompt[];
28
+ sessions: SessionSummary[];
29
+ totalTokens: number;
30
+ totalMessages: number;
31
+ totalToolCalls: number;
32
+ projectsWorkedOn: string[];
33
+ }
34
+ export interface SpecificExample {
35
+ before: string;
36
+ after: string;
37
+ }
38
+ export interface Insight {
39
+ dimension: Dimension;
40
+ lesson: string;
41
+ tip: string;
42
+ specificExample: SpecificExample | null;
43
+ encouragement: string;
44
+ }
45
+ export interface StoredInsight extends Insight {
46
+ date: string;
47
+ rating: "helpful" | "not_helpful" | null;
48
+ }
49
+ export interface CoachState {
50
+ streak: number;
51
+ lastRunDate: string | null;
52
+ recentDimensions: Dimension[];
53
+ totalInsights: number;
54
+ helpfulCount: number;
55
+ notHelpfulCount: number;
56
+ apiKey?: string;
57
+ }
58
+ export interface HistoryEntry {
59
+ display: string;
60
+ pastedContents: Record<string, unknown>;
61
+ timestamp: number;
62
+ project: string;
63
+ sessionId: string;
64
+ }
65
+ export interface ConversationMessage {
66
+ type: "user" | "assistant" | "file-history-snapshot";
67
+ sessionId?: string;
68
+ timestamp?: string;
69
+ cwd?: string;
70
+ gitBranch?: string;
71
+ message?: {
72
+ role: string;
73
+ content: string | ContentBlock[];
74
+ usage?: TokenUsage;
75
+ };
76
+ }
77
+ export interface ContentBlock {
78
+ type: string;
79
+ text?: string;
80
+ thinking?: string;
81
+ name?: string;
82
+ input?: unknown;
83
+ }
84
+ export interface TokenUsage {
85
+ input_tokens: number;
86
+ output_tokens: number;
87
+ cache_creation_input_tokens?: number;
88
+ cache_read_input_tokens?: number;
89
+ }
90
+ export interface DesktopSessionMeta {
91
+ sessionId: string;
92
+ title: string;
93
+ model: string;
94
+ initialMessage: string;
95
+ createdAt: number;
96
+ lastActivityAt: number;
97
+ cwd?: string;
98
+ }
99
+ export interface AuditEntry {
100
+ type: "user" | "assistant" | "tool" | "tool_result";
101
+ uuid: string;
102
+ session_id: string;
103
+ parent_tool_use_id: string | null;
104
+ message: {
105
+ role: string;
106
+ content: string | ContentBlock[];
107
+ };
108
+ _audit_timestamp: string;
109
+ }
@@ -0,0 +1,11 @@
1
+ // === Analysis Dimensions ===
2
+ export const DIMENSIONS = [
3
+ "Prompting Craft",
4
+ "Workflow Efficiency",
5
+ "Architecture Thinking",
6
+ "Learning Patterns",
7
+ "Focus & Deep Work",
8
+ "Communication Style",
9
+ "Tool Leverage",
10
+ "Problem Decomposition",
11
+ ];
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@pkprosol/coach",
3
+ "version": "1.0.0",
4
+ "description": "Daily AI work coach — analyzes your Claude Code & Claude App sessions to deliver one lesson + one tip daily",
5
+ "type": "module",
6
+ "bin": {
7
+ "coach": "./dist/bin/coach.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepublishOnly": "npm run build",
12
+ "postinstall": "node dist/postinstall.js || true"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "skill"
17
+ ],
18
+ "keywords": [
19
+ "claude",
20
+ "claude-code",
21
+ "ai",
22
+ "coach",
23
+ "productivity",
24
+ "developer-tools",
25
+ "cli"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/pkprosol/coach"
30
+ },
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@anthropic-ai/sdk": "^0.39.0",
34
+ "chalk": "^5.4.1",
35
+ "ora": "^8.2.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.0.0",
39
+ "tsx": "^4.19.0",
40
+ "typescript": "^5.7.0"
41
+ }
42
+ }