@pkprosol/coach 1.0.0 → 1.0.1
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 +39 -19
- package/dist/bin/coach.js +188 -32
- package/dist/postinstall.js +14 -9
- package/dist/src/analyzer.d.ts +4 -1
- package/dist/src/analyzer.js +121 -9
- package/dist/src/display.d.ts +18 -2
- package/dist/src/display.js +148 -3
- package/dist/src/storage.d.ts +5 -3
- package/dist/src/storage.js +48 -10
- package/dist/src/types.d.ts +16 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Coach — Daily AI Work Coach
|
|
2
2
|
|
|
3
|
-
Analyzes your **Claude Code** and **Claude App** sessions to deliver
|
|
3
|
+
Analyzes your **Claude Code** and **Claude App** sessions to deliver insights, handoff notes, focus analysis, and more. A swiss army knife for developer productivity.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
┌──────────────────────────────────────────────────┐
|
|
@@ -32,28 +32,52 @@ npm install -g @pkprosol/coach
|
|
|
32
32
|
|
|
33
33
|
This installs the `coach` CLI and auto-registers `/coach` as a slash command in Claude Code.
|
|
34
34
|
|
|
35
|
-
##
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Node.js 18+
|
|
38
|
+
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated
|
|
39
|
+
- Claude Code and/or Claude Desktop App (for session data)
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
### Core
|
|
36
44
|
|
|
37
45
|
```bash
|
|
38
|
-
coach
|
|
46
|
+
coach # Today's lesson + tip (AI-powered)
|
|
47
|
+
coach handoff # Generate a handoff note for your current work
|
|
48
|
+
coach focus # Analyze context-switching and focus patterns
|
|
39
49
|
```
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
### Quick Stats (no AI)
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
```bash
|
|
54
|
+
coach recap # Summary of today's sessions, prompts, tokens, tools
|
|
55
|
+
coach compare # Compare today vs your 7-day averages
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Goals
|
|
44
59
|
|
|
45
|
-
### Terminal
|
|
46
60
|
```bash
|
|
47
|
-
coach
|
|
48
|
-
coach
|
|
49
|
-
coach
|
|
61
|
+
coach goals # Show current goals
|
|
62
|
+
coach goals set "text" # Add a new goal
|
|
63
|
+
coach goals done 1 # Mark goal #1 complete
|
|
64
|
+
coach goals clear # Clear completed goals
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Meta
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
coach history # Browse past insights
|
|
71
|
+
coach streak # Current streak + stats
|
|
72
|
+
coach help # Show all commands
|
|
50
73
|
```
|
|
51
74
|
|
|
52
75
|
### Claude Code
|
|
76
|
+
|
|
53
77
|
```
|
|
54
|
-
/coach
|
|
55
|
-
/coach
|
|
56
|
-
/coach
|
|
78
|
+
/coach # Same thing, right inside Claude
|
|
79
|
+
/coach handoff
|
|
80
|
+
/coach recap
|
|
57
81
|
```
|
|
58
82
|
|
|
59
83
|
## How it works
|
|
@@ -63,6 +87,8 @@ coach history # browse past insights
|
|
|
63
87
|
3. **Delivers** one specific lesson + one actionable tip, with examples from your actual sessions
|
|
64
88
|
4. **Learns** from your ratings to improve future insights
|
|
65
89
|
|
|
90
|
+
AI-powered commands (`coach`, `handoff`, `focus`) use the Claude CLI under the hood — no separate API key needed.
|
|
91
|
+
|
|
66
92
|
## Data sources
|
|
67
93
|
|
|
68
94
|
| Source | Location | What's collected |
|
|
@@ -70,10 +96,4 @@ coach history # browse past insights
|
|
|
70
96
|
| Claude Code | `~/.claude/` | Prompts, session transcripts, tool usage, tokens |
|
|
71
97
|
| Claude App | `~/Library/Application Support/Claude/` | Cowork/agent mode audit logs |
|
|
72
98
|
|
|
73
|
-
All data stays local. Only a summary is sent to
|
|
74
|
-
|
|
75
|
-
## Requirements
|
|
76
|
-
|
|
77
|
-
- Node.js 18+
|
|
78
|
-
- An Anthropic API key
|
|
79
|
-
- Claude Code and/or Claude Desktop App
|
|
99
|
+
All data stays local. Only a summary is sent to Claude for analysis.
|
package/dist/bin/coach.js
CHANGED
|
@@ -3,9 +3,9 @@ import { createInterface } from "node:readline";
|
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { collectToday } from "../src/collector.js";
|
|
6
|
-
import { analyze } from "../src/analyzer.js";
|
|
7
|
-
import { renderInsight, renderStreak, renderHistory,
|
|
8
|
-
import { loadState, saveState,
|
|
6
|
+
import { analyze, runClaude, buildHandoffPrompt, buildFocusPrompt } from "../src/analyzer.js";
|
|
7
|
+
import { renderInsight, renderStreak, renderHistory, renderNoData, renderError, renderHandoff, renderFocus, renderRecap, renderGoals, renderCompare, } from "../src/display.js";
|
|
8
|
+
import { loadState, saveState, loadInsights, appendInsight, updateLastInsightRating, updateStreak, recordDimension, addGoal, completeGoal, clearCompletedGoals, recordDailyStat, } from "../src/storage.js";
|
|
9
9
|
function askQuestion(prompt) {
|
|
10
10
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
11
11
|
return new Promise((resolve) => {
|
|
@@ -15,28 +15,7 @@ function askQuestion(prompt) {
|
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
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
18
|
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
19
|
const spinner = ora({ text: "Collecting today's sessions...", color: "cyan" }).start();
|
|
41
20
|
// Collect data
|
|
42
21
|
const data = collectToday();
|
|
@@ -49,15 +28,26 @@ async function handleDefault() {
|
|
|
49
28
|
// Load state and past insights
|
|
50
29
|
let state = loadState();
|
|
51
30
|
const pastInsights = loadInsights();
|
|
31
|
+
// Record daily stat
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
34
|
+
recordDailyStat({
|
|
35
|
+
date: today,
|
|
36
|
+
sessions: data.sessions.length,
|
|
37
|
+
prompts: data.prompts.length,
|
|
38
|
+
tokens: data.totalTokens,
|
|
39
|
+
projects: data.projectsWorkedOn,
|
|
40
|
+
toolCalls: data.totalToolCalls,
|
|
41
|
+
});
|
|
52
42
|
// Analyze
|
|
53
43
|
let insight;
|
|
54
44
|
try {
|
|
55
|
-
insight = await analyze(data, state.recentDimensions, pastInsights
|
|
45
|
+
insight = await analyze(data, state.recentDimensions, pastInsights);
|
|
56
46
|
}
|
|
57
47
|
catch (err) {
|
|
58
48
|
spinner.stop();
|
|
59
|
-
if (err.
|
|
60
|
-
console.log(renderError("
|
|
49
|
+
if (err.message?.includes("claude CLI not found")) {
|
|
50
|
+
console.log(renderError("claude CLI not found. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code"));
|
|
61
51
|
}
|
|
62
52
|
else {
|
|
63
53
|
console.log(renderError(err.message ?? "Analysis failed."));
|
|
@@ -66,8 +56,6 @@ async function handleDefault() {
|
|
|
66
56
|
}
|
|
67
57
|
spinner.stop();
|
|
68
58
|
// 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
59
|
state = updateStreak(state, today);
|
|
72
60
|
state = recordDimension(state, insight.dimension);
|
|
73
61
|
// Display
|
|
@@ -130,13 +118,169 @@ function handleRate(value) {
|
|
|
130
118
|
console.log(renderError("Usage: coach rate y|n"));
|
|
131
119
|
}
|
|
132
120
|
}
|
|
121
|
+
async function handleHandoff() {
|
|
122
|
+
const spinner = ora({ text: "Collecting sessions for handoff...", color: "cyan" }).start();
|
|
123
|
+
const data = collectToday();
|
|
124
|
+
if (data.prompts.length === 0) {
|
|
125
|
+
spinner.stop();
|
|
126
|
+
console.log(renderNoData());
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
spinner.text = "Generating handoff note...";
|
|
130
|
+
try {
|
|
131
|
+
const prompt = buildHandoffPrompt(data);
|
|
132
|
+
const text = await runClaude(prompt);
|
|
133
|
+
spinner.stop();
|
|
134
|
+
const cleaned = text.replace(/^```json?\s*/, "").replace(/\s*```$/, "").trim();
|
|
135
|
+
const handoff = JSON.parse(cleaned);
|
|
136
|
+
console.log("");
|
|
137
|
+
console.log(renderHandoff(handoff));
|
|
138
|
+
console.log("");
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
spinner.stop();
|
|
142
|
+
if (err.message?.includes("claude CLI not found")) {
|
|
143
|
+
console.log(renderError("claude CLI not found. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code"));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(renderError(err.message ?? "Handoff generation failed."));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function handleFocus() {
|
|
151
|
+
const spinner = ora({ text: "Analyzing focus patterns...", color: "cyan" }).start();
|
|
152
|
+
const data = collectToday();
|
|
153
|
+
if (data.prompts.length === 0) {
|
|
154
|
+
spinner.stop();
|
|
155
|
+
console.log(renderNoData());
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
spinner.text = "Building focus analysis...";
|
|
159
|
+
try {
|
|
160
|
+
const prompt = buildFocusPrompt(data);
|
|
161
|
+
const text = await runClaude(prompt);
|
|
162
|
+
spinner.stop();
|
|
163
|
+
const cleaned = text.replace(/^```json?\s*/, "").replace(/\s*```$/, "").trim();
|
|
164
|
+
const focus = JSON.parse(cleaned);
|
|
165
|
+
console.log("");
|
|
166
|
+
console.log(renderFocus(focus));
|
|
167
|
+
console.log("");
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
spinner.stop();
|
|
171
|
+
if (err.message?.includes("claude CLI not found")) {
|
|
172
|
+
console.log(renderError("claude CLI not found. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code"));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(renderError(err.message ?? "Focus analysis failed."));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function handleRecap() {
|
|
180
|
+
const data = collectToday();
|
|
181
|
+
if (data.prompts.length === 0) {
|
|
182
|
+
console.log(renderNoData());
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
console.log("");
|
|
186
|
+
console.log(renderRecap(data));
|
|
187
|
+
console.log("");
|
|
188
|
+
}
|
|
189
|
+
function handleGoals(args) {
|
|
190
|
+
const sub = args[0];
|
|
191
|
+
if (!sub) {
|
|
192
|
+
// Show goals
|
|
193
|
+
const state = loadState();
|
|
194
|
+
console.log("");
|
|
195
|
+
console.log(renderGoals(state.goals));
|
|
196
|
+
console.log("");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (sub === "set") {
|
|
200
|
+
const text = args.slice(1).join(" ").replace(/^["']|["']$/g, "");
|
|
201
|
+
if (!text) {
|
|
202
|
+
console.log(renderError('Usage: coach goals set "your goal"'));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const goal = addGoal(text);
|
|
206
|
+
console.log(chalk.green("✓") + ` Goal #${goal.id} added: ${goal.text}`);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (sub === "done") {
|
|
210
|
+
const id = parseInt(args[1], 10);
|
|
211
|
+
if (isNaN(id)) {
|
|
212
|
+
console.log(renderError("Usage: coach goals done <id>"));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const ok = completeGoal(id);
|
|
216
|
+
if (ok) {
|
|
217
|
+
console.log(chalk.green("✓") + ` Goal #${id} marked complete.`);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.log(renderError(`Goal #${id} not found or already completed.`));
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (sub === "clear") {
|
|
225
|
+
const count = clearCompletedGoals();
|
|
226
|
+
console.log(chalk.green("✓") + ` Cleared ${count} completed goal${count !== 1 ? "s" : ""}.`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
console.log(renderError(`Unknown goals subcommand: ${sub}`));
|
|
230
|
+
}
|
|
231
|
+
function handleCompare() {
|
|
232
|
+
const data = collectToday();
|
|
233
|
+
if (data.prompts.length === 0) {
|
|
234
|
+
console.log(renderNoData());
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const state = loadState();
|
|
238
|
+
const now = new Date();
|
|
239
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
240
|
+
const todayStat = {
|
|
241
|
+
date: today,
|
|
242
|
+
sessions: data.sessions.length,
|
|
243
|
+
prompts: data.prompts.length,
|
|
244
|
+
tokens: data.totalTokens,
|
|
245
|
+
projects: data.projectsWorkedOn,
|
|
246
|
+
toolCalls: data.totalToolCalls,
|
|
247
|
+
};
|
|
248
|
+
// Compute 7-day average from stored stats (excluding today)
|
|
249
|
+
const pastStats = state.dailyStats.filter((s) => s.date !== today).slice(-7);
|
|
250
|
+
if (pastStats.length === 0) {
|
|
251
|
+
console.log("");
|
|
252
|
+
console.log(renderRecap(data));
|
|
253
|
+
console.log(chalk.dim(" No historical data yet for comparison. Run `coach` daily to build history."));
|
|
254
|
+
console.log("");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const n = pastStats.length;
|
|
258
|
+
const avg = {
|
|
259
|
+
date: `${n}-day avg`,
|
|
260
|
+
sessions: pastStats.reduce((s, d) => s + d.sessions, 0) / n,
|
|
261
|
+
prompts: pastStats.reduce((s, d) => s + d.prompts, 0) / n,
|
|
262
|
+
tokens: pastStats.reduce((s, d) => s + d.tokens, 0) / n,
|
|
263
|
+
projects: Array(Math.round(pastStats.reduce((s, d) => s + d.projects.length, 0) / n)).fill(""),
|
|
264
|
+
toolCalls: pastStats.reduce((s, d) => s + d.toolCalls, 0) / n,
|
|
265
|
+
};
|
|
266
|
+
console.log("");
|
|
267
|
+
console.log(renderCompare(todayStat, avg));
|
|
268
|
+
console.log("");
|
|
269
|
+
}
|
|
133
270
|
function handleHelp() {
|
|
134
271
|
console.log(`
|
|
135
272
|
${chalk.bold("coach")} — Daily AI Work Coach
|
|
136
273
|
|
|
137
274
|
${chalk.bold("Usage:")}
|
|
138
275
|
coach Today's lesson + tip (default)
|
|
139
|
-
coach
|
|
276
|
+
coach handoff Generate a handoff note for your current work
|
|
277
|
+
coach focus Analyze context-switching and focus patterns
|
|
278
|
+
coach recap Quick summary of today's stats (no AI)
|
|
279
|
+
coach goals Show current goals
|
|
280
|
+
coach goals set Add a goal: coach goals set "finish auth"
|
|
281
|
+
coach goals done Mark complete: coach goals done 1
|
|
282
|
+
coach goals clear Clear completed goals
|
|
283
|
+
coach compare Compare today vs recent averages
|
|
140
284
|
coach history Browse past insights
|
|
141
285
|
coach streak Show current streak + stats
|
|
142
286
|
coach help Show this help message
|
|
@@ -145,8 +289,20 @@ ${chalk.bold("Usage:")}
|
|
|
145
289
|
// --- Main ---
|
|
146
290
|
const command = process.argv[2];
|
|
147
291
|
switch (command) {
|
|
148
|
-
case "
|
|
149
|
-
|
|
292
|
+
case "handoff":
|
|
293
|
+
handleHandoff();
|
|
294
|
+
break;
|
|
295
|
+
case "focus":
|
|
296
|
+
handleFocus();
|
|
297
|
+
break;
|
|
298
|
+
case "recap":
|
|
299
|
+
handleRecap();
|
|
300
|
+
break;
|
|
301
|
+
case "goals":
|
|
302
|
+
handleGoals(process.argv.slice(3));
|
|
303
|
+
break;
|
|
304
|
+
case "compare":
|
|
305
|
+
handleCompare();
|
|
150
306
|
break;
|
|
151
307
|
case "history":
|
|
152
308
|
handleHistoryCmd();
|
package/dist/postinstall.js
CHANGED
|
@@ -5,28 +5,33 @@ const skillDir = join(homedir(), ".claude", "skills", "coach");
|
|
|
5
5
|
const skillFile = join(skillDir, "SKILL.md");
|
|
6
6
|
const SKILL_CONTENT = `---
|
|
7
7
|
name: coach
|
|
8
|
-
description: Daily AI work coach
|
|
8
|
+
description: Daily AI work coach — analyzes your Claude sessions to deliver insights, handoff notes, focus analysis, and more
|
|
9
9
|
disable-model-invocation: true
|
|
10
10
|
user-invocable: true
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
Run the Coach CLI tool
|
|
13
|
+
Run the Coach CLI tool and display the output exactly as-is to the user (it contains formatted terminal UI).
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
If the user passes an argument, route it as a subcommand:
|
|
16
|
+
- \`/coach\` → \`coach\` (today's lesson + tip)
|
|
17
|
+
- \`/coach handoff\` → \`coach handoff\` (generate handoff note)
|
|
18
|
+
- \`/coach focus\` → \`coach focus\` (focus analysis)
|
|
19
|
+
- \`/coach recap\` → \`coach recap\` (quick stats)
|
|
20
|
+
- \`/coach goals\` → \`coach goals\` (show goals)
|
|
21
|
+
- \`/coach goals set "text"\` → \`coach goals set "text"\`
|
|
22
|
+
- \`/coach goals done 1\` → \`coach goals done 1\`
|
|
23
|
+
- \`/coach compare\` → \`coach compare\` (today vs averages)
|
|
24
|
+
- \`/coach streak\` → \`coach streak\`
|
|
25
|
+
- \`/coach history\` → \`coach history\`
|
|
16
26
|
|
|
17
27
|
\`\`\`bash
|
|
18
28
|
coach
|
|
19
29
|
\`\`\`
|
|
20
30
|
|
|
21
|
-
After showing the output, ask the user: "Was this helpful? (y/n)" and based on their answer, run:
|
|
31
|
+
After showing the output of the default command, ask the user: "Was this helpful? (y/n)" and based on their answer, run:
|
|
22
32
|
|
|
23
33
|
- If yes: \`coach rate y\`
|
|
24
34
|
- 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
35
|
`;
|
|
31
36
|
try {
|
|
32
37
|
if (!existsSync(join(homedir(), ".claude"))) {
|
package/dist/src/analyzer.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { CollectedData, Dimension, Insight, StoredInsight } from "./types.js";
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function runClaude(prompt: string): Promise<string>;
|
|
3
|
+
export declare function buildHandoffPrompt(data: CollectedData): string;
|
|
4
|
+
export declare function buildFocusPrompt(data: CollectedData): string;
|
|
5
|
+
export declare function analyze(data: CollectedData, recentDimensions: Dimension[], pastInsights: StoredInsight[]): Promise<Insight>;
|
package/dist/src/analyzer.js
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
2
|
import { DIMENSIONS } from "./types.js";
|
|
3
|
+
export function runClaude(prompt) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const proc = spawn("claude", ["-p", "--output-format", "text", "--max-tokens", "1024"], {
|
|
6
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
7
|
+
});
|
|
8
|
+
let stdout = "";
|
|
9
|
+
let stderr = "";
|
|
10
|
+
proc.stdout.on("data", (chunk) => {
|
|
11
|
+
stdout += chunk.toString();
|
|
12
|
+
});
|
|
13
|
+
proc.stderr.on("data", (chunk) => {
|
|
14
|
+
stderr += chunk.toString();
|
|
15
|
+
});
|
|
16
|
+
proc.on("error", (err) => {
|
|
17
|
+
if (err.code === "ENOENT") {
|
|
18
|
+
reject(new Error("claude CLI not found. Install Claude Code first: https://docs.anthropic.com/en/docs/claude-code"));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
reject(err);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
proc.on("close", (code) => {
|
|
25
|
+
if (code === 0) {
|
|
26
|
+
resolve(stdout.trim());
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
reject(new Error(stderr.trim() || `claude exited with code ${code}`));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
proc.stdin.write(prompt);
|
|
33
|
+
proc.stdin.end();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
3
36
|
function pickDimension(recentDimensions, data) {
|
|
4
37
|
// Filter out recently used dimensions
|
|
5
38
|
const available = DIMENSIONS.filter((d) => !recentDimensions.includes(d));
|
|
@@ -120,16 +153,95 @@ Guidelines:
|
|
|
120
153
|
|
|
121
154
|
Respond with ONLY the JSON object, no markdown fences or other text.`;
|
|
122
155
|
}
|
|
123
|
-
export
|
|
156
|
+
export function buildHandoffPrompt(data) {
|
|
157
|
+
const sessionSummaries = data.sessions.map((s) => ({
|
|
158
|
+
project: s.project,
|
|
159
|
+
branch: s.gitBranch,
|
|
160
|
+
messages: s.messageCount,
|
|
161
|
+
toolCalls: s.toolCallCount,
|
|
162
|
+
tools: s.toolNames,
|
|
163
|
+
duration: s.startTime && s.endTime
|
|
164
|
+
? `${Math.round((new Date(s.endTime).getTime() - new Date(s.startTime).getTime()) / 60000)}min`
|
|
165
|
+
: "unknown",
|
|
166
|
+
}));
|
|
167
|
+
const samplePrompts = data.prompts.slice(0, 30).map((p) => ({
|
|
168
|
+
text: p.text.slice(0, 400),
|
|
169
|
+
project: p.project,
|
|
170
|
+
}));
|
|
171
|
+
return `You are a work session analyzer. Given the developer's Claude Code sessions from today, produce a structured handoff note for when they pause or stop working.
|
|
172
|
+
|
|
173
|
+
## Today's Session Data
|
|
174
|
+
|
|
175
|
+
Date: ${data.date}
|
|
176
|
+
Projects: ${data.projectsWorkedOn.join(", ")}
|
|
177
|
+
|
|
178
|
+
### Sessions
|
|
179
|
+
${JSON.stringify(sessionSummaries, null, 2)}
|
|
180
|
+
|
|
181
|
+
### User Prompts
|
|
182
|
+
${JSON.stringify(samplePrompts, null, 2)}
|
|
183
|
+
|
|
184
|
+
## Your Task
|
|
185
|
+
|
|
186
|
+
Produce a handoff note as a JSON object with these fields:
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
"workingOn": "Brief description of what was being worked on (projects, branches, features)",
|
|
190
|
+
"currentState": "What's done, what's in progress",
|
|
191
|
+
"keyDecisions": ["Decision 1", "Decision 2"],
|
|
192
|
+
"nextSteps": ["Next step 1", "Next step 2"],
|
|
193
|
+
"openQuestions": ["Question 1"] or []
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
Be specific — reference actual projects, branches, and prompt content.
|
|
197
|
+
Respond with ONLY the JSON object, no markdown fences or other text.`;
|
|
198
|
+
}
|
|
199
|
+
export function buildFocusPrompt(data) {
|
|
200
|
+
const sessionTimeline = data.sessions.map((s) => ({
|
|
201
|
+
project: s.project,
|
|
202
|
+
start: s.startTime,
|
|
203
|
+
end: s.endTime,
|
|
204
|
+
prompts: s.userMessageCount,
|
|
205
|
+
}));
|
|
206
|
+
const projectSwitches = [];
|
|
207
|
+
for (let i = 1; i < data.prompts.length; i++) {
|
|
208
|
+
if (data.prompts[i].project !== data.prompts[i - 1].project) {
|
|
209
|
+
projectSwitches.push(`${data.prompts[i - 1].project} → ${data.prompts[i].project} at ${data.prompts[i].timestamp}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return `You are a focus and productivity analyst. Analyze this developer's context-switching patterns and suggest optimal focus blocks.
|
|
213
|
+
|
|
214
|
+
## Today's Data
|
|
215
|
+
|
|
216
|
+
Date: ${data.date}
|
|
217
|
+
Total sessions: ${data.sessions.length}
|
|
218
|
+
Projects: ${data.projectsWorkedOn.join(", ")}
|
|
219
|
+
|
|
220
|
+
### Session Timeline
|
|
221
|
+
${JSON.stringify(sessionTimeline, null, 2)}
|
|
222
|
+
|
|
223
|
+
### Context Switches
|
|
224
|
+
${projectSwitches.length > 0 ? projectSwitches.join("\n") : "No context switches detected"}
|
|
225
|
+
|
|
226
|
+
## Your Task
|
|
227
|
+
|
|
228
|
+
Analyze the patterns and return a JSON object:
|
|
229
|
+
|
|
230
|
+
{
|
|
231
|
+
"contextSwitches": ${projectSwitches.length},
|
|
232
|
+
"longestFocusPeriod": "Description of longest uninterrupted focus period",
|
|
233
|
+
"shortestFocusPeriod": "Description of shortest period before switching",
|
|
234
|
+
"pattern": "Overall observation about their focus pattern today (2-3 sentences)",
|
|
235
|
+
"suggestions": ["Suggestion 1", "Suggestion 2", "Suggestion 3"]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
Be specific and reference actual project names and times.
|
|
239
|
+
Respond with ONLY the JSON object, no markdown fences or other text.`;
|
|
240
|
+
}
|
|
241
|
+
export async function analyze(data, recentDimensions, pastInsights) {
|
|
124
242
|
const dimension = pickDimension(recentDimensions, data);
|
|
125
243
|
const prompt = buildPrompt(data, dimension, pastInsights);
|
|
126
|
-
const
|
|
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 : "";
|
|
244
|
+
const text = await runClaude(prompt);
|
|
133
245
|
// Parse JSON from response — handle possible markdown fences
|
|
134
246
|
const cleaned = text.replace(/^```json?\s*/, "").replace(/\s*```$/, "").trim();
|
|
135
247
|
const result = JSON.parse(cleaned);
|
package/dist/src/display.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
import type { Insight, CoachState, StoredInsight } from "./types.js";
|
|
1
|
+
import type { Insight, CoachState, StoredInsight, Goal, CollectedData, DailyStat } from "./types.js";
|
|
2
2
|
export declare function renderInsight(insight: Insight, state: CoachState): string;
|
|
3
3
|
export declare function renderStreak(state: CoachState): string;
|
|
4
4
|
export declare function renderHistory(insights: StoredInsight[]): string;
|
|
5
|
-
export declare function renderSetupSuccess(): string;
|
|
6
5
|
export declare function renderNoData(): string;
|
|
7
6
|
export declare function renderError(msg: string): string;
|
|
7
|
+
export declare function renderHandoff(handoff: {
|
|
8
|
+
workingOn: string;
|
|
9
|
+
currentState: string;
|
|
10
|
+
keyDecisions: string[];
|
|
11
|
+
nextSteps: string[];
|
|
12
|
+
openQuestions: string[];
|
|
13
|
+
}): string;
|
|
14
|
+
export declare function renderFocus(focus: {
|
|
15
|
+
contextSwitches: number;
|
|
16
|
+
longestFocusPeriod: string;
|
|
17
|
+
shortestFocusPeriod: string;
|
|
18
|
+
pattern: string;
|
|
19
|
+
suggestions: string[];
|
|
20
|
+
}): string;
|
|
21
|
+
export declare function renderRecap(data: CollectedData): string;
|
|
22
|
+
export declare function renderGoals(goals: Goal[]): string;
|
|
23
|
+
export declare function renderCompare(today: DailyStat, avg: DailyStat): string;
|
package/dist/src/display.js
CHANGED
|
@@ -126,9 +126,6 @@ export function renderHistory(insights) {
|
|
|
126
126
|
}
|
|
127
127
|
return out.join("\n");
|
|
128
128
|
}
|
|
129
|
-
export function renderSetupSuccess() {
|
|
130
|
-
return chalk.green("✓") + " API key saved. Run " + chalk.bold("coach") + " to get your first insight!";
|
|
131
|
-
}
|
|
132
129
|
export function renderNoData() {
|
|
133
130
|
return chalk.yellow("No Claude Code sessions found for today.") +
|
|
134
131
|
"\n" +
|
|
@@ -137,3 +134,151 @@ export function renderNoData() {
|
|
|
137
134
|
export function renderError(msg) {
|
|
138
135
|
return chalk.red("Error: ") + msg;
|
|
139
136
|
}
|
|
137
|
+
// === Handoff ===
|
|
138
|
+
export function renderHandoff(handoff) {
|
|
139
|
+
const out = [];
|
|
140
|
+
out.push(boxTop());
|
|
141
|
+
out.push(padLine(chalk.bold.white(" HANDOFF NOTE")));
|
|
142
|
+
out.push(boxMid());
|
|
143
|
+
out.push(...renderSection(" Working On", handoff.workingOn));
|
|
144
|
+
out.push(...renderSection(" Current State", handoff.currentState));
|
|
145
|
+
if (handoff.keyDecisions.length > 0) {
|
|
146
|
+
out.push(padLine(""));
|
|
147
|
+
out.push(padLine(chalk.bold(" Key Decisions")));
|
|
148
|
+
for (const d of handoff.keyDecisions) {
|
|
149
|
+
for (const line of wrapText(`- ${d}`, WIDTH - 6)) {
|
|
150
|
+
out.push(padLine(" " + line));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (handoff.nextSteps.length > 0) {
|
|
155
|
+
out.push(padLine(""));
|
|
156
|
+
out.push(padLine(chalk.bold(" Next Steps")));
|
|
157
|
+
for (const s of handoff.nextSteps) {
|
|
158
|
+
for (const line of wrapText(`- ${s}`, WIDTH - 6)) {
|
|
159
|
+
out.push(padLine(" " + line));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (handoff.openQuestions.length > 0) {
|
|
164
|
+
out.push(padLine(""));
|
|
165
|
+
out.push(padLine(chalk.bold(" Open Questions")));
|
|
166
|
+
for (const q of handoff.openQuestions) {
|
|
167
|
+
for (const line of wrapText(`? ${q}`, WIDTH - 6)) {
|
|
168
|
+
out.push(padLine(" " + line));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
out.push(padLine(""));
|
|
173
|
+
out.push(boxBot());
|
|
174
|
+
return out.join("\n");
|
|
175
|
+
}
|
|
176
|
+
// === Focus ===
|
|
177
|
+
export function renderFocus(focus) {
|
|
178
|
+
const out = [];
|
|
179
|
+
out.push(boxTop());
|
|
180
|
+
out.push(padLine(chalk.bold.white(" FOCUS ANALYSIS")));
|
|
181
|
+
out.push(boxMid());
|
|
182
|
+
out.push(padLine(""));
|
|
183
|
+
out.push(padLine(` Context switches: ${chalk.bold.yellow(String(focus.contextSwitches))}`));
|
|
184
|
+
out.push(...renderSection(" Longest Focus", focus.longestFocusPeriod));
|
|
185
|
+
out.push(...renderSection(" Shortest Focus", focus.shortestFocusPeriod));
|
|
186
|
+
out.push(...renderSection(" Pattern", focus.pattern));
|
|
187
|
+
if (focus.suggestions.length > 0) {
|
|
188
|
+
out.push(padLine(""));
|
|
189
|
+
out.push(padLine(chalk.bold(" Suggestions")));
|
|
190
|
+
for (const s of focus.suggestions) {
|
|
191
|
+
for (const line of wrapText(`- ${s}`, WIDTH - 6)) {
|
|
192
|
+
out.push(padLine(" " + line));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
out.push(padLine(""));
|
|
197
|
+
out.push(boxBot());
|
|
198
|
+
return out.join("\n");
|
|
199
|
+
}
|
|
200
|
+
// === Recap ===
|
|
201
|
+
export function renderRecap(data) {
|
|
202
|
+
const out = [];
|
|
203
|
+
out.push(boxTop());
|
|
204
|
+
out.push(padLine(chalk.bold.white(" TODAY'S RECAP")));
|
|
205
|
+
out.push(boxMid());
|
|
206
|
+
out.push(padLine(""));
|
|
207
|
+
out.push(padLine(` Date: ${chalk.bold(data.date)}`));
|
|
208
|
+
out.push(padLine(` Projects: ${chalk.cyan(data.projectsWorkedOn.join(", ") || "none")}`));
|
|
209
|
+
out.push(padLine(` Sessions: ${chalk.bold(String(data.sessions.length))}`));
|
|
210
|
+
out.push(padLine(` Prompts: ${chalk.bold(String(data.prompts.length))}`));
|
|
211
|
+
out.push(padLine(` Tokens: ${chalk.bold(data.totalTokens.toLocaleString())}`));
|
|
212
|
+
out.push(padLine(` Tool calls: ${chalk.bold(String(data.totalToolCalls))}`));
|
|
213
|
+
// Time spent
|
|
214
|
+
let totalMinutes = 0;
|
|
215
|
+
for (const s of data.sessions) {
|
|
216
|
+
if (s.startTime && s.endTime) {
|
|
217
|
+
totalMinutes += Math.round((new Date(s.endTime).getTime() - new Date(s.startTime).getTime()) / 60000);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (totalMinutes > 0) {
|
|
221
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
222
|
+
const mins = totalMinutes % 60;
|
|
223
|
+
out.push(padLine(` Time: ${chalk.bold(hours > 0 ? `${hours}h ${mins}m` : `${mins}m`)}`));
|
|
224
|
+
}
|
|
225
|
+
// Unique tools
|
|
226
|
+
const allTools = new Set();
|
|
227
|
+
for (const s of data.sessions) {
|
|
228
|
+
for (const t of s.toolNames)
|
|
229
|
+
allTools.add(t);
|
|
230
|
+
}
|
|
231
|
+
if (allTools.size > 0) {
|
|
232
|
+
out.push(padLine(` Tools used: ${chalk.dim([...allTools].join(", "))}`));
|
|
233
|
+
}
|
|
234
|
+
out.push(padLine(""));
|
|
235
|
+
out.push(boxBot());
|
|
236
|
+
return out.join("\n");
|
|
237
|
+
}
|
|
238
|
+
// === Goals ===
|
|
239
|
+
export function renderGoals(goals) {
|
|
240
|
+
const out = [];
|
|
241
|
+
out.push(boxTop());
|
|
242
|
+
out.push(padLine(chalk.bold.white(" GOALS")));
|
|
243
|
+
out.push(boxMid());
|
|
244
|
+
if (goals.length === 0) {
|
|
245
|
+
out.push(padLine(""));
|
|
246
|
+
out.push(padLine(chalk.dim(" No goals set. Use `coach goals set \"your goal\"` to add one.")));
|
|
247
|
+
out.push(padLine(""));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
out.push(padLine(""));
|
|
251
|
+
for (const g of goals) {
|
|
252
|
+
const status = g.completedDate
|
|
253
|
+
? chalk.green("[done]")
|
|
254
|
+
: chalk.yellow("[ ]");
|
|
255
|
+
const text = g.completedDate ? chalk.strikethrough.dim(g.text) : g.text;
|
|
256
|
+
out.push(padLine(` ${status} ${chalk.dim(`#${g.id}`)} ${text}`));
|
|
257
|
+
}
|
|
258
|
+
out.push(padLine(""));
|
|
259
|
+
}
|
|
260
|
+
out.push(boxBot());
|
|
261
|
+
return out.join("\n");
|
|
262
|
+
}
|
|
263
|
+
// === Compare ===
|
|
264
|
+
export function renderCompare(today, avg) {
|
|
265
|
+
const out = [];
|
|
266
|
+
out.push(boxTop());
|
|
267
|
+
out.push(padLine(chalk.bold.white(" TODAY vs 7-DAY AVERAGE")));
|
|
268
|
+
out.push(boxMid());
|
|
269
|
+
function compareVal(label, todayVal, avgVal) {
|
|
270
|
+
const diff = todayVal - avgVal;
|
|
271
|
+
const arrow = diff > 0 ? chalk.green("^") : diff < 0 ? chalk.red("v") : chalk.dim("=");
|
|
272
|
+
const diffStr = diff !== 0 ? ` (${diff > 0 ? "+" : ""}${Math.round(diff)})` : "";
|
|
273
|
+
return ` ${label.padEnd(14)} ${chalk.bold(String(todayVal).padStart(6))} ${chalk.dim("avg")} ${String(Math.round(avgVal)).padStart(6)} ${arrow}${diffStr}`;
|
|
274
|
+
}
|
|
275
|
+
out.push(padLine(""));
|
|
276
|
+
out.push(padLine(compareVal("Sessions", today.sessions, avg.sessions)));
|
|
277
|
+
out.push(padLine(compareVal("Prompts", today.prompts, avg.prompts)));
|
|
278
|
+
out.push(padLine(compareVal("Tokens", today.tokens, avg.tokens)));
|
|
279
|
+
out.push(padLine(compareVal("Tool calls", today.toolCalls, avg.toolCalls)));
|
|
280
|
+
out.push(padLine(` ${"Projects".padEnd(14)} ${chalk.bold(String(today.projects.length).padStart(6))} ${chalk.dim("avg")} ${String(Math.round(avg.projects.length)).padStart(6)}`));
|
|
281
|
+
out.push(padLine(""));
|
|
282
|
+
out.push(boxBot());
|
|
283
|
+
return out.join("\n");
|
|
284
|
+
}
|
package/dist/src/storage.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { CoachState, StoredInsight } from "./types.js";
|
|
1
|
+
import type { CoachState, StoredInsight, Goal, DailyStat } from "./types.js";
|
|
2
2
|
export declare function loadState(): CoachState;
|
|
3
3
|
export declare function saveState(state: CoachState): void;
|
|
4
|
-
export declare function getApiKey(): string | undefined;
|
|
5
|
-
export declare function setApiKey(key: string): void;
|
|
6
4
|
export declare function updateStreak(state: CoachState, today: string): CoachState;
|
|
7
5
|
export declare function recordDimension(state: CoachState, dimension: string): CoachState;
|
|
8
6
|
export declare function appendInsight(insight: StoredInsight): void;
|
|
9
7
|
export declare function loadInsights(): StoredInsight[];
|
|
10
8
|
export declare function updateLastInsightRating(rating: "helpful" | "not_helpful"): void;
|
|
9
|
+
export declare function addGoal(text: string): Goal;
|
|
10
|
+
export declare function completeGoal(id: number): boolean;
|
|
11
|
+
export declare function clearCompletedGoals(): number;
|
|
12
|
+
export declare function recordDailyStat(stat: DailyStat): void;
|
package/dist/src/storage.js
CHANGED
|
@@ -16,6 +16,8 @@ const DEFAULT_STATE = {
|
|
|
16
16
|
totalInsights: 0,
|
|
17
17
|
helpfulCount: 0,
|
|
18
18
|
notHelpfulCount: 0,
|
|
19
|
+
goals: [],
|
|
20
|
+
dailyStats: [],
|
|
19
21
|
};
|
|
20
22
|
export function loadState() {
|
|
21
23
|
ensureDir();
|
|
@@ -23,21 +25,12 @@ export function loadState() {
|
|
|
23
25
|
return { ...DEFAULT_STATE };
|
|
24
26
|
}
|
|
25
27
|
const raw = readFileSync(STATE_FILE, "utf-8");
|
|
26
|
-
return JSON.parse(raw);
|
|
28
|
+
return { ...DEFAULT_STATE, ...JSON.parse(raw) };
|
|
27
29
|
}
|
|
28
30
|
export function saveState(state) {
|
|
29
31
|
ensureDir();
|
|
30
32
|
writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
31
33
|
}
|
|
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
34
|
export function updateStreak(state, today) {
|
|
42
35
|
if (state.lastRunDate === today) {
|
|
43
36
|
return state; // Already ran today
|
|
@@ -89,3 +82,48 @@ export function updateLastInsightRating(rating) {
|
|
|
89
82
|
ensureDir();
|
|
90
83
|
writeFileSync(INSIGHTS_FILE, insights.map((i) => JSON.stringify(i)).join("\n") + "\n");
|
|
91
84
|
}
|
|
85
|
+
// === Goal management ===
|
|
86
|
+
export function addGoal(text) {
|
|
87
|
+
const state = loadState();
|
|
88
|
+
const id = (state.goals.length > 0 ? Math.max(...state.goals.map((g) => g.id)) : 0) + 1;
|
|
89
|
+
const now = new Date();
|
|
90
|
+
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
91
|
+
const goal = { id, text, createdDate: today, completedDate: null };
|
|
92
|
+
state.goals.push(goal);
|
|
93
|
+
saveState(state);
|
|
94
|
+
return goal;
|
|
95
|
+
}
|
|
96
|
+
export function completeGoal(id) {
|
|
97
|
+
const state = loadState();
|
|
98
|
+
const goal = state.goals.find((g) => g.id === id);
|
|
99
|
+
if (!goal || goal.completedDate)
|
|
100
|
+
return false;
|
|
101
|
+
const now = new Date();
|
|
102
|
+
goal.completedDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
103
|
+
saveState(state);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
export function clearCompletedGoals() {
|
|
107
|
+
const state = loadState();
|
|
108
|
+
const before = state.goals.length;
|
|
109
|
+
state.goals = state.goals.filter((g) => !g.completedDate);
|
|
110
|
+
saveState(state);
|
|
111
|
+
return before - state.goals.length;
|
|
112
|
+
}
|
|
113
|
+
// === Daily stat recording ===
|
|
114
|
+
export function recordDailyStat(stat) {
|
|
115
|
+
const state = loadState();
|
|
116
|
+
// Replace if same date exists, otherwise append
|
|
117
|
+
const idx = state.dailyStats.findIndex((s) => s.date === stat.date);
|
|
118
|
+
if (idx >= 0) {
|
|
119
|
+
state.dailyStats[idx] = stat;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
state.dailyStats.push(stat);
|
|
123
|
+
}
|
|
124
|
+
// Keep last 30 days
|
|
125
|
+
if (state.dailyStats.length > 30) {
|
|
126
|
+
state.dailyStats = state.dailyStats.slice(-30);
|
|
127
|
+
}
|
|
128
|
+
saveState(state);
|
|
129
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -46,6 +46,20 @@ export interface StoredInsight extends Insight {
|
|
|
46
46
|
date: string;
|
|
47
47
|
rating: "helpful" | "not_helpful" | null;
|
|
48
48
|
}
|
|
49
|
+
export interface Goal {
|
|
50
|
+
id: number;
|
|
51
|
+
text: string;
|
|
52
|
+
createdDate: string;
|
|
53
|
+
completedDate: string | null;
|
|
54
|
+
}
|
|
55
|
+
export interface DailyStat {
|
|
56
|
+
date: string;
|
|
57
|
+
sessions: number;
|
|
58
|
+
prompts: number;
|
|
59
|
+
tokens: number;
|
|
60
|
+
projects: string[];
|
|
61
|
+
toolCalls: number;
|
|
62
|
+
}
|
|
49
63
|
export interface CoachState {
|
|
50
64
|
streak: number;
|
|
51
65
|
lastRunDate: string | null;
|
|
@@ -53,7 +67,8 @@ export interface CoachState {
|
|
|
53
67
|
totalInsights: number;
|
|
54
68
|
helpfulCount: number;
|
|
55
69
|
notHelpfulCount: number;
|
|
56
|
-
|
|
70
|
+
goals: Goal[];
|
|
71
|
+
dailyStats: DailyStat[];
|
|
57
72
|
}
|
|
58
73
|
export interface HistoryEntry {
|
|
59
74
|
display: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pkprosol/coach",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Daily AI work coach — analyzes your Claude Code & Claude App sessions to deliver one lesson + one tip daily",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@anthropic-ai/sdk": "^0.39.0",
|
|
34
33
|
"chalk": "^5.4.1",
|
|
35
34
|
"ora": "^8.2.0"
|
|
36
35
|
},
|