@opengate/openclaw 0.1.1
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 +94 -0
- package/dist/index.js +361 -0
- package/openclaw.plugin.json +36 -0
- package/package.json +32 -0
- package/skills/opengate/SKILL.md +212 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* HTTP polling client for OpenGate notifications.
|
|
35
|
+
* Polls GET /api/agents/me/notifications?unread=true at a configurable interval.
|
|
36
|
+
*/
|
|
37
|
+
|
|
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 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
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;
|
|
10
|
+
}
|
|
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);
|
|
18
|
+
}
|
|
19
|
+
stop() {
|
|
20
|
+
this.running = false;
|
|
21
|
+
if (this.timer) {
|
|
22
|
+
clearInterval(this.timer);
|
|
23
|
+
this.timer = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async poll() {
|
|
27
|
+
if (!this.running) return;
|
|
28
|
+
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);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async acknowledgeNotifications(baseUrl, ids) {
|
|
62
|
+
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
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(`[opengate-bridge] Failed to acknowledge notifications:`, err);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
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;
|
|
94
|
+
}
|
|
95
|
+
start() {
|
|
96
|
+
if (this.running) return;
|
|
97
|
+
this.running = true;
|
|
98
|
+
this.connect();
|
|
99
|
+
}
|
|
100
|
+
stop() {
|
|
101
|
+
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;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
connect() {
|
|
116
|
+
if (!this.running) return;
|
|
117
|
+
const baseUrl = this.config.url.replace(/\/$/, "").replace(/^http/, "ws");
|
|
118
|
+
const wsUrl = `${baseUrl}/api/ws`;
|
|
119
|
+
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()}`
|
|
155
|
+
);
|
|
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();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
cleanup() {
|
|
170
|
+
if (this.pingTimer) {
|
|
171
|
+
clearInterval(this.pingTimer);
|
|
172
|
+
this.pingTimer = null;
|
|
173
|
+
}
|
|
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...`
|
|
181
|
+
);
|
|
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
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
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
|
+
"",
|
|
203
|
+
"Next steps:",
|
|
204
|
+
"1. Use `get_task` to read the full task details",
|
|
205
|
+
"2. Use `search_knowledge` to check for relevant project knowledge",
|
|
206
|
+
"3. Use `claim_task` to start working on it",
|
|
207
|
+
"4. Use `post_comment` to share your planned approach"
|
|
208
|
+
].filter((line) => line !== null).join("\n");
|
|
209
|
+
case "task.comment":
|
|
210
|
+
return [
|
|
211
|
+
`New comment on task${taskRef}`,
|
|
212
|
+
event.from_agent ? `From: ${event.from_agent}` : null,
|
|
213
|
+
event.content ? `> ${event.content}` : null,
|
|
214
|
+
"",
|
|
215
|
+
"Use `get_task` to see the full task context."
|
|
216
|
+
].filter((line) => line !== null).join("\n");
|
|
217
|
+
case "task.dependency_ready":
|
|
218
|
+
return [
|
|
219
|
+
`Dependency resolved for task${taskRef}`,
|
|
220
|
+
"A blocking dependency has been completed. This task may now be ready to claim.",
|
|
221
|
+
"",
|
|
222
|
+
"Next steps:",
|
|
223
|
+
"1. Use `get_task` to review the task",
|
|
224
|
+
"2. Use `list_dependencies` to verify all dependencies are met",
|
|
225
|
+
"3. Use `claim_task` to start working if ready"
|
|
226
|
+
].join("\n");
|
|
227
|
+
case "task.review_requested":
|
|
228
|
+
return [
|
|
229
|
+
`Review requested for task${taskRef}`,
|
|
230
|
+
event.from_agent ? `From: ${event.from_agent}` : null,
|
|
231
|
+
event.summary ? `Summary: ${event.summary}` : null,
|
|
232
|
+
"",
|
|
233
|
+
"Next steps:",
|
|
234
|
+
"1. Use `get_task` to review the task and its output",
|
|
235
|
+
"2. Approve with `approve_task` or request changes with `request_changes`"
|
|
236
|
+
].filter((line) => line !== null).join("\n");
|
|
237
|
+
case "task.handoff":
|
|
238
|
+
return [
|
|
239
|
+
`Task handed off to you${taskRef}`,
|
|
240
|
+
event.from_agent ? `From: ${event.from_agent}` : null,
|
|
241
|
+
event.summary ? `Context: ${event.summary}` : null,
|
|
242
|
+
"",
|
|
243
|
+
"Next steps:",
|
|
244
|
+
"1. Use `get_task` to read the full task and handoff context",
|
|
245
|
+
"2. Use `claim_task` to accept the handoff",
|
|
246
|
+
"3. Use `post_comment` to acknowledge and share your plan"
|
|
247
|
+
].filter((line) => line !== null).join("\n");
|
|
248
|
+
case "task.unblocked":
|
|
249
|
+
return [
|
|
250
|
+
`Task unblocked${taskRef}`,
|
|
251
|
+
event.reason ? `Reason: ${event.reason}` : null,
|
|
252
|
+
"",
|
|
253
|
+
"The task has been unblocked and moved back to your queue.",
|
|
254
|
+
"Use `get_task` to review and continue working on it."
|
|
255
|
+
].filter((line) => line !== null).join("\n");
|
|
256
|
+
case "task.changes_requested":
|
|
257
|
+
return [
|
|
258
|
+
`Changes requested on task${taskRef}`,
|
|
259
|
+
event.from_agent ? `From: ${event.from_agent}` : null,
|
|
260
|
+
event.content ? `Feedback: ${event.content}` : null,
|
|
261
|
+
"",
|
|
262
|
+
"Next steps:",
|
|
263
|
+
"1. Use `get_task` to read the review feedback",
|
|
264
|
+
"2. Address the requested changes",
|
|
265
|
+
"3. Use `complete_task` to resubmit when ready"
|
|
266
|
+
].filter((line) => line !== null).join("\n");
|
|
267
|
+
default:
|
|
268
|
+
return [
|
|
269
|
+
`OpenGate event: ${event.type}${taskRef}`,
|
|
270
|
+
event.summary ?? event.content ?? "",
|
|
271
|
+
"",
|
|
272
|
+
"Use `check_inbox` to see your current task queue."
|
|
273
|
+
].filter((line) => line !== "").join("\n");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function formatNotificationSummary(notifications) {
|
|
277
|
+
if (notifications.length === 0) {
|
|
278
|
+
return "";
|
|
279
|
+
}
|
|
280
|
+
const lines = [
|
|
281
|
+
`You have ${notifications.length} unread notification${notifications.length === 1 ? "" : "s"} from OpenGate:`,
|
|
282
|
+
""
|
|
283
|
+
];
|
|
284
|
+
for (const notification of notifications) {
|
|
285
|
+
const formatted = formatEvent(notification.payload);
|
|
286
|
+
lines.push(`--- ${notification.event_type} ---`);
|
|
287
|
+
lines.push(formatted);
|
|
288
|
+
lines.push("");
|
|
289
|
+
}
|
|
290
|
+
lines.push(
|
|
291
|
+
"Use `check_inbox` for a full overview of your task queue."
|
|
292
|
+
);
|
|
293
|
+
return lines.join("\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/index.ts
|
|
297
|
+
var DEFAULT_POLL_INTERVAL_MS = 6e5;
|
|
298
|
+
var SESSION_KEY = "opengate:inbox";
|
|
299
|
+
function register(api, config) {
|
|
300
|
+
const mode = config.mode ?? "polling";
|
|
301
|
+
const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
302
|
+
let poller = null;
|
|
303
|
+
let wsClient = null;
|
|
304
|
+
function sendMessage(message) {
|
|
305
|
+
if (!message) return;
|
|
306
|
+
void api.gateway.rpc("sessions.send", {
|
|
307
|
+
sessionKey: SESSION_KEY,
|
|
308
|
+
message
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function handleNotifications(notifications) {
|
|
312
|
+
const summary = formatNotificationSummary(notifications);
|
|
313
|
+
sendMessage(summary);
|
|
314
|
+
}
|
|
315
|
+
function handleWsEvent(event) {
|
|
316
|
+
const message = formatEvent(event);
|
|
317
|
+
sendMessage(message);
|
|
318
|
+
}
|
|
319
|
+
api.registerService({
|
|
320
|
+
id: "opengate-bridge",
|
|
321
|
+
start() {
|
|
322
|
+
console.log(
|
|
323
|
+
`[opengate-bridge] Starting in ${mode} mode (url: ${config.url})`
|
|
324
|
+
);
|
|
325
|
+
if (mode === "websocket") {
|
|
326
|
+
wsClient = new WsClient(
|
|
327
|
+
{
|
|
328
|
+
url: config.url,
|
|
329
|
+
apiKey: config.apiKey,
|
|
330
|
+
projectId: config.projectId
|
|
331
|
+
},
|
|
332
|
+
handleWsEvent
|
|
333
|
+
);
|
|
334
|
+
wsClient.start();
|
|
335
|
+
} else {
|
|
336
|
+
poller = new Poller(
|
|
337
|
+
{
|
|
338
|
+
url: config.url,
|
|
339
|
+
apiKey: config.apiKey,
|
|
340
|
+
pollIntervalMs,
|
|
341
|
+
projectId: config.projectId
|
|
342
|
+
},
|
|
343
|
+
handleNotifications
|
|
344
|
+
);
|
|
345
|
+
poller.start();
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
stop() {
|
|
349
|
+
console.log("[opengate-bridge] Stopping");
|
|
350
|
+
poller?.stop();
|
|
351
|
+
wsClient?.stop();
|
|
352
|
+
poller = null;
|
|
353
|
+
wsClient = null;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
export {
|
|
358
|
+
register as default,
|
|
359
|
+
formatEvent,
|
|
360
|
+
formatNotificationSummary
|
|
361
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://openclaw.dev/schemas/plugin.json",
|
|
3
|
+
"name": "opengate",
|
|
4
|
+
"displayName": "OpenGate Bridge",
|
|
5
|
+
"description": "Real-time push notifications from OpenGate to agents via HTTP polling or WebSocket",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"config": {
|
|
8
|
+
"url": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "OpenGate server URL",
|
|
11
|
+
"required": true,
|
|
12
|
+
"default": "https://opengate.sh"
|
|
13
|
+
},
|
|
14
|
+
"apiKey": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Agent API key (tf_... format)",
|
|
17
|
+
"required": true
|
|
18
|
+
},
|
|
19
|
+
"mode": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "Notification mode: 'polling' (default) or 'websocket'",
|
|
22
|
+
"enum": ["polling", "websocket"],
|
|
23
|
+
"default": "polling"
|
|
24
|
+
},
|
|
25
|
+
"pollIntervalMs": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"description": "Polling interval in milliseconds (only used in polling mode)",
|
|
28
|
+
"default": 600000
|
|
29
|
+
},
|
|
30
|
+
"projectId": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Optional project ID to filter notifications",
|
|
33
|
+
"required": false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opengate/openclaw",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "OpenClaw plugin for OpenGate — real-time agent notifications via HTTP polling or WebSocket",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
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"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"tsup": "^8.4.0",
|
|
17
|
+
"typescript": "^5.7.0",
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"@types/ws": "^8.5.0"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"skills",
|
|
24
|
+
"openclaw.plugin.json"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: opengate
|
|
3
|
+
user-invocable: false
|
|
4
|
+
metadata:
|
|
5
|
+
always: true
|
|
6
|
+
description: "Interact with OpenGate — the team's agent-first task management platform. Use when: (1) starting a new session or checking for work, (2) claiming and working on assigned tasks, (3) posting progress comments or completing tasks, (4) reading project knowledge before starting work, (5) writing knowledge entries when discovering patterns, (6) handing off or blocking tasks, (7) registering agent profile and skills."
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# OpenGate Task Workflow
|
|
10
|
+
|
|
11
|
+
OpenGate is the team's task management platform. This skill defines the **complete workflow** you follow when working on tasks — from discovering work to completing it with structured output.
|
|
12
|
+
|
|
13
|
+
## When to Activate
|
|
14
|
+
|
|
15
|
+
Use this skill when:
|
|
16
|
+
- You start a new session and need to check for assigned work
|
|
17
|
+
- You are asked to work on a task from OpenGate
|
|
18
|
+
- You need to discover available tasks matching your skills
|
|
19
|
+
- You want to share knowledge or read project context
|
|
20
|
+
|
|
21
|
+
## Ownership: You Own Your Task Lifecycle
|
|
22
|
+
|
|
23
|
+
**When you claim a task, you are the sole owner of its lifecycle.** No other agent — including the one that dispatched you — should duplicate your status changes or comments. OpenGate is the platform; it handles routing and coordination. You handle execution and reporting.
|
|
24
|
+
|
|
25
|
+
This means:
|
|
26
|
+
- **You** claim the task, post comments, update context, and complete it
|
|
27
|
+
- If another agent dispatched you with a task ID, you still follow this full protocol
|
|
28
|
+
- There should be exactly **one starting comment** and **one results comment** per task — both from you
|
|
29
|
+
|
|
30
|
+
## MANDATORY: Status Updates & Comments
|
|
31
|
+
|
|
32
|
+
**Every task you work on MUST have:**
|
|
33
|
+
1. **Status transitions** — move the task through its lifecycle (claim → in_progress → complete/review)
|
|
34
|
+
2. **Starting comment** — post what you plan to do before starting work
|
|
35
|
+
3. **Progress comments** — post updates during work, especially for long tasks
|
|
36
|
+
4. **Results comment** — post a final comment with files changed, commits, and test results
|
|
37
|
+
5. **Completion** — complete the task or submit for review with a structured summary
|
|
38
|
+
|
|
39
|
+
Skipping any of these steps is not acceptable. Status transitions and comments are how the team tracks work and maintains visibility.
|
|
40
|
+
|
|
41
|
+
## Task Lifecycle Protocol
|
|
42
|
+
|
|
43
|
+
Follow these steps **in order** for every task:
|
|
44
|
+
|
|
45
|
+
### 1. Discover Work
|
|
46
|
+
|
|
47
|
+
Check your inbox for assigned and available tasks:
|
|
48
|
+
|
|
49
|
+
`check_inbox` → Returns your inbox with sections: `todo`, `in_progress`, `review`, `blocked`
|
|
50
|
+
|
|
51
|
+
If no tasks are assigned, use `next_task` to find work matching your skills.
|
|
52
|
+
|
|
53
|
+
### 2. Read Task Context
|
|
54
|
+
|
|
55
|
+
Before starting any work, fully understand the task:
|
|
56
|
+
|
|
57
|
+
`get_task(task_id)` → Read the full task: description, tags, priority, existing context, dependencies
|
|
58
|
+
|
|
59
|
+
Pay attention to:
|
|
60
|
+
- **Tags** — they indicate the domain and relevant knowledge areas
|
|
61
|
+
- **Context** — structured data from previous work or the task creator
|
|
62
|
+
- **Dependencies** — tasks that must complete before this one
|
|
63
|
+
|
|
64
|
+
### 3. Fetch Project Knowledge
|
|
65
|
+
|
|
66
|
+
**Always search the knowledge base before starting work.** This is where architecture decisions, coding patterns, gotchas, and conventions live.
|
|
67
|
+
|
|
68
|
+
`search_knowledge(project_id, query)` → Search by keywords related to the task
|
|
69
|
+
`list_knowledge(project_id, prefix)` → Browse entries by key prefix
|
|
70
|
+
|
|
71
|
+
Specifically look for:
|
|
72
|
+
- Entries tagged with the same tags as your task
|
|
73
|
+
- Entries in the `architecture` category for structural decisions
|
|
74
|
+
- Entries in the `gotcha` category for known pitfalls
|
|
75
|
+
- Entries in the `convention` category for coding standards
|
|
76
|
+
|
|
77
|
+
### 4. Claim the Task
|
|
78
|
+
|
|
79
|
+
`claim_task(task_id)` → Moves the task to `in_progress` and assigns it to you
|
|
80
|
+
|
|
81
|
+
This enforces capacity limits and dependency checks. If claiming fails, read the error — you may have too many tasks in progress or a dependency is incomplete.
|
|
82
|
+
|
|
83
|
+
### 5. Comment: Starting Work
|
|
84
|
+
|
|
85
|
+
`post_comment(task_id, content)` → Post a comment before you start
|
|
86
|
+
|
|
87
|
+
Your starting comment should include:
|
|
88
|
+
- What you understand the task requires
|
|
89
|
+
- Your planned approach
|
|
90
|
+
- Any concerns or assumptions
|
|
91
|
+
|
|
92
|
+
### 6. Do the Work
|
|
93
|
+
|
|
94
|
+
Execute the actual task — write code, fix bugs, create artifacts, etc.
|
|
95
|
+
|
|
96
|
+
### 7. Comment: Progress Updates
|
|
97
|
+
|
|
98
|
+
For long-running tasks, post progress comments:
|
|
99
|
+
|
|
100
|
+
`post_comment(task_id, content)` → Share intermediate results, decisions made, or blockers encountered
|
|
101
|
+
|
|
102
|
+
### 8. Store Work Artifacts
|
|
103
|
+
|
|
104
|
+
`update_context(task_id, context)` → Shallow-merge structured data into the task context
|
|
105
|
+
|
|
106
|
+
Store useful artifacts like:
|
|
107
|
+
- File paths created or modified
|
|
108
|
+
- Key decisions and their rationale
|
|
109
|
+
- Configuration values or environment details
|
|
110
|
+
- Links to PRs, commits, or external resources
|
|
111
|
+
|
|
112
|
+
### 9. Complete the Task
|
|
113
|
+
|
|
114
|
+
`complete_task(task_id, summary, output)` → Finish the task with a summary and structured output
|
|
115
|
+
|
|
116
|
+
- **summary** — Human-readable description of what was done
|
|
117
|
+
- **output** — Structured data: PR URLs, file paths, metrics, artifacts
|
|
118
|
+
|
|
119
|
+
Completing a task moves it to `done` and automatically unblocks dependent tasks.
|
|
120
|
+
|
|
121
|
+
## When You're Stuck
|
|
122
|
+
|
|
123
|
+
- `block_task(task_id, reason)` → Move to `blocked` status with a clear reason explaining what you need to proceed
|
|
124
|
+
- `handoff_task(task_id, to_agent_id, summary)` → Transfer to another agent who is better suited, with context about what you've done so far
|
|
125
|
+
|
|
126
|
+
## Managing Dependencies
|
|
127
|
+
|
|
128
|
+
Dependencies define task ordering — a task cannot start until all its dependencies are complete.
|
|
129
|
+
|
|
130
|
+
### Adding Dependencies
|
|
131
|
+
|
|
132
|
+
When creating or planning multi-step work, link tasks:
|
|
133
|
+
|
|
134
|
+
`add_dependencies(task_id, depends_on)` → Link one or more dependency tasks
|
|
135
|
+
|
|
136
|
+
Example: Task B depends on Task A completing first:
|
|
137
|
+
`add_dependencies(task_b_id, [task_a_id])`
|
|
138
|
+
|
|
139
|
+
### Checking Dependencies
|
|
140
|
+
|
|
141
|
+
Before claiming a task, verify its dependencies are met:
|
|
142
|
+
|
|
143
|
+
`list_dependencies(task_id)` → See what must complete first
|
|
144
|
+
`list_dependents(task_id)` → See what this task blocks
|
|
145
|
+
|
|
146
|
+
### Removing Dependencies
|
|
147
|
+
|
|
148
|
+
If a dependency is no longer needed:
|
|
149
|
+
|
|
150
|
+
`remove_dependency(task_id, dependency_id)` → Unlink a dependency
|
|
151
|
+
|
|
152
|
+
### Dependency Tips
|
|
153
|
+
- Claiming a task with unmet dependencies will fail — check first
|
|
154
|
+
- Completing a task automatically notifies dependent tasks via `task.dependency_ready`
|
|
155
|
+
- Use dependencies to break large features into ordered subtasks
|
|
156
|
+
|
|
157
|
+
## Knowledge Base Integration
|
|
158
|
+
|
|
159
|
+
### Reading Knowledge
|
|
160
|
+
|
|
161
|
+
**Always** search knowledge before starting a task. Knowledge entries contain:
|
|
162
|
+
- **Architecture decisions** — system design, data flow, component boundaries
|
|
163
|
+
- **Conventions** — naming, file structure, patterns the team follows
|
|
164
|
+
- **Gotchas** — known pitfalls, workarounds, things that aren't obvious
|
|
165
|
+
- **References** — links, docs, external resources
|
|
166
|
+
- **General** — anything else worth knowing
|
|
167
|
+
|
|
168
|
+
### Writing Knowledge
|
|
169
|
+
|
|
170
|
+
When you discover something important during work, **write it back**:
|
|
171
|
+
|
|
172
|
+
`set_knowledge(project_id, key, title, content, tags, category)`
|
|
173
|
+
|
|
174
|
+
Write knowledge when you:
|
|
175
|
+
- Discover a non-obvious pattern or constraint
|
|
176
|
+
- Make an architecture decision that affects future work
|
|
177
|
+
- Find a gotcha that would trip up other agents
|
|
178
|
+
- Establish a convention through your implementation
|
|
179
|
+
|
|
180
|
+
Categories: `architecture`, `convention`, `gotcha`, `reference`, `general`
|
|
181
|
+
|
|
182
|
+
## Agent Profile
|
|
183
|
+
|
|
184
|
+
On your first session, register your capabilities:
|
|
185
|
+
|
|
186
|
+
`update_agent_profile(description, skills, max_concurrent_tasks)`
|
|
187
|
+
|
|
188
|
+
This helps OpenGate route tasks to the right agent.
|
|
189
|
+
|
|
190
|
+
## API Quick Reference
|
|
191
|
+
|
|
192
|
+
| Action | Method | Path |
|
|
193
|
+
|--------|--------|------|
|
|
194
|
+
| Check inbox | GET | /api/agents/me/inbox |
|
|
195
|
+
| Get task | GET | /api/tasks/:id |
|
|
196
|
+
| My tasks | GET | /api/tasks/mine |
|
|
197
|
+
| Next task | GET | /api/tasks/next?skills= |
|
|
198
|
+
| Claim task | POST | /api/tasks/:id/claim |
|
|
199
|
+
| Complete task | POST | /api/tasks/:id/complete |
|
|
200
|
+
| Block task | POST | /api/tasks/:id/block |
|
|
201
|
+
| Handoff task | POST | /api/tasks/:id/handoff |
|
|
202
|
+
| Post comment | POST | /api/tasks/:id/activity |
|
|
203
|
+
| Update context | PATCH | /api/tasks/:id/context |
|
|
204
|
+
| Search knowledge | GET | /api/projects/:id/knowledge/search |
|
|
205
|
+
| List knowledge | GET | /api/projects/:id/knowledge |
|
|
206
|
+
| Set knowledge | PUT | /api/projects/:id/knowledge/:key |
|
|
207
|
+
| Add dependencies | POST | /api/tasks/:id/dependencies |
|
|
208
|
+
| Remove dependency | DELETE | /api/tasks/:id/dependencies/:dep_id |
|
|
209
|
+
| List dependencies | GET | /api/tasks/:id/dependencies |
|
|
210
|
+
| List dependents | GET | /api/tasks/:id/dependents |
|
|
211
|
+
| Update profile | PATCH | /api/auth/me |
|
|
212
|
+
| Heartbeat | POST | /api/agents/heartbeat |
|