@ai-devkit/agent-manager 0.26.1 → 0.26.3
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 +21 -4
- package/dist/AgentManager.js.map +1 -1
- package/dist/__tests__/AgentManager.test.js +184 -0
- 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 +68 -46
- 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 +22 -0
- package/dist/database/connection.d.ts.map +1 -0
- package/dist/database/connection.js +58 -0
- package/dist/database/connection.js.map +1 -0
- package/dist/database/index.d.ts +4 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +4 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/migrations/001_initial.sql +12 -0
- package/dist/database/schema.d.ts +4 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/database/schema.js +47 -0
- package/dist/database/schema.js.map +1 -0
- package/dist/index.d.ts +2 -1
- 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 +7 -3
- package/dist/utils/AgentRegistry.d.ts.map +1 -1
- package/dist/utils/AgentRegistry.js +108 -57
- 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 +2 -2
- package/src/AgentManager.ts +28 -4
- package/src/__tests__/AgentManager.test.ts +126 -0
- 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 +48 -40
- 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 +74 -0
- package/src/database/index.ts +7 -0
- package/src/database/migrations/001_initial.sql +12 -0
- package/src/database/schema.ts +62 -0
- package/src/index.ts +2 -0
- package/src/utils/AgentRegistry.ts +109 -49
- package/src/utils/index.ts +1 -1
- package/src/utils/process.ts +160 -10
|
@@ -10,7 +10,7 @@ import * as path from 'path';
|
|
|
10
10
|
import { CopilotAdapter } from '../../adapters/CopilotAdapter.js';
|
|
11
11
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
13
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
13
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
14
14
|
import { generateAgentName } from '../../utils/matching.js';
|
|
15
15
|
import { AgentRegistry } from '../../utils/AgentRegistry.js';
|
|
16
16
|
|
|
@@ -20,6 +20,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
|
20
20
|
...actual,
|
|
21
21
|
listAgentProcesses: vi.fn(),
|
|
22
22
|
enrichProcesses: vi.fn(),
|
|
23
|
+
captureProcessSnapshot: vi.fn(),
|
|
23
24
|
};
|
|
24
25
|
});
|
|
25
26
|
|
|
@@ -29,6 +30,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
29
30
|
|
|
30
31
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
31
32
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
33
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
32
34
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
33
35
|
|
|
34
36
|
describe('CopilotAdapter', () => {
|
|
@@ -45,8 +47,13 @@ describe('CopilotAdapter', () => {
|
|
|
45
47
|
|
|
46
48
|
mockedListAgentProcesses.mockReset();
|
|
47
49
|
mockedEnrichProcesses.mockReset();
|
|
50
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
48
51
|
mockedGenerateAgentName.mockReset();
|
|
49
52
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
53
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
54
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
55
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
56
|
+
));
|
|
50
57
|
mockedGenerateAgentName.mockImplementation((cwd, pid) => `${path.basename(cwd) || 'unknown'} (${pid})`);
|
|
51
58
|
});
|
|
52
59
|
|
|
@@ -11,7 +11,7 @@ import { GeminiCliAdapter } from '../../adapters/GeminiCliAdapter.js';
|
|
|
11
11
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
13
13
|
import { AgentRegistry, type RegistryEntry } from '../../utils/AgentRegistry.js';
|
|
14
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
14
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
15
15
|
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
16
16
|
import * as crypto from 'crypto';
|
|
17
17
|
|
|
@@ -21,6 +21,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
|
21
21
|
...actual,
|
|
22
22
|
listAgentProcesses: vi.fn(),
|
|
23
23
|
enrichProcesses: vi.fn(),
|
|
24
|
+
captureProcessSnapshot: vi.fn(),
|
|
24
25
|
};
|
|
25
26
|
});
|
|
26
27
|
|
|
@@ -31,6 +32,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
31
32
|
|
|
32
33
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
33
34
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
35
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
34
36
|
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
35
37
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
36
38
|
|
|
@@ -45,10 +47,15 @@ describe('GeminiCliAdapter', () => {
|
|
|
45
47
|
adapter = new GeminiCliAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
|
|
46
48
|
mockedListAgentProcesses.mockReset();
|
|
47
49
|
mockedEnrichProcesses.mockReset();
|
|
50
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
48
51
|
mockedMatchProcessesToSessions.mockReset();
|
|
49
52
|
mockedGenerateAgentName.mockReset();
|
|
50
53
|
|
|
51
54
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
55
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
56
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
57
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
58
|
+
));
|
|
52
59
|
mockedMatchProcessesToSessions.mockReturnValue([]);
|
|
53
60
|
mockedGenerateAgentName.mockImplementation((cwd: string, pid: number) => {
|
|
54
61
|
const folder = path.basename(cwd) || 'unknown';
|
|
@@ -110,6 +117,15 @@ describe('GeminiCliAdapter', () => {
|
|
|
110
117
|
tty: 'ttys006',
|
|
111
118
|
})).toBe(true);
|
|
112
119
|
});
|
|
120
|
+
|
|
121
|
+
it('should recognize Windows executable and entrypoint paths', () => {
|
|
122
|
+
expect(adapter.canHandle({
|
|
123
|
+
pid: 7,
|
|
124
|
+
command: 'C:\\tools\\node.exe C:\\lib\\gemini.js',
|
|
125
|
+
cwd: 'C:\\repo',
|
|
126
|
+
tty: '',
|
|
127
|
+
})).toBe(true);
|
|
128
|
+
});
|
|
113
129
|
});
|
|
114
130
|
|
|
115
131
|
describe('detectAgents', () => {
|
|
@@ -144,7 +160,7 @@ describe('GeminiCliAdapter', () => {
|
|
|
144
160
|
it('should return process-only agents when no session files exist for the process', async () => {
|
|
145
161
|
const proc: ProcessInfo = {
|
|
146
162
|
pid: 1234,
|
|
147
|
-
command: 'gemini',
|
|
163
|
+
command: 'node /usr/local/bin/gemini',
|
|
148
164
|
cwd: '/repo',
|
|
149
165
|
tty: 'ttys001',
|
|
150
166
|
startTime: new Date('2026-04-18T00:00:00Z'),
|
|
@@ -257,7 +273,7 @@ describe('GeminiCliAdapter', () => {
|
|
|
257
273
|
|
|
258
274
|
const proc: ProcessInfo = {
|
|
259
275
|
pid: 42,
|
|
260
|
-
command: 'gemini',
|
|
276
|
+
command: 'node /usr/local/bin/gemini',
|
|
261
277
|
cwd,
|
|
262
278
|
tty: 'ttys001',
|
|
263
279
|
startTime: new Date('2026-04-18T00:00:00Z'),
|
|
@@ -383,7 +399,7 @@ describe('GeminiCliAdapter', () => {
|
|
|
383
399
|
|
|
384
400
|
const proc: ProcessInfo = {
|
|
385
401
|
pid: 7,
|
|
386
|
-
command: 'gemini',
|
|
402
|
+
command: 'node /usr/local/bin/gemini',
|
|
387
403
|
cwd: procCwd,
|
|
388
404
|
tty: 'ttys001',
|
|
389
405
|
startTime: new Date(),
|
|
@@ -13,7 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import { GrokCliAdapter } from '../../adapters/GrokCliAdapter.js';
|
|
14
14
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
15
15
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
16
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
16
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
17
17
|
import { generateAgentName } from '../../utils/matching.js';
|
|
18
18
|
|
|
19
19
|
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
@@ -22,6 +22,7 @@ vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
|
22
22
|
...actual,
|
|
23
23
|
listAgentProcesses: vi.fn(),
|
|
24
24
|
enrichProcesses: vi.fn(),
|
|
25
|
+
captureProcessSnapshot: vi.fn(),
|
|
25
26
|
};
|
|
26
27
|
});
|
|
27
28
|
|
|
@@ -35,6 +36,7 @@ vi.mock('../../utils/matching.js', async (importOriginal) => {
|
|
|
35
36
|
|
|
36
37
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
37
38
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
39
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
38
40
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
39
41
|
|
|
40
42
|
const SESSION_ID = '019f16c3-5d5d-7dc3-85d1-bc629416ca2d';
|
|
@@ -64,9 +66,14 @@ describe('GrokCliAdapter', () => {
|
|
|
64
66
|
|
|
65
67
|
mockedListAgentProcesses.mockReset();
|
|
66
68
|
mockedEnrichProcesses.mockReset();
|
|
69
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
67
70
|
mockedGenerateAgentName.mockReset();
|
|
68
71
|
|
|
69
72
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
73
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
74
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
75
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
76
|
+
));
|
|
70
77
|
mockedGenerateAgentName.mockImplementation((c: string, pid: number) => `${path.basename(c) || 'unknown'}-${pid}`);
|
|
71
78
|
});
|
|
72
79
|
|
|
@@ -9,14 +9,19 @@ import * as path from 'path';
|
|
|
9
9
|
import { OpenCodeAdapter } from '../../adapters/OpenCodeAdapter.js';
|
|
10
10
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
11
11
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
12
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
12
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
13
13
|
import { generateAgentName } from '../../utils/matching.js';
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
|
|
16
|
-
vi.mock('../../utils/process.js', () =>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal() as typeof import('../../utils/process.js');
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
listAgentProcesses: vi.fn(),
|
|
21
|
+
enrichProcesses: vi.fn(),
|
|
22
|
+
captureProcessSnapshot: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
20
25
|
|
|
21
26
|
vi.mock('../../utils/matching.js', () => ({
|
|
22
27
|
generateAgentName: vi.fn(),
|
|
@@ -25,6 +30,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
25
30
|
|
|
26
31
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
27
32
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
33
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
28
34
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
29
35
|
|
|
30
36
|
function makeDb(queries: {
|
|
@@ -104,9 +110,14 @@ describe('OpenCodeAdapter', () => {
|
|
|
104
110
|
|
|
105
111
|
mockedListAgentProcesses.mockReset();
|
|
106
112
|
mockedEnrichProcesses.mockReset();
|
|
113
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
107
114
|
mockedGenerateAgentName.mockReset();
|
|
108
115
|
|
|
109
116
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
117
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
118
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
119
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
120
|
+
));
|
|
110
121
|
mockedGenerateAgentName.mockImplementation((cwd, pid) => {
|
|
111
122
|
const folder = path.basename(cwd) || 'unknown';
|
|
112
123
|
return `${folder}-${pid}`;
|
|
@@ -11,13 +11,18 @@ import { PiAdapter } from '../../adapters/PiAdapter.js';
|
|
|
11
11
|
import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
|
|
12
12
|
import { AgentStatus } from '../../adapters/AgentAdapter.js';
|
|
13
13
|
import { AgentRegistry } from '../../utils/AgentRegistry.js';
|
|
14
|
-
import { listAgentProcesses, enrichProcesses } from '../../utils/process.js';
|
|
14
|
+
import { listAgentProcesses, enrichProcesses, captureProcessSnapshot } from '../../utils/process.js';
|
|
15
15
|
import { matchProcessesToSessions, generateAgentName } from '../../utils/matching.js';
|
|
16
16
|
|
|
17
|
-
vi.mock('../../utils/process.js', () =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
vi.mock('../../utils/process.js', async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal() as typeof import('../../utils/process.js');
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
listAgentProcesses: vi.fn(),
|
|
22
|
+
enrichProcesses: vi.fn(),
|
|
23
|
+
captureProcessSnapshot: vi.fn(),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
21
26
|
|
|
22
27
|
vi.mock('../../utils/matching.js', () => ({
|
|
23
28
|
matchProcessesToSessions: vi.fn(),
|
|
@@ -26,6 +31,7 @@ vi.mock('../../utils/matching.js', () => ({
|
|
|
26
31
|
|
|
27
32
|
const mockedListAgentProcesses = listAgentProcesses as MockedFunction<typeof listAgentProcesses>;
|
|
28
33
|
const mockedEnrichProcesses = enrichProcesses as MockedFunction<typeof enrichProcesses>;
|
|
34
|
+
const mockedCaptureProcessSnapshot = captureProcessSnapshot as MockedFunction<typeof captureProcessSnapshot>;
|
|
29
35
|
const mockedMatchProcessesToSessions = matchProcessesToSessions as MockedFunction<typeof matchProcessesToSessions>;
|
|
30
36
|
const mockedGenerateAgentName = generateAgentName as MockedFunction<typeof generateAgentName>;
|
|
31
37
|
|
|
@@ -43,10 +49,15 @@ describe('PiAdapter', () => {
|
|
|
43
49
|
adapter = new PiAdapter(new AgentRegistry(path.join(tmpHome, 'agents.json')));
|
|
44
50
|
mockedListAgentProcesses.mockReset();
|
|
45
51
|
mockedEnrichProcesses.mockReset();
|
|
52
|
+
mockedCaptureProcessSnapshot.mockReset();
|
|
46
53
|
mockedMatchProcessesToSessions.mockReset();
|
|
47
54
|
mockedGenerateAgentName.mockReset();
|
|
48
55
|
|
|
49
56
|
mockedEnrichProcesses.mockImplementation((procs) => procs);
|
|
57
|
+
// Compatibility shim for standalone adapter discovery; the manager captures once and slices by name.
|
|
58
|
+
mockedCaptureProcessSnapshot.mockImplementation(async (names) => (
|
|
59
|
+
enrichProcesses(names.flatMap((name) => listAgentProcesses(name)))
|
|
60
|
+
));
|
|
50
61
|
mockedMatchProcessesToSessions.mockReturnValue([]);
|
|
51
62
|
mockedGenerateAgentName.mockImplementation((cwd: string, pid: number) => {
|
|
52
63
|
const folder = path.basename(cwd) || 'unknown';
|
|
@@ -67,6 +78,18 @@ describe('PiAdapter', () => {
|
|
|
67
78
|
expect(adapter.canHandle({ pid: 2, command: '/usr/local/bin/PI --model x', cwd: '/repo', tty: 'ttys002' })).toBe(true);
|
|
68
79
|
expect(adapter.canHandle({ pid: 3, command: 'node /opt/pi/bin/pi.js', cwd: '/repo', tty: 'ttys003' })).toBe(true);
|
|
69
80
|
expect(adapter.canHandle({ pid: 4, command: 'node /repo/feature-pi-adapter/script.js', cwd: '/repo', tty: 'ttys004' })).toBe(false);
|
|
81
|
+
expect(adapter.canHandle({ pid: 5, command: 'C:\\tools\\node.exe C:\\lib\\pi.js', cwd: '/repo', tty: 'ttys005' })).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects foreign executables from a hand-built snapshot context', async () => {
|
|
85
|
+
const foreign: ProcessInfo = {
|
|
86
|
+
pid: 6,
|
|
87
|
+
command: 'claude --resume /Users/x/repos/pi/session.jsonl',
|
|
88
|
+
cwd: '/repo',
|
|
89
|
+
tty: 'ttys006',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
expect(await adapter.detectAgents({ processes: [foreign] })).toEqual([]);
|
|
70
93
|
});
|
|
71
94
|
|
|
72
95
|
it('maps a running Pi process to the tracker session for its PID', async () => {
|
|
@@ -33,29 +33,28 @@ describe('AgentRegistry', () => {
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
describe('register', () => {
|
|
36
|
-
it('creates the
|
|
36
|
+
it('creates the SQLite database and parent directory if missing', () => {
|
|
37
37
|
registry.register(makeEntry());
|
|
38
|
-
expect(fs.existsSync(regPath)).toBe(true);
|
|
39
|
-
|
|
40
|
-
expect(parsed.entries).toHaveLength(1);
|
|
41
|
-
expect(parsed.entries[0].name).toBe('agent1');
|
|
38
|
+
expect(fs.existsSync(regPath.replace(/\.json$/, '.db'))).toBe(true);
|
|
39
|
+
expect(registry.list()[0].name).toBe('agent1');
|
|
42
40
|
});
|
|
43
41
|
|
|
44
42
|
it('appends a new entry when name is unique', () => {
|
|
45
43
|
registry.register(makeEntry({ name: 'a' }));
|
|
46
|
-
registry.register(makeEntry({ name: 'b' }));
|
|
44
|
+
registry.register(makeEntry({ name: 'b', pid: process.ppid }));
|
|
47
45
|
expect(registry.list()).toHaveLength(2);
|
|
48
46
|
});
|
|
49
47
|
|
|
50
|
-
it('upserts in place when
|
|
51
|
-
registry.register(makeEntry({ name: 'a', pid:
|
|
52
|
-
registry.register(makeEntry({ name: '
|
|
48
|
+
it('upserts in place when type and pid already exist', () => {
|
|
49
|
+
registry.register(makeEntry({ name: 'a', pid: process.pid }));
|
|
50
|
+
registry.register(makeEntry({ name: 'fallback', pid: process.pid, tmuxSession: '' }));
|
|
53
51
|
const all = registry.list();
|
|
54
52
|
expect(all).toHaveLength(1);
|
|
55
|
-
expect(all[0].pid).toBe(
|
|
53
|
+
expect(all[0].pid).toBe(process.pid);
|
|
54
|
+
expect(all[0].name).toBe('a');
|
|
56
55
|
});
|
|
57
56
|
|
|
58
|
-
it('
|
|
57
|
+
it('does not write through the legacy fixed .tmp path', () => {
|
|
59
58
|
registry.register(makeEntry());
|
|
60
59
|
expect(fs.existsSync(`${regPath}.tmp`)).toBe(false);
|
|
61
60
|
});
|
|
@@ -69,16 +68,18 @@ describe('AgentRegistry', () => {
|
|
|
69
68
|
|
|
70
69
|
it('preserves existing tmuxSession when incoming is empty string', () => {
|
|
71
70
|
registry.register(makeEntry({ name: 'a', tmuxSession: 'pinned' }));
|
|
72
|
-
registry.register(makeEntry({ name: '
|
|
71
|
+
registry.register(makeEntry({ name: 'fallback', tmuxSession: '', pid: process.pid }));
|
|
73
72
|
const saved = registry.lookup('a');
|
|
74
73
|
expect(saved?.tmuxSession).toBe('pinned');
|
|
75
|
-
expect(saved?.pid).toBe(
|
|
74
|
+
expect(saved?.pid).toBe(process.pid);
|
|
76
75
|
});
|
|
77
76
|
|
|
78
|
-
it('
|
|
79
|
-
registry.register(makeEntry({ name:
|
|
80
|
-
registry.register(makeEntry({ name: '
|
|
81
|
-
expect(registry.lookup('
|
|
77
|
+
it('lets a managed start entry replace a generated fallback for the same pid', () => {
|
|
78
|
+
registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, tmuxSession: '' }));
|
|
79
|
+
registry.register(makeEntry({ name: 'custom-name', tmuxSession: 'custom-name' }));
|
|
80
|
+
expect(registry.lookup('custom-name')?.tmuxSession).toBe('custom-name');
|
|
81
|
+
expect(registry.lookup(`ai-devkit-${process.pid}`)).toBeNull();
|
|
82
|
+
expect(registry.list()).toHaveLength(1);
|
|
82
83
|
});
|
|
83
84
|
});
|
|
84
85
|
|
|
@@ -88,28 +89,35 @@ describe('AgentRegistry', () => {
|
|
|
88
89
|
expect(fs.existsSync(regPath)).toBe(false);
|
|
89
90
|
});
|
|
90
91
|
|
|
91
|
-
it('upserts multiple entries
|
|
92
|
-
const writeSpy = vi.spyOn(fs, 'writeFileSync');
|
|
92
|
+
it('upserts multiple entries in a single batch', () => {
|
|
93
93
|
registry.registerBatch([
|
|
94
94
|
makeEntry({ name: 'a' }),
|
|
95
|
-
makeEntry({ name: 'b' }),
|
|
96
|
-
makeEntry({ name: 'c' }),
|
|
95
|
+
makeEntry({ name: 'b', pid: process.pid + 1 }),
|
|
96
|
+
makeEntry({ name: 'c', pid: process.pid + 2 }),
|
|
97
97
|
]);
|
|
98
|
-
expect(writeSpy).toHaveBeenCalledTimes(1);
|
|
99
|
-
writeSpy.mockRestore();
|
|
100
98
|
expect(registry.list()).toHaveLength(3);
|
|
101
99
|
});
|
|
102
100
|
|
|
103
101
|
it('applies the tmuxSession merge per entry', () => {
|
|
104
102
|
registry.register(makeEntry({ name: 'a', tmuxSession: 'pinned' }));
|
|
105
103
|
registry.registerBatch([
|
|
106
|
-
makeEntry({ name: '
|
|
107
|
-
makeEntry({ name: 'b', tmuxSession: '' }),
|
|
104
|
+
makeEntry({ name: 'fallback', tmuxSession: '', pid: process.pid }),
|
|
105
|
+
makeEntry({ name: 'b', tmuxSession: '', pid: process.pid + 1 }),
|
|
108
106
|
]);
|
|
109
107
|
expect(registry.lookup('a')?.tmuxSession).toBe('pinned');
|
|
110
|
-
expect(registry.lookup('a')?.pid).toBe(
|
|
108
|
+
expect(registry.lookup('a')?.pid).toBe(process.pid);
|
|
111
109
|
expect(registry.lookup('b')?.tmuxSession).toBe('');
|
|
112
110
|
});
|
|
111
|
+
|
|
112
|
+
it('handles concurrent registry instances without duplicate pid rows', () => {
|
|
113
|
+
const other = new AgentRegistry(regPath);
|
|
114
|
+
registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, tmuxSession: '' }));
|
|
115
|
+
other.register(makeEntry({ name: 'custom-name', tmuxSession: 'custom-name' }));
|
|
116
|
+
registry.register(makeEntry({ name: `ai-devkit-${process.pid}`, tmuxSession: '' }));
|
|
117
|
+
|
|
118
|
+
expect(registry.list()).toHaveLength(1);
|
|
119
|
+
expect(registry.lookup('custom-name')?.pid).toBe(process.pid);
|
|
120
|
+
});
|
|
113
121
|
});
|
|
114
122
|
|
|
115
123
|
describe('lookup', () => {
|
|
@@ -124,20 +132,20 @@ describe('AgentRegistry', () => {
|
|
|
124
132
|
});
|
|
125
133
|
|
|
126
134
|
describe('list', () => {
|
|
127
|
-
it('returns empty array when
|
|
135
|
+
it('returns empty array when database does not contain entries', () => {
|
|
128
136
|
expect(registry.list()).toEqual([]);
|
|
129
137
|
});
|
|
130
138
|
|
|
131
|
-
it('
|
|
139
|
+
it('ignores existing legacy agents.json entries', () => {
|
|
140
|
+
const legacyEntry = makeEntry({ name: 'legacy', tmuxSession: 'legacy' });
|
|
132
141
|
fs.mkdirSync(path.dirname(regPath), { recursive: true });
|
|
133
|
-
fs.writeFileSync(regPath,
|
|
134
|
-
expect(registry.list()).toEqual([]);
|
|
135
|
-
});
|
|
142
|
+
fs.writeFileSync(regPath, JSON.stringify({ entries: [legacyEntry] }), 'utf8');
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
expect(
|
|
144
|
+
const legacyRegistry = new AgentRegistry(regPath);
|
|
145
|
+
|
|
146
|
+
expect(legacyRegistry.lookup('legacy')).toBeNull();
|
|
147
|
+
expect(legacyRegistry.list()).toEqual([]);
|
|
148
|
+
expect(fs.existsSync(regPath.replace(/\.json$/, '.db'))).toBe(true);
|
|
141
149
|
});
|
|
142
150
|
});
|
|
143
151
|
|
|
@@ -163,10 +171,10 @@ describe('AgentRegistry', () => {
|
|
|
163
171
|
|
|
164
172
|
it('is a no-op when all entries are alive', () => {
|
|
165
173
|
registry.register(makeEntry({ pid: process.pid }));
|
|
166
|
-
const before =
|
|
174
|
+
const before = registry.list();
|
|
167
175
|
registry.prune();
|
|
168
|
-
const after =
|
|
169
|
-
expect(after).
|
|
176
|
+
const after = registry.list();
|
|
177
|
+
expect(after).toEqual(before);
|
|
170
178
|
});
|
|
171
179
|
|
|
172
180
|
it('does nothing when file is missing', () => {
|
|
@@ -203,7 +211,7 @@ describe('AgentRegistry', () => {
|
|
|
203
211
|
|
|
204
212
|
it('throws RenameConflictError when new name is already in use by a live entry', () => {
|
|
205
213
|
registry.register(makeEntry({ name: 'agent-a', pid: process.pid }));
|
|
206
|
-
registry.register(makeEntry({ name: 'agent-b', pid: process.
|
|
214
|
+
registry.register(makeEntry({ name: 'agent-b', pid: process.ppid }));
|
|
207
215
|
expect(() => registry.rename('agent-a', 'agent-b')).toThrow(RenameConflictError);
|
|
208
216
|
});
|
|
209
217
|
|
|
@@ -214,7 +222,7 @@ describe('AgentRegistry', () => {
|
|
|
214
222
|
expect(registry.lookup('agent-b')?.pid).toBe(process.pid);
|
|
215
223
|
});
|
|
216
224
|
|
|
217
|
-
it('
|
|
225
|
+
it('does not create the legacy fixed .tmp path on rename', () => {
|
|
218
226
|
registry.register(makeEntry({ name: 'old-name', pid: process.pid }));
|
|
219
227
|
registry.rename('old-name', 'new-name');
|
|
220
228
|
expect(fs.existsSync(`${regPath}.tmp`)).toBe(false);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { MockedFunction } from 'vitest';
|
|
6
6
|
|
|
7
|
-
import { execFileSync } from 'child_process';
|
|
7
|
+
import { execFile, execFileSync } from 'child_process';
|
|
8
8
|
import {
|
|
9
9
|
listAgentProcesses,
|
|
10
10
|
batchGetProcessCwds,
|
|
@@ -12,13 +12,84 @@ import {
|
|
|
12
12
|
enrichProcesses,
|
|
13
13
|
findWrapperProcess,
|
|
14
14
|
findWrapperProcessPids,
|
|
15
|
+
captureProcessSnapshot,
|
|
16
|
+
filterByProcessNames,
|
|
15
17
|
} from '../../utils/process.js';
|
|
16
18
|
|
|
17
19
|
vi.mock('child_process', () => ({
|
|
20
|
+
execFile: vi.fn(),
|
|
18
21
|
execFileSync: vi.fn(),
|
|
19
22
|
}));
|
|
20
23
|
|
|
21
24
|
const mockedExecFileSync = execFileSync as MockedFunction<typeof execFileSync>;
|
|
25
|
+
const mockedExecFile = execFile as unknown as MockedFunction<(
|
|
26
|
+
file: string,
|
|
27
|
+
args: readonly string[],
|
|
28
|
+
options: object,
|
|
29
|
+
callback: (error: Error | null, stdout: string, stderr: string) => void,
|
|
30
|
+
) => void>;
|
|
31
|
+
|
|
32
|
+
describe('captureProcessSnapshot', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
mockedExecFile.mockReset();
|
|
35
|
+
mockedExecFileSync.mockReset();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('captures and enriches relevant processes without synchronous scans', async () => {
|
|
39
|
+
mockedExecFile.mockImplementation((file, args, _options, callback) => {
|
|
40
|
+
queueMicrotask(() => {
|
|
41
|
+
if (file === 'ps' && args.includes('-axo')) {
|
|
42
|
+
callback(null,
|
|
43
|
+
'100 1 s001 /usr/bin/claude --resume abc\n' +
|
|
44
|
+
'200 1 s002 C:\\\\tools\\\\node.exe C:\\\\bin\\\\pi.js\n' +
|
|
45
|
+
'300 1 s003 /usr/bin/unrelated\n',
|
|
46
|
+
'');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (file === 'lsof') {
|
|
50
|
+
callback(null, 'p100\nn/projects/claude\np200\nn/projects/pi\n', '');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (file === 'ps' && args.some((arg) => arg.includes('lstart='))) {
|
|
54
|
+
callback(null,
|
|
55
|
+
'100 Wed Mar 18 23:18:01 2026\n' +
|
|
56
|
+
'200 Thu Mar 19 10:00:00 2026\n',
|
|
57
|
+
'');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
callback(new Error(`unexpected command: ${file} ${args.join(' ')}`), '', '');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const snapshot = await captureProcessSnapshot(['claude', 'node']);
|
|
65
|
+
|
|
66
|
+
expect(snapshot.map((process) => process.pid)).toEqual([100, 200]);
|
|
67
|
+
expect(snapshot.map((process) => process.cwd)).toEqual(['/projects/claude', '/projects/pi']);
|
|
68
|
+
expect(snapshot.map((process) => process.startTime)).toEqual([
|
|
69
|
+
expect.any(Date),
|
|
70
|
+
expect.any(Date),
|
|
71
|
+
]);
|
|
72
|
+
expect(mockedExecFileSync).not.toHaveBeenCalled();
|
|
73
|
+
expect(mockedExecFile.mock.calls.filter(([, args]) => args.includes('-axo'))).toHaveLength(1);
|
|
74
|
+
for (const [, , options] of mockedExecFile.mock.calls) {
|
|
75
|
+
expect(options).toMatchObject({ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
76
|
+
expect(options).not.toHaveProperty('stdio');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('filterByProcessNames', () => {
|
|
82
|
+
it('matches only argv[0] and normalizes Windows paths and executable suffixes', () => {
|
|
83
|
+
const processes: ProcessInfo[] = [
|
|
84
|
+
{ pid: 1, command: 'C:\\tools\\node.exe C:\\bin\\pi.js', cwd: '', tty: '' },
|
|
85
|
+
{ pid: 2, command: '/usr/local/bin/node /opt/gemini.js', cwd: '', tty: '' },
|
|
86
|
+
{ pid: 3, command: 'codex exec --cd C:\\repos\\node', cwd: '', tty: '' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
expect(filterByProcessNames(processes, ['node'])).toEqual([processes[0], processes[1]]);
|
|
90
|
+
expect(filterByProcessNames(processes, ['node.exe'])).toEqual([processes[0], processes[1]]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
22
93
|
|
|
23
94
|
describe('listAgentProcesses', () => {
|
|
24
95
|
beforeEach(() => {
|
|
@@ -59,7 +59,7 @@ export interface ProcessInfo {
|
|
|
59
59
|
/** Process ID */
|
|
60
60
|
pid: number;
|
|
61
61
|
|
|
62
|
-
/** Parent process ID, populated by
|
|
62
|
+
/** Parent process ID, populated by process discovery when available */
|
|
63
63
|
ppid?: number;
|
|
64
64
|
|
|
65
65
|
/** Process command */
|
|
@@ -71,7 +71,7 @@ export interface ProcessInfo {
|
|
|
71
71
|
/** Terminal TTY (e.g., "ttys030") */
|
|
72
72
|
tty: string;
|
|
73
73
|
|
|
74
|
-
/** Process start time, populated by
|
|
74
|
+
/** Process start time, populated by process enrichment */
|
|
75
75
|
startTime?: Date;
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -148,6 +148,11 @@ export interface ListSessionsOptions {
|
|
|
148
148
|
type?: AgentType;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
export interface AgentDetectionContext {
|
|
152
|
+
/** One enriched process snapshot shared across this manager refresh. */
|
|
153
|
+
readonly processes: readonly ProcessInfo[];
|
|
154
|
+
}
|
|
155
|
+
|
|
151
156
|
/**
|
|
152
157
|
* Agent Adapter Interface
|
|
153
158
|
*
|
|
@@ -157,11 +162,14 @@ export interface AgentAdapter {
|
|
|
157
162
|
/** Type of agent this adapter handles */
|
|
158
163
|
readonly type: AgentType;
|
|
159
164
|
|
|
165
|
+
/** Executable basenames required for shared process discovery. */
|
|
166
|
+
readonly processNames?: readonly string[];
|
|
167
|
+
|
|
160
168
|
/**
|
|
161
169
|
* Detect running agents of this type
|
|
162
170
|
* @returns List of detected agents
|
|
163
171
|
*/
|
|
164
|
-
detectAgents(): Promise<AgentInfo[]>;
|
|
172
|
+
detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]>;
|
|
165
173
|
|
|
166
174
|
/**
|
|
167
175
|
* Check if this adapter can handle the given process
|
|
@@ -7,9 +7,10 @@ import type {
|
|
|
7
7
|
ConversationMessage,
|
|
8
8
|
SessionSummary,
|
|
9
9
|
ListSessionsOptions,
|
|
10
|
+
AgentDetectionContext,
|
|
10
11
|
} from './AgentAdapter.js';
|
|
11
12
|
import { AgentStatus } from './AgentAdapter.js';
|
|
12
|
-
import {
|
|
13
|
+
import { captureProcessSnapshot, executableBasename, filterByProcessNames } from '../utils/process.js';
|
|
13
14
|
import { batchGetSessionFileBirthtimes, isDirectory, listJsonl, safeReaddir, safeStat } from '../utils/session.js';
|
|
14
15
|
import type { SessionFile } from '../utils/session.js';
|
|
15
16
|
import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
|
|
@@ -60,14 +61,15 @@ const PID_FILE_STALENESS_MS = 60000;
|
|
|
60
61
|
* Claude Code Adapter
|
|
61
62
|
*
|
|
62
63
|
* Detects Claude Code agents by:
|
|
63
|
-
* 1.
|
|
64
|
-
* 2.
|
|
64
|
+
* 1. Filtering Claude processes from a shared asynchronous process snapshot
|
|
65
|
+
* 2. Using snapshot CWD and start-time enrichment
|
|
65
66
|
* 3. Attempting authoritative PID-file matching via ~/.claude/sessions/<pid>.json
|
|
66
67
|
* 4. Falling back to CWD+birthtime heuristic (matchProcessesToSessions) for processes without a PID file
|
|
67
68
|
* 5. Extracting summary from last user message in session JSONL
|
|
68
69
|
*/
|
|
69
70
|
export class ClaudeCodeAdapter implements AgentAdapter {
|
|
70
71
|
readonly type = 'claude' as const;
|
|
72
|
+
readonly processNames = ['claude'] as const;
|
|
71
73
|
|
|
72
74
|
private projectsDir: string;
|
|
73
75
|
private sessionsDir: string;
|
|
@@ -85,13 +87,14 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
private isClaudeExecutable(command: string): boolean {
|
|
88
|
-
const
|
|
89
|
-
const base = path.basename(executable).toLowerCase();
|
|
90
|
+
const base = executableBasename(command);
|
|
90
91
|
return base === 'claude' || base === 'claude.exe';
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
async detectAgents(): Promise<AgentInfo[]> {
|
|
94
|
-
const
|
|
94
|
+
async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
|
|
95
|
+
const snapshot = context?.processes ?? await captureProcessSnapshot(this.processNames);
|
|
96
|
+
const relevant = filterByProcessNames(snapshot, this.processNames);
|
|
97
|
+
const processes = relevant.filter((process) => this.canHandle(process));
|
|
95
98
|
if (processes.length === 0) {
|
|
96
99
|
return [];
|
|
97
100
|
}
|