@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 +3 -92
- package/dist/index.js +248 -336
- package/openclaw.plugin.json +25 -25
- package/package.json +6 -9
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
|
-
|
|
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/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
132
|
+
save() {
|
|
62
133
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
constructor(
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
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.
|
|
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.
|
|
103
|
-
|
|
104
|
-
this.
|
|
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
|
-
|
|
205
|
+
async poll() {
|
|
116
206
|
if (!this.running) return;
|
|
117
|
-
const
|
|
118
|
-
const
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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-
|
|
339
|
-
start() {
|
|
340
|
-
|
|
341
|
-
|
|
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
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "opengate",
|
|
3
3
|
"name": "OpenGate",
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "0.
|
|
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
|
|
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
|
|
18
|
+
"description": "Agent API key for OpenGate"
|
|
18
19
|
},
|
|
19
|
-
"
|
|
20
|
+
"agentId": {
|
|
20
21
|
"type": "string",
|
|
21
|
-
"description": "
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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": "
|
|
31
|
-
"default": 600000
|
|
30
|
+
"description": "How often to poll OpenGate for tasks in milliseconds (default: 30000)"
|
|
32
31
|
},
|
|
33
|
-
"
|
|
34
|
-
"type": "
|
|
35
|
-
"description": "
|
|
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
|
-
"
|
|
44
|
-
"
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|