@orgloop/agentctl 1.0.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/dist/adapters/claude-code.d.ts +83 -0
  4. package/dist/adapters/claude-code.js +783 -0
  5. package/dist/adapters/openclaw.d.ts +88 -0
  6. package/dist/adapters/openclaw.js +297 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +808 -0
  9. package/dist/client/daemon-client.d.ts +6 -0
  10. package/dist/client/daemon-client.js +81 -0
  11. package/dist/compat-shim.d.ts +2 -0
  12. package/dist/compat-shim.js +15 -0
  13. package/dist/core/types.d.ts +68 -0
  14. package/dist/core/types.js +2 -0
  15. package/dist/daemon/fuse-engine.d.ts +30 -0
  16. package/dist/daemon/fuse-engine.js +118 -0
  17. package/dist/daemon/launchagent.d.ts +7 -0
  18. package/dist/daemon/launchagent.js +49 -0
  19. package/dist/daemon/lock-manager.d.ts +16 -0
  20. package/dist/daemon/lock-manager.js +71 -0
  21. package/dist/daemon/metrics.d.ts +20 -0
  22. package/dist/daemon/metrics.js +72 -0
  23. package/dist/daemon/server.d.ts +33 -0
  24. package/dist/daemon/server.js +283 -0
  25. package/dist/daemon/session-tracker.d.ts +28 -0
  26. package/dist/daemon/session-tracker.js +121 -0
  27. package/dist/daemon/state.d.ts +61 -0
  28. package/dist/daemon/state.js +126 -0
  29. package/dist/daemon/supervisor.d.ts +24 -0
  30. package/dist/daemon/supervisor.js +79 -0
  31. package/dist/hooks.d.ts +19 -0
  32. package/dist/hooks.js +39 -0
  33. package/dist/merge.d.ts +24 -0
  34. package/dist/merge.js +65 -0
  35. package/dist/migration/migrate-locks.d.ts +5 -0
  36. package/dist/migration/migrate-locks.js +41 -0
  37. package/dist/worktree.d.ts +24 -0
  38. package/dist/worktree.js +65 -0
  39. package/package.json +60 -0
@@ -0,0 +1,88 @@
1
+ import type { AgentAdapter, AgentSession, LaunchOpts, LifecycleEvent, ListOpts, PeekOpts, StopOpts } from "../core/types.js";
2
+ export interface OpenClawAdapterOpts {
3
+ baseUrl?: string;
4
+ authToken?: string;
5
+ /** Override for testing — replaces the real WebSocket RPC call */
6
+ rpcCall?: RpcCallFn;
7
+ }
8
+ /**
9
+ * Shape of a single RPC exchange: send method+params, get back the payload.
10
+ * Injected in tests to avoid a real WebSocket connection.
11
+ */
12
+ export type RpcCallFn = (method: string, params: Record<string, unknown>) => Promise<unknown>;
13
+ /** Row returned by the gateway's `sessions.list` method */
14
+ export interface GatewaySessionRow {
15
+ key: string;
16
+ kind: "direct" | "group" | "global" | "unknown";
17
+ label?: string;
18
+ displayName?: string;
19
+ derivedTitle?: string;
20
+ lastMessagePreview?: string;
21
+ channel?: string;
22
+ updatedAt: number | null;
23
+ sessionId?: string;
24
+ inputTokens?: number;
25
+ outputTokens?: number;
26
+ totalTokens?: number;
27
+ model?: string;
28
+ modelProvider?: string;
29
+ }
30
+ /** Result envelope from `sessions.list` */
31
+ export interface SessionsListResult {
32
+ ts: number;
33
+ path: string;
34
+ count: number;
35
+ defaults: {
36
+ modelProvider: string | null;
37
+ model: string | null;
38
+ contextTokens: number | null;
39
+ };
40
+ sessions: GatewaySessionRow[];
41
+ }
42
+ /** Single preview entry from `sessions.preview` */
43
+ export interface SessionsPreviewEntry {
44
+ key: string;
45
+ status: "ok" | "empty" | "missing" | "error";
46
+ items: Array<{
47
+ role: string;
48
+ text: string;
49
+ }>;
50
+ }
51
+ /** Result envelope from `sessions.preview` */
52
+ export interface SessionsPreviewResult {
53
+ ts: number;
54
+ previews: SessionsPreviewEntry[];
55
+ }
56
+ /**
57
+ * OpenClaw adapter — reads session data from the OpenClaw gateway via
58
+ * its WebSocket RPC protocol. Falls back gracefully when the gateway
59
+ * is unreachable.
60
+ */
61
+ export declare class OpenClawAdapter implements AgentAdapter {
62
+ readonly id = "openclaw";
63
+ private readonly baseUrl;
64
+ private readonly authToken;
65
+ private readonly rpcCall;
66
+ constructor(opts?: OpenClawAdapterOpts);
67
+ list(opts?: ListOpts): Promise<AgentSession[]>;
68
+ peek(sessionId: string, opts?: PeekOpts): Promise<string>;
69
+ status(sessionId: string): Promise<AgentSession>;
70
+ launch(_opts: LaunchOpts): Promise<AgentSession>;
71
+ stop(_sessionId: string, _opts?: StopOpts): Promise<void>;
72
+ resume(sessionId: string, _message: string): Promise<void>;
73
+ events(): AsyncIterable<LifecycleEvent>;
74
+ /**
75
+ * Map a gateway session row to the standard AgentSession interface.
76
+ * OpenClaw sessions with a recent updatedAt are considered "running".
77
+ */
78
+ private mapRowToSession;
79
+ /**
80
+ * Resolve a sessionId (or prefix) to a gateway session key.
81
+ */
82
+ private resolveKey;
83
+ /**
84
+ * Real WebSocket RPC call — connects, performs handshake, sends one
85
+ * request, reads the response, then disconnects.
86
+ */
87
+ private defaultRpcCall;
88
+ }
@@ -0,0 +1,297 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const DEFAULT_BASE_URL = "http://127.0.0.1:18789";
3
+ /**
4
+ * OpenClaw adapter — reads session data from the OpenClaw gateway via
5
+ * its WebSocket RPC protocol. Falls back gracefully when the gateway
6
+ * is unreachable.
7
+ */
8
+ export class OpenClawAdapter {
9
+ id = "openclaw";
10
+ baseUrl;
11
+ authToken;
12
+ rpcCall;
13
+ constructor(opts) {
14
+ this.baseUrl = opts?.baseUrl || DEFAULT_BASE_URL;
15
+ this.authToken =
16
+ opts?.authToken || process.env.OPENCLAW_WEBHOOK_TOKEN || "";
17
+ this.rpcCall = opts?.rpcCall || this.defaultRpcCall.bind(this);
18
+ }
19
+ async list(opts) {
20
+ let result;
21
+ try {
22
+ result = (await this.rpcCall("sessions.list", {
23
+ includeDerivedTitles: true,
24
+ includeLastMessage: true,
25
+ }));
26
+ }
27
+ catch {
28
+ // Gateway unreachable — return empty
29
+ return [];
30
+ }
31
+ let sessions = result.sessions.map((row) => this.mapRowToSession(row, result.defaults));
32
+ if (opts?.status) {
33
+ sessions = sessions.filter((s) => s.status === opts.status);
34
+ }
35
+ if (!opts?.all && !opts?.status) {
36
+ sessions = sessions.filter((s) => s.status === "running" || s.status === "idle");
37
+ }
38
+ return sessions;
39
+ }
40
+ async peek(sessionId, opts) {
41
+ const key = await this.resolveKey(sessionId);
42
+ if (!key)
43
+ throw new Error(`Session not found: ${sessionId}`);
44
+ const limit = opts?.lines ?? 20;
45
+ let result;
46
+ try {
47
+ result = (await this.rpcCall("sessions.preview", {
48
+ keys: [key],
49
+ limit,
50
+ maxChars: 4000,
51
+ }));
52
+ }
53
+ catch (err) {
54
+ throw new Error(`Failed to peek session ${sessionId}: ${err.message}`);
55
+ }
56
+ const preview = result.previews?.[0];
57
+ if (!preview || preview.status === "missing") {
58
+ throw new Error(`Session not found: ${sessionId}`);
59
+ }
60
+ if (preview.items.length === 0)
61
+ return "(no messages)";
62
+ const assistantMessages = preview.items
63
+ .filter((item) => item.role === "assistant")
64
+ .map((item) => item.text);
65
+ if (assistantMessages.length === 0)
66
+ return "(no assistant messages)";
67
+ return assistantMessages.slice(-limit).join("\n---\n");
68
+ }
69
+ async status(sessionId) {
70
+ let result;
71
+ try {
72
+ result = (await this.rpcCall("sessions.list", {
73
+ includeDerivedTitles: true,
74
+ search: sessionId,
75
+ }));
76
+ }
77
+ catch (err) {
78
+ throw new Error(`Failed to get status for ${sessionId}: ${err.message}`);
79
+ }
80
+ const row = result.sessions.find((s) => s.sessionId === sessionId ||
81
+ s.key === sessionId ||
82
+ s.sessionId?.startsWith(sessionId) ||
83
+ s.key.startsWith(sessionId));
84
+ if (!row)
85
+ throw new Error(`Session not found: ${sessionId}`);
86
+ return this.mapRowToSession(row, result.defaults);
87
+ }
88
+ async launch(_opts) {
89
+ throw new Error("OpenClaw sessions cannot be launched via agentctl");
90
+ }
91
+ async stop(_sessionId, _opts) {
92
+ throw new Error("OpenClaw sessions cannot be stopped via agentctl");
93
+ }
94
+ async resume(sessionId, _message) {
95
+ // OpenClaw sessions receive messages through their configured channels,
96
+ // not through a direct CLI interface.
97
+ throw new Error(`Cannot resume OpenClaw session ${sessionId} — use the gateway UI or configured channel`);
98
+ }
99
+ async *events() {
100
+ // Poll-based diffing (same pattern as claude-code)
101
+ let knownSessions = new Map();
102
+ // Initial snapshot
103
+ const initial = await this.list({ all: true });
104
+ for (const s of initial) {
105
+ knownSessions.set(s.id, s);
106
+ }
107
+ while (true) {
108
+ await sleep(5000);
109
+ let current;
110
+ try {
111
+ current = await this.list({ all: true });
112
+ }
113
+ catch {
114
+ continue;
115
+ }
116
+ const currentMap = new Map(current.map((s) => [s.id, s]));
117
+ for (const [id, session] of currentMap) {
118
+ const prev = knownSessions.get(id);
119
+ if (!prev) {
120
+ yield {
121
+ type: "session.started",
122
+ adapter: this.id,
123
+ sessionId: id,
124
+ session,
125
+ timestamp: new Date(),
126
+ };
127
+ }
128
+ else if (prev.status === "running" && session.status === "stopped") {
129
+ yield {
130
+ type: "session.stopped",
131
+ adapter: this.id,
132
+ sessionId: id,
133
+ session,
134
+ timestamp: new Date(),
135
+ };
136
+ }
137
+ else if (prev.status === "running" && session.status === "idle") {
138
+ yield {
139
+ type: "session.idle",
140
+ adapter: this.id,
141
+ sessionId: id,
142
+ session,
143
+ timestamp: new Date(),
144
+ };
145
+ }
146
+ }
147
+ knownSessions = currentMap;
148
+ }
149
+ }
150
+ // --- Private helpers ---
151
+ /**
152
+ * Map a gateway session row to the standard AgentSession interface.
153
+ * OpenClaw sessions with a recent updatedAt are considered "running".
154
+ */
155
+ mapRowToSession(row, defaults) {
156
+ const now = Date.now();
157
+ const updatedAt = row.updatedAt ?? 0;
158
+ const ageMs = now - updatedAt;
159
+ // Consider "running" if updated in the last 5 minutes
160
+ const isActive = updatedAt > 0 && ageMs < 5 * 60 * 1000;
161
+ const model = row.model || defaults.model || undefined;
162
+ const input = row.inputTokens ?? 0;
163
+ const output = row.outputTokens ?? 0;
164
+ return {
165
+ id: row.sessionId || row.key,
166
+ adapter: this.id,
167
+ status: isActive ? "running" : "idle",
168
+ startedAt: updatedAt > 0 ? new Date(updatedAt) : new Date(),
169
+ cwd: undefined,
170
+ model,
171
+ prompt: row.derivedTitle || row.displayName || row.label,
172
+ tokens: input || output ? { in: input, out: output } : undefined,
173
+ meta: {
174
+ key: row.key,
175
+ kind: row.kind,
176
+ channel: row.channel,
177
+ displayName: row.displayName,
178
+ modelProvider: row.modelProvider || defaults.modelProvider,
179
+ lastMessagePreview: row.lastMessagePreview,
180
+ },
181
+ };
182
+ }
183
+ /**
184
+ * Resolve a sessionId (or prefix) to a gateway session key.
185
+ */
186
+ async resolveKey(sessionId) {
187
+ let result;
188
+ try {
189
+ result = (await this.rpcCall("sessions.list", {
190
+ search: sessionId,
191
+ }));
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ const row = result.sessions.find((s) => s.sessionId === sessionId ||
197
+ s.key === sessionId ||
198
+ s.sessionId?.startsWith(sessionId) ||
199
+ s.key.startsWith(sessionId));
200
+ return row?.key ?? null;
201
+ }
202
+ /**
203
+ * Real WebSocket RPC call — connects, performs handshake, sends one
204
+ * request, reads the response, then disconnects.
205
+ */
206
+ async defaultRpcCall(method, params) {
207
+ // Dynamic import so tests can inject a mock without loading ws
208
+ const { WebSocket } = await import("ws").catch(() => {
209
+ // Fall back to globalThis.WebSocket (available in Node 22+)
210
+ return { WebSocket: globalThis.WebSocket };
211
+ });
212
+ const wsUrl = this.baseUrl.replace(/^http/, "ws");
213
+ const ws = new WebSocket(wsUrl);
214
+ return new Promise((resolve, reject) => {
215
+ const timeout = setTimeout(() => {
216
+ ws.close();
217
+ reject(new Error("OpenClaw gateway connection timed out"));
218
+ }, 10_000);
219
+ const reqId = randomUUID();
220
+ let connected = false;
221
+ ws.onopen = () => {
222
+ // Wait for challenge event, then send connect
223
+ };
224
+ ws.onmessage = (event) => {
225
+ try {
226
+ const raw = typeof event.data === "string" ? event.data : String(event.data);
227
+ const frame = JSON.parse(raw);
228
+ // Step 1: Receive challenge, send connect
229
+ if (frame.type === "event" && frame.event === "connect.challenge") {
230
+ ws.send(JSON.stringify({
231
+ type: "req",
232
+ id: randomUUID(),
233
+ method: "connect",
234
+ params: {
235
+ minProtocol: 1,
236
+ maxProtocol: 1,
237
+ client: {
238
+ id: "agentctl",
239
+ version: "0.1.0",
240
+ platform: process.platform,
241
+ mode: "cli",
242
+ },
243
+ role: "operator",
244
+ scopes: ["operator.read"],
245
+ auth: { token: this.authToken || null },
246
+ },
247
+ }));
248
+ return;
249
+ }
250
+ // Step 2: Receive hello-ok, send actual RPC
251
+ if (frame.type === "res" && frame.ok && !connected) {
252
+ connected = true;
253
+ ws.send(JSON.stringify({
254
+ type: "req",
255
+ id: reqId,
256
+ method,
257
+ params,
258
+ }));
259
+ return;
260
+ }
261
+ // Step 3: Receive RPC response
262
+ if (frame.type === "res" && frame.id === reqId) {
263
+ clearTimeout(timeout);
264
+ ws.close();
265
+ if (frame.ok) {
266
+ resolve(frame.payload);
267
+ }
268
+ else {
269
+ reject(new Error(frame.error?.message || `RPC error: ${method}`));
270
+ }
271
+ return;
272
+ }
273
+ // Auth failure
274
+ if (frame.type === "res" && !frame.ok && !connected) {
275
+ clearTimeout(timeout);
276
+ ws.close();
277
+ reject(new Error(frame.error?.message || "OpenClaw gateway auth failed"));
278
+ }
279
+ }
280
+ catch {
281
+ // Ignore malformed frames
282
+ }
283
+ };
284
+ ws.onerror = (err) => {
285
+ clearTimeout(timeout);
286
+ reject(new Error(`OpenClaw gateway error: ${err?.message || "connection failed"}`));
287
+ };
288
+ ws.onclose = () => {
289
+ clearTimeout(timeout);
290
+ // Only reject if we haven't resolved yet
291
+ };
292
+ });
293
+ }
294
+ }
295
+ function sleep(ms) {
296
+ return new Promise((resolve) => setTimeout(resolve, ms));
297
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};