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 +0 -0
- package/DEMO_2.png +0 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/lib/cli.js +343 -0
- package/lib/deps.js +128 -0
- package/lib/onboard.js +895 -0
- package/lib/platform.js +190 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +45 -0
- package/plugin/index.ts +136 -0
- package/plugin/openclaw.plugin.json +38 -0
- package/plugin/package.json +10 -0
- package/templates/daemon/linux.service +13 -0
- package/templates/daemon/macos.plist +23 -0
- package/templates/scripts/opencode-models.sh +41 -0
- package/templates/scripts/opencode-new-session.sh +46 -0
- package/templates/scripts/opencode-send.sh +31 -0
- package/templates/scripts/opencode-session.sh +37 -0
- package/templates/scripts/opencode-setmodel.sh +92 -0
- package/templates/scripts/opencode-stats.sh +27 -0
- package/templates/skills/cc/SKILL.md +70 -0
- package/templates/skills/ccn/SKILL.md +70 -0
- package/templates/skills/ccu/SKILL.md +44 -0
- package/templates/workspace/OPENCODE.md +41 -0
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
|
+
[](https://www.npmjs.com/package/openclaw-opencode-bridge)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](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
|
+
};
|