@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.
Files changed (50) hide show
  1. package/dist/adapters/AgentAdapter.d.ts +2 -0
  2. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  3. package/dist/adapters/ClaudeCodeAdapter.d.ts +49 -38
  4. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  5. package/dist/adapters/ClaudeCodeAdapter.js +286 -293
  6. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  7. package/dist/adapters/CodexAdapter.d.ts +32 -30
  8. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  9. package/dist/adapters/CodexAdapter.js +148 -284
  10. package/dist/adapters/CodexAdapter.js.map +1 -1
  11. package/dist/index.d.ts +1 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -10
  14. package/dist/index.js.map +1 -1
  15. package/dist/utils/index.d.ts +6 -3
  16. package/dist/utils/index.d.ts.map +1 -1
  17. package/dist/utils/index.js +12 -11
  18. package/dist/utils/index.js.map +1 -1
  19. package/dist/utils/matching.d.ts +39 -0
  20. package/dist/utils/matching.d.ts.map +1 -0
  21. package/dist/utils/matching.js +103 -0
  22. package/dist/utils/matching.js.map +1 -0
  23. package/dist/utils/process.d.ts +25 -40
  24. package/dist/utils/process.d.ts.map +1 -1
  25. package/dist/utils/process.js +151 -105
  26. package/dist/utils/process.js.map +1 -1
  27. package/dist/utils/session.d.ts +30 -0
  28. package/dist/utils/session.d.ts.map +1 -0
  29. package/dist/utils/session.js +101 -0
  30. package/dist/utils/session.js.map +1 -0
  31. package/package.json +2 -2
  32. package/src/__tests__/AgentManager.test.ts +0 -25
  33. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +921 -205
  34. package/src/__tests__/adapters/CodexAdapter.test.ts +468 -269
  35. package/src/__tests__/utils/matching.test.ts +191 -0
  36. package/src/__tests__/utils/process.test.ts +202 -0
  37. package/src/__tests__/utils/session.test.ts +117 -0
  38. package/src/adapters/AgentAdapter.ts +3 -0
  39. package/src/adapters/ClaudeCodeAdapter.ts +341 -418
  40. package/src/adapters/CodexAdapter.ts +155 -420
  41. package/src/index.ts +1 -3
  42. package/src/utils/index.ts +6 -3
  43. package/src/utils/matching.ts +92 -0
  44. package/src/utils/process.ts +133 -119
  45. package/src/utils/session.ts +92 -0
  46. package/dist/utils/file.d.ts +0 -52
  47. package/dist/utils/file.d.ts.map +0 -1
  48. package/dist/utils/file.js +0 -135
  49. package/dist/utils/file.js.map +0 -1
  50. 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 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 = {}));
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. Reading session files from ~/.claude/projects/
64
- * 3. Matching sessions to processes via CWD
65
- * 4. Extracting status from session JSONL
66
- * 5. Extracting summary from history.jsonl
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.claudeDir = path.join(homeDir, '.claude');
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');
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.toLowerCase().includes('claude');
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 claudeProcesses = (0, process_1.listProcesses)({ namePattern: 'claude' }).filter((processInfo) => this.canHandle(processInfo));
87
- if (claudeProcesses.length === 0) {
75
+ const processes = (0, process_1.enrichProcesses)((0, process_1.listAgentProcesses)('claude'));
76
+ if (processes.length === 0) {
88
77
  return [];
89
78
  }
90
- const sessions = this.readSessions();
91
- const history = this.readHistory();
92
- const historyByProjectPath = this.indexHistoryByProjectPath(history);
93
- const historyBySessionId = new Map();
94
- for (const entry of history) {
95
- historyBySessionId.set(entry.sessionId, entry);
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
- });
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
- this.assignSessionsForMode('cwd', claudeProcesses, sortedSessions, usedSessionIds, assignedPids, historyBySessionId, agents);
106
- this.assignHistoryEntriesForExactProcessCwd(claudeProcesses, assignedPids, historyByProjectPath, usedSessionIds, agents);
107
- this.assignSessionsForMode('project-parent', claudeProcesses, sortedSessions, usedSessionIds, assignedPids, historyBySessionId, agents);
108
- for (const processInfo of claudeProcesses) {
109
- if (assignedPids.has(processInfo.pid)) {
110
- continue;
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
- return agents;
116
- }
117
- assignHistoryEntriesForExactProcessCwd(claudeProcesses, assignedPids, historyByProjectPath, usedSessionIds, agents) {
118
- for (const processInfo of claudeProcesses) {
119
- if (assignedPids.has(processInfo.pid)) {
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
- const historyEntry = this.selectHistoryForProcess(processInfo.cwd || '', historyByProjectPath, usedSessionIds);
123
- if (!historyEntry) {
124
- continue;
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
- assignSessionsForMode(mode, claudeProcesses, sessions, usedSessionIds, assignedPids, historyBySessionId, agents) {
132
- for (const processInfo of claudeProcesses) {
133
- if (assignedPids.has(processInfo.pid)) {
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
- const session = this.selectBestSession(processInfo, sessions, usedSessionIds, mode);
137
- if (!session) {
139
+ catch {
138
140
  continue;
139
141
  }
140
- usedSessionIds.add(session.sessionId);
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
- selectBestSession(processInfo, sessions, usedSessionIds, mode) {
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);
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
- if (mode === 'project-parent') {
155
- return this.isChildPath(processInfo.cwd, session.projectPath)
156
- || this.isChildPath(processInfo.cwd, session.lastCwd);
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
- 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];
201
+ return { direct, fallback };
176
202
  }
177
- mapSessionToAgent(session, processInfo, historyBySessionId, existingAgents) {
178
- const historyEntry = historyBySessionId.get(session.sessionId);
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: this.generateAgentName(session, existingAgents),
215
+ name: (0, matching_1.generateAgentName)(processInfo.cwd, processInfo.pid),
181
216
  type: this.type,
182
217
  status: this.determineStatus(session),
183
- summary: historyEntry?.display || 'Session started',
218
+ summary: session.lastUserMessage || 'Session started',
184
219
  pid: processInfo.pid,
185
- projectPath: session.projectPath || processInfo.cwd || '',
186
- sessionId: session.sessionId,
220
+ projectPath: sessionFile.resolvedCwd || processInfo.cwd || '',
221
+ sessionId: sessionFile.sessionId,
187
222
  slug: session.slug,
188
- lastActive: session.lastActive || new Date(),
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
- mapHistoryToAgent(processInfo, historyEntry, existingAgents) {
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: this.generateAgentName(historySession, existingAgents),
228
+ name: (0, matching_1.generateAgentName)(processInfo.cwd || '', processInfo.pid),
228
229
  type: this.type,
229
- status: AgentAdapter_1.AgentStatus.RUNNING,
230
- summary: historyEntry.display || 'Claude process running',
230
+ status: AgentAdapter_1.AgentStatus.IDLE,
231
+ summary: 'Unknown',
231
232
  pid: processInfo.pid,
232
- projectPath,
233
- sessionId: historySession.sessionId,
234
- lastActive: historySession.lastActive || new Date(),
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
- * Read all Claude Code sessions
239
+ * Parse a single session file into ClaudeSession
259
240
  */
260
- readSessions() {
261
- if (!fs.existsSync(this.projectsDir)) {
262
- return [];
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
- const sessions = [];
265
- const projectDirs = fs.readdirSync(this.projectsDir);
266
- for (const dirName of projectDirs) {
267
- if (dirName.startsWith('.')) {
268
- continue;
269
- }
270
- const projectDir = path.join(this.projectsDir, dirName);
271
- if (!fs.statSync(projectDir).isDirectory()) {
272
- continue;
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;
278
- }
279
- const sessionsIndex = (0, file_1.readJson)(indexPath);
280
- if (!sessionsIndex) {
281
- console.error(`Failed to parse ${indexPath}`);
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
- return sessions;
307
- }
308
- /**
309
- * Read a session JSONL file
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 lastEntry;
273
+ let lastEntryType;
316
274
  let lastActive;
317
275
  let lastCwd;
318
- for (const line of lines) {
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 (error) {
314
+ catch {
333
315
  continue;
334
316
  }
335
317
  }
336
- return { slug, lastEntry, lastActive, lastCwd };
337
- }
338
- /**
339
- * Read history.jsonl for user prompts
340
- * Only reads last 100 lines for performance
341
- */
342
- readHistory() {
343
- return (0, file_1.readJsonLines)(this.historyPath, 100);
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 entry
331
+ * Determine agent status from session state
347
332
  */
348
333
  determineStatus(session) {
349
- if (!session.lastEntry) {
334
+ if (!session.lastEntryType) {
350
335
  return AgentAdapter_1.AgentStatus.UNKNOWN;
351
336
  }
352
- const entryType = session.lastEntry.type;
353
- const lastActive = session.lastActive || new Date(0);
354
- const ageMinutes = (Date.now() - lastActive.getTime()) / 1000 / 60;
355
- if (ageMinutes > ClaudeCodeAdapter.IDLE_THRESHOLD_MINUTES) {
356
- return AgentAdapter_1.AgentStatus.IDLE;
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 (entryType === SessionEntryType.USER) {
359
- // Check if user interrupted manually - this puts agent back in waiting state
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 (entryType === SessionEntryType.PROGRESS || entryType === SessionEntryType.THINKING) {
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
- else if (entryType === SessionEntryType.SYSTEM) {
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
- * Generate unique agent name
382
- * Uses project basename, appends slug if multiple sessions for same project
358
+ * Extract meaningful text from a user message content.
359
+ * Handles string and array formats, skill command expansion, and noise filtering.
383
360
  */
384
- generateAgentName(session, existingAgents) {
385
- const projectName = path.basename(session.projectPath) || 'claude';
386
- const sameProjectAgents = existingAgents.filter(a => a.projectPath === session.projectPath);
387
- if (sameProjectAgents.length === 0) {
388
- return projectName;
361
+ extractUserMessageText(content) {
362
+ if (!content) {
363
+ return undefined;
389
364
  }
390
- // Multiple sessions for same project, append slug
391
- if (session.slug) {
392
- // Use first word of slug for brevity (with safety check for format)
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
- // No slug available, use session ID prefix
399
- return `${projectName} (${session.sessionId.slice(0, 8)})`;
400
- }
401
- pathEquals(a, b) {
402
- if (!a || !b) {
403
- return false;
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
- return this.normalizePath(a) === this.normalizePath(b);
406
- }
407
- isChildPath(child, parent) {
408
- if (!child || !parent) {
409
- return false;
377
+ if (!raw) {
378
+ return undefined;
410
379
  }
411
- const normalizedChild = this.normalizePath(child);
412
- const normalizedParent = this.normalizePath(parent);
413
- return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
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
- return resolved;
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
- pathDepth(value) {
423
- if (!value) {
424
- return 0;
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
- return this.normalizePath(value).split(path.sep).filter(Boolean).length;
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