@ai-devkit/agent-manager 0.4.0 → 0.6.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 +21 -2
  2. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  3. package/dist/adapters/ClaudeCodeAdapter.d.ts +44 -35
  4. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  5. package/dist/adapters/ClaudeCodeAdapter.js +230 -298
  6. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  7. package/dist/adapters/CodexAdapter.d.ts +41 -31
  8. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  9. package/dist/adapters/CodexAdapter.js +198 -278
  10. package/dist/adapters/CodexAdapter.js.map +1 -1
  11. package/dist/index.d.ts +2 -4
  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 +107 -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 +1 -1
  32. package/src/__tests__/AgentManager.test.ts +5 -27
  33. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +754 -830
  34. package/src/__tests__/adapters/CodexAdapter.test.ts +581 -273
  35. package/src/__tests__/utils/matching.test.ts +199 -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 +23 -4
  39. package/src/adapters/ClaudeCodeAdapter.ts +285 -437
  40. package/src/adapters/CodexAdapter.ts +202 -400
  41. package/src/index.ts +2 -4
  42. package/src/utils/index.ts +6 -3
  43. package/src/utils/matching.ts +96 -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
@@ -36,18 +36,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ClaudeCodeAdapter = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const child_process_1 = require("child_process");
40
39
  const AgentAdapter_1 = require("./AgentAdapter");
41
40
  const process_1 = require("../utils/process");
42
- const file_1 = require("../utils/file");
41
+ const session_1 = require("../utils/session");
42
+ const matching_1 = require("../utils/matching");
43
43
  /**
44
44
  * Claude Code Adapter
45
45
  *
46
46
  * Detects Claude Code agents by:
47
- * 1. Finding running claude processes
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
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
51
  * 5. Extracting summary from last user message in session JSONL
52
52
  */
53
53
  class ClaudeCodeAdapter {
@@ -55,6 +55,7 @@ class ClaudeCodeAdapter {
55
55
  this.type = 'claude';
56
56
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
57
57
  this.projectsDir = path.join(homeDir, '.claude', 'projects');
58
+ this.sessionsDir = path.join(homeDir, '.claude', 'sessions');
58
59
  }
59
60
  /**
60
61
  * Check if this adapter can handle a given process
@@ -71,277 +72,169 @@ class ClaudeCodeAdapter {
71
72
  * Detect running Claude Code agents
72
73
  */
73
74
  async detectAgents() {
74
- const claudeProcesses = this.listClaudeProcesses();
75
- if (claudeProcesses.length === 0) {
75
+ const processes = (0, process_1.enrichProcesses)((0, process_1.listAgentProcesses)('claude'));
76
+ if (processes.length === 0) {
76
77
  return [];
77
78
  }
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());
85
- const usedSessionIds = new Set();
86
- 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
+ ]);
87
90
  const agents = [];
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
+ // 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);
99
+ }
91
100
  }
92
- for (const processInfo of claudeProcesses) {
93
- if (assignedPids.has(processInfo.pid)) {
94
- 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));
106
+ }
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));
95
115
  }
96
- assignedPids.add(processInfo.pid);
97
- agents.push(this.mapProcessOnlyAgent(processInfo, agents));
98
116
  }
99
117
  return agents;
100
118
  }
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);
106
- }
107
- assignSessionsForMode(mode, claudeProcesses, sessions, usedSessionIds, assignedPids, processStartByPid, agents) {
108
- for (const processInfo of claudeProcesses) {
109
- 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)
131
+ continue;
132
+ const projectDir = this.getProjectDir(proc.cwd);
133
+ if (dirToCwd.has(projectDir))
110
134
  continue;
135
+ try {
136
+ if (!fs.statSync(projectDir).isDirectory())
137
+ continue;
111
138
  }
112
- const session = this.selectBestSession(processInfo, sessions, usedSessionIds, processStartByPid, mode);
113
- if (!session) {
139
+ catch {
114
140
  continue;
115
141
  }
116
- usedSessionIds.add(session.sessionId);
117
- assignedPids.add(processInfo.pid);
118
- agents.push(this.mapSessionToAgent(session, processInfo, agents));
142
+ dirToCwd.set(projectDir, proc.cwd);
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;
153
+ }
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
+ });
195
+ }
196
+ catch {
197
+ // PID file absent, unreadable, or malformed — fall back per-process
198
+ fallback.push(proc);
199
+ }
119
200
  }
201
+ return { direct, fallback };
120
202
  }
121
- mapSessionToAgent(session, processInfo, existingAgents) {
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) {
122
214
  return {
123
- name: this.generateAgentName(session, existingAgents),
215
+ name: (0, matching_1.generateAgentName)(processInfo.cwd, processInfo.pid),
124
216
  type: this.type,
125
217
  status: this.determineStatus(session),
126
218
  summary: session.lastUserMessage || 'Session started',
127
219
  pid: processInfo.pid,
128
- projectPath: session.projectPath || processInfo.cwd || '',
129
- sessionId: session.sessionId,
130
- slug: session.slug,
220
+ projectPath: sessionFile.resolvedCwd || processInfo.cwd || '',
221
+ sessionId: sessionFile.sessionId,
131
222
  lastActive: session.lastActive,
223
+ sessionFilePath: sessionFile.filePath,
132
224
  };
133
225
  }
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);
226
+ mapProcessOnlyAgent(processInfo) {
138
227
  return {
139
- name: hasDuplicate ? `${projectName} (pid-${processInfo.pid})` : projectName,
228
+ name: (0, matching_1.generateAgentName)(processInfo.cwd || '', processInfo.pid),
140
229
  type: this.type,
141
230
  status: AgentAdapter_1.AgentStatus.IDLE,
142
231
  summary: 'Unknown',
143
232
  pid: processInfo.pid,
144
- projectPath: processCwd,
233
+ projectPath: processInfo.cwd || '',
145
234
  sessionId: `pid-${processInfo.pid}`,
146
235
  lastActive: new Date(),
147
236
  };
148
237
  }
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;
172
- }
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
- });
191
- }
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();
223
- }
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;
259
- }
260
- /**
261
- * Read Claude Code sessions with bounded scanning
262
- */
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) {
283
- if (!fs.existsSync(this.projectsDir)) {
284
- return [];
285
- }
286
- const files = [];
287
- for (const dirName of fs.readdirSync(this.projectsDir)) {
288
- if (dirName.startsWith('.')) {
289
- continue;
290
- }
291
- const projectDir = path.join(this.projectsDir, dirName);
292
- try {
293
- if (!fs.statSync(projectDir).isDirectory())
294
- continue;
295
- }
296
- catch {
297
- continue;
298
- }
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);
307
- try {
308
- files.push({
309
- filePath,
310
- projectPath,
311
- mtimeMs: fs.statSync(filePath).mtimeMs,
312
- });
313
- }
314
- catch {
315
- continue;
316
- }
317
- }
318
- }
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);
344
- }
345
238
  /**
346
239
  * Parse a single session file into ClaudeSession
347
240
  */
@@ -376,7 +269,6 @@ class ClaudeCodeAdapter {
376
269
  /* skip */
377
270
  }
378
271
  // Parse all lines for session state (file already in memory)
379
- let slug;
380
272
  let lastEntryType;
381
273
  let lastActive;
382
274
  let lastCwd;
@@ -391,9 +283,6 @@ class ClaudeCodeAdapter {
391
283
  lastActive = ts;
392
284
  }
393
285
  }
394
- if (entry.slug && !slug) {
395
- slug = entry.slug;
396
- }
397
286
  if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
398
287
  lastCwd = entry.cwd;
399
288
  }
@@ -426,7 +315,6 @@ class ClaudeCodeAdapter {
426
315
  sessionId,
427
316
  projectPath: projectPath || lastCwd || '',
428
317
  lastCwd,
429
- slug,
430
318
  sessionStart: sessionStart || lastActive || new Date(),
431
319
  lastActive: lastActive || new Date(),
432
320
  lastEntryType,
@@ -461,42 +349,6 @@ class ClaudeCodeAdapter {
461
349
  }
462
350
  return AgentAdapter_1.AgentStatus.UNKNOWN;
463
351
  }
464
- /**
465
- * Generate unique agent name
466
- * Uses project basename, appends slug if multiple sessions for same project
467
- */
468
- generateAgentName(session, existingAgents) {
469
- const projectName = path.basename(session.projectPath) || 'claude';
470
- const sameProjectAgents = existingAgents.filter((a) => a.projectPath === session.projectPath);
471
- if (sameProjectAgents.length === 0) {
472
- return projectName;
473
- }
474
- if (session.slug) {
475
- const slugPart = session.slug.includes('-')
476
- ? session.slug.split('-')[0]
477
- : session.slug.slice(0, 8);
478
- return `${projectName} (${slugPart})`;
479
- }
480
- return `${projectName} (${session.sessionId.slice(0, 8)})`;
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
- }
486
- pathEquals(a, b) {
487
- if (!a || !b) {
488
- return false;
489
- }
490
- return this.normalizePath(a) === this.normalizePath(b);
491
- }
492
- isChildPath(child, parent) {
493
- if (!child || !parent) {
494
- return false;
495
- }
496
- const normalizedChild = this.normalizePath(child);
497
- const normalizedParent = this.normalizePath(parent);
498
- return normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
499
- }
500
352
  /**
501
353
  * Extract meaningful text from a user message content.
502
354
  * Handles string and array formats, skill command expansion, and noise filtering.
@@ -563,19 +415,99 @@ class ClaudeCodeAdapter {
563
415
  isMetadataEntryType(type) {
564
416
  return type === 'last-prompt' || type === 'file-history-snapshot';
565
417
  }
566
- normalizePath(value) {
567
- const resolved = path.resolve(value);
568
- if (resolved.length > 1 && resolved.endsWith(path.sep)) {
569
- return resolved.slice(0, -1);
418
+ /**
419
+ * Read the full conversation from a Claude Code session JSONL file.
420
+ *
421
+ * Default mode returns only text content from user/assistant/system messages.
422
+ * Verbose mode also includes tool_use and tool_result blocks.
423
+ */
424
+ getConversation(sessionFilePath, options) {
425
+ const verbose = options?.verbose ?? false;
426
+ let content;
427
+ try {
428
+ content = fs.readFileSync(sessionFilePath, 'utf-8');
570
429
  }
571
- return resolved;
430
+ catch {
431
+ return [];
432
+ }
433
+ const lines = content.trim().split('\n');
434
+ const messages = [];
435
+ for (const line of lines) {
436
+ let entry;
437
+ try {
438
+ entry = JSON.parse(line);
439
+ }
440
+ catch {
441
+ continue;
442
+ }
443
+ const entryType = entry.type;
444
+ if (!entryType || this.isMetadataEntryType(entryType))
445
+ continue;
446
+ if (entryType === 'progress' || entryType === 'thinking')
447
+ continue;
448
+ let role;
449
+ if (entryType === 'user') {
450
+ role = 'user';
451
+ }
452
+ else if (entryType === 'assistant') {
453
+ role = 'assistant';
454
+ }
455
+ else if (entryType === 'system') {
456
+ role = 'system';
457
+ }
458
+ else {
459
+ continue;
460
+ }
461
+ const text = this.extractConversationContent(entry.message?.content, role, verbose);
462
+ if (!text)
463
+ continue;
464
+ messages.push({
465
+ role,
466
+ content: text,
467
+ timestamp: entry.timestamp,
468
+ });
469
+ }
470
+ return messages;
471
+ }
472
+ /**
473
+ * Extract displayable content from a message content field.
474
+ */
475
+ extractConversationContent(content, role, verbose) {
476
+ if (!content)
477
+ return undefined;
478
+ if (typeof content === 'string') {
479
+ const trimmed = content.trim();
480
+ if (role === 'user' && this.isNoiseMessage(trimmed))
481
+ return undefined;
482
+ return trimmed || undefined;
483
+ }
484
+ if (!Array.isArray(content))
485
+ return undefined;
486
+ const parts = [];
487
+ for (const block of content) {
488
+ if (block.type === 'text' && block.text?.trim()) {
489
+ if (role === 'user' && this.isNoiseMessage(block.text.trim()))
490
+ continue;
491
+ parts.push(block.text.trim());
492
+ }
493
+ else if (block.type === 'tool_use' && verbose) {
494
+ const inputSummary = block.input?.file_path || block.input?.pattern || block.input?.command || '';
495
+ parts.push(`[Tool: ${block.name}]${inputSummary ? ' ' + inputSummary : ''}`);
496
+ }
497
+ else if (block.type === 'tool_result' && verbose) {
498
+ const truncated = this.truncateToolResult(block.content || '');
499
+ const prefix = block.is_error ? '[Tool Error]' : '[Tool Result]';
500
+ parts.push(`${prefix} ${truncated}`);
501
+ }
502
+ }
503
+ return parts.length > 0 ? parts.join('\n') : undefined;
504
+ }
505
+ truncateToolResult(content, maxLength = 200) {
506
+ const firstLine = content.split('\n')[0] || '';
507
+ if (firstLine.length <= maxLength)
508
+ return firstLine;
509
+ return firstLine.slice(0, maxLength - 3) + '...';
572
510
  }
573
511
  }
574
512
  exports.ClaudeCodeAdapter = 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;
581
513
  //# sourceMappingURL=ClaudeCodeAdapter.js.map