@ai-devkit/agent-manager 0.26.2 → 0.26.4
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/AgentManager.d.ts +5 -2
- package/dist/AgentManager.d.ts.map +1 -1
- package/dist/AgentManager.js +28 -10
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +289 -4
- package/dist/__tests__/AgentManager.test.js.map +1 -1
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +13 -4
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CodexAdapter.test.js +13 -4
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/CopilotAdapter.test.js +7 -2
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js +18 -5
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GrokCliAdapter.test.js +7 -2
- package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js +13 -4
- package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/PiAdapter.test.js +32 -4
- package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -1
- package/dist/__tests__/utils/AgentRegistry.test.js +140 -0
- package/dist/__tests__/utils/AgentRegistry.test.js.map +1 -1
- package/dist/__tests__/utils/process.test.js +90 -2
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +9 -3
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +5 -4
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +11 -7
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +5 -4
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +11 -7
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/CopilotAdapter.d.ts +5 -4
- package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
- package/dist/adapters/CopilotAdapter.js +11 -7
- package/dist/adapters/CopilotAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +6 -5
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +12 -8
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/adapters/GrokCliAdapter.d.ts +3 -2
- package/dist/adapters/GrokCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GrokCliAdapter.js +10 -6
- package/dist/adapters/GrokCliAdapter.js.map +1 -1
- package/dist/adapters/OpenCodeAdapter.d.ts +5 -4
- package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/OpenCodeAdapter.js +11 -7
- package/dist/adapters/OpenCodeAdapter.js.map +1 -1
- package/dist/adapters/PiAdapter.d.ts +3 -2
- package/dist/adapters/PiAdapter.d.ts.map +1 -1
- package/dist/adapters/PiAdapter.js +12 -9
- package/dist/adapters/PiAdapter.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/database/connection.d.ts +1 -1
- package/dist/database/connection.d.ts.map +1 -1
- package/dist/database/connection.js +1 -1
- package/dist/database/connection.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/AgentRegistry.d.ts +14 -1
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +65 -20
- package/dist/utils/AgentRegistry.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/process.d.ts +9 -1
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +155 -10
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/AgentManager.ts +35 -8
- package/src/__tests__/AgentManager.test.ts +231 -4
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +16 -5
- package/src/__tests__/adapters/CodexAdapter.test.ts +16 -5
- package/src/__tests__/adapters/CopilotAdapter.test.ts +8 -1
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +20 -4
- package/src/__tests__/adapters/GrokCliAdapter.test.ts +8 -1
- package/src/__tests__/adapters/OpenCodeAdapter.test.ts +16 -5
- package/src/__tests__/adapters/PiAdapter.test.ts +28 -5
- package/src/__tests__/utils/AgentRegistry.test.ts +119 -0
- package/src/__tests__/utils/process.test.ts +72 -1
- package/src/adapters/AgentAdapter.ts +11 -3
- package/src/adapters/ClaudeCodeAdapter.ts +10 -7
- package/src/adapters/CodexAdapter.ts +10 -7
- package/src/adapters/CopilotAdapter.ts +16 -7
- package/src/adapters/GeminiCliAdapter.ts +17 -8
- package/src/adapters/GrokCliAdapter.ts +9 -6
- package/src/adapters/OpenCodeAdapter.ts +10 -7
- package/src/adapters/PiAdapter.ts +10 -9
- package/src/adapters/index.ts +1 -1
- package/src/database/connection.ts +4 -2
- package/src/index.ts +3 -1
- package/src/utils/AgentRegistry.ts +85 -17
- package/src/utils/index.ts +1 -1
- package/src/utils/process.ts +160 -10
package/dist/utils/process.js
CHANGED
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
* Process Detection Utilities
|
|
3
3
|
*
|
|
4
4
|
* Shared shell command wrappers for detecting and inspecting running processes.
|
|
5
|
-
*
|
|
5
|
+
* Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers
|
|
6
|
+
* remain exported for compatibility with existing consumers.
|
|
6
7
|
*/ import * as path from 'path';
|
|
7
|
-
import { execFileSync } from 'child_process';
|
|
8
|
+
import { execFile, execFileSync } from 'child_process';
|
|
9
|
+
const PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;
|
|
10
|
+
const VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
11
|
+
export function executableBasename(command) {
|
|
12
|
+
const executable = command.trim().split(/\s+/)[0] || '';
|
|
13
|
+
return path.basename(executable.replace(/\\/g, '/')).toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function normalizeExecutableName(name) {
|
|
16
|
+
const lower = name.toLowerCase();
|
|
17
|
+
return lower.endsWith('.exe') ? lower.slice(0, -4) : lower;
|
|
18
|
+
}
|
|
19
|
+
function normalizedProcessNames(namePatterns) {
|
|
20
|
+
return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));
|
|
21
|
+
}
|
|
22
|
+
export function filterByProcessNames(processes, namePatterns) {
|
|
23
|
+
const names = normalizedProcessNames(namePatterns);
|
|
24
|
+
if (names.size === 0) return [];
|
|
25
|
+
return processes.filter((process)=>names.has(normalizeExecutableName(executableBasename(process.command))));
|
|
26
|
+
}
|
|
8
27
|
/**
|
|
9
28
|
* List running processes matching an agent executable name.
|
|
10
29
|
*
|
|
@@ -15,7 +34,7 @@ import { execFileSync } from 'child_process';
|
|
|
15
34
|
* cwd and startTime are NOT populated — call enrichProcesses() to fill them.
|
|
16
35
|
*/ export function listAgentProcesses(namePattern) {
|
|
17
36
|
// Validate pattern contains only safe characters (alphanumeric, dash, underscore)
|
|
18
|
-
if (!namePattern ||
|
|
37
|
+
if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {
|
|
19
38
|
return [];
|
|
20
39
|
}
|
|
21
40
|
try {
|
|
@@ -25,7 +44,9 @@ import { execFileSync } from 'child_process';
|
|
|
25
44
|
], {
|
|
26
45
|
encoding: 'utf-8'
|
|
27
46
|
});
|
|
28
|
-
const
|
|
47
|
+
const names = normalizedProcessNames([
|
|
48
|
+
namePattern
|
|
49
|
+
]);
|
|
29
50
|
const processes = [];
|
|
30
51
|
for (const line of output.trim().split('\n')){
|
|
31
52
|
if (!line.trim()) continue;
|
|
@@ -36,12 +57,7 @@ import { execFileSync } from 'child_process';
|
|
|
36
57
|
if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;
|
|
37
58
|
const tty = match[3];
|
|
38
59
|
const command = match[4];
|
|
39
|
-
|
|
40
|
-
const executable = command.trim().split(/\s+/)[0] || '';
|
|
41
|
-
const base = path.basename(executable).toLowerCase();
|
|
42
|
-
if (base !== lowerPattern && base !== `${lowerPattern}.exe`) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
60
|
+
if (!names.has(normalizeExecutableName(executableBasename(command)))) continue;
|
|
45
61
|
const ttyShort = tty.startsWith('/dev/') ? tty.slice(5) : tty;
|
|
46
62
|
processes.push({
|
|
47
63
|
pid,
|
|
@@ -56,6 +72,135 @@ import { execFileSync } from 'child_process';
|
|
|
56
72
|
return [];
|
|
57
73
|
}
|
|
58
74
|
}
|
|
75
|
+
function execFileText(file, args) {
|
|
76
|
+
return new Promise((resolve, reject)=>{
|
|
77
|
+
execFile(file, args, {
|
|
78
|
+
encoding: 'utf-8',
|
|
79
|
+
maxBuffer: PROCESS_EXEC_MAX_BUFFER
|
|
80
|
+
}, (error, stdout)=>{
|
|
81
|
+
if (error) {
|
|
82
|
+
reject(error);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
resolve(stdout);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function parseProcessList(output, namePatterns) {
|
|
90
|
+
const processes = [];
|
|
91
|
+
for (const line of output.trim().split('\n')){
|
|
92
|
+
if (!line.trim()) continue;
|
|
93
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
94
|
+
if (!match) continue;
|
|
95
|
+
const pid = parseInt(match[1], 10);
|
|
96
|
+
const ppid = parseInt(match[2], 10);
|
|
97
|
+
if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;
|
|
98
|
+
const tty = match[3];
|
|
99
|
+
const command = match[4];
|
|
100
|
+
const base = executableBasename(command);
|
|
101
|
+
const normalizedBase = normalizeExecutableName(base);
|
|
102
|
+
if (!namePatterns.has(normalizedBase)) continue;
|
|
103
|
+
processes.push({
|
|
104
|
+
pid,
|
|
105
|
+
ppid,
|
|
106
|
+
command,
|
|
107
|
+
cwd: '',
|
|
108
|
+
tty: tty.startsWith('/dev/') ? tty.slice(5) : tty
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return processes;
|
|
112
|
+
}
|
|
113
|
+
async function batchGetProcessCwdsAsync(pids) {
|
|
114
|
+
const result = new Map();
|
|
115
|
+
if (pids.length === 0) return result;
|
|
116
|
+
try {
|
|
117
|
+
const output = await execFileText('lsof', [
|
|
118
|
+
'-a',
|
|
119
|
+
'-d',
|
|
120
|
+
'cwd',
|
|
121
|
+
'-Fn',
|
|
122
|
+
'-p',
|
|
123
|
+
pids.join(',')
|
|
124
|
+
]);
|
|
125
|
+
let currentPid = null;
|
|
126
|
+
for (const line of output.trim().split('\n')){
|
|
127
|
+
if (line.startsWith('p')) {
|
|
128
|
+
currentPid = parseInt(line.slice(1), 10);
|
|
129
|
+
} else if (line.startsWith('n') && currentPid !== null) {
|
|
130
|
+
result.set(currentPid, line.slice(1));
|
|
131
|
+
currentPid = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
} catch {
|
|
136
|
+
const entries = await Promise.all(pids.map(async (pid)=>{
|
|
137
|
+
try {
|
|
138
|
+
const output = await execFileText('pwdx', [
|
|
139
|
+
String(pid)
|
|
140
|
+
]);
|
|
141
|
+
const match = output.match(/^\d+:\s*(.+)$/);
|
|
142
|
+
return match ? [
|
|
143
|
+
pid,
|
|
144
|
+
match[1].trim()
|
|
145
|
+
] : null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}));
|
|
150
|
+
for (const entry of entries){
|
|
151
|
+
if (entry) result.set(entry[0], entry[1]);
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function batchGetProcessStartTimesAsync(pids) {
|
|
157
|
+
const result = new Map();
|
|
158
|
+
if (pids.length === 0) return result;
|
|
159
|
+
try {
|
|
160
|
+
const output = await execFileText('ps', [
|
|
161
|
+
'-o',
|
|
162
|
+
'pid=,lstart=',
|
|
163
|
+
'-p',
|
|
164
|
+
pids.join(',')
|
|
165
|
+
]);
|
|
166
|
+
for (const rawLine of output.split('\n')){
|
|
167
|
+
const match = rawLine.trim().match(/^(\d+)\s+(.+)$/);
|
|
168
|
+
if (!match) continue;
|
|
169
|
+
const pid = parseInt(match[1], 10);
|
|
170
|
+
const date = new Date(match[2].trim());
|
|
171
|
+
if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// Return partial/empty data, matching the synchronous helper.
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Capture and enrich relevant processes without blocking the event loop.
|
|
180
|
+
* One base process listing is shared by every requested executable name.
|
|
181
|
+
*/ export async function captureProcessSnapshot(namePatterns) {
|
|
182
|
+
const names = normalizedProcessNames(namePatterns.filter((name)=>Boolean(name) && VALID_EXECUTABLE_NAME.test(name)));
|
|
183
|
+
if (names.size === 0) return [];
|
|
184
|
+
try {
|
|
185
|
+
const output = await execFileText('ps', [
|
|
186
|
+
'-axo',
|
|
187
|
+
'pid=,ppid=,tty=,command='
|
|
188
|
+
]);
|
|
189
|
+
const processes = parseProcessList(output, names);
|
|
190
|
+
const pids = processes.map((process)=>process.pid);
|
|
191
|
+
const [cwdMap, startTimeMap] = await Promise.all([
|
|
192
|
+
batchGetProcessCwdsAsync(pids),
|
|
193
|
+
batchGetProcessStartTimesAsync(pids)
|
|
194
|
+
]);
|
|
195
|
+
return processes.map((process)=>({
|
|
196
|
+
...process,
|
|
197
|
+
cwd: cwdMap.get(process.pid) || '',
|
|
198
|
+
startTime: startTimeMap.get(process.pid)
|
|
199
|
+
}));
|
|
200
|
+
} catch {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
59
204
|
/**
|
|
60
205
|
* Batch-get current working directories for multiple PIDs.
|
|
61
206
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/process.ts"],"sourcesContent":["/**\n * Process Detection Utilities\n *\n * Shared shell command wrappers for detecting and inspecting running processes.\n * All execFileSync calls for process data live here — adapters must not call execFileSync directly.\n */\n\nimport * as path from 'path';\nimport { execFileSync } from 'child_process';\nimport type { ProcessInfo } from '../adapters/AgentAdapter.js';\n\n/**\n * List running processes matching an agent executable name.\n *\n * Uses `ps -axo` then filters in JS for exact executable basename match.\n * This avoids shell pipelines and string interpolation.\n *\n * Returned ProcessInfo has pid, ppid, command, tty populated.\n * cwd and startTime are NOT populated — call enrichProcesses() to fill them.\n */\nexport function listAgentProcesses(namePattern: string): ProcessInfo[] {\n // Validate pattern contains only safe characters (alphanumeric, dash, underscore)\n if (!namePattern || !/^[a-zA-Z0-9_-]+$/.test(namePattern)) {\n return [];\n }\n\n try {\n const output = execFileSync('ps', ['-axo', 'pid=,ppid=,tty=,command='], { encoding: 'utf-8' });\n\n const lowerPattern = namePattern.toLowerCase();\n const processes: ProcessInfo[] = [];\n\n for (const line of output.trim().split('\\n')) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n\n const tty = match[3];\n const command = match[4];\n\n // Check that the executable basename matches exactly\n const executable = command.trim().split(/\\s+/)[0] || '';\n const base = path.basename(executable).toLowerCase();\n if (base !== lowerPattern && base !== `${lowerPattern}.exe`) {\n continue;\n }\n\n const ttyShort = tty.startsWith('/dev/') ? tty.slice(5) : tty;\n\n processes.push({\n pid,\n ppid,\n command,\n cwd: '',\n tty: ttyShort,\n });\n }\n\n return processes;\n } catch {\n return [];\n }\n}\n\n/**\n * Batch-get current working directories for multiple PIDs.\n *\n * Single `lsof -a -d cwd -Fn -p PID1,PID2,...` call.\n * Returns partial results — if lsof fails for one PID, others still return.\n */\nexport function batchGetProcessCwds(pids: number[]): Map<number, string> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\n 'lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.join(',')],\n { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },\n );\n\n // lsof output format: p{PID}\\nn{path}\\np{PID}\\nn{path}...\n let currentPid: number | null = null;\n for (const line of output.trim().split('\\n')) {\n if (line.startsWith('p')) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith('n') && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n } catch {\n // Try per-PID fallback with pwdx (Linux)\n for (const pid of pids) {\n try {\n const output = execFileSync(\n 'pwdx', [String(pid)],\n { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },\n );\n const match = output.match(/^\\d+:\\s*(.+)$/);\n if (match) {\n result.set(pid, match[1].trim());\n }\n } catch {\n // Skip this PID\n }\n }\n }\n\n return result;\n}\n\n/**\n * Batch-get process start times for multiple PIDs.\n *\n * Single `ps -o pid=,lstart= -p PID1,PID2,...` call.\n * Uses lstart format which gives full timestamp (e.g., \"Thu Feb 5 16:00:57 2026\").\n * Returns partial results.\n */\nexport function batchGetProcessStartTimes(pids: number[]): Map<number, Date> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\n 'ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')],\n { encoding: 'utf-8' },\n );\n\n for (const rawLine of output.split('\\n')) {\n const line = rawLine.trim();\n if (!line) continue;\n\n // Format: \" PID DAY MON DD HH:MM:SS YYYY\"\n // e.g., \" 78070 Wed Mar 18 23:18:01 2026\"\n const match = line.match(/^\\s*(\\d+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const dateStr = match[2].trim();\n\n if (!Number.isFinite(pid)) continue;\n\n const date = new Date(dateStr);\n if (!Number.isNaN(date.getTime())) {\n result.set(pid, date);\n }\n }\n } catch {\n // Return whatever we have\n }\n\n return result;\n}\n\n/**\n * Enrich ProcessInfo array with cwd and startTime.\n *\n * Calls batchGetProcessCwds and batchGetProcessStartTimes in batched shell calls,\n * then populates each ProcessInfo in-place. Returns partial results —\n * if a PID fails, that process keeps empty cwd / undefined startTime.\n */\nexport function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {\n if (processes.length === 0) return processes;\n\n const pids = processes.map(p => p.pid);\n const cwdMap = batchGetProcessCwds(pids);\n const startTimeMap = batchGetProcessStartTimes(pids);\n\n for (const proc of processes) {\n proc.cwd = cwdMap.get(proc.pid) || '';\n proc.startTime = startTimeMap.get(proc.pid);\n }\n\n return processes;\n}\n\nfunction isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {\n const sameTty = proc.tty !== '' && proc.tty !== '?' && proc.tty === matched.tty;\n const sameCwd = proc.cwd !== '' && proc.cwd === matched.cwd;\n\n return sameTty && (sameCwd || proc.cwd === '' || matched.cwd === '');\n}\n\nfunction matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {\n return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);\n}\n\nexport function findWrapperProcess(\n processes: ProcessInfo[],\n child: ProcessInfo,\n): ProcessInfo | undefined {\n return processes.find((proc) => (\n proc.pid !== child.pid &&\n child.ppid === proc.pid &&\n matchesProcessIdentity(proc, child)\n ));\n}\n\n/**\n * Find parent wrapper processes that should not be reported as separate agents.\n *\n * A process is considered a wrapper when it is the parent of another candidate\n * agent process in the same terminal/worktree, or when it points at the same\n * terminal/worktree as an already session-matched process.\n */\nexport function findWrapperProcessPids(\n processes: ProcessInfo[],\n matchedProcesses: ProcessInfo[] = [],\n): Set<number> {\n const wrappers = new Set<number>();\n\n for (const child of processes) {\n const wrapper = findWrapperProcess(processes, child);\n if (wrapper) {\n wrappers.add(wrapper.pid);\n }\n }\n\n for (const proc of processes) {\n if (matchedProcesses.some((matched) => (\n proc.pid !== matched.pid && isSameTerminalProcess(proc, matched)\n ))) {\n wrappers.add(proc.pid);\n }\n }\n\n return wrappers;\n}\n\n/**\n * Get the TTY device for a specific process\n */\nexport function getProcessTty(pid: number): string {\n try {\n const output = execFileSync(\n 'ps', ['-p', String(pid), '-o', 'tty='],\n { encoding: 'utf-8' },\n );\n\n const tty = output.trim();\n return tty.startsWith('/dev/') ? tty.slice(5) : tty;\n } catch {\n return '?';\n }\n}\n"],"names":["path","execFileSync","listAgentProcesses","namePattern","test","output","encoding","lowerPattern","toLowerCase","processes","line","trim","split","match","pid","parseInt","ppid","Number","isNaN","tty","command","executable","base","basename","ttyShort","startsWith","slice","push","cwd","batchGetProcessCwds","pids","result","Map","length","join","stdio","currentPid","set","String","batchGetProcessStartTimes","rawLine","dateStr","isFinite","date","Date","getTime","enrichProcesses","map","p","cwdMap","startTimeMap","proc","get","startTime","isSameTerminalProcess","matched","sameTty","sameCwd","matchesProcessIdentity","findWrapperProcess","child","find","findWrapperProcessPids","matchedProcesses","wrappers","Set","wrapper","add","some","getProcessTty"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/process.ts"],"sourcesContent":["/**\n * Process Detection Utilities\n *\n * Shared shell command wrappers for detecting and inspecting running processes.\n * Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers\n * remain exported for compatibility with existing consumers.\n */\n\nimport * as path from 'path';\nimport { execFile, execFileSync } from 'child_process';\nimport type { ProcessInfo } from '../adapters/AgentAdapter.js';\n\nconst PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;\nconst VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;\n\nexport function executableBasename(command: string): string {\n const executable = command.trim().split(/\\s+/)[0] || '';\n return path.basename(executable.replace(/\\\\/g, '/')).toLowerCase();\n}\n\nfunction normalizeExecutableName(name: string): string {\n const lower = name.toLowerCase();\n return lower.endsWith('.exe') ? lower.slice(0, -4) : lower;\n}\n\nfunction normalizedProcessNames(namePatterns: readonly string[]): Set<string> {\n return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));\n}\n\nexport function filterByProcessNames(\n processes: readonly ProcessInfo[],\n namePatterns: readonly string[],\n): ProcessInfo[] {\n const names = normalizedProcessNames(namePatterns);\n if (names.size === 0) return [];\n return processes.filter((process) => (\n names.has(normalizeExecutableName(executableBasename(process.command)))\n ));\n}\n\n/**\n * List running processes matching an agent executable name.\n *\n * Uses `ps -axo` then filters in JS for exact executable basename match.\n * This avoids shell pipelines and string interpolation.\n *\n * Returned ProcessInfo has pid, ppid, command, tty populated.\n * cwd and startTime are NOT populated — call enrichProcesses() to fill them.\n */\nexport function listAgentProcesses(namePattern: string): ProcessInfo[] {\n // Validate pattern contains only safe characters (alphanumeric, dash, underscore)\n if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {\n return [];\n }\n\n try {\n const output = execFileSync('ps', ['-axo', 'pid=,ppid=,tty=,command='], { encoding: 'utf-8' });\n\n const names = normalizedProcessNames([namePattern]);\n const processes: ProcessInfo[] = [];\n\n for (const line of output.trim().split('\\n')) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n\n const tty = match[3];\n const command = match[4];\n\n if (!names.has(normalizeExecutableName(executableBasename(command)))) continue;\n\n const ttyShort = tty.startsWith('/dev/') ? tty.slice(5) : tty;\n\n processes.push({\n pid,\n ppid,\n command,\n cwd: '',\n tty: ttyShort,\n });\n }\n\n return processes;\n } catch {\n return [];\n }\n}\n\nfunction execFileText(\n file: string,\n args: readonly string[],\n): Promise<string> {\n return new Promise((resolve, reject) => {\n execFile(file, args, { encoding: 'utf-8', maxBuffer: PROCESS_EXEC_MAX_BUFFER }, (error, stdout) => {\n if (error) {\n reject(error);\n return;\n }\n resolve(stdout);\n });\n });\n}\n\nfunction parseProcessList(output: string, namePatterns: ReadonlySet<string>): ProcessInfo[] {\n const processes: ProcessInfo[] = [];\n\n for (const line of output.trim().split('\\n')) {\n if (!line.trim()) continue;\n\n const match = line.match(/^\\s*(\\d+)\\s+(\\d+)\\s+(\\S+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const ppid = parseInt(match[2], 10);\n if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;\n\n const tty = match[3];\n const command = match[4];\n const base = executableBasename(command);\n const normalizedBase = normalizeExecutableName(base);\n if (!namePatterns.has(normalizedBase)) continue;\n\n processes.push({\n pid,\n ppid,\n command,\n cwd: '',\n tty: tty.startsWith('/dev/') ? tty.slice(5) : tty,\n });\n }\n\n return processes;\n}\n\nasync function batchGetProcessCwdsAsync(pids: number[]): Promise<Map<number, string>> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = await execFileText('lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.join(',')]);\n let currentPid: number | null = null;\n for (const line of output.trim().split('\\n')) {\n if (line.startsWith('p')) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith('n') && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n return result;\n } catch {\n const entries = await Promise.all(pids.map(async (pid) => {\n try {\n const output = await execFileText('pwdx', [String(pid)]);\n const match = output.match(/^\\d+:\\s*(.+)$/);\n return match ? [pid, match[1].trim()] as const : null;\n } catch {\n return null;\n }\n }));\n for (const entry of entries) {\n if (entry) result.set(entry[0], entry[1]);\n }\n return result;\n }\n}\n\nasync function batchGetProcessStartTimesAsync(pids: number[]): Promise<Map<number, Date>> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = await execFileText('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')]);\n for (const rawLine of output.split('\\n')) {\n const match = rawLine.trim().match(/^(\\d+)\\s+(.+)$/);\n if (!match) continue;\n const pid = parseInt(match[1], 10);\n const date = new Date(match[2].trim());\n if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);\n }\n } catch {\n // Return partial/empty data, matching the synchronous helper.\n }\n return result;\n}\n\n/**\n * Capture and enrich relevant processes without blocking the event loop.\n * One base process listing is shared by every requested executable name.\n */\nexport async function captureProcessSnapshot(namePatterns: readonly string[]): Promise<ProcessInfo[]> {\n const names = normalizedProcessNames(\n namePatterns.filter((name) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name)),\n );\n if (names.size === 0) return [];\n\n try {\n const output = await execFileText('ps', ['-axo', 'pid=,ppid=,tty=,command=']);\n const processes = parseProcessList(output, names);\n const pids = processes.map((process) => process.pid);\n const [cwdMap, startTimeMap] = await Promise.all([\n batchGetProcessCwdsAsync(pids),\n batchGetProcessStartTimesAsync(pids),\n ]);\n return processes.map((process) => ({\n ...process,\n cwd: cwdMap.get(process.pid) || '',\n startTime: startTimeMap.get(process.pid),\n }));\n } catch {\n return [];\n }\n}\n\n/**\n * Batch-get current working directories for multiple PIDs.\n *\n * Single `lsof -a -d cwd -Fn -p PID1,PID2,...` call.\n * Returns partial results — if lsof fails for one PID, others still return.\n */\nexport function batchGetProcessCwds(pids: number[]): Map<number, string> {\n const result = new Map<number, string>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\n 'lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.join(',')],\n { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },\n );\n\n // lsof output format: p{PID}\\nn{path}\\np{PID}\\nn{path}...\n let currentPid: number | null = null;\n for (const line of output.trim().split('\\n')) {\n if (line.startsWith('p')) {\n currentPid = parseInt(line.slice(1), 10);\n } else if (line.startsWith('n') && currentPid !== null) {\n result.set(currentPid, line.slice(1));\n currentPid = null;\n }\n }\n } catch {\n // Try per-PID fallback with pwdx (Linux)\n for (const pid of pids) {\n try {\n const output = execFileSync(\n 'pwdx', [String(pid)],\n { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] },\n );\n const match = output.match(/^\\d+:\\s*(.+)$/);\n if (match) {\n result.set(pid, match[1].trim());\n }\n } catch {\n // Skip this PID\n }\n }\n }\n\n return result;\n}\n\n/**\n * Batch-get process start times for multiple PIDs.\n *\n * Single `ps -o pid=,lstart= -p PID1,PID2,...` call.\n * Uses lstart format which gives full timestamp (e.g., \"Thu Feb 5 16:00:57 2026\").\n * Returns partial results.\n */\nexport function batchGetProcessStartTimes(pids: number[]): Map<number, Date> {\n const result = new Map<number, Date>();\n if (pids.length === 0) return result;\n\n try {\n const output = execFileSync(\n 'ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')],\n { encoding: 'utf-8' },\n );\n\n for (const rawLine of output.split('\\n')) {\n const line = rawLine.trim();\n if (!line) continue;\n\n // Format: \" PID DAY MON DD HH:MM:SS YYYY\"\n // e.g., \" 78070 Wed Mar 18 23:18:01 2026\"\n const match = line.match(/^\\s*(\\d+)\\s+(.+)$/);\n if (!match) continue;\n\n const pid = parseInt(match[1], 10);\n const dateStr = match[2].trim();\n\n if (!Number.isFinite(pid)) continue;\n\n const date = new Date(dateStr);\n if (!Number.isNaN(date.getTime())) {\n result.set(pid, date);\n }\n }\n } catch {\n // Return whatever we have\n }\n\n return result;\n}\n\n/**\n * Enrich ProcessInfo array with cwd and startTime.\n *\n * Calls batchGetProcessCwds and batchGetProcessStartTimes in batched shell calls,\n * then populates each ProcessInfo in-place. Returns partial results —\n * if a PID fails, that process keeps empty cwd / undefined startTime.\n */\nexport function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {\n if (processes.length === 0) return processes;\n\n const pids = processes.map(p => p.pid);\n const cwdMap = batchGetProcessCwds(pids);\n const startTimeMap = batchGetProcessStartTimes(pids);\n\n for (const proc of processes) {\n proc.cwd = cwdMap.get(proc.pid) || '';\n proc.startTime = startTimeMap.get(proc.pid);\n }\n\n return processes;\n}\n\nfunction isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {\n const sameTty = proc.tty !== '' && proc.tty !== '?' && proc.tty === matched.tty;\n const sameCwd = proc.cwd !== '' && proc.cwd === matched.cwd;\n\n return sameTty && (sameCwd || proc.cwd === '' || matched.cwd === '');\n}\n\nfunction matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {\n return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);\n}\n\nexport function findWrapperProcess(\n processes: ProcessInfo[],\n child: ProcessInfo,\n): ProcessInfo | undefined {\n return processes.find((proc) => (\n proc.pid !== child.pid &&\n child.ppid === proc.pid &&\n matchesProcessIdentity(proc, child)\n ));\n}\n\n/**\n * Find parent wrapper processes that should not be reported as separate agents.\n *\n * A process is considered a wrapper when it is the parent of another candidate\n * agent process in the same terminal/worktree, or when it points at the same\n * terminal/worktree as an already session-matched process.\n */\nexport function findWrapperProcessPids(\n processes: ProcessInfo[],\n matchedProcesses: ProcessInfo[] = [],\n): Set<number> {\n const wrappers = new Set<number>();\n\n for (const child of processes) {\n const wrapper = findWrapperProcess(processes, child);\n if (wrapper) {\n wrappers.add(wrapper.pid);\n }\n }\n\n for (const proc of processes) {\n if (matchedProcesses.some((matched) => (\n proc.pid !== matched.pid && isSameTerminalProcess(proc, matched)\n ))) {\n wrappers.add(proc.pid);\n }\n }\n\n return wrappers;\n}\n\n/**\n * Get the TTY device for a specific process\n */\nexport function getProcessTty(pid: number): string {\n try {\n const output = execFileSync(\n 'ps', ['-p', String(pid), '-o', 'tty='],\n { encoding: 'utf-8' },\n );\n\n const tty = output.trim();\n return tty.startsWith('/dev/') ? tty.slice(5) : tty;\n } catch {\n return '?';\n }\n}\n"],"names":["path","execFile","execFileSync","PROCESS_EXEC_MAX_BUFFER","VALID_EXECUTABLE_NAME","executableBasename","command","executable","trim","split","basename","replace","toLowerCase","normalizeExecutableName","name","lower","endsWith","slice","normalizedProcessNames","namePatterns","Set","filter","Boolean","map","filterByProcessNames","processes","names","size","process","has","listAgentProcesses","namePattern","test","output","encoding","line","match","pid","parseInt","ppid","Number","isNaN","tty","ttyShort","startsWith","push","cwd","execFileText","file","args","Promise","resolve","reject","maxBuffer","error","stdout","parseProcessList","base","normalizedBase","batchGetProcessCwdsAsync","pids","result","Map","length","join","currentPid","set","entries","all","String","entry","batchGetProcessStartTimesAsync","rawLine","date","Date","isFinite","getTime","captureProcessSnapshot","cwdMap","startTimeMap","get","startTime","batchGetProcessCwds","stdio","batchGetProcessStartTimes","dateStr","enrichProcesses","p","proc","isSameTerminalProcess","matched","sameTty","sameCwd","matchesProcessIdentity","findWrapperProcess","child","find","findWrapperProcessPids","matchedProcesses","wrappers","wrapper","add","some","getProcessTty"],"mappings":"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"}
|
package/package.json
CHANGED
package/src/AgentManager.ts
CHANGED
|
@@ -10,9 +10,13 @@ import type {
|
|
|
10
10
|
AgentInfo,
|
|
11
11
|
SessionSummary,
|
|
12
12
|
ListSessionsOptions,
|
|
13
|
+
ProcessInfo,
|
|
13
14
|
} from './adapters/AgentAdapter.js';
|
|
14
15
|
import { sortAgents, type AgentSortKey } from './utils/sortAgents.js';
|
|
15
16
|
import { AgentRegistry, type RegistryEntry } from './utils/AgentRegistry.js';
|
|
17
|
+
import { captureProcessSnapshot, filterByProcessNames } from './utils/process.js';
|
|
18
|
+
|
|
19
|
+
type ProcessSnapshotCapture = (namePatterns: readonly string[]) => Promise<ProcessInfo[]>;
|
|
16
20
|
|
|
17
21
|
export interface ListAgentsOptions {
|
|
18
22
|
/**
|
|
@@ -41,7 +45,10 @@ export class AgentManager {
|
|
|
41
45
|
private adapters: Map<string, AgentAdapter> = new Map();
|
|
42
46
|
private registry: AgentRegistry;
|
|
43
47
|
|
|
44
|
-
constructor(
|
|
48
|
+
constructor(
|
|
49
|
+
registry: AgentRegistry = AgentRegistry.default(),
|
|
50
|
+
private readonly captureSnapshot: ProcessSnapshotCapture = captureProcessSnapshot,
|
|
51
|
+
) {
|
|
45
52
|
this.registry = registry;
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -126,10 +133,27 @@ export class AgentManager {
|
|
|
126
133
|
const allAgents: AgentInfo[] = [];
|
|
127
134
|
const errors: Array<{ type: string; error: Error }> = [];
|
|
128
135
|
|
|
129
|
-
|
|
130
|
-
const
|
|
136
|
+
const adapters = Array.from(this.adapters.values());
|
|
137
|
+
const processNames = Array.from(new Set(adapters.flatMap(
|
|
138
|
+
(adapter) => adapter.processNames ? [...adapter.processNames] : [],
|
|
139
|
+
)));
|
|
140
|
+
let processes: readonly ProcessInfo[] = [];
|
|
141
|
+
if (processNames.length > 0) {
|
|
142
|
+
try {
|
|
143
|
+
processes = await this.captureSnapshot(processNames);
|
|
144
|
+
} catch {
|
|
145
|
+
processes = [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Query all adapters in parallel using executable-scoped slices of the shared snapshot.
|
|
150
|
+
const adapterPromises = adapters.map(async (adapter) => {
|
|
131
151
|
try {
|
|
132
|
-
const agents =
|
|
152
|
+
const agents = adapter.processNames
|
|
153
|
+
? await adapter.detectAgents({
|
|
154
|
+
processes: filterByProcessNames(processes, adapter.processNames),
|
|
155
|
+
})
|
|
156
|
+
: await adapter.detectAgents();
|
|
133
157
|
return { type: adapter.type, agents, error: null };
|
|
134
158
|
} catch (error) {
|
|
135
159
|
// Capture error but don't throw - allow other adapters to continue
|
|
@@ -156,15 +180,18 @@ export class AgentManager {
|
|
|
156
180
|
});
|
|
157
181
|
}
|
|
158
182
|
|
|
159
|
-
const
|
|
183
|
+
const identityKey = (type: string, pid: number): string => `${type}:${pid}`;
|
|
184
|
+
const preExistingByIdentity = new Map(
|
|
185
|
+
this.registry.list().map((entry) => [identityKey(entry.type, entry.pid), entry]),
|
|
186
|
+
);
|
|
160
187
|
const entries = allAgents.map((agent) =>
|
|
161
|
-
this.toRegistryEntry(agent,
|
|
188
|
+
this.toRegistryEntry(agent, preExistingByIdentity.get(identityKey(agent.type, agent.pid))),
|
|
162
189
|
);
|
|
163
190
|
if (entries.length > 0) this.registry.registerBatch(entries);
|
|
164
|
-
this.registry.
|
|
191
|
+
this.registry.pruneIfDue();
|
|
165
192
|
|
|
166
193
|
for (const agent of allAgents) {
|
|
167
|
-
const entry =
|
|
194
|
+
const entry = preExistingByIdentity.get(identityKey(agent.type, agent.pid));
|
|
168
195
|
if (entry) {
|
|
169
196
|
agent.name = entry.name;
|
|
170
197
|
}
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
AgentType,
|
|
14
14
|
ConversationMessage,
|
|
15
15
|
SessionSummary,
|
|
16
|
+
ProcessInfo,
|
|
16
17
|
} from '../adapters/AgentAdapter.js';
|
|
17
18
|
import { AgentStatus } from '../adapters/AgentAdapter.js';
|
|
18
19
|
import { AgentRegistry, type RegistryEntry } from '../utils/AgentRegistry.js';
|
|
@@ -170,6 +171,94 @@ describe('AgentManager', () => {
|
|
|
170
171
|
});
|
|
171
172
|
|
|
172
173
|
describe('listAgents', () => {
|
|
174
|
+
it('shares one process capture while giving each adapter only its declared executables', async () => {
|
|
175
|
+
const processes: ProcessInfo[] = [
|
|
176
|
+
{ pid: 101, command: 'claude', cwd: '/claude', tty: 's001' },
|
|
177
|
+
{ pid: 202, command: 'node /bin/pi', cwd: '/pi', tty: 's002' },
|
|
178
|
+
];
|
|
179
|
+
const captureSnapshot = vi.fn(async () => processes);
|
|
180
|
+
const createSnapshotAdapter = (type: AgentType, processNames: string[]) => ({
|
|
181
|
+
type,
|
|
182
|
+
processNames,
|
|
183
|
+
detectAgents: vi.fn(async () => []),
|
|
184
|
+
canHandle: () => true,
|
|
185
|
+
getConversation: () => [],
|
|
186
|
+
listSessions: async () => [],
|
|
187
|
+
});
|
|
188
|
+
const claude = createSnapshotAdapter('claude', ['claude']);
|
|
189
|
+
const pi = createSnapshotAdapter('pi', ['pi', 'node']);
|
|
190
|
+
const snapshotManager = new AgentManager(
|
|
191
|
+
new AgentRegistry(path.join(tmpDir, 'snapshot-agents.json')),
|
|
192
|
+
captureSnapshot,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
snapshotManager.registerAdapter(claude as AgentAdapter);
|
|
196
|
+
snapshotManager.registerAdapter(pi as AgentAdapter);
|
|
197
|
+
|
|
198
|
+
await snapshotManager.listAgents();
|
|
199
|
+
|
|
200
|
+
expect(captureSnapshot).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(captureSnapshot).toHaveBeenCalledWith(['claude', 'pi', 'node']);
|
|
202
|
+
expect(claude.detectAgents).toHaveBeenCalledWith({ processes: [processes[0]] });
|
|
203
|
+
expect(pi.detectAgents).toHaveBeenCalledWith({ processes: [processes[1]] });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('does not expose foreign command arguments to broad Pi and Gemini matchers', async () => {
|
|
207
|
+
const processes: ProcessInfo[] = [
|
|
208
|
+
{ pid: 100, command: 'node /usr/local/lib/gemini.js', cwd: '/g', tty: 's001' },
|
|
209
|
+
{ pid: 200, command: 'codex exec --cd /Users/x/repos/gemini', cwd: '/c', tty: 's002' },
|
|
210
|
+
{ pid: 300, command: 'node /usr/local/lib/pi.js', cwd: '/p', tty: 's003' },
|
|
211
|
+
{ pid: 400, command: 'claude --resume /Users/x/pi/session.jsonl', cwd: '/a', tty: 's004' },
|
|
212
|
+
];
|
|
213
|
+
const captureSnapshot = vi.fn(async () => processes);
|
|
214
|
+
const createSnapshotAdapter = (type: AgentType, processNames: string[]) => ({
|
|
215
|
+
type,
|
|
216
|
+
processNames,
|
|
217
|
+
detectAgents: vi.fn(async () => []),
|
|
218
|
+
canHandle: () => true,
|
|
219
|
+
getConversation: () => [],
|
|
220
|
+
listSessions: async () => [],
|
|
221
|
+
});
|
|
222
|
+
const gemini = createSnapshotAdapter('gemini_cli', ['node']);
|
|
223
|
+
const codex = createSnapshotAdapter('codex', ['codex']);
|
|
224
|
+
const pi = createSnapshotAdapter('pi', ['pi', 'node']);
|
|
225
|
+
const claude = createSnapshotAdapter('claude', ['claude']);
|
|
226
|
+
const snapshotManager = new AgentManager(
|
|
227
|
+
new AgentRegistry(path.join(tmpDir, 'filtered-snapshot-agents.json')),
|
|
228
|
+
captureSnapshot,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
snapshotManager.registerAdapter(gemini as AgentAdapter);
|
|
232
|
+
snapshotManager.registerAdapter(codex as AgentAdapter);
|
|
233
|
+
snapshotManager.registerAdapter(pi as AgentAdapter);
|
|
234
|
+
snapshotManager.registerAdapter(claude as AgentAdapter);
|
|
235
|
+
|
|
236
|
+
await snapshotManager.listAgents();
|
|
237
|
+
|
|
238
|
+
expect(captureSnapshot).toHaveBeenCalledTimes(1);
|
|
239
|
+
expect(captureSnapshot).toHaveBeenCalledWith(['node', 'codex', 'pi', 'claude']);
|
|
240
|
+
expect(gemini.detectAgents).toHaveBeenCalledWith({ processes: [processes[0], processes[2]] });
|
|
241
|
+
expect(codex.detectAgents).toHaveBeenCalledWith({ processes: [processes[1]] });
|
|
242
|
+
expect(pi.detectAgents).toHaveBeenCalledWith({ processes: [processes[0], processes[2]] });
|
|
243
|
+
expect(claude.detectAgents).toHaveBeenCalledWith({ processes: [processes[3]] });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('does not pass a snapshot context to legacy adapters', async () => {
|
|
247
|
+
const captureSnapshot = vi.fn(async () => []);
|
|
248
|
+
const legacy = new MockAdapter('claude');
|
|
249
|
+
const detect = vi.spyOn(legacy, 'detectAgents');
|
|
250
|
+
const snapshotManager = new AgentManager(
|
|
251
|
+
new AgentRegistry(path.join(tmpDir, 'legacy-agents.json')),
|
|
252
|
+
captureSnapshot,
|
|
253
|
+
);
|
|
254
|
+
snapshotManager.registerAdapter(legacy);
|
|
255
|
+
|
|
256
|
+
await snapshotManager.listAgents();
|
|
257
|
+
|
|
258
|
+
expect(captureSnapshot).not.toHaveBeenCalled();
|
|
259
|
+
expect(detect).toHaveBeenCalledWith();
|
|
260
|
+
});
|
|
261
|
+
|
|
173
262
|
it('should return empty array when no adapters registered', async () => {
|
|
174
263
|
const agents = await manager.listAgents();
|
|
175
264
|
expect(agents).toEqual([]);
|
|
@@ -255,15 +344,25 @@ describe('AgentManager', () => {
|
|
|
255
344
|
let regPath: string;
|
|
256
345
|
let registry: AgentRegistry;
|
|
257
346
|
let scopedManager: AgentManager;
|
|
347
|
+
let nowMs: number;
|
|
348
|
+
let databaseOperations: string[];
|
|
258
349
|
|
|
259
350
|
beforeEach(() => {
|
|
260
351
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-manager-'));
|
|
261
352
|
regPath = path.join(tmpDir, 'agents.json');
|
|
262
|
-
|
|
353
|
+
nowMs = Date.parse('2026-08-14T10:00:00.000Z');
|
|
354
|
+
databaseOperations = [];
|
|
355
|
+
registry = new AgentRegistry(regPath, {
|
|
356
|
+
now: () => new Date(nowMs),
|
|
357
|
+
pruneIntervalMs: 30_000,
|
|
358
|
+
onDatabaseOperation: (sql) => databaseOperations.push(sql),
|
|
359
|
+
});
|
|
263
360
|
scopedManager = new AgentManager(registry);
|
|
361
|
+
databaseOperations = [];
|
|
264
362
|
});
|
|
265
363
|
|
|
266
364
|
afterEach(() => {
|
|
365
|
+
vi.restoreAllMocks();
|
|
267
366
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
268
367
|
});
|
|
269
368
|
|
|
@@ -340,6 +439,35 @@ describe('AgentManager', () => {
|
|
|
340
439
|
expect(registry.list()[0].startedAt).toBe('2026-05-30T00:00:00.000Z');
|
|
341
440
|
});
|
|
342
441
|
|
|
442
|
+
it('preserves custom name and tmux session across two EPERM refresh cycles', async () => {
|
|
443
|
+
registry.register({
|
|
444
|
+
name: 'merry',
|
|
445
|
+
type: 'claude',
|
|
446
|
+
pid: process.pid,
|
|
447
|
+
tmuxSession: 'merry-tmux',
|
|
448
|
+
cwd: '/cwd/merry',
|
|
449
|
+
startedAt: '2026-05-30T00:00:00.000Z',
|
|
450
|
+
sessionId: 'sid-merry',
|
|
451
|
+
sessionFilePath: '/path/merry.jsonl',
|
|
452
|
+
});
|
|
453
|
+
scopedManager.registerAdapter(new MockAdapter('claude', [
|
|
454
|
+
createMockAgent({ name: `ai-devkit-${process.pid}`, pid: process.pid }),
|
|
455
|
+
]));
|
|
456
|
+
vi.spyOn(process, 'kill').mockImplementation(() => {
|
|
457
|
+
throw Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const firstRefresh = await scopedManager.listAgents();
|
|
461
|
+
const secondRefresh = await scopedManager.listAgents();
|
|
462
|
+
|
|
463
|
+
expect(firstRefresh[0].name).toBe('merry');
|
|
464
|
+
expect(secondRefresh[0].name).toBe('merry');
|
|
465
|
+
expect(registry.lookup('merry')).toMatchObject({
|
|
466
|
+
name: 'merry',
|
|
467
|
+
tmuxSession: 'merry-tmux',
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
343
471
|
it('preserves a user-managed name when a fallback row was written later for the same pid', async () => {
|
|
344
472
|
registry.register({
|
|
345
473
|
name: 'agent-list-debug',
|
|
@@ -406,15 +534,114 @@ describe('AgentManager', () => {
|
|
|
406
534
|
.toEqual(['a', 'b']);
|
|
407
535
|
});
|
|
408
536
|
|
|
409
|
-
it('
|
|
537
|
+
it('performs zero database writes on an unchanged refresh', async () => {
|
|
538
|
+
const adapter = new MockAdapter('claude', [
|
|
539
|
+
createMockAgent({
|
|
540
|
+
name: 'stable',
|
|
541
|
+
pid: process.pid,
|
|
542
|
+
projectPath: '/cwd/stable',
|
|
543
|
+
sessionId: 'stable-session',
|
|
544
|
+
sessionFilePath: '/sessions/stable.jsonl',
|
|
545
|
+
}),
|
|
546
|
+
]);
|
|
547
|
+
scopedManager.registerAdapter(adapter);
|
|
548
|
+
await scopedManager.listAgents();
|
|
549
|
+
databaseOperations = [];
|
|
550
|
+
|
|
551
|
+
await scopedManager.listAgents();
|
|
552
|
+
|
|
553
|
+
const writes = databaseOperations.filter((sql) => /^\s*(BEGIN|COMMIT|INSERT|UPDATE|DELETE)/i.test(sql));
|
|
554
|
+
expect(writes).toEqual([]);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('persists changed fields once in one write transaction', async () => {
|
|
558
|
+
const adapter = new MockAdapter('claude', [
|
|
559
|
+
createMockAgent({ name: 'changing', pid: process.pid, projectPath: '/cwd/before' }),
|
|
560
|
+
]);
|
|
561
|
+
scopedManager.registerAdapter(adapter);
|
|
562
|
+
await scopedManager.listAgents();
|
|
563
|
+
databaseOperations = [];
|
|
564
|
+
nowMs += 1_000;
|
|
565
|
+
adapter.setAgents([
|
|
566
|
+
createMockAgent({ name: 'changing', pid: process.pid, projectPath: '/cwd/after' }),
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
await scopedManager.listAgents();
|
|
570
|
+
|
|
571
|
+
const upserts = databaseOperations.filter((sql) => /^\s*INSERT INTO agents/i.test(sql));
|
|
572
|
+
const transactions = databaseOperations.filter((sql) => /^\s*(BEGIN|COMMIT)/i.test(sql));
|
|
573
|
+
expect(upserts).toHaveLength(1);
|
|
574
|
+
expect(upserts[0]).toContain("'2026-08-14T10:00:01.000Z'");
|
|
575
|
+
expect(transactions).toHaveLength(2);
|
|
576
|
+
expect(registry.lookup('changing')?.cwd).toBe('/cwd/after');
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it('prunes newly dead entries only when the passive cadence is due', async () => {
|
|
580
|
+
registry.register({
|
|
581
|
+
name: 'cadenced',
|
|
582
|
+
type: 'claude',
|
|
583
|
+
pid: process.pid,
|
|
584
|
+
tmuxSession: '',
|
|
585
|
+
cwd: '/cwd/cadenced',
|
|
586
|
+
startedAt: '2026-05-30T00:00:00.000Z',
|
|
587
|
+
sessionId: 'sid-cadenced',
|
|
588
|
+
sessionFilePath: '',
|
|
589
|
+
});
|
|
590
|
+
const alive = vi.spyOn(registry, 'isAlive').mockReturnValue(true);
|
|
591
|
+
|
|
592
|
+
await scopedManager.listAgents();
|
|
593
|
+
expect(alive).toHaveBeenCalledTimes(1);
|
|
594
|
+
alive.mockReturnValue(false);
|
|
595
|
+
nowMs += 29_999;
|
|
596
|
+
|
|
597
|
+
await scopedManager.listAgents();
|
|
598
|
+
expect(alive).toHaveBeenCalledTimes(1);
|
|
599
|
+
expect(registry.lookup('cadenced')).not.toBeNull();
|
|
600
|
+
|
|
601
|
+
nowMs += 1;
|
|
602
|
+
await scopedManager.listAgents();
|
|
603
|
+
expect(alive).toHaveBeenCalledTimes(2);
|
|
604
|
+
expect(registry.lookup('cadenced')).toBeNull();
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('does not inherit a name when the same pid is reused by another agent type', async () => {
|
|
608
|
+
registry.register({
|
|
609
|
+
name: 'old-claude',
|
|
610
|
+
type: 'claude',
|
|
611
|
+
pid: process.pid,
|
|
612
|
+
tmuxSession: 'old-claude',
|
|
613
|
+
cwd: '/cwd/old',
|
|
614
|
+
startedAt: '2026-05-30T00:00:00.000Z',
|
|
615
|
+
sessionId: 'old-session',
|
|
616
|
+
sessionFilePath: '',
|
|
617
|
+
});
|
|
618
|
+
scopedManager.registerAdapter(new MockAdapter('codex', [
|
|
619
|
+
createMockAgent({
|
|
620
|
+
name: 'new-codex',
|
|
621
|
+
type: 'codex',
|
|
622
|
+
pid: process.pid,
|
|
623
|
+
projectPath: '/cwd/new',
|
|
624
|
+
sessionId: 'new-session',
|
|
625
|
+
}),
|
|
626
|
+
]));
|
|
627
|
+
|
|
628
|
+
const agents = await scopedManager.listAgents();
|
|
629
|
+
|
|
630
|
+
expect(agents[0].name).toBe('new-codex');
|
|
631
|
+
expect(registry.lookup('old-claude')).toBeNull();
|
|
632
|
+
expect(registry.lookup('new-codex')).toMatchObject({ type: 'codex', pid: process.pid });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('skips registerBatch when no agents are detected and prune is not due', async () => {
|
|
410
636
|
const writeSpy = vi.spyOn(registry, 'registerBatch');
|
|
411
|
-
const pruneSpy = vi.spyOn(registry, '
|
|
637
|
+
const pruneSpy = vi.spyOn(registry, 'pruneIfDue');
|
|
412
638
|
|
|
413
639
|
scopedManager.registerAdapter(new MockAdapter('claude', []));
|
|
414
640
|
await scopedManager.listAgents();
|
|
641
|
+
await scopedManager.listAgents();
|
|
415
642
|
|
|
416
643
|
expect(writeSpy).not.toHaveBeenCalled();
|
|
417
|
-
expect(pruneSpy).toHaveBeenCalledTimes(
|
|
644
|
+
expect(pruneSpy).toHaveBeenCalledTimes(2);
|
|
418
645
|
});
|
|
419
646
|
});
|
|
420
647
|
|