openclaw-opencode-bridge 2.1.4 → 2.1.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/README.md +61 -42
- package/README_EN.md +126 -0
- package/lib/cli.js +1 -1
- package/lib/onboard.js +47 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/plugin/index.ts +11 -18
- package/plugin/openclaw.plugin.json +1 -1
- package/plugin/package.json +2 -2
- package/templates/scripts/opencode-new-session.sh +100 -49
- package/templates/scripts/opencode-send.sh +104 -47
package/README.md
CHANGED
|
@@ -4,31 +4,41 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](package.json)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Language: [English](README_EN.md)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Bridge untuk menghubungkan channel OpenClaw ke OpenCode lewat command prefix seperti `/cc` atau `@cc`.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
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.
|
|
11
|
+
Pesan dari user akan langsung dieksekusi oleh OpenCode CLI (`opencode run`), lalu hasilnya dikirim balik lewat `openclaw message send`.
|
|
14
12
|
|
|
15
13
|
<p>
|
|
16
14
|
<img src="DEMO_1.png" alt="Telegram demo — sending a command" width="400" />
|
|
17
15
|
<img src="DEMO_2.png" alt="Telegram demo — receiving a response" width="400" />
|
|
18
16
|
</p>
|
|
19
17
|
|
|
20
|
-
>
|
|
18
|
+
> ⚠️ Fokus pengujian saat ini adalah Telegram. Channel lain mungkin butuh penyesuaian format.
|
|
19
|
+
|
|
20
|
+
## Donasi
|
|
21
|
+
|
|
22
|
+
Kalau project ini membantu, kamu bisa dukung lewat:
|
|
23
|
+
|
|
24
|
+
`0xe81c32383C8F21A14E6C2264939dA512e9F9bb42`
|
|
21
25
|
|
|
22
|
-
##
|
|
26
|
+
## Fitur Utama
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
- Prefix command: `@cc`, `/cc`, `@ccn`, `/ccn`, `@ccu`, `@ccm`, `@ccms`
|
|
29
|
+
- Reply OpenCode dikirim otomatis ke user lewat channel OpenClaw
|
|
30
|
+
- Output dibersihkan dari noise terminal/tool logs
|
|
31
|
+
- Timeout bersifat batas maksimum, bukan delay tetap
|
|
32
|
+
- Dukungan onboarding/uninstall otomatis
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
2. The plugin intercepts the message and suppresses the gateway LLM
|
|
28
|
-
3. A shell script forwards the message to OpenCode in a persistent tmux session
|
|
29
|
-
4. OpenCode replies back through the same channel via `openclaw message send`
|
|
34
|
+
## Alur Kerja Singkat
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
1. User kirim pesan ber-prefix, contoh: `/cc buat script python`.
|
|
37
|
+
2. Plugin menangkap pesan dan menahan reply default gateway.
|
|
38
|
+
3. Script bridge menjalankan `opencode run`.
|
|
39
|
+
4. Hasil OpenCode dikirim balik ke user melalui `openclaw message send`.
|
|
40
|
+
|
|
41
|
+
## Prasyarat
|
|
32
42
|
|
|
33
43
|
| Dependency | Install |
|
|
34
44
|
|---|---|
|
|
@@ -36,53 +46,63 @@ Send `@cc` or `/cc` from any chat — your message is routed directly to OpenCod
|
|
|
36
46
|
| [OpenCode](https://opencode.ai) | `npm i -g opencode-ai` |
|
|
37
47
|
| [tmux](https://github.com/tmux/tmux) | Auto-installed during onboard if missing |
|
|
38
48
|
|
|
39
|
-
>
|
|
49
|
+
> Sistem operasi yang didukung: Linux dan macOS.
|
|
40
50
|
|
|
41
|
-
##
|
|
51
|
+
## Instalasi Cepat
|
|
42
52
|
|
|
43
53
|
```bash
|
|
44
54
|
npm i -g openclaw-opencode-bridge
|
|
45
55
|
openclaw-opencode-bridge onboard
|
|
46
56
|
```
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
Wizard onboarding akan mengatur plugin, script, AGENTS.md, daemon, dan konfigurasi channel.
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
Tes awal:
|
|
51
61
|
|
|
52
|
-
```
|
|
53
|
-
|
|
62
|
+
```bash
|
|
63
|
+
/cc hello
|
|
54
64
|
```
|
|
55
65
|
|
|
56
|
-
##
|
|
66
|
+
## Daftar Perintah
|
|
57
67
|
|
|
58
|
-
| Prefix |
|
|
68
|
+
| Prefix | Fungsi |
|
|
59
69
|
|---|---|
|
|
60
|
-
| `@cc` · `/cc` |
|
|
61
|
-
| `@ccn` · `/ccn` |
|
|
62
|
-
| `@ccu` · `/ccu` |
|
|
63
|
-
| `@ccm` · `/ccm` |
|
|
64
|
-
| `@ccms` · `/ccms` |
|
|
70
|
+
| `@cc` · `/cc` | Lanjut ke sesi terbaru (`--continue`) |
|
|
71
|
+
| `@ccn` · `/ccn` | Jalankan sesi baru tanpa `--continue` (konteks fresh) |
|
|
72
|
+
| `@ccu` · `/ccu` | Tampilkan statistik pemakaian OpenCode |
|
|
73
|
+
| `@ccm` · `/ccm` | Tampilkan daftar model OpenCode |
|
|
74
|
+
| `@ccms` · `/ccms` | Ganti model OpenCode (nomor atau model-id) |
|
|
65
75
|
|
|
66
|
-
|
|
76
|
+
Contoh:
|
|
67
77
|
|
|
78
|
+
```bash
|
|
79
|
+
/cc refactor auth module dan tambah unit test
|
|
80
|
+
/ccn review PR ini: https://github.com/org/repo/pull/42
|
|
81
|
+
/ccu
|
|
68
82
|
```
|
|
69
|
-
@cc refactor the auth module and add tests
|
|
70
|
-
@ccn review this PR: https://github.com/org/repo/pull/42
|
|
71
|
-
@ccu
|
|
72
|
-
```
|
|
73
83
|
|
|
74
|
-
|
|
84
|
+
## Perilaku Timeout
|
|
85
|
+
|
|
86
|
+
- `/cc` memakai timeout adaptif dengan base `60s` dan maksimum `300s`.
|
|
87
|
+
- `/ccn` memakai timeout adaptif dengan base `90s` dan maksimum `420s`.
|
|
88
|
+
- Timeout adalah batas maksimal proses. Kalau OpenCode selesai lebih cepat, reply langsung dikirim saat itu juga.
|
|
75
89
|
|
|
76
|
-
##
|
|
90
|
+
## Catatan Sesi
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
- `/ccn` tidak menghapus semua history OpenCode.
|
|
93
|
+
- `/cc` biasanya melanjutkan sesi terbaru.
|
|
94
|
+
- Data sesi OpenCode tersimpan di direktori data OpenCode user (contoh Linux: `~/.local/share/opencode`).
|
|
95
|
+
|
|
96
|
+
## Migrasi dari Versi Lama
|
|
97
|
+
|
|
98
|
+
Versi 2+ menggantikan sistem skill/hook lama menjadi plugin OpenClaw tunggal:
|
|
79
99
|
|
|
80
100
|
```bash
|
|
81
101
|
npm i -g openclaw-opencode-bridge
|
|
82
102
|
openclaw-opencode-bridge onboard
|
|
83
103
|
```
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
Komponen legacy akan dibersihkan otomatis saat onboarding.
|
|
86
106
|
|
|
87
107
|
## Uninstall
|
|
88
108
|
|
|
@@ -90,17 +110,16 @@ The wizard detects and removes legacy components automatically.
|
|
|
90
110
|
openclaw-opencode-bridge uninstall
|
|
91
111
|
```
|
|
92
112
|
|
|
93
|
-
|
|
113
|
+
Ini akan menghapus plugin, script bridge, AGENTS.md hasil instalasi bridge, dan daemon.
|
|
94
114
|
|
|
95
115
|
## Troubleshooting
|
|
96
116
|
|
|
97
|
-
|
|
|
117
|
+
| Gejala | Solusi |
|
|
98
118
|
|---|---|
|
|
99
|
-
| LLM
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
103
|
-
> 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.
|
|
119
|
+
| Gateway LLM tetap membalas | Jalankan `openclaw gateway restart` |
|
|
120
|
+
| Ada “OpenCode will reply shortly” tapi tidak ada balasan akhir | Cek log `/tmp/opencode-bridge-send.log` lalu ulangi `openclaw-opencode-bridge onboard` |
|
|
121
|
+
| Perintah lambat/timeout | Cek prompt terlalu berat, lihat log bridge, pastikan OpenCode CLI normal |
|
|
122
|
+
| Output aneh/berantakan | Ulangi onboarding agar script/plugin terbaru terpasang |
|
|
104
123
|
|
|
105
124
|
## License
|
|
106
125
|
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# openclaw-opencode-bridge
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/openclaw-opencode-bridge)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](package.json)
|
|
6
|
+
|
|
7
|
+
Bridge OpenClaw channels to OpenCode using command prefixes like `/cc` or `@cc`.
|
|
8
|
+
|
|
9
|
+
User messages are executed via OpenCode CLI (`opencode run`), then sent back through `openclaw message send`.
|
|
10
|
+
|
|
11
|
+
Language: [Bahasa Indonesia](README.md)
|
|
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
|
+
> ⚠️ Current testing focus is Telegram. Other channels may need format-specific adjustments.
|
|
19
|
+
|
|
20
|
+
## Key Features
|
|
21
|
+
|
|
22
|
+
- Prefix commands: `@cc`, `/cc`, `@ccn`, `/ccn`, `@ccu`, `@ccm`, `@ccms`
|
|
23
|
+
- OpenCode output is automatically sent back to users via OpenClaw
|
|
24
|
+
- Output is sanitized to remove terminal/tool noise
|
|
25
|
+
- Timeout acts as a maximum limit, not a fixed delay
|
|
26
|
+
- Automatic onboarding and uninstall workflow
|
|
27
|
+
|
|
28
|
+
## How It Works
|
|
29
|
+
|
|
30
|
+
1. User sends a prefixed message, for example: `/cc build a python script`.
|
|
31
|
+
2. The plugin intercepts the message and suppresses the default gateway reply.
|
|
32
|
+
3. Bridge scripts run `opencode run`.
|
|
33
|
+
4. OpenCode output is delivered back to the same user/channel.
|
|
34
|
+
|
|
35
|
+
## Prerequisites
|
|
36
|
+
|
|
37
|
+
| Dependency | Install |
|
|
38
|
+
|---|---|
|
|
39
|
+
| [OpenClaw](https://openclaw.ai) | `npm i -g openclaw` |
|
|
40
|
+
| [OpenCode](https://opencode.ai) | `npm i -g opencode-ai` |
|
|
41
|
+
| [tmux](https://github.com/tmux/tmux) | Auto-installed during onboard if missing |
|
|
42
|
+
|
|
43
|
+
> Supported OS: Linux and macOS.
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm i -g openclaw-opencode-bridge
|
|
49
|
+
openclaw-opencode-bridge onboard
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Onboarding configures plugin entries, scripts, AGENTS.md, daemon, and channel settings.
|
|
53
|
+
|
|
54
|
+
Quick test:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
/cc hello
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Command List
|
|
61
|
+
|
|
62
|
+
| Prefix | Function |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `@cc` · `/cc` | Continue the latest session (`--continue`) |
|
|
65
|
+
| `@ccn` · `/ccn` | Start a fresh run without `--continue` |
|
|
66
|
+
| `@ccu` · `/ccu` | Show OpenCode usage stats |
|
|
67
|
+
| `@ccm` · `/ccm` | List OpenCode models |
|
|
68
|
+
| `@ccms` · `/ccms` | Set OpenCode model (number or model-id) |
|
|
69
|
+
|
|
70
|
+
Examples:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
/cc refactor auth module and add tests
|
|
74
|
+
/ccn review this PR: https://github.com/org/repo/pull/42
|
|
75
|
+
/ccu
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Timeout Behavior
|
|
79
|
+
|
|
80
|
+
- `/cc` uses adaptive timeout with base `60s` and max `300s`.
|
|
81
|
+
- `/ccn` uses adaptive timeout with base `90s` and max `420s`.
|
|
82
|
+
- Timeout is a hard upper bound. If OpenCode finishes early, output is sent immediately.
|
|
83
|
+
|
|
84
|
+
## Session Notes
|
|
85
|
+
|
|
86
|
+
- `/ccn` does not delete existing OpenCode history.
|
|
87
|
+
- `/cc` usually continues the latest session.
|
|
88
|
+
- OpenCode session data is stored in OpenCode user data directory (Linux example: `~/.local/share/opencode`).
|
|
89
|
+
|
|
90
|
+
## Migration from Legacy Version
|
|
91
|
+
|
|
92
|
+
Version 2+ replaces the legacy skill/hook flow with a single OpenClaw plugin:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm i -g openclaw-opencode-bridge
|
|
96
|
+
openclaw-opencode-bridge onboard
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Legacy components are cleaned automatically during onboarding.
|
|
100
|
+
|
|
101
|
+
## Uninstall
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
openclaw-opencode-bridge uninstall
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This removes plugin entries, bridge scripts, bridge-managed AGENTS.md, and daemon registration.
|
|
108
|
+
|
|
109
|
+
## Troubleshooting
|
|
110
|
+
|
|
111
|
+
| Symptom | Fix |
|
|
112
|
+
|---|---|
|
|
113
|
+
| Gateway LLM still responds | Run `openclaw gateway restart` |
|
|
114
|
+
| Delivery message appears but no final reply | Check `/tmp/opencode-bridge-send.log`, then rerun `openclaw-opencode-bridge onboard` |
|
|
115
|
+
| Slow response / timeout | Check prompt complexity and bridge logs; confirm OpenCode CLI is healthy |
|
|
116
|
+
| Messy output | Rerun onboarding to ensure latest scripts/plugin are installed |
|
|
117
|
+
|
|
118
|
+
## Donation
|
|
119
|
+
|
|
120
|
+
If this project helps you, you can support it via:
|
|
121
|
+
|
|
122
|
+
`0xe81c32383C8F21A14E6C2264939dA512e9F9bb42`
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
[MIT](LICENSE)
|
package/lib/cli.js
CHANGED
|
@@ -42,7 +42,7 @@ const program = new Command();
|
|
|
42
42
|
program
|
|
43
43
|
.name("openclaw-opencode-bridge")
|
|
44
44
|
.description(
|
|
45
|
-
"Bridge OpenClaw messaging channels to OpenCode via
|
|
45
|
+
"Bridge OpenClaw messaging channels to OpenCode via persistent OpenCode sessions",
|
|
46
46
|
)
|
|
47
47
|
.version(pkg.version);
|
|
48
48
|
|
package/lib/onboard.js
CHANGED
|
@@ -347,11 +347,40 @@ function compactError(err) {
|
|
|
347
347
|
|
|
348
348
|
async function runGatewayCommand(action, timeoutMs) {
|
|
349
349
|
try {
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
const { stdout = "", stderr = "" } = await execAsync(
|
|
351
|
+
`openclaw gateway ${action}`,
|
|
352
|
+
{ timeout: timeoutMs },
|
|
353
|
+
);
|
|
354
|
+
return { ok: true, action, stdout, stderr };
|
|
352
355
|
} catch (e) {
|
|
353
|
-
return {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
action,
|
|
359
|
+
error: compactError(e),
|
|
360
|
+
stdout: e?.stdout || "",
|
|
361
|
+
stderr: e?.stderr || "",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function isGatewayHealthy() {
|
|
367
|
+
const status = await runGatewayCommand("status", 20000);
|
|
368
|
+
if (!status.ok) return false;
|
|
369
|
+
|
|
370
|
+
const text = `${status.stdout || ""}\n${status.stderr || ""}`.toLowerCase();
|
|
371
|
+
return (
|
|
372
|
+
text.includes("runtime: running") ||
|
|
373
|
+
(text.includes("rpc probe: ok") && text.includes("listening:"))
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function ensureGatewayHint(errorText) {
|
|
378
|
+
if (!errorText) return "gateway restart failed";
|
|
379
|
+
const text = String(errorText).trim();
|
|
380
|
+
if (/running|active|already|ok/i.test(text)) {
|
|
381
|
+
return `${text}; gateway appears to be running`;
|
|
354
382
|
}
|
|
383
|
+
return text;
|
|
355
384
|
}
|
|
356
385
|
|
|
357
386
|
async function restartGatewayResilient() {
|
|
@@ -377,6 +406,17 @@ async function restartGatewayResilient() {
|
|
|
377
406
|
return { ok: true, mode: "restart-retry" };
|
|
378
407
|
}
|
|
379
408
|
|
|
409
|
+
// 4) If command failed but status is healthy, treat as success with warning.
|
|
410
|
+
if (await isGatewayHealthy()) {
|
|
411
|
+
return {
|
|
412
|
+
ok: true,
|
|
413
|
+
mode: "status-healthy",
|
|
414
|
+
warning: ensureGatewayHint(
|
|
415
|
+
retry.error || start.error || primary.error || stop.error || "",
|
|
416
|
+
),
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
380
420
|
return {
|
|
381
421
|
ok: false,
|
|
382
422
|
error: retry.error || start.error || primary.error || "gateway restart failed",
|
|
@@ -877,8 +917,11 @@ async function onboard() {
|
|
|
877
917
|
const restartError = gatewayRestart.error || "";
|
|
878
918
|
|
|
879
919
|
if (restarted) {
|
|
920
|
+
const warningSuffix = gatewayRestart.warning
|
|
921
|
+
? ` ${chalk.yellow.dim(`(${gatewayRestart.warning})`)}`
|
|
922
|
+
: "";
|
|
880
923
|
spinner5.stop(
|
|
881
|
-
`Gateway ${chalk.dim("->")} restarted ${chalk.dim(`(${gatewayRestart.mode})`)}`,
|
|
924
|
+
`Gateway ${chalk.dim("->")} restarted ${chalk.dim(`(${gatewayRestart.mode})`)}${warningSuffix}`,
|
|
882
925
|
);
|
|
883
926
|
} else {
|
|
884
927
|
spinner5.fail(
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "opencode-bridge",
|
|
3
3
|
"name": "OpenCode Bridge",
|
|
4
|
-
"description": "Route @cc/@ccn/@ccu prefix messages to OpenCode
|
|
4
|
+
"description": "Route @cc/@ccn/@ccu prefix messages to OpenCode CLI, suppressing the default LLM response.",
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-opencode-bridge",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "Bridge OpenClaw messaging channels to OpenCode via
|
|
3
|
+
"version": "2.1.6",
|
|
4
|
+
"description": "Bridge OpenClaw messaging channels to OpenCode via persistent OpenCode sessions",
|
|
5
5
|
"main": "./lib/onboard.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"openclaw-opencode-bridge": "./lib/cli.js"
|
package/plugin/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ const SCRIPT_MAP: Record<string, string> = {
|
|
|
13
13
|
|
|
14
14
|
const REQUIRES_ARG = new Set(["cc", "ccn"]);
|
|
15
15
|
const EXEC_TIMEOUT = 15_000;
|
|
16
|
-
const
|
|
16
|
+
const DELIVERY_TTL_MS = 10_000;
|
|
17
17
|
|
|
18
18
|
const DELIVERY_MSG = "🔗 OpenCode will reply shortly.";
|
|
19
19
|
|
|
@@ -24,15 +24,13 @@ const SILENT_PROMPT =
|
|
|
24
24
|
"You MUST NOT call any tools or functions.\n" +
|
|
25
25
|
`Output ONLY this exact text, nothing else: ${DELIVERY_MSG}`;
|
|
26
26
|
|
|
27
|
-
// NOTE: Single-user assumption — concurrent users may see cross-suppression
|
|
28
|
-
let bridgeSuppressUntil = 0;
|
|
29
|
-
|
|
30
27
|
/**
|
|
31
28
|
* Flag set by message_received (fires FIRST) and consumed by before_prompt_build (fires SECOND).
|
|
32
29
|
* This bypasses the unreliable extractLastUserText approach entirely.
|
|
33
30
|
* message_received gets event.content (raw user text) which always correctly detects @cc prefix.
|
|
34
31
|
*/
|
|
35
32
|
let pendingBridgeCommand = false;
|
|
33
|
+
let pendingDeliveryUntil = 0;
|
|
36
34
|
|
|
37
35
|
export default function register(api: OpenClawPluginApi) {
|
|
38
36
|
const config = api.pluginConfig as {
|
|
@@ -67,9 +65,6 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
67
65
|
|
|
68
66
|
// Set flag for before_prompt_build to consume
|
|
69
67
|
pendingBridgeCommand = true;
|
|
70
|
-
// Keep suppression narrow to this turn to avoid cross-message blocking.
|
|
71
|
-
bridgeSuppressUntil = Date.now() + SUPPRESSION_WINDOW;
|
|
72
|
-
|
|
73
68
|
api.logger.debug?.(
|
|
74
69
|
`[opencode-bridge] message_received: command=${command}, pendingBridgeCommand=true`,
|
|
75
70
|
);
|
|
@@ -102,42 +97,40 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
102
97
|
// No longer relies on extractLastUserText for prefix detection.
|
|
103
98
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
104
99
|
const shouldSuppress = pendingBridgeCommand;
|
|
100
|
+
const deliveryPending = Date.now() < pendingDeliveryUntil;
|
|
105
101
|
|
|
106
102
|
api.logger.debug?.(
|
|
107
|
-
`[opencode-bridge] before_prompt_build: pendingBridgeCommand=${pendingBridgeCommand},
|
|
103
|
+
`[opencode-bridge] before_prompt_build: pendingBridgeCommand=${pendingBridgeCommand}, deliveryPending=${deliveryPending}`,
|
|
108
104
|
);
|
|
109
105
|
|
|
110
106
|
if (shouldSuppress) {
|
|
111
107
|
pendingBridgeCommand = false;
|
|
112
|
-
|
|
108
|
+
pendingDeliveryUntil = Date.now() + DELIVERY_TTL_MS;
|
|
113
109
|
return { systemPrompt: SILENT_PROMPT, prependContext: SILENT_PROMPT };
|
|
114
|
-
} else {
|
|
115
|
-
// Clear suppression for non-bridge messages
|
|
116
|
-
bridgeSuppressUntil = 0;
|
|
117
110
|
}
|
|
118
111
|
});
|
|
119
112
|
|
|
120
113
|
// --- Hook 3: message_sending (modifying) ---
|
|
121
|
-
// Replace LLM
|
|
114
|
+
// Replace one outgoing LLM message with delivery confirmation
|
|
122
115
|
api.on("message_sending", async (_event, _ctx) => {
|
|
123
|
-
const suppressing = Date.now() <
|
|
116
|
+
const suppressing = Date.now() < pendingDeliveryUntil;
|
|
124
117
|
api.logger.debug?.(
|
|
125
118
|
`[opencode-bridge] message_sending: suppressing=${suppressing}`,
|
|
126
119
|
);
|
|
127
120
|
|
|
128
121
|
if (suppressing) {
|
|
129
122
|
// One-shot override for the intercepted bridge message.
|
|
130
|
-
|
|
123
|
+
pendingDeliveryUntil = 0;
|
|
131
124
|
return { content: DELIVERY_MSG, cancel: false };
|
|
132
125
|
}
|
|
133
126
|
});
|
|
134
127
|
|
|
135
128
|
// --- Hook 4: before_tool_call (modifying) ---
|
|
136
|
-
// Block ALL tool calls while
|
|
129
|
+
// Block ALL tool calls while delivery override is pending
|
|
137
130
|
api.on("before_tool_call", async (_event, _ctx) => {
|
|
138
|
-
if (Date.now() <
|
|
131
|
+
if (Date.now() < pendingDeliveryUntil) {
|
|
139
132
|
api.logger.debug?.(
|
|
140
|
-
`[opencode-bridge] before_tool_call: BLOCKED (
|
|
133
|
+
`[opencode-bridge] before_tool_call: BLOCKED (delivery pending)`,
|
|
141
134
|
);
|
|
142
135
|
return { block: true, blockReason: "opencode-bridge: message intercepted, tools disabled" };
|
|
143
136
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "opencode-bridge",
|
|
3
3
|
"name": "OpenCode Bridge",
|
|
4
|
-
"description": "Route @cc/@ccn/@ccu prefix messages to OpenCode
|
|
4
|
+
"description": "Route @cc/@ccn/@ccu prefix messages to OpenCode CLI, suppressing the default LLM response.",
|
|
5
5
|
"configSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
package/plugin/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-bridge",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.12",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "OpenClaw plugin: route prefix messages to OpenCode
|
|
5
|
+
"description": "OpenClaw plugin: route prefix messages to OpenCode CLI",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"openclaw": {
|
|
8
8
|
"extensions": ["./index.ts"]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# bridge-version:
|
|
2
|
+
# bridge-version: 16
|
|
3
3
|
# Start fresh session asynchronously and send instruction
|
|
4
4
|
MSG="$1"
|
|
5
5
|
OPENCODE="{{OPENCODE_BIN}}"
|
|
@@ -7,9 +7,9 @@ CHANNEL="{{CHANNEL}}"
|
|
|
7
7
|
TARGET="{{TARGET_ID}}"
|
|
8
8
|
WORKSPACE="{{WORKSPACE}}"
|
|
9
9
|
LOG_FILE="/tmp/opencode-bridge-send.log"
|
|
10
|
-
BASE_TIMEOUT_SEC=
|
|
11
|
-
MAX_TIMEOUT_SEC=
|
|
12
|
-
LOCK_WAIT_SEC=
|
|
10
|
+
BASE_TIMEOUT_SEC=90
|
|
11
|
+
MAX_TIMEOUT_SEC=420
|
|
12
|
+
LOCK_WAIT_SEC=3
|
|
13
13
|
|
|
14
14
|
if [ -z "$MSG" ]; then
|
|
15
15
|
echo "ERROR: No message provided"
|
|
@@ -30,20 +30,43 @@ normalize_text() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
acquire_bridge_lock() {
|
|
33
|
-
local safe_channel safe_target
|
|
33
|
+
local safe_channel safe_target holder_pid waited
|
|
34
34
|
safe_channel="$(printf '%s' "$CHANNEL" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
|
|
35
35
|
safe_target="$(printf '%s' "$TARGET" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
|
|
36
|
-
|
|
36
|
+
LOCK_DIR="/tmp/opencode-bridge-${safe_channel}-${safe_target}.lockdir"
|
|
37
|
+
waited=0
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
40
|
+
if [ ! -s "$LOCK_DIR/pid" ]; then
|
|
41
|
+
rm -rf "$LOCK_DIR"
|
|
42
|
+
continue
|
|
43
|
+
fi
|
|
44
|
+
if [ -f "$LOCK_DIR/pid" ]; then
|
|
45
|
+
holder_pid="$(cat "$LOCK_DIR/pid" 2>/dev/null || true)"
|
|
46
|
+
if [ -n "$holder_pid" ] && ! kill -0 "$holder_pid" 2>/dev/null; then
|
|
47
|
+
rm -rf "$LOCK_DIR"
|
|
48
|
+
continue
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
43
51
|
|
|
52
|
+
if [ "$waited" -ge "$LOCK_WAIT_SEC" ]; then
|
|
53
|
+
return 1
|
|
54
|
+
fi
|
|
55
|
+
sleep 1
|
|
56
|
+
waited=$((waited + 1))
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
printf '%s\n' "${BASHPID:-$$}" > "$LOCK_DIR/pid"
|
|
60
|
+
trap 'release_bridge_lock' EXIT INT TERM
|
|
44
61
|
return 0
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
release_bridge_lock() {
|
|
65
|
+
if [ -n "${LOCK_DIR:-}" ] && [ -d "$LOCK_DIR" ]; then
|
|
66
|
+
rm -rf "$LOCK_DIR"
|
|
67
|
+
fi
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
compute_timeout() {
|
|
48
71
|
local msg="$1"
|
|
49
72
|
local timeout="$BASE_TIMEOUT_SEC"
|
|
@@ -56,7 +79,7 @@ compute_timeout() {
|
|
|
56
79
|
if [ "$chars" -gt 280 ]; then timeout=$((timeout + 20)); fi
|
|
57
80
|
if [ "$words" -gt 25 ]; then timeout=$((timeout + 20)); fi
|
|
58
81
|
|
|
59
|
-
if printf '%s' "$msg" | grep -Eqi '\b(create|build|write|implement|script|code|program|refactor|debug|fix|test|automation|deploy|migrate|api|database|project)\b'; then
|
|
82
|
+
if printf '%s' "$msg" | grep -Eqi '\b(create|build|write|implement|script|code|program|refactor|debug|fix|test|automation|deploy|migrate|api|database|project|search|news|weather|forecast|cuaca|berita)\b'; then
|
|
60
83
|
timeout=$((timeout + 60))
|
|
61
84
|
fi
|
|
62
85
|
|
|
@@ -78,11 +101,6 @@ is_trivial_echo() {
|
|
|
78
101
|
[ -n "$message_norm" ] && [ "$output_norm" = "$message_norm" ]
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
has_external_delivery_success() {
|
|
82
|
-
local raw="$1"
|
|
83
|
-
printf '%s\n' "$raw" | grep -Eqi 'Sent via [A-Za-z]+|Message ID:[[:space:]]*[0-9]+'
|
|
84
|
-
}
|
|
85
|
-
|
|
86
104
|
extract_last_marked_block() {
|
|
87
105
|
local raw="$1"
|
|
88
106
|
if ! printf '%s' "$raw" | grep -q '🔗'; then
|
|
@@ -119,7 +137,7 @@ sanitize_output() {
|
|
|
119
137
|
cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/^[[:space:]]*>+[[:space:]]?//; /^```/d')"
|
|
120
138
|
|
|
121
139
|
cleaned="$(printf '%s\n' "$cleaned" | grep -Eiv \
|
|
122
|
-
'^[[:space:]]*$|^[[:space:]]*(build[[:space:]]*·|◇[[:space:]]+doctor warnings)[[:space:]]*$|^[[:space:]]*◇[[:space:]]+|^[[:space:]]*exa[[:space:]]+(web|code)[[:space:]]+search([[:space:]]+.*)?$|^[[:space:]]*[←→↳].*|^[[:space:]]*wrote file successfully\.?$|^[[:space:]]*(\$[[:space:]]*)?openclaw message send --channel[[:space:]]+|^[[:space:]]*error:[[:space:]]*too many arguments for '\''send'\''.*$|^[[:space:]]*sent via telegram|^[[:space:]]*\[(telegram|discord|slack|whatsapp|signal|irc|matrix|line|mattermost|teams)\]|autoselectfamily=|dnsresultorder=|^[[:space:]]*[│┌┐└┘├┤┬┴┼─═╭╮╰╯]+[[:space:]]*$')"
|
|
140
|
+
'^[[:space:]]*$|^[[:space:]]*(build[[:space:]]*·|◇[[:space:]]+doctor warnings)[[:space:]]*$|^[[:space:]]*◇[[:space:]]+|^[[:space:]]*exa[[:space:]]+(web|code)[[:space:]]+search([[:space:]]+.*)?$|^[[:space:]]*[←→↳].*|^[[:space:]]*wrote file successfully\.?$|^[[:space:]]*(\$[[:space:]]*)?openclaw message send --channel[[:space:]]+|^[[:space:]]*bridge-guard:[[:space:]]*blocked openclaw message send.*$|^[[:space:]]*error:[[:space:]]*too many arguments for '\''send'\''.*$|^[[:space:]]*sent via telegram|^[[:space:]]*\[(telegram|discord|slack|whatsapp|signal|irc|matrix|line|mattermost|teams)\]|autoselectfamily=|dnsresultorder=|^[[:space:]]*[│┌┐└┘├┤┬┴┼─═╭╮╰╯]+[[:space:]]*$')"
|
|
123
141
|
|
|
124
142
|
marked="$(extract_last_marked_block "$cleaned")"
|
|
125
143
|
if [ -n "$marked" ]; then
|
|
@@ -131,6 +149,14 @@ sanitize_output() {
|
|
|
131
149
|
printf '%s' "$(trim_text "$cleaned")"
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
extract_text_from_json_stream() {
|
|
153
|
+
local raw="$1"
|
|
154
|
+
local parsed
|
|
155
|
+
|
|
156
|
+
parsed="$(printf '%s\n' "$raw" | node -e "const fs=require('fs');const lines=fs.readFileSync(0,'utf8').split(/\r?\n/);let out='';for(const line of lines){const s=line.trim();if(!s)continue;try{const obj=JSON.parse(s);if(obj&&obj.type==='text'&&obj.part&&typeof obj.part.text==='string'){out+=obj.part.text;}}catch{}}process.stdout.write(out);" 2>/dev/null)"
|
|
157
|
+
printf '%s' "$(trim_text "$parsed")"
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
sentence_case_first() {
|
|
135
161
|
local text="$1"
|
|
136
162
|
printf '%s' "$text" | awk '
|
|
@@ -166,38 +192,60 @@ apply_reply_style() {
|
|
|
166
192
|
run_with_timeout() {
|
|
167
193
|
local mode="$1"
|
|
168
194
|
local prompt="$2"
|
|
169
|
-
local output rc tmp pid watchdog
|
|
195
|
+
local output rc tmp pid watchdog guard_dir guarded_path real_openclaw
|
|
170
196
|
|
|
171
197
|
tmp="$(mktemp /tmp/opencode-run.XXXXXX)"
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"$
|
|
198
|
+
guarded_path="$PATH"
|
|
199
|
+
real_openclaw="$(command -v openclaw 2>/dev/null || true)"
|
|
200
|
+
if [ -n "$real_openclaw" ]; then
|
|
201
|
+
guard_dir="$(mktemp -d /tmp/opencode-bridge-guard.XXXXXX)"
|
|
202
|
+
printf '#!/bin/sh\nif [ "$1" = "message" ] && [ "$2" = "send" ]; then\n echo "bridge-guard: blocked openclaw message send from opencode runtime" >&2\n exit 64\nfi\nexec "%s" "$@"\n' "$real_openclaw" > "${guard_dir}/openclaw"
|
|
203
|
+
chmod +x "${guard_dir}/openclaw"
|
|
204
|
+
guarded_path="${guard_dir}:${PATH}"
|
|
176
205
|
fi
|
|
177
|
-
pid=$!
|
|
178
|
-
|
|
179
|
-
(
|
|
180
|
-
sleep "$RUN_TIMEOUT_SEC"
|
|
181
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
182
|
-
touch "${tmp}.timeout"
|
|
183
|
-
kill "$pid" 2>/dev/null
|
|
184
|
-
sleep 2
|
|
185
|
-
kill -9 "$pid" 2>/dev/null
|
|
186
|
-
fi
|
|
187
|
-
) &
|
|
188
|
-
watchdog=$!
|
|
189
206
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
207
|
+
# Timeout here is an upper bound only; command returns immediately when OpenCode finishes.
|
|
208
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
209
|
+
if [ -n "$mode" ]; then
|
|
210
|
+
PATH="$guarded_path" timeout --signal=TERM --kill-after=2 "${RUN_TIMEOUT_SEC}s" \
|
|
211
|
+
"$OPENCODE" run "$mode" --no-fork --format json "$prompt" >"$tmp" 2>&1
|
|
212
|
+
else
|
|
213
|
+
PATH="$guarded_path" timeout --signal=TERM --kill-after=2 "${RUN_TIMEOUT_SEC}s" \
|
|
214
|
+
"$OPENCODE" run --no-fork --format json "$prompt" >"$tmp" 2>&1
|
|
215
|
+
fi
|
|
216
|
+
rc=$?
|
|
217
|
+
else
|
|
218
|
+
if [ -n "$mode" ]; then
|
|
219
|
+
PATH="$guarded_path" "$OPENCODE" run "$mode" --no-fork --format json "$prompt" >"$tmp" 2>&1 &
|
|
220
|
+
else
|
|
221
|
+
PATH="$guarded_path" "$OPENCODE" run --no-fork --format json "$prompt" >"$tmp" 2>&1 &
|
|
222
|
+
fi
|
|
223
|
+
pid=$!
|
|
224
|
+
|
|
225
|
+
(
|
|
226
|
+
sleep "$RUN_TIMEOUT_SEC"
|
|
227
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
228
|
+
touch "${tmp}.timeout"
|
|
229
|
+
kill "$pid" 2>/dev/null
|
|
230
|
+
sleep 2
|
|
231
|
+
kill -9 "$pid" 2>/dev/null
|
|
232
|
+
fi
|
|
233
|
+
) &
|
|
234
|
+
watchdog=$!
|
|
235
|
+
|
|
236
|
+
wait "$pid"
|
|
237
|
+
rc=$?
|
|
238
|
+
kill "$watchdog" 2>/dev/null
|
|
239
|
+
|
|
240
|
+
if [ -f "${tmp}.timeout" ]; then
|
|
241
|
+
rc=124
|
|
242
|
+
rm -f "${tmp}.timeout"
|
|
243
|
+
fi
|
|
197
244
|
fi
|
|
198
245
|
|
|
199
246
|
output="$(cat "$tmp")"
|
|
200
247
|
rm -f "$tmp"
|
|
248
|
+
[ -n "${guard_dir:-}" ] && rm -rf "$guard_dir"
|
|
201
249
|
|
|
202
250
|
printf '%s\n' "$rc"
|
|
203
251
|
printf '%s' "$output"
|
|
@@ -219,10 +267,17 @@ run_with_timeout() {
|
|
|
219
267
|
run_result="$(run_with_timeout "" "$FULL_MSG")"
|
|
220
268
|
rc="$(printf '%s' "$run_result" | head -n 1)"
|
|
221
269
|
raw_output="$(printf '%s' "$run_result" | tail -n +2)"
|
|
222
|
-
output="$(
|
|
270
|
+
output="$(extract_text_from_json_stream "$raw_output")"
|
|
271
|
+
if [ -z "$output" ]; then
|
|
272
|
+
output="$(sanitize_output "$raw_output")"
|
|
273
|
+
fi
|
|
223
274
|
|
|
224
275
|
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
|
|
225
|
-
|
|
276
|
+
if [ -n "$output" ] && ! is_trivial_echo "$output" "$MSG"; then
|
|
277
|
+
output="${output}\n\n(Stopped after ${RUN_TIMEOUT_SEC}s timeout.)"
|
|
278
|
+
else
|
|
279
|
+
output="OpenCode timed out after ${RUN_TIMEOUT_SEC}s. Try a narrower prompt or send a follow-up."
|
|
280
|
+
fi
|
|
226
281
|
elif [ -z "$output" ]; then
|
|
227
282
|
output="OpenCode finished, but returned an empty response."
|
|
228
283
|
elif is_trivial_echo "$output" "$MSG"; then
|
|
@@ -231,11 +286,7 @@ run_with_timeout() {
|
|
|
231
286
|
|
|
232
287
|
output="$(apply_reply_style "$output")"
|
|
233
288
|
|
|
234
|
-
|
|
235
|
-
printf '[%s] /ccn skipped bridge send (already sent by OpenCode)\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
|
236
|
-
else
|
|
237
|
-
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
238
|
-
fi
|
|
289
|
+
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
239
290
|
|
|
240
291
|
ended_at=$(date +%s)
|
|
241
292
|
elapsed=$((ended_at - started_at))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# bridge-version:
|
|
2
|
+
# bridge-version: 18
|
|
3
3
|
# Dispatch instruction to OpenCode asynchronously and relay response
|
|
4
4
|
MSG="$1"
|
|
5
5
|
OPENCODE="{{OPENCODE_BIN}}"
|
|
@@ -7,9 +7,9 @@ CHANNEL="{{CHANNEL}}"
|
|
|
7
7
|
TARGET="{{TARGET_ID}}"
|
|
8
8
|
WORKSPACE="{{WORKSPACE}}"
|
|
9
9
|
LOG_FILE="/tmp/opencode-bridge-send.log"
|
|
10
|
-
BASE_TIMEOUT_SEC=
|
|
10
|
+
BASE_TIMEOUT_SEC=60
|
|
11
11
|
MAX_TIMEOUT_SEC=300
|
|
12
|
-
LOCK_WAIT_SEC=
|
|
12
|
+
LOCK_WAIT_SEC=3
|
|
13
13
|
|
|
14
14
|
if [ -z "$MSG" ]; then
|
|
15
15
|
echo "ERROR: No message provided"
|
|
@@ -30,20 +30,43 @@ normalize_text() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
acquire_bridge_lock() {
|
|
33
|
-
local safe_channel safe_target
|
|
33
|
+
local safe_channel safe_target holder_pid waited
|
|
34
34
|
safe_channel="$(printf '%s' "$CHANNEL" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
|
|
35
35
|
safe_target="$(printf '%s' "$TARGET" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
|
|
36
|
-
|
|
36
|
+
LOCK_DIR="/tmp/opencode-bridge-${safe_channel}-${safe_target}.lockdir"
|
|
37
|
+
waited=0
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
40
|
+
if [ ! -s "$LOCK_DIR/pid" ]; then
|
|
41
|
+
rm -rf "$LOCK_DIR"
|
|
42
|
+
continue
|
|
43
|
+
fi
|
|
44
|
+
if [ -f "$LOCK_DIR/pid" ]; then
|
|
45
|
+
holder_pid="$(cat "$LOCK_DIR/pid" 2>/dev/null || true)"
|
|
46
|
+
if [ -n "$holder_pid" ] && ! kill -0 "$holder_pid" 2>/dev/null; then
|
|
47
|
+
rm -rf "$LOCK_DIR"
|
|
48
|
+
continue
|
|
49
|
+
fi
|
|
50
|
+
fi
|
|
43
51
|
|
|
52
|
+
if [ "$waited" -ge "$LOCK_WAIT_SEC" ]; then
|
|
53
|
+
return 1
|
|
54
|
+
fi
|
|
55
|
+
sleep 1
|
|
56
|
+
waited=$((waited + 1))
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
printf '%s\n' "${BASHPID:-$$}" > "$LOCK_DIR/pid"
|
|
60
|
+
trap 'release_bridge_lock' EXIT INT TERM
|
|
44
61
|
return 0
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
release_bridge_lock() {
|
|
65
|
+
if [ -n "${LOCK_DIR:-}" ] && [ -d "$LOCK_DIR" ]; then
|
|
66
|
+
rm -rf "$LOCK_DIR"
|
|
67
|
+
fi
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
compute_timeout() {
|
|
48
71
|
local msg="$1"
|
|
49
72
|
local timeout="$BASE_TIMEOUT_SEC"
|
|
@@ -56,7 +79,7 @@ compute_timeout() {
|
|
|
56
79
|
if [ "$chars" -gt 280 ]; then timeout=$((timeout + 20)); fi
|
|
57
80
|
if [ "$words" -gt 25 ]; then timeout=$((timeout + 20)); fi
|
|
58
81
|
|
|
59
|
-
if printf '%s' "$msg" | grep -Eqi '\b(create|build|write|implement|script|code|program|refactor|debug|fix|test|automation|deploy|migrate|api|database|project)\b'; then
|
|
82
|
+
if printf '%s' "$msg" | grep -Eqi '\b(create|build|write|implement|script|code|program|refactor|debug|fix|test|automation|deploy|migrate|api|database|project|search|news|weather|forecast|cuaca|berita)\b'; then
|
|
60
83
|
timeout=$((timeout + 60))
|
|
61
84
|
fi
|
|
62
85
|
|
|
@@ -78,11 +101,6 @@ is_trivial_echo() {
|
|
|
78
101
|
[ -n "$message_norm" ] && [ "$output_norm" = "$message_norm" ]
|
|
79
102
|
}
|
|
80
103
|
|
|
81
|
-
has_external_delivery_success() {
|
|
82
|
-
local raw="$1"
|
|
83
|
-
printf '%s\n' "$raw" | grep -Eqi 'Sent via [A-Za-z]+|Message ID:[[:space:]]*[0-9]+'
|
|
84
|
-
}
|
|
85
|
-
|
|
86
104
|
extract_last_marked_block() {
|
|
87
105
|
local raw="$1"
|
|
88
106
|
if ! printf '%s' "$raw" | grep -q '🔗'; then
|
|
@@ -119,7 +137,7 @@ sanitize_output() {
|
|
|
119
137
|
cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/^[[:space:]]*>+[[:space:]]?//; /^```/d')"
|
|
120
138
|
|
|
121
139
|
cleaned="$(printf '%s\n' "$cleaned" | grep -Eiv \
|
|
122
|
-
'^[[:space:]]*$|^[[:space:]]*(build[[:space:]]*·|◇[[:space:]]+doctor warnings)[[:space:]]*$|^[[:space:]]*◇[[:space:]]+|^[[:space:]]*exa[[:space:]]+(web|code)[[:space:]]+search([[:space:]]+.*)?$|^[[:space:]]*[←→↳].*|^[[:space:]]*wrote file successfully\.?$|^[[:space:]]*(\$[[:space:]]*)?openclaw message send --channel[[:space:]]+|^[[:space:]]*error:[[:space:]]*too many arguments for '\''send'\''.*$|^[[:space:]]*sent via telegram|^[[:space:]]*\[(telegram|discord|slack|whatsapp|signal|irc|matrix|line|mattermost|teams)\]|autoselectfamily=|dnsresultorder=|^[[:space:]]*[│┌┐└┘├┤┬┴┼─═╭╮╰╯]+[[:space:]]*$')"
|
|
140
|
+
'^[[:space:]]*$|^[[:space:]]*(build[[:space:]]*·|◇[[:space:]]+doctor warnings)[[:space:]]*$|^[[:space:]]*◇[[:space:]]+|^[[:space:]]*exa[[:space:]]+(web|code)[[:space:]]+search([[:space:]]+.*)?$|^[[:space:]]*[←→↳].*|^[[:space:]]*wrote file successfully\.?$|^[[:space:]]*(\$[[:space:]]*)?openclaw message send --channel[[:space:]]+|^[[:space:]]*bridge-guard:[[:space:]]*blocked openclaw message send.*$|^[[:space:]]*error:[[:space:]]*too many arguments for '\''send'\''.*$|^[[:space:]]*sent via telegram|^[[:space:]]*\[(telegram|discord|slack|whatsapp|signal|irc|matrix|line|mattermost|teams)\]|autoselectfamily=|dnsresultorder=|^[[:space:]]*[│┌┐└┘├┤┬┴┼─═╭╮╰╯]+[[:space:]]*$')"
|
|
123
141
|
|
|
124
142
|
marked="$(extract_last_marked_block "$cleaned")"
|
|
125
143
|
if [ -n "$marked" ]; then
|
|
@@ -131,6 +149,14 @@ sanitize_output() {
|
|
|
131
149
|
printf '%s' "$(trim_text "$cleaned")"
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
extract_text_from_json_stream() {
|
|
153
|
+
local raw="$1"
|
|
154
|
+
local parsed
|
|
155
|
+
|
|
156
|
+
parsed="$(printf '%s\n' "$raw" | node -e "const fs=require('fs');const lines=fs.readFileSync(0,'utf8').split(/\r?\n/);let out='';for(const line of lines){const s=line.trim();if(!s)continue;try{const obj=JSON.parse(s);if(obj&&obj.type==='text'&&obj.part&&typeof obj.part.text==='string'){out+=obj.part.text;}}catch{}}process.stdout.write(out);" 2>/dev/null)"
|
|
157
|
+
printf '%s' "$(trim_text "$parsed")"
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
sentence_case_first() {
|
|
135
161
|
local text="$1"
|
|
136
162
|
printf '%s' "$text" | awk '
|
|
@@ -166,34 +192,60 @@ apply_reply_style() {
|
|
|
166
192
|
run_with_timeout() {
|
|
167
193
|
local mode="$1"
|
|
168
194
|
local prompt="$2"
|
|
169
|
-
local output rc tmp pid watchdog
|
|
195
|
+
local output rc tmp pid watchdog guard_dir guarded_path real_openclaw
|
|
170
196
|
|
|
171
197
|
tmp="$(mktemp /tmp/opencode-run.XXXXXX)"
|
|
172
|
-
"$
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
sleep 2
|
|
181
|
-
kill -9 "$pid" 2>/dev/null
|
|
182
|
-
fi
|
|
183
|
-
) &
|
|
184
|
-
watchdog=$!
|
|
185
|
-
|
|
186
|
-
wait "$pid"
|
|
187
|
-
rc=$?
|
|
188
|
-
kill "$watchdog" 2>/dev/null
|
|
198
|
+
guarded_path="$PATH"
|
|
199
|
+
real_openclaw="$(command -v openclaw 2>/dev/null || true)"
|
|
200
|
+
if [ -n "$real_openclaw" ]; then
|
|
201
|
+
guard_dir="$(mktemp -d /tmp/opencode-bridge-guard.XXXXXX)"
|
|
202
|
+
printf '#!/bin/sh\nif [ "$1" = "message" ] && [ "$2" = "send" ]; then\n echo "bridge-guard: blocked openclaw message send from opencode runtime" >&2\n exit 64\nfi\nexec "%s" "$@"\n' "$real_openclaw" > "${guard_dir}/openclaw"
|
|
203
|
+
chmod +x "${guard_dir}/openclaw"
|
|
204
|
+
guarded_path="${guard_dir}:${PATH}"
|
|
205
|
+
fi
|
|
189
206
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
207
|
+
# Timeout here is an upper bound only; command returns immediately when OpenCode finishes.
|
|
208
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
209
|
+
if [ -n "$mode" ]; then
|
|
210
|
+
PATH="$guarded_path" timeout --signal=TERM --kill-after=2 "${RUN_TIMEOUT_SEC}s" \
|
|
211
|
+
"$OPENCODE" run "$mode" --no-fork --format json "$prompt" >"$tmp" 2>&1
|
|
212
|
+
else
|
|
213
|
+
PATH="$guarded_path" timeout --signal=TERM --kill-after=2 "${RUN_TIMEOUT_SEC}s" \
|
|
214
|
+
"$OPENCODE" run --no-fork --format json "$prompt" >"$tmp" 2>&1
|
|
215
|
+
fi
|
|
216
|
+
rc=$?
|
|
217
|
+
else
|
|
218
|
+
if [ -n "$mode" ]; then
|
|
219
|
+
PATH="$guarded_path" "$OPENCODE" run "$mode" --no-fork --format json "$prompt" >"$tmp" 2>&1 &
|
|
220
|
+
else
|
|
221
|
+
PATH="$guarded_path" "$OPENCODE" run --no-fork --format json "$prompt" >"$tmp" 2>&1 &
|
|
222
|
+
fi
|
|
223
|
+
pid=$!
|
|
224
|
+
|
|
225
|
+
(
|
|
226
|
+
sleep "$RUN_TIMEOUT_SEC"
|
|
227
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
228
|
+
touch "${tmp}.timeout"
|
|
229
|
+
kill "$pid" 2>/dev/null
|
|
230
|
+
sleep 2
|
|
231
|
+
kill -9 "$pid" 2>/dev/null
|
|
232
|
+
fi
|
|
233
|
+
) &
|
|
234
|
+
watchdog=$!
|
|
235
|
+
|
|
236
|
+
wait "$pid"
|
|
237
|
+
rc=$?
|
|
238
|
+
kill "$watchdog" 2>/dev/null
|
|
239
|
+
|
|
240
|
+
if [ -f "${tmp}.timeout" ]; then
|
|
241
|
+
rc=124
|
|
242
|
+
rm -f "${tmp}.timeout"
|
|
243
|
+
fi
|
|
193
244
|
fi
|
|
194
245
|
|
|
195
246
|
output="$(cat "$tmp")"
|
|
196
247
|
rm -f "$tmp"
|
|
248
|
+
[ -n "${guard_dir:-}" ] && rm -rf "$guard_dir"
|
|
197
249
|
|
|
198
250
|
printf '%s\n' "$rc"
|
|
199
251
|
printf '%s' "$output"
|
|
@@ -202,6 +254,7 @@ run_with_timeout() {
|
|
|
202
254
|
(
|
|
203
255
|
started_at=$(date +%s)
|
|
204
256
|
RUN_TIMEOUT_SEC="$(compute_timeout "$MSG")"
|
|
257
|
+
RUN_MODE="--continue"
|
|
205
258
|
cd "$WORKSPACE" || exit 1
|
|
206
259
|
FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
|
|
207
260
|
|
|
@@ -211,13 +264,20 @@ run_with_timeout() {
|
|
|
211
264
|
exit 0
|
|
212
265
|
fi
|
|
213
266
|
|
|
214
|
-
run_result="$(run_with_timeout
|
|
267
|
+
run_result="$(run_with_timeout "$RUN_MODE" "$FULL_MSG")"
|
|
215
268
|
rc="$(printf '%s' "$run_result" | head -n 1)"
|
|
216
269
|
raw_output="$(printf '%s' "$run_result" | tail -n +2)"
|
|
217
|
-
output="$(
|
|
270
|
+
output="$(extract_text_from_json_stream "$raw_output")"
|
|
271
|
+
if [ -z "$output" ]; then
|
|
272
|
+
output="$(sanitize_output "$raw_output")"
|
|
273
|
+
fi
|
|
218
274
|
|
|
219
275
|
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
|
|
220
|
-
|
|
276
|
+
if [ -n "$output" ] && ! is_trivial_echo "$output" "$MSG"; then
|
|
277
|
+
output="${output}\n\n(Stopped after ${RUN_TIMEOUT_SEC}s timeout.)"
|
|
278
|
+
else
|
|
279
|
+
output="OpenCode timed out after ${RUN_TIMEOUT_SEC}s. Try a narrower prompt or send a follow-up."
|
|
280
|
+
fi
|
|
221
281
|
elif [ -z "$output" ]; then
|
|
222
282
|
output="OpenCode finished, but returned an empty response."
|
|
223
283
|
elif is_trivial_echo "$output" "$MSG"; then
|
|
@@ -226,15 +286,12 @@ run_with_timeout() {
|
|
|
226
286
|
|
|
227
287
|
output="$(apply_reply_style "$output")"
|
|
228
288
|
|
|
229
|
-
|
|
230
|
-
printf '[%s] /cc skipped bridge send (already sent by OpenCode)\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
|
231
|
-
else
|
|
232
|
-
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
233
|
-
fi
|
|
289
|
+
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
234
290
|
|
|
235
291
|
ended_at=$(date +%s)
|
|
236
292
|
elapsed=$((ended_at - started_at))
|
|
237
|
-
|
|
293
|
+
mode_label="${RUN_MODE:---new}"
|
|
294
|
+
printf '[%s] /cc completed in %ss (exit=%s timeout=%ss mode=%s)\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$elapsed" "$rc" "$RUN_TIMEOUT_SEC" "$mode_label"
|
|
238
295
|
) >>"$LOG_FILE" 2>&1 &
|
|
239
296
|
|
|
240
297
|
echo "✅ OpenCode request queued."
|