@ai-devkit/agent-manager 0.1.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 (57) hide show
  1. package/.eslintrc.json +31 -0
  2. package/dist/AgentManager.d.ts +104 -0
  3. package/dist/AgentManager.d.ts.map +1 -0
  4. package/dist/AgentManager.js +185 -0
  5. package/dist/AgentManager.js.map +1 -0
  6. package/dist/adapters/AgentAdapter.d.ts +76 -0
  7. package/dist/adapters/AgentAdapter.d.ts.map +1 -0
  8. package/dist/adapters/AgentAdapter.js +20 -0
  9. package/dist/adapters/AgentAdapter.js.map +1 -0
  10. package/dist/adapters/ClaudeCodeAdapter.d.ts +58 -0
  11. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -0
  12. package/dist/adapters/ClaudeCodeAdapter.js +274 -0
  13. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -0
  14. package/dist/adapters/index.d.ts +4 -0
  15. package/dist/adapters/index.d.ts.map +1 -0
  16. package/dist/adapters/index.js +8 -0
  17. package/dist/adapters/index.js.map +1 -0
  18. package/dist/index.d.ts +10 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +23 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/terminal/TerminalFocusManager.d.ts +22 -0
  23. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -0
  24. package/dist/terminal/TerminalFocusManager.js +196 -0
  25. package/dist/terminal/TerminalFocusManager.js.map +1 -0
  26. package/dist/terminal/index.d.ts +3 -0
  27. package/dist/terminal/index.d.ts.map +1 -0
  28. package/dist/terminal/index.js +6 -0
  29. package/dist/terminal/index.js.map +1 -0
  30. package/dist/utils/file.d.ts +52 -0
  31. package/dist/utils/file.d.ts.map +1 -0
  32. package/dist/utils/file.js +135 -0
  33. package/dist/utils/file.js.map +1 -0
  34. package/dist/utils/index.d.ts +4 -0
  35. package/dist/utils/index.d.ts.map +1 -0
  36. package/dist/utils/index.js +15 -0
  37. package/dist/utils/index.js.map +1 -0
  38. package/dist/utils/process.d.ts +61 -0
  39. package/dist/utils/process.d.ts.map +1 -0
  40. package/dist/utils/process.js +166 -0
  41. package/dist/utils/process.js.map +1 -0
  42. package/jest.config.js +21 -0
  43. package/package.json +42 -0
  44. package/project.json +29 -0
  45. package/src/AgentManager.ts +198 -0
  46. package/src/__tests__/AgentManager.test.ts +308 -0
  47. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +286 -0
  48. package/src/adapters/AgentAdapter.ts +94 -0
  49. package/src/adapters/ClaudeCodeAdapter.ts +344 -0
  50. package/src/adapters/index.ts +3 -0
  51. package/src/index.ts +12 -0
  52. package/src/terminal/TerminalFocusManager.ts +206 -0
  53. package/src/terminal/index.ts +2 -0
  54. package/src/utils/file.ts +100 -0
  55. package/src/utils/index.ts +3 -0
  56. package/src/utils/process.ts +184 -0
  57. package/tsconfig.json +17 -0
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Claude Code Adapter
3
+ *
4
+ * Detects running Claude Code agents by reading session files
5
+ * from ~/.claude/ directory and correlating with running processes.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import type { AgentAdapter, AgentInfo, ProcessInfo } from './AgentAdapter';
11
+ import { AgentStatus } from './AgentAdapter';
12
+ import { listProcesses } from '../utils/process';
13
+ import { readLastLines, readJsonLines, readJson } from '../utils/file';
14
+
15
+ /**
16
+ * Structure of ~/.claude/projects/{path}/sessions-index.json
17
+ */
18
+ interface SessionsIndex {
19
+ originalPath: string;
20
+ }
21
+
22
+ /**
23
+ * Entry in session JSONL file
24
+ */
25
+ interface SessionEntry {
26
+ type?: 'assistant' | 'user' | 'progress' | 'thinking' | 'system' | 'message' | 'text';
27
+ timestamp?: string;
28
+ slug?: string;
29
+ cwd?: string;
30
+ sessionId?: string;
31
+ message?: {
32
+ content?: Array<{
33
+ type?: string;
34
+ text?: string;
35
+ content?: string;
36
+ }>;
37
+ };
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ /**
42
+ * Entry in ~/.claude/history.jsonl
43
+ */
44
+ interface HistoryEntry {
45
+ display: string;
46
+ timestamp: number;
47
+ project: string;
48
+ sessionId: string;
49
+ }
50
+
51
+ /**
52
+ * Claude Code session information
53
+ */
54
+ interface ClaudeSession {
55
+ sessionId: string;
56
+ projectPath: string;
57
+ slug?: string;
58
+ sessionLogPath: string;
59
+ lastEntry?: SessionEntry;
60
+ lastActive?: Date;
61
+ }
62
+
63
+ /**
64
+ * Claude Code Adapter
65
+ *
66
+ * Detects Claude Code agents by:
67
+ * 1. Finding running claude processes
68
+ * 2. Reading session files from ~/.claude/projects/
69
+ * 3. Matching sessions to processes via CWD
70
+ * 4. Extracting status from session JSONL
71
+ * 5. Extracting summary from history.jsonl
72
+ */
73
+ export class ClaudeCodeAdapter implements AgentAdapter {
74
+ readonly type = 'claude' as const;
75
+
76
+ /** Threshold in minutes before considering a session idle */
77
+ private static readonly IDLE_THRESHOLD_MINUTES = 5;
78
+
79
+ private claudeDir: string;
80
+ private projectsDir: string;
81
+ private historyPath: string;
82
+
83
+ constructor() {
84
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
85
+ this.claudeDir = path.join(homeDir, '.claude');
86
+ this.projectsDir = path.join(this.claudeDir, 'projects');
87
+ this.historyPath = path.join(this.claudeDir, 'history.jsonl');
88
+ }
89
+
90
+ /**
91
+ * Check if this adapter can handle a given process
92
+ */
93
+ canHandle(processInfo: ProcessInfo): boolean {
94
+ return processInfo.command.toLowerCase().includes('claude');
95
+ }
96
+
97
+ /**
98
+ * Detect running Claude Code agents
99
+ */
100
+ async detectAgents(): Promise<AgentInfo[]> {
101
+ // 1. Find running claude processes
102
+ const claudeProcesses = listProcesses({ namePattern: 'claude' });
103
+
104
+ if (claudeProcesses.length === 0) {
105
+ return [];
106
+ }
107
+
108
+ // 2. Read all sessions
109
+ const sessions = this.readSessions();
110
+
111
+ // 3. Read history for summaries
112
+ const history = this.readHistory();
113
+
114
+ // 4. Group processes by CWD
115
+ const processesByCwd = new Map<string, ProcessInfo[]>();
116
+ for (const p of claudeProcesses) {
117
+ const list = processesByCwd.get(p.cwd) || [];
118
+ list.push(p);
119
+ processesByCwd.set(p.cwd, list);
120
+ }
121
+
122
+ // 5. Match sessions to processes
123
+ const agents: AgentInfo[] = [];
124
+
125
+ for (const [cwd, processes] of processesByCwd) {
126
+ // Find sessions for this project path
127
+ const projectSessions = sessions.filter(s => s.projectPath === cwd);
128
+
129
+ if (projectSessions.length === 0) {
130
+ continue;
131
+ }
132
+
133
+ // Sort sessions by last active time (newest first)
134
+ projectSessions.sort((a, b) => {
135
+ const timeA = a.lastActive?.getTime() || 0;
136
+ const timeB = b.lastActive?.getTime() || 0;
137
+ return timeB - timeA;
138
+ });
139
+
140
+ // Map processes to the most recent sessions
141
+ // If there are 2 processes, we take the 2 most recent sessions
142
+ const activeSessions = projectSessions.slice(0, processes.length);
143
+
144
+ for (let i = 0; i < activeSessions.length; i++) {
145
+ const session = activeSessions[i];
146
+ const process = processes[i]; // Assign process to session (arbitrary 1-to-1 mapping)
147
+
148
+ const historyEntry = [...history].reverse().find(
149
+ h => h.sessionId === session.sessionId
150
+ );
151
+ const summary = historyEntry?.display || 'Session started';
152
+ const status = this.determineStatus(session);
153
+ const agentName = this.generateAgentName(session, agents); // Pass currently built agents for collision checks
154
+
155
+ agents.push({
156
+ name: agentName,
157
+ type: this.type,
158
+ status,
159
+ summary,
160
+ pid: process.pid,
161
+ projectPath: session.projectPath,
162
+ sessionId: session.sessionId,
163
+ slug: session.slug,
164
+ lastActive: session.lastActive || new Date(),
165
+ });
166
+ }
167
+ }
168
+
169
+ return agents;
170
+ }
171
+
172
+ /**
173
+ * Read all Claude Code sessions
174
+ */
175
+ private readSessions(): ClaudeSession[] {
176
+ if (!fs.existsSync(this.projectsDir)) {
177
+ return [];
178
+ }
179
+
180
+ const sessions: ClaudeSession[] = [];
181
+ const projectDirs = fs.readdirSync(this.projectsDir);
182
+
183
+ for (const dirName of projectDirs) {
184
+ if (dirName.startsWith('.')) {
185
+ continue;
186
+ }
187
+
188
+ const projectDir = path.join(this.projectsDir, dirName);
189
+ if (!fs.statSync(projectDir).isDirectory()) {
190
+ continue;
191
+ }
192
+
193
+ // Read sessions-index.json to get original project path
194
+ const indexPath = path.join(projectDir, 'sessions-index.json');
195
+ if (!fs.existsSync(indexPath)) {
196
+ continue;
197
+ }
198
+
199
+ const sessionsIndex = readJson<SessionsIndex>(indexPath);
200
+ if (!sessionsIndex) {
201
+ console.error(`Failed to parse ${indexPath}`);
202
+ continue;
203
+ }
204
+
205
+ const sessionFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
206
+
207
+ for (const sessionFile of sessionFiles) {
208
+ const sessionId = sessionFile.replace('.jsonl', '');
209
+ const sessionLogPath = path.join(projectDir, sessionFile);
210
+
211
+ try {
212
+ const sessionData = this.readSessionLog(sessionLogPath);
213
+
214
+ sessions.push({
215
+ sessionId,
216
+ projectPath: sessionsIndex.originalPath,
217
+ slug: sessionData.slug,
218
+ sessionLogPath,
219
+ lastEntry: sessionData.lastEntry,
220
+ lastActive: sessionData.lastActive,
221
+ });
222
+ } catch (error) {
223
+ console.error(`Failed to read session ${sessionId}:`, error);
224
+ continue;
225
+ }
226
+ }
227
+ }
228
+
229
+ return sessions;
230
+ }
231
+
232
+ /**
233
+ * Read a session JSONL file
234
+ * Only reads last 100 lines for performance with large files
235
+ */
236
+ private readSessionLog(logPath: string): {
237
+ slug?: string;
238
+ lastEntry?: SessionEntry;
239
+ lastActive?: Date;
240
+ } {
241
+ const lines = readLastLines(logPath, 100);
242
+
243
+ let slug: string | undefined;
244
+ let lastEntry: SessionEntry | undefined;
245
+ let lastActive: Date | undefined;
246
+
247
+ for (const line of lines) {
248
+ try {
249
+ const entry: SessionEntry = JSON.parse(line);
250
+
251
+ if (entry.slug && !slug) {
252
+ slug = entry.slug;
253
+ }
254
+
255
+ lastEntry = entry;
256
+
257
+ if (entry.timestamp) {
258
+ lastActive = new Date(entry.timestamp);
259
+ }
260
+ } catch (error) {
261
+ continue;
262
+ }
263
+ }
264
+
265
+ return { slug, lastEntry, lastActive };
266
+ }
267
+
268
+ /**
269
+ * Read history.jsonl for user prompts
270
+ * Only reads last 100 lines for performance
271
+ */
272
+ private readHistory(): HistoryEntry[] {
273
+ return readJsonLines<HistoryEntry>(this.historyPath, 100);
274
+ }
275
+
276
+ /**
277
+ * Determine agent status from session entry
278
+ */
279
+ private determineStatus(session: ClaudeSession): AgentStatus {
280
+ if (!session.lastEntry) {
281
+ return AgentStatus.UNKNOWN;
282
+ }
283
+
284
+ const entryType = session.lastEntry.type;
285
+ const lastActive = session.lastActive || new Date(0);
286
+ const ageMinutes = (Date.now() - lastActive.getTime()) / 1000 / 60;
287
+
288
+ if (ageMinutes > ClaudeCodeAdapter.IDLE_THRESHOLD_MINUTES) {
289
+ return AgentStatus.IDLE;
290
+ }
291
+
292
+ if (entryType === 'user') {
293
+ // Check if user interrupted manually - this puts agent back in waiting state
294
+ const content = session.lastEntry.message?.content;
295
+ if (Array.isArray(content)) {
296
+ const isInterrupted = content.some(c =>
297
+ (c.type === 'text' && c.text?.includes('[Request interrupted')) ||
298
+ (c.type === 'tool_result' && c.content?.includes('[Request interrupted'))
299
+ );
300
+ if (isInterrupted) return AgentStatus.WAITING;
301
+ }
302
+ return AgentStatus.RUNNING;
303
+ }
304
+
305
+ if (entryType === 'progress' || entryType === 'thinking') {
306
+ return AgentStatus.RUNNING;
307
+ } else if (entryType === 'assistant') {
308
+ return AgentStatus.WAITING;
309
+ } else if (entryType === 'system') {
310
+ return AgentStatus.IDLE;
311
+ }
312
+
313
+ return AgentStatus.UNKNOWN;
314
+ }
315
+
316
+ /**
317
+ * Generate unique agent name
318
+ * Uses project basename, appends slug if multiple sessions for same project
319
+ */
320
+ private generateAgentName(session: ClaudeSession, existingAgents: AgentInfo[]): string {
321
+ const projectName = path.basename(session.projectPath);
322
+
323
+ const sameProjectAgents = existingAgents.filter(
324
+ a => a.projectPath === session.projectPath
325
+ );
326
+
327
+ if (sameProjectAgents.length === 0) {
328
+ return projectName;
329
+ }
330
+
331
+ // Multiple sessions for same project, append slug
332
+ if (session.slug) {
333
+ // Use first word of slug for brevity (with safety check for format)
334
+ const slugPart = session.slug.includes('-')
335
+ ? session.slug.split('-')[0]
336
+ : session.slug.slice(0, 8);
337
+ return `${projectName} (${slugPart})`;
338
+ }
339
+
340
+ // No slug available, use session ID prefix
341
+ return `${projectName} (${session.sessionId.slice(0, 8)})`;
342
+ }
343
+
344
+ }
@@ -0,0 +1,3 @@
1
+ export { ClaudeCodeAdapter } from './ClaudeCodeAdapter';
2
+ export { AgentStatus } from './AgentAdapter';
3
+ export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './AgentAdapter';
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ export { AgentManager } from './AgentManager';
2
+
3
+ export { ClaudeCodeAdapter } from './adapters/ClaudeCodeAdapter';
4
+ export { AgentStatus } from './adapters/AgentAdapter';
5
+ export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './adapters/AgentAdapter';
6
+
7
+ export { TerminalFocusManager } from './terminal/TerminalFocusManager';
8
+ export type { TerminalLocation } from './terminal/TerminalFocusManager';
9
+
10
+ export { listProcesses, getProcessCwd, getProcessTty, isProcessRunning, getProcessInfo } from './utils/process';
11
+ export type { ListProcessesOptions } from './utils/process';
12
+ export { readLastLines, readJsonLines, fileExists, readJson } from './utils/file';
@@ -0,0 +1,206 @@
1
+ import { exec, execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { getProcessTty } from '../utils/process';
4
+
5
+ const execAsync = promisify(exec);
6
+ const execFileAsync = promisify(execFile);
7
+
8
+ export interface TerminalLocation {
9
+ type: 'tmux' | 'iterm2' | 'terminal-app' | 'unknown';
10
+ identifier: string; // e.g., "session:window.pane" for tmux, or TTY for others
11
+ tty: string; // e.g., "/dev/ttys030"
12
+ }
13
+
14
+ export class TerminalFocusManager {
15
+ /**
16
+ * Find the terminal location (emulator info) for a given process ID
17
+ */
18
+ async findTerminal(pid: number): Promise<TerminalLocation | null> {
19
+ const ttyShort = getProcessTty(pid);
20
+
21
+ // If no TTY or invalid, we can't find the terminal
22
+ if (!ttyShort || ttyShort === '?') {
23
+ return null;
24
+ }
25
+
26
+ const fullTty = `/dev/${ttyShort}`;
27
+
28
+ // 1. Check tmux (most specific if running inside it)
29
+ const tmuxLocation = await this.findTmuxPane(fullTty);
30
+ if (tmuxLocation) return tmuxLocation;
31
+
32
+ // 2. Check iTerm2
33
+ const itermLocation = await this.findITerm2Session(fullTty);
34
+ if (itermLocation) return itermLocation;
35
+
36
+ // 3. Check Terminal.app
37
+ const terminalAppLocation = await this.findTerminalAppWindow(fullTty);
38
+ if (terminalAppLocation) return terminalAppLocation;
39
+
40
+ // 4. Fallback: we know the TTY but not the emulator wrapper
41
+ return {
42
+ type: 'unknown',
43
+ identifier: '',
44
+ tty: fullTty
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Focus the terminal identified by the location
50
+ */
51
+ async focusTerminal(location: TerminalLocation): Promise<boolean> {
52
+ try {
53
+ switch (location.type) {
54
+ case 'tmux':
55
+ return await this.focusTmuxPane(location.identifier);
56
+ case 'iterm2':
57
+ return await this.focusITerm2Session(location.tty);
58
+ case 'terminal-app':
59
+ return await this.focusTerminalAppWindow(location.tty);
60
+ default:
61
+ return false;
62
+ }
63
+ } catch (error) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ private async findTmuxPane(tty: string): Promise<TerminalLocation | null> {
69
+ try {
70
+ // List all panes with their TTYs and identifiers
71
+ // Format: /dev/ttys001|my-session:1.1
72
+ // using | as separator to handle spaces in session names
73
+ const { stdout } = await execAsync("tmux list-panes -a -F '#{pane_tty}|#{session_name}:#{window_index}.#{pane_index}'");
74
+
75
+ const lines = stdout.trim().split('\n');
76
+ for (const line of lines) {
77
+ if (!line.trim()) continue;
78
+ const [paneTty, identifier] = line.split('|');
79
+ if (paneTty === tty && identifier) {
80
+ return {
81
+ type: 'tmux',
82
+ identifier,
83
+ tty
84
+ };
85
+ }
86
+ }
87
+ } catch (error) {
88
+ // tmux might not be installed or running
89
+ }
90
+ return null;
91
+ }
92
+
93
+ private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {
94
+ try {
95
+ // Check if iTerm2 is running first to avoid launching it
96
+ const { stdout: isRunning } = await execAsync('pgrep -x iTerm2 || echo "no"');
97
+ if (isRunning.trim() === "no") return null;
98
+
99
+ const script = `
100
+ tell application "iTerm"
101
+ repeat with w in windows
102
+ repeat with t in tabs of w
103
+ repeat with s in sessions of t
104
+ if tty of s is "${tty}" then
105
+ return "found"
106
+ end if
107
+ end repeat
108
+ end repeat
109
+ end repeat
110
+ end tell
111
+ `;
112
+
113
+ const { stdout } = await execAsync(`osascript -e '${script}'`);
114
+ if (stdout.trim() === "found") {
115
+ return {
116
+ type: 'iterm2',
117
+ identifier: tty,
118
+ tty
119
+ };
120
+ }
121
+ } catch (error) {
122
+ // iTerm2 not found or script failed
123
+ }
124
+ return null;
125
+ }
126
+
127
+ private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {
128
+ try {
129
+ // Check if Terminal is running
130
+ const { stdout: isRunning } = await execAsync('pgrep -x Terminal || echo "no"');
131
+ if (isRunning.trim() === "no") return null;
132
+
133
+ const script = `
134
+ tell application "Terminal"
135
+ repeat with w in windows
136
+ repeat with t in tabs of w
137
+ if tty of t is "${tty}" then
138
+ return "found"
139
+ end if
140
+ end repeat
141
+ end repeat
142
+ end tell
143
+ `;
144
+
145
+ const { stdout } = await execAsync(`osascript -e '${script}'`);
146
+ if (stdout.trim() === "found") {
147
+ return {
148
+ type: 'terminal-app',
149
+ identifier: tty,
150
+ tty
151
+ };
152
+ }
153
+ } catch (error) {
154
+ // Terminal not found or script failed
155
+ }
156
+ return null;
157
+ }
158
+
159
+ private async focusTmuxPane(identifier: string): Promise<boolean> {
160
+ try {
161
+ await execFileAsync('tmux', ['switch-client', '-t', identifier]);
162
+ return true;
163
+ } catch (error) {
164
+ return false;
165
+ }
166
+ }
167
+
168
+ private async focusITerm2Session(tty: string): Promise<boolean> {
169
+ const script = `
170
+ tell application "iTerm"
171
+ activate
172
+ repeat with w in windows
173
+ repeat with t in tabs of w
174
+ repeat with s in sessions of t
175
+ if tty of s is "${tty}" then
176
+ select s
177
+ return "true"
178
+ end if
179
+ end repeat
180
+ end repeat
181
+ end repeat
182
+ end tell
183
+ `;
184
+ const { stdout } = await execAsync(`osascript -e '${script}'`);
185
+ return stdout.trim() === "true";
186
+ }
187
+
188
+ private async focusTerminalAppWindow(tty: string): Promise<boolean> {
189
+ const script = `
190
+ tell application "Terminal"
191
+ activate
192
+ repeat with w in windows
193
+ repeat with t in tabs of w
194
+ if tty of t is "${tty}" then
195
+ set index of w to 1
196
+ set selected tab of w to t
197
+ return "true"
198
+ end if
199
+ end repeat
200
+ end repeat
201
+ end tell
202
+ `;
203
+ const { stdout } = await execAsync(`osascript -e '${script}'`);
204
+ return stdout.trim() === "true";
205
+ }
206
+ }
@@ -0,0 +1,2 @@
1
+ export { TerminalFocusManager } from './TerminalFocusManager';
2
+ export type { TerminalLocation } from './TerminalFocusManager';
@@ -0,0 +1,100 @@
1
+ /**
2
+ * File Utilities
3
+ *
4
+ * Helper functions for reading files efficiently
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+
9
+ /**
10
+ * Read last N lines from a file efficiently
11
+ *
12
+ * @param filePath Path to the file
13
+ * @param lineCount Number of lines to read from the end (default: 100)
14
+ * @returns Array of lines
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const lastLines = readLastLines('/path/to/log.txt', 50);
19
+ * ```
20
+ */
21
+ export function readLastLines(filePath: string, lineCount: number = 100): string[] {
22
+ if (!fs.existsSync(filePath)) {
23
+ return [];
24
+ }
25
+
26
+ try {
27
+ const content = fs.readFileSync(filePath, 'utf-8');
28
+ const allLines = content.trim().split('\n');
29
+
30
+ // Return last N lines (or all if file has fewer lines)
31
+ return allLines.slice(-lineCount);
32
+ } catch (error) {
33
+ console.error(`Failed to read ${filePath}:`, error);
34
+ return [];
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Read a JSONL (JSON Lines) file and parse each line
40
+ *
41
+ * @param filePath Path to the JSONL file
42
+ * @param maxLines Maximum number of lines to read from end (default: 1000)
43
+ * @returns Array of parsed objects
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const entries = readJsonLines<MyType>('/path/to/data.jsonl');
48
+ * const recent = readJsonLines<MyType>('/path/to/data.jsonl', 100);
49
+ * ```
50
+ */
51
+ export function readJsonLines<T = unknown>(filePath: string, maxLines: number = 1000): T[] {
52
+ const lines = readLastLines(filePath, maxLines);
53
+
54
+ return lines.map(line => {
55
+ try {
56
+ return JSON.parse(line) as T;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }).filter((entry): entry is T => entry !== null);
61
+ }
62
+
63
+ /**
64
+ * Check if a file exists
65
+ *
66
+ * @param filePath Path to check
67
+ * @returns True if file exists
68
+ */
69
+ export function fileExists(filePath: string): boolean {
70
+ try {
71
+ return fs.existsSync(filePath);
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Read a JSON file safely
79
+ *
80
+ * @param filePath Path to JSON file
81
+ * @returns Parsed JSON object or null if error
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const config = readJson<ConfigType>('/path/to/config.json');
86
+ * ```
87
+ */
88
+ export function readJson<T = unknown>(filePath: string): T | null {
89
+ if (!fs.existsSync(filePath)) {
90
+ return null;
91
+ }
92
+
93
+ try {
94
+ const content = fs.readFileSync(filePath, 'utf-8');
95
+ return JSON.parse(content) as T;
96
+ } catch (error) {
97
+ console.error(`Failed to parse JSON from ${filePath}:`, error);
98
+ return null;
99
+ }
100
+ }
@@ -0,0 +1,3 @@
1
+ export { listProcesses, getProcessCwd, getProcessTty, isProcessRunning, getProcessInfo } from './process';
2
+ export type { ListProcessesOptions } from './process';
3
+ export { readLastLines, readJsonLines, fileExists, readJson } from './file';