gencode-ai 0.1.2 → 0.2.0

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 (180) hide show
  1. package/README.md +15 -17
  2. package/dist/agent/agent.d.ts +43 -0
  3. package/dist/agent/agent.d.ts.map +1 -1
  4. package/dist/agent/agent.js +107 -4
  5. package/dist/agent/agent.js.map +1 -1
  6. package/dist/agent/index.d.ts +1 -0
  7. package/dist/agent/index.d.ts.map +1 -1
  8. package/dist/agent/types.d.ts +20 -1
  9. package/dist/agent/types.d.ts.map +1 -1
  10. package/dist/checkpointing/checkpoint-manager.d.ts +87 -0
  11. package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -0
  12. package/dist/checkpointing/checkpoint-manager.js +281 -0
  13. package/dist/checkpointing/checkpoint-manager.js.map +1 -0
  14. package/dist/checkpointing/index.d.ts +29 -0
  15. package/dist/checkpointing/index.d.ts.map +1 -0
  16. package/dist/checkpointing/index.js +29 -0
  17. package/dist/checkpointing/index.js.map +1 -0
  18. package/dist/checkpointing/types.d.ts +98 -0
  19. package/dist/checkpointing/types.d.ts.map +1 -0
  20. package/dist/checkpointing/types.js +7 -0
  21. package/dist/checkpointing/types.js.map +1 -0
  22. package/dist/cli/components/App.d.ts.map +1 -1
  23. package/dist/cli/components/App.js +193 -7
  24. package/dist/cli/components/App.js.map +1 -1
  25. package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
  26. package/dist/cli/components/CommandSuggestions.js +5 -0
  27. package/dist/cli/components/CommandSuggestions.js.map +1 -1
  28. package/dist/cli/components/Messages.d.ts +7 -1
  29. package/dist/cli/components/Messages.d.ts.map +1 -1
  30. package/dist/cli/components/Messages.js +28 -2
  31. package/dist/cli/components/Messages.js.map +1 -1
  32. package/dist/cli/components/ModeIndicator.d.ts +42 -0
  33. package/dist/cli/components/ModeIndicator.d.ts.map +1 -0
  34. package/dist/cli/components/ModeIndicator.js +52 -0
  35. package/dist/cli/components/ModeIndicator.js.map +1 -0
  36. package/dist/cli/components/PlanApproval.d.ts +36 -0
  37. package/dist/cli/components/PlanApproval.d.ts.map +1 -0
  38. package/dist/cli/components/PlanApproval.js +154 -0
  39. package/dist/cli/components/PlanApproval.js.map +1 -0
  40. package/dist/cli/components/QuestionPrompt.d.ts +23 -0
  41. package/dist/cli/components/QuestionPrompt.d.ts.map +1 -0
  42. package/dist/cli/components/QuestionPrompt.js +231 -0
  43. package/dist/cli/components/QuestionPrompt.js.map +1 -0
  44. package/dist/cli/components/index.d.ts +1 -0
  45. package/dist/cli/components/index.d.ts.map +1 -1
  46. package/dist/cli/components/index.js +1 -0
  47. package/dist/cli/components/index.js.map +1 -1
  48. package/dist/cli/components/theme.d.ts +9 -0
  49. package/dist/cli/components/theme.d.ts.map +1 -1
  50. package/dist/cli/components/theme.js +14 -1
  51. package/dist/cli/components/theme.js.map +1 -1
  52. package/dist/index.d.ts +1 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/permissions/types.d.ts.map +1 -1
  57. package/dist/permissions/types.js +2 -0
  58. package/dist/permissions/types.js.map +1 -1
  59. package/dist/planning/index.d.ts +13 -0
  60. package/dist/planning/index.d.ts.map +1 -0
  61. package/dist/planning/index.js +15 -0
  62. package/dist/planning/index.js.map +1 -0
  63. package/dist/planning/plan-file.d.ts +59 -0
  64. package/dist/planning/plan-file.d.ts.map +1 -0
  65. package/dist/planning/plan-file.js +278 -0
  66. package/dist/planning/plan-file.js.map +1 -0
  67. package/dist/planning/state.d.ts +127 -0
  68. package/dist/planning/state.d.ts.map +1 -0
  69. package/dist/planning/state.js +261 -0
  70. package/dist/planning/state.js.map +1 -0
  71. package/dist/planning/tools/enter-plan-mode.d.ts +25 -0
  72. package/dist/planning/tools/enter-plan-mode.d.ts.map +1 -0
  73. package/dist/planning/tools/enter-plan-mode.js +98 -0
  74. package/dist/planning/tools/enter-plan-mode.js.map +1 -0
  75. package/dist/planning/tools/exit-plan-mode.d.ts +24 -0
  76. package/dist/planning/tools/exit-plan-mode.d.ts.map +1 -0
  77. package/dist/planning/tools/exit-plan-mode.js +149 -0
  78. package/dist/planning/tools/exit-plan-mode.js.map +1 -0
  79. package/dist/planning/types.d.ts +100 -0
  80. package/dist/planning/types.d.ts.map +1 -0
  81. package/dist/planning/types.js +28 -0
  82. package/dist/planning/types.js.map +1 -0
  83. package/dist/pricing/calculator.d.ts +21 -0
  84. package/dist/pricing/calculator.d.ts.map +1 -0
  85. package/dist/pricing/calculator.js +59 -0
  86. package/dist/pricing/calculator.js.map +1 -0
  87. package/dist/pricing/index.d.ts +7 -0
  88. package/dist/pricing/index.d.ts.map +1 -0
  89. package/dist/pricing/index.js +7 -0
  90. package/dist/pricing/index.js.map +1 -0
  91. package/dist/pricing/models.d.ts +20 -0
  92. package/dist/pricing/models.d.ts.map +1 -0
  93. package/dist/pricing/models.js +322 -0
  94. package/dist/pricing/models.js.map +1 -0
  95. package/dist/pricing/types.d.ts +30 -0
  96. package/dist/pricing/types.d.ts.map +1 -0
  97. package/dist/pricing/types.js +5 -0
  98. package/dist/pricing/types.js.map +1 -0
  99. package/dist/providers/anthropic.d.ts.map +1 -1
  100. package/dist/providers/anthropic.js +17 -10
  101. package/dist/providers/anthropic.js.map +1 -1
  102. package/dist/providers/gemini.d.ts.map +1 -1
  103. package/dist/providers/gemini.js +21 -14
  104. package/dist/providers/gemini.js.map +1 -1
  105. package/dist/providers/openai.d.ts.map +1 -1
  106. package/dist/providers/openai.js +12 -8
  107. package/dist/providers/openai.js.map +1 -1
  108. package/dist/providers/types.d.ts +2 -0
  109. package/dist/providers/types.d.ts.map +1 -1
  110. package/dist/providers/vertex-ai.d.ts.map +1 -1
  111. package/dist/providers/vertex-ai.js +17 -10
  112. package/dist/providers/vertex-ai.js.map +1 -1
  113. package/dist/session/manager.d.ts +4 -0
  114. package/dist/session/manager.d.ts.map +1 -1
  115. package/dist/session/manager.js +8 -0
  116. package/dist/session/manager.js.map +1 -1
  117. package/dist/tools/builtin/ask-user.d.ts +64 -0
  118. package/dist/tools/builtin/ask-user.d.ts.map +1 -0
  119. package/dist/tools/builtin/ask-user.js +148 -0
  120. package/dist/tools/builtin/ask-user.js.map +1 -0
  121. package/dist/tools/index.d.ts +19 -1
  122. package/dist/tools/index.d.ts.map +1 -1
  123. package/dist/tools/index.js +11 -0
  124. package/dist/tools/index.js.map +1 -1
  125. package/dist/tools/registry.d.ts +13 -0
  126. package/dist/tools/registry.d.ts.map +1 -1
  127. package/dist/tools/registry.js +79 -2
  128. package/dist/tools/registry.js.map +1 -1
  129. package/dist/tools/types.d.ts +17 -0
  130. package/dist/tools/types.d.ts.map +1 -1
  131. package/dist/tools/types.js.map +1 -1
  132. package/docs/cost-tracking-comparison.md +904 -0
  133. package/docs/operating-modes.md +96 -0
  134. package/docs/proposals/0012-ask-user-question.md +66 -1
  135. package/docs/proposals/0025-cost-tracking.md +60 -2
  136. package/docs/proposals/README.md +2 -2
  137. package/examples/test-ask-user.ts +167 -0
  138. package/examples/test-checkpointing.ts +121 -0
  139. package/examples/test-cost-tracking.ts +77 -0
  140. package/examples/test-interrupt-cleanup.ts +94 -0
  141. package/package.json +1 -1
  142. package/src/agent/agent.ts +130 -4
  143. package/src/agent/index.ts +1 -0
  144. package/src/agent/types.ts +19 -1
  145. package/src/checkpointing/checkpoint-manager.ts +327 -0
  146. package/src/checkpointing/index.ts +45 -0
  147. package/src/checkpointing/types.ts +104 -0
  148. package/src/cli/components/App.tsx +259 -8
  149. package/src/cli/components/CommandSuggestions.tsx +5 -0
  150. package/src/cli/components/Messages.tsx +66 -4
  151. package/src/cli/components/ModeIndicator.tsx +174 -0
  152. package/src/cli/components/PlanApproval.tsx +327 -0
  153. package/src/cli/components/QuestionPrompt.tsx +462 -0
  154. package/src/cli/components/index.ts +1 -0
  155. package/src/cli/components/theme.ts +14 -1
  156. package/src/index.ts +15 -0
  157. package/src/permissions/types.ts +2 -0
  158. package/src/planning/index.ts +53 -0
  159. package/src/planning/plan-file.ts +326 -0
  160. package/src/planning/state.ts +305 -0
  161. package/src/planning/tools/enter-plan-mode.ts +111 -0
  162. package/src/planning/tools/exit-plan-mode.ts +170 -0
  163. package/src/planning/types.ts +150 -0
  164. package/src/pricing/calculator.ts +71 -0
  165. package/src/pricing/index.ts +7 -0
  166. package/src/pricing/models.ts +334 -0
  167. package/src/pricing/types.ts +32 -0
  168. package/src/prompts/system/base.txt +42 -0
  169. package/src/prompts/tools/ask-user.txt +110 -0
  170. package/src/providers/anthropic.ts +21 -10
  171. package/src/providers/gemini.ts +25 -14
  172. package/src/providers/openai.ts +17 -8
  173. package/src/providers/types.ts +3 -0
  174. package/src/providers/vertex-ai.ts +21 -10
  175. package/src/session/manager.ts +9 -0
  176. package/src/tools/builtin/ask-user.ts +185 -0
  177. package/src/tools/index.ts +23 -0
  178. package/src/tools/registry.ts +95 -2
  179. package/src/tools/types.ts +18 -0
  180. package/.gencode/settings.local.json +0 -7
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Plan File Utilities
3
+ *
4
+ * Manages plan files stored in .gencode/plans/ directory.
5
+ * Generates unique filenames with timestamps and slugs.
6
+ */
7
+
8
+ import * as fs from 'fs/promises';
9
+ import * as path from 'path';
10
+ import { existsSync } from 'fs';
11
+ import type { PlanFile } from './types.js';
12
+
13
+ // ============================================================================
14
+ // Constants
15
+ // ============================================================================
16
+
17
+ const PLANS_DIR = '.gencode/plans';
18
+ const PLAN_FILE_EXTENSION = '.md';
19
+
20
+ // Word lists for generating memorable names (like Claude Code)
21
+ const ADJECTIVES = [
22
+ 'agile', 'bold', 'calm', 'deft', 'eager', 'fair', 'glad', 'humble',
23
+ 'ideal', 'jolly', 'keen', 'lively', 'merry', 'noble', 'polite', 'quiet',
24
+ 'rapid', 'smart', 'tidy', 'unique', 'vivid', 'warm', 'zealous', 'bright',
25
+ 'clear', 'crisp', 'fresh', 'golden', 'happy', 'lovely', 'neat', 'proud',
26
+ ];
27
+
28
+ const NOUNS = [
29
+ 'alpine', 'beacon', 'cipher', 'delta', 'ember', 'falcon', 'glacier', 'harbor',
30
+ 'island', 'jasper', 'kayak', 'lantern', 'marble', 'nebula', 'oracle', 'prism',
31
+ 'quartz', 'rapids', 'summit', 'timber', 'unity', 'vertex', 'willow', 'zenith',
32
+ 'arrow', 'bridge', 'canyon', 'dawn', 'echo', 'forest', 'grove', 'hollow',
33
+ ];
34
+
35
+ // ============================================================================
36
+ // Name Generation
37
+ // ============================================================================
38
+
39
+ /**
40
+ * Generate a memorable plan name (adjective-noun)
41
+ * Example: "agile-beacon", "bold-cipher"
42
+ */
43
+ function generateMemorableName(): string {
44
+ const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
45
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
46
+ return `${adjective}-${noun}`;
47
+ }
48
+
49
+ /**
50
+ * Generate a slug from text (for task-specific naming)
51
+ */
52
+ function generateSlug(text: string): string {
53
+ return text
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special chars
56
+ .replace(/\s+/g, '-') // Replace spaces with dashes
57
+ .replace(/-+/g, '-') // Collapse multiple dashes
58
+ .slice(0, 30) // Limit length
59
+ .replace(/^-|-$/g, ''); // Trim dashes from ends
60
+ }
61
+
62
+ /**
63
+ * Generate plan file name
64
+ * Format: <memorable-name>.md (like Claude Code)
65
+ */
66
+ export function generatePlanFileName(taskDescription?: string): string {
67
+ // Use memorable name like Claude Code (melodic-humming-lampson)
68
+ const name = generateMemorableName();
69
+ return `${name}${PLAN_FILE_EXTENSION}`;
70
+ }
71
+
72
+ // ============================================================================
73
+ // Directory Management
74
+ // ============================================================================
75
+
76
+ /**
77
+ * Get the plans directory path for a project
78
+ */
79
+ export function getPlansDir(cwd: string): string {
80
+ return path.join(cwd, PLANS_DIR);
81
+ }
82
+
83
+ /**
84
+ * Ensure the plans directory exists
85
+ */
86
+ export async function ensurePlansDir(cwd: string): Promise<string> {
87
+ const plansDir = getPlansDir(cwd);
88
+
89
+ if (!existsSync(plansDir)) {
90
+ await fs.mkdir(plansDir, { recursive: true });
91
+ }
92
+
93
+ return plansDir;
94
+ }
95
+
96
+ // ============================================================================
97
+ // Plan File Operations
98
+ // ============================================================================
99
+
100
+ /**
101
+ * Create a new plan file
102
+ */
103
+ export async function createPlanFile(
104
+ cwd: string,
105
+ taskDescription?: string
106
+ ): Promise<PlanFile> {
107
+ const plansDir = await ensurePlansDir(cwd);
108
+ const fileName = generatePlanFileName(taskDescription);
109
+ const filePath = path.join(plansDir, fileName);
110
+
111
+ // Initial plan template
112
+ const initialContent = `# Implementation Plan
113
+
114
+ ## Task
115
+ ${taskDescription || 'Describe the task here...'}
116
+
117
+ ## Analysis
118
+ _Understanding the codebase and requirements..._
119
+
120
+ ## Approach
121
+ _Design decisions and implementation strategy..._
122
+
123
+ ## Files to Change
124
+ - [ ] File 1 (action)
125
+ - [ ] File 2 (action)
126
+
127
+ ## Steps
128
+ 1. Step 1
129
+ 2. Step 2
130
+ 3. Step 3
131
+
132
+ ## Pre-approved Permissions
133
+ _Commands that will be allowed during execution..._
134
+
135
+ ---
136
+ _Generated by GenCode Plan Mode_
137
+ `;
138
+
139
+ const now = new Date();
140
+ await fs.writeFile(filePath, initialContent, 'utf-8');
141
+
142
+ return {
143
+ path: filePath,
144
+ content: initialContent,
145
+ createdAt: now,
146
+ updatedAt: now,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Read a plan file
152
+ */
153
+ export async function readPlanFile(filePath: string): Promise<PlanFile | null> {
154
+ try {
155
+ const content = await fs.readFile(filePath, 'utf-8');
156
+ const stats = await fs.stat(filePath);
157
+
158
+ return {
159
+ path: filePath,
160
+ content,
161
+ createdAt: stats.birthtime,
162
+ updatedAt: stats.mtime,
163
+ };
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Write to a plan file
171
+ */
172
+ export async function writePlanFile(filePath: string, content: string): Promise<void> {
173
+ // Ensure directory exists
174
+ const dir = path.dirname(filePath);
175
+ if (!existsSync(dir)) {
176
+ await fs.mkdir(dir, { recursive: true });
177
+ }
178
+
179
+ await fs.writeFile(filePath, content, 'utf-8');
180
+ }
181
+
182
+ /**
183
+ * List all plan files in the project
184
+ */
185
+ export async function listPlanFiles(cwd: string): Promise<PlanFile[]> {
186
+ const plansDir = getPlansDir(cwd);
187
+
188
+ if (!existsSync(plansDir)) {
189
+ return [];
190
+ }
191
+
192
+ try {
193
+ const files = await fs.readdir(plansDir);
194
+ const planFiles: PlanFile[] = [];
195
+
196
+ for (const file of files) {
197
+ if (file.endsWith(PLAN_FILE_EXTENSION)) {
198
+ const filePath = path.join(plansDir, file);
199
+ const planFile = await readPlanFile(filePath);
200
+ if (planFile) {
201
+ planFiles.push(planFile);
202
+ }
203
+ }
204
+ }
205
+
206
+ // Sort by updated time, newest first
207
+ planFiles.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
208
+
209
+ return planFiles;
210
+ } catch {
211
+ return [];
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Delete a plan file
217
+ */
218
+ export async function deletePlanFile(filePath: string): Promise<boolean> {
219
+ try {
220
+ await fs.unlink(filePath);
221
+ return true;
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ // ============================================================================
228
+ // Plan Content Parsing
229
+ // ============================================================================
230
+
231
+ /**
232
+ * Extract files to change from plan content
233
+ */
234
+ export function parseFilesToChange(
235
+ content: string
236
+ ): Array<{ path: string; action: 'create' | 'modify' | 'delete' }> {
237
+ const files: Array<{ path: string; action: 'create' | 'modify' | 'delete' }> = [];
238
+
239
+ // Look for patterns like:
240
+ // - [ ] src/file.ts (create)
241
+ // - [x] src/file.ts (modify)
242
+ // + src/file.ts (create)
243
+ // ~ src/file.ts (modify)
244
+ // - src/file.ts (delete)
245
+
246
+ const lines = content.split('\n');
247
+ for (const line of lines) {
248
+ // Checkbox format
249
+ let match = line.match(/^[-*]\s*\[[ x]\]\s+([^\s(]+)\s*\((\w+)\)/);
250
+ if (match) {
251
+ const [, filePath, action] = match;
252
+ if (action === 'create' || action === 'modify' || action === 'delete') {
253
+ files.push({ path: filePath, action });
254
+ }
255
+ continue;
256
+ }
257
+
258
+ // Symbol format (+, ~, -)
259
+ match = line.match(/^\s*([+~-])\s+([^\s(]+)(?:\s*\((\w+)\))?/);
260
+ if (match) {
261
+ const [, symbol, filePath, explicitAction] = match;
262
+ let action: 'create' | 'modify' | 'delete';
263
+
264
+ if (explicitAction === 'create' || explicitAction === 'modify' || explicitAction === 'delete') {
265
+ action = explicitAction;
266
+ } else {
267
+ action = symbol === '+' ? 'create' : symbol === '~' ? 'modify' : 'delete';
268
+ }
269
+
270
+ files.push({ path: filePath, action });
271
+ }
272
+ }
273
+
274
+ return files;
275
+ }
276
+
277
+ /**
278
+ * Extract pre-approved permissions from plan content
279
+ */
280
+ export function parsePreApprovedPermissions(content: string): Array<{ tool: 'Bash'; prompt: string }> {
281
+ const permissions: Array<{ tool: 'Bash'; prompt: string }> = [];
282
+
283
+ // Look for patterns like:
284
+ // - Bash: run tests
285
+ // - npm test
286
+ // - npm install
287
+
288
+ const permissionSection = content.match(/## Pre-approved Permissions[\s\S]*?(?=##|$)/i);
289
+ if (!permissionSection) {
290
+ return permissions;
291
+ }
292
+
293
+ const lines = permissionSection[0].split('\n');
294
+ for (const line of lines) {
295
+ // Skip header and empty lines
296
+ if (line.startsWith('##') || line.startsWith('_') || !line.trim()) {
297
+ continue;
298
+ }
299
+
300
+ // Match "- Bash: description" or "- description"
301
+ const match = line.match(/^[-*]\s+(?:Bash:\s+)?(.+)/);
302
+ if (match) {
303
+ permissions.push({ tool: 'Bash', prompt: match[1].trim() });
304
+ }
305
+ }
306
+
307
+ return permissions;
308
+ }
309
+
310
+ /**
311
+ * Get relative path for display
312
+ */
313
+ export function getDisplayPath(fullPath: string, cwd: string): string {
314
+ const relativePath = path.relative(cwd, fullPath);
315
+ const home = process.env.HOME || '';
316
+
317
+ if (relativePath.startsWith('..')) {
318
+ // Path is outside cwd, try home-relative
319
+ if (fullPath.startsWith(home)) {
320
+ return '~' + fullPath.slice(home.length);
321
+ }
322
+ return fullPath;
323
+ }
324
+
325
+ return relativePath;
326
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Plan Mode State Management
3
+ *
4
+ * Singleton state manager for plan mode. Tracks whether plan mode
5
+ * is active, current phase, and manages tool filtering.
6
+ */
7
+
8
+ import type {
9
+ PlanModeState,
10
+ PlanPhase,
11
+ AllowedPrompt,
12
+ ModeType,
13
+ PlanModeEvent,
14
+ } from './types.js';
15
+ import { PLAN_MODE_ALLOWED_TOOLS, PLAN_MODE_BLOCKED_TOOLS } from './types.js';
16
+
17
+ // ============================================================================
18
+ // State Manager
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Plan Mode State Manager
23
+ *
24
+ * Manages the plan mode state and provides methods for:
25
+ * - Entering/exiting plan mode
26
+ * - Phase transitions
27
+ * - Tool filtering
28
+ * - Event notifications
29
+ */
30
+ export class PlanModeManager {
31
+ private state: PlanModeState;
32
+ private eventListeners: Set<(event: PlanModeEvent) => void>;
33
+
34
+ constructor() {
35
+ this.state = this.getInitialState();
36
+ this.eventListeners = new Set();
37
+ }
38
+
39
+ /**
40
+ * Get initial state
41
+ */
42
+ private getInitialState(): PlanModeState {
43
+ return {
44
+ active: false,
45
+ phase: 'understanding',
46
+ planFilePath: null,
47
+ originalRequest: null,
48
+ requestedPermissions: [],
49
+ enteredAt: null,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Get current state (readonly)
55
+ */
56
+ getState(): Readonly<PlanModeState> {
57
+ return { ...this.state };
58
+ }
59
+
60
+ /**
61
+ * Check if plan mode is active
62
+ */
63
+ isActive(): boolean {
64
+ return this.state.active;
65
+ }
66
+
67
+ /**
68
+ * Get current mode type for UI
69
+ * Note: Returns 'plan' or 'normal'. 'accept' mode is managed at the App level.
70
+ */
71
+ getCurrentMode(): ModeType {
72
+ return this.state.active ? 'plan' : 'normal';
73
+ }
74
+
75
+ /**
76
+ * Enter plan mode
77
+ */
78
+ enter(planFilePath: string, originalRequest?: string): void {
79
+ this.state = {
80
+ active: true,
81
+ phase: 'understanding',
82
+ planFilePath,
83
+ originalRequest: originalRequest ?? null,
84
+ requestedPermissions: [],
85
+ enteredAt: new Date(),
86
+ };
87
+
88
+ this.emit({ type: 'enter', planFilePath });
89
+ }
90
+
91
+ /**
92
+ * Exit plan mode
93
+ */
94
+ exit(approved: boolean = false): void {
95
+ const wasActive = this.state.active;
96
+ this.state = this.getInitialState();
97
+
98
+ if (wasActive) {
99
+ this.emit({ type: 'exit', approved });
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Toggle plan mode (for Shift+Tab)
105
+ */
106
+ toggle(planFilePath?: string): void {
107
+ if (this.state.active) {
108
+ this.exit(false);
109
+ } else if (planFilePath) {
110
+ this.enter(planFilePath);
111
+ }
112
+ this.emit({ type: 'toggle' });
113
+ }
114
+
115
+ /**
116
+ * Update phase
117
+ */
118
+ setPhase(phase: PlanPhase): void {
119
+ if (this.state.active) {
120
+ this.state.phase = phase;
121
+ this.emit({ type: 'phase_change', phase });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get current phase
127
+ */
128
+ getPhase(): PlanPhase {
129
+ return this.state.phase;
130
+ }
131
+
132
+ /**
133
+ * Set requested permissions (from ExitPlanMode)
134
+ */
135
+ setRequestedPermissions(permissions: AllowedPrompt[]): void {
136
+ this.state.requestedPermissions = permissions;
137
+ }
138
+
139
+ /**
140
+ * Get requested permissions
141
+ */
142
+ getRequestedPermissions(): AllowedPrompt[] {
143
+ return [...this.state.requestedPermissions];
144
+ }
145
+
146
+ /**
147
+ * Get plan file path
148
+ */
149
+ getPlanFilePath(): string | null {
150
+ return this.state.planFilePath;
151
+ }
152
+
153
+ // ============================================================================
154
+ // Tool Filtering
155
+ // ============================================================================
156
+
157
+ /**
158
+ * Check if a tool is allowed in the current mode
159
+ */
160
+ isToolAllowed(toolName: string): boolean {
161
+ if (!this.state.active) {
162
+ return true; // All tools allowed in build mode
163
+ }
164
+
165
+ // In plan mode, only allow read-only tools
166
+ return (PLAN_MODE_ALLOWED_TOOLS as readonly string[]).includes(toolName);
167
+ }
168
+
169
+ /**
170
+ * Check if a tool is blocked in the current mode
171
+ */
172
+ isToolBlocked(toolName: string): boolean {
173
+ if (!this.state.active) {
174
+ return false; // No tools blocked in build mode
175
+ }
176
+
177
+ return (PLAN_MODE_BLOCKED_TOOLS as readonly string[]).includes(toolName);
178
+ }
179
+
180
+ /**
181
+ * Get list of allowed tools for current mode
182
+ */
183
+ getAllowedTools(): string[] {
184
+ if (!this.state.active) {
185
+ return []; // Empty means all allowed
186
+ }
187
+ return [...PLAN_MODE_ALLOWED_TOOLS];
188
+ }
189
+
190
+ /**
191
+ * Get list of blocked tools for current mode
192
+ */
193
+ getBlockedTools(): string[] {
194
+ if (!this.state.active) {
195
+ return [];
196
+ }
197
+ return [...PLAN_MODE_BLOCKED_TOOLS];
198
+ }
199
+
200
+ /**
201
+ * Filter tool list based on current mode
202
+ */
203
+ filterTools(toolNames: string[]): string[] {
204
+ if (!this.state.active) {
205
+ return toolNames;
206
+ }
207
+
208
+ return toolNames.filter((name) => this.isToolAllowed(name));
209
+ }
210
+
211
+ // ============================================================================
212
+ // Event System
213
+ // ============================================================================
214
+
215
+ /**
216
+ * Subscribe to plan mode events
217
+ */
218
+ subscribe(listener: (event: PlanModeEvent) => void): () => void {
219
+ this.eventListeners.add(listener);
220
+ return () => {
221
+ this.eventListeners.delete(listener);
222
+ };
223
+ }
224
+
225
+ /**
226
+ * Emit an event to all listeners
227
+ */
228
+ private emit(event: PlanModeEvent): void {
229
+ for (const listener of this.eventListeners) {
230
+ try {
231
+ listener(event);
232
+ } catch (error) {
233
+ console.error('Error in plan mode event listener:', error);
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ // ============================================================================
240
+ // Singleton Instance
241
+ // ============================================================================
242
+
243
+ /**
244
+ * Global plan mode manager instance
245
+ */
246
+ let globalPlanModeManager: PlanModeManager | null = null;
247
+
248
+ /**
249
+ * Get the global plan mode manager
250
+ */
251
+ export function getPlanModeManager(): PlanModeManager {
252
+ if (!globalPlanModeManager) {
253
+ globalPlanModeManager = new PlanModeManager();
254
+ }
255
+ return globalPlanModeManager;
256
+ }
257
+
258
+ /**
259
+ * Reset the global plan mode manager (for testing)
260
+ */
261
+ export function resetPlanModeManager(): void {
262
+ if (globalPlanModeManager) {
263
+ globalPlanModeManager.exit(false);
264
+ }
265
+ globalPlanModeManager = null;
266
+ }
267
+
268
+ // ============================================================================
269
+ // Convenience Functions
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Check if plan mode is currently active
274
+ */
275
+ export function isPlanModeActive(): boolean {
276
+ return getPlanModeManager().isActive();
277
+ }
278
+
279
+ /**
280
+ * Get the current mode type
281
+ */
282
+ export function getCurrentMode(): ModeType {
283
+ return getPlanModeManager().getCurrentMode();
284
+ }
285
+
286
+ /**
287
+ * Enter plan mode
288
+ */
289
+ export function enterPlanMode(planFilePath: string, originalRequest?: string): void {
290
+ getPlanModeManager().enter(planFilePath, originalRequest);
291
+ }
292
+
293
+ /**
294
+ * Exit plan mode
295
+ */
296
+ export function exitPlanMode(approved: boolean = false): void {
297
+ getPlanModeManager().exit(approved);
298
+ }
299
+
300
+ /**
301
+ * Toggle plan mode
302
+ */
303
+ export function togglePlanMode(planFilePath?: string): void {
304
+ getPlanModeManager().toggle(planFilePath);
305
+ }