@kosuke-ai/cli 0.0.41 ā 0.0.43
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +80 -41
- package/dist/index.js.map +1 -1
- package/dist/kosuke/commands/build.d.ts +2 -2
- package/dist/kosuke/commands/build.d.ts.map +1 -1
- package/dist/kosuke/commands/build.js +21 -49
- package/dist/kosuke/commands/build.js.map +1 -1
- package/dist/kosuke/commands/plan.d.ts +44 -0
- package/dist/kosuke/commands/plan.d.ts.map +1 -0
- package/dist/kosuke/commands/plan.js +838 -0
- package/dist/kosuke/commands/plan.js.map +1 -0
- package/dist/kosuke/commands/requirements.d.ts.map +1 -1
- package/dist/kosuke/commands/requirements.js +2 -106
- package/dist/kosuke/commands/requirements.js.map +1 -1
- package/dist/kosuke/commands/review.d.ts.map +1 -1
- package/dist/kosuke/commands/review.js +4 -2
- package/dist/kosuke/commands/review.js.map +1 -1
- package/dist/kosuke/commands/ship.d.ts.map +1 -1
- package/dist/kosuke/commands/ship.js +8 -2
- package/dist/kosuke/commands/ship.js.map +1 -1
- package/dist/kosuke/commands/tickets.d.ts +17 -7
- package/dist/kosuke/commands/tickets.d.ts.map +1 -1
- package/dist/kosuke/commands/tickets.js +86 -308
- package/dist/kosuke/commands/tickets.js.map +1 -1
- package/dist/kosuke/types.d.ts +9 -1
- package/dist/kosuke/types.d.ts.map +1 -1
- package/dist/kosuke/utils/claude-agent.d.ts.map +1 -1
- package/dist/kosuke/utils/claude-agent.js +12 -0
- package/dist/kosuke/utils/claude-agent.js.map +1 -1
- package/dist/kosuke/utils/git.d.ts +4 -0
- package/dist/kosuke/utils/git.d.ts.map +1 -1
- package/dist/kosuke/utils/git.js +7 -0
- package/dist/kosuke/utils/git.js.map +1 -1
- package/dist/kosuke/utils/interactive-input.d.ts +22 -0
- package/dist/kosuke/utils/interactive-input.d.ts.map +1 -0
- package/dist/kosuke/utils/interactive-input.js +124 -0
- package/dist/kosuke/utils/interactive-input.js.map +1 -0
- package/dist/kosuke/utils/logger.d.ts +1 -1
- package/dist/kosuke/utils/logger.d.ts.map +1 -1
- package/dist/kosuke/utils/tickets-manager.d.ts +41 -1
- package/dist/kosuke/utils/tickets-manager.d.ts.map +1 -1
- package/dist/kosuke/utils/tickets-manager.js +278 -1
- package/dist/kosuke/utils/tickets-manager.js.map +1 -1
- package/dist/lib.d.ts +6 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +3 -0
- package/dist/lib.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan command - AI-driven ticket planning from feature/bug descriptions
|
|
3
|
+
*
|
|
4
|
+
* This command takes a prompt describing a feature or bug and an existing codebase,
|
|
5
|
+
* asks clarification questions (non-technical, user-focused), and generates tickets.json
|
|
6
|
+
* that can be processed by the build command.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Analyzes existing codebase to understand patterns
|
|
10
|
+
* - Asks AI-generated clarification questions (non-technical)
|
|
11
|
+
* - Auto-detects ticket types (SCHEMA-, BACKEND-, FRONTEND-, WEB-TEST-)
|
|
12
|
+
* - Generates tickets.json compatible with build command
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* kosuke plan --prompt="Add dark mode toggle" --directory=./my-project
|
|
16
|
+
* kosuke plan --prompt="Fix login timeout bug" --dir=./app
|
|
17
|
+
* kosuke plan --prompt="Add notes feature" --no-test # Skip WEB-TEST tickets
|
|
18
|
+
*/
|
|
19
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
21
|
+
import { join, resolve } from 'path';
|
|
22
|
+
import { glob } from 'glob';
|
|
23
|
+
import { calculateCost } from '../utils/claude-agent.js';
|
|
24
|
+
import { askQuestion } from '../utils/interactive-input.js';
|
|
25
|
+
import { logger, setupCancellationHandler } from '../utils/logger.js';
|
|
26
|
+
import { processAndWriteTickets, sortTicketsByOrder } from '../utils/tickets-manager.js';
|
|
27
|
+
/**
|
|
28
|
+
* Generate timestamp-based tickets path in tickets/ folder
|
|
29
|
+
* Creates the tickets/ folder if it doesn't exist
|
|
30
|
+
*/
|
|
31
|
+
function generateTicketsPath(cwd) {
|
|
32
|
+
const ticketsDir = join(cwd, 'tickets');
|
|
33
|
+
// Create tickets folder if it doesn't exist
|
|
34
|
+
if (!existsSync(ticketsDir)) {
|
|
35
|
+
mkdirSync(ticketsDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
// Generate timestamp: YYYY-MM-DD-HH-mm-ss
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const timestamp = now
|
|
40
|
+
.toISOString()
|
|
41
|
+
.replace(/[T:]/g, '-')
|
|
42
|
+
.replace(/\.\d{3}Z$/, '');
|
|
43
|
+
return join(ticketsDir, `${timestamp}.ticket.json`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Tool definitions for planning - includes file exploration and ticket generation
|
|
47
|
+
*/
|
|
48
|
+
const PLAN_TOOLS = [
|
|
49
|
+
{
|
|
50
|
+
name: 'read_file',
|
|
51
|
+
description: 'Read the contents of a file. Use this to explore the codebase and understand existing patterns, conventions, and implementations.',
|
|
52
|
+
input_schema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
properties: {
|
|
55
|
+
path: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
description: 'Path to the file to read (relative to project root)',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: ['path'],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'list_directory',
|
|
65
|
+
description: 'List files and directories in a given path. Use this to explore the project structure.',
|
|
66
|
+
input_schema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
path: {
|
|
70
|
+
type: 'string',
|
|
71
|
+
description: 'Path to the directory to list (relative to project root, use "." for root)',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ['path'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'glob_search',
|
|
79
|
+
description: 'Find files matching a glob pattern. Use this to find specific file types or locate files by name pattern.',
|
|
80
|
+
input_schema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
pattern: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Glob pattern to match (e.g., "**/*.ts", "lib/db/**/*.ts", "**/schema*.ts")',
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
required: ['pattern'],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'write_tickets',
|
|
93
|
+
description: 'Create tickets.json file with implementation tickets. Use this when all clarification questions have been answered and you have enough information to create actionable tickets.',
|
|
94
|
+
input_schema: {
|
|
95
|
+
type: 'object',
|
|
96
|
+
properties: {
|
|
97
|
+
tickets: {
|
|
98
|
+
type: 'array',
|
|
99
|
+
description: 'Array of tickets to create',
|
|
100
|
+
items: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
id: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: 'Ticket ID with prefix: PLAN-SCHEMA- for database, PLAN-ENGINE- for Python microservice, PLAN-BACKEND- for API, PLAN-FRONTEND- for UI, PLAN-WEB-TEST- for E2E tests',
|
|
106
|
+
},
|
|
107
|
+
title: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
description: 'Short descriptive title',
|
|
110
|
+
},
|
|
111
|
+
description: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Detailed description with acceptance criteria, implementation notes, and technical requirements based on codebase analysis',
|
|
114
|
+
},
|
|
115
|
+
type: {
|
|
116
|
+
type: 'string',
|
|
117
|
+
enum: ['schema', 'engine', 'backend', 'frontend', 'test'],
|
|
118
|
+
description: 'Ticket type: schema (database), engine (Python microservice), backend (API), frontend (UI), test (E2E)',
|
|
119
|
+
},
|
|
120
|
+
estimatedEffort: {
|
|
121
|
+
type: 'number',
|
|
122
|
+
description: 'Effort estimate 1-10',
|
|
123
|
+
},
|
|
124
|
+
category: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Feature category (e.g., auth, billing, tasks, ui)',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
required: ['id', 'title', 'description', 'type', 'estimatedEffort'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
required: ['tickets'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
/**
|
|
138
|
+
* Read CLAUDE.md from project directory if it exists
|
|
139
|
+
*/
|
|
140
|
+
function readClaudeMd(cwd) {
|
|
141
|
+
const claudeMdPath = join(cwd, 'CLAUDE.md');
|
|
142
|
+
if (existsSync(claudeMdPath)) {
|
|
143
|
+
try {
|
|
144
|
+
return readFileSync(claudeMdPath, 'utf-8');
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Build system prompt for plan command
|
|
154
|
+
* @param claudeMdContent - Content of CLAUDE.md file if it exists
|
|
155
|
+
* @param noTest - If true, exclude WEB-TEST tickets from generation
|
|
156
|
+
*/
|
|
157
|
+
function buildPlanSystemPrompt(claudeMdContent, noTest = false) {
|
|
158
|
+
const claudeSection = claudeMdContent
|
|
159
|
+
? `
|
|
160
|
+
|
|
161
|
+
**PROJECT CONTEXT (from CLAUDE.md):**
|
|
162
|
+
|
|
163
|
+
${claudeMdContent}
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
`
|
|
167
|
+
: '';
|
|
168
|
+
return `You are an expert software architect helping plan implementation tickets for a feature or bug fix.
|
|
169
|
+
|
|
170
|
+
**YOUR PRIMARY OBJECTIVE:** Gather enough information through clarification questions to create actionable implementation tickets that can be processed by an automated build system.
|
|
171
|
+
${claudeSection}
|
|
172
|
+
**Your Workflow:**
|
|
173
|
+
|
|
174
|
+
1. **Explore Codebase**:
|
|
175
|
+
- Use list_directory to explore relevant parts of the codebase
|
|
176
|
+
- Read existing similar implementations to understand patterns
|
|
177
|
+
- Analyze what the user wants to achieve
|
|
178
|
+
- Identify what's unclear or needs user input
|
|
179
|
+
|
|
180
|
+
2. **Ask Clarification Questions**: Present questions in this format:
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
## Understanding Your Request
|
|
184
|
+
|
|
185
|
+
[Brief summary of what you understood]
|
|
186
|
+
|
|
187
|
+
## Clarification Questions
|
|
188
|
+
|
|
189
|
+
For each question, provide BOTH the question AND a recommended approach:
|
|
190
|
+
|
|
191
|
+
1. **[Topic]**
|
|
192
|
+
- Question: [User-focused question - NOT technical]
|
|
193
|
+
- š” Recommendation: [Simple, practical default choice]
|
|
194
|
+
|
|
195
|
+
2. **[Topic]**
|
|
196
|
+
- Question: [User-focused question - NOT technical]
|
|
197
|
+
- š” Recommendation: [Simple, practical default choice]
|
|
198
|
+
|
|
199
|
+
**Quick Option:** Reply "go with recommendations" to accept all defaults.
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
3. **Iterative Refinement**: As the user answers:
|
|
203
|
+
- If user says "go with recommendations", accept all defaults
|
|
204
|
+
- If user provides specific answers, incorporate them
|
|
205
|
+
- Ask follow-up questions ONLY if critical information is still missing
|
|
206
|
+
- Bias towards simplicity - this is an MVP
|
|
207
|
+
|
|
208
|
+
4. **Generate Tickets**: Once requirements are clear, use \`write_tickets\` tool to create tickets:
|
|
209
|
+
|
|
210
|
+
**Ticket Types & Prefixes:**
|
|
211
|
+
- \`PLAN-SCHEMA-N\`: Database schema changes (Drizzle ORM migrations)
|
|
212
|
+
- \`PLAN-ENGINE-N\`: Python microservice (FastAPI endpoints)
|
|
213
|
+
- \`PLAN-BACKEND-N\`: API/server-side logic (tRPC, server actions)
|
|
214
|
+
- \`PLAN-FRONTEND-N\`: UI components and pages (React, Next.js)
|
|
215
|
+
|
|
216
|
+
**When to use ENGINE vs BACKEND:**
|
|
217
|
+
- **Use BACKEND (Next.js)** for: CRUD operations, auth logic, business rules, anything TypeScript handles well (90% of features)
|
|
218
|
+
- **Use ENGINE (Python)** for: ML/AI, data science (numpy/pandas), complex algorithms, PDF/document parsing, image processing, or when Python libraries are required${noTest
|
|
219
|
+
? ''
|
|
220
|
+
: `
|
|
221
|
+
- \`PLAN-WEB-TEST-N\`: E2E tests (Playwright, browser testing)`}
|
|
222
|
+
|
|
223
|
+
**Ticket Order (build system processes in this order):**
|
|
224
|
+
1. PLAN-SCHEMA tickets first (database changes)
|
|
225
|
+
2. PLAN-ENGINE tickets (Python microservice - so backend can call it)
|
|
226
|
+
3. PLAN-BACKEND tickets (API layer)
|
|
227
|
+
4. PLAN-FRONTEND tickets (UI layer)${noTest
|
|
228
|
+
? ''
|
|
229
|
+
: `
|
|
230
|
+
5. PLAN-WEB-TEST tickets last (validate everything works)
|
|
231
|
+
|
|
232
|
+
**WEB TEST TICKETS - Stagehand Agent E2E Tests:**
|
|
233
|
+
|
|
234
|
+
Web test tickets are executed by Stagehand agent. Follow these guidelines:
|
|
235
|
+
|
|
236
|
+
**Test User Discovery:**
|
|
237
|
+
- Read seed files (lib/db/seed.ts or src/lib/db/seed.ts) to find test users
|
|
238
|
+
- Pattern: Any email ending with "+kosuke_test@example.com" uses OTP code "424242"
|
|
239
|
+
- Example: john+kosuke_test@example.com ā OTP: 424242
|
|
240
|
+
|
|
241
|
+
**Each Web Test Ticket MUST Include:**
|
|
242
|
+
1. **Test User Credentials** (at the top)
|
|
243
|
+
- Email addresses of test users
|
|
244
|
+
- OTP code: 424242
|
|
245
|
+
- User roles if applicable
|
|
246
|
+
|
|
247
|
+
2. **Test Steps** (numbered, detailed natural language)
|
|
248
|
+
- Navigation: "Navigate to /sign-in"
|
|
249
|
+
- Interactions: "Click button labeled 'New Task'"
|
|
250
|
+
- Inputs: "Enter 'Test Task' in title field"
|
|
251
|
+
- Expected outcomes: "Expected: Task appears in list"
|
|
252
|
+
- Use CLEAR element descriptions (button text, labels)
|
|
253
|
+
|
|
254
|
+
3. **Acceptance Criteria**
|
|
255
|
+
- Final expected state
|
|
256
|
+
- Data validation points
|
|
257
|
+
|
|
258
|
+
**Authentication Steps Template:**
|
|
259
|
+
1. Navigate to /sign-in
|
|
260
|
+
2. Enter email: {test_user}+kosuke_test@example.com
|
|
261
|
+
3. Click "Send Code" button
|
|
262
|
+
4. Enter OTP: 424242
|
|
263
|
+
5. Click "Verify" button
|
|
264
|
+
6. Expected: Redirected to main app`}
|
|
265
|
+
|
|
266
|
+
**CRITICAL RULES FOR QUESTIONS:**
|
|
267
|
+
- Questions must be NON-TECHNICAL and USER-FOCUSED
|
|
268
|
+
- Focus ONLY on user experience, behavior, and business logic
|
|
269
|
+
- YOU decide all technical/algorithmic details (libraries, caching, performance, architecture)
|
|
270
|
+
- Never ask about: URLs, database design, APIs, algorithms, processing timing, or implementation approach
|
|
271
|
+
- Include technical decisions in ticket DESCRIPTIONS, not in questions to users
|
|
272
|
+
|
|
273
|
+
**Examples:**
|
|
274
|
+
- ā BAD: "Should we cache results or process on-demand?"
|
|
275
|
+
- ā BAD: "Should this use Python or TypeScript?"
|
|
276
|
+
- ā
GOOD: "Should invoices be per-user or shared per company?"
|
|
277
|
+
- ā
GOOD: "For empty descriptions, show neutral mood or hide it?"
|
|
278
|
+
|
|
279
|
+
**Ticket Generation Rules:**
|
|
280
|
+
- Generate only the tickets actually needed
|
|
281
|
+
- Ensure tickets are atomic and independently implementable
|
|
282
|
+
- Include clear acceptance criteria in each ticket description
|
|
283
|
+
|
|
284
|
+
**Example Tickets (Full JSON):**
|
|
285
|
+
[
|
|
286
|
+
{
|
|
287
|
+
"id": "PLAN-SCHEMA-1",
|
|
288
|
+
"title": "Create tasks schema",
|
|
289
|
+
"description": "Create database schema for tasks feature:\\n- Create taskStatusEnum: 'todo', 'in_progress', 'done'\\n- Create tasks table with userId foreign key\\n- Export inferred types\\n\\n**Acceptance Criteria:**\\n- Tasks table created\\n- Enums defined at database level\\n- Migrations generated\\n\\n**Technical Notes:**\\n- Follow existing schema patterns in lib/db/schema/\\n- Use Drizzle ORM conventions",
|
|
290
|
+
"type": "schema",
|
|
291
|
+
"estimatedEffort": 4,
|
|
292
|
+
"category": "tasks"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"id": "PLAN-BACKEND-1",
|
|
296
|
+
"title": "Create tasks tRPC router",
|
|
297
|
+
"description": "Create backend API for tasks:\\n- Create lib/trpc/routers/tasks.ts\\n- Implement CRUD operations (list, create, update, delete)\\n- Server-side filtering by status\\n\\n**Acceptance Criteria:**\\n- All CRUD operations work\\n- Authorization enforced\\n- Type-safe implementation",
|
|
298
|
+
"type": "backend",
|
|
299
|
+
"estimatedEffort": 5,
|
|
300
|
+
"category": "tasks"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"id": "PLAN-FRONTEND-1",
|
|
304
|
+
"title": "Create tasks page with list and filters",
|
|
305
|
+
"description": "Create tasks management UI:\\n- Create app/(logged-in)/tasks/page.tsx\\n- Task list with status filters\\n- Add new task dialog\\n- Edit/delete actions\\n\\n**Acceptance Criteria:**\\n- Task list displays correctly\\n- Filters work\\n- CRUD operations functional\\n- Responsive design\\n\\n**Technical Notes:**\\n- Use existing UI components from components/ui/\\n- Follow page patterns from existing routes",
|
|
306
|
+
"type": "frontend",
|
|
307
|
+
"estimatedEffort": 6,
|
|
308
|
+
"category": "tasks"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"id": "PLAN-WEB-TEST-1",
|
|
312
|
+
"title": "E2E: User creates and manages tasks",
|
|
313
|
+
"description": "**Test User Credentials:**\\n- Email: john+kosuke_test@example.com\\n- OTP Code: 424242\\n\\n**Test Steps:**\\n\\n1. **Sign in**\\n - Navigate to /sign-in\\n - Enter email: john+kosuke_test@example.com\\n - Click 'Send Code' button\\n - Enter OTP: 424242\\n - Click 'Verify'\\n - Expected: Redirected to /tasks\\n\\n2. **Create task**\\n - Click 'New Task' button\\n - Enter title: 'Test Task'\\n - Click 'Create'\\n - Expected: Task appears in list\\n\\n3. **Delete task**\\n - Click delete button on task\\n - Confirm deletion\\n - Expected: Task removed\\n\\n**Acceptance Criteria:**\\n- User authenticates successfully\\n- Task CRUD operations work\\n- UI provides feedback",
|
|
314
|
+
"type": "test",
|
|
315
|
+
"estimatedEffort": 4,
|
|
316
|
+
"category": "tasks"
|
|
317
|
+
}
|
|
318
|
+
]`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Execute read_file tool
|
|
322
|
+
*/
|
|
323
|
+
function executeReadFile(toolInput, cwd) {
|
|
324
|
+
try {
|
|
325
|
+
const filePath = toolInput.path;
|
|
326
|
+
const fullPath = join(cwd, filePath);
|
|
327
|
+
if (!existsSync(fullPath)) {
|
|
328
|
+
return { success: false, content: `File not found: ${filePath}` };
|
|
329
|
+
}
|
|
330
|
+
const stats = statSync(fullPath);
|
|
331
|
+
if (stats.isDirectory()) {
|
|
332
|
+
return { success: false, content: `Path is a directory, not a file: ${filePath}` };
|
|
333
|
+
}
|
|
334
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
335
|
+
console.log(`\n š Reading: ${filePath}`);
|
|
336
|
+
return { success: true, content };
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
340
|
+
return { success: false, content: `Error reading file: ${msg}` };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Execute list_directory tool
|
|
345
|
+
*/
|
|
346
|
+
function executeListDirectory(toolInput, cwd) {
|
|
347
|
+
try {
|
|
348
|
+
const dirPath = toolInput.path || '.';
|
|
349
|
+
const fullPath = join(cwd, dirPath);
|
|
350
|
+
if (!existsSync(fullPath)) {
|
|
351
|
+
return { success: false, content: `Directory not found: ${dirPath}` };
|
|
352
|
+
}
|
|
353
|
+
const stats = statSync(fullPath);
|
|
354
|
+
if (!stats.isDirectory()) {
|
|
355
|
+
return { success: false, content: `Path is not a directory: ${dirPath}` };
|
|
356
|
+
}
|
|
357
|
+
const entries = readdirSync(fullPath);
|
|
358
|
+
const items = [];
|
|
359
|
+
// Filter out common ignored directories
|
|
360
|
+
const ignoreDirs = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.tmp'];
|
|
361
|
+
for (const entry of entries.sort()) {
|
|
362
|
+
if (entry.startsWith('.') && entry !== '.env.example')
|
|
363
|
+
continue;
|
|
364
|
+
if (ignoreDirs.includes(entry))
|
|
365
|
+
continue;
|
|
366
|
+
const entryPath = join(fullPath, entry);
|
|
367
|
+
try {
|
|
368
|
+
const entryStat = statSync(entryPath);
|
|
369
|
+
if (entryStat.isDirectory()) {
|
|
370
|
+
items.push(`š ${entry}/`);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
items.push(`š ${entry}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
items.push(`ā ${entry}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
console.log(`\n š Listing: ${dirPath}`);
|
|
381
|
+
return { success: true, content: items.join('\n') || '(empty directory)' };
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
385
|
+
return { success: false, content: `Error listing directory: ${msg}` };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Execute glob_search tool
|
|
390
|
+
*/
|
|
391
|
+
async function executeGlobSearch(toolInput, cwd) {
|
|
392
|
+
try {
|
|
393
|
+
const pattern = toolInput.pattern;
|
|
394
|
+
const files = await glob(pattern, {
|
|
395
|
+
cwd,
|
|
396
|
+
nodir: true,
|
|
397
|
+
ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '.next/**', '__pycache__/**'],
|
|
398
|
+
});
|
|
399
|
+
if (files.length === 0) {
|
|
400
|
+
return { success: true, content: `No files found matching: ${pattern}` };
|
|
401
|
+
}
|
|
402
|
+
// Limit results
|
|
403
|
+
const maxResults = 50;
|
|
404
|
+
const truncated = files.length > maxResults;
|
|
405
|
+
const displayFiles = files.slice(0, maxResults);
|
|
406
|
+
console.log(`\n š Found ${files.length} file(s) matching: ${pattern}`);
|
|
407
|
+
let content = displayFiles.join('\n');
|
|
408
|
+
if (truncated) {
|
|
409
|
+
content += `\n\n...[showing ${maxResults} of ${files.length} files]`;
|
|
410
|
+
}
|
|
411
|
+
return { success: true, content };
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
415
|
+
return { success: false, content: `Error searching files: ${msg}` };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Execute write_tickets tool
|
|
420
|
+
* Returns parsed tickets for later validation - does NOT write to file
|
|
421
|
+
*/
|
|
422
|
+
function executeWriteTickets(toolInput) {
|
|
423
|
+
try {
|
|
424
|
+
const inputTickets = toolInput.tickets;
|
|
425
|
+
// Transform to full Ticket objects
|
|
426
|
+
const tickets = inputTickets.map((t) => ({
|
|
427
|
+
id: t.id,
|
|
428
|
+
title: t.title,
|
|
429
|
+
description: t.description,
|
|
430
|
+
type: t.type,
|
|
431
|
+
estimatedEffort: t.estimatedEffort,
|
|
432
|
+
status: 'Todo',
|
|
433
|
+
category: t.category,
|
|
434
|
+
}));
|
|
435
|
+
// Sort tickets by processing order (using shared utility)
|
|
436
|
+
const sortedTickets = sortTicketsByOrder(tickets);
|
|
437
|
+
console.log(`\nš Generated ${sortedTickets.length} ticket(s) - validating...`);
|
|
438
|
+
return {
|
|
439
|
+
success: true,
|
|
440
|
+
message: `Generated ${sortedTickets.length} tickets - will validate and save after confirmation`,
|
|
441
|
+
tickets: sortedTickets,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
446
|
+
console.error(`\nā Failed to parse tickets: ${errorMessage}`);
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
message: `Error: ${errorMessage}`,
|
|
450
|
+
tickets: [],
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Format token usage for display
|
|
456
|
+
*/
|
|
457
|
+
function formatTokenUsage(inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens, cost) {
|
|
458
|
+
const breakdown = [];
|
|
459
|
+
if (inputTokens > 0)
|
|
460
|
+
breakdown.push(`${inputTokens.toLocaleString()} input`);
|
|
461
|
+
if (outputTokens > 0)
|
|
462
|
+
breakdown.push(`${outputTokens.toLocaleString()} output`);
|
|
463
|
+
if (cacheCreationTokens > 0)
|
|
464
|
+
breakdown.push(`${cacheCreationTokens.toLocaleString()} cache write`);
|
|
465
|
+
if (cacheReadTokens > 0)
|
|
466
|
+
breakdown.push(`${cacheReadTokens.toLocaleString()} cache read`);
|
|
467
|
+
return `š° Cost: $${cost.toFixed(4)} (${breakdown.join(' + ')} tokens)`;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Process a single Claude interaction with streaming
|
|
471
|
+
* Handles multiple tool calls in a loop until Claude stops calling tools
|
|
472
|
+
*/
|
|
473
|
+
async function processClaudeInteraction(messages, systemPrompt, cwd) {
|
|
474
|
+
const anthropic = new Anthropic({
|
|
475
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
476
|
+
});
|
|
477
|
+
let responseText = '';
|
|
478
|
+
let tickets = [];
|
|
479
|
+
let ticketsCreated = false;
|
|
480
|
+
let totalInputTokens = 0;
|
|
481
|
+
let totalOutputTokens = 0;
|
|
482
|
+
let totalCacheCreationTokens = 0;
|
|
483
|
+
let totalCacheReadTokens = 0;
|
|
484
|
+
let isFirstOutput = true;
|
|
485
|
+
// Loop until Claude stops calling tools
|
|
486
|
+
const maxIterations = 20; // Safety limit
|
|
487
|
+
let iterations = 0;
|
|
488
|
+
while (iterations < maxIterations) {
|
|
489
|
+
iterations++;
|
|
490
|
+
// Stream the response
|
|
491
|
+
const stream = await anthropic.messages.stream({
|
|
492
|
+
model: 'claude-sonnet-4-20250514',
|
|
493
|
+
max_tokens: 8096,
|
|
494
|
+
system: systemPrompt,
|
|
495
|
+
tools: PLAN_TOOLS,
|
|
496
|
+
messages,
|
|
497
|
+
});
|
|
498
|
+
let currentText = '';
|
|
499
|
+
const toolUses = [];
|
|
500
|
+
// Process stream events
|
|
501
|
+
for await (const event of stream) {
|
|
502
|
+
if (event.type === 'content_block_start') {
|
|
503
|
+
if (event.content_block.type === 'text') {
|
|
504
|
+
if (isFirstOutput) {
|
|
505
|
+
process.stdout.write('\n> Claude:\n');
|
|
506
|
+
isFirstOutput = false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else if (event.type === 'content_block_delta') {
|
|
511
|
+
if (event.delta.type === 'text_delta') {
|
|
512
|
+
const delta = event.delta.text;
|
|
513
|
+
currentText += delta;
|
|
514
|
+
process.stdout.write(delta);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
responseText += currentText;
|
|
519
|
+
// Get final message
|
|
520
|
+
const finalMessage = await stream.finalMessage();
|
|
521
|
+
// Track token usage
|
|
522
|
+
const usage = finalMessage.usage;
|
|
523
|
+
totalInputTokens += usage.input_tokens;
|
|
524
|
+
totalOutputTokens += usage.output_tokens;
|
|
525
|
+
totalCacheCreationTokens += usage.cache_creation_input_tokens || 0;
|
|
526
|
+
totalCacheReadTokens += usage.cache_read_input_tokens || 0;
|
|
527
|
+
// Extract tool uses
|
|
528
|
+
for (const block of finalMessage.content) {
|
|
529
|
+
if (block.type === 'tool_use') {
|
|
530
|
+
toolUses.push({
|
|
531
|
+
id: block.id,
|
|
532
|
+
name: block.name,
|
|
533
|
+
input: block.input,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// If no tools called, we're done
|
|
538
|
+
if (toolUses.length === 0) {
|
|
539
|
+
messages = [...messages, { role: 'assistant', content: finalMessage.content }];
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
// Execute tools
|
|
543
|
+
messages = [...messages, { role: 'assistant', content: finalMessage.content }];
|
|
544
|
+
const toolResults = [];
|
|
545
|
+
for (const tool of toolUses) {
|
|
546
|
+
if (tool.name === 'read_file') {
|
|
547
|
+
const result = executeReadFile(tool.input, cwd);
|
|
548
|
+
toolResults.push({
|
|
549
|
+
type: 'tool_result',
|
|
550
|
+
tool_use_id: tool.id,
|
|
551
|
+
content: result.content,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
else if (tool.name === 'list_directory') {
|
|
555
|
+
const result = executeListDirectory(tool.input, cwd);
|
|
556
|
+
toolResults.push({
|
|
557
|
+
type: 'tool_result',
|
|
558
|
+
tool_use_id: tool.id,
|
|
559
|
+
content: result.content,
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
else if (tool.name === 'glob_search') {
|
|
563
|
+
const result = await executeGlobSearch(tool.input, cwd);
|
|
564
|
+
toolResults.push({
|
|
565
|
+
type: 'tool_result',
|
|
566
|
+
tool_use_id: tool.id,
|
|
567
|
+
content: result.content,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
else if (tool.name === 'write_tickets') {
|
|
571
|
+
const result = executeWriteTickets(tool.input);
|
|
572
|
+
tickets = result.tickets;
|
|
573
|
+
ticketsCreated = result.success;
|
|
574
|
+
toolResults.push({
|
|
575
|
+
type: 'tool_result',
|
|
576
|
+
tool_use_id: tool.id,
|
|
577
|
+
content: result.message,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
messages = [...messages, { role: 'user', content: toolResults }];
|
|
582
|
+
// If tickets were created, get final response and stop
|
|
583
|
+
if (ticketsCreated) {
|
|
584
|
+
const followupStream = await anthropic.messages.stream({
|
|
585
|
+
model: 'claude-sonnet-4-20250514',
|
|
586
|
+
max_tokens: 8096,
|
|
587
|
+
system: systemPrompt,
|
|
588
|
+
tools: PLAN_TOOLS,
|
|
589
|
+
messages,
|
|
590
|
+
});
|
|
591
|
+
let followupText = '';
|
|
592
|
+
for await (const event of followupStream) {
|
|
593
|
+
if (event.type === 'content_block_delta') {
|
|
594
|
+
if (event.delta.type === 'text_delta') {
|
|
595
|
+
const delta = event.delta.text;
|
|
596
|
+
followupText += delta;
|
|
597
|
+
process.stdout.write(delta);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const followupMessage = await followupStream.finalMessage();
|
|
602
|
+
responseText += '\n' + followupText;
|
|
603
|
+
messages = [...messages, { role: 'assistant', content: followupMessage.content }];
|
|
604
|
+
// Add followup token usage
|
|
605
|
+
const followupUsage = followupMessage.usage;
|
|
606
|
+
totalInputTokens += followupUsage.input_tokens;
|
|
607
|
+
totalOutputTokens += followupUsage.output_tokens;
|
|
608
|
+
totalCacheCreationTokens += followupUsage.cache_creation_input_tokens || 0;
|
|
609
|
+
totalCacheReadTokens += followupUsage.cache_read_input_tokens || 0;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
response: responseText,
|
|
615
|
+
messages,
|
|
616
|
+
inputTokens: totalInputTokens,
|
|
617
|
+
outputTokens: totalOutputTokens,
|
|
618
|
+
cacheCreationTokens: totalCacheCreationTokens,
|
|
619
|
+
cacheReadTokens: totalCacheReadTokens,
|
|
620
|
+
tickets,
|
|
621
|
+
ticketsCreated,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Interactive planning session
|
|
626
|
+
*/
|
|
627
|
+
async function interactivePlanSession(initialPrompt, cwd, ticketsPath, logContext, noTest = false) {
|
|
628
|
+
console.log(`
|
|
629
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
630
|
+
ā Kosuke Plan - AI-Driven Ticket Planning ā
|
|
631
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
632
|
+
`);
|
|
633
|
+
console.log('š” This tool will help you create implementation tickets from your feature/bug description.\n');
|
|
634
|
+
console.log('š¤ Claude will explore your codebase to understand patterns and conventions.\n');
|
|
635
|
+
// Read CLAUDE.md and inject into system prompt
|
|
636
|
+
const claudeMdContent = readClaudeMd(cwd);
|
|
637
|
+
if (claudeMdContent) {
|
|
638
|
+
console.log(`š Loaded CLAUDE.md (${Math.round(claudeMdContent.length / 1000)}k chars)\n`);
|
|
639
|
+
}
|
|
640
|
+
const systemPrompt = buildPlanSystemPrompt(claudeMdContent, noTest);
|
|
641
|
+
console.log(`${'ā'.repeat(60)}`);
|
|
642
|
+
console.log('š¤ Using model: claude-sonnet-4-5');
|
|
643
|
+
console.log(`${'ā'.repeat(60)}\n`);
|
|
644
|
+
console.log('⨠Tip: Enter to submit, Ctrl+J for new lines.\n');
|
|
645
|
+
// Set up Ctrl+C handler
|
|
646
|
+
const handleSigInt = async () => {
|
|
647
|
+
console.log('\n\nš Exiting planning session...\n');
|
|
648
|
+
if (logContext) {
|
|
649
|
+
await logger.complete(logContext, 'cancelled');
|
|
650
|
+
}
|
|
651
|
+
process.exit(0);
|
|
652
|
+
};
|
|
653
|
+
process.on('SIGINT', handleSigInt);
|
|
654
|
+
const session = {
|
|
655
|
+
prompt: initialPrompt,
|
|
656
|
+
messages: [],
|
|
657
|
+
};
|
|
658
|
+
let totalInputTokens = 0;
|
|
659
|
+
let totalOutputTokens = 0;
|
|
660
|
+
let totalCacheCreationTokens = 0;
|
|
661
|
+
let totalCacheReadTokens = 0;
|
|
662
|
+
let totalCost = 0;
|
|
663
|
+
let finalTickets = [];
|
|
664
|
+
try {
|
|
665
|
+
// Start with initial prompt
|
|
666
|
+
session.messages.push({ role: 'user', content: initialPrompt });
|
|
667
|
+
let continueConversation = true;
|
|
668
|
+
while (continueConversation) {
|
|
669
|
+
console.log('\nš¤ Claude is analyzing...\n');
|
|
670
|
+
const result = await processClaudeInteraction(session.messages, systemPrompt, cwd);
|
|
671
|
+
session.messages = result.messages;
|
|
672
|
+
// Track costs
|
|
673
|
+
totalInputTokens += result.inputTokens;
|
|
674
|
+
totalOutputTokens += result.outputTokens;
|
|
675
|
+
totalCacheCreationTokens += result.cacheCreationTokens;
|
|
676
|
+
totalCacheReadTokens += result.cacheReadTokens;
|
|
677
|
+
const batchCost = calculateCost(result.inputTokens, result.outputTokens, result.cacheCreationTokens, result.cacheReadTokens);
|
|
678
|
+
totalCost += batchCost;
|
|
679
|
+
// Display cost
|
|
680
|
+
console.log('\n' + 'ā'.repeat(90));
|
|
681
|
+
console.log(formatTokenUsage(result.inputTokens, result.outputTokens, result.cacheCreationTokens, result.cacheReadTokens, batchCost));
|
|
682
|
+
console.log('ā'.repeat(90) + '\n');
|
|
683
|
+
// Check if tickets were created
|
|
684
|
+
if (result.ticketsCreated) {
|
|
685
|
+
// Validate and write tickets using shared utility
|
|
686
|
+
const { tickets: validatedTickets } = await processAndWriteTickets(result.tickets, ticketsPath, cwd, { displaySummary: true });
|
|
687
|
+
finalTickets = validatedTickets;
|
|
688
|
+
console.log('ā'.repeat(90));
|
|
689
|
+
console.log('š Total Session Cost:');
|
|
690
|
+
console.log(formatTokenUsage(totalInputTokens, totalOutputTokens, totalCacheCreationTokens, totalCacheReadTokens, totalCost));
|
|
691
|
+
console.log('ā'.repeat(90));
|
|
692
|
+
// Get relative path for cleaner output
|
|
693
|
+
const relativeTicketsPath = ticketsPath.replace(cwd + '/', '');
|
|
694
|
+
console.log('\nš Planning complete!\n');
|
|
695
|
+
console.log('š” Next steps:');
|
|
696
|
+
console.log(' - Review tickets: cat "' + ticketsPath + '"');
|
|
697
|
+
console.log(' - Build tickets: kosuke build --directory="' +
|
|
698
|
+
cwd +
|
|
699
|
+
'" --tickets="' +
|
|
700
|
+
relativeTicketsPath +
|
|
701
|
+
'"');
|
|
702
|
+
console.log(' - List all tickets: ls ' + join(cwd, 'tickets'));
|
|
703
|
+
continueConversation = false;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
// Ask for user response
|
|
707
|
+
console.log('š¬ Your response (type "exit" to quit):\n');
|
|
708
|
+
const userResponse = await askQuestion('You: ');
|
|
709
|
+
if (!userResponse) {
|
|
710
|
+
console.log('\nā ļø Empty response. Please provide an answer or type "exit".');
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (userResponse.toLowerCase() === 'exit') {
|
|
714
|
+
console.log('\nš Exiting planning session.\n');
|
|
715
|
+
console.log('ā'.repeat(90));
|
|
716
|
+
console.log('š Session Cost:');
|
|
717
|
+
console.log(formatTokenUsage(totalInputTokens, totalOutputTokens, totalCacheCreationTokens, totalCacheReadTokens, totalCost));
|
|
718
|
+
console.log('ā'.repeat(90) + '\n');
|
|
719
|
+
continueConversation = false;
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
// Add user response to messages
|
|
723
|
+
session.messages = [...session.messages, { role: 'user', content: userResponse }];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
console.error('\nā Error during planning:', error);
|
|
728
|
+
throw error;
|
|
729
|
+
}
|
|
730
|
+
finally {
|
|
731
|
+
process.removeListener('SIGINT', handleSigInt);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
messages: session.messages,
|
|
735
|
+
tickets: finalTickets,
|
|
736
|
+
tokensUsed: {
|
|
737
|
+
input: totalInputTokens,
|
|
738
|
+
output: totalOutputTokens,
|
|
739
|
+
cacheCreation: totalCacheCreationTokens,
|
|
740
|
+
cacheRead: totalCacheReadTokens,
|
|
741
|
+
},
|
|
742
|
+
cost: totalCost,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Core plan function for programmatic use
|
|
747
|
+
*/
|
|
748
|
+
export async function planCore(options) {
|
|
749
|
+
const { prompt, directory, noTest = false } = options;
|
|
750
|
+
// Validate directory
|
|
751
|
+
const cwd = directory ? resolve(directory) : process.cwd();
|
|
752
|
+
if (!existsSync(cwd)) {
|
|
753
|
+
return {
|
|
754
|
+
success: false,
|
|
755
|
+
tickets: [],
|
|
756
|
+
ticketsFile: '',
|
|
757
|
+
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
758
|
+
cost: 0,
|
|
759
|
+
error: `Directory not found: ${cwd}`,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
const stats = statSync(cwd);
|
|
763
|
+
if (!stats.isDirectory()) {
|
|
764
|
+
return {
|
|
765
|
+
success: false,
|
|
766
|
+
tickets: [],
|
|
767
|
+
ticketsFile: '',
|
|
768
|
+
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
769
|
+
cost: 0,
|
|
770
|
+
error: `Path is not a directory: ${cwd}`,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const ticketsPath = generateTicketsPath(cwd);
|
|
774
|
+
try {
|
|
775
|
+
const result = await interactivePlanSession(prompt, cwd, ticketsPath, undefined, noTest);
|
|
776
|
+
return {
|
|
777
|
+
success: result.tickets.length > 0,
|
|
778
|
+
tickets: result.tickets,
|
|
779
|
+
ticketsFile: ticketsPath,
|
|
780
|
+
tokensUsed: result.tokensUsed,
|
|
781
|
+
cost: result.cost,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
return {
|
|
786
|
+
success: false,
|
|
787
|
+
tickets: [],
|
|
788
|
+
ticketsFile: '',
|
|
789
|
+
tokensUsed: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
790
|
+
cost: 0,
|
|
791
|
+
error: error instanceof Error ? error.message : String(error),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Main plan command
|
|
797
|
+
*/
|
|
798
|
+
export async function planCommand(options) {
|
|
799
|
+
// Initialize logging
|
|
800
|
+
const logContext = logger.createContext('plan', { noLogs: options.noLogs ?? false });
|
|
801
|
+
const cleanupHandler = setupCancellationHandler(logContext);
|
|
802
|
+
try {
|
|
803
|
+
// Validate environment
|
|
804
|
+
if (!process.env.ANTHROPIC_API_KEY) {
|
|
805
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is required');
|
|
806
|
+
}
|
|
807
|
+
// Validate prompt
|
|
808
|
+
if (!options.prompt) {
|
|
809
|
+
throw new Error('Prompt is required. Use --prompt="Your feature or bug description"\n' +
|
|
810
|
+
'Example: kosuke plan --prompt="Add dark mode toggle" --directory=./my-project');
|
|
811
|
+
}
|
|
812
|
+
// Resolve directory
|
|
813
|
+
const cwd = options.directory ? resolve(options.directory) : process.cwd();
|
|
814
|
+
if (!existsSync(cwd)) {
|
|
815
|
+
throw new Error(`Directory not found: ${cwd}`);
|
|
816
|
+
}
|
|
817
|
+
const stats = statSync(cwd);
|
|
818
|
+
if (!stats.isDirectory()) {
|
|
819
|
+
throw new Error(`Path is not a directory: ${cwd}`);
|
|
820
|
+
}
|
|
821
|
+
console.log(`š Using project directory: ${cwd}\n`);
|
|
822
|
+
const ticketsPath = generateTicketsPath(cwd);
|
|
823
|
+
// Run interactive session
|
|
824
|
+
const sessionData = await interactivePlanSession(options.prompt, cwd, ticketsPath, logContext, options.noTest ?? false);
|
|
825
|
+
// Track metrics
|
|
826
|
+
logger.trackTokens(logContext, sessionData.tokensUsed);
|
|
827
|
+
// Log successful execution
|
|
828
|
+
await logger.complete(logContext, 'success');
|
|
829
|
+
cleanupHandler();
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
console.error('\nā Plan command failed:', error);
|
|
833
|
+
await logger.complete(logContext, 'error', error);
|
|
834
|
+
cleanupHandler();
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
//# sourceMappingURL=plan.js.map
|