@automagik/genie 0.260202.1607 → 0.260202.1833

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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Claude Code output patterns for state detection
3
+ *
4
+ * These patterns are used to identify different interactive states
5
+ * when Claude Code is running in a terminal session.
6
+ */
7
+
8
+ export interface PatternMatch {
9
+ type: string;
10
+ pattern: RegExp;
11
+ extract?: (match: RegExpMatchArray) => Record<string, string>;
12
+ }
13
+
14
+ // Permission request patterns
15
+ export const permissionPatterns: PatternMatch[] = [
16
+ {
17
+ type: 'bash_permission',
18
+ pattern: /Allow (?:Bash|bash|command|shell).*\?\s*(?:\[([YyNn])\/([YyNn])\])?/i,
19
+ extract: (match) => ({ default: match[1] || 'y' }),
20
+ },
21
+ {
22
+ type: 'file_permission',
23
+ pattern: /Allow (?:Edit|Write|Read|file|reading|writing|editing).*\?\s*(?:\[([YyNn])\/([YyNn])\])?/i,
24
+ extract: (match) => ({ default: match[1] || 'y' }),
25
+ },
26
+ {
27
+ type: 'mcp_permission',
28
+ pattern: /Allow (?:MCP|mcp|tool).*\?\s*(?:\[([YyNn])\/([YyNn])\])?/i,
29
+ extract: (match) => ({ default: match[1] || 'y' }),
30
+ },
31
+ {
32
+ type: 'generic_permission',
33
+ // Only match actual permission prompts, not questions with "proceed"
34
+ // Must start with "Allow" and typically have a tool/action context
35
+ pattern: /^(?:Allow|Confirm|Approve)\s+(?:this|the|once|always)?\s*(?:\w+)?\s*\?\s*(?:\[([YyNn])\/([YyNn])\])?/im,
36
+ extract: (match) => ({ default: match[1] || 'y' }),
37
+ },
38
+ {
39
+ type: 'claude_code_yes_no',
40
+ // Claude Code uses Yes/No prompts
41
+ pattern: /(?:Yes|No)\s*$/m,
42
+ },
43
+ {
44
+ type: 'claude_code_permission_block',
45
+ // Claude Code permission blocks often show the tool name
46
+ pattern: /(?:Allow|Run|Execute)\s+(?:once|always)/i,
47
+ },
48
+ ];
49
+
50
+ // Question with options patterns
51
+ export const questionPatterns: PatternMatch[] = [
52
+ {
53
+ type: 'numbered_options',
54
+ pattern: /\[(\d+)\]\s+(.+?)(?=\[(\d+)\]|$)/g,
55
+ extract: (match) => ({ number: match[1], option: match[2].trim() }),
56
+ },
57
+ {
58
+ type: 'lettered_options',
59
+ pattern: /\(([a-z])\)\s+(.+?)(?=\([a-z]\)|$)/gi,
60
+ extract: (match) => ({ letter: match[1], option: match[2].trim() }),
61
+ },
62
+ {
63
+ type: 'yes_no_question',
64
+ pattern: /\?\s*\[([YyNn])\/([YyNn])\]\s*$/,
65
+ extract: (match) => ({ default: match[1] }),
66
+ },
67
+ {
68
+ type: 'claude_code_numbered_options',
69
+ // Claude Code uses "❯ 1." or " 2." format for menu selection
70
+ pattern: /(?:❯|>)?\s*(\d+)\.\s+(.+?)(?:\n|$)/g,
71
+ extract: (match) => ({ number: match[1], option: match[2].trim() }),
72
+ },
73
+ {
74
+ type: 'claude_code_plan_approval',
75
+ // Claude Code plan approval prompt
76
+ pattern: /Would you like to proceed\?/i,
77
+ },
78
+ ];
79
+
80
+ // Error patterns
81
+ export const errorPatterns: PatternMatch[] = [
82
+ {
83
+ type: 'error',
84
+ pattern: /(?:^|\n)\s*(?:Error|ERROR|error):\s*(.+)/,
85
+ extract: (match) => ({ message: match[1] }),
86
+ },
87
+ {
88
+ type: 'failed',
89
+ pattern: /(?:^|\n)\s*(?:Failed|FAILED|failed):\s*(.+)/,
90
+ extract: (match) => ({ message: match[1] }),
91
+ },
92
+ {
93
+ type: 'exception',
94
+ pattern: /(?:^|\n)\s*(?:Exception|EXCEPTION|Uncaught|Unhandled):\s*(.+)/,
95
+ extract: (match) => ({ message: match[1] }),
96
+ },
97
+ {
98
+ type: 'api_error',
99
+ pattern: /(?:API|api)\s+(?:error|Error|ERROR):\s*(.+)/,
100
+ extract: (match) => ({ message: match[1] }),
101
+ },
102
+ ];
103
+
104
+ // Completion/success patterns
105
+ export const completionPatterns: PatternMatch[] = [
106
+ {
107
+ type: 'checkmark',
108
+ pattern: /[✓✔☑︎]/,
109
+ },
110
+ {
111
+ type: 'success_message',
112
+ pattern: /(?:Successfully|Completed|Done|Finished|Created|Updated|Saved)/i,
113
+ },
114
+ {
115
+ type: 'task_complete',
116
+ pattern: /(?:task|operation|process)\s+(?:complete|completed|finished|done)/i,
117
+ },
118
+ ];
119
+
120
+ // Working/thinking patterns
121
+ export const workingPatterns: PatternMatch[] = [
122
+ {
123
+ type: 'thinking',
124
+ pattern: /(?:Thinking|thinking|Processing|processing)\.\.\./,
125
+ },
126
+ {
127
+ type: 'spinner',
128
+ pattern: /[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⣾⣽⣻⢿⡿⣟⣯⣷]/,
129
+ },
130
+ {
131
+ type: 'loading',
132
+ pattern: /(?:Loading|loading|Working|working)\.\.\./,
133
+ },
134
+ {
135
+ type: 'claude_code_working',
136
+ // Claude Code shows tool icons when working
137
+ pattern: /[🛠️🔧⚙️]\s*(?:Read|Edit|Write|Bash|Glob|Grep|Task)/,
138
+ },
139
+ {
140
+ type: 'claude_code_streaming',
141
+ // Partial response streaming
142
+ pattern: /▌$/,
143
+ },
144
+ {
145
+ type: 'claude_code_propagating',
146
+ // Claude shows "Propagating..." when applying changes
147
+ pattern: /Propagating…/,
148
+ },
149
+ ];
150
+
151
+ // Idle/prompt patterns - Claude Code is waiting for input
152
+ export const idlePatterns: PatternMatch[] = [
153
+ {
154
+ type: 'claude_prompt',
155
+ // Look for the Claude Code prompt character (typically > or similar)
156
+ // But NOT when followed by a number (that's a menu)
157
+ pattern: /(?:^|\n)\s*>\s*$/,
158
+ },
159
+ {
160
+ type: 'claude_code_prompt',
161
+ // Claude Code prompt with ❯ alone (NOT followed by number - that's a menu)
162
+ // Match ❯ at end of line, or ❯ followed by text (user input)
163
+ pattern: /❯\s*(?!\d\.)/,
164
+ },
165
+ {
166
+ type: 'claude_code_input_line',
167
+ // Claude Code shows ❯ followed by user's typed input, then a line of dashes
168
+ // This indicates the input prompt is active
169
+ pattern: /❯\s*.+\n─+\n/,
170
+ },
171
+ {
172
+ type: 'idle_indicator',
173
+ // Claude Code status bar showing idle
174
+ pattern: /\|\s*idle\s*$/i,
175
+ },
176
+ {
177
+ type: 'input_prompt',
178
+ // Generic input prompt
179
+ pattern: /(?:^|\n)(?:Enter|Input|Type|Provide).*:\s*$/i,
180
+ },
181
+ ];
182
+
183
+ // Tool use patterns
184
+ export const toolUsePatterns: PatternMatch[] = [
185
+ {
186
+ type: 'run_command',
187
+ pattern: /(?:Run|Running|Executing)\s+(?:command|bash):\s*(.+)/i,
188
+ extract: (match) => ({ command: match[1] }),
189
+ },
190
+ {
191
+ type: 'read_file',
192
+ pattern: /(?:Read|Reading)\s+(?:file):\s*(.+)/i,
193
+ extract: (match) => ({ file: match[1] }),
194
+ },
195
+ {
196
+ type: 'write_file',
197
+ pattern: /(?:Write|Writing|Edit|Editing)\s+(?:file|to):\s*(.+)/i,
198
+ extract: (match) => ({ file: match[1] }),
199
+ },
200
+ {
201
+ type: 'search',
202
+ pattern: /(?:Search|Searching|Grep|Glob)(?:ing)?:\s*(.+)/i,
203
+ extract: (match) => ({ query: match[1] }),
204
+ },
205
+ ];
206
+
207
+ // Plan file patterns - extract plan file paths from Claude Code output
208
+ export const planFilePatterns: PatternMatch[] = [
209
+ {
210
+ type: 'claude_plan_file',
211
+ // Matches: ~/.claude/plans/something.md or full paths
212
+ pattern: /(~\/\.claude\/plans\/[\w-]+\.md|\/[^\s]+\/\.claude\/plans\/[\w-]+\.md)/,
213
+ extract: (match) => ({ path: match[1] }),
214
+ },
215
+ ];
216
+
217
+ // ANSI escape code stripper
218
+ export function stripAnsi(str: string): string {
219
+ // eslint-disable-next-line no-control-regex
220
+ return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
221
+ }
222
+
223
+ // Extract plan file path from output
224
+ export function extractPlanFile(content: string): string | null {
225
+ const cleanContent = stripAnsi(content);
226
+ const match = getFirstMatch(cleanContent, planFilePatterns);
227
+ if (match?.extracted?.path) {
228
+ // Expand ~ to home directory
229
+ let path = match.extracted.path;
230
+ if (path.startsWith('~')) {
231
+ path = path.replace('~', process.env.HOME || '');
232
+ }
233
+ return path;
234
+ }
235
+ return null;
236
+ }
237
+
238
+ // Match all patterns of a type against content
239
+ export function matchPatterns(
240
+ content: string,
241
+ patterns: PatternMatch[]
242
+ ): { type: string; match: RegExpMatchArray; extracted?: Record<string, string> }[] {
243
+ const cleanContent = stripAnsi(content);
244
+ const matches: { type: string; match: RegExpMatchArray; extracted?: Record<string, string> }[] = [];
245
+
246
+ for (const pattern of patterns) {
247
+ const regex = new RegExp(pattern.pattern.source, pattern.pattern.flags || 'g');
248
+ let match: RegExpMatchArray | null;
249
+
250
+ while ((match = regex.exec(cleanContent)) !== null) {
251
+ matches.push({
252
+ type: pattern.type,
253
+ match,
254
+ extracted: pattern.extract ? pattern.extract(match) : undefined,
255
+ });
256
+
257
+ // If not global, only match once
258
+ if (!pattern.pattern.flags?.includes('g')) break;
259
+ }
260
+ }
261
+
262
+ return matches;
263
+ }
264
+
265
+ // Check if content matches any pattern in a set
266
+ export function hasMatch(content: string, patterns: PatternMatch[]): boolean {
267
+ return matchPatterns(content, patterns).length > 0;
268
+ }
269
+
270
+ // Get the first match from a pattern set
271
+ export function getFirstMatch(
272
+ content: string,
273
+ patterns: PatternMatch[]
274
+ ): { type: string; match: RegExpMatchArray; extracted?: Record<string, string> } | null {
275
+ const matches = matchPatterns(content, patterns);
276
+ return matches.length > 0 ? matches[0] : null;
277
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * State detector for Claude Code sessions
3
+ *
4
+ * Analyzes terminal output to determine the current interactive state
5
+ * of a Claude Code session.
6
+ */
7
+
8
+ import {
9
+ permissionPatterns,
10
+ questionPatterns,
11
+ errorPatterns,
12
+ completionPatterns,
13
+ workingPatterns,
14
+ idlePatterns,
15
+ toolUsePatterns,
16
+ stripAnsi,
17
+ hasMatch,
18
+ getFirstMatch,
19
+ matchPatterns,
20
+ } from './patterns.js';
21
+
22
+ export type ClaudeStateType =
23
+ | 'idle'
24
+ | 'working'
25
+ | 'permission'
26
+ | 'question'
27
+ | 'error'
28
+ | 'complete'
29
+ | 'tool_use'
30
+ | 'unknown';
31
+
32
+ export interface ClaudeState {
33
+ type: ClaudeStateType;
34
+ detail?: string;
35
+ options?: string[];
36
+ timestamp: number;
37
+ rawOutput: string;
38
+ confidence: number; // 0-1, how confident we are in this detection
39
+ }
40
+
41
+ export interface StateDetectorOptions {
42
+ /** Number of lines from end to analyze (default: 50) */
43
+ linesToAnalyze?: number;
44
+ /** Minimum confidence threshold (default: 0.5) */
45
+ minConfidence?: number;
46
+ }
47
+
48
+ /**
49
+ * Detect the current state of a Claude Code session from terminal output
50
+ */
51
+ export function detectState(
52
+ output: string,
53
+ options: StateDetectorOptions = {}
54
+ ): ClaudeState {
55
+ const { linesToAnalyze = 50, minConfidence = 0.3 } = options;
56
+
57
+ // Get the last N lines for analysis
58
+ const lines = output.split('\n');
59
+ const recentLines = lines.slice(-linesToAnalyze).join('\n');
60
+ const cleanOutput = stripAnsi(recentLines);
61
+
62
+ const timestamp = Date.now();
63
+ const baseState = {
64
+ timestamp,
65
+ rawOutput: recentLines,
66
+ };
67
+
68
+ // Check for permission requests (highest priority - needs user action)
69
+ const permissionMatch = getFirstMatch(cleanOutput, permissionPatterns);
70
+ if (permissionMatch) {
71
+ return {
72
+ ...baseState,
73
+ type: 'permission',
74
+ detail: permissionMatch.type.replace('_permission', ''),
75
+ confidence: 0.9,
76
+ };
77
+ }
78
+
79
+ // Check for plan approval or question prompts
80
+ const hasPlanApproval = hasMatch(cleanOutput, questionPatterns.filter(p => p.type === 'claude_code_plan_approval'));
81
+
82
+ // Check for questions with options - only look at last 15 lines for actual menu options
83
+ const lastMenuLines = lines.slice(-15).join('\n');
84
+ const cleanMenuLines = stripAnsi(lastMenuLines);
85
+ const questionMatches = matchPatterns(cleanMenuLines, questionPatterns);
86
+
87
+ if (questionMatches.length > 0 || hasPlanApproval) {
88
+ // Extract options only from the numbered list at the end
89
+ const menuOptions = questionMatches
90
+ .filter((m) => m.type === 'claude_code_numbered_options' && m.extracted?.option)
91
+ .map((m) => m.extracted!.option);
92
+
93
+ if (menuOptions.length >= 2 || hasPlanApproval) {
94
+ return {
95
+ ...baseState,
96
+ type: 'question',
97
+ options: menuOptions.length > 0 ? menuOptions : undefined,
98
+ detail: hasPlanApproval ? 'plan_approval' : undefined,
99
+ confidence: 0.85,
100
+ };
101
+ }
102
+
103
+ // Fall back to other option types
104
+ const otherOptions = questionMatches
105
+ .filter((m) => m.extracted?.option && m.type !== 'claude_code_numbered_options')
106
+ .map((m) => m.extracted!.option);
107
+
108
+ if (otherOptions.length >= 2) {
109
+ return {
110
+ ...baseState,
111
+ type: 'question',
112
+ options: otherOptions,
113
+ confidence: 0.85,
114
+ };
115
+ }
116
+ }
117
+
118
+ // Check for yes/no questions
119
+ const ynMatch = questionMatches.find((m) => m.type === 'yes_no_question');
120
+ if (ynMatch) {
121
+ return {
122
+ ...baseState,
123
+ type: 'question',
124
+ options: ['Yes', 'No'],
125
+ detail: `default: ${ynMatch.extracted?.default || 'y'}`,
126
+ confidence: 0.85,
127
+ };
128
+ }
129
+
130
+ // Check for errors
131
+ const errorMatch = getFirstMatch(cleanOutput, errorPatterns);
132
+ if (errorMatch) {
133
+ return {
134
+ ...baseState,
135
+ type: 'error',
136
+ detail: errorMatch.extracted?.message || errorMatch.match[0],
137
+ confidence: 0.8,
138
+ };
139
+ }
140
+
141
+ // Check for tool use
142
+ const toolMatch = getFirstMatch(cleanOutput, toolUsePatterns);
143
+ if (toolMatch) {
144
+ return {
145
+ ...baseState,
146
+ type: 'tool_use',
147
+ detail: `${toolMatch.type}: ${toolMatch.extracted?.command || toolMatch.extracted?.file || toolMatch.extracted?.query || ''}`,
148
+ confidence: 0.75,
149
+ };
150
+ }
151
+
152
+ // Check for working/thinking indicators
153
+ if (hasMatch(cleanOutput, workingPatterns)) {
154
+ return {
155
+ ...baseState,
156
+ type: 'working',
157
+ confidence: 0.7,
158
+ };
159
+ }
160
+
161
+ // Check for completion indicators
162
+ if (hasMatch(cleanOutput, completionPatterns)) {
163
+ return {
164
+ ...baseState,
165
+ type: 'complete',
166
+ confidence: 0.6,
167
+ };
168
+ }
169
+
170
+ // Check for idle/prompt state
171
+ // Look at just the last few lines for prompt detection
172
+ const lastFewLines = lines.slice(-5).join('\n');
173
+ const cleanLastLines = stripAnsi(lastFewLines);
174
+
175
+ if (hasMatch(cleanLastLines, idlePatterns)) {
176
+ return {
177
+ ...baseState,
178
+ type: 'idle',
179
+ confidence: 0.7,
180
+ };
181
+ }
182
+
183
+ // Check for common Claude Code prompt patterns more specifically
184
+ // Claude Code often shows a ">" prompt when waiting for input
185
+ const trimmedLast = cleanLastLines.trim();
186
+ if (trimmedLast.endsWith('>') || trimmedLast.match(/>\s*$/)) {
187
+ return {
188
+ ...baseState,
189
+ type: 'idle',
190
+ detail: 'prompt detected',
191
+ confidence: 0.65,
192
+ };
193
+ }
194
+
195
+ // Default: unknown state
196
+ return {
197
+ ...baseState,
198
+ type: 'unknown',
199
+ confidence: minConfidence,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Detect if output appears to be complete based on multiple signals
205
+ */
206
+ export function detectCompletion(
207
+ output: string,
208
+ previousOutput: string
209
+ ): { complete: boolean; reason: string; confidence: number } {
210
+ const currentState = detectState(output);
211
+ const prevState = detectState(previousOutput);
212
+
213
+ // Permission or question = definitely not complete (needs user input)
214
+ if (currentState.type === 'permission' || currentState.type === 'question') {
215
+ return {
216
+ complete: false,
217
+ reason: `awaiting ${currentState.type}`,
218
+ confidence: 0.95,
219
+ };
220
+ }
221
+
222
+ // Error state = complete (but with error)
223
+ if (currentState.type === 'error') {
224
+ return {
225
+ complete: true,
226
+ reason: 'error detected',
227
+ confidence: 0.8,
228
+ };
229
+ }
230
+
231
+ // Idle state = likely complete
232
+ if (currentState.type === 'idle') {
233
+ return {
234
+ complete: true,
235
+ reason: 'idle prompt detected',
236
+ confidence: currentState.confidence,
237
+ };
238
+ }
239
+
240
+ // Explicit completion markers
241
+ if (currentState.type === 'complete') {
242
+ return {
243
+ complete: true,
244
+ reason: 'completion marker detected',
245
+ confidence: currentState.confidence,
246
+ };
247
+ }
248
+
249
+ // Transition from working to not working
250
+ if (prevState.type === 'working' && currentState.type !== 'working') {
251
+ return {
252
+ complete: true,
253
+ reason: 'work finished',
254
+ confidence: 0.6,
255
+ };
256
+ }
257
+
258
+ // Still working
259
+ if (currentState.type === 'working' || currentState.type === 'tool_use') {
260
+ return {
261
+ complete: false,
262
+ reason: 'still working',
263
+ confidence: 0.7,
264
+ };
265
+ }
266
+
267
+ // Unknown - inconclusive
268
+ return {
269
+ complete: false,
270
+ reason: 'unknown state',
271
+ confidence: 0.3,
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Extract permission details from output
277
+ */
278
+ export function extractPermissionDetails(output: string): {
279
+ type: string;
280
+ command?: string;
281
+ file?: string;
282
+ } | null {
283
+ const cleanOutput = stripAnsi(output);
284
+ const match = getFirstMatch(cleanOutput, permissionPatterns);
285
+
286
+ if (!match) return null;
287
+
288
+ // Try to extract command or file from surrounding context
289
+ const lines = cleanOutput.split('\n');
290
+ const matchLine = lines.findIndex((line) =>
291
+ line.match(permissionPatterns[0].pattern) ||
292
+ line.match(permissionPatterns[1].pattern) ||
293
+ line.match(permissionPatterns[2].pattern)
294
+ );
295
+
296
+ let command: string | undefined;
297
+ let file: string | undefined;
298
+
299
+ // Look at lines before the permission prompt for context
300
+ if (matchLine > 0) {
301
+ const contextLines = lines.slice(Math.max(0, matchLine - 5), matchLine).join('\n');
302
+
303
+ // Extract command
304
+ const cmdMatch = contextLines.match(/(?:Command|command|Bash|bash):\s*(.+)/);
305
+ if (cmdMatch) {
306
+ command = cmdMatch[1].trim();
307
+ }
308
+
309
+ // Extract file
310
+ const fileMatch = contextLines.match(/(?:File|file|Path|path):\s*(.+)/);
311
+ if (fileMatch) {
312
+ file = fileMatch[1].trim();
313
+ }
314
+ }
315
+
316
+ return {
317
+ type: match.type.replace('_permission', ''),
318
+ command,
319
+ file,
320
+ };
321
+ }
322
+
323
+ /**
324
+ * Extract question options from output
325
+ */
326
+ export function extractQuestionOptions(output: string): string[] {
327
+ const cleanOutput = stripAnsi(output);
328
+ const matches = matchPatterns(cleanOutput, questionPatterns);
329
+
330
+ const options: string[] = [];
331
+
332
+ for (const match of matches) {
333
+ if (match.extracted?.option) {
334
+ options.push(match.extracted.option);
335
+ }
336
+ }
337
+
338
+ return options;
339
+ }
@@ -1,5 +1,5 @@
1
1
  // Runtime version (baked in at build time)
2
- export const VERSION = '0.260202.1607';
2
+ export const VERSION = '0.260202.1833';
3
3
 
4
4
  // Generate version string from current datetime
5
5
  // Format: 0.YYMMDD.HHMM (e.g., 0.260201.1430 = Feb 1, 2026 at 14:30)