@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,442 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event monitor for Claude Code sessions
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time monitoring of Claude Code sessions via polling,
|
|
5
|
+
* emitting events for state changes, output, and silence detection.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'events';
|
|
9
|
+
import * as tmux from '../tmux.js';
|
|
10
|
+
import { ClaudeState, detectState, detectCompletion } from './state-detector.js';
|
|
11
|
+
|
|
12
|
+
export interface ClaudeEvent {
|
|
13
|
+
type:
|
|
14
|
+
| 'state_change'
|
|
15
|
+
| 'output'
|
|
16
|
+
| 'silence'
|
|
17
|
+
| 'activity'
|
|
18
|
+
| 'permission'
|
|
19
|
+
| 'question'
|
|
20
|
+
| 'error'
|
|
21
|
+
| 'complete';
|
|
22
|
+
state?: ClaudeState;
|
|
23
|
+
output?: string;
|
|
24
|
+
silenceMs?: number;
|
|
25
|
+
timestamp: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface EventMonitorOptions {
|
|
29
|
+
/** Polling interval in milliseconds (default: 500) */
|
|
30
|
+
pollIntervalMs?: number;
|
|
31
|
+
/** Number of lines to capture (default: 30) */
|
|
32
|
+
captureLines?: number;
|
|
33
|
+
/** Silence threshold for completion detection (default: 3000) */
|
|
34
|
+
silenceThresholdMs?: number;
|
|
35
|
+
/** Specific pane ID to monitor (default: first pane of first window) */
|
|
36
|
+
paneId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class EventMonitor extends EventEmitter {
|
|
40
|
+
private sessionName: string;
|
|
41
|
+
private paneId: string | null = null;
|
|
42
|
+
private explicitPaneId: string | null = null;
|
|
43
|
+
private options: Required<Omit<EventMonitorOptions, 'paneId'>>;
|
|
44
|
+
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
45
|
+
private lastOutput: string = '';
|
|
46
|
+
private lastOutputTime: number = Date.now();
|
|
47
|
+
private lastState: ClaudeState | null = null;
|
|
48
|
+
private running: boolean = false;
|
|
49
|
+
|
|
50
|
+
constructor(sessionName: string, options: EventMonitorOptions = {}) {
|
|
51
|
+
super();
|
|
52
|
+
this.sessionName = sessionName;
|
|
53
|
+
this.explicitPaneId = options.paneId || null;
|
|
54
|
+
this.options = {
|
|
55
|
+
pollIntervalMs: options.pollIntervalMs ?? 500,
|
|
56
|
+
captureLines: options.captureLines ?? 30,
|
|
57
|
+
silenceThresholdMs: options.silenceThresholdMs ?? 3000,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Start monitoring the session
|
|
63
|
+
*/
|
|
64
|
+
async start(): Promise<void> {
|
|
65
|
+
if (this.running) return;
|
|
66
|
+
|
|
67
|
+
// Use explicit pane ID if provided
|
|
68
|
+
if (this.explicitPaneId) {
|
|
69
|
+
this.paneId = this.explicitPaneId.startsWith('%')
|
|
70
|
+
? this.explicitPaneId
|
|
71
|
+
: `%${this.explicitPaneId}`;
|
|
72
|
+
} else {
|
|
73
|
+
// Find session and get pane ID
|
|
74
|
+
const session = await tmux.findSessionByName(this.sessionName);
|
|
75
|
+
if (!session) {
|
|
76
|
+
throw new Error(`Session "${this.sessionName}" not found`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const windows = await tmux.listWindows(session.id);
|
|
80
|
+
if (!windows || windows.length === 0) {
|
|
81
|
+
throw new Error(`No windows found in session "${this.sessionName}"`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const panes = await tmux.listPanes(windows[0].id);
|
|
85
|
+
if (!panes || panes.length === 0) {
|
|
86
|
+
throw new Error(`No panes found in session "${this.sessionName}"`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.paneId = panes[0].id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.running = true;
|
|
93
|
+
this.lastOutputTime = Date.now();
|
|
94
|
+
|
|
95
|
+
// Initial capture
|
|
96
|
+
await this.poll();
|
|
97
|
+
|
|
98
|
+
// Start polling
|
|
99
|
+
this.pollTimer = setInterval(() => this.poll(), this.options.pollIntervalMs);
|
|
100
|
+
|
|
101
|
+
this.emit('started', { sessionName: this.sessionName, paneId: this.paneId });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Stop monitoring
|
|
106
|
+
*/
|
|
107
|
+
stop(): void {
|
|
108
|
+
if (this.pollTimer) {
|
|
109
|
+
clearInterval(this.pollTimer);
|
|
110
|
+
this.pollTimer = null;
|
|
111
|
+
}
|
|
112
|
+
this.running = false;
|
|
113
|
+
this.emit('stopped');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if monitor is running
|
|
118
|
+
*/
|
|
119
|
+
isRunning(): boolean {
|
|
120
|
+
return this.running;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get current state
|
|
125
|
+
*/
|
|
126
|
+
getCurrentState(): ClaudeState | null {
|
|
127
|
+
return this.lastState;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get time since last output change
|
|
132
|
+
*/
|
|
133
|
+
getSilenceMs(): number {
|
|
134
|
+
return Date.now() - this.lastOutputTime;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Poll for changes
|
|
139
|
+
*/
|
|
140
|
+
private async poll(): Promise<void> {
|
|
141
|
+
if (!this.paneId || !this.running) return;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const output = await tmux.capturePaneContent(
|
|
145
|
+
this.paneId,
|
|
146
|
+
this.options.captureLines
|
|
147
|
+
);
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
|
|
150
|
+
// Check for new output
|
|
151
|
+
if (output !== this.lastOutput) {
|
|
152
|
+
const newContent = this.getNewContent(this.lastOutput, output);
|
|
153
|
+
|
|
154
|
+
if (newContent) {
|
|
155
|
+
this.lastOutputTime = now;
|
|
156
|
+
this.emitEvent({
|
|
157
|
+
type: 'output',
|
|
158
|
+
output: newContent,
|
|
159
|
+
timestamp: now,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.emitEvent({
|
|
163
|
+
type: 'activity',
|
|
164
|
+
timestamp: now,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Detect state from new output
|
|
169
|
+
const newState = detectState(output);
|
|
170
|
+
|
|
171
|
+
// Check for state changes
|
|
172
|
+
if (this.lastState && newState.type !== this.lastState.type) {
|
|
173
|
+
this.emitEvent({
|
|
174
|
+
type: 'state_change',
|
|
175
|
+
state: newState,
|
|
176
|
+
timestamp: now,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Emit specific events for important state changes
|
|
180
|
+
if (newState.type === 'permission') {
|
|
181
|
+
this.emitEvent({
|
|
182
|
+
type: 'permission',
|
|
183
|
+
state: newState,
|
|
184
|
+
timestamp: now,
|
|
185
|
+
});
|
|
186
|
+
} else if (newState.type === 'question') {
|
|
187
|
+
this.emitEvent({
|
|
188
|
+
type: 'question',
|
|
189
|
+
state: newState,
|
|
190
|
+
timestamp: now,
|
|
191
|
+
});
|
|
192
|
+
} else if (newState.type === 'error') {
|
|
193
|
+
this.emitEvent({
|
|
194
|
+
type: 'error',
|
|
195
|
+
state: newState,
|
|
196
|
+
timestamp: now,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check for completion
|
|
201
|
+
const completion = detectCompletion(output, this.lastOutput);
|
|
202
|
+
if (completion.complete && completion.confidence > 0.6) {
|
|
203
|
+
this.emitEvent({
|
|
204
|
+
type: 'complete',
|
|
205
|
+
state: newState,
|
|
206
|
+
timestamp: now,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
this.lastState = newState;
|
|
212
|
+
this.lastOutput = output;
|
|
213
|
+
} else {
|
|
214
|
+
// No change - check silence threshold
|
|
215
|
+
const silenceMs = now - this.lastOutputTime;
|
|
216
|
+
|
|
217
|
+
// Emit silence events at threshold intervals
|
|
218
|
+
if (
|
|
219
|
+
silenceMs >= this.options.silenceThresholdMs &&
|
|
220
|
+
silenceMs % this.options.silenceThresholdMs < this.options.pollIntervalMs
|
|
221
|
+
) {
|
|
222
|
+
this.emitEvent({
|
|
223
|
+
type: 'silence',
|
|
224
|
+
silenceMs,
|
|
225
|
+
timestamp: now,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// Emit error but continue polling
|
|
231
|
+
this.emit('poll_error', error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get new content since last poll
|
|
237
|
+
*/
|
|
238
|
+
private getNewContent(oldOutput: string, newOutput: string): string | null {
|
|
239
|
+
if (oldOutput === newOutput) return null;
|
|
240
|
+
|
|
241
|
+
// If old output is empty, return all new output
|
|
242
|
+
if (!oldOutput) return newOutput;
|
|
243
|
+
|
|
244
|
+
// Find where old output ends in new output
|
|
245
|
+
const oldLines = oldOutput.split('\n');
|
|
246
|
+
const newLines = newOutput.split('\n');
|
|
247
|
+
|
|
248
|
+
// Simple approach: find the last line of old output in new output
|
|
249
|
+
const lastOldLine = oldLines[oldLines.length - 1];
|
|
250
|
+
const lastOldLineIndex = newLines.lastIndexOf(lastOldLine);
|
|
251
|
+
|
|
252
|
+
if (lastOldLineIndex >= 0 && lastOldLineIndex < newLines.length - 1) {
|
|
253
|
+
return newLines.slice(lastOldLineIndex + 1).join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If we can't find exact match, return the diff
|
|
257
|
+
// (this happens when lines scroll out of the capture buffer)
|
|
258
|
+
const oldSet = new Set(oldLines);
|
|
259
|
+
const newContent = newLines.filter((line) => !oldSet.has(line));
|
|
260
|
+
|
|
261
|
+
return newContent.length > 0 ? newContent.join('\n') : null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Emit a Claude event
|
|
266
|
+
*/
|
|
267
|
+
private emitEvent(event: ClaudeEvent): void {
|
|
268
|
+
this.emit(event.type, event);
|
|
269
|
+
this.emit('event', event);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Wait for a specific state or condition
|
|
275
|
+
*/
|
|
276
|
+
export async function waitForState(
|
|
277
|
+
monitor: EventMonitor,
|
|
278
|
+
predicate: (state: ClaudeState) => boolean,
|
|
279
|
+
timeoutMs: number = 60000
|
|
280
|
+
): Promise<ClaudeState> {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const timeout = setTimeout(() => {
|
|
283
|
+
cleanup();
|
|
284
|
+
reject(new Error('Timeout waiting for state'));
|
|
285
|
+
}, timeoutMs);
|
|
286
|
+
|
|
287
|
+
const handler = (event: ClaudeEvent) => {
|
|
288
|
+
if (event.state && predicate(event.state)) {
|
|
289
|
+
cleanup();
|
|
290
|
+
resolve(event.state);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const cleanup = () => {
|
|
295
|
+
clearTimeout(timeout);
|
|
296
|
+
monitor.off('state_change', handler);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Check current state first
|
|
300
|
+
const current = monitor.getCurrentState();
|
|
301
|
+
if (current && predicate(current)) {
|
|
302
|
+
cleanup();
|
|
303
|
+
resolve(current);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
monitor.on('state_change', handler);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Wait for silence (no output) for a duration
|
|
313
|
+
*/
|
|
314
|
+
export async function waitForSilence(
|
|
315
|
+
monitor: EventMonitor,
|
|
316
|
+
silenceMs: number,
|
|
317
|
+
timeoutMs: number = 120000
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const startTime = Date.now();
|
|
321
|
+
let lastActivityTime = startTime;
|
|
322
|
+
|
|
323
|
+
const timeout = setTimeout(() => {
|
|
324
|
+
cleanup();
|
|
325
|
+
reject(new Error('Timeout waiting for silence'));
|
|
326
|
+
}, timeoutMs);
|
|
327
|
+
|
|
328
|
+
const activityHandler = () => {
|
|
329
|
+
lastActivityTime = Date.now();
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const checkSilence = setInterval(() => {
|
|
333
|
+
const silenceDuration = Date.now() - lastActivityTime;
|
|
334
|
+
if (silenceDuration >= silenceMs) {
|
|
335
|
+
cleanup();
|
|
336
|
+
resolve();
|
|
337
|
+
}
|
|
338
|
+
}, 100);
|
|
339
|
+
|
|
340
|
+
const cleanup = () => {
|
|
341
|
+
clearTimeout(timeout);
|
|
342
|
+
clearInterval(checkSilence);
|
|
343
|
+
monitor.off('activity', activityHandler);
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
monitor.on('activity', activityHandler);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Wait for completion (idle state after activity)
|
|
352
|
+
*/
|
|
353
|
+
export async function waitForCompletion(
|
|
354
|
+
monitor: EventMonitor,
|
|
355
|
+
options: {
|
|
356
|
+
silenceMs?: number;
|
|
357
|
+
timeoutMs?: number;
|
|
358
|
+
requireIdle?: boolean;
|
|
359
|
+
} = {}
|
|
360
|
+
): Promise<{ state: ClaudeState; reason: string }> {
|
|
361
|
+
const { silenceMs = 3000, timeoutMs = 120000, requireIdle = true } = options;
|
|
362
|
+
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
const timeout = setTimeout(() => {
|
|
365
|
+
cleanup();
|
|
366
|
+
reject(new Error('Timeout waiting for completion'));
|
|
367
|
+
}, timeoutMs);
|
|
368
|
+
|
|
369
|
+
let lastActivityTime = Date.now();
|
|
370
|
+
let silenceCheckInterval: ReturnType<typeof setInterval> | null = null;
|
|
371
|
+
|
|
372
|
+
const completeHandler = (event: ClaudeEvent) => {
|
|
373
|
+
if (event.state) {
|
|
374
|
+
cleanup();
|
|
375
|
+
resolve({ state: event.state, reason: 'complete event' });
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const activityHandler = () => {
|
|
380
|
+
lastActivityTime = Date.now();
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const stateHandler = (event: ClaudeEvent) => {
|
|
384
|
+
// If we get a permission or question, we're not complete
|
|
385
|
+
if (event.state?.type === 'permission' || event.state?.type === 'question') {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// If idle and requireIdle, resolve
|
|
390
|
+
if (requireIdle && event.state?.type === 'idle') {
|
|
391
|
+
cleanup();
|
|
392
|
+
resolve({ state: event.state, reason: 'idle state' });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// If error, resolve (task failed but is "complete")
|
|
396
|
+
if (event.state?.type === 'error') {
|
|
397
|
+
cleanup();
|
|
398
|
+
resolve({ state: event.state, reason: 'error' });
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const cleanup = () => {
|
|
403
|
+
clearTimeout(timeout);
|
|
404
|
+
if (silenceCheckInterval) clearInterval(silenceCheckInterval);
|
|
405
|
+
monitor.off('complete', completeHandler);
|
|
406
|
+
monitor.off('activity', activityHandler);
|
|
407
|
+
monitor.off('state_change', stateHandler);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Check silence periodically
|
|
411
|
+
silenceCheckInterval = setInterval(() => {
|
|
412
|
+
const silenceDuration = Date.now() - lastActivityTime;
|
|
413
|
+
const currentState = monitor.getCurrentState();
|
|
414
|
+
|
|
415
|
+
if (silenceDuration >= silenceMs) {
|
|
416
|
+
// Check if current state indicates completion
|
|
417
|
+
if (
|
|
418
|
+
currentState &&
|
|
419
|
+
(currentState.type === 'idle' ||
|
|
420
|
+
currentState.type === 'complete' ||
|
|
421
|
+
currentState.type === 'error')
|
|
422
|
+
) {
|
|
423
|
+
cleanup();
|
|
424
|
+
resolve({
|
|
425
|
+
state: currentState,
|
|
426
|
+
reason: `silence (${silenceDuration}ms)`,
|
|
427
|
+
});
|
|
428
|
+
} else if (!requireIdle) {
|
|
429
|
+
cleanup();
|
|
430
|
+
resolve({
|
|
431
|
+
state: currentState || { type: 'unknown', timestamp: Date.now(), rawOutput: '', confidence: 0 },
|
|
432
|
+
reason: `silence (${silenceDuration}ms) - non-idle`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}, 500);
|
|
437
|
+
|
|
438
|
+
monitor.on('complete', completeHandler);
|
|
439
|
+
monitor.on('activity', activityHandler);
|
|
440
|
+
monitor.on('state_change', stateHandler);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator - Claude Code session orchestration library
|
|
3
|
+
*
|
|
4
|
+
* Provides tools for monitoring, controlling, and automating
|
|
5
|
+
* Claude Code sessions running in tmux.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Re-export all public APIs
|
|
9
|
+
export * from './patterns.js';
|
|
10
|
+
export * from './state-detector.js';
|
|
11
|
+
export * from './event-monitor.js';
|
|
12
|
+
export * from './completion.js';
|