@orgloop/agentctl 1.2.0 → 1.3.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/claude-code.d.ts +3 -1
- package/dist/adapters/claude-code.js +65 -3
- package/dist/adapters/codex.d.ts +3 -1
- package/dist/adapters/codex.js +48 -3
- package/dist/adapters/openclaw.d.ts +3 -1
- package/dist/adapters/openclaw.js +61 -4
- package/dist/adapters/opencode.d.ts +3 -1
- package/dist/adapters/opencode.js +70 -3
- package/dist/adapters/pi-rust.d.ts +3 -1
- package/dist/adapters/pi-rust.js +87 -3
- package/dist/adapters/pi.d.ts +3 -1
- package/dist/adapters/pi.js +51 -3
- package/dist/cli.js +110 -97
- package/dist/core/types.d.ts +26 -2
- package/dist/daemon/fuse-engine.d.ts +13 -10
- package/dist/daemon/fuse-engine.js +69 -46
- package/dist/daemon/metrics.d.ts +2 -3
- package/dist/daemon/metrics.js +4 -7
- package/dist/daemon/server.js +136 -21
- package/dist/daemon/session-tracker.d.ts +13 -0
- package/dist/daemon/session-tracker.js +102 -10
- package/dist/daemon/state.d.ts +12 -2
- package/dist/hooks.d.ts +1 -1
- package/dist/utils/daemon-env.d.ts +16 -0
- package/dist/utils/daemon-env.js +85 -0
- package/dist/utils/resolve-binary.d.ts +14 -0
- package/dist/utils/resolve-binary.js +66 -0
- package/package.json +1 -1
- package/dist/merge.d.ts +0 -24
- package/dist/merge.js +0 -65
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
1
|
+
import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
2
|
export interface PidInfo {
|
|
3
3
|
pid: number;
|
|
4
4
|
cwd: string;
|
|
@@ -38,6 +38,8 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
38
38
|
private readonly getPids;
|
|
39
39
|
private readonly isProcessAlive;
|
|
40
40
|
constructor(opts?: ClaudeCodeAdapterOpts);
|
|
41
|
+
discover(): Promise<DiscoveredSession[]>;
|
|
42
|
+
isAlive(sessionId: string): Promise<boolean>;
|
|
41
43
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
42
44
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
43
45
|
status(sessionId: string): Promise<AgentSession>;
|
|
@@ -5,7 +5,9 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { buildSpawnEnv } from "../utils/daemon-env.js";
|
|
8
9
|
import { readHead, readTail } from "../utils/partial-read.js";
|
|
10
|
+
import { resolveBinaryPath } from "../utils/resolve-binary.js";
|
|
9
11
|
const execFileAsync = promisify(execFile);
|
|
10
12
|
const DEFAULT_CLAUDE_DIR = path.join(os.homedir(), ".claude");
|
|
11
13
|
// Default: only show stopped sessions from the last 7 days
|
|
@@ -30,6 +32,57 @@ export class ClaudeCodeAdapter {
|
|
|
30
32
|
this.getPids = opts?.getPids || getClaudePids;
|
|
31
33
|
this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
|
|
32
34
|
}
|
|
35
|
+
async discover() {
|
|
36
|
+
const runningPids = await this.getPids();
|
|
37
|
+
const results = [];
|
|
38
|
+
let projectDirs;
|
|
39
|
+
try {
|
|
40
|
+
projectDirs = await fs.readdir(this.projectsDir);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
for (const projDir of projectDirs) {
|
|
46
|
+
const projPath = path.join(this.projectsDir, projDir);
|
|
47
|
+
const stat = await fs.stat(projPath).catch(() => null);
|
|
48
|
+
if (!stat?.isDirectory())
|
|
49
|
+
continue;
|
|
50
|
+
const entries = await this.getEntriesForProject(projPath, projDir);
|
|
51
|
+
for (const { entry, index } of entries) {
|
|
52
|
+
if (entry.isSidechain)
|
|
53
|
+
continue;
|
|
54
|
+
const isRunning = await this.isSessionRunning(entry, index, runningPids);
|
|
55
|
+
const { model, tokens } = await this.parseSessionTail(entry.fullPath);
|
|
56
|
+
results.push({
|
|
57
|
+
id: entry.sessionId,
|
|
58
|
+
status: isRunning ? "running" : "stopped",
|
|
59
|
+
adapter: this.id,
|
|
60
|
+
cwd: index.originalPath || entry.projectPath,
|
|
61
|
+
model,
|
|
62
|
+
startedAt: new Date(entry.created),
|
|
63
|
+
stoppedAt: isRunning ? undefined : new Date(entry.modified),
|
|
64
|
+
pid: isRunning
|
|
65
|
+
? await this.findMatchingPid(entry, index, runningPids)
|
|
66
|
+
: undefined,
|
|
67
|
+
prompt: entry.firstPrompt?.slice(0, 200),
|
|
68
|
+
tokens,
|
|
69
|
+
nativeMetadata: {
|
|
70
|
+
projectDir: index.originalPath || entry.projectPath,
|
|
71
|
+
gitBranch: entry.gitBranch,
|
|
72
|
+
messageCount: entry.messageCount,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
async isAlive(sessionId) {
|
|
80
|
+
const runningPids = await this.getPids();
|
|
81
|
+
const entry = await this.findIndexEntry(sessionId);
|
|
82
|
+
if (!entry)
|
|
83
|
+
return false;
|
|
84
|
+
return this.isSessionRunning(entry.entry, entry.index, runningPids);
|
|
85
|
+
}
|
|
33
86
|
async list(opts) {
|
|
34
87
|
const runningPids = await this.getPids();
|
|
35
88
|
const sessions = [];
|
|
@@ -123,7 +176,7 @@ export class ClaudeCodeAdapter {
|
|
|
123
176
|
args.push("--model", opts.model);
|
|
124
177
|
}
|
|
125
178
|
args.push("-p", opts.prompt);
|
|
126
|
-
const env =
|
|
179
|
+
const env = buildSpawnEnv(undefined, opts.env);
|
|
127
180
|
const cwd = opts.cwd || process.cwd();
|
|
128
181
|
// Write stdout to a log file so we can extract the session ID
|
|
129
182
|
// without keeping a pipe open (which would prevent full detachment).
|
|
@@ -131,12 +184,17 @@ export class ClaudeCodeAdapter {
|
|
|
131
184
|
const logPath = path.join(this.sessionsMetaDir, `launch-${Date.now()}.log`);
|
|
132
185
|
const logFd = await fs.open(logPath, "w");
|
|
133
186
|
// Capture stderr to the same log file for debugging launch failures
|
|
134
|
-
const
|
|
187
|
+
const claudePath = await resolveBinaryPath("claude");
|
|
188
|
+
const child = spawn(claudePath, args, {
|
|
135
189
|
cwd,
|
|
136
190
|
env,
|
|
137
191
|
stdio: ["ignore", logFd.fd, logFd.fd],
|
|
138
192
|
detached: true,
|
|
139
193
|
});
|
|
194
|
+
// Handle spawn errors (e.g. ENOENT) gracefully instead of crashing the daemon
|
|
195
|
+
child.on("error", (err) => {
|
|
196
|
+
console.error(`[claude-code] spawn error: ${err.message}`);
|
|
197
|
+
});
|
|
140
198
|
// Fully detach: child runs in its own process group.
|
|
141
199
|
// When the wrapper gets SIGTERM, the child keeps running.
|
|
142
200
|
child.unref();
|
|
@@ -250,11 +308,15 @@ export class ClaudeCodeAdapter {
|
|
|
250
308
|
];
|
|
251
309
|
const session = await this.status(sessionId).catch(() => null);
|
|
252
310
|
const cwd = session?.cwd || process.cwd();
|
|
253
|
-
const
|
|
311
|
+
const claudePath = await resolveBinaryPath("claude");
|
|
312
|
+
const child = spawn(claudePath, args, {
|
|
254
313
|
cwd,
|
|
255
314
|
stdio: ["pipe", "pipe", "pipe"],
|
|
256
315
|
detached: true,
|
|
257
316
|
});
|
|
317
|
+
child.on("error", (err) => {
|
|
318
|
+
console.error(`[claude-code] resume spawn error: ${err.message}`);
|
|
319
|
+
});
|
|
258
320
|
child.unref();
|
|
259
321
|
}
|
|
260
322
|
async *events() {
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
1
|
+
import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
2
|
export interface CodexPidInfo {
|
|
3
3
|
pid: number;
|
|
4
4
|
cwd: string;
|
|
@@ -34,6 +34,8 @@ export declare class CodexAdapter implements AgentAdapter {
|
|
|
34
34
|
private readonly getPids;
|
|
35
35
|
private readonly isProcessAlive;
|
|
36
36
|
constructor(opts?: CodexAdapterOpts);
|
|
37
|
+
discover(): Promise<DiscoveredSession[]>;
|
|
38
|
+
isAlive(sessionId: string): Promise<boolean>;
|
|
37
39
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
38
40
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
39
41
|
status(sessionId: string): Promise<AgentSession>;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -5,6 +5,8 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { buildSpawnEnv } from "../utils/daemon-env.js";
|
|
9
|
+
import { resolveBinaryPath } from "../utils/resolve-binary.js";
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
9
11
|
const DEFAULT_CODEX_DIR = path.join(os.homedir(), ".codex");
|
|
10
12
|
// Default: only show stopped sessions from the last 7 days
|
|
@@ -28,6 +30,41 @@ export class CodexAdapter {
|
|
|
28
30
|
this.getPids = opts?.getPids || getCodexPids;
|
|
29
31
|
this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
|
|
30
32
|
}
|
|
33
|
+
async discover() {
|
|
34
|
+
const runningPids = await this.getPids();
|
|
35
|
+
const sessionInfos = await this.discoverSessions();
|
|
36
|
+
const results = [];
|
|
37
|
+
for (const info of sessionInfos) {
|
|
38
|
+
const isRunning = this.isSessionRunning(info, runningPids);
|
|
39
|
+
const pid = isRunning
|
|
40
|
+
? this.findMatchingPid(info, runningPids)
|
|
41
|
+
: undefined;
|
|
42
|
+
results.push({
|
|
43
|
+
id: info.id,
|
|
44
|
+
status: isRunning ? "running" : "stopped",
|
|
45
|
+
adapter: this.id,
|
|
46
|
+
cwd: info.cwd,
|
|
47
|
+
model: info.model,
|
|
48
|
+
startedAt: info.created,
|
|
49
|
+
stoppedAt: isRunning ? undefined : info.modified,
|
|
50
|
+
pid,
|
|
51
|
+
prompt: info.firstPrompt,
|
|
52
|
+
tokens: info.tokens,
|
|
53
|
+
nativeMetadata: {
|
|
54
|
+
cliVersion: info.cliVersion,
|
|
55
|
+
lastMessage: info.lastMessage,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
async isAlive(sessionId) {
|
|
62
|
+
const runningPids = await this.getPids();
|
|
63
|
+
const info = await this.findSession(sessionId);
|
|
64
|
+
if (!info)
|
|
65
|
+
return false;
|
|
66
|
+
return this.isSessionRunning(info, runningPids);
|
|
67
|
+
}
|
|
31
68
|
async list(opts) {
|
|
32
69
|
const runningPids = await this.getPids();
|
|
33
70
|
const sessionInfos = await this.discoverSessions();
|
|
@@ -102,16 +139,20 @@ export class CodexAdapter {
|
|
|
102
139
|
const cwd = opts.cwd || process.cwd();
|
|
103
140
|
args.push("--cd", cwd);
|
|
104
141
|
args.push(opts.prompt);
|
|
105
|
-
const env =
|
|
142
|
+
const env = buildSpawnEnv(undefined, opts.env);
|
|
106
143
|
await fs.mkdir(this.sessionsMetaDir, { recursive: true });
|
|
107
144
|
const logPath = path.join(this.sessionsMetaDir, `launch-${Date.now()}.log`);
|
|
108
145
|
const logFd = await fs.open(logPath, "w");
|
|
109
|
-
const
|
|
146
|
+
const codexPath = await resolveBinaryPath("codex");
|
|
147
|
+
const child = spawn(codexPath, args, {
|
|
110
148
|
cwd,
|
|
111
149
|
env,
|
|
112
150
|
stdio: ["ignore", logFd.fd, "ignore"],
|
|
113
151
|
detached: true,
|
|
114
152
|
});
|
|
153
|
+
child.on("error", (err) => {
|
|
154
|
+
console.error(`[codex] spawn error: ${err.message}`);
|
|
155
|
+
});
|
|
115
156
|
child.unref();
|
|
116
157
|
const pid = child.pid;
|
|
117
158
|
const now = new Date();
|
|
@@ -219,11 +260,15 @@ export class CodexAdapter {
|
|
|
219
260
|
sessionId,
|
|
220
261
|
message,
|
|
221
262
|
];
|
|
222
|
-
const
|
|
263
|
+
const codexPath = await resolveBinaryPath("codex");
|
|
264
|
+
const child = spawn(codexPath, args, {
|
|
223
265
|
cwd,
|
|
224
266
|
stdio: ["pipe", "pipe", "pipe"],
|
|
225
267
|
detached: true,
|
|
226
268
|
});
|
|
269
|
+
child.on("error", (err) => {
|
|
270
|
+
console.error(`[codex] resume spawn error: ${err.message}`);
|
|
271
|
+
});
|
|
227
272
|
child.unref();
|
|
228
273
|
}
|
|
229
274
|
async *events() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
1
|
+
import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
2
|
export interface DeviceIdentity {
|
|
3
3
|
deviceId: string;
|
|
4
4
|
publicKeyPem: string;
|
|
@@ -122,6 +122,8 @@ export declare class OpenClawAdapter implements AgentAdapter {
|
|
|
122
122
|
private readonly rpcCall;
|
|
123
123
|
constructor(opts?: OpenClawAdapterOpts);
|
|
124
124
|
private tryLoadDeviceIdentity;
|
|
125
|
+
discover(): Promise<DiscoveredSession[]>;
|
|
126
|
+
isAlive(sessionId: string): Promise<boolean>;
|
|
125
127
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
126
128
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
127
129
|
status(sessionId: string): Promise<AgentSession>;
|
|
@@ -197,6 +197,62 @@ export class OpenClawAdapter {
|
|
|
197
197
|
return null;
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
+
async discover() {
|
|
201
|
+
if (!this.authToken)
|
|
202
|
+
return [];
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
result = (await this.rpcCall("sessions.list", {
|
|
206
|
+
includeDerivedTitles: true,
|
|
207
|
+
includeLastMessage: true,
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
return result.sessions.map((row) => {
|
|
214
|
+
const updatedAt = row.updatedAt ?? 0;
|
|
215
|
+
const model = row.model || result.defaults.model || undefined;
|
|
216
|
+
const input = row.inputTokens ?? 0;
|
|
217
|
+
const output = row.outputTokens ?? 0;
|
|
218
|
+
// Gateway is the source of truth: if a session is in the list, it's alive.
|
|
219
|
+
// Don't guess from timestamps — sessions can be idle for hours between messages.
|
|
220
|
+
return {
|
|
221
|
+
id: row.sessionId || row.key,
|
|
222
|
+
status: "running",
|
|
223
|
+
adapter: this.id,
|
|
224
|
+
model,
|
|
225
|
+
startedAt: updatedAt > 0 ? new Date(updatedAt) : new Date(),
|
|
226
|
+
prompt: row.derivedTitle || row.displayName || row.label,
|
|
227
|
+
tokens: input || output ? { in: input, out: output } : undefined,
|
|
228
|
+
nativeMetadata: {
|
|
229
|
+
key: row.key,
|
|
230
|
+
kind: row.kind,
|
|
231
|
+
channel: row.channel,
|
|
232
|
+
displayName: row.displayName,
|
|
233
|
+
modelProvider: row.modelProvider || result.defaults.modelProvider,
|
|
234
|
+
lastMessagePreview: row.lastMessagePreview,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async isAlive(sessionId) {
|
|
240
|
+
if (!this.authToken)
|
|
241
|
+
return false;
|
|
242
|
+
try {
|
|
243
|
+
const result = (await this.rpcCall("sessions.list", {
|
|
244
|
+
search: sessionId,
|
|
245
|
+
}));
|
|
246
|
+
// Gateway is the source of truth: if a session is in the list, it's alive.
|
|
247
|
+
return result.sessions.some((s) => s.sessionId === sessionId ||
|
|
248
|
+
s.key === sessionId ||
|
|
249
|
+
s.sessionId?.startsWith(sessionId) ||
|
|
250
|
+
s.key.startsWith(sessionId));
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
200
256
|
async list(opts) {
|
|
201
257
|
if (!this.authToken) {
|
|
202
258
|
console.warn("Warning: OPENCLAW_GATEWAY_TOKEN is not set — OpenClaw adapter cannot authenticate. " +
|
|
@@ -342,17 +398,18 @@ export class OpenClawAdapter {
|
|
|
342
398
|
}
|
|
343
399
|
// --- Private helpers ---
|
|
344
400
|
mapRowToSession(row, defaults) {
|
|
345
|
-
const now = Date.now();
|
|
346
401
|
const updatedAt = row.updatedAt ?? 0;
|
|
347
|
-
const ageMs = now - updatedAt;
|
|
348
|
-
|
|
402
|
+
const ageMs = Date.now() - updatedAt;
|
|
403
|
+
// Gateway is the source of truth: if a session is in the list, it's alive.
|
|
404
|
+
// Use "running" for recently active sessions, "idle" for quiet ones.
|
|
405
|
+
const status = updatedAt > 0 && ageMs < 5 * 60 * 1000 ? "running" : "idle";
|
|
349
406
|
const model = row.model || defaults.model || undefined;
|
|
350
407
|
const input = row.inputTokens ?? 0;
|
|
351
408
|
const output = row.outputTokens ?? 0;
|
|
352
409
|
return {
|
|
353
410
|
id: row.sessionId || row.key,
|
|
354
411
|
adapter: this.id,
|
|
355
|
-
status
|
|
412
|
+
status,
|
|
356
413
|
startedAt: updatedAt > 0 ? new Date(updatedAt) : new Date(),
|
|
357
414
|
cwd: undefined,
|
|
358
415
|
model,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
1
|
+
import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
2
|
export interface PidInfo {
|
|
3
3
|
pid: number;
|
|
4
4
|
cwd: string;
|
|
@@ -95,6 +95,8 @@ export declare class OpenCodeAdapter implements AgentAdapter {
|
|
|
95
95
|
private readonly getPids;
|
|
96
96
|
private readonly isProcessAlive;
|
|
97
97
|
constructor(opts?: OpenCodeAdapterOpts);
|
|
98
|
+
discover(): Promise<DiscoveredSession[]>;
|
|
99
|
+
isAlive(sessionId: string): Promise<boolean>;
|
|
98
100
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
99
101
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
100
102
|
status(sessionId: string): Promise<AgentSession>;
|
|
@@ -5,6 +5,8 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { buildSpawnEnv } from "../utils/daemon-env.js";
|
|
9
|
+
import { resolveBinaryPath } from "../utils/resolve-binary.js";
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
9
11
|
const DEFAULT_STORAGE_DIR = path.join(os.homedir(), ".local", "share", "opencode", "storage");
|
|
10
12
|
// Default: only show stopped sessions from the last 7 days
|
|
@@ -37,6 +39,63 @@ export class OpenCodeAdapter {
|
|
|
37
39
|
this.getPids = opts?.getPids || getOpenCodePids;
|
|
38
40
|
this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
|
|
39
41
|
}
|
|
42
|
+
async discover() {
|
|
43
|
+
const runningPids = await this.getPids();
|
|
44
|
+
const results = [];
|
|
45
|
+
let projectDirs;
|
|
46
|
+
try {
|
|
47
|
+
projectDirs = await fs.readdir(this.sessionDir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
for (const projHash of projectDirs) {
|
|
53
|
+
const projPath = path.join(this.sessionDir, projHash);
|
|
54
|
+
const stat = await fs.stat(projPath).catch(() => null);
|
|
55
|
+
if (!stat?.isDirectory())
|
|
56
|
+
continue;
|
|
57
|
+
const sessionFiles = await this.getSessionFilesForProject(projPath);
|
|
58
|
+
for (const sessionData of sessionFiles) {
|
|
59
|
+
const isRunning = await this.isSessionRunning(sessionData, runningPids);
|
|
60
|
+
const { model, tokens, cost } = await this.aggregateMessageStats(sessionData.id);
|
|
61
|
+
const createdAt = sessionData.time?.created
|
|
62
|
+
? new Date(sessionData.time.created)
|
|
63
|
+
: new Date();
|
|
64
|
+
const updatedAt = sessionData.time?.updated
|
|
65
|
+
? new Date(sessionData.time.updated)
|
|
66
|
+
: undefined;
|
|
67
|
+
results.push({
|
|
68
|
+
id: sessionData.id,
|
|
69
|
+
status: isRunning ? "running" : "stopped",
|
|
70
|
+
adapter: this.id,
|
|
71
|
+
cwd: sessionData.directory,
|
|
72
|
+
model,
|
|
73
|
+
startedAt: createdAt,
|
|
74
|
+
stoppedAt: isRunning ? undefined : updatedAt,
|
|
75
|
+
pid: isRunning
|
|
76
|
+
? await this.findMatchingPid(sessionData, runningPids)
|
|
77
|
+
: undefined,
|
|
78
|
+
prompt: sessionData.title?.slice(0, 200),
|
|
79
|
+
tokens,
|
|
80
|
+
cost,
|
|
81
|
+
nativeMetadata: {
|
|
82
|
+
projectID: sessionData.projectID,
|
|
83
|
+
slug: sessionData.slug,
|
|
84
|
+
summary: sessionData.summary,
|
|
85
|
+
version: sessionData.version,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
async isAlive(sessionId) {
|
|
93
|
+
const runningPids = await this.getPids();
|
|
94
|
+
const resolved = await this.resolveSessionId(sessionId);
|
|
95
|
+
if (!resolved)
|
|
96
|
+
return false;
|
|
97
|
+
return this.isSessionRunning(resolved, runningPids);
|
|
98
|
+
}
|
|
40
99
|
async list(opts) {
|
|
41
100
|
const runningPids = await this.getPids();
|
|
42
101
|
const sessions = [];
|
|
@@ -116,15 +175,19 @@ export class OpenCodeAdapter {
|
|
|
116
175
|
}
|
|
117
176
|
async launch(opts) {
|
|
118
177
|
const args = ["run", opts.prompt];
|
|
119
|
-
const env =
|
|
178
|
+
const env = buildSpawnEnv(undefined, opts.env);
|
|
120
179
|
const cwd = opts.cwd || process.cwd();
|
|
121
180
|
await fs.mkdir(this.sessionsMetaDir, { recursive: true });
|
|
122
|
-
const
|
|
181
|
+
const opencodePath = await resolveBinaryPath("opencode");
|
|
182
|
+
const child = spawn(opencodePath, args, {
|
|
123
183
|
cwd,
|
|
124
184
|
env,
|
|
125
185
|
stdio: ["ignore", "pipe", "pipe"],
|
|
126
186
|
detached: true,
|
|
127
187
|
});
|
|
188
|
+
child.on("error", (err) => {
|
|
189
|
+
console.error(`[opencode] spawn error: ${err.message}`);
|
|
190
|
+
});
|
|
128
191
|
child.unref();
|
|
129
192
|
const pid = child.pid;
|
|
130
193
|
const now = new Date();
|
|
@@ -183,11 +246,15 @@ export class OpenCodeAdapter {
|
|
|
183
246
|
if (!resolved)
|
|
184
247
|
throw new Error(`Session not found for resume: ${sessionId}`);
|
|
185
248
|
const cwd = resolved.directory || process.cwd();
|
|
186
|
-
const
|
|
249
|
+
const opencodePath = await resolveBinaryPath("opencode");
|
|
250
|
+
const child = spawn(opencodePath, ["run", message], {
|
|
187
251
|
cwd,
|
|
188
252
|
stdio: ["ignore", "pipe", "pipe"],
|
|
189
253
|
detached: true,
|
|
190
254
|
});
|
|
255
|
+
child.on("error", (err) => {
|
|
256
|
+
console.error(`[opencode] resume spawn error: ${err.message}`);
|
|
257
|
+
});
|
|
191
258
|
child.unref();
|
|
192
259
|
}
|
|
193
260
|
async *events() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
1
|
+
import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
2
|
export interface PidInfo {
|
|
3
3
|
pid: number;
|
|
4
4
|
cwd: string;
|
|
@@ -41,6 +41,8 @@ export declare class PiRustAdapter implements AgentAdapter {
|
|
|
41
41
|
private readonly getPids;
|
|
42
42
|
private readonly isProcessAlive;
|
|
43
43
|
constructor(opts?: PiRustAdapterOpts);
|
|
44
|
+
discover(): Promise<DiscoveredSession[]>;
|
|
45
|
+
isAlive(sessionId: string): Promise<boolean>;
|
|
44
46
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
45
47
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
46
48
|
status(sessionId: string): Promise<AgentSession>;
|
package/dist/adapters/pi-rust.js
CHANGED
|
@@ -5,6 +5,8 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { buildSpawnEnv } from "../utils/daemon-env.js";
|
|
9
|
+
import { resolveBinaryPath } from "../utils/resolve-binary.js";
|
|
8
10
|
const execFileAsync = promisify(execFile);
|
|
9
11
|
const DEFAULT_SESSION_DIR = path.join(os.homedir(), ".pi", "agent", "sessions");
|
|
10
12
|
// Default: only show stopped sessions from the last 7 days
|
|
@@ -31,6 +33,80 @@ export class PiRustAdapter {
|
|
|
31
33
|
this.getPids = opts?.getPids || getPiRustPids;
|
|
32
34
|
this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
|
|
33
35
|
}
|
|
36
|
+
async discover() {
|
|
37
|
+
const runningPids = await this.getPids();
|
|
38
|
+
const results = [];
|
|
39
|
+
let projectDirs;
|
|
40
|
+
try {
|
|
41
|
+
const entries = await fs.readdir(this.sessionDir);
|
|
42
|
+
projectDirs = entries.filter((e) => e.startsWith("--"));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
for (const projDir of projectDirs) {
|
|
48
|
+
const projPath = path.join(this.sessionDir, projDir);
|
|
49
|
+
const stat = await fs.stat(projPath).catch(() => null);
|
|
50
|
+
if (!stat?.isDirectory())
|
|
51
|
+
continue;
|
|
52
|
+
const projectCwd = decodeProjDir(projDir);
|
|
53
|
+
const sessionFiles = await this.getSessionFiles(projPath);
|
|
54
|
+
for (const file of sessionFiles) {
|
|
55
|
+
const filePath = path.join(projPath, file);
|
|
56
|
+
const header = await this.readSessionHeader(filePath);
|
|
57
|
+
if (!header)
|
|
58
|
+
continue;
|
|
59
|
+
const isRunning = await this.isSessionRunning(header, projectCwd, runningPids);
|
|
60
|
+
const { model, tokens, cost } = await this.parseSessionTail(filePath);
|
|
61
|
+
const firstPrompt = await this.readFirstPrompt(filePath);
|
|
62
|
+
let fileStat;
|
|
63
|
+
try {
|
|
64
|
+
fileStat = await fs.stat(filePath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
results.push({
|
|
70
|
+
id: header.id,
|
|
71
|
+
status: isRunning ? "running" : "stopped",
|
|
72
|
+
adapter: this.id,
|
|
73
|
+
cwd: header.cwd || projectCwd,
|
|
74
|
+
model: model || header.modelId,
|
|
75
|
+
startedAt: new Date(header.timestamp),
|
|
76
|
+
stoppedAt: isRunning
|
|
77
|
+
? undefined
|
|
78
|
+
: fileStat
|
|
79
|
+
? new Date(Number(fileStat.mtimeMs))
|
|
80
|
+
: undefined,
|
|
81
|
+
pid: isRunning
|
|
82
|
+
? await this.findMatchingPid(header, projectCwd, runningPids)
|
|
83
|
+
: undefined,
|
|
84
|
+
prompt: firstPrompt?.slice(0, 200),
|
|
85
|
+
tokens,
|
|
86
|
+
cost: cost ?? undefined,
|
|
87
|
+
nativeMetadata: {
|
|
88
|
+
provider: header.provider,
|
|
89
|
+
thinkingLevel: header.thinkingLevel,
|
|
90
|
+
projectDir: projectCwd,
|
|
91
|
+
sessionFile: filePath,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
async isAlive(sessionId) {
|
|
99
|
+
const runningPids = await this.getPids();
|
|
100
|
+
const filePath = await this.findSessionFile(sessionId);
|
|
101
|
+
if (!filePath)
|
|
102
|
+
return false;
|
|
103
|
+
const header = await this.readSessionHeader(filePath);
|
|
104
|
+
if (!header)
|
|
105
|
+
return false;
|
|
106
|
+
const projDir = path.basename(path.dirname(filePath));
|
|
107
|
+
const projectCwd = decodeProjDir(projDir);
|
|
108
|
+
return this.isSessionRunning(header, projectCwd, runningPids);
|
|
109
|
+
}
|
|
34
110
|
async list(opts) {
|
|
35
111
|
const runningPids = await this.getPids();
|
|
36
112
|
const sessions = [];
|
|
@@ -129,18 +205,22 @@ export class PiRustAdapter {
|
|
|
129
205
|
if (opts.model) {
|
|
130
206
|
args.unshift("--model", opts.model);
|
|
131
207
|
}
|
|
132
|
-
const env =
|
|
208
|
+
const env = buildSpawnEnv(undefined, opts.env);
|
|
133
209
|
const cwd = opts.cwd || process.cwd();
|
|
134
210
|
// Write stdout to a log file so we can extract the session ID
|
|
135
211
|
await fs.mkdir(this.sessionsMetaDir, { recursive: true });
|
|
136
212
|
const logPath = path.join(this.sessionsMetaDir, `launch-${Date.now()}.log`);
|
|
137
213
|
const logFd = await fs.open(logPath, "w");
|
|
138
|
-
const
|
|
214
|
+
const piRustPath = await resolveBinaryPath("pi-rust");
|
|
215
|
+
const child = spawn(piRustPath, args, {
|
|
139
216
|
cwd,
|
|
140
217
|
env,
|
|
141
218
|
stdio: ["ignore", logFd.fd, "ignore"],
|
|
142
219
|
detached: true,
|
|
143
220
|
});
|
|
221
|
+
child.on("error", (err) => {
|
|
222
|
+
console.error(`[pi-rust] spawn error: ${err.message}`);
|
|
223
|
+
});
|
|
144
224
|
child.unref();
|
|
145
225
|
const pid = child.pid;
|
|
146
226
|
const now = new Date();
|
|
@@ -251,11 +331,15 @@ export class PiRustAdapter {
|
|
|
251
331
|
else {
|
|
252
332
|
args.unshift("--continue");
|
|
253
333
|
}
|
|
254
|
-
const
|
|
334
|
+
const piRustPath = await resolveBinaryPath("pi-rust");
|
|
335
|
+
const child = spawn(piRustPath, args, {
|
|
255
336
|
cwd,
|
|
256
337
|
stdio: ["pipe", "pipe", "pipe"],
|
|
257
338
|
detached: true,
|
|
258
339
|
});
|
|
340
|
+
child.on("error", (err) => {
|
|
341
|
+
console.error(`[pi-rust] resume spawn error: ${err.message}`);
|
|
342
|
+
});
|
|
259
343
|
child.unref();
|
|
260
344
|
}
|
|
261
345
|
async *events() {
|
package/dist/adapters/pi.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
1
|
+
import type { AgentAdapter, AgentSession, DiscoveredSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
|
|
2
2
|
export interface PidInfo {
|
|
3
3
|
pid: number;
|
|
4
4
|
cwd: string;
|
|
@@ -41,6 +41,8 @@ export declare class PiAdapter implements AgentAdapter {
|
|
|
41
41
|
private readonly getPids;
|
|
42
42
|
private readonly isProcessAlive;
|
|
43
43
|
constructor(opts?: PiAdapterOpts);
|
|
44
|
+
discover(): Promise<DiscoveredSession[]>;
|
|
45
|
+
isAlive(sessionId: string): Promise<boolean>;
|
|
44
46
|
list(opts?: ListOpts): Promise<AgentSession[]>;
|
|
45
47
|
peek(sessionId: string, opts?: PeekOpts): Promise<string>;
|
|
46
48
|
/** Extract assistant messages from a JSONL session file */
|