@ai-devkit/agent-manager 0.9.0 → 0.11.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 (63) hide show
  1. package/dist/AgentManager.d.ts +21 -1
  2. package/dist/AgentManager.d.ts.map +1 -1
  3. package/dist/AgentManager.js +47 -0
  4. package/dist/AgentManager.js.map +1 -1
  5. package/dist/adapters/AgentAdapter.d.ts +66 -0
  6. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  7. package/dist/adapters/ClaudeCodeAdapter.d.ts +14 -43
  8. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
  9. package/dist/adapters/ClaudeCodeAdapter.js +60 -275
  10. package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
  11. package/dist/adapters/CodexAdapter.d.ts +14 -1
  12. package/dist/adapters/CodexAdapter.d.ts.map +1 -1
  13. package/dist/adapters/CodexAdapter.js +105 -6
  14. package/dist/adapters/CodexAdapter.js.map +1 -1
  15. package/dist/adapters/GeminiCliAdapter.d.ts +9 -1
  16. package/dist/adapters/GeminiCliAdapter.d.ts.map +1 -1
  17. package/dist/adapters/GeminiCliAdapter.js +77 -6
  18. package/dist/adapters/GeminiCliAdapter.js.map +1 -1
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/terminal/TerminalFocusManager.d.ts.map +1 -1
  23. package/dist/terminal/TerminalFocusManager.js +36 -27
  24. package/dist/terminal/TerminalFocusManager.js.map +1 -1
  25. package/dist/terminal/TtyWriter.d.ts.map +1 -1
  26. package/dist/terminal/TtyWriter.js +3 -12
  27. package/dist/terminal/TtyWriter.js.map +1 -1
  28. package/dist/utils/ClaudeSessionParser.d.ts +114 -0
  29. package/dist/utils/ClaudeSessionParser.d.ts.map +1 -0
  30. package/dist/utils/ClaudeSessionParser.js +377 -0
  31. package/dist/utils/ClaudeSessionParser.js.map +1 -0
  32. package/dist/utils/applescript.d.ts +6 -0
  33. package/dist/utils/applescript.d.ts.map +1 -0
  34. package/dist/utils/applescript.js +14 -0
  35. package/dist/utils/applescript.js.map +1 -0
  36. package/dist/utils/process.d.ts +3 -4
  37. package/dist/utils/process.d.ts.map +1 -1
  38. package/dist/utils/process.js +11 -15
  39. package/dist/utils/process.js.map +1 -1
  40. package/dist/utils/session.d.ts +34 -5
  41. package/dist/utils/session.d.ts.map +1 -1
  42. package/dist/utils/session.js +90 -44
  43. package/dist/utils/session.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/AgentManager.ts +66 -4
  46. package/src/__tests__/AgentManager.test.ts +134 -2
  47. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +188 -32
  48. package/src/__tests__/adapters/CodexAdapter.test.ts +123 -3
  49. package/src/__tests__/adapters/GeminiCliAdapter.test.ts +133 -0
  50. package/src/__tests__/utils/ClaudeSessionParser.test.ts +195 -0
  51. package/src/__tests__/utils/process.test.ts +27 -27
  52. package/src/__tests__/utils/session.test.ts +79 -43
  53. package/src/adapters/AgentAdapter.ts +76 -0
  54. package/src/adapters/ClaudeCodeAdapter.ts +82 -356
  55. package/src/adapters/CodexAdapter.ts +126 -8
  56. package/src/adapters/GeminiCliAdapter.ts +102 -7
  57. package/src/index.ts +9 -1
  58. package/src/terminal/TerminalFocusManager.ts +35 -26
  59. package/src/terminal/TtyWriter.ts +1 -11
  60. package/src/utils/ClaudeSessionParser.ts +437 -0
  61. package/src/utils/applescript.ts +10 -0
  62. package/src/utils/process.ts +21 -24
  63. package/src/utils/session.ts +86 -45
@@ -1,66 +1,45 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import type { AgentAdapter, AgentInfo, ProcessInfo, ConversationMessage } from './AgentAdapter';
3
+ import type {
4
+ AgentAdapter,
5
+ AgentInfo,
6
+ ProcessInfo,
7
+ ConversationMessage,
8
+ SessionSummary,
9
+ ListSessionsOptions,
10
+ } from './AgentAdapter';
4
11
  import { AgentStatus } from './AgentAdapter';
5
12
  import { listAgentProcesses, enrichProcesses } from '../utils/process';
6
- import { batchGetSessionFileBirthtimes } from '../utils/session';
13
+ import { batchGetSessionFileBirthtimes, isDirectory, listJsonl, safeReaddir, safeStat } from '../utils/session';
7
14
  import type { SessionFile } from '../utils/session';
8
15
  import { matchProcessesToSessions, generateAgentName } from '../utils/matching';
9
- /**
10
- * Entry in session JSONL file
11
- */
12
- interface ContentBlock {
13
- type?: string;
14
- text?: string;
15
- content?: string;
16
- name?: string;
17
- input?: Record<string, unknown>;
18
- tool_use_id?: string;
19
- is_error?: boolean;
20
- }
21
-
22
- interface SessionEntry {
23
- type?: string;
24
- timestamp?: string;
25
- cwd?: string;
26
- message?: {
27
- content?: string | ContentBlock[];
28
- };
29
- }
16
+ import { ClaudeSessionParser } from '../utils/ClaudeSessionParser';
17
+ import type { ClaudeSession } from '../utils/ClaudeSessionParser';
30
18
 
31
19
  /**
32
- * Entry in ~/.claude/sessions/<pid>.json written by Claude Code
20
+ * Entry in ~/.claude/sessions/<pid>.json written by Claude Code.
21
+ * Maps a running process to its session file via PID.
33
22
  */
34
23
  interface PidFileEntry {
35
24
  pid: number;
36
25
  sessionId: string;
37
26
  cwd: string;
38
- startedAt: number; // epoch milliseconds
27
+ /** Epoch milliseconds when the Claude Code process started */
28
+ startedAt: number;
39
29
  kind: string;
40
30
  entrypoint: string;
41
31
  }
42
32
 
43
33
  /**
44
- * A process directly matched to a session via PID file (authoritative path)
34
+ * A process directly matched to a session via PID file (authoritative path).
45
35
  */
46
36
  interface DirectMatch {
47
37
  process: ProcessInfo;
48
38
  sessionFile: SessionFile;
49
39
  }
50
40
 
51
- /**
52
- * Claude Code session information
53
- */
54
- interface ClaudeSession {
55
- sessionId: string;
56
- projectPath: string;
57
- lastCwd?: string;
58
- sessionStart: Date;
59
- lastActive: Date;
60
- lastEntryType?: string;
61
- isInterrupted: boolean;
62
- lastUserMessage?: string;
63
- }
41
+ /** Maximum allowed delta (ms) between process start time and PID file startedAt. */
42
+ const PID_FILE_STALENESS_MS = 60000;
64
43
 
65
44
  /**
66
45
  * Claude Code Adapter
@@ -77,16 +56,15 @@ export class ClaudeCodeAdapter implements AgentAdapter {
77
56
 
78
57
  private projectsDir: string;
79
58
  private sessionsDir: string;
59
+ private parser: ClaudeSessionParser;
80
60
 
81
61
  constructor() {
82
62
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
83
63
  this.projectsDir = path.join(homeDir, '.claude', 'projects');
84
64
  this.sessionsDir = path.join(homeDir, '.claude', 'sessions');
65
+ this.parser = new ClaudeSessionParser();
85
66
  }
86
67
 
87
- /**
88
- * Check if this adapter can handle a given process
89
- */
90
68
  canHandle(processInfo: ProcessInfo): boolean {
91
69
  return this.isClaudeExecutable(processInfo.command);
92
70
  }
@@ -97,9 +75,6 @@ export class ClaudeCodeAdapter implements AgentAdapter {
97
75
  return base === 'claude' || base === 'claude.exe';
98
76
  }
99
77
 
100
- /**
101
- * Detect running Claude Code agents
102
- */
103
78
  async detectAgents(): Promise<AgentInfo[]> {
104
79
  const processes = enrichProcesses(listAgentProcesses('claude'));
105
80
  if (processes.length === 0) {
@@ -125,7 +100,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
125
100
 
126
101
  // Build agents from direct (PID-file) matches
127
102
  for (const { process: proc, sessionFile } of direct) {
128
- const sessionData = this.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
103
+ const sessionData = this.parser.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
129
104
  if (sessionData) {
130
105
  agents.push(this.mapSessionToAgent(sessionData, proc, sessionFile));
131
106
  } else {
@@ -135,7 +110,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
135
110
 
136
111
  // Build agents from legacy matches
137
112
  for (const match of legacyMatches) {
138
- const sessionData = this.readSession(
113
+ const sessionData = this.parser.readSession(
139
114
  match.session.filePath,
140
115
  match.session.resolvedCwd,
141
116
  );
@@ -164,7 +139,6 @@ export class ClaudeCodeAdapter implements AgentAdapter {
164
139
  * via a single batched stat call across all directories.
165
140
  */
166
141
  private discoverSessions(processes: ProcessInfo[]): SessionFile[] {
167
- // Collect valid project dirs and map them back to their CWD
168
142
  const dirToCwd = new Map<string, string>();
169
143
 
170
144
  for (const proc of processes) {
@@ -184,10 +158,8 @@ export class ClaudeCodeAdapter implements AgentAdapter {
184
158
 
185
159
  if (dirToCwd.size === 0) return [];
186
160
 
187
- // Single batched stat call across all directories
188
161
  const files = batchGetSessionFileBirthtimes([...dirToCwd.keys()]);
189
162
 
190
- // Set resolvedCwd based on which project dir the file belongs to
191
163
  for (const file of files) {
192
164
  file.resolvedCwd = dirToCwd.get(file.projectDir) || '';
193
165
  }
@@ -203,7 +175,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
203
175
  * fallback — processes with no valid PID file (sent to legacy matching)
204
176
  *
205
177
  * Per-process fallback triggers on: file absent, malformed JSON,
206
- * stale startedAt (>60 s from proc.startTime), or missing JSONL.
178
+ * stale startedAt (>60s from proc.startTime), or missing JSONL.
207
179
  */
208
180
  private tryPidFileMatching(processes: ProcessInfo[]): {
209
181
  direct: DirectMatch[];
@@ -222,7 +194,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
222
194
  // Stale-file guard: reject PID files from a previous process with the same PID
223
195
  if (proc.startTime) {
224
196
  const deltaMs = Math.abs(proc.startTime.getTime() - entry.startedAt);
225
- if (deltaMs > 60000) {
197
+ if (deltaMs > PID_FILE_STALENESS_MS) {
226
198
  fallback.push(proc);
227
199
  continue;
228
200
  }
@@ -274,7 +246,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
274
246
  return {
275
247
  name: generateAgentName(processInfo.cwd, processInfo.pid),
276
248
  type: this.type,
277
- status: this.determineStatus(session),
249
+ status: this.parser.determineStatus(session),
278
250
  summary: session.lastUserMessage || 'Session started',
279
251
  pid: processInfo.pid,
280
252
  projectPath: sessionFile.resolvedCwd || processInfo.cwd || '',
@@ -297,320 +269,74 @@ export class ClaudeCodeAdapter implements AgentAdapter {
297
269
  };
298
270
  }
299
271
 
300
- /**
301
- * Parse a single session file into ClaudeSession
302
- */
303
- private readSession(
304
- filePath: string,
305
- projectPath: string,
306
- ): ClaudeSession | null {
307
- const sessionId = path.basename(filePath, '.jsonl');
308
-
309
- let content: string;
310
- try {
311
- content = fs.readFileSync(filePath, 'utf-8');
312
- } catch {
313
- return null;
314
- }
315
-
316
- const allLines = content.trim().split('\n');
317
- if (allLines.length === 0) {
318
- return null;
319
- }
320
-
321
- // Parse first line for sessionStart.
322
- // Claude Code may emit a "file-history-snapshot" as the first entry, which
323
- // stores its timestamp inside "snapshot.timestamp" rather than at the root.
324
- let sessionStart: Date | null = null;
325
- try {
326
- const firstEntry = JSON.parse(allLines[0]);
327
- const rawTs: string | undefined =
328
- firstEntry.timestamp || firstEntry.snapshot?.timestamp;
329
- if (rawTs) {
330
- const ts = new Date(rawTs);
331
- if (!Number.isNaN(ts.getTime())) {
332
- sessionStart = ts;
333
- }
334
- }
335
- } catch {
336
- /* skip */
337
- }
338
-
339
- // Parse all lines for session state (file already in memory)
340
- let lastEntryType: string | undefined;
341
- let lastActive: Date | undefined;
342
- let lastCwd: string | undefined;
343
- let isInterrupted = false;
344
- let lastUserMessage: string | undefined;
345
-
346
- for (const line of allLines) {
347
- try {
348
- const entry: SessionEntry = JSON.parse(line);
349
-
350
- if (entry.timestamp) {
351
- const ts = new Date(entry.timestamp);
352
- if (!Number.isNaN(ts.getTime())) {
353
- lastActive = ts;
354
- }
355
- }
356
-
357
- if (typeof entry.cwd === 'string' && entry.cwd.trim().length > 0) {
358
- lastCwd = entry.cwd;
359
- }
360
-
361
- if (entry.type && !this.isMetadataEntryType(entry.type)) {
362
- lastEntryType = entry.type;
363
-
364
- if (entry.type === 'user') {
365
- const msgContent = entry.message?.content;
366
- isInterrupted =
367
- Array.isArray(msgContent) &&
368
- msgContent.some(
369
- (c) =>
370
- (c.type === 'text' &&
371
- c.text?.includes('[Request interrupted')) ||
372
- (c.type === 'tool_result' &&
373
- c.content?.includes('[Request interrupted')),
374
- );
375
-
376
- // Extract user message text for summary fallback
377
- const text = this.extractUserMessageText(msgContent);
378
- if (text) {
379
- lastUserMessage = text;
380
- }
381
- } else {
382
- isInterrupted = false;
383
- }
384
- }
385
- } catch {
386
- continue;
387
- }
388
- }
389
-
390
- return {
391
- sessionId,
392
- projectPath: projectPath || lastCwd || '',
393
- lastCwd,
394
- sessionStart: sessionStart || lastActive || new Date(),
395
- lastActive: lastActive || new Date(),
396
- lastEntryType,
397
- isInterrupted,
398
- lastUserMessage,
399
- };
400
- }
401
-
402
- /**
403
- * Determine agent status from session state
404
- */
405
- private determineStatus(session: ClaudeSession): AgentStatus {
406
- if (!session.lastEntryType) {
407
- return AgentStatus.UNKNOWN;
408
- }
409
-
410
- // No age-based IDLE override: every agent in the list is backed by
411
- // a running process (found via ps), so the entry type is the best
412
- // indicator of actual state.
413
-
414
- if (session.lastEntryType === 'user') {
415
- return session.isInterrupted
416
- ? AgentStatus.WAITING
417
- : AgentStatus.RUNNING;
418
- }
419
-
420
- if (
421
- session.lastEntryType === 'progress' ||
422
- session.lastEntryType === 'thinking'
423
- ) {
424
- return AgentStatus.RUNNING;
425
- }
426
-
427
- if (session.lastEntryType === 'assistant') {
428
- return AgentStatus.WAITING;
429
- }
430
-
431
- if (session.lastEntryType === 'system') {
432
- return AgentStatus.IDLE;
433
- }
434
-
435
- return AgentStatus.UNKNOWN;
436
- }
437
-
438
- /**
439
- * Extract meaningful text from a user message content.
440
- * Handles string and array formats, skill command expansion, and noise filtering.
441
- */
442
- private extractUserMessageText(
443
- content: string | Array<{ type?: string; text?: string }> | undefined,
444
- ): string | undefined {
445
- if (!content) {
446
- return undefined;
447
- }
448
-
449
- let raw: string | undefined;
450
-
451
- if (typeof content === 'string') {
452
- raw = content.trim();
453
- } else if (Array.isArray(content)) {
454
- for (const block of content) {
455
- if (block.type === 'text' && block.text?.trim()) {
456
- raw = block.text.trim();
457
- break;
458
- }
459
- }
460
- }
461
-
462
- if (!raw) {
463
- return undefined;
464
- }
465
-
466
- // Skill slash-command: extract /command-name and args
467
- if (raw.startsWith('<command-message>')) {
468
- return this.parseCommandMessage(raw);
469
- }
470
-
471
- // Expanded skill content: extract ARGUMENTS line if present, skip otherwise
472
- if (raw.startsWith('Base directory for this skill:')) {
473
- const argsMatch = raw.match(/\nARGUMENTS:\s*(.+)/);
474
- return argsMatch?.[1]?.trim() || undefined;
475
- }
476
-
477
- // Filter noise
478
- if (this.isNoiseMessage(raw)) {
479
- return undefined;
480
- }
481
-
482
- return raw;
483
- }
484
-
485
- /**
486
- * Parse a <command-message> string into "/command args" format.
487
- */
488
- private parseCommandMessage(raw: string): string | undefined {
489
- const nameMatch = raw.match(/<command-name>([^<]+)<\/command-name>/);
490
- const argsMatch = raw.match(/<command-args>([^<]+)<\/command-args>/);
491
- const name = nameMatch?.[1]?.trim();
492
- if (!name) {
493
- return undefined;
494
- }
495
- const args = argsMatch?.[1]?.trim();
496
- return args ? `${name} ${args}` : name;
497
- }
498
-
499
- /**
500
- * Check if a message is noise (not a meaningful user intent).
501
- */
502
- private isNoiseMessage(text: string): boolean {
503
- return (
504
- text.startsWith('[Request interrupted') ||
505
- text === 'Tool loaded.' ||
506
- text.startsWith('This session is being continued')
507
- );
508
- }
509
-
510
- /**
511
- * Check if an entry type is metadata (not conversation state).
512
- * These should not overwrite lastEntryType used for status determination.
513
- */
514
- private isMetadataEntryType(type: string): boolean {
515
- return type === 'last-prompt' || type === 'file-history-snapshot';
516
- }
517
-
518
- /**
519
- * Read the full conversation from a Claude Code session JSONL file.
520
- *
521
- * Default mode returns only text content from user/assistant/system messages.
522
- * Verbose mode also includes tool_use and tool_result blocks.
523
- */
524
272
  getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
525
- const verbose = options?.verbose ?? false;
526
-
527
- let content: string;
528
- try {
529
- content = fs.readFileSync(sessionFilePath, 'utf-8');
530
- } catch {
531
- return [];
532
- }
533
-
534
- const lines = content.trim().split('\n');
535
- const messages: ConversationMessage[] = [];
536
-
537
- for (const line of lines) {
538
- let entry: SessionEntry;
539
- try {
540
- entry = JSON.parse(line);
541
- } catch {
542
- continue;
543
- }
544
-
545
- const entryType = entry.type;
546
- if (!entryType || this.isMetadataEntryType(entryType)) continue;
547
- if (entryType === 'progress' || entryType === 'thinking') continue;
548
-
549
- let role: ConversationMessage['role'];
550
- if (entryType === 'user') {
551
- role = 'user';
552
- } else if (entryType === 'assistant') {
553
- role = 'assistant';
554
- } else if (entryType === 'system') {
555
- role = 'system';
556
- } else {
557
- continue;
558
- }
559
-
560
- const text = this.extractConversationContent(entry.message?.content, role, verbose);
561
- if (!text) continue;
273
+ return this.parser.getConversation(sessionFilePath, options);
274
+ }
562
275
 
563
- messages.push({
564
- role,
565
- content: text,
566
- timestamp: entry.timestamp,
276
+ async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
277
+ const filterCwd = opts?.cwd;
278
+ const candidates = this.discoverSessionFiles();
279
+ const summaries: SessionSummary[] = [];
280
+
281
+ for (const { filePath, defaultCwd } of candidates) {
282
+ const session = this.parser.readSession(filePath, defaultCwd);
283
+ if (!session) continue;
284
+
285
+ // Drop sessions whose JSONL had no parseable conversation entries.
286
+ // readSession is permissive (returns a shell record even when every
287
+ // line fails to parse); listSessions needs at least one real entry
288
+ // so we don't surface garbage files.
289
+ if (!session.lastEntryType) continue;
290
+
291
+ const recordedCwd = session.lastCwd || defaultCwd;
292
+ if (filterCwd !== undefined && recordedCwd !== filterCwd) continue;
293
+
294
+ const stat = safeStat(filePath);
295
+
296
+ summaries.push({
297
+ type: 'claude',
298
+ sessionId: session.sessionId,
299
+ cwd: recordedCwd,
300
+ firstUserMessage: session.firstUserMessage || '',
301
+ lastActive: session.lastActive ?? stat?.mtime ?? new Date(),
302
+ startedAt: session.sessionStart ?? stat?.birthtime ?? stat?.mtime ?? new Date(),
303
+ sessionFilePath: filePath,
567
304
  });
568
305
  }
569
306
 
570
- return messages;
307
+ return summaries;
571
308
  }
572
309
 
573
310
  /**
574
- * Extract displayable content from a message content field.
311
+ * Discover candidate session files for {@link listSessions}.
312
+ *
313
+ * Always walks every subdirectory of `projectsDir`. We can't use the
314
+ * encoded-dir shortcut for the cwd-scoped path because Claude Code
315
+ * indexes session files by where the *process was launched*, not by
316
+ * the recorded `cwd` field inside the session — these diverge in
317
+ * worktrees and similar setups. The cwd filter is applied later
318
+ * against `session.lastCwd` so callers see exactly the sessions whose
319
+ * recorded cwd matches.
575
320
  */
576
- private extractConversationContent(
577
- content: string | ContentBlock[] | undefined,
578
- role: ConversationMessage['role'],
579
- verbose: boolean,
580
- ): string | undefined {
581
- if (!content) return undefined;
582
-
583
- if (typeof content === 'string') {
584
- const trimmed = content.trim();
585
- if (role === 'user' && this.isNoiseMessage(trimmed)) return undefined;
586
- return trimmed || undefined;
587
- }
588
-
589
- if (!Array.isArray(content)) return undefined;
590
-
591
- const parts: string[] = [];
592
-
593
- for (const block of content) {
594
- if (block.type === 'text' && block.text?.trim()) {
595
- if (role === 'user' && this.isNoiseMessage(block.text.trim())) continue;
596
- parts.push(block.text.trim());
597
- } else if (block.type === 'tool_use' && verbose) {
598
- const inputSummary = block.input?.file_path || block.input?.pattern || block.input?.command || '';
599
- parts.push(`[Tool: ${block.name}]${inputSummary ? ' ' + inputSummary : ''}`);
600
- } else if (block.type === 'tool_result' && verbose) {
601
- const truncated = this.truncateToolResult(block.content || '');
602
- const prefix = block.is_error ? '[Tool Error]' : '[Tool Result]';
603
- parts.push(`${prefix} ${truncated}`);
321
+ private discoverSessionFiles(): Array<{ filePath: string; defaultCwd: string }> {
322
+ const out: Array<{ filePath: string; defaultCwd: string }> = [];
323
+
324
+ if (!isDirectory(this.projectsDir)) return out;
325
+
326
+ for (const dirName of safeReaddir(this.projectsDir)) {
327
+ const projectDir = path.join(this.projectsDir, dirName);
328
+ if (!isDirectory(projectDir)) continue;
329
+
330
+ // Best-effort decode for the rare case session content has no
331
+ // recorded cwd: '-Users-foo-bar' → '/Users/foo/bar'. Lossy for
332
+ // paths containing '-'; session content's lastCwd overrides
333
+ // this when available.
334
+ const decoded = dirName.replace(/-/g, '/');
335
+ for (const name of listJsonl(projectDir)) {
336
+ out.push({ filePath: path.join(projectDir, name), defaultCwd: decoded });
604
337
  }
605
338
  }
606
339
 
607
- return parts.length > 0 ? parts.join('\n') : undefined;
340
+ return out;
608
341
  }
609
-
610
- private truncateToolResult(content: string, maxLength = 200): string {
611
- const firstLine = content.split('\n')[0] || '';
612
- if (firstLine.length <= maxLength) return firstLine;
613
- return firstLine.slice(0, maxLength - 3) + '...';
614
- }
615
-
616
342
  }