@mrc2204/opencode-bridge 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/README.en.md +115 -0
- package/README.md +117 -0
- package/dist/chunk-6NIQKNRA.js +176 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +814 -0
- package/dist/observability.d.ts +52 -0
- package/dist/observability.js +16 -0
- package/openclaw.plugin.json +56 -0
- package/package.json +49 -0
- package/skills/opencode-orchestration/SKILL.md +658 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeTypedEventV1,
|
|
3
|
+
parseSseFramesFromBuffer,
|
|
4
|
+
resolveSessionId
|
|
5
|
+
} from "./chunk-6NIQKNRA.js";
|
|
6
|
+
|
|
7
|
+
// src/runtime.ts
|
|
8
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { spawn } from "child_process";
|
|
11
|
+
import { createServer } from "net";
|
|
12
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
13
|
+
var DEFAULT_SOFT_STALL_MS = 60 * 1e3;
|
|
14
|
+
var DEFAULT_HARD_STALL_MS = 180 * 1e3;
|
|
15
|
+
var DEFAULT_OBS_TIMEOUT_MS = 3e3;
|
|
16
|
+
var DEFAULT_TAIL_LIMIT = 20;
|
|
17
|
+
var DEFAULT_EVENT_LIMIT = 10;
|
|
18
|
+
var HOOK_PREFIX = "hook:opencode:";
|
|
19
|
+
function asArray(value) {
|
|
20
|
+
return Array.isArray(value) ? value : [];
|
|
21
|
+
}
|
|
22
|
+
function asString(value) {
|
|
23
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
24
|
+
}
|
|
25
|
+
function asNumber(value) {
|
|
26
|
+
const n = Number(value);
|
|
27
|
+
return Number.isFinite(n) ? n : void 0;
|
|
28
|
+
}
|
|
29
|
+
function buildSessionKey(agentId, taskId) {
|
|
30
|
+
return `${HOOK_PREFIX}${agentId}:${taskId}`;
|
|
31
|
+
}
|
|
32
|
+
function getBridgeStateDir() {
|
|
33
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR || `${process.env.HOME}/.openclaw`;
|
|
34
|
+
return join(stateDir, "opencode-bridge");
|
|
35
|
+
}
|
|
36
|
+
function getBridgeConfigPath() {
|
|
37
|
+
return join(getBridgeStateDir(), "config.json");
|
|
38
|
+
}
|
|
39
|
+
function ensureBridgeConfigFile() {
|
|
40
|
+
const dir = getBridgeStateDir();
|
|
41
|
+
mkdirSync(dir, { recursive: true });
|
|
42
|
+
const path = getBridgeConfigPath();
|
|
43
|
+
if (!existsSync(path)) {
|
|
44
|
+
const initial = {
|
|
45
|
+
opencodeServerUrl: "http://127.0.0.1:4096",
|
|
46
|
+
projectRegistry: []
|
|
47
|
+
};
|
|
48
|
+
writeFileSync(path, JSON.stringify(initial, null, 2), "utf8");
|
|
49
|
+
return initial;
|
|
50
|
+
}
|
|
51
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
52
|
+
}
|
|
53
|
+
function getRuntimeConfig(cfg) {
|
|
54
|
+
const fileCfg = ensureBridgeConfigFile();
|
|
55
|
+
return {
|
|
56
|
+
opencodeServerUrl: fileCfg.opencodeServerUrl || cfg?.opencodeServerUrl,
|
|
57
|
+
projectRegistry: fileCfg.projectRegistry || cfg?.projectRegistry || [],
|
|
58
|
+
hookBaseUrl: fileCfg.hookBaseUrl || cfg?.hookBaseUrl,
|
|
59
|
+
hookToken: fileCfg.hookToken || cfg?.hookToken
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function normalizeRegistry(raw) {
|
|
63
|
+
return asArray(raw).map((item) => {
|
|
64
|
+
const obj = item && typeof item === "object" ? item : {};
|
|
65
|
+
const projectId = asString(obj.projectId);
|
|
66
|
+
const repoRoot = asString(obj.repoRoot);
|
|
67
|
+
const serverUrl = asString(obj.serverUrl);
|
|
68
|
+
const idleTimeoutMs = Number(obj.idleTimeoutMs || DEFAULT_IDLE_TIMEOUT_MS);
|
|
69
|
+
if (!projectId || !repoRoot || !serverUrl) return null;
|
|
70
|
+
return {
|
|
71
|
+
projectId,
|
|
72
|
+
repoRoot,
|
|
73
|
+
serverUrl,
|
|
74
|
+
idleTimeoutMs: Number.isFinite(idleTimeoutMs) ? idleTimeoutMs : DEFAULT_IDLE_TIMEOUT_MS
|
|
75
|
+
};
|
|
76
|
+
}).filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
function findRegistryEntry(cfg, projectId, repoRoot) {
|
|
79
|
+
const runtimeCfg = getRuntimeConfig(cfg);
|
|
80
|
+
const dynamicRegistry = normalizeServeRegistry(readServeRegistry()).entries.map((x) => ({
|
|
81
|
+
projectId: x.project_id,
|
|
82
|
+
repoRoot: x.repo_root,
|
|
83
|
+
serverUrl: x.opencode_server_url,
|
|
84
|
+
idleTimeoutMs: x.idle_timeout_ms
|
|
85
|
+
}));
|
|
86
|
+
const registry = [...dynamicRegistry, ...normalizeRegistry(runtimeCfg.projectRegistry)].filter(Boolean);
|
|
87
|
+
if (projectId) {
|
|
88
|
+
const byProject = registry.find((x) => x.projectId === projectId);
|
|
89
|
+
if (byProject) return byProject;
|
|
90
|
+
}
|
|
91
|
+
if (repoRoot) {
|
|
92
|
+
const byRoot = registry.find((x) => x.repoRoot === repoRoot);
|
|
93
|
+
if (byRoot) return byRoot;
|
|
94
|
+
}
|
|
95
|
+
return void 0;
|
|
96
|
+
}
|
|
97
|
+
function buildEnvelope(input) {
|
|
98
|
+
return {
|
|
99
|
+
task_id: input.taskId,
|
|
100
|
+
run_id: input.runId,
|
|
101
|
+
agent_id: input.agentId,
|
|
102
|
+
session_key: buildSessionKey(input.agentId, input.taskId),
|
|
103
|
+
origin_session_key: input.originSessionKey,
|
|
104
|
+
project_id: input.projectId,
|
|
105
|
+
repo_root: input.repoRoot,
|
|
106
|
+
opencode_server_url: input.serverUrl,
|
|
107
|
+
...input.channel ? { channel: input.channel } : {},
|
|
108
|
+
...input.to ? { to: input.to } : {},
|
|
109
|
+
...input.deliver !== void 0 ? { deliver: input.deliver } : {},
|
|
110
|
+
...input.priority ? { priority: input.priority } : {}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function mapEventToState(event) {
|
|
114
|
+
switch (event) {
|
|
115
|
+
case "task.started":
|
|
116
|
+
case "task.progress":
|
|
117
|
+
return "running";
|
|
118
|
+
case "permission.requested":
|
|
119
|
+
return "awaiting_permission";
|
|
120
|
+
case "task.stalled":
|
|
121
|
+
return "stalled";
|
|
122
|
+
case "task.failed":
|
|
123
|
+
return "failed";
|
|
124
|
+
case "task.completed":
|
|
125
|
+
return "completed";
|
|
126
|
+
default:
|
|
127
|
+
return "running";
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function evaluateLifecycle(input) {
|
|
131
|
+
const nowMs = input.nowMs ?? Date.now();
|
|
132
|
+
const softStallMs = input.softStallMs ?? DEFAULT_SOFT_STALL_MS;
|
|
133
|
+
const hardStallMs = input.hardStallMs ?? DEFAULT_HARD_STALL_MS;
|
|
134
|
+
if (input.lastEventKind) {
|
|
135
|
+
const state = mapEventToState(input.lastEventKind);
|
|
136
|
+
return {
|
|
137
|
+
state,
|
|
138
|
+
escalateToMain: state === "failed" || state === "completed",
|
|
139
|
+
needsPermissionHandling: state === "awaiting_permission",
|
|
140
|
+
stallSeverity: null
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!input.lastEventAtMs) {
|
|
144
|
+
return {
|
|
145
|
+
state: "queued",
|
|
146
|
+
escalateToMain: false,
|
|
147
|
+
needsPermissionHandling: false,
|
|
148
|
+
stallSeverity: null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const age = nowMs - input.lastEventAtMs;
|
|
152
|
+
if (age >= hardStallMs) {
|
|
153
|
+
return { state: "stalled", escalateToMain: true, needsPermissionHandling: false, stallSeverity: "hard" };
|
|
154
|
+
}
|
|
155
|
+
if (age >= softStallMs) {
|
|
156
|
+
return { state: "stalled", escalateToMain: false, needsPermissionHandling: false, stallSeverity: "soft" };
|
|
157
|
+
}
|
|
158
|
+
return { state: "running", escalateToMain: false, needsPermissionHandling: false, stallSeverity: null };
|
|
159
|
+
}
|
|
160
|
+
function getRunStateDir() {
|
|
161
|
+
return join(getBridgeStateDir(), "runs");
|
|
162
|
+
}
|
|
163
|
+
function readRunStatus(runId) {
|
|
164
|
+
const path = join(getRunStateDir(), `${runId}.json`);
|
|
165
|
+
if (!existsSync(path)) return null;
|
|
166
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
167
|
+
}
|
|
168
|
+
function getAuditDir() {
|
|
169
|
+
return join(getBridgeStateDir(), "audit");
|
|
170
|
+
}
|
|
171
|
+
function getServeRegistryPath() {
|
|
172
|
+
return join(getBridgeStateDir(), "registry.json");
|
|
173
|
+
}
|
|
174
|
+
function readServeRegistry() {
|
|
175
|
+
const path = getServeRegistryPath();
|
|
176
|
+
if (!existsSync(path)) return { entries: [] };
|
|
177
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
178
|
+
}
|
|
179
|
+
function normalizeServeRegistry(registry) {
|
|
180
|
+
const entries = asArray(registry.entries).map((entry) => {
|
|
181
|
+
const e = entry && typeof entry === "object" ? entry : {};
|
|
182
|
+
const project_id = asString(e.project_id);
|
|
183
|
+
const repo_root = asString(e.repo_root);
|
|
184
|
+
const opencode_server_url = asString(e.opencode_server_url);
|
|
185
|
+
if (!project_id || !repo_root || !opencode_server_url) return null;
|
|
186
|
+
return {
|
|
187
|
+
project_id,
|
|
188
|
+
repo_root,
|
|
189
|
+
opencode_server_url,
|
|
190
|
+
...asNumber(e.pid) !== void 0 ? { pid: asNumber(e.pid) } : {},
|
|
191
|
+
...asString(e.status) ? { status: asString(e.status) } : {},
|
|
192
|
+
...asString(e.last_event_at) ? { last_event_at: asString(e.last_event_at) } : {},
|
|
193
|
+
idle_timeout_ms: asNumber(e.idle_timeout_ms) ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
194
|
+
updated_at: asString(e.updated_at) || (/* @__PURE__ */ new Date()).toISOString()
|
|
195
|
+
};
|
|
196
|
+
}).filter(Boolean);
|
|
197
|
+
return { entries };
|
|
198
|
+
}
|
|
199
|
+
function writeServeRegistryFile(data) {
|
|
200
|
+
const path = getServeRegistryPath();
|
|
201
|
+
writeFileSync(path, JSON.stringify(normalizeServeRegistry(data), null, 2), "utf8");
|
|
202
|
+
return path;
|
|
203
|
+
}
|
|
204
|
+
function upsertServeRegistry(entry) {
|
|
205
|
+
const registry = normalizeServeRegistry(readServeRegistry());
|
|
206
|
+
const idx = registry.entries.findIndex((x) => x.project_id === entry.project_id || x.repo_root === entry.repo_root);
|
|
207
|
+
if (idx >= 0) registry.entries[idx] = entry;
|
|
208
|
+
else registry.entries.push(entry);
|
|
209
|
+
const path = writeServeRegistryFile(registry);
|
|
210
|
+
return { path, registry };
|
|
211
|
+
}
|
|
212
|
+
function evaluateServeIdle(entry, nowMs) {
|
|
213
|
+
const now = nowMs ?? Date.now();
|
|
214
|
+
const last = entry.last_event_at ? Date.parse(entry.last_event_at) : NaN;
|
|
215
|
+
const idleTimeoutMs = entry.idle_timeout_ms ?? DEFAULT_IDLE_TIMEOUT_MS;
|
|
216
|
+
if (!Number.isFinite(last)) {
|
|
217
|
+
return { shouldShutdown: false, idleMs: null, reason: "missing_last_event_at" };
|
|
218
|
+
}
|
|
219
|
+
const idleMs = now - last;
|
|
220
|
+
return {
|
|
221
|
+
shouldShutdown: idleMs >= idleTimeoutMs,
|
|
222
|
+
idleMs,
|
|
223
|
+
idleTimeoutMs,
|
|
224
|
+
reason: idleMs >= idleTimeoutMs ? "idle_timeout_exceeded" : "within_idle_window"
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function allocatePort() {
|
|
228
|
+
return await new Promise((resolve, reject) => {
|
|
229
|
+
const server = createServer();
|
|
230
|
+
server.listen(0, "127.0.0.1", () => {
|
|
231
|
+
const address = server.address();
|
|
232
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
233
|
+
server.close(() => resolve(port));
|
|
234
|
+
});
|
|
235
|
+
server.on("error", reject);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async function waitForHealth(serverUrl, timeoutMs = 1e4) {
|
|
239
|
+
const started = Date.now();
|
|
240
|
+
while (Date.now() - started < timeoutMs) {
|
|
241
|
+
try {
|
|
242
|
+
const r = await fetch(`${serverUrl}/global/health`);
|
|
243
|
+
if (r.ok) return true;
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
async function spawnServeForProject(input) {
|
|
251
|
+
const existing = normalizeServeRegistry(readServeRegistry()).entries.find((x) => x.project_id === input.project_id || x.repo_root === input.repo_root);
|
|
252
|
+
if (existing && existing.status === "running") {
|
|
253
|
+
const healthy2 = await waitForHealth(existing.opencode_server_url, 2e3);
|
|
254
|
+
if (healthy2) {
|
|
255
|
+
return { reused: true, entry: existing, registryPath: getServeRegistryPath() };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const port = await allocatePort();
|
|
259
|
+
const child = spawn("opencode", ["serve", "--hostname", "127.0.0.1", "--port", String(port)], {
|
|
260
|
+
detached: true,
|
|
261
|
+
stdio: "ignore"
|
|
262
|
+
});
|
|
263
|
+
child.unref();
|
|
264
|
+
const serverUrl = `http://127.0.0.1:${port}`;
|
|
265
|
+
const healthy = await waitForHealth(serverUrl, 1e4);
|
|
266
|
+
const entry = {
|
|
267
|
+
project_id: input.project_id,
|
|
268
|
+
repo_root: input.repo_root,
|
|
269
|
+
opencode_server_url: serverUrl,
|
|
270
|
+
pid: child.pid,
|
|
271
|
+
status: healthy ? "running" : "unknown",
|
|
272
|
+
last_event_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
273
|
+
idle_timeout_ms: input.idle_timeout_ms ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
274
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
275
|
+
};
|
|
276
|
+
const result = upsertServeRegistry(entry);
|
|
277
|
+
return { reused: false, entry, healthy, registryPath: result.path };
|
|
278
|
+
}
|
|
279
|
+
function markServeStopped(projectId) {
|
|
280
|
+
const registry = normalizeServeRegistry(readServeRegistry());
|
|
281
|
+
const entry = registry.entries.find((x) => x.project_id === projectId);
|
|
282
|
+
if (!entry) return { ok: false, error: "Project entry not found" };
|
|
283
|
+
entry.status = "stopped";
|
|
284
|
+
entry.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
285
|
+
const path = writeServeRegistryFile(registry);
|
|
286
|
+
return { ok: true, path, entry, registry };
|
|
287
|
+
}
|
|
288
|
+
function shutdownServe(entry) {
|
|
289
|
+
if (entry.pid) {
|
|
290
|
+
try {
|
|
291
|
+
process.kill(entry.pid, "SIGTERM");
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return markServeStopped(entry.project_id);
|
|
296
|
+
}
|
|
297
|
+
function resolveServerUrl(cfg, params) {
|
|
298
|
+
return asString(params?.opencodeServerUrl) || asString(params?.serverUrl) || getRuntimeConfig(cfg).opencodeServerUrl || "http://127.0.0.1:4096";
|
|
299
|
+
}
|
|
300
|
+
async function fetchJsonSafe(url) {
|
|
301
|
+
try {
|
|
302
|
+
const response = await fetch(url);
|
|
303
|
+
const text = await response.text();
|
|
304
|
+
let data = void 0;
|
|
305
|
+
try {
|
|
306
|
+
data = text ? JSON.parse(text) : void 0;
|
|
307
|
+
} catch {
|
|
308
|
+
data = text;
|
|
309
|
+
}
|
|
310
|
+
return { ok: response.ok, status: response.status, data };
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return { ok: false, error: error?.message || String(error) };
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function resolveSessionForRun(input) {
|
|
316
|
+
const artifactEnvelope = input.runStatus?.envelope;
|
|
317
|
+
const resolved = resolveSessionId({
|
|
318
|
+
explicitSessionId: input.sessionId,
|
|
319
|
+
runId: input.runId || input.runStatus?.runId,
|
|
320
|
+
taskId: input.runStatus?.taskId,
|
|
321
|
+
sessionKey: artifactEnvelope?.session_key,
|
|
322
|
+
artifactSessionId: (typeof artifactEnvelope?.session_id === "string" ? artifactEnvelope.session_id : void 0) || (typeof artifactEnvelope?.sessionId === "string" ? artifactEnvelope.sessionId : void 0),
|
|
323
|
+
sessionList: input.sessionList
|
|
324
|
+
});
|
|
325
|
+
return { sessionId: resolved.sessionId, strategy: resolved.strategy, ...resolved.score !== void 0 ? { score: resolved.score } : {} };
|
|
326
|
+
}
|
|
327
|
+
async function collectSseEvents(serverUrl, scope, options) {
|
|
328
|
+
const eventPath = scope === "session" ? "/event" : "/global/event";
|
|
329
|
+
const limit = Math.max(1, asNumber(options?.limit) || DEFAULT_EVENT_LIMIT);
|
|
330
|
+
const timeoutMs = Math.max(200, asNumber(options?.timeoutMs) || DEFAULT_OBS_TIMEOUT_MS);
|
|
331
|
+
const controller = new AbortController();
|
|
332
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
333
|
+
const events = [];
|
|
334
|
+
try {
|
|
335
|
+
const response = await fetch(`${serverUrl.replace(/\/$/, "")}${eventPath}`, {
|
|
336
|
+
headers: { Accept: "text/event-stream" },
|
|
337
|
+
signal: controller.signal
|
|
338
|
+
});
|
|
339
|
+
if (!response.ok || !response.body) return events;
|
|
340
|
+
const reader = response.body.getReader();
|
|
341
|
+
const decoder = new TextDecoder();
|
|
342
|
+
let buffer = "";
|
|
343
|
+
while (events.length < limit) {
|
|
344
|
+
const chunk = await reader.read();
|
|
345
|
+
if (chunk.done) break;
|
|
346
|
+
buffer += decoder.decode(chunk.value, { stream: true });
|
|
347
|
+
const parsed = parseSseFramesFromBuffer(buffer);
|
|
348
|
+
buffer = parsed.remainder;
|
|
349
|
+
for (const frame of parsed.frames) {
|
|
350
|
+
const typed = normalizeTypedEventV1(frame, scope);
|
|
351
|
+
events.push({
|
|
352
|
+
index: events.length,
|
|
353
|
+
scope,
|
|
354
|
+
rawLine: frame.raw,
|
|
355
|
+
data: typed.payload,
|
|
356
|
+
normalizedKind: typed.kind,
|
|
357
|
+
summary: typed.summary,
|
|
358
|
+
runId: typed.runId || options?.runIdHint,
|
|
359
|
+
taskId: typed.taskId || options?.taskIdHint,
|
|
360
|
+
sessionId: typed.sessionId || options?.sessionIdHint,
|
|
361
|
+
typedEvent: typed,
|
|
362
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
363
|
+
});
|
|
364
|
+
if (events.length >= limit) break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (events.length < limit && buffer.trim()) {
|
|
368
|
+
const tail = parseSseFramesFromBuffer(`${buffer}
|
|
369
|
+
|
|
370
|
+
`);
|
|
371
|
+
for (const frame of tail.frames) {
|
|
372
|
+
const typed = normalizeTypedEventV1(frame, scope);
|
|
373
|
+
events.push({
|
|
374
|
+
index: events.length,
|
|
375
|
+
scope,
|
|
376
|
+
rawLine: frame.raw,
|
|
377
|
+
data: typed.payload,
|
|
378
|
+
normalizedKind: typed.kind,
|
|
379
|
+
summary: typed.summary,
|
|
380
|
+
runId: typed.runId || options?.runIdHint,
|
|
381
|
+
taskId: typed.taskId || options?.taskIdHint,
|
|
382
|
+
sessionId: typed.sessionId || options?.sessionIdHint,
|
|
383
|
+
typedEvent: typed,
|
|
384
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
385
|
+
});
|
|
386
|
+
if (events.length >= limit) break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
controller.abort();
|
|
391
|
+
} catch {
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
await reader.cancel();
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
return events;
|
|
398
|
+
} catch {
|
|
399
|
+
return events;
|
|
400
|
+
} finally {
|
|
401
|
+
clearTimeout(timeout);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function buildHookPolicyChecklist(agentId, sessionKey) {
|
|
405
|
+
return {
|
|
406
|
+
callbackPrimary: "/hooks/agent",
|
|
407
|
+
requirements: {
|
|
408
|
+
hooksEnabled: true,
|
|
409
|
+
allowRequestSessionKey: true,
|
|
410
|
+
allowedAgentIdsMustInclude: agentId,
|
|
411
|
+
allowedSessionKeyPrefixesMustInclude: HOOK_PREFIX,
|
|
412
|
+
deliverDefault: false
|
|
413
|
+
},
|
|
414
|
+
sessionKey,
|
|
415
|
+
suggestedConfig: {
|
|
416
|
+
hooks: {
|
|
417
|
+
enabled: true,
|
|
418
|
+
allowRequestSessionKey: true,
|
|
419
|
+
allowedAgentIds: [agentId],
|
|
420
|
+
allowedSessionKeyPrefixes: [HOOK_PREFIX]
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// src/registrar.ts
|
|
427
|
+
function registerOpenCodeBridgeTools(api, cfg) {
|
|
428
|
+
console.log("[opencode-bridge] scaffold loaded");
|
|
429
|
+
console.log(`[opencode-bridge] opencodeServerUrl=${cfg.opencodeServerUrl || "(unset)"}`);
|
|
430
|
+
console.log("[opencode-bridge] registering opencode_* tool set");
|
|
431
|
+
api.registerTool({
|
|
432
|
+
name: "opencode_status",
|
|
433
|
+
label: "OpenCode Status",
|
|
434
|
+
description: "Hi\u1EC3n th\u1ECB contract hi\u1EC7n t\u1EA1i c\u1EE7a OpenCode bridge: sessionKey convention, routing envelope schema, registry, lifecycle state skeleton v\xE0 assumption 1 project = 1 serve.",
|
|
435
|
+
parameters: { type: "object", properties: {} },
|
|
436
|
+
async execute() {
|
|
437
|
+
const runtimeCfg = getRuntimeConfig(cfg);
|
|
438
|
+
const registry = normalizeRegistry(runtimeCfg.projectRegistry);
|
|
439
|
+
return {
|
|
440
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, pluginId: "opencode-bridge", version: "0.1.0", assumption: "1 project = 1 opencode serve instance", sessionKeyConvention: "hook:opencode:<agentId>:<taskId>", lifecycleStates: ["queued", "server_ready", "session_created", "prompt_sent", "running", "awaiting_permission", "stalled", "failed", "completed"], requiredEnvelopeFields: ["task_id", "run_id", "agent_id", "session_key", "origin_session_key", "project_id", "repo_root", "opencode_server_url"], callbackPrimary: "/hooks/agent", callbackNotPrimary: ["/hooks/wake", "cron", "group:sessions"], config: { bridgeConfigPath: getBridgeConfigPath(), opencodeServerUrl: runtimeCfg.opencodeServerUrl || null, hookBaseUrl: runtimeCfg.hookBaseUrl || null, hookTokenPresent: Boolean(runtimeCfg.hookToken), projectRegistry: registry, stateDir: getBridgeStateDir(), runStateDir: getRunStateDir(), auditDir: getAuditDir() }, note: "Runtime-ops scaffold in progress. Plugin-owned config/state is stored under ~/.openclaw/opencode-bridge. New projects are auto-registered only when using opencode_serve_spawn (not by passive envelope build alone)." }, null, 2) }]
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
}, { optional: true });
|
|
444
|
+
api.registerTool({
|
|
445
|
+
name: "opencode_resolve_project",
|
|
446
|
+
label: "OpenCode Resolve Project",
|
|
447
|
+
description: "Resolve project registry entry theo projectId ho\u1EB7c repoRoot, \xE1p d\u1EE5ng assumption 1 project = 1 serve instance.",
|
|
448
|
+
parameters: { type: "object", properties: { projectId: { type: "string" }, repoRoot: { type: "string" } } },
|
|
449
|
+
async execute(_id, params) {
|
|
450
|
+
const entry = findRegistryEntry(cfg, params?.projectId, params?.repoRoot);
|
|
451
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, match: entry || null }, null, 2) }] };
|
|
452
|
+
}
|
|
453
|
+
}, { optional: true });
|
|
454
|
+
api.registerTool({
|
|
455
|
+
name: "opencode_build_envelope",
|
|
456
|
+
label: "OpenCode Build Envelope",
|
|
457
|
+
description: "D\u1EF1ng routing envelope chu\u1EA9n cho task delegate sang OpenCode v\u1EDBi sessionKey convention hook:opencode:<agentId>:<taskId>.",
|
|
458
|
+
parameters: { type: "object", properties: { taskId: { type: "string" }, runId: { type: "string" }, agentId: { type: "string" }, originSessionKey: { type: "string" }, projectId: { type: "string" }, repoRoot: { type: "string" }, channel: { type: "string" }, to: { type: "string" }, deliver: { type: "boolean" }, priority: { type: "string" } }, required: ["taskId", "runId", "agentId", "originSessionKey", "projectId", "repoRoot"] },
|
|
459
|
+
async execute(_id, params) {
|
|
460
|
+
const entry = findRegistryEntry(cfg, params?.projectId, params?.repoRoot);
|
|
461
|
+
const serverUrl = entry?.serverUrl;
|
|
462
|
+
if (!serverUrl) {
|
|
463
|
+
return {
|
|
464
|
+
isError: true,
|
|
465
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing project registry mapping. Use opencode_serve_spawn for the project or add a matching projectRegistry entry in ~/.openclaw/opencode-bridge/config.json first." }, null, 2) }]
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const envelope = buildEnvelope({
|
|
469
|
+
taskId: params.taskId,
|
|
470
|
+
runId: params.runId,
|
|
471
|
+
agentId: params.agentId,
|
|
472
|
+
originSessionKey: params.originSessionKey,
|
|
473
|
+
projectId: params.projectId,
|
|
474
|
+
repoRoot: params.repoRoot,
|
|
475
|
+
serverUrl,
|
|
476
|
+
channel: params.channel,
|
|
477
|
+
to: params.to,
|
|
478
|
+
deliver: params.deliver,
|
|
479
|
+
priority: params.priority
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, envelope, registryMatch: entry || null }, null, 2) }]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}, { optional: true });
|
|
486
|
+
api.registerTool({
|
|
487
|
+
name: "opencode_check_hook_policy",
|
|
488
|
+
label: "OpenCode Check Hook Policy",
|
|
489
|
+
description: "Ki\u1EC3m tra checklist/policy t\u1ED1i thi\u1EC3u cho callback `/hooks/agent` v\u1EDBi agentId v\xE0 sessionKey c\u1EE5 th\u1EC3.",
|
|
490
|
+
parameters: { type: "object", properties: { agentId: { type: "string" }, sessionKey: { type: "string" } }, required: ["agentId", "sessionKey"] },
|
|
491
|
+
async execute(_id, params) {
|
|
492
|
+
const checklist = buildHookPolicyChecklist(params.agentId, params.sessionKey);
|
|
493
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, checklist }, null, 2) }] };
|
|
494
|
+
}
|
|
495
|
+
}, { optional: true });
|
|
496
|
+
api.registerTool({
|
|
497
|
+
name: "opencode_evaluate_lifecycle",
|
|
498
|
+
label: "OpenCode Evaluate Lifecycle",
|
|
499
|
+
description: "\u0110\xE1nh gi\xE1 lifecycle state hi\u1EC7n t\u1EA1i t\u1EEB event cu\u1ED1i c\xF9ng ho\u1EB7c th\u1EDDi gian im l\u1EB7ng \u0111\u1EC3 h\u1ED7 tr\u1EE3 stalled/permission/failure handling baseline.",
|
|
500
|
+
parameters: { type: "object", properties: { lastEventKind: { type: "string", enum: ["task.started", "task.progress", "permission.requested", "task.stalled", "task.failed", "task.completed"] }, lastEventAtMs: { type: "number" }, nowMs: { type: "number" }, softStallMs: { type: "number" }, hardStallMs: { type: "number" } } },
|
|
501
|
+
async execute(_id, params) {
|
|
502
|
+
const evaluation = evaluateLifecycle({ lastEventKind: params.lastEventKind, lastEventAtMs: asNumber(params.lastEventAtMs), nowMs: asNumber(params.nowMs), softStallMs: asNumber(params.softStallMs), hardStallMs: asNumber(params.hardStallMs) });
|
|
503
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, evaluation }, null, 2) }] };
|
|
504
|
+
}
|
|
505
|
+
}, { optional: true });
|
|
506
|
+
api.registerTool({
|
|
507
|
+
name: "opencode_run_status",
|
|
508
|
+
label: "OpenCode Run Status",
|
|
509
|
+
description: "Read-only run snapshot: h\u1EE3p nh\u1EA5t artifact run status local v\xE0 API snapshot t\u1EEB OpenCode serve (/global/health, /session, /session/status).",
|
|
510
|
+
parameters: {
|
|
511
|
+
type: "object",
|
|
512
|
+
properties: {
|
|
513
|
+
runId: { type: "string" },
|
|
514
|
+
sessionId: { type: "string" },
|
|
515
|
+
opencodeServerUrl: { type: "string" }
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
async execute(_id, params) {
|
|
519
|
+
const serverUrl = resolveServerUrl(cfg, params);
|
|
520
|
+
const runId = asString(params?.runId);
|
|
521
|
+
const artifact = runId ? readRunStatus(runId) : null;
|
|
522
|
+
const [healthRes, sessionRes, sessionStatusRes] = await Promise.all([
|
|
523
|
+
fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/global/health`),
|
|
524
|
+
fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/session`),
|
|
525
|
+
fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/session/status`)
|
|
526
|
+
]);
|
|
527
|
+
const sessionList = Array.isArray(sessionRes.data) ? sessionRes.data : [];
|
|
528
|
+
const resolution = resolveSessionForRun({
|
|
529
|
+
sessionId: asString(params?.sessionId),
|
|
530
|
+
runStatus: artifact,
|
|
531
|
+
sessionList,
|
|
532
|
+
runId
|
|
533
|
+
});
|
|
534
|
+
const sessionId = resolution.sessionId;
|
|
535
|
+
const state = artifact?.state || (sessionId ? "running" : "queued");
|
|
536
|
+
const response = {
|
|
537
|
+
ok: true,
|
|
538
|
+
source: {
|
|
539
|
+
runStatusArtifact: Boolean(artifact),
|
|
540
|
+
opencodeApi: true
|
|
541
|
+
},
|
|
542
|
+
runId: runId || void 0,
|
|
543
|
+
taskId: artifact?.taskId,
|
|
544
|
+
projectId: artifact?.envelope?.project_id,
|
|
545
|
+
sessionId,
|
|
546
|
+
correlation: {
|
|
547
|
+
sessionResolution: {
|
|
548
|
+
strategy: resolution.strategy,
|
|
549
|
+
...resolution.score !== void 0 ? { score: resolution.score } : {}
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
state,
|
|
553
|
+
lastEvent: artifact?.lastEvent,
|
|
554
|
+
lastSummary: artifact?.lastSummary,
|
|
555
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
556
|
+
timestamps: {
|
|
557
|
+
...artifact?.updatedAt ? { artifactUpdatedAt: artifact.updatedAt } : {},
|
|
558
|
+
apiFetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
559
|
+
},
|
|
560
|
+
health: {
|
|
561
|
+
ok: Boolean(healthRes.ok && (healthRes.data?.healthy === true || healthRes.status === 200)),
|
|
562
|
+
...asString(healthRes?.data?.version) ? { version: asString(healthRes?.data?.version) } : {}
|
|
563
|
+
},
|
|
564
|
+
apiSnapshot: {
|
|
565
|
+
health: healthRes.data,
|
|
566
|
+
sessionList,
|
|
567
|
+
sessionStatus: sessionStatusRes.data,
|
|
568
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
569
|
+
},
|
|
570
|
+
...artifact ? {} : { note: "No local run artifact found for runId. Returned API-only snapshot." }
|
|
571
|
+
};
|
|
572
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
|
|
573
|
+
}
|
|
574
|
+
}, { optional: true });
|
|
575
|
+
api.registerTool({
|
|
576
|
+
name: "opencode_run_events",
|
|
577
|
+
label: "OpenCode Run Events",
|
|
578
|
+
description: "Read-only event probe: l\u1EA5y SSE event t\u1EEB /event ho\u1EB7c /global/event, normalize s\u01A1 b\u1ED9 v\u1EC1 OpenCodeEventKind.",
|
|
579
|
+
parameters: {
|
|
580
|
+
type: "object",
|
|
581
|
+
properties: {
|
|
582
|
+
scope: { type: "string", enum: ["session", "global"] },
|
|
583
|
+
limit: { type: "number" },
|
|
584
|
+
timeoutMs: { type: "number" },
|
|
585
|
+
runId: { type: "string" },
|
|
586
|
+
sessionId: { type: "string" },
|
|
587
|
+
opencodeServerUrl: { type: "string" }
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
async execute(_id, params) {
|
|
591
|
+
const serverUrl = resolveServerUrl(cfg, params);
|
|
592
|
+
const runId = asString(params?.runId);
|
|
593
|
+
const artifact = runId ? readRunStatus(runId) : null;
|
|
594
|
+
const scope = params?.scope === "global" ? "global" : "session";
|
|
595
|
+
const timeoutMs = Math.max(200, asNumber(params?.timeoutMs) || DEFAULT_OBS_TIMEOUT_MS);
|
|
596
|
+
const limit = Math.max(1, asNumber(params?.limit) || DEFAULT_EVENT_LIMIT);
|
|
597
|
+
const sessionListRes = await fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/session`);
|
|
598
|
+
const sessionList = Array.isArray(sessionListRes.data) ? sessionListRes.data : [];
|
|
599
|
+
const resolution = resolveSessionForRun({
|
|
600
|
+
sessionId: asString(params?.sessionId),
|
|
601
|
+
runStatus: artifact,
|
|
602
|
+
sessionList,
|
|
603
|
+
runId
|
|
604
|
+
});
|
|
605
|
+
const sessionId = resolution.sessionId;
|
|
606
|
+
const events = await collectSseEvents(serverUrl, scope, {
|
|
607
|
+
limit,
|
|
608
|
+
timeoutMs,
|
|
609
|
+
runIdHint: runId,
|
|
610
|
+
taskIdHint: artifact?.taskId,
|
|
611
|
+
sessionIdHint: sessionId
|
|
612
|
+
});
|
|
613
|
+
const response = {
|
|
614
|
+
ok: true,
|
|
615
|
+
...runId ? { runId } : {},
|
|
616
|
+
...artifact?.taskId ? { taskId: artifact.taskId } : {},
|
|
617
|
+
...sessionId ? { sessionId } : {},
|
|
618
|
+
correlation: {
|
|
619
|
+
sessionResolution: {
|
|
620
|
+
strategy: resolution.strategy,
|
|
621
|
+
...resolution.score !== void 0 ? { score: resolution.score } : {}
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
scope,
|
|
625
|
+
schemaVersion: "opencode.event.v1",
|
|
626
|
+
eventPath: scope === "global" ? "/global/event" : "/event",
|
|
627
|
+
eventCount: events.length,
|
|
628
|
+
events,
|
|
629
|
+
truncated: events.length >= limit,
|
|
630
|
+
timeoutMs
|
|
631
|
+
};
|
|
632
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
|
|
633
|
+
}
|
|
634
|
+
}, { optional: true });
|
|
635
|
+
api.registerTool({
|
|
636
|
+
name: "opencode_session_tail",
|
|
637
|
+
label: "OpenCode Session Tail",
|
|
638
|
+
description: "Read-only session tail: \u0111\u1ECDc message tail t\u1EEB /session/{id}/message v\xE0 optional diff t\u1EEB /session/{id}/diff.",
|
|
639
|
+
parameters: {
|
|
640
|
+
type: "object",
|
|
641
|
+
properties: {
|
|
642
|
+
sessionId: { type: "string" },
|
|
643
|
+
runId: { type: "string" },
|
|
644
|
+
limit: { type: "number" },
|
|
645
|
+
includeDiff: { type: "boolean" },
|
|
646
|
+
opencodeServerUrl: { type: "string" }
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
async execute(_id, params) {
|
|
650
|
+
const serverUrl = resolveServerUrl(cfg, params);
|
|
651
|
+
const runId = asString(params?.runId);
|
|
652
|
+
const artifact = runId ? readRunStatus(runId) : null;
|
|
653
|
+
const sessionListRes = await fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/session`);
|
|
654
|
+
const sessionList = Array.isArray(sessionListRes.data) ? sessionListRes.data : [];
|
|
655
|
+
const resolution = resolveSessionForRun({
|
|
656
|
+
sessionId: asString(params?.sessionId),
|
|
657
|
+
runStatus: artifact,
|
|
658
|
+
sessionList,
|
|
659
|
+
runId
|
|
660
|
+
});
|
|
661
|
+
const sessionId = resolution.sessionId;
|
|
662
|
+
if (!sessionId) {
|
|
663
|
+
return {
|
|
664
|
+
isError: true,
|
|
665
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Missing sessionId and could not resolve from run artifact/session list." }, null, 2) }]
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const limit = Math.max(1, asNumber(params?.limit) || DEFAULT_TAIL_LIMIT);
|
|
669
|
+
const includeDiff = params?.includeDiff !== false;
|
|
670
|
+
const [messagesRes, diffRes, sessionRes] = await Promise.all([
|
|
671
|
+
fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/session/${sessionId}/message`),
|
|
672
|
+
includeDiff ? fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/session/${sessionId}/diff`) : Promise.resolve({ ok: false, data: void 0 }),
|
|
673
|
+
fetchJsonSafe(`${serverUrl.replace(/\/$/, "")}/session/${sessionId}`)
|
|
674
|
+
]);
|
|
675
|
+
const rawMessages = Array.isArray(messagesRes.data) ? messagesRes.data : [];
|
|
676
|
+
const tail = rawMessages.slice(Math.max(0, rawMessages.length - limit)).map((msg, idx) => {
|
|
677
|
+
const info = msg?.info || {};
|
|
678
|
+
const parts = Array.isArray(msg?.parts) ? msg.parts : [];
|
|
679
|
+
const text = parts.filter((p) => p?.type === "text" && typeof p?.text === "string").map((p) => p.text).join("\n");
|
|
680
|
+
return {
|
|
681
|
+
index: idx,
|
|
682
|
+
role: asString(info.role),
|
|
683
|
+
text: text || void 0,
|
|
684
|
+
createdAt: info?.time?.created,
|
|
685
|
+
id: asString(info.id),
|
|
686
|
+
agent: asString(info.agent),
|
|
687
|
+
model: asString(info?.model?.modelID),
|
|
688
|
+
raw: msg
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
const response = {
|
|
692
|
+
ok: true,
|
|
693
|
+
sessionId,
|
|
694
|
+
...runId ? { runId } : {},
|
|
695
|
+
...artifact?.taskId ? { taskId: artifact.taskId } : {},
|
|
696
|
+
correlation: {
|
|
697
|
+
sessionResolution: {
|
|
698
|
+
strategy: resolution.strategy,
|
|
699
|
+
...resolution.score !== void 0 ? { score: resolution.score } : {}
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
limit,
|
|
703
|
+
totalMessages: rawMessages.length,
|
|
704
|
+
messages: tail,
|
|
705
|
+
...includeDiff ? { diff: diffRes.data } : {},
|
|
706
|
+
latestSummary: sessionRes.data,
|
|
707
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
708
|
+
};
|
|
709
|
+
return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }] };
|
|
710
|
+
}
|
|
711
|
+
}, { optional: true });
|
|
712
|
+
api.registerTool({
|
|
713
|
+
name: "opencode_serve_spawn",
|
|
714
|
+
label: "OpenCode Serve Spawn",
|
|
715
|
+
description: "B\u1EADt m\u1ED9t opencode serve ri\xEAng cho project, t\u1EF1 c\u1EA5p port \u0111\u1ED9ng v\xE0 ghi registry entry t\u01B0\u01A1ng \u1EE9ng.",
|
|
716
|
+
parameters: {
|
|
717
|
+
type: "object",
|
|
718
|
+
properties: {
|
|
719
|
+
project_id: { type: "string" },
|
|
720
|
+
repo_root: { type: "string" },
|
|
721
|
+
idle_timeout_ms: { type: "number" }
|
|
722
|
+
},
|
|
723
|
+
required: ["project_id", "repo_root"]
|
|
724
|
+
},
|
|
725
|
+
async execute(_id, params) {
|
|
726
|
+
const result = await spawnServeForProject({
|
|
727
|
+
project_id: params.project_id,
|
|
728
|
+
repo_root: params.repo_root,
|
|
729
|
+
idle_timeout_ms: asNumber(params.idle_timeout_ms)
|
|
730
|
+
});
|
|
731
|
+
return {
|
|
732
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, ...result }, null, 2) }]
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
}, { optional: true });
|
|
736
|
+
api.registerTool({
|
|
737
|
+
name: "opencode_registry_get",
|
|
738
|
+
label: "OpenCode Registry Get",
|
|
739
|
+
description: "\u0110\u1ECDc serve registry hi\u1EC7n t\u1EA1i c\u1EE7a OpenCode bridge \u0111\u1EC3 xem mapping project -> serve URL -> pid -> status.",
|
|
740
|
+
parameters: { type: "object", properties: {} },
|
|
741
|
+
async execute() {
|
|
742
|
+
const registry = readServeRegistry();
|
|
743
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, path: getServeRegistryPath(), registry }, null, 2) }] };
|
|
744
|
+
}
|
|
745
|
+
}, { optional: true });
|
|
746
|
+
api.registerTool({
|
|
747
|
+
name: "opencode_registry_upsert",
|
|
748
|
+
label: "OpenCode Registry Upsert",
|
|
749
|
+
description: "Ghi ho\u1EB7c c\u1EADp nh\u1EADt m\u1ED9t serve registry entry cho project hi\u1EC7n t\u1EA1i (1 project = 1 serve).",
|
|
750
|
+
parameters: { type: "object", properties: { project_id: { type: "string" }, repo_root: { type: "string" }, opencode_server_url: { type: "string" }, pid: { type: "number" }, status: { type: "string", enum: ["running", "stopped", "unknown"] }, last_event_at: { type: "string" }, idle_timeout_ms: { type: "number" } }, required: ["project_id", "repo_root", "opencode_server_url"] },
|
|
751
|
+
async execute(_id, params) {
|
|
752
|
+
const entry = { project_id: params.project_id, repo_root: params.repo_root, opencode_server_url: params.opencode_server_url, ...params.pid !== void 0 ? { pid: Number(params.pid) } : {}, ...params.status ? { status: params.status } : {}, ...params.last_event_at ? { last_event_at: params.last_event_at } : {}, ...params.idle_timeout_ms !== void 0 ? { idle_timeout_ms: Number(params.idle_timeout_ms) } : {}, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
753
|
+
const result = upsertServeRegistry(entry);
|
|
754
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, path: result.path, entry, registry: result.registry }, null, 2) }] };
|
|
755
|
+
}
|
|
756
|
+
}, { optional: true });
|
|
757
|
+
api.registerTool({
|
|
758
|
+
name: "opencode_registry_cleanup",
|
|
759
|
+
label: "OpenCode Registry Cleanup",
|
|
760
|
+
description: "Cleanup/normalize serve registry: lo\u1EA1i b\u1ECF entry kh\xF4ng \u0111\u1EE7 field ho\u1EB7c normalize schema l\u01B0u tr\u1EEF hi\u1EC7n t\u1EA1i.",
|
|
761
|
+
parameters: { type: "object", properties: {} },
|
|
762
|
+
async execute() {
|
|
763
|
+
const before = readServeRegistry();
|
|
764
|
+
const normalized = normalizeServeRegistry(before);
|
|
765
|
+
const path = writeServeRegistryFile(normalized);
|
|
766
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, path, before, after: normalized }, null, 2) }] };
|
|
767
|
+
}
|
|
768
|
+
}, { optional: true });
|
|
769
|
+
api.registerTool({
|
|
770
|
+
name: "opencode_serve_shutdown",
|
|
771
|
+
label: "OpenCode Serve Shutdown",
|
|
772
|
+
description: "\u0110\xE1nh d\u1EA5u stopped v\xE0 g\u1EEDi SIGTERM cho serve c\u1EE7a m\u1ED9t project n\u1EBFu registry c\xF3 pid.",
|
|
773
|
+
parameters: { type: "object", properties: { project_id: { type: "string" } }, required: ["project_id"] },
|
|
774
|
+
async execute(_id, params) {
|
|
775
|
+
const registry = normalizeServeRegistry(readServeRegistry());
|
|
776
|
+
const entry = registry.entries.find((x) => x.project_id === params.project_id);
|
|
777
|
+
if (!entry) {
|
|
778
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Project entry not found" }, null, 2) }] };
|
|
779
|
+
}
|
|
780
|
+
const result = shutdownServe(entry);
|
|
781
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], ...result.ok ? {} : { isError: true } };
|
|
782
|
+
}
|
|
783
|
+
}, { optional: true });
|
|
784
|
+
api.registerTool({
|
|
785
|
+
name: "opencode_serve_idle_check",
|
|
786
|
+
label: "OpenCode Serve Idle Check",
|
|
787
|
+
description: "\u0110\xE1nh gi\xE1 m\u1ED9t serve registry entry c\xF3 n\xEAn shutdown theo idle timeout hay ch\u01B0a.",
|
|
788
|
+
parameters: { type: "object", properties: { project_id: { type: "string" }, nowMs: { type: "number" } }, required: ["project_id"] },
|
|
789
|
+
async execute(_id, params) {
|
|
790
|
+
const registry = normalizeServeRegistry(readServeRegistry());
|
|
791
|
+
const entry = registry.entries.find((x) => x.project_id === params.project_id);
|
|
792
|
+
if (!entry) {
|
|
793
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, error: "Project entry not found" }, null, 2) }] };
|
|
794
|
+
}
|
|
795
|
+
const evaluation = evaluateServeIdle(entry, asNumber(params.nowMs));
|
|
796
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, entry, evaluation }, null, 2) }] };
|
|
797
|
+
}
|
|
798
|
+
}, { optional: true });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/index.ts
|
|
802
|
+
var plugin = {
|
|
803
|
+
id: "opencode-bridge",
|
|
804
|
+
name: "OpenCode Bridge",
|
|
805
|
+
version: "0.1.0",
|
|
806
|
+
register(api) {
|
|
807
|
+
const cfg = api?.pluginConfig || {};
|
|
808
|
+
registerOpenCodeBridgeTools(api, cfg);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
var index_default = plugin;
|
|
812
|
+
export {
|
|
813
|
+
index_default as default
|
|
814
|
+
};
|