@ai-devkit/agent-manager 0.17.0 → 0.18.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/CodexAdapter.test.js +80 -15
- package/dist/__tests__/adapters/CodexAdapter.test.js.map +1 -1
- package/dist/__tests__/adapters/PiAdapter.test.js +590 -0
- package/dist/__tests__/adapters/PiAdapter.test.js.map +1 -0
- package/dist/adapters/AgentAdapter.d.ts +1 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/CodexAdapter.d.ts +4 -0
- package/dist/adapters/CodexAdapter.d.ts.map +1 -1
- package/dist/adapters/CodexAdapter.js +38 -1
- package/dist/adapters/CodexAdapter.js.map +1 -1
- package/dist/adapters/PiAdapter.d.ts +62 -0
- package/dist/adapters/PiAdapter.d.ts.map +1 -0
- package/dist/adapters/PiAdapter.js +450 -0
- package/dist/adapters/PiAdapter.js.map +1 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters/CodexAdapter.test.ts +64 -13
- package/src/__tests__/adapters/PiAdapter.test.ts +435 -0
- package/src/adapters/AgentAdapter.ts +1 -1
- package/src/adapters/CodexAdapter.ts +51 -1
- package/src/adapters/PiAdapter.ts +597 -0
- package/src/adapters/index.ts +1 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects running Pi agents by:
|
|
5
|
+
* 1. Finding running Pi processes
|
|
6
|
+
* 2. Matching exact PID-to-session metadata from ~/.pi/agent/sessions.json
|
|
7
|
+
* 3. Falling back to shared process/session matching over Pi JSONL session files
|
|
8
|
+
* 4. Parsing Pi JSONL entries defensively for summary and conversation output
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import type {
|
|
14
|
+
AgentAdapter,
|
|
15
|
+
AgentInfo,
|
|
16
|
+
ProcessInfo,
|
|
17
|
+
ConversationMessage,
|
|
18
|
+
SessionSummary,
|
|
19
|
+
ListSessionsOptions,
|
|
20
|
+
} from './AgentAdapter.js';
|
|
21
|
+
import { AgentStatus } from './AgentAdapter.js';
|
|
22
|
+
import { listAgentProcesses, enrichProcesses } from '../utils/process.js';
|
|
23
|
+
import { isDirectory, safeReadFile, safeReaddir, safeStat } from '../utils/session.js';
|
|
24
|
+
import type { SessionFile } from '../utils/session.js';
|
|
25
|
+
import { matchProcessesToSessions, generateAgentName } from '../utils/matching.js';
|
|
26
|
+
import { AgentRegistry } from '../utils/AgentRegistry.js';
|
|
27
|
+
|
|
28
|
+
interface PiSession {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
projectPath: string;
|
|
31
|
+
summary: string;
|
|
32
|
+
sessionStart: Date;
|
|
33
|
+
lastActive: Date;
|
|
34
|
+
lastRole?: ConversationMessage['role'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PiLine {
|
|
38
|
+
timestamp?: string;
|
|
39
|
+
role?: string;
|
|
40
|
+
type?: string;
|
|
41
|
+
content?: unknown;
|
|
42
|
+
text?: unknown;
|
|
43
|
+
message?: unknown;
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
session_id?: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
cwd?: string;
|
|
48
|
+
projectPath?: string;
|
|
49
|
+
project_path?: string;
|
|
50
|
+
payload?: Record<string, unknown>;
|
|
51
|
+
data?: Record<string, unknown>;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type PiRecord = Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
interface TrackerMatch {
|
|
58
|
+
process: ProcessInfo;
|
|
59
|
+
filePath: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface TrackerAgentResult {
|
|
63
|
+
agents: AgentInfo[];
|
|
64
|
+
fallback: ProcessInfo[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class PiAdapter implements AgentAdapter {
|
|
68
|
+
readonly type = 'pi' as const;
|
|
69
|
+
|
|
70
|
+
private static readonly IDLE_THRESHOLD_MINUTES = 5;
|
|
71
|
+
|
|
72
|
+
private piAgentDir: string;
|
|
73
|
+
private piSessionsDir: string;
|
|
74
|
+
private trackerPath: string;
|
|
75
|
+
private registry: AgentRegistry;
|
|
76
|
+
|
|
77
|
+
constructor(registry: AgentRegistry = AgentRegistry.default()) {
|
|
78
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
79
|
+
this.piAgentDir = path.join(homeDir, '.pi', 'agent');
|
|
80
|
+
this.piSessionsDir = path.join(this.piAgentDir, 'sessions');
|
|
81
|
+
this.trackerPath = path.join(this.piAgentDir, 'sessions.json');
|
|
82
|
+
this.registry = registry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
canHandle(processInfo: ProcessInfo): boolean {
|
|
86
|
+
return this.isPiExecutable(processInfo.command);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async detectAgents(): Promise<AgentInfo[]> {
|
|
90
|
+
const processes = enrichProcesses(this.listPiProcesses());
|
|
91
|
+
if (processes.length === 0) return [];
|
|
92
|
+
|
|
93
|
+
const { cachedAgents, remaining } = this.tryRegistryCache(processes);
|
|
94
|
+
if (remaining.length === 0) return cachedAgents;
|
|
95
|
+
|
|
96
|
+
const trackerResult = this.mapTrackerMatches(remaining);
|
|
97
|
+
const fallbackAgents = this.mapFallbackMatches(trackerResult.fallback);
|
|
98
|
+
|
|
99
|
+
return [
|
|
100
|
+
...cachedAgents,
|
|
101
|
+
...trackerResult.agents,
|
|
102
|
+
...fallbackAgents,
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private mapTrackerMatches(processes: ProcessInfo[]): TrackerAgentResult {
|
|
107
|
+
const { matches: trackerMatches, fallback } = this.matchFromTracker(processes);
|
|
108
|
+
const agents: AgentInfo[] = [];
|
|
109
|
+
|
|
110
|
+
for (const match of trackerMatches) {
|
|
111
|
+
const session = this.parseSession(match.filePath, match.process.cwd);
|
|
112
|
+
if (session) {
|
|
113
|
+
agents.push(this.mapSessionToAgent(session, match.process, match.filePath));
|
|
114
|
+
} else {
|
|
115
|
+
fallback.push(match.process);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { agents, fallback };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private mapFallbackMatches(processes: ProcessInfo[]): AgentInfo[] {
|
|
123
|
+
if (processes.length === 0) return [];
|
|
124
|
+
|
|
125
|
+
const sessions = this.discoverSessions(processes);
|
|
126
|
+
if (sessions.length === 0) {
|
|
127
|
+
return processes.map((p) => this.mapProcessOnlyAgent(p));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const matches = matchProcessesToSessions(processes, sessions);
|
|
131
|
+
const matchedPids = new Set(matches.map((m) => m.process.pid));
|
|
132
|
+
const agents: AgentInfo[] = [];
|
|
133
|
+
|
|
134
|
+
for (const match of matches) {
|
|
135
|
+
const session = this.parseSession(match.session.filePath, match.process.cwd);
|
|
136
|
+
if (session) {
|
|
137
|
+
agents.push(this.mapSessionToAgent(session, match.process, match.session.filePath));
|
|
138
|
+
} else {
|
|
139
|
+
matchedPids.delete(match.process.pid);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const proc of processes) {
|
|
144
|
+
if (!matchedPids.has(proc.pid)) {
|
|
145
|
+
agents.push(this.mapProcessOnlyAgent(proc));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return agents;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private listPiProcesses(): ProcessInfo[] {
|
|
153
|
+
const byPid = new Map<number, ProcessInfo>();
|
|
154
|
+
for (const proc of listAgentProcesses('pi')) {
|
|
155
|
+
if (this.canHandle(proc)) byPid.set(proc.pid, proc);
|
|
156
|
+
}
|
|
157
|
+
for (const proc of listAgentProcesses('node')) {
|
|
158
|
+
if (this.canHandle(proc)) byPid.set(proc.pid, proc);
|
|
159
|
+
}
|
|
160
|
+
return Array.from(byPid.values());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private tryRegistryCache(processes: ProcessInfo[]): {
|
|
164
|
+
cachedAgents: AgentInfo[];
|
|
165
|
+
remaining: ProcessInfo[];
|
|
166
|
+
} {
|
|
167
|
+
const cachedAgents: AgentInfo[] = [];
|
|
168
|
+
const remaining: ProcessInfo[] = [];
|
|
169
|
+
const byPid = new Map(this.registry.list().map((e) => [e.pid, e]));
|
|
170
|
+
|
|
171
|
+
for (const proc of processes) {
|
|
172
|
+
const entry = byPid.get(proc.pid);
|
|
173
|
+
if (
|
|
174
|
+
!entry ||
|
|
175
|
+
entry.type !== this.type ||
|
|
176
|
+
!entry.sessionFilePath ||
|
|
177
|
+
!fs.existsSync(entry.sessionFilePath)
|
|
178
|
+
) {
|
|
179
|
+
remaining.push(proc);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const session = this.parseSession(entry.sessionFilePath, proc.cwd);
|
|
184
|
+
if (!session) {
|
|
185
|
+
remaining.push(proc);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
cachedAgents.push(this.mapSessionToAgent(session, proc, entry.sessionFilePath));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { cachedAgents, remaining };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private matchFromTracker(processes: ProcessInfo[]): {
|
|
196
|
+
matches: TrackerMatch[];
|
|
197
|
+
fallback: ProcessInfo[];
|
|
198
|
+
} {
|
|
199
|
+
const tracker = this.readTracker();
|
|
200
|
+
if (tracker.size === 0) return { matches: [], fallback: processes };
|
|
201
|
+
|
|
202
|
+
const matches: TrackerMatch[] = [];
|
|
203
|
+
const fallback: ProcessInfo[] = [];
|
|
204
|
+
|
|
205
|
+
for (const proc of processes) {
|
|
206
|
+
const filePath = tracker.get(proc.pid);
|
|
207
|
+
if (!filePath || !this.isTrustedSessionPath(filePath) || !fs.existsSync(filePath)) {
|
|
208
|
+
fallback.push(proc);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
matches.push({ process: proc, filePath });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { matches, fallback };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private readTracker(): Map<number, string> {
|
|
218
|
+
const content = safeReadFile(this.trackerPath);
|
|
219
|
+
if (content === undefined) return new Map();
|
|
220
|
+
|
|
221
|
+
let parsed: unknown;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(content);
|
|
224
|
+
} catch {
|
|
225
|
+
return new Map();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return new Map();
|
|
229
|
+
|
|
230
|
+
const map = new Map<number, string>();
|
|
231
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
232
|
+
const keyPid = this.toPid(key);
|
|
233
|
+
if (keyPid !== null && typeof value === 'string' && value) {
|
|
234
|
+
map.set(keyPid, value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return map;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private toPid(value: unknown): number | null {
|
|
241
|
+
if (typeof value === 'number' && Number.isInteger(value) && value > 0) return value;
|
|
242
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value)) return null;
|
|
243
|
+
const parsed = Number(value);
|
|
244
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private isTrustedSessionPath(filePath: string): boolean {
|
|
248
|
+
const resolvedRoot = path.resolve(this.piSessionsDir);
|
|
249
|
+
const resolvedPath = path.resolve(filePath);
|
|
250
|
+
return resolvedPath === resolvedRoot || resolvedPath.startsWith(`${resolvedRoot}${path.sep}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private discoverSessions(processes: ProcessInfo[] = []): SessionFile[] {
|
|
254
|
+
if (!isDirectory(this.piSessionsDir)) return [];
|
|
255
|
+
|
|
256
|
+
const cwdByProjectDir = this.buildProjectDirCwdMap(processes);
|
|
257
|
+
const sessions: SessionFile[] = [];
|
|
258
|
+
for (const filePath of this.collectJsonlFiles(this.piSessionsDir)) {
|
|
259
|
+
const stat = safeStat(filePath);
|
|
260
|
+
if (!stat) continue;
|
|
261
|
+
|
|
262
|
+
const session = this.parseSession(filePath);
|
|
263
|
+
const sessionId = session?.sessionId || this.sessionIdFromFile(filePath);
|
|
264
|
+
const projectDir = path.dirname(filePath);
|
|
265
|
+
sessions.push({
|
|
266
|
+
sessionId,
|
|
267
|
+
filePath,
|
|
268
|
+
projectDir,
|
|
269
|
+
birthtimeMs: stat.birthtimeMs || stat.mtimeMs,
|
|
270
|
+
resolvedCwd: session?.projectPath || cwdByProjectDir.get(path.basename(projectDir)) || '',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return sessions;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private buildProjectDirCwdMap(processes: ProcessInfo[]): Map<string, string> {
|
|
278
|
+
const map = new Map<string, string>();
|
|
279
|
+
for (const proc of processes) {
|
|
280
|
+
if (!proc.cwd) continue;
|
|
281
|
+
map.set(this.encodeProjectDir(proc.cwd), proc.cwd);
|
|
282
|
+
}
|
|
283
|
+
return map;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private collectJsonlFiles(dir: string): string[] {
|
|
287
|
+
const files: string[] = [];
|
|
288
|
+
for (const entry of safeReaddir(dir)) {
|
|
289
|
+
const fullPath = path.join(dir, entry);
|
|
290
|
+
const stat = safeStat(fullPath);
|
|
291
|
+
if (!stat) continue;
|
|
292
|
+
if (stat.isDirectory()) {
|
|
293
|
+
files.push(...this.collectJsonlFiles(fullPath));
|
|
294
|
+
} else if (stat.isFile() && entry.endsWith('.jsonl')) {
|
|
295
|
+
files.push(fullPath);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return files;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private parseSession(filePath: string, fallbackCwd = ''): PiSession | null {
|
|
302
|
+
const entries = this.readJsonl(filePath);
|
|
303
|
+
if (entries.length === 0) return null;
|
|
304
|
+
return this.sessionFromEntries(entries, filePath, fallbackCwd);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private sessionFromEntries(entries: PiLine[], filePath: string, fallbackCwd = ''): PiSession {
|
|
308
|
+
const stat = safeStat(filePath);
|
|
309
|
+
const timestamps = entries
|
|
310
|
+
.map((entry) => this.parseTimestamp(this.entryTimestamp(entry)))
|
|
311
|
+
.filter((value): value is Date => value !== null);
|
|
312
|
+
|
|
313
|
+
const sessionStart = timestamps[0] ?? stat?.birthtime ?? stat?.mtime ?? new Date();
|
|
314
|
+
const lastActive = timestamps[timestamps.length - 1] ?? stat?.mtime ?? sessionStart;
|
|
315
|
+
const messages = this.entriesToMessages(entries, true);
|
|
316
|
+
const lastUser = [...messages].reverse().find((msg) => msg.role === 'user');
|
|
317
|
+
const lastMessage = messages[messages.length - 1];
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
sessionId: this.sessionIdFromEntries(entries) || this.sessionIdFromFile(filePath),
|
|
321
|
+
projectPath: this.cwdFromEntries(entries) || fallbackCwd,
|
|
322
|
+
summary: lastUser?.content ? this.truncate(lastUser.content, 120) : 'Pi session active',
|
|
323
|
+
sessionStart,
|
|
324
|
+
lastActive,
|
|
325
|
+
lastRole: lastMessage?.role,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private readJsonl(filePath: string): PiLine[] {
|
|
330
|
+
const content = safeReadFile(filePath);
|
|
331
|
+
if (content === undefined) return [];
|
|
332
|
+
|
|
333
|
+
const entries: PiLine[] = [];
|
|
334
|
+
for (const line of content.split(/\r?\n/)) {
|
|
335
|
+
const trimmed = line.trim();
|
|
336
|
+
if (!trimmed) continue;
|
|
337
|
+
try {
|
|
338
|
+
const parsed = JSON.parse(trimmed);
|
|
339
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
340
|
+
entries.push(parsed as PiLine);
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return entries;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private entryToMessage(entry: PiLine, includeSystem: boolean): ConversationMessage | null {
|
|
350
|
+
const role = this.entryRole(entry);
|
|
351
|
+
if (!role) return null;
|
|
352
|
+
if (role === 'system' && !includeSystem) return null;
|
|
353
|
+
|
|
354
|
+
const content = this.entryContent(entry).trim();
|
|
355
|
+
if (!content) return null;
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
role,
|
|
359
|
+
content,
|
|
360
|
+
timestamp: this.entryTimestamp(entry),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private entryRole(entry: PiLine): ConversationMessage['role'] | null {
|
|
365
|
+
const message = this.messageRecord(entry);
|
|
366
|
+
const raw = this.firstString(
|
|
367
|
+
entry.role,
|
|
368
|
+
message?.role,
|
|
369
|
+
this.roleLikeType(entry.type),
|
|
370
|
+
entry.payload?.role,
|
|
371
|
+
entry.payload?.type,
|
|
372
|
+
entry.data?.role,
|
|
373
|
+
entry.data?.type,
|
|
374
|
+
);
|
|
375
|
+
if (!raw) return null;
|
|
376
|
+
const normalized = raw.toLowerCase();
|
|
377
|
+
if (normalized === 'user' || normalized === 'human') return 'user';
|
|
378
|
+
if (normalized === 'assistant' || normalized === 'ai' || normalized === 'pi') return 'assistant';
|
|
379
|
+
if (normalized === 'system') return 'system';
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private entryContent(entry: PiLine): string {
|
|
384
|
+
const message = this.messageRecord(entry);
|
|
385
|
+
const candidates = [
|
|
386
|
+
entry.content,
|
|
387
|
+
entry.text,
|
|
388
|
+
message?.content,
|
|
389
|
+
message?.text,
|
|
390
|
+
message?.message,
|
|
391
|
+
entry.message,
|
|
392
|
+
entry.payload?.content,
|
|
393
|
+
entry.payload?.text,
|
|
394
|
+
entry.payload?.message,
|
|
395
|
+
entry.data?.content,
|
|
396
|
+
entry.data?.text,
|
|
397
|
+
entry.data?.message,
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
for (const candidate of candidates) {
|
|
401
|
+
const text = this.contentToString(candidate);
|
|
402
|
+
if (text) return text;
|
|
403
|
+
}
|
|
404
|
+
return '';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private contentToString(value: unknown): string {
|
|
408
|
+
if (typeof value === 'string') return value;
|
|
409
|
+
if (Array.isArray(value)) {
|
|
410
|
+
return value.map((item) => this.contentToString(item)).filter(Boolean).join('');
|
|
411
|
+
}
|
|
412
|
+
if (!value || typeof value !== 'object') return '';
|
|
413
|
+
|
|
414
|
+
const record = value as Record<string, unknown>;
|
|
415
|
+
return this.contentToString(record.content ?? record.text ?? record.value);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private asRecord(value: unknown): PiRecord | null {
|
|
419
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
420
|
+
return value as PiRecord;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private messageRecord(entry: PiLine): PiRecord | null {
|
|
424
|
+
return this.asRecord(entry.message);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private roleLikeType(value: unknown): string | undefined {
|
|
428
|
+
if (typeof value !== 'string') return undefined;
|
|
429
|
+
const normalized = value.toLowerCase();
|
|
430
|
+
if (['user', 'human', 'assistant', 'ai', 'pi', 'system'].includes(normalized)) {
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private entryTimestamp(entry: PiLine): string | undefined {
|
|
437
|
+
return this.firstString(
|
|
438
|
+
entry.timestamp,
|
|
439
|
+
entry.payload?.timestamp,
|
|
440
|
+
entry.data?.timestamp,
|
|
441
|
+
entry.createdAt,
|
|
442
|
+
entry.created_at,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private sessionIdFromEntries(entries: PiLine[]): string | null {
|
|
447
|
+
for (const entry of entries) {
|
|
448
|
+
const sessionId = this.firstString(
|
|
449
|
+
entry.sessionId,
|
|
450
|
+
entry.session_id,
|
|
451
|
+
entry.id,
|
|
452
|
+
entry.payload?.sessionId,
|
|
453
|
+
entry.payload?.session_id,
|
|
454
|
+
entry.payload?.id,
|
|
455
|
+
entry.data?.sessionId,
|
|
456
|
+
entry.data?.session_id,
|
|
457
|
+
entry.data?.id,
|
|
458
|
+
);
|
|
459
|
+
if (sessionId) return sessionId;
|
|
460
|
+
}
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private cwdFromEntries(entries: PiLine[]): string {
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
const cwd = this.firstString(
|
|
467
|
+
entry.cwd,
|
|
468
|
+
entry.projectPath,
|
|
469
|
+
entry.project_path,
|
|
470
|
+
entry.payload?.cwd,
|
|
471
|
+
entry.payload?.projectPath,
|
|
472
|
+
entry.payload?.project_path,
|
|
473
|
+
entry.data?.cwd,
|
|
474
|
+
entry.data?.projectPath,
|
|
475
|
+
entry.data?.project_path,
|
|
476
|
+
);
|
|
477
|
+
if (cwd) return cwd;
|
|
478
|
+
}
|
|
479
|
+
return '';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private sessionIdFromFile(filePath: string): string {
|
|
483
|
+
const base = path.basename(filePath, '.jsonl');
|
|
484
|
+
const underscore = base.lastIndexOf('_');
|
|
485
|
+
return underscore >= 0 ? base.slice(underscore + 1) : base;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private encodeProjectDir(cwd: string): string {
|
|
489
|
+
const normalized = path.resolve(cwd);
|
|
490
|
+
return `--${normalized.replace(/^\//, '').replace(/\//g, '-')}--`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private firstString(...values: unknown[]): string | undefined {
|
|
494
|
+
for (const value of values) {
|
|
495
|
+
if (typeof value === 'string' && value) return value;
|
|
496
|
+
}
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private parseTimestamp(value?: string): Date | null {
|
|
501
|
+
if (!value) return null;
|
|
502
|
+
const timestamp = new Date(value);
|
|
503
|
+
return Number.isNaN(timestamp.getTime()) ? null : timestamp;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private mapSessionToAgent(session: PiSession, processInfo: ProcessInfo, filePath: string): AgentInfo {
|
|
507
|
+
const projectPath = session.projectPath || processInfo.cwd || '';
|
|
508
|
+
return {
|
|
509
|
+
name: generateAgentName(projectPath, processInfo.pid),
|
|
510
|
+
type: this.type,
|
|
511
|
+
status: this.determineStatus(session),
|
|
512
|
+
summary: session.summary || 'Pi session active',
|
|
513
|
+
pid: processInfo.pid,
|
|
514
|
+
projectPath,
|
|
515
|
+
sessionId: session.sessionId,
|
|
516
|
+
lastActive: session.lastActive,
|
|
517
|
+
sessionFilePath: filePath,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private mapProcessOnlyAgent(processInfo: ProcessInfo): AgentInfo {
|
|
522
|
+
return {
|
|
523
|
+
name: generateAgentName(processInfo.cwd || '', processInfo.pid),
|
|
524
|
+
type: this.type,
|
|
525
|
+
status: AgentStatus.RUNNING,
|
|
526
|
+
summary: 'Pi process running',
|
|
527
|
+
pid: processInfo.pid,
|
|
528
|
+
projectPath: processInfo.cwd || '',
|
|
529
|
+
sessionId: `pid-${processInfo.pid}`,
|
|
530
|
+
lastActive: new Date(),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private determineStatus(session: PiSession): AgentStatus {
|
|
535
|
+
const diffMs = Date.now() - session.lastActive.getTime();
|
|
536
|
+
const diffMinutes = diffMs / 60000;
|
|
537
|
+
|
|
538
|
+
if (diffMinutes > PiAdapter.IDLE_THRESHOLD_MINUTES) return AgentStatus.IDLE;
|
|
539
|
+
if (session.lastRole === 'assistant') return AgentStatus.WAITING;
|
|
540
|
+
return AgentStatus.RUNNING;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private truncate(value: string, maxLength: number): string {
|
|
544
|
+
if (value.length <= maxLength) return value;
|
|
545
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private isPiExecutable(command: string): boolean {
|
|
549
|
+
for (const token of command.trim().split(/\s+/)) {
|
|
550
|
+
const base = path.basename(token).toLowerCase();
|
|
551
|
+
if (base === 'pi' || base === 'pi.exe' || base === 'pi.js') return true;
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] {
|
|
557
|
+
const includeSystem = options?.verbose ?? false;
|
|
558
|
+
return this.entriesToMessages(this.readJsonl(sessionFilePath), includeSystem);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private entriesToMessages(entries: PiLine[], includeSystem: boolean): ConversationMessage[] {
|
|
562
|
+
return entries
|
|
563
|
+
.map((entry) => this.entryToMessage(entry, includeSystem))
|
|
564
|
+
.filter((msg): msg is ConversationMessage => msg !== null);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]> {
|
|
568
|
+
if (!isDirectory(this.piSessionsDir)) return [];
|
|
569
|
+
|
|
570
|
+
const summaries: SessionSummary[] = [];
|
|
571
|
+
for (const filePath of this.collectJsonlFiles(this.piSessionsDir)) {
|
|
572
|
+
const summary = this.fileToSessionSummary(filePath);
|
|
573
|
+
if (!summary) continue;
|
|
574
|
+
if (opts?.cwd !== undefined && summary.cwd !== opts.cwd) continue;
|
|
575
|
+
summaries.push(summary);
|
|
576
|
+
}
|
|
577
|
+
return summaries;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
private fileToSessionSummary(filePath: string): SessionSummary | null {
|
|
581
|
+
const entries = this.readJsonl(filePath);
|
|
582
|
+
if (entries.length === 0) return null;
|
|
583
|
+
|
|
584
|
+
const session = this.sessionFromEntries(entries, filePath);
|
|
585
|
+
const firstUserMessage = this.entriesToMessages(entries, false)
|
|
586
|
+
.find((msg) => msg.role === 'user')?.content ?? '';
|
|
587
|
+
return {
|
|
588
|
+
type: this.type,
|
|
589
|
+
sessionId: session.sessionId,
|
|
590
|
+
cwd: session.projectPath,
|
|
591
|
+
firstUserMessage,
|
|
592
|
+
lastActive: session.lastActive,
|
|
593
|
+
startedAt: session.sessionStart,
|
|
594
|
+
sessionFilePath: filePath,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
package/src/adapters/index.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { CodexAdapter } from './CodexAdapter.js';
|
|
|
3
3
|
export { CopilotAdapter } from './CopilotAdapter.js';
|
|
4
4
|
export { GeminiCliAdapter } from './GeminiCliAdapter.js';
|
|
5
5
|
export { OpenCodeAdapter } from './OpenCodeAdapter.js';
|
|
6
|
+
export { PiAdapter } from './PiAdapter.js';
|
|
6
7
|
export { AgentStatus } from './AgentAdapter.js';
|
|
7
8
|
export type { AgentAdapter, AgentType, AgentInfo, ProcessInfo } from './AgentAdapter.js';
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { CodexAdapter } from './adapters/CodexAdapter.js';
|
|
|
5
5
|
export { CopilotAdapter } from './adapters/CopilotAdapter.js';
|
|
6
6
|
export { GeminiCliAdapter } from './adapters/GeminiCliAdapter.js';
|
|
7
7
|
export { OpenCodeAdapter } from './adapters/OpenCodeAdapter.js';
|
|
8
|
+
export { PiAdapter } from './adapters/PiAdapter.js';
|
|
8
9
|
export { AgentStatus } from './adapters/AgentAdapter.js';
|
|
9
10
|
export type {
|
|
10
11
|
AgentAdapter,
|