nodebench-mcp 2.32.0 → 2.33.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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * founderTrackingTools — Temporal action tracking across sessions, days, weeks,
3
+ * months, quarters, and years.
4
+ *
5
+ * Every significant action records before/after state and reasoning.
6
+ * Aggregation queries run in SQL for efficiency.
7
+ */
8
+ import type { McpTool } from "../types.js";
9
+ export declare const founderTrackingTools: McpTool[];
@@ -0,0 +1,644 @@
1
+ /**
2
+ * founderTrackingTools — Temporal action tracking across sessions, days, weeks,
3
+ * months, quarters, and years.
4
+ *
5
+ * Every significant action records before/after state and reasoning.
6
+ * Aggregation queries run in SQL for efficiency.
7
+ */
8
+ import { getDb, genId } from "../db.js";
9
+ /* ------------------------------------------------------------------ */
10
+ /* Module-level session ID (unique per process) */
11
+ /* ------------------------------------------------------------------ */
12
+ const SESSION_ID = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
13
+ /* ------------------------------------------------------------------ */
14
+ /* Schema bootstrap (idempotent — runs on first getDb() call per */
15
+ /* process) */
16
+ /* ------------------------------------------------------------------ */
17
+ let _schemaReady = false;
18
+ function ensureSchema() {
19
+ if (_schemaReady)
20
+ return;
21
+ const db = getDb();
22
+ db.exec(`
23
+ CREATE TABLE IF NOT EXISTS tracking_actions (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ actionId TEXT UNIQUE NOT NULL,
26
+ sessionId TEXT NOT NULL,
27
+ timestamp TEXT NOT NULL,
28
+ action TEXT NOT NULL,
29
+ category TEXT NOT NULL,
30
+ beforeState TEXT,
31
+ afterState TEXT,
32
+ reasoning TEXT,
33
+ filesChanged TEXT,
34
+ impactLevel TEXT NOT NULL,
35
+ dayOfWeek TEXT NOT NULL,
36
+ weekNumber INTEGER NOT NULL,
37
+ month TEXT NOT NULL,
38
+ quarter TEXT NOT NULL,
39
+ year INTEGER NOT NULL
40
+ );
41
+
42
+ CREATE INDEX IF NOT EXISTS idx_tracking_actions_session ON tracking_actions(sessionId);
43
+ CREATE INDEX IF NOT EXISTS idx_tracking_actions_timestamp ON tracking_actions(timestamp);
44
+ CREATE INDEX IF NOT EXISTS idx_tracking_actions_category ON tracking_actions(category);
45
+ CREATE INDEX IF NOT EXISTS idx_tracking_actions_year ON tracking_actions(year);
46
+ CREATE INDEX IF NOT EXISTS idx_tracking_actions_month ON tracking_actions(month);
47
+ CREATE INDEX IF NOT EXISTS idx_tracking_actions_quarter ON tracking_actions(quarter);
48
+
49
+ CREATE TABLE IF NOT EXISTS tracking_milestones (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ milestoneId TEXT UNIQUE NOT NULL,
52
+ sessionId TEXT NOT NULL,
53
+ timestamp TEXT NOT NULL,
54
+ title TEXT NOT NULL,
55
+ description TEXT NOT NULL,
56
+ category TEXT NOT NULL,
57
+ evidence TEXT,
58
+ metrics TEXT,
59
+ dayOfWeek TEXT NOT NULL,
60
+ weekNumber INTEGER NOT NULL,
61
+ month TEXT NOT NULL,
62
+ quarter TEXT NOT NULL,
63
+ year INTEGER NOT NULL
64
+ );
65
+
66
+ CREATE INDEX IF NOT EXISTS idx_tracking_milestones_session ON tracking_milestones(sessionId);
67
+ CREATE INDEX IF NOT EXISTS idx_tracking_milestones_timestamp ON tracking_milestones(timestamp);
68
+ CREATE INDEX IF NOT EXISTS idx_tracking_milestones_category ON tracking_milestones(category);
69
+ CREATE INDEX IF NOT EXISTS idx_tracking_milestones_year ON tracking_milestones(year);
70
+ CREATE INDEX IF NOT EXISTS idx_tracking_milestones_month ON tracking_milestones(month);
71
+ CREATE INDEX IF NOT EXISTS idx_tracking_milestones_quarter ON tracking_milestones(quarter);
72
+ `);
73
+ _schemaReady = true;
74
+ }
75
+ /* ------------------------------------------------------------------ */
76
+ /* Temporal helpers */
77
+ /* ------------------------------------------------------------------ */
78
+ const DAY_NAMES = [
79
+ "Sunday",
80
+ "Monday",
81
+ "Tuesday",
82
+ "Wednesday",
83
+ "Thursday",
84
+ "Friday",
85
+ "Saturday",
86
+ ];
87
+ function getISOWeek(d) {
88
+ const tmp = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
89
+ tmp.setUTCDate(tmp.getUTCDate() + 4 - (tmp.getUTCDay() || 7));
90
+ const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1));
91
+ return Math.ceil(((tmp.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
92
+ }
93
+ function deriveQuarter(month) {
94
+ if (month <= 3)
95
+ return "Q1";
96
+ if (month <= 6)
97
+ return "Q2";
98
+ if (month <= 9)
99
+ return "Q3";
100
+ return "Q4";
101
+ }
102
+ function temporalMeta(now) {
103
+ const d = now ?? new Date();
104
+ const m = d.getMonth() + 1;
105
+ const y = d.getFullYear();
106
+ return {
107
+ timestamp: d.toISOString(),
108
+ dayOfWeek: DAY_NAMES[d.getDay()],
109
+ weekNumber: getISOWeek(d),
110
+ month: `${y}-${String(m).padStart(2, "0")}`,
111
+ quarter: `${y}-${deriveQuarter(m)}`,
112
+ year: y,
113
+ };
114
+ }
115
+ /* ------------------------------------------------------------------ */
116
+ /* Tools */
117
+ /* ------------------------------------------------------------------ */
118
+ export const founderTrackingTools = [
119
+ // ─── 1. track_action ─────────────────────────────────────────────
120
+ {
121
+ name: "track_action",
122
+ description: "Record any significant action with before/after state, reasoning, and temporal metadata. Auto-captures session, day, week, month, quarter, year.",
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {
126
+ action: {
127
+ type: "string",
128
+ description: "What was done (concise imperative description)",
129
+ },
130
+ category: {
131
+ type: "string",
132
+ enum: [
133
+ "build",
134
+ "deploy",
135
+ "fix",
136
+ "design",
137
+ "research",
138
+ "decision",
139
+ "config",
140
+ "test",
141
+ ],
142
+ description: "Action category",
143
+ },
144
+ beforeState: {
145
+ type: "string",
146
+ description: "State before the action (optional)",
147
+ },
148
+ afterState: {
149
+ type: "string",
150
+ description: "State after the action (optional)",
151
+ },
152
+ reasoning: {
153
+ type: "string",
154
+ description: "Why this action was taken (optional)",
155
+ },
156
+ filesChanged: {
157
+ type: "array",
158
+ items: { type: "string" },
159
+ description: "File paths changed (optional)",
160
+ },
161
+ impactLevel: {
162
+ type: "string",
163
+ enum: ["minor", "moderate", "major", "critical"],
164
+ description: "Impact level of the action",
165
+ },
166
+ },
167
+ required: ["action", "category", "impactLevel"],
168
+ },
169
+ handler: async (args) => {
170
+ ensureSchema();
171
+ const db = getDb();
172
+ const meta = temporalMeta();
173
+ const actionId = genId("act");
174
+ db.prepare(`INSERT INTO tracking_actions
175
+ (actionId, sessionId, timestamp, action, category, beforeState, afterState, reasoning, filesChanged, impactLevel, dayOfWeek, weekNumber, month, quarter, year)
176
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(actionId, SESSION_ID, meta.timestamp, args.action, args.category, args.beforeState ?? null, args.afterState ?? null, args.reasoning ?? null, args.filesChanged ? JSON.stringify(args.filesChanged) : null, args.impactLevel, meta.dayOfWeek, meta.weekNumber, meta.month, meta.quarter, meta.year);
177
+ return {
178
+ actionId,
179
+ sessionId: SESSION_ID,
180
+ timestamp: meta.timestamp,
181
+ recorded: true,
182
+ };
183
+ },
184
+ },
185
+ // ─── 2. track_milestone ──────────────────────────────────────────
186
+ {
187
+ name: "track_milestone",
188
+ description: "Record a significant milestone (phase complete, deploy, ship, launch, pivot, decision) with optional evidence and metrics.",
189
+ inputSchema: {
190
+ type: "object",
191
+ properties: {
192
+ title: { type: "string", description: "Milestone title" },
193
+ description: {
194
+ type: "string",
195
+ description: "What was achieved and why it matters",
196
+ },
197
+ category: {
198
+ type: "string",
199
+ enum: [
200
+ "phase_complete",
201
+ "deploy",
202
+ "ship",
203
+ "launch",
204
+ "pivot",
205
+ "decision",
206
+ ],
207
+ description: "Milestone category",
208
+ },
209
+ evidence: {
210
+ type: "string",
211
+ description: "Evidence or proof (URL, screenshot path, metric)",
212
+ },
213
+ metrics: {
214
+ type: "object",
215
+ description: "Key metrics at milestone time (e.g. {tools: 304, tests: 1510})",
216
+ },
217
+ },
218
+ required: ["title", "description", "category"],
219
+ },
220
+ handler: async (args) => {
221
+ ensureSchema();
222
+ const db = getDb();
223
+ const meta = temporalMeta();
224
+ const milestoneId = genId("ms");
225
+ db.prepare(`INSERT INTO tracking_milestones
226
+ (milestoneId, sessionId, timestamp, title, description, category, evidence, metrics, dayOfWeek, weekNumber, month, quarter, year)
227
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(milestoneId, SESSION_ID, meta.timestamp, args.title, args.description, args.category, args.evidence ?? null, args.metrics ? JSON.stringify(args.metrics) : null, meta.dayOfWeek, meta.weekNumber, meta.month, meta.quarter, meta.year);
228
+ return {
229
+ milestoneId,
230
+ sessionId: SESSION_ID,
231
+ timestamp: meta.timestamp,
232
+ recorded: true,
233
+ };
234
+ },
235
+ },
236
+ // ─── 3. get_session_journal ──────────────────────────────────────
237
+ {
238
+ name: "get_session_journal",
239
+ description: "Get all tracked actions from the current or a specified session, in chronological order.",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: {
243
+ sessionId: {
244
+ type: "string",
245
+ description: "Session ID to query (defaults to current process session)",
246
+ },
247
+ limit: {
248
+ type: "number",
249
+ description: "Max actions to return (default 100)",
250
+ },
251
+ },
252
+ },
253
+ annotations: { readOnlyHint: true },
254
+ handler: async (args) => {
255
+ ensureSchema();
256
+ const db = getDb();
257
+ const sid = args.sessionId ?? SESSION_ID;
258
+ const limit = args.limit ?? 100;
259
+ const actions = db
260
+ .prepare(`SELECT actionId, timestamp, action, category, beforeState, afterState, reasoning, filesChanged, impactLevel
261
+ FROM tracking_actions WHERE sessionId = ? ORDER BY timestamp ASC LIMIT ?`)
262
+ .all(sid, limit);
263
+ const milestones = db
264
+ .prepare(`SELECT milestoneId, timestamp, title, description, category, evidence, metrics
265
+ FROM tracking_milestones WHERE sessionId = ? ORDER BY timestamp ASC`)
266
+ .all(sid);
267
+ return {
268
+ sessionId: sid,
269
+ currentSession: sid === SESSION_ID,
270
+ actionCount: actions.length,
271
+ milestoneCount: milestones.length,
272
+ actions: actions.map((a) => ({
273
+ ...a,
274
+ filesChanged: a.filesChanged ? JSON.parse(a.filesChanged) : null,
275
+ })),
276
+ milestones: milestones.map((m) => ({
277
+ ...m,
278
+ metrics: m.metrics ? JSON.parse(m.metrics) : null,
279
+ })),
280
+ };
281
+ },
282
+ },
283
+ // ─── 4. get_daily_log ────────────────────────────────────────────
284
+ {
285
+ name: "get_daily_log",
286
+ description: "Get all tracked actions for a specific date, grouped by session with milestone highlights.",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ date: {
291
+ type: "string",
292
+ description: "Date in YYYY-MM-DD format (defaults to today)",
293
+ },
294
+ },
295
+ },
296
+ annotations: { readOnlyHint: true },
297
+ handler: async (args) => {
298
+ ensureSchema();
299
+ const db = getDb();
300
+ const targetDate = args.date ?? new Date().toISOString().slice(0, 10);
301
+ const actions = db
302
+ .prepare(`SELECT actionId, sessionId, timestamp, action, category, beforeState, afterState, reasoning, filesChanged, impactLevel
303
+ FROM tracking_actions
304
+ WHERE date(timestamp) = ?
305
+ ORDER BY timestamp ASC`)
306
+ .all(targetDate);
307
+ const milestones = db
308
+ .prepare(`SELECT milestoneId, sessionId, timestamp, title, description, category, evidence, metrics
309
+ FROM tracking_milestones
310
+ WHERE date(timestamp) = ?
311
+ ORDER BY timestamp ASC`)
312
+ .all(targetDate);
313
+ // Group actions by session
314
+ const bySession = {};
315
+ for (const a of actions) {
316
+ const sid = a.sessionId;
317
+ if (!bySession[sid])
318
+ bySession[sid] = [];
319
+ bySession[sid].push({
320
+ ...a,
321
+ filesChanged: a.filesChanged ? JSON.parse(a.filesChanged) : null,
322
+ });
323
+ }
324
+ return {
325
+ date: targetDate,
326
+ totalActions: actions.length,
327
+ totalMilestones: milestones.length,
328
+ sessions: Object.entries(bySession).map(([sid, acts]) => ({
329
+ sessionId: sid,
330
+ actionCount: acts.length,
331
+ actions: acts,
332
+ })),
333
+ milestones: milestones.map((m) => ({
334
+ ...m,
335
+ metrics: m.metrics ? JSON.parse(m.metrics) : null,
336
+ })),
337
+ };
338
+ },
339
+ },
340
+ // ─── 5. get_weekly_summary ───────────────────────────────────────
341
+ {
342
+ name: "get_weekly_summary",
343
+ description: "Summarize a week's tracked actions: totals by category and impact, files most changed, milestones.",
344
+ inputSchema: {
345
+ type: "object",
346
+ properties: {
347
+ weekStart: {
348
+ type: "string",
349
+ description: "Monday of the week in YYYY-MM-DD (defaults to current week's Monday)",
350
+ },
351
+ },
352
+ },
353
+ annotations: { readOnlyHint: true },
354
+ handler: async (args) => {
355
+ ensureSchema();
356
+ const db = getDb();
357
+ let monday;
358
+ if (args.weekStart) {
359
+ monday = args.weekStart;
360
+ }
361
+ else {
362
+ const now = new Date();
363
+ const day = now.getDay();
364
+ const diff = day === 0 ? 6 : day - 1;
365
+ const m = new Date(now);
366
+ m.setDate(m.getDate() - diff);
367
+ monday = m.toISOString().slice(0, 10);
368
+ }
369
+ const sundayDate = new Date(monday);
370
+ sundayDate.setDate(sundayDate.getDate() + 6);
371
+ const sunday = sundayDate.toISOString().slice(0, 10);
372
+ const byCategory = db
373
+ .prepare(`SELECT category, COUNT(*) as count
374
+ FROM tracking_actions
375
+ WHERE date(timestamp) BETWEEN ? AND ?
376
+ GROUP BY category ORDER BY count DESC`)
377
+ .all(monday, sunday);
378
+ const byImpact = db
379
+ .prepare(`SELECT impactLevel, COUNT(*) as count
380
+ FROM tracking_actions
381
+ WHERE date(timestamp) BETWEEN ? AND ?
382
+ GROUP BY impactLevel ORDER BY count DESC`)
383
+ .all(monday, sunday);
384
+ const totalActions = db
385
+ .prepare(`SELECT COUNT(*) as count
386
+ FROM tracking_actions
387
+ WHERE date(timestamp) BETWEEN ? AND ?`)
388
+ .get(monday, sunday);
389
+ const milestones = db
390
+ .prepare(`SELECT milestoneId, timestamp, title, category
391
+ FROM tracking_milestones
392
+ WHERE date(timestamp) BETWEEN ? AND ?
393
+ ORDER BY timestamp ASC`)
394
+ .all(monday, sunday);
395
+ // Files most changed
396
+ const allFiles = db
397
+ .prepare(`SELECT filesChanged
398
+ FROM tracking_actions
399
+ WHERE date(timestamp) BETWEEN ? AND ? AND filesChanged IS NOT NULL`)
400
+ .all(monday, sunday);
401
+ const fileCounts = {};
402
+ for (const row of allFiles) {
403
+ const files = JSON.parse(row.filesChanged);
404
+ for (const f of files) {
405
+ fileCounts[f] = (fileCounts[f] ?? 0) + 1;
406
+ }
407
+ }
408
+ const topFiles = Object.entries(fileCounts)
409
+ .sort((a, b) => b[1] - a[1])
410
+ .slice(0, 10)
411
+ .map(([file, count]) => ({ file, count }));
412
+ return {
413
+ weekStart: monday,
414
+ weekEnd: sunday,
415
+ totalActions: totalActions.count,
416
+ byCategory: Object.fromEntries(byCategory.map((r) => [r.category, r.count])),
417
+ byImpact: Object.fromEntries(byImpact.map((r) => [r.impactLevel, r.count])),
418
+ milestones,
419
+ topFiles,
420
+ };
421
+ },
422
+ },
423
+ // ─── 6. get_monthly_report ───────────────────────────────────────
424
+ {
425
+ name: "get_monthly_report",
426
+ description: "Monthly rollup of actions: weekly breakdown, trending categories, velocity (actions/day), milestone timeline.",
427
+ inputSchema: {
428
+ type: "object",
429
+ properties: {
430
+ month: {
431
+ type: "string",
432
+ description: "Month in YYYY-MM format (defaults to current month)",
433
+ },
434
+ },
435
+ },
436
+ annotations: { readOnlyHint: true },
437
+ handler: async (args) => {
438
+ ensureSchema();
439
+ const db = getDb();
440
+ const month = args.month ?? new Date().toISOString().slice(0, 7);
441
+ const byCategory = db
442
+ .prepare(`SELECT category, COUNT(*) as count
443
+ FROM tracking_actions WHERE month = ?
444
+ GROUP BY category ORDER BY count DESC`)
445
+ .all(month);
446
+ const byWeek = db
447
+ .prepare(`SELECT weekNumber, COUNT(*) as count
448
+ FROM tracking_actions WHERE month = ?
449
+ GROUP BY weekNumber ORDER BY weekNumber ASC`)
450
+ .all(month);
451
+ const totalActions = db
452
+ .prepare(`SELECT COUNT(*) as count FROM tracking_actions WHERE month = ?`)
453
+ .get(month);
454
+ const milestones = db
455
+ .prepare(`SELECT milestoneId, timestamp, title, category, metrics
456
+ FROM tracking_milestones WHERE month = ?
457
+ ORDER BY timestamp ASC`)
458
+ .all(month);
459
+ const byImpact = db
460
+ .prepare(`SELECT impactLevel, COUNT(*) as count
461
+ FROM tracking_actions WHERE month = ?
462
+ GROUP BY impactLevel ORDER BY count DESC`)
463
+ .all(month);
464
+ // Distinct days with actions for velocity
465
+ const activeDays = db
466
+ .prepare(`SELECT COUNT(DISTINCT date(timestamp)) as days
467
+ FROM tracking_actions WHERE month = ?`)
468
+ .get(month);
469
+ const velocity = activeDays.days > 0
470
+ ? Math.round((totalActions.count / activeDays.days) * 10) / 10
471
+ : 0;
472
+ return {
473
+ month,
474
+ totalActions: totalActions.count,
475
+ activeDays: activeDays.days,
476
+ velocityPerDay: velocity,
477
+ byCategory: Object.fromEntries(byCategory.map((r) => [r.category, r.count])),
478
+ byImpact: Object.fromEntries(byImpact.map((r) => [r.impactLevel, r.count])),
479
+ byWeek: byWeek.map((r) => ({
480
+ week: r.weekNumber,
481
+ actions: r.count,
482
+ })),
483
+ milestones: milestones.map((m) => ({
484
+ ...m,
485
+ metrics: m.metrics ? JSON.parse(m.metrics) : null,
486
+ })),
487
+ };
488
+ },
489
+ },
490
+ // ─── 7. get_quarterly_review ─────────────────────────────────────
491
+ {
492
+ name: "get_quarterly_review",
493
+ description: "Quarterly strategic view: monthly trends, category shifts, biggest milestones, velocity curve.",
494
+ inputSchema: {
495
+ type: "object",
496
+ properties: {
497
+ quarter: {
498
+ type: "string",
499
+ description: "Quarter in YYYY-Q1/Q2/Q3/Q4 format (defaults to current quarter)",
500
+ },
501
+ },
502
+ },
503
+ annotations: { readOnlyHint: true },
504
+ handler: async (args) => {
505
+ ensureSchema();
506
+ const db = getDb();
507
+ let quarter;
508
+ if (args.quarter) {
509
+ quarter = args.quarter;
510
+ }
511
+ else {
512
+ const now = new Date();
513
+ quarter = `${now.getFullYear()}-${deriveQuarter(now.getMonth() + 1)}`;
514
+ }
515
+ const byMonth = db
516
+ .prepare(`SELECT month, COUNT(*) as count
517
+ FROM tracking_actions WHERE quarter = ?
518
+ GROUP BY month ORDER BY month ASC`)
519
+ .all(quarter);
520
+ const byCategory = db
521
+ .prepare(`SELECT category, COUNT(*) as count
522
+ FROM tracking_actions WHERE quarter = ?
523
+ GROUP BY category ORDER BY count DESC`)
524
+ .all(quarter);
525
+ const totalActions = db
526
+ .prepare(`SELECT COUNT(*) as count FROM tracking_actions WHERE quarter = ?`)
527
+ .get(quarter);
528
+ const milestones = db
529
+ .prepare(`SELECT milestoneId, timestamp, title, category, metrics
530
+ FROM tracking_milestones WHERE quarter = ?
531
+ ORDER BY timestamp ASC`)
532
+ .all(quarter);
533
+ const activeDays = db
534
+ .prepare(`SELECT COUNT(DISTINCT date(timestamp)) as days
535
+ FROM tracking_actions WHERE quarter = ?`)
536
+ .get(quarter);
537
+ const velocity = activeDays.days > 0
538
+ ? Math.round((totalActions.count / activeDays.days) * 10) / 10
539
+ : 0;
540
+ // First and last action timestamps for state span
541
+ const firstAction = db
542
+ .prepare(`SELECT timestamp, action FROM tracking_actions WHERE quarter = ? ORDER BY timestamp ASC LIMIT 1`)
543
+ .get(quarter);
544
+ const lastAction = db
545
+ .prepare(`SELECT timestamp, action FROM tracking_actions WHERE quarter = ? ORDER BY timestamp DESC LIMIT 1`)
546
+ .get(quarter);
547
+ return {
548
+ quarter,
549
+ totalActions: totalActions.count,
550
+ activeDays: activeDays.days,
551
+ velocityPerDay: velocity,
552
+ byMonth: byMonth.map((r) => ({
553
+ month: r.month,
554
+ actions: r.count,
555
+ })),
556
+ byCategory: Object.fromEntries(byCategory.map((r) => [r.category, r.count])),
557
+ milestones: milestones.map((m) => ({
558
+ ...m,
559
+ metrics: m.metrics ? JSON.parse(m.metrics) : null,
560
+ })),
561
+ stateSpan: {
562
+ firstAction: firstAction ?? null,
563
+ lastAction: lastAction ?? null,
564
+ },
565
+ };
566
+ },
567
+ },
568
+ // ─── 8. get_annual_retrospective ─────────────────────────────────
569
+ {
570
+ name: "get_annual_retrospective",
571
+ description: "Full year view: quarterly summaries, yearly milestones, category distribution, growth metrics.",
572
+ inputSchema: {
573
+ type: "object",
574
+ properties: {
575
+ year: {
576
+ type: "number",
577
+ description: "Year to review (defaults to current year)",
578
+ },
579
+ },
580
+ },
581
+ annotations: { readOnlyHint: true },
582
+ handler: async (args) => {
583
+ ensureSchema();
584
+ const db = getDb();
585
+ const year = args.year ?? new Date().getFullYear();
586
+ const byQuarter = db
587
+ .prepare(`SELECT quarter, COUNT(*) as count
588
+ FROM tracking_actions WHERE year = ?
589
+ GROUP BY quarter ORDER BY quarter ASC`)
590
+ .all(year);
591
+ const byCategory = db
592
+ .prepare(`SELECT category, COUNT(*) as count
593
+ FROM tracking_actions WHERE year = ?
594
+ GROUP BY category ORDER BY count DESC`)
595
+ .all(year);
596
+ const byMonth = db
597
+ .prepare(`SELECT month, COUNT(*) as count
598
+ FROM tracking_actions WHERE year = ?
599
+ GROUP BY month ORDER BY month ASC`)
600
+ .all(year);
601
+ const totalActions = db
602
+ .prepare(`SELECT COUNT(*) as count FROM tracking_actions WHERE year = ?`)
603
+ .get(year);
604
+ const milestones = db
605
+ .prepare(`SELECT milestoneId, timestamp, title, category, metrics
606
+ FROM tracking_milestones WHERE year = ?
607
+ ORDER BY timestamp ASC`)
608
+ .all(year);
609
+ const activeDays = db
610
+ .prepare(`SELECT COUNT(DISTINCT date(timestamp)) as days
611
+ FROM tracking_actions WHERE year = ?`)
612
+ .get(year);
613
+ const totalSessions = db
614
+ .prepare(`SELECT COUNT(DISTINCT sessionId) as count
615
+ FROM tracking_actions WHERE year = ?`)
616
+ .get(year);
617
+ const velocity = activeDays.days > 0
618
+ ? Math.round((totalActions.count / activeDays.days) * 10) / 10
619
+ : 0;
620
+ return {
621
+ year,
622
+ totalActions: totalActions.count,
623
+ totalSessions: totalSessions.count,
624
+ totalMilestones: milestones.length,
625
+ activeDays: activeDays.days,
626
+ velocityPerDay: velocity,
627
+ byQuarter: byQuarter.map((r) => ({
628
+ quarter: r.quarter,
629
+ actions: r.count,
630
+ })),
631
+ byMonth: byMonth.map((r) => ({
632
+ month: r.month,
633
+ actions: r.count,
634
+ })),
635
+ byCategory: Object.fromEntries(byCategory.map((r) => [r.category, r.count])),
636
+ milestones: milestones.map((m) => ({
637
+ ...m,
638
+ metrics: m.metrics ? JSON.parse(m.metrics) : null,
639
+ })),
640
+ };
641
+ },
642
+ },
643
+ ];
644
+ //# sourceMappingURL=founderTrackingTools.js.map