@ai-devkit/agent-manager 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/AgentAdapter.d.ts +2 -0
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.d.ts +29 -34
- package/dist/adapters/ClaudeCodeAdapter.d.ts.map +1 -1
- package/dist/adapters/ClaudeCodeAdapter.js +138 -294
- package/dist/adapters/ClaudeCodeAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +32 -30
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +148 -282
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -10
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +6 -3
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +12 -11
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/matching.d.ts +39 -0
- package/dist/utils/matching.d.ts.map +1 -0
- package/dist/utils/matching.js +103 -0
- package/dist/utils/matching.js.map +1 -0
- package/dist/utils/process.d.ts +25 -40
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +151 -105
- package/dist/utils/process.js.map +1 -1
- package/dist/utils/session.d.ts +30 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +101 -0
- package/dist/utils/session.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/AgentManager.test.ts +0 -25
- package/src/__tests__/adapters/ClaudeCodeAdapter.test.ts +598 -845
- package/src/__tests__/adapters/CodexAdapter.test.ts +467 -274
- package/src/__tests__/utils/matching.test.ts +191 -0
- package/src/__tests__/utils/process.test.ts +202 -0
- package/src/__tests__/utils/session.test.ts +117 -0
- package/src/adapters/AgentAdapter.ts +3 -0
- package/src/adapters/ClaudeCodeAdapter.ts +177 -425
- package/src/adapters/CodexAdapter.ts +155 -409
- package/src/index.ts +1 -3
- package/src/utils/index.ts +6 -3
- package/src/utils/matching.ts +92 -0
- package/src/utils/process.ts +133 -119
- package/src/utils/session.ts +92 -0
- package/dist/utils/file.d.ts +0 -52
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -135
- package/dist/utils/file.js.map +0 -1
- package/src/utils/file.ts +0 -100
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/AgentAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,oBAAY,WAAW;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,OAAO,YAAY;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IAEb,oBAAoB;IACpB,IAAI,EAAE,SAAS,CAAC;IAEhB,qBAAqB;IACrB,MAAM,EAAE,WAAW,CAAC;IAEpB,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAEhB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IAEpB,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAElB,wGAAwG;IACxG,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,iCAAiC;IACjC,UAAU,EAAE,IAAI,CAAC;CAEpB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAEhB,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"AgentAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/AgentAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;AAEpE;;GAEG;AACH,oBAAY,WAAW;IACnB,OAAO,YAAY;IACnB,OAAO,YAAY;IACnB,IAAI,SAAS;IACb,OAAO,YAAY;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACtB,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IAEb,oBAAoB;IACpB,IAAI,EAAE,SAAS,CAAC;IAEhB,qBAAqB;IACrB,MAAM,EAAE,WAAW,CAAC;IAEpB,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC;IAEhB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,WAAW,EAAE,MAAM,CAAC;IAEpB,mBAAmB;IACnB,SAAS,EAAE,MAAM,CAAC;IAElB,wGAAwG;IACxG,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,iCAAiC;IACjC,UAAU,EAAE,IAAI,CAAC;CAEpB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IAEZ,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAEhB,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IAEZ,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IAEZ,uDAAuD;IACvD,SAAS,CAAC,EAAE,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IACzB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAEzB;;;OAGG;IACH,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAErC;;;;OAIG;IACH,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC;CAChD"}
|
|
@@ -3,21 +3,16 @@ import type { AgentAdapter, AgentInfo, ProcessInfo } from './AgentAdapter';
|
|
|
3
3
|
* Claude Code Adapter
|
|
4
4
|
*
|
|
5
5
|
* Detects Claude Code agents by:
|
|
6
|
-
* 1. Finding running claude processes
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
6
|
+
* 1. Finding running claude processes via shared listAgentProcesses()
|
|
7
|
+
* 2. Enriching with CWD and start times via shared enrichProcesses()
|
|
8
|
+
* 3. Attempting authoritative PID-file matching via ~/.claude/sessions/<pid>.json
|
|
9
|
+
* 4. Falling back to CWD+birthtime heuristic (matchProcessesToSessions) for processes without a PID file
|
|
10
10
|
* 5. Extracting summary from last user message in session JSONL
|
|
11
11
|
*/
|
|
12
12
|
export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
13
13
|
readonly type: "claude";
|
|
14
|
-
/** Limit session parsing per run to keep list latency bounded. */
|
|
15
|
-
private static readonly MIN_SESSION_SCAN;
|
|
16
|
-
private static readonly MAX_SESSION_SCAN;
|
|
17
|
-
private static readonly SESSION_SCAN_MULTIPLIER;
|
|
18
|
-
/** Matching tolerance between process start time and session start time. */
|
|
19
|
-
private static readonly PROCESS_SESSION_TIME_TOLERANCE_MS;
|
|
20
14
|
private projectsDir;
|
|
15
|
+
private sessionsDir;
|
|
21
16
|
constructor();
|
|
22
17
|
/**
|
|
23
18
|
* Check if this adapter can handle a given process
|
|
@@ -28,24 +23,34 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
28
23
|
* Detect running Claude Code agents
|
|
29
24
|
*/
|
|
30
25
|
detectAgents(): Promise<AgentInfo[]>;
|
|
31
|
-
private listClaudeProcesses;
|
|
32
|
-
private calculateSessionScanLimit;
|
|
33
|
-
private assignSessionsForMode;
|
|
34
|
-
private mapSessionToAgent;
|
|
35
|
-
private mapProcessOnlyAgent;
|
|
36
|
-
private selectBestSession;
|
|
37
|
-
private filterCandidateSessions;
|
|
38
|
-
private rankCandidatesByStartTime;
|
|
39
|
-
private getProcessStartTimes;
|
|
40
|
-
private parseElapsedSeconds;
|
|
41
26
|
/**
|
|
42
|
-
*
|
|
27
|
+
* Discover session files for the given processes.
|
|
28
|
+
*
|
|
29
|
+
* For each unique process CWD, encodes it to derive the expected
|
|
30
|
+
* ~/.claude/projects/<encoded>/ directory, then gets session file birthtimes
|
|
31
|
+
* via a single batched stat call across all directories.
|
|
32
|
+
*/
|
|
33
|
+
private discoverSessions;
|
|
34
|
+
/**
|
|
35
|
+
* Attempt to match each process to its session via ~/.claude/sessions/<pid>.json.
|
|
36
|
+
*
|
|
37
|
+
* Returns:
|
|
38
|
+
* direct — processes matched authoritatively via PID file
|
|
39
|
+
* fallback — processes with no valid PID file (sent to legacy matching)
|
|
40
|
+
*
|
|
41
|
+
* Per-process fallback triggers on: file absent, malformed JSON,
|
|
42
|
+
* stale startedAt (>60 s from proc.startTime), or missing JSONL.
|
|
43
43
|
*/
|
|
44
|
-
private
|
|
44
|
+
private tryPidFileMatching;
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Derive the Claude Code project directory for a given CWD.
|
|
47
|
+
*
|
|
48
|
+
* Claude Code encodes paths by replacing '/' with '-':
|
|
49
|
+
* /Users/foo/bar → ~/.claude/projects/-Users-foo-bar/
|
|
47
50
|
*/
|
|
48
|
-
private
|
|
51
|
+
private getProjectDir;
|
|
52
|
+
private mapSessionToAgent;
|
|
53
|
+
private mapProcessOnlyAgent;
|
|
49
54
|
/**
|
|
50
55
|
* Parse a single session file into ClaudeSession
|
|
51
56
|
*/
|
|
@@ -54,15 +59,6 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
54
59
|
* Determine agent status from session state
|
|
55
60
|
*/
|
|
56
61
|
private determineStatus;
|
|
57
|
-
/**
|
|
58
|
-
* Generate unique agent name
|
|
59
|
-
* Uses project basename, appends slug if multiple sessions for same project
|
|
60
|
-
*/
|
|
61
|
-
private generateAgentName;
|
|
62
|
-
/** Check if two paths are equal, or one is a parent/child of the other. */
|
|
63
|
-
private pathRelated;
|
|
64
|
-
private pathEquals;
|
|
65
|
-
private isChildPath;
|
|
66
62
|
/**
|
|
67
63
|
* Extract meaningful text from a user message content.
|
|
68
64
|
* Handles string and array formats, skill command expansion, and noise filtering.
|
|
@@ -81,6 +77,5 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
81
77
|
* These should not overwrite lastEntryType used for status determination.
|
|
82
78
|
*/
|
|
83
79
|
private isMetadataEntryType;
|
|
84
|
-
private normalizePath;
|
|
85
80
|
}
|
|
86
81
|
//# sourceMappingURL=ClaudeCodeAdapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClaudeCodeAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/ClaudeCodeAdapter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ClaudeCodeAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/ClaudeCodeAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA0D3E;;;;;;;;;GASG;AACH,qBAAa,iBAAkB,YAAW,YAAY;IAClD,QAAQ,CAAC,IAAI,EAAG,QAAQ,CAAU;IAElC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;;IAQ5B;;OAEG;IACH,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAI5C,OAAO,CAAC,kBAAkB;IAM1B;;OAEG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;IAwD1C;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IAgCxB;;;;;;;;;OASG;IACH,OAAO,CAAC,kBAAkB;IAkD1B;;;;;OAKG;IACH,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,mBAAmB;IAa3B;;OAEG;IACH,OAAO,CAAC,WAAW;IAyGnB;;OAEG;IACH,OAAO,CAAC,eAAe;IAiCvB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IA2C9B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAW3B;;OAEG;IACH,OAAO,CAAC,cAAc;IAQtB;;;OAGG;IACH,OAAO,CAAC,mBAAmB;CAI9B"}
|
|
@@ -36,18 +36,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.ClaudeCodeAdapter = void 0;
|
|
37
37
|
const fs = __importStar(require("fs"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
|
-
const child_process_1 = require("child_process");
|
|
40
39
|
const AgentAdapter_1 = require("./AgentAdapter");
|
|
41
40
|
const process_1 = require("../utils/process");
|
|
42
|
-
const
|
|
41
|
+
const session_1 = require("../utils/session");
|
|
42
|
+
const matching_1 = require("../utils/matching");
|
|
43
43
|
/**
|
|
44
44
|
* Claude Code Adapter
|
|
45
45
|
*
|
|
46
46
|
* Detects Claude Code agents by:
|
|
47
|
-
* 1. Finding running claude processes
|
|
48
|
-
* 2.
|
|
49
|
-
* 3.
|
|
50
|
-
* 4.
|
|
47
|
+
* 1. Finding running claude processes via shared listAgentProcesses()
|
|
48
|
+
* 2. Enriching with CWD and start times via shared enrichProcesses()
|
|
49
|
+
* 3. Attempting authoritative PID-file matching via ~/.claude/sessions/<pid>.json
|
|
50
|
+
* 4. Falling back to CWD+birthtime heuristic (matchProcessesToSessions) for processes without a PID file
|
|
51
51
|
* 5. Extracting summary from last user message in session JSONL
|
|
52
52
|
*/
|
|
53
53
|
class ClaudeCodeAdapter {
|
|
@@ -55,6 +55,7 @@ class ClaudeCodeAdapter {
|
|
|
55
55
|
this.type = 'claude';
|
|
56
56
|
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
57
57
|
this.projectsDir = path.join(homeDir, '.claude', 'projects');
|
|
58
|
+
this.sessionsDir = path.join(homeDir, '.claude', 'sessions');
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
60
61
|
* Check if this adapter can handle a given process
|
|
@@ -71,277 +72,169 @@ class ClaudeCodeAdapter {
|
|
|
71
72
|
* Detect running Claude Code agents
|
|
72
73
|
*/
|
|
73
74
|
async detectAgents() {
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
75
|
+
const processes = (0, process_1.enrichProcesses)((0, process_1.listAgentProcesses)('claude'));
|
|
76
|
+
if (processes.length === 0) {
|
|
76
77
|
return [];
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
79
|
+
// Step 1: try authoritative PID-file matching for every process
|
|
80
|
+
const { direct, fallback } = this.tryPidFileMatching(processes);
|
|
81
|
+
// Step 2: run legacy CWD+birthtime matching only for processes without a PID file
|
|
82
|
+
const legacySessions = this.discoverSessions(fallback);
|
|
83
|
+
const legacyMatches = fallback.length > 0 && legacySessions.length > 0
|
|
84
|
+
? (0, matching_1.matchProcessesToSessions)(fallback, legacySessions)
|
|
85
|
+
: [];
|
|
86
|
+
const matchedPids = new Set([
|
|
87
|
+
...direct.map((d) => d.process.pid),
|
|
88
|
+
...legacyMatches.map((m) => m.process.pid),
|
|
89
|
+
]);
|
|
87
90
|
const agents = [];
|
|
88
|
-
|
|
89
|
-
for (const
|
|
90
|
-
this.
|
|
91
|
+
// Build agents from direct (PID-file) matches
|
|
92
|
+
for (const { process: proc, sessionFile } of direct) {
|
|
93
|
+
const sessionData = this.readSession(sessionFile.filePath, sessionFile.resolvedCwd);
|
|
94
|
+
if (sessionData) {
|
|
95
|
+
agents.push(this.mapSessionToAgent(sessionData, proc, sessionFile));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
matchedPids.delete(proc.pid);
|
|
99
|
+
}
|
|
91
100
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
// Build agents from legacy matches
|
|
102
|
+
for (const match of legacyMatches) {
|
|
103
|
+
const sessionData = this.readSession(match.session.filePath, match.session.resolvedCwd);
|
|
104
|
+
if (sessionData) {
|
|
105
|
+
agents.push(this.mapSessionToAgent(sessionData, match.process, match.session));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
matchedPids.delete(match.process.pid);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Any process with no match (direct or legacy) appears as IDLE
|
|
112
|
+
for (const proc of processes) {
|
|
113
|
+
if (!matchedPids.has(proc.pid)) {
|
|
114
|
+
agents.push(this.mapProcessOnlyAgent(proc));
|
|
95
115
|
}
|
|
96
|
-
assignedPids.add(processInfo.pid);
|
|
97
|
-
agents.push(this.mapProcessOnlyAgent(processInfo, agents));
|
|
98
116
|
}
|
|
99
117
|
return agents;
|
|
100
118
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Discover session files for the given processes.
|
|
121
|
+
*
|
|
122
|
+
* For each unique process CWD, encodes it to derive the expected
|
|
123
|
+
* ~/.claude/projects/<encoded>/ directory, then gets session file birthtimes
|
|
124
|
+
* via a single batched stat call across all directories.
|
|
125
|
+
*/
|
|
126
|
+
discoverSessions(processes) {
|
|
127
|
+
// Collect valid project dirs and map them back to their CWD
|
|
128
|
+
const dirToCwd = new Map();
|
|
129
|
+
for (const proc of processes) {
|
|
130
|
+
if (!proc.cwd)
|
|
110
131
|
continue;
|
|
132
|
+
const projectDir = this.getProjectDir(proc.cwd);
|
|
133
|
+
if (dirToCwd.has(projectDir))
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
if (!fs.statSync(projectDir).isDirectory())
|
|
137
|
+
continue;
|
|
111
138
|
}
|
|
112
|
-
|
|
113
|
-
if (!session) {
|
|
139
|
+
catch {
|
|
114
140
|
continue;
|
|
115
141
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
dirToCwd.set(projectDir, proc.cwd);
|
|
143
|
+
}
|
|
144
|
+
if (dirToCwd.size === 0)
|
|
145
|
+
return [];
|
|
146
|
+
// Single batched stat call across all directories
|
|
147
|
+
const files = (0, session_1.batchGetSessionFileBirthtimes)([...dirToCwd.keys()]);
|
|
148
|
+
// Set resolvedCwd based on which project dir the file belongs to
|
|
149
|
+
for (const file of files) {
|
|
150
|
+
file.resolvedCwd = dirToCwd.get(file.projectDir) || '';
|
|
119
151
|
}
|
|
152
|
+
return files;
|
|
120
153
|
}
|
|
121
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Attempt to match each process to its session via ~/.claude/sessions/<pid>.json.
|
|
156
|
+
*
|
|
157
|
+
* Returns:
|
|
158
|
+
* direct — processes matched authoritatively via PID file
|
|
159
|
+
* fallback — processes with no valid PID file (sent to legacy matching)
|
|
160
|
+
*
|
|
161
|
+
* Per-process fallback triggers on: file absent, malformed JSON,
|
|
162
|
+
* stale startedAt (>60 s from proc.startTime), or missing JSONL.
|
|
163
|
+
*/
|
|
164
|
+
tryPidFileMatching(processes) {
|
|
165
|
+
const direct = [];
|
|
166
|
+
const fallback = [];
|
|
167
|
+
for (const proc of processes) {
|
|
168
|
+
const pidFilePath = path.join(this.sessionsDir, `${proc.pid}.json`);
|
|
169
|
+
try {
|
|
170
|
+
const entry = JSON.parse(fs.readFileSync(pidFilePath, 'utf-8'));
|
|
171
|
+
// Stale-file guard: reject PID files from a previous process with the same PID
|
|
172
|
+
if (proc.startTime) {
|
|
173
|
+
const deltaMs = Math.abs(proc.startTime.getTime() - entry.startedAt);
|
|
174
|
+
if (deltaMs > 60000) {
|
|
175
|
+
fallback.push(proc);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const projectDir = this.getProjectDir(entry.cwd);
|
|
180
|
+
const jsonlPath = path.join(projectDir, `${entry.sessionId}.jsonl`);
|
|
181
|
+
if (!fs.existsSync(jsonlPath)) {
|
|
182
|
+
fallback.push(proc);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
direct.push({
|
|
186
|
+
process: proc,
|
|
187
|
+
sessionFile: {
|
|
188
|
+
sessionId: entry.sessionId,
|
|
189
|
+
filePath: jsonlPath,
|
|
190
|
+
projectDir,
|
|
191
|
+
birthtimeMs: entry.startedAt,
|
|
192
|
+
resolvedCwd: entry.cwd,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// PID file absent, unreadable, or malformed — fall back per-process
|
|
198
|
+
fallback.push(proc);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { direct, fallback };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Derive the Claude Code project directory for a given CWD.
|
|
205
|
+
*
|
|
206
|
+
* Claude Code encodes paths by replacing '/' with '-':
|
|
207
|
+
* /Users/foo/bar → ~/.claude/projects/-Users-foo-bar/
|
|
208
|
+
*/
|
|
209
|
+
getProjectDir(cwd) {
|
|
210
|
+
const encoded = cwd.replace(/\//g, '-');
|
|
211
|
+
return path.join(this.projectsDir, encoded);
|
|
212
|
+
}
|
|
213
|
+
mapSessionToAgent(session, processInfo, sessionFile) {
|
|
122
214
|
return {
|
|
123
|
-
name:
|
|
215
|
+
name: (0, matching_1.generateAgentName)(processInfo.cwd, processInfo.pid),
|
|
124
216
|
type: this.type,
|
|
125
217
|
status: this.determineStatus(session),
|
|
126
218
|
summary: session.lastUserMessage || 'Session started',
|
|
127
219
|
pid: processInfo.pid,
|
|
128
|
-
projectPath:
|
|
129
|
-
sessionId:
|
|
220
|
+
projectPath: sessionFile.resolvedCwd || processInfo.cwd || '',
|
|
221
|
+
sessionId: sessionFile.sessionId,
|
|
130
222
|
slug: session.slug,
|
|
131
223
|
lastActive: session.lastActive,
|
|
132
224
|
};
|
|
133
225
|
}
|
|
134
|
-
mapProcessOnlyAgent(processInfo
|
|
135
|
-
const processCwd = processInfo.cwd || '';
|
|
136
|
-
const projectName = path.basename(processCwd) || 'claude';
|
|
137
|
-
const hasDuplicate = existingAgents.some((a) => a.projectPath === processCwd);
|
|
226
|
+
mapProcessOnlyAgent(processInfo) {
|
|
138
227
|
return {
|
|
139
|
-
name:
|
|
228
|
+
name: (0, matching_1.generateAgentName)(processInfo.cwd || '', processInfo.pid),
|
|
140
229
|
type: this.type,
|
|
141
230
|
status: AgentAdapter_1.AgentStatus.IDLE,
|
|
142
231
|
summary: 'Unknown',
|
|
143
232
|
pid: processInfo.pid,
|
|
144
|
-
projectPath:
|
|
233
|
+
projectPath: processInfo.cwd || '',
|
|
145
234
|
sessionId: `pid-${processInfo.pid}`,
|
|
146
235
|
lastActive: new Date(),
|
|
147
236
|
};
|
|
148
237
|
}
|
|
149
|
-
selectBestSession(processInfo, sessions, usedSessionIds, processStartByPid, mode) {
|
|
150
|
-
const candidates = this.filterCandidateSessions(processInfo, sessions, usedSessionIds, mode);
|
|
151
|
-
if (candidates.length === 0) {
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
const processStart = processStartByPid.get(processInfo.pid);
|
|
155
|
-
if (!processStart) {
|
|
156
|
-
return candidates.sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime())[0];
|
|
157
|
-
}
|
|
158
|
-
const best = this.rankCandidatesByStartTime(candidates, processStart)[0];
|
|
159
|
-
if (!best) {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
// In early modes (cwd/missing-cwd), defer assignment when the best
|
|
163
|
-
// candidate is outside start-time tolerance — a closer match may
|
|
164
|
-
// exist in parent-child mode (e.g., worktree sessions).
|
|
165
|
-
if (mode !== 'parent-child') {
|
|
166
|
-
const diffMs = Math.abs(best.sessionStart.getTime() - processStart.getTime());
|
|
167
|
-
if (diffMs > ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return best;
|
|
172
|
-
}
|
|
173
|
-
filterCandidateSessions(processInfo, sessions, usedSessionIds, mode) {
|
|
174
|
-
return sessions.filter((session) => {
|
|
175
|
-
if (usedSessionIds.has(session.sessionId)) {
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
if (mode === 'cwd') {
|
|
179
|
-
return (this.pathEquals(processInfo.cwd, session.projectPath) ||
|
|
180
|
-
this.pathEquals(processInfo.cwd, session.lastCwd));
|
|
181
|
-
}
|
|
182
|
-
if (mode === 'missing-cwd') {
|
|
183
|
-
return !session.projectPath;
|
|
184
|
-
}
|
|
185
|
-
// parent-child mode: match if process CWD equals, is under, or is
|
|
186
|
-
// a parent of session project/lastCwd. This also catches exact CWD
|
|
187
|
-
// matches that were deferred from `cwd` mode due to start-time tolerance.
|
|
188
|
-
return (this.pathRelated(processInfo.cwd, session.projectPath) ||
|
|
189
|
-
this.pathRelated(processInfo.cwd, session.lastCwd));
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
rankCandidatesByStartTime(candidates, processStart) {
|
|
193
|
-
const toleranceMs = ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS;
|
|
194
|
-
return candidates
|
|
195
|
-
.map((session) => {
|
|
196
|
-
const diffMs = Math.abs(session.sessionStart.getTime() - processStart.getTime());
|
|
197
|
-
const outsideTolerance = diffMs > toleranceMs ? 1 : 0;
|
|
198
|
-
return {
|
|
199
|
-
session,
|
|
200
|
-
rank: outsideTolerance,
|
|
201
|
-
diffMs,
|
|
202
|
-
recency: session.lastActive.getTime(),
|
|
203
|
-
};
|
|
204
|
-
})
|
|
205
|
-
.sort((a, b) => {
|
|
206
|
-
if (a.rank !== b.rank)
|
|
207
|
-
return a.rank - b.rank;
|
|
208
|
-
// Within tolerance (rank 0): prefer most recently active session.
|
|
209
|
-
// The exact diff is noise — a 6s vs 45s difference is meaningless,
|
|
210
|
-
// but the session with recent activity is more likely the real one.
|
|
211
|
-
if (a.rank === 0)
|
|
212
|
-
return b.recency - a.recency;
|
|
213
|
-
// Outside tolerance: prefer smallest time difference, then recency.
|
|
214
|
-
if (a.diffMs !== b.diffMs)
|
|
215
|
-
return a.diffMs - b.diffMs;
|
|
216
|
-
return b.recency - a.recency;
|
|
217
|
-
})
|
|
218
|
-
.map((ranked) => ranked.session);
|
|
219
|
-
}
|
|
220
|
-
getProcessStartTimes(pids) {
|
|
221
|
-
if (pids.length === 0 || process.env.JEST_WORKER_ID) {
|
|
222
|
-
return new Map();
|
|
223
|
-
}
|
|
224
|
-
try {
|
|
225
|
-
const output = (0, child_process_1.execSync)(`ps -o pid=,etime= -p ${pids.join(',')}`, { encoding: 'utf-8' });
|
|
226
|
-
const nowMs = Date.now();
|
|
227
|
-
const startTimes = new Map();
|
|
228
|
-
for (const rawLine of output.split('\n')) {
|
|
229
|
-
const line = rawLine.trim();
|
|
230
|
-
if (!line)
|
|
231
|
-
continue;
|
|
232
|
-
const parts = line.split(/\s+/);
|
|
233
|
-
if (parts.length < 2)
|
|
234
|
-
continue;
|
|
235
|
-
const pid = Number.parseInt(parts[0], 10);
|
|
236
|
-
const elapsedSeconds = this.parseElapsedSeconds(parts[1]);
|
|
237
|
-
if (!Number.isFinite(pid) || elapsedSeconds === null)
|
|
238
|
-
continue;
|
|
239
|
-
startTimes.set(pid, new Date(nowMs - elapsedSeconds * 1000));
|
|
240
|
-
}
|
|
241
|
-
return startTimes;
|
|
242
|
-
}
|
|
243
|
-
catch {
|
|
244
|
-
return new Map();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
parseElapsedSeconds(etime) {
|
|
248
|
-
const match = etime
|
|
249
|
-
.trim()
|
|
250
|
-
.match(/^(?:(\d+)-)?(?:(\d{1,2}):)?(\d{1,2}):(\d{2})$/);
|
|
251
|
-
if (!match) {
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
const days = Number.parseInt(match[1] || '0', 10);
|
|
255
|
-
const hours = Number.parseInt(match[2] || '0', 10);
|
|
256
|
-
const minutes = Number.parseInt(match[3] || '0', 10);
|
|
257
|
-
const seconds = Number.parseInt(match[4] || '0', 10);
|
|
258
|
-
return ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Read Claude Code sessions with bounded scanning
|
|
262
|
-
*/
|
|
263
|
-
readSessions(limit) {
|
|
264
|
-
const sessionFiles = this.findSessionFiles(limit);
|
|
265
|
-
const sessions = [];
|
|
266
|
-
for (const file of sessionFiles) {
|
|
267
|
-
try {
|
|
268
|
-
const session = this.readSession(file.filePath, file.projectPath);
|
|
269
|
-
if (session) {
|
|
270
|
-
sessions.push(session);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
catch (error) {
|
|
274
|
-
console.error(`Failed to parse Claude session ${file.filePath}:`, error);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return sessions;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Find session files bounded by mtime, sorted most-recent first
|
|
281
|
-
*/
|
|
282
|
-
findSessionFiles(limit) {
|
|
283
|
-
if (!fs.existsSync(this.projectsDir)) {
|
|
284
|
-
return [];
|
|
285
|
-
}
|
|
286
|
-
const files = [];
|
|
287
|
-
for (const dirName of fs.readdirSync(this.projectsDir)) {
|
|
288
|
-
if (dirName.startsWith('.')) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
const projectDir = path.join(this.projectsDir, dirName);
|
|
292
|
-
try {
|
|
293
|
-
if (!fs.statSync(projectDir).isDirectory())
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
continue;
|
|
298
|
-
}
|
|
299
|
-
const indexPath = path.join(projectDir, 'sessions-index.json');
|
|
300
|
-
const index = (0, file_1.readJson)(indexPath);
|
|
301
|
-
const projectPath = index?.originalPath || '';
|
|
302
|
-
for (const entry of fs.readdirSync(projectDir)) {
|
|
303
|
-
if (!entry.endsWith('.jsonl')) {
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
const filePath = path.join(projectDir, entry);
|
|
307
|
-
try {
|
|
308
|
-
files.push({
|
|
309
|
-
filePath,
|
|
310
|
-
projectPath,
|
|
311
|
-
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
// Ensure breadth: include at least the most recent session per project,
|
|
320
|
-
// then fill remaining slots with globally most-recent sessions.
|
|
321
|
-
const sorted = files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
322
|
-
const result = [];
|
|
323
|
-
const seenProjects = new Set();
|
|
324
|
-
// First pass: one most-recent session per project directory
|
|
325
|
-
for (const file of sorted) {
|
|
326
|
-
const projDir = path.dirname(file.filePath);
|
|
327
|
-
if (!seenProjects.has(projDir)) {
|
|
328
|
-
seenProjects.add(projDir);
|
|
329
|
-
result.push(file);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
// Second pass: fill remaining slots with globally most-recent
|
|
333
|
-
if (result.length < limit) {
|
|
334
|
-
const resultSet = new Set(result.map((f) => f.filePath));
|
|
335
|
-
for (const file of sorted) {
|
|
336
|
-
if (result.length >= limit)
|
|
337
|
-
break;
|
|
338
|
-
if (!resultSet.has(file.filePath)) {
|
|
339
|
-
result.push(file);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return result.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, limit);
|
|
344
|
-
}
|
|
345
238
|
/**
|
|
346
239
|
* Parse a single session file into ClaudeSession
|
|
347
240
|
*/
|
|
@@ -461,42 +354,6 @@ class ClaudeCodeAdapter {
|
|
|
461
354
|
}
|
|
462
355
|
return AgentAdapter_1.AgentStatus.UNKNOWN;
|
|
463
356
|
}
|
|
464
|
-
/**
|
|
465
|
-
* Generate unique agent name
|
|
466
|
-
* Uses project basename, appends slug if multiple sessions for same project
|
|
467
|
-
*/
|
|
468
|
-
generateAgentName(session, existingAgents) {
|
|
469
|
-
const projectName = path.basename(session.projectPath) || 'claude';
|
|
470
|
-
const sameProjectAgents = existingAgents.filter((a) => a.projectPath === session.projectPath);
|
|
471
|
-
if (sameProjectAgents.length === 0) {
|
|
472
|
-
return projectName;
|
|
473
|
-
}
|
|
474
|
-
if (session.slug) {
|
|
475
|
-
const slugPart = session.slug.includes('-')
|
|
476
|
-
? session.slug.split('-')[0]
|
|
477
|
-
: session.slug.slice(0, 8);
|
|
478
|
-
return `${projectName} (${slugPart})`;
|
|
479
|
-
}
|
|
480
|
-
return `${projectName} (${session.sessionId.slice(0, 8)})`;
|
|
481
|
-
}
|
|
482
|
-
/** Check if two paths are equal, or one is a parent/child of the other. */
|
|
483
|
-
pathRelated(a, b) {
|
|
484
|
-
return this.pathEquals(a, b) || this.isChildPath(a, b) || this.isChildPath(b, a);
|
|
485
|
-
}
|
|
486
|
-
pathEquals(a, b) {
|
|
487
|
-
if (!a || !b) {
|
|
488
|
-
return false;
|
|
489
|
-
}
|
|
490
|
-
return this.normalizePath(a) === this.normalizePath(b);
|
|
491
|
-
}
|
|
492
|
-
isChildPath(child, parent) {
|
|
493
|
-
if (!child || !parent) {
|
|
494
|
-
return false;
|
|
495
|
-
}
|
|
496
|
-
const normalizedChild = this.normalizePath(child);
|
|
497
|
-
const normalizedParent = this.normalizePath(parent);
|
|
498
|
-
return normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
|
|
499
|
-
}
|
|
500
357
|
/**
|
|
501
358
|
* Extract meaningful text from a user message content.
|
|
502
359
|
* Handles string and array formats, skill command expansion, and noise filtering.
|
|
@@ -563,19 +420,6 @@ class ClaudeCodeAdapter {
|
|
|
563
420
|
isMetadataEntryType(type) {
|
|
564
421
|
return type === 'last-prompt' || type === 'file-history-snapshot';
|
|
565
422
|
}
|
|
566
|
-
normalizePath(value) {
|
|
567
|
-
const resolved = path.resolve(value);
|
|
568
|
-
if (resolved.length > 1 && resolved.endsWith(path.sep)) {
|
|
569
|
-
return resolved.slice(0, -1);
|
|
570
|
-
}
|
|
571
|
-
return resolved;
|
|
572
|
-
}
|
|
573
423
|
}
|
|
574
424
|
exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
|
|
575
|
-
/** Limit session parsing per run to keep list latency bounded. */
|
|
576
|
-
ClaudeCodeAdapter.MIN_SESSION_SCAN = 12;
|
|
577
|
-
ClaudeCodeAdapter.MAX_SESSION_SCAN = 40;
|
|
578
|
-
ClaudeCodeAdapter.SESSION_SCAN_MULTIPLIER = 4;
|
|
579
|
-
/** Matching tolerance between process start time and session start time. */
|
|
580
|
-
ClaudeCodeAdapter.PROCESS_SESSION_TIME_TOLERANCE_MS = 2 * 60 * 1000;
|
|
581
425
|
//# sourceMappingURL=ClaudeCodeAdapter.js.map
|