openclaw-opencode-bridge 2.1.3 → 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 +103 -50
- package/templates/scripts/opencode-send.sh +107 -48
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
|
|
@@ -116,8 +134,10 @@ sanitize_output() {
|
|
|
116
134
|
|
|
117
135
|
cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/\[[0-9]{1,3}m//g')"
|
|
118
136
|
|
|
119
|
-
cleaned="$(printf '%s\n' "$cleaned" |
|
|
120
|
-
|
|
137
|
+
cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/^[[:space:]]*>+[[:space:]]?//; /^```/d')"
|
|
138
|
+
|
|
139
|
+
cleaned="$(printf '%s\n' "$cleaned" | grep -Eiv \
|
|
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:]]*$')"
|
|
121
141
|
|
|
122
142
|
marked="$(extract_last_marked_block "$cleaned")"
|
|
123
143
|
if [ -n "$marked" ]; then
|
|
@@ -129,6 +149,14 @@ sanitize_output() {
|
|
|
129
149
|
printf '%s' "$(trim_text "$cleaned")"
|
|
130
150
|
}
|
|
131
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
|
+
|
|
132
160
|
sentence_case_first() {
|
|
133
161
|
local text="$1"
|
|
134
162
|
printf '%s' "$text" | awk '
|
|
@@ -164,38 +192,60 @@ apply_reply_style() {
|
|
|
164
192
|
run_with_timeout() {
|
|
165
193
|
local mode="$1"
|
|
166
194
|
local prompt="$2"
|
|
167
|
-
local output rc tmp pid watchdog
|
|
195
|
+
local output rc tmp pid watchdog guard_dir guarded_path real_openclaw
|
|
168
196
|
|
|
169
197
|
tmp="$(mktemp /tmp/opencode-run.XXXXXX)"
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"$
|
|
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}"
|
|
174
205
|
fi
|
|
175
|
-
pid=$!
|
|
176
|
-
|
|
177
|
-
(
|
|
178
|
-
sleep "$RUN_TIMEOUT_SEC"
|
|
179
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
180
|
-
touch "${tmp}.timeout"
|
|
181
|
-
kill "$pid" 2>/dev/null
|
|
182
|
-
sleep 2
|
|
183
|
-
kill -9 "$pid" 2>/dev/null
|
|
184
|
-
fi
|
|
185
|
-
) &
|
|
186
|
-
watchdog=$!
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
195
244
|
fi
|
|
196
245
|
|
|
197
246
|
output="$(cat "$tmp")"
|
|
198
247
|
rm -f "$tmp"
|
|
248
|
+
[ -n "${guard_dir:-}" ] && rm -rf "$guard_dir"
|
|
199
249
|
|
|
200
250
|
printf '%s\n' "$rc"
|
|
201
251
|
printf '%s' "$output"
|
|
@@ -217,10 +267,17 @@ run_with_timeout() {
|
|
|
217
267
|
run_result="$(run_with_timeout "" "$FULL_MSG")"
|
|
218
268
|
rc="$(printf '%s' "$run_result" | head -n 1)"
|
|
219
269
|
raw_output="$(printf '%s' "$run_result" | tail -n +2)"
|
|
220
|
-
output="$(
|
|
270
|
+
output="$(extract_text_from_json_stream "$raw_output")"
|
|
271
|
+
if [ -z "$output" ]; then
|
|
272
|
+
output="$(sanitize_output "$raw_output")"
|
|
273
|
+
fi
|
|
221
274
|
|
|
222
275
|
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
|
|
223
|
-
|
|
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
|
|
224
281
|
elif [ -z "$output" ]; then
|
|
225
282
|
output="OpenCode finished, but returned an empty response."
|
|
226
283
|
elif is_trivial_echo "$output" "$MSG"; then
|
|
@@ -229,11 +286,7 @@ run_with_timeout() {
|
|
|
229
286
|
|
|
230
287
|
output="$(apply_reply_style "$output")"
|
|
231
288
|
|
|
232
|
-
|
|
233
|
-
printf '[%s] /ccn skipped bridge send (already sent by OpenCode)\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
|
234
|
-
else
|
|
235
|
-
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
236
|
-
fi
|
|
289
|
+
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
237
290
|
|
|
238
291
|
ended_at=$(date +%s)
|
|
239
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
|
|
@@ -116,8 +134,10 @@ sanitize_output() {
|
|
|
116
134
|
|
|
117
135
|
cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/\[[0-9]{1,3}m//g')"
|
|
118
136
|
|
|
119
|
-
cleaned="$(printf '%s\n' "$cleaned" |
|
|
120
|
-
|
|
137
|
+
cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/^[[:space:]]*>+[[:space:]]?//; /^```/d')"
|
|
138
|
+
|
|
139
|
+
cleaned="$(printf '%s\n' "$cleaned" | grep -Eiv \
|
|
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:]]*$')"
|
|
121
141
|
|
|
122
142
|
marked="$(extract_last_marked_block "$cleaned")"
|
|
123
143
|
if [ -n "$marked" ]; then
|
|
@@ -129,6 +149,14 @@ sanitize_output() {
|
|
|
129
149
|
printf '%s' "$(trim_text "$cleaned")"
|
|
130
150
|
}
|
|
131
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
|
+
|
|
132
160
|
sentence_case_first() {
|
|
133
161
|
local text="$1"
|
|
134
162
|
printf '%s' "$text" | awk '
|
|
@@ -164,34 +192,60 @@ apply_reply_style() {
|
|
|
164
192
|
run_with_timeout() {
|
|
165
193
|
local mode="$1"
|
|
166
194
|
local prompt="$2"
|
|
167
|
-
local output rc tmp pid watchdog
|
|
195
|
+
local output rc tmp pid watchdog guard_dir guarded_path real_openclaw
|
|
168
196
|
|
|
169
197
|
tmp="$(mktemp /tmp/opencode-run.XXXXXX)"
|
|
170
|
-
"$
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
sleep 2
|
|
179
|
-
kill -9 "$pid" 2>/dev/null
|
|
180
|
-
fi
|
|
181
|
-
) &
|
|
182
|
-
watchdog=$!
|
|
183
|
-
|
|
184
|
-
wait "$pid"
|
|
185
|
-
rc=$?
|
|
186
|
-
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
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
191
244
|
fi
|
|
192
245
|
|
|
193
246
|
output="$(cat "$tmp")"
|
|
194
247
|
rm -f "$tmp"
|
|
248
|
+
[ -n "${guard_dir:-}" ] && rm -rf "$guard_dir"
|
|
195
249
|
|
|
196
250
|
printf '%s\n' "$rc"
|
|
197
251
|
printf '%s' "$output"
|
|
@@ -200,6 +254,7 @@ run_with_timeout() {
|
|
|
200
254
|
(
|
|
201
255
|
started_at=$(date +%s)
|
|
202
256
|
RUN_TIMEOUT_SEC="$(compute_timeout "$MSG")"
|
|
257
|
+
RUN_MODE="--continue"
|
|
203
258
|
cd "$WORKSPACE" || exit 1
|
|
204
259
|
FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
|
|
205
260
|
|
|
@@ -209,13 +264,20 @@ run_with_timeout() {
|
|
|
209
264
|
exit 0
|
|
210
265
|
fi
|
|
211
266
|
|
|
212
|
-
run_result="$(run_with_timeout
|
|
267
|
+
run_result="$(run_with_timeout "$RUN_MODE" "$FULL_MSG")"
|
|
213
268
|
rc="$(printf '%s' "$run_result" | head -n 1)"
|
|
214
269
|
raw_output="$(printf '%s' "$run_result" | tail -n +2)"
|
|
215
|
-
output="$(
|
|
270
|
+
output="$(extract_text_from_json_stream "$raw_output")"
|
|
271
|
+
if [ -z "$output" ]; then
|
|
272
|
+
output="$(sanitize_output "$raw_output")"
|
|
273
|
+
fi
|
|
216
274
|
|
|
217
275
|
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
|
|
218
|
-
|
|
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
|
|
219
281
|
elif [ -z "$output" ]; then
|
|
220
282
|
output="OpenCode finished, but returned an empty response."
|
|
221
283
|
elif is_trivial_echo "$output" "$MSG"; then
|
|
@@ -224,15 +286,12 @@ run_with_timeout() {
|
|
|
224
286
|
|
|
225
287
|
output="$(apply_reply_style "$output")"
|
|
226
288
|
|
|
227
|
-
|
|
228
|
-
printf '[%s] /cc skipped bridge send (already sent by OpenCode)\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
|
229
|
-
else
|
|
230
|
-
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
231
|
-
fi
|
|
289
|
+
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
|
|
232
290
|
|
|
233
291
|
ended_at=$(date +%s)
|
|
234
292
|
elapsed=$((ended_at - started_at))
|
|
235
|
-
|
|
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"
|
|
236
295
|
) >>"$LOG_FILE" 2>&1 &
|
|
237
296
|
|
|
238
297
|
echo "✅ OpenCode request queued."
|