@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.
- package/package.json +50 -57
- package/src/commands/agentboard.ts +176 -0
- package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
- package/src/commands/auto-claude/list.ts +114 -0
- package/src/commands/auto-claude/retry.test.ts +138 -0
- package/src/commands/auto-claude/retry.ts +139 -0
- package/src/commands/auto-claude/status.test.ts +147 -0
- package/src/commands/auto-claude/status.ts +123 -0
- package/src/commands/base.ts +7 -2
- package/src/commands/config.ts +5 -7
- package/src/commands/doctor.ts +111 -12
- package/src/commands/gh/branch.ts +4 -4
- package/src/commands/gh/pr.ts +1 -0
- package/src/commands/graph/index.ts +169 -0
- package/src/commands/graph.test.ts +1 -1
- package/src/commands/install.ts +40 -68
- package/src/commands/journal/daily-notes.ts +3 -3
- package/src/commands/journal/meeting.ts +3 -3
- package/src/commands/journal/note.ts +3 -3
- package/src/lib/auto-claude/claude-cli.ts +183 -0
- package/src/lib/auto-claude/config.test.ts +6 -8
- package/src/lib/auto-claude/config.ts +3 -4
- package/src/lib/auto-claude/index.ts +2 -3
- package/src/lib/auto-claude/labels.test.ts +85 -0
- package/src/lib/auto-claude/labels.ts +42 -0
- package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
- package/src/lib/auto-claude/pipeline.test.ts +2 -2
- package/src/lib/auto-claude/pipeline.ts +120 -36
- package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
- package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
- package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
- package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
- package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
- package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
- package/src/lib/auto-claude/run-claude.test.ts +48 -68
- package/src/lib/auto-claude/shell.ts +6 -0
- package/src/lib/auto-claude/steps/create-pr.ts +89 -25
- package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
- package/src/lib/auto-claude/steps/implement.ts +9 -16
- package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
- package/src/lib/auto-claude/steps/steps.test.ts +68 -63
- package/src/lib/auto-claude/templates.test.ts +91 -0
- package/src/lib/auto-claude/templates.ts +34 -0
- package/src/lib/auto-claude/test-helpers.ts +2 -1
- package/src/lib/auto-claude/utils-execution.test.ts +9 -57
- package/src/lib/auto-claude/utils.test.ts +5 -9
- package/src/lib/auto-claude/utils.ts +27 -253
- package/src/lib/graph/analyzer.test.ts +451 -0
- package/src/lib/graph/analyzer.ts +165 -0
- package/src/lib/graph/index.ts +24 -0
- package/src/lib/graph/labels.ts +87 -0
- package/src/lib/graph/parser.test.ts +150 -0
- package/src/lib/graph/parser.ts +65 -0
- package/src/lib/graph/render.ts +25 -0
- package/src/lib/graph/server.ts +70 -0
- package/src/lib/graph/sessions.ts +104 -0
- package/src/lib/graph/tools.ts +90 -0
- package/src/lib/graph/treemap.ts +211 -0
- package/src/lib/graph/types.ts +80 -0
- package/src/lib/install/claude-settings.ts +64 -0
- package/src/lib/journal/editor.ts +33 -0
- package/src/lib/journal/fs.ts +13 -0
- package/src/lib/journal/index.ts +11 -0
- package/src/lib/journal/paths.ts +106 -0
- package/src/lib/journal/{utils.ts → templates.ts} +3 -151
- package/src/utils/fs.ts +19 -0
- package/src/utils/git/exec.ts +18 -0
- package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
- package/src/utils/git/gh-cli-wrapper.ts +31 -19
- package/src/utils/render.ts +3 -1
- package/src/commands/graph.ts +0 -970
- package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
- package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
- package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
- package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
- package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
- package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
- package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
- package/src/lib/auto-claude/steps/plan.ts +0 -14
- package/src/lib/auto-claude/steps/refresh.ts +0 -114
- package/src/lib/auto-claude/steps/remove-label.ts +0 -22
- package/src/lib/auto-claude/steps/research.ts +0 -21
- package/src/lib/auto-claude/steps/review.ts +0 -14
package/src/commands/gh/pr.ts
CHANGED
|
@@ -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
|
|
5
|
+
import { analyzeSession, calculateCutoffMs, filterByDays } from "../lib/graph/index.js";
|
|
6
6
|
|
|
7
7
|
describe("graph --days filtering", () => {
|
|
8
8
|
describe("calculateCutoffMs", () => {
|
package/src/commands/install.ts
CHANGED
|
@@ -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
|
|
2
|
+
import { colors } from "consola/utils";
|
|
6
3
|
import consola from "consola";
|
|
7
4
|
import { BaseCommand } from "./base.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
42
|
+
this.log(colors.bold("\n🔧 towles-tool install\n"));
|
|
49
43
|
|
|
50
44
|
// Load or create Claude settings
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
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(
|
|
49
|
+
this.log(colors.dim(`No Claude settings file found, will create one`));
|
|
64
50
|
}
|
|
65
51
|
|
|
66
|
-
//
|
|
67
|
-
|
|
52
|
+
// Apply recommended settings
|
|
53
|
+
const { settings, changes } = applyRecommendedSettings(existing);
|
|
68
54
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
this.log(
|
|
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(
|
|
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(
|
|
80
|
+
this.log(colors.bold("\n📦 Claude Plugins\n"));
|
|
101
81
|
await this.ensureClaudePlugins();
|
|
102
82
|
|
|
103
|
-
this.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
163
|
-
this.log(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
161
|
+
colors.dim("For more info, see: https://github.com/anthropics/claude-code-monitoring-guide"),
|
|
190
162
|
);
|
|
191
163
|
this.log("");
|
|
192
|
-
this.log(
|
|
193
|
-
this.log(
|
|
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/
|
|
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.
|
|
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.
|
|
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/
|
|
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.
|
|
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.
|
|
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/
|
|
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.
|
|
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.
|
|
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
|
-
|
|
28
|
-
|
|
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.
|
|
34
|
-
expect(cfg.
|
|
31
|
+
expect(cfg.model).toBe("sonnet");
|
|
32
|
+
expect(cfg.maxReviewRetries).toBe(5);
|
|
35
33
|
});
|
|
36
34
|
|
|
37
35
|
it("should require repo field", () => {
|