@towles/tool 0.0.61 → 0.0.63

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 (83) hide show
  1. package/package.json +50 -57
  2. package/src/commands/agentboard.ts +176 -0
  3. package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
  4. package/src/commands/auto-claude/list.ts +114 -0
  5. package/src/commands/auto-claude/retry.test.ts +138 -0
  6. package/src/commands/auto-claude/retry.ts +139 -0
  7. package/src/commands/auto-claude/status.test.ts +147 -0
  8. package/src/commands/auto-claude/status.ts +123 -0
  9. package/src/commands/base.ts +7 -2
  10. package/src/commands/config.ts +5 -7
  11. package/src/commands/doctor.ts +111 -12
  12. package/src/commands/gh/branch.ts +4 -4
  13. package/src/commands/gh/pr.ts +1 -0
  14. package/src/commands/graph/index.ts +169 -0
  15. package/src/commands/graph.test.ts +1 -1
  16. package/src/commands/install.ts +40 -68
  17. package/src/commands/journal/daily-notes.ts +3 -3
  18. package/src/commands/journal/meeting.ts +3 -3
  19. package/src/commands/journal/note.ts +3 -3
  20. package/src/lib/auto-claude/claude-cli.ts +183 -0
  21. package/src/lib/auto-claude/config.test.ts +6 -8
  22. package/src/lib/auto-claude/config.ts +3 -4
  23. package/src/lib/auto-claude/index.ts +2 -3
  24. package/src/lib/auto-claude/labels.test.ts +85 -0
  25. package/src/lib/auto-claude/labels.ts +42 -0
  26. package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
  27. package/src/lib/auto-claude/pipeline.test.ts +2 -2
  28. package/src/lib/auto-claude/pipeline.ts +120 -36
  29. package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
  30. package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
  31. package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
  32. package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
  33. package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
  34. package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
  35. package/src/lib/auto-claude/run-claude.test.ts +48 -68
  36. package/src/lib/auto-claude/shell.ts +6 -0
  37. package/src/lib/auto-claude/steps/create-pr.ts +89 -25
  38. package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
  39. package/src/lib/auto-claude/steps/implement.ts +9 -16
  40. package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
  41. package/src/lib/auto-claude/steps/steps.test.ts +68 -63
  42. package/src/lib/auto-claude/templates.test.ts +91 -0
  43. package/src/lib/auto-claude/templates.ts +34 -0
  44. package/src/lib/auto-claude/test-helpers.ts +2 -1
  45. package/src/lib/auto-claude/utils-execution.test.ts +9 -57
  46. package/src/lib/auto-claude/utils.test.ts +5 -9
  47. package/src/lib/auto-claude/utils.ts +27 -253
  48. package/src/lib/graph/analyzer.test.ts +451 -0
  49. package/src/lib/graph/analyzer.ts +165 -0
  50. package/src/lib/graph/index.ts +24 -0
  51. package/src/lib/graph/labels.ts +87 -0
  52. package/src/lib/graph/parser.test.ts +150 -0
  53. package/src/lib/graph/parser.ts +65 -0
  54. package/src/lib/graph/render.ts +25 -0
  55. package/src/lib/graph/server.ts +70 -0
  56. package/src/lib/graph/sessions.ts +104 -0
  57. package/src/lib/graph/tools.ts +90 -0
  58. package/src/lib/graph/treemap.ts +211 -0
  59. package/src/lib/graph/types.ts +80 -0
  60. package/src/lib/install/claude-settings.ts +64 -0
  61. package/src/lib/journal/editor.ts +33 -0
  62. package/src/lib/journal/fs.ts +13 -0
  63. package/src/lib/journal/index.ts +11 -0
  64. package/src/lib/journal/paths.ts +106 -0
  65. package/src/lib/journal/{utils.ts → templates.ts} +3 -151
  66. package/src/utils/fs.ts +19 -0
  67. package/src/utils/git/exec.ts +18 -0
  68. package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
  69. package/src/utils/git/gh-cli-wrapper.ts +31 -19
  70. package/src/utils/render.ts +3 -1
  71. package/src/commands/graph.ts +0 -970
  72. package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
  73. package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
  74. package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
  75. package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
  76. package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
  77. package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
  78. package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
  79. package/src/lib/auto-claude/steps/plan.ts +0 -14
  80. package/src/lib/auto-claude/steps/refresh.ts +0 -114
  81. package/src/lib/auto-claude/steps/remove-label.ts +0 -22
  82. package/src/lib/auto-claude/steps/research.ts +0 -21
  83. package/src/lib/auto-claude/steps/review.ts +0 -14
@@ -11,6 +11,7 @@ import { isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
11
11
  * Note: Uses tinyexec which is safe (execFile-based, no shell injection)
12
12
  */
13
13
  export default class Pr extends BaseCommand {
14
+ static override aliases = ["pr"];
14
15
  static override description = "Create a pull request from the current branch";
15
16
 
16
17
  static override examples = [
@@ -0,0 +1,169 @@
1
+ import { Flags } from "@oclif/core";
2
+ import { DateTime } from "luxon";
3
+ import * as fs from "node:fs";
4
+ import * as os from "node:os";
5
+ import * as path from "node:path";
6
+ import { x } from "tinyexec";
7
+ import { BaseCommand } from "../base.js";
8
+ import {
9
+ buildAllSessionsTreemap,
10
+ buildBarChartData,
11
+ buildSessionTreemap,
12
+ findRecentSessions,
13
+ findSessionPath,
14
+ generateTreemapHtml,
15
+ openInBrowser,
16
+ parseJsonl,
17
+ startServer,
18
+ waitForShutdown,
19
+ } from "../../lib/graph/index.js";
20
+
21
+ // Re-export public API for consumers and tests
22
+ export {
23
+ analyzeSession,
24
+ buildAllSessionsTreemap,
25
+ buildBarChartData,
26
+ buildSessionTreemap,
27
+ calculateCutoffMs,
28
+ extractSessionLabel,
29
+ filterByDays,
30
+ findRecentSessions,
31
+ findSessionPath,
32
+ generateTreemapHtml,
33
+ parseJsonl,
34
+ } from "../../lib/graph/index.js";
35
+ export type {
36
+ BarChartData,
37
+ BarChartDay,
38
+ ProjectBar,
39
+ SessionResult,
40
+ TreemapNode,
41
+ } from "../../lib/graph/index.js";
42
+
43
+ /**
44
+ * Generate interactive HTML treemap from session token data
45
+ */
46
+ export default class Graph extends BaseCommand {
47
+ static override description = "Generate interactive HTML treemap from session token data";
48
+
49
+ static override examples = [
50
+ {
51
+ description: "Generate treemap for all recent sessions",
52
+ command: "<%= config.bin %> <%= command.id %>",
53
+ },
54
+ {
55
+ description: "Generate treemap for a specific session",
56
+ command: "<%= config.bin %> <%= command.id %> --session abc123",
57
+ },
58
+ {
59
+ description: "Generate and auto-open in browser",
60
+ command: "<%= config.bin %> <%= command.id %> --open",
61
+ },
62
+ ];
63
+
64
+ static override flags = {
65
+ ...BaseCommand.baseFlags,
66
+ session: Flags.string({
67
+ char: "s",
68
+ description: "Session ID to analyze (shows all sessions if not provided)",
69
+ }),
70
+ open: Flags.boolean({
71
+ char: "o",
72
+ description: "Open treemap in browser after generating",
73
+ default: true,
74
+ allowNo: true,
75
+ }),
76
+ serve: Flags.boolean({
77
+ description: "Start local HTTP server to serve treemap (default: true)",
78
+ default: true,
79
+ allowNo: true,
80
+ }),
81
+ port: Flags.integer({
82
+ char: "p",
83
+ description: "Port for local server",
84
+ default: 8765,
85
+ }),
86
+ days: Flags.integer({
87
+ description: "Filter to sessions from last N days (0=no limit)",
88
+ default: 7,
89
+ }),
90
+ };
91
+
92
+ async run(): Promise<void> {
93
+ const { flags } = await this.parse(Graph);
94
+
95
+ const projectsDir = path.join(os.homedir(), ".claude", "projects");
96
+ if (!fs.existsSync(projectsDir)) {
97
+ this.error("No Claude projects directory found at ~/.claude/projects/");
98
+ }
99
+
100
+ const sessionId = flags.session;
101
+ let treemapData;
102
+ let barChartData = { days: [] as any[] };
103
+
104
+ if (!sessionId) {
105
+ // All sessions mode
106
+ const sessions = findRecentSessions(projectsDir, 500, flags.days);
107
+ if (sessions.length === 0) {
108
+ this.error("No sessions found");
109
+ }
110
+
111
+ const daysMsg = flags.days > 0 ? ` (last ${flags.days} days)` : "";
112
+ this.log(`📊 Generating treemap for ${sessions.length} sessions${daysMsg}...`);
113
+ treemapData = buildAllSessionsTreemap(sessions);
114
+ barChartData = buildBarChartData(sessions);
115
+ } else {
116
+ // Single session mode
117
+ const sessionPath = findSessionPath(projectsDir, sessionId);
118
+ if (!sessionPath) {
119
+ this.error(`Session ${sessionId} not found`);
120
+ }
121
+
122
+ this.log(`📊 Generating treemap for session ${sessionId}...`);
123
+ const entries = parseJsonl(sessionPath);
124
+ treemapData = buildSessionTreemap(sessionId, entries);
125
+ // Bar chart not meaningful for single session, leave empty
126
+ }
127
+
128
+ // Generate HTML
129
+ const html = generateTreemapHtml(treemapData, barChartData);
130
+
131
+ // Write output file
132
+ const reportsDir = path.join(os.homedir(), ".claude", "reports");
133
+ if (!fs.existsSync(reportsDir)) {
134
+ fs.mkdirSync(reportsDir, { recursive: true });
135
+ }
136
+
137
+ const timestamp = DateTime.now().toFormat("yyyy-MM-dd'T'HH-mmZZZ");
138
+ const daysLabel = flags.days > 0 ? `${flags.days}d` : "all";
139
+ const filename = sessionId
140
+ ? `treemap-${sessionId.slice(0, 8)}-${timestamp}.html`
141
+ : `treemap-${daysLabel}-${timestamp}.html`;
142
+ const outputPath = path.join(reportsDir, filename);
143
+
144
+ fs.writeFileSync(outputPath, html);
145
+ this.log(`✓ Saved to ${outputPath}`);
146
+
147
+ if (flags.serve) {
148
+ const { server, port: actualPort } = await startServer(html, filename, flags.port);
149
+ const url = `http://localhost:${actualPort}/`;
150
+ if (actualPort !== flags.port) {
151
+ this.log(`\n⚠️ Port ${flags.port} in use, using ${actualPort}`);
152
+ }
153
+ this.log(`🌐 Server running at ${url}`);
154
+ this.log(" Press Ctrl+C to stop\n");
155
+
156
+ if (flags.open) {
157
+ openInBrowser(url);
158
+ }
159
+
160
+ // Keep server running until Ctrl+C
161
+ await waitForShutdown(server);
162
+ this.log("\n👋 Stopping server...");
163
+ } else if (flags.open) {
164
+ this.log("\n📈 Opening treemap...");
165
+ const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
166
+ await x(openCmd, [outputPath]);
167
+ }
168
+ }
169
+ }
@@ -2,7 +2,7 @@
2
2
  * Tests for graph command --days filtering and bar chart data
3
3
  */
4
4
  import { describe, it, expect } from "vitest";
5
- import { calculateCutoffMs, filterByDays, analyzeSession } from "./graph.js";
5
+ import { analyzeSession, calculateCutoffMs, filterByDays } from "../lib/graph/index.js";
6
6
 
7
7
  describe("graph --days filtering", () => {
8
8
  describe("calculateCutoffMs", () => {
@@ -1,19 +1,13 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { homedir } from "node:os";
4
1
  import { Flags } from "@oclif/core";
5
- import pc from "picocolors";
2
+ import { colors } from "consola/utils";
6
3
  import consola from "consola";
7
4
  import { BaseCommand } from "./base.js";
8
-
9
- const CLAUDE_SETTINGS_PATH = path.join(homedir(), ".claude", "settings.json");
10
-
11
- interface ClaudeSettings {
12
- cleanupPeriodDays?: number;
13
- alwaysThinkingEnabled?: boolean;
14
- hooks?: Record<string, unknown[]>;
15
- [key: string]: unknown;
16
- }
5
+ import {
6
+ CLAUDE_SETTINGS_PATH,
7
+ loadClaudeSettings,
8
+ applyRecommendedSettings,
9
+ saveClaudeSettings,
10
+ } from "../lib/install/claude-settings.js";
17
11
 
18
12
  /**
19
13
  * Install and configure towles-tool with Claude Code
@@ -45,62 +39,48 @@ export default class Install extends BaseCommand {
45
39
  async run(): Promise<void> {
46
40
  const { flags } = await this.parse(Install);
47
41
 
48
- this.log(pc.bold("\n🔧 towles-tool install\n"));
42
+ this.log(colors.bold("\n🔧 towles-tool install\n"));
49
43
 
50
44
  // Load or create Claude settings
51
- let claudeSettings: ClaudeSettings = {};
52
- if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
53
- try {
54
- const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
55
- claudeSettings = JSON.parse(content);
56
- this.log(pc.dim(`Found existing Claude settings at ${CLAUDE_SETTINGS_PATH}`));
57
- } catch {
58
- this.log(
59
- pc.yellow(`Warning: Could not parse ${CLAUDE_SETTINGS_PATH}, will create fresh settings`),
60
- );
61
- }
45
+ const existing = loadClaudeSettings(CLAUDE_SETTINGS_PATH);
46
+ if (Object.keys(existing).length > 0) {
47
+ this.log(colors.dim(`Found existing Claude settings at ${CLAUDE_SETTINGS_PATH}`));
62
48
  } else {
63
- this.log(pc.dim(`No Claude settings file found, will create one`));
49
+ this.log(colors.dim(`No Claude settings file found, will create one`));
64
50
  }
65
51
 
66
- // Configure recommended settings
67
- let modified = false;
52
+ // Apply recommended settings
53
+ const { settings, changes } = applyRecommendedSettings(existing);
68
54
 
69
- // Prevent log deletion (set to ~274 years)
70
- if (claudeSettings.cleanupPeriodDays !== 99999) {
71
- claudeSettings.cleanupPeriodDays = 99999;
72
- modified = true;
73
- this.log(pc.green("✓ Set cleanupPeriodDays: 99999 (prevent log deletion)"));
74
- } else {
75
- this.log(pc.dim("✓ cleanupPeriodDays already set to 99999"));
55
+ for (const change of changes) {
56
+ this.log(colors.green(`✓ ${change}`));
76
57
  }
77
58
 
78
- // Enable thinking by default
79
- if (claudeSettings.alwaysThinkingEnabled !== true) {
80
- claudeSettings.alwaysThinkingEnabled = true;
81
- modified = true;
82
- this.log(pc.green("✓ Set alwaysThinkingEnabled: true"));
83
- } else {
84
- this.log(pc.dim("✓ alwaysThinkingEnabled already set to true"));
59
+ // Report already-correct settings
60
+ if (!changes.some((c) => c.includes("cleanupPeriodDays"))) {
61
+ this.log(colors.dim("✓ cleanupPeriodDays already set to 99999"));
62
+ }
63
+ if (!changes.some((c) => c.includes("alwaysThinkingEnabled"))) {
64
+ this.log(colors.dim("✓ alwaysThinkingEnabled already set to true"));
85
65
  }
86
66
 
87
- // Save settings if modified
88
- if (modified) {
89
- this.saveClaudeSettings(claudeSettings);
90
- this.log(pc.green(`\n✓ Saved Claude settings to ${CLAUDE_SETTINGS_PATH}`));
67
+ // Save settings if anything changed
68
+ if (changes.length > 0) {
69
+ saveClaudeSettings(CLAUDE_SETTINGS_PATH, settings);
70
+ this.log(colors.green(`\n✓ Saved Claude settings to ${CLAUDE_SETTINGS_PATH}`));
91
71
  }
92
72
 
93
73
  // Show observability setup if requested
94
74
  if (flags.observability) {
95
- this.log(pc.bold("\n📊 Observability Setup\n"));
75
+ this.log(colors.bold("\n📊 Observability Setup\n"));
96
76
  this.showOtelInstructions();
97
77
  }
98
78
 
99
79
  // Install Claude plugins
100
- this.log(pc.bold("\n📦 Claude Plugins\n"));
80
+ this.log(colors.bold("\n📦 Claude Plugins\n"));
101
81
  await this.ensureClaudePlugins();
102
82
 
103
- this.log(pc.bold(pc.green("\n✅ Installation complete!\n")));
83
+ this.log(colors.bold(colors.green("\n✅ Installation complete!\n")));
104
84
  }
105
85
 
106
86
  private async ensureClaudePlugins(): Promise<void> {
@@ -126,7 +106,7 @@ export default class Install extends BaseCommand {
126
106
  const plugins: { id: string }[] = JSON.parse(result.stdout);
127
107
  installedIds = new Set(plugins.map((p) => p.id));
128
108
  } catch {
129
- this.log(pc.yellow("⚠ Could not list Claude plugins"));
109
+ this.log(colors.yellow("⚠ Could not list Claude plugins"));
130
110
  }
131
111
 
132
112
  // Ensure marketplaces are added first
@@ -134,7 +114,7 @@ export default class Install extends BaseCommand {
134
114
  if (plugin.marketplaceUrl && !installedIds.has(plugin.id)) {
135
115
  try {
136
116
  await x("claude", ["plugin", "marketplace", "add", plugin.marketplaceUrl]);
137
- this.log(pc.dim(` Added marketplace: ${plugin.marketplace}`));
117
+ this.log(colors.dim(` Added marketplace: ${plugin.marketplace}`));
138
118
  } catch {
139
119
  // marketplace may already be added
140
120
  }
@@ -144,7 +124,7 @@ export default class Install extends BaseCommand {
144
124
  // Install missing plugins
145
125
  for (const plugin of requiredPlugins) {
146
126
  if (installedIds.has(plugin.id)) {
147
- this.log(pc.dim(`✓ ${plugin.name} already installed`));
127
+ this.log(colors.dim(`✓ ${plugin.name} already installed`));
148
128
  continue;
149
129
  }
150
130
 
@@ -156,28 +136,20 @@ export default class Install extends BaseCommand {
156
136
  if (answer) {
157
137
  const result = await x("claude", ["plugin", "install", plugin.id, "--scope", "user"]);
158
138
  if (result.exitCode === 0) {
159
- this.log(pc.green(`✓ ${plugin.name} installed`));
139
+ this.log(colors.green(`✓ ${plugin.name} installed`));
160
140
  } else {
161
141
  if (result.stdout) this.log(result.stdout);
162
- if (result.stderr) this.log(pc.dim(result.stderr));
163
- this.log(pc.yellow(`⚠ ${plugin.name} install exited with code ${result.exitCode}`));
142
+ if (result.stderr) this.log(colors.dim(result.stderr));
143
+ this.log(colors.yellow(`⚠ ${plugin.name} install exited with code ${result.exitCode}`));
164
144
  }
165
145
  } else {
166
- this.log(pc.dim(` Skipped ${plugin.name}`));
146
+ this.log(colors.dim(` Skipped ${plugin.name}`));
167
147
  }
168
148
  }
169
149
  }
170
150
 
171
- private saveClaudeSettings(settings: ClaudeSettings): void {
172
- const dir = path.dirname(CLAUDE_SETTINGS_PATH);
173
- if (!fs.existsSync(dir)) {
174
- fs.mkdirSync(dir, { recursive: true });
175
- }
176
- fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
177
- }
178
-
179
151
  private showOtelInstructions(): void {
180
- this.log(pc.cyan("Add these environment variables to your shell profile:\n"));
152
+ this.log(colors.cyan("Add these environment variables to your shell profile:\n"));
181
153
 
182
154
  consola.box(`export CLAUDE_CODE_ENABLE_TELEMETRY=1
183
155
  export OTEL_METRICS_EXPORTER=otlp
@@ -186,10 +158,10 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`);
186
158
 
187
159
  this.log("");
188
160
  this.log(
189
- pc.dim("For more info, see: https://github.com/anthropics/claude-code-monitoring-guide"),
161
+ colors.dim("For more info, see: https://github.com/anthropics/claude-code-monitoring-guide"),
190
162
  );
191
163
  this.log("");
192
- this.log(pc.cyan("Quick cost analysis (no setup required):"));
193
- this.log(pc.dim(" npx ccusage@latest --breakdown"));
164
+ this.log(colors.cyan("Quick cost analysis (no setup required):"));
165
+ this.log(colors.dim(" npx ccusage@latest --breakdown"));
194
166
  }
195
167
  }
@@ -11,7 +11,7 @@ import {
11
11
  ensureTemplatesExist,
12
12
  generateJournalFileInfoByType,
13
13
  openInEditor,
14
- } from "../../lib/journal/utils.js";
14
+ } from "../../lib/journal/index.js";
15
15
 
16
16
  /**
17
17
  * Create or open daily notes journal file
@@ -32,7 +32,7 @@ export default class DailyNotes extends BaseCommand {
32
32
  await this.parse(DailyNotes);
33
33
 
34
34
  try {
35
- const journalSettings = this.settings.settings.journalSettings;
35
+ const journalSettings = this.userSettings.journalSettings;
36
36
  const templateDir = journalSettings.templateDir;
37
37
 
38
38
  // Ensure templates exist on first run
@@ -58,7 +58,7 @@ export default class DailyNotes extends BaseCommand {
58
58
  }
59
59
 
60
60
  await openInEditor({
61
- editor: this.settings.settings.preferredEditor,
61
+ editor: this.userSettings.preferredEditor,
62
62
  filePath: fileInfo.fullPath,
63
63
  folderPath: journalSettings.baseFolder,
64
64
  });
@@ -12,7 +12,7 @@ import {
12
12
  ensureTemplatesExist,
13
13
  generateJournalFileInfoByType,
14
14
  openInEditor,
15
- } from "../../lib/journal/utils.js";
15
+ } from "../../lib/journal/index.js";
16
16
 
17
17
  /**
18
18
  * Create or open meeting notes file
@@ -43,7 +43,7 @@ export default class Meeting extends BaseCommand {
43
43
  const { args } = await this.parse(Meeting);
44
44
 
45
45
  try {
46
- const journalSettings = this.settings.settings.journalSettings;
46
+ const journalSettings = this.userSettings.journalSettings;
47
47
  const templateDir = journalSettings.templateDir;
48
48
 
49
49
  // Ensure templates exist on first run
@@ -77,7 +77,7 @@ export default class Meeting extends BaseCommand {
77
77
  }
78
78
 
79
79
  await openInEditor({
80
- editor: this.settings.settings.preferredEditor,
80
+ editor: this.userSettings.preferredEditor,
81
81
  filePath: fileInfo.fullPath,
82
82
  folderPath: journalSettings.baseFolder,
83
83
  });
@@ -12,7 +12,7 @@ import {
12
12
  ensureTemplatesExist,
13
13
  generateJournalFileInfoByType,
14
14
  openInEditor,
15
- } from "../../lib/journal/utils.js";
15
+ } from "../../lib/journal/index.js";
16
16
 
17
17
  /**
18
18
  * Create or open general-purpose note file
@@ -43,7 +43,7 @@ export default class Note extends BaseCommand {
43
43
  const { args } = await this.parse(Note);
44
44
 
45
45
  try {
46
- const journalSettings = this.settings.settings.journalSettings;
46
+ const journalSettings = this.userSettings.journalSettings;
47
47
  const templateDir = journalSettings.templateDir;
48
48
 
49
49
  // Ensure templates exist on first run
@@ -77,7 +77,7 @@ export default class Note extends BaseCommand {
77
77
  }
78
78
 
79
79
  await openInEditor({
80
- editor: this.settings.settings.preferredEditor,
80
+ editor: this.userSettings.preferredEditor,
81
81
  filePath: fileInfo.fullPath,
82
82
  folderPath: journalSettings.baseFolder,
83
83
  });
@@ -0,0 +1,183 @@
1
+ import { createInterface } from "node:readline";
2
+
3
+ import consola from "consola";
4
+ import pc from "picocolors";
5
+
6
+ import { getConfig } from "./config.js";
7
+ import { sleep } from "./shell.js";
8
+ import { spawnClaude } from "./spawn-claude.js";
9
+
10
+ // ── Claude CLI ──
11
+
12
+ export interface ClaudeResult {
13
+ result: string;
14
+ is_error: boolean;
15
+ total_cost_usd: number;
16
+ num_turns: number;
17
+ }
18
+
19
+ const PROCESS_RETRIES = 3;
20
+ const PROCESS_RETRY_DELAY_MS = 5_000;
21
+
22
+ export async function runClaude(opts: {
23
+ promptFile: string;
24
+ maxTurns?: number;
25
+ }): Promise<ClaudeResult> {
26
+ const cfg = getConfig();
27
+ const args = [
28
+ "-p",
29
+ "--output-format",
30
+ "stream-json",
31
+ "--verbose",
32
+ "--include-partial-messages",
33
+ "--dangerously-skip-permissions",
34
+ "--model",
35
+ cfg.model,
36
+ ...(opts.maxTurns ? ["--max-turns", String(opts.maxTurns)] : []),
37
+ `@${opts.promptFile}`,
38
+ ];
39
+
40
+ consola.info(
41
+ `${pc.dim("▶")} Calling Claude${opts.maxTurns ? ` (max ${opts.maxTurns} turns)` : ""}…`,
42
+ );
43
+
44
+ let lastError: Error | undefined;
45
+ for (let attempt = 1; attempt <= PROCESS_RETRIES; attempt++) {
46
+ try {
47
+ const result = await runClaudeStreaming(args);
48
+ consola.success(`Done — ${result.num_turns} turns, $${result.total_cost_usd.toFixed(4)}`);
49
+ if (result.result) {
50
+ consola.log(result.result);
51
+ }
52
+ return result;
53
+ } catch (err) {
54
+ lastError = err instanceof Error ? err : new Error(String(err));
55
+ if (attempt < PROCESS_RETRIES) {
56
+ consola.warn(
57
+ `Claude process failed (attempt ${attempt}/${PROCESS_RETRIES}), retrying in ${PROCESS_RETRY_DELAY_MS / 1000}s…`,
58
+ );
59
+ await sleep(PROCESS_RETRY_DELAY_MS);
60
+ }
61
+ }
62
+ }
63
+ throw lastError ?? new Error("runClaude failed after all retries");
64
+ }
65
+
66
+ function runClaudeStreaming(args: string[]): Promise<ClaudeResult> {
67
+ return new Promise((resolve, reject) => {
68
+ const proc = spawnClaude(args);
69
+ let capturedResult: ClaudeResult | null = null;
70
+ let turnCount = 0;
71
+
72
+ if (!proc.stdout) {
73
+ reject(new Error("Claude process has no stdout"));
74
+ return;
75
+ }
76
+
77
+ const rl = createInterface({ input: proc.stdout });
78
+
79
+ rl.on("line", (line) => {
80
+ if (!line.trim()) return;
81
+ try {
82
+ const event = JSON.parse(line) as Record<string, unknown>;
83
+ handleStreamEvent(event, (turns) => {
84
+ turnCount = turns;
85
+ });
86
+
87
+ if ("result" in event && "is_error" in event && "num_turns" in event) {
88
+ capturedResult = {
89
+ result: String(event.result ?? ""),
90
+ is_error: Boolean(event.is_error),
91
+ total_cost_usd: Number(event.total_cost_usd ?? 0),
92
+ num_turns: Number(event.num_turns),
93
+ };
94
+ }
95
+ } catch {
96
+ // Skip non-JSON lines
97
+ }
98
+ });
99
+
100
+ proc.on("error", (err) => {
101
+ rl.close();
102
+ reject(err);
103
+ });
104
+
105
+ proc.on("close", (code) => {
106
+ rl.close();
107
+ if (capturedResult) {
108
+ resolve(capturedResult);
109
+ } else if (code !== 0) {
110
+ reject(new Error(`Claude process exited with code ${code}`));
111
+ } else {
112
+ resolve({ result: "", is_error: false, total_cost_usd: 0, num_turns: turnCount });
113
+ }
114
+ });
115
+ });
116
+ }
117
+
118
+ function truncate(s: string, max: number): string {
119
+ return s.length > max ? s.slice(0, max) + "\u2026" : s;
120
+ }
121
+
122
+ function toolDetail(block: Record<string, unknown>): string {
123
+ const input =
124
+ typeof block.input === "object" && block.input !== null
125
+ ? (block.input as Record<string, unknown>)
126
+ : null;
127
+ if (!input) return "";
128
+
129
+ const filePath = input.file_path ?? input.path;
130
+ if (typeof filePath === "string") {
131
+ let detail = pc.dim(` ${filePath}`);
132
+ // Show edit context for Edit tool
133
+ if (typeof input.old_string === "string" && typeof input.new_string === "string") {
134
+ const old = truncate(input.old_string.split("\n")[0].trim(), 40);
135
+ const replacement = truncate(input.new_string.split("\n")[0].trim(), 40);
136
+ detail += pc.dim(` "${old}" → "${replacement}"`);
137
+ }
138
+ return detail;
139
+ }
140
+ if (typeof input.pattern === "string") return pc.dim(` ${input.pattern}`);
141
+ if (typeof input.command === "string") {
142
+ return pc.dim(` ${truncate(input.command, 60)}`);
143
+ }
144
+ // TodoWrite/TaskCreate — show subject
145
+ if (typeof input.subject === "string") return pc.dim(` ${truncate(input.subject, 60)}`);
146
+ return "";
147
+ }
148
+
149
+ function logToolUse(block: Record<string, unknown>): void {
150
+ const name = block.name;
151
+ if (typeof name === "string") {
152
+ consola.info(` ${pc.dim("\u21B3")} ${name}${toolDetail(block)}`);
153
+ }
154
+ }
155
+
156
+ function handleStreamEvent(event: Record<string, unknown>, onTurn: (count: number) => void): void {
157
+ // Only handle stream_event — assistant turn events duplicate the same tools
158
+ if (event.type === "stream_event" && typeof event.event === "object" && event.event !== null) {
159
+ const inner = event.event as Record<string, unknown>;
160
+
161
+ if (
162
+ inner.type === "content_block_start" &&
163
+ typeof inner.content_block === "object" &&
164
+ inner.content_block !== null
165
+ ) {
166
+ const block = inner.content_block as Record<string, unknown>;
167
+ if (block.type === "tool_use") {
168
+ logToolUse(block);
169
+ } else if (block.type === "thinking") {
170
+ const thinkingText =
171
+ typeof block.thinking === "string" && block.thinking.length > 0
172
+ ? pc.dim(` ${truncate(block.thinking.split("\n")[0].trim(), 60)}`)
173
+ : "";
174
+ consola.info(` ${pc.dim("\u21B3")} ${pc.italic("thinking")}${thinkingText}`);
175
+ }
176
+ }
177
+ }
178
+
179
+ // Turn count tracking
180
+ if (typeof event.num_turns === "number" && !("result" in event)) {
181
+ onTurn(event.num_turns as number);
182
+ }
183
+ }
@@ -12,11 +12,9 @@ describe("AutoClaudeConfigSchema", () => {
12
12
  expect(cfg.remote).toBe("origin");
13
13
  expect(cfg.maxImplementIterations).toBe(5);
14
14
  expect(cfg.maxTurns).toBeUndefined();
15
+ expect(cfg.model).toBe("opus");
16
+ expect(cfg.maxReviewRetries).toBe(2);
15
17
  expect(cfg.loopIntervalMinutes).toBe(30);
16
- expect(cfg.loopRetryEnabled).toBe(false);
17
- expect(cfg.maxRetries).toBe(5);
18
- expect(cfg.retryDelayMs).toBe(30_000);
19
- expect(cfg.maxRetryDelayMs).toBe(300_000);
20
18
  });
21
19
 
22
20
  it("should allow overriding defaults", () => {
@@ -24,14 +22,14 @@ describe("AutoClaudeConfigSchema", () => {
24
22
  repo: "owner/repo",
25
23
  triggerLabel: "bot",
26
24
  maxImplementIterations: 10,
27
- maxRetries: 3,
28
- loopRetryEnabled: true,
25
+ model: "sonnet",
26
+ maxReviewRetries: 5,
29
27
  });
30
28
 
31
29
  expect(cfg.triggerLabel).toBe("bot");
32
30
  expect(cfg.maxImplementIterations).toBe(10);
33
- expect(cfg.maxRetries).toBe(3);
34
- expect(cfg.loopRetryEnabled).toBe(true);
31
+ expect(cfg.model).toBe("sonnet");
32
+ expect(cfg.maxReviewRetries).toBe(5);
35
33
  });
36
34
 
37
35
  it("should require repo field", () => {