@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.
- package/README.md +67 -109
- package/package.json +51 -41
- package/src/commands/base.ts +3 -18
- package/src/commands/config.ts +9 -8
- package/src/commands/doctor.ts +4 -1
- package/src/commands/gh/branch-clean.ts +10 -4
- package/src/commands/gh/branch.ts +6 -3
- package/src/commands/gh/pr.ts +10 -3
- package/src/commands/graph-template.html +1214 -0
- package/src/commands/graph.test.ts +176 -0
- package/src/commands/graph.ts +970 -0
- package/src/commands/install.ts +8 -2
- package/src/commands/journal/daily-notes.ts +9 -5
- package/src/commands/journal/meeting.ts +12 -6
- package/src/commands/journal/note.ts +12 -6
- package/src/commands/ralph/plan/add.ts +75 -0
- package/src/commands/ralph/plan/done.ts +82 -0
- package/src/commands/ralph/{task → plan}/list.test.ts +5 -5
- package/src/commands/ralph/{task → plan}/list.ts +28 -39
- package/src/commands/ralph/plan/remove.ts +71 -0
- package/src/commands/ralph/run.test.ts +521 -0
- package/src/commands/ralph/run.ts +126 -189
- package/src/commands/ralph/show.ts +88 -0
- package/src/config/settings.ts +8 -27
- package/src/{commands/ralph/lib → lib/ralph}/execution.ts +4 -14
- package/src/lib/ralph/formatter.ts +238 -0
- package/src/{commands/ralph/lib → lib/ralph}/state.ts +17 -42
- package/src/utils/date-utils.test.ts +2 -1
- package/src/utils/date-utils.ts +2 -2
- package/LICENSE.md +0 -20
- package/src/commands/index.ts +0 -55
- package/src/commands/observe/graph.test.ts +0 -89
- package/src/commands/observe/graph.ts +0 -1640
- package/src/commands/observe/report.ts +0 -166
- package/src/commands/observe/session.ts +0 -385
- package/src/commands/observe/setup.ts +0 -180
- package/src/commands/observe/status.ts +0 -146
- package/src/commands/ralph/lib/formatter.ts +0 -298
- package/src/commands/ralph/lib/marker.ts +0 -108
- package/src/commands/ralph/marker/create.ts +0 -23
- package/src/commands/ralph/plan.ts +0 -73
- package/src/commands/ralph/progress.ts +0 -44
- package/src/commands/ralph/ralph.test.ts +0 -673
- package/src/commands/ralph/task/add.ts +0 -105
- package/src/commands/ralph/task/done.ts +0 -73
- package/src/commands/ralph/task/remove.ts +0 -62
- package/src/config/context.ts +0 -7
- package/src/constants.ts +0 -3
- package/src/utils/anthropic/types.ts +0 -158
- package/src/utils/exec.ts +0 -8
- package/src/utils/git/git.ts +0 -25
- /package/src/{commands → lib}/journal/utils.ts +0 -0
- /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
|
-
//
|
|
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 "
|
|
6
|
-
import { RalphSettingsSchema } from "
|
|
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
|
-
//
|
|
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" | "
|
|
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
|
|
66
|
+
const PlanStatusSchema = z.enum(["ready", "done", "blocked", "cancelled"]);
|
|
70
67
|
|
|
71
|
-
const
|
|
68
|
+
const RalphPlanSchema = z.object({
|
|
72
69
|
id: z.number(),
|
|
73
70
|
description: z.string(),
|
|
74
|
-
status:
|
|
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
|
-
|
|
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
|
|
108
|
-
export type
|
|
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(
|
|
106
|
+
export function createInitialState(): RalphState {
|
|
116
107
|
return {
|
|
117
108
|
version: 1,
|
|
118
|
-
|
|
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
|
|
171
|
-
state:
|
|
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
|
|
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.
|
|
190
|
-
return
|
|
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
|
-
|
|
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
|
|
package/src/utils/date-utils.ts
CHANGED
|
@@ -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.
|
|
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
|
package/src/commands/index.ts
DELETED
|
@@ -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
|
-
});
|