@kosuke-ai/cli 0.0.41 → 0.0.42

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