@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,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import { Flags } from "@oclif/core";
3
- import pc from "picocolors";
3
+ import consola from "consola";
4
+ import { colors } from "consola/utils";
4
5
  import { BaseCommand } from "../base.js";
5
6
  import {
6
7
  DEFAULT_STATE_FILE,
@@ -13,33 +14,22 @@ import {
13
14
  appendHistory,
14
15
  resolveRalphPath,
15
16
  getRalphPaths,
16
- } from "./lib/state.js";
17
+ } from "../../lib/ralph/state.js";
17
18
  import {
18
19
  buildIterationPrompt,
19
20
  formatDuration,
20
21
  extractOutputSummary,
21
22
  detectCompletionMarker,
22
- formatTasksForPrompt,
23
- } from "./lib/formatter.js";
24
- import { checkClaudeCli, runIteration } from "./lib/execution.js";
25
-
26
- /**
27
- * Read last N iterations from progress file. Only returns iteration entries,
28
- * excluding headers/status sections that could confuse the model.
29
- */
30
- function readLastIterations(filePath: string, count: number): string {
31
- if (!fs.existsSync(filePath)) return "";
32
- try {
33
- const content = fs.readFileSync(filePath, "utf-8");
34
- // Split by iteration headers, keeping the delimiter
35
- const parts = content.split(/(?=### Iteration)/g);
36
- // Skip first part (header/status content) - only want iteration entries
37
- const iterations = parts.filter((p) => p.startsWith("### Iteration"));
38
- if (iterations.length === 0) return "";
39
- return iterations.slice(-count).join("\n").trim();
40
- } catch {
41
- return "";
23
+ } from "../../lib/ralph/formatter.js";
24
+ import { checkClaudeCli, runIteration } from "../../lib/ralph/execution.js";
25
+ import type { RalphPlan } from "../../lib/ralph/state.js";
26
+
27
+ /** Get the plan to work on: focused plan or first incomplete */
28
+ function getCurrentPlan(plans: RalphPlan[], focusedPlanId: number | null): RalphPlan | undefined {
29
+ if (focusedPlanId !== null) {
30
+ return plans.find((p) => p.id === focusedPlanId);
42
31
  }
32
+ return plans.find((p) => p.status !== "done");
43
33
  }
44
34
 
45
35
  /**
@@ -49,14 +39,23 @@ export default class Run extends BaseCommand {
49
39
  static override description = "Start the autonomous ralph loop";
50
40
 
51
41
  static override examples = [
52
- "<%= config.bin %> ralph run",
53
- "<%= config.bin %> ralph run --maxIterations 20",
54
- "<%= config.bin %> ralph run --taskId 5",
55
- "<%= config.bin %> ralph run --no-autoCommit",
56
- "<%= config.bin %> ralph run --noFork",
57
- "<%= config.bin %> ralph run --dryRun",
58
- "<%= config.bin %> ralph run --addIterations 5",
59
- "<%= config.bin %> ralph run --label backend",
42
+ { description: "Start the autonomous loop", command: "<%= config.bin %> <%= command.id %>" },
43
+ {
44
+ description: "Limit to 20 iterations",
45
+ command: "<%= config.bin %> <%= command.id %> --maxIterations 20",
46
+ },
47
+ {
48
+ description: "Focus on specific plan",
49
+ command: "<%= config.bin %> <%= command.id %> --planId 5",
50
+ },
51
+ {
52
+ description: "Run without auto-committing",
53
+ command: "<%= config.bin %> <%= command.id %> --no-autoCommit",
54
+ },
55
+ {
56
+ description: "Preview config without executing",
57
+ command: "<%= config.bin %> <%= command.id %> --dryRun",
58
+ },
60
59
  ];
61
60
 
62
61
  static override flags = {
@@ -65,29 +64,20 @@ export default class Run extends BaseCommand {
65
64
  char: "s",
66
65
  description: `State file path (default: ${DEFAULT_STATE_FILE})`,
67
66
  }),
68
- taskId: Flags.integer({
69
- char: "t",
70
- description: "Focus on specific task ID",
67
+ planId: Flags.integer({
68
+ char: "p",
69
+ description: "Focus on specific plan ID",
71
70
  }),
72
71
  maxIterations: Flags.integer({
73
72
  char: "m",
74
73
  description: "Max iterations",
75
74
  default: DEFAULT_MAX_ITERATIONS,
76
75
  }),
77
- addIterations: Flags.integer({
78
- char: "a",
79
- description:
80
- "Add iterations to current count (e.g., at 5/10, --addIterations 10 makes it 5/20)",
81
- }),
82
76
  autoCommit: Flags.boolean({
83
- description: "Auto-commit after each completed task",
77
+ description: "Auto-commit after each completed plan",
84
78
  default: true,
85
79
  allowNo: true,
86
80
  }),
87
- noFork: Flags.boolean({
88
- description: "Disable session forking (start fresh session)",
89
- default: false,
90
- }),
91
81
  dryRun: Flags.boolean({
92
82
  char: "n",
93
83
  description: "Show config without executing",
@@ -103,108 +93,79 @@ export default class Run extends BaseCommand {
103
93
  description: "Completion marker",
104
94
  default: DEFAULT_COMPLETION_MARKER,
105
95
  }),
106
- label: Flags.string({
107
- char: "l",
108
- description: "Only run tasks with this label",
109
- }),
110
96
  };
111
97
 
112
98
  async run(): Promise<void> {
113
99
  const { flags } = await this.parse(Run);
114
- const ralphSettings = this.settings.settingsFile.settings.ralphSettings;
100
+ const ralphSettings = this.settings.settings.ralphSettings;
115
101
  const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
116
102
  const logFile = resolveRalphPath(flags.logFile, "logFile", ralphSettings);
117
103
  const ralphPaths = getRalphPaths(ralphSettings);
118
104
 
119
- let maxIterations = flags.maxIterations;
120
- const addIterations = flags.addIterations;
105
+ const maxIterations = flags.maxIterations;
121
106
  const extraClaudeArgs = flags.claudeArgs?.split(" ").filter(Boolean) || [];
122
- const focusedTaskId = flags.taskId ?? null;
107
+ const focusedPlanId = flags.planId ?? null;
123
108
 
124
109
  // Load existing state
125
110
  let state = loadState(stateFile);
126
111
 
127
112
  if (!state) {
128
- this.error(`No state file found at: ${stateFile}\nUse: tt ralph task add "description"`);
113
+ this.error(`No state file found at: ${stateFile}\nUse: tt ralph plan add --file path.md`);
129
114
  }
130
115
 
131
- // Handle --addIterations: extend max from current iteration
132
- if (addIterations !== undefined) {
133
- maxIterations = state.iteration + addIterations;
134
- console.log(
135
- pc.cyan(
136
- `Adding ${addIterations} iterations: ${state.iteration}/${state.maxIterations} → ${state.iteration}/${maxIterations}`,
137
- ),
138
- );
139
- }
140
-
141
- // Filter by label if specified
142
- const labelFilter = flags.label;
143
- let remainingTasks = state.tasks.filter((t) => t.status !== "done");
144
- if (labelFilter) {
145
- remainingTasks = remainingTasks.filter((t) => t.label === labelFilter);
146
- }
147
- if (remainingTasks.length === 0) {
148
- const msg = labelFilter
149
- ? `All tasks with label '${labelFilter}' are done!`
150
- : "All tasks are done!";
151
- console.log(pc.green(`✅ ${msg}`));
116
+ const remainingPlans = state.plans.filter((t) => t.status !== "done");
117
+ if (remainingPlans.length === 0) {
118
+ consola.log(colors.green("✅ All plans are done!"));
152
119
  return;
153
120
  }
154
121
 
155
- // Validate focused task if specified
156
- if (focusedTaskId !== null) {
157
- const focusedTask = state.tasks.find((t) => t.id === focusedTaskId);
158
- if (!focusedTask) {
159
- this.error(`Task #${focusedTaskId} not found. Use: tt ralph task list`);
122
+ // Validate focused plan if specified
123
+ if (focusedPlanId !== null) {
124
+ const focusedPlan = state.plans.find((t) => t.id === focusedPlanId);
125
+ if (!focusedPlan) {
126
+ this.error(`Plan #${focusedPlanId} not found. Use: tt ralph plan list`);
160
127
  }
161
- if (focusedTask.status === "done") {
162
- console.log(pc.yellow(`Task #${focusedTaskId} is already done.`));
128
+ if (focusedPlan.status === "done") {
129
+ consola.log(colors.yellow(`Plan #${focusedPlanId} is already done.`));
163
130
  return;
164
131
  }
165
132
  }
166
133
 
134
+ // Get current plan to work on
135
+ const currentPlan = getCurrentPlan(state.plans, focusedPlanId);
136
+ if (!currentPlan) {
137
+ consola.log(colors.green("✅ All plans are done!"));
138
+ return;
139
+ }
140
+
167
141
  // Dry run mode
168
142
  if (flags.dryRun) {
169
- console.log(pc.bold("\n=== DRY RUN ===\n"));
170
- console.log(pc.cyan("Config:"));
171
- console.log(` Focus: ${focusedTaskId ? `Task #${focusedTaskId}` : "Ralph picks"}`);
172
- console.log(` Label filter: ${labelFilter || "(none)"}`);
173
- console.log(` Max iterations: ${maxIterations}`);
174
- console.log(` State file: ${stateFile}`);
175
- console.log(` Log file: ${logFile}`);
176
- console.log(` Completion marker: ${flags.completionMarker}`);
177
- console.log(` Auto-commit: ${flags.autoCommit}`);
178
- console.log(` Fork session: ${!flags.noFork}`);
179
- console.log(` Session ID: ${state.sessionId || "(none)"}`);
180
- console.log(` Claude args: ${[...CLAUDE_DEFAULT_ARGS, ...extraClaudeArgs].join(" ")}`);
181
- console.log(` Remaining tasks: ${remainingTasks.length}`);
182
-
183
- console.log(pc.cyan("\nTasks:"));
184
- for (const t of state.tasks) {
185
- const icon = t.status === "done" ? "✓" : "○";
186
- const focus = focusedTaskId === t.id ? pc.cyan(" ← FOCUS") : "";
187
- console.log(` ${icon} ${t.id}. ${t.description} (${t.status})${focus}`);
188
- }
143
+ consola.log(colors.bold("\n=== DRY RUN ===\n"));
144
+ consola.log(colors.cyan("Config:"));
145
+ consola.log(` Max iterations: ${maxIterations}`);
146
+ consola.log(` State file: ${stateFile}`);
147
+ consola.log(` Log file: ${logFile}`);
148
+ consola.log(` Completion marker: ${flags.completionMarker}`);
149
+ consola.log(` Auto-commit: ${flags.autoCommit}`);
150
+ consola.log(` Claude args: ${[...CLAUDE_DEFAULT_ARGS, ...extraClaudeArgs].join(" ")}`);
151
+ consola.log(` Remaining plans: ${remainingPlans.length}`);
152
+
153
+ consola.log(colors.cyan("\nCurrent plan:"));
154
+ consola.log(` #${currentPlan.id}: ${currentPlan.description}`);
189
155
 
190
156
  // Show prompt preview
191
- const progressContent = readLastIterations(ralphPaths.progressFile, 3);
192
- const taskList = formatTasksForPrompt(remainingTasks);
193
157
  const prompt = buildIterationPrompt({
194
158
  completionMarker: flags.completionMarker,
195
- progressFile: ralphPaths.progressFile,
196
- focusedTaskId,
159
+ plan: currentPlan,
197
160
  skipCommit: !flags.autoCommit,
198
- progressContent: progressContent || undefined,
199
- taskList,
200
161
  });
201
- console.log(pc.dim("─".repeat(60)));
202
- console.log(pc.bold("Prompt Preview"));
203
- console.log(pc.dim("─".repeat(60)));
204
- console.log(prompt);
205
- console.log(pc.dim("─".repeat(60)));
162
+ consola.log(colors.dim("─".repeat(60)));
163
+ consola.log(colors.bold("Prompt Preview"));
164
+ consola.log(colors.dim("─".repeat(60)));
165
+ consola.log(prompt);
166
+ consola.log(colors.dim("─".repeat(60)));
206
167
 
207
- console.log(pc.bold("\n=== END DRY RUN ===\n"));
168
+ consola.log(colors.bold("\n=== END DRY RUN ===\n"));
208
169
  return;
209
170
  }
210
171
 
@@ -216,38 +177,29 @@ export default class Run extends BaseCommand {
216
177
  }
217
178
 
218
179
  // Update state for this run
219
- state.maxIterations = maxIterations;
220
180
  state.status = "running";
221
181
 
222
182
  // Create log stream (append mode)
223
183
  const logStream = fs.createWriteStream(logFile, { flags: "a" });
224
184
 
225
- const ready = state.tasks.filter((t) => t.status === "ready").length;
226
- const done = state.tasks.filter((t) => t.status === "done").length;
185
+ const ready = state.plans.filter((t) => t.status === "ready").length;
186
+ const done = state.plans.filter((t) => t.status === "done").length;
227
187
 
228
188
  logStream.write(`\n${"=".repeat(60)}\n`);
229
189
  logStream.write(`Ralph Loop Started: ${new Date().toISOString()}\n`);
230
190
  logStream.write(`${"=".repeat(60)}\n\n`);
231
191
 
232
- console.log(pc.bold(pc.blue("\nRalph Loop Starting\n")));
233
- console.log(pc.dim(`Focus: ${focusedTaskId ? `Task #${focusedTaskId}` : "Ralph picks"}`));
234
- if (labelFilter) {
235
- console.log(pc.dim(`Label filter: ${labelFilter}`));
236
- }
237
- console.log(pc.dim(`Max iterations: ${maxIterations}`));
238
- console.log(pc.dim(`Log file: ${logFile}`));
239
- console.log(pc.dim(`Auto-commit: ${flags.autoCommit}`));
240
- console.log(
241
- pc.dim(
242
- `Fork session: ${!flags.noFork}${state.sessionId ? ` (session: ${state.sessionId.slice(0, 8)}...)` : ""}`,
243
- ),
244
- );
245
- console.log(pc.dim(`Tasks: ${state.tasks.length} (${done} done, ${ready} ready)`));
246
- console.log();
247
-
248
- logStream.write(`Focus: ${focusedTaskId ? `Task #${focusedTaskId}` : "Ralph picks"}\n`);
192
+ consola.log(colors.bold(colors.blue("\nRalph Loop Starting\n")));
193
+ consola.log(colors.dim(`Focus: ${focusedPlanId ? `Plan #${focusedPlanId}` : "Ralph picks"}`));
194
+ consola.log(colors.dim(`Max iterations: ${maxIterations}`));
195
+ consola.log(colors.dim(`Log file: ${logFile}`));
196
+ consola.log(colors.dim(`Auto-commit: ${flags.autoCommit}`));
197
+ consola.log(colors.dim(`Tasks: ${state.plans.length} (${done} done, ${ready} ready)`));
198
+ consola.log("");
199
+
200
+ logStream.write(`Focus: ${focusedPlanId ? `Plan #${focusedPlanId}` : "Ralph picks"}\n`);
249
201
  logStream.write(`Max iterations: ${maxIterations}\n`);
250
- logStream.write(`Tasks: ${state.tasks.length} (${done} done, ${ready} ready)\n\n`);
202
+ logStream.write(`Tasks: ${state.plans.length} (${done} done, ${ready} ready)\n\n`);
251
203
 
252
204
  // Handle SIGINT gracefully
253
205
  let interrupted = false;
@@ -258,7 +210,7 @@ export default class Run extends BaseCommand {
258
210
  }
259
211
  interrupted = true;
260
212
  const msg = "\n\nInterrupted. Press Ctrl+C again to force exit.\n";
261
- console.log(pc.yellow(msg));
213
+ consola.log(colors.yellow(msg));
262
214
  logStream.write(msg);
263
215
  state.status = "error";
264
216
  saveState(state, stateFile);
@@ -266,30 +218,31 @@ export default class Run extends BaseCommand {
266
218
 
267
219
  // Main loop
268
220
  let completed = false;
221
+ let iteration = 0;
269
222
 
270
- while (state.iteration < maxIterations && !interrupted && !completed) {
271
- state.iteration++;
272
- const currentIteration = state.iteration;
223
+ while (iteration < maxIterations && !interrupted && !completed) {
224
+ iteration++;
273
225
 
274
- const iterHeader = `Iteration ${currentIteration}/${maxIterations}`;
226
+ const iterHeader = `Iteration ${iteration}/${maxIterations}`;
275
227
  logStream.write(`\n━━━ ${iterHeader} ━━━\n`);
276
228
 
277
229
  const iterationStart = new Date().toISOString();
278
- const progressContent = readLastIterations(ralphPaths.progressFile, 3);
279
- // Reload remaining tasks for current state
280
- const currentRemainingTasks = state.tasks.filter((t) => t.status !== "done");
281
- const taskList = formatTasksForPrompt(
282
- labelFilter
283
- ? currentRemainingTasks.filter((t) => t.label === labelFilter)
284
- : currentRemainingTasks,
285
- );
230
+ // Get current plan for this iteration
231
+ const plan = getCurrentPlan(state.plans, focusedPlanId);
232
+ if (!plan) {
233
+ completed = true;
234
+ state.status = "completed";
235
+ saveState(state, stateFile);
236
+ consola.log(
237
+ colors.bold(colors.green(`\n✅ All plans completed after ${iteration} iteration(s)`)),
238
+ );
239
+ logStream.write(`\n✅ All plans completed after ${iteration} iteration(s)\n`);
240
+ break;
241
+ }
286
242
  const prompt = buildIterationPrompt({
287
243
  completionMarker: flags.completionMarker,
288
- progressFile: ralphPaths.progressFile,
289
- focusedTaskId,
244
+ plan: plan,
290
245
  skipCommit: !flags.autoCommit,
291
- progressContent: progressContent || undefined,
292
- taskList,
293
246
  });
294
247
 
295
248
  // Log the prompt
@@ -297,25 +250,15 @@ export default class Run extends BaseCommand {
297
250
 
298
251
  // Build claude args
299
252
  const iterClaudeArgs = [...extraClaudeArgs];
300
- const currentTask = focusedTaskId
301
- ? state.tasks.find((t) => t.id === focusedTaskId)
302
- : state.tasks.find((t) => t.status === "ready");
303
-
304
- // Fork from task's sessionId (or state-level fallback) unless disabled
305
- const taskSessionId = currentTask?.sessionId || state.sessionId;
306
- if (!flags.noFork && taskSessionId) {
307
- iterClaudeArgs.push("--fork-session", taskSessionId);
308
- }
309
253
 
310
254
  // Print iteration header
311
- const sessionInfo = taskSessionId ? pc.dim(` (fork: ${taskSessionId.slice(0, 8)}...)`) : "";
312
- console.log();
313
- console.log(pc.bold(pc.blue(`━━━ ${iterHeader}${sessionInfo} ━━━`)));
314
- console.log(pc.dim("".repeat(60)));
315
- console.log(pc.bold("Prompt"));
316
- console.log(pc.dim("─".repeat(60)));
317
- console.log(prompt);
318
- console.log(pc.dim("─".repeat(60)));
255
+ consola.log("");
256
+ consola.log(colors.bold(colors.blue(`━━━ ${iterHeader} ━━━`)));
257
+ consola.log(colors.dim("─".repeat(60)));
258
+ consola.log(colors.bold("Prompt"));
259
+ consola.log(colors.dim("".repeat(60)));
260
+ consola.log(prompt);
261
+ consola.log(colors.dim("─".repeat(60)));
319
262
 
320
263
  // Run iteration - output goes directly to stdout
321
264
  const iterResult = await runIteration(prompt, iterClaudeArgs, logStream);
@@ -323,17 +266,7 @@ export default class Run extends BaseCommand {
323
266
  // Reload state from disk to pick up changes made by child claude process
324
267
  const freshState = loadState(stateFile);
325
268
  if (freshState) {
326
- const currentIter = state.iteration;
327
- Object.assign(state, freshState, { iteration: currentIter });
328
- }
329
-
330
- // Store session ID on the current task for future resumption
331
- const taskToUpdate = currentTask
332
- ? state.tasks.find((t) => t.id === currentTask.id)
333
- : undefined;
334
- if (iterResult.sessionId && taskToUpdate && !taskToUpdate.sessionId) {
335
- taskToUpdate.sessionId = iterResult.sessionId;
336
- state.sessionId = iterResult.sessionId;
269
+ Object.assign(state, freshState);
337
270
  }
338
271
 
339
272
  const iterationEnd = new Date().toISOString();
@@ -348,7 +281,7 @@ export default class Run extends BaseCommand {
348
281
  // Record history
349
282
  appendHistory(
350
283
  {
351
- iteration: state.iteration,
284
+ iteration,
352
285
  startedAt: iterationStart,
353
286
  completedAt: iterationEnd,
354
287
  durationMs,
@@ -369,11 +302,11 @@ export default class Run extends BaseCommand {
369
302
  ? ` | Context: ${iterResult.contextUsedPercent}%`
370
303
  : "";
371
304
  logStream.write(
372
- `\n━━━ Iteration ${state.iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\nMarker found: ${markerFound ? "yes" : "no"}\n`,
305
+ `\n━━━ Iteration ${iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\nMarker found: ${markerFound ? "yes" : "no"}\n`,
373
306
  );
374
- console.log(
375
- pc.dim(
376
- `Duration: ${durationHuman}${contextInfo} | Marker: ${markerFound ? pc.green("yes") : pc.yellow("no")}`,
307
+ consola.log(
308
+ colors.dim(
309
+ `Duration: ${durationHuman}${contextInfo} | Marker: ${markerFound ? colors.green("yes") : colors.yellow("no")}`,
377
310
  ),
378
311
  );
379
312
 
@@ -382,8 +315,10 @@ export default class Run extends BaseCommand {
382
315
  completed = true;
383
316
  state.status = "completed";
384
317
  saveState(state, stateFile);
385
- console.log(pc.bold(pc.green(`\n✅ Task completed after ${state.iteration} iteration(s)`)));
386
- logStream.write(`\n✅ Task completed after ${state.iteration} iteration(s)\n`);
318
+ consola.log(
319
+ colors.bold(colors.green(`\n✅ Plan completed after ${iteration} iteration(s)`)),
320
+ );
321
+ logStream.write(`\n✅ Plan completed after ${iteration} iteration(s)\n`);
387
322
  }
388
323
  }
389
324
 
@@ -394,13 +329,15 @@ export default class Run extends BaseCommand {
394
329
  return;
395
330
  }
396
331
 
397
- if (!interrupted && state.iteration >= maxIterations) {
332
+ if (!interrupted && iteration >= maxIterations) {
398
333
  state.status = "max_iterations_reached";
399
334
  saveState(state, stateFile);
400
- console.log(
401
- pc.bold(pc.yellow(`\n⚠️ Max iterations (${maxIterations}) reached without completion`)),
335
+ consola.log(
336
+ colors.bold(
337
+ colors.yellow(`\n⚠️ Max iterations (${maxIterations}) reached without completion`),
338
+ ),
402
339
  );
403
- console.log(pc.dim(`State saved to: ${stateFile}`));
340
+ consola.log(colors.dim(`State saved to: ${stateFile}`));
404
341
  logStream.write(`\n⚠️ Max iterations (${maxIterations}) reached without completion\n`);
405
342
  this.exit(1);
406
343
  }
@@ -0,0 +1,88 @@
1
+ import { Flags } from "@oclif/core";
2
+ import consola from "consola";
3
+ import { colors } from "consola/utils";
4
+ import { BaseCommand } from "../base.js";
5
+ import { DEFAULT_STATE_FILE, loadState, resolveRalphPath } from "../../lib/ralph/state.js";
6
+ import {
7
+ formatPlanAsMarkdown,
8
+ formatPlanAsJson,
9
+ copyToClipboard,
10
+ } from "../../lib/ralph/formatter.js";
11
+
12
+ /**
13
+ * Show plan summary with status, tasks, and mermaid graph
14
+ */
15
+ export default class Show extends BaseCommand {
16
+ static override description = "Show plan summary with status, tasks, and mermaid graph";
17
+
18
+ static override examples = [
19
+ {
20
+ description: "Show plan summary with mermaid graph",
21
+ command: "<%= config.bin %> <%= command.id %>",
22
+ },
23
+ {
24
+ description: "Output plan as JSON",
25
+ command: "<%= config.bin %> <%= command.id %> --format json",
26
+ },
27
+ {
28
+ description: "Copy plan output to clipboard",
29
+ command: "<%= config.bin %> <%= command.id %> --copy",
30
+ },
31
+ ];
32
+
33
+ static override flags = {
34
+ ...BaseCommand.baseFlags,
35
+ stateFile: Flags.string({
36
+ char: "s",
37
+ description: `State file path (default: ${DEFAULT_STATE_FILE})`,
38
+ }),
39
+ format: Flags.string({
40
+ char: "f",
41
+ description: "Output format",
42
+ default: "default",
43
+ options: ["default", "markdown", "json"],
44
+ }),
45
+ copy: Flags.boolean({
46
+ char: "c",
47
+ description: "Copy output to clipboard",
48
+ default: false,
49
+ }),
50
+ };
51
+
52
+ async run(): Promise<void> {
53
+ const { flags } = await this.parse(Show);
54
+ const ralphSettings = this.settings.settings.ralphSettings;
55
+ const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
56
+
57
+ const state = loadState(stateFile);
58
+
59
+ if (!state) {
60
+ consola.log(colors.yellow(`No state file found at: ${stateFile}`));
61
+ return;
62
+ }
63
+
64
+ if (state.plans.length === 0) {
65
+ consola.log(colors.yellow("No tasks in state file."));
66
+ consola.log(colors.dim('Use: tt ralph plan add "description"'));
67
+ return;
68
+ }
69
+
70
+ let output: string;
71
+
72
+ if (flags.format === "json") {
73
+ output = formatPlanAsJson(state.plans, state);
74
+ } else {
75
+ output = formatPlanAsMarkdown(state.plans, state);
76
+ }
77
+
78
+ consola.log(output);
79
+
80
+ if (flags.copy) {
81
+ if (copyToClipboard(output)) {
82
+ consola.log(colors.green("✓ Copied to clipboard"));
83
+ } else {
84
+ consola.log(colors.yellow("⚠ Could not copy to clipboard (xclip/xsel not installed?)"));
85
+ }
86
+ }
87
+ }
88
+ }
@@ -2,26 +2,16 @@ import { z } from "zod/v4";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { homedir } from "node:os";
5
- import { AppInfo } from "../constants";
6
5
  import consola from "consola";
7
6
  import { colors } from "consola/utils";
8
7
 
8
+ const TOOL_NAME = "towles-tool";
9
+
9
10
  /** Default config directory */
10
- export const DEFAULT_CONFIG_DIR = path.join(homedir(), ".config", AppInfo.toolName);
11
+ export const DEFAULT_CONFIG_DIR = path.join(homedir(), ".config", TOOL_NAME);
11
12
 
12
13
  /** User settings file path */
13
- export const USER_SETTINGS_PATH = path.join(
14
- DEFAULT_CONFIG_DIR,
15
- `${AppInfo.toolName}.settings.json`,
16
- );
17
-
18
- /** Settings filename used within configDir */
19
- export const SETTINGS_FILENAME = `${AppInfo.toolName}.settings.json`;
20
-
21
- /** Get settings file path for a given configDir */
22
- export function getSettingsPath(configDir: string): string {
23
- return path.join(configDir, SETTINGS_FILENAME);
24
- }
14
+ export const USER_SETTINGS_PATH = path.join(DEFAULT_CONFIG_DIR, `${TOOL_NAME}.settings.json`);
25
15
 
26
16
  export const RalphSettingsSchema = z.object({
27
17
  // Base directory for ralph files (relative to cwd or absolute)
@@ -48,7 +38,7 @@ export const JournalSettingsSchema = z.object({
48
38
  .string()
49
39
  .default(path.join("journal/{yyyy}/{MM}/notes/{yyyy}-{MM}-{dd}-{title}.md")),
50
40
  // Directory for external templates (fallback to hardcoded if not found)
51
- templateDir: z.string().default(path.join(homedir(), ".config", AppInfo.toolName, "templates")),
41
+ templateDir: z.string().default(path.join(homedir(), ".config", TOOL_NAME, "templates")),
52
42
  });
53
43
 
54
44
  export type JournalSettings = z.infer<typeof JournalSettingsSchema>;
@@ -70,15 +60,6 @@ export interface SettingsFile {
70
60
  path: string;
71
61
  }
72
62
 
73
- // TODO refactor this.
74
- export class LoadedSettings {
75
- constructor(settingsFile: SettingsFile) {
76
- this.settingsFile = settingsFile;
77
- }
78
-
79
- readonly settingsFile: SettingsFile;
80
- }
81
-
82
63
  function createDefaultSettings(): UserSettings {
83
64
  return UserSettingsSchema.parse({});
84
65
  }
@@ -106,7 +87,7 @@ export function saveSettings(settingsFile: SettingsFile): void {
106
87
  }
107
88
  }
108
89
 
109
- export async function loadSettings(): Promise<LoadedSettings> {
90
+ export async function loadSettings(): Promise<SettingsFile> {
110
91
  let userSettings: UserSettings | null = null;
111
92
 
112
93
  // Load user settings
@@ -148,8 +129,8 @@ export async function loadSettings(): Promise<LoadedSettings> {
148
129
  }
149
130
  }
150
131
 
151
- return new LoadedSettings({
132
+ return {
152
133
  path: USER_SETTINGS_PATH,
153
134
  settings: userSettings!,
154
- });
135
+ };
155
136
  }