@ai-devkit/agent-manager 0.3.0 → 0.4.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 +44 -28
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +383 -234
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +1 -3
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1045 -82
- package/src/__tests__/adapters/CodexAdapter.test.ts +7 -1
- package/src/adapters/ClaudeCodeAdapter.ts +498 -327
- package/src/adapters/CodexAdapter.ts +2 -13
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Claude Code Adapter
|
|
3
|
-
*
|
|
4
|
-
* Detects running Claude Code agents by reading session files
|
|
5
|
-
* from ~/.claude/ directory and correlating with running processes.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import * as fs from 'fs';
|
|
9
2
|
import * as path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
10
4
|
import type { AgentAdapter, AgentInfo, ProcessInfo } from './AgentAdapter';
|
|
11
5
|
import { AgentStatus } from './AgentAdapter';
|
|
12
6
|
import { listProcesses } from '../utils/process';
|
|
13
|
-
import {
|
|
7
|
+
import { readJson } from '../utils/file';
|
|
14
8
|
|
|
15
9
|
/**
|
|
16
10
|
* Structure of ~/.claude/projects/{path}/sessions-index.json
|
|
@@ -19,43 +13,21 @@ interface SessionsIndex {
|
|
|
19
13
|
originalPath: string;
|
|
20
14
|
}
|
|
21
15
|
|
|
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
|
-
|
|
32
16
|
/**
|
|
33
17
|
* Entry in session JSONL file
|
|
34
18
|
*/
|
|
35
19
|
interface SessionEntry {
|
|
36
|
-
type?:
|
|
20
|
+
type?: string;
|
|
37
21
|
timestamp?: string;
|
|
38
22
|
slug?: string;
|
|
39
23
|
cwd?: string;
|
|
40
|
-
sessionId?: string;
|
|
41
24
|
message?: {
|
|
42
|
-
content?: Array<{
|
|
25
|
+
content?: string | Array<{
|
|
43
26
|
type?: string;
|
|
44
27
|
text?: string;
|
|
45
28
|
content?: string;
|
|
46
29
|
}>;
|
|
47
30
|
};
|
|
48
|
-
[key: string]: unknown;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Entry in ~/.claude/history.jsonl
|
|
53
|
-
*/
|
|
54
|
-
interface HistoryEntry {
|
|
55
|
-
display: string;
|
|
56
|
-
timestamp: number;
|
|
57
|
-
project: string;
|
|
58
|
-
sessionId: string;
|
|
59
31
|
}
|
|
60
32
|
|
|
61
33
|
/**
|
|
@@ -66,135 +38,122 @@ interface ClaudeSession {
|
|
|
66
38
|
projectPath: string;
|
|
67
39
|
lastCwd?: string;
|
|
68
40
|
slug?: string;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
41
|
+
sessionStart: Date;
|
|
42
|
+
lastActive: Date;
|
|
43
|
+
lastEntryType?: string;
|
|
44
|
+
isInterrupted: boolean;
|
|
45
|
+
lastUserMessage?: string;
|
|
72
46
|
}
|
|
73
47
|
|
|
74
|
-
type SessionMatchMode = 'cwd' | '
|
|
48
|
+
type SessionMatchMode = 'cwd' | 'missing-cwd' | 'parent-child';
|
|
75
49
|
|
|
76
50
|
/**
|
|
77
51
|
* Claude Code Adapter
|
|
78
|
-
*
|
|
52
|
+
*
|
|
79
53
|
* Detects Claude Code agents by:
|
|
80
54
|
* 1. Finding running claude processes
|
|
81
|
-
* 2.
|
|
82
|
-
* 3.
|
|
83
|
-
* 4.
|
|
84
|
-
* 5. Extracting summary from
|
|
55
|
+
* 2. Getting process start times for accurate session matching
|
|
56
|
+
* 3. Reading bounded session files from ~/.claude/projects/
|
|
57
|
+
* 4. Matching sessions to processes via CWD then start time ranking
|
|
58
|
+
* 5. Extracting summary from last user message in session JSONL
|
|
85
59
|
*/
|
|
86
60
|
export class ClaudeCodeAdapter implements AgentAdapter {
|
|
87
61
|
readonly type = 'claude' as const;
|
|
88
62
|
|
|
89
|
-
/**
|
|
90
|
-
private static readonly
|
|
63
|
+
/** Limit session parsing per run to keep list latency bounded. */
|
|
64
|
+
private static readonly MIN_SESSION_SCAN = 12;
|
|
65
|
+
private static readonly MAX_SESSION_SCAN = 40;
|
|
66
|
+
private static readonly SESSION_SCAN_MULTIPLIER = 4;
|
|
67
|
+
/** Matching tolerance between process start time and session start time. */
|
|
68
|
+
private static readonly PROCESS_SESSION_TIME_TOLERANCE_MS = 2 * 60 * 1000;
|
|
91
69
|
|
|
92
|
-
private claudeDir: string;
|
|
93
70
|
private projectsDir: string;
|
|
94
|
-
private historyPath: string;
|
|
95
71
|
|
|
96
72
|
constructor() {
|
|
97
73
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
98
|
-
this.
|
|
99
|
-
this.projectsDir = path.join(this.claudeDir, 'projects');
|
|
100
|
-
this.historyPath = path.join(this.claudeDir, 'history.jsonl');
|
|
74
|
+
this.projectsDir = path.join(homeDir, '.claude', 'projects');
|
|
101
75
|
}
|
|
102
76
|
|
|
103
77
|
/**
|
|
104
78
|
* Check if this adapter can handle a given process
|
|
105
79
|
*/
|
|
106
80
|
canHandle(processInfo: ProcessInfo): boolean {
|
|
107
|
-
return processInfo.command
|
|
81
|
+
return this.isClaudeExecutable(processInfo.command);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private isClaudeExecutable(command: string): boolean {
|
|
85
|
+
const executable = command.trim().split(/\s+/)[0] || '';
|
|
86
|
+
const base = path.basename(executable).toLowerCase();
|
|
87
|
+
return base === 'claude' || base === 'claude.exe';
|
|
108
88
|
}
|
|
109
89
|
|
|
110
90
|
/**
|
|
111
91
|
* Detect running Claude Code agents
|
|
112
92
|
*/
|
|
113
93
|
async detectAgents(): Promise<AgentInfo[]> {
|
|
114
|
-
const claudeProcesses =
|
|
115
|
-
this.canHandle(processInfo),
|
|
116
|
-
);
|
|
117
|
-
|
|
94
|
+
const claudeProcesses = this.listClaudeProcesses();
|
|
118
95
|
if (claudeProcesses.length === 0) {
|
|
119
96
|
return [];
|
|
120
97
|
}
|
|
121
98
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
historyBySessionId.set(entry.sessionId, entry);
|
|
128
|
-
}
|
|
99
|
+
const processStartByPid = this.getProcessStartTimes(
|
|
100
|
+
claudeProcesses.map((p) => p.pid),
|
|
101
|
+
);
|
|
102
|
+
const sessionScanLimit = this.calculateSessionScanLimit(claudeProcesses.length);
|
|
103
|
+
const sessions = this.readSessions(sessionScanLimit);
|
|
129
104
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
105
|
+
if (sessions.length === 0) {
|
|
106
|
+
return claudeProcesses.map((p) =>
|
|
107
|
+
this.mapProcessOnlyAgent(p, []),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
135
110
|
|
|
111
|
+
const sortedSessions = [...sessions].sort(
|
|
112
|
+
(a, b) => b.lastActive.getTime() - a.lastActive.getTime(),
|
|
113
|
+
);
|
|
136
114
|
const usedSessionIds = new Set<string>();
|
|
137
115
|
const assignedPids = new Set<number>();
|
|
138
116
|
const agents: AgentInfo[] = [];
|
|
139
117
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
usedSessionIds,
|
|
154
|
-
agents,
|
|
155
|
-
);
|
|
156
|
-
this.assignSessionsForMode(
|
|
157
|
-
'project-parent',
|
|
158
|
-
claudeProcesses,
|
|
159
|
-
sortedSessions,
|
|
160
|
-
usedSessionIds,
|
|
161
|
-
assignedPids,
|
|
162
|
-
historyBySessionId,
|
|
163
|
-
agents,
|
|
164
|
-
);
|
|
118
|
+
const modes: SessionMatchMode[] = ['cwd', 'missing-cwd', 'parent-child'];
|
|
119
|
+
for (const mode of modes) {
|
|
120
|
+
this.assignSessionsForMode(
|
|
121
|
+
mode,
|
|
122
|
+
claudeProcesses,
|
|
123
|
+
sortedSessions,
|
|
124
|
+
usedSessionIds,
|
|
125
|
+
assignedPids,
|
|
126
|
+
processStartByPid,
|
|
127
|
+
agents,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
165
131
|
for (const processInfo of claudeProcesses) {
|
|
166
132
|
if (assignedPids.has(processInfo.pid)) {
|
|
167
133
|
continue;
|
|
168
134
|
}
|
|
169
135
|
|
|
170
136
|
assignedPids.add(processInfo.pid);
|
|
171
|
-
agents.push(this.mapProcessOnlyAgent(processInfo, agents
|
|
137
|
+
agents.push(this.mapProcessOnlyAgent(processInfo, agents));
|
|
172
138
|
}
|
|
173
139
|
|
|
174
140
|
return agents;
|
|
175
141
|
}
|
|
176
142
|
|
|
177
|
-
private
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
agents: AgentInfo[],
|
|
183
|
-
): void {
|
|
184
|
-
for (const processInfo of claudeProcesses) {
|
|
185
|
-
if (assignedPids.has(processInfo.pid)) {
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const historyEntry = this.selectHistoryForProcess(processInfo.cwd || '', historyByProjectPath, usedSessionIds);
|
|
190
|
-
if (!historyEntry) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
143
|
+
private listClaudeProcesses(): ProcessInfo[] {
|
|
144
|
+
return listProcesses({ namePattern: 'claude' }).filter((p) =>
|
|
145
|
+
this.canHandle(p),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
193
148
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
149
|
+
private calculateSessionScanLimit(processCount: number): number {
|
|
150
|
+
return Math.min(
|
|
151
|
+
Math.max(
|
|
152
|
+
processCount * ClaudeCodeAdapter.SESSION_SCAN_MULTIPLIER,
|
|
153
|
+
ClaudeCodeAdapter.MIN_SESSION_SCAN,
|
|
154
|
+
),
|
|
155
|
+
ClaudeCodeAdapter.MAX_SESSION_SCAN,
|
|
156
|
+
);
|
|
198
157
|
}
|
|
199
158
|
|
|
200
159
|
private assignSessionsForMode(
|
|
@@ -203,7 +162,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
203
162
|
sessions: ClaudeSession[],
|
|
204
163
|
usedSessionIds: Set<string>,
|
|
205
164
|
assignedPids: Set<number>,
|
|
206
|
-
|
|
165
|
+
processStartByPid: Map<number, Date>,
|
|
207
166
|
agents: AgentInfo[],
|
|
208
167
|
): void {
|
|
209
168
|
for (const processInfo of claudeProcesses) {
|
|
@@ -211,321 +170,457 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
211
170
|
continue;
|
|
212
171
|
}
|
|
213
172
|
|
|
214
|
-
const session = this.selectBestSession(
|
|
173
|
+
const session = this.selectBestSession(
|
|
174
|
+
processInfo,
|
|
175
|
+
sessions,
|
|
176
|
+
usedSessionIds,
|
|
177
|
+
processStartByPid,
|
|
178
|
+
mode,
|
|
179
|
+
);
|
|
215
180
|
if (!session) {
|
|
216
181
|
continue;
|
|
217
182
|
}
|
|
218
183
|
|
|
219
184
|
usedSessionIds.add(session.sessionId);
|
|
220
185
|
assignedPids.add(processInfo.pid);
|
|
221
|
-
agents.push(this.mapSessionToAgent(session, processInfo,
|
|
186
|
+
agents.push(this.mapSessionToAgent(session, processInfo, agents));
|
|
222
187
|
}
|
|
223
188
|
}
|
|
224
189
|
|
|
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
190
|
private mapSessionToAgent(
|
|
271
191
|
session: ClaudeSession,
|
|
272
192
|
processInfo: ProcessInfo,
|
|
273
|
-
historyBySessionId: Map<string, HistoryEntry>,
|
|
274
193
|
existingAgents: AgentInfo[],
|
|
275
194
|
): AgentInfo {
|
|
276
|
-
const historyEntry = historyBySessionId.get(session.sessionId);
|
|
277
|
-
|
|
278
195
|
return {
|
|
279
196
|
name: this.generateAgentName(session, existingAgents),
|
|
280
197
|
type: this.type,
|
|
281
198
|
status: this.determineStatus(session),
|
|
282
|
-
summary:
|
|
199
|
+
summary: session.lastUserMessage || 'Session started',
|
|
283
200
|
pid: processInfo.pid,
|
|
284
201
|
projectPath: session.projectPath || processInfo.cwd || '',
|
|
285
202
|
sessionId: session.sessionId,
|
|
286
203
|
slug: session.slug,
|
|
287
|
-
lastActive: session.lastActive
|
|
204
|
+
lastActive: session.lastActive,
|
|
288
205
|
};
|
|
289
206
|
}
|
|
290
207
|
|
|
291
208
|
private mapProcessOnlyAgent(
|
|
292
209
|
processInfo: ProcessInfo,
|
|
293
210
|
existingAgents: AgentInfo[],
|
|
294
|
-
historyByProjectPath: Map<string, HistoryEntry[]>,
|
|
295
|
-
usedSessionIds: Set<string>,
|
|
296
211
|
): AgentInfo {
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
const
|
|
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
|
-
};
|
|
212
|
+
const processCwd = processInfo.cwd || '';
|
|
213
|
+
const projectName = path.basename(processCwd) || 'claude';
|
|
214
|
+
const hasDuplicate = existingAgents.some((a) => a.projectPath === processCwd);
|
|
312
215
|
|
|
313
216
|
return {
|
|
314
|
-
name:
|
|
217
|
+
name: hasDuplicate ? `${projectName} (pid-${processInfo.pid})` : projectName,
|
|
315
218
|
type: this.type,
|
|
316
|
-
status: AgentStatus.
|
|
317
|
-
summary:
|
|
219
|
+
status: AgentStatus.IDLE,
|
|
220
|
+
summary: 'Unknown',
|
|
318
221
|
pid: processInfo.pid,
|
|
319
|
-
projectPath,
|
|
320
|
-
sessionId:
|
|
321
|
-
lastActive:
|
|
222
|
+
projectPath: processCwd,
|
|
223
|
+
sessionId: `pid-${processInfo.pid}`,
|
|
224
|
+
lastActive: new Date(),
|
|
322
225
|
};
|
|
323
226
|
}
|
|
324
227
|
|
|
325
|
-
private
|
|
228
|
+
private selectBestSession(
|
|
326
229
|
processInfo: ProcessInfo,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
230
|
+
sessions: ClaudeSession[],
|
|
231
|
+
usedSessionIds: Set<string>,
|
|
232
|
+
processStartByPid: Map<number, Date>,
|
|
233
|
+
mode: SessionMatchMode,
|
|
234
|
+
): ClaudeSession | undefined {
|
|
235
|
+
const candidates = this.filterCandidateSessions(
|
|
236
|
+
processInfo,
|
|
237
|
+
sessions,
|
|
238
|
+
usedSessionIds,
|
|
239
|
+
mode,
|
|
240
|
+
);
|
|
338
241
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
}
|
|
242
|
+
if (candidates.length === 0) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
350
245
|
|
|
351
|
-
|
|
352
|
-
|
|
246
|
+
const processStart = processStartByPid.get(processInfo.pid);
|
|
247
|
+
if (!processStart) {
|
|
248
|
+
return candidates.sort(
|
|
249
|
+
(a, b) => b.lastActive.getTime() - a.lastActive.getTime(),
|
|
250
|
+
)[0];
|
|
251
|
+
}
|
|
353
252
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
list.push(entry);
|
|
358
|
-
grouped.set(key, list);
|
|
253
|
+
const best = this.rankCandidatesByStartTime(candidates, processStart)[0];
|
|
254
|
+
if (!best) {
|
|
255
|
+
return undefined;
|
|
359
256
|
}
|
|
360
257
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
258
|
+
// In early modes (cwd/missing-cwd), defer assignment when the best
|
|
259
|
+
// candidate is outside start-time tolerance — a closer match may
|
|
260
|
+
// exist in parent-child mode (e.g., worktree sessions).
|
|
261
|
+
if (mode !== 'parent-child') {
|
|
262
|
+
const diffMs = Math.abs(
|
|
263
|
+
best.sessionStart.getTime() - processStart.getTime(),
|
|
365
264
|
);
|
|
265
|
+
if (diffMs > ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
366
268
|
}
|
|
367
269
|
|
|
368
|
-
return
|
|
270
|
+
return best;
|
|
369
271
|
}
|
|
370
272
|
|
|
371
|
-
private
|
|
372
|
-
|
|
373
|
-
|
|
273
|
+
private filterCandidateSessions(
|
|
274
|
+
processInfo: ProcessInfo,
|
|
275
|
+
sessions: ClaudeSession[],
|
|
374
276
|
usedSessionIds: Set<string>,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
277
|
+
mode: SessionMatchMode,
|
|
278
|
+
): ClaudeSession[] {
|
|
279
|
+
return sessions.filter((session) => {
|
|
280
|
+
if (usedSessionIds.has(session.sessionId)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (mode === 'cwd') {
|
|
285
|
+
return (
|
|
286
|
+
this.pathEquals(processInfo.cwd, session.projectPath) ||
|
|
287
|
+
this.pathEquals(processInfo.cwd, session.lastCwd)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (mode === 'missing-cwd') {
|
|
292
|
+
return !session.projectPath;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// parent-child mode: match if process CWD equals, is under, or is
|
|
296
|
+
// a parent of session project/lastCwd. This also catches exact CWD
|
|
297
|
+
// matches that were deferred from `cwd` mode due to start-time tolerance.
|
|
298
|
+
return (
|
|
299
|
+
this.pathRelated(processInfo.cwd, session.projectPath) ||
|
|
300
|
+
this.pathRelated(processInfo.cwd, session.lastCwd)
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private rankCandidatesByStartTime(
|
|
306
|
+
candidates: ClaudeSession[],
|
|
307
|
+
processStart: Date,
|
|
308
|
+
): ClaudeSession[] {
|
|
309
|
+
const toleranceMs = ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
|
|
310
|
+
|
|
311
|
+
return candidates
|
|
312
|
+
.map((session) => {
|
|
313
|
+
const diffMs = Math.abs(
|
|
314
|
+
session.sessionStart.getTime() - processStart.getTime(),
|
|
315
|
+
);
|
|
316
|
+
const outsideTolerance = diffMs > toleranceMs ? 1 : 0;
|
|
317
|
+
return {
|
|
318
|
+
session,
|
|
319
|
+
rank: outsideTolerance,
|
|
320
|
+
diffMs,
|
|
321
|
+
recency: session.lastActive.getTime(),
|
|
322
|
+
};
|
|
323
|
+
})
|
|
324
|
+
.sort((a, b) => {
|
|
325
|
+
if (a.rank !== b.rank) return a.rank - b.rank;
|
|
326
|
+
// Within tolerance (rank 0): prefer most recently active session.
|
|
327
|
+
// The exact diff is noise — a 6s vs 45s difference is meaningless,
|
|
328
|
+
// but the session with recent activity is more likely the real one.
|
|
329
|
+
if (a.rank === 0) return b.recency - a.recency;
|
|
330
|
+
// Outside tolerance: prefer smallest time difference, then recency.
|
|
331
|
+
if (a.diffMs !== b.diffMs) return a.diffMs - b.diffMs;
|
|
332
|
+
return b.recency - a.recency;
|
|
333
|
+
})
|
|
334
|
+
.map((ranked) => ranked.session);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private getProcessStartTimes(pids: number[]): Map<number, Date> {
|
|
338
|
+
if (pids.length === 0 || process.env.JEST_WORKER_ID) {
|
|
339
|
+
return new Map();
|
|
378
340
|
}
|
|
379
341
|
|
|
380
|
-
|
|
381
|
-
|
|
342
|
+
try {
|
|
343
|
+
const output = execSync(
|
|
344
|
+
`ps -o pid=,etime= -p ${pids.join(',')}`,
|
|
345
|
+
{ encoding: 'utf-8' },
|
|
346
|
+
);
|
|
347
|
+
const nowMs = Date.now();
|
|
348
|
+
const startTimes = new Map<number, Date>();
|
|
349
|
+
|
|
350
|
+
for (const rawLine of output.split('\n')) {
|
|
351
|
+
const line = rawLine.trim();
|
|
352
|
+
if (!line) continue;
|
|
353
|
+
|
|
354
|
+
const parts = line.split(/\s+/);
|
|
355
|
+
if (parts.length < 2) continue;
|
|
356
|
+
|
|
357
|
+
const pid = Number.parseInt(parts[0], 10);
|
|
358
|
+
const elapsedSeconds = this.parseElapsedSeconds(parts[1]);
|
|
359
|
+
if (!Number.isFinite(pid) || elapsedSeconds === null) continue;
|
|
360
|
+
|
|
361
|
+
startTimes.set(pid, new Date(nowMs - elapsedSeconds * 1000));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return startTimes;
|
|
365
|
+
} catch {
|
|
366
|
+
return new Map();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private parseElapsedSeconds(etime: string): number | null {
|
|
371
|
+
const match = etime
|
|
372
|
+
.trim()
|
|
373
|
+
.match(/^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
|
|
374
|
+
if (!match) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const days = Number.parseInt(match[1] || '0', 10);
|
|
379
|
+
const hours = Number.parseInt(match[2] || '0', 10);
|
|
380
|
+
const minutes = Number.parseInt(match[3] || '0', 10);
|
|
381
|
+
const seconds = Number.parseInt(match[4] || '0', 10);
|
|
382
|
+
|
|
383
|
+
return ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
|
|
382
384
|
}
|
|
383
385
|
|
|
384
386
|
/**
|
|
385
|
-
* Read
|
|
387
|
+
* Read Claude Code sessions with bounded scanning
|
|
386
388
|
*/
|
|
387
|
-
private readSessions(): ClaudeSession[] {
|
|
389
|
+
private readSessions(limit: number): ClaudeSession[] {
|
|
390
|
+
const sessionFiles = this.findSessionFiles(limit);
|
|
391
|
+
const sessions: ClaudeSession[] = [];
|
|
392
|
+
|
|
393
|
+
for (const file of sessionFiles) {
|
|
394
|
+
try {
|
|
395
|
+
const session = this.readSession(file.filePath, file.projectPath);
|
|
396
|
+
if (session) {
|
|
397
|
+
sessions.push(session);
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
console.error(`Failed to parse Claude session ${file.filePath}:`, error);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return sessions;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Find session files bounded by mtime, sorted most-recent first
|
|
409
|
+
*/
|
|
410
|
+
private findSessionFiles(
|
|
411
|
+
limit: number,
|
|
412
|
+
): Array<{ filePath: string; projectPath: string; mtimeMs: number }> {
|
|
388
413
|
if (!fs.existsSync(this.projectsDir)) {
|
|
389
414
|
return [];
|
|
390
415
|
}
|
|
391
416
|
|
|
392
|
-
const
|
|
393
|
-
|
|
417
|
+
const files: Array<{
|
|
418
|
+
filePath: string;
|
|
419
|
+
projectPath: string;
|
|
420
|
+
mtimeMs: number;
|
|
421
|
+
}> = [];
|
|
394
422
|
|
|
395
|
-
for (const dirName of
|
|
423
|
+
for (const dirName of fs.readdirSync(this.projectsDir)) {
|
|
396
424
|
if (dirName.startsWith('.')) {
|
|
397
425
|
continue;
|
|
398
426
|
}
|
|
399
427
|
|
|
400
428
|
const projectDir = path.join(this.projectsDir, dirName);
|
|
401
|
-
|
|
429
|
+
try {
|
|
430
|
+
if (!fs.statSync(projectDir).isDirectory()) continue;
|
|
431
|
+
} catch {
|
|
402
432
|
continue;
|
|
403
433
|
}
|
|
404
434
|
|
|
405
|
-
// Read sessions-index.json to get original project path
|
|
406
435
|
const indexPath = path.join(projectDir, 'sessions-index.json');
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const sessionsIndex = readJson<SessionsIndex>(indexPath);
|
|
412
|
-
if (!sessionsIndex) {
|
|
413
|
-
console.error(`Failed to parse ${indexPath}`);
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
436
|
+
const index = readJson<SessionsIndex>(indexPath);
|
|
437
|
+
const projectPath = index?.originalPath || '';
|
|
416
438
|
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
const sessionLogPath = path.join(projectDir, sessionFile);
|
|
439
|
+
for (const entry of fs.readdirSync(projectDir)) {
|
|
440
|
+
if (!entry.endsWith('.jsonl')) {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
422
443
|
|
|
444
|
+
const filePath = path.join(projectDir, entry);
|
|
423
445
|
try {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
projectPath: sessionsIndex.originalPath,
|
|
429
|
-
lastCwd: sessionData.lastCwd,
|
|
430
|
-
slug: sessionData.slug,
|
|
431
|
-
sessionLogPath,
|
|
432
|
-
lastEntry: sessionData.lastEntry,
|
|
433
|
-
lastActive: sessionData.lastActive,
|
|
446
|
+
files.push({
|
|
447
|
+
filePath,
|
|
448
|
+
projectPath,
|
|
449
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
434
450
|
});
|
|
435
|
-
} catch
|
|
436
|
-
console.error(`Failed to read session ${sessionId}:`, error);
|
|
451
|
+
} catch {
|
|
437
452
|
continue;
|
|
438
453
|
}
|
|
439
454
|
}
|
|
440
455
|
}
|
|
441
456
|
|
|
442
|
-
|
|
457
|
+
// Ensure breadth: include at least the most recent session per project,
|
|
458
|
+
// then fill remaining slots with globally most-recent sessions.
|
|
459
|
+
const sorted = files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
460
|
+
const result: typeof files = [];
|
|
461
|
+
const seenProjects = new Set<string>();
|
|
462
|
+
|
|
463
|
+
// First pass: one most-recent session per project directory
|
|
464
|
+
for (const file of sorted) {
|
|
465
|
+
const projDir = path.dirname(file.filePath);
|
|
466
|
+
if (!seenProjects.has(projDir)) {
|
|
467
|
+
seenProjects.add(projDir);
|
|
468
|
+
result.push(file);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Second pass: fill remaining slots with globally most-recent
|
|
473
|
+
if (result.length < limit) {
|
|
474
|
+
const resultSet = new Set(result.map((f) => f.filePath));
|
|
475
|
+
for (const file of sorted) {
|
|
476
|
+
if (result.length >= limit) break;
|
|
477
|
+
if (!resultSet.has(file.filePath)) {
|
|
478
|
+
result.push(file);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return result.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
443
484
|
}
|
|
444
485
|
|
|
445
486
|
/**
|
|
446
|
-
*
|
|
447
|
-
* Only reads last 100 lines for performance with large files
|
|
487
|
+
* Parse a single session file into ClaudeSession
|
|
448
488
|
*/
|
|
449
|
-
private
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
} {
|
|
455
|
-
const lines = readLastLines(logPath, 100);
|
|
489
|
+
private readSession(
|
|
490
|
+
filePath: string,
|
|
491
|
+
projectPath: string,
|
|
492
|
+
): ClaudeSession | null {
|
|
493
|
+
const sessionId = path.basename(filePath, '.jsonl');
|
|
456
494
|
|
|
495
|
+
let content: string;
|
|
496
|
+
try {
|
|
497
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
498
|
+
} catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const allLines = content.trim().split('\n');
|
|
503
|
+
if (allLines.length === 0) {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Parse first line for sessionStart.
|
|
508
|
+
// Claude Code may emit a "file-history-snapshot" as the first entry, which
|
|
509
|
+
// stores its timestamp inside "snapshot.timestamp" rather than at the root.
|
|
510
|
+
let sessionStart: Date | null = null;
|
|
511
|
+
try {
|
|
512
|
+
const firstEntry = JSON.parse(allLines[0]);
|
|
513
|
+
const rawTs: string | undefined =
|
|
514
|
+
firstEntry.timestamp || firstEntry.snapshot?.timestamp;
|
|
515
|
+
if (rawTs) {
|
|
516
|
+
const ts = new Date(rawTs);
|
|
517
|
+
if (!Number.isNaN(ts.getTime())) {
|
|
518
|
+
sessionStart = ts;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
/* skip */
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Parse all lines for session state (file already in memory)
|
|
457
526
|
let slug: string | undefined;
|
|
458
|
-
let
|
|
527
|
+
let lastEntryType: string | undefined;
|
|
459
528
|
let lastActive: Date | undefined;
|
|
460
529
|
let lastCwd: string | undefined;
|
|
530
|
+
let isInterrupted = false;
|
|
531
|
+
let lastUserMessage: string | undefined;
|
|
461
532
|
|
|
462
|
-
for (const line of
|
|
533
|
+
for (const line of allLines) {
|
|
463
534
|
try {
|
|
464
535
|
const entry: SessionEntry = JSON.parse(line);
|
|
465
536
|
|
|
466
|
-
if (entry.
|
|
467
|
-
|
|
537
|
+
if (entry.timestamp) {
|
|
538
|
+
const ts = new Date(entry.timestamp);
|
|
539
|
+
if (!Number.isNaN(ts.getTime())) {
|
|
540
|
+
lastActive = ts;
|
|
541
|
+
}
|
|
468
542
|
}
|
|
469
543
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (entry.timestamp) {
|
|
473
|
-
lastActive = new Date(entry.timestamp);
|
|
544
|
+
if (entry.slug && !slug) {
|
|
545
|
+
slug = entry.slug;
|
|
474
546
|
}
|
|
475
547
|
|
|
476
548
|
if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
|
|
477
549
|
lastCwd = entry.cwd;
|
|
478
550
|
}
|
|
479
|
-
|
|
551
|
+
|
|
552
|
+
if (entry.type && !this.isMetadataEntryType(entry.type)) {
|
|
553
|
+
lastEntryType = entry.type;
|
|
554
|
+
|
|
555
|
+
if (entry.type === 'user') {
|
|
556
|
+
const msgContent = entry.message?.content;
|
|
557
|
+
isInterrupted =
|
|
558
|
+
Array.isArray(msgContent) &&
|
|
559
|
+
msgContent.some(
|
|
560
|
+
(c) =>
|
|
561
|
+
(c.type === 'text' &&
|
|
562
|
+
c.text?.includes('[Request interrupted')) ||
|
|
563
|
+
(c.type === 'tool_result' &&
|
|
564
|
+
c.content?.includes('[Request interrupted')),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
// Extract user message text for summary fallback
|
|
568
|
+
const text = this.extractUserMessageText(msgContent);
|
|
569
|
+
if (text) {
|
|
570
|
+
lastUserMessage = text;
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
isInterrupted = false;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
} catch {
|
|
480
577
|
continue;
|
|
481
578
|
}
|
|
482
579
|
}
|
|
483
580
|
|
|
484
|
-
return {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
581
|
+
return {
|
|
582
|
+
sessionId,
|
|
583
|
+
projectPath: projectPath || lastCwd || '',
|
|
584
|
+
lastCwd,
|
|
585
|
+
slug,
|
|
586
|
+
sessionStart: sessionStart || lastActive || new Date(),
|
|
587
|
+
lastActive: lastActive || new Date(),
|
|
588
|
+
lastEntryType,
|
|
589
|
+
isInterrupted,
|
|
590
|
+
lastUserMessage,
|
|
591
|
+
};
|
|
493
592
|
}
|
|
494
593
|
|
|
495
594
|
/**
|
|
496
|
-
* Determine agent status from session
|
|
595
|
+
* Determine agent status from session state
|
|
497
596
|
*/
|
|
498
597
|
private determineStatus(session: ClaudeSession): AgentStatus {
|
|
499
|
-
if (!session.
|
|
598
|
+
if (!session.lastEntryType) {
|
|
500
599
|
return AgentStatus.UNKNOWN;
|
|
501
600
|
}
|
|
502
601
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
602
|
+
// No age-based IDLE override: every agent in the list is backed by
|
|
603
|
+
// a running process (found via ps), so the entry type is the best
|
|
604
|
+
// indicator of actual state.
|
|
506
605
|
|
|
507
|
-
if (
|
|
508
|
-
return
|
|
606
|
+
if (session.lastEntryType === 'user') {
|
|
607
|
+
return session.isInterrupted
|
|
608
|
+
? AgentStatus.WAITING
|
|
609
|
+
: AgentStatus.RUNNING;
|
|
509
610
|
}
|
|
510
611
|
|
|
511
|
-
if (
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const isInterrupted = content.some(c =>
|
|
516
|
-
(c.type === SessionEntryType.TEXT && c.text?.includes('[Request interrupted')) ||
|
|
517
|
-
(c.type === 'tool_result' && c.content?.includes('[Request interrupted'))
|
|
518
|
-
);
|
|
519
|
-
if (isInterrupted) return AgentStatus.WAITING;
|
|
520
|
-
}
|
|
612
|
+
if (
|
|
613
|
+
session.lastEntryType === 'progress' ||
|
|
614
|
+
session.lastEntryType === 'thinking'
|
|
615
|
+
) {
|
|
521
616
|
return AgentStatus.RUNNING;
|
|
522
617
|
}
|
|
523
618
|
|
|
524
|
-
if (
|
|
525
|
-
return AgentStatus.RUNNING;
|
|
526
|
-
} else if (entryType === SessionEntryType.ASSISTANT) {
|
|
619
|
+
if (session.lastEntryType === 'assistant') {
|
|
527
620
|
return AgentStatus.WAITING;
|
|
528
|
-
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (session.lastEntryType === 'system') {
|
|
529
624
|
return AgentStatus.IDLE;
|
|
530
625
|
}
|
|
531
626
|
|
|
@@ -536,30 +631,35 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
536
631
|
* Generate unique agent name
|
|
537
632
|
* Uses project basename, appends slug if multiple sessions for same project
|
|
538
633
|
*/
|
|
539
|
-
private generateAgentName(
|
|
634
|
+
private generateAgentName(
|
|
635
|
+
session: ClaudeSession,
|
|
636
|
+
existingAgents: AgentInfo[],
|
|
637
|
+
): string {
|
|
540
638
|
const projectName = path.basename(session.projectPath) || 'claude';
|
|
541
639
|
|
|
542
640
|
const sameProjectAgents = existingAgents.filter(
|
|
543
|
-
a => a.projectPath === session.projectPath
|
|
641
|
+
(a) => a.projectPath === session.projectPath,
|
|
544
642
|
);
|
|
545
643
|
|
|
546
644
|
if (sameProjectAgents.length === 0) {
|
|
547
645
|
return projectName;
|
|
548
646
|
}
|
|
549
647
|
|
|
550
|
-
// Multiple sessions for same project, append slug
|
|
551
648
|
if (session.slug) {
|
|
552
|
-
// Use first word of slug for brevity (with safety check for format)
|
|
553
649
|
const slugPart = session.slug.includes('-')
|
|
554
650
|
? session.slug.split('-')[0]
|
|
555
651
|
: session.slug.slice(0, 8);
|
|
556
652
|
return `${projectName} (${slugPart})`;
|
|
557
653
|
}
|
|
558
654
|
|
|
559
|
-
// No slug available, use session ID prefix
|
|
560
655
|
return `${projectName} (${session.sessionId.slice(0, 8)})`;
|
|
561
656
|
}
|
|
562
657
|
|
|
658
|
+
/** Check if two paths are equal, or one is a parent/child of the other. */
|
|
659
|
+
private pathRelated(a?: string, b?: string): boolean {
|
|
660
|
+
return this.pathEquals(a, b) || this.isChildPath(a, b) || this.isChildPath(b, a);
|
|
661
|
+
}
|
|
662
|
+
|
|
563
663
|
private pathEquals(a?: string, b?: string): boolean {
|
|
564
664
|
if (!a || !b) {
|
|
565
665
|
return false;
|
|
@@ -575,23 +675,94 @@ export class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
575
675
|
|
|
576
676
|
const normalizedChild = this.normalizePath(child);
|
|
577
677
|
const normalizedParent = this.normalizePath(parent);
|
|
578
|
-
return normalizedChild
|
|
678
|
+
return normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
|
|
579
679
|
}
|
|
580
680
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Extract meaningful text from a user message content.
|
|
683
|
+
* Handles string and array formats, skill command expansion, and noise filtering.
|
|
684
|
+
*/
|
|
685
|
+
private extractUserMessageText(
|
|
686
|
+
content: string | Array<{ type?: string; text?: string }> | undefined,
|
|
687
|
+
): string | undefined {
|
|
688
|
+
if (!content) {
|
|
689
|
+
return undefined;
|
|
585
690
|
}
|
|
586
|
-
|
|
691
|
+
|
|
692
|
+
let raw: string | undefined;
|
|
693
|
+
|
|
694
|
+
if (typeof content === 'string') {
|
|
695
|
+
raw = content.trim();
|
|
696
|
+
} else if (Array.isArray(content)) {
|
|
697
|
+
for (const block of content) {
|
|
698
|
+
if (block.type === 'text' && block.text?.trim()) {
|
|
699
|
+
raw = block.text.trim();
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (!raw) {
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Skill slash-command: extract /command-name and args
|
|
710
|
+
if (raw.startsWith('<command-message>')) {
|
|
711
|
+
return this.parseCommandMessage(raw);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Expanded skill content: extract ARGUMENTS line if present, skip otherwise
|
|
715
|
+
if (raw.startsWith('Base directory for this skill:')) {
|
|
716
|
+
const argsMatch = raw.match(/\nARGUMENTS:\s*(.+)/);
|
|
717
|
+
return argsMatch?.[1]?.trim() || undefined;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Filter noise
|
|
721
|
+
if (this.isNoiseMessage(raw)) {
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return raw;
|
|
587
726
|
}
|
|
588
727
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
728
|
+
/**
|
|
729
|
+
* Parse a <command-message> string into "/command args" format.
|
|
730
|
+
*/
|
|
731
|
+
private parseCommandMessage(raw: string): string | undefined {
|
|
732
|
+
const nameMatch = raw.match(/<command-name>([^<]+)<\/command-name>/);
|
|
733
|
+
const argsMatch = raw.match(/<command-args>([^<]+)<\/command-args>/);
|
|
734
|
+
const name = nameMatch?.[1]?.trim();
|
|
735
|
+
if (!name) {
|
|
736
|
+
return undefined;
|
|
592
737
|
}
|
|
738
|
+
const args = argsMatch?.[1]?.trim();
|
|
739
|
+
return args ? `${name} ${args}` : name;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Check if a message is noise (not a meaningful user intent).
|
|
744
|
+
*/
|
|
745
|
+
private isNoiseMessage(text: string): boolean {
|
|
746
|
+
return (
|
|
747
|
+
text.startsWith('[Request interrupted') ||
|
|
748
|
+
text === 'Tool loaded.' ||
|
|
749
|
+
text.startsWith('This session is being continued')
|
|
750
|
+
);
|
|
751
|
+
}
|
|
593
752
|
|
|
594
|
-
|
|
753
|
+
/**
|
|
754
|
+
* Check if an entry type is metadata (not conversation state).
|
|
755
|
+
* These should not overwrite lastEntryType used for status determination.
|
|
756
|
+
*/
|
|
757
|
+
private isMetadataEntryType(type: string): boolean {
|
|
758
|
+
return type === 'last-prompt' || type === 'file-history-snapshot';
|
|
595
759
|
}
|
|
596
760
|
|
|
761
|
+
private normalizePath(value: string): string {
|
|
762
|
+
const resolved = path.resolve(value);
|
|
763
|
+
if (resolved.length > 1 && resolved.endsWith(path.sep)) {
|
|
764
|
+
return resolved.slice(0, -1);
|
|
765
|
+
}
|
|
766
|
+
return resolved;
|
|
767
|
+
}
|
|
597
768
|
}
|