@ai-devkit/agent-manager 0.26.2 → 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.
Files changed (91) hide show
  1. package/dist/AgentManager.d.ts +5 -2
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +21 -4
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/__tests__/AgentManager.test.js +147 -0
  6. package/dist/__tests__/AgentManager.test.js.map +1 -1
  7. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +13 -4
  8. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  9. package/dist/__tests__/adapters/CodexAdapter.test.js +13 -4
  10. package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
  11. package/dist/__tests__/adapters/CopilotAdapter.test.js +7 -2
  12. package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
  13. package/dist/__tests__/adapters/GeminiCliAdapter.test.js +18 -5
  14. package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
  15. package/dist/__tests__/adapters/GrokCliAdapter.test.js +7 -2
  16. package/dist/__tests__/adapters/GrokCliAdapter.test.js.map +1 -1
  17. package/dist/__tests__/adapters/OpenCodeAdapter.test.js +13 -4
  18. package/dist/__tests__/adapters/OpenCodeAdapter.test.js.map +1 -1
  19. package/dist/__tests__/adapters/PiAdapter.test.js +32 -4
  20. package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -1
  21. package/dist/__tests__/utils/process.test.js +90 -2
  22. package/dist/__tests__/utils/process.test.js.map +1 -1
  23. package/dist/adapters/AgentAdapter.d.ts +9 -3
  24. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  25. package/dist/adapters/AgentAdapter.js.map +1 -1
  26. package/dist/adapters/ClaudeCodeAdapter.d.ts +5 -4
  27. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  28. package/dist/adapters/ClaudeCodeAdapter.js +11 -7
  29. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  30. package/dist/adapters/CodexAdapter.d.ts +5 -4
  31. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  32. package/dist/adapters/CodexAdapter.js +11 -7
  33. package/dist/adapters/CodexAdapter.js.map +1 -1
  34. package/dist/adapters/CopilotAdapter.d.ts +5 -4
  35. package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
  36. package/dist/adapters/CopilotAdapter.js +11 -7
  37. package/dist/adapters/CopilotAdapter.js.map +1 -1
  38. package/dist/adapters/GeminiCliAdapter.d.ts +6 -5
  39. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  40. package/dist/adapters/GeminiCliAdapter.js +12 -8
  41. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  42. package/dist/adapters/GrokCliAdapter.d.ts +3 -2
  43. package/dist/adapters/GrokCliAdapter.d.ts.map +1 -1
  44. package/dist/adapters/GrokCliAdapter.js +10 -6
  45. package/dist/adapters/GrokCliAdapter.js.map +1 -1
  46. package/dist/adapters/OpenCodeAdapter.d.ts +5 -4
  47. package/dist/adapters/OpenCodeAdapter.d.ts.map +1 -1
  48. package/dist/adapters/OpenCodeAdapter.js +11 -7
  49. package/dist/adapters/OpenCodeAdapter.js.map +1 -1
  50. package/dist/adapters/PiAdapter.d.ts +3 -2
  51. package/dist/adapters/PiAdapter.d.ts.map +1 -1
  52. package/dist/adapters/PiAdapter.js +12 -9
  53. package/dist/adapters/PiAdapter.js.map +1 -1
  54. package/dist/adapters/index.d.ts +1 -1
  55. package/dist/adapters/index.d.ts.map +1 -1
  56. package/dist/adapters/index.js.map +1 -1
  57. package/dist/index.d.ts +2 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +1 -0
  60. package/dist/index.js.map +1 -1
  61. package/dist/utils/index.d.ts +1 -1
  62. package/dist/utils/index.d.ts.map +1 -1
  63. package/dist/utils/index.js +1 -1
  64. package/dist/utils/index.js.map +1 -1
  65. package/dist/utils/process.d.ts +9 -1
  66. package/dist/utils/process.d.ts.map +1 -1
  67. package/dist/utils/process.js +155 -10
  68. package/dist/utils/process.js.map +1 -1
  69. package/package.json +1 -1
  70. package/src/AgentManager.ts +28 -4
  71. package/src/__tests__/AgentManager.test.ts +89 -0
  72. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +16 -5
  73. package/src/__tests__/adapters/CodexAdapter.test.ts +16 -5
  74. package/src/__tests__/adapters/CopilotAdapter.test.ts +8 -1
  75. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +20 -4
  76. package/src/__tests__/adapters/GrokCliAdapter.test.ts +8 -1
  77. package/src/__tests__/adapters/OpenCodeAdapter.test.ts +16 -5
  78. package/src/__tests__/adapters/PiAdapter.test.ts +28 -5
  79. package/src/__tests__/utils/process.test.ts +72 -1
  80. package/src/adapters/AgentAdapter.ts +11 -3
  81. package/src/adapters/ClaudeCodeAdapter.ts +10 -7
  82. package/src/adapters/CodexAdapter.ts +10 -7
  83. package/src/adapters/CopilotAdapter.ts +16 -7
  84. package/src/adapters/GeminiCliAdapter.ts +17 -8
  85. package/src/adapters/GrokCliAdapter.ts +9 -6
  86. package/src/adapters/OpenCodeAdapter.ts +10 -7
  87. package/src/adapters/PiAdapter.ts +10 -9
  88. package/src/adapters/index.ts +1 -1
  89. package/src/index.ts +2 -0
  90. package/src/utils/index.ts +1 -1
  91. package/src/utils/process.ts +160 -10
@@ -2,8 +2,8 @@
2
2
  * OpenCode Adapter
3
3
  *
4
4
  * Detects running OpenCode agents by:
5
- * 1. Finding running opencode processes via shared listAgentProcesses()
6
- * 2. Enriching with CWD and start times via shared enrichProcesses()
5
+ * 1. Filtering OpenCode processes from a shared asynchronous process snapshot
6
+ * 2. Using snapshot CWD and start-time enrichment
7
7
  * 3. Querying OpenCode's SQLite DB (~/.local/share/opencode/opencode.db) to
8
8
  * find the session matching each process's CWD and read status from message.time.completed
9
9
  *
@@ -21,9 +21,10 @@ import type {
21
21
  ConversationMessage,
22
22
  SessionSummary,
23
23
  ListSessionsOptions,
24
+ AgentDetectionContext,
24
25
  } from './AgentAdapter.js';
25
26
  import { AgentStatus } from './AgentAdapter.js';
26
- import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
27
+ import { captureProcessSnapshot, executableBasename, filterByProcessNames } from '../utils/process.js';
27
28
  import { generateAgentName } from '../utils/matching.js';
28
29
 
29
30
  const SESSION_REF_SEP = '::';
@@ -55,6 +56,7 @@ interface OpenCodeSessionStats {
55
56
 
56
57
  export class OpenCodeAdapter implements AgentAdapter {
57
58
  readonly type = 'opencode' as const;
59
+ readonly processNames = ['opencode'] as const;
58
60
 
59
61
  private static readonly IDLE_THRESHOLD_MINUTES = 5;
60
62
 
@@ -84,13 +86,14 @@ export class OpenCodeAdapter implements AgentAdapter {
84
86
  }
85
87
 
86
88
  canHandle(processInfo: ProcessInfo): boolean {
87
- const exe = (processInfo.command.trim().split(/\s+/)[0] || '').toLowerCase();
88
- const base = path.basename(exe);
89
+ const base = executableBasename(processInfo.command);
89
90
  return base === 'opencode' || base === 'opencode.exe';
90
91
  }
91
92
 
92
- async detectAgents(): Promise<AgentInfo[]> {
93
- const processes = enrichProcesses(listAgentProcesses('opencode'));
93
+ async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
94
+ const snapshot = context?.processes ?? await captureProcessSnapshot(this.processNames);
95
+ const relevant = filterByProcessNames(snapshot, this.processNames);
96
+ const processes = relevant.filter((process) => this.canHandle(process));
94
97
  if (processes.length === 0) return [];
95
98
 
96
99
  const db = this.openDb();
@@ -17,9 +17,10 @@ import type {
17
17
  ConversationMessage,
18
18
  SessionSummary,
19
19
  ListSessionsOptions,
20
+ AgentDetectionContext,
20
21
  } from './AgentAdapter.js';
21
22
  import { AgentStatus } from './AgentAdapter.js';
22
- import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
23
+ import { captureProcessSnapshot, executableBasename, filterByProcessNames } from '../utils/process.js';
23
24
  import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
24
25
  import type { SessionFile } from '../utils/session.js';
25
26
  import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
@@ -66,6 +67,7 @@ interface TrackerAgentResult {
66
67
 
67
68
  export class PiAdapter implements AgentAdapter {
68
69
  readonly type = 'pi' as const;
70
+ readonly processNames = ['pi', 'node'] as const;
69
71
 
70
72
  private static readonly IDLE_THRESHOLD_MINUTES = 5;
71
73
 
@@ -86,8 +88,10 @@ export class PiAdapter implements AgentAdapter {
86
88
  return this.isPiExecutable(processInfo.command);
87
89
  }
88
90
 
89
- async detectAgents(): Promise<AgentInfo[]> {
90
- const processes = enrichProcesses(this.listPiProcesses());
91
+ async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
92
+ const snapshot = context?.processes ?? await captureProcessSnapshot(this.processNames);
93
+ const relevant = filterByProcessNames(snapshot, this.processNames);
94
+ const processes = this.listPiProcesses(relevant);
91
95
  if (processes.length === 0) return [];
92
96
 
93
97
  const { cachedAgents, remaining } = this.tryRegistryCache(processes);
@@ -149,12 +153,9 @@ export class PiAdapter implements AgentAdapter {
149
153
  return agents;
150
154
  }
151
155
 
152
- private listPiProcesses(): ProcessInfo[] {
156
+ private listPiProcesses(snapshot: readonly ProcessInfo[]): ProcessInfo[] {
153
157
  const byPid = new Map<number, ProcessInfo>();
154
- for (const proc of listAgentProcesses('pi')) {
155
- if (this.canHandle(proc)) byPid.set(proc.pid, proc);
156
- }
157
- for (const proc of listAgentProcesses('node')) {
158
+ for (const proc of snapshot) {
158
159
  if (this.canHandle(proc)) byPid.set(proc.pid, proc);
159
160
  }
160
161
  return Array.from(byPid.values());
@@ -547,7 +548,7 @@ export class PiAdapter implements AgentAdapter {
547
548
 
548
549
  private isPiExecutable(command: string): boolean {
549
550
  for (const token of command.trim().split(/\s+/)) {
550
- const base = path.basename(token).toLowerCase();
551
+ const base = executableBasename(token);
551
552
  if (base === 'pi' || base === 'pi.exe' || base === 'pi.js') return true;
552
553
  }
553
554
  return false;
@@ -6,4 +6,4 @@ export { GrokCliAdapter } from './GrokCliAdapter.js';
6
6
  export { OpenCodeAdapter } from './OpenCodeAdapter.js';
7
7
  export { PiAdapter } from './PiAdapter.js';
8
8
  export { AgentStatus } from './AgentAdapter.js';
9
- export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './AgentAdapter.js';
9
+ export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo, AgentDetectionContext } from './AgentAdapter.js';
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export type {
16
16
  ConversationMessage,
17
17
  SessionSummary,
18
18
  ListSessionsOptions,
19
+ AgentDetectionContext,
19
20
  } from './adapters/AgentAdapter.js';
20
21
 
21
22
  export { TerminalFocusManager, TerminalType } from './terminal/TerminalFocusManager.js';
@@ -23,6 +24,7 @@ export type { TerminalLocation } from './terminal/TerminalFocusManager.js';
23
24
  export { TtyWriter } from './terminal/TtyWriter.js';
24
25
 
25
26
  export { getProcessTty } from './utils/process.js';
27
+ export { captureProcessSnapshot, executableBasename, filterByProcessNames } from './utils/process.js';
26
28
  export type { AgentSortKey } from './utils/sortAgents.js';
27
29
  export type { ListAgentsOptions } from './AgentManager.js';
28
30
 
@@ -1,4 +1,4 @@
1
- export { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses } from './process.js';
1
+ export { listAgentProcesses, batchGetProcessCwds, batchGetProcessStartTimes, enrichProcesses, captureProcessSnapshot } from './process.js';
2
2
  export { getProcessTty } from './process.js';
3
3
  export { batchGetSessionFileBirthtimes } from './session.js';
4
4
  export type { SessionFile } from './session.js';
@@ -2,13 +2,42 @@
2
2
  * Process Detection Utilities
3
3
  *
4
4
  * Shared shell command wrappers for detecting and inspecting running processes.
5
- * All execFileSync calls for process data live here — adapters must not call execFileSync directly.
5
+ * Built-in refresh discovery uses captureProcessSnapshot(); synchronous helpers
6
+ * remain exported for compatibility with existing consumers.
6
7
  */
7
8
 
8
9
  import * as path from 'path';
9
- import { execFileSync } from 'child_process';
10
+ import { execFile, execFileSync } from 'child_process';
10
11
  import type { ProcessInfo } from '../adapters/AgentAdapter.js';
11
12
 
13
+ const PROCESS_EXEC_MAX_BUFFER = 10 * 1024 * 1024;
14
+ const VALID_EXECUTABLE_NAME = /^[a-zA-Z0-9_-]+$/;
15
+
16
+ export function executableBasename(command: string): string {
17
+ const executable = command.trim().split(/\s+/)[0] || '';
18
+ return path.basename(executable.replace(/\\/g, '/')).toLowerCase();
19
+ }
20
+
21
+ function normalizeExecutableName(name: string): string {
22
+ const lower = name.toLowerCase();
23
+ return lower.endsWith('.exe') ? lower.slice(0, -4) : lower;
24
+ }
25
+
26
+ function normalizedProcessNames(namePatterns: readonly string[]): Set<string> {
27
+ return new Set(namePatterns.filter(Boolean).map(normalizeExecutableName));
28
+ }
29
+
30
+ export function filterByProcessNames(
31
+ processes: readonly ProcessInfo[],
32
+ namePatterns: readonly string[],
33
+ ): ProcessInfo[] {
34
+ const names = normalizedProcessNames(namePatterns);
35
+ if (names.size === 0) return [];
36
+ return processes.filter((process) => (
37
+ names.has(normalizeExecutableName(executableBasename(process.command)))
38
+ ));
39
+ }
40
+
12
41
  /**
13
42
  * List running processes matching an agent executable name.
14
43
  *
@@ -20,14 +49,14 @@ import type { ProcessInfo } from '../adapters/AgentAdapter.js';
20
49
  */
21
50
  export function listAgentProcesses(namePattern: string): ProcessInfo[] {
22
51
  // Validate pattern contains only safe characters (alphanumeric, dash, underscore)
23
- if (!namePattern || !/^[a-zA-Z0-9_-]+$/.test(namePattern)) {
52
+ if (!namePattern || !VALID_EXECUTABLE_NAME.test(namePattern)) {
24
53
  return [];
25
54
  }
26
55
 
27
56
  try {
28
57
  const output = execFileSync('ps', ['-axo', 'pid=,ppid=,tty=,command='], { encoding: 'utf-8' });
29
58
 
30
- const lowerPattern = namePattern.toLowerCase();
59
+ const names = normalizedProcessNames([namePattern]);
31
60
  const processes: ProcessInfo[] = [];
32
61
 
33
62
  for (const line of output.trim().split('\n')) {
@@ -43,12 +72,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
43
72
  const tty = match[3];
44
73
  const command = match[4];
45
74
 
46
- // Check that the executable basename matches exactly
47
- const executable = command.trim().split(/\s+/)[0] || '';
48
- const base = path.basename(executable).toLowerCase();
49
- if (base !== lowerPattern && base !== `${lowerPattern}.exe`) {
50
- continue;
51
- }
75
+ if (!names.has(normalizeExecutableName(executableBasename(command)))) continue;
52
76
 
53
77
  const ttyShort = tty.startsWith('/dev/') ? tty.slice(5) : tty;
54
78
 
@@ -67,6 +91,132 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
67
91
  }
68
92
  }
69
93
 
94
+ function execFileText(
95
+ file: string,
96
+ args: readonly string[],
97
+ ): Promise<string> {
98
+ return new Promise((resolve, reject) => {
99
+ execFile(file, args, { encoding: 'utf-8', maxBuffer: PROCESS_EXEC_MAX_BUFFER }, (error, stdout) => {
100
+ if (error) {
101
+ reject(error);
102
+ return;
103
+ }
104
+ resolve(stdout);
105
+ });
106
+ });
107
+ }
108
+
109
+ function parseProcessList(output: string, namePatterns: ReadonlySet<string>): ProcessInfo[] {
110
+ const processes: ProcessInfo[] = [];
111
+
112
+ for (const line of output.trim().split('\n')) {
113
+ if (!line.trim()) continue;
114
+
115
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
116
+ if (!match) continue;
117
+
118
+ const pid = parseInt(match[1], 10);
119
+ const ppid = parseInt(match[2], 10);
120
+ if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;
121
+
122
+ const tty = match[3];
123
+ const command = match[4];
124
+ const base = executableBasename(command);
125
+ const normalizedBase = normalizeExecutableName(base);
126
+ if (!namePatterns.has(normalizedBase)) continue;
127
+
128
+ processes.push({
129
+ pid,
130
+ ppid,
131
+ command,
132
+ cwd: '',
133
+ tty: tty.startsWith('/dev/') ? tty.slice(5) : tty,
134
+ });
135
+ }
136
+
137
+ return processes;
138
+ }
139
+
140
+ async function batchGetProcessCwdsAsync(pids: number[]): Promise<Map<number, string>> {
141
+ const result = new Map<number, string>();
142
+ if (pids.length === 0) return result;
143
+
144
+ try {
145
+ const output = await execFileText('lsof', ['-a', '-d', 'cwd', '-Fn', '-p', pids.join(',')]);
146
+ let currentPid: number | null = null;
147
+ for (const line of output.trim().split('\n')) {
148
+ if (line.startsWith('p')) {
149
+ currentPid = parseInt(line.slice(1), 10);
150
+ } else if (line.startsWith('n') && currentPid !== null) {
151
+ result.set(currentPid, line.slice(1));
152
+ currentPid = null;
153
+ }
154
+ }
155
+ return result;
156
+ } catch {
157
+ const entries = await Promise.all(pids.map(async (pid) => {
158
+ try {
159
+ const output = await execFileText('pwdx', [String(pid)]);
160
+ const match = output.match(/^\d+:\s*(.+)$/);
161
+ return match ? [pid, match[1].trim()] as const : null;
162
+ } catch {
163
+ return null;
164
+ }
165
+ }));
166
+ for (const entry of entries) {
167
+ if (entry) result.set(entry[0], entry[1]);
168
+ }
169
+ return result;
170
+ }
171
+ }
172
+
173
+ async function batchGetProcessStartTimesAsync(pids: number[]): Promise<Map<number, Date>> {
174
+ const result = new Map<number, Date>();
175
+ if (pids.length === 0) return result;
176
+
177
+ try {
178
+ const output = await execFileText('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')]);
179
+ for (const rawLine of output.split('\n')) {
180
+ const match = rawLine.trim().match(/^(\d+)\s+(.+)$/);
181
+ if (!match) continue;
182
+ const pid = parseInt(match[1], 10);
183
+ const date = new Date(match[2].trim());
184
+ if (Number.isFinite(pid) && !Number.isNaN(date.getTime())) result.set(pid, date);
185
+ }
186
+ } catch {
187
+ // Return partial/empty data, matching the synchronous helper.
188
+ }
189
+ return result;
190
+ }
191
+
192
+ /**
193
+ * Capture and enrich relevant processes without blocking the event loop.
194
+ * One base process listing is shared by every requested executable name.
195
+ */
196
+ export async function captureProcessSnapshot(namePatterns: readonly string[]): Promise<ProcessInfo[]> {
197
+ const names = normalizedProcessNames(
198
+ namePatterns.filter((name) => Boolean(name) && VALID_EXECUTABLE_NAME.test(name)),
199
+ );
200
+ if (names.size === 0) return [];
201
+
202
+ try {
203
+ const output = await execFileText('ps', ['-axo', 'pid=,ppid=,tty=,command=']);
204
+ const processes = parseProcessList(output, names);
205
+ const pids = processes.map((process) => process.pid);
206
+ const [cwdMap, startTimeMap] = await Promise.all([
207
+ batchGetProcessCwdsAsync(pids),
208
+ batchGetProcessStartTimesAsync(pids),
209
+ ]);
210
+ return processes.map((process) => ({
211
+ ...process,
212
+ cwd: cwdMap.get(process.pid) || '',
213
+ startTime: startTimeMap.get(process.pid),
214
+ }));
215
+ } catch {
216
+ return [];
217
+ }
218
+ }
219
+
70
220
  /**
71
221
  * Batch-get current working directories for multiple PIDs.
72
222
  *