@towles/tool 0.0.41 → 0.0.49

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.
Files changed (53) hide show
  1. package/README.md +67 -109
  2. package/package.json +51 -41
  3. package/src/commands/base.ts +3 -18
  4. package/src/commands/config.ts +9 -8
  5. package/src/commands/doctor.ts +4 -1
  6. package/src/commands/gh/branch-clean.ts +10 -4
  7. package/src/commands/gh/branch.ts +6 -3
  8. package/src/commands/gh/pr.ts +10 -3
  9. package/src/commands/graph-template.html +1214 -0
  10. package/src/commands/graph.test.ts +176 -0
  11. package/src/commands/graph.ts +970 -0
  12. package/src/commands/install.ts +8 -2
  13. package/src/commands/journal/daily-notes.ts +9 -5
  14. package/src/commands/journal/meeting.ts +12 -6
  15. package/src/commands/journal/note.ts +12 -6
  16. package/src/commands/ralph/plan/add.ts +75 -0
  17. package/src/commands/ralph/plan/done.ts +82 -0
  18. package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
  19. package/src/commands/ralph/{task → plan}/list.ts +28 -39
  20. package/src/commands/ralph/plan/remove.ts +71 -0
  21. package/src/commands/ralph/run.test.ts +521 -0
  22. package/src/commands/ralph/run.ts +126 -189
  23. package/src/commands/ralph/show.ts +88 -0
  24. package/src/config/settings.ts +8 -27
  25. package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
  26. package/src/lib/ralph/formatter.ts +238 -0
  27. package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
  28. package/src/utils/date-utils.test.ts +2 -1
  29. package/src/utils/date-utils.ts +2 -2
  30. package/LICENSE.md +0 -20
  31. package/src/commands/index.ts +0 -55
  32. package/src/commands/observe/graph.test.ts +0 -89
  33. package/src/commands/observe/graph.ts +0 -1640
  34. package/src/commands/observe/report.ts +0 -166
  35. package/src/commands/observe/session.ts +0 -385
  36. package/src/commands/observe/setup.ts +0 -180
  37. package/src/commands/observe/status.ts +0 -146
  38. package/src/commands/ralph/lib/formatter.ts +0 -298
  39. package/src/commands/ralph/lib/marker.ts +0 -108
  40. package/src/commands/ralph/marker/create.ts +0 -23
  41. package/src/commands/ralph/plan.ts +0 -73
  42. package/src/commands/ralph/progress.ts +0 -44
  43. package/src/commands/ralph/ralph.test.ts +0 -673
  44. package/src/commands/ralph/task/add.ts +0 -105
  45. package/src/commands/ralph/task/done.ts +0 -73
  46. package/src/commands/ralph/task/remove.ts +0 -62
  47. package/src/config/context.ts +0 -7
  48. package/src/constants.ts +0 -3
  49. package/src/utils/anthropic/types.ts +0 -158
  50. package/src/utils/exec.ts +0 -8
  51. package/src/utils/git/git.ts +0 -25
  52. /package/src/{commands → lib}/journal/utils.ts +0 -0
  53. /package/src/{commands/ralph/lib → lib/ralph}/index.ts +0 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Tests for graph command --days filtering and bar chart data
3
+ */
4
+ import { describe, it, expect } from "vitest";
5
+ import { calculateCutoffMs, filterByDays, analyzeSession } from "./graph.js";
6
+
7
+ describe("graph --days filtering", () => {
8
+ describe("calculateCutoffMs", () => {
9
+ it("returns 0 when days <= 0", () => {
10
+ expect(calculateCutoffMs(0)).toBe(0);
11
+ expect(calculateCutoffMs(-1)).toBe(0);
12
+ });
13
+
14
+ it("returns cutoff timestamp for positive days", () => {
15
+ const now = Date.now();
16
+ const cutoff = calculateCutoffMs(7);
17
+ // Should be roughly 7 days ago (within 100ms tolerance for test execution time)
18
+ const expected = now - 7 * 24 * 60 * 60 * 1000;
19
+ expect(Math.abs(cutoff - expected)).toBeLessThan(100);
20
+ });
21
+ });
22
+
23
+ describe("filterByDays", () => {
24
+ it("filters sessions older than N days when days > 0", () => {
25
+ const now = Date.now();
26
+ const sessions = [
27
+ { mtime: now - 1 * 24 * 60 * 60 * 1000 }, // 1 day ago - included
28
+ { mtime: now - 2 * 24 * 60 * 60 * 1000 }, // 2 days ago - included
29
+ { mtime: now - 5 * 24 * 60 * 60 * 1000 }, // 5 days ago - excluded
30
+ { mtime: now - 10 * 24 * 60 * 60 * 1000 }, // 10 days ago - excluded
31
+ ];
32
+
33
+ const filtered = filterByDays(sessions, 3);
34
+ expect(filtered).toHaveLength(2);
35
+ });
36
+
37
+ it("returns all sessions when days=0", () => {
38
+ const now = Date.now();
39
+ const sessions = [
40
+ { mtime: now - 1 * 24 * 60 * 60 * 1000 },
41
+ { mtime: now - 100 * 24 * 60 * 60 * 1000 },
42
+ { mtime: now - 365 * 24 * 60 * 60 * 1000 },
43
+ ];
44
+
45
+ const filtered = filterByDays(sessions, 0);
46
+ expect(filtered).toHaveLength(3);
47
+ });
48
+
49
+ it("default 7 days filters correctly", () => {
50
+ const now = Date.now();
51
+ const sessions = [
52
+ { mtime: now - 1 * 24 * 60 * 60 * 1000 }, // 1 day ago - included
53
+ { mtime: now - 6 * 24 * 60 * 60 * 1000 }, // 6 days ago - included
54
+ { mtime: now - 8 * 24 * 60 * 60 * 1000 }, // 8 days ago - excluded
55
+ { mtime: now - 30 * 24 * 60 * 60 * 1000 }, // 30 days ago - excluded
56
+ ];
57
+
58
+ const filtered = filterByDays(sessions, 7);
59
+ expect(filtered).toHaveLength(2);
60
+ // Verify the right sessions were kept
61
+ expect(filtered[0].mtime).toBeGreaterThan(now - 7 * 24 * 60 * 60 * 1000);
62
+ expect(filtered[1].mtime).toBeGreaterThan(now - 7 * 24 * 60 * 60 * 1000);
63
+ });
64
+
65
+ it("--days 1 filters to today only", () => {
66
+ const now = Date.now();
67
+ const sessions = [
68
+ { mtime: now - 12 * 60 * 60 * 1000 }, // 12 hours ago - included
69
+ { mtime: now - 25 * 60 * 60 * 1000 }, // 25 hours ago - excluded
70
+ ];
71
+
72
+ const filtered = filterByDays(sessions, 1);
73
+ expect(filtered).toHaveLength(1);
74
+ });
75
+
76
+ it("preserves additional properties on items", () => {
77
+ const now = Date.now();
78
+ const sessions = [
79
+ { mtime: now, sessionId: "abc", tokens: 100 },
80
+ { mtime: now - 10 * 24 * 60 * 60 * 1000, sessionId: "old", tokens: 50 },
81
+ ];
82
+
83
+ const filtered = filterByDays(sessions, 7);
84
+ expect(filtered).toHaveLength(1);
85
+ expect(filtered[0].sessionId).toBe("abc");
86
+ expect(filtered[0].tokens).toBe(100);
87
+ });
88
+ });
89
+ });
90
+
91
+ describe("analyzeSession (bar chart token aggregation)", () => {
92
+ // Helper to create JournalEntry with message.usage structure
93
+ function makeEntry(model: string, inputTokens: number, outputTokens: number) {
94
+ return {
95
+ type: "assistant",
96
+ sessionId: "test-session",
97
+ timestamp: new Date().toISOString(),
98
+ message: {
99
+ role: "assistant" as const,
100
+ model,
101
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens },
102
+ },
103
+ };
104
+ }
105
+
106
+ it("returns zeros for empty entries array", () => {
107
+ const result = analyzeSession([]);
108
+ expect(result.opusTokens).toBe(0);
109
+ expect(result.sonnetTokens).toBe(0);
110
+ expect(result.haikuTokens).toBe(0);
111
+ expect(result.inputTokens).toBe(0);
112
+ expect(result.outputTokens).toBe(0);
113
+ });
114
+
115
+ it("aggregates Opus tokens correctly", () => {
116
+ const entries = [
117
+ makeEntry("claude-opus-4-20250514", 100, 50),
118
+ makeEntry("claude-opus-4-20250514", 200, 100),
119
+ ];
120
+ const result = analyzeSession(entries);
121
+ expect(result.opusTokens).toBe(450); // 100+50+200+100
122
+ expect(result.sonnetTokens).toBe(0);
123
+ expect(result.haikuTokens).toBe(0);
124
+ });
125
+
126
+ it("aggregates Sonnet tokens correctly", () => {
127
+ const entries = [makeEntry("claude-sonnet-4-20250514", 500, 200)];
128
+ const result = analyzeSession(entries);
129
+ expect(result.sonnetTokens).toBe(700);
130
+ expect(result.opusTokens).toBe(0);
131
+ expect(result.haikuTokens).toBe(0);
132
+ });
133
+
134
+ it("aggregates Haiku tokens correctly", () => {
135
+ const entries = [makeEntry("claude-3-5-haiku-20241022", 1000, 500)];
136
+ const result = analyzeSession(entries);
137
+ expect(result.haikuTokens).toBe(1500);
138
+ expect(result.opusTokens).toBe(0);
139
+ expect(result.sonnetTokens).toBe(0);
140
+ });
141
+
142
+ it("aggregates mixed model tokens correctly", () => {
143
+ const entries = [
144
+ makeEntry("claude-opus-4-20250514", 100, 50),
145
+ makeEntry("claude-sonnet-4-20250514", 200, 100),
146
+ makeEntry("claude-3-5-haiku-20241022", 300, 150),
147
+ ];
148
+ const result = analyzeSession(entries);
149
+ expect(result.opusTokens).toBe(150);
150
+ expect(result.sonnetTokens).toBe(300);
151
+ expect(result.haikuTokens).toBe(450);
152
+ expect(result.inputTokens).toBe(600);
153
+ expect(result.outputTokens).toBe(300);
154
+ });
155
+
156
+ it("ignores entries without message.usage", () => {
157
+ const entries = [
158
+ { type: "user", sessionId: "x", timestamp: "", message: { role: "user" as const } },
159
+ makeEntry("claude-opus-4-20250514", 100, 50),
160
+ { type: "tool_result", sessionId: "x", timestamp: "" },
161
+ ];
162
+ const result = analyzeSession(entries);
163
+ expect(result.opusTokens).toBe(150);
164
+ });
165
+
166
+ it("calculates modelEfficiency as opus ratio", () => {
167
+ const entries = [
168
+ makeEntry("claude-opus-4-20250514", 100, 100),
169
+ makeEntry("claude-sonnet-4-20250514", 200, 200),
170
+ ];
171
+ const result = analyzeSession(entries);
172
+ // Opus: 200, Sonnet: 400, Total: 600
173
+ // modelEfficiency = 200/600 = 0.333...
174
+ expect(result.modelEfficiency).toBeCloseTo(0.333, 2);
175
+ });
176
+ });