@kosuke-ai/cli 0.0.40 → 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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +77 -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 +817 -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 +27 -18
- package/dist/kosuke/commands/tickets.d.ts.map +1 -1
- package/dist/kosuke/commands/tickets.js +176 -152
- package/dist/kosuke/commands/tickets.js.map +1 -1
- package/dist/kosuke/types.d.ts +8 -0
- 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/ticket-writer.d.ts +48 -0
- package/dist/kosuke/utils/ticket-writer.d.ts.map +1 -0
- package/dist/kosuke/utils/ticket-writer.js +259 -0
- package/dist/kosuke/utils/ticket-writer.js.map +1 -0
- 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,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
|