@mrc2204/opencode-bridge 0.1.2 → 0.1.3
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 +73 -94
- package/README.md +73 -94
- package/dist/opencode-plugin/openclaw-bridge-callback.d.ts +5 -0
- package/dist/opencode-plugin/openclaw-bridge-callback.js +179 -0
- package/dist/{chunk-LCJRXKI3.js → src/chunk-OVQ5X54C.js} +4 -3
- package/dist/src/chunk-TDVN5AFB.js +36 -0
- package/dist/{index.js → src/index.js} +480 -14
- package/dist/{observability.d.ts → src/observability.d.ts} +1 -0
- package/dist/{observability.js → src/observability.js} +1 -1
- package/dist/src/shared-contracts.d.ts +33 -0
- package/dist/src/shared-contracts.js +10 -0
- package/openclaw.plugin.json +2 -2
- package/opencode-plugin/README.md +25 -0
- package/opencode-plugin/openclaw-bridge-callback.ts +186 -0
- package/package.json +17 -9
- package/scripts/install-bridge.mjs +60 -0
- package/scripts/materialize-opencode-plugin.mjs +75 -0
- package/skills/opencode-orchestration/SKILL.md +76 -66
- package/src/shared-contracts.ts +58 -0
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
|
@@ -3,10 +3,13 @@ import {
|
|
|
3
3
|
parseSseFramesFromBuffer,
|
|
4
4
|
resolveSessionId,
|
|
5
5
|
summarizeLifecycle
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-OVQ5X54C.js";
|
|
7
|
+
import {
|
|
8
|
+
buildTaggedSessionTitle
|
|
9
|
+
} from "./chunk-TDVN5AFB.js";
|
|
7
10
|
|
|
8
11
|
// src/runtime.ts
|
|
9
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
|
|
10
13
|
import { join } from "path";
|
|
11
14
|
import { spawn } from "child_process";
|
|
12
15
|
import { createServer } from "net";
|
|
@@ -183,6 +186,30 @@ function mapEventToState(event) {
|
|
|
183
186
|
return "running";
|
|
184
187
|
}
|
|
185
188
|
}
|
|
189
|
+
function assertCallbackTargetSessionKey(envelope) {
|
|
190
|
+
const callbackTargetSessionKey = asString(envelope?.callback_target_session_key);
|
|
191
|
+
if (!callbackTargetSessionKey) {
|
|
192
|
+
throw new Error("Invalid routing envelope: missing callback_target_session_key (required for current-session callback integrity)");
|
|
193
|
+
}
|
|
194
|
+
return callbackTargetSessionKey;
|
|
195
|
+
}
|
|
196
|
+
function buildHooksAgentCallback(input) {
|
|
197
|
+
const state = mapEventToState(input.event);
|
|
198
|
+
const message = input.summary || `OpenCode event=${input.event} state=${state} task=${input.envelope.task_id} run=${input.envelope.run_id} project=${input.envelope.project_id} requestedAgent=${input.envelope.requested_agent_id} resolvedAgent=${input.envelope.resolved_agent_id}`;
|
|
199
|
+
const callbackTargetSessionKey = assertCallbackTargetSessionKey(input.envelope);
|
|
200
|
+
return {
|
|
201
|
+
message,
|
|
202
|
+
name: "OpenCode",
|
|
203
|
+
// callback should wake requester/origin lane, not execution lane
|
|
204
|
+
agentId: input.envelope.requested_agent_id,
|
|
205
|
+
sessionKey: callbackTargetSessionKey,
|
|
206
|
+
...input.envelope.callback_target_session_id ? { sessionId: input.envelope.callback_target_session_id } : {},
|
|
207
|
+
wakeMode: "now",
|
|
208
|
+
deliver: false,
|
|
209
|
+
...input.envelope.channel ? { channel: input.envelope.channel } : {},
|
|
210
|
+
...input.envelope.to ? { to: input.envelope.to } : {}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
186
213
|
function evaluateLifecycle(input) {
|
|
187
214
|
const nowMs = input.nowMs ?? Date.now();
|
|
188
215
|
const softStallMs = input.softStallMs ?? DEFAULT_SOFT_STALL_MS;
|
|
@@ -216,14 +243,46 @@ function evaluateLifecycle(input) {
|
|
|
216
243
|
function getRunStateDir() {
|
|
217
244
|
return join(getBridgeStateDir(), "runs");
|
|
218
245
|
}
|
|
246
|
+
function writeRunStatus(status) {
|
|
247
|
+
const dir = getRunStateDir();
|
|
248
|
+
mkdirSync(dir, { recursive: true });
|
|
249
|
+
const path = join(dir, `${status.runId}.json`);
|
|
250
|
+
writeFileSync(path, JSON.stringify(status, null, 2), "utf8");
|
|
251
|
+
return path;
|
|
252
|
+
}
|
|
219
253
|
function readRunStatus(runId) {
|
|
220
254
|
const path = join(getRunStateDir(), `${runId}.json`);
|
|
221
255
|
if (!existsSync(path)) return null;
|
|
222
256
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
223
257
|
}
|
|
258
|
+
function patchRunStatus(runId, patch) {
|
|
259
|
+
const current = readRunStatus(runId);
|
|
260
|
+
if (!current) return null;
|
|
261
|
+
const next = typeof patch === "function" ? patch(current) : { ...current, ...patch };
|
|
262
|
+
writeRunStatus(next);
|
|
263
|
+
return next;
|
|
264
|
+
}
|
|
265
|
+
function listRunStatuses() {
|
|
266
|
+
const dir = getRunStateDir();
|
|
267
|
+
if (!existsSync(dir)) return [];
|
|
268
|
+
return readdirSync(dir).filter((name) => name.endsWith(".json")).map((name) => {
|
|
269
|
+
try {
|
|
270
|
+
return JSON.parse(readFileSync(join(dir, name), "utf8"));
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}).filter(Boolean);
|
|
275
|
+
}
|
|
224
276
|
function getAuditDir() {
|
|
225
277
|
return join(getBridgeStateDir(), "audit");
|
|
226
278
|
}
|
|
279
|
+
function appendAudit(record) {
|
|
280
|
+
const dir = getAuditDir();
|
|
281
|
+
mkdirSync(dir, { recursive: true });
|
|
282
|
+
const path = join(dir, "callbacks.jsonl");
|
|
283
|
+
writeFileSync(path, JSON.stringify(record) + "\n", { encoding: "utf8", flag: "a" });
|
|
284
|
+
return path;
|
|
285
|
+
}
|
|
227
286
|
function getServeRegistryPath() {
|
|
228
287
|
return join(getBridgeStateDir(), "registry.json");
|
|
229
288
|
}
|
|
@@ -303,16 +362,43 @@ async function waitForHealth(serverUrl, timeoutMs = 1e4) {
|
|
|
303
362
|
}
|
|
304
363
|
return false;
|
|
305
364
|
}
|
|
365
|
+
async function fetchServeProjectBinding(serverUrl) {
|
|
366
|
+
const normalizedUrl = serverUrl.replace(/\/$/, "");
|
|
367
|
+
const [projectRes, pathRes] = await Promise.all([
|
|
368
|
+
fetchJsonSafe(`${normalizedUrl}/project/current`),
|
|
369
|
+
fetchJsonSafe(`${normalizedUrl}/path`)
|
|
370
|
+
]);
|
|
371
|
+
const project = projectRes.ok ? projectRes.data : void 0;
|
|
372
|
+
const path = pathRes.ok ? pathRes.data : void 0;
|
|
373
|
+
const directory = asString(project?.directory) || asString(project?.path) || asString(path?.directory) || asString(path?.cwd) || asString(path?.path);
|
|
374
|
+
if (directory) {
|
|
375
|
+
return { ok: true, directory, project, path };
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
project,
|
|
380
|
+
path,
|
|
381
|
+
error: projectRes.error || pathRes.error || "Could not determine serve project binding"
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async function isServeBoundToRepo(serverUrl, repoRoot) {
|
|
385
|
+
const binding = await fetchServeProjectBinding(serverUrl);
|
|
386
|
+
return binding.ok && binding.directory === repoRoot;
|
|
387
|
+
}
|
|
306
388
|
async function spawnServeForProject(input) {
|
|
307
389
|
const existing = normalizeServeRegistry(readServeRegistry()).entries.find((x) => x.project_id === input.project_id || x.repo_root === input.repo_root);
|
|
308
390
|
if (existing && existing.status === "running") {
|
|
309
391
|
const healthy2 = await waitForHealth(existing.opencode_server_url, 2e3);
|
|
310
392
|
if (healthy2) {
|
|
311
|
-
|
|
393
|
+
const boundToRepo = await isServeBoundToRepo(existing.opencode_server_url, input.repo_root);
|
|
394
|
+
if (boundToRepo) {
|
|
395
|
+
return { reused: true, entry: existing, registryPath: getServeRegistryPath() };
|
|
396
|
+
}
|
|
312
397
|
}
|
|
313
398
|
}
|
|
314
399
|
const port = await allocatePort();
|
|
315
400
|
const child = spawn("opencode", ["serve", "--hostname", "127.0.0.1", "--port", String(port)], {
|
|
401
|
+
cwd: input.repo_root,
|
|
316
402
|
detached: true,
|
|
317
403
|
stdio: "ignore"
|
|
318
404
|
});
|
|
@@ -475,9 +561,275 @@ async function collectSseEvents(serverUrl, scope, options) {
|
|
|
475
561
|
clearTimeout(timeout);
|
|
476
562
|
}
|
|
477
563
|
}
|
|
564
|
+
async function executeHooksAgentCallback(hookBaseUrl, hookToken, callback) {
|
|
565
|
+
const response = await fetch(`${hookBaseUrl.replace(/\/$/, "")}/hooks/agent`, {
|
|
566
|
+
method: "POST",
|
|
567
|
+
headers: {
|
|
568
|
+
Authorization: `Bearer ${hookToken}`,
|
|
569
|
+
"Content-Type": "application/json"
|
|
570
|
+
},
|
|
571
|
+
body: JSON.stringify(callback)
|
|
572
|
+
});
|
|
573
|
+
const text = await response.text();
|
|
574
|
+
return {
|
|
575
|
+
ok: response.ok,
|
|
576
|
+
status: response.status,
|
|
577
|
+
body: text
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function coerceLifecycleState(value) {
|
|
581
|
+
if (!value) return "running";
|
|
582
|
+
if (value === "queued" || value === "server_ready" || value === "session_created" || value === "prompt_sent" || value === "planning" || value === "coding" || value === "verifying" || value === "blocked" || value === "running" || value === "awaiting_permission" || value === "stalled" || value === "failed" || value === "completed") {
|
|
583
|
+
return value;
|
|
584
|
+
}
|
|
585
|
+
return "running";
|
|
586
|
+
}
|
|
587
|
+
function isTerminalLifecycleState(state) {
|
|
588
|
+
return state === "completed" || state === "failed";
|
|
589
|
+
}
|
|
590
|
+
function buildTerminalEventFromState(state, fallback) {
|
|
591
|
+
if (state === "completed") return "task.completed";
|
|
592
|
+
if (state === "failed") return "task.failed";
|
|
593
|
+
return fallback || "task.progress";
|
|
594
|
+
}
|
|
595
|
+
async function createSessionForEnvelope(envelope) {
|
|
596
|
+
const serverUrl = envelope.opencode_server_url.replace(/\/$/, "");
|
|
597
|
+
const taggedTitle = buildTaggedSessionTitle({
|
|
598
|
+
runId: envelope.run_id,
|
|
599
|
+
taskId: envelope.task_id,
|
|
600
|
+
requested: envelope.requested_agent_id,
|
|
601
|
+
resolved: envelope.resolved_agent_id,
|
|
602
|
+
callbackSession: envelope.callback_target_session_key,
|
|
603
|
+
callbackSessionId: envelope.callback_target_session_id,
|
|
604
|
+
projectId: envelope.project_id,
|
|
605
|
+
repoRoot: envelope.repo_root
|
|
606
|
+
});
|
|
607
|
+
const createdRes = await fetch(`${serverUrl}/session`, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers: { "Content-Type": "application/json" },
|
|
610
|
+
body: JSON.stringify({ title: taggedTitle })
|
|
611
|
+
});
|
|
612
|
+
const createdText = await createdRes.text();
|
|
613
|
+
let created;
|
|
614
|
+
try {
|
|
615
|
+
created = createdText ? JSON.parse(createdText) : null;
|
|
616
|
+
} catch {
|
|
617
|
+
created = { raw: createdText };
|
|
618
|
+
}
|
|
619
|
+
if (!createdRes.ok) {
|
|
620
|
+
throw new Error(`session create failed: ${createdRes.status} ${createdText}`);
|
|
621
|
+
}
|
|
622
|
+
const sessionId = asString(created?.id);
|
|
623
|
+
if (!sessionId) {
|
|
624
|
+
throw new Error("session create failed: missing session id");
|
|
625
|
+
}
|
|
626
|
+
return { sessionId, created };
|
|
627
|
+
}
|
|
628
|
+
async function sendPromptAsyncForEnvelope(envelope, sessionId, prompt) {
|
|
629
|
+
const serverUrl = envelope.opencode_server_url.replace(/\/$/, "");
|
|
630
|
+
const response = await fetch(`${serverUrl}/session/${sessionId}/prompt_async`, {
|
|
631
|
+
method: "POST",
|
|
632
|
+
headers: { "Content-Type": "application/json" },
|
|
633
|
+
body: JSON.stringify({
|
|
634
|
+
agent: envelope.resolved_agent_id,
|
|
635
|
+
parts: [{ type: "text", text: prompt }]
|
|
636
|
+
})
|
|
637
|
+
});
|
|
638
|
+
const text = await response.text();
|
|
639
|
+
if (!response.ok) {
|
|
640
|
+
throw new Error(`prompt_async failed: ${response.status} ${text}`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async function maybeSendTerminalCallback(input) {
|
|
644
|
+
const runtimeCfg = getRuntimeConfig(input.cfg);
|
|
645
|
+
if (!runtimeCfg.hookBaseUrl || !runtimeCfg.hookToken) {
|
|
646
|
+
const updated = patchRunStatus(input.runStatus.runId, (current) => ({
|
|
647
|
+
...current,
|
|
648
|
+
callbackError: "hook config missing",
|
|
649
|
+
callbackAttempts: (current.callbackAttempts || 0) + 1,
|
|
650
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
651
|
+
}));
|
|
652
|
+
return { ok: false, skipped: true, reason: "hook_config_missing", runStatus: updated || input.runStatus };
|
|
653
|
+
}
|
|
654
|
+
if (input.runStatus.callbackSentAt) {
|
|
655
|
+
return { ok: true, skipped: true, reason: "already_sent", runStatus: input.runStatus };
|
|
656
|
+
}
|
|
657
|
+
const callback = buildHooksAgentCallback({
|
|
658
|
+
event: input.event,
|
|
659
|
+
envelope: input.runStatus.envelope,
|
|
660
|
+
summary: input.summary || input.runStatus.lastSummary
|
|
661
|
+
});
|
|
662
|
+
const attempts = (input.runStatus.callbackAttempts || 0) + 1;
|
|
663
|
+
try {
|
|
664
|
+
const result = await executeHooksAgentCallback(runtimeCfg.hookBaseUrl, runtimeCfg.hookToken, callback);
|
|
665
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
666
|
+
appendAudit({
|
|
667
|
+
taskId: input.runStatus.taskId,
|
|
668
|
+
runId: input.runStatus.runId,
|
|
669
|
+
agentId: callback.agentId,
|
|
670
|
+
requestedAgentId: input.runStatus.envelope.requested_agent_id,
|
|
671
|
+
resolvedAgentId: input.runStatus.envelope.resolved_agent_id,
|
|
672
|
+
sessionKey: input.runStatus.envelope.session_key,
|
|
673
|
+
callbackTargetSessionKey: input.runStatus.envelope.callback_target_session_key,
|
|
674
|
+
callbackTargetSessionId: input.runStatus.envelope.callback_target_session_id,
|
|
675
|
+
event: input.event,
|
|
676
|
+
callbackStatus: result.status,
|
|
677
|
+
callbackOk: result.ok,
|
|
678
|
+
callbackBody: result.body,
|
|
679
|
+
createdAt
|
|
680
|
+
});
|
|
681
|
+
const updated = patchRunStatus(input.runStatus.runId, (current) => ({
|
|
682
|
+
...current,
|
|
683
|
+
callbackAttempts: attempts,
|
|
684
|
+
callbackSentAt: result.ok ? createdAt : current.callbackSentAt,
|
|
685
|
+
callbackStatus: result.status,
|
|
686
|
+
callbackOk: result.ok,
|
|
687
|
+
callbackBody: result.body,
|
|
688
|
+
callbackError: result.ok ? void 0 : `hook returned status=${result.status}`,
|
|
689
|
+
updatedAt: createdAt
|
|
690
|
+
}));
|
|
691
|
+
return { ok: result.ok, skipped: false, callback, result, runStatus: updated || input.runStatus };
|
|
692
|
+
} catch (error) {
|
|
693
|
+
const message = error?.message || String(error);
|
|
694
|
+
const updated = patchRunStatus(input.runStatus.runId, (current) => ({
|
|
695
|
+
...current,
|
|
696
|
+
callbackAttempts: attempts,
|
|
697
|
+
callbackError: message,
|
|
698
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
699
|
+
}));
|
|
700
|
+
return { ok: false, skipped: false, error: message, runStatus: updated || input.runStatus };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async function watchRunToTerminal(input) {
|
|
704
|
+
const pollIntervalMs = Math.max(250, asNumber(input.pollIntervalMs) || 1e3);
|
|
705
|
+
const maxWaitMs = Math.max(1e3, asNumber(input.maxWaitMs) || 5 * 60 * 1e3);
|
|
706
|
+
const startedAt = Date.now();
|
|
707
|
+
let current = patchRunStatus(input.runId, (status) => ({
|
|
708
|
+
...status,
|
|
709
|
+
watcherStartedAt: status.watcherStartedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
710
|
+
watcherHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
711
|
+
watcherState: "active",
|
|
712
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
713
|
+
}));
|
|
714
|
+
if (!current) {
|
|
715
|
+
throw new Error(`Run status not found for runId=${input.runId}`);
|
|
716
|
+
}
|
|
717
|
+
while (Date.now() - startedAt <= maxWaitMs) {
|
|
718
|
+
current = readRunStatus(input.runId);
|
|
719
|
+
if (!current) throw new Error(`Run status disappeared for runId=${input.runId}`);
|
|
720
|
+
const events = await collectSseEvents(current.envelope.opencode_server_url, "global", {
|
|
721
|
+
limit: DEFAULT_EVENT_LIMIT,
|
|
722
|
+
timeoutMs: Math.max(200, pollIntervalMs),
|
|
723
|
+
runIdHint: current.runId,
|
|
724
|
+
taskIdHint: current.taskId,
|
|
725
|
+
sessionIdHint: current.sessionId
|
|
726
|
+
});
|
|
727
|
+
if (events.length > 0) {
|
|
728
|
+
const summary = summarizeLifecycle(
|
|
729
|
+
events.map((event) => ({
|
|
730
|
+
kind: event.normalizedKind,
|
|
731
|
+
summary: event.summary,
|
|
732
|
+
lifecycleState: event.lifecycle_state,
|
|
733
|
+
filesChanged: event.files_changed,
|
|
734
|
+
verifySummary: event.verify_summary,
|
|
735
|
+
blockers: event.blockers,
|
|
736
|
+
completionSummary: event.completion_summary,
|
|
737
|
+
timestamp: event.timestamp
|
|
738
|
+
}))
|
|
739
|
+
);
|
|
740
|
+
current = patchRunStatus(input.runId, (status) => ({
|
|
741
|
+
...status,
|
|
742
|
+
state: coerceLifecycleState(summary.currentState),
|
|
743
|
+
lastEvent: summary.last_event_kind || status.lastEvent || null,
|
|
744
|
+
lastSummary: summary.completion_summary || events[events.length - 1]?.summary || status.lastSummary,
|
|
745
|
+
sessionId: status.sessionId || events.find((event) => event.sessionId)?.sessionId,
|
|
746
|
+
watcherHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
747
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
748
|
+
})) || current;
|
|
749
|
+
} else {
|
|
750
|
+
current = patchRunStatus(input.runId, (status) => ({
|
|
751
|
+
...status,
|
|
752
|
+
watcherHeartbeatAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
753
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
754
|
+
})) || current;
|
|
755
|
+
}
|
|
756
|
+
if (isTerminalLifecycleState(current.state)) {
|
|
757
|
+
const callback = await maybeSendTerminalCallback({
|
|
758
|
+
cfg: input.cfg,
|
|
759
|
+
runStatus: current,
|
|
760
|
+
event: buildTerminalEventFromState(current.state, current.lastEvent),
|
|
761
|
+
summary: current.lastSummary
|
|
762
|
+
});
|
|
763
|
+
current = patchRunStatus(input.runId, (status) => ({
|
|
764
|
+
...status,
|
|
765
|
+
watcherCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
766
|
+
watcherState: callback.ok ? "completed" : "failed",
|
|
767
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
768
|
+
})) || current;
|
|
769
|
+
return { ok: callback.ok, terminal: true, runStatus: current, callback };
|
|
770
|
+
}
|
|
771
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
772
|
+
}
|
|
773
|
+
current = patchRunStatus(input.runId, (status) => ({
|
|
774
|
+
...status,
|
|
775
|
+
watcherCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
776
|
+
watcherState: "failed",
|
|
777
|
+
callbackError: status.callbackError || "watcher_timeout",
|
|
778
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
779
|
+
})) || current;
|
|
780
|
+
return { ok: false, terminal: false, timeout: true, runStatus: current };
|
|
781
|
+
}
|
|
782
|
+
async function startExecutionRun(input) {
|
|
783
|
+
const initial = {
|
|
784
|
+
taskId: input.envelope.task_id,
|
|
785
|
+
runId: input.envelope.run_id,
|
|
786
|
+
state: "queued",
|
|
787
|
+
lastEvent: null,
|
|
788
|
+
lastSummary: void 0,
|
|
789
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
790
|
+
envelope: input.envelope,
|
|
791
|
+
watcherState: "pending"
|
|
792
|
+
};
|
|
793
|
+
writeRunStatus(initial);
|
|
794
|
+
const session = await createSessionForEnvelope(input.envelope);
|
|
795
|
+
let current = patchRunStatus(input.envelope.run_id, (status) => ({
|
|
796
|
+
...status,
|
|
797
|
+
sessionId: session.sessionId,
|
|
798
|
+
state: "session_created",
|
|
799
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
800
|
+
})) || initial;
|
|
801
|
+
await sendPromptAsyncForEnvelope(input.envelope, session.sessionId, input.prompt);
|
|
802
|
+
current = patchRunStatus(input.envelope.run_id, (status) => ({
|
|
803
|
+
...status,
|
|
804
|
+
state: "prompt_sent",
|
|
805
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
806
|
+
})) || current;
|
|
807
|
+
void watchRunToTerminal({
|
|
808
|
+
cfg: input.cfg,
|
|
809
|
+
runId: input.envelope.run_id,
|
|
810
|
+
pollIntervalMs: input.pollIntervalMs,
|
|
811
|
+
maxWaitMs: input.maxWaitMs
|
|
812
|
+
}).catch((error) => {
|
|
813
|
+
patchRunStatus(input.envelope.run_id, (status) => ({
|
|
814
|
+
...status,
|
|
815
|
+
watcherCompletedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
816
|
+
watcherState: "failed",
|
|
817
|
+
callbackError: status.callbackError || error?.message || String(error),
|
|
818
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
819
|
+
}));
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
ok: true,
|
|
823
|
+
runId: input.envelope.run_id,
|
|
824
|
+
taskId: input.envelope.task_id,
|
|
825
|
+
sessionId: session.sessionId,
|
|
826
|
+
state: current.state,
|
|
827
|
+
watcherStarted: true
|
|
828
|
+
};
|
|
829
|
+
}
|
|
478
830
|
function buildHookPolicyChecklist(agentId, sessionKey) {
|
|
479
831
|
return {
|
|
480
|
-
|
|
832
|
+
primaryCallbackPath: "/hooks/agent",
|
|
481
833
|
requirements: {
|
|
482
834
|
hooksEnabled: true,
|
|
483
835
|
allowRequestSessionKey: true,
|
|
@@ -498,11 +850,30 @@ function buildHookPolicyChecklist(agentId, sessionKey) {
|
|
|
498
850
|
}
|
|
499
851
|
|
|
500
852
|
// src/registrar.ts
|
|
501
|
-
var PLUGIN_VERSION = "0.1.
|
|
853
|
+
var PLUGIN_VERSION = "0.1.3";
|
|
854
|
+
function buildExecutionPrompt(params, envelope) {
|
|
855
|
+
const providedPrompt = asString(params?.prompt);
|
|
856
|
+
if (providedPrompt) return providedPrompt;
|
|
857
|
+
const objective = asString(params?.objective) || asString(params?.message) || `Complete task ${envelope.task_id}`;
|
|
858
|
+
const constraints = Array.isArray(params?.constraints) ? params.constraints.filter((x) => typeof x === "string" && x.trim()) : [];
|
|
859
|
+
const acceptance = Array.isArray(params?.acceptanceCriteria) ? params.acceptanceCriteria.filter((x) => typeof x === "string" && x.trim()) : [];
|
|
860
|
+
return [
|
|
861
|
+
`Task: ${objective}`,
|
|
862
|
+
`Run ID: ${envelope.run_id}`,
|
|
863
|
+
`Task ID: ${envelope.task_id}`,
|
|
864
|
+
`Requested agent: ${envelope.requested_agent_id}`,
|
|
865
|
+
`Resolved execution agent: ${envelope.resolved_agent_id}`,
|
|
866
|
+
constraints.length ? `Constraints:
|
|
867
|
+
- ${constraints.join("\n- ")}` : void 0,
|
|
868
|
+
acceptance.length ? `Acceptance criteria:
|
|
869
|
+
- ${acceptance.join("\n- ")}` : void 0,
|
|
870
|
+
"When complete, summarize files changed, verification performed, blockers (if any), and completion outcome in the session output."
|
|
871
|
+
].filter(Boolean).join("\n\n");
|
|
872
|
+
}
|
|
502
873
|
function registerOpenCodeBridgeTools(api, cfg) {
|
|
503
|
-
console.log("[opencode-bridge]
|
|
874
|
+
console.log("[opencode-bridge] plugin loaded");
|
|
504
875
|
console.log(`[opencode-bridge] opencodeServerUrl=${cfg.opencodeServerUrl || "(unset)"}`);
|
|
505
|
-
console.log("[opencode-bridge] registering opencode_*
|
|
876
|
+
console.log("[opencode-bridge] registering opencode_* tools");
|
|
506
877
|
api.registerTool({
|
|
507
878
|
name: "opencode_status",
|
|
508
879
|
label: "OpenCode Status",
|
|
@@ -512,7 +883,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
512
883
|
const runtimeCfg = getRuntimeConfig(cfg);
|
|
513
884
|
const registry = normalizeRegistry(runtimeCfg.projectRegistry);
|
|
514
885
|
return {
|
|
515
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, pluginId: "opencode-bridge", version: PLUGIN_VERSION, 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", "requested_agent_id", "resolved_agent_id", "session_key", "origin_session_key", "callback_target_session_key", "project_id", "repo_root", "opencode_server_url"],
|
|
886
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, pluginId: "opencode-bridge", version: PLUGIN_VERSION, assumption: "1 project = 1 opencode serve instance", sessionKeyConvention: "hook:opencode:<agentId>:<taskId>", lifecycleStates: ["queued", "server_ready", "session_created", "prompt_sent", "planning", "coding", "verifying", "blocked", "running", "awaiting_permission", "stalled", "failed", "completed"], requiredEnvelopeFields: ["task_id", "run_id", "agent_id", "requested_agent_id", "resolved_agent_id", "session_key", "origin_session_key", "callback_target_session_key", "project_id", "repo_root", "opencode_server_url"], primaryCallbackPath: "/hooks/agent", alternativeSignalPaths: ["/hooks/wake", "cron", "group:sessions"], config: { bridgeConfigPath: getBridgeConfigPath(), opencodeServerUrl: runtimeCfg.opencodeServerUrl || null, hookBaseUrl: runtimeCfg.hookBaseUrl || null, hookTokenPresent: Boolean(runtimeCfg.hookToken), projectRegistry: registry, executionAgentMappings: runtimeCfg.executionAgentMappings || [], stateDir: getBridgeStateDir(), runStateDir: getRunStateDir(), auditDir: getAuditDir() }, note: "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) }]
|
|
516
887
|
};
|
|
517
888
|
}
|
|
518
889
|
}, { optional: true });
|
|
@@ -582,6 +953,100 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
582
953
|
return { content: [{ type: "text", text: JSON.stringify({ ok: true, checklist }, null, 2) }] };
|
|
583
954
|
}
|
|
584
955
|
}, { optional: true });
|
|
956
|
+
api.registerTool({
|
|
957
|
+
name: "opencode_execute_task",
|
|
958
|
+
label: "OpenCode Execute Task",
|
|
959
|
+
description: "Execution entrypoint: resolve/spawn serve, create session, send prompt async, start SSE-driven watcher, callback once v\u1EC1 /hooks/agent khi terminal.",
|
|
960
|
+
parameters: {
|
|
961
|
+
type: "object",
|
|
962
|
+
properties: {
|
|
963
|
+
taskId: { type: "string" },
|
|
964
|
+
runId: { type: "string" },
|
|
965
|
+
agentId: { type: "string", description: "Requester/origin agent id" },
|
|
966
|
+
executionAgentId: { type: "string" },
|
|
967
|
+
originSessionKey: { type: "string" },
|
|
968
|
+
originSessionId: { type: "string" },
|
|
969
|
+
projectId: { type: "string" },
|
|
970
|
+
repoRoot: { type: "string" },
|
|
971
|
+
prompt: { type: "string" },
|
|
972
|
+
objective: { type: "string" },
|
|
973
|
+
message: { type: "string" },
|
|
974
|
+
constraints: { type: "array", items: { type: "string" } },
|
|
975
|
+
acceptanceCriteria: { type: "array", items: { type: "string" } },
|
|
976
|
+
channel: { type: "string" },
|
|
977
|
+
to: { type: "string" },
|
|
978
|
+
deliver: { type: "boolean" },
|
|
979
|
+
priority: { type: "string" },
|
|
980
|
+
idleTimeoutMs: { type: "number" },
|
|
981
|
+
pollIntervalMs: { type: "number" },
|
|
982
|
+
maxWaitMs: { type: "number" }
|
|
983
|
+
},
|
|
984
|
+
required: ["taskId", "runId", "agentId", "originSessionKey", "projectId", "repoRoot"]
|
|
985
|
+
},
|
|
986
|
+
async execute(_id, params) {
|
|
987
|
+
const resolved = resolveExecutionAgent({
|
|
988
|
+
cfg,
|
|
989
|
+
requestedAgentId: asString(params?.agentId) || "",
|
|
990
|
+
explicitExecutionAgentId: asString(params?.executionAgentId)
|
|
991
|
+
});
|
|
992
|
+
if (!resolved.ok) {
|
|
993
|
+
return {
|
|
994
|
+
isError: true,
|
|
995
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: resolved.error, requestedAgentId: resolved.requestedAgentId, mappingConfigured: resolved.mappingConfigured }, null, 2) }]
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const spawned = await spawnServeForProject({
|
|
999
|
+
project_id: params.projectId,
|
|
1000
|
+
repo_root: params.repoRoot,
|
|
1001
|
+
idle_timeout_ms: asNumber(params.idleTimeoutMs)
|
|
1002
|
+
});
|
|
1003
|
+
const serverUrl = spawned.entry.opencode_server_url;
|
|
1004
|
+
const envelope = buildEnvelope({
|
|
1005
|
+
taskId: params.taskId,
|
|
1006
|
+
runId: params.runId,
|
|
1007
|
+
requestedAgentId: resolved.requestedAgentId,
|
|
1008
|
+
resolvedAgentId: resolved.resolvedAgentId,
|
|
1009
|
+
originSessionKey: params.originSessionKey,
|
|
1010
|
+
originSessionId: asString(params.originSessionId),
|
|
1011
|
+
projectId: params.projectId,
|
|
1012
|
+
repoRoot: params.repoRoot,
|
|
1013
|
+
serverUrl,
|
|
1014
|
+
channel: params.channel,
|
|
1015
|
+
to: params.to,
|
|
1016
|
+
deliver: params.deliver,
|
|
1017
|
+
priority: params.priority
|
|
1018
|
+
});
|
|
1019
|
+
const prompt = buildExecutionPrompt(params, envelope);
|
|
1020
|
+
const execution = await startExecutionRun({
|
|
1021
|
+
cfg,
|
|
1022
|
+
envelope,
|
|
1023
|
+
prompt,
|
|
1024
|
+
pollIntervalMs: asNumber(params.pollIntervalMs),
|
|
1025
|
+
maxWaitMs: asNumber(params.maxWaitMs)
|
|
1026
|
+
});
|
|
1027
|
+
const snapshot = listRunStatuses().find((item) => item.runId === params.runId) || null;
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: JSON.stringify({
|
|
1032
|
+
ok: true,
|
|
1033
|
+
spawned,
|
|
1034
|
+
envelope,
|
|
1035
|
+
prompt,
|
|
1036
|
+
execution: {
|
|
1037
|
+
ok: execution.ok,
|
|
1038
|
+
runId: execution.runId,
|
|
1039
|
+
taskId: execution.taskId,
|
|
1040
|
+
sessionId: execution.sessionId,
|
|
1041
|
+
state: execution.state,
|
|
1042
|
+
watcherStarted: execution.watcherStarted
|
|
1043
|
+
},
|
|
1044
|
+
runStatus: snapshot
|
|
1045
|
+
}, null, 2)
|
|
1046
|
+
}]
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
}, { optional: true });
|
|
585
1050
|
api.registerTool({
|
|
586
1051
|
name: "opencode_evaluate_lifecycle",
|
|
587
1052
|
label: "OpenCode Evaluate Lifecycle",
|
|
@@ -641,11 +1106,11 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
641
1106
|
timestamp: event.timestamp
|
|
642
1107
|
}))
|
|
643
1108
|
);
|
|
644
|
-
const state = lifecycleSummary.
|
|
1109
|
+
const state = lifecycleSummary.currentState || artifact?.state || (sessionId ? "running" : "queued");
|
|
645
1110
|
const response = {
|
|
646
1111
|
ok: true,
|
|
647
1112
|
source: {
|
|
648
|
-
|
|
1113
|
+
runArtifact: Boolean(artifact),
|
|
649
1114
|
opencodeApi: true
|
|
650
1115
|
},
|
|
651
1116
|
runId: runId || void 0,
|
|
@@ -659,7 +1124,8 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
659
1124
|
}
|
|
660
1125
|
},
|
|
661
1126
|
state,
|
|
662
|
-
|
|
1127
|
+
currentState: lifecycleSummary.currentState || state,
|
|
1128
|
+
current_state: lifecycleSummary.currentState || state,
|
|
663
1129
|
lastEvent: artifact?.lastEvent,
|
|
664
1130
|
last_event_kind: artifact?.lastEvent,
|
|
665
1131
|
lastSummary: artifact?.lastSummary,
|
|
@@ -915,7 +1381,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
915
1381
|
}
|
|
916
1382
|
|
|
917
1383
|
// src/index.ts
|
|
918
|
-
var PLUGIN_VERSION2 = "0.1.
|
|
1384
|
+
var PLUGIN_VERSION2 = "0.1.3";
|
|
919
1385
|
var plugin = {
|
|
920
1386
|
id: "opencode-bridge",
|
|
921
1387
|
name: "OpenCode Bridge",
|
|
@@ -925,7 +1391,7 @@ var plugin = {
|
|
|
925
1391
|
registerOpenCodeBridgeTools(api, cfg);
|
|
926
1392
|
}
|
|
927
1393
|
};
|
|
928
|
-
var
|
|
1394
|
+
var src_default = plugin;
|
|
929
1395
|
export {
|
|
930
|
-
|
|
1396
|
+
src_default as default
|
|
931
1397
|
};
|
|
@@ -68,6 +68,7 @@ declare function summarizeLifecycle(events?: Array<{
|
|
|
68
68
|
completionSummary?: string | null;
|
|
69
69
|
timestamp?: string;
|
|
70
70
|
}>): {
|
|
71
|
+
currentState: "planning" | "coding" | "verifying" | "blocked" | "running" | "awaiting_permission" | "stalled" | "failed" | "completed" | null;
|
|
71
72
|
current_state: "planning" | "coding" | "verifying" | "blocked" | "running" | "awaiting_permission" | "stalled" | "failed" | "completed" | null;
|
|
72
73
|
last_event_kind: OpenCodeEventKind | null;
|
|
73
74
|
last_event_at: string | null;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type BridgeSessionTagFields = {
|
|
2
|
+
runId: string;
|
|
3
|
+
taskId: string;
|
|
4
|
+
requested: string;
|
|
5
|
+
resolved: string;
|
|
6
|
+
callbackSession: string;
|
|
7
|
+
callbackSessionId?: string;
|
|
8
|
+
projectId?: string;
|
|
9
|
+
repoRoot?: string;
|
|
10
|
+
};
|
|
11
|
+
type OpenCodePluginCallbackAuditRecord = {
|
|
12
|
+
phase?: string;
|
|
13
|
+
event_type?: string;
|
|
14
|
+
session_id?: string;
|
|
15
|
+
title?: string;
|
|
16
|
+
tags?: Record<string, string> | null;
|
|
17
|
+
dedupeKey?: string;
|
|
18
|
+
ok?: boolean;
|
|
19
|
+
status?: number;
|
|
20
|
+
reason?: string;
|
|
21
|
+
body?: string;
|
|
22
|
+
payload?: any;
|
|
23
|
+
raw?: any;
|
|
24
|
+
created_at: string;
|
|
25
|
+
};
|
|
26
|
+
declare function buildTaggedSessionTitle(fields: BridgeSessionTagFields): string;
|
|
27
|
+
declare function parseTaggedSessionTitle(title?: string): Record<string, string> | null;
|
|
28
|
+
declare function buildPluginCallbackDedupeKey(input: {
|
|
29
|
+
sessionId?: string;
|
|
30
|
+
runId?: string;
|
|
31
|
+
}): string;
|
|
32
|
+
|
|
33
|
+
export { type BridgeSessionTagFields, type OpenCodePluginCallbackAuditRecord, buildPluginCallbackDedupeKey, buildTaggedSessionTitle, parseTaggedSessionTitle };
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "opencode-bridge",
|
|
3
3
|
"name": "OpenCode Bridge",
|
|
4
|
-
"description": "Bridge plugin
|
|
5
|
-
"version": "0.1.
|
|
4
|
+
"description": "Bridge plugin for OpenClaw ↔ OpenCode orchestration, execution routing, and callback observability.",
|
|
5
|
+
"version": "0.1.3",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|