@ai-devkit/agent-manager 0.28.1 → 0.30.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 (219) hide show
  1. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +1 -342
  2. package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
  3. package/dist/__tests__/capacity/codex.test.js +343 -0
  4. package/dist/__tests__/capacity/codex.test.js.map +1 -0
  5. package/dist/__tests__/capacity/index.test.js +50 -0
  6. package/dist/__tests__/capacity/index.test.js.map +1 -0
  7. package/dist/__tests__/database/DurableAgentsDatabase.test.js +31 -3
  8. package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -1
  9. package/dist/__tests__/durable/CodexCliProbe.test.js +129 -0
  10. package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -0
  11. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +84 -0
  12. package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -0
  13. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +126 -0
  14. package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -0
  15. package/dist/__tests__/durable/CodexPrintAgentService.test.js +271 -0
  16. package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -0
  17. package/dist/__tests__/durable/CodexPrintRunner.test.js +393 -0
  18. package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -0
  19. package/dist/__tests__/print/ClaudeCliProbe.test.js +3 -7
  20. package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
  21. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +33 -27
  22. package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
  23. package/dist/__tests__/print/ClaudePrintAgentService.test.js +65 -0
  24. package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
  25. package/dist/__tests__/print/ClaudePrintRunner.test.js +4 -10
  26. package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
  27. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +79 -0
  28. package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -0
  29. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +436 -0
  30. package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -0
  31. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +76 -0
  32. package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -0
  33. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +43 -0
  34. package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -0
  35. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +202 -0
  36. package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -0
  37. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +119 -0
  38. package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -0
  39. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +386 -0
  40. package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -0
  41. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +34 -0
  42. package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -0
  43. package/dist/__tests__/utils/ClaudeSessionParser.test.js +2 -2
  44. package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -1
  45. package/dist/adapters/index.d.ts +1 -1
  46. package/dist/adapters/index.d.ts.map +1 -1
  47. package/dist/adapters/index.js +1 -1
  48. package/dist/adapters/index.js.map +1 -1
  49. package/dist/capacity/codex.d.ts +36 -0
  50. package/dist/capacity/codex.d.ts.map +1 -0
  51. package/dist/capacity/codex.js +317 -0
  52. package/dist/capacity/codex.js.map +1 -0
  53. package/dist/capacity/index.d.ts +11 -0
  54. package/dist/capacity/index.d.ts.map +1 -0
  55. package/dist/capacity/index.js +49 -0
  56. package/dist/capacity/index.js.map +1 -0
  57. package/dist/capacity/types.d.ts +17 -0
  58. package/dist/capacity/types.d.ts.map +1 -0
  59. package/dist/capacity/types.js +3 -0
  60. package/dist/capacity/types.js.map +1 -0
  61. package/dist/database/migrations/004_codex_durable_sessions.sql +1 -0
  62. package/dist/durable/DurableAgent.d.ts +23 -3
  63. package/dist/durable/DurableAgent.d.ts.map +1 -1
  64. package/dist/durable/DurableAgent.js +12 -0
  65. package/dist/durable/DurableAgent.js.map +1 -1
  66. package/dist/durable/DurableAgentRepository.d.ts +6 -7
  67. package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
  68. package/dist/durable/DurableAgentRepository.js +73 -42
  69. package/dist/durable/DurableAgentRepository.js.map +1 -1
  70. package/dist/durable/process.d.ts +8 -0
  71. package/dist/durable/process.d.ts.map +1 -0
  72. package/dist/durable/process.js +40 -0
  73. package/dist/durable/process.js.map +1 -0
  74. package/dist/durable/run.d.ts +32 -0
  75. package/dist/durable/run.d.ts.map +1 -0
  76. package/dist/durable/run.js +46 -0
  77. package/dist/durable/run.js.map +1 -0
  78. package/dist/durable/utils.d.ts +19 -0
  79. package/dist/durable/utils.d.ts.map +1 -0
  80. package/dist/durable/utils.js +52 -0
  81. package/dist/durable/utils.js.map +1 -0
  82. package/dist/index.d.ts +19 -13
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +10 -8
  85. package/dist/index.js.map +1 -1
  86. package/dist/providers/claude/ClaudeAgentMapper.d.ts +21 -0
  87. package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -0
  88. package/dist/providers/claude/ClaudeAgentMapper.js +42 -0
  89. package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -0
  90. package/dist/providers/claude/ClaudeCodeAdapter.d.ts +29 -0
  91. package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -0
  92. package/dist/providers/claude/ClaudeCodeAdapter.js +129 -0
  93. package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -0
  94. package/dist/{adapters/ClaudeCodeAdapter.d.ts → providers/claude/ClaudeSessionLocator.d.ts} +68 -52
  95. package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -0
  96. package/dist/{adapters/ClaudeCodeAdapter.js → providers/claude/ClaudeSessionLocator.js} +71 -189
  97. package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -0
  98. package/dist/{utils → providers/claude}/ClaudeSessionParser.d.ts +2 -2
  99. package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -0
  100. package/dist/{utils → providers/claude}/ClaudeSessionParser.js +1 -1
  101. package/dist/providers/claude/ClaudeSessionParser.js.map +1 -0
  102. package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -0
  103. package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.js +4 -9
  104. package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -0
  105. package/dist/{durable → providers/claude/durable}/ClaudePrintAgentService.d.ts +4 -9
  106. package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -0
  107. package/dist/providers/claude/durable/ClaudePrintAgentService.js +45 -0
  108. package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -0
  109. package/dist/{durable → providers/claude/durable}/ClaudePrintRunner.d.ts +3 -3
  110. package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -0
  111. package/dist/providers/claude/durable/ClaudePrintRunner.js +75 -0
  112. package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -0
  113. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +17 -0
  114. package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -0
  115. package/dist/providers/claude/durable/ClaudeStreamParser.js +69 -0
  116. package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -0
  117. package/dist/providers/codex/durable/CodexCliProbe.d.ts +20 -0
  118. package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -0
  119. package/dist/providers/codex/durable/CodexCliProbe.js +61 -0
  120. package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -0
  121. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +40 -0
  122. package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -0
  123. package/dist/providers/codex/durable/CodexPrintAgentService.js +50 -0
  124. package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -0
  125. package/dist/providers/codex/durable/CodexPrintRunner.d.ts +34 -0
  126. package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -0
  127. package/dist/providers/codex/durable/CodexPrintRunner.js +78 -0
  128. package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -0
  129. package/dist/providers/codex/durable/CodexStreamParser.d.ts +21 -0
  130. package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -0
  131. package/dist/providers/codex/durable/CodexStreamParser.js +86 -0
  132. package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -0
  133. package/dist/providers/pi/durable/PiCliProbe.d.ts +20 -0
  134. package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -0
  135. package/dist/providers/pi/durable/PiCliProbe.js +51 -0
  136. package/dist/providers/pi/durable/PiCliProbe.js.map +1 -0
  137. package/dist/providers/pi/durable/PiPrintAgentService.d.ts +39 -0
  138. package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -0
  139. package/dist/providers/pi/durable/PiPrintAgentService.js +47 -0
  140. package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -0
  141. package/dist/providers/pi/durable/PiPrintProtocol.d.ts +4 -0
  142. package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -0
  143. package/dist/providers/pi/durable/PiPrintProtocol.js +44 -0
  144. package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -0
  145. package/dist/providers/pi/durable/PiPrintRunner.d.ts +32 -0
  146. package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -0
  147. package/dist/providers/pi/durable/PiPrintRunner.js +61 -0
  148. package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -0
  149. package/dist/providers/pi/durable/PiStreamParser.d.ts +21 -0
  150. package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -0
  151. package/dist/providers/pi/durable/PiStreamParser.js +71 -0
  152. package/dist/providers/pi/durable/PiStreamParser.js.map +1 -0
  153. package/package.json +1 -1
  154. package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1 -307
  155. package/src/__tests__/capacity/codex.test.ts +184 -0
  156. package/src/__tests__/capacity/index.test.ts +38 -0
  157. package/src/__tests__/database/DurableAgentsDatabase.test.ts +28 -3
  158. package/src/__tests__/durable/CodexCliProbe.test.ts +63 -0
  159. package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +54 -0
  160. package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +79 -0
  161. package/src/__tests__/durable/CodexPrintAgentService.test.ts +139 -0
  162. package/src/__tests__/durable/CodexPrintRunner.test.ts +189 -0
  163. package/src/__tests__/fixtures/fake-codex.cjs +45 -0
  164. package/src/__tests__/print/ClaudeCliProbe.test.ts +3 -7
  165. package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +19 -22
  166. package/src/__tests__/print/ClaudePrintAgentService.test.ts +33 -1
  167. package/src/__tests__/print/ClaudePrintRunner.test.ts +4 -10
  168. package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +84 -0
  169. package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +411 -0
  170. package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +38 -0
  171. package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +32 -0
  172. package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +69 -0
  173. package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +33 -0
  174. package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +107 -0
  175. package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +24 -0
  176. package/src/__tests__/utils/ClaudeSessionParser.test.ts +2 -2
  177. package/src/adapters/index.ts +1 -1
  178. package/src/capacity/codex.ts +320 -0
  179. package/src/capacity/index.ts +57 -0
  180. package/src/capacity/types.ts +18 -0
  181. package/src/database/migrations/004_codex_durable_sessions.sql +1 -0
  182. package/src/durable/DurableAgent.ts +51 -3
  183. package/src/durable/DurableAgentRepository.ts +60 -31
  184. package/src/durable/process.ts +28 -0
  185. package/src/durable/run.ts +74 -0
  186. package/src/durable/utils.ts +61 -0
  187. package/src/index.ts +37 -15
  188. package/src/providers/claude/ClaudeAgentMapper.ts +62 -0
  189. package/src/providers/claude/ClaudeCodeAdapter.ts +160 -0
  190. package/src/{adapters/ClaudeCodeAdapter.ts → providers/claude/ClaudeSessionLocator.ts} +97 -239
  191. package/src/{utils → providers/claude}/ClaudeSessionParser.ts +2 -2
  192. package/src/{durable → providers/claude/durable}/ClaudeCliProbe.ts +4 -10
  193. package/src/providers/claude/durable/ClaudePrintAgentService.ts +66 -0
  194. package/src/providers/claude/durable/ClaudePrintRunner.ts +92 -0
  195. package/src/providers/claude/durable/ClaudeStreamParser.ts +79 -0
  196. package/src/providers/codex/durable/CodexCliProbe.ts +60 -0
  197. package/src/providers/codex/durable/CodexPrintAgentService.ts +65 -0
  198. package/src/providers/codex/durable/CodexPrintRunner.ts +94 -0
  199. package/src/providers/codex/durable/CodexStreamParser.ts +104 -0
  200. package/src/providers/pi/durable/PiCliProbe.ts +48 -0
  201. package/src/providers/pi/durable/PiPrintAgentService.ts +69 -0
  202. package/src/providers/pi/durable/PiPrintProtocol.ts +39 -0
  203. package/src/providers/pi/durable/PiPrintRunner.ts +88 -0
  204. package/src/providers/pi/durable/PiStreamParser.ts +84 -0
  205. package/dist/adapters/ClaudeCodeAdapter.d.ts.map +0 -1
  206. package/dist/adapters/ClaudeCodeAdapter.js.map +0 -1
  207. package/dist/durable/ClaudeCliProbe.d.ts.map +0 -1
  208. package/dist/durable/ClaudeCliProbe.js.map +0 -1
  209. package/dist/durable/ClaudePrintAgentService.d.ts.map +0 -1
  210. package/dist/durable/ClaudePrintAgentService.js +0 -66
  211. package/dist/durable/ClaudePrintAgentService.js.map +0 -1
  212. package/dist/durable/ClaudePrintRunner.d.ts.map +0 -1
  213. package/dist/durable/ClaudePrintRunner.js +0 -128
  214. package/dist/durable/ClaudePrintRunner.js.map +0 -1
  215. package/dist/utils/ClaudeSessionParser.d.ts.map +0 -1
  216. package/dist/utils/ClaudeSessionParser.js.map +0 -1
  217. package/src/durable/ClaudePrintAgentService.ts +0 -94
  218. package/src/durable/ClaudePrintRunner.ts +0 -139
  219. /package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.d.ts +0 -0
@@ -0,0 +1,160 @@
1
+ import * as path from 'path';
2
+ import type {
3
+ AgentAdapter,
4
+ AgentInfo,
5
+ ProcessInfo,
6
+ ConversationMessage,
7
+ SessionSummary,
8
+ ListSessionsOptions,
9
+ AgentDetectionContext,
10
+ } from '../../adapters/AgentAdapter.js';
11
+ import { captureProcessSnapshot, executableBasename, filterByProcessNames } from '../../utils/process.js';
12
+ import { safeStat } from '../../utils/session.js';
13
+ import { ClaudeSessionParser } from './ClaudeSessionParser.js';
14
+ import { ClaudeAgentMapper } from './ClaudeAgentMapper.js';
15
+ import { ClaudeSessionLocator } from './ClaudeSessionLocator.js';
16
+
17
+ /**
18
+ * Claude Code Adapter
19
+ *
20
+ * Detects Claude Code agents by:
21
+ * 1. Filtering Claude processes from a shared asynchronous process snapshot
22
+ * 2. Using snapshot CWD and start-time enrichment
23
+ * 3. Attempting authoritative PID-file matching via ~/.claude/sessions/<pid>.json
24
+ * 4. Falling back to CWD+birthtime heuristic (matchProcessesToSessions) for processes without a PID file
25
+ * 5. Extracting summary from last user message in session JSONL
26
+ */
27
+ export class ClaudeCodeAdapter implements AgentAdapter {
28
+ readonly type = 'claude' as const;
29
+ readonly processNames = ['claude'] as const;
30
+
31
+ private parser: ClaudeSessionParser;
32
+ private mapper: ClaudeAgentMapper;
33
+ private projectsDir: string;
34
+ private sessionsDir: string;
35
+
36
+ constructor() {
37
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
38
+ this.projectsDir = path.join(homeDir, '.claude', 'projects');
39
+ this.sessionsDir = path.join(homeDir, '.claude', 'sessions');
40
+ this.parser = new ClaudeSessionParser();
41
+ this.mapper = new ClaudeAgentMapper(this.parser);
42
+ }
43
+
44
+ canHandle(processInfo: ProcessInfo): boolean {
45
+ return this.isClaudeExecutable(processInfo.command);
46
+ }
47
+
48
+ private isClaudeExecutable(command: string): boolean {
49
+ const base = executableBasename(command);
50
+ return base === 'claude' || base === 'claude.exe';
51
+ }
52
+
53
+ async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
54
+ const snapshot = context?.processes ?? await captureProcessSnapshot(this.processNames);
55
+ const relevant = filterByProcessNames(snapshot, this.processNames);
56
+ const processes = relevant.filter((process) => this.canHandle(process));
57
+ if (processes.length === 0) {
58
+ return [];
59
+ }
60
+
61
+ const { direct, legacyMatches } = this.createLocator().matchRunningProcesses(processes);
62
+
63
+ const matchedPids = new Set([
64
+ ...direct.map((d) => d.process.pid),
65
+ ...legacyMatches.map((m) => m.process.pid),
66
+ ]);
67
+
68
+ const agents: AgentInfo[] = [];
69
+
70
+ // Build agents from direct (resume + PID-file) matches
71
+ for (const match of direct) {
72
+ const { process: proc, sessionFile } = match;
73
+ const sessionData = this.parser.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
74
+ if (sessionData) {
75
+ agents.push(this.mapper.mapSessionToAgent({
76
+ session: sessionData,
77
+ processInfo: proc,
78
+ sessionFile,
79
+ liveInfo: {
80
+ pidStatus: match.pidStatus,
81
+ waitingFor: match.waitingFor,
82
+ },
83
+ }));
84
+ } else {
85
+ matchedPids.delete(proc.pid);
86
+ }
87
+ }
88
+
89
+ // Build agents from legacy matches
90
+ for (const match of legacyMatches) {
91
+ const sessionData = this.parser.readSession(
92
+ match.session.filePath,
93
+ match.session.resolvedCwd,
94
+ );
95
+ if (sessionData) {
96
+ agents.push(this.mapper.mapSessionToAgent({
97
+ session: sessionData,
98
+ processInfo: match.process,
99
+ sessionFile: match.session,
100
+ }));
101
+ } else {
102
+ matchedPids.delete(match.process.pid);
103
+ }
104
+ }
105
+
106
+ // Any process with no match (direct or legacy) appears as IDLE
107
+ for (const proc of processes) {
108
+ if (!matchedPids.has(proc.pid)) {
109
+ agents.push(this.mapper.mapProcessOnlyAgent(proc));
110
+ }
111
+ }
112
+
113
+ return agents;
114
+ }
115
+
116
+ private createLocator(): ClaudeSessionLocator {
117
+ return new ClaudeSessionLocator({
118
+ projectsDir: this.projectsDir,
119
+ sessionsDir: this.sessionsDir,
120
+ });
121
+ }
122
+
123
+ getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
124
+ return this.parser.getConversation(sessionFilePath, options);
125
+ }
126
+
127
+ async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
128
+ const filterCwd = opts?.cwd;
129
+ const candidates = this.createLocator().discoverHistoricalSessionFiles();
130
+ const summaries: SessionSummary[] = [];
131
+
132
+ for (const { filePath, defaultCwd } of candidates) {
133
+ const session = this.parser.readSession(filePath, defaultCwd);
134
+ if (!session) continue;
135
+
136
+ // Drop sessions whose JSONL had no parseable conversation entries.
137
+ // readSession is permissive (returns a shell record even when every
138
+ // line fails to parse); listSessions needs at least one real entry
139
+ // so we don't surface garbage files.
140
+ if (!session.lastEntryType) continue;
141
+
142
+ const recordedCwd = session.lastCwd || defaultCwd;
143
+ if (filterCwd !== undefined && recordedCwd !== filterCwd) continue;
144
+
145
+ const stat = safeStat(filePath);
146
+
147
+ summaries.push({
148
+ type: 'claude',
149
+ sessionId: session.sessionId,
150
+ cwd: recordedCwd,
151
+ firstUserMessage: session.firstUserMessage || '',
152
+ lastActive: session.lastActive ?? stat?.mtime ?? new Date(),
153
+ startedAt: session.sessionStart ?? stat?.birthtime ?? stat?.mtime ?? new Date(),
154
+ sessionFilePath: filePath,
155
+ });
156
+ }
157
+
158
+ return summaries;
159
+ }
160
+ }
@@ -1,21 +1,16 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import type {
4
- AgentAdapter,
5
- AgentInfo,
6
- ProcessInfo,
7
- ConversationMessage,
8
- SessionSummary,
9
- ListSessionsOptions,
10
- AgentDetectionContext,
11
- } from './AgentAdapter.js';
12
- import { AgentStatus } from './AgentAdapter.js';
13
- import { captureProcessSnapshot, executableBasename, filterByProcessNames } from '../utils/process.js';
14
- import { batchGetSessionFileBirthtimes, isDirectory, listJsonl, safeReaddir, safeStat } from '../utils/session.js';
15
- import type { SessionFile } from '../utils/session.js';
16
- import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
17
- import { ClaudeSessionParser } from '../utils/ClaudeSessionParser.js';
18
- import type { ClaudeSession } from '../utils/ClaudeSessionParser.js';
3
+ import type { ProcessInfo } from '../../adapters/AgentAdapter.js';
4
+ import { AgentStatus } from '../../adapters/AgentAdapter.js';
5
+ import { matchProcessesToSessions, type MatchResult } from '../../utils/matching.js';
6
+ import {
7
+ batchGetSessionFileBirthtimes,
8
+ isDirectory,
9
+ listJsonl,
10
+ safeReaddir,
11
+ safeStat,
12
+ type SessionFile,
13
+ } from '../../utils/session.js';
19
14
 
20
15
  /**
21
16
  * Entry in ~/.claude/sessions/<pid>.json written by Claude Code.
@@ -44,120 +39,113 @@ interface PidFileEntry {
44
39
  * A process directly matched to a session via PID file (authoritative path).
45
40
  *
46
41
  * When the matching PID file also exposes live status/waitingFor metadata,
47
- * those values are carried here so `mapSessionToAgent` can prefer them
48
- * over the JSONL-derived heuristic.
42
+ * those values are carried here so the agent mapper can prefer them over
43
+ * the JSONL-derived heuristic.
49
44
  */
50
- interface DirectMatch {
45
+ export interface ClaudeDirectMatch {
51
46
  process: ProcessInfo;
52
47
  sessionFile: SessionFile;
53
48
  pidStatus?: AgentStatus;
54
49
  waitingFor?: string;
55
50
  }
56
51
 
52
+ export interface ClaudeProcessSessionMatches {
53
+ direct: ClaudeDirectMatch[];
54
+ legacyMatches: MatchResult[];
55
+ }
56
+
57
+ export interface ClaudeSessionLocatorOptions {
58
+ projectsDir?: string;
59
+ sessionsDir?: string;
60
+ }
61
+
57
62
  /** Maximum allowed delta (ms) between process start time and PID file startedAt. */
58
63
  const PID_FILE_STALENESS_MS = 60000;
59
64
 
60
- /**
61
- * Claude Code Adapter
62
- *
63
- * Detects Claude Code agents by:
64
- * 1. Filtering Claude processes from a shared asynchronous process snapshot
65
- * 2. Using snapshot CWD and start-time enrichment
66
- * 3. Attempting authoritative PID-file matching via ~/.claude/sessions/<pid>.json
67
- * 4. Falling back to CWD+birthtime heuristic (matchProcessesToSessions) for processes without a PID file
68
- * 5. Extracting summary from last user message in session JSONL
69
- */
70
- export class ClaudeCodeAdapter implements AgentAdapter {
71
- readonly type = 'claude' as const;
72
- readonly processNames = ['claude'] as const;
65
+ export class ClaudeSessionLocator {
66
+ private readonly projectsDir: string;
67
+ private readonly sessionsDir: string;
73
68
 
74
- private projectsDir: string;
75
- private sessionsDir: string;
76
- private parser: ClaudeSessionParser;
77
-
78
- constructor() {
69
+ constructor(options: ClaudeSessionLocatorOptions = {}) {
79
70
  const homeDir = process.env.HOME || process.env.USERPROFILE || '';
80
- this.projectsDir = path.join(homeDir, '.claude', 'projects');
81
- this.sessionsDir = path.join(homeDir, '.claude', 'sessions');
82
- this.parser = new ClaudeSessionParser();
71
+ this.projectsDir = options.projectsDir ?? path.join(homeDir, '.claude', 'projects');
72
+ this.sessionsDir = options.sessionsDir ?? path.join(homeDir, '.claude', 'sessions');
83
73
  }
84
74
 
85
- canHandle(processInfo: ProcessInfo): boolean {
86
- return this.isClaudeExecutable(processInfo.command);
87
- }
88
-
89
- private isClaudeExecutable(command: string): boolean {
90
- const base = executableBasename(command);
91
- return base === 'claude' || base === 'claude.exe';
92
- }
93
-
94
- async detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]> {
95
- const snapshot = context?.processes ?? await captureProcessSnapshot(this.processNames);
96
- const relevant = filterByProcessNames(snapshot, this.processNames);
97
- const processes = relevant.filter((process) => this.canHandle(process));
98
- if (processes.length === 0) {
99
- return [];
100
- }
101
-
102
- // Step 1: extract `--resume <id>` from command line — authoritative for
103
- // resumed sessions where the JSONL predates the process and PID-file/
104
- // birthtime heuristics can't match it.
75
+ /**
76
+ * Pair live Claude processes with their session files.
77
+ *
78
+ * Runs three staged strategies, each handing its unmatched processes
79
+ * to the next:
80
+ * 1. `--resume <id>` on the command line — authoritative for resumed
81
+ * sessions whose JSONL predates the process.
82
+ * 2. PID-file matching via ~/.claude/sessions/<pid>.json.
83
+ * 3. Legacy CWD+birthtime heuristic, for processes with no PID file.
84
+ */
85
+ matchRunningProcesses(processes: ProcessInfo[]): ClaudeProcessSessionMatches {
105
86
  const { direct: resumeDirect, fallback: noResume } = this.tryResumeMatching(processes);
106
-
107
- // Step 2: try authoritative PID-file matching for the rest
108
87
  const { direct: pidDirect, fallback } = this.tryPidFileMatching(noResume);
109
-
110
- const direct = [...resumeDirect, ...pidDirect];
111
-
112
- // Step 3: run legacy CWD+birthtime matching only for processes without a PID file
113
- const legacySessions = this.discoverSessions(fallback);
88
+ const legacySessions = this.discoverLiveSessions(fallback);
114
89
  const legacyMatches =
115
90
  fallback.length > 0 && legacySessions.length > 0
116
91
  ? matchProcessesToSessions(fallback, legacySessions)
117
92
  : [];
118
93
 
119
- const matchedPids = new Set([
120
- ...direct.map((d) => d.process.pid),
121
- ...legacyMatches.map((m) => m.process.pid),
122
- ]);
123
-
124
- const agents: AgentInfo[] = [];
125
-
126
- // Build agents from direct (resume + PID-file) matches
127
- for (const match of direct) {
128
- const { process: proc, sessionFile } = match;
129
- const sessionData = this.parser.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
130
- if (sessionData) {
131
- agents.push(this.mapSessionToAgent(sessionData, proc, sessionFile, {
132
- pidStatus: match.pidStatus,
133
- waitingFor: match.waitingFor,
134
- }));
135
- } else {
136
- matchedPids.delete(proc.pid);
137
- }
138
- }
94
+ return {
95
+ direct: [...resumeDirect, ...pidDirect],
96
+ legacyMatches,
97
+ };
98
+ }
139
99
 
140
- // Build agents from legacy matches
141
- for (const match of legacyMatches) {
142
- const sessionData = this.parser.readSession(
143
- match.session.filePath,
144
- match.session.resolvedCwd,
145
- );
146
- if (sessionData) {
147
- agents.push(this.mapSessionToAgent(sessionData, match.process, match.session));
148
- } else {
149
- matchedPids.delete(match.process.pid);
150
- }
151
- }
100
+ /**
101
+ * Discover candidate session files for listing historical sessions.
102
+ *
103
+ * Always walks every subdirectory of `projectsDir`. We can't use the
104
+ * encoded-dir shortcut for the cwd-scoped path because Claude Code
105
+ * indexes session files by where the *process was launched*, not by
106
+ * the recorded `cwd` field inside the session — these diverge in
107
+ * worktrees and similar setups. The cwd filter is applied later
108
+ * against `session.lastCwd` so callers see exactly the sessions whose
109
+ * recorded cwd matches.
110
+ */
111
+ discoverHistoricalSessionFiles(): Array<{ filePath: string; defaultCwd: string }> {
112
+ const out: Array<{ filePath: string; defaultCwd: string }> = [];
152
113
 
153
- // Any process with no match (direct or legacy) appears as IDLE
154
- for (const proc of processes) {
155
- if (!matchedPids.has(proc.pid)) {
156
- agents.push(this.mapProcessOnlyAgent(proc));
114
+ if (!isDirectory(this.projectsDir)) return out;
115
+
116
+ for (const dirName of safeReaddir(this.projectsDir)) {
117
+ const projectDir = path.join(this.projectsDir, dirName);
118
+ if (!isDirectory(projectDir)) continue;
119
+
120
+ // Best-effort decode for the rare case session content has no
121
+ // recorded cwd: '-Users-foo-bar' → '/Users/foo/bar'. Lossy for
122
+ // paths containing '-'; session content's lastCwd overrides
123
+ // this when available.
124
+ const decoded = dirName.replace(/-/g, '/');
125
+ for (const name of listJsonl(projectDir)) {
126
+ out.push({ filePath: path.join(projectDir, name), defaultCwd: decoded });
157
127
  }
158
128
  }
159
129
 
160
- return agents;
130
+ return out;
131
+ }
132
+
133
+ /**
134
+ * Derive the Claude Code project directory for a given CWD.
135
+ *
136
+ * Claude Code encodes paths by replacing every non-alphanumeric
137
+ * character with '-', so '/', '_', '.', spaces, etc. all collapse:
138
+ * /Users/foo/bar → -Users-foo-bar
139
+ * /Users/foo/my_project → -Users-foo-my-project
140
+ * /Users/foo/.worktrees/x → -Users-foo--worktrees-x
141
+ *
142
+ * The encoding is lossy — multiple real paths can collide on the
143
+ * same encoded dir. Callers that need to disambiguate must read the
144
+ * `cwd` field inside each session JSONL.
145
+ */
146
+ private getProjectDir(cwd: string): string {
147
+ const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
148
+ return path.join(this.projectsDir, encoded);
161
149
  }
162
150
 
163
151
  /**
@@ -167,7 +155,7 @@ export class ClaudeCodeAdapter implements AgentAdapter {
167
155
  * ~/.claude/projects/<encoded>/ directory, then gets session file birthtimes
168
156
  * via a single batched stat call across all directories.
169
157
  */
170
- private discoverSessions(processes: ProcessInfo[]): SessionFile[] {
158
+ discoverLiveSessions(processes: ProcessInfo[]): SessionFile[] {
171
159
  const dirToCwd = new Map<string, string>();
172
160
 
173
161
  for (const proc of processes) {
@@ -203,10 +191,10 @@ export class ClaudeCodeAdapter implements AgentAdapter {
203
191
  * matcher can't pair them) and the PID file may also be misaligned.
204
192
  */
205
193
  private tryResumeMatching(processes: ProcessInfo[]): {
206
- direct: DirectMatch[];
194
+ direct: ClaudeDirectMatch[];
207
195
  fallback: ProcessInfo[];
208
196
  } {
209
- const direct: DirectMatch[] = [];
197
+ const direct: ClaudeDirectMatch[] = [];
210
198
  const fallback: ProcessInfo[] = [];
211
199
 
212
200
  for (const proc of processes) {
@@ -312,10 +300,10 @@ export class ClaudeCodeAdapter implements AgentAdapter {
312
300
  * stale startedAt (>60s from proc.startTime), or missing JSONL.
313
301
  */
314
302
  private tryPidFileMatching(processes: ProcessInfo[]): {
315
- direct: DirectMatch[];
303
+ direct: ClaudeDirectMatch[];
316
304
  fallback: ProcessInfo[];
317
305
  } {
318
- const direct: DirectMatch[] = [];
306
+ const direct: ClaudeDirectMatch[] = [];
319
307
  const fallback: ProcessInfo[] = [];
320
308
 
321
309
  for (const proc of processes) {
@@ -349,134 +337,4 @@ export class ClaudeCodeAdapter implements AgentAdapter {
349
337
 
350
338
  return { direct, fallback };
351
339
  }
352
-
353
- /**
354
- * Derive the Claude Code project directory for a given CWD.
355
- *
356
- * Claude Code encodes paths by replacing every non-alphanumeric
357
- * character with '-', so '/', '_', '.', spaces, etc. all collapse:
358
- * /Users/foo/bar → -Users-foo-bar
359
- * /Users/foo/my_project → -Users-foo-my-project
360
- * /Users/foo/.worktrees/x → -Users-foo--worktrees-x
361
- *
362
- * The encoding is lossy — multiple real paths can collide on the
363
- * same encoded dir. Callers that need to disambiguate must read the
364
- * `cwd` field inside each session JSONL.
365
- */
366
- private getProjectDir(cwd: string): string {
367
- const encoded = cwd.replace(/[^a-zA-Z0-9]/g, '-');
368
- return path.join(this.projectsDir, encoded);
369
- }
370
-
371
- private mapSessionToAgent(
372
- session: ClaudeSession,
373
- processInfo: ProcessInfo,
374
- sessionFile: SessionFile,
375
- liveInfo?: { pidStatus?: AgentStatus; waitingFor?: string },
376
- ): AgentInfo {
377
- // Live PID-file status is authoritative when present — JSONL-derived
378
- // status mis-classifies sessions whose latest entry is a UI-state
379
- // event like `permission-mode` or `ai-title`.
380
- const status = liveInfo?.pidStatus ?? this.parser.determineStatus(session);
381
- const baseSummary = session.lastUserMessage || 'Session started';
382
- const summary = status === AgentStatus.WAITING && liveInfo?.waitingFor
383
- ? `${baseSummary} — waiting for ${liveInfo.waitingFor}`
384
- : baseSummary;
385
-
386
- return {
387
- name: generateAgentName(processInfo.cwd, processInfo.pid),
388
- type: this.type,
389
- status,
390
- summary,
391
- pid: processInfo.pid,
392
- projectPath: sessionFile.resolvedCwd || processInfo.cwd || '',
393
- sessionId: sessionFile.sessionId,
394
- lastActive: session.lastActive,
395
- sessionFilePath: sessionFile.filePath,
396
- };
397
- }
398
-
399
- private mapProcessOnlyAgent(processInfo: ProcessInfo): AgentInfo {
400
- return {
401
- name: generateAgentName(processInfo.cwd || '', processInfo.pid),
402
- type: this.type,
403
- status: AgentStatus.IDLE,
404
- summary: 'Unknown',
405
- pid: processInfo.pid,
406
- projectPath: processInfo.cwd || '',
407
- sessionId: `pid-${processInfo.pid}`,
408
- lastActive: new Date(),
409
- };
410
- }
411
-
412
- getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
413
- return this.parser.getConversation(sessionFilePath, options);
414
- }
415
-
416
- async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
417
- const filterCwd = opts?.cwd;
418
- const candidates = this.discoverSessionFiles();
419
- const summaries: SessionSummary[] = [];
420
-
421
- for (const { filePath, defaultCwd } of candidates) {
422
- const session = this.parser.readSession(filePath, defaultCwd);
423
- if (!session) continue;
424
-
425
- // Drop sessions whose JSONL had no parseable conversation entries.
426
- // readSession is permissive (returns a shell record even when every
427
- // line fails to parse); listSessions needs at least one real entry
428
- // so we don't surface garbage files.
429
- if (!session.lastEntryType) continue;
430
-
431
- const recordedCwd = session.lastCwd || defaultCwd;
432
- if (filterCwd !== undefined && recordedCwd !== filterCwd) continue;
433
-
434
- const stat = safeStat(filePath);
435
-
436
- summaries.push({
437
- type: 'claude',
438
- sessionId: session.sessionId,
439
- cwd: recordedCwd,
440
- firstUserMessage: session.firstUserMessage || '',
441
- lastActive: session.lastActive ?? stat?.mtime ?? new Date(),
442
- startedAt: session.sessionStart ?? stat?.birthtime ?? stat?.mtime ?? new Date(),
443
- sessionFilePath: filePath,
444
- });
445
- }
446
-
447
- return summaries;
448
- }
449
-
450
- /**
451
- * Discover candidate session files for {@link listSessions}.
452
- *
453
- * Always walks every subdirectory of `projectsDir`. We can't use the
454
- * encoded-dir shortcut for the cwd-scoped path because Claude Code
455
- * indexes session files by where the *process was launched*, not by
456
- * the recorded `cwd` field inside the session — these diverge in
457
- * worktrees and similar setups. The cwd filter is applied later
458
- * against `session.lastCwd` so callers see exactly the sessions whose
459
- * recorded cwd matches.
460
- */
461
- private discoverSessionFiles(): Array<{ filePath: string; defaultCwd: string }> {
462
- const out: Array<{ filePath: string; defaultCwd: string }> = [];
463
-
464
- if (!isDirectory(this.projectsDir)) return out;
465
-
466
- for (const dirName of safeReaddir(this.projectsDir)) {
467
- const projectDir = path.join(this.projectsDir, dirName);
468
- if (!isDirectory(projectDir)) continue;
469
-
470
- // Best-effort decode for the rare case session content has no
471
- // recorded cwd: '-Users-foo-bar' → '/Users/foo/bar'. Lossy for
472
- // paths containing '-'; session content's lastCwd overrides
473
- // this when available.
474
- const decoded = dirName.replace(/-/g, '/');
475
- for (const name of listJsonl(projectDir)) {
476
- out.push({ filePath: path.join(projectDir, name), defaultCwd: decoded });
477
- }
478
- }
479
-
480
- return out;
481
- }
482
340
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import type { ConversationMessage } from '../adapters/AgentAdapter.js';
4
- import { AgentStatus } from '../adapters/AgentAdapter.js';
3
+ import type { ConversationMessage } from '../../adapters/AgentAdapter.js';
4
+ import { AgentStatus } from '../../adapters/AgentAdapter.js';
5
5
 
6
6
  /**
7
7
  * Content block within a Claude Code JSONL message entry.
@@ -1,6 +1,7 @@
1
1
  import { execFile } from 'child_process';
2
2
  import { promisify } from 'util';
3
- import { ClaudePrintError } from './DurableAgent.js';
3
+ import { ClaudePrintError } from '../../../durable/DurableAgent.js';
4
+ import { sanitizeText } from '../../../durable/utils.js';
4
5
 
5
6
  type ExecResult = { stdout: string; stderr: string };
6
7
  type Exec = (file: string, args: string[]) => Promise<ExecResult>;
@@ -38,21 +39,14 @@ export class ClaudeCliProbe {
38
39
  }
39
40
  return {
40
41
  executable: this.executable,
41
- version: sanitize(versionResult.stdout, 256) || 'unknown',
42
+ version: sanitizeText(versionResult.stdout, 256) || 'unknown',
42
43
  };
43
44
  } catch (error) {
44
45
  if (error instanceof ClaudePrintError) throw error;
45
46
  throw new ClaudePrintError(
46
- `Claude CLI validation failed: ${sanitize((error as Error).message, 512)}`,
47
+ `Claude CLI validation failed: ${sanitizeText((error as Error).message, 512)}`,
47
48
  'CLAUDE_CLI_UNAVAILABLE',
48
49
  );
49
50
  }
50
51
  }
51
52
  }
52
-
53
- function sanitize(value: string, max: number): string {
54
- return Array.from(value, (character) => {
55
- const code = character.charCodeAt(0);
56
- return code <= 31 || code === 127 ? ' ' : character;
57
- }).join('').trim().slice(0, max);
58
- }
@@ -0,0 +1,66 @@
1
+ import type { DurableAgent, ProcessIdentity } from '../../../durable/DurableAgent.js';
2
+ import { ClaudePrintError } from '../../../durable/DurableAgent.js';
3
+ import { ClaudeCliProbe } from './ClaudeCliProbe.js';
4
+ import { ClaudePrintRunner, type ClaudePrintRunResult } from './ClaudePrintRunner.js';
5
+ import { DurableAgentRepository, type CreateDurableAgentInput } from '../../../durable/DurableAgentRepository.js';
6
+ import { runDurableAgent, type DurableRunRepository } from '../../../durable/run.js';
7
+
8
+ interface RepositoryLike extends DurableRunRepository {
9
+ create(input: CreateDurableAgentInput): Promise<DurableAgent>;
10
+ list(): Promise<DurableAgent[]>;
11
+ recordProviderProcess(id: string, token: string, identity: ProcessIdentity): Promise<void>;
12
+ }
13
+
14
+ interface ProbeLike { validate(): Promise<{ executable: string; version: string }> }
15
+ interface RunnerLike { run(request: Parameters<ClaudePrintRunner['run']>[0]): Promise<ClaudePrintRunResult> }
16
+
17
+ export interface ClaudePrintAgentServiceOptions {
18
+ repository?: RepositoryLike;
19
+ probe?: ProbeLike;
20
+ runner?: RunnerLike;
21
+ executable?: string;
22
+ }
23
+
24
+ export interface ClaudePrintSendResult extends ClaudePrintRunResult {
25
+ agentId: string;
26
+ agentName: string;
27
+ }
28
+
29
+ export class ClaudePrintAgentService {
30
+ readonly repository: RepositoryLike;
31
+ private readonly probe: ProbeLike;
32
+ private readonly runner: RunnerLike;
33
+ private readonly executable?: string;
34
+
35
+ constructor(options: ClaudePrintAgentServiceOptions = {}) {
36
+ this.repository = options.repository ?? new DurableAgentRepository();
37
+ this.probe = options.probe ?? new ClaudeCliProbe();
38
+ this.runner = options.runner ?? new ClaudePrintRunner();
39
+ this.executable = options.executable;
40
+ }
41
+
42
+ async create(input: CreateDurableAgentInput): Promise<DurableAgent> {
43
+ await this.probe.validate();
44
+ return this.repository.create(input);
45
+ }
46
+
47
+ async send(reference: string, prompt: string): Promise<ClaudePrintSendResult> {
48
+ const completed = await runDurableAgent({
49
+ reference, provider: 'claude', repository: this.repository,
50
+ ambiguousError: () => new ClaudePrintError(
51
+ `Multiple durable agents match "${reference}".`, 'DURABLE_AGENT_AMBIGUOUS'),
52
+ providerError: () => new ClaudePrintError(
53
+ 'Durable agent provider is not Claude.', 'CLAUDE_PRINT_UNSUPPORTED'),
54
+ execute: (agent, token) => this.runner.run({
55
+ agent,
56
+ prompt,
57
+ executable: this.executable,
58
+ firstRun: agent.sessionHealth === 'uninitialized',
59
+ onSpawn: (identity) => this.repository.recordProviderProcess(agent.id, token, identity),
60
+ }),
61
+ isSessionMismatch: (error) => error instanceof ClaudePrintError
62
+ && error.code === 'CLAUDE_SESSION_MISMATCH',
63
+ });
64
+ return { ...completed.result, agentId: completed.agent.id, agentName: completed.agent.name };
65
+ }
66
+ }