@automagik/genie 0.260202.530 → 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.
- package/dist/claudio.js +44 -45
- package/dist/genie.js +58 -135
- package/dist/term.js +134 -67
- package/install.sh +43 -7
- package/package.json +1 -1
- package/src/claudio.ts +31 -21
- package/src/commands/launch.ts +12 -68
- package/src/genie-commands/doctor.ts +327 -0
- package/src/genie-commands/setup.ts +317 -199
- package/src/genie-commands/uninstall.ts +176 -0
- package/src/genie.ts +24 -44
- package/src/lib/claude-settings.ts +22 -64
- package/src/lib/genie-config.ts +169 -57
- 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/version.ts +1 -1
- package/src/lib/worker-registry.ts +229 -0
- package/src/term-commands/close.ts +221 -0
- package/src/term-commands/exec.ts +28 -6
- package/src/term-commands/kill.ts +143 -0
- package/src/term-commands/orchestrate.ts +844 -0
- package/src/term-commands/read.ts +6 -1
- package/src/term-commands/shortcuts.ts +14 -14
- package/src/term-commands/work.ts +415 -0
- package/src/term-commands/workers.ts +264 -0
- package/src/term.ts +201 -3
- package/src/types/genie-config.ts +49 -81
- package/src/genie-commands/hooks.ts +0 -317
- package/src/lib/hook-script.ts +0 -263
- package/src/lib/hooks/compose.ts +0 -72
- package/src/lib/hooks/index.ts +0 -163
- package/src/lib/hooks/presets/audited.ts +0 -191
- package/src/lib/hooks/presets/collaborative.ts +0 -143
- package/src/lib/hooks/presets/sandboxed.ts +0 -153
- package/src/lib/hooks/presets/supervised.ts +0 -66
- package/src/lib/hooks/utils/escape.ts +0 -46
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrate command - Claude Code session orchestration
|
|
3
|
+
*
|
|
4
|
+
* Provides commands for monitoring and controlling Claude Code sessions:
|
|
5
|
+
* - start: Start Claude Code in a session with optional monitoring
|
|
6
|
+
* - send: Send message and track completion
|
|
7
|
+
* - status: Show current Claude state
|
|
8
|
+
* - watch: Real-time event streaming
|
|
9
|
+
* - approve: Handle permission requests
|
|
10
|
+
* - answer: Answer questions
|
|
11
|
+
* - experiment: Test completion detection methods
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as tmux from '../lib/tmux.js';
|
|
15
|
+
import {
|
|
16
|
+
EventMonitor,
|
|
17
|
+
ClaudeEvent,
|
|
18
|
+
ClaudeState,
|
|
19
|
+
detectState,
|
|
20
|
+
extractPermissionDetails,
|
|
21
|
+
extractQuestionOptions,
|
|
22
|
+
extractPlanFile,
|
|
23
|
+
getMethod,
|
|
24
|
+
getDefaultMethod,
|
|
25
|
+
presetMethods,
|
|
26
|
+
PresetMethodName,
|
|
27
|
+
CompletionMethodMetrics,
|
|
28
|
+
stripAnsi,
|
|
29
|
+
} from '../lib/orchestrator/index.js';
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Types
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
export interface StartOptions {
|
|
36
|
+
pane?: string;
|
|
37
|
+
monitor?: boolean;
|
|
38
|
+
command?: string;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RunOptions {
|
|
43
|
+
pane?: string;
|
|
44
|
+
autoApprove?: boolean;
|
|
45
|
+
timeout?: number;
|
|
46
|
+
json?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SendOptions {
|
|
50
|
+
method?: string;
|
|
51
|
+
timeout?: number;
|
|
52
|
+
json?: boolean;
|
|
53
|
+
noWait?: boolean;
|
|
54
|
+
pane?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StatusOptions {
|
|
58
|
+
json?: boolean;
|
|
59
|
+
pane?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface WatchOptions {
|
|
63
|
+
json?: boolean;
|
|
64
|
+
poll?: number;
|
|
65
|
+
pane?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ApproveOptions {
|
|
69
|
+
pane?: string;
|
|
70
|
+
auto?: boolean;
|
|
71
|
+
deny?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ExperimentOptions {
|
|
75
|
+
runs?: number;
|
|
76
|
+
task?: string;
|
|
77
|
+
json?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Helper Functions
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
async function getSessionPane(
|
|
85
|
+
sessionName: string,
|
|
86
|
+
targetPaneId?: string
|
|
87
|
+
): Promise<{ session: tmux.TmuxSession; paneId: string }> {
|
|
88
|
+
const session = await tmux.findSessionByName(sessionName);
|
|
89
|
+
if (!session) {
|
|
90
|
+
console.error(`❌ Session "${sessionName}" not found`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// If specific pane ID provided, validate and use it
|
|
95
|
+
if (targetPaneId) {
|
|
96
|
+
// Normalize pane ID (add % prefix if missing)
|
|
97
|
+
const paneId = targetPaneId.startsWith('%') ? targetPaneId : `%${targetPaneId}`;
|
|
98
|
+
return { session, paneId };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const windows = await tmux.listWindows(session.id);
|
|
102
|
+
if (!windows || windows.length === 0) {
|
|
103
|
+
console.error(`❌ No windows found in session "${sessionName}"`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const panes = await tmux.listPanes(windows[0].id);
|
|
108
|
+
if (!panes || panes.length === 0) {
|
|
109
|
+
console.error(`❌ No panes found in session "${sessionName}"`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { session, paneId: panes[0].id };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function formatState(state: ClaudeState): string {
|
|
117
|
+
let result = `${state.type}`;
|
|
118
|
+
if (state.detail) {
|
|
119
|
+
result += ` (${state.detail})`;
|
|
120
|
+
}
|
|
121
|
+
if (state.options && state.options.length > 0) {
|
|
122
|
+
result += `\n Options: ${state.options.join(', ')}`;
|
|
123
|
+
}
|
|
124
|
+
result += ` [confidence: ${(state.confidence * 100).toFixed(0)}%]`;
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatEvent(event: ClaudeEvent): string {
|
|
129
|
+
const time = new Date(event.timestamp).toISOString().split('T')[1].split('.')[0];
|
|
130
|
+
|
|
131
|
+
switch (event.type) {
|
|
132
|
+
case 'output':
|
|
133
|
+
return `[${time}] OUTPUT: ${(event.output || '').substring(0, 100).replace(/\n/g, '\\n')}`;
|
|
134
|
+
case 'state_change':
|
|
135
|
+
return `[${time}] STATE: ${event.state?.type || 'unknown'}${event.state?.detail ? ` (${event.state.detail})` : ''}`;
|
|
136
|
+
case 'silence':
|
|
137
|
+
return `[${time}] SILENCE: ${event.silenceMs}ms`;
|
|
138
|
+
case 'activity':
|
|
139
|
+
return `[${time}] ACTIVITY`;
|
|
140
|
+
case 'permission':
|
|
141
|
+
return `[${time}] PERMISSION: ${event.state?.detail || 'unknown'}`;
|
|
142
|
+
case 'question':
|
|
143
|
+
return `[${time}] QUESTION: ${event.state?.options?.join(', ') || 'unknown'}`;
|
|
144
|
+
case 'error':
|
|
145
|
+
return `[${time}] ERROR: ${event.state?.detail || 'unknown'}`;
|
|
146
|
+
case 'complete':
|
|
147
|
+
return `[${time}] COMPLETE`;
|
|
148
|
+
default:
|
|
149
|
+
return `[${time}] ${event.type}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Commands
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Start Claude Code in a session with optional monitoring
|
|
159
|
+
*/
|
|
160
|
+
export async function startSession(
|
|
161
|
+
sessionName: string,
|
|
162
|
+
options: StartOptions = {}
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
try {
|
|
165
|
+
// Check if session exists
|
|
166
|
+
let session = await tmux.findSessionByName(sessionName);
|
|
167
|
+
|
|
168
|
+
if (!session) {
|
|
169
|
+
// Create new session
|
|
170
|
+
session = await tmux.createSession(sessionName);
|
|
171
|
+
if (!session) {
|
|
172
|
+
console.error(`❌ Failed to create session "${sessionName}"`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
console.log(`✅ Created session "${sessionName}"`);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(`ℹ️ Session "${sessionName}" already exists`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get pane (use specified pane or default to first pane)
|
|
181
|
+
let paneId: string;
|
|
182
|
+
if (options.pane) {
|
|
183
|
+
paneId = options.pane.startsWith('%') ? options.pane : `%${options.pane}`;
|
|
184
|
+
} else {
|
|
185
|
+
const windows = await tmux.listWindows(session.id);
|
|
186
|
+
const panes = await tmux.listPanes(windows[0].id);
|
|
187
|
+
paneId = panes[0].id;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Start Claude Code (or custom command)
|
|
191
|
+
const command = options.command || 'claude';
|
|
192
|
+
await tmux.executeCommand(paneId, command, false, false);
|
|
193
|
+
console.log(`✅ Started "${command}" in session "${sessionName}"`);
|
|
194
|
+
|
|
195
|
+
// Start monitoring if requested
|
|
196
|
+
if (options.monitor) {
|
|
197
|
+
console.log(`ℹ️ Starting event monitor...`);
|
|
198
|
+
|
|
199
|
+
const monitor = new EventMonitor(sessionName, {
|
|
200
|
+
pollIntervalMs: 500,
|
|
201
|
+
paneId: options.pane,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
monitor.on('event', (event: ClaudeEvent) => {
|
|
205
|
+
if (options.json) {
|
|
206
|
+
console.log(JSON.stringify(event));
|
|
207
|
+
} else {
|
|
208
|
+
console.log(formatEvent(event));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
monitor.on('poll_error', (error: Error) => {
|
|
213
|
+
console.error(`⚠️ Poll error: ${error.message}`);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await monitor.start();
|
|
217
|
+
console.log(`✅ Monitor active. Press Ctrl+C to stop.`);
|
|
218
|
+
|
|
219
|
+
// Keep running until interrupted
|
|
220
|
+
process.on('SIGINT', () => {
|
|
221
|
+
monitor.stop();
|
|
222
|
+
console.log('\n✅ Monitor stopped.');
|
|
223
|
+
process.exit(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Keep process alive
|
|
227
|
+
await new Promise(() => {});
|
|
228
|
+
}
|
|
229
|
+
} catch (error: any) {
|
|
230
|
+
console.error(`❌ Error: ${error.message}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Send a message to Claude and track completion
|
|
237
|
+
*/
|
|
238
|
+
export async function sendMessage(
|
|
239
|
+
sessionName: string,
|
|
240
|
+
message: string,
|
|
241
|
+
options: SendOptions = {}
|
|
242
|
+
): Promise<void> {
|
|
243
|
+
try {
|
|
244
|
+
const { paneId } = await getSessionPane(sessionName, options.pane);
|
|
245
|
+
|
|
246
|
+
// Send the message
|
|
247
|
+
await tmux.executeCommand(paneId, message, false, false);
|
|
248
|
+
|
|
249
|
+
if (options.noWait) {
|
|
250
|
+
console.log(`✅ Message sent to session "${sessionName}"`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Start monitoring for completion
|
|
255
|
+
const monitor = new EventMonitor(sessionName, {
|
|
256
|
+
pollIntervalMs: 250,
|
|
257
|
+
paneId: options.pane,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await monitor.start();
|
|
261
|
+
|
|
262
|
+
// Use specified completion method or default
|
|
263
|
+
const method = options.method ? getMethod(options.method) : getDefaultMethod();
|
|
264
|
+
const timeoutMs = options.timeout || 120000;
|
|
265
|
+
|
|
266
|
+
console.log(`ℹ️ Waiting for completion using "${method.name}"...`);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const result = await method.detect(monitor, timeoutMs);
|
|
270
|
+
monitor.stop();
|
|
271
|
+
|
|
272
|
+
if (options.json) {
|
|
273
|
+
console.log(JSON.stringify(result, null, 2));
|
|
274
|
+
} else {
|
|
275
|
+
console.log(`✅ Completion detected: ${result.reason}`);
|
|
276
|
+
console.log(` Latency: ${result.latencyMs}ms`);
|
|
277
|
+
if (result.state) {
|
|
278
|
+
console.log(` State: ${formatState(result.state)}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Capture and output the response
|
|
283
|
+
const output = await tmux.capturePaneContent(paneId, 100);
|
|
284
|
+
console.log('\n--- Response ---');
|
|
285
|
+
console.log(stripAnsi(output).trim());
|
|
286
|
+
} catch (error: any) {
|
|
287
|
+
monitor.stop();
|
|
288
|
+
console.error(`❌ Completion detection failed: ${error.message}`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
} catch (error: any) {
|
|
292
|
+
console.error(`❌ Error: ${error.message}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Show current Claude state
|
|
299
|
+
*/
|
|
300
|
+
export async function showStatus(
|
|
301
|
+
sessionName: string,
|
|
302
|
+
options: StatusOptions = {}
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
try {
|
|
305
|
+
const { paneId } = await getSessionPane(sessionName, options.pane);
|
|
306
|
+
|
|
307
|
+
// Capture current output
|
|
308
|
+
const output = await tmux.capturePaneContent(paneId, 100);
|
|
309
|
+
|
|
310
|
+
// Detect state
|
|
311
|
+
const state = detectState(output);
|
|
312
|
+
|
|
313
|
+
// Get permission/question details if applicable
|
|
314
|
+
let permissionDetails = null;
|
|
315
|
+
let questionOptions: string[] = [];
|
|
316
|
+
let planFile: string | null = null;
|
|
317
|
+
|
|
318
|
+
if (state.type === 'permission') {
|
|
319
|
+
permissionDetails = extractPermissionDetails(output);
|
|
320
|
+
} else if (state.type === 'question') {
|
|
321
|
+
questionOptions = extractQuestionOptions(output);
|
|
322
|
+
// Check for plan file when in plan approval state
|
|
323
|
+
if (state.detail === 'plan_approval') {
|
|
324
|
+
planFile = extractPlanFile(output);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (options.json) {
|
|
329
|
+
console.log(
|
|
330
|
+
JSON.stringify(
|
|
331
|
+
{
|
|
332
|
+
session: sessionName,
|
|
333
|
+
state: state.type,
|
|
334
|
+
detail: state.detail,
|
|
335
|
+
confidence: state.confidence,
|
|
336
|
+
timestamp: state.timestamp,
|
|
337
|
+
permissionDetails,
|
|
338
|
+
questionOptions,
|
|
339
|
+
planFile,
|
|
340
|
+
},
|
|
341
|
+
null,
|
|
342
|
+
2
|
|
343
|
+
)
|
|
344
|
+
);
|
|
345
|
+
} else {
|
|
346
|
+
console.log(`Session: ${sessionName}`);
|
|
347
|
+
console.log(`State: ${state.type}`);
|
|
348
|
+
if (state.detail) {
|
|
349
|
+
console.log(`Detail: ${state.detail}`);
|
|
350
|
+
}
|
|
351
|
+
console.log(`Confidence: ${(state.confidence * 100).toFixed(0)}%`);
|
|
352
|
+
|
|
353
|
+
if (permissionDetails) {
|
|
354
|
+
console.log(`\nPermission Request:`);
|
|
355
|
+
console.log(` Type: ${permissionDetails.type}`);
|
|
356
|
+
if (permissionDetails.command) {
|
|
357
|
+
console.log(` Command: ${permissionDetails.command}`);
|
|
358
|
+
}
|
|
359
|
+
if (permissionDetails.file) {
|
|
360
|
+
console.log(` File: ${permissionDetails.file}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (questionOptions.length > 0) {
|
|
365
|
+
console.log(`\nQuestion Options:`);
|
|
366
|
+
questionOptions.forEach((opt, i) => {
|
|
367
|
+
console.log(` [${i + 1}] ${opt}`);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (planFile) {
|
|
372
|
+
console.log(`\nPlan File: ${planFile}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Show last few lines of output
|
|
376
|
+
const lines = stripAnsi(output).trim().split('\n');
|
|
377
|
+
const lastLines = lines.slice(-5);
|
|
378
|
+
console.log(`\nLast output:`);
|
|
379
|
+
lastLines.forEach((line) => console.log(` ${line}`));
|
|
380
|
+
}
|
|
381
|
+
} catch (error: any) {
|
|
382
|
+
console.error(`❌ Error: ${error.message}`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Watch session events in real-time
|
|
389
|
+
*/
|
|
390
|
+
export async function watchSession(
|
|
391
|
+
sessionName: string,
|
|
392
|
+
options: WatchOptions = {}
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
try {
|
|
395
|
+
const monitor = new EventMonitor(sessionName, {
|
|
396
|
+
pollIntervalMs: options.poll || 500,
|
|
397
|
+
paneId: options.pane,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
monitor.on('event', (event: ClaudeEvent) => {
|
|
401
|
+
if (options.json) {
|
|
402
|
+
console.log(JSON.stringify(event));
|
|
403
|
+
} else {
|
|
404
|
+
console.log(formatEvent(event));
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
monitor.on('poll_error', (error: Error) => {
|
|
409
|
+
console.error(`⚠️ Poll error: ${error.message}`);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await monitor.start();
|
|
413
|
+
console.log(`✅ Watching session "${sessionName}". Press Ctrl+C to stop.`);
|
|
414
|
+
|
|
415
|
+
// Show initial state
|
|
416
|
+
const state = monitor.getCurrentState();
|
|
417
|
+
if (state) {
|
|
418
|
+
console.log(`Initial state: ${formatState(state)}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Handle Ctrl+C
|
|
422
|
+
process.on('SIGINT', () => {
|
|
423
|
+
monitor.stop();
|
|
424
|
+
console.log('\n✅ Watch stopped.');
|
|
425
|
+
process.exit(0);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// Keep process alive
|
|
429
|
+
await new Promise(() => {});
|
|
430
|
+
} catch (error: any) {
|
|
431
|
+
console.error(`❌ Error: ${error.message}`);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Approve a pending permission request
|
|
438
|
+
*/
|
|
439
|
+
export async function approvePermission(
|
|
440
|
+
sessionName: string,
|
|
441
|
+
options: ApproveOptions = {}
|
|
442
|
+
): Promise<void> {
|
|
443
|
+
try {
|
|
444
|
+
const { paneId } = await getSessionPane(sessionName, options.pane);
|
|
445
|
+
|
|
446
|
+
// Check current state
|
|
447
|
+
const output = await tmux.capturePaneContent(paneId, 50);
|
|
448
|
+
const state = detectState(output);
|
|
449
|
+
|
|
450
|
+
if (state.type !== 'permission' && !options.auto) {
|
|
451
|
+
console.log(`ℹ️ No permission request pending (state: ${state.type})`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// For Claude Code, permissions use a menu with ❯ cursor
|
|
456
|
+
// We need to navigate to the right option and press Enter
|
|
457
|
+
// Option 1 is "Yes" (approve), other options are deny or more specific
|
|
458
|
+
if (options.deny) {
|
|
459
|
+
// Navigate down to "No" option (typically option 2 or 3)
|
|
460
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Down`);
|
|
461
|
+
await sleep(100);
|
|
462
|
+
}
|
|
463
|
+
// Press Enter to confirm selection
|
|
464
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
|
|
465
|
+
|
|
466
|
+
console.log(`✅ ${options.deny ? 'Denied' : 'Approved'} permission in session "${sessionName}"`);
|
|
467
|
+
|
|
468
|
+
// If auto mode, keep monitoring and approving
|
|
469
|
+
if (options.auto) {
|
|
470
|
+
console.log(`ℹ️ Auto-approve mode enabled. Press Ctrl+C to stop.`);
|
|
471
|
+
|
|
472
|
+
const monitor = new EventMonitor(sessionName, {
|
|
473
|
+
pollIntervalMs: 250,
|
|
474
|
+
paneId: options.pane,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
monitor.on('permission', async (event: ClaudeEvent) => {
|
|
478
|
+
try {
|
|
479
|
+
const { paneId: currentPaneId } = await getSessionPane(sessionName, options.pane);
|
|
480
|
+
const response = options.deny ? 'n' : 'y';
|
|
481
|
+
await tmux.executeCommand(currentPaneId, response, false, true);
|
|
482
|
+
console.log(`✅ Auto-${options.deny ? 'denied' : 'approved'}: ${event.state?.detail || 'unknown'}`);
|
|
483
|
+
} catch (err: any) {
|
|
484
|
+
console.error(`⚠️ Auto-approve failed: ${err.message}`);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
await monitor.start();
|
|
489
|
+
|
|
490
|
+
process.on('SIGINT', () => {
|
|
491
|
+
monitor.stop();
|
|
492
|
+
console.log('\n✅ Auto-approve stopped.');
|
|
493
|
+
process.exit(0);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await new Promise(() => {});
|
|
497
|
+
}
|
|
498
|
+
} catch (error: any) {
|
|
499
|
+
console.error(`❌ Error: ${error.message}`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Answer a question with options
|
|
506
|
+
*
|
|
507
|
+
* For Claude Code menus:
|
|
508
|
+
* - Numeric choice (1-9): Navigate to that option and select
|
|
509
|
+
* - "text:..." prefix: Type text directly (for option 4 "Type here...")
|
|
510
|
+
* - Other: Send as raw keystrokes
|
|
511
|
+
*/
|
|
512
|
+
export async function answerQuestion(
|
|
513
|
+
sessionName: string,
|
|
514
|
+
choice: string,
|
|
515
|
+
options: { pane?: string } = {}
|
|
516
|
+
): Promise<void> {
|
|
517
|
+
try {
|
|
518
|
+
const { paneId } = await getSessionPane(sessionName, options.pane);
|
|
519
|
+
|
|
520
|
+
// Check current state
|
|
521
|
+
const output = await tmux.capturePaneContent(paneId, 50);
|
|
522
|
+
const state = detectState(output);
|
|
523
|
+
|
|
524
|
+
if (state.type !== 'question') {
|
|
525
|
+
console.log(`ℹ️ No question pending (state: ${state.type})`);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Handle different choice formats
|
|
530
|
+
if (choice.startsWith('text:')) {
|
|
531
|
+
// Send text directly (for "Type here to tell Claude what to change")
|
|
532
|
+
const text = choice.slice(5);
|
|
533
|
+
// First select option 4 (or last option) to get to text input
|
|
534
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' End`);
|
|
535
|
+
await sleep(100);
|
|
536
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
|
|
537
|
+
await sleep(100);
|
|
538
|
+
// Now type the feedback
|
|
539
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' ${shellEscape(text)}`);
|
|
540
|
+
await sleep(100);
|
|
541
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
|
|
542
|
+
console.log(`✅ Sent feedback: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
|
|
543
|
+
} else if (/^\d+$/.test(choice)) {
|
|
544
|
+
// Numeric choice - navigate using arrow keys to the option
|
|
545
|
+
const targetOption = parseInt(choice, 10);
|
|
546
|
+
|
|
547
|
+
// Find current selection position by looking for ❯
|
|
548
|
+
const cleanOutput = stripAnsi(output);
|
|
549
|
+
const lines = cleanOutput.split('\n');
|
|
550
|
+
let currentOption = 1;
|
|
551
|
+
for (let i = 0; i < lines.length; i++) {
|
|
552
|
+
if (lines[i].match(/^\s*❯\s*\d+\./)) {
|
|
553
|
+
const match = lines[i].match(/❯\s*(\d+)\./);
|
|
554
|
+
if (match) currentOption = parseInt(match[1], 10);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Navigate to target option
|
|
560
|
+
const diff = targetOption - currentOption;
|
|
561
|
+
if (diff > 0) {
|
|
562
|
+
for (let i = 0; i < diff; i++) {
|
|
563
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Down`);
|
|
564
|
+
await sleep(50);
|
|
565
|
+
}
|
|
566
|
+
} else if (diff < 0) {
|
|
567
|
+
for (let i = 0; i < Math.abs(diff); i++) {
|
|
568
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Up`);
|
|
569
|
+
await sleep(50);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Press Enter to select
|
|
574
|
+
await sleep(100);
|
|
575
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
|
|
576
|
+
console.log(`✅ Selected option ${targetOption} in session "${sessionName}"`);
|
|
577
|
+
} else {
|
|
578
|
+
// Raw keystroke (e.g., 'y', 'n', 'Enter')
|
|
579
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' '${choice}'`);
|
|
580
|
+
console.log(`✅ Sent '${choice}' to session "${sessionName}"`);
|
|
581
|
+
}
|
|
582
|
+
} catch (error: any) {
|
|
583
|
+
console.error(`❌ Error: ${error.message}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Helper to escape shell arguments
|
|
589
|
+
function shellEscape(str: string): string {
|
|
590
|
+
return `"${str.replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Helper to sleep
|
|
594
|
+
function sleep(ms: number): Promise<void> {
|
|
595
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Test a completion detection method
|
|
600
|
+
*/
|
|
601
|
+
export async function runExperiment(
|
|
602
|
+
methodName: string,
|
|
603
|
+
options: ExperimentOptions = {}
|
|
604
|
+
): Promise<void> {
|
|
605
|
+
const runs = options.runs || 1;
|
|
606
|
+
const testTask = options.task || 'echo "Hello, World!"';
|
|
607
|
+
|
|
608
|
+
console.log(`🧪 Experiment: Testing "${methodName}" method`);
|
|
609
|
+
console.log(` Runs: ${runs}`);
|
|
610
|
+
console.log(` Task: ${testTask}`);
|
|
611
|
+
console.log('');
|
|
612
|
+
|
|
613
|
+
const method = getMethod(methodName);
|
|
614
|
+
const results: Array<{
|
|
615
|
+
run: number;
|
|
616
|
+
latencyMs: number;
|
|
617
|
+
complete: boolean;
|
|
618
|
+
reason: string;
|
|
619
|
+
}> = [];
|
|
620
|
+
|
|
621
|
+
// Create a test session
|
|
622
|
+
const testSessionName = `orc-experiment-${Date.now()}`;
|
|
623
|
+
let session = await tmux.createSession(testSessionName);
|
|
624
|
+
if (!session) {
|
|
625
|
+
console.error('❌ Failed to create test session');
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
const windows = await tmux.listWindows(session.id);
|
|
631
|
+
const panes = await tmux.listPanes(windows[0].id);
|
|
632
|
+
const paneId = panes[0].id;
|
|
633
|
+
|
|
634
|
+
for (let i = 0; i < runs; i++) {
|
|
635
|
+
console.log(`\nRun ${i + 1}/${runs}...`);
|
|
636
|
+
|
|
637
|
+
// Clear pane
|
|
638
|
+
await tmux.executeCommand(paneId, 'clear', false, false);
|
|
639
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
640
|
+
|
|
641
|
+
// Start monitor
|
|
642
|
+
const monitor = new EventMonitor(testSessionName, {
|
|
643
|
+
pollIntervalMs: 100,
|
|
644
|
+
});
|
|
645
|
+
await monitor.start();
|
|
646
|
+
|
|
647
|
+
// Execute task
|
|
648
|
+
await tmux.executeCommand(paneId, testTask, false, false);
|
|
649
|
+
|
|
650
|
+
// Detect completion
|
|
651
|
+
const result = await method.detect(monitor, 30000);
|
|
652
|
+
monitor.stop();
|
|
653
|
+
|
|
654
|
+
results.push({
|
|
655
|
+
run: i + 1,
|
|
656
|
+
latencyMs: result.latencyMs,
|
|
657
|
+
complete: result.complete,
|
|
658
|
+
reason: result.reason,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
console.log(` Complete: ${result.complete}`);
|
|
662
|
+
console.log(` Latency: ${result.latencyMs}ms`);
|
|
663
|
+
console.log(` Reason: ${result.reason}`);
|
|
664
|
+
|
|
665
|
+
// Wait between runs
|
|
666
|
+
if (i < runs - 1) {
|
|
667
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Calculate statistics
|
|
672
|
+
const successfulRuns = results.filter((r) => r.complete);
|
|
673
|
+
const avgLatency =
|
|
674
|
+
successfulRuns.reduce((sum, r) => sum + r.latencyMs, 0) / successfulRuns.length || 0;
|
|
675
|
+
const minLatency = Math.min(...successfulRuns.map((r) => r.latencyMs)) || 0;
|
|
676
|
+
const maxLatency = Math.max(...successfulRuns.map((r) => r.latencyMs)) || 0;
|
|
677
|
+
|
|
678
|
+
const summary = {
|
|
679
|
+
method: methodName,
|
|
680
|
+
totalRuns: runs,
|
|
681
|
+
successfulRuns: successfulRuns.length,
|
|
682
|
+
successRate: (successfulRuns.length / runs) * 100,
|
|
683
|
+
avgLatencyMs: Math.round(avgLatency),
|
|
684
|
+
minLatencyMs: minLatency,
|
|
685
|
+
maxLatencyMs: maxLatency,
|
|
686
|
+
results,
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
console.log('\n--- Summary ---');
|
|
690
|
+
if (options.json) {
|
|
691
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
692
|
+
} else {
|
|
693
|
+
console.log(`Method: ${summary.method}`);
|
|
694
|
+
console.log(`Success Rate: ${summary.successRate.toFixed(1)}%`);
|
|
695
|
+
console.log(`Avg Latency: ${summary.avgLatencyMs}ms`);
|
|
696
|
+
console.log(`Min Latency: ${summary.minLatencyMs}ms`);
|
|
697
|
+
console.log(`Max Latency: ${summary.maxLatencyMs}ms`);
|
|
698
|
+
}
|
|
699
|
+
} finally {
|
|
700
|
+
// Cleanup test session
|
|
701
|
+
await tmux.killSession(session.id);
|
|
702
|
+
console.log(`\n✅ Cleaned up test session`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* List available completion methods
|
|
708
|
+
*/
|
|
709
|
+
export async function listMethods(): Promise<void> {
|
|
710
|
+
console.log('Available completion methods:');
|
|
711
|
+
console.log('');
|
|
712
|
+
|
|
713
|
+
for (const name of Object.keys(presetMethods) as PresetMethodName[]) {
|
|
714
|
+
const method = presetMethods[name]();
|
|
715
|
+
console.log(` ${name}`);
|
|
716
|
+
console.log(` ${method.description}`);
|
|
717
|
+
console.log('');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
console.log('Custom methods:');
|
|
721
|
+
console.log(' silence-Xms - Silence timeout (e.g., silence-2000ms)');
|
|
722
|
+
console.log(' silence-Xs - Silence timeout (e.g., silence-5s)');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Run a task with auto-approval until idle
|
|
727
|
+
*
|
|
728
|
+
* Fire-and-forget: send message, auto-approve permissions, wait for idle
|
|
729
|
+
*/
|
|
730
|
+
export async function runTask(
|
|
731
|
+
sessionName: string,
|
|
732
|
+
message: string,
|
|
733
|
+
options: RunOptions = {}
|
|
734
|
+
): Promise<void> {
|
|
735
|
+
try {
|
|
736
|
+
const { paneId } = await getSessionPane(sessionName, options.pane);
|
|
737
|
+
const timeoutMs = options.timeout || 300000; // 5 minute default
|
|
738
|
+
|
|
739
|
+
// Send the message
|
|
740
|
+
await tmux.executeCommand(paneId, message, false, false);
|
|
741
|
+
console.log(`✅ Sent task: "${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`);
|
|
742
|
+
|
|
743
|
+
// Start monitoring
|
|
744
|
+
const monitor = new EventMonitor(sessionName, {
|
|
745
|
+
pollIntervalMs: 250,
|
|
746
|
+
paneId: options.pane,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
await monitor.start();
|
|
750
|
+
const startTime = Date.now();
|
|
751
|
+
|
|
752
|
+
console.log(`ℹ️ Monitoring for completion...${options.autoApprove ? ' (auto-approve enabled)' : ''}`);
|
|
753
|
+
|
|
754
|
+
// Main monitoring loop
|
|
755
|
+
let lastState: string | null = null;
|
|
756
|
+
|
|
757
|
+
const checkState = async (): Promise<boolean> => {
|
|
758
|
+
const output = await tmux.capturePaneContent(paneId, 30);
|
|
759
|
+
const state = detectState(output);
|
|
760
|
+
|
|
761
|
+
// Log state changes
|
|
762
|
+
if (state.type !== lastState) {
|
|
763
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
764
|
+
console.log(`[${elapsed}s] State: ${state.type}${state.detail ? ` (${state.detail})` : ''}`);
|
|
765
|
+
lastState = state.type;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Handle permission requests
|
|
769
|
+
if (state.type === 'permission' && options.autoApprove) {
|
|
770
|
+
console.log(` ↳ Auto-approving permission...`);
|
|
771
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
|
|
772
|
+
await sleep(200);
|
|
773
|
+
return false; // Continue monitoring
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Handle question states
|
|
777
|
+
if (state.type === 'question') {
|
|
778
|
+
if (state.detail === 'plan_approval' && options.autoApprove) {
|
|
779
|
+
// Auto-approve plans
|
|
780
|
+
console.log(` ↳ Auto-approving plan...`);
|
|
781
|
+
await tmux.executeTmux(`send-keys -t '${paneId}' Enter`);
|
|
782
|
+
await sleep(200);
|
|
783
|
+
return false; // Continue monitoring
|
|
784
|
+
}
|
|
785
|
+
// Other questions require user input - exit and report
|
|
786
|
+
console.log(`⚠️ Question requires manual input`);
|
|
787
|
+
const questionOptions = extractQuestionOptions(output);
|
|
788
|
+
if (questionOptions.length > 0) {
|
|
789
|
+
console.log(` Options:`);
|
|
790
|
+
questionOptions.forEach((opt, i) => console.log(` [${i + 1}] ${opt}`));
|
|
791
|
+
}
|
|
792
|
+
return true; // Stop monitoring, need user input
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Check for idle (task complete)
|
|
796
|
+
if (state.type === 'idle') {
|
|
797
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
798
|
+
console.log(`✅ Task complete (${elapsed}s)`);
|
|
799
|
+
return true; // Done
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Check for error
|
|
803
|
+
if (state.type === 'error') {
|
|
804
|
+
console.log(`❌ Task encountered error`);
|
|
805
|
+
return true; // Done (with error)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return false; // Continue monitoring
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
// Poll until done or timeout
|
|
812
|
+
const pollInterval = 500;
|
|
813
|
+
const maxIterations = Math.ceil(timeoutMs / pollInterval);
|
|
814
|
+
|
|
815
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
816
|
+
const done = await checkState();
|
|
817
|
+
if (done) break;
|
|
818
|
+
|
|
819
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
820
|
+
console.log(`⚠️ Timeout after ${timeoutMs / 1000}s`);
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
await sleep(pollInterval);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
monitor.stop();
|
|
828
|
+
|
|
829
|
+
// Output final state as JSON if requested
|
|
830
|
+
if (options.json) {
|
|
831
|
+
const output = await tmux.capturePaneContent(paneId, 30);
|
|
832
|
+
const state = detectState(output);
|
|
833
|
+
console.log(JSON.stringify({
|
|
834
|
+
session: sessionName,
|
|
835
|
+
state: state.type,
|
|
836
|
+
detail: state.detail,
|
|
837
|
+
elapsedMs: Date.now() - startTime,
|
|
838
|
+
}, null, 2));
|
|
839
|
+
}
|
|
840
|
+
} catch (error: any) {
|
|
841
|
+
console.error(`❌ Error: ${error.message}`);
|
|
842
|
+
process.exit(1);
|
|
843
|
+
}
|
|
844
|
+
}
|