@mrc2204/opencode-bridge 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md ADDED
@@ -0,0 +1,115 @@
1
+ # @mrc2204/opencode-bridge
2
+
3
+ OpenClaw ↔ OpenCode bridge plugin for multi-agent orchestration, callback routing, SSE probing, run observability, and runtime serve management.
4
+
5
+ ## Goals
6
+ - Standardize callback flow from OpenCode back into OpenClaw via `/hooks/agent`
7
+ - Standardize `sessionKey` and routing envelope
8
+ - Support **1 project = 1 OpenCode serve instance**
9
+ - Provide runtime-ops baseline for:
10
+ - SSE probe / event normalization
11
+ - run status / run events / session tail
12
+ - serve registry / spawn / reuse / shutdown / idle evaluation
13
+
14
+ ## Install from npm
15
+ ```bash
16
+ openclaw plugins install @mrc2204/openclaw-opencode-bridge
17
+ ```
18
+
19
+ ## Repository structure (build-based)
20
+ ```text
21
+ openclaw-opencode-bridge/
22
+ ├── openclaw.plugin.json
23
+ ├── package.json
24
+ ├── tsconfig.json
25
+ ├── src/
26
+ │ ├── index.ts
27
+ │ ├── observability.ts
28
+ │ └── types.ts
29
+ ├── dist/ # generated by npm run build
30
+ ├── test/
31
+ │ ├── run-tests.ts
32
+ │ └── observability.test.ts
33
+ ├── skills/
34
+ │ └── opencode-orchestration/
35
+ │ └── SKILL.md
36
+ ├── README.md
37
+ └── README.en.md
38
+ ```
39
+
40
+ ## Available tools
41
+ - `opencode_status`
42
+ - `opencode_resolve_project`
43
+ - `opencode_build_envelope`
44
+ - `opencode_check_hook_policy`
45
+ - `opencode_evaluate_lifecycle`
46
+ - `opencode_run_status`
47
+ - `opencode_run_events`
48
+ - `opencode_session_tail`
49
+ - `opencode_serve_spawn`
50
+ - `opencode_registry_get`
51
+ - `opencode_registry_upsert`
52
+ - `opencode_registry_cleanup`
53
+ - `opencode_serve_shutdown`
54
+ - `opencode_serve_idle_check`
55
+
56
+ ## Current assumptions
57
+ - `1 project = 1 opencode serve instance`
58
+ - callback primary = `/hooks/agent`
59
+ - `sessionKey` convention = `hook:opencode:<agentId>:<taskId>`
60
+ - `opencode_server_url` is required in practical routing envelopes
61
+
62
+ ## Build & test
63
+ ```bash
64
+ npm install
65
+ npm run build
66
+ npm run typecheck
67
+ npm run test
68
+ ```
69
+
70
+ After `npm run build`, runtime entrypoints point to built artifacts in `dist/`:
71
+ - `main`: `./dist/index.js`
72
+ - `types`: `./dist/index.d.ts`
73
+ - `openclaw.extensions`: `./dist/index.js`
74
+
75
+ ## Publish flow (safe)
76
+ ```bash
77
+ npm run build
78
+ npm run test
79
+ npm pack --dry-run
80
+ # if all good:
81
+ # npm publish
82
+ ```
83
+
84
+ Use `npm pack --dry-run` to confirm package contents are build artifacts (`dist/`, `skills/`, readmes, plugin manifest), not raw TypeScript entrypoints.
85
+
86
+ ## Runtime config
87
+ Plugin-owned config path:
88
+ ```text
89
+ ~/.openclaw/opencode-bridge/config.json
90
+ ```
91
+
92
+ Example:
93
+ ```json
94
+ {
95
+ "opencodeServerUrl": "http://127.0.0.1:4096",
96
+ "hookBaseUrl": "http://127.0.0.1:18789",
97
+ "hookToken": "<OPENCLAW_HOOK_TOKEN>",
98
+ "projectRegistry": [
99
+ {
100
+ "projectId": "agent-smart-memo",
101
+ "repoRoot": "/Users/me/Work/projects/agent-smart-memo",
102
+ "serverUrl": "http://127.0.0.1:4096",
103
+ "idleTimeoutMs": 900000
104
+ }
105
+ ]
106
+ }
107
+ ```
108
+
109
+ ## Local development install
110
+ ```bash
111
+ openclaw plugins install -l ~/Work/projects/opencode-bridge
112
+ ```
113
+
114
+ ## License
115
+ MIT
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @mrc2204/opencode-bridge
2
+
3
+ > English README: [README.en.md](./README.en.md)
4
+
5
+ Plugin bridge giữa **OpenClaw** và **OpenCode** cho orchestration multi-agent, callback routing, SSE probing, run observability và serve runtime management.
6
+
7
+ ## Mục tiêu
8
+ - Chuẩn hóa callback path từ OpenCode về OpenClaw qua `/hooks/agent`
9
+ - Chuẩn hóa `sessionKey` / `routing envelope`
10
+ - Hỗ trợ mô hình vận hành: **1 project = 1 OpenCode serve instance**
11
+ - Cung cấp runtime-ops baseline:
12
+ - SSE probe / event normalize
13
+ - run status / run events / session tail
14
+ - serve registry / spawn / reuse / shutdown / idle evaluation
15
+
16
+ ## Cài package từ npm
17
+ ```bash
18
+ openclaw plugins install @mrc2204/openclaw-opencode-bridge
19
+ ```
20
+
21
+ ## Cấu trúc repo (build-based)
22
+ ```text
23
+ openclaw-opencode-bridge/
24
+ ├── openclaw.plugin.json
25
+ ├── package.json
26
+ ├── tsconfig.json
27
+ ├── src/
28
+ │ ├── index.ts
29
+ │ ├── observability.ts
30
+ │ └── types.ts
31
+ ├── dist/ # artifact tạo bởi npm run build
32
+ ├── test/
33
+ │ ├── run-tests.ts
34
+ │ └── observability.test.ts
35
+ ├── skills/
36
+ │ └── opencode-orchestration/
37
+ │ └── SKILL.md
38
+ ├── README.md
39
+ └── README.en.md
40
+ ```
41
+
42
+ ## Tool hiện có
43
+ - `opencode_status`
44
+ - `opencode_resolve_project`
45
+ - `opencode_build_envelope`
46
+ - `opencode_check_hook_policy`
47
+ - `opencode_evaluate_lifecycle`
48
+ - `opencode_run_status`
49
+ - `opencode_run_events`
50
+ - `opencode_session_tail`
51
+ - `opencode_serve_spawn`
52
+ - `opencode_registry_get`
53
+ - `opencode_registry_upsert`
54
+ - `opencode_registry_cleanup`
55
+ - `opencode_serve_shutdown`
56
+ - `opencode_serve_idle_check`
57
+
58
+ ## Assumptions hiện tại
59
+ - `1 project = 1 opencode serve instance`
60
+ - callback primary = `/hooks/agent`
61
+ - `sessionKey` convention = `hook:opencode:<agentId>:<taskId>`
62
+ - `opencode_server_url` là field bắt buộc trong envelope routing thực tế
63
+
64
+ ## Build/Test
65
+ ```bash
66
+ npm install
67
+ npm run build
68
+ npm run typecheck
69
+ npm run test
70
+ ```
71
+
72
+ Sau `npm run build`, entrypoint runtime dùng artifact trong `dist/`:
73
+ - `main`: `./dist/index.js`
74
+ - `types`: `./dist/index.d.ts`
75
+ - `openclaw.extensions`: `./dist/index.js`
76
+
77
+ ## Publish flow (safe)
78
+ ```bash
79
+ npm run build
80
+ npm run test
81
+ npm pack --dry-run
82
+ # nếu OK:
83
+ # npm publish
84
+ ```
85
+
86
+ `npm pack --dry-run` dùng để kiểm tra package chỉ publish artifact cần thiết (`dist/`, `skills/`, readme, plugin manifest), không trỏ trực tiếp vào source TypeScript.
87
+
88
+ ## Config runtime
89
+ Plugin dùng plugin-owned config tại:
90
+ ```text
91
+ ~/.openclaw/opencode-bridge/config.json
92
+ ```
93
+
94
+ Ví dụ:
95
+ ```json
96
+ {
97
+ "opencodeServerUrl": "http://127.0.0.1:4096",
98
+ "hookBaseUrl": "http://127.0.0.1:18789",
99
+ "hookToken": "<OPENCLAW_HOOK_TOKEN>",
100
+ "projectRegistry": [
101
+ {
102
+ "projectId": "agent-smart-memo",
103
+ "repoRoot": "/Users/me/Work/projects/agent-smart-memo",
104
+ "serverUrl": "http://127.0.0.1:4096",
105
+ "idleTimeoutMs": 900000
106
+ }
107
+ ]
108
+ }
109
+ ```
110
+
111
+ ## Local dev plugin install
112
+ ```bash
113
+ openclaw plugins install -l ~/Work/projects/opencode-bridge
114
+ ```
115
+
116
+ ## License
117
+ MIT
@@ -0,0 +1,176 @@
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 normalizeOpenCodeEvent(raw) {
73
+ const text = JSON.stringify(raw || {}).toLowerCase();
74
+ if (text.includes("permission") || text.includes("question.asked")) {
75
+ return { kind: "permission.requested", summary: "OpenCode \u0111ang ch\u1EDD permission/user input", raw };
76
+ }
77
+ if (text.includes("error") || text.includes("failed")) {
78
+ return { kind: "task.failed", summary: "OpenCode task failed", raw };
79
+ }
80
+ if (text.includes("stalled")) {
81
+ return { kind: "task.stalled", summary: "OpenCode task stalled", raw };
82
+ }
83
+ if (text.includes("complete") || text.includes("completed") || text.includes("done")) {
84
+ return { kind: "task.completed", summary: "OpenCode task completed", raw };
85
+ }
86
+ if (text.includes("progress") || text.includes("delta") || text.includes("assistant")) {
87
+ return { kind: "task.progress", summary: "OpenCode task made progress", raw };
88
+ }
89
+ if (text.includes("start") || text.includes("created") || text.includes("connected")) {
90
+ return { kind: "task.started", summary: "OpenCode task/server started", raw };
91
+ }
92
+ return { kind: null, raw };
93
+ }
94
+ function normalizeTypedEventV1(frame, scope) {
95
+ const parsed = parseSseData(frame.data);
96
+ const unwrapped = unwrapGlobalPayload(parsed);
97
+ const normalized = normalizeOpenCodeEvent(unwrapped.payload);
98
+ return {
99
+ schema: "opencode.event.v1",
100
+ scope,
101
+ eventName: frame.event,
102
+ eventId: frame.id,
103
+ kind: normalized.kind,
104
+ summary: normalized.summary,
105
+ runId: pickFirstString(unwrapped.payload, ["run_id", "runId"]),
106
+ taskId: pickFirstString(unwrapped.payload, ["task_id", "taskId"]),
107
+ sessionId: pickFirstString(unwrapped.payload, ["session_id", "sessionId", "session"]),
108
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
109
+ wrappers: unwrapped.wrappers,
110
+ payload: unwrapped.payload
111
+ };
112
+ }
113
+ function asString(value) {
114
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
115
+ }
116
+ function scoreSessionCandidate(candidate, ctx) {
117
+ const id = asString(candidate?.id) || "";
118
+ const candidateText = JSON.stringify(candidate || {}).toLowerCase();
119
+ const runId = ctx.runId?.toLowerCase();
120
+ const taskId = ctx.taskId?.toLowerCase();
121
+ const sessionKey = ctx.sessionKey?.toLowerCase();
122
+ const artifactSessionId = ctx.artifactSessionId;
123
+ let score = 0;
124
+ if (artifactSessionId && id === artifactSessionId) score += 1e3;
125
+ if (runId && candidateText.includes(runId)) score += 120;
126
+ if (taskId && candidateText.includes(taskId)) score += 70;
127
+ if (sessionKey && candidateText.includes(sessionKey)) score += 60;
128
+ if (runId && id.toLowerCase().includes(runId)) score += 30;
129
+ if (taskId && id.toLowerCase().includes(taskId)) score += 20;
130
+ score += Math.max(0, 20 - ctx.recencyRank);
131
+ return score;
132
+ }
133
+ function resolveSessionId(input) {
134
+ if (input.explicitSessionId) {
135
+ return { sessionId: input.explicitSessionId, strategy: "explicit", score: 9999 };
136
+ }
137
+ const list = Array.isArray(input.sessionList) ? input.sessionList : [];
138
+ const withRecency = [...list].map((item, idx) => {
139
+ const updated = Number(item?.time?.updated || 0);
140
+ return { item, idx, updated };
141
+ }).sort((a, b) => b.updated - a.updated).map((x, rank) => ({ ...x, rank }));
142
+ if (input.artifactSessionId) {
143
+ const direct = withRecency.find((x) => asString(x.item?.id) === input.artifactSessionId);
144
+ if (direct) {
145
+ return { sessionId: input.artifactSessionId, strategy: "artifact", score: 1e3 };
146
+ }
147
+ }
148
+ let best = { score: -1 };
149
+ for (const candidate of withRecency) {
150
+ const id = asString(candidate.item?.id);
151
+ if (!id) continue;
152
+ const score = scoreSessionCandidate(candidate.item, {
153
+ runId: input.runId,
154
+ taskId: input.taskId,
155
+ sessionKey: input.sessionKey,
156
+ artifactSessionId: input.artifactSessionId,
157
+ recencyRank: candidate.rank
158
+ });
159
+ if (score > best.score) best = { id, score };
160
+ }
161
+ if (best.id && best.score > 0) {
162
+ return { sessionId: best.id, strategy: "scored_fallback", score: best.score };
163
+ }
164
+ const latest = withRecency.find((x) => asString(x.item?.id));
165
+ if (latest) return { sessionId: asString(latest.item?.id), strategy: "latest", score: 0 };
166
+ return { strategy: "none", score: -1 };
167
+ }
168
+
169
+ export {
170
+ parseSseFramesFromBuffer,
171
+ parseSseData,
172
+ unwrapGlobalPayload,
173
+ normalizeOpenCodeEvent,
174
+ normalizeTypedEventV1,
175
+ resolveSessionId
176
+ };
@@ -0,0 +1,8 @@
1
+ declare const plugin: {
2
+ id: string;
3
+ name: string;
4
+ version: string;
5
+ register(api: any): void;
6
+ };
7
+
8
+ export { plugin as default };