@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 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 one lesson + one tip daily. Built around the [Hooked model](https://www.nirandfar.com/hooked/) to make self-improvement habitual.
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
- ## Setup
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 setup # paste your Anthropic API key
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
- Or set `ANTHROPIC_API_KEY` in your environment.
51
+ ### Quick Stats (no AI)
42
52
 
43
- ## Usage
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 # today's lesson + tip
48
- coach streak # current streak + stats
49
- coach history # browse past insights
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 # same thing, right inside Claude
55
- /coach streak
56
- /coach history
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 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
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, renderSetupSuccess, renderNoData, renderError, } from "../src/display.js";
8
- import { loadState, saveState, getApiKey, setApiKey, loadInsights, appendInsight, updateLastInsightRating, updateStreak, recordDimension, } from "../src/storage.js";
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, apiKey);
45
+ insight = await analyze(data, state.recentDimensions, pastInsights);
56
46
  }
57
47
  catch (err) {
58
48
  spinner.stop();
59
- if (err.status === 401) {
60
- console.log(renderError("Invalid API key. Run `coach setup` to update it."));
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 setup Set your Anthropic API key
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 "setup":
149
- handleSetup();
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();
@@ -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 - analyzes your Claude Code and Claude App sessions to deliver one lesson and one tip
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 to analyze today's Claude usage and deliver a personalized insight.
13
+ Run the Coach CLI tool and display the output exactly as-is to the user (it contains formatted terminal UI).
14
14
 
15
- Execute this command and display the output exactly as-is to the user (it contains formatted terminal UI):
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"))) {
@@ -1,2 +1,5 @@
1
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>;
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>;
@@ -1,5 +1,38 @@
1
- import Anthropic from "@anthropic-ai/sdk";
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 async function analyze(data, recentDimensions, pastInsights, apiKey) {
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 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 : "";
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);
@@ -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;
@@ -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
+ }
@@ -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;
@@ -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
+ }
@@ -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
- apiKey?: string;
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.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
  },