@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.
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js +1 -342
- package/dist/__tests__/adapters/ClaudeCodeAdapter.test.js.map +1 -1
- package/dist/__tests__/capacity/codex.test.js +343 -0
- package/dist/__tests__/capacity/codex.test.js.map +1 -0
- package/dist/__tests__/capacity/index.test.js +50 -0
- package/dist/__tests__/capacity/index.test.js.map +1 -0
- package/dist/__tests__/database/DurableAgentsDatabase.test.js +31 -3
- package/dist/__tests__/database/DurableAgentsDatabase.test.js.map +1 -1
- package/dist/__tests__/durable/CodexCliProbe.test.js +129 -0
- package/dist/__tests__/durable/CodexCliProbe.test.js.map +1 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js +84 -0
- package/dist/__tests__/durable/CodexDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js +126 -0
- package/dist/__tests__/durable/CodexPrintAgent.integration.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js +271 -0
- package/dist/__tests__/durable/CodexPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js +393 -0
- package/dist/__tests__/durable/CodexPrintRunner.test.js.map +1 -0
- package/dist/__tests__/print/ClaudeCliProbe.test.js +3 -7
- package/dist/__tests__/print/ClaudeCliProbe.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js +33 -27
- package/dist/__tests__/print/ClaudePrintAgent.integration.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintAgentService.test.js +65 -0
- package/dist/__tests__/print/ClaudePrintAgentService.test.js.map +1 -1
- package/dist/__tests__/print/ClaudePrintRunner.test.js +4 -10
- package/dist/__tests__/print/ClaudePrintRunner.test.js.map +1 -1
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js +79 -0
- package/dist/__tests__/providers/claude/ClaudeAgentMapper.test.js.map +1 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js +436 -0
- package/dist/__tests__/providers/claude/ClaudeSessionLocator.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js +76 -0
- package/dist/__tests__/providers/pi/durable/PiCliProbe.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js +43 -0
- package/dist/__tests__/providers/pi/durable/PiDurableAgentRepository.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js +202 -0
- package/dist/__tests__/providers/pi/durable/PiPrintAgentService.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js +119 -0
- package/dist/__tests__/providers/pi/durable/PiPrintProtocol.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js +386 -0
- package/dist/__tests__/providers/pi/durable/PiPrintRunner.test.js.map +1 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js +34 -0
- package/dist/__tests__/providers/pi/durable/PiStreamParser.test.js.map +1 -0
- package/dist/__tests__/utils/ClaudeSessionParser.test.js +2 -2
- package/dist/__tests__/utils/ClaudeSessionParser.test.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/capacity/codex.d.ts +36 -0
- package/dist/capacity/codex.d.ts.map +1 -0
- package/dist/capacity/codex.js +317 -0
- package/dist/capacity/codex.js.map +1 -0
- package/dist/capacity/index.d.ts +11 -0
- package/dist/capacity/index.d.ts.map +1 -0
- package/dist/capacity/index.js +49 -0
- package/dist/capacity/index.js.map +1 -0
- package/dist/capacity/types.d.ts +17 -0
- package/dist/capacity/types.d.ts.map +1 -0
- package/dist/capacity/types.js +3 -0
- package/dist/capacity/types.js.map +1 -0
- package/dist/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/dist/durable/DurableAgent.d.ts +23 -3
- package/dist/durable/DurableAgent.d.ts.map +1 -1
- package/dist/durable/DurableAgent.js +12 -0
- package/dist/durable/DurableAgent.js.map +1 -1
- package/dist/durable/DurableAgentRepository.d.ts +6 -7
- package/dist/durable/DurableAgentRepository.d.ts.map +1 -1
- package/dist/durable/DurableAgentRepository.js +73 -42
- package/dist/durable/DurableAgentRepository.js.map +1 -1
- package/dist/durable/process.d.ts +8 -0
- package/dist/durable/process.d.ts.map +1 -0
- package/dist/durable/process.js +40 -0
- package/dist/durable/process.js.map +1 -0
- package/dist/durable/run.d.ts +32 -0
- package/dist/durable/run.d.ts.map +1 -0
- package/dist/durable/run.js +46 -0
- package/dist/durable/run.js.map +1 -0
- package/dist/durable/utils.d.ts +19 -0
- package/dist/durable/utils.d.ts.map +1 -0
- package/dist/durable/utils.js +52 -0
- package/dist/durable/utils.js.map +1 -0
- package/dist/index.d.ts +19 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/ClaudeAgentMapper.d.ts +21 -0
- package/dist/providers/claude/ClaudeAgentMapper.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeAgentMapper.js +42 -0
- package/dist/providers/claude/ClaudeAgentMapper.js.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts +29 -0
- package/dist/providers/claude/ClaudeCodeAdapter.d.ts.map +1 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js +129 -0
- package/dist/providers/claude/ClaudeCodeAdapter.js.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.d.ts → providers/claude/ClaudeSessionLocator.d.ts} +68 -52
- package/dist/providers/claude/ClaudeSessionLocator.d.ts.map +1 -0
- package/dist/{adapters/ClaudeCodeAdapter.js → providers/claude/ClaudeSessionLocator.js} +71 -189
- package/dist/providers/claude/ClaudeSessionLocator.js.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.d.ts +2 -2
- package/dist/providers/claude/ClaudeSessionParser.d.ts.map +1 -0
- package/dist/{utils → providers/claude}/ClaudeSessionParser.js +1 -1
- package/dist/providers/claude/ClaudeSessionParser.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeCliProbe.d.ts.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudeCliProbe.js +4 -9
- package/dist/providers/claude/durable/ClaudeCliProbe.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintAgentService.d.ts +4 -9
- package/dist/providers/claude/durable/ClaudePrintAgentService.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js +45 -0
- package/dist/providers/claude/durable/ClaudePrintAgentService.js.map +1 -0
- package/dist/{durable → providers/claude/durable}/ClaudePrintRunner.d.ts +3 -3
- package/dist/providers/claude/durable/ClaudePrintRunner.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js +75 -0
- package/dist/providers/claude/durable/ClaudePrintRunner.js.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts +17 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.d.ts.map +1 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js +69 -0
- package/dist/providers/claude/durable/ClaudeStreamParser.js.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts +20 -0
- package/dist/providers/codex/durable/CodexCliProbe.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexCliProbe.js +61 -0
- package/dist/providers/codex/durable/CodexCliProbe.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts +40 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js +50 -0
- package/dist/providers/codex/durable/CodexPrintAgentService.js.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts +34 -0
- package/dist/providers/codex/durable/CodexPrintRunner.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js +78 -0
- package/dist/providers/codex/durable/CodexPrintRunner.js.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts +21 -0
- package/dist/providers/codex/durable/CodexStreamParser.d.ts.map +1 -0
- package/dist/providers/codex/durable/CodexStreamParser.js +86 -0
- package/dist/providers/codex/durable/CodexStreamParser.js.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts +20 -0
- package/dist/providers/pi/durable/PiCliProbe.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiCliProbe.js +51 -0
- package/dist/providers/pi/durable/PiCliProbe.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts +39 -0
- package/dist/providers/pi/durable/PiPrintAgentService.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js +47 -0
- package/dist/providers/pi/durable/PiPrintAgentService.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts +4 -0
- package/dist/providers/pi/durable/PiPrintProtocol.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js +44 -0
- package/dist/providers/pi/durable/PiPrintProtocol.js.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts +32 -0
- package/dist/providers/pi/durable/PiPrintRunner.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiPrintRunner.js +61 -0
- package/dist/providers/pi/durable/PiPrintRunner.js.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts +21 -0
- package/dist/providers/pi/durable/PiStreamParser.d.ts.map +1 -0
- package/dist/providers/pi/durable/PiStreamParser.js +71 -0
- package/dist/providers/pi/durable/PiStreamParser.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +1 -307
- package/src/__tests__/capacity/codex.test.ts +184 -0
- package/src/__tests__/capacity/index.test.ts +38 -0
- package/src/__tests__/database/DurableAgentsDatabase.test.ts +28 -3
- package/src/__tests__/durable/CodexCliProbe.test.ts +63 -0
- package/src/__tests__/durable/CodexDurableAgentRepository.test.ts +54 -0
- package/src/__tests__/durable/CodexPrintAgent.integration.test.ts +79 -0
- package/src/__tests__/durable/CodexPrintAgentService.test.ts +139 -0
- package/src/__tests__/durable/CodexPrintRunner.test.ts +189 -0
- package/src/__tests__/fixtures/fake-codex.cjs +45 -0
- package/src/__tests__/print/ClaudeCliProbe.test.ts +3 -7
- package/src/__tests__/print/ClaudePrintAgent.integration.test.ts +19 -22
- package/src/__tests__/print/ClaudePrintAgentService.test.ts +33 -1
- package/src/__tests__/print/ClaudePrintRunner.test.ts +4 -10
- package/src/__tests__/providers/claude/ClaudeAgentMapper.test.ts +84 -0
- package/src/__tests__/providers/claude/ClaudeSessionLocator.test.ts +411 -0
- package/src/__tests__/providers/pi/durable/PiCliProbe.test.ts +38 -0
- package/src/__tests__/providers/pi/durable/PiDurableAgentRepository.test.ts +32 -0
- package/src/__tests__/providers/pi/durable/PiPrintAgentService.test.ts +69 -0
- package/src/__tests__/providers/pi/durable/PiPrintProtocol.test.ts +33 -0
- package/src/__tests__/providers/pi/durable/PiPrintRunner.test.ts +107 -0
- package/src/__tests__/providers/pi/durable/PiStreamParser.test.ts +24 -0
- package/src/__tests__/utils/ClaudeSessionParser.test.ts +2 -2
- package/src/adapters/index.ts +1 -1
- package/src/capacity/codex.ts +320 -0
- package/src/capacity/index.ts +57 -0
- package/src/capacity/types.ts +18 -0
- package/src/database/migrations/004_codex_durable_sessions.sql +1 -0
- package/src/durable/DurableAgent.ts +51 -3
- package/src/durable/DurableAgentRepository.ts +60 -31
- package/src/durable/process.ts +28 -0
- package/src/durable/run.ts +74 -0
- package/src/durable/utils.ts +61 -0
- package/src/index.ts +37 -15
- package/src/providers/claude/ClaudeAgentMapper.ts +62 -0
- package/src/providers/claude/ClaudeCodeAdapter.ts +160 -0
- package/src/{adapters/ClaudeCodeAdapter.ts → providers/claude/ClaudeSessionLocator.ts} +97 -239
- package/src/{utils → providers/claude}/ClaudeSessionParser.ts +2 -2
- package/src/{durable → providers/claude/durable}/ClaudeCliProbe.ts +4 -10
- package/src/providers/claude/durable/ClaudePrintAgentService.ts +66 -0
- package/src/providers/claude/durable/ClaudePrintRunner.ts +92 -0
- package/src/providers/claude/durable/ClaudeStreamParser.ts +79 -0
- package/src/providers/codex/durable/CodexCliProbe.ts +60 -0
- package/src/providers/codex/durable/CodexPrintAgentService.ts +65 -0
- package/src/providers/codex/durable/CodexPrintRunner.ts +94 -0
- package/src/providers/codex/durable/CodexStreamParser.ts +104 -0
- package/src/providers/pi/durable/PiCliProbe.ts +48 -0
- package/src/providers/pi/durable/PiPrintAgentService.ts +69 -0
- package/src/providers/pi/durable/PiPrintProtocol.ts +39 -0
- package/src/providers/pi/durable/PiPrintRunner.ts +88 -0
- package/src/providers/pi/durable/PiStreamParser.ts +84 -0
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +0 -1
- package/dist/adapters/ClaudeCodeAdapter.js.map +0 -1
- package/dist/durable/ClaudeCliProbe.d.ts.map +0 -1
- package/dist/durable/ClaudeCliProbe.js.map +0 -1
- package/dist/durable/ClaudePrintAgentService.d.ts.map +0 -1
- package/dist/durable/ClaudePrintAgentService.js +0 -66
- package/dist/durable/ClaudePrintAgentService.js.map +0 -1
- package/dist/durable/ClaudePrintRunner.d.ts.map +0 -1
- package/dist/durable/ClaudePrintRunner.js +0 -128
- package/dist/durable/ClaudePrintRunner.js.map +0 -1
- package/dist/utils/ClaudeSessionParser.d.ts.map +0 -1
- package/dist/utils/ClaudeSessionParser.js.map +0 -1
- package/src/durable/ClaudePrintAgentService.ts +0 -94
- package/src/durable/ClaudePrintRunner.ts +0 -139
- /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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
48
|
-
*
|
|
42
|
+
* those values are carried here so the agent mapper can prefer them over
|
|
43
|
+
* the JSONL-derived heuristic.
|
|
49
44
|
*/
|
|
50
|
-
interface
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
194
|
+
direct: ClaudeDirectMatch[];
|
|
207
195
|
fallback: ProcessInfo[];
|
|
208
196
|
} {
|
|
209
|
-
const direct:
|
|
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:
|
|
303
|
+
direct: ClaudeDirectMatch[];
|
|
316
304
|
fallback: ProcessInfo[];
|
|
317
305
|
} {
|
|
318
|
-
const direct:
|
|
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 '
|
|
4
|
-
import { AgentStatus } from '
|
|
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 '
|
|
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:
|
|
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: ${
|
|
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
|
+
}
|