@towles/tool 0.0.20 → 0.0.48
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/{LICENSE.md → LICENSE} +1 -1
- package/README.md +86 -85
- package/bin/run.ts +5 -0
- package/package.json +84 -64
- package/patches/prompts.patch +34 -0
- package/src/commands/base.ts +27 -0
- package/src/commands/config.test.ts +15 -0
- package/src/commands/config.ts +44 -0
- package/src/commands/doctor.ts +136 -0
- package/src/commands/gh/branch-clean.ts +116 -0
- package/src/commands/gh/branch.test.ts +124 -0
- package/src/commands/gh/branch.ts +135 -0
- package/src/commands/gh/pr.ts +175 -0
- 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 +154 -0
- package/src/commands/journal/daily-notes.ts +70 -0
- package/src/commands/journal/meeting.ts +89 -0
- package/src/commands/journal/note.ts +89 -0
- package/src/commands/ralph/plan/add.ts +75 -0
- package/src/commands/ralph/plan/done.ts +82 -0
- package/src/commands/ralph/plan/list.test.ts +48 -0
- package/src/commands/ralph/plan/list.ts +99 -0
- package/src/commands/ralph/plan/remove.ts +71 -0
- package/src/commands/ralph/run.test.ts +521 -0
- package/src/commands/ralph/run.ts +345 -0
- package/src/commands/ralph/show.ts +88 -0
- package/src/config/settings.ts +136 -0
- package/src/lib/journal/utils.ts +399 -0
- package/src/lib/ralph/execution.ts +292 -0
- package/src/lib/ralph/formatter.ts +238 -0
- package/src/lib/ralph/index.ts +4 -0
- package/src/lib/ralph/state.ts +166 -0
- package/src/types/journal.ts +16 -0
- package/src/utils/date-utils.test.ts +97 -0
- package/src/utils/date-utils.ts +54 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
- package/src/utils/git/gh-cli-wrapper.ts +54 -0
- package/src/utils/git/git-wrapper.test.ts +26 -0
- package/src/utils/git/git-wrapper.ts +15 -0
- package/src/utils/render.test.ts +71 -0
- package/src/utils/render.ts +34 -0
- package/dist/index.d.mts +0 -1
- package/dist/index.mjs +0 -805
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { Flags } from "@oclif/core";
|
|
3
|
+
import consola from "consola";
|
|
4
|
+
import { colors } from "consola/utils";
|
|
5
|
+
import { BaseCommand } from "../base.js";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_STATE_FILE,
|
|
8
|
+
DEFAULT_LOG_FILE,
|
|
9
|
+
DEFAULT_MAX_ITERATIONS,
|
|
10
|
+
DEFAULT_COMPLETION_MARKER,
|
|
11
|
+
CLAUDE_DEFAULT_ARGS,
|
|
12
|
+
loadState,
|
|
13
|
+
saveState,
|
|
14
|
+
appendHistory,
|
|
15
|
+
resolveRalphPath,
|
|
16
|
+
getRalphPaths,
|
|
17
|
+
} from "../../lib/ralph/state.js";
|
|
18
|
+
import {
|
|
19
|
+
buildIterationPrompt,
|
|
20
|
+
formatDuration,
|
|
21
|
+
extractOutputSummary,
|
|
22
|
+
detectCompletionMarker,
|
|
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);
|
|
31
|
+
}
|
|
32
|
+
return plans.find((p) => p.status !== "done");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run the autonomous ralph loop
|
|
37
|
+
*/
|
|
38
|
+
export default class Run extends BaseCommand {
|
|
39
|
+
static override description = "Start the autonomous ralph loop";
|
|
40
|
+
|
|
41
|
+
static override examples = [
|
|
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
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
static override flags = {
|
|
62
|
+
...BaseCommand.baseFlags,
|
|
63
|
+
stateFile: Flags.string({
|
|
64
|
+
char: "s",
|
|
65
|
+
description: `State file path (default: ${DEFAULT_STATE_FILE})`,
|
|
66
|
+
}),
|
|
67
|
+
planId: Flags.integer({
|
|
68
|
+
char: "p",
|
|
69
|
+
description: "Focus on specific plan ID",
|
|
70
|
+
}),
|
|
71
|
+
maxIterations: Flags.integer({
|
|
72
|
+
char: "m",
|
|
73
|
+
description: "Max iterations",
|
|
74
|
+
default: DEFAULT_MAX_ITERATIONS,
|
|
75
|
+
}),
|
|
76
|
+
autoCommit: Flags.boolean({
|
|
77
|
+
description: "Auto-commit after each completed plan",
|
|
78
|
+
default: true,
|
|
79
|
+
allowNo: true,
|
|
80
|
+
}),
|
|
81
|
+
dryRun: Flags.boolean({
|
|
82
|
+
char: "n",
|
|
83
|
+
description: "Show config without executing",
|
|
84
|
+
default: false,
|
|
85
|
+
}),
|
|
86
|
+
claudeArgs: Flags.string({
|
|
87
|
+
description: "Extra args to pass to claude CLI (space-separated)",
|
|
88
|
+
}),
|
|
89
|
+
logFile: Flags.string({
|
|
90
|
+
description: `Log file path (default: ${DEFAULT_LOG_FILE})`,
|
|
91
|
+
}),
|
|
92
|
+
completionMarker: Flags.string({
|
|
93
|
+
description: "Completion marker",
|
|
94
|
+
default: DEFAULT_COMPLETION_MARKER,
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
async run(): Promise<void> {
|
|
99
|
+
const { flags } = await this.parse(Run);
|
|
100
|
+
const ralphSettings = this.settings.settings.ralphSettings;
|
|
101
|
+
const stateFile = resolveRalphPath(flags.stateFile, "stateFile", ralphSettings);
|
|
102
|
+
const logFile = resolveRalphPath(flags.logFile, "logFile", ralphSettings);
|
|
103
|
+
const ralphPaths = getRalphPaths(ralphSettings);
|
|
104
|
+
|
|
105
|
+
const maxIterations = flags.maxIterations;
|
|
106
|
+
const extraClaudeArgs = flags.claudeArgs?.split(" ").filter(Boolean) || [];
|
|
107
|
+
const focusedPlanId = flags.planId ?? null;
|
|
108
|
+
|
|
109
|
+
// Load existing state
|
|
110
|
+
let state = loadState(stateFile);
|
|
111
|
+
|
|
112
|
+
if (!state) {
|
|
113
|
+
this.error(`No state file found at: ${stateFile}\nUse: tt ralph plan add --file path.md`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const remainingPlans = state.plans.filter((t) => t.status !== "done");
|
|
117
|
+
if (remainingPlans.length === 0) {
|
|
118
|
+
consola.log(colors.green("✅ All plans are done!"));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
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`);
|
|
127
|
+
}
|
|
128
|
+
if (focusedPlan.status === "done") {
|
|
129
|
+
consola.log(colors.yellow(`Plan #${focusedPlanId} is already done.`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
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
|
+
|
|
141
|
+
// Dry run mode
|
|
142
|
+
if (flags.dryRun) {
|
|
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}`);
|
|
155
|
+
|
|
156
|
+
// Show prompt preview
|
|
157
|
+
const prompt = buildIterationPrompt({
|
|
158
|
+
completionMarker: flags.completionMarker,
|
|
159
|
+
plan: currentPlan,
|
|
160
|
+
skipCommit: !flags.autoCommit,
|
|
161
|
+
});
|
|
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)));
|
|
167
|
+
|
|
168
|
+
consola.log(colors.bold("\n=== END DRY RUN ===\n"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check claude CLI is available
|
|
173
|
+
if (!(await checkClaudeCli())) {
|
|
174
|
+
this.error(
|
|
175
|
+
"claude CLI not found in PATH\nInstall Claude Code: https://docs.anthropic.com/en/docs/claude-code",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Update state for this run
|
|
180
|
+
state.status = "running";
|
|
181
|
+
|
|
182
|
+
// Create log stream (append mode)
|
|
183
|
+
const logStream = fs.createWriteStream(logFile, { flags: "a" });
|
|
184
|
+
|
|
185
|
+
const ready = state.plans.filter((t) => t.status === "ready").length;
|
|
186
|
+
const done = state.plans.filter((t) => t.status === "done").length;
|
|
187
|
+
|
|
188
|
+
logStream.write(`\n${"=".repeat(60)}\n`);
|
|
189
|
+
logStream.write(`Ralph Loop Started: ${new Date().toISOString()}\n`);
|
|
190
|
+
logStream.write(`${"=".repeat(60)}\n\n`);
|
|
191
|
+
|
|
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`);
|
|
201
|
+
logStream.write(`Max iterations: ${maxIterations}\n`);
|
|
202
|
+
logStream.write(`Tasks: ${state.plans.length} (${done} done, ${ready} ready)\n\n`);
|
|
203
|
+
|
|
204
|
+
// Handle SIGINT gracefully
|
|
205
|
+
let interrupted = false;
|
|
206
|
+
process.on("SIGINT", () => {
|
|
207
|
+
if (interrupted) {
|
|
208
|
+
logStream.end();
|
|
209
|
+
process.exit(130);
|
|
210
|
+
}
|
|
211
|
+
interrupted = true;
|
|
212
|
+
const msg = "\n\nInterrupted. Press Ctrl+C again to force exit.\n";
|
|
213
|
+
consola.log(colors.yellow(msg));
|
|
214
|
+
logStream.write(msg);
|
|
215
|
+
state.status = "error";
|
|
216
|
+
saveState(state, stateFile);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Main loop
|
|
220
|
+
let completed = false;
|
|
221
|
+
let iteration = 0;
|
|
222
|
+
|
|
223
|
+
while (iteration < maxIterations && !interrupted && !completed) {
|
|
224
|
+
iteration++;
|
|
225
|
+
|
|
226
|
+
const iterHeader = `Iteration ${iteration}/${maxIterations}`;
|
|
227
|
+
logStream.write(`\n━━━ ${iterHeader} ━━━\n`);
|
|
228
|
+
|
|
229
|
+
const iterationStart = new Date().toISOString();
|
|
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
|
+
}
|
|
242
|
+
const prompt = buildIterationPrompt({
|
|
243
|
+
completionMarker: flags.completionMarker,
|
|
244
|
+
plan: plan,
|
|
245
|
+
skipCommit: !flags.autoCommit,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Log the prompt
|
|
249
|
+
logStream.write(`\n--- Prompt ---\n${prompt}\n--- End Prompt ---\n\n`);
|
|
250
|
+
|
|
251
|
+
// Build claude args
|
|
252
|
+
const iterClaudeArgs = [...extraClaudeArgs];
|
|
253
|
+
|
|
254
|
+
// Print iteration header
|
|
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)));
|
|
262
|
+
|
|
263
|
+
// Run iteration - output goes directly to stdout
|
|
264
|
+
const iterResult = await runIteration(prompt, iterClaudeArgs, logStream);
|
|
265
|
+
|
|
266
|
+
// Reload state from disk to pick up changes made by child claude process
|
|
267
|
+
const freshState = loadState(stateFile);
|
|
268
|
+
if (freshState) {
|
|
269
|
+
Object.assign(state, freshState);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const iterationEnd = new Date().toISOString();
|
|
273
|
+
const markerFound = detectCompletionMarker(iterResult.output, flags.completionMarker);
|
|
274
|
+
|
|
275
|
+
// Calculate duration
|
|
276
|
+
const startTime = new Date(iterationStart).getTime();
|
|
277
|
+
const endTime = new Date(iterationEnd).getTime();
|
|
278
|
+
const durationMs = endTime - startTime;
|
|
279
|
+
const durationHuman = formatDuration(durationMs);
|
|
280
|
+
|
|
281
|
+
// Record history
|
|
282
|
+
appendHistory(
|
|
283
|
+
{
|
|
284
|
+
iteration,
|
|
285
|
+
startedAt: iterationStart,
|
|
286
|
+
completedAt: iterationEnd,
|
|
287
|
+
durationMs,
|
|
288
|
+
durationHuman,
|
|
289
|
+
outputSummary: extractOutputSummary(iterResult.output),
|
|
290
|
+
markerFound,
|
|
291
|
+
contextUsedPercent: iterResult.contextUsedPercent,
|
|
292
|
+
},
|
|
293
|
+
ralphPaths.historyFile,
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// Save state
|
|
297
|
+
saveState(state, stateFile);
|
|
298
|
+
|
|
299
|
+
// Log summary
|
|
300
|
+
const contextInfo =
|
|
301
|
+
iterResult.contextUsedPercent !== undefined
|
|
302
|
+
? ` | Context: ${iterResult.contextUsedPercent}%`
|
|
303
|
+
: "";
|
|
304
|
+
logStream.write(
|
|
305
|
+
`\n━━━ Iteration ${iteration} Summary ━━━\nDuration: ${durationHuman}${contextInfo}\nMarker found: ${markerFound ? "yes" : "no"}\n`,
|
|
306
|
+
);
|
|
307
|
+
consola.log(
|
|
308
|
+
colors.dim(
|
|
309
|
+
`Duration: ${durationHuman}${contextInfo} | Marker: ${markerFound ? colors.green("yes") : colors.yellow("no")}`,
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Check completion
|
|
314
|
+
if (markerFound) {
|
|
315
|
+
completed = true;
|
|
316
|
+
state.status = "completed";
|
|
317
|
+
saveState(state, stateFile);
|
|
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`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
logStream.end();
|
|
326
|
+
|
|
327
|
+
// Final status
|
|
328
|
+
if (completed) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!interrupted && iteration >= maxIterations) {
|
|
333
|
+
state.status = "max_iterations_reached";
|
|
334
|
+
saveState(state, stateFile);
|
|
335
|
+
consola.log(
|
|
336
|
+
colors.bold(
|
|
337
|
+
colors.yellow(`\n⚠️ Max iterations (${maxIterations}) reached without completion`),
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
consola.log(colors.dim(`State saved to: ${stateFile}`));
|
|
341
|
+
logStream.write(`\n⚠️ Max iterations (${maxIterations}) reached without completion\n`);
|
|
342
|
+
this.exit(1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { colors } from "consola/utils";
|
|
7
|
+
|
|
8
|
+
const TOOL_NAME = "towles-tool";
|
|
9
|
+
|
|
10
|
+
/** Default config directory */
|
|
11
|
+
export const DEFAULT_CONFIG_DIR = path.join(homedir(), ".config", TOOL_NAME);
|
|
12
|
+
|
|
13
|
+
/** User settings file path */
|
|
14
|
+
export const USER_SETTINGS_PATH = path.join(DEFAULT_CONFIG_DIR, `${TOOL_NAME}.settings.json`);
|
|
15
|
+
|
|
16
|
+
export const RalphSettingsSchema = z.object({
|
|
17
|
+
// Base directory for ralph files (relative to cwd or absolute)
|
|
18
|
+
stateDir: z.string().default("./.claude/.ralph"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type RalphSettings = z.infer<typeof RalphSettingsSchema>;
|
|
22
|
+
|
|
23
|
+
export const JournalSettingsSchema = z.object({
|
|
24
|
+
// Base folder where all journal files are stored
|
|
25
|
+
baseFolder: z.string().default(path.join(homedir())),
|
|
26
|
+
// https://moment.github.io/luxon/#/formatting?id=table-of-tokens
|
|
27
|
+
dailyPathTemplate: z
|
|
28
|
+
.string()
|
|
29
|
+
.default(
|
|
30
|
+
path.join(
|
|
31
|
+
"journal/{monday:yyyy}/{monday:MM}/daily-notes/{monday:yyyy}-{monday:MM}-{monday:dd}-daily-notes.md",
|
|
32
|
+
),
|
|
33
|
+
),
|
|
34
|
+
meetingPathTemplate: z
|
|
35
|
+
.string()
|
|
36
|
+
.default(path.join("journal/{yyyy}/{MM}/meetings/{yyyy}-{MM}-{dd}-{title}.md")),
|
|
37
|
+
notePathTemplate: z
|
|
38
|
+
.string()
|
|
39
|
+
.default(path.join("journal/{yyyy}/{MM}/notes/{yyyy}-{MM}-{dd}-{title}.md")),
|
|
40
|
+
// Directory for external templates (fallback to hardcoded if not found)
|
|
41
|
+
templateDir: z.string().default(path.join(homedir(), ".config", TOOL_NAME, "templates")),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type JournalSettings = z.infer<typeof JournalSettingsSchema>;
|
|
45
|
+
|
|
46
|
+
export const UserSettingsSchema = z.object({
|
|
47
|
+
preferredEditor: z.string().default("code"),
|
|
48
|
+
journalSettings: JournalSettingsSchema.optional().transform(
|
|
49
|
+
(v) => v ?? JournalSettingsSchema.parse({}),
|
|
50
|
+
),
|
|
51
|
+
ralphSettings: RalphSettingsSchema.optional().transform(
|
|
52
|
+
(v) => v ?? RalphSettingsSchema.parse({}),
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
type UserSettings = z.infer<typeof UserSettingsSchema>;
|
|
57
|
+
|
|
58
|
+
export interface SettingsFile {
|
|
59
|
+
settings: UserSettings;
|
|
60
|
+
path: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createDefaultSettings(): UserSettings {
|
|
64
|
+
return UserSettingsSchema.parse({});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createAndSaveDefaultSettings(): UserSettings {
|
|
68
|
+
const userSettings = createDefaultSettings();
|
|
69
|
+
saveSettings({
|
|
70
|
+
path: USER_SETTINGS_PATH,
|
|
71
|
+
settings: userSettings,
|
|
72
|
+
});
|
|
73
|
+
return userSettings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function saveSettings(settingsFile: SettingsFile): void {
|
|
77
|
+
try {
|
|
78
|
+
// Ensure the directory exists
|
|
79
|
+
const dirPath = path.dirname(settingsFile.path);
|
|
80
|
+
if (!fs.existsSync(dirPath)) {
|
|
81
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fs.writeFileSync(settingsFile.path, JSON.stringify(settingsFile.settings, null, 2), "utf-8");
|
|
85
|
+
} catch (error) {
|
|
86
|
+
consola.error("Error saving user settings file:", error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function loadSettings(): Promise<SettingsFile> {
|
|
91
|
+
let userSettings: UserSettings | null = null;
|
|
92
|
+
|
|
93
|
+
// Load user settings
|
|
94
|
+
if (fs.existsSync(USER_SETTINGS_PATH)) {
|
|
95
|
+
const userContent = fs.readFileSync(USER_SETTINGS_PATH, "utf-8");
|
|
96
|
+
const parsedUserSettings: unknown = JSON.parse(userContent);
|
|
97
|
+
|
|
98
|
+
userSettings = UserSettingsSchema.parse(parsedUserSettings);
|
|
99
|
+
// made add a save here if the default values differ from the current values
|
|
100
|
+
if (JSON.stringify(parsedUserSettings) !== JSON.stringify(userSettings)) {
|
|
101
|
+
consola.warn(`Settings file ${USER_SETTINGS_PATH} has been updated with default values.`);
|
|
102
|
+
const tempSettingsFile: SettingsFile = {
|
|
103
|
+
path: USER_SETTINGS_PATH,
|
|
104
|
+
settings: userSettings,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
saveSettings(tempSettingsFile);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
// Settings file doesn't exist
|
|
111
|
+
const isNonInteractive = process.env.CI || !process.stdout.isTTY;
|
|
112
|
+
|
|
113
|
+
if (isNonInteractive) {
|
|
114
|
+
// Auto-create in CI/non-TTY environments
|
|
115
|
+
consola.info(`Creating settings file: ${USER_SETTINGS_PATH}`);
|
|
116
|
+
userSettings = createAndSaveDefaultSettings();
|
|
117
|
+
} else {
|
|
118
|
+
// Interactive: ask user if they want to create it
|
|
119
|
+
const confirmed = await consola.prompt(
|
|
120
|
+
`Settings file not found. Create ${colors.cyan(USER_SETTINGS_PATH)}?`,
|
|
121
|
+
{
|
|
122
|
+
type: "confirm",
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
if (!confirmed) {
|
|
126
|
+
throw new Error(`Settings file not found and user chose not to create it.`);
|
|
127
|
+
}
|
|
128
|
+
userSettings = createAndSaveDefaultSettings();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
path: USER_SETTINGS_PATH,
|
|
134
|
+
settings: userSettings!,
|
|
135
|
+
};
|
|
136
|
+
}
|