@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
@@ -1,4 +1,7 @@
1
1
  import type { WriteStream } from "node:fs";
2
+ // NOTE: We use spawn instead of tinyexec for runIteration because we need
3
+ // real-time streaming of stdout/stderr. tinyexec waits for command completion
4
+ // before returning output, which doesn't work for long-running claude sessions.
2
5
  import { spawn } from "node:child_process";
3
6
  import pc from "picocolors";
4
7
  import { x } from "tinyexec";
@@ -10,11 +13,6 @@ import { CLAUDE_DEFAULT_ARGS } from "./state.js";
10
13
 
11
14
  interface StreamEvent {
12
15
  type: string;
13
- event?: {
14
- type: string;
15
- delta?: { text?: string };
16
- };
17
- // New format: assistant message
18
16
  message?: {
19
17
  content?: Array<{ type: string; text?: string }>;
20
18
  usage?: {
@@ -136,15 +134,7 @@ function parseStreamLine(line: string): ParsedLine {
136
134
  return { text: null, tool: { name, summary } };
137
135
  }
138
136
 
139
- // Extract text from streaming deltas (legacy format)
140
- if (data.type === "stream_event" && data.event?.type === "content_block_delta") {
141
- return { text: data.event.delta?.text || null };
142
- }
143
- // Add newline after content block ends (legacy format)
144
- if (data.type === "stream_event" && data.event?.type === "content_block_stop") {
145
- return { text: "\n" };
146
- }
147
- // NEW FORMAT: Handle assistant messages with content array
137
+ // Handle assistant messages with content array
148
138
  if (data.type === "assistant" && data.message) {
149
139
  // Check for tool_use in content blocks
150
140
  const toolBlocks = data.message.content?.filter((c) => c.type === "tool_use") || [];
@@ -0,0 +1,238 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import type { RalphPlan, PlanStatus, RalphState } from "./state.js";
3
+
4
+ // ============================================================================
5
+ // Clipboard Utility
6
+ // ============================================================================
7
+
8
+ export function copyToClipboard(text: string): boolean {
9
+ try {
10
+ const platform = process.platform;
11
+ if (platform === "darwin") {
12
+ execFileSync("pbcopy", [], { input: text });
13
+ } else if (platform === "linux") {
14
+ // Try xclip first, then xsel
15
+ try {
16
+ execFileSync("xclip", ["-selection", "clipboard"], { input: text });
17
+ } catch {
18
+ execFileSync("xsel", ["--clipboard", "--input"], { input: text });
19
+ }
20
+ } else if (platform === "win32") {
21
+ execFileSync("clip", [], { input: text });
22
+ } else {
23
+ return false;
24
+ }
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Format plans as markdown with checkboxes and status badges.
33
+ */
34
+ export function formatPlansAsMarkdown(plans: RalphPlan[]): string {
35
+ if (plans.length === 0) {
36
+ return "# Plans\n\nNo plans.\n";
37
+ }
38
+
39
+ const statusBadge = (status: PlanStatus): string => {
40
+ switch (status) {
41
+ case "done":
42
+ return "`✓ done`";
43
+ case "ready":
44
+ return "`○ ready`";
45
+ case "blocked":
46
+ return "`⏸ blocked`";
47
+ case "cancelled":
48
+ return "`✗ cancelled`";
49
+ }
50
+ };
51
+
52
+ const ready = plans.filter((p) => p.status === "ready");
53
+ const done = plans.filter((p) => p.status === "done");
54
+
55
+ const lines: string[] = ["# Plans", ""];
56
+ lines.push(
57
+ `**Total:** ${plans.length} | **Done:** ${done.length} | **Ready:** ${ready.length}`,
58
+ "",
59
+ );
60
+
61
+ if (ready.length > 0) {
62
+ lines.push("## Ready", "");
63
+ for (const p of ready) {
64
+ lines.push(`- [ ] **#${p.id}** ${p.description} ${statusBadge(p.status)}`);
65
+ }
66
+ lines.push("");
67
+ }
68
+
69
+ if (done.length > 0) {
70
+ lines.push("## Done", "");
71
+ for (const p of done) {
72
+ lines.push(`- [x] **#${p.id}** ${p.description} ${statusBadge(p.status)}`);
73
+ }
74
+ lines.push("");
75
+ }
76
+
77
+ return lines.join("\n");
78
+ }
79
+
80
+ /**
81
+ * Format plans with markdown and optional mermaid graph.
82
+ */
83
+ export function formatPlanAsMarkdown(plans: RalphPlan[], state: RalphState): string {
84
+ const lines: string[] = ["# Ralph Plan", ""];
85
+
86
+ // Summary section
87
+ const ready = plans.filter((p) => p.status === "ready").length;
88
+ const done = plans.filter((p) => p.status === "done").length;
89
+
90
+ lines.push("## Summary", "");
91
+ lines.push(`- **Status:** ${state.status}`);
92
+ lines.push(`- **Total:** ${plans.length}`);
93
+ lines.push(`- **Done:** ${done} | **Ready:** ${ready}`);
94
+ lines.push("");
95
+
96
+ // Plans section with checkboxes
97
+ lines.push("## Plans", "");
98
+ for (const p of plans) {
99
+ const checkbox = p.status === "done" ? "[x]" : "[ ]";
100
+ const status = p.status === "done" ? "`done`" : "`ready`";
101
+ lines.push(`- ${checkbox} **#${p.id}** ${p.description} ${status}`);
102
+ }
103
+ lines.push("");
104
+
105
+ // Mermaid graph section
106
+ lines.push("## Progress Graph", "");
107
+ lines.push("```mermaid");
108
+ lines.push("graph LR");
109
+ lines.push(` subgraph Progress["Plans: ${done}/${plans.length} done"]`);
110
+
111
+ for (const p of plans) {
112
+ const shortDesc =
113
+ p.description.length > 30 ? p.description.slice(0, 27) + "..." : p.description;
114
+ // Escape quotes in descriptions
115
+ const safeDesc = shortDesc.replace(/"/g, "'");
116
+ const nodeId = `P${p.id}`;
117
+
118
+ if (p.status === "done") {
119
+ lines.push(` ${nodeId}["#${p.id}: ${safeDesc}"]:::done`);
120
+ } else {
121
+ lines.push(` ${nodeId}["#${p.id}: ${safeDesc}"]:::ready`);
122
+ }
123
+ }
124
+
125
+ lines.push(" end");
126
+ lines.push(" classDef done fill:#22c55e,color:#fff");
127
+ lines.push(" classDef ready fill:#94a3b8,color:#000");
128
+ lines.push("```");
129
+ lines.push("");
130
+
131
+ return lines.join("\n");
132
+ }
133
+
134
+ /**
135
+ * Format plans as JSON for programmatic consumption.
136
+ */
137
+ export function formatPlanAsJson(plans: RalphPlan[], state: RalphState): string {
138
+ return JSON.stringify(
139
+ {
140
+ status: state.status,
141
+ summary: {
142
+ total: plans.length,
143
+ done: plans.filter((p) => p.status === "done").length,
144
+ ready: plans.filter((p) => p.status === "ready").length,
145
+ },
146
+ plans: plans.map((p) => ({
147
+ id: p.id,
148
+ description: p.description,
149
+ status: p.status,
150
+ addedAt: p.addedAt,
151
+ completedAt: p.completedAt,
152
+ })),
153
+ },
154
+ null,
155
+ 2,
156
+ );
157
+ }
158
+
159
+ // ============================================================================
160
+ // Duration Formatting
161
+ // ============================================================================
162
+
163
+ export function formatDuration(ms: number): string {
164
+ const seconds = Math.floor(ms / 1000);
165
+ const minutes = Math.floor(seconds / 60);
166
+ const hours = Math.floor(minutes / 60);
167
+
168
+ if (hours > 0) {
169
+ const remainingMins = minutes % 60;
170
+ return `${hours}h ${remainingMins}m`;
171
+ }
172
+ if (minutes > 0) {
173
+ const remainingSecs = seconds % 60;
174
+ return `${minutes}m ${remainingSecs}s`;
175
+ }
176
+ return `${seconds}s`;
177
+ }
178
+
179
+ // ============================================================================
180
+ // Output Summary
181
+ // ============================================================================
182
+
183
+ export function extractOutputSummary(output: string, maxLength: number = 2000): string {
184
+ const lines = output
185
+ .split("\n")
186
+ .filter((l) => l.trim())
187
+ .slice(-5);
188
+ let summary = lines.join(" ").trim();
189
+
190
+ if (summary.length > maxLength) {
191
+ summary = summary.substring(0, maxLength) + "...";
192
+ }
193
+
194
+ return summary || "(no output)";
195
+ }
196
+
197
+ // ============================================================================
198
+ // Prompt Building
199
+ // ============================================================================
200
+
201
+ export interface BuildPromptOptions {
202
+ completionMarker: string;
203
+ plan: RalphPlan;
204
+ skipCommit?: boolean;
205
+ }
206
+
207
+ export function buildIterationPrompt({
208
+ completionMarker,
209
+ plan,
210
+ skipCommit = false,
211
+ }: BuildPromptOptions): string {
212
+ let step = 1;
213
+
214
+ const prompt = `
215
+ <plan>
216
+ #${plan.id}: ${plan.description}
217
+ </plan>
218
+
219
+ <instructions>
220
+ ${step++}. Work on the plan above.
221
+ ${step++}. Run type checks and tests.
222
+ ${step++}. Mark done: \`tt ralph plan done ${plan.id}\`
223
+ ${skipCommit ? "" : `${step++}. Make a git commit.`}
224
+
225
+ **Before ending:** Run \`tt ralph plan list\` to check remaining plans.
226
+ **ONLY if ALL PLANS are done** then Output: <promise>${completionMarker}</promise>
227
+ </instructions>
228
+ `;
229
+ return prompt.trim();
230
+ }
231
+
232
+ // ============================================================================
233
+ // Marker Detection
234
+ // ============================================================================
235
+
236
+ export function detectCompletionMarker(output: string, marker: string): boolean {
237
+ return output.includes(marker);
238
+ }
@@ -2,8 +2,8 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import pc from "picocolors";
4
4
  import { z } from "zod";
5
- import type { RalphSettings } from "../../../config/settings";
6
- import { RalphSettingsSchema } from "../../../config/settings";
5
+ import type { RalphSettings } from "../../config/settings.js";
6
+ import { RalphSettingsSchema } from "../../config/settings.js";
7
7
 
8
8
  // ============================================================================
9
9
  // Constants
@@ -16,7 +16,6 @@ export const DEFAULT_COMPLETION_MARKER = "RALPH_DONE";
16
16
  // File names within stateDir
17
17
  const STATE_FILE_NAME = "ralph-state.local.json";
18
18
  const LOG_FILE_NAME = "ralph-log.local.md";
19
- const PROGRESS_FILE_NAME = "ralph-progress.local.md";
20
19
  const HISTORY_FILE_NAME = "ralph-history.local.log";
21
20
 
22
21
  // ============================================================================
@@ -28,15 +27,13 @@ export function getRalphPaths(settings?: RalphSettings) {
28
27
  return {
29
28
  stateFile: path.join(stateDir, STATE_FILE_NAME),
30
29
  logFile: path.join(stateDir, LOG_FILE_NAME),
31
- progressFile: path.join(stateDir, PROGRESS_FILE_NAME),
32
30
  historyFile: path.join(stateDir, HISTORY_FILE_NAME),
33
31
  };
34
32
  }
35
33
 
36
- // Legacy defaults for backwards compatibility (used in flag descriptions)
34
+ // Defaults used in flag descriptions
37
35
  export const DEFAULT_STATE_FILE = `${DEFAULT_STATE_DIR}/${STATE_FILE_NAME}`;
38
36
  export const DEFAULT_LOG_FILE = `${DEFAULT_STATE_DIR}/${LOG_FILE_NAME}`;
39
- export const DEFAULT_PROGRESS_FILE = `${DEFAULT_STATE_DIR}/${PROGRESS_FILE_NAME}`;
40
37
  export const DEFAULT_HISTORY_FILE = `${DEFAULT_STATE_DIR}/${HISTORY_FILE_NAME}`;
41
38
 
42
39
  /**
@@ -44,7 +41,7 @@ export const DEFAULT_HISTORY_FILE = `${DEFAULT_STATE_DIR}/${HISTORY_FILE_NAME}`;
44
41
  */
45
42
  export function resolveRalphPath(
46
43
  flagValue: string | undefined,
47
- pathType: "stateFile" | "logFile" | "progressFile" | "historyFile",
44
+ pathType: "stateFile" | "logFile" | "historyFile",
48
45
  settings?: RalphSettings,
49
46
  ): string {
50
47
  if (flagValue !== undefined) {
@@ -66,27 +63,21 @@ export const CLAUDE_DEFAULT_ARGS = [
66
63
  // State Validation Schemas
67
64
  // ============================================================================
68
65
 
69
- const TaskStatusSchema = z.enum(["ready", "done", "blocked", "cancelled"]);
66
+ const PlanStatusSchema = z.enum(["ready", "done", "blocked", "cancelled"]);
70
67
 
71
- const RalphTaskSchema = z.object({
68
+ const RalphPlanSchema = z.object({
72
69
  id: z.number(),
73
70
  description: z.string(),
74
- status: TaskStatusSchema,
71
+ status: PlanStatusSchema,
75
72
  addedAt: z.string(),
76
73
  completedAt: z.string().optional(),
77
- sessionId: z.string().optional(),
78
- marker: z.string().optional(),
79
- label: z.string().optional(),
80
74
  });
81
75
 
82
76
  const RalphStateSchema = z.object({
83
77
  version: z.number(),
84
- tasks: z.array(RalphTaskSchema),
78
+ plans: z.array(RalphPlanSchema),
85
79
  startedAt: z.string(),
86
- iteration: z.number(),
87
- maxIterations: z.number(),
88
80
  status: z.enum(["running", "completed", "max_iterations_reached", "error"]),
89
- sessionId: z.string().optional(),
90
81
  });
91
82
 
92
83
  // ============================================================================
@@ -104,21 +95,19 @@ export interface IterationHistory {
104
95
  contextUsedPercent?: number;
105
96
  }
106
97
 
107
- export type TaskStatus = z.infer<typeof TaskStatusSchema>;
108
- export type RalphTask = z.infer<typeof RalphTaskSchema>;
98
+ export type PlanStatus = z.infer<typeof PlanStatusSchema>;
99
+ export type RalphPlan = z.infer<typeof RalphPlanSchema>;
109
100
  export type RalphState = z.infer<typeof RalphStateSchema>;
110
101
 
111
102
  // ============================================================================
112
103
  // State Management
113
104
  // ============================================================================
114
105
 
115
- export function createInitialState(maxIterations: number): RalphState {
106
+ export function createInitialState(): RalphState {
116
107
  return {
117
108
  version: 1,
118
- tasks: [],
109
+ plans: [],
119
110
  startedAt: new Date().toISOString(),
120
- iteration: 0,
121
- maxIterations,
122
111
  status: "running",
123
112
  };
124
113
  }
@@ -149,11 +138,6 @@ export function loadState(stateFile: string): RalphState | null {
149
138
  const content = fs.readFileSync(stateFile, "utf-8");
150
139
  const parsed = JSON.parse(content);
151
140
 
152
- // Ensure tasks array exists for backwards compatibility
153
- if (!parsed.tasks) {
154
- parsed.tasks = [];
155
- }
156
-
157
141
  const result = RalphStateSchema.safeParse(parsed);
158
142
  if (!result.success) {
159
143
  const errors = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
@@ -167,25 +151,16 @@ export function loadState(stateFile: string): RalphState | null {
167
151
  }
168
152
  }
169
153
 
170
- export function addTaskToState(
171
- state: RalphState,
172
- description: string,
173
- sessionId?: string,
174
- marker?: string,
175
- label?: string,
176
- ): RalphTask {
177
- const nextId = state.tasks.length > 0 ? Math.max(...state.tasks.map((t) => t.id)) + 1 : 1;
154
+ export function addPlanToState(state: RalphState, description: string): RalphPlan {
155
+ const nextId = state.plans.length > 0 ? Math.max(...state.plans.map((p) => p.id)) + 1 : 1;
178
156
 
179
- const newTask: RalphTask = {
157
+ const newPlan: RalphPlan = {
180
158
  id: nextId,
181
159
  description,
182
160
  status: "ready",
183
161
  addedAt: new Date().toISOString(),
184
- ...(sessionId && { sessionId }),
185
- ...(marker && { marker }),
186
- ...(label && { label }),
187
162
  };
188
163
 
189
- state.tasks.push(newTask);
190
- return newTask;
164
+ state.plans.push(newPlan);
165
+ return newPlan;
191
166
  }
@@ -36,7 +36,8 @@ describe("date utilities", () => {
36
36
  });
37
37
 
38
38
  it("should format date correctly", () => {
39
- const date = new Date("2025-07-07");
39
+ // Use local date constructor (year, month-1, day), not ISO string which is UTC
40
+ const date = new Date(2025, 6, 7); // July 7, 2025 in local time
40
41
  expect(formatDate(date)).toBe("2025-07-07");
41
42
  });
42
43
 
@@ -38,10 +38,10 @@ export function getWeekInfo(mondayDate: Date): weekInfo {
38
38
  }
39
39
 
40
40
  /**
41
- * Format date as YYYY-MM-DD
41
+ * Format date as YYYY-MM-DD in local timezone
42
42
  */
43
43
  export function formatDate(date: Date): string {
44
- return date.toISOString().split("T")[0];
44
+ return date.toLocaleDateString("en-CA");
45
45
  }
46
46
 
47
47
  /**
package/LICENSE.md DELETED
@@ -1,20 +0,0 @@
1
- # Proprietary License
2
-
3
- Copyright (c) 2025 Chris Towles. All rights reserved.
4
-
5
- This software and associated documentation files (the "Software") are proprietary
6
- and confidential. Unauthorized copying, modification, distribution, or use of
7
- this Software, via any medium, is strictly prohibited.
8
-
9
- The Software is provided for the sole use of authorized individuals or entities
10
- who have been granted explicit written permission by the copyright holder.
11
-
12
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
18
- SOFTWARE.
19
-
20
- For licensing inquiries, contact: Chris.Towles.Dev@gmail.com
@@ -1,55 +0,0 @@
1
- // Explicit command exports for Bun compiled binaries
2
- // oclif's pattern-based discovery doesn't work with bundled executables
3
-
4
- import GhBranchClean from "./gh/branch-clean.js";
5
- import Config from "./config.js";
6
- import Doctor from "./doctor.js";
7
- import GhBranch from "./gh/branch.js";
8
- import GhPr from "./gh/pr.js";
9
- import Install from "./install.js";
10
- import RalphRun from "./ralph/run.js";
11
- import RalphPlan from "./ralph/plan.js";
12
- import RalphProgress from "./ralph/progress.js";
13
- import RalphMarkerCreate from "./ralph/marker/create.js";
14
- import RalphTaskAdd from "./ralph/task/add.js";
15
- import RalphTaskDone from "./ralph/task/done.js";
16
- import RalphTaskList from "./ralph/task/list.js";
17
- import RalphTaskRemove from "./ralph/task/remove.js";
18
- import JournalDailyNotes from "./journal/daily-notes.js";
19
- import JournalMeeting from "./journal/meeting.js";
20
- import JournalNote from "./journal/note.js";
21
- import ObserveSetup from "./observe/setup.js";
22
- import ObserveStatus from "./observe/status.js";
23
- import ObserveReport from "./observe/report.js";
24
- import ObserveGraph from "./observe/graph.js";
25
- import ObserveSession from "./observe/session.js";
26
-
27
- export default {
28
- config: Config,
29
- doctor: Doctor,
30
- "gh:branch": GhBranch,
31
- "gh:branch-clean": GhBranchClean,
32
- "gh:pr": GhPr,
33
- install: Install,
34
- "ralph:run": RalphRun,
35
- "ralph:plan": RalphPlan,
36
- "ralph:progress": RalphProgress,
37
- "ralph:marker:create": RalphMarkerCreate,
38
- "ralph:task:add": RalphTaskAdd,
39
- "ralph:task:done": RalphTaskDone,
40
- "ralph:task:list": RalphTaskList,
41
- "ralph:task:remove": RalphTaskRemove,
42
- "journal:daily-notes": JournalDailyNotes,
43
- "journal:meeting": JournalMeeting,
44
- "journal:note": JournalNote,
45
- "observe:setup": ObserveSetup,
46
- "observe:status": ObserveStatus,
47
- "observe:report": ObserveReport,
48
- "observe:graph": ObserveGraph,
49
- "observe:session": ObserveSession,
50
- // Aliases
51
- graph: ObserveGraph,
52
- today: JournalDailyNotes,
53
- pr: GhPr,
54
- run: RalphRun,
55
- };
@@ -1,89 +0,0 @@
1
- /**
2
- * Tests for observe graph command --days filtering
3
- */
4
- import { describe, it, expect } from "vitest";
5
- import { calculateCutoffMs, filterByDays } from "./graph.js";
6
-
7
- describe("observe 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
- });