@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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// src/observability.ts
|
|
2
|
+
function parseSseFramesFromBuffer(input) {
|
|
3
|
+
const normalized = input.replace(/\r\n/g, "\n");
|
|
4
|
+
const parts = normalized.split("\n\n");
|
|
5
|
+
const remainder = parts.pop() ?? "";
|
|
6
|
+
const frames = [];
|
|
7
|
+
for (const rawFrame of parts) {
|
|
8
|
+
const lines = rawFrame.split("\n");
|
|
9
|
+
let event;
|
|
10
|
+
let id;
|
|
11
|
+
let retry;
|
|
12
|
+
const dataLines = [];
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
if (!line || line.startsWith(":")) continue;
|
|
15
|
+
const idx = line.indexOf(":");
|
|
16
|
+
const field = idx >= 0 ? line.slice(0, idx).trim() : line.trim();
|
|
17
|
+
const value = idx >= 0 ? line.slice(idx + 1).replace(/^\s/, "") : "";
|
|
18
|
+
if (field === "event") event = value;
|
|
19
|
+
else if (field === "id") id = value;
|
|
20
|
+
else if (field === "retry") {
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
if (Number.isFinite(n)) retry = n;
|
|
23
|
+
} else if (field === "data") dataLines.push(value);
|
|
24
|
+
}
|
|
25
|
+
if (dataLines.length <= 0) continue;
|
|
26
|
+
frames.push({
|
|
27
|
+
event,
|
|
28
|
+
id,
|
|
29
|
+
retry,
|
|
30
|
+
data: dataLines.join("\n"),
|
|
31
|
+
raw: rawFrame
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return { frames, remainder };
|
|
35
|
+
}
|
|
36
|
+
function parseSseData(data) {
|
|
37
|
+
const trimmed = data.trim();
|
|
38
|
+
if (!trimmed) return null;
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(trimmed);
|
|
41
|
+
} catch {
|
|
42
|
+
return { raw: trimmed };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function unwrapGlobalPayload(raw) {
|
|
46
|
+
let current = raw;
|
|
47
|
+
const wrappers = [];
|
|
48
|
+
for (let i = 0; i < 3; i += 1) {
|
|
49
|
+
if (!current || typeof current !== "object") break;
|
|
50
|
+
if ("payload" in current && current.payload !== void 0) {
|
|
51
|
+
wrappers.push("payload");
|
|
52
|
+
current = current.payload;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if ("data" in current && current.data !== void 0 && Object.keys(current).length <= 4) {
|
|
56
|
+
wrappers.push("data");
|
|
57
|
+
current = current.data;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
return { payload: current, wrappers };
|
|
63
|
+
}
|
|
64
|
+
function pickFirstString(obj, keys) {
|
|
65
|
+
if (!obj || typeof obj !== "object") return void 0;
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
const value = obj[key];
|
|
68
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
function asObject(value) {
|
|
73
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
74
|
+
}
|
|
75
|
+
function asArray(value) {
|
|
76
|
+
return Array.isArray(value) ? value : [];
|
|
77
|
+
}
|
|
78
|
+
function uniqueStrings(values) {
|
|
79
|
+
return [...new Set(values.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()))];
|
|
80
|
+
}
|
|
81
|
+
function inferLifecycleState(raw) {
|
|
82
|
+
const text = JSON.stringify(raw || {}).toLowerCase();
|
|
83
|
+
if (text.includes("permission") || text.includes("question.asked")) return "awaiting_permission";
|
|
84
|
+
if (text.includes("blocked") || text.includes("missing config") || text.includes("missing token") || text.includes("missing api_key")) return "blocked";
|
|
85
|
+
if (text.includes("error") || text.includes("failed")) return "failed";
|
|
86
|
+
if (text.includes("stalled")) return "stalled";
|
|
87
|
+
if (text.includes("compile") || text.includes("pytest") || text.includes("test") || text.includes("verify")) return "verifying";
|
|
88
|
+
if (text.includes("apply_patch") || text.includes("diff") || text.includes("edit") || text.includes("write")) return "coding";
|
|
89
|
+
if (text.includes("plan") || text.includes("discovery") || text.includes("ground") || text.includes("calibrate")) return "planning";
|
|
90
|
+
if (text.includes("complete") || text.includes("completed") || text.includes("done")) return "completed";
|
|
91
|
+
return "running";
|
|
92
|
+
}
|
|
93
|
+
function extractFilesChanged(raw) {
|
|
94
|
+
const files = [];
|
|
95
|
+
const root = asObject(raw) || {};
|
|
96
|
+
const payload = asObject(root.payload) || root;
|
|
97
|
+
const state = asObject(root.state) || asObject(payload.state) || {};
|
|
98
|
+
const metadata = asObject(state.metadata) || asObject(payload.metadata) || {};
|
|
99
|
+
for (const file of asArray(metadata.files)) {
|
|
100
|
+
const obj = asObject(file);
|
|
101
|
+
if (!obj) continue;
|
|
102
|
+
if (typeof obj.relativePath === "string") files.push(obj.relativePath);
|
|
103
|
+
else if (typeof obj.filePath === "string") files.push(obj.filePath);
|
|
104
|
+
}
|
|
105
|
+
for (const file of asArray(root.files)) {
|
|
106
|
+
if (typeof file === "string") files.push(file);
|
|
107
|
+
}
|
|
108
|
+
return uniqueStrings(files);
|
|
109
|
+
}
|
|
110
|
+
function extractVerifySummary(raw) {
|
|
111
|
+
const root = asObject(raw) || {};
|
|
112
|
+
const payload = asObject(root.payload) || root;
|
|
113
|
+
const state = asObject(root.state) || asObject(payload.state);
|
|
114
|
+
const input = asObject(state?.input);
|
|
115
|
+
const metadata = asObject(state?.metadata);
|
|
116
|
+
const command = typeof input?.command === "string" ? input.command : void 0;
|
|
117
|
+
const exit = typeof metadata?.exit === "number" ? metadata.exit : null;
|
|
118
|
+
const output = typeof state?.output === "string" ? state.output : typeof metadata?.output === "string" ? metadata.output : null;
|
|
119
|
+
if (!command && output == null) return null;
|
|
120
|
+
return {
|
|
121
|
+
command,
|
|
122
|
+
exit,
|
|
123
|
+
output_preview: typeof output === "string" ? output.slice(0, 500) : null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function extractBlockers(raw) {
|
|
127
|
+
const text = JSON.stringify(raw || {}).toLowerCase();
|
|
128
|
+
const blockers = [];
|
|
129
|
+
if (text.includes("no module named pytest")) blockers.push("pytest_missing");
|
|
130
|
+
if (text.includes("missing token") || text.includes("missing api_key") || text.includes("pow service not configured")) blockers.push("runtime_config_missing");
|
|
131
|
+
if (text.includes("permission") || text.includes("question.asked")) blockers.push("awaiting_permission");
|
|
132
|
+
return uniqueStrings(blockers);
|
|
133
|
+
}
|
|
134
|
+
function extractCompletionSummary(raw) {
|
|
135
|
+
const root = asObject(raw) || {};
|
|
136
|
+
const text = typeof root.text === "string" ? root.text : typeof root.summary === "string" ? root.summary : null;
|
|
137
|
+
return text ? text.slice(0, 2e3) : null;
|
|
138
|
+
}
|
|
139
|
+
function normalizeOpenCodeEvent(raw) {
|
|
140
|
+
const text = JSON.stringify(raw || {}).toLowerCase();
|
|
141
|
+
let kind = null;
|
|
142
|
+
let summary;
|
|
143
|
+
if (text.includes("permission") || text.includes("question.asked")) {
|
|
144
|
+
kind = "permission.requested";
|
|
145
|
+
summary = "OpenCode \u0111ang ch\u1EDD permission/user input";
|
|
146
|
+
} else if (text.includes("error") || text.includes("failed")) {
|
|
147
|
+
kind = "task.failed";
|
|
148
|
+
summary = "OpenCode task failed";
|
|
149
|
+
} else if (text.includes("stalled")) {
|
|
150
|
+
kind = "task.stalled";
|
|
151
|
+
summary = "OpenCode task stalled";
|
|
152
|
+
} else if (text.includes("complete") || text.includes("completed") || text.includes("done")) {
|
|
153
|
+
kind = "task.completed";
|
|
154
|
+
summary = "OpenCode task completed";
|
|
155
|
+
} else if (text.includes("progress") || text.includes("delta") || text.includes("assistant")) {
|
|
156
|
+
kind = "task.progress";
|
|
157
|
+
summary = "OpenCode task made progress";
|
|
158
|
+
} else if (text.includes("start") || text.includes("created") || text.includes("connected")) {
|
|
159
|
+
kind = "task.started";
|
|
160
|
+
summary = "OpenCode task/server started";
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
kind,
|
|
164
|
+
summary,
|
|
165
|
+
raw,
|
|
166
|
+
lifecycleState: inferLifecycleState(raw),
|
|
167
|
+
filesChanged: extractFilesChanged(raw),
|
|
168
|
+
verifySummary: extractVerifySummary(raw),
|
|
169
|
+
blockers: extractBlockers(raw),
|
|
170
|
+
completionSummary: extractCompletionSummary(raw)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function normalizeTypedEventV1(frame, scope) {
|
|
174
|
+
const parsed = parseSseData(frame.data);
|
|
175
|
+
const unwrapped = unwrapGlobalPayload(parsed);
|
|
176
|
+
const normalized = normalizeOpenCodeEvent(unwrapped.payload);
|
|
177
|
+
return {
|
|
178
|
+
schema: "opencode.event.v1",
|
|
179
|
+
scope,
|
|
180
|
+
eventName: frame.event,
|
|
181
|
+
eventId: frame.id,
|
|
182
|
+
kind: normalized.kind,
|
|
183
|
+
summary: normalized.summary,
|
|
184
|
+
lifecycleState: normalized.lifecycleState,
|
|
185
|
+
filesChanged: normalized.filesChanged,
|
|
186
|
+
verifySummary: normalized.verifySummary,
|
|
187
|
+
blockers: normalized.blockers,
|
|
188
|
+
completionSummary: normalized.completionSummary,
|
|
189
|
+
runId: pickFirstString(unwrapped.payload, ["run_id", "runId"]),
|
|
190
|
+
taskId: pickFirstString(unwrapped.payload, ["task_id", "taskId"]),
|
|
191
|
+
sessionId: pickFirstString(unwrapped.payload, ["session_id", "sessionId", "session"]),
|
|
192
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
193
|
+
wrappers: unwrapped.wrappers,
|
|
194
|
+
payload: unwrapped.payload
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function asString(value) {
|
|
198
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
199
|
+
}
|
|
200
|
+
function scoreSessionCandidate(candidate, ctx) {
|
|
201
|
+
const id = asString(candidate?.id) || "";
|
|
202
|
+
const candidateText = JSON.stringify(candidate || {}).toLowerCase();
|
|
203
|
+
const runId = ctx.runId?.toLowerCase();
|
|
204
|
+
const taskId = ctx.taskId?.toLowerCase();
|
|
205
|
+
const sessionKey = ctx.sessionKey?.toLowerCase();
|
|
206
|
+
const artifactSessionId = ctx.artifactSessionId;
|
|
207
|
+
let score = 0;
|
|
208
|
+
if (artifactSessionId && id === artifactSessionId) score += 1e3;
|
|
209
|
+
if (runId && candidateText.includes(runId)) score += 120;
|
|
210
|
+
if (taskId && candidateText.includes(taskId)) score += 70;
|
|
211
|
+
if (sessionKey && candidateText.includes(sessionKey)) score += 60;
|
|
212
|
+
if (runId && id.toLowerCase().includes(runId)) score += 30;
|
|
213
|
+
if (taskId && id.toLowerCase().includes(taskId)) score += 20;
|
|
214
|
+
score += Math.max(0, 20 - ctx.recencyRank);
|
|
215
|
+
return score;
|
|
216
|
+
}
|
|
217
|
+
function summarizeLifecycle(events = []) {
|
|
218
|
+
let currentState = null;
|
|
219
|
+
let last_event_kind = null;
|
|
220
|
+
let last_event_at = null;
|
|
221
|
+
const files_changed = /* @__PURE__ */ new Set();
|
|
222
|
+
const verify_summary = [];
|
|
223
|
+
const blockers = /* @__PURE__ */ new Set();
|
|
224
|
+
let completion_summary = null;
|
|
225
|
+
for (const item of events) {
|
|
226
|
+
if (item.lifecycleState) currentState = item.lifecycleState;
|
|
227
|
+
if (item.kind) last_event_kind = item.kind;
|
|
228
|
+
if (item.timestamp) last_event_at = item.timestamp;
|
|
229
|
+
for (const file of item.filesChanged || []) files_changed.add(file);
|
|
230
|
+
if (item.verifySummary) verify_summary.push(item.verifySummary);
|
|
231
|
+
for (const blocker of item.blockers || []) blockers.add(blocker);
|
|
232
|
+
if (item.completionSummary) completion_summary = item.completionSummary;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
currentState,
|
|
236
|
+
current_state: currentState,
|
|
237
|
+
last_event_kind,
|
|
238
|
+
last_event_at,
|
|
239
|
+
files_changed: [...files_changed],
|
|
240
|
+
verify_summary,
|
|
241
|
+
blockers: [...blockers],
|
|
242
|
+
completion_summary
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function resolveSessionId(input) {
|
|
246
|
+
if (input.explicitSessionId) {
|
|
247
|
+
return { sessionId: input.explicitSessionId, strategy: "explicit", score: 9999 };
|
|
248
|
+
}
|
|
249
|
+
const list = Array.isArray(input.sessionList) ? input.sessionList : [];
|
|
250
|
+
const withRecency = [...list].map((item, idx) => {
|
|
251
|
+
const updated = Number(item?.time?.updated || 0);
|
|
252
|
+
return { item, idx, updated };
|
|
253
|
+
}).sort((a, b) => b.updated - a.updated).map((x, rank) => ({ ...x, rank }));
|
|
254
|
+
if (input.artifactSessionId) {
|
|
255
|
+
const direct = withRecency.find((x) => asString(x.item?.id) === input.artifactSessionId);
|
|
256
|
+
if (direct) {
|
|
257
|
+
return { sessionId: input.artifactSessionId, strategy: "artifact", score: 1e3 };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
let best = { score: -1 };
|
|
261
|
+
for (const candidate of withRecency) {
|
|
262
|
+
const id = asString(candidate.item?.id);
|
|
263
|
+
if (!id) continue;
|
|
264
|
+
const score = scoreSessionCandidate(candidate.item, {
|
|
265
|
+
runId: input.runId,
|
|
266
|
+
taskId: input.taskId,
|
|
267
|
+
sessionKey: input.sessionKey,
|
|
268
|
+
artifactSessionId: input.artifactSessionId,
|
|
269
|
+
recencyRank: candidate.rank
|
|
270
|
+
});
|
|
271
|
+
if (score > best.score) best = { id, score };
|
|
272
|
+
}
|
|
273
|
+
if (best.id && best.score > 0) {
|
|
274
|
+
return { sessionId: best.id, strategy: "scored_fallback", score: best.score };
|
|
275
|
+
}
|
|
276
|
+
const latest = withRecency.find((x) => asString(x.item?.id));
|
|
277
|
+
if (latest) return { sessionId: asString(latest.item?.id), strategy: "latest", score: 0 };
|
|
278
|
+
return { strategy: "none", score: -1 };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export {
|
|
282
|
+
parseSseFramesFromBuffer,
|
|
283
|
+
parseSseData,
|
|
284
|
+
unwrapGlobalPayload,
|
|
285
|
+
normalizeOpenCodeEvent,
|
|
286
|
+
normalizeTypedEventV1,
|
|
287
|
+
summarizeLifecycle,
|
|
288
|
+
resolveSessionId
|
|
289
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/shared-contracts.ts
|
|
2
|
+
function buildTaggedSessionTitle(fields) {
|
|
3
|
+
return [
|
|
4
|
+
`${fields.taskId}`,
|
|
5
|
+
`runId=${fields.runId}`,
|
|
6
|
+
`taskId=${fields.taskId}`,
|
|
7
|
+
`requested=${fields.requested}`,
|
|
8
|
+
`resolved=${fields.resolved}`,
|
|
9
|
+
`callbackSession=${fields.callbackSession}`,
|
|
10
|
+
...fields.callbackSessionId ? [`callbackSessionId=${fields.callbackSessionId}`] : [],
|
|
11
|
+
...fields.projectId ? [`projectId=${fields.projectId}`] : [],
|
|
12
|
+
...fields.repoRoot ? [`repoRoot=${fields.repoRoot}`] : []
|
|
13
|
+
].join(" ");
|
|
14
|
+
}
|
|
15
|
+
function parseTaggedSessionTitle(title) {
|
|
16
|
+
if (!title || !title.trim()) return null;
|
|
17
|
+
const tags = {};
|
|
18
|
+
for (const token of title.split(/\s+/)) {
|
|
19
|
+
const idx = token.indexOf("=");
|
|
20
|
+
if (idx <= 0) continue;
|
|
21
|
+
const key = token.slice(0, idx).trim();
|
|
22
|
+
const raw = token.slice(idx + 1).trim();
|
|
23
|
+
if (!key || !raw) continue;
|
|
24
|
+
tags[key] = raw;
|
|
25
|
+
}
|
|
26
|
+
return Object.keys(tags).length > 0 ? tags : null;
|
|
27
|
+
}
|
|
28
|
+
function buildPluginCallbackDedupeKey(input) {
|
|
29
|
+
return `${input.sessionId || "no-session"}|${input.runId || "no-run"}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
buildTaggedSessionTitle,
|
|
34
|
+
parseTaggedSessionTitle,
|
|
35
|
+
buildPluginCallbackDedupeKey
|
|
36
|
+
};
|