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/lib/onboard.js
ADDED
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const readline = require("readline");
|
|
7
|
+
const util = require("util");
|
|
8
|
+
const { exec, execSync } = require("child_process");
|
|
9
|
+
const chalk = require("chalk");
|
|
10
|
+
|
|
11
|
+
const execAsync = util.promisify(exec);
|
|
12
|
+
const { checkAll, getInstallCmd, installDep } = require("./deps");
|
|
13
|
+
const {
|
|
14
|
+
detectOS,
|
|
15
|
+
getHomeDir,
|
|
16
|
+
getWorkspace,
|
|
17
|
+
getScriptsDir,
|
|
18
|
+
installDaemon,
|
|
19
|
+
substituteTemplate,
|
|
20
|
+
} = require("./platform");
|
|
21
|
+
|
|
22
|
+
const CORE_CHANNELS = [
|
|
23
|
+
"telegram",
|
|
24
|
+
"discord",
|
|
25
|
+
"slack",
|
|
26
|
+
"whatsapp",
|
|
27
|
+
"signal",
|
|
28
|
+
"irc",
|
|
29
|
+
];
|
|
30
|
+
const PLUGIN_CHANNELS = ["matrix", "line", "mattermost", "teams"];
|
|
31
|
+
|
|
32
|
+
const SESSION_NAME = "opencode-daemon";
|
|
33
|
+
const CHANNELS_URL = "http://127.0.0.1:18789/channels";
|
|
34
|
+
|
|
35
|
+
// Commands to register in openclaw.json customCommands
|
|
36
|
+
const BRIDGE_COMMANDS = [
|
|
37
|
+
{ command: "cc", description: "Send instruction to existing OpenCode session" },
|
|
38
|
+
{
|
|
39
|
+
command: "ccn",
|
|
40
|
+
description: "Create new OpenCode session and send instruction",
|
|
41
|
+
},
|
|
42
|
+
{ command: "ccu", description: "Query OpenCode usage/stats" },
|
|
43
|
+
{ command: "ccm", description: "List available FREE OpenCode models" },
|
|
44
|
+
{ command: "ccms", description: "Set OpenCode model (number or shortcut)" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Variable names used in each script (for patching existing scripts)
|
|
48
|
+
const SCRIPT_VARS = {
|
|
49
|
+
"opencode-session.sh": {
|
|
50
|
+
TMUX: "TMUX_BIN",
|
|
51
|
+
OPENCODE: "OPENCODE_BIN",
|
|
52
|
+
WORKSPACE: "WORKSPACE",
|
|
53
|
+
SESSION: "SESSION_NAME",
|
|
54
|
+
},
|
|
55
|
+
"opencode-send.sh": {
|
|
56
|
+
TMUX: "TMUX_BIN",
|
|
57
|
+
CHANNEL: "CHANNEL",
|
|
58
|
+
TARGET: "TARGET_ID",
|
|
59
|
+
SESSION: "SESSION_NAME",
|
|
60
|
+
},
|
|
61
|
+
"opencode-new-session.sh": {
|
|
62
|
+
TMUX: "TMUX_BIN",
|
|
63
|
+
OPENCODE: "OPENCODE_BIN",
|
|
64
|
+
WORKSPACE: "WORKSPACE",
|
|
65
|
+
CHANNEL: "CHANNEL",
|
|
66
|
+
TARGET: "TARGET_ID",
|
|
67
|
+
SESSION: "SESSION_NAME",
|
|
68
|
+
},
|
|
69
|
+
"opencode-stats.sh": {
|
|
70
|
+
TMUX: "TMUX_BIN",
|
|
71
|
+
OPENCODE: "OPENCODE_BIN",
|
|
72
|
+
CHANNEL: "CHANNEL",
|
|
73
|
+
TARGET: "TARGET_ID",
|
|
74
|
+
WORKSPACE: "WORKSPACE",
|
|
75
|
+
},
|
|
76
|
+
"opencode-models.sh": {
|
|
77
|
+
TMUX: "TMUX_BIN",
|
|
78
|
+
OPENCODE: "OPENCODE_BIN",
|
|
79
|
+
CHANNEL: "CHANNEL",
|
|
80
|
+
TARGET: "TARGET_ID",
|
|
81
|
+
},
|
|
82
|
+
"opencode-setmodel.sh": {
|
|
83
|
+
TMUX: "TMUX_BIN",
|
|
84
|
+
OPENCODE: "OPENCODE_BIN",
|
|
85
|
+
CHANNEL: "CHANNEL",
|
|
86
|
+
TARGET: "TARGET_ID",
|
|
87
|
+
OPENCODE_CONFIG: "OPENCODE_CONFIG",
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// --- Utility functions ---
|
|
92
|
+
|
|
93
|
+
function sleep(ms) {
|
|
94
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ask(rl, question, defaultVal) {
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
const suffix = defaultVal ? chalk.dim(` [${defaultVal}]`) : "";
|
|
100
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
101
|
+
resolve(answer.trim() || defaultVal || "");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Interactive list selector with arrow keys.
|
|
108
|
+
* items: [{ label, tag?, separator? }]
|
|
109
|
+
* defaultIndex: initially highlighted index
|
|
110
|
+
* Returns the selected item's label.
|
|
111
|
+
*
|
|
112
|
+
* Falls back to simple numbered list if stdin is not a TTY (piped input).
|
|
113
|
+
*/
|
|
114
|
+
function selectList(items, defaultIndex = 0) {
|
|
115
|
+
const { stdin, stdout } = process;
|
|
116
|
+
|
|
117
|
+
// Fallback for non-TTY (piped input, CI, etc.)
|
|
118
|
+
if (!stdin.isTTY) {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const selectable = items.filter((i) => !i.separator);
|
|
121
|
+
for (let i = 0; i < selectable.length; i++) {
|
|
122
|
+
const marker = i === defaultIndex ? chalk.cyan.bold(">") : " ";
|
|
123
|
+
const tag = selectable[i].tag ? chalk.dim(` ${selectable[i].tag}`) : "";
|
|
124
|
+
console.log(` ${marker} ${i + 1}. ${selectable[i].label}${tag}`);
|
|
125
|
+
}
|
|
126
|
+
console.log();
|
|
127
|
+
const chosen = selectable[defaultIndex];
|
|
128
|
+
console.log(
|
|
129
|
+
` ${chalk.green("\u2713")} Selected: ${chalk.bold(chosen.label)}`,
|
|
130
|
+
);
|
|
131
|
+
resolve(chosen.label);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const wasRaw = stdin.isRaw;
|
|
137
|
+
|
|
138
|
+
// Ensure defaultIndex points to a selectable item
|
|
139
|
+
const selectableIndices = items
|
|
140
|
+
.map((item, i) => (item.separator ? -1 : i))
|
|
141
|
+
.filter((i) => i >= 0);
|
|
142
|
+
if (!selectableIndices.includes(defaultIndex)) {
|
|
143
|
+
defaultIndex = selectableIndices[0] ?? 0;
|
|
144
|
+
}
|
|
145
|
+
let cursor = defaultIndex;
|
|
146
|
+
|
|
147
|
+
const cleanup = () => {
|
|
148
|
+
try {
|
|
149
|
+
stdin.setRawMode(wasRaw || false);
|
|
150
|
+
} catch {}
|
|
151
|
+
stdin.removeListener("data", onKey);
|
|
152
|
+
stdin.pause();
|
|
153
|
+
process.removeListener("exit", cleanup);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
process.once("exit", cleanup);
|
|
157
|
+
|
|
158
|
+
function render(first) {
|
|
159
|
+
// Move cursor up to overwrite previous render (except on first draw)
|
|
160
|
+
if (!first) {
|
|
161
|
+
stdout.write(`\x1b[${items.length + 1}A`); // +1 for hint line
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < items.length; i++) {
|
|
165
|
+
const item = items[i];
|
|
166
|
+
|
|
167
|
+
// Separator line
|
|
168
|
+
if (item.separator) {
|
|
169
|
+
stdout.write(`\r\x1b[K ${chalk.dim("\u2500".repeat(36))}\n`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const isActive = i === cursor;
|
|
174
|
+
const pointer = isActive ? chalk.cyan.bold("\u276f") : " ";
|
|
175
|
+
const label = isActive
|
|
176
|
+
? chalk.cyan.bold(item.label)
|
|
177
|
+
: chalk.white(item.label);
|
|
178
|
+
const tag = item.tag
|
|
179
|
+
? isActive
|
|
180
|
+
? chalk.cyan.dim(` ${item.tag}`)
|
|
181
|
+
: chalk.dim(` ${item.tag}`)
|
|
182
|
+
: "";
|
|
183
|
+
|
|
184
|
+
stdout.write(`\r\x1b[K ${pointer} ${label}${tag}\n`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Hint line
|
|
188
|
+
stdout.write(
|
|
189
|
+
`\r\x1b[K ${chalk.dim("\u2191\u2193 navigate enter select")}\n`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function moveCursor(direction) {
|
|
194
|
+
const currentPos = selectableIndices.indexOf(cursor);
|
|
195
|
+
if (currentPos < 0) return;
|
|
196
|
+
|
|
197
|
+
let nextPos = currentPos + direction;
|
|
198
|
+
if (nextPos < 0) nextPos = selectableIndices.length - 1;
|
|
199
|
+
if (nextPos >= selectableIndices.length) nextPos = 0;
|
|
200
|
+
cursor = selectableIndices[nextPos];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
stdin.setRawMode(true);
|
|
204
|
+
stdin.resume();
|
|
205
|
+
stdin.setEncoding("utf8");
|
|
206
|
+
|
|
207
|
+
render(true);
|
|
208
|
+
|
|
209
|
+
function onKey(key) {
|
|
210
|
+
// Ctrl+C
|
|
211
|
+
if (key === "\x03") {
|
|
212
|
+
cleanup();
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Arrow up / k
|
|
217
|
+
if (key === "\x1b[A" || key === "k") {
|
|
218
|
+
moveCursor(-1);
|
|
219
|
+
render(false);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Arrow down / j
|
|
224
|
+
if (key === "\x1b[B" || key === "j") {
|
|
225
|
+
moveCursor(1);
|
|
226
|
+
render(false);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Enter
|
|
231
|
+
if (key === "\r" || key === "\n") {
|
|
232
|
+
cleanup();
|
|
233
|
+
|
|
234
|
+
// Overwrite the hint line with the selection result
|
|
235
|
+
stdout.write(
|
|
236
|
+
`\x1b[1A\r\x1b[K ${chalk.green("\u2713")} Selected: ${chalk.bold(items[cursor].label)}\n`,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
resolve(items[cursor].label);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
stdin.on("data", onKey);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
249
|
+
|
|
250
|
+
let activeSpinner = null;
|
|
251
|
+
|
|
252
|
+
function startSpinner(text) {
|
|
253
|
+
let i = 0;
|
|
254
|
+
const id = setInterval(() => {
|
|
255
|
+
process.stdout.write(
|
|
256
|
+
`\r\x1b[K ${chalk.cyan(SPINNER_FRAMES[i % SPINNER_FRAMES.length])} ${text}`,
|
|
257
|
+
);
|
|
258
|
+
i++;
|
|
259
|
+
}, 80);
|
|
260
|
+
const spinner = {
|
|
261
|
+
_id: id,
|
|
262
|
+
stop(result) {
|
|
263
|
+
clearInterval(id);
|
|
264
|
+
activeSpinner = null;
|
|
265
|
+
process.stdout.write(`\r\x1b[K ${chalk.green("\u2713")} ${result}\n`);
|
|
266
|
+
},
|
|
267
|
+
fail(result) {
|
|
268
|
+
clearInterval(id);
|
|
269
|
+
activeSpinner = null;
|
|
270
|
+
process.stdout.write(`\r\x1b[K ${chalk.red("\u2717")} ${result}\n`);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
activeSpinner = spinner;
|
|
274
|
+
return spinner;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatInstallMsg(label, dir, created, patched) {
|
|
278
|
+
const parts = [];
|
|
279
|
+
if (created > 0) parts.push(`${created} created`);
|
|
280
|
+
if (patched > 0) parts.push(`${patched} updated`);
|
|
281
|
+
return `${label.padEnd(11)} ${chalk.dim("->")} ${dir} ${chalk.dim(`(${parts.join(", ")})`)}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getScriptVersion(filePath) {
|
|
285
|
+
try {
|
|
286
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
287
|
+
const match = content.match(/^# bridge-version:\s*(\d+)/m);
|
|
288
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
289
|
+
} catch {
|
|
290
|
+
return 0;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getTemplateVersion(srcRel) {
|
|
295
|
+
const srcPath = path.join(__dirname, "..", "templates", srcRel);
|
|
296
|
+
return getScriptVersion(srcPath);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function copyTemplate(srcRel, destPath, vars) {
|
|
300
|
+
const srcPath = path.join(__dirname, "..", "templates", srcRel);
|
|
301
|
+
let content = fs.readFileSync(srcPath, "utf8");
|
|
302
|
+
content = substituteTemplate(content, vars);
|
|
303
|
+
const dir = path.dirname(destPath);
|
|
304
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
305
|
+
fs.writeFileSync(destPath, content);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function patchScriptVars(filePath, varMapping, vars) {
|
|
309
|
+
let content = fs.readFileSync(filePath, "utf8");
|
|
310
|
+
let patched = 0;
|
|
311
|
+
|
|
312
|
+
for (const [scriptVar, varsKey] of Object.entries(varMapping)) {
|
|
313
|
+
const newValue = vars[varsKey];
|
|
314
|
+
if (!newValue) continue;
|
|
315
|
+
|
|
316
|
+
const escaped = newValue.replace(/["\\`$]/g, "\\$&");
|
|
317
|
+
const regex = new RegExp(`^(${scriptVar}=).*$`, "gm");
|
|
318
|
+
const updated = content.replace(regex, `$1"${escaped}"`);
|
|
319
|
+
if (updated !== content) {
|
|
320
|
+
content = updated;
|
|
321
|
+
patched++;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (patched > 0) fs.writeFileSync(filePath, content);
|
|
326
|
+
return patched;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function getDateStr() {
|
|
330
|
+
const d = new Date();
|
|
331
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function openBrowser(url) {
|
|
335
|
+
try {
|
|
336
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
337
|
+
execSync(`${cmd} "${url}"`, { stdio: "ignore" });
|
|
338
|
+
return true;
|
|
339
|
+
} catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// --- Main onboard flow ---
|
|
345
|
+
|
|
346
|
+
async function onboard() {
|
|
347
|
+
const pkg = require("../package.json");
|
|
348
|
+
console.log();
|
|
349
|
+
console.log(
|
|
350
|
+
` ${chalk.bold("openclaw-opencode-bridge")} ${chalk.dim(`v${pkg.version}`)}`,
|
|
351
|
+
);
|
|
352
|
+
console.log(` ${"─".repeat(40)}`);
|
|
353
|
+
console.log();
|
|
354
|
+
await sleep(300);
|
|
355
|
+
|
|
356
|
+
// ========== Step 1: Check dependencies ==========
|
|
357
|
+
console.log(
|
|
358
|
+
` ${chalk.bold("[1/5]")} ${chalk.bold("Checking dependencies...")}`,
|
|
359
|
+
);
|
|
360
|
+
console.log();
|
|
361
|
+
await sleep(200);
|
|
362
|
+
|
|
363
|
+
const { results } = checkAll();
|
|
364
|
+
const missing = [];
|
|
365
|
+
|
|
366
|
+
for (const dep of results) {
|
|
367
|
+
await sleep(250);
|
|
368
|
+
if (dep.found) {
|
|
369
|
+
console.log(
|
|
370
|
+
` ${chalk.green("\u2713")} ${chalk.bold(dep.name.padEnd(10))} ${chalk.dim(dep.path)}`,
|
|
371
|
+
);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!dep.autoInstall) {
|
|
376
|
+
const cmd = getInstallCmd(dep);
|
|
377
|
+
console.log(
|
|
378
|
+
` ${chalk.red("\u2717")} ${chalk.bold(dep.name.padEnd(10))} ${chalk.red("not found")}`,
|
|
379
|
+
);
|
|
380
|
+
console.log(` ${chalk.dim("Install:")} ${chalk.cyan(cmd)}`);
|
|
381
|
+
if (dep.url) {
|
|
382
|
+
console.log(
|
|
383
|
+
` ${chalk.dim("Docs:")} ${chalk.underline(dep.url)}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
missing.push(dep);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const cmd = getInstallCmd(dep);
|
|
391
|
+
console.log(
|
|
392
|
+
` ${chalk.red("\u2717")} ${chalk.bold(dep.name.padEnd(10))} ${chalk.red("not found")}`,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
if (cmd) {
|
|
396
|
+
console.log(` ${chalk.cyan("\u2192")} Installing: ${chalk.bold(cmd)}`);
|
|
397
|
+
console.log();
|
|
398
|
+
const binPath = installDep(dep);
|
|
399
|
+
console.log();
|
|
400
|
+
|
|
401
|
+
if (binPath) {
|
|
402
|
+
dep.path = binPath;
|
|
403
|
+
dep.found = true;
|
|
404
|
+
console.log(
|
|
405
|
+
` ${chalk.green("\u2713")} ${chalk.bold(dep.name.padEnd(10))} ${chalk.dim(binPath)}`,
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
console.log(
|
|
409
|
+
` ${chalk.red("\u2717")} ${chalk.bold(dep.name.padEnd(10))} ${chalk.red("installation failed")}`,
|
|
410
|
+
);
|
|
411
|
+
missing.push(dep);
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
console.log(
|
|
415
|
+
` ${chalk.yellow("!")} No package manager found. Install manually.`,
|
|
416
|
+
);
|
|
417
|
+
missing.push(dep);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log();
|
|
422
|
+
|
|
423
|
+
if (missing.length > 0) {
|
|
424
|
+
const hasPrereqs = missing.some((d) => !d.autoInstall);
|
|
425
|
+
if (hasPrereqs) {
|
|
426
|
+
console.log(
|
|
427
|
+
chalk.red(
|
|
428
|
+
" OpenClaw and OpenCode must be installed before running this tool.",
|
|
429
|
+
),
|
|
430
|
+
);
|
|
431
|
+
console.log(
|
|
432
|
+
chalk.red(" Install them first, then run this command again."),
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(
|
|
436
|
+
chalk.red(
|
|
437
|
+
" Some dependencies could not be installed. Fix them and try again.",
|
|
438
|
+
),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await sleep(400);
|
|
445
|
+
|
|
446
|
+
// ========== Step 2: Detect environment ==========
|
|
447
|
+
console.log(
|
|
448
|
+
` ${chalk.bold("[2/5]")} ${chalk.bold("Detecting environment...")}`,
|
|
449
|
+
);
|
|
450
|
+
console.log();
|
|
451
|
+
|
|
452
|
+
const homeDir = getHomeDir();
|
|
453
|
+
const workspace = getWorkspace();
|
|
454
|
+
const osName = detectOS();
|
|
455
|
+
const tmuxBin = results.find((r) => r.name === "tmux").path;
|
|
456
|
+
const opencodeBin = results.find((r) => r.name === "opencode").path;
|
|
457
|
+
|
|
458
|
+
const envItems = [
|
|
459
|
+
["OS", `${osName} (${process.platform})`],
|
|
460
|
+
["Workspace", workspace],
|
|
461
|
+
["Home", homeDir],
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
for (const [label, value] of envItems) {
|
|
465
|
+
await sleep(200);
|
|
466
|
+
console.log(
|
|
467
|
+
` ${chalk.green("\u2713")} ${chalk.bold(label.padEnd(13))} ${chalk.dim(value)}`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
console.log();
|
|
472
|
+
await sleep(400);
|
|
473
|
+
|
|
474
|
+
// ========== Step 3: OpenCode config ==========
|
|
475
|
+
console.log(` ${chalk.bold("[3/5]")} ${chalk.bold("Configuring OpenCode...")}`);
|
|
476
|
+
console.log();
|
|
477
|
+
|
|
478
|
+
const opencodeConfigDir = path.join(homeDir, ".config", "opencode");
|
|
479
|
+
const opencodeConfigPath = path.join(opencodeConfigDir, "opencode.json");
|
|
480
|
+
|
|
481
|
+
if (!fs.existsSync(opencodeConfigDir)) {
|
|
482
|
+
fs.mkdirSync(opencodeConfigDir, { recursive: true });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let opencodeConfig = {};
|
|
486
|
+
const opencodeConfigExists = fs.existsSync(opencodeConfigPath);
|
|
487
|
+
|
|
488
|
+
if (opencodeConfigExists) {
|
|
489
|
+
try {
|
|
490
|
+
opencodeConfig = JSON.parse(fs.readFileSync(opencodeConfigPath, "utf8"));
|
|
491
|
+
console.log(` ${chalk.green("\u2713")} Existing config found`);
|
|
492
|
+
} catch {
|
|
493
|
+
opencodeConfig = {};
|
|
494
|
+
console.log(` ${chalk.yellow("!")} Corrupted config, creating new`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const defaultOpencodeConfig = {
|
|
499
|
+
$schema: "https://opencode.ai/config.json",
|
|
500
|
+
permission: {
|
|
501
|
+
read: "allow",
|
|
502
|
+
grep: "allow",
|
|
503
|
+
glob: "allow",
|
|
504
|
+
list: "allow",
|
|
505
|
+
edit: "allow",
|
|
506
|
+
write: "allow",
|
|
507
|
+
bash: "allow",
|
|
508
|
+
webfetch: "allow",
|
|
509
|
+
websearch: "allow",
|
|
510
|
+
question: "allow",
|
|
511
|
+
todowrite: "allow",
|
|
512
|
+
todoread: "allow",
|
|
513
|
+
skill: "allow",
|
|
514
|
+
patch: "allow",
|
|
515
|
+
lsp: "allow",
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
let configUpdated = false;
|
|
520
|
+
if (!opencodeConfig.permission) {
|
|
521
|
+
opencodeConfig = { ...defaultOpencodeConfig, ...opencodeConfig };
|
|
522
|
+
configUpdated = true;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (configUpdated || !opencodeConfigExists) {
|
|
526
|
+
fs.writeFileSync(opencodeConfigPath, JSON.stringify(opencodeConfig, null, 2));
|
|
527
|
+
console.log(` ${chalk.green("\u2713")} Config saved: ${opencodeConfigPath}`);
|
|
528
|
+
} else {
|
|
529
|
+
console.log(` ${chalk.green("\u2713")} Permissions already configured`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
console.log();
|
|
533
|
+
await sleep(400);
|
|
534
|
+
|
|
535
|
+
// ========== Step 4: Channel setup ==========
|
|
536
|
+
console.log(` ${chalk.bold("[4/5]")} ${chalk.bold("Channel setup")}`);
|
|
537
|
+
console.log();
|
|
538
|
+
await sleep(200);
|
|
539
|
+
|
|
540
|
+
console.log(` ${chalk.bold("Select a channel:")}`);
|
|
541
|
+
console.log();
|
|
542
|
+
|
|
543
|
+
const channelItems = [
|
|
544
|
+
...CORE_CHANNELS.map((c) => ({ label: c })),
|
|
545
|
+
{ separator: true },
|
|
546
|
+
...PLUGIN_CHANNELS.map((c) => ({ label: c, tag: "plugin" })),
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
const channel = await selectList(channelItems, 0);
|
|
550
|
+
console.log();
|
|
551
|
+
|
|
552
|
+
// --- Channel target ID ---
|
|
553
|
+
console.log(` ${chalk.yellow("?")} ${chalk.bold("Your ${channel} User/Chat ID")}`);
|
|
554
|
+
console.log();
|
|
555
|
+
console.log(` OpenCode needs to know where to send replies.`);
|
|
556
|
+
console.log();
|
|
557
|
+
|
|
558
|
+
if (channel === "telegram") {
|
|
559
|
+
console.log(` ${chalk.cyan("1.")} Open this bot: ${chalk.underline("https://t.me/userinfobot")}`);
|
|
560
|
+
console.log(` ${chalk.cyan("2.")} It will show your Telegram User ID`);
|
|
561
|
+
console.log(` ${chalk.cyan("3.")} Copy and paste it below`);
|
|
562
|
+
} else {
|
|
563
|
+
console.log(
|
|
564
|
+
` ${chalk.dim("Enter your channel/chat ID for")} ${chalk.bold(channel)}`,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
console.log();
|
|
568
|
+
|
|
569
|
+
const rl = readline.createInterface({
|
|
570
|
+
input: process.stdin,
|
|
571
|
+
output: process.stdout,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
const targetId = await ask(rl, "Paste your user ID");
|
|
575
|
+
rl.close();
|
|
576
|
+
|
|
577
|
+
if (!targetId) {
|
|
578
|
+
console.log(chalk.red("\n Target ID is required."));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
console.log();
|
|
583
|
+
await sleep(300);
|
|
584
|
+
|
|
585
|
+
// ========== Step 5: Install ==========
|
|
586
|
+
console.log(` ${chalk.bold("[5/5]")} ${chalk.bold("Installing...")}`);
|
|
587
|
+
console.log();
|
|
588
|
+
|
|
589
|
+
const scriptsDir = getScriptsDir();
|
|
590
|
+
const skillsDir = path.join(workspace, "skills");
|
|
591
|
+
|
|
592
|
+
const vars = {
|
|
593
|
+
CHANNEL: channel,
|
|
594
|
+
TARGET_ID: targetId,
|
|
595
|
+
WORKSPACE: workspace,
|
|
596
|
+
TMUX_BIN: tmuxBin,
|
|
597
|
+
OPENCODE_BIN: opencodeBin,
|
|
598
|
+
HOME_DIR: homeDir,
|
|
599
|
+
SESSION_NAME: SESSION_NAME,
|
|
600
|
+
SCRIPTS_DIR: scriptsDir,
|
|
601
|
+
OPENCODE_CONFIG: path.join(homeDir, ".config", "opencode", "opencode.json"),
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// --- 4a. Scripts ---
|
|
605
|
+
const spinner1 = startSpinner("Installing scripts...");
|
|
606
|
+
await sleep(400);
|
|
607
|
+
|
|
608
|
+
const scriptFiles = Object.keys(SCRIPT_VARS);
|
|
609
|
+
let scriptsCreated = 0;
|
|
610
|
+
let scriptsPatched = 0;
|
|
611
|
+
|
|
612
|
+
for (const file of scriptFiles) {
|
|
613
|
+
const dest = path.join(scriptsDir, file);
|
|
614
|
+
const tplRel = path.join("scripts", file);
|
|
615
|
+
|
|
616
|
+
if (fs.existsSync(dest)) {
|
|
617
|
+
const installedVer = getScriptVersion(dest);
|
|
618
|
+
const templateVer = getTemplateVersion(tplRel);
|
|
619
|
+
|
|
620
|
+
if (installedVer < templateVer) {
|
|
621
|
+
// Structure changed — replace with new template
|
|
622
|
+
copyTemplate(tplRel, dest, vars);
|
|
623
|
+
fs.chmodSync(dest, 0o755);
|
|
624
|
+
scriptsCreated++;
|
|
625
|
+
} else {
|
|
626
|
+
// Same version — patch variables only
|
|
627
|
+
const varMapping = SCRIPT_VARS[file];
|
|
628
|
+
if (varMapping) {
|
|
629
|
+
patchScriptVars(dest, varMapping, vars);
|
|
630
|
+
scriptsPatched++;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
copyTemplate(tplRel, dest, vars);
|
|
635
|
+
fs.chmodSync(dest, 0o755);
|
|
636
|
+
scriptsCreated++;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
spinner1.stop(
|
|
641
|
+
formatInstallMsg("Scripts", scriptsDir, scriptsCreated, scriptsPatched),
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
await sleep(300);
|
|
645
|
+
|
|
646
|
+
// --- 4b. Plugin (prefix detection + LLM suppression) ---
|
|
647
|
+
const spinner2a = startSpinner("Installing plugin...");
|
|
648
|
+
await sleep(400);
|
|
649
|
+
|
|
650
|
+
const pluginDir = path.join(homeDir, ".openclaw", "plugins", "opencode-bridge");
|
|
651
|
+
const pluginFiles = ["openclaw.plugin.json", "package.json", "index.ts"];
|
|
652
|
+
let pluginCreated = 0;
|
|
653
|
+
let pluginPatched = 0;
|
|
654
|
+
|
|
655
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
656
|
+
|
|
657
|
+
for (const file of pluginFiles) {
|
|
658
|
+
const dest = path.join(pluginDir, file);
|
|
659
|
+
const existed = fs.existsSync(dest);
|
|
660
|
+
|
|
661
|
+
const srcPath = path.join(__dirname, "..", "plugin", file);
|
|
662
|
+
fs.writeFileSync(dest, fs.readFileSync(srcPath, "utf8"));
|
|
663
|
+
|
|
664
|
+
if (existed) pluginPatched++;
|
|
665
|
+
else pluginCreated++;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Write plugin config + clean legacy commands in openclaw.json (single read/write)
|
|
669
|
+
const openclawConfigPath = path.join(homeDir, ".openclaw", "openclaw.json");
|
|
670
|
+
try {
|
|
671
|
+
const config = JSON.parse(fs.readFileSync(openclawConfigPath, "utf8"));
|
|
672
|
+
if (!config.plugins) config.plugins = {};
|
|
673
|
+
|
|
674
|
+
// Register plugin load path
|
|
675
|
+
if (!config.plugins.load) config.plugins.load = {};
|
|
676
|
+
if (!Array.isArray(config.plugins.load.paths))
|
|
677
|
+
config.plugins.load.paths = [];
|
|
678
|
+
if (!config.plugins.load.paths.includes(pluginDir)) {
|
|
679
|
+
config.plugins.load.paths.push(pluginDir);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Explicitly allow plugin to avoid auto-load warnings
|
|
683
|
+
if (!Array.isArray(config.plugins.allow)) config.plugins.allow = [];
|
|
684
|
+
if (!config.plugins.allow.includes("opencode-bridge")) {
|
|
685
|
+
config.plugins.allow.push("opencode-bridge");
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Plugin entry config
|
|
689
|
+
if (!config.plugins.entries) config.plugins.entries = {};
|
|
690
|
+
config.plugins.entries["opencode-bridge"] = {
|
|
691
|
+
enabled: true,
|
|
692
|
+
config: {
|
|
693
|
+
scriptsDir: scriptsDir,
|
|
694
|
+
channel: channel,
|
|
695
|
+
targetId: targetId,
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
// Install record
|
|
700
|
+
if (!config.plugins.installs) config.plugins.installs = {};
|
|
701
|
+
config.plugins.installs["opencode-bridge"] = {
|
|
702
|
+
source: "path",
|
|
703
|
+
sourcePath: pluginDir,
|
|
704
|
+
installPath: pluginDir,
|
|
705
|
+
version: pkg.version,
|
|
706
|
+
installedAt: new Date().toISOString(),
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// Register customCommands so OpenClaw passes them to the plugin
|
|
710
|
+
if (!config.channels) config.channels = {};
|
|
711
|
+
if (!config.channels[channel]) config.channels[channel] = {};
|
|
712
|
+
if (!Array.isArray(config.channels[channel].customCommands)) {
|
|
713
|
+
config.channels[channel].customCommands = [];
|
|
714
|
+
}
|
|
715
|
+
const cmds = config.channels[channel].customCommands;
|
|
716
|
+
for (const bc of BRIDGE_COMMANDS) {
|
|
717
|
+
const idx = cmds.findIndex((c) => c.command === bc.command);
|
|
718
|
+
if (idx >= 0) cmds[idx].description = bc.description;
|
|
719
|
+
else cmds.push({ command: bc.command, description: bc.description });
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
fs.writeFileSync(
|
|
723
|
+
openclawConfigPath,
|
|
724
|
+
`${JSON.stringify(config, null, 2)}\n`,
|
|
725
|
+
);
|
|
726
|
+
} catch {
|
|
727
|
+
console.log(
|
|
728
|
+
` ${chalk.yellow("!")} Could not write openclaw.json — plugin may need manual config`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Remove legacy workspace hook if present
|
|
733
|
+
const legacyHookDir = path.join(workspace, "hooks", "opencode-bridge");
|
|
734
|
+
if (fs.existsSync(legacyHookDir)) {
|
|
735
|
+
fs.rmSync(legacyHookDir, { recursive: true, force: true });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
spinner2a.stop(
|
|
739
|
+
formatInstallMsg("Plugin", pluginDir, pluginCreated, pluginPatched),
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
await sleep(300);
|
|
743
|
+
|
|
744
|
+
// --- 4c. Remove legacy skills (replaced by hook) ---
|
|
745
|
+
const skillNames = BRIDGE_COMMANDS.map((c) => c.command);
|
|
746
|
+
let skillsToRemove = [];
|
|
747
|
+
|
|
748
|
+
for (const skill of skillNames) {
|
|
749
|
+
const skillFile = path.join(skillsDir, skill, "SKILL.md");
|
|
750
|
+
if (fs.existsSync(skillFile)) {
|
|
751
|
+
skillsToRemove.push(skill);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (skillsToRemove.length > 0) {
|
|
756
|
+
const spinner2 = startSpinner("Cleaning legacy skills...");
|
|
757
|
+
await sleep(400);
|
|
758
|
+
let skillsRemoved = 0;
|
|
759
|
+
for (const skill of skillsToRemove) {
|
|
760
|
+
const skillFile = path.join(skillsDir, skill, "SKILL.md");
|
|
761
|
+
const skillDir = path.join(skillsDir, skill);
|
|
762
|
+
fs.unlinkSync(skillFile);
|
|
763
|
+
try {
|
|
764
|
+
fs.rmdirSync(skillDir);
|
|
765
|
+
} catch {}
|
|
766
|
+
skillsRemoved++;
|
|
767
|
+
}
|
|
768
|
+
spinner2.stop(
|
|
769
|
+
`Skills ${chalk.dim("->")} ${skillsRemoved} legacy skills removed ${chalk.dim("(replaced by hook)")}`,
|
|
770
|
+
);
|
|
771
|
+
await sleep(300);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// --- 4d. Global AGENTS.md for OpenCode ---
|
|
775
|
+
const spinner3 = startSpinner("Installing AGENTS.md...");
|
|
776
|
+
await sleep(400);
|
|
777
|
+
|
|
778
|
+
const opencodeAgentsDir = path.join(homeDir, ".config", "opencode");
|
|
779
|
+
const opencodeAgentsDest = path.join(opencodeAgentsDir, "AGENTS.md");
|
|
780
|
+
|
|
781
|
+
if (!fs.existsSync(opencodeAgentsDir)) {
|
|
782
|
+
fs.mkdirSync(opencodeAgentsDir, { recursive: true });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (fs.existsSync(opencodeAgentsDest)) {
|
|
786
|
+
const dateStr = getDateStr();
|
|
787
|
+
let backupPath = path.join(opencodeAgentsDir, `AGENTS.${dateStr}.md`);
|
|
788
|
+
let suffix = 1;
|
|
789
|
+
while (fs.existsSync(backupPath)) {
|
|
790
|
+
backupPath = path.join(opencodeAgentsDir, `AGENTS.${dateStr}-${suffix}.md`);
|
|
791
|
+
suffix++;
|
|
792
|
+
}
|
|
793
|
+
fs.renameSync(opencodeAgentsDest, backupPath);
|
|
794
|
+
spinner3.stop(
|
|
795
|
+
`AGENTS.md ${chalk.dim("->")} existing backed up as ${chalk.yellow(path.basename(backupPath))}`,
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
await sleep(200);
|
|
799
|
+
const spinner3b = startSpinner("Writing new AGENTS.md...");
|
|
800
|
+
await sleep(300);
|
|
801
|
+
copyTemplate(path.join("workspace", "OPENCODE.md"), opencodeAgentsDest, vars);
|
|
802
|
+
spinner3b.stop(`AGENTS.md ${chalk.dim("->")} ${opencodeAgentsDest}`);
|
|
803
|
+
} else {
|
|
804
|
+
copyTemplate(path.join("workspace", "OPENCODE.md"), opencodeAgentsDest, vars);
|
|
805
|
+
spinner3.stop(`AGENTS.md ${chalk.dim("->")} ${opencodeAgentsDest}`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
await sleep(300);
|
|
809
|
+
|
|
810
|
+
// --- 4e. Daemon ---
|
|
811
|
+
|
|
812
|
+
const spinner4 = startSpinner("Registering daemon...");
|
|
813
|
+
await sleep(500);
|
|
814
|
+
|
|
815
|
+
const sessionScript = path.join(scriptsDir, "opencode-session.sh");
|
|
816
|
+
const daemonOk = installDaemon(sessionScript);
|
|
817
|
+
if (daemonOk) {
|
|
818
|
+
const daemonType =
|
|
819
|
+
process.platform === "darwin" ? "LaunchAgent" : "systemd";
|
|
820
|
+
spinner4.stop(
|
|
821
|
+
`Daemon ${chalk.dim("->")} ${daemonType} registered ${chalk.dim("(30s interval)")}`,
|
|
822
|
+
);
|
|
823
|
+
} else {
|
|
824
|
+
spinner4.fail(`Daemon ${chalk.dim("->")} manual setup required`);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
await sleep(300);
|
|
828
|
+
|
|
829
|
+
// --- 4f. Restart OpenClaw Gateway ---
|
|
830
|
+
const spinner5 = startSpinner("Restarting OpenClaw Gateway...");
|
|
831
|
+
await sleep(300);
|
|
832
|
+
|
|
833
|
+
let restarted = false;
|
|
834
|
+
let restartError = "";
|
|
835
|
+
try {
|
|
836
|
+
await execAsync("openclaw gateway restart", { timeout: 15000 });
|
|
837
|
+
restarted = true;
|
|
838
|
+
} catch (e) {
|
|
839
|
+
restartError = e.stderr?.trim() || e.message;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (restarted) {
|
|
843
|
+
spinner5.stop(
|
|
844
|
+
`Gateway ${chalk.dim("->")} restarted ${chalk.dim("(skills loaded)")}`,
|
|
845
|
+
);
|
|
846
|
+
} else {
|
|
847
|
+
spinner5.fail(
|
|
848
|
+
`Gateway ${chalk.dim("->")} could not restart ${restartError ? chalk.dim(`(${restartError})`) : ""}`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// ========== Done ==========
|
|
853
|
+
console.log();
|
|
854
|
+
await sleep(300);
|
|
855
|
+
console.log(` ${chalk.green.bold("\u2705 Setup complete!")}`);
|
|
856
|
+
console.log();
|
|
857
|
+
|
|
858
|
+
if (!restarted) {
|
|
859
|
+
console.log(` ${chalk.bold.yellow("Action required:")}`);
|
|
860
|
+
console.log(` Restart OpenClaw to load the new skills:`);
|
|
861
|
+
console.log(
|
|
862
|
+
` ${chalk.cyan("/restart")} from your ${chalk.bold(channel)}`,
|
|
863
|
+
);
|
|
864
|
+
console.log(` ${chalk.cyan("openclaw gateway restart")} from terminal`);
|
|
865
|
+
console.log();
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
console.log(` ${chalk.bold("Quick test:")}`);
|
|
869
|
+
console.log(
|
|
870
|
+
` Send ${chalk.cyan.bold("@cc hello")} from your ${chalk.bold(channel)}`,
|
|
871
|
+
);
|
|
872
|
+
console.log();
|
|
873
|
+
console.log(` ${chalk.bold("Commands:")}`);
|
|
874
|
+
console.log(` ${chalk.cyan("@cc message")} Send to existing session`);
|
|
875
|
+
console.log(
|
|
876
|
+
` ${chalk.cyan("@ccn message")} Create new session and send`,
|
|
877
|
+
);
|
|
878
|
+
console.log(` ${chalk.cyan("@ccu")} Check usage/stats`);
|
|
879
|
+
console.log(` ${chalk.cyan("@ccm")} List available models`);
|
|
880
|
+
console.log(` ${chalk.cyan("@ccms <num>")} Set model (use @ccm first)`);
|
|
881
|
+
console.log();
|
|
882
|
+
console.log(
|
|
883
|
+
` ${chalk.dim("Prefix @cc and /cc are both supported. Quotes are optional.")}`,
|
|
884
|
+
);
|
|
885
|
+
console.log();
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
process.on("exit", () => {
|
|
889
|
+
if (activeSpinner) {
|
|
890
|
+
clearInterval(activeSpinner._id);
|
|
891
|
+
activeSpinner = null;
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
module.exports = { onboard, BRIDGE_COMMANDS, SCRIPT_VARS };
|