@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.
Files changed (50) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +80 -41
  4. package/dist/index.js.map +1 -1
  5. package/dist/kosuke/commands/build.d.ts +2 -2
  6. package/dist/kosuke/commands/build.d.ts.map +1 -1
  7. package/dist/kosuke/commands/build.js +21 -49
  8. package/dist/kosuke/commands/build.js.map +1 -1
  9. package/dist/kosuke/commands/plan.d.ts +44 -0
  10. package/dist/kosuke/commands/plan.d.ts.map +1 -0
  11. package/dist/kosuke/commands/plan.js +838 -0
  12. package/dist/kosuke/commands/plan.js.map +1 -0
  13. package/dist/kosuke/commands/requirements.d.ts.map +1 -1
  14. package/dist/kosuke/commands/requirements.js +2 -106
  15. package/dist/kosuke/commands/requirements.js.map +1 -1
  16. package/dist/kosuke/commands/review.d.ts.map +1 -1
  17. package/dist/kosuke/commands/review.js +4 -2
  18. package/dist/kosuke/commands/review.js.map +1 -1
  19. package/dist/kosuke/commands/ship.d.ts.map +1 -1
  20. package/dist/kosuke/commands/ship.js +8 -2
  21. package/dist/kosuke/commands/ship.js.map +1 -1
  22. package/dist/kosuke/commands/tickets.d.ts +17 -7
  23. package/dist/kosuke/commands/tickets.d.ts.map +1 -1
  24. package/dist/kosuke/commands/tickets.js +86 -308
  25. package/dist/kosuke/commands/tickets.js.map +1 -1
  26. package/dist/kosuke/types.d.ts +9 -1
  27. package/dist/kosuke/types.d.ts.map +1 -1
  28. package/dist/kosuke/utils/claude-agent.d.ts.map +1 -1
  29. package/dist/kosuke/utils/claude-agent.js +12 -0
  30. package/dist/kosuke/utils/claude-agent.js.map +1 -1
  31. package/dist/kosuke/utils/git.d.ts +4 -0
  32. package/dist/kosuke/utils/git.d.ts.map +1 -1
  33. package/dist/kosuke/utils/git.js +7 -0
  34. package/dist/kosuke/utils/git.js.map +1 -1
  35. package/dist/kosuke/utils/interactive-input.d.ts +22 -0
  36. package/dist/kosuke/utils/interactive-input.d.ts.map +1 -0
  37. package/dist/kosuke/utils/interactive-input.js +124 -0
  38. package/dist/kosuke/utils/interactive-input.js.map +1 -0
  39. package/dist/kosuke/utils/logger.d.ts +1 -1
  40. package/dist/kosuke/utils/logger.d.ts.map +1 -1
  41. package/dist/kosuke/utils/tickets-manager.d.ts +41 -1
  42. package/dist/kosuke/utils/tickets-manager.d.ts.map +1 -1
  43. package/dist/kosuke/utils/tickets-manager.js +278 -1
  44. package/dist/kosuke/utils/tickets-manager.js.map +1 -1
  45. package/dist/lib.d.ts +6 -1
  46. package/dist/lib.d.ts.map +1 -1
  47. package/dist/lib.js +3 -0
  48. package/dist/lib.js.map +1 -1
  49. package/dist/package.json +1 -1
  50. 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