@orgloop/agentctl 1.2.1 → 1.4.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.
@@ -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>;
@@ -32,6 +32,57 @@ export class ClaudeCodeAdapter {
32
32
  this.getPids = opts?.getPids || getClaudePids;
33
33
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
34
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
+ }
35
86
  async list(opts) {
36
87
  const runningPids = await this.getPids();
37
88
  const sessions = [];
@@ -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>;
@@ -30,6 +30,41 @@ export class CodexAdapter {
30
30
  this.getPids = opts?.getPids || getCodexPids;
31
31
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
32
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
+ }
33
68
  async list(opts) {
34
69
  const runningPids = await this.getPids();
35
70
  const sessionInfos = await this.discoverSessions();
@@ -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
- const isActive = updatedAt > 0 && ageMs < 5 * 60 * 1000;
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: isActive ? "running" : "idle",
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>;
@@ -39,6 +39,63 @@ export class OpenCodeAdapter {
39
39
  this.getPids = opts?.getPids || getOpenCodePids;
40
40
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
41
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
+ }
42
99
  async list(opts) {
43
100
  const runningPids = await this.getPids();
44
101
  const sessions = [];
@@ -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>;
@@ -33,6 +33,80 @@ export class PiRustAdapter {
33
33
  this.getPids = opts?.getPids || getPiRustPids;
34
34
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
35
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
+ }
36
110
  async list(opts) {
37
111
  const runningPids = await this.getPids();
38
112
  const sessions = [];
@@ -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 */
@@ -33,6 +33,44 @@ export class PiAdapter {
33
33
  this.getPids = opts?.getPids || getPiPids;
34
34
  this.isProcessAlive = opts?.isProcessAlive || defaultIsProcessAlive;
35
35
  }
36
+ async discover() {
37
+ const runningPids = await this.getPids();
38
+ const discovered = await this.discoverSessions();
39
+ const results = [];
40
+ for (const disc of discovered) {
41
+ const isRunning = await this.isSessionRunning(disc, runningPids);
42
+ const { model, tokens, cost } = await this.parseSessionTail(disc.filePath, disc.header);
43
+ results.push({
44
+ id: disc.sessionId,
45
+ status: isRunning ? "running" : "stopped",
46
+ adapter: this.id,
47
+ cwd: disc.header.cwd,
48
+ model,
49
+ startedAt: disc.created,
50
+ stoppedAt: isRunning ? undefined : disc.modified,
51
+ pid: isRunning
52
+ ? await this.findMatchingPid(disc, runningPids)
53
+ : undefined,
54
+ prompt: await this.getFirstPrompt(disc.filePath),
55
+ tokens,
56
+ cost,
57
+ nativeMetadata: {
58
+ provider: disc.header.provider,
59
+ thinkingLevel: disc.header.thinkingLevel,
60
+ version: disc.header.version,
61
+ cwdSlug: disc.cwdSlug,
62
+ },
63
+ });
64
+ }
65
+ return results;
66
+ }
67
+ async isAlive(sessionId) {
68
+ const runningPids = await this.getPids();
69
+ const disc = await this.findSession(sessionId);
70
+ if (!disc)
71
+ return false;
72
+ return this.isSessionRunning(disc, runningPids);
73
+ }
36
74
  async list(opts) {
37
75
  const runningPids = await this.getPids();
38
76
  const discovered = await this.discoverSessions();