@ai-devkit/agent-manager 0.3.0 → 0.5.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.
- package/dist/adapters/AgentAdapter.d.ts +2 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +49 -38
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +286 -293
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +32 -30
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +148 -284
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +6 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +12 -11
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.d.ts +39 -0
- package/dist/utils/matching.d.ts.map +1 -0
- package/dist/utils/matching.js +103 -0
- package/dist/utils/matching.js.map +1 -0
- package/dist/utils/process.d.ts +25 -40
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +151 -105
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.d.ts +30 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +101 -0
- package/dist/utils/session.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/AgentManager.test.ts +0 -25
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +921 -205
- package/src/__tests__/adapters/CodexAdapter.test.ts +468 -269
- package/src/__tests__/utils/matching.test.ts +191 -0
- package/src/__tests__/utils/process.test.ts +202 -0
- package/src/__tests__/utils/session.test.ts +117 -0
- package/src/adapters/AgentAdapter.ts +3 -0
- package/src/adapters/ClaudeCodeAdapter.ts +341 -418
- package/src/adapters/CodexAdapter.ts +155 -420
- package/src/index.ts +1 -3
- package/src/utils/index.ts +6 -3
- package/src/utils/matching.ts +92 -0
- package/src/utils/process.ts +133 -119
- package/src/utils/session.ts +92 -0
- package/dist/utils/file.d.ts +0 -52
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -135
- package/dist/utils/file.js.map +0 -1
- package/src/utils/file.ts +0 -100
package/src/index.ts
CHANGED
|
@@ -9,6 +9,4 @@ export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusMana
|
|
|
9
9
|
export type { TerminalLocation } from './terminal/TerminalFocusManager';
|
|
10
10
|
export { TtyWriter } from './terminal/TtyWriter';
|
|
11
11
|
|
|
12
|
-
export {
|
|
13
|
-
export type { ListProcessesOptions } from './utils/process';
|
|
14
|
-
export { readLastLines, readJsonLines, fileExists, readJson } from './utils/file';
|
|
12
|
+
export { getProcessTty } from './utils/process';
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
3
|
-
export {
|
|
1
|
+
export { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses } from './process';
|
|
2
|
+
export { getProcessTty } from './process';
|
|
3
|
+
export { batchGetSessionFileBirthtimes } from './session';
|
|
4
|
+
export type { SessionFile } from './session';
|
|
5
|
+
export { matchProcessesToSessions, generateAgentName } from './matching';
|
|
6
|
+
export type { MatchResult } from './matching';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Matching Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared 1:1 greedy matching algorithm that pairs running processes with session files
|
|
5
|
+
* based on CWD and birth-time proximity to process start time.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import type { ProcessInfo } from '../adapters/AgentAdapter';
|
|
10
|
+
import type { SessionFile } from './session';
|
|
11
|
+
|
|
12
|
+
/** Maximum allowed delta between process start time and session file birth time. */
|
|
13
|
+
const TOLERANCE_MS = 3 * 60 * 1000; // 3 minutes
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of matching a process to a session file.
|
|
17
|
+
*/
|
|
18
|
+
export interface MatchResult {
|
|
19
|
+
/** The matched process */
|
|
20
|
+
process: ProcessInfo;
|
|
21
|
+
|
|
22
|
+
/** The matched session file */
|
|
23
|
+
session: SessionFile;
|
|
24
|
+
|
|
25
|
+
/** Absolute time delta in ms between process start and session birth time */
|
|
26
|
+
deltaMs: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Match processes to session files using 1:1 greedy assignment.
|
|
31
|
+
*
|
|
32
|
+
* Algorithm:
|
|
33
|
+
* 1. Exclude processes without startTime (they become process-only fallback).
|
|
34
|
+
* 2. Build candidate pairs where process.cwd === session.resolvedCwd
|
|
35
|
+
* and |process.startTime - session.birthtimeMs| <= 3 minutes.
|
|
36
|
+
* 3. Sort candidates by deltaMs ascending (best matches first).
|
|
37
|
+
* 4. Greedily assign: once a process or session is matched, skip it.
|
|
38
|
+
*
|
|
39
|
+
* Adapters must set session.resolvedCwd before calling this function.
|
|
40
|
+
*/
|
|
41
|
+
export function matchProcessesToSessions(
|
|
42
|
+
processes: ProcessInfo[],
|
|
43
|
+
sessions: SessionFile[],
|
|
44
|
+
): MatchResult[] {
|
|
45
|
+
// Build all candidate pairs
|
|
46
|
+
const candidates: Array<{ process: ProcessInfo; session: SessionFile; deltaMs: number }> = [];
|
|
47
|
+
|
|
48
|
+
for (const proc of processes) {
|
|
49
|
+
if (!proc.startTime || !proc.cwd) continue;
|
|
50
|
+
|
|
51
|
+
const processStartMs = proc.startTime.getTime();
|
|
52
|
+
|
|
53
|
+
for (const session of sessions) {
|
|
54
|
+
if (!session.resolvedCwd) continue;
|
|
55
|
+
if (proc.cwd !== session.resolvedCwd) continue;
|
|
56
|
+
|
|
57
|
+
const deltaMs = Math.abs(processStartMs - session.birthtimeMs);
|
|
58
|
+
if (deltaMs > TOLERANCE_MS) continue;
|
|
59
|
+
|
|
60
|
+
candidates.push({ process: proc, session, deltaMs });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Sort by smallest delta first
|
|
65
|
+
candidates.sort((a, b) => a.deltaMs - b.deltaMs);
|
|
66
|
+
|
|
67
|
+
// Greedy 1:1 assignment
|
|
68
|
+
const matchedPids = new Set<number>();
|
|
69
|
+
const matchedSessionIds = new Set<string>();
|
|
70
|
+
const results: MatchResult[] = [];
|
|
71
|
+
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
if (matchedPids.has(candidate.process.pid)) continue;
|
|
74
|
+
if (matchedSessionIds.has(candidate.session.sessionId)) continue;
|
|
75
|
+
|
|
76
|
+
matchedPids.add(candidate.process.pid);
|
|
77
|
+
matchedSessionIds.add(candidate.session.sessionId);
|
|
78
|
+
results.push(candidate);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate a deterministic agent name from CWD and PID.
|
|
86
|
+
*
|
|
87
|
+
* Format: "folderName (pid)"
|
|
88
|
+
*/
|
|
89
|
+
export function generateAgentName(cwd: string, pid: number): string {
|
|
90
|
+
const folderName = path.basename(cwd) || 'unknown';
|
|
91
|
+
return `${folderName} (${pid})`;
|
|
92
|
+
}
|
package/src/utils/process.ts
CHANGED
|
@@ -1,145 +1,187 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Process Detection Utilities
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Shared shell command wrappers for detecting and inspecting running processes.
|
|
5
|
+
* All execSync calls for process data live here — adapters must not call execSync directly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import * as path from 'path';
|
|
8
9
|
import { execSync } from 'child_process';
|
|
9
10
|
import type { ProcessInfo } from '../adapters/AgentAdapter';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
+
* List running processes matching an agent executable name.
|
|
14
|
+
*
|
|
15
|
+
* Uses `ps aux | grep <pattern>` at shell level for performance, then post-filters
|
|
16
|
+
* by checking that the executable basename matches exactly (avoids matching
|
|
17
|
+
* `claude-helper`, `vscode-claude-extension`, or the grep process itself).
|
|
18
|
+
*
|
|
19
|
+
* Returned ProcessInfo has pid, command, tty populated.
|
|
20
|
+
* cwd and startTime are NOT populated — call enrichProcesses() to fill them.
|
|
13
21
|
*/
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
namePattern
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
pids?: number[];
|
|
20
|
-
}
|
|
22
|
+
export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
23
|
+
// Validate pattern contains only safe characters (alphanumeric, dash, underscore)
|
|
24
|
+
if (!namePattern || !/^[a-zA-Z0-9_-]+$/.test(namePattern)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
21
27
|
|
|
22
|
-
/**
|
|
23
|
-
* List running processes on the system
|
|
24
|
-
*
|
|
25
|
-
* @param options Filtering options
|
|
26
|
-
* @returns Array of process information
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```typescript
|
|
30
|
-
* // List all Claude Code processes
|
|
31
|
-
* const processes = listProcesses({ namePattern: 'claude' });
|
|
32
|
-
*
|
|
33
|
-
* // Get specific process info
|
|
34
|
-
* const process = listProcesses({ pids: [12345] });
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export function listProcesses(options: ListProcessesOptions = {}): ProcessInfo[] {
|
|
38
28
|
try {
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
const psOutput = execSync('ps aux', { encoding: 'utf-8' });
|
|
29
|
+
// Use [c]laude trick to avoid matching the grep process itself
|
|
30
|
+
const escapedPattern = `[${namePattern[0]}]${namePattern.slice(1)}`;
|
|
42
31
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
const output = execSync(
|
|
33
|
+
`ps aux | grep -i '${escapedPattern}'`,
|
|
34
|
+
{ encoding: 'utf-8' },
|
|
35
|
+
);
|
|
46
36
|
|
|
47
37
|
const processes: ProcessInfo[] = [];
|
|
48
38
|
|
|
49
|
-
for (const line of
|
|
50
|
-
|
|
51
|
-
// Format: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
|
|
52
|
-
const parts = line.trim().split(/\s+/);
|
|
39
|
+
for (const line of output.trim().split('\n')) {
|
|
40
|
+
if (!line.trim()) continue;
|
|
53
41
|
|
|
42
|
+
const parts = line.trim().split(/\s+/);
|
|
54
43
|
if (parts.length < 11) continue;
|
|
55
44
|
|
|
56
45
|
const pid = parseInt(parts[1], 10);
|
|
57
|
-
if (isNaN(pid)) continue;
|
|
46
|
+
if (Number.isNaN(pid)) continue;
|
|
58
47
|
|
|
59
48
|
const tty = parts[6];
|
|
60
49
|
const command = parts.slice(10).join(' ');
|
|
61
50
|
|
|
62
|
-
//
|
|
63
|
-
|
|
51
|
+
// Post-filter: check that the executable basename matches exactly
|
|
52
|
+
const executable = command.trim().split(/\s+/)[0] || '';
|
|
53
|
+
const base = path.basename(executable).toLowerCase();
|
|
54
|
+
if (base !== namePattern.toLowerCase() && base !== `${namePattern.toLowerCase()}.exe`) {
|
|
64
55
|
continue;
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
// Apply name pattern filter (case-insensitive)
|
|
68
|
-
if (options.namePattern) {
|
|
69
|
-
const pattern = options.namePattern.toLowerCase();
|
|
70
|
-
const commandLower = command.toLowerCase();
|
|
71
|
-
if (!commandLower.includes(pattern)) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Get working directory for this process
|
|
77
|
-
const cwd = getProcessCwd(pid);
|
|
78
|
-
|
|
79
|
-
// Get TTY in short format (remove /dev/ prefix if present)
|
|
80
58
|
const ttyShort = tty.startsWith('/dev/') ? tty.slice(5) : tty;
|
|
81
59
|
|
|
82
60
|
processes.push({
|
|
83
61
|
pid,
|
|
84
62
|
command,
|
|
85
|
-
cwd,
|
|
63
|
+
cwd: '',
|
|
86
64
|
tty: ttyShort,
|
|
87
65
|
});
|
|
88
66
|
}
|
|
89
67
|
|
|
90
68
|
return processes;
|
|
91
|
-
} catch
|
|
92
|
-
// If ps command fails, return empty array
|
|
93
|
-
console.error('Failed to list processes:', error);
|
|
69
|
+
} catch {
|
|
94
70
|
return [];
|
|
95
71
|
}
|
|
96
72
|
}
|
|
97
73
|
|
|
98
74
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
75
|
+
* Batch-get current working directories for multiple PIDs.
|
|
76
|
+
*
|
|
77
|
+
* Single `lsof -a -d cwd -Fn -p PID1,PID2,...` call.
|
|
78
|
+
* Returns partial results — if lsof fails for one PID, others still return.
|
|
103
79
|
*/
|
|
104
|
-
export function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// -a: AND the selections, -d cwd: get cwd only, -Fn: output format (file names only)
|
|
108
|
-
const output = execSync(`lsof -a -p ${pid} -d cwd -Fn 2>/dev/null`, {
|
|
109
|
-
encoding: 'utf-8',
|
|
110
|
-
});
|
|
80
|
+
export function batchGetProcessCwds(pids: number[]): Map<number, string> {
|
|
81
|
+
const result = new Map<number, string>();
|
|
82
|
+
if (pids.length === 0) return result;
|
|
111
83
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
84
|
+
try {
|
|
85
|
+
const output = execSync(
|
|
86
|
+
`lsof -a -d cwd -Fn -p ${pids.join(',')} 2>/dev/null`,
|
|
87
|
+
{ encoding: 'utf-8' },
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// lsof output format: p{PID}\nn{path}\np{PID}\nn{path}...
|
|
91
|
+
let currentPid: number | null = null;
|
|
92
|
+
for (const line of output.trim().split('\n')) {
|
|
93
|
+
if (line.startsWith('p')) {
|
|
94
|
+
currentPid = parseInt(line.slice(1), 10);
|
|
95
|
+
} else if (line.startsWith('n') && currentPid !== null) {
|
|
96
|
+
result.set(currentPid, line.slice(1));
|
|
97
|
+
currentPid = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Try per-PID fallback with pwdx (Linux)
|
|
102
|
+
for (const pid of pids) {
|
|
103
|
+
try {
|
|
104
|
+
const output = execSync(`pwdx ${pid} 2>/dev/null`, { encoding: 'utf-8' });
|
|
105
|
+
const match = output.match(/^\d+:\s*(.+)$/);
|
|
106
|
+
if (match) {
|
|
107
|
+
result.set(pid, match[1].trim());
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Skip this PID
|
|
118
111
|
}
|
|
119
112
|
}
|
|
113
|
+
}
|
|
120
114
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Batch-get process start times for multiple PIDs.
|
|
120
|
+
*
|
|
121
|
+
* Single `ps -o pid=,lstart= -p PID1,PID2,...` call.
|
|
122
|
+
* Uses lstart format which gives full timestamp (e.g., "Thu Feb 5 16:00:57 2026").
|
|
123
|
+
* Returns partial results.
|
|
124
|
+
*/
|
|
125
|
+
export function batchGetProcessStartTimes(pids: number[]): Map<number, Date> {
|
|
126
|
+
const result = new Map<number, Date>();
|
|
127
|
+
if (pids.length === 0) return result;
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const output = execSync(
|
|
131
|
+
`ps -o pid=,lstart= -p ${pids.join(',')}`,
|
|
132
|
+
{ encoding: 'utf-8' },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
for (const rawLine of output.split('\n')) {
|
|
136
|
+
const line = rawLine.trim();
|
|
137
|
+
if (!line) continue;
|
|
138
|
+
|
|
139
|
+
// Format: " PID DAY MON DD HH:MM:SS YYYY"
|
|
140
|
+
// e.g., " 78070 Wed Mar 18 23:18:01 2026"
|
|
141
|
+
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
142
|
+
if (!match) continue;
|
|
143
|
+
|
|
144
|
+
const pid = parseInt(match[1], 10);
|
|
145
|
+
const dateStr = match[2].trim();
|
|
146
|
+
|
|
147
|
+
if (!Number.isFinite(pid)) continue;
|
|
148
|
+
|
|
149
|
+
const date = new Date(dateStr);
|
|
150
|
+
if (!Number.isNaN(date.getTime())) {
|
|
151
|
+
result.set(pid, date);
|
|
152
|
+
}
|
|
134
153
|
}
|
|
154
|
+
} catch {
|
|
155
|
+
// Return whatever we have
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Enrich ProcessInfo array with cwd and startTime.
|
|
163
|
+
*
|
|
164
|
+
* Calls batchGetProcessCwds and batchGetProcessStartTimes in batched shell calls,
|
|
165
|
+
* then populates each ProcessInfo in-place. Returns partial results —
|
|
166
|
+
* if a PID fails, that process keeps empty cwd / undefined startTime.
|
|
167
|
+
*/
|
|
168
|
+
export function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {
|
|
169
|
+
if (processes.length === 0) return processes;
|
|
170
|
+
|
|
171
|
+
const pids = processes.map(p => p.pid);
|
|
172
|
+
const cwdMap = batchGetProcessCwds(pids);
|
|
173
|
+
const startTimeMap = batchGetProcessStartTimes(pids);
|
|
174
|
+
|
|
175
|
+
for (const proc of processes) {
|
|
176
|
+
proc.cwd = cwdMap.get(proc.pid) || '';
|
|
177
|
+
proc.startTime = startTimeMap.get(proc.pid);
|
|
135
178
|
}
|
|
179
|
+
|
|
180
|
+
return processes;
|
|
136
181
|
}
|
|
137
182
|
|
|
138
183
|
/**
|
|
139
184
|
* Get the TTY device for a specific process
|
|
140
|
-
*
|
|
141
|
-
* @param pid Process ID
|
|
142
|
-
* @returns TTY device name (e.g., "ttys030"), or "?" if unavailable
|
|
143
185
|
*/
|
|
144
186
|
export function getProcessTty(pid: number): string {
|
|
145
187
|
try {
|
|
@@ -148,37 +190,9 @@ export function getProcessTty(pid: number): string {
|
|
|
148
190
|
});
|
|
149
191
|
|
|
150
192
|
const tty = output.trim();
|
|
151
|
-
// Remove /dev/ prefix if present
|
|
152
193
|
return tty.startsWith('/dev/') ? tty.slice(5) : tty;
|
|
153
|
-
} catch (error) {
|
|
154
|
-
return '?';
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Check if a process with the given PID is running
|
|
160
|
-
*
|
|
161
|
-
* @param pid Process ID
|
|
162
|
-
* @returns True if process is running
|
|
163
|
-
*/
|
|
164
|
-
export function isProcessRunning(pid: number): boolean {
|
|
165
|
-
try {
|
|
166
|
-
// Send signal 0 to check if process exists
|
|
167
|
-
// This doesn't actually send a signal, just checks if we can
|
|
168
|
-
execSync(`kill -0 ${pid} 2>/dev/null`);
|
|
169
|
-
return true;
|
|
170
194
|
} catch {
|
|
171
|
-
return
|
|
195
|
+
return '?';
|
|
172
196
|
}
|
|
173
197
|
}
|
|
174
198
|
|
|
175
|
-
/**
|
|
176
|
-
* Get detailed information for a specific process
|
|
177
|
-
*
|
|
178
|
-
* @param pid Process ID
|
|
179
|
-
* @returns Process information, or null if process not found
|
|
180
|
-
*/
|
|
181
|
-
export function getProcessInfo(pid: number): ProcessInfo | null {
|
|
182
|
-
const processes = listProcesses({ pids: [pid] });
|
|
183
|
-
return processes.length > 0 ? processes[0] : null;
|
|
184
|
-
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session File Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shell command wrappers for discovering session files and their birth times.
|
|
5
|
+
* Uses `stat` to get exact epoch-second birth timestamps without reading file contents.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Represents a session file with its birth time metadata.
|
|
13
|
+
*/
|
|
14
|
+
export interface SessionFile {
|
|
15
|
+
/** Session identifier (filename without .jsonl extension) */
|
|
16
|
+
sessionId: string;
|
|
17
|
+
|
|
18
|
+
/** Full path to the session file */
|
|
19
|
+
filePath: string;
|
|
20
|
+
|
|
21
|
+
/** Parent directory of the session file */
|
|
22
|
+
projectDir: string;
|
|
23
|
+
|
|
24
|
+
/** File creation time in milliseconds since epoch */
|
|
25
|
+
birthtimeMs: number;
|
|
26
|
+
|
|
27
|
+
/** CWD this session maps to — set by the adapter after calling batchGetSessionFileBirthtimes() */
|
|
28
|
+
resolvedCwd: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get birth times for .jsonl session files across multiple directories in a single shell call.
|
|
33
|
+
*
|
|
34
|
+
* Combines all directory globs into one `stat` command to avoid per-directory exec overhead.
|
|
35
|
+
* Returns empty array if no directories have .jsonl files or command fails.
|
|
36
|
+
* resolvedCwd is left empty — the adapter must set it.
|
|
37
|
+
*/
|
|
38
|
+
export function batchGetSessionFileBirthtimes(dirs: string[]): SessionFile[] {
|
|
39
|
+
if (dirs.length === 0) return [];
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const isMacOS = process.platform === 'darwin';
|
|
43
|
+
const globs = dirs.map((d) => `"${d}"/*.jsonl`).join(' ');
|
|
44
|
+
// || true prevents non-zero exit when some globs have no .jsonl matches
|
|
45
|
+
const command = isMacOS
|
|
46
|
+
? `stat -f '%B %N' ${globs} 2>/dev/null || true`
|
|
47
|
+
: `stat --format='%W %n' ${globs} 2>/dev/null || true`;
|
|
48
|
+
|
|
49
|
+
const output = execSync(command, { encoding: 'utf-8' });
|
|
50
|
+
|
|
51
|
+
return parseStatOutput(output);
|
|
52
|
+
} catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse stat output lines into SessionFile entries.
|
|
59
|
+
*/
|
|
60
|
+
function parseStatOutput(output: string): SessionFile[] {
|
|
61
|
+
const results: SessionFile[] = [];
|
|
62
|
+
|
|
63
|
+
for (const rawLine of output.trim().split('\n')) {
|
|
64
|
+
const line = rawLine.trim();
|
|
65
|
+
if (!line) continue;
|
|
66
|
+
|
|
67
|
+
// Format: "<epoch_seconds> <filepath>"
|
|
68
|
+
const spaceIdx = line.indexOf(' ');
|
|
69
|
+
if (spaceIdx === -1) continue;
|
|
70
|
+
|
|
71
|
+
const epochStr = line.slice(0, spaceIdx);
|
|
72
|
+
const filePath = line.slice(spaceIdx + 1).trim();
|
|
73
|
+
|
|
74
|
+
const epochSeconds = parseInt(epochStr, 10);
|
|
75
|
+
if (!Number.isFinite(epochSeconds) || epochSeconds <= 0) continue;
|
|
76
|
+
|
|
77
|
+
const fileName = path.basename(filePath);
|
|
78
|
+
if (!fileName.endsWith('.jsonl')) continue;
|
|
79
|
+
|
|
80
|
+
const sessionId = fileName.replace(/\.jsonl$/, '');
|
|
81
|
+
|
|
82
|
+
results.push({
|
|
83
|
+
sessionId,
|
|
84
|
+
filePath,
|
|
85
|
+
projectDir: path.dirname(filePath),
|
|
86
|
+
birthtimeMs: epochSeconds * 1000,
|
|
87
|
+
resolvedCwd: '',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return results;
|
|
92
|
+
}
|
package/dist/utils/file.d.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File Utilities
|
|
3
|
-
*
|
|
4
|
-
* Helper functions for reading files efficiently
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Read last N lines from a file efficiently
|
|
8
|
-
*
|
|
9
|
-
* @param filePath Path to the file
|
|
10
|
-
* @param lineCount Number of lines to read from the end (default: 100)
|
|
11
|
-
* @returns Array of lines
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* const lastLines = readLastLines('/path/to/log.txt', 50);
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
export declare function readLastLines(filePath: string, lineCount?: number): string[];
|
|
19
|
-
/**
|
|
20
|
-
* Read a JSONL (JSON Lines) file and parse each line
|
|
21
|
-
*
|
|
22
|
-
* @param filePath Path to the JSONL file
|
|
23
|
-
* @param maxLines Maximum number of lines to read from end (default: 1000)
|
|
24
|
-
* @returns Array of parsed objects
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```typescript
|
|
28
|
-
* const entries = readJsonLines<MyType>('/path/to/data.jsonl');
|
|
29
|
-
* const recent = readJsonLines<MyType>('/path/to/data.jsonl', 100);
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export declare function readJsonLines<T = unknown>(filePath: string, maxLines?: number): T[];
|
|
33
|
-
/**
|
|
34
|
-
* Check if a file exists
|
|
35
|
-
*
|
|
36
|
-
* @param filePath Path to check
|
|
37
|
-
* @returns True if file exists
|
|
38
|
-
*/
|
|
39
|
-
export declare function fileExists(filePath: string): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Read a JSON file safely
|
|
42
|
-
*
|
|
43
|
-
* @param filePath Path to JSON file
|
|
44
|
-
* @returns Parsed JSON object or null if error
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```typescript
|
|
48
|
-
* const config = readJson<ConfigType>('/path/to/config.json');
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
export declare function readJson<T = unknown>(filePath: string): T | null;
|
|
52
|
-
//# sourceMappingURL=file.d.ts.map
|
package/dist/utils/file.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/utils/file.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,GAAE,MAAY,GAAG,MAAM,EAAE,CAejF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAa,GAAG,CAAC,EAAE,CAUzF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAYhE"}
|