openclaw-opencode-bridge 2.0.6

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/DEMO_1.png ADDED
Binary file
package/DEMO_2.png ADDED
Binary file
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bettep (hongdaesik88@gmail.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # openclaw-opencode-bridge
2
+
3
+ [![npm version](https://img.shields.io/npm/v/openclaw-opencode-bridge)](https://www.npmjs.com/package/openclaw-opencode-bridge)
4
+ [![license](https://img.shields.io/npm/l/openclaw-opencode-bridge)](LICENSE)
5
+ [![node](https://img.shields.io/node/v/openclaw-opencode-bridge)](package.json)
6
+
7
+ > Forked from [openclaw-claude-bridge](https://github.com/bettep-dev/openclaw-claude-bridge) by [@bettep-dev](https://github.com/bettep-dev) — modified to work with OpenCode instead of Claude CLI.
8
+
9
+ Bridge [OpenClaw](https://openclaw.ai) messaging channels to [OpenCode](https://opencode.ai) via persistent tmux sessions.
10
+
11
+ Send `@cc` or `/cc` from any chat — your message is routed directly to OpenCode running in your terminal, bypassing the gateway LLM entirely. No separate API keys, no OAuth, no extra costs.
12
+
13
+ <p>
14
+ <img src="DEMO_1.png" alt="Telegram demo — sending a command" width="400" />
15
+ <img src="DEMO_2.png" alt="Telegram demo — receiving a response" width="400" />
16
+ </p>
17
+
18
+ > **⚠️ Telegram only.** This plugin has been developed and tested exclusively with the Telegram channel. Other channels (Discord, Slack, etc.) may use different message formats or metadata wrapping that could break prefix detection or LLM suppression. Community contributions for additional channels are welcome — please open an issue if you encounter problems.
19
+
20
+ ## How It Works
21
+
22
+ <img alt="Architecture" src="https://mermaid.ink/img/Z3JhcGggVEQKICAgIEFbIkNoYXQiXSAtLT58IkBjYyBtZXNzYWdlInwgQlsiT3BlbkNsYXcgR2F0ZXdheSJdCiAgICBCIC0tPiBDWyJvcGVuY29kZS1icmlkZ2UgcGx1Z2luIl0KICAgIEMgLS0+fHN1cHByZXNzIExMTHwgQgogICAgQyAtLT58ZXhlY0ZpbGV8IERbIlNoZWxsIFNjcmlwdCJdCiAgICBEIC0tPnx0bXV4IHBhc3RlLWJ1ZmZlcnwgRVsiT3BlbkNvZGUgLyB0bXV4Il0KICAgIEUgLS0+fCJvcGVuY2xhdyBtZXNzYWdlIHNlbmQifCBB" />
23
+
24
+ 1. User sends a prefixed message (e.g. `@cc deploy the app`)
25
+ 2. The plugin intercepts the message and suppresses the gateway LLM
26
+ 3. A shell script forwards the message to OpenCode in a persistent tmux session
27
+ 4. OpenCode replies back through the same channel via `openclaw message send`
28
+
29
+ ## Prerequisites
30
+
31
+ | Dependency | Install |
32
+ |---|---|
33
+ | [OpenClaw](https://openclaw.ai) | `npm i -g openclaw` |
34
+ | [OpenCode](https://opencode.ai) | `npm i -g opencode-ai` |
35
+ | [tmux](https://github.com/tmux/tmux) | Auto-installed during onboard if missing |
36
+
37
+ > **Note:** macOS and Linux only. Windows is not supported (tmux dependency).
38
+
39
+ ## Quick Start
40
+
41
+ ```bash
42
+ npm i -g openclaw-opencode-bridge
43
+ openclaw-opencode-bridge onboard
44
+ ```
45
+
46
+ The interactive wizard configures everything — plugin, shell scripts, OPENCODE.md, daemon, and channel settings.
47
+
48
+ Verify the connection:
49
+
50
+ ```
51
+ @cc hello
52
+ ```
53
+
54
+ ## Commands
55
+
56
+ | Prefix | Description |
57
+ |---|---|
58
+ | `@cc` · `/cc` | Send to the current session (retains conversation context) |
59
+ | `@ccn` · `/ccn` | Start a fresh session (kills existing, creates new) |
60
+ | `@ccu` · `/ccu` | Show OpenCode usage stats |
61
+ | `@ccm` · `/ccm` | List OpenCode models |
62
+ | `@ccms` · `/ccms` | Set OpenCode model (by number or model-id) |
63
+
64
+ Messages are sent as-is — no quoting needed:
65
+
66
+ ```
67
+ @cc refactor the auth module and add tests
68
+ @ccn review this PR: https://github.com/org/repo/pull/42
69
+ @ccu
70
+ ```
71
+
72
+ Multiline messages and special characters (`$`, `` ` ``, `\`, quotes) are preserved exactly as typed.
73
+
74
+ ## Migration from v1
75
+
76
+ v2 replaces the legacy skill/hook system with a single OpenClaw plugin:
77
+
78
+ ```bash
79
+ npm i -g openclaw-opencode-bridge
80
+ openclaw-opencode-bridge onboard
81
+ ```
82
+
83
+ The wizard detects and removes legacy components automatically.
84
+
85
+ ## Uninstall
86
+
87
+ ```bash
88
+ openclaw-opencode-bridge uninstall
89
+ ```
90
+
91
+ Removes all installed components — plugin, shell scripts, OPENCODE.md additions, and daemon.
92
+
93
+ ## Troubleshooting
94
+
95
+ | Symptom | Fix |
96
+ |---|---|
97
+ | LLM responds instead of delivery message | `openclaw gateway restart` |
98
+ | Delivery confirmed but no reply | Check `tmux ls` — session may have crashed |
99
+ | Multiline sends only first line | Re-run `openclaw-opencode-bridge onboard` (v2.0.6+) |
100
+
101
+ ## License
102
+
103
+ [MIT](LICENSE)
package/lib/cli.js ADDED
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { Command } = require("commander");
5
+ const util = require("util");
6
+ const { exec } = require("child_process");
7
+ const chalk = require("chalk");
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const pkg = require("../package.json");
11
+
12
+ const execAsync = util.promisify(exec);
13
+
14
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
15
+
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => setTimeout(resolve, ms));
18
+ }
19
+
20
+ function startSpinner(text) {
21
+ let i = 0;
22
+ const id = setInterval(() => {
23
+ process.stdout.write(
24
+ `\r\x1b[K ${chalk.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length])} ${text}`,
25
+ );
26
+ i++;
27
+ }, 80);
28
+ return {
29
+ stop(result) {
30
+ clearInterval(id);
31
+ process.stdout.write(`\r\x1b[K ${chalk.green("\u2713")} ${result}\n`);
32
+ },
33
+ fail(result) {
34
+ clearInterval(id);
35
+ process.stdout.write(`\r\x1b[K ${chalk.red("\u2717")} ${result}\n`);
36
+ },
37
+ };
38
+ }
39
+
40
+ const program = new Command();
41
+
42
+ program
43
+ .name("openclaw-opencode-bridge")
44
+ .description(
45
+ "Bridge OpenClaw messaging channels to OpenCode via tmux persistent sessions",
46
+ )
47
+ .version(pkg.version);
48
+
49
+ // --- onboard ---
50
+ program
51
+ .command("onboard")
52
+ .description(
53
+ "Set up the bridge (auto-detects most settings, just press Enter)",
54
+ )
55
+ .action(async () => {
56
+ try {
57
+ const { onboard } = require("./onboard");
58
+ await onboard();
59
+ } catch (e) {
60
+ console.error(chalk.red(`\n Setup failed: ${e.message}`));
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ // --- check ---
66
+ program
67
+ .command("check")
68
+ .description("Check if all dependencies are installed")
69
+ .action(() => {
70
+ const { checkAll, printStatus } = require("./deps");
71
+ const { detectOS, getWorkspace } = require("./platform");
72
+
73
+ console.log();
74
+ console.log(` ${chalk.bold("openclaw-opencode-bridge")} dependency check`);
75
+ console.log(` ${"─".repeat(35)}`);
76
+ console.log();
77
+
78
+ console.log(` ${chalk.bold("Dependencies:")}`);
79
+ const { results, allFound } = checkAll();
80
+ printStatus(results);
81
+ console.log();
82
+
83
+ console.log(` ${chalk.bold("Environment:")}`);
84
+ console.log(` OS: ${detectOS()} (${process.platform})`);
85
+ console.log(` Workspace: ${getWorkspace()}`);
86
+ console.log();
87
+
88
+ if (allFound) {
89
+ console.log(` ${chalk.green("\u2705")} All dependencies found.`);
90
+ } else {
91
+ console.log(
92
+ ` ${chalk.red("\u274c")} Missing dependencies. See above for install commands.`,
93
+ );
94
+ }
95
+ console.log();
96
+
97
+ process.exit(allFound ? 0 : 1);
98
+ });
99
+
100
+ // --- uninstall ---
101
+ program
102
+ .command("uninstall")
103
+ .description(
104
+ "Remove daemon, scripts, plugin, legacy hooks, skills, and AGENTS.md",
105
+ )
106
+ .action(async () => {
107
+ const {
108
+ removeDaemon,
109
+ getScriptsDir,
110
+ getWorkspace,
111
+ getHomeDir,
112
+ } = require("./platform");
113
+ const { BRIDGE_COMMANDS, SCRIPT_VARS } = require("./onboard");
114
+
115
+ console.log();
116
+ console.log(
117
+ ` ${chalk.bold.red("openclaw-opencode-bridge")} ${chalk.dim("uninstall")}`,
118
+ );
119
+ console.log(` ${"─".repeat(40)}`);
120
+ console.log();
121
+ await sleep(300);
122
+
123
+ // Daemon
124
+ const sp1 = startSpinner("Stopping daemon...");
125
+ await sleep(400);
126
+ const daemonRemoved = removeDaemon();
127
+ sp1.stop(
128
+ daemonRemoved
129
+ ? "Daemon stopped and removed"
130
+ : chalk.dim("No daemon found"),
131
+ );
132
+
133
+ await sleep(200);
134
+
135
+ // Scripts
136
+ const sp2 = startSpinner("Removing scripts...");
137
+ await sleep(400);
138
+ const scriptsDir = getScriptsDir();
139
+ const scriptFiles = Object.keys(SCRIPT_VARS);
140
+ let scriptsRemoved = 0;
141
+ for (const file of scriptFiles) {
142
+ const fp = path.join(scriptsDir, file);
143
+ if (fs.existsSync(fp)) {
144
+ fs.unlinkSync(fp);
145
+ scriptsRemoved++;
146
+ }
147
+ }
148
+ sp2.stop(
149
+ scriptsRemoved > 0
150
+ ? `Scripts removed from ${scriptsDir}`
151
+ : chalk.dim("No scripts found"),
152
+ );
153
+
154
+ await sleep(200);
155
+
156
+ // Plugin
157
+ const sp3 = startSpinner("Removing plugin...");
158
+ await sleep(400);
159
+ const homeDir = getHomeDir();
160
+ const pluginDir = path.join(
161
+ homeDir,
162
+ ".openclaw",
163
+ "plugins",
164
+ "opencode-bridge",
165
+ );
166
+ const legacyPluginDir = path.join(
167
+ homeDir,
168
+ ".openclaw",
169
+ "plugins",
170
+ "claude-bridge",
171
+ );
172
+ let pluginRemoved = false;
173
+
174
+ for (const dir of [pluginDir, legacyPluginDir]) {
175
+ if (fs.existsSync(dir)) {
176
+ fs.rmSync(dir, { recursive: true, force: true });
177
+ pluginRemoved = true;
178
+ }
179
+ }
180
+
181
+ sp3.stop(
182
+ pluginRemoved
183
+ ? "Plugin removed (opencode/claude-bridge)"
184
+ : chalk.dim("No plugin found"),
185
+ );
186
+
187
+ await sleep(200);
188
+
189
+ // Legacy hook
190
+ const workspace = getWorkspace();
191
+ for (const hook of ["opencode-bridge", "claude-bridge"]) {
192
+ const hookDir = path.join(workspace, "hooks", hook);
193
+ if (fs.existsSync(hookDir)) {
194
+ fs.rmSync(hookDir, { recursive: true, force: true });
195
+ }
196
+ }
197
+
198
+ // Legacy Skills
199
+ const skillsDir = path.join(workspace, "skills");
200
+ const skillNames = BRIDGE_COMMANDS.map((c) => c.command);
201
+ let skillsToRemove = [];
202
+ for (const skill of skillNames) {
203
+ const skillFile = path.join(skillsDir, skill, "SKILL.md");
204
+ if (fs.existsSync(skillFile)) {
205
+ skillsToRemove.push(skill);
206
+ }
207
+ }
208
+
209
+ if (skillsToRemove.length > 0) {
210
+ const sp4 = startSpinner("Cleaning legacy skills...");
211
+ await sleep(400);
212
+ let skillsRemoved = 0;
213
+ for (const skill of skillsToRemove) {
214
+ const skillFile = path.join(skillsDir, skill, "SKILL.md");
215
+ const skillDir = path.join(skillsDir, skill);
216
+ fs.unlinkSync(skillFile);
217
+ try {
218
+ fs.rmdirSync(skillDir);
219
+ } catch {}
220
+ skillsRemoved++;
221
+ }
222
+ sp4.stop(`Legacy skills removed (${skillsToRemove.join(", ")})`);
223
+ await sleep(200);
224
+ }
225
+
226
+ // Global AGENTS.md for OpenCode
227
+ const sp5 = startSpinner("Removing AGENTS.md...");
228
+ await sleep(400);
229
+ const opencodeAgentsPath = path.join(homeDir, ".config", "opencode", "AGENTS.md");
230
+ const opencodeAgentsExisted = fs.existsSync(opencodeAgentsPath);
231
+ if (opencodeAgentsExisted) {
232
+ fs.unlinkSync(opencodeAgentsPath);
233
+ }
234
+ sp5.stop(
235
+ opencodeAgentsExisted ? "AGENTS.md removed" : chalk.dim("No AGENTS.md found"),
236
+ );
237
+
238
+ await sleep(200);
239
+
240
+ // openclaw.json cleanup
241
+ const sp6 = startSpinner("Cleaning openclaw.json...");
242
+ await sleep(400);
243
+ const openclawConfigPath = path.join(homeDir, ".openclaw", "openclaw.json");
244
+ let configCleaned = false;
245
+ let commandsCleaned = false;
246
+
247
+ if (fs.existsSync(openclawConfigPath)) {
248
+ try {
249
+ const config = JSON.parse(fs.readFileSync(openclawConfigPath, "utf8"));
250
+ let configChanged = false;
251
+
252
+ for (const pluginId of ["opencode-bridge", "claude-bridge"]) {
253
+ if (config.plugins?.entries?.[pluginId]) {
254
+ delete config.plugins.entries[pluginId];
255
+ configChanged = true;
256
+ }
257
+ if (config.plugins?.installs?.[pluginId]) {
258
+ delete config.plugins.installs[pluginId];
259
+ configChanged = true;
260
+ }
261
+ if (Array.isArray(config.plugins?.allow)) {
262
+ const before = config.plugins.allow.length;
263
+ config.plugins.allow = config.plugins.allow.filter(
264
+ (id) => id !== pluginId,
265
+ );
266
+ if (config.plugins.allow.length < before) configChanged = true;
267
+ }
268
+ if (config.plugins?.[pluginId]) {
269
+ delete config.plugins[pluginId];
270
+ configChanged = true;
271
+ }
272
+ }
273
+
274
+ if (Array.isArray(config.plugins?.load?.paths)) {
275
+ const before = config.plugins.load.paths.length;
276
+ config.plugins.load.paths = config.plugins.load.paths.filter(
277
+ (p) => p !== pluginDir && p !== legacyPluginDir,
278
+ );
279
+ if (config.plugins.load.paths.length < before) configChanged = true;
280
+ }
281
+ configCleaned = configChanged;
282
+
283
+ let commandsRemoved = 0;
284
+ if (config.channels) {
285
+ for (const ch of Object.values(config.channels)) {
286
+ if (Array.isArray(ch.customCommands)) {
287
+ const before = ch.customCommands.length;
288
+ ch.customCommands = ch.customCommands.filter(
289
+ (c) => !skillNames.includes(c.command),
290
+ );
291
+ commandsRemoved += before - ch.customCommands.length;
292
+ }
293
+ }
294
+ }
295
+ commandsCleaned = commandsRemoved > 0;
296
+ if (commandsCleaned) configChanged = true;
297
+
298
+ if (configChanged) {
299
+ fs.writeFileSync(
300
+ openclawConfigPath,
301
+ `${JSON.stringify(config, null, 2)}\n`,
302
+ );
303
+ }
304
+ } catch {}
305
+ }
306
+
307
+ const configParts = [];
308
+ if (configCleaned) configParts.push("plugin config");
309
+ if (commandsCleaned) configParts.push("channel commands");
310
+ sp6.stop(
311
+ configParts.length > 0
312
+ ? `Cleaned openclaw.json (${configParts.join(", ")})`
313
+ : chalk.dim("No config changes needed"),
314
+ );
315
+
316
+ await sleep(200);
317
+
318
+ // Restart gateway
319
+ const sp7 = startSpinner("Restarting OpenClaw Gateway...");
320
+ await sleep(300);
321
+ let restarted = false;
322
+ try {
323
+ await execAsync("openclaw gateway restart", { timeout: 15000 });
324
+ restarted = true;
325
+ } catch {}
326
+
327
+ if (restarted) {
328
+ sp7.stop("Gateway restarted (plugin unloaded)");
329
+ } else {
330
+ sp7.fail(
331
+ chalk.dim("Could not restart gateway (restart manually if needed)"),
332
+ );
333
+ }
334
+
335
+ console.log();
336
+ await sleep(300);
337
+ console.log(` ${chalk.green("\u2705")} Uninstall complete.`);
338
+ console.log();
339
+ });
340
+
341
+ (async () => {
342
+ await program.parseAsync(process.argv);
343
+ })();
package/lib/deps.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { execSync } = require("child_process");
5
+ const chalk = require("chalk");
6
+
7
+ const DEPS = [
8
+ {
9
+ name: "openclaw",
10
+ check: "openclaw",
11
+ install: "npm i -g openclaw",
12
+ url: "https://openclaw.ai",
13
+ autoInstall: false,
14
+ },
15
+ {
16
+ name: "opencode",
17
+ check: "opencode",
18
+ install: "curl -fsSL https://opencode.ai/install | bash && source ~/.bashrc",
19
+ url: "https://opencode.ai",
20
+ autoInstall: true,
21
+ },
22
+ {
23
+ name: "tmux",
24
+ check: "tmux",
25
+ macOS: "brew install tmux",
26
+ linux: "sudo apt install -y tmux",
27
+ autoInstall: true,
28
+ },
29
+ {
30
+ name: "flock",
31
+ check: "flock",
32
+ macOS: "brew install flock",
33
+ linux: "sudo apt install -y util-linux",
34
+ autoInstall: true,
35
+ },
36
+ ];
37
+
38
+ function whichBin(name) {
39
+ if (!/^[a-z0-9_-]+$/i.test(name)) return null;
40
+ try {
41
+ return execSync(`command -v ${name}`, { encoding: "utf8" }).trim();
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function detectPackageManager() {
48
+ const platform = process.platform;
49
+ if (platform === "darwin") {
50
+ return whichBin("brew") ? "brew" : null;
51
+ }
52
+ if (whichBin("apt")) return "apt";
53
+ if (whichBin("yum")) return "yum";
54
+ return null;
55
+ }
56
+
57
+ function checkAll() {
58
+ const results = [];
59
+ let allFound = true;
60
+
61
+ for (const dep of DEPS) {
62
+ const path = whichBin(dep.check);
63
+ results.push({ ...dep, path, found: !!path });
64
+ if (!path) allFound = false;
65
+ }
66
+
67
+ return { results, allFound };
68
+ }
69
+
70
+ function printStatus(results) {
71
+ for (const dep of results) {
72
+ if (dep.found) {
73
+ console.log(
74
+ ` ${chalk.green("\u2713")} ${dep.name.padEnd(10)} ${chalk.dim(dep.path)}`,
75
+ );
76
+ } else {
77
+ const cmd = getInstallCmd(dep);
78
+ console.log(
79
+ ` ${chalk.red("\u2717")} ${dep.name.padEnd(10)} ${chalk.red("not found")}`,
80
+ );
81
+ if (cmd) console.log(` ${chalk.dim("Install:")} ${chalk.cyan(cmd)}`);
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get the install command for a dependency on the current platform.
88
+ */
89
+ function getInstallCmd(dep) {
90
+ if (dep.install) return dep.install;
91
+
92
+ const platform = process.platform;
93
+ if (platform === "darwin") return dep.macOS;
94
+
95
+ if (dep.name === "tmux") {
96
+ const pm = detectPackageManager();
97
+ if (pm === "apt") return "sudo apt install -y tmux";
98
+ if (pm === "yum") return "sudo yum install -y tmux";
99
+ return null;
100
+ }
101
+
102
+ return dep.linux;
103
+ }
104
+
105
+ /**
106
+ * Attempt to install a single dependency.
107
+ * Returns the binary path on success, null on failure.
108
+ */
109
+ function installDep(dep) {
110
+ const cmd = getInstallCmd(dep);
111
+ if (!cmd) return null;
112
+
113
+ try {
114
+ execSync(cmd, { stdio: "inherit", timeout: 120000 });
115
+ return whichBin(dep.check);
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ module.exports = {
122
+ DEPS,
123
+ checkAll,
124
+ printStatus,
125
+ whichBin,
126
+ getInstallCmd,
127
+ installDep,
128
+ };