@ai-devkit/agent-manager 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/ClaudeCodeAdapter.d.ts +12 -0
  2. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  3. package/dist/adapters/ClaudeCodeAdapter.js +208 -50
  4. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  5. package/dist/adapters/CodexAdapter.d.ts +52 -0
  6. package/dist/adapters/CodexAdapter.d.ts.map +1 -0
  7. package/dist/adapters/CodexAdapter.js +432 -0
  8. package/dist/adapters/CodexAdapter.js.map +1 -0
  9. package/dist/adapters/index.d.ts +1 -0
  10. package/dist/adapters/index.d.ts.map +1 -1
  11. package/dist/adapters/index.js +3 -1
  12. package/dist/adapters/index.js.map +1 -1
  13. package/dist/index.d.ts +3 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +6 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/terminal/TerminalFocusManager.d.ts +7 -1
  18. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
  19. package/dist/terminal/TerminalFocusManager.js +15 -8
  20. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  21. package/dist/terminal/TtyWriter.d.ts +23 -0
  22. package/dist/terminal/TtyWriter.d.ts.map +1 -0
  23. package/dist/terminal/TtyWriter.js +106 -0
  24. package/dist/terminal/TtyWriter.js.map +1 -0
  25. package/dist/terminal/index.d.ts +2 -0
  26. package/dist/terminal/index.d.ts.map +1 -1
  27. package/dist/terminal/index.js +5 -1
  28. package/dist/terminal/index.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +120 -2
  31. package/src/__tests__/adapters/CodexAdapter.test.ts +319 -0
  32. package/src/__tests__/terminal/TtyWriter.test.ts +154 -0
  33. package/src/adapters/ClaudeCodeAdapter.ts +309 -56
  34. package/src/adapters/CodexAdapter.ts +584 -0
  35. package/src/adapters/index.ts +1 -0
  36. package/src/index.ts +3 -1
  37. package/src/terminal/TerminalFocusManager.ts +15 -8
  38. package/src/terminal/TtyWriter.ts +112 -0
  39. package/src/terminal/index.ts +2 -0
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Codex Adapter
3
+ *
4
+ * Detects running Codex agents by combining:
5
+ * 1. Running `codex` processes
6
+ * 2. Session metadata under ~/.codex/sessions
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { execSync } from 'child_process';
12
+ import type { AgentAdapter, AgentInfo, ProcessInfo } from './AgentAdapter';
13
+ import { AgentStatus } from './AgentAdapter';
14
+ import { listProcesses } from '../utils/process';
15
+ import { readJsonLines } from '../utils/file';
16
+
17
+ interface CodexSessionMetaPayload {
18
+ id?: string;
19
+ timestamp?: string;
20
+ cwd?: string;
21
+ }
22
+
23
+ interface CodexSessionMetaEntry {
24
+ type?: string;
25
+ payload?: CodexSessionMetaPayload;
26
+ }
27
+
28
+ interface CodexEventEntry {
29
+ timestamp?: string;
30
+ type?: string;
31
+ payload?: {
32
+ type?: string;
33
+ message?: string;
34
+ };
35
+ }
36
+
37
+ interface CodexSession {
38
+ sessionId: string;
39
+ projectPath: string;
40
+ summary: string;
41
+ sessionStart: Date;
42
+ lastActive: Date;
43
+ lastPayloadType?: string;
44
+ }
45
+
46
+ type SessionMatchMode = 'cwd' | 'missing-cwd' | 'any';
47
+
48
+ export class CodexAdapter implements AgentAdapter {
49
+ readonly type = 'codex' as const;
50
+
51
+ /** Keep status thresholds aligned across adapters. */
52
+ private static readonly IDLE_THRESHOLD_MINUTES = 5;
53
+ /** Limit session parsing per run to keep list latency bounded. */
54
+ private static readonly MIN_SESSION_SCAN = 12;
55
+ private static readonly MAX_SESSION_SCAN = 40;
56
+ private static readonly SESSION_SCAN_MULTIPLIER = 4;
57
+ /** Also include session files around process start day to recover long-lived processes. */
58
+ private static readonly PROCESS_START_DAY_WINDOW_DAYS = 1;
59
+ /** Matching tolerance between process start time and session start time. */
60
+ private static readonly PROCESS_SESSION_TIME_TOLERANCE_MS = 2 * 60 * 1000;
61
+
62
+ private codexSessionsDir: string;
63
+
64
+ constructor() {
65
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
66
+ this.codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
67
+ }
68
+
69
+ canHandle(processInfo: ProcessInfo): boolean {
70
+ return this.isCodexExecutable(processInfo.command);
71
+ }
72
+
73
+ async detectAgents(): Promise<AgentInfo[]> {
74
+ const codexProcesses = this.listCodexProcesses();
75
+
76
+ if (codexProcesses.length === 0) {
77
+ return [];
78
+ }
79
+
80
+ const processStartByPid = this.getProcessStartTimes(codexProcesses.map((processInfo) => processInfo.pid));
81
+
82
+ const sessionScanLimit = this.calculateSessionScanLimit(codexProcesses.length);
83
+ const sessions = this.readSessions(sessionScanLimit, processStartByPid);
84
+ if (sessions.length === 0) {
85
+ return codexProcesses.map((processInfo) =>
86
+ this.mapProcessOnlyAgent(processInfo, [], 'No Codex session metadata found'),
87
+ );
88
+ }
89
+
90
+ const sortedSessions = [...sessions].sort(
91
+ (a, b) => b.lastActive.getTime() - a.lastActive.getTime(),
92
+ );
93
+ const usedSessionIds = new Set<string>();
94
+ const assignedPids = new Set<number>();
95
+ const agents: AgentInfo[] = [];
96
+
97
+ // Match exact cwd first, then missing-cwd sessions, then any available session.
98
+ this.assignSessionsForMode(
99
+ 'cwd',
100
+ codexProcesses,
101
+ sortedSessions,
102
+ usedSessionIds,
103
+ assignedPids,
104
+ processStartByPid,
105
+ agents,
106
+ );
107
+ this.assignSessionsForMode(
108
+ 'missing-cwd',
109
+ codexProcesses,
110
+ sortedSessions,
111
+ usedSessionIds,
112
+ assignedPids,
113
+ processStartByPid,
114
+ agents,
115
+ );
116
+ this.assignSessionsForMode(
117
+ 'any',
118
+ codexProcesses,
119
+ sortedSessions,
120
+ usedSessionIds,
121
+ assignedPids,
122
+ processStartByPid,
123
+ agents,
124
+ );
125
+
126
+ // Every running codex process should still be listed.
127
+ for (const processInfo of codexProcesses) {
128
+ if (assignedPids.has(processInfo.pid)) {
129
+ continue;
130
+ }
131
+
132
+ this.addProcessOnlyAgent(processInfo, assignedPids, agents);
133
+ }
134
+
135
+ return agents;
136
+ }
137
+
138
+ private listCodexProcesses(): ProcessInfo[] {
139
+ return listProcesses({ namePattern: 'codex' }).filter((processInfo) =>
140
+ this.canHandle(processInfo),
141
+ );
142
+ }
143
+
144
+ private calculateSessionScanLimit(processCount: number): number {
145
+ return Math.min(
146
+ Math.max(
147
+ processCount * CodexAdapter.SESSION_SCAN_MULTIPLIER,
148
+ CodexAdapter.MIN_SESSION_SCAN,
149
+ ),
150
+ CodexAdapter.MAX_SESSION_SCAN,
151
+ );
152
+ }
153
+
154
+ private assignSessionsForMode(
155
+ mode: SessionMatchMode,
156
+ codexProcesses: ProcessInfo[],
157
+ sessions: CodexSession[],
158
+ usedSessionIds: Set<string>,
159
+ assignedPids: Set<number>,
160
+ processStartByPid: Map<number, Date>,
161
+ agents: AgentInfo[],
162
+ ): void {
163
+ for (const processInfo of codexProcesses) {
164
+ if (assignedPids.has(processInfo.pid)) {
165
+ continue;
166
+ }
167
+
168
+ const session = this.selectBestSession(
169
+ processInfo,
170
+ sessions,
171
+ usedSessionIds,
172
+ processStartByPid,
173
+ mode,
174
+ );
175
+ if (!session) {
176
+ continue;
177
+ }
178
+
179
+ this.addMappedSessionAgent(session, processInfo, usedSessionIds, assignedPids, agents);
180
+ }
181
+ }
182
+
183
+ private addMappedSessionAgent(
184
+ session: CodexSession,
185
+ processInfo: ProcessInfo,
186
+ usedSessionIds: Set<string>,
187
+ assignedPids: Set<number>,
188
+ agents: AgentInfo[],
189
+ ): void {
190
+ usedSessionIds.add(session.sessionId);
191
+ assignedPids.add(processInfo.pid);
192
+ agents.push(this.mapSessionToAgent(session, processInfo, agents));
193
+ }
194
+
195
+ private addProcessOnlyAgent(
196
+ processInfo: ProcessInfo,
197
+ assignedPids: Set<number>,
198
+ agents: AgentInfo[],
199
+ ): void {
200
+ assignedPids.add(processInfo.pid);
201
+ agents.push(this.mapProcessOnlyAgent(processInfo, agents));
202
+ }
203
+
204
+ private mapSessionToAgent(
205
+ session: CodexSession,
206
+ processInfo: ProcessInfo,
207
+ existingAgents: AgentInfo[],
208
+ ): AgentInfo {
209
+ return {
210
+ name: this.generateAgentName(session, existingAgents),
211
+ type: this.type,
212
+ status: this.determineStatus(session),
213
+ summary: session.summary || 'Codex session active',
214
+ pid: processInfo.pid,
215
+ projectPath: session.projectPath || processInfo.cwd || '',
216
+ sessionId: session.sessionId,
217
+ lastActive: session.lastActive,
218
+ };
219
+ }
220
+
221
+ private mapProcessOnlyAgent(
222
+ processInfo: ProcessInfo,
223
+ existingAgents: AgentInfo[],
224
+ summary: string = 'Codex process running',
225
+ ): AgentInfo {
226
+ const syntheticSession: CodexSession = {
227
+ sessionId: `pid-${processInfo.pid}`,
228
+ projectPath: processInfo.cwd || '',
229
+ summary,
230
+ sessionStart: new Date(),
231
+ lastActive: new Date(),
232
+ lastPayloadType: 'process_only',
233
+ };
234
+
235
+ return {
236
+ name: this.generateAgentName(syntheticSession, existingAgents),
237
+ type: this.type,
238
+ status: AgentStatus.RUNNING,
239
+ summary,
240
+ pid: processInfo.pid,
241
+ projectPath: processInfo.cwd || '',
242
+ sessionId: syntheticSession.sessionId,
243
+ lastActive: syntheticSession.lastActive,
244
+ };
245
+ }
246
+
247
+ private readSessions(limit: number, processStartByPid: Map<number, Date>): CodexSession[] {
248
+ const sessionFiles = this.findSessionFiles(limit, processStartByPid);
249
+ const sessions: CodexSession[] = [];
250
+
251
+ for (const sessionFile of sessionFiles) {
252
+ try {
253
+ const session = this.readSession(sessionFile);
254
+ if (session) {
255
+ sessions.push(session);
256
+ }
257
+ } catch (error) {
258
+ console.error(`Failed to parse Codex session ${sessionFile}:`, error);
259
+ }
260
+ }
261
+
262
+ return sessions;
263
+ }
264
+
265
+ private findSessionFiles(limit: number, processStartByPid: Map<number, Date>): string[] {
266
+ if (!fs.existsSync(this.codexSessionsDir)) {
267
+ return [];
268
+ }
269
+
270
+ const files: Array<{ path: string; mtimeMs: number }> = [];
271
+ const stack: string[] = [this.codexSessionsDir];
272
+
273
+ while (stack.length > 0) {
274
+ const currentDir = stack.pop();
275
+ if (!currentDir || !fs.existsSync(currentDir)) {
276
+ continue;
277
+ }
278
+
279
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
280
+ const fullPath = path.join(currentDir, entry.name);
281
+ if (entry.isDirectory()) {
282
+ stack.push(fullPath);
283
+ continue;
284
+ }
285
+
286
+ if (entry.isFile() && entry.name.endsWith('.jsonl')) {
287
+ try {
288
+ files.push({
289
+ path: fullPath,
290
+ mtimeMs: fs.statSync(fullPath).mtimeMs,
291
+ });
292
+ } catch {
293
+ continue;
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ const recentFiles = files
300
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
301
+ .slice(0, limit)
302
+ .map((file) => file.path);
303
+ const processDayFiles = this.findProcessDaySessionFiles(processStartByPid);
304
+
305
+ const selectedPaths = new Set(recentFiles);
306
+ for (const processDayFile of processDayFiles) {
307
+ selectedPaths.add(processDayFile);
308
+ }
309
+
310
+ return Array.from(selectedPaths);
311
+ }
312
+
313
+ private findProcessDaySessionFiles(processStartByPid: Map<number, Date>): string[] {
314
+ const files: string[] = [];
315
+ const dayKeys = new Set<string>();
316
+ const dayWindow = CodexAdapter.PROCESS_START_DAY_WINDOW_DAYS;
317
+
318
+ for (const processStart of processStartByPid.values()) {
319
+ for (let offset = -dayWindow; offset <= dayWindow; offset++) {
320
+ const day = new Date(processStart.getTime());
321
+ day.setDate(day.getDate() + offset);
322
+ dayKeys.add(this.toSessionDayKey(day));
323
+ }
324
+ }
325
+
326
+ for (const dayKey of dayKeys) {
327
+ const dayDir = path.join(this.codexSessionsDir, dayKey);
328
+ if (!fs.existsSync(dayDir)) {
329
+ continue;
330
+ }
331
+
332
+ for (const entry of fs.readdirSync(dayDir, { withFileTypes: true })) {
333
+ if (entry.isFile() && entry.name.endsWith('.jsonl')) {
334
+ files.push(path.join(dayDir, entry.name));
335
+ }
336
+ }
337
+ }
338
+
339
+ return files;
340
+ }
341
+
342
+ private toSessionDayKey(date: Date): string {
343
+ const yyyy = String(date.getFullYear()).padStart(4, '0');
344
+ const mm = String(date.getMonth() + 1).padStart(2, '0');
345
+ const dd = String(date.getDate()).padStart(2, '0');
346
+ return path.join(yyyy, mm, dd);
347
+ }
348
+
349
+ private readSession(filePath: string): CodexSession | null {
350
+ const firstLine = this.readFirstLine(filePath);
351
+ if (!firstLine) {
352
+ return null;
353
+ }
354
+
355
+ const metaEntry = this.parseSessionMeta(firstLine);
356
+ if (!metaEntry?.payload?.id) {
357
+ return null;
358
+ }
359
+
360
+ const entries = readJsonLines<CodexEventEntry>(filePath, 300);
361
+ const lastEntry = this.findLastEventEntry(entries);
362
+ const lastPayloadType = lastEntry?.payload?.type;
363
+
364
+ const lastActive =
365
+ this.parseTimestamp(lastEntry?.timestamp) ||
366
+ this.parseTimestamp(metaEntry.payload.timestamp) ||
367
+ fs.statSync(filePath).mtime;
368
+ const sessionStart =
369
+ this.parseTimestamp(metaEntry.payload.timestamp) ||
370
+ lastActive;
371
+
372
+ return {
373
+ sessionId: metaEntry.payload.id,
374
+ projectPath: metaEntry.payload.cwd || '',
375
+ summary: this.extractSummary(entries),
376
+ sessionStart,
377
+ lastActive,
378
+ lastPayloadType,
379
+ };
380
+ }
381
+
382
+ private readFirstLine(filePath: string): string {
383
+ const content = fs.readFileSync(filePath, 'utf-8');
384
+ return content.split('\n')[0]?.trim() || '';
385
+ }
386
+
387
+ private parseSessionMeta(line: string): CodexSessionMetaEntry | null {
388
+ try {
389
+ const parsed = JSON.parse(line) as CodexSessionMetaEntry;
390
+ if (parsed.type !== 'session_meta') {
391
+ return null;
392
+ }
393
+ return parsed;
394
+ } catch {
395
+ return null;
396
+ }
397
+ }
398
+
399
+ private findLastEventEntry(entries: CodexEventEntry[]): CodexEventEntry | undefined {
400
+ for (let i = entries.length - 1; i >= 0; i--) {
401
+ const entry = entries[i];
402
+ if (entry && typeof entry.type === 'string') {
403
+ return entry;
404
+ }
405
+ }
406
+ return undefined;
407
+ }
408
+
409
+ private parseTimestamp(value?: string): Date | null {
410
+ if (!value) {
411
+ return null;
412
+ }
413
+
414
+ const timestamp = new Date(value);
415
+ return Number.isNaN(timestamp.getTime()) ? null : timestamp;
416
+ }
417
+
418
+ private selectBestSession(
419
+ processInfo: ProcessInfo,
420
+ sessions: CodexSession[],
421
+ usedSessionIds: Set<string>,
422
+ processStartByPid: Map<number, Date>,
423
+ mode: SessionMatchMode,
424
+ ): CodexSession | undefined {
425
+ const candidates = this.filterCandidateSessions(processInfo, sessions, usedSessionIds, mode);
426
+
427
+ if (candidates.length === 0) {
428
+ return undefined;
429
+ }
430
+
431
+ const processStart = processStartByPid.get(processInfo.pid);
432
+ if (!processStart) {
433
+ return candidates.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime())[0];
434
+ }
435
+
436
+ return this.rankCandidatesByStartTime(candidates, processStart)[0];
437
+ }
438
+
439
+ private filterCandidateSessions(
440
+ processInfo: ProcessInfo,
441
+ sessions: CodexSession[],
442
+ usedSessionIds: Set<string>,
443
+ mode: SessionMatchMode,
444
+ ): CodexSession[] {
445
+ return sessions.filter((session) => {
446
+ if (usedSessionIds.has(session.sessionId)) {
447
+ return false;
448
+ }
449
+
450
+ if (mode === 'cwd') {
451
+ return session.projectPath === processInfo.cwd;
452
+ }
453
+
454
+ if (mode === 'missing-cwd') {
455
+ return !session.projectPath;
456
+ }
457
+
458
+ return true;
459
+ });
460
+ }
461
+
462
+ private rankCandidatesByStartTime(candidates: CodexSession[], processStart: Date): CodexSession[] {
463
+ const toleranceMs = CodexAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
464
+
465
+ return candidates
466
+ .map((session) => {
467
+ const diffMs = Math.abs(session.sessionStart.getTime() - processStart.getTime());
468
+ const outsideTolerance = diffMs > toleranceMs ? 1 : 0;
469
+ return {
470
+ session,
471
+ rank: outsideTolerance,
472
+ diffMs,
473
+ recency: session.lastActive.getTime(),
474
+ };
475
+ })
476
+ .sort((a, b) => {
477
+ if (a.rank !== b.rank) return a.rank - b.rank;
478
+ if (a.diffMs !== b.diffMs) return a.diffMs - b.diffMs;
479
+ return b.recency - a.recency;
480
+ })
481
+ .map((ranked) => ranked.session);
482
+ }
483
+
484
+ private getProcessStartTimes(pids: number[]): Map<number, Date> {
485
+ if (pids.length === 0 || process.env.JEST_WORKER_ID) {
486
+ return new Map();
487
+ }
488
+
489
+ try {
490
+ const output = execSync(`ps -o pid=,etime= -p ${pids.join(',')}`, {
491
+ encoding: 'utf-8',
492
+ });
493
+ const nowMs = Date.now();
494
+ const startTimes = new Map<number, Date>();
495
+
496
+ for (const rawLine of output.split('\n')) {
497
+ const line = rawLine.trim();
498
+ if (!line) continue;
499
+
500
+ const parts = line.split(/\s+/);
501
+ if (parts.length < 2) continue;
502
+
503
+ const pid = Number.parseInt(parts[0], 10);
504
+ const elapsedSeconds = this.parseElapsedSeconds(parts[1]);
505
+ if (!Number.isFinite(pid) || elapsedSeconds === null) continue;
506
+
507
+ startTimes.set(pid, new Date(nowMs - elapsedSeconds * 1000));
508
+ }
509
+
510
+ return startTimes;
511
+ } catch {
512
+ return new Map();
513
+ }
514
+ }
515
+
516
+ private parseElapsedSeconds(etime: string): number | null {
517
+ const match = etime.trim().match(/^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
518
+ if (!match) {
519
+ return null;
520
+ }
521
+
522
+ const days = Number.parseInt(match[1] || '0', 10);
523
+ const hours = Number.parseInt(match[2] || '0', 10);
524
+ const minutes = Number.parseInt(match[3] || '0', 10);
525
+ const seconds = Number.parseInt(match[4] || '0', 10);
526
+
527
+ return (((days * 24 + hours) * 60 + minutes) * 60) + seconds;
528
+ }
529
+
530
+ private extractSummary(entries: CodexEventEntry[]): string {
531
+ for (let i = entries.length - 1; i >= 0; i--) {
532
+ const message = entries[i]?.payload?.message;
533
+ if (typeof message === 'string' && message.trim().length > 0) {
534
+ return this.truncate(message.trim(), 120);
535
+ }
536
+ }
537
+
538
+ return 'Codex session active';
539
+ }
540
+
541
+ private truncate(value: string, maxLength: number): string {
542
+ if (value.length <= maxLength) {
543
+ return value;
544
+ }
545
+ return `${value.slice(0, maxLength - 3)}...`;
546
+ }
547
+
548
+ private isCodexExecutable(command: string): boolean {
549
+ const executable = command.trim().split(/\s+/)[0] || '';
550
+ const base = path.basename(executable).toLowerCase();
551
+ return base === 'codex' || base === 'codex.exe';
552
+ }
553
+
554
+ private determineStatus(session: CodexSession): AgentStatus {
555
+ const diffMs = Date.now() - session.lastActive.getTime();
556
+ const diffMinutes = diffMs / 60000;
557
+
558
+ if (diffMinutes > CodexAdapter.IDLE_THRESHOLD_MINUTES) {
559
+ return AgentStatus.IDLE;
560
+ }
561
+
562
+ if (
563
+ session.lastPayloadType === 'agent_message' ||
564
+ session.lastPayloadType === 'task_complete' ||
565
+ session.lastPayloadType === 'turn_aborted'
566
+ ) {
567
+ return AgentStatus.WAITING;
568
+ }
569
+
570
+ return AgentStatus.RUNNING;
571
+ }
572
+
573
+ private generateAgentName(session: CodexSession, existingAgents: AgentInfo[]): string {
574
+ const fallback = `codex-${session.sessionId.slice(0, 8)}`;
575
+ const baseName = session.projectPath ? path.basename(path.normalize(session.projectPath)) : fallback;
576
+
577
+ const conflict = existingAgents.some((agent) => agent.name === baseName);
578
+ if (!conflict) {
579
+ return baseName || fallback;
580
+ }
581
+
582
+ return `${baseName || fallback} (${session.sessionId.slice(0, 8)})`;
583
+ }
584
+ }
@@ -1,3 +1,4 @@
1
1
  export { ClaudeCodeAdapter } from './ClaudeCodeAdapter';
2
+ export { CodexAdapter } from './CodexAdapter';
2
3
  export { AgentStatus } from './AgentAdapter';
3
4
  export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './AgentAdapter';
package/src/index.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export { AgentManager } from './AgentManager';
2
2
 
3
3
  export { ClaudeCodeAdapter } from './adapters/ClaudeCodeAdapter';
4
+ export { CodexAdapter } from './adapters/CodexAdapter';
4
5
  export { AgentStatus } from './adapters/AgentAdapter';
5
6
  export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './adapters/AgentAdapter';
6
7
 
7
- export { TerminalFocusManager } from './terminal/TerminalFocusManager';
8
+ export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusManager';
8
9
  export type { TerminalLocation } from './terminal/TerminalFocusManager';
10
+ export { TtyWriter } from './terminal/TtyWriter';
9
11
 
10
12
  export { listProcesses, getProcessCwd, getProcessTty, isProcessRunning, getProcessInfo } from './utils/process';
11
13
  export type { ListProcessesOptions } from './utils/process';
@@ -5,8 +5,15 @@ import { getProcessTty } from '../utils/process';
5
5
  const execAsync = promisify(exec);
6
6
  const execFileAsync = promisify(execFile);
7
7
 
8
+ export enum TerminalType {
9
+ TMUX = 'tmux',
10
+ ITERM2 = 'iterm2',
11
+ TERMINAL_APP = 'terminal-app',
12
+ UNKNOWN = 'unknown',
13
+ }
14
+
8
15
  export interface TerminalLocation {
9
- type: 'tmux' | 'iterm2' | 'terminal-app' | 'unknown';
16
+ type: TerminalType;
10
17
  identifier: string; // e.g., "session:window.pane" for tmux, or TTY for others
11
18
  tty: string; // e.g., "/dev/ttys030"
12
19
  }
@@ -39,7 +46,7 @@ export class TerminalFocusManager {
39
46
 
40
47
  // 4. Fallback: we know the TTY but not the emulator wrapper
41
48
  return {
42
- type: 'unknown',
49
+ type: TerminalType.UNKNOWN,
43
50
  identifier: '',
44
51
  tty: fullTty
45
52
  };
@@ -51,11 +58,11 @@ export class TerminalFocusManager {
51
58
  async focusTerminal(location: TerminalLocation): Promise<boolean> {
52
59
  try {
53
60
  switch (location.type) {
54
- case 'tmux':
61
+ case TerminalType.TMUX:
55
62
  return await this.focusTmuxPane(location.identifier);
56
- case 'iterm2':
63
+ case TerminalType.ITERM2:
57
64
  return await this.focusITerm2Session(location.tty);
58
- case 'terminal-app':
65
+ case TerminalType.TERMINAL_APP:
59
66
  return await this.focusTerminalAppWindow(location.tty);
60
67
  default:
61
68
  return false;
@@ -78,7 +85,7 @@ export class TerminalFocusManager {
78
85
  const [paneTty, identifier] = line.split('|');
79
86
  if (paneTty === tty && identifier) {
80
87
  return {
81
- type: 'tmux',
88
+ type: TerminalType.TMUX,
82
89
  identifier,
83
90
  tty
84
91
  };
@@ -113,7 +120,7 @@ export class TerminalFocusManager {
113
120
  const { stdout } = await execAsync(`osascript -e '${script}'`);
114
121
  if (stdout.trim() === "found") {
115
122
  return {
116
- type: 'iterm2',
123
+ type: TerminalType.ITERM2,
117
124
  identifier: tty,
118
125
  tty
119
126
  };
@@ -145,7 +152,7 @@ export class TerminalFocusManager {
145
152
  const { stdout } = await execAsync(`osascript -e '${script}'`);
146
153
  if (stdout.trim() === "found") {
147
154
  return {
148
- type: 'terminal-app',
155
+ type: TerminalType.TERMINAL_APP,
149
156
  identifier: tty,
150
157
  tty
151
158
  };