@mrc2204/opencode-bridge 0.1.1 → 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/src/chunk-OVQ5X54C.js +289 -0
- package/dist/src/chunk-TDVN5AFB.js +36 -0
- package/dist/{index.js → src/index.js} +605 -22
- package/dist/src/observability.d.ts +97 -0
- package/dist/{observability.js → src/observability.js} +3 -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 +108 -66
- package/src/shared-contracts.ts +58 -0
- package/dist/chunk-6NIQKNRA.js +0 -176
- package/dist/observability.d.ts +0 -52
- /package/dist/{index.d.ts → src/index.d.ts} +0 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeTypedEventV1,
|
|
3
3
|
parseSseFramesFromBuffer,
|
|
4
|
-
resolveSessionId
|
|
5
|
-
|
|
4
|
+
resolveSessionId,
|
|
5
|
+
summarizeLifecycle
|
|
6
|
+
} from "./chunk-OVQ5X54C.js";
|
|
7
|
+
import {
|
|
8
|
+
buildTaggedSessionTitle
|
|
9
|
+
} from "./chunk-TDVN5AFB.js";
|
|
6
10
|
|
|
7
11
|
// src/runtime.ts
|
|
8
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, readdirSync } from "fs";
|
|
9
13
|
import { join } from "path";
|
|
10
14
|
import { spawn } from "child_process";
|
|
11
15
|
import { createServer } from "net";
|
|
@@ -55,6 +59,7 @@ function getRuntimeConfig(cfg) {
|
|
|
55
59
|
return {
|
|
56
60
|
opencodeServerUrl: fileCfg.opencodeServerUrl || cfg?.opencodeServerUrl,
|
|
57
61
|
projectRegistry: fileCfg.projectRegistry || cfg?.projectRegistry || [],
|
|
62
|
+
executionAgentMappings: fileCfg.executionAgentMappings || cfg?.executionAgentMappings || [],
|
|
58
63
|
hookBaseUrl: fileCfg.hookBaseUrl || cfg?.hookBaseUrl,
|
|
59
64
|
hookToken: fileCfg.hookToken || cfg?.hookToken
|
|
60
65
|
};
|
|
@@ -94,13 +99,66 @@ function findRegistryEntry(cfg, projectId, repoRoot) {
|
|
|
94
99
|
}
|
|
95
100
|
return void 0;
|
|
96
101
|
}
|
|
102
|
+
function resolveExecutionAgent(input) {
|
|
103
|
+
const requestedAgentId = asString(input.requestedAgentId);
|
|
104
|
+
if (!requestedAgentId) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
requestedAgentId: "",
|
|
108
|
+
mappingConfigured: false,
|
|
109
|
+
error: "requestedAgentId is required"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const explicitExecutionAgentId = asString(input.explicitExecutionAgentId);
|
|
113
|
+
if (explicitExecutionAgentId) {
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
requestedAgentId,
|
|
117
|
+
resolvedAgentId: explicitExecutionAgentId,
|
|
118
|
+
strategy: "explicit_param"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const runtimeCfg = getRuntimeConfig(input.cfg);
|
|
122
|
+
const mappings = asArray(runtimeCfg.executionAgentMappings).map((x) => x && typeof x === "object" ? x : {}).map((x) => ({
|
|
123
|
+
requestedAgentId: asString(x.requestedAgentId),
|
|
124
|
+
executionAgentId: asString(x.executionAgentId)
|
|
125
|
+
})).filter((x) => x.requestedAgentId && x.executionAgentId);
|
|
126
|
+
if (mappings.length > 0) {
|
|
127
|
+
const matched = mappings.find((x) => x.requestedAgentId === requestedAgentId);
|
|
128
|
+
if (!matched) {
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
requestedAgentId,
|
|
132
|
+
mappingConfigured: true,
|
|
133
|
+
error: `Execution agent mapping is configured but no mapping matched requestedAgentId=${requestedAgentId}`
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
requestedAgentId,
|
|
139
|
+
resolvedAgentId: matched.executionAgentId,
|
|
140
|
+
strategy: "config_mapping"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
requestedAgentId,
|
|
146
|
+
resolvedAgentId: requestedAgentId,
|
|
147
|
+
strategy: "identity"
|
|
148
|
+
};
|
|
149
|
+
}
|
|
97
150
|
function buildEnvelope(input) {
|
|
98
151
|
return {
|
|
99
152
|
task_id: input.taskId,
|
|
100
153
|
run_id: input.runId,
|
|
101
|
-
agent_id: input.
|
|
102
|
-
|
|
154
|
+
agent_id: input.resolvedAgentId,
|
|
155
|
+
requested_agent_id: input.requestedAgentId,
|
|
156
|
+
resolved_agent_id: input.resolvedAgentId,
|
|
157
|
+
session_key: buildSessionKey(input.resolvedAgentId, input.taskId),
|
|
103
158
|
origin_session_key: input.originSessionKey,
|
|
159
|
+
...input.originSessionId ? { origin_session_id: input.originSessionId } : {},
|
|
160
|
+
callback_target_session_key: input.originSessionKey,
|
|
161
|
+
...input.originSessionId ? { callback_target_session_id: input.originSessionId } : {},
|
|
104
162
|
project_id: input.projectId,
|
|
105
163
|
repo_root: input.repoRoot,
|
|
106
164
|
opencode_server_url: input.serverUrl,
|
|
@@ -113,6 +171,7 @@ function buildEnvelope(input) {
|
|
|
113
171
|
function mapEventToState(event) {
|
|
114
172
|
switch (event) {
|
|
115
173
|
case "task.started":
|
|
174
|
+
return "planning";
|
|
116
175
|
case "task.progress":
|
|
117
176
|
return "running";
|
|
118
177
|
case "permission.requested":
|
|
@@ -127,6 +186,30 @@ function mapEventToState(event) {
|
|
|
127
186
|
return "running";
|
|
128
187
|
}
|
|
129
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
|
+
}
|
|
130
213
|
function evaluateLifecycle(input) {
|
|
131
214
|
const nowMs = input.nowMs ?? Date.now();
|
|
132
215
|
const softStallMs = input.softStallMs ?? DEFAULT_SOFT_STALL_MS;
|
|
@@ -160,14 +243,46 @@ function evaluateLifecycle(input) {
|
|
|
160
243
|
function getRunStateDir() {
|
|
161
244
|
return join(getBridgeStateDir(), "runs");
|
|
162
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
|
+
}
|
|
163
253
|
function readRunStatus(runId) {
|
|
164
254
|
const path = join(getRunStateDir(), `${runId}.json`);
|
|
165
255
|
if (!existsSync(path)) return null;
|
|
166
256
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
167
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
|
+
}
|
|
168
276
|
function getAuditDir() {
|
|
169
277
|
return join(getBridgeStateDir(), "audit");
|
|
170
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
|
+
}
|
|
171
286
|
function getServeRegistryPath() {
|
|
172
287
|
return join(getBridgeStateDir(), "registry.json");
|
|
173
288
|
}
|
|
@@ -247,16 +362,43 @@ async function waitForHealth(serverUrl, timeoutMs = 1e4) {
|
|
|
247
362
|
}
|
|
248
363
|
return false;
|
|
249
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
|
+
}
|
|
250
388
|
async function spawnServeForProject(input) {
|
|
251
389
|
const existing = normalizeServeRegistry(readServeRegistry()).entries.find((x) => x.project_id === input.project_id || x.repo_root === input.repo_root);
|
|
252
390
|
if (existing && existing.status === "running") {
|
|
253
391
|
const healthy2 = await waitForHealth(existing.opencode_server_url, 2e3);
|
|
254
392
|
if (healthy2) {
|
|
255
|
-
|
|
393
|
+
const boundToRepo = await isServeBoundToRepo(existing.opencode_server_url, input.repo_root);
|
|
394
|
+
if (boundToRepo) {
|
|
395
|
+
return { reused: true, entry: existing, registryPath: getServeRegistryPath() };
|
|
396
|
+
}
|
|
256
397
|
}
|
|
257
398
|
}
|
|
258
399
|
const port = await allocatePort();
|
|
259
400
|
const child = spawn("opencode", ["serve", "--hostname", "127.0.0.1", "--port", String(port)], {
|
|
401
|
+
cwd: input.repo_root,
|
|
260
402
|
detached: true,
|
|
261
403
|
stdio: "ignore"
|
|
262
404
|
});
|
|
@@ -314,12 +456,20 @@ async function fetchJsonSafe(url) {
|
|
|
314
456
|
}
|
|
315
457
|
function resolveSessionForRun(input) {
|
|
316
458
|
const artifactEnvelope = input.runStatus?.envelope;
|
|
459
|
+
const callbackTargetSessionId = typeof artifactEnvelope?.callback_target_session_id === "string" ? artifactEnvelope.callback_target_session_id : void 0;
|
|
460
|
+
const originSessionId = typeof artifactEnvelope?.origin_session_id === "string" ? artifactEnvelope.origin_session_id : void 0;
|
|
461
|
+
const callbackTargetSessionKey = typeof artifactEnvelope?.callback_target_session_key === "string" ? artifactEnvelope.callback_target_session_key : void 0;
|
|
462
|
+
const originSessionKey = typeof artifactEnvelope?.origin_session_key === "string" ? artifactEnvelope.origin_session_key : void 0;
|
|
463
|
+
const executionSessionKey = typeof artifactEnvelope?.session_key === "string" ? artifactEnvelope.session_key : void 0;
|
|
464
|
+
const artifactSessionId = callbackTargetSessionId || originSessionId || (typeof artifactEnvelope?.session_id === "string" ? artifactEnvelope.session_id : void 0) || (typeof artifactEnvelope?.sessionId === "string" ? artifactEnvelope.sessionId : void 0);
|
|
465
|
+
const artifactSessionKey = callbackTargetSessionKey || originSessionKey || executionSessionKey;
|
|
466
|
+
const hasCallerSessionKey = Boolean(callbackTargetSessionKey || originSessionKey);
|
|
317
467
|
const resolved = resolveSessionId({
|
|
318
468
|
explicitSessionId: input.sessionId,
|
|
319
|
-
runId: input.runId || input.runStatus?.runId,
|
|
320
|
-
taskId: input.runStatus?.taskId,
|
|
321
|
-
sessionKey:
|
|
322
|
-
artifactSessionId
|
|
469
|
+
runId: hasCallerSessionKey ? void 0 : input.runId || input.runStatus?.runId,
|
|
470
|
+
taskId: hasCallerSessionKey ? void 0 : input.runStatus?.taskId,
|
|
471
|
+
sessionKey: artifactSessionKey,
|
|
472
|
+
artifactSessionId,
|
|
323
473
|
sessionList: input.sessionList
|
|
324
474
|
});
|
|
325
475
|
return { sessionId: resolved.sessionId, strategy: resolved.strategy, ...resolved.score !== void 0 ? { score: resolved.score } : {} };
|
|
@@ -355,6 +505,11 @@ async function collectSseEvents(serverUrl, scope, options) {
|
|
|
355
505
|
data: typed.payload,
|
|
356
506
|
normalizedKind: typed.kind,
|
|
357
507
|
summary: typed.summary,
|
|
508
|
+
lifecycle_state: typed.lifecycleState,
|
|
509
|
+
files_changed: typed.filesChanged,
|
|
510
|
+
verify_summary: typed.verifySummary,
|
|
511
|
+
blockers: typed.blockers,
|
|
512
|
+
completion_summary: typed.completionSummary,
|
|
358
513
|
runId: typed.runId || options?.runIdHint,
|
|
359
514
|
taskId: typed.taskId || options?.taskIdHint,
|
|
360
515
|
sessionId: typed.sessionId || options?.sessionIdHint,
|
|
@@ -377,6 +532,11 @@ async function collectSseEvents(serverUrl, scope, options) {
|
|
|
377
532
|
data: typed.payload,
|
|
378
533
|
normalizedKind: typed.kind,
|
|
379
534
|
summary: typed.summary,
|
|
535
|
+
lifecycle_state: typed.lifecycleState,
|
|
536
|
+
files_changed: typed.filesChanged,
|
|
537
|
+
verify_summary: typed.verifySummary,
|
|
538
|
+
blockers: typed.blockers,
|
|
539
|
+
completion_summary: typed.completionSummary,
|
|
380
540
|
runId: typed.runId || options?.runIdHint,
|
|
381
541
|
taskId: typed.taskId || options?.taskIdHint,
|
|
382
542
|
sessionId: typed.sessionId || options?.sessionIdHint,
|
|
@@ -401,9 +561,275 @@ async function collectSseEvents(serverUrl, scope, options) {
|
|
|
401
561
|
clearTimeout(timeout);
|
|
402
562
|
}
|
|
403
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
|
+
}
|
|
404
830
|
function buildHookPolicyChecklist(agentId, sessionKey) {
|
|
405
831
|
return {
|
|
406
|
-
|
|
832
|
+
primaryCallbackPath: "/hooks/agent",
|
|
407
833
|
requirements: {
|
|
408
834
|
hooksEnabled: true,
|
|
409
835
|
allowRequestSessionKey: true,
|
|
@@ -424,10 +850,30 @@ function buildHookPolicyChecklist(agentId, sessionKey) {
|
|
|
424
850
|
}
|
|
425
851
|
|
|
426
852
|
// src/registrar.ts
|
|
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
|
+
}
|
|
427
873
|
function registerOpenCodeBridgeTools(api, cfg) {
|
|
428
|
-
console.log("[opencode-bridge]
|
|
874
|
+
console.log("[opencode-bridge] plugin loaded");
|
|
429
875
|
console.log(`[opencode-bridge] opencodeServerUrl=${cfg.opencodeServerUrl || "(unset)"}`);
|
|
430
|
-
console.log("[opencode-bridge] registering opencode_*
|
|
876
|
+
console.log("[opencode-bridge] registering opencode_* tools");
|
|
431
877
|
api.registerTool({
|
|
432
878
|
name: "opencode_status",
|
|
433
879
|
label: "OpenCode Status",
|
|
@@ -437,7 +883,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
437
883
|
const runtimeCfg = getRuntimeConfig(cfg);
|
|
438
884
|
const registry = normalizeRegistry(runtimeCfg.projectRegistry);
|
|
439
885
|
return {
|
|
440
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, pluginId: "opencode-bridge", version:
|
|
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) }]
|
|
441
887
|
};
|
|
442
888
|
}
|
|
443
889
|
}, { optional: true });
|
|
@@ -455,7 +901,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
455
901
|
name: "opencode_build_envelope",
|
|
456
902
|
label: "OpenCode Build Envelope",
|
|
457
903
|
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"] },
|
|
904
|
+
parameters: { type: "object", properties: { taskId: { type: "string" }, runId: { type: "string" }, agentId: { type: "string", description: "Requester/origin agent id" }, executionAgentId: { type: "string", description: "Optional explicit OpenCode execution agent id" }, originSessionKey: { type: "string" }, originSessionId: { 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
905
|
async execute(_id, params) {
|
|
460
906
|
const entry = findRegistryEntry(cfg, params?.projectId, params?.repoRoot);
|
|
461
907
|
const serverUrl = entry?.serverUrl;
|
|
@@ -465,11 +911,25 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
465
911
|
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
912
|
};
|
|
467
913
|
}
|
|
914
|
+
const requestedAgentId = asString(params?.agentId);
|
|
915
|
+
const resolved = resolveExecutionAgent({
|
|
916
|
+
cfg,
|
|
917
|
+
requestedAgentId: requestedAgentId || "",
|
|
918
|
+
explicitExecutionAgentId: asString(params?.executionAgentId)
|
|
919
|
+
});
|
|
920
|
+
if (!resolved.ok) {
|
|
921
|
+
return {
|
|
922
|
+
isError: true,
|
|
923
|
+
content: [{ type: "text", text: JSON.stringify({ ok: false, error: resolved.error, requestedAgentId: resolved.requestedAgentId, mappingConfigured: resolved.mappingConfigured }, null, 2) }]
|
|
924
|
+
};
|
|
925
|
+
}
|
|
468
926
|
const envelope = buildEnvelope({
|
|
469
927
|
taskId: params.taskId,
|
|
470
928
|
runId: params.runId,
|
|
471
|
-
|
|
929
|
+
requestedAgentId: resolved.requestedAgentId,
|
|
930
|
+
resolvedAgentId: resolved.resolvedAgentId,
|
|
472
931
|
originSessionKey: params.originSessionKey,
|
|
932
|
+
originSessionId: asString(params.originSessionId),
|
|
473
933
|
projectId: params.projectId,
|
|
474
934
|
repoRoot: params.repoRoot,
|
|
475
935
|
serverUrl,
|
|
@@ -479,7 +939,7 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
479
939
|
priority: params.priority
|
|
480
940
|
});
|
|
481
941
|
return {
|
|
482
|
-
content: [{ type: "text", text: JSON.stringify({ ok: true, envelope, registryMatch: entry || null }, null, 2) }]
|
|
942
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, envelope, agentResolution: resolved, registryMatch: entry || null }, null, 2) }]
|
|
483
943
|
};
|
|
484
944
|
}
|
|
485
945
|
}, { optional: true });
|
|
@@ -493,6 +953,100 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
493
953
|
return { content: [{ type: "text", text: JSON.stringify({ ok: true, checklist }, null, 2) }] };
|
|
494
954
|
}
|
|
495
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 });
|
|
496
1050
|
api.registerTool({
|
|
497
1051
|
name: "opencode_evaluate_lifecycle",
|
|
498
1052
|
label: "OpenCode Evaluate Lifecycle",
|
|
@@ -532,11 +1086,31 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
532
1086
|
runId
|
|
533
1087
|
});
|
|
534
1088
|
const sessionId = resolution.sessionId;
|
|
535
|
-
const
|
|
1089
|
+
const eventScope = sessionId ? "session" : "global";
|
|
1090
|
+
const events = await collectSseEvents(serverUrl, eventScope, {
|
|
1091
|
+
limit: DEFAULT_EVENT_LIMIT,
|
|
1092
|
+
timeoutMs: DEFAULT_OBS_TIMEOUT_MS,
|
|
1093
|
+
runIdHint: runId,
|
|
1094
|
+
taskIdHint: artifact?.taskId,
|
|
1095
|
+
sessionIdHint: sessionId
|
|
1096
|
+
});
|
|
1097
|
+
const lifecycleSummary = summarizeLifecycle(
|
|
1098
|
+
events.map((event) => ({
|
|
1099
|
+
kind: event.normalizedKind,
|
|
1100
|
+
summary: event.summary,
|
|
1101
|
+
lifecycleState: event.lifecycle_state,
|
|
1102
|
+
filesChanged: event.files_changed,
|
|
1103
|
+
verifySummary: event.verify_summary,
|
|
1104
|
+
blockers: event.blockers,
|
|
1105
|
+
completionSummary: event.completion_summary,
|
|
1106
|
+
timestamp: event.timestamp
|
|
1107
|
+
}))
|
|
1108
|
+
);
|
|
1109
|
+
const state = lifecycleSummary.currentState || artifact?.state || (sessionId ? "running" : "queued");
|
|
536
1110
|
const response = {
|
|
537
1111
|
ok: true,
|
|
538
1112
|
source: {
|
|
539
|
-
|
|
1113
|
+
runArtifact: Boolean(artifact),
|
|
540
1114
|
opencodeApi: true
|
|
541
1115
|
},
|
|
542
1116
|
runId: runId || void 0,
|
|
@@ -550,8 +1124,16 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
550
1124
|
}
|
|
551
1125
|
},
|
|
552
1126
|
state,
|
|
1127
|
+
currentState: lifecycleSummary.currentState || state,
|
|
1128
|
+
current_state: lifecycleSummary.currentState || state,
|
|
553
1129
|
lastEvent: artifact?.lastEvent,
|
|
1130
|
+
last_event_kind: artifact?.lastEvent,
|
|
554
1131
|
lastSummary: artifact?.lastSummary,
|
|
1132
|
+
last_event_at: artifact?.updatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
1133
|
+
files_changed: lifecycleSummary.files_changed,
|
|
1134
|
+
verify_summary: lifecycleSummary.verify_summary,
|
|
1135
|
+
blockers: lifecycleSummary.blockers,
|
|
1136
|
+
completion_summary: lifecycleSummary.completion_summary || artifact?.lastSummary || null,
|
|
555
1137
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
556
1138
|
timestamps: {
|
|
557
1139
|
...artifact?.updatedAt ? { artifactUpdatedAt: artifact.updatedAt } : {},
|
|
@@ -799,16 +1381,17 @@ function registerOpenCodeBridgeTools(api, cfg) {
|
|
|
799
1381
|
}
|
|
800
1382
|
|
|
801
1383
|
// src/index.ts
|
|
1384
|
+
var PLUGIN_VERSION2 = "0.1.3";
|
|
802
1385
|
var plugin = {
|
|
803
1386
|
id: "opencode-bridge",
|
|
804
1387
|
name: "OpenCode Bridge",
|
|
805
|
-
version:
|
|
1388
|
+
version: PLUGIN_VERSION2,
|
|
806
1389
|
register(api) {
|
|
807
1390
|
const cfg = api?.pluginConfig || {};
|
|
808
1391
|
registerOpenCodeBridgeTools(api, cfg);
|
|
809
1392
|
}
|
|
810
1393
|
};
|
|
811
|
-
var
|
|
1394
|
+
var src_default = plugin;
|
|
812
1395
|
export {
|
|
813
|
-
|
|
1396
|
+
src_default as default
|
|
814
1397
|
};
|