@ai-devkit/agent-manager 0.2.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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.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 +1 -0
- package/dist/terminal/index.d.ts.map +1 -1
- package/dist/terminal/index.js +3 -1
- package/dist/terminal/index.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/__tests__/terminal/TtyWriter.test.ts +154 -0
- package/src/adapters/ClaudeCodeAdapter.ts +498 -327
- package/src/adapters/CodexAdapter.ts +2 -13
- package/src/index.ts +1 -0
- package/src/terminal/TtyWriter.ts +112 -0
- package/src/terminal/index.ts +1 -0
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Claude Code Adapter
|
|
4
|
-
*
|
|
5
|
-
* Detects running Claude Code agents by reading session files
|
|
6
|
-
* from ~/.claude/ directory and correlating with running processes.
|
|
7
|
-
*/
|
|
8
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
3
|
if (k2 === undefined) k2 = k;
|
|
10
4
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -42,337 +36,427 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
42
36
|
exports.ClaudeCodeAdapter = void 0;
|
|
43
37
|
const fs = __importStar(require("fs"));
|
|
44
38
|
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
45
40
|
const AgentAdapter_1 = require("./AgentAdapter");
|
|
46
41
|
const process_1 = require("../utils/process");
|
|
47
42
|
const file_1 = require("../utils/file");
|
|
48
|
-
var SessionEntryType;
|
|
49
|
-
(function (SessionEntryType) {
|
|
50
|
-
SessionEntryType["ASSISTANT"] = "assistant";
|
|
51
|
-
SessionEntryType["USER"] = "user";
|
|
52
|
-
SessionEntryType["PROGRESS"] = "progress";
|
|
53
|
-
SessionEntryType["THINKING"] = "thinking";
|
|
54
|
-
SessionEntryType["SYSTEM"] = "system";
|
|
55
|
-
SessionEntryType["MESSAGE"] = "message";
|
|
56
|
-
SessionEntryType["TEXT"] = "text";
|
|
57
|
-
})(SessionEntryType || (SessionEntryType = {}));
|
|
58
43
|
/**
|
|
59
44
|
* Claude Code Adapter
|
|
60
45
|
*
|
|
61
46
|
* Detects Claude Code agents by:
|
|
62
47
|
* 1. Finding running claude processes
|
|
63
|
-
* 2.
|
|
64
|
-
* 3.
|
|
65
|
-
* 4.
|
|
66
|
-
* 5. Extracting summary from
|
|
48
|
+
* 2. Getting process start times for accurate session matching
|
|
49
|
+
* 3. Reading bounded session files from ~/.claude/projects/
|
|
50
|
+
* 4. Matching sessions to processes via CWD then start time ranking
|
|
51
|
+
* 5. Extracting summary from last user message in session JSONL
|
|
67
52
|
*/
|
|
68
53
|
class ClaudeCodeAdapter {
|
|
69
54
|
constructor() {
|
|
70
55
|
this.type = 'claude';
|
|
71
56
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
72
|
-
this.
|
|
73
|
-
this.projectsDir = path.join(this.claudeDir, 'projects');
|
|
74
|
-
this.historyPath = path.join(this.claudeDir, 'history.jsonl');
|
|
57
|
+
this.projectsDir = path.join(homeDir, '.claude', 'projects');
|
|
75
58
|
}
|
|
76
59
|
/**
|
|
77
60
|
* Check if this adapter can handle a given process
|
|
78
61
|
*/
|
|
79
62
|
canHandle(processInfo) {
|
|
80
|
-
return processInfo.command
|
|
63
|
+
return this.isClaudeExecutable(processInfo.command);
|
|
64
|
+
}
|
|
65
|
+
isClaudeExecutable(command) {
|
|
66
|
+
const executable = command.trim().split(/\s+/)[0] || '';
|
|
67
|
+
const base = path.basename(executable).toLowerCase();
|
|
68
|
+
return base === 'claude' || base === 'claude.exe';
|
|
81
69
|
}
|
|
82
70
|
/**
|
|
83
71
|
* Detect running Claude Code agents
|
|
84
72
|
*/
|
|
85
73
|
async detectAgents() {
|
|
86
|
-
const claudeProcesses =
|
|
74
|
+
const claudeProcesses = this.listClaudeProcesses();
|
|
87
75
|
if (claudeProcesses.length === 0) {
|
|
88
76
|
return [];
|
|
89
77
|
}
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const sortedSessions = [...sessions].sort((a, b) => {
|
|
98
|
-
const timeA = a.lastActive?.getTime() || 0;
|
|
99
|
-
const timeB = b.lastActive?.getTime() || 0;
|
|
100
|
-
return timeB - timeA;
|
|
101
|
-
});
|
|
78
|
+
const processStartByPid = this.getProcessStartTimes(claudeProcesses.map((p) => p.pid));
|
|
79
|
+
const sessionScanLimit = this.calculateSessionScanLimit(claudeProcesses.length);
|
|
80
|
+
const sessions = this.readSessions(sessionScanLimit);
|
|
81
|
+
if (sessions.length === 0) {
|
|
82
|
+
return claudeProcesses.map((p) => this.mapProcessOnlyAgent(p, []));
|
|
83
|
+
}
|
|
84
|
+
const sortedSessions = [...sessions].sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime());
|
|
102
85
|
const usedSessionIds = new Set();
|
|
103
86
|
const assignedPids = new Set();
|
|
104
87
|
const agents = [];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
88
|
+
const modes = ['cwd', 'missing-cwd', 'parent-child'];
|
|
89
|
+
for (const mode of modes) {
|
|
90
|
+
this.assignSessionsForMode(mode, claudeProcesses, sortedSessions, usedSessionIds, assignedPids, processStartByPid, agents);
|
|
91
|
+
}
|
|
108
92
|
for (const processInfo of claudeProcesses) {
|
|
109
93
|
if (assignedPids.has(processInfo.pid)) {
|
|
110
94
|
continue;
|
|
111
95
|
}
|
|
112
96
|
assignedPids.add(processInfo.pid);
|
|
113
|
-
agents.push(this.mapProcessOnlyAgent(processInfo, agents
|
|
97
|
+
agents.push(this.mapProcessOnlyAgent(processInfo, agents));
|
|
114
98
|
}
|
|
115
99
|
return agents;
|
|
116
100
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const historyEntry = this.selectHistoryForProcess(processInfo.cwd || '', historyByProjectPath, usedSessionIds);
|
|
123
|
-
if (!historyEntry) {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
assignedPids.add(processInfo.pid);
|
|
127
|
-
usedSessionIds.add(historyEntry.sessionId);
|
|
128
|
-
agents.push(this.mapHistoryToAgent(processInfo, historyEntry, agents));
|
|
129
|
-
}
|
|
101
|
+
listClaudeProcesses() {
|
|
102
|
+
return (0, process_1.listProcesses)({ namePattern: 'claude' }).filter((p) => this.canHandle(p));
|
|
103
|
+
}
|
|
104
|
+
calculateSessionScanLimit(processCount) {
|
|
105
|
+
return Math.min(Math.max(processCount * ClaudeCodeAdapter.SESSION_SCAN_MULTIPLIER, ClaudeCodeAdapter.MIN_SESSION_SCAN), ClaudeCodeAdapter.MAX_SESSION_SCAN);
|
|
130
106
|
}
|
|
131
|
-
assignSessionsForMode(mode, claudeProcesses, sessions, usedSessionIds, assignedPids,
|
|
107
|
+
assignSessionsForMode(mode, claudeProcesses, sessions, usedSessionIds, assignedPids, processStartByPid, agents) {
|
|
132
108
|
for (const processInfo of claudeProcesses) {
|
|
133
109
|
if (assignedPids.has(processInfo.pid)) {
|
|
134
110
|
continue;
|
|
135
111
|
}
|
|
136
|
-
const session = this.selectBestSession(processInfo, sessions, usedSessionIds, mode);
|
|
112
|
+
const session = this.selectBestSession(processInfo, sessions, usedSessionIds, processStartByPid, mode);
|
|
137
113
|
if (!session) {
|
|
138
114
|
continue;
|
|
139
115
|
}
|
|
140
116
|
usedSessionIds.add(session.sessionId);
|
|
141
117
|
assignedPids.add(processInfo.pid);
|
|
142
|
-
agents.push(this.mapSessionToAgent(session, processInfo,
|
|
118
|
+
agents.push(this.mapSessionToAgent(session, processInfo, agents));
|
|
143
119
|
}
|
|
144
120
|
}
|
|
145
|
-
|
|
146
|
-
const candidates = sessions.filter((session) => {
|
|
147
|
-
if (usedSessionIds.has(session.sessionId)) {
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
if (mode === 'cwd') {
|
|
151
|
-
return this.pathEquals(processInfo.cwd, session.lastCwd)
|
|
152
|
-
|| this.pathEquals(processInfo.cwd, session.projectPath);
|
|
153
|
-
}
|
|
154
|
-
if (mode === 'project-parent') {
|
|
155
|
-
return this.isChildPath(processInfo.cwd, session.projectPath)
|
|
156
|
-
|| this.isChildPath(processInfo.cwd, session.lastCwd);
|
|
157
|
-
}
|
|
158
|
-
return false;
|
|
159
|
-
});
|
|
160
|
-
if (candidates.length === 0) {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
if (mode !== 'project-parent') {
|
|
164
|
-
return candidates[0];
|
|
165
|
-
}
|
|
166
|
-
return candidates.sort((a, b) => {
|
|
167
|
-
const depthA = Math.max(this.pathDepth(a.projectPath), this.pathDepth(a.lastCwd));
|
|
168
|
-
const depthB = Math.max(this.pathDepth(b.projectPath), this.pathDepth(b.lastCwd));
|
|
169
|
-
if (depthA !== depthB) {
|
|
170
|
-
return depthB - depthA;
|
|
171
|
-
}
|
|
172
|
-
const lastActiveA = a.lastActive?.getTime() || 0;
|
|
173
|
-
const lastActiveB = b.lastActive?.getTime() || 0;
|
|
174
|
-
return lastActiveB - lastActiveA;
|
|
175
|
-
})[0];
|
|
176
|
-
}
|
|
177
|
-
mapSessionToAgent(session, processInfo, historyBySessionId, existingAgents) {
|
|
178
|
-
const historyEntry = historyBySessionId.get(session.sessionId);
|
|
121
|
+
mapSessionToAgent(session, processInfo, existingAgents) {
|
|
179
122
|
return {
|
|
180
123
|
name: this.generateAgentName(session, existingAgents),
|
|
181
124
|
type: this.type,
|
|
182
125
|
status: this.determineStatus(session),
|
|
183
|
-
summary:
|
|
126
|
+
summary: session.lastUserMessage || 'Session started',
|
|
184
127
|
pid: processInfo.pid,
|
|
185
128
|
projectPath: session.projectPath || processInfo.cwd || '',
|
|
186
129
|
sessionId: session.sessionId,
|
|
187
130
|
slug: session.slug,
|
|
188
|
-
lastActive: session.lastActive
|
|
131
|
+
lastActive: session.lastActive,
|
|
189
132
|
};
|
|
190
133
|
}
|
|
191
|
-
mapProcessOnlyAgent(processInfo, existingAgents
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const lastActive = historyEntry ? new Date(historyEntry.timestamp) : new Date();
|
|
196
|
-
if (historyEntry) {
|
|
197
|
-
usedSessionIds.add(historyEntry.sessionId);
|
|
198
|
-
}
|
|
199
|
-
const processSession = {
|
|
200
|
-
sessionId,
|
|
201
|
-
projectPath,
|
|
202
|
-
lastCwd: projectPath,
|
|
203
|
-
sessionLogPath: '',
|
|
204
|
-
lastActive,
|
|
205
|
-
};
|
|
134
|
+
mapProcessOnlyAgent(processInfo, existingAgents) {
|
|
135
|
+
const processCwd = processInfo.cwd || '';
|
|
136
|
+
const projectName = path.basename(processCwd) || 'claude';
|
|
137
|
+
const hasDuplicate = existingAgents.some((a) => a.projectPath === processCwd);
|
|
206
138
|
return {
|
|
207
|
-
name:
|
|
139
|
+
name: hasDuplicate ? `${projectName} (pid-${processInfo.pid})` : projectName,
|
|
208
140
|
type: this.type,
|
|
209
|
-
status: AgentAdapter_1.AgentStatus.
|
|
210
|
-
summary:
|
|
141
|
+
status: AgentAdapter_1.AgentStatus.IDLE,
|
|
142
|
+
summary: 'Unknown',
|
|
211
143
|
pid: processInfo.pid,
|
|
212
|
-
projectPath,
|
|
213
|
-
sessionId:
|
|
214
|
-
lastActive:
|
|
144
|
+
projectPath: processCwd,
|
|
145
|
+
sessionId: `pid-${processInfo.pid}`,
|
|
146
|
+
lastActive: new Date(),
|
|
215
147
|
};
|
|
216
148
|
}
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
lastActive
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
149
|
+
selectBestSession(processInfo, sessions, usedSessionIds, processStartByPid, mode) {
|
|
150
|
+
const candidates = this.filterCandidateSessions(processInfo, sessions, usedSessionIds, mode);
|
|
151
|
+
if (candidates.length === 0) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const processStart = processStartByPid.get(processInfo.pid);
|
|
155
|
+
if (!processStart) {
|
|
156
|
+
return candidates.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime())[0];
|
|
157
|
+
}
|
|
158
|
+
const best = this.rankCandidatesByStartTime(candidates, processStart)[0];
|
|
159
|
+
if (!best) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
// In early modes (cwd/missing-cwd), defer assignment when the best
|
|
163
|
+
// candidate is outside start-time tolerance — a closer match may
|
|
164
|
+
// exist in parent-child mode (e.g., worktree sessions).
|
|
165
|
+
if (mode !== 'parent-child') {
|
|
166
|
+
const diffMs = Math.abs(best.sessionStart.getTime() - processStart.getTime());
|
|
167
|
+
if (diffMs > ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return best;
|
|
236
172
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
173
|
+
filterCandidateSessions(processInfo, sessions, usedSessionIds, mode) {
|
|
174
|
+
return sessions.filter((session) => {
|
|
175
|
+
if (usedSessionIds.has(session.sessionId)) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
if (mode === 'cwd') {
|
|
179
|
+
return (this.pathEquals(processInfo.cwd, session.projectPath) ||
|
|
180
|
+
this.pathEquals(processInfo.cwd, session.lastCwd));
|
|
181
|
+
}
|
|
182
|
+
if (mode === 'missing-cwd') {
|
|
183
|
+
return !session.projectPath;
|
|
184
|
+
}
|
|
185
|
+
// parent-child mode: match if process CWD equals, is under, or is
|
|
186
|
+
// a parent of session project/lastCwd. This also catches exact CWD
|
|
187
|
+
// matches that were deferred from `cwd` mode due to start-time tolerance.
|
|
188
|
+
return (this.pathRelated(processInfo.cwd, session.projectPath) ||
|
|
189
|
+
this.pathRelated(processInfo.cwd, session.lastCwd));
|
|
190
|
+
});
|
|
249
191
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
192
|
+
rankCandidatesByStartTime(candidates, processStart) {
|
|
193
|
+
const toleranceMs = ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
|
|
194
|
+
return candidates
|
|
195
|
+
.map((session) => {
|
|
196
|
+
const diffMs = Math.abs(session.sessionStart.getTime() - processStart.getTime());
|
|
197
|
+
const outsideTolerance = diffMs > toleranceMs ? 1 : 0;
|
|
198
|
+
return {
|
|
199
|
+
session,
|
|
200
|
+
rank: outsideTolerance,
|
|
201
|
+
diffMs,
|
|
202
|
+
recency: session.lastActive.getTime(),
|
|
203
|
+
};
|
|
204
|
+
})
|
|
205
|
+
.sort((a, b) => {
|
|
206
|
+
if (a.rank !== b.rank)
|
|
207
|
+
return a.rank - b.rank;
|
|
208
|
+
// Within tolerance (rank 0): prefer most recently active session.
|
|
209
|
+
// The exact diff is noise — a 6s vs 45s difference is meaningless,
|
|
210
|
+
// but the session with recent activity is more likely the real one.
|
|
211
|
+
if (a.rank === 0)
|
|
212
|
+
return b.recency - a.recency;
|
|
213
|
+
// Outside tolerance: prefer smallest time difference, then recency.
|
|
214
|
+
if (a.diffMs !== b.diffMs)
|
|
215
|
+
return a.diffMs - b.diffMs;
|
|
216
|
+
return b.recency - a.recency;
|
|
217
|
+
})
|
|
218
|
+
.map((ranked) => ranked.session);
|
|
219
|
+
}
|
|
220
|
+
getProcessStartTimes(pids) {
|
|
221
|
+
if (pids.length === 0 || process.env.JEST_WORKER_ID) {
|
|
222
|
+
return new Map();
|
|
253
223
|
}
|
|
254
|
-
|
|
255
|
-
|
|
224
|
+
try {
|
|
225
|
+
const output = (0, child_process_1.execSync)(`ps -o pid=,etime= -p ${pids.join(',')}`, { encoding: 'utf-8' });
|
|
226
|
+
const nowMs = Date.now();
|
|
227
|
+
const startTimes = new Map();
|
|
228
|
+
for (const rawLine of output.split('\n')) {
|
|
229
|
+
const line = rawLine.trim();
|
|
230
|
+
if (!line)
|
|
231
|
+
continue;
|
|
232
|
+
const parts = line.split(/\s+/);
|
|
233
|
+
if (parts.length < 2)
|
|
234
|
+
continue;
|
|
235
|
+
const pid = Number.parseInt(parts[0], 10);
|
|
236
|
+
const elapsedSeconds = this.parseElapsedSeconds(parts[1]);
|
|
237
|
+
if (!Number.isFinite(pid) || elapsedSeconds === null)
|
|
238
|
+
continue;
|
|
239
|
+
startTimes.set(pid, new Date(nowMs - elapsedSeconds * 1000));
|
|
240
|
+
}
|
|
241
|
+
return startTimes;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return new Map();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
parseElapsedSeconds(etime) {
|
|
248
|
+
const match = etime
|
|
249
|
+
.trim()
|
|
250
|
+
.match(/^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
|
|
251
|
+
if (!match) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const days = Number.parseInt(match[1] || '0', 10);
|
|
255
|
+
const hours = Number.parseInt(match[2] || '0', 10);
|
|
256
|
+
const minutes = Number.parseInt(match[3] || '0', 10);
|
|
257
|
+
const seconds = Number.parseInt(match[4] || '0', 10);
|
|
258
|
+
return ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
|
|
256
259
|
}
|
|
257
260
|
/**
|
|
258
|
-
* Read
|
|
261
|
+
* Read Claude Code sessions with bounded scanning
|
|
259
262
|
*/
|
|
260
|
-
readSessions() {
|
|
263
|
+
readSessions(limit) {
|
|
264
|
+
const sessionFiles = this.findSessionFiles(limit);
|
|
265
|
+
const sessions = [];
|
|
266
|
+
for (const file of sessionFiles) {
|
|
267
|
+
try {
|
|
268
|
+
const session = this.readSession(file.filePath, file.projectPath);
|
|
269
|
+
if (session) {
|
|
270
|
+
sessions.push(session);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
console.error(`Failed to parse Claude session ${file.filePath}:`, error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return sessions;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Find session files bounded by mtime, sorted most-recent first
|
|
281
|
+
*/
|
|
282
|
+
findSessionFiles(limit) {
|
|
261
283
|
if (!fs.existsSync(this.projectsDir)) {
|
|
262
284
|
return [];
|
|
263
285
|
}
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
for (const dirName of projectDirs) {
|
|
286
|
+
const files = [];
|
|
287
|
+
for (const dirName of fs.readdirSync(this.projectsDir)) {
|
|
267
288
|
if (dirName.startsWith('.')) {
|
|
268
289
|
continue;
|
|
269
290
|
}
|
|
270
291
|
const projectDir = path.join(this.projectsDir, dirName);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Read sessions-index.json to get original project path
|
|
275
|
-
const indexPath = path.join(projectDir, 'sessions-index.json');
|
|
276
|
-
if (!fs.existsSync(indexPath)) {
|
|
277
|
-
continue;
|
|
292
|
+
try {
|
|
293
|
+
if (!fs.statSync(projectDir).isDirectory())
|
|
294
|
+
continue;
|
|
278
295
|
}
|
|
279
|
-
|
|
280
|
-
if (!sessionsIndex) {
|
|
281
|
-
console.error(`Failed to parse ${indexPath}`);
|
|
296
|
+
catch {
|
|
282
297
|
continue;
|
|
283
298
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
299
|
+
const indexPath = path.join(projectDir, 'sessions-index.json');
|
|
300
|
+
const index = (0, file_1.readJson)(indexPath);
|
|
301
|
+
const projectPath = index?.originalPath || '';
|
|
302
|
+
for (const entry of fs.readdirSync(projectDir)) {
|
|
303
|
+
if (!entry.endsWith('.jsonl')) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const filePath = path.join(projectDir, entry);
|
|
288
307
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
lastCwd: sessionData.lastCwd,
|
|
294
|
-
slug: sessionData.slug,
|
|
295
|
-
sessionLogPath,
|
|
296
|
-
lastEntry: sessionData.lastEntry,
|
|
297
|
-
lastActive: sessionData.lastActive,
|
|
308
|
+
files.push({
|
|
309
|
+
filePath,
|
|
310
|
+
projectPath,
|
|
311
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
298
312
|
});
|
|
299
313
|
}
|
|
300
|
-
catch
|
|
301
|
-
console.error(`Failed to read session ${sessionId}:`, error);
|
|
314
|
+
catch {
|
|
302
315
|
continue;
|
|
303
316
|
}
|
|
304
317
|
}
|
|
305
318
|
}
|
|
306
|
-
|
|
319
|
+
// Ensure breadth: include at least the most recent session per project,
|
|
320
|
+
// then fill remaining slots with globally most-recent sessions.
|
|
321
|
+
const sorted = files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
322
|
+
const result = [];
|
|
323
|
+
const seenProjects = new Set();
|
|
324
|
+
// First pass: one most-recent session per project directory
|
|
325
|
+
for (const file of sorted) {
|
|
326
|
+
const projDir = path.dirname(file.filePath);
|
|
327
|
+
if (!seenProjects.has(projDir)) {
|
|
328
|
+
seenProjects.add(projDir);
|
|
329
|
+
result.push(file);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Second pass: fill remaining slots with globally most-recent
|
|
333
|
+
if (result.length < limit) {
|
|
334
|
+
const resultSet = new Set(result.map((f) => f.filePath));
|
|
335
|
+
for (const file of sorted) {
|
|
336
|
+
if (result.length >= limit)
|
|
337
|
+
break;
|
|
338
|
+
if (!resultSet.has(file.filePath)) {
|
|
339
|
+
result.push(file);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return result.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
307
344
|
}
|
|
308
345
|
/**
|
|
309
|
-
*
|
|
310
|
-
* Only reads last 100 lines for performance with large files
|
|
346
|
+
* Parse a single session file into ClaudeSession
|
|
311
347
|
*/
|
|
312
|
-
|
|
313
|
-
const
|
|
348
|
+
readSession(filePath, projectPath) {
|
|
349
|
+
const sessionId = path.basename(filePath, '.jsonl');
|
|
350
|
+
let content;
|
|
351
|
+
try {
|
|
352
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
const allLines = content.trim().split('\n');
|
|
358
|
+
if (allLines.length === 0) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
// Parse first line for sessionStart.
|
|
362
|
+
// Claude Code may emit a "file-history-snapshot" as the first entry, which
|
|
363
|
+
// stores its timestamp inside "snapshot.timestamp" rather than at the root.
|
|
364
|
+
let sessionStart = null;
|
|
365
|
+
try {
|
|
366
|
+
const firstEntry = JSON.parse(allLines[0]);
|
|
367
|
+
const rawTs = firstEntry.timestamp || firstEntry.snapshot?.timestamp;
|
|
368
|
+
if (rawTs) {
|
|
369
|
+
const ts = new Date(rawTs);
|
|
370
|
+
if (!Number.isNaN(ts.getTime())) {
|
|
371
|
+
sessionStart = ts;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
/* skip */
|
|
377
|
+
}
|
|
378
|
+
// Parse all lines for session state (file already in memory)
|
|
314
379
|
let slug;
|
|
315
|
-
let
|
|
380
|
+
let lastEntryType;
|
|
316
381
|
let lastActive;
|
|
317
382
|
let lastCwd;
|
|
318
|
-
|
|
383
|
+
let isInterrupted = false;
|
|
384
|
+
let lastUserMessage;
|
|
385
|
+
for (const line of allLines) {
|
|
319
386
|
try {
|
|
320
387
|
const entry = JSON.parse(line);
|
|
388
|
+
if (entry.timestamp) {
|
|
389
|
+
const ts = new Date(entry.timestamp);
|
|
390
|
+
if (!Number.isNaN(ts.getTime())) {
|
|
391
|
+
lastActive = ts;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
321
394
|
if (entry.slug && !slug) {
|
|
322
395
|
slug = entry.slug;
|
|
323
396
|
}
|
|
324
|
-
lastEntry = entry;
|
|
325
|
-
if (entry.timestamp) {
|
|
326
|
-
lastActive = new Date(entry.timestamp);
|
|
327
|
-
}
|
|
328
397
|
if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
|
|
329
398
|
lastCwd = entry.cwd;
|
|
330
399
|
}
|
|
400
|
+
if (entry.type && !this.isMetadataEntryType(entry.type)) {
|
|
401
|
+
lastEntryType = entry.type;
|
|
402
|
+
if (entry.type === 'user') {
|
|
403
|
+
const msgContent = entry.message?.content;
|
|
404
|
+
isInterrupted =
|
|
405
|
+
Array.isArray(msgContent) &&
|
|
406
|
+
msgContent.some((c) => (c.type === 'text' &&
|
|
407
|
+
c.text?.includes('[Request interrupted')) ||
|
|
408
|
+
(c.type === 'tool_result' &&
|
|
409
|
+
c.content?.includes('[Request interrupted')));
|
|
410
|
+
// Extract user message text for summary fallback
|
|
411
|
+
const text = this.extractUserMessageText(msgContent);
|
|
412
|
+
if (text) {
|
|
413
|
+
lastUserMessage = text;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
isInterrupted = false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
331
420
|
}
|
|
332
|
-
catch
|
|
421
|
+
catch {
|
|
333
422
|
continue;
|
|
334
423
|
}
|
|
335
424
|
}
|
|
336
|
-
return {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
425
|
+
return {
|
|
426
|
+
sessionId,
|
|
427
|
+
projectPath: projectPath || lastCwd || '',
|
|
428
|
+
lastCwd,
|
|
429
|
+
slug,
|
|
430
|
+
sessionStart: sessionStart || lastActive || new Date(),
|
|
431
|
+
lastActive: lastActive || new Date(),
|
|
432
|
+
lastEntryType,
|
|
433
|
+
isInterrupted,
|
|
434
|
+
lastUserMessage,
|
|
435
|
+
};
|
|
344
436
|
}
|
|
345
437
|
/**
|
|
346
|
-
* Determine agent status from session
|
|
438
|
+
* Determine agent status from session state
|
|
347
439
|
*/
|
|
348
440
|
determineStatus(session) {
|
|
349
|
-
if (!session.
|
|
441
|
+
if (!session.lastEntryType) {
|
|
350
442
|
return AgentAdapter_1.AgentStatus.UNKNOWN;
|
|
351
443
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
return
|
|
444
|
+
// No age-based IDLE override: every agent in the list is backed by
|
|
445
|
+
// a running process (found via ps), so the entry type is the best
|
|
446
|
+
// indicator of actual state.
|
|
447
|
+
if (session.lastEntryType === 'user') {
|
|
448
|
+
return session.isInterrupted
|
|
449
|
+
? AgentAdapter_1.AgentStatus.WAITING
|
|
450
|
+
: AgentAdapter_1.AgentStatus.RUNNING;
|
|
357
451
|
}
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
const content = session.lastEntry.message?.content;
|
|
361
|
-
if (Array.isArray(content)) {
|
|
362
|
-
const isInterrupted = content.some(c => (c.type === SessionEntryType.TEXT && c.text?.includes('[Request interrupted')) ||
|
|
363
|
-
(c.type === 'tool_result' && c.content?.includes('[Request interrupted')));
|
|
364
|
-
if (isInterrupted)
|
|
365
|
-
return AgentAdapter_1.AgentStatus.WAITING;
|
|
366
|
-
}
|
|
452
|
+
if (session.lastEntryType === 'progress' ||
|
|
453
|
+
session.lastEntryType === 'thinking') {
|
|
367
454
|
return AgentAdapter_1.AgentStatus.RUNNING;
|
|
368
455
|
}
|
|
369
|
-
if (
|
|
370
|
-
return AgentAdapter_1.AgentStatus.RUNNING;
|
|
371
|
-
}
|
|
372
|
-
else if (entryType === SessionEntryType.ASSISTANT) {
|
|
456
|
+
if (session.lastEntryType === 'assistant') {
|
|
373
457
|
return AgentAdapter_1.AgentStatus.WAITING;
|
|
374
458
|
}
|
|
375
|
-
|
|
459
|
+
if (session.lastEntryType === 'system') {
|
|
376
460
|
return AgentAdapter_1.AgentStatus.IDLE;
|
|
377
461
|
}
|
|
378
462
|
return AgentAdapter_1.AgentStatus.UNKNOWN;
|
|
@@ -383,21 +467,22 @@ class ClaudeCodeAdapter {
|
|
|
383
467
|
*/
|
|
384
468
|
generateAgentName(session, existingAgents) {
|
|
385
469
|
const projectName = path.basename(session.projectPath) || 'claude';
|
|
386
|
-
const sameProjectAgents = existingAgents.filter(a => a.projectPath === session.projectPath);
|
|
470
|
+
const sameProjectAgents = existingAgents.filter((a) => a.projectPath === session.projectPath);
|
|
387
471
|
if (sameProjectAgents.length === 0) {
|
|
388
472
|
return projectName;
|
|
389
473
|
}
|
|
390
|
-
// Multiple sessions for same project, append slug
|
|
391
474
|
if (session.slug) {
|
|
392
|
-
// Use first word of slug for brevity (with safety check for format)
|
|
393
475
|
const slugPart = session.slug.includes('-')
|
|
394
476
|
? session.slug.split('-')[0]
|
|
395
477
|
: session.slug.slice(0, 8);
|
|
396
478
|
return `${projectName} (${slugPart})`;
|
|
397
479
|
}
|
|
398
|
-
// No slug available, use session ID prefix
|
|
399
480
|
return `${projectName} (${session.sessionId.slice(0, 8)})`;
|
|
400
481
|
}
|
|
482
|
+
/** Check if two paths are equal, or one is a parent/child of the other. */
|
|
483
|
+
pathRelated(a, b) {
|
|
484
|
+
return this.pathEquals(a, b) || this.isChildPath(a, b) || this.isChildPath(b, a);
|
|
485
|
+
}
|
|
401
486
|
pathEquals(a, b) {
|
|
402
487
|
if (!a || !b) {
|
|
403
488
|
return false;
|
|
@@ -410,7 +495,73 @@ class ClaudeCodeAdapter {
|
|
|
410
495
|
}
|
|
411
496
|
const normalizedChild = this.normalizePath(child);
|
|
412
497
|
const normalizedParent = this.normalizePath(parent);
|
|
413
|
-
return normalizedChild
|
|
498
|
+
return normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Extract meaningful text from a user message content.
|
|
502
|
+
* Handles string and array formats, skill command expansion, and noise filtering.
|
|
503
|
+
*/
|
|
504
|
+
extractUserMessageText(content) {
|
|
505
|
+
if (!content) {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
let raw;
|
|
509
|
+
if (typeof content === 'string') {
|
|
510
|
+
raw = content.trim();
|
|
511
|
+
}
|
|
512
|
+
else if (Array.isArray(content)) {
|
|
513
|
+
for (const block of content) {
|
|
514
|
+
if (block.type === 'text' && block.text?.trim()) {
|
|
515
|
+
raw = block.text.trim();
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (!raw) {
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
// Skill slash-command: extract /command-name and args
|
|
524
|
+
if (raw.startsWith('<command-message>')) {
|
|
525
|
+
return this.parseCommandMessage(raw);
|
|
526
|
+
}
|
|
527
|
+
// Expanded skill content: extract ARGUMENTS line if present, skip otherwise
|
|
528
|
+
if (raw.startsWith('Base directory for this skill:')) {
|
|
529
|
+
const argsMatch = raw.match(/\nARGUMENTS:\s*(.+)/);
|
|
530
|
+
return argsMatch?.[1]?.trim() || undefined;
|
|
531
|
+
}
|
|
532
|
+
// Filter noise
|
|
533
|
+
if (this.isNoiseMessage(raw)) {
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
return raw;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Parse a <command-message> string into "/command args" format.
|
|
540
|
+
*/
|
|
541
|
+
parseCommandMessage(raw) {
|
|
542
|
+
const nameMatch = raw.match(/<command-name>([^<]+)<\/command-name>/);
|
|
543
|
+
const argsMatch = raw.match(/<command-args>([^<]+)<\/command-args>/);
|
|
544
|
+
const name = nameMatch?.[1]?.trim();
|
|
545
|
+
if (!name) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
const args = argsMatch?.[1]?.trim();
|
|
549
|
+
return args ? `${name} ${args}` : name;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Check if a message is noise (not a meaningful user intent).
|
|
553
|
+
*/
|
|
554
|
+
isNoiseMessage(text) {
|
|
555
|
+
return (text.startsWith('[Request interrupted') ||
|
|
556
|
+
text === 'Tool loaded.' ||
|
|
557
|
+
text.startsWith('This session is being continued'));
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Check if an entry type is metadata (not conversation state).
|
|
561
|
+
* These should not overwrite lastEntryType used for status determination.
|
|
562
|
+
*/
|
|
563
|
+
isMetadataEntryType(type) {
|
|
564
|
+
return type === 'last-prompt' || type === 'file-history-snapshot';
|
|
414
565
|
}
|
|
415
566
|
normalizePath(value) {
|
|
416
567
|
const resolved = path.resolve(value);
|
|
@@ -419,14 +570,12 @@ class ClaudeCodeAdapter {
|
|
|
419
570
|
}
|
|
420
571
|
return resolved;
|
|
421
572
|
}
|
|
422
|
-
pathDepth(value) {
|
|
423
|
-
if (!value) {
|
|
424
|
-
return 0;
|
|
425
|
-
}
|
|
426
|
-
return this.normalizePath(value).split(path.sep).filter(Boolean).length;
|
|
427
|
-
}
|
|
428
573
|
}
|
|
429
574
|
exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
|
|
430
|
-
/**
|
|
431
|
-
ClaudeCodeAdapter.
|
|
575
|
+
/** Limit session parsing per run to keep list latency bounded. */
|
|
576
|
+
ClaudeCodeAdapter.MIN_SESSION_SCAN = 12;
|
|
577
|
+
ClaudeCodeAdapter.MAX_SESSION_SCAN = 40;
|
|
578
|
+
ClaudeCodeAdapter.SESSION_SCAN_MULTIPLIER = 4;
|
|
579
|
+
/** Matching tolerance between process start time and session start time. */
|
|
580
|
+
ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS = 2 * 60 * 1000;
|
|
432
581
|
//# sourceMappingURL=ClaudeCodeAdapter.js.map
|