@automagik/genie 0.260202.1607 → 0.260202.1901
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/.beads/README.md +81 -0
- package/.beads/config.yaml +67 -0
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +0 -0
- package/.beads/metadata.json +4 -0
- package/.gitattributes +3 -0
- package/AGENTS.md +40 -0
- package/dist/claudio.js +5 -5
- package/dist/genie.js +6 -6
- package/dist/term.js +116 -53
- package/package.json +1 -1
- package/src/lib/beads-registry.ts +546 -0
- package/src/lib/orchestrator/completion.ts +392 -0
- package/src/lib/orchestrator/event-monitor.ts +442 -0
- package/src/lib/orchestrator/index.ts +12 -0
- package/src/lib/orchestrator/patterns.ts +277 -0
- package/src/lib/orchestrator/state-detector.ts +339 -0
- package/src/lib/tmux.ts +15 -1
- package/src/lib/version.ts +1 -1
- package/src/lib/worker-registry.ts +229 -0
- package/src/term-commands/close.ts +256 -0
- package/src/term-commands/daemon.ts +176 -0
- package/src/term-commands/kill.ts +186 -0
- package/src/term-commands/orchestrate.ts +844 -0
- package/src/term-commands/split.ts +8 -7
- package/src/term-commands/work.ts +497 -0
- package/src/term-commands/workers.ts +298 -0
- package/src/term.ts +227 -1
|
@@ -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
|
+
}
|
package/src/lib/tmux.ts
CHANGED
|
@@ -222,13 +222,22 @@ export async function killPane(paneId: string): Promise<void> {
|
|
|
222
222
|
await executeTmux(`kill-pane -t '${paneId}'`);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Escape a string for safe use in shell single quotes.
|
|
227
|
+
* Replaces ' with '\'' (end quote, escaped quote, start quote).
|
|
228
|
+
*/
|
|
229
|
+
function escapeShellPath(path: string): string {
|
|
230
|
+
return path.replace(/'/g, "'\\''");
|
|
231
|
+
}
|
|
232
|
+
|
|
225
233
|
/**
|
|
226
234
|
* Split a tmux pane horizontally or vertically
|
|
227
235
|
*/
|
|
228
236
|
export async function splitPane(
|
|
229
237
|
targetPaneId: string,
|
|
230
238
|
direction: 'horizontal' | 'vertical' = 'vertical',
|
|
231
|
-
size?: number
|
|
239
|
+
size?: number,
|
|
240
|
+
workingDir?: string
|
|
232
241
|
): Promise<TmuxPane | null> {
|
|
233
242
|
// Build the split-window command
|
|
234
243
|
let splitCommand = 'split-window';
|
|
@@ -248,6 +257,11 @@ export async function splitPane(
|
|
|
248
257
|
splitCommand += ` -p ${size}`;
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
// Add working directory if specified
|
|
261
|
+
if (workingDir) {
|
|
262
|
+
splitCommand += ` -c '${escapeShellPath(workingDir)}'`;
|
|
263
|
+
}
|
|
264
|
+
|
|
251
265
|
// Execute the split command
|
|
252
266
|
await executeTmux(splitCommand);
|
|
253
267
|
|
package/src/lib/version.ts
CHANGED