@ai-devkit/agent-manager 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/ClaudeCodeAdapter.d.ts +12 -0
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +208 -50
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +52 -0
- package/dist/adapters/CodexAdapter.d.ts.map +1 -0
- package/dist/adapters/CodexAdapter.js +432 -0
- package/dist/adapters/CodexAdapter.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +3 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/terminal/TerminalFocusManager.d.ts +7 -1
- package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
- package/dist/terminal/TerminalFocusManager.js +15 -8
- package/dist/terminal/TerminalFocusManager.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts +23 -0
- package/dist/terminal/TtyWriter.d.ts.map +1 -0
- package/dist/terminal/TtyWriter.js +106 -0
- package/dist/terminal/TtyWriter.js.map +1 -0
- package/dist/terminal/index.d.ts +2 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +5 -1
- package/dist/terminal/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +120 -2
- package/src/__tests__/adapters/CodexAdapter.test.ts +319 -0
- package/src/__tests__/terminal/TtyWriter.test.ts +154 -0
- package/src/adapters/ClaudeCodeAdapter.ts +309 -56
- package/src/adapters/CodexAdapter.ts +584 -0
- package/src/adapters/index.ts +1 -0
- package/src/index.ts +3 -1
- package/src/terminal/TerminalFocusManager.ts +15 -8
- package/src/terminal/TtyWriter.ts +112 -0
- package/src/terminal/index.ts +2 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
2
|
+
import { TtyWriter } from '../../terminal/TtyWriter';
|
|
3
|
+
import { TerminalType } from '../../terminal/TerminalFocusManager';
|
|
4
|
+
import type { TerminalLocation } from '../../terminal/TerminalFocusManager';
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
|
|
7
|
+
jest.mock('child_process', () => {
|
|
8
|
+
const actual = jest.requireActual<typeof import('child_process')>('child_process');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
execFile: jest.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const mockedExecFile = execFile as unknown as jest.Mock;
|
|
16
|
+
|
|
17
|
+
function mockExecFileSuccess(stdout = '') {
|
|
18
|
+
mockedExecFile.mockImplementation((...args: unknown[]) => {
|
|
19
|
+
const cb = args[args.length - 1] as (err: Error | null, result: { stdout: string }, stderr: string) => void;
|
|
20
|
+
cb(null, { stdout }, '');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mockExecFileError(message: string) {
|
|
25
|
+
mockedExecFile.mockImplementation((...args: unknown[]) => {
|
|
26
|
+
const cb = args[args.length - 1] as (err: Error | null, result: null, stderr: string) => void;
|
|
27
|
+
cb(new Error(message), null, '');
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('TtyWriter', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('tmux', () => {
|
|
37
|
+
const location: TerminalLocation = {
|
|
38
|
+
type: TerminalType.TMUX,
|
|
39
|
+
identifier: 'main:0.1',
|
|
40
|
+
tty: '/dev/ttys030',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
it('sends message via tmux send-keys', async () => {
|
|
44
|
+
mockExecFileSuccess();
|
|
45
|
+
|
|
46
|
+
await TtyWriter.send(location, 'continue');
|
|
47
|
+
|
|
48
|
+
expect(mockedExecFile).toHaveBeenCalledWith(
|
|
49
|
+
'tmux',
|
|
50
|
+
['send-keys', '-t', 'main:0.1', 'continue', 'Enter'],
|
|
51
|
+
expect.any(Function),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws on tmux failure', async () => {
|
|
56
|
+
mockExecFileError('tmux not running');
|
|
57
|
+
|
|
58
|
+
await expect(TtyWriter.send(location, 'hello'))
|
|
59
|
+
.rejects.toThrow('tmux not running');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('iTerm2', () => {
|
|
64
|
+
const location: TerminalLocation = {
|
|
65
|
+
type: TerminalType.ITERM2,
|
|
66
|
+
identifier: '/dev/ttys030',
|
|
67
|
+
tty: '/dev/ttys030',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
it('sends message via osascript with execFile (no shell)', async () => {
|
|
71
|
+
mockExecFileSuccess('ok');
|
|
72
|
+
|
|
73
|
+
await TtyWriter.send(location, 'hello');
|
|
74
|
+
|
|
75
|
+
expect(mockedExecFile).toHaveBeenCalledWith(
|
|
76
|
+
'osascript',
|
|
77
|
+
['-e', expect.stringContaining('write text "hello"')],
|
|
78
|
+
expect.any(Function),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('escapes special characters in message', async () => {
|
|
83
|
+
mockExecFileSuccess('ok');
|
|
84
|
+
|
|
85
|
+
await TtyWriter.send(location, 'say "hi" \\ there');
|
|
86
|
+
|
|
87
|
+
expect(mockedExecFile).toHaveBeenCalledWith(
|
|
88
|
+
'osascript',
|
|
89
|
+
['-e', expect.stringContaining('write text "say \\"hi\\" \\\\ there"')],
|
|
90
|
+
expect.any(Function),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('throws when session not found', async () => {
|
|
95
|
+
mockExecFileSuccess('not_found');
|
|
96
|
+
|
|
97
|
+
await expect(TtyWriter.send(location, 'test'))
|
|
98
|
+
.rejects.toThrow('iTerm2 session not found');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('Terminal.app', () => {
|
|
103
|
+
const location: TerminalLocation = {
|
|
104
|
+
type: TerminalType.TERMINAL_APP,
|
|
105
|
+
identifier: '/dev/ttys030',
|
|
106
|
+
tty: '/dev/ttys030',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
it('sends message via System Events keystroke (not do script)', async () => {
|
|
110
|
+
mockExecFileSuccess('ok');
|
|
111
|
+
|
|
112
|
+
await TtyWriter.send(location, 'hello');
|
|
113
|
+
|
|
114
|
+
const scriptArg = (mockedExecFile.mock.calls[0] as unknown[])[1] as string[];
|
|
115
|
+
const script = scriptArg[1];
|
|
116
|
+
// Must use keystroke, NOT do script
|
|
117
|
+
expect(script).toContain('keystroke "hello"');
|
|
118
|
+
expect(script).toContain('key code 36');
|
|
119
|
+
expect(script).not.toContain('do script');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('uses execFile to avoid shell injection', async () => {
|
|
123
|
+
mockExecFileSuccess('ok');
|
|
124
|
+
|
|
125
|
+
await TtyWriter.send(location, "don't stop");
|
|
126
|
+
|
|
127
|
+
expect(mockedExecFile).toHaveBeenCalledWith(
|
|
128
|
+
'osascript',
|
|
129
|
+
['-e', expect.any(String)],
|
|
130
|
+
expect.any(Function),
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('throws when tab not found', async () => {
|
|
135
|
+
mockExecFileSuccess('not_found');
|
|
136
|
+
|
|
137
|
+
await expect(TtyWriter.send(location, 'test'))
|
|
138
|
+
.rejects.toThrow('Terminal.app tab not found');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('unsupported terminal', () => {
|
|
143
|
+
it('throws for unknown terminal type', async () => {
|
|
144
|
+
const location: TerminalLocation = {
|
|
145
|
+
type: TerminalType.UNKNOWN,
|
|
146
|
+
identifier: '',
|
|
147
|
+
tty: '/dev/ttys030',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
await expect(TtyWriter.send(location, 'test'))
|
|
151
|
+
.rejects.toThrow('Cannot send input: unsupported terminal type');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -19,11 +19,21 @@ interface SessionsIndex {
|
|
|
19
19
|
originalPath: string;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
enum SessionEntryType {
|
|
23
|
+
ASSISTANT = 'assistant',
|
|
24
|
+
USER = 'user',
|
|
25
|
+
PROGRESS = 'progress',
|
|
26
|
+
THINKING = 'thinking',
|
|
27
|
+
SYSTEM = 'system',
|
|
28
|
+
MESSAGE = 'message',
|
|
29
|
+
TEXT = 'text',
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Entry in session JSONL file
|
|
24
34
|
*/
|
|
25
35
|
interface SessionEntry {
|
|
26
|
-
type?:
|
|
36
|
+
type?: SessionEntryType;
|
|
27
37
|
timestamp?: string;
|
|
28
38
|
slug?: string;
|
|
29
39
|
cwd?: string;
|
|
@@ -54,12 +64,15 @@ interface HistoryEntry {
|
|
|
54
64
|
interface ClaudeSession {
|
|
55
65
|
sessionId: string;
|
|
56
66
|
projectPath: string;
|
|
67
|
+
lastCwd?: string;
|
|
57
68
|
slug?: string;
|
|
58
69
|
sessionLogPath: string;
|
|
59
70
|
lastEntry?: SessionEntry;
|
|
60
71
|
lastActive?: Date;
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
type SessionMatchMode = 'cwd' | 'project-parent';
|
|
75
|
+
|
|
63
76
|
/**
|
|
64
77
|
* Claude Code Adapter
|
|
65
78
|
*
|
|
@@ -98,75 +111,274 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
98
111
|
* Detect running Claude Code agents
|
|
99
112
|
*/
|
|
100
113
|
async detectAgents(): Promise<AgentInfo[]> {
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
const claudeProcesses = listProcesses({ namePattern: 'claude' }).filter((processInfo) =>
|
|
115
|
+
this.canHandle(processInfo),
|
|
116
|
+
);
|
|
103
117
|
|
|
104
118
|
if (claudeProcesses.length === 0) {
|
|
105
119
|
return [];
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
// 2. Read all sessions
|
|
109
122
|
const sessions = this.readSessions();
|
|
110
|
-
|
|
111
|
-
// 3. Read history for summaries
|
|
112
123
|
const history = this.readHistory();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const list = processesByCwd.get(p.cwd) || [];
|
|
118
|
-
list.push(p);
|
|
119
|
-
processesByCwd.set(p.cwd, list);
|
|
124
|
+
const historyByProjectPath = this.indexHistoryByProjectPath(history);
|
|
125
|
+
const historyBySessionId = new Map<string, HistoryEntry>();
|
|
126
|
+
for (const entry of history) {
|
|
127
|
+
historyBySessionId.set(entry.sessionId, entry);
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
const sortedSessions = [...sessions].sort((a, b) => {
|
|
131
|
+
const timeA = a.lastActive?.getTime() || 0;
|
|
132
|
+
const timeB = b.lastActive?.getTime() || 0;
|
|
133
|
+
return timeB - timeA;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const usedSessionIds = new Set<string>();
|
|
137
|
+
const assignedPids = new Set<number>();
|
|
123
138
|
const agents: AgentInfo[] = [];
|
|
124
139
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
this.assignSessionsForMode(
|
|
141
|
+
'cwd',
|
|
142
|
+
claudeProcesses,
|
|
143
|
+
sortedSessions,
|
|
144
|
+
usedSessionIds,
|
|
145
|
+
assignedPids,
|
|
146
|
+
historyBySessionId,
|
|
147
|
+
agents,
|
|
148
|
+
);
|
|
149
|
+
this.assignHistoryEntriesForExactProcessCwd(
|
|
150
|
+
claudeProcesses,
|
|
151
|
+
assignedPids,
|
|
152
|
+
historyByProjectPath,
|
|
153
|
+
usedSessionIds,
|
|
154
|
+
agents,
|
|
155
|
+
);
|
|
156
|
+
this.assignSessionsForMode(
|
|
157
|
+
'project-parent',
|
|
158
|
+
claudeProcesses,
|
|
159
|
+
sortedSessions,
|
|
160
|
+
usedSessionIds,
|
|
161
|
+
assignedPids,
|
|
162
|
+
historyBySessionId,
|
|
163
|
+
agents,
|
|
164
|
+
);
|
|
165
|
+
for (const processInfo of claudeProcesses) {
|
|
166
|
+
if (assignedPids.has(processInfo.pid)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
assignedPids.add(processInfo.pid);
|
|
171
|
+
agents.push(this.mapProcessOnlyAgent(processInfo, agents, historyByProjectPath, usedSessionIds));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return agents;
|
|
175
|
+
}
|
|
128
176
|
|
|
129
|
-
|
|
177
|
+
private assignHistoryEntriesForExactProcessCwd(
|
|
178
|
+
claudeProcesses: ProcessInfo[],
|
|
179
|
+
assignedPids: Set<number>,
|
|
180
|
+
historyByProjectPath: Map<string, HistoryEntry[]>,
|
|
181
|
+
usedSessionIds: Set<string>,
|
|
182
|
+
agents: AgentInfo[],
|
|
183
|
+
): void {
|
|
184
|
+
for (const processInfo of claudeProcesses) {
|
|
185
|
+
if (assignedPids.has(processInfo.pid)) {
|
|
130
186
|
continue;
|
|
131
187
|
}
|
|
132
188
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return timeB - timeA;
|
|
138
|
-
});
|
|
189
|
+
const historyEntry = this.selectHistoryForProcess(processInfo.cwd || '', historyByProjectPath, usedSessionIds);
|
|
190
|
+
if (!historyEntry) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
139
193
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
194
|
+
assignedPids.add(processInfo.pid);
|
|
195
|
+
usedSessionIds.add(historyEntry.sessionId);
|
|
196
|
+
agents.push(this.mapHistoryToAgent(processInfo, historyEntry, agents));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
143
199
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
200
|
+
private assignSessionsForMode(
|
|
201
|
+
mode: SessionMatchMode,
|
|
202
|
+
claudeProcesses: ProcessInfo[],
|
|
203
|
+
sessions: ClaudeSession[],
|
|
204
|
+
usedSessionIds: Set<string>,
|
|
205
|
+
assignedPids: Set<number>,
|
|
206
|
+
historyBySessionId: Map<string, HistoryEntry>,
|
|
207
|
+
agents: AgentInfo[],
|
|
208
|
+
): void {
|
|
209
|
+
for (const processInfo of claudeProcesses) {
|
|
210
|
+
if (assignedPids.has(processInfo.pid)) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
147
213
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const summary = historyEntry?.display || 'Session started';
|
|
152
|
-
const status = this.determineStatus(session);
|
|
153
|
-
const agentName = this.generateAgentName(session, agents); // Pass currently built agents for collision checks
|
|
154
|
-
|
|
155
|
-
agents.push({
|
|
156
|
-
name: agentName,
|
|
157
|
-
type: this.type,
|
|
158
|
-
status,
|
|
159
|
-
summary,
|
|
160
|
-
pid: process.pid,
|
|
161
|
-
projectPath: session.projectPath,
|
|
162
|
-
sessionId: session.sessionId,
|
|
163
|
-
slug: session.slug,
|
|
164
|
-
lastActive: session.lastActive || new Date(),
|
|
165
|
-
});
|
|
214
|
+
const session = this.selectBestSession(processInfo, sessions, usedSessionIds, mode);
|
|
215
|
+
if (!session) {
|
|
216
|
+
continue;
|
|
166
217
|
}
|
|
218
|
+
|
|
219
|
+
usedSessionIds.add(session.sessionId);
|
|
220
|
+
assignedPids.add(processInfo.pid);
|
|
221
|
+
agents.push(this.mapSessionToAgent(session, processInfo, historyBySessionId, agents));
|
|
167
222
|
}
|
|
223
|
+
}
|
|
168
224
|
|
|
169
|
-
|
|
225
|
+
private selectBestSession(
|
|
226
|
+
processInfo: ProcessInfo,
|
|
227
|
+
sessions: ClaudeSession[],
|
|
228
|
+
usedSessionIds: Set<string>,
|
|
229
|
+
mode: SessionMatchMode,
|
|
230
|
+
): ClaudeSession | null {
|
|
231
|
+
const candidates = sessions.filter((session) => {
|
|
232
|
+
if (usedSessionIds.has(session.sessionId)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (mode === 'cwd') {
|
|
237
|
+
return this.pathEquals(processInfo.cwd, session.lastCwd)
|
|
238
|
+
|| this.pathEquals(processInfo.cwd, session.projectPath);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (mode === 'project-parent') {
|
|
242
|
+
return this.isChildPath(processInfo.cwd, session.projectPath)
|
|
243
|
+
|| this.isChildPath(processInfo.cwd, session.lastCwd);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return false;
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (candidates.length === 0) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (mode !== 'project-parent') {
|
|
254
|
+
return candidates[0];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return candidates.sort((a, b) => {
|
|
258
|
+
const depthA = Math.max(this.pathDepth(a.projectPath), this.pathDepth(a.lastCwd));
|
|
259
|
+
const depthB = Math.max(this.pathDepth(b.projectPath), this.pathDepth(b.lastCwd));
|
|
260
|
+
if (depthA !== depthB) {
|
|
261
|
+
return depthB - depthA;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const lastActiveA = a.lastActive?.getTime() || 0;
|
|
265
|
+
const lastActiveB = b.lastActive?.getTime() || 0;
|
|
266
|
+
return lastActiveB - lastActiveA;
|
|
267
|
+
})[0];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private mapSessionToAgent(
|
|
271
|
+
session: ClaudeSession,
|
|
272
|
+
processInfo: ProcessInfo,
|
|
273
|
+
historyBySessionId: Map<string, HistoryEntry>,
|
|
274
|
+
existingAgents: AgentInfo[],
|
|
275
|
+
): AgentInfo {
|
|
276
|
+
const historyEntry = historyBySessionId.get(session.sessionId);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
name: this.generateAgentName(session, existingAgents),
|
|
280
|
+
type: this.type,
|
|
281
|
+
status: this.determineStatus(session),
|
|
282
|
+
summary: historyEntry?.display || 'Session started',
|
|
283
|
+
pid: processInfo.pid,
|
|
284
|
+
projectPath: session.projectPath || processInfo.cwd || '',
|
|
285
|
+
sessionId: session.sessionId,
|
|
286
|
+
slug: session.slug,
|
|
287
|
+
lastActive: session.lastActive || new Date(),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private mapProcessOnlyAgent(
|
|
292
|
+
processInfo: ProcessInfo,
|
|
293
|
+
existingAgents: AgentInfo[],
|
|
294
|
+
historyByProjectPath: Map<string, HistoryEntry[]>,
|
|
295
|
+
usedSessionIds: Set<string>,
|
|
296
|
+
): AgentInfo {
|
|
297
|
+
const projectPath = processInfo.cwd || '';
|
|
298
|
+
const historyEntry = this.selectHistoryForProcess(projectPath, historyByProjectPath, usedSessionIds);
|
|
299
|
+
const sessionId = historyEntry?.sessionId || `pid-${processInfo.pid}`;
|
|
300
|
+
const lastActive = historyEntry ? new Date(historyEntry.timestamp) : new Date();
|
|
301
|
+
if (historyEntry) {
|
|
302
|
+
usedSessionIds.add(historyEntry.sessionId);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const processSession: ClaudeSession = {
|
|
306
|
+
sessionId,
|
|
307
|
+
projectPath,
|
|
308
|
+
lastCwd: projectPath,
|
|
309
|
+
sessionLogPath: '',
|
|
310
|
+
lastActive,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
name: this.generateAgentName(processSession, existingAgents),
|
|
315
|
+
type: this.type,
|
|
316
|
+
status: AgentStatus.RUNNING,
|
|
317
|
+
summary: historyEntry?.display || 'Claude process running',
|
|
318
|
+
pid: processInfo.pid,
|
|
319
|
+
projectPath,
|
|
320
|
+
sessionId: processSession.sessionId,
|
|
321
|
+
lastActive: processSession.lastActive || new Date(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private mapHistoryToAgent(
|
|
326
|
+
processInfo: ProcessInfo,
|
|
327
|
+
historyEntry: HistoryEntry,
|
|
328
|
+
existingAgents: AgentInfo[],
|
|
329
|
+
): AgentInfo {
|
|
330
|
+
const projectPath = processInfo.cwd || historyEntry.project;
|
|
331
|
+
const historySession: ClaudeSession = {
|
|
332
|
+
sessionId: historyEntry.sessionId,
|
|
333
|
+
projectPath,
|
|
334
|
+
lastCwd: projectPath,
|
|
335
|
+
sessionLogPath: '',
|
|
336
|
+
lastActive: new Date(historyEntry.timestamp),
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
name: this.generateAgentName(historySession, existingAgents),
|
|
341
|
+
type: this.type,
|
|
342
|
+
status: AgentStatus.RUNNING,
|
|
343
|
+
summary: historyEntry.display || 'Claude process running',
|
|
344
|
+
pid: processInfo.pid,
|
|
345
|
+
projectPath,
|
|
346
|
+
sessionId: historySession.sessionId,
|
|
347
|
+
lastActive: historySession.lastActive || new Date(),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private indexHistoryByProjectPath(historyEntries: HistoryEntry[]): Map<string, HistoryEntry[]> {
|
|
352
|
+
const grouped = new Map<string, HistoryEntry[]>();
|
|
353
|
+
|
|
354
|
+
for (const entry of historyEntries) {
|
|
355
|
+
const key = this.normalizePath(entry.project);
|
|
356
|
+
const list = grouped.get(key) || [];
|
|
357
|
+
list.push(entry);
|
|
358
|
+
grouped.set(key, list);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const [key, list] of grouped.entries()) {
|
|
362
|
+
grouped.set(
|
|
363
|
+
key,
|
|
364
|
+
[...list].sort((a, b) => b.timestamp - a.timestamp),
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return grouped;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private selectHistoryForProcess(
|
|
372
|
+
processCwd: string,
|
|
373
|
+
historyByProjectPath: Map<string, HistoryEntry[]>,
|
|
374
|
+
usedSessionIds: Set<string>,
|
|
375
|
+
): HistoryEntry | undefined {
|
|
376
|
+
if (!processCwd) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const candidates = historyByProjectPath.get(this.normalizePath(processCwd)) || [];
|
|
381
|
+
return candidates.find((entry) => !usedSessionIds.has(entry.sessionId));
|
|
170
382
|
}
|
|
171
383
|
|
|
172
384
|
/**
|
|
@@ -214,6 +426,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
214
426
|
sessions.push({
|
|
215
427
|
sessionId,
|
|
216
428
|
projectPath: sessionsIndex.originalPath,
|
|
429
|
+
lastCwd: sessionData.lastCwd,
|
|
217
430
|
slug: sessionData.slug,
|
|
218
431
|
sessionLogPath,
|
|
219
432
|
lastEntry: sessionData.lastEntry,
|
|
@@ -237,12 +450,14 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
237
450
|
slug?: string;
|
|
238
451
|
lastEntry?: SessionEntry;
|
|
239
452
|
lastActive?: Date;
|
|
453
|
+
lastCwd?: string;
|
|
240
454
|
} {
|
|
241
455
|
const lines = readLastLines(logPath, 100);
|
|
242
456
|
|
|
243
457
|
let slug: string | undefined;
|
|
244
458
|
let lastEntry: SessionEntry | undefined;
|
|
245
459
|
let lastActive: Date | undefined;
|
|
460
|
+
let lastCwd: string | undefined;
|
|
246
461
|
|
|
247
462
|
for (const line of lines) {
|
|
248
463
|
try {
|
|
@@ -257,12 +472,16 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
257
472
|
if (entry.timestamp) {
|
|
258
473
|
lastActive = new Date(entry.timestamp);
|
|
259
474
|
}
|
|
475
|
+
|
|
476
|
+
if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
|
|
477
|
+
lastCwd = entry.cwd;
|
|
478
|
+
}
|
|
260
479
|
} catch (error) {
|
|
261
480
|
continue;
|
|
262
481
|
}
|
|
263
482
|
}
|
|
264
483
|
|
|
265
|
-
return { slug, lastEntry, lastActive };
|
|
484
|
+
return { slug, lastEntry, lastActive, lastCwd };
|
|
266
485
|
}
|
|
267
486
|
|
|
268
487
|
/**
|
|
@@ -289,12 +508,12 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
289
508
|
return AgentStatus.IDLE;
|
|
290
509
|
}
|
|
291
510
|
|
|
292
|
-
if (entryType ===
|
|
511
|
+
if (entryType === SessionEntryType.USER) {
|
|
293
512
|
// Check if user interrupted manually - this puts agent back in waiting state
|
|
294
513
|
const content = session.lastEntry.message?.content;
|
|
295
514
|
if (Array.isArray(content)) {
|
|
296
515
|
const isInterrupted = content.some(c =>
|
|
297
|
-
(c.type ===
|
|
516
|
+
(c.type === SessionEntryType.TEXT && c.text?.includes('[Request interrupted')) ||
|
|
298
517
|
(c.type === 'tool_result' && c.content?.includes('[Request interrupted'))
|
|
299
518
|
);
|
|
300
519
|
if (isInterrupted) return AgentStatus.WAITING;
|
|
@@ -302,11 +521,11 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
302
521
|
return AgentStatus.RUNNING;
|
|
303
522
|
}
|
|
304
523
|
|
|
305
|
-
if (entryType ===
|
|
524
|
+
if (entryType === SessionEntryType.PROGRESS || entryType === SessionEntryType.THINKING) {
|
|
306
525
|
return AgentStatus.RUNNING;
|
|
307
|
-
} else if (entryType ===
|
|
526
|
+
} else if (entryType === SessionEntryType.ASSISTANT) {
|
|
308
527
|
return AgentStatus.WAITING;
|
|
309
|
-
} else if (entryType ===
|
|
528
|
+
} else if (entryType === SessionEntryType.SYSTEM) {
|
|
310
529
|
return AgentStatus.IDLE;
|
|
311
530
|
}
|
|
312
531
|
|
|
@@ -318,7 +537,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
318
537
|
* Uses project basename, appends slug if multiple sessions for same project
|
|
319
538
|
*/
|
|
320
539
|
private generateAgentName(session: ClaudeSession, existingAgents: AgentInfo[]): string {
|
|
321
|
-
const projectName = path.basename(session.projectPath);
|
|
540
|
+
const projectName = path.basename(session.projectPath) || 'claude';
|
|
322
541
|
|
|
323
542
|
const sameProjectAgents = existingAgents.filter(
|
|
324
543
|
a => a.projectPath === session.projectPath
|
|
@@ -341,4 +560,38 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
341
560
|
return `${projectName} (${session.sessionId.slice(0, 8)})`;
|
|
342
561
|
}
|
|
343
562
|
|
|
563
|
+
private pathEquals(a?: string, b?: string): boolean {
|
|
564
|
+
if (!a || !b) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return this.normalizePath(a) === this.normalizePath(b);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private isChildPath(child?: string, parent?: string): boolean {
|
|
572
|
+
if (!child || !parent) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const normalizedChild = this.normalizePath(child);
|
|
577
|
+
const normalizedParent = this.normalizePath(parent);
|
|
578
|
+
return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private normalizePath(value: string): string {
|
|
582
|
+
const resolved = path.resolve(value);
|
|
583
|
+
if (resolved.length > 1 && resolved.endsWith(path.sep)) {
|
|
584
|
+
return resolved.slice(0, -1);
|
|
585
|
+
}
|
|
586
|
+
return resolved;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private pathDepth(value?: string): number {
|
|
590
|
+
if (!value) {
|
|
591
|
+
return 0;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return this.normalizePath(value).split(path.sep).filter(Boolean).length;
|
|
595
|
+
}
|
|
596
|
+
|
|
344
597
|
}
|