@ai-devkit/agent-manager 0.3.0 → 0.5.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/AgentAdapter.d.ts +2 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +49 -38
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +286 -293
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +32 -30
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +148 -284
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +6 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +12 -11
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.d.ts +39 -0
- package/dist/utils/matching.d.ts.map +1 -0
- package/dist/utils/matching.js +103 -0
- package/dist/utils/matching.js.map +1 -0
- package/dist/utils/process.d.ts +25 -40
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +151 -105
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.d.ts +30 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +101 -0
- package/dist/utils/session.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/AgentManager.test.ts +0 -25
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +921 -205
- package/src/__tests__/adapters/CodexAdapter.test.ts +468 -269
- package/src/__tests__/utils/matching.test.ts +191 -0
- package/src/__tests__/utils/process.test.ts +202 -0
- package/src/__tests__/utils/session.test.ts +117 -0
- package/src/adapters/AgentAdapter.ts +3 -0
- package/src/adapters/ClaudeCodeAdapter.ts +341 -418
- package/src/adapters/CodexAdapter.ts +155 -420
- package/src/index.ts +1 -3
- package/src/utils/index.ts +6 -3
- package/src/utils/matching.ts +92 -0
- package/src/utils/process.ts +133 -119
- package/src/utils/session.ts +92 -0
- package/dist/utils/file.d.ts +0 -52
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -135
- package/dist/utils/file.js.map +0 -1
- package/src/utils/file.ts +0 -100
|
@@ -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);
|
|
@@ -44,389 +38,388 @@ const fs = __importStar(require("fs"));
|
|
|
44
38
|
const path = __importStar(require("path"));
|
|
45
39
|
const AgentAdapter_1 = require("./AgentAdapter");
|
|
46
40
|
const process_1 = require("../utils/process");
|
|
47
|
-
const
|
|
48
|
-
|
|
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 = {}));
|
|
41
|
+
const session_1 = require("../utils/session");
|
|
42
|
+
const matching_1 = require("../utils/matching");
|
|
58
43
|
/**
|
|
59
44
|
* Claude Code Adapter
|
|
60
45
|
*
|
|
61
46
|
* Detects Claude Code agents by:
|
|
62
|
-
* 1. Finding running claude processes
|
|
63
|
-
* 2.
|
|
64
|
-
* 3.
|
|
65
|
-
* 4.
|
|
66
|
-
* 5. Extracting summary from
|
|
47
|
+
* 1. Finding running claude processes via shared listAgentProcesses()
|
|
48
|
+
* 2. Enriching with CWD and start times via shared enrichProcesses()
|
|
49
|
+
* 3. Attempting authoritative PID-file matching via ~/.claude/sessions/<pid>.json
|
|
50
|
+
* 4. Falling back to CWD+birthtime heuristic (matchProcessesToSessions) for processes without a PID file
|
|
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.
|
|
74
|
-
this.historyPath = path.join(this.claudeDir, 'history.jsonl');
|
|
57
|
+
this.projectsDir = path.join(homeDir, '.claude', 'projects');
|
|
58
|
+
this.sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
|
75
59
|
}
|
|
76
60
|
/**
|
|
77
61
|
* Check if this adapter can handle a given process
|
|
78
62
|
*/
|
|
79
63
|
canHandle(processInfo) {
|
|
80
|
-
return processInfo.command
|
|
64
|
+
return this.isClaudeExecutable(processInfo.command);
|
|
65
|
+
}
|
|
66
|
+
isClaudeExecutable(command) {
|
|
67
|
+
const executable = command.trim().split(/\s+/)[0] || '';
|
|
68
|
+
const base = path.basename(executable).toLowerCase();
|
|
69
|
+
return base === 'claude' || base === 'claude.exe';
|
|
81
70
|
}
|
|
82
71
|
/**
|
|
83
72
|
* Detect running Claude Code agents
|
|
84
73
|
*/
|
|
85
74
|
async detectAgents() {
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
75
|
+
const processes = (0, process_1.enrichProcesses)((0, process_1.listAgentProcesses)('claude'));
|
|
76
|
+
if (processes.length === 0) {
|
|
88
77
|
return [];
|
|
89
78
|
}
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
});
|
|
102
|
-
const usedSessionIds = new Set();
|
|
103
|
-
const assignedPids = new Set();
|
|
79
|
+
// Step 1: try authoritative PID-file matching for every process
|
|
80
|
+
const { direct, fallback } = this.tryPidFileMatching(processes);
|
|
81
|
+
// Step 2: run legacy CWD+birthtime matching only for processes without a PID file
|
|
82
|
+
const legacySessions = this.discoverSessions(fallback);
|
|
83
|
+
const legacyMatches = fallback.length > 0 && legacySessions.length > 0
|
|
84
|
+
? (0, matching_1.matchProcessesToSessions)(fallback, legacySessions)
|
|
85
|
+
: [];
|
|
86
|
+
const matchedPids = new Set([
|
|
87
|
+
...direct.map((d) => d.process.pid),
|
|
88
|
+
...legacyMatches.map((m) => m.process.pid),
|
|
89
|
+
]);
|
|
104
90
|
const agents = [];
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
// Build agents from direct (PID-file) matches
|
|
92
|
+
for (const { process: proc, sessionFile } of direct) {
|
|
93
|
+
const sessionData = this.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
|
|
94
|
+
if (sessionData) {
|
|
95
|
+
agents.push(this.mapSessionToAgent(sessionData, proc, sessionFile));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
matchedPids.delete(proc.pid);
|
|
111
99
|
}
|
|
112
|
-
assignedPids.add(processInfo.pid);
|
|
113
|
-
agents.push(this.mapProcessOnlyAgent(processInfo, agents, historyByProjectPath, usedSessionIds));
|
|
114
100
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
continue;
|
|
101
|
+
// Build agents from legacy matches
|
|
102
|
+
for (const match of legacyMatches) {
|
|
103
|
+
const sessionData = this.readSession(match.session.filePath, match.session.resolvedCwd);
|
|
104
|
+
if (sessionData) {
|
|
105
|
+
agents.push(this.mapSessionToAgent(sessionData, match.process, match.session));
|
|
121
106
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
107
|
+
else {
|
|
108
|
+
matchedPids.delete(match.process.pid);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Any process with no match (direct or legacy) appears as IDLE
|
|
112
|
+
for (const proc of processes) {
|
|
113
|
+
if (!matchedPids.has(proc.pid)) {
|
|
114
|
+
agents.push(this.mapProcessOnlyAgent(proc));
|
|
125
115
|
}
|
|
126
|
-
assignedPids.add(processInfo.pid);
|
|
127
|
-
usedSessionIds.add(historyEntry.sessionId);
|
|
128
|
-
agents.push(this.mapHistoryToAgent(processInfo, historyEntry, agents));
|
|
129
116
|
}
|
|
117
|
+
return agents;
|
|
130
118
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Discover session files for the given processes.
|
|
121
|
+
*
|
|
122
|
+
* For each unique process CWD, encodes it to derive the expected
|
|
123
|
+
* ~/.claude/projects/<encoded>/ directory, then gets session file birthtimes
|
|
124
|
+
* via a single batched stat call across all directories.
|
|
125
|
+
*/
|
|
126
|
+
discoverSessions(processes) {
|
|
127
|
+
// Collect valid project dirs and map them back to their CWD
|
|
128
|
+
const dirToCwd = new Map();
|
|
129
|
+
for (const proc of processes) {
|
|
130
|
+
if (!proc.cwd)
|
|
134
131
|
continue;
|
|
132
|
+
const projectDir = this.getProjectDir(proc.cwd);
|
|
133
|
+
if (dirToCwd.has(projectDir))
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
if (!fs.statSync(projectDir).isDirectory())
|
|
137
|
+
continue;
|
|
135
138
|
}
|
|
136
|
-
|
|
137
|
-
if (!session) {
|
|
139
|
+
catch {
|
|
138
140
|
continue;
|
|
139
141
|
}
|
|
140
|
-
|
|
141
|
-
assignedPids.add(processInfo.pid);
|
|
142
|
-
agents.push(this.mapSessionToAgent(session, processInfo, historyBySessionId, agents));
|
|
142
|
+
dirToCwd.set(projectDir, proc.cwd);
|
|
143
143
|
}
|
|
144
|
+
if (dirToCwd.size === 0)
|
|
145
|
+
return [];
|
|
146
|
+
// Single batched stat call across all directories
|
|
147
|
+
const files = (0, session_1.batchGetSessionFileBirthtimes)([...dirToCwd.keys()]);
|
|
148
|
+
// Set resolvedCwd based on which project dir the file belongs to
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
file.resolvedCwd = dirToCwd.get(file.projectDir) || '';
|
|
151
|
+
}
|
|
152
|
+
return files;
|
|
144
153
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Attempt to match each process to its session via ~/.claude/sessions/<pid>.json.
|
|
156
|
+
*
|
|
157
|
+
* Returns:
|
|
158
|
+
* direct — processes matched authoritatively via PID file
|
|
159
|
+
* fallback — processes with no valid PID file (sent to legacy matching)
|
|
160
|
+
*
|
|
161
|
+
* Per-process fallback triggers on: file absent, malformed JSON,
|
|
162
|
+
* stale startedAt (>60 s from proc.startTime), or missing JSONL.
|
|
163
|
+
*/
|
|
164
|
+
tryPidFileMatching(processes) {
|
|
165
|
+
const direct = [];
|
|
166
|
+
const fallback = [];
|
|
167
|
+
for (const proc of processes) {
|
|
168
|
+
const pidFilePath = path.join(this.sessionsDir, `${proc.pid}.json`);
|
|
169
|
+
try {
|
|
170
|
+
const entry = JSON.parse(fs.readFileSync(pidFilePath, 'utf-8'));
|
|
171
|
+
// Stale-file guard: reject PID files from a previous process with the same PID
|
|
172
|
+
if (proc.startTime) {
|
|
173
|
+
const deltaMs = Math.abs(proc.startTime.getTime() - entry.startedAt);
|
|
174
|
+
if (deltaMs > 60000) {
|
|
175
|
+
fallback.push(proc);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const projectDir = this.getProjectDir(entry.cwd);
|
|
180
|
+
const jsonlPath = path.join(projectDir, `${entry.sessionId}.jsonl`);
|
|
181
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
182
|
+
fallback.push(proc);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
direct.push({
|
|
186
|
+
process: proc,
|
|
187
|
+
sessionFile: {
|
|
188
|
+
sessionId: entry.sessionId,
|
|
189
|
+
filePath: jsonlPath,
|
|
190
|
+
projectDir,
|
|
191
|
+
birthtimeMs: entry.startedAt,
|
|
192
|
+
resolvedCwd: entry.cwd,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
153
195
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
196
|
+
catch {
|
|
197
|
+
// PID file absent, unreadable, or malformed — fall back per-process
|
|
198
|
+
fallback.push(proc);
|
|
157
199
|
}
|
|
158
|
-
return false;
|
|
159
|
-
});
|
|
160
|
-
if (candidates.length === 0) {
|
|
161
|
-
return null;
|
|
162
200
|
}
|
|
163
|
-
|
|
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];
|
|
201
|
+
return { direct, fallback };
|
|
176
202
|
}
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Derive the Claude Code project directory for a given CWD.
|
|
205
|
+
*
|
|
206
|
+
* Claude Code encodes paths by replacing '/' with '-':
|
|
207
|
+
* /Users/foo/bar → ~/.claude/projects/-Users-foo-bar/
|
|
208
|
+
*/
|
|
209
|
+
getProjectDir(cwd) {
|
|
210
|
+
const encoded = cwd.replace(/\//g, '-');
|
|
211
|
+
return path.join(this.projectsDir, encoded);
|
|
212
|
+
}
|
|
213
|
+
mapSessionToAgent(session, processInfo, sessionFile) {
|
|
179
214
|
return {
|
|
180
|
-
name:
|
|
215
|
+
name: (0, matching_1.generateAgentName)(processInfo.cwd, processInfo.pid),
|
|
181
216
|
type: this.type,
|
|
182
217
|
status: this.determineStatus(session),
|
|
183
|
-
summary:
|
|
218
|
+
summary: session.lastUserMessage || 'Session started',
|
|
184
219
|
pid: processInfo.pid,
|
|
185
|
-
projectPath:
|
|
186
|
-
sessionId:
|
|
220
|
+
projectPath: sessionFile.resolvedCwd || processInfo.cwd || '',
|
|
221
|
+
sessionId: sessionFile.sessionId,
|
|
187
222
|
slug: session.slug,
|
|
188
|
-
lastActive: session.lastActive
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
mapProcessOnlyAgent(processInfo, existingAgents, historyByProjectPath, usedSessionIds) {
|
|
192
|
-
const projectPath = processInfo.cwd || '';
|
|
193
|
-
const historyEntry = this.selectHistoryForProcess(projectPath, historyByProjectPath, usedSessionIds);
|
|
194
|
-
const sessionId = historyEntry?.sessionId || `pid-${processInfo.pid}`;
|
|
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
|
-
};
|
|
206
|
-
return {
|
|
207
|
-
name: this.generateAgentName(processSession, existingAgents),
|
|
208
|
-
type: this.type,
|
|
209
|
-
status: AgentAdapter_1.AgentStatus.RUNNING,
|
|
210
|
-
summary: historyEntry?.display || 'Claude process running',
|
|
211
|
-
pid: processInfo.pid,
|
|
212
|
-
projectPath,
|
|
213
|
-
sessionId: processSession.sessionId,
|
|
214
|
-
lastActive: processSession.lastActive || new Date(),
|
|
223
|
+
lastActive: session.lastActive,
|
|
215
224
|
};
|
|
216
225
|
}
|
|
217
|
-
|
|
218
|
-
const projectPath = processInfo.cwd || historyEntry.project;
|
|
219
|
-
const historySession = {
|
|
220
|
-
sessionId: historyEntry.sessionId,
|
|
221
|
-
projectPath,
|
|
222
|
-
lastCwd: projectPath,
|
|
223
|
-
sessionLogPath: '',
|
|
224
|
-
lastActive: new Date(historyEntry.timestamp),
|
|
225
|
-
};
|
|
226
|
+
mapProcessOnlyAgent(processInfo) {
|
|
226
227
|
return {
|
|
227
|
-
name:
|
|
228
|
+
name: (0, matching_1.generateAgentName)(processInfo.cwd || '', processInfo.pid),
|
|
228
229
|
type: this.type,
|
|
229
|
-
status: AgentAdapter_1.AgentStatus.
|
|
230
|
-
summary:
|
|
230
|
+
status: AgentAdapter_1.AgentStatus.IDLE,
|
|
231
|
+
summary: 'Unknown',
|
|
231
232
|
pid: processInfo.pid,
|
|
232
|
-
projectPath,
|
|
233
|
-
sessionId:
|
|
234
|
-
lastActive:
|
|
233
|
+
projectPath: processInfo.cwd || '',
|
|
234
|
+
sessionId: `pid-${processInfo.pid}`,
|
|
235
|
+
lastActive: new Date(),
|
|
235
236
|
};
|
|
236
237
|
}
|
|
237
|
-
indexHistoryByProjectPath(historyEntries) {
|
|
238
|
-
const grouped = new Map();
|
|
239
|
-
for (const entry of historyEntries) {
|
|
240
|
-
const key = this.normalizePath(entry.project);
|
|
241
|
-
const list = grouped.get(key) || [];
|
|
242
|
-
list.push(entry);
|
|
243
|
-
grouped.set(key, list);
|
|
244
|
-
}
|
|
245
|
-
for (const [key, list] of grouped.entries()) {
|
|
246
|
-
grouped.set(key, [...list].sort((a, b) => b.timestamp - a.timestamp));
|
|
247
|
-
}
|
|
248
|
-
return grouped;
|
|
249
|
-
}
|
|
250
|
-
selectHistoryForProcess(processCwd, historyByProjectPath, usedSessionIds) {
|
|
251
|
-
if (!processCwd) {
|
|
252
|
-
return undefined;
|
|
253
|
-
}
|
|
254
|
-
const candidates = historyByProjectPath.get(this.normalizePath(processCwd)) || [];
|
|
255
|
-
return candidates.find((entry) => !usedSessionIds.has(entry.sessionId));
|
|
256
|
-
}
|
|
257
238
|
/**
|
|
258
|
-
*
|
|
239
|
+
* Parse a single session file into ClaudeSession
|
|
259
240
|
*/
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
241
|
+
readSession(filePath, projectPath) {
|
|
242
|
+
const sessionId = path.basename(filePath, '.jsonl');
|
|
243
|
+
let content;
|
|
244
|
+
try {
|
|
245
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
263
246
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
const sessionFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
285
|
-
for (const sessionFile of sessionFiles) {
|
|
286
|
-
const sessionId = sessionFile.replace('.jsonl', '');
|
|
287
|
-
const sessionLogPath = path.join(projectDir, sessionFile);
|
|
288
|
-
try {
|
|
289
|
-
const sessionData = this.readSessionLog(sessionLogPath);
|
|
290
|
-
sessions.push({
|
|
291
|
-
sessionId,
|
|
292
|
-
projectPath: sessionsIndex.originalPath,
|
|
293
|
-
lastCwd: sessionData.lastCwd,
|
|
294
|
-
slug: sessionData.slug,
|
|
295
|
-
sessionLogPath,
|
|
296
|
-
lastEntry: sessionData.lastEntry,
|
|
297
|
-
lastActive: sessionData.lastActive,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
console.error(`Failed to read session ${sessionId}:`, error);
|
|
302
|
-
continue;
|
|
247
|
+
catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const allLines = content.trim().split('\n');
|
|
251
|
+
if (allLines.length === 0) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
// Parse first line for sessionStart.
|
|
255
|
+
// Claude Code may emit a "file-history-snapshot" as the first entry, which
|
|
256
|
+
// stores its timestamp inside "snapshot.timestamp" rather than at the root.
|
|
257
|
+
let sessionStart = null;
|
|
258
|
+
try {
|
|
259
|
+
const firstEntry = JSON.parse(allLines[0]);
|
|
260
|
+
const rawTs = firstEntry.timestamp || firstEntry.snapshot?.timestamp;
|
|
261
|
+
if (rawTs) {
|
|
262
|
+
const ts = new Date(rawTs);
|
|
263
|
+
if (!Number.isNaN(ts.getTime())) {
|
|
264
|
+
sessionStart = ts;
|
|
303
265
|
}
|
|
304
266
|
}
|
|
305
267
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
* Only reads last 100 lines for performance with large files
|
|
311
|
-
*/
|
|
312
|
-
readSessionLog(logPath) {
|
|
313
|
-
const lines = (0, file_1.readLastLines)(logPath, 100);
|
|
268
|
+
catch {
|
|
269
|
+
/* skip */
|
|
270
|
+
}
|
|
271
|
+
// Parse all lines for session state (file already in memory)
|
|
314
272
|
let slug;
|
|
315
|
-
let
|
|
273
|
+
let lastEntryType;
|
|
316
274
|
let lastActive;
|
|
317
275
|
let lastCwd;
|
|
318
|
-
|
|
276
|
+
let isInterrupted = false;
|
|
277
|
+
let lastUserMessage;
|
|
278
|
+
for (const line of allLines) {
|
|
319
279
|
try {
|
|
320
280
|
const entry = JSON.parse(line);
|
|
281
|
+
if (entry.timestamp) {
|
|
282
|
+
const ts = new Date(entry.timestamp);
|
|
283
|
+
if (!Number.isNaN(ts.getTime())) {
|
|
284
|
+
lastActive = ts;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
321
287
|
if (entry.slug && !slug) {
|
|
322
288
|
slug = entry.slug;
|
|
323
289
|
}
|
|
324
|
-
lastEntry = entry;
|
|
325
|
-
if (entry.timestamp) {
|
|
326
|
-
lastActive = new Date(entry.timestamp);
|
|
327
|
-
}
|
|
328
290
|
if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
|
|
329
291
|
lastCwd = entry.cwd;
|
|
330
292
|
}
|
|
293
|
+
if (entry.type && !this.isMetadataEntryType(entry.type)) {
|
|
294
|
+
lastEntryType = entry.type;
|
|
295
|
+
if (entry.type === 'user') {
|
|
296
|
+
const msgContent = entry.message?.content;
|
|
297
|
+
isInterrupted =
|
|
298
|
+
Array.isArray(msgContent) &&
|
|
299
|
+
msgContent.some((c) => (c.type === 'text' &&
|
|
300
|
+
c.text?.includes('[Request interrupted')) ||
|
|
301
|
+
(c.type === 'tool_result' &&
|
|
302
|
+
c.content?.includes('[Request interrupted')));
|
|
303
|
+
// Extract user message text for summary fallback
|
|
304
|
+
const text = this.extractUserMessageText(msgContent);
|
|
305
|
+
if (text) {
|
|
306
|
+
lastUserMessage = text;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
isInterrupted = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
331
313
|
}
|
|
332
|
-
catch
|
|
314
|
+
catch {
|
|
333
315
|
continue;
|
|
334
316
|
}
|
|
335
317
|
}
|
|
336
|
-
return {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
318
|
+
return {
|
|
319
|
+
sessionId,
|
|
320
|
+
projectPath: projectPath || lastCwd || '',
|
|
321
|
+
lastCwd,
|
|
322
|
+
slug,
|
|
323
|
+
sessionStart: sessionStart || lastActive || new Date(),
|
|
324
|
+
lastActive: lastActive || new Date(),
|
|
325
|
+
lastEntryType,
|
|
326
|
+
isInterrupted,
|
|
327
|
+
lastUserMessage,
|
|
328
|
+
};
|
|
344
329
|
}
|
|
345
330
|
/**
|
|
346
|
-
* Determine agent status from session
|
|
331
|
+
* Determine agent status from session state
|
|
347
332
|
*/
|
|
348
333
|
determineStatus(session) {
|
|
349
|
-
if (!session.
|
|
334
|
+
if (!session.lastEntryType) {
|
|
350
335
|
return AgentAdapter_1.AgentStatus.UNKNOWN;
|
|
351
336
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
return
|
|
337
|
+
// No age-based IDLE override: every agent in the list is backed by
|
|
338
|
+
// a running process (found via ps), so the entry type is the best
|
|
339
|
+
// indicator of actual state.
|
|
340
|
+
if (session.lastEntryType === 'user') {
|
|
341
|
+
return session.isInterrupted
|
|
342
|
+
? AgentAdapter_1.AgentStatus.WAITING
|
|
343
|
+
: AgentAdapter_1.AgentStatus.RUNNING;
|
|
357
344
|
}
|
|
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
|
-
}
|
|
345
|
+
if (session.lastEntryType === 'progress' ||
|
|
346
|
+
session.lastEntryType === 'thinking') {
|
|
367
347
|
return AgentAdapter_1.AgentStatus.RUNNING;
|
|
368
348
|
}
|
|
369
|
-
if (
|
|
370
|
-
return AgentAdapter_1.AgentStatus.RUNNING;
|
|
371
|
-
}
|
|
372
|
-
else if (entryType === SessionEntryType.ASSISTANT) {
|
|
349
|
+
if (session.lastEntryType === 'assistant') {
|
|
373
350
|
return AgentAdapter_1.AgentStatus.WAITING;
|
|
374
351
|
}
|
|
375
|
-
|
|
352
|
+
if (session.lastEntryType === 'system') {
|
|
376
353
|
return AgentAdapter_1.AgentStatus.IDLE;
|
|
377
354
|
}
|
|
378
355
|
return AgentAdapter_1.AgentStatus.UNKNOWN;
|
|
379
356
|
}
|
|
380
357
|
/**
|
|
381
|
-
*
|
|
382
|
-
*
|
|
358
|
+
* Extract meaningful text from a user message content.
|
|
359
|
+
* Handles string and array formats, skill command expansion, and noise filtering.
|
|
383
360
|
*/
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (sameProjectAgents.length === 0) {
|
|
388
|
-
return projectName;
|
|
361
|
+
extractUserMessageText(content) {
|
|
362
|
+
if (!content) {
|
|
363
|
+
return undefined;
|
|
389
364
|
}
|
|
390
|
-
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
const slugPart = session.slug.includes('-')
|
|
394
|
-
? session.slug.split('-')[0]
|
|
395
|
-
: session.slug.slice(0, 8);
|
|
396
|
-
return `${projectName} (${slugPart})`;
|
|
365
|
+
let raw;
|
|
366
|
+
if (typeof content === 'string') {
|
|
367
|
+
raw = content.trim();
|
|
397
368
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
369
|
+
else if (Array.isArray(content)) {
|
|
370
|
+
for (const block of content) {
|
|
371
|
+
if (block.type === 'text' && block.text?.trim()) {
|
|
372
|
+
raw = block.text.trim();
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
404
376
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
isChildPath(child, parent) {
|
|
408
|
-
if (!child || !parent) {
|
|
409
|
-
return false;
|
|
377
|
+
if (!raw) {
|
|
378
|
+
return undefined;
|
|
410
379
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
normalizePath(value) {
|
|
416
|
-
const resolved = path.resolve(value);
|
|
417
|
-
if (resolved.length > 1 && resolved.endsWith(path.sep)) {
|
|
418
|
-
return resolved.slice(0, -1);
|
|
380
|
+
// Skill slash-command: extract /command-name and args
|
|
381
|
+
if (raw.startsWith('<command-message>')) {
|
|
382
|
+
return this.parseCommandMessage(raw);
|
|
419
383
|
}
|
|
420
|
-
|
|
384
|
+
// Expanded skill content: extract ARGUMENTS line if present, skip otherwise
|
|
385
|
+
if (raw.startsWith('Base directory for this skill:')) {
|
|
386
|
+
const argsMatch = raw.match(/\nARGUMENTS:\s*(.+)/);
|
|
387
|
+
return argsMatch?.[1]?.trim() || undefined;
|
|
388
|
+
}
|
|
389
|
+
// Filter noise
|
|
390
|
+
if (this.isNoiseMessage(raw)) {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
return raw;
|
|
421
394
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Parse a <command-message> string into "/command args" format.
|
|
397
|
+
*/
|
|
398
|
+
parseCommandMessage(raw) {
|
|
399
|
+
const nameMatch = raw.match(/<command-name>([^<]+)<\/command-name>/);
|
|
400
|
+
const argsMatch = raw.match(/<command-args>([^<]+)<\/command-args>/);
|
|
401
|
+
const name = nameMatch?.[1]?.trim();
|
|
402
|
+
if (!name) {
|
|
403
|
+
return undefined;
|
|
425
404
|
}
|
|
426
|
-
|
|
405
|
+
const args = argsMatch?.[1]?.trim();
|
|
406
|
+
return args ? `${name} ${args}` : name;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if a message is noise (not a meaningful user intent).
|
|
410
|
+
*/
|
|
411
|
+
isNoiseMessage(text) {
|
|
412
|
+
return (text.startsWith('[Request interrupted') ||
|
|
413
|
+
text === 'Tool loaded.' ||
|
|
414
|
+
text.startsWith('This session is being continued'));
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Check if an entry type is metadata (not conversation state).
|
|
418
|
+
* These should not overwrite lastEntryType used for status determination.
|
|
419
|
+
*/
|
|
420
|
+
isMetadataEntryType(type) {
|
|
421
|
+
return type === 'last-prompt' || type === 'file-history-snapshot';
|
|
427
422
|
}
|
|
428
423
|
}
|
|
429
424
|
exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
|
|
430
|
-
/** Threshold in minutes before considering a session idle */
|
|
431
|
-
ClaudeCodeAdapter.IDLE_THRESHOLD_MINUTES = 5;
|
|
432
425
|
//# sourceMappingURL=ClaudeCodeAdapter.js.map
|