@ai-devkit/agent-manager 0.18.0 → 0.19.1
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/__tests__/adapters/CopilotAdapter.test.js +127 -2
- package/dist/__tests__/adapters/CopilotAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js +144 -2
- package/dist/__tests__/adapters/GeminiCliAdapter.test.js.map +1 -1
- package/dist/__tests__/terminal/TerminalFocusManager.test.js +73 -0
- package/dist/__tests__/terminal/TerminalFocusManager.test.js.map +1 -0
- package/dist/__tests__/utils/agents.test.js +17 -0
- package/dist/__tests__/utils/agents.test.js.map +1 -0
- package/dist/__tests__/utils/process.test.js +54 -5
- package/dist/__tests__/utils/process.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +2 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/CopilotAdapter.d.ts +4 -2
- package/dist/adapters/CopilotAdapter.d.ts.map +1 -1
- package/dist/adapters/CopilotAdapter.js +23 -12
- package/dist/adapters/CopilotAdapter.js.map +1 -1
- package/dist/adapters/GeminiCliAdapter.d.ts +1 -0
- package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
- package/dist/adapters/GeminiCliAdapter.js +36 -11
- package/dist/adapters/GeminiCliAdapter.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.d.ts +1 -0
- package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +10 -9
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/utils/agents.d.ts +1 -1
- package/dist/utils/agents.d.ts.map +1 -1
- package/dist/utils/agents.js +29 -3
- package/dist/utils/agents.js.map +1 -1
- package/dist/utils/process.d.ts +11 -2
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +44 -9
- package/dist/utils/process.js.map +1 -1
- package/package.json +6 -1
- package/src/__tests__/adapters/CopilotAdapter.test.ts +97 -5
- package/src/__tests__/adapters/GeminiCliAdapter.test.ts +139 -4
- package/src/__tests__/terminal/TerminalFocusManager.test.ts +92 -0
- package/src/__tests__/utils/agents.test.ts +17 -0
- package/src/__tests__/utils/process.test.ts +31 -7
- package/src/adapters/AgentAdapter.ts +3 -0
- package/src/adapters/CopilotAdapter.ts +25 -14
- package/src/adapters/GeminiCliAdapter.ts +42 -11
- package/src/terminal/TerminalFocusManager.ts +11 -3
- package/src/utils/agents.ts +22 -2
- package/src/utils/process.ts +64 -9
|
@@ -22,11 +22,11 @@ import type {
|
|
|
22
22
|
ListSessionsOptions,
|
|
23
23
|
} from './AgentAdapter.js';
|
|
24
24
|
import { AgentStatus } from './AgentAdapter.js';
|
|
25
|
-
import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
|
|
25
|
+
import { listAgentProcesses, enrichProcesses, findWrapperProcess, findWrapperProcessPids } from '../utils/process.js';
|
|
26
26
|
import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
|
|
27
27
|
import type { SessionFile } from '../utils/session.js';
|
|
28
28
|
import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
|
|
29
|
-
import { AgentRegistry } from '../utils/AgentRegistry.js';
|
|
29
|
+
import { AgentRegistry, type RegistryEntry } from '../utils/AgentRegistry.js';
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* A single Gemini CLI message content part. Mirrors the `{text?: string}`
|
|
@@ -117,40 +117,70 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
117
117
|
const processes = nodeProcesses.filter((proc) => this.isGeminiExecutable(proc.command));
|
|
118
118
|
if (processes.length === 0) return [];
|
|
119
119
|
|
|
120
|
-
const
|
|
120
|
+
const wrapperPids = findWrapperProcessPids(processes);
|
|
121
|
+
const { cachedAgents, remaining } = this.tryRegistryCache(processes, wrapperPids);
|
|
121
122
|
if (remaining.length === 0) return this.deduplicateSessionAgents(cachedAgents);
|
|
122
123
|
|
|
123
|
-
const
|
|
124
|
+
const candidateProcesses = remaining.filter((proc) => !wrapperPids.has(proc.pid));
|
|
125
|
+
const registryEntriesByPid = new Map(this.registry.list().map((entry) => [entry.pid, entry]));
|
|
126
|
+
|
|
127
|
+
const { sessions, contentCache } = this.discoverSessions(candidateProcesses);
|
|
124
128
|
if (sessions.length === 0) {
|
|
129
|
+
const processOnlyAgents = candidateProcesses.map((proc) => {
|
|
130
|
+
const agent = this.mapProcessOnlyAgent(proc);
|
|
131
|
+
this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
|
|
132
|
+
return agent;
|
|
133
|
+
});
|
|
134
|
+
|
|
125
135
|
return this.deduplicateSessionAgents([
|
|
126
136
|
...cachedAgents,
|
|
127
|
-
...
|
|
137
|
+
...processOnlyAgents,
|
|
128
138
|
]);
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
const matches = matchProcessesToSessions(
|
|
141
|
+
const matches = matchProcessesToSessions(candidateProcesses, sessions);
|
|
132
142
|
const matchedPids = new Set(matches.map((m) => m.process.pid));
|
|
143
|
+
const matchedProcesses: ProcessInfo[] = [];
|
|
133
144
|
const agents: AgentInfo[] = [];
|
|
134
145
|
|
|
135
146
|
for (const match of matches) {
|
|
136
147
|
const cachedContent = contentCache.get(match.session.filePath);
|
|
137
148
|
const sessionData = this.parseSession(cachedContent, match.session.filePath);
|
|
138
149
|
if (sessionData) {
|
|
139
|
-
|
|
150
|
+
const agent = this.mapSessionToAgent(sessionData, match.process, match.session.filePath);
|
|
151
|
+
this.applyWrapperRegistryName(agent, match.process, processes, registryEntriesByPid);
|
|
152
|
+
agents.push(agent);
|
|
153
|
+
matchedProcesses.push(match.process);
|
|
140
154
|
} else {
|
|
141
155
|
matchedPids.delete(match.process.pid);
|
|
142
156
|
}
|
|
143
157
|
}
|
|
144
158
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
const matchedWrapperPids = findWrapperProcessPids(candidateProcesses, matchedProcesses);
|
|
160
|
+
for (const proc of candidateProcesses) {
|
|
161
|
+
if (!matchedPids.has(proc.pid) && !matchedWrapperPids.has(proc.pid)) {
|
|
162
|
+
const agent = this.mapProcessOnlyAgent(proc);
|
|
163
|
+
this.applyWrapperRegistryName(agent, proc, processes, registryEntriesByPid);
|
|
164
|
+
agents.push(agent);
|
|
148
165
|
}
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
return this.deduplicateSessionAgents([...cachedAgents, ...agents]);
|
|
152
169
|
}
|
|
153
170
|
|
|
171
|
+
private applyWrapperRegistryName(
|
|
172
|
+
agent: AgentInfo,
|
|
173
|
+
processInfo: ProcessInfo,
|
|
174
|
+
processes: ProcessInfo[],
|
|
175
|
+
registryEntriesByPid: Map<number, RegistryEntry>,
|
|
176
|
+
): void {
|
|
177
|
+
const wrapper = findWrapperProcess(processes, processInfo);
|
|
178
|
+
const wrapperEntry = wrapper ? registryEntriesByPid.get(wrapper.pid) : undefined;
|
|
179
|
+
if (wrapperEntry?.type === this.type) {
|
|
180
|
+
agent.name = wrapperEntry.name;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
154
184
|
private deduplicateSessionAgents(agents: AgentInfo[]): AgentInfo[] {
|
|
155
185
|
const bySession = new Map<string, AgentInfo>();
|
|
156
186
|
const result: AgentInfo[] = [];
|
|
@@ -187,7 +217,7 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
187
217
|
return null;
|
|
188
218
|
}
|
|
189
219
|
|
|
190
|
-
private tryRegistryCache(processes: ProcessInfo[]): {
|
|
220
|
+
private tryRegistryCache(processes: ProcessInfo[], wrapperPids: Set<number>): {
|
|
191
221
|
cachedAgents: AgentInfo[];
|
|
192
222
|
remaining: ProcessInfo[];
|
|
193
223
|
} {
|
|
@@ -198,6 +228,7 @@ export class GeminiCliAdapter implements AgentAdapter {
|
|
|
198
228
|
for (const proc of processes) {
|
|
199
229
|
const entry = byPid.get(proc.pid);
|
|
200
230
|
if (
|
|
231
|
+
wrapperPids.has(proc.pid) ||
|
|
201
232
|
!entry ||
|
|
202
233
|
entry.type !== this.type ||
|
|
203
234
|
!entry.sessionFilePath ||
|
|
@@ -98,8 +98,8 @@ export class TerminalFocusManager {
|
|
|
98
98
|
|
|
99
99
|
private async findITerm2Session(tty: string): Promise<TerminalLocation | null> {
|
|
100
100
|
try {
|
|
101
|
-
// Check if iTerm2 is running first to avoid launching it
|
|
102
|
-
await
|
|
101
|
+
// Check if iTerm2 is running first to avoid launching it.
|
|
102
|
+
if (!await this.isProcessRunning('iTerm2')) return null;
|
|
103
103
|
} catch {
|
|
104
104
|
return null;
|
|
105
105
|
}
|
|
@@ -137,7 +137,7 @@ export class TerminalFocusManager {
|
|
|
137
137
|
private async findTerminalAppWindow(tty: string): Promise<TerminalLocation | null> {
|
|
138
138
|
try {
|
|
139
139
|
// Check if Terminal.app is running
|
|
140
|
-
await
|
|
140
|
+
if (!await this.isProcessRunning('Terminal')) return null;
|
|
141
141
|
} catch {
|
|
142
142
|
return null;
|
|
143
143
|
}
|
|
@@ -170,6 +170,14 @@ export class TerminalFocusManager {
|
|
|
170
170
|
return null;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
private async isProcessRunning(name: string): Promise<boolean> {
|
|
174
|
+
const { stdout } = await execFileAsync('ps', ['-Axo', 'comm']);
|
|
175
|
+
return stdout
|
|
176
|
+
.split('\n')
|
|
177
|
+
.map((line) => line.trim())
|
|
178
|
+
.some((command) => command === name || command.endsWith(`/${name}`));
|
|
179
|
+
}
|
|
180
|
+
|
|
173
181
|
private async focusTmuxPane(identifier: string): Promise<boolean> {
|
|
174
182
|
try {
|
|
175
183
|
await execFileAsync('tmux', ['switch-client', '-t', identifier]);
|
package/src/utils/agents.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import type { AgentType } from '../adapters/AgentAdapter.js';
|
|
3
3
|
|
|
4
|
-
export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'gemini_cli' | 'opencode'>;
|
|
4
|
+
export type StartableAgentType = Extract<AgentType, 'claude' | 'codex' | 'copilot' | 'gemini_cli' | 'opencode' | 'pi'>;
|
|
5
5
|
|
|
6
6
|
export interface AgentConfig {
|
|
7
7
|
/** Shell command to launch the agent (sent to tmux via `send-keys`). */
|
|
@@ -18,8 +18,10 @@ export interface AgentConfig {
|
|
|
18
18
|
export const AGENTS: Record<StartableAgentType, AgentConfig> = {
|
|
19
19
|
claude: { command: 'claude', matches: matchArgv0('claude') },
|
|
20
20
|
codex: { command: 'codex', matches: matchArgv0('codex') },
|
|
21
|
-
|
|
21
|
+
copilot: { command: 'copilot', matches: matchArgv0Name('copilot-cli') },
|
|
22
22
|
gemini_cli: { command: 'gemini', matches: matchAnyToken('gemini') },
|
|
23
|
+
opencode: { command: 'opencode', matches: matchArgv0('opencode') },
|
|
24
|
+
pi: { command: 'pi', matches: matchAnyBasename(['pi']) },
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
function matchArgv0(name: string): (psCommand: string) => boolean {
|
|
@@ -30,6 +32,14 @@ function matchArgv0(name: string): (psCommand: string) => boolean {
|
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
function matchArgv0Name(name: string): (psCommand: string) => boolean {
|
|
36
|
+
const lower = name.toLowerCase();
|
|
37
|
+
return (psCommand) => {
|
|
38
|
+
const token = psCommand.trim().split(/\s+/)[0];
|
|
39
|
+
return token ? token.toLowerCase().includes(lower) : false;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
function matchAnyToken(name: string): (psCommand: string) => boolean {
|
|
34
44
|
const lower = name.toLowerCase();
|
|
35
45
|
return (psCommand) => {
|
|
@@ -39,3 +49,13 @@ function matchAnyToken(name: string): (psCommand: string) => boolean {
|
|
|
39
49
|
return false;
|
|
40
50
|
};
|
|
41
51
|
}
|
|
52
|
+
|
|
53
|
+
function matchAnyBasename(names: string[]): (psCommand: string) => boolean {
|
|
54
|
+
const lowers = new Set(names.map((name) => name.toLowerCase()));
|
|
55
|
+
return (psCommand) => {
|
|
56
|
+
for (const token of psCommand.trim().split(/\s+/)) {
|
|
57
|
+
if (lowers.has(path.basename(token).toLowerCase())) return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
}
|
package/src/utils/process.ts
CHANGED
|
@@ -12,10 +12,10 @@ import type { ProcessInfo } from '../adapters/AgentAdapter.js';
|
|
|
12
12
|
/**
|
|
13
13
|
* List running processes matching an agent executable name.
|
|
14
14
|
*
|
|
15
|
-
* Uses `ps
|
|
15
|
+
* Uses `ps -axo` then filters in JS for exact executable basename match.
|
|
16
16
|
* This avoids shell pipelines and string interpolation.
|
|
17
17
|
*
|
|
18
|
-
* Returned ProcessInfo has pid, command, tty populated.
|
|
18
|
+
* Returned ProcessInfo has pid, ppid, command, tty populated.
|
|
19
19
|
* cwd and startTime are NOT populated — call enrichProcesses() to fill them.
|
|
20
20
|
*/
|
|
21
21
|
export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
@@ -25,7 +25,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
try {
|
|
28
|
-
const output = execFileSync('ps', ['
|
|
28
|
+
const output = execFileSync('ps', ['-axo', 'pid=,ppid=,tty=,command='], { encoding: 'utf-8' });
|
|
29
29
|
|
|
30
30
|
const lowerPattern = namePattern.toLowerCase();
|
|
31
31
|
const processes: ProcessInfo[] = [];
|
|
@@ -33,14 +33,15 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
33
33
|
for (const line of output.trim().split('\n')) {
|
|
34
34
|
if (!line.trim()) continue;
|
|
35
35
|
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
36
|
+
const match = line.match(/^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+)$/);
|
|
37
|
+
if (!match) continue;
|
|
38
38
|
|
|
39
|
-
const pid = parseInt(
|
|
40
|
-
|
|
39
|
+
const pid = parseInt(match[1], 10);
|
|
40
|
+
const ppid = parseInt(match[2], 10);
|
|
41
|
+
if (Number.isNaN(pid) || Number.isNaN(ppid)) continue;
|
|
41
42
|
|
|
42
|
-
const tty =
|
|
43
|
-
const command =
|
|
43
|
+
const tty = match[3];
|
|
44
|
+
const command = match[4];
|
|
44
45
|
|
|
45
46
|
// Check that the executable basename matches exactly
|
|
46
47
|
const executable = command.trim().split(/\s+/)[0] || '';
|
|
@@ -53,6 +54,7 @@ export function listAgentProcesses(namePattern: string): ProcessInfo[] {
|
|
|
53
54
|
|
|
54
55
|
processes.push({
|
|
55
56
|
pid,
|
|
57
|
+
ppid,
|
|
56
58
|
command,
|
|
57
59
|
cwd: '',
|
|
58
60
|
tty: ttyShort,
|
|
@@ -177,6 +179,59 @@ export function enrichProcesses(processes: ProcessInfo[]): ProcessInfo[] {
|
|
|
177
179
|
return processes;
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
function isSameTerminalProcess(proc: ProcessInfo, matched: ProcessInfo): boolean {
|
|
183
|
+
const sameTty = proc.tty !== '' && proc.tty !== '?' && proc.tty === matched.tty;
|
|
184
|
+
const sameCwd = proc.cwd !== '' && proc.cwd === matched.cwd;
|
|
185
|
+
|
|
186
|
+
return sameTty && (sameCwd || proc.cwd === '' || matched.cwd === '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesProcessIdentity(proc: ProcessInfo, matched: ProcessInfo): boolean {
|
|
190
|
+
return proc.pid === matched.pid || isSameTerminalProcess(proc, matched);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function findWrapperProcess(
|
|
194
|
+
processes: ProcessInfo[],
|
|
195
|
+
child: ProcessInfo,
|
|
196
|
+
): ProcessInfo | undefined {
|
|
197
|
+
return processes.find((proc) => (
|
|
198
|
+
proc.pid !== child.pid &&
|
|
199
|
+
child.ppid === proc.pid &&
|
|
200
|
+
matchesProcessIdentity(proc, child)
|
|
201
|
+
));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Find parent wrapper processes that should not be reported as separate agents.
|
|
206
|
+
*
|
|
207
|
+
* A process is considered a wrapper when it is the parent of another candidate
|
|
208
|
+
* agent process in the same terminal/worktree, or when it points at the same
|
|
209
|
+
* terminal/worktree as an already session-matched process.
|
|
210
|
+
*/
|
|
211
|
+
export function findWrapperProcessPids(
|
|
212
|
+
processes: ProcessInfo[],
|
|
213
|
+
matchedProcesses: ProcessInfo[] = [],
|
|
214
|
+
): Set<number> {
|
|
215
|
+
const wrappers = new Set<number>();
|
|
216
|
+
|
|
217
|
+
for (const child of processes) {
|
|
218
|
+
const wrapper = findWrapperProcess(processes, child);
|
|
219
|
+
if (wrapper) {
|
|
220
|
+
wrappers.add(wrapper.pid);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const proc of processes) {
|
|
225
|
+
if (matchedProcesses.some((matched) => (
|
|
226
|
+
proc.pid !== matched.pid && isSameTerminalProcess(proc, matched)
|
|
227
|
+
))) {
|
|
228
|
+
wrappers.add(proc.pid);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return wrappers;
|
|
233
|
+
}
|
|
234
|
+
|
|
180
235
|
/**
|
|
181
236
|
* Get the TTY device for a specific process
|
|
182
237
|
*/
|