@towles/tool 0.0.20 → 0.0.41
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 +21 -0
- package/LICENSE.md +9 -10
- package/README.md +121 -78
- package/bin/run.ts +5 -0
- package/package.json +63 -53
- package/patches/prompts.patch +34 -0
- package/src/commands/base.ts +42 -0
- package/src/commands/config.test.ts +15 -0
- package/src/commands/config.ts +43 -0
- package/src/commands/doctor.ts +133 -0
- package/src/commands/gh/branch-clean.ts +110 -0
- package/src/commands/gh/branch.test.ts +124 -0
- package/src/commands/gh/branch.ts +132 -0
- package/src/commands/gh/pr.ts +168 -0
- package/src/commands/index.ts +55 -0
- package/src/commands/install.ts +148 -0
- package/src/commands/journal/daily-notes.ts +66 -0
- package/src/commands/journal/meeting.ts +83 -0
- package/src/commands/journal/note.ts +83 -0
- package/src/commands/journal/utils.ts +399 -0
- package/src/commands/observe/graph.test.ts +89 -0
- package/src/commands/observe/graph.ts +1640 -0
- package/src/commands/observe/report.ts +166 -0
- package/src/commands/observe/session.ts +385 -0
- package/src/commands/observe/setup.ts +180 -0
- package/src/commands/observe/status.ts +146 -0
- package/src/commands/ralph/lib/execution.ts +302 -0
- package/src/commands/ralph/lib/formatter.ts +298 -0
- package/src/commands/ralph/lib/index.ts +4 -0
- package/src/commands/ralph/lib/marker.ts +108 -0
- package/src/commands/ralph/lib/state.ts +191 -0
- package/src/commands/ralph/marker/create.ts +23 -0
- package/src/commands/ralph/plan.ts +73 -0
- package/src/commands/ralph/progress.ts +44 -0
- package/src/commands/ralph/ralph.test.ts +673 -0
- package/src/commands/ralph/run.ts +408 -0
- package/src/commands/ralph/task/add.ts +105 -0
- package/src/commands/ralph/task/done.ts +73 -0
- package/src/commands/ralph/task/list.test.ts +48 -0
- package/src/commands/ralph/task/list.ts +110 -0
- package/src/commands/ralph/task/remove.ts +62 -0
- package/src/config/context.ts +7 -0
- package/src/config/settings.ts +155 -0
- package/src/constants.ts +3 -0
- package/src/types/journal.ts +16 -0
- package/src/utils/anthropic/types.ts +158 -0
- package/src/utils/date-utils.test.ts +96 -0
- package/src/utils/date-utils.ts +54 -0
- package/src/utils/exec.ts +8 -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/git/git.ts +25 -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,180 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import consola from "consola";
|
|
6
|
+
import { BaseCommand } from "../base.js";
|
|
7
|
+
|
|
8
|
+
const CLAUDE_DIR = path.join(homedir(), ".claude");
|
|
9
|
+
const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
10
|
+
const REPORTS_DIR = path.join(CLAUDE_DIR, "reports");
|
|
11
|
+
|
|
12
|
+
interface ClaudeSettings {
|
|
13
|
+
cleanupPeriodDays?: number;
|
|
14
|
+
alwaysThinkingEnabled?: boolean;
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
hooks?: {
|
|
17
|
+
SubagentStop?: Array<{
|
|
18
|
+
matcher?: Record<string, unknown>;
|
|
19
|
+
hooks?: Array<{
|
|
20
|
+
type: string;
|
|
21
|
+
command: string;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const OTEL_ENV_VARS: Record<string, string> = {
|
|
30
|
+
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
|
|
31
|
+
OTEL_METRICS_EXPORTER: "otlp",
|
|
32
|
+
OTEL_LOGS_EXPORTER: "otlp",
|
|
33
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4317",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Configure observability settings for Claude Code
|
|
38
|
+
*/
|
|
39
|
+
export default class ObserveSetup extends BaseCommand {
|
|
40
|
+
static override description = "Configure Claude Code observability settings";
|
|
41
|
+
|
|
42
|
+
static override examples = [
|
|
43
|
+
"<%= config.bin %> observe setup",
|
|
44
|
+
"<%= config.bin %> observe setup # Adds SubagentStop hook for lineage tracking",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
async run(): Promise<void> {
|
|
48
|
+
await this.parse(ObserveSetup);
|
|
49
|
+
|
|
50
|
+
this.log(pc.bold("\n📊 Claude Code Observability Setup\n"));
|
|
51
|
+
|
|
52
|
+
// Load or create Claude settings
|
|
53
|
+
let claudeSettings: ClaudeSettings = {};
|
|
54
|
+
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
57
|
+
claudeSettings = JSON.parse(content);
|
|
58
|
+
this.log(pc.dim(`Found existing Claude settings at ${CLAUDE_SETTINGS_PATH}`));
|
|
59
|
+
} catch {
|
|
60
|
+
this.log(
|
|
61
|
+
pc.yellow(`Warning: Could not parse ${CLAUDE_SETTINGS_PATH}, will create fresh settings`),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
this.log(pc.dim(`No Claude settings file found, will create one`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let modified = false;
|
|
69
|
+
|
|
70
|
+
// 1. Ensure cleanupPeriodDays is set to prevent log deletion
|
|
71
|
+
if (claudeSettings.cleanupPeriodDays !== 99999) {
|
|
72
|
+
claudeSettings.cleanupPeriodDays = 99999;
|
|
73
|
+
modified = true;
|
|
74
|
+
this.log(pc.green("✓ Set cleanupPeriodDays: 99999 (prevent log deletion)"));
|
|
75
|
+
} else {
|
|
76
|
+
this.log(pc.dim("✓ cleanupPeriodDays already set to 99999"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Configure SubagentStop hook for lineage tracking
|
|
80
|
+
const subagentLogPath = path.join(REPORTS_DIR, "subagent-log.jsonl");
|
|
81
|
+
const subagentHookCommand = `jq -c '. + {parent: env.SESSION_ID, timestamp: now}' >> ${subagentLogPath}`;
|
|
82
|
+
|
|
83
|
+
if (!claudeSettings.hooks) {
|
|
84
|
+
claudeSettings.hooks = {};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const existingSubagentHook = claudeSettings.hooks.SubagentStop;
|
|
88
|
+
const hasSubagentHook =
|
|
89
|
+
existingSubagentHook &&
|
|
90
|
+
Array.isArray(existingSubagentHook) &&
|
|
91
|
+
existingSubagentHook.length > 0;
|
|
92
|
+
|
|
93
|
+
if (!hasSubagentHook) {
|
|
94
|
+
claudeSettings.hooks.SubagentStop = [
|
|
95
|
+
{
|
|
96
|
+
hooks: [
|
|
97
|
+
{
|
|
98
|
+
type: "command",
|
|
99
|
+
command: subagentHookCommand,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
modified = true;
|
|
105
|
+
this.log(pc.green("✓ Added SubagentStop hook for subagent lineage tracking"));
|
|
106
|
+
} else {
|
|
107
|
+
this.log(pc.dim("✓ SubagentStop hook already configured"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 3. Add OTEL environment variables to settings
|
|
111
|
+
if (!claudeSettings.env) {
|
|
112
|
+
claudeSettings.env = {};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const addedVars: string[] = [];
|
|
116
|
+
const skippedVars: string[] = [];
|
|
117
|
+
for (const [key, value] of Object.entries(OTEL_ENV_VARS)) {
|
|
118
|
+
if (claudeSettings.env[key] === undefined) {
|
|
119
|
+
claudeSettings.env[key] = value;
|
|
120
|
+
addedVars.push(key);
|
|
121
|
+
modified = true;
|
|
122
|
+
} else {
|
|
123
|
+
skippedVars.push(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (addedVars.length > 0) {
|
|
128
|
+
this.log(pc.green(`✓ Added env vars: ${addedVars.join(", ")}`));
|
|
129
|
+
}
|
|
130
|
+
if (skippedVars.length > 0) {
|
|
131
|
+
this.log(pc.dim(`✓ Env vars already set: ${skippedVars.join(", ")}`));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Save settings if modified
|
|
135
|
+
if (modified) {
|
|
136
|
+
this.saveClaudeSettings(claudeSettings);
|
|
137
|
+
this.log(pc.green(`\n✓ Saved settings to ${CLAUDE_SETTINGS_PATH}`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 4. Create reports directory
|
|
141
|
+
if (!fs.existsSync(REPORTS_DIR)) {
|
|
142
|
+
fs.mkdirSync(REPORTS_DIR, { recursive: true });
|
|
143
|
+
this.log(pc.green(`✓ Created reports directory at ${REPORTS_DIR}`));
|
|
144
|
+
} else {
|
|
145
|
+
this.log(pc.dim(`✓ Reports directory exists at ${REPORTS_DIR}`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 5. Show OTEL environment variables setup
|
|
149
|
+
this.log(pc.bold("\n🔧 OTEL Environment Variables\n"));
|
|
150
|
+
this.log(pc.cyan("Add these to your shell profile (~/.bashrc, ~/.zshrc, etc.):\n"));
|
|
151
|
+
|
|
152
|
+
consola.box(`export CLAUDE_CODE_ENABLE_TELEMETRY=1
|
|
153
|
+
export OTEL_METRICS_EXPORTER=otlp
|
|
154
|
+
export OTEL_LOGS_EXPORTER=otlp
|
|
155
|
+
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317`);
|
|
156
|
+
|
|
157
|
+
this.log("");
|
|
158
|
+
this.log(pc.dim("For a full monitoring stack, see:"));
|
|
159
|
+
this.log(pc.dim(" https://github.com/anthropics/claude-code-monitoring-guide"));
|
|
160
|
+
this.log("");
|
|
161
|
+
|
|
162
|
+
// Quick usage tips
|
|
163
|
+
this.log(pc.bold("📈 Quick Analysis Commands\n"));
|
|
164
|
+
this.log(pc.dim(" tt observe status # Check current config"));
|
|
165
|
+
this.log(pc.dim(" tt observe report # Token/cost breakdown"));
|
|
166
|
+
this.log(pc.dim(" tt observe session # List sessions"));
|
|
167
|
+
this.log(pc.dim(" tt observe graph # Visualize token usage"));
|
|
168
|
+
this.log("");
|
|
169
|
+
|
|
170
|
+
this.log(pc.bold(pc.green("✅ Observability setup complete!\n")));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private saveClaudeSettings(settings: ClaudeSettings): void {
|
|
174
|
+
const dir = path.dirname(CLAUDE_SETTINGS_PATH);
|
|
175
|
+
if (!fs.existsSync(dir)) {
|
|
176
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { BaseCommand } from "../base.js";
|
|
6
|
+
|
|
7
|
+
const CLAUDE_DIR = path.join(homedir(), ".claude");
|
|
8
|
+
const CLAUDE_SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
|
|
9
|
+
const REPORTS_DIR = path.join(CLAUDE_DIR, "reports");
|
|
10
|
+
|
|
11
|
+
interface ClaudeSettings {
|
|
12
|
+
cleanupPeriodDays?: number;
|
|
13
|
+
alwaysThinkingEnabled?: boolean;
|
|
14
|
+
hooks?: {
|
|
15
|
+
SubagentStop?: unknown[];
|
|
16
|
+
PreToolUse?: unknown[];
|
|
17
|
+
PostToolUse?: unknown[];
|
|
18
|
+
Stop?: unknown[];
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Display current observability configuration status
|
|
26
|
+
*/
|
|
27
|
+
export default class ObserveStatus extends BaseCommand {
|
|
28
|
+
static override description = "Display current observability configuration status";
|
|
29
|
+
|
|
30
|
+
static override examples = [
|
|
31
|
+
"<%= config.bin %> observe status",
|
|
32
|
+
"<%= config.bin %> observe status # Check if observability is properly configured",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
async run(): Promise<void> {
|
|
36
|
+
await this.parse(ObserveStatus);
|
|
37
|
+
|
|
38
|
+
this.log(pc.bold("\n📊 Observability Status\n"));
|
|
39
|
+
|
|
40
|
+
// Load Claude settings
|
|
41
|
+
let settings: ClaudeSettings = {};
|
|
42
|
+
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) {
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
45
|
+
settings = JSON.parse(content);
|
|
46
|
+
} catch {
|
|
47
|
+
this.log(pc.red(`✗ Could not parse ${CLAUDE_SETTINGS_PATH}`));
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
this.log(pc.yellow(`⚠ No settings file at ${CLAUDE_SETTINGS_PATH}`));
|
|
51
|
+
this.log(pc.dim(" Run: tt observe setup"));
|
|
52
|
+
this.log("");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 1. Claude Settings
|
|
56
|
+
this.log(pc.bold("Claude Settings"));
|
|
57
|
+
this.log(pc.dim(` Path: ${CLAUDE_SETTINGS_PATH}\n`));
|
|
58
|
+
|
|
59
|
+
// cleanupPeriodDays
|
|
60
|
+
const cleanup = settings.cleanupPeriodDays;
|
|
61
|
+
if (cleanup === 99999) {
|
|
62
|
+
this.log(pc.green(" ✓ cleanupPeriodDays: 99999 (logs preserved)"));
|
|
63
|
+
} else if (cleanup !== undefined) {
|
|
64
|
+
this.log(pc.yellow(` ⚠ cleanupPeriodDays: ${cleanup} (logs may be deleted)`));
|
|
65
|
+
} else {
|
|
66
|
+
this.log(pc.red(" ✗ cleanupPeriodDays: not set (default cleanup applies)"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// alwaysThinkingEnabled
|
|
70
|
+
if (settings.alwaysThinkingEnabled) {
|
|
71
|
+
this.log(pc.green(" ✓ alwaysThinkingEnabled: true"));
|
|
72
|
+
} else {
|
|
73
|
+
this.log(pc.dim(" ○ alwaysThinkingEnabled: false"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.log("");
|
|
77
|
+
|
|
78
|
+
// 2. Hooks
|
|
79
|
+
this.log(pc.bold("Hooks Configured"));
|
|
80
|
+
const hooks = settings.hooks || {};
|
|
81
|
+
const hookNames = ["SubagentStop", "PreToolUse", "PostToolUse", "Stop"];
|
|
82
|
+
let hasAnyHook = false;
|
|
83
|
+
|
|
84
|
+
for (const name of hookNames) {
|
|
85
|
+
const hook = hooks[name];
|
|
86
|
+
if (hook && Array.isArray(hook) && hook.length > 0) {
|
|
87
|
+
this.log(pc.green(` ✓ ${name}: ${hook.length} handler(s)`));
|
|
88
|
+
hasAnyHook = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for other hooks
|
|
93
|
+
const otherHooks = Object.keys(hooks).filter((k) => !hookNames.includes(k));
|
|
94
|
+
for (const name of otherHooks) {
|
|
95
|
+
const hook = hooks[name];
|
|
96
|
+
if (hook && Array.isArray(hook) && hook.length > 0) {
|
|
97
|
+
this.log(pc.green(` ✓ ${name}: ${(hook as unknown[]).length} handler(s)`));
|
|
98
|
+
hasAnyHook = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!hasAnyHook) {
|
|
103
|
+
this.log(pc.dim(" ○ No hooks configured"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.log("");
|
|
107
|
+
|
|
108
|
+
// 3. Reports Directory
|
|
109
|
+
this.log(pc.bold("Reports Directory"));
|
|
110
|
+
if (fs.existsSync(REPORTS_DIR)) {
|
|
111
|
+
const files = fs.readdirSync(REPORTS_DIR);
|
|
112
|
+
this.log(pc.green(` ✓ ${REPORTS_DIR}`));
|
|
113
|
+
this.log(pc.dim(` ${files.length} file(s)`));
|
|
114
|
+
} else {
|
|
115
|
+
this.log(pc.yellow(` ⚠ ${REPORTS_DIR} does not exist`));
|
|
116
|
+
this.log(pc.dim(" Run: tt observe setup"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.log("");
|
|
120
|
+
|
|
121
|
+
// 4. OTEL Environment Variables
|
|
122
|
+
this.log(pc.bold("OTEL Environment Variables"));
|
|
123
|
+
|
|
124
|
+
const otelVars = [
|
|
125
|
+
{ name: "CLAUDE_CODE_ENABLE_TELEMETRY", expected: "1" },
|
|
126
|
+
{ name: "OTEL_METRICS_EXPORTER", expected: "otlp" },
|
|
127
|
+
{ name: "OTEL_LOGS_EXPORTER", expected: "otlp" },
|
|
128
|
+
{ name: "OTEL_EXPORTER_OTLP_ENDPOINT", expected: undefined },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const { name, expected } of otelVars) {
|
|
132
|
+
const value = process.env[name];
|
|
133
|
+
if (value) {
|
|
134
|
+
if (expected && value !== expected) {
|
|
135
|
+
this.log(pc.yellow(` ⚠ ${name}=${value} (expected: ${expected})`));
|
|
136
|
+
} else {
|
|
137
|
+
this.log(pc.green(` ✓ ${name}=${value}`));
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
this.log(pc.dim(` ○ ${name}: not set`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.log("");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { WriteStream } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { x } from "tinyexec";
|
|
5
|
+
import { CLAUDE_DEFAULT_ARGS } from "./state.js";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Types
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
interface StreamEvent {
|
|
12
|
+
type: string;
|
|
13
|
+
event?: {
|
|
14
|
+
type: string;
|
|
15
|
+
delta?: { text?: string };
|
|
16
|
+
};
|
|
17
|
+
// New format: assistant message
|
|
18
|
+
message?: {
|
|
19
|
+
content?: Array<{ type: string; text?: string }>;
|
|
20
|
+
usage?: {
|
|
21
|
+
input_tokens?: number;
|
|
22
|
+
output_tokens?: number;
|
|
23
|
+
cache_read_input_tokens?: number;
|
|
24
|
+
cache_creation_input_tokens?: number;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
result?: string;
|
|
28
|
+
total_cost_usd?: number;
|
|
29
|
+
num_turns?: number;
|
|
30
|
+
session_id?: string;
|
|
31
|
+
usage?: {
|
|
32
|
+
input_tokens?: number;
|
|
33
|
+
output_tokens?: number;
|
|
34
|
+
cache_read_input_tokens?: number;
|
|
35
|
+
cache_creation_input_tokens?: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Claude model context windows (tokens)
|
|
40
|
+
const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
41
|
+
"claude-sonnet-4-20250514": 200000,
|
|
42
|
+
"claude-opus-4-20250514": 200000,
|
|
43
|
+
"claude-3-5-sonnet-20241022": 200000,
|
|
44
|
+
"claude-3-opus-20240229": 200000,
|
|
45
|
+
default: 200000,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface IterationResult {
|
|
49
|
+
output: string;
|
|
50
|
+
exitCode: number;
|
|
51
|
+
contextUsedPercent?: number;
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ParsedLine {
|
|
56
|
+
text: string | null;
|
|
57
|
+
tool?: { name: string; summary: string };
|
|
58
|
+
usage?: StreamEvent["usage"];
|
|
59
|
+
sessionId?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================================
|
|
63
|
+
// Claude CLI Check
|
|
64
|
+
// ============================================================================
|
|
65
|
+
|
|
66
|
+
export async function checkClaudeCli(): Promise<boolean> {
|
|
67
|
+
try {
|
|
68
|
+
const result = await x("which", ["claude"]);
|
|
69
|
+
return result.exitCode === 0;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Stream Parsing
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
// Track accumulated text from assistant messages to compute deltas
|
|
80
|
+
let lastAssistantText = "";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reset stream parsing state between iterations.
|
|
84
|
+
*/
|
|
85
|
+
export function resetStreamState(): void {
|
|
86
|
+
lastAssistantText = "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function summarizeTool(name: string, input: Record<string, unknown>): string {
|
|
90
|
+
switch (name) {
|
|
91
|
+
case "Read":
|
|
92
|
+
return (
|
|
93
|
+
String(input.file_path || input.path || "")
|
|
94
|
+
.split("/")
|
|
95
|
+
.pop() || "file"
|
|
96
|
+
);
|
|
97
|
+
case "Write":
|
|
98
|
+
case "Edit":
|
|
99
|
+
return (
|
|
100
|
+
String(input.file_path || input.path || "")
|
|
101
|
+
.split("/")
|
|
102
|
+
.pop() || "file"
|
|
103
|
+
);
|
|
104
|
+
case "Glob":
|
|
105
|
+
return String(input.pattern || "");
|
|
106
|
+
case "Grep":
|
|
107
|
+
return String(input.pattern || "");
|
|
108
|
+
case "Bash":
|
|
109
|
+
return String(input.command || "").substring(0, 40);
|
|
110
|
+
case "TodoWrite":
|
|
111
|
+
return "updating todos";
|
|
112
|
+
default:
|
|
113
|
+
return Object.values(input)[0]?.toString().substring(0, 30) || "";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseStreamLine(line: string): ParsedLine {
|
|
118
|
+
if (!line.trim()) return { text: null };
|
|
119
|
+
try {
|
|
120
|
+
const data = JSON.parse(line) as StreamEvent & {
|
|
121
|
+
tool_use?: { name: string; input: Record<string, unknown> };
|
|
122
|
+
content_block?: { type: string; name?: string; input?: Record<string, unknown> };
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Handle tool_use events
|
|
126
|
+
if (data.type === "tool_use" && data.tool_use) {
|
|
127
|
+
const name = data.tool_use.name;
|
|
128
|
+
const summary = summarizeTool(name, data.tool_use.input || {});
|
|
129
|
+
return { text: null, tool: { name, summary } };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handle content_block with tool_use (streaming format)
|
|
133
|
+
if (data.type === "content_block" && data.content_block?.type === "tool_use") {
|
|
134
|
+
const name = data.content_block.name || "Tool";
|
|
135
|
+
const summary = summarizeTool(name, data.content_block.input || {});
|
|
136
|
+
return { text: null, tool: { name, summary } };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract text from streaming deltas (legacy format)
|
|
140
|
+
if (data.type === "stream_event" && data.event?.type === "content_block_delta") {
|
|
141
|
+
return { text: data.event.delta?.text || null };
|
|
142
|
+
}
|
|
143
|
+
// Add newline after content block ends (legacy format)
|
|
144
|
+
if (data.type === "stream_event" && data.event?.type === "content_block_stop") {
|
|
145
|
+
return { text: "\n" };
|
|
146
|
+
}
|
|
147
|
+
// NEW FORMAT: Handle assistant messages with content array
|
|
148
|
+
if (data.type === "assistant" && data.message) {
|
|
149
|
+
// Check for tool_use in content blocks
|
|
150
|
+
const toolBlocks = data.message.content?.filter((c) => c.type === "tool_use") || [];
|
|
151
|
+
if (toolBlocks.length > 0) {
|
|
152
|
+
const tb = toolBlocks[toolBlocks.length - 1] as {
|
|
153
|
+
name?: string;
|
|
154
|
+
input?: Record<string, unknown>;
|
|
155
|
+
};
|
|
156
|
+
const name = tb.name || "Tool";
|
|
157
|
+
const summary = summarizeTool(name, tb.input || {});
|
|
158
|
+
return {
|
|
159
|
+
text: null,
|
|
160
|
+
tool: { name, summary },
|
|
161
|
+
usage: data.message.usage || data.usage,
|
|
162
|
+
sessionId: data.session_id,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Extract full text from content blocks
|
|
167
|
+
const fullText =
|
|
168
|
+
data.message.content
|
|
169
|
+
?.filter((c) => c.type === "text" && c.text)
|
|
170
|
+
.map((c) => c.text)
|
|
171
|
+
.join("") || "";
|
|
172
|
+
|
|
173
|
+
// Compute delta (only new portion) to avoid duplicate output
|
|
174
|
+
let delta: string | null = null;
|
|
175
|
+
if (fullText.startsWith(lastAssistantText)) {
|
|
176
|
+
delta = fullText.slice(lastAssistantText.length) || null;
|
|
177
|
+
} else {
|
|
178
|
+
// Text doesn't match prefix - new context
|
|
179
|
+
delta = fullText || null;
|
|
180
|
+
}
|
|
181
|
+
lastAssistantText = fullText;
|
|
182
|
+
|
|
183
|
+
return { text: delta, usage: data.message.usage || data.usage, sessionId: data.session_id };
|
|
184
|
+
}
|
|
185
|
+
// Capture final result with usage and session_id
|
|
186
|
+
if (data.type === "result") {
|
|
187
|
+
const resultText = data.result
|
|
188
|
+
? `\n[Result: ${data.result.substring(0, 100)}${data.result.length > 100 ? "..." : ""}]\n`
|
|
189
|
+
: null;
|
|
190
|
+
return { text: resultText, usage: data.usage, sessionId: data.session_id };
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// Not JSON, return raw
|
|
194
|
+
return { text: line };
|
|
195
|
+
}
|
|
196
|
+
return { text: null };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Run Iteration
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
export async function runIteration(
|
|
204
|
+
prompt: string,
|
|
205
|
+
claudeArgs: string[],
|
|
206
|
+
logStream?: WriteStream,
|
|
207
|
+
): Promise<IterationResult> {
|
|
208
|
+
// Reset accumulated text state from previous iteration
|
|
209
|
+
resetStreamState();
|
|
210
|
+
|
|
211
|
+
// Pass task context as system prompt via --append-system-prompt
|
|
212
|
+
// 'continue' is the user prompt - required by claude CLI when using --print
|
|
213
|
+
const allArgs = [
|
|
214
|
+
...CLAUDE_DEFAULT_ARGS,
|
|
215
|
+
...claudeArgs,
|
|
216
|
+
"--append-system-prompt",
|
|
217
|
+
prompt,
|
|
218
|
+
"continue",
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
let output = "";
|
|
222
|
+
let lineBuffer = "";
|
|
223
|
+
let finalUsage: StreamEvent["usage"] | undefined;
|
|
224
|
+
let sessionId: string | undefined;
|
|
225
|
+
let lastCharWasNewline = true;
|
|
226
|
+
|
|
227
|
+
const processLine = (line: string) => {
|
|
228
|
+
const { text: parsed, tool, usage, sessionId: sid } = parseStreamLine(line);
|
|
229
|
+
if (usage) finalUsage = usage;
|
|
230
|
+
if (sid) sessionId = sid;
|
|
231
|
+
if (tool) {
|
|
232
|
+
const prefix = lastCharWasNewline ? "" : "\n";
|
|
233
|
+
const toolLine = `${prefix}${pc.yellow("⚡")} ${pc.cyan(tool.name)}: ${tool.summary}\n`;
|
|
234
|
+
process.stdout.write(toolLine);
|
|
235
|
+
logStream?.write(`${prefix}⚡ ${tool.name}: ${tool.summary}\n`);
|
|
236
|
+
lastCharWasNewline = true;
|
|
237
|
+
}
|
|
238
|
+
if (parsed) {
|
|
239
|
+
process.stdout.write(parsed);
|
|
240
|
+
logStream?.write(parsed);
|
|
241
|
+
output += parsed;
|
|
242
|
+
lastCharWasNewline = parsed.endsWith("\n");
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return new Promise((resolve) => {
|
|
247
|
+
const proc = spawn("claude", allArgs, {
|
|
248
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
proc.stdout.on("data", (chunk: Buffer) => {
|
|
252
|
+
const text = chunk.toString();
|
|
253
|
+
lineBuffer += text;
|
|
254
|
+
|
|
255
|
+
const lines = lineBuffer.split("\n");
|
|
256
|
+
lineBuffer = lines.pop() || "";
|
|
257
|
+
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
processLine(line);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
proc.stderr.on("data", (chunk: Buffer) => {
|
|
264
|
+
const text = chunk.toString();
|
|
265
|
+
process.stderr.write(text);
|
|
266
|
+
logStream?.write(text);
|
|
267
|
+
output += text;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
proc.on("close", (code: number | null) => {
|
|
271
|
+
if (lineBuffer) {
|
|
272
|
+
processLine(lineBuffer);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (output && !output.endsWith("\n")) {
|
|
276
|
+
process.stdout.write("\n");
|
|
277
|
+
logStream?.write("\n");
|
|
278
|
+
output += "\n";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Calculate context usage percent
|
|
282
|
+
let contextUsedPercent: number | undefined;
|
|
283
|
+
if (finalUsage) {
|
|
284
|
+
const totalTokens =
|
|
285
|
+
(finalUsage.input_tokens || 0) +
|
|
286
|
+
(finalUsage.output_tokens || 0) +
|
|
287
|
+
(finalUsage.cache_read_input_tokens || 0) +
|
|
288
|
+
(finalUsage.cache_creation_input_tokens || 0);
|
|
289
|
+
const maxContext = MODEL_CONTEXT_WINDOWS.default;
|
|
290
|
+
contextUsedPercent = Math.round((totalTokens / maxContext) * 100);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
resolve({ output, exitCode: code ?? 0, contextUsedPercent, sessionId });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
proc.on("error", (err: Error) => {
|
|
297
|
+
console.error(pc.red(`Error running claude: ${err}`));
|
|
298
|
+
logStream?.write(`Error running claude: ${err}\n`);
|
|
299
|
+
resolve({ output, exitCode: 1 });
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
}
|