@opengate/openclaw 0.1.8 → 0.2.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/index.d.ts CHANGED
@@ -1,94 +1,5 @@
1
- /**
2
- * Formats OpenGate events into human-readable messages with MCP tool instructions.
3
- */
4
- interface OpenGateEvent {
5
- type: string;
6
- task_id?: string;
7
- task_title?: string;
8
- project_id?: string;
9
- from_agent?: string;
10
- reason?: string;
11
- summary?: string;
12
- content?: string;
13
- priority?: string;
14
- tags?: string[];
15
- [key: string]: unknown;
16
- }
17
- interface Notification {
18
- id: string;
19
- event_type: string;
20
- payload: OpenGateEvent;
21
- read: boolean;
22
- created_at: string;
23
- }
24
- /**
25
- * Format a single OpenGate event into a readable message for the agent.
26
- */
27
- declare function formatEvent(event: OpenGateEvent): string;
28
- /**
29
- * Format a list of notifications into a summary message.
30
- */
31
- declare function formatNotificationSummary(notifications: Notification[]): string;
1
+ import { OpenClawPluginApi } from 'openclaw/plugin-sdk';
32
2
 
33
- /**
34
- * HTTP polling client for OpenGate notifications.
35
- * Polls GET /api/agents/me/notifications?unread=true at a configurable interval.
36
- */
3
+ declare function register(api: OpenClawPluginApi): void;
37
4
 
38
- interface PollerConfig {
39
- url: string;
40
- apiKey: string;
41
- pollIntervalMs: number;
42
- projectId?: string;
43
- }
44
-
45
- /**
46
- * WebSocket client for OpenGate real-time notifications.
47
- * Connects to /api/ws, authenticates, and subscribes to agent events.
48
- * Features auto-reconnect with exponential backoff (1s -> 2s -> 4s -> ... -> max 60s).
49
- */
50
-
51
- interface WsClientConfig {
52
- url: string;
53
- apiKey: string;
54
- projectId?: string;
55
- }
56
-
57
- /**
58
- * @opengate/openclaw — OpenClaw plugin for OpenGate agent notifications.
59
- *
60
- * Provides real-time push notifications from OpenGate to agents via
61
- * HTTP polling (default) or WebSocket.
62
- *
63
- * Registers a service "opengate-bridge" that:
64
- * - Connects to an OpenGate instance
65
- * - Listens for agent events (task assignments, comments, unblocks, etc.)
66
- * - Injects formatted messages into the agent's session via sessions.send()
67
- */
68
- interface PluginConfig {
69
- url: string;
70
- apiKey: string;
71
- mode?: "polling" | "websocket";
72
- pollIntervalMs?: number;
73
- projectId?: string;
74
- }
75
- /**
76
- * OpenClaw Plugin API surface (subset relevant to this plugin).
77
- * These types represent what OpenClaw provides to plugins.
78
- */
79
- interface OpenClawPluginApi {
80
- registerService(service: {
81
- id: string;
82
- start: () => void | Promise<void>;
83
- stop: () => void | Promise<void>;
84
- }): void;
85
- gateway: {
86
- rpc(method: string, params: Record<string, unknown>): Promise<unknown>;
87
- };
88
- }
89
- /**
90
- * Plugin entry point. Called by OpenClaw when the plugin is loaded.
91
- */
92
- declare function register(api: OpenClawPluginApi, config: PluginConfig): void;
93
-
94
- export { type Notification, type OpenClawPluginApi, type OpenGateEvent, type PluginConfig, type PollerConfig, type WsClientConfig, register as default, formatEvent, formatNotificationSummary };
5
+ export { register as default };
package/dist/index.js CHANGED
@@ -1,381 +1,293 @@
1
- // src/poller.ts
2
- var Poller = class {
3
- config;
4
- handler;
5
- timer = null;
6
- running = false;
7
- constructor(config, handler) {
8
- this.config = config;
9
- this.handler = handler;
1
+ // src/config.ts
2
+ function resolveConfig(raw) {
3
+ const url = typeof raw.url === "string" ? raw.url.replace(/\/$/, "") : "";
4
+ const apiKey = typeof raw.apiKey === "string" ? raw.apiKey : "";
5
+ if (!url) throw new Error("[opengate] plugin config missing: url");
6
+ if (!apiKey) throw new Error("[opengate] plugin config missing: apiKey");
7
+ return {
8
+ url,
9
+ apiKey,
10
+ agentId: typeof raw.agentId === "string" ? raw.agentId : "main",
11
+ model: typeof raw.model === "string" ? raw.model : void 0,
12
+ pollIntervalMs: typeof raw.pollIntervalMs === "number" ? raw.pollIntervalMs : 3e4,
13
+ maxConcurrent: typeof raw.maxConcurrent === "number" ? raw.maxConcurrent : 3
14
+ };
15
+ }
16
+
17
+ // src/bootstrap.ts
18
+ function buildBootstrapPrompt(task, openGateUrl, apiKey) {
19
+ const tags = Array.isArray(task.tags) && task.tags.length > 0 ? task.tags.join(", ") : "none";
20
+ const contextBlock = task.context && Object.keys(task.context).length > 0 ? `
21
+ ## Task Context
22
+ \`\`\`json
23
+ ${JSON.stringify(task.context, null, 2)}
24
+ \`\`\`
25
+ ` : "";
26
+ return `You are an autonomous coding agent assigned a task via OpenGate.
27
+
28
+ ## Your Task
29
+ **ID:** ${task.id}
30
+ **Title:** ${task.title}
31
+ **Priority:** ${task.priority ?? "medium"}
32
+ **Tags:** ${tags}
33
+ **Project ID:** ${task.project_id ?? "unknown"}
34
+
35
+ **Description:**
36
+ ${task.description ?? "(no description provided)"}
37
+ ${contextBlock}
38
+ ## OpenGate API
39
+ - **Base URL:** ${openGateUrl}
40
+ - **Auth:** Bearer ${apiKey}
41
+
42
+ ## Protocol \u2014 Follow This Exactly
43
+ You MUST follow these steps in order. Skipping any step is not acceptable.
44
+
45
+ 1. **Claim** \u2014 \`POST /api/tasks/${task.id}/claim\`
46
+ 2. **Post starting comment** \u2014 \`POST /api/tasks/${task.id}/activity\` with body: \`{"content": "Starting: <your plan in 1-2 sentences>"}\`
47
+ 3. **Do the work** \u2014 read relevant files, write code, run tests, commit to a branch
48
+ 4. **Post results comment** \u2014 \`POST /api/tasks/${task.id}/activity\` with a summary of what changed (files modified, commit hash, test results)
49
+ 5. **Complete** \u2014 \`POST /api/tasks/${task.id}/complete\` with body: \`{"summary": "<what was done>", "output": {"branch": "...", "commits": [...]}}\`
50
+
51
+ If you encounter a blocker that requires human input:
52
+ - Post a question: \`POST /api/tasks/${task.id}/activity\` with \`{"content": "BLOCKED: <question>"}\`
53
+ - Block the task: \`POST /api/tasks/${task.id}/block\` with \`{"reason": "<reason>"}\`
54
+ - Then stop \u2014 do NOT mark it complete.
55
+
56
+ ## Notes
57
+ - Always work on a feature branch, never commit directly to main
58
+ - Run tests before completing
59
+ - If you discover something worth remembering (pattern, gotcha, decision), write it to a file in your workspace
60
+
61
+ Now begin. Start by claiming the task.`;
62
+ }
63
+
64
+ // src/spawner.ts
65
+ async function spawnTaskSession(taskId, message, pluginCfg, openclawCfg) {
66
+ const port = openclawCfg?.gateway?.port ?? 18789;
67
+ const hooksToken = openclawCfg?.hooks?.token;
68
+ if (!hooksToken) {
69
+ return {
70
+ ok: false,
71
+ error: "[opengate] hooks.token not configured in OpenClaw config. Add hooks.enabled=true and hooks.token=<secret> to enable task spawning."
72
+ };
10
73
  }
11
- start() {
12
- if (this.running) return;
13
- this.running = true;
14
- void this.poll();
15
- this.timer = setInterval(() => {
16
- void this.poll();
17
- }, this.config.pollIntervalMs);
74
+ const sessionKey = `opengate-task:${taskId}`;
75
+ const agentId = pluginCfg.agentId ?? "main";
76
+ const payload = {
77
+ message,
78
+ agentId,
79
+ sessionKey,
80
+ wakeMode: "now",
81
+ deliver: false,
82
+ name: "OpenGate"
83
+ };
84
+ if (pluginCfg.model) {
85
+ payload.model = pluginCfg.model;
18
86
  }
19
- stop() {
20
- this.running = false;
21
- if (this.timer) {
22
- clearInterval(this.timer);
23
- this.timer = null;
87
+ try {
88
+ const resp = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ Authorization: `Bearer ${hooksToken}`
93
+ },
94
+ body: JSON.stringify(payload)
95
+ });
96
+ if (resp.status === 202 || resp.status === 200) {
97
+ return { ok: true, sessionKey };
24
98
  }
99
+ const body = await resp.text().catch(() => "(no body)");
100
+ return {
101
+ ok: false,
102
+ error: `[opengate] hooks/agent returned HTTP ${resp.status}: ${body}`
103
+ };
104
+ } catch (e) {
105
+ return {
106
+ ok: false,
107
+ error: `[opengate] failed to reach hooks/agent: ${e instanceof Error ? e.message : String(e)}`
108
+ };
25
109
  }
26
- async poll() {
27
- if (!this.running) return;
110
+ }
111
+
112
+ // src/state.ts
113
+ import fs from "fs";
114
+ import path from "path";
115
+ var TTL_MS = 24 * 60 * 60 * 1e3;
116
+ var TaskState = class {
117
+ filePath;
118
+ data;
119
+ constructor(stateDir) {
120
+ this.filePath = path.join(stateDir, "opengate-spawned.json");
121
+ this.data = this.load();
122
+ this.cleanup();
123
+ }
124
+ load() {
28
125
  try {
29
- const baseUrl = this.config.url.replace(/\/$/, "");
30
- const params = new URLSearchParams({ unread: "true" });
31
- if (this.config.projectId) {
32
- params.set("project_id", this.config.projectId);
33
- }
34
- const response = await fetch(
35
- `${baseUrl}/api/agents/me/notifications?${params.toString()}`,
36
- {
37
- headers: {
38
- Authorization: `Bearer ${this.config.apiKey}`,
39
- "Content-Type": "application/json"
40
- }
41
- }
42
- );
43
- if (!response.ok) {
44
- console.error(
45
- `[opengate-bridge] Poll failed: ${response.status} ${response.statusText}`
46
- );
47
- return;
48
- }
49
- const notifications = await response.json();
50
- if (notifications.length > 0) {
51
- this.handler(notifications);
52
- await this.acknowledgeNotifications(
53
- baseUrl,
54
- notifications.map((n) => n.id)
55
- );
56
- }
57
- } catch (err) {
58
- console.error(`[opengate-bridge] Poll error:`, err);
126
+ const raw = fs.readFileSync(this.filePath, "utf-8");
127
+ return JSON.parse(raw);
128
+ } catch {
129
+ return { spawned: {} };
59
130
  }
60
131
  }
61
- async acknowledgeNotifications(baseUrl, ids) {
132
+ save() {
62
133
  try {
63
- for (const id of ids) {
64
- await fetch(`${baseUrl}/api/agents/me/notifications/${id}/ack`, {
65
- method: "POST",
66
- headers: {
67
- Authorization: `Bearer ${this.config.apiKey}`,
68
- "Content-Type": "application/json"
69
- }
70
- });
134
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true });
135
+ fs.writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), "utf-8");
136
+ } catch (e) {
137
+ console.error("[opengate] failed to save state:", e);
138
+ }
139
+ }
140
+ cleanup() {
141
+ const cutoff = Date.now() - TTL_MS;
142
+ let changed = false;
143
+ for (const [id, entry] of Object.entries(this.data.spawned)) {
144
+ if (entry.spawnedAt < cutoff) {
145
+ delete this.data.spawned[id];
146
+ changed = true;
71
147
  }
72
- } catch (err) {
73
- console.error(`[opengate-bridge] Failed to acknowledge notifications:`, err);
74
148
  }
149
+ if (changed) this.save();
150
+ }
151
+ isSpawned(taskId) {
152
+ return taskId in this.data.spawned;
153
+ }
154
+ markSpawned(taskId, sessionKey) {
155
+ this.data.spawned[taskId] = { taskId, sessionKey, spawnedAt: Date.now() };
156
+ this.save();
157
+ }
158
+ remove(taskId) {
159
+ delete this.data.spawned[taskId];
160
+ this.save();
161
+ }
162
+ activeCount() {
163
+ return Object.keys(this.data.spawned).length;
75
164
  }
76
165
  };
77
166
 
78
- // src/ws-client.ts
79
- import WebSocket from "ws";
80
- var INITIAL_BACKOFF_MS = 1e3;
81
- var MAX_BACKOFF_MS = 6e4;
82
- var BACKOFF_MULTIPLIER = 2;
83
- var WsClient = class {
84
- config;
85
- handler;
86
- ws = null;
87
- running = false;
88
- backoffMs = INITIAL_BACKOFF_MS;
89
- reconnectTimer = null;
90
- pingTimer = null;
91
- constructor(config, handler) {
92
- this.config = config;
93
- this.handler = handler;
167
+ // src/poller.ts
168
+ async function fetchInbox(url, apiKey) {
169
+ const resp = await fetch(`${url}/api/agents/me/inbox`, {
170
+ headers: { Authorization: `Bearer ${apiKey}` },
171
+ signal: AbortSignal.timeout(1e4)
172
+ });
173
+ if (!resp.ok) {
174
+ throw new Error(`OpenGate inbox returned HTTP ${resp.status}`);
175
+ }
176
+ const body = await resp.json();
177
+ return body.todo ?? [];
178
+ }
179
+ var OpenGatePoller = class {
180
+ constructor(pluginCfg, openclawCfg, logger, stateDir) {
181
+ this.pluginCfg = pluginCfg;
182
+ this.openclawCfg = openclawCfg;
183
+ this.logger = logger;
184
+ this.state = new TaskState(stateDir);
94
185
  }
186
+ timer = null;
187
+ state;
188
+ running = false;
95
189
  start() {
96
190
  if (this.running) return;
97
191
  this.running = true;
98
- this.connect();
192
+ const intervalMs = this.pluginCfg.pollIntervalMs ?? 3e4;
193
+ this.logger.info(`[opengate] Starting poller \u2014 interval: ${intervalMs}ms`);
194
+ void this.poll();
195
+ this.timer = setInterval(() => void this.poll(), intervalMs);
99
196
  }
100
197
  stop() {
101
198
  this.running = false;
102
- if (this.reconnectTimer) {
103
- clearTimeout(this.reconnectTimer);
104
- this.reconnectTimer = null;
105
- }
106
- if (this.pingTimer) {
107
- clearInterval(this.pingTimer);
108
- this.pingTimer = null;
109
- }
110
- if (this.ws) {
111
- this.ws.close(1e3, "Plugin stopped");
112
- this.ws = null;
199
+ if (this.timer) {
200
+ clearInterval(this.timer);
201
+ this.timer = null;
113
202
  }
203
+ this.logger.info("[opengate] Poller stopped");
114
204
  }
115
- connect() {
205
+ async poll() {
116
206
  if (!this.running) return;
117
- const baseUrl = this.config.url.replace(/\/$/, "").replace(/^http/, "ws");
118
- const wsUrl = `${baseUrl}/api/ws`;
207
+ const maxConcurrent = this.pluginCfg.maxConcurrent ?? 3;
208
+ const active = this.state.activeCount();
209
+ if (active >= maxConcurrent) {
210
+ this.logger.info(
211
+ `[opengate] At capacity (${active}/${maxConcurrent} active) \u2014 skipping poll`
212
+ );
213
+ return;
214
+ }
215
+ let tasks;
119
216
  try {
120
- this.ws = new WebSocket(wsUrl, {
121
- headers: {
122
- Authorization: `Bearer ${this.config.apiKey}`
123
- }
124
- });
125
- this.ws.on("open", () => {
126
- console.log("[opengate-bridge] WebSocket connected");
127
- this.backoffMs = INITIAL_BACKOFF_MS;
128
- const subscribe = JSON.stringify({
129
- type: "subscribe",
130
- channels: ["agent.notifications"],
131
- project_id: this.config.projectId
132
- });
133
- this.ws?.send(subscribe);
134
- this.pingTimer = setInterval(() => {
135
- if (this.ws?.readyState === WebSocket.OPEN) {
136
- this.ws.ping();
137
- }
138
- }, 3e4);
139
- });
140
- this.ws.on("message", (data) => {
141
- try {
142
- const event = JSON.parse(data.toString());
143
- if (event.type && event.type !== "pong") {
144
- this.handler(event);
145
- }
146
- } catch {
147
- console.error(
148
- "[opengate-bridge] Failed to parse WebSocket message"
149
- );
150
- }
151
- });
152
- this.ws.on("close", (code, reason) => {
153
- console.log(
154
- `[opengate-bridge] WebSocket closed: ${code} ${reason.toString()}`
217
+ tasks = await fetchInbox(this.pluginCfg.url, this.pluginCfg.apiKey);
218
+ } catch (e) {
219
+ this.logger.warn(
220
+ `[opengate] Failed to fetch inbox: ${e instanceof Error ? e.message : String(e)}`
221
+ );
222
+ return;
223
+ }
224
+ if (tasks.length === 0) return;
225
+ this.logger.info(`[opengate] Found ${tasks.length} todo task(s)`);
226
+ for (const task of tasks) {
227
+ if (!this.running) break;
228
+ const currentActive = this.state.activeCount();
229
+ if (currentActive >= maxConcurrent) {
230
+ this.logger.info(
231
+ `[opengate] Reached capacity (${currentActive}/${maxConcurrent}) \u2014 deferring remaining tasks`
155
232
  );
156
- this.cleanup();
157
- this.scheduleReconnect();
158
- });
159
- this.ws.on("error", (err) => {
160
- console.error(`[opengate-bridge] WebSocket error:`, err.message);
161
- this.cleanup();
162
- this.scheduleReconnect();
163
- });
164
- } catch (err) {
165
- console.error(`[opengate-bridge] WebSocket connection failed:`, err);
166
- this.scheduleReconnect();
233
+ break;
234
+ }
235
+ if (this.state.isSpawned(task.id)) {
236
+ this.logger.info(`[opengate] Task ${task.id} already spawned \u2014 skipping`);
237
+ continue;
238
+ }
239
+ await this.spawnTask(task);
167
240
  }
168
241
  }
169
- cleanup() {
170
- if (this.pingTimer) {
171
- clearInterval(this.pingTimer);
172
- this.pingTimer = null;
242
+ async spawnTask(task) {
243
+ this.logger.info(`[opengate] Spawning session for task: "${task.title}" (${task.id})`);
244
+ const prompt = buildBootstrapPrompt(task, this.pluginCfg.url, this.pluginCfg.apiKey);
245
+ const result = await spawnTaskSession(
246
+ task.id,
247
+ prompt,
248
+ this.pluginCfg,
249
+ this.openclawCfg
250
+ );
251
+ if (!result.ok) {
252
+ this.logger.error(result.error);
253
+ return;
173
254
  }
174
- this.ws = null;
175
- }
176
- scheduleReconnect() {
177
- if (!this.running) return;
178
- if (this.reconnectTimer) return;
179
- console.log(
180
- `[opengate-bridge] Reconnecting in ${this.backoffMs / 1e3}s...`
255
+ this.state.markSpawned(task.id, result.sessionKey);
256
+ this.logger.info(
257
+ `[opengate] Session spawned for task ${task.id} \u2192 session key: ${result.sessionKey}`
181
258
  );
182
- this.reconnectTimer = setTimeout(() => {
183
- this.reconnectTimer = null;
184
- this.backoffMs = Math.min(
185
- this.backoffMs * BACKOFF_MULTIPLIER,
186
- MAX_BACKOFF_MS
187
- );
188
- this.connect();
189
- }, this.backoffMs);
190
259
  }
191
260
  };
192
261
 
193
- // src/message-formatter.ts
194
- function formatEvent(event) {
195
- const taskRef = event.task_id ? ` (task: ${event.task_title ?? event.task_id})` : "";
196
- switch (event.type) {
197
- case "task.assigned":
198
- return [
199
- `New task assigned to you${taskRef}`,
200
- event.priority ? `Priority: ${event.priority}` : null,
201
- event.tags?.length ? `Tags: ${event.tags.join(", ")}` : null,
202
- event.repo_url ? `Repo: ${event.repo_url}` : null,
203
- "",
204
- "Next steps:",
205
- "1. Use `get_task` to read the full task details",
206
- event.project_id ? "2. Use `get_workspace_info` to set up project workspace" : null,
207
- "3. Use `search_knowledge` to check for relevant project knowledge",
208
- "4. Use `claim_task` to start working on it",
209
- "5. Use `post_comment` to share your planned approach"
210
- ].filter((line) => line !== null).join("\n");
211
- case "task.comment":
212
- return [
213
- `New comment on task${taskRef}`,
214
- event.from_agent ? `From: ${event.from_agent}` : null,
215
- event.content ? `> ${event.content}` : null,
216
- "",
217
- "Use `get_task` to see the full task context."
218
- ].filter((line) => line !== null).join("\n");
219
- case "task.dependency_ready":
220
- return [
221
- `Dependency resolved for task${taskRef}`,
222
- "A blocking dependency has been completed. This task may now be ready to claim.",
223
- "",
224
- "Next steps:",
225
- "1. Use `get_task` to review the task",
226
- "2. Use `list_dependencies` to verify all dependencies are met",
227
- "3. Use `claim_task` to start working if ready"
228
- ].join("\n");
229
- case "task.review_requested":
230
- return [
231
- `Review requested for task${taskRef}`,
232
- event.from_agent ? `From: ${event.from_agent}` : null,
233
- event.summary ? `Summary: ${event.summary}` : null,
234
- "",
235
- "Next steps:",
236
- "1. Use `get_task` to review the task and its output",
237
- "2. Approve with `approve_task` or request changes with `request_changes`"
238
- ].filter((line) => line !== null).join("\n");
239
- case "task.handoff":
240
- return [
241
- `Task handed off to you${taskRef}`,
242
- event.from_agent ? `From: ${event.from_agent}` : null,
243
- event.summary ? `Context: ${event.summary}` : null,
244
- "",
245
- "Next steps:",
246
- "1. Use `get_task` to read the full task and handoff context",
247
- "2. Use `claim_task` to accept the handoff",
248
- "3. Use `post_comment` to acknowledge and share your plan"
249
- ].filter((line) => line !== null).join("\n");
250
- case "task.unblocked":
251
- return [
252
- `Task unblocked${taskRef}`,
253
- event.reason ? `Reason: ${event.reason}` : null,
254
- "",
255
- "The task has been unblocked and moved back to your queue.",
256
- "Use `get_task` to review and continue working on it."
257
- ].filter((line) => line !== null).join("\n");
258
- case "task.changes_requested":
259
- return [
260
- `Changes requested on task${taskRef}`,
261
- event.from_agent ? `From: ${event.from_agent}` : null,
262
- event.content ? `Feedback: ${event.content}` : null,
263
- "",
264
- "Next steps:",
265
- "1. Use `get_task` to read the review feedback",
266
- "2. Address the requested changes",
267
- "3. Use `complete_task` to resubmit when ready"
268
- ].filter((line) => line !== null).join("\n");
269
- default:
270
- return [
271
- `OpenGate event: ${event.type}${taskRef}`,
272
- event.summary ?? event.content ?? "",
273
- "",
274
- "Use `check_inbox` to see your current task queue."
275
- ].filter((line) => line !== "").join("\n");
276
- }
277
- }
278
- function formatNotificationSummary(notifications) {
279
- if (notifications.length === 0) {
280
- return "";
281
- }
282
- const lines = [
283
- `You have ${notifications.length} unread notification${notifications.length === 1 ? "" : "s"} from OpenGate:`,
284
- ""
285
- ];
286
- for (const notification of notifications) {
287
- const formatted = formatEvent(notification.payload);
288
- lines.push(`--- ${notification.event_type} ---`);
289
- lines.push(formatted);
290
- lines.push("");
291
- }
292
- lines.push(
293
- "Use `check_inbox` for a full overview of your task queue."
294
- );
295
- return lines.join("\n");
296
- }
297
-
298
262
  // src/index.ts
299
- var PKG_VERSION = "0.1.8";
300
- async function checkForUpdate() {
301
- try {
302
- const res = await fetch("https://registry.npmjs.org/@opengate/openclaw/latest", {
303
- headers: { "Accept": "application/json" },
304
- signal: AbortSignal.timeout(5e3)
305
- });
306
- if (!res.ok) return;
307
- const data = await res.json();
308
- const latest = data.version;
309
- if (latest && latest !== PKG_VERSION) {
310
- console.log(`[opengate-bridge] Update available: ${PKG_VERSION} \u2192 ${latest}. Run: openclaw plugins update`);
311
- }
312
- } catch {
313
- }
314
- }
315
- var DEFAULT_POLL_INTERVAL_MS = 6e5;
316
- var SESSION_KEY = "main";
317
- function register(api, config) {
318
- const mode = config.mode ?? "polling";
319
- const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
263
+ function register(api) {
320
264
  let poller = null;
321
- let wsClient = null;
322
- function sendMessage(message) {
323
- if (!message) return;
324
- void api.gateway.rpc("sessions.send", {
325
- sessionKey: SESSION_KEY,
326
- message
327
- });
328
- }
329
- function handleNotifications(notifications) {
330
- const summary = formatNotificationSummary(notifications);
331
- sendMessage(summary);
265
+ let pluginCfg;
266
+ try {
267
+ pluginCfg = resolveConfig(api.pluginConfig ?? {});
268
+ } catch (e) {
269
+ api.logger.error(e instanceof Error ? e.message : String(e));
270
+ return;
332
271
  }
333
- function handleWsEvent(event) {
334
- const message = formatEvent(event);
335
- sendMessage(message);
272
+ const hooksToken = api.config?.hooks?.token;
273
+ if (!hooksToken) {
274
+ api.logger.error(
275
+ '[opengate] hooks.token is not configured. Add the following to your OpenClaw config to enable task spawning:\n "hooks": { "enabled": true, "token": "<your-secret>", "allowRequestSessionKey": true, "allowedSessionKeyPrefixes": ["opengate-task:"] }'
276
+ );
277
+ return;
336
278
  }
337
279
  api.registerService({
338
- id: "opengate-bridge",
339
- start() {
340
- console.log(
341
- `[opengate-bridge] Starting in ${mode} mode (url: ${config.url})`
342
- );
343
- checkForUpdate().catch(() => {
344
- });
345
- if (mode === "websocket") {
346
- wsClient = new WsClient(
347
- {
348
- url: config.url,
349
- apiKey: config.apiKey,
350
- projectId: config.projectId
351
- },
352
- handleWsEvent
353
- );
354
- wsClient.start();
355
- } else {
356
- poller = new Poller(
357
- {
358
- url: config.url,
359
- apiKey: config.apiKey,
360
- pollIntervalMs,
361
- projectId: config.projectId
362
- },
363
- handleNotifications
364
- );
365
- poller.start();
366
- }
280
+ id: "opengate-poller",
281
+ start(ctx) {
282
+ poller = new OpenGatePoller(pluginCfg, api.config, ctx.logger, ctx.stateDir);
283
+ poller.start();
367
284
  },
368
- stop() {
369
- console.log("[opengate-bridge] Stopping");
285
+ stop(ctx) {
370
286
  poller?.stop();
371
- wsClient?.stop();
372
287
  poller = null;
373
- wsClient = null;
374
288
  }
375
289
  });
376
290
  }
377
291
  export {
378
- register as default,
379
- formatEvent,
380
- formatNotificationSummary
292
+ register as default
381
293
  };
@@ -1,46 +1,46 @@
1
1
  {
2
2
  "id": "opengate",
3
3
  "name": "OpenGate",
4
- "description": "Real-time push notifications from OpenGate to agents via HTTP polling or WebSocket",
5
- "version": "0.1.8",
4
+ "description": "Polls OpenGate for assigned tasks and spawns isolated OpenClaw sessions to execute them. Turns OpenGate into the orchestrator.",
5
+ "version": "0.2.0",
6
+ "skills": ["./skills/opengate"],
6
7
  "configSchema": {
7
8
  "type": "object",
8
9
  "additionalProperties": false,
10
+ "required": ["url", "apiKey"],
9
11
  "properties": {
10
12
  "url": {
11
13
  "type": "string",
12
- "description": "OpenGate server URL",
13
- "default": "https://opengate.sh"
14
+ "description": "OpenGate base URL (e.g. https://opengate.sh)"
14
15
  },
15
16
  "apiKey": {
16
17
  "type": "string",
17
- "description": "Agent API key (tf_... format)"
18
+ "description": "Agent API key for OpenGate"
18
19
  },
19
- "mode": {
20
+ "agentId": {
20
21
  "type": "string",
21
- "description": "Notification mode: 'polling' (default) or 'websocket'",
22
- "enum": [
23
- "polling",
24
- "websocket"
25
- ],
26
- "default": "polling"
22
+ "description": "OpenClaw agent ID to spawn sessions as (default: 'main')"
23
+ },
24
+ "model": {
25
+ "type": "string",
26
+ "description": "Model override for spawned task sessions (e.g. anthropic/claude-sonnet-4-6)"
27
27
  },
28
28
  "pollIntervalMs": {
29
29
  "type": "number",
30
- "description": "Polling interval in milliseconds (only used in polling mode)",
31
- "default": 600000
30
+ "description": "How often to poll OpenGate for tasks in milliseconds (default: 30000)"
32
31
  },
33
- "projectId": {
34
- "type": "string",
35
- "description": "Optional project ID to filter notifications"
32
+ "maxConcurrent": {
33
+ "type": "number",
34
+ "description": "Max concurrent task sessions (default: 3)"
36
35
  }
37
- },
38
- "required": [
39
- "url",
40
- "apiKey"
41
- ]
36
+ }
42
37
  },
43
- "skills": [
44
- "skills/opengate"
45
- ]
38
+ "uiHints": {
39
+ "url": { "label": "OpenGate URL", "placeholder": "https://opengate.sh" },
40
+ "apiKey": { "label": "Agent API Key", "sensitive": true },
41
+ "agentId": { "label": "OpenClaw Agent ID", "placeholder": "main" },
42
+ "model": { "label": "Model Override", "placeholder": "anthropic/claude-sonnet-4-6" },
43
+ "pollIntervalMs": { "label": "Poll Interval (ms)", "advanced": true },
44
+ "maxConcurrent": { "label": "Max Concurrent Tasks", "advanced": true }
45
+ }
46
46
  }
package/package.json CHANGED
@@ -1,22 +1,19 @@
1
1
  {
2
2
  "name": "@opengate/openclaw",
3
- "version": "0.1.8",
4
- "description": "OpenClaw plugin for OpenGate \u2014 real-time agent notifications via HTTP polling or WebSocket",
3
+ "version": "0.2.0",
4
+ "description": "OpenGate task executor plugin for OpenClaw polls assigned tasks and spawns isolated agent sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "scripts": {
9
- "build": "tsup src/index.ts --format esm --dts",
10
- "dev": "tsup src/index.ts --format esm --dts --watch"
11
- },
12
- "dependencies": {
13
- "ws": "^8.18.0"
9
+ "build": "tsup src/index.ts --format esm --dts --external openclaw",
10
+ "dev": "tsup src/index.ts --format esm --dts --external openclaw --watch"
14
11
  },
12
+ "dependencies": {},
15
13
  "devDependencies": {
16
14
  "tsup": "^8.4.0",
17
15
  "typescript": "^5.7.0",
18
- "@types/node": "^22.0.0",
19
- "@types/ws": "^8.5.0"
16
+ "@types/node": "^22.0.0"
20
17
  },
21
18
  "files": [
22
19
  "dist",