openclaw-opencode-bridge 2.1.1 → 2.1.3

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 CHANGED
@@ -4,7 +4,9 @@
4
4
  [![license](https://img.shields.io/npm/l/openclaw-opencode-bridge)](LICENSE)
5
5
  [![node](https://img.shields.io/node/v/openclaw-opencode-bridge)](package.json)
6
6
 
7
- > Forked from [openclaw-claude-bridge](https://github.com/bettep-dev/openclaw-claude-bridge) by [@bettep-dev](https://github.com/bettep-dev) — modified to work with OpenCode instead of Claude CLI.
7
+ > Liked this project? Consider donating!
8
+
9
+ > EVM Address: 0xe81c32383C8F21A14E6C2264939dA512e9F9bb42
8
10
 
9
11
  Bridge [OpenClaw](https://openclaw.ai) messaging channels to [OpenCode](https://opencode.ai) via persistent tmux sessions.
10
12
 
@@ -98,6 +100,8 @@ Removes all installed components — plugin, shell scripts, OPENCODE.md addition
98
100
  | Delivery confirmed but no reply | Check `tmux ls` — session may have crashed |
99
101
  | Multiline sends only first line | Re-run `openclaw-opencode-bridge onboard` (v2.0.6+) |
100
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.
104
+
101
105
  ## License
102
106
 
103
107
  [MIT](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-opencode-bridge",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "Bridge OpenClaw messaging channels to OpenCode via tmux persistent sessions",
5
5
  "main": "./lib/onboard.js",
6
6
  "bin": {
@@ -37,10 +37,10 @@
37
37
  "name": "Febrian",
38
38
  "email": "febro.aw20@gmail.com"
39
39
  },
40
- "homepage": "https://febro.fun",
40
+ "homepage": "http://febro.fun",
41
41
  "license": "MIT",
42
42
  "repository": {
43
43
  "type": "git",
44
- "url": "https://github.com/bettep-dev/openclaw-opencode-bridge"
44
+ "url": "https://github.com/AganFebro/openclaw-opencode-bridge"
45
45
  }
46
46
  }
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # bridge-version: 5
2
+ # bridge-version: 9
3
3
  # Start fresh session asynchronously and send instruction
4
4
  MSG="$1"
5
5
  OPENCODE="{{OPENCODE_BIN}}"
@@ -9,6 +9,7 @@ WORKSPACE="{{WORKSPACE}}"
9
9
  LOG_FILE="/tmp/opencode-bridge-send.log"
10
10
  BASE_TIMEOUT_SEC=45
11
11
  MAX_TIMEOUT_SEC=300
12
+ LOCK_WAIT_SEC=600
12
13
 
13
14
  if [ -z "$MSG" ]; then
14
15
  echo "ERROR: No message provided"
@@ -28,6 +29,21 @@ normalize_text() {
28
29
  | sed -E 's/^🔗[[:space:]]*//; s/^["'\''`]+|["'\''`]+$//g; s/[[:space:]]+/ /g; s/^[[:space:]]+|[[:space:]]+$//g'
29
30
  }
30
31
 
32
+ acquire_bridge_lock() {
33
+ local safe_channel safe_target lock_file
34
+ safe_channel="$(printf '%s' "$CHANNEL" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
35
+ safe_target="$(printf '%s' "$TARGET" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
36
+ lock_file="/tmp/opencode-bridge-${safe_channel}-${safe_target}.lock"
37
+
38
+ if command -v flock >/dev/null 2>&1; then
39
+ exec 200>"$lock_file"
40
+ flock -w "$LOCK_WAIT_SEC" 200
41
+ return $?
42
+ fi
43
+
44
+ return 0
45
+ }
46
+
31
47
  compute_timeout() {
32
48
  local msg="$1"
33
49
  local timeout="$BASE_TIMEOUT_SEC"
@@ -62,49 +78,100 @@ is_trivial_echo() {
62
78
  [ -n "$message_norm" ] && [ "$output_norm" = "$message_norm" ]
63
79
  }
64
80
 
65
- extract_embedded_send_message() {
81
+ has_external_delivery_success() {
66
82
  local raw="$1"
67
- local line message
68
-
69
- line="$(printf '%s\n' "$raw" | grep -E 'openclaw message send --channel' | tail -n 1)"
70
- [ -z "$line" ] && return 0
83
+ printf '%s\n' "$raw" | grep -Eqi 'Sent via [A-Za-z]+|Message ID:[[:space:]]*[0-9]+'
84
+ }
71
85
 
72
- if printf '%s' "$line" | grep -q -- "-m '"; then
73
- message="$(printf '%s' "$line" | sed -n "s/.* -m '\(.*\)'.*/\1/p")"
74
- elif printf '%s' "$line" | grep -q -- '-m "'; then
75
- message="$(printf '%s' "$line" | sed -n 's/.* -m "\(.*\)".*/\1/p')"
86
+ extract_last_marked_block() {
87
+ local raw="$1"
88
+ if ! printf '%s' "$raw" | grep -q '🔗'; then
89
+ return 0
76
90
  fi
77
91
 
78
- printf '%s' "$(trim_text "$message")"
92
+ printf '%s\n' "$raw" | awk '
93
+ {
94
+ pos = index($0, "🔗");
95
+ if (pos > 0) {
96
+ out = substr($0, pos);
97
+ capture = 1;
98
+ next;
99
+ }
100
+ if (capture) {
101
+ out = out "\n" $0;
102
+ }
103
+ }
104
+ END {
105
+ if (capture) print out;
106
+ }'
79
107
  }
80
108
 
81
109
  sanitize_output() {
82
110
  local raw="$1"
83
- local extracted cleaned
84
-
85
- extracted="$(extract_embedded_send_message "$raw")"
86
- if [ -n "$extracted" ]; then
87
- printf '%s' "$extracted"
88
- return 0
89
- fi
111
+ local cleaned marked
90
112
 
91
113
  cleaned="$(printf '%s' "$raw" \
92
114
  | tr '\r' '\n' \
93
115
  | sed -E $'s/\x1B\\[[0-9;?]*[ -/]*[@-~]//g; s/\x1B\\][^\a]*(\a|\x1B\\\\)//g')"
94
116
 
117
+ cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/\[[0-9]{1,3}m//g')"
118
+
95
119
  cleaned="$(printf '%s\n' "$cleaned" | grep -Ev \
96
- '^[[:space:]]*$|^[[:space:]]*(build ·|◇ Doctor warnings)[[:space:]]*$|^[[:space:]]*openclaw message send --channel[[:space:]]+|^[[:space:]]*Sent via Telegram|^[[:space:]]*\[[0-9]{1,3}m|^[[:space:]]*\[(telegram|discord|slack|whatsapp|signal|irc|matrix|line|mattermost|teams)\]|autoSelectFamily=|dnsResultOrder=|^[[:space:]]*[│┌┐└┘├┤┬┴┼─═╭╮╰╯]+[[:space:]]*$|^[[:space:]]*\$[[:space:]]*\[[0-9]{1,3}m')"
120
+ '^[[:space:]]*$|^[[:space:]]*(build[[:space:]]*·|◇[[:space:]]+Doctor warnings)[[:space:]]*$|^[[:space:]]*◇[[: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:]]*$')"
121
+
122
+ marked="$(extract_last_marked_block "$cleaned")"
123
+ if [ -n "$marked" ]; then
124
+ cleaned="$marked"
125
+ fi
126
+
127
+ cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/^🔗[[:space:]]*//')"
97
128
 
98
129
  printf '%s' "$(trim_text "$cleaned")"
99
130
  }
100
131
 
132
+ sentence_case_first() {
133
+ local text="$1"
134
+ printf '%s' "$text" | awk '
135
+ BEGIN { done = 0 }
136
+ {
137
+ if (done) { print; next }
138
+ line = $0
139
+ for (i = 1; i <= length(line); i++) {
140
+ ch = substr(line, i, 1)
141
+ if (ch ~ /[a-z]/) {
142
+ pre = substr(line, 1, i - 1)
143
+ post = substr(line, i + 1)
144
+ line = pre toupper(ch) post
145
+ done = 1
146
+ break
147
+ } else if (ch ~ /[A-Z]/) {
148
+ done = 1
149
+ break
150
+ }
151
+ }
152
+ print line
153
+ }'
154
+ }
155
+
156
+ apply_reply_style() {
157
+ local text="$1"
158
+ text="$(trim_text "$text")"
159
+ [ -z "$text" ] && { printf '%s' "$text"; return; }
160
+ text="$(sentence_case_first "$text")"
161
+ printf '%s' "$text"
162
+ }
163
+
101
164
  run_with_timeout() {
102
165
  local mode="$1"
103
166
  local prompt="$2"
104
167
  local output rc tmp pid watchdog
105
168
 
106
169
  tmp="$(mktemp /tmp/opencode-run.XXXXXX)"
107
- "$OPENCODE" run "$mode" "$prompt" >"$tmp" 2>&1 &
170
+ if [ -n "$mode" ]; then
171
+ "$OPENCODE" run "$mode" "$prompt" >"$tmp" 2>&1 &
172
+ else
173
+ "$OPENCODE" run "$prompt" >"$tmp" 2>&1 &
174
+ fi
108
175
  pid=$!
109
176
 
110
177
  (
@@ -140,10 +207,17 @@ run_with_timeout() {
140
207
  cd "$WORKSPACE" || exit 1
141
208
  FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
142
209
 
143
- run_result="$(run_with_timeout --fork "$FULL_MSG")"
210
+ if ! acquire_bridge_lock; then
211
+ openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "Bridge is still processing a previous request. Please retry in a moment."
212
+ printf '[%s] /ccn lock timeout after %ss\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$LOCK_WAIT_SEC"
213
+ exit 0
214
+ fi
215
+
216
+ # Fresh request: run without --continue to avoid session carryover.
217
+ run_result="$(run_with_timeout "" "$FULL_MSG")"
144
218
  rc="$(printf '%s' "$run_result" | head -n 1)"
145
- output="$(printf '%s' "$run_result" | tail -n +2)"
146
- output="$(sanitize_output "$output")"
219
+ raw_output="$(printf '%s' "$run_result" | tail -n +2)"
220
+ output="$(sanitize_output "$raw_output")"
147
221
 
148
222
  if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
149
223
  output="OpenCode timed out after ${RUN_TIMEOUT_SEC}s. Task may still be running. Try waiting a bit or send a follow-up."
@@ -153,7 +227,13 @@ run_with_timeout() {
153
227
  output="OpenCode ran, but returned a non-informative echo. Please retry with a more specific prompt."
154
228
  fi
155
229
 
156
- openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
230
+ output="$(apply_reply_style "$output")"
231
+
232
+ if has_external_delivery_success "$raw_output"; then
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
157
237
 
158
238
  ended_at=$(date +%s)
159
239
  elapsed=$((ended_at - started_at))
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # bridge-version: 5
2
+ # bridge-version: 9
3
3
  # Dispatch instruction to OpenCode asynchronously and relay response
4
4
  MSG="$1"
5
5
  OPENCODE="{{OPENCODE_BIN}}"
@@ -9,6 +9,7 @@ WORKSPACE="{{WORKSPACE}}"
9
9
  LOG_FILE="/tmp/opencode-bridge-send.log"
10
10
  BASE_TIMEOUT_SEC=45
11
11
  MAX_TIMEOUT_SEC=300
12
+ LOCK_WAIT_SEC=600
12
13
 
13
14
  if [ -z "$MSG" ]; then
14
15
  echo "ERROR: No message provided"
@@ -28,6 +29,21 @@ normalize_text() {
28
29
  | sed -E 's/^🔗[[:space:]]*//; s/^["'\''`]+|["'\''`]+$//g; s/[[:space:]]+/ /g; s/^[[:space:]]+|[[:space:]]+$//g'
29
30
  }
30
31
 
32
+ acquire_bridge_lock() {
33
+ local safe_channel safe_target lock_file
34
+ safe_channel="$(printf '%s' "$CHANNEL" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
35
+ safe_target="$(printf '%s' "$TARGET" | sed -E 's/[^a-zA-Z0-9._-]/_/g')"
36
+ lock_file="/tmp/opencode-bridge-${safe_channel}-${safe_target}.lock"
37
+
38
+ if command -v flock >/dev/null 2>&1; then
39
+ exec 200>"$lock_file"
40
+ flock -w "$LOCK_WAIT_SEC" 200
41
+ return $?
42
+ fi
43
+
44
+ return 0
45
+ }
46
+
31
47
  compute_timeout() {
32
48
  local msg="$1"
33
49
  local timeout="$BASE_TIMEOUT_SEC"
@@ -62,42 +78,89 @@ is_trivial_echo() {
62
78
  [ -n "$message_norm" ] && [ "$output_norm" = "$message_norm" ]
63
79
  }
64
80
 
65
- extract_embedded_send_message() {
81
+ has_external_delivery_success() {
66
82
  local raw="$1"
67
- local line message
68
-
69
- line="$(printf '%s\n' "$raw" | grep -E 'openclaw message send --channel' | tail -n 1)"
70
- [ -z "$line" ] && return 0
83
+ printf '%s\n' "$raw" | grep -Eqi 'Sent via [A-Za-z]+|Message ID:[[:space:]]*[0-9]+'
84
+ }
71
85
 
72
- if printf '%s' "$line" | grep -q -- "-m '"; then
73
- message="$(printf '%s' "$line" | sed -n "s/.* -m '\(.*\)'.*/\1/p")"
74
- elif printf '%s' "$line" | grep -q -- '-m "'; then
75
- message="$(printf '%s' "$line" | sed -n 's/.* -m "\(.*\)".*/\1/p')"
86
+ extract_last_marked_block() {
87
+ local raw="$1"
88
+ if ! printf '%s' "$raw" | grep -q '🔗'; then
89
+ return 0
76
90
  fi
77
91
 
78
- printf '%s' "$(trim_text "$message")"
92
+ printf '%s\n' "$raw" | awk '
93
+ {
94
+ pos = index($0, "🔗");
95
+ if (pos > 0) {
96
+ out = substr($0, pos);
97
+ capture = 1;
98
+ next;
99
+ }
100
+ if (capture) {
101
+ out = out "\n" $0;
102
+ }
103
+ }
104
+ END {
105
+ if (capture) print out;
106
+ }'
79
107
  }
80
108
 
81
109
  sanitize_output() {
82
110
  local raw="$1"
83
- local extracted cleaned
84
-
85
- extracted="$(extract_embedded_send_message "$raw")"
86
- if [ -n "$extracted" ]; then
87
- printf '%s' "$extracted"
88
- return 0
89
- fi
111
+ local cleaned marked
90
112
 
91
113
  cleaned="$(printf '%s' "$raw" \
92
114
  | tr '\r' '\n' \
93
115
  | sed -E $'s/\x1B\\[[0-9;?]*[ -/]*[@-~]//g; s/\x1B\\][^\a]*(\a|\x1B\\\\)//g')"
94
116
 
117
+ cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/\[[0-9]{1,3}m//g')"
118
+
95
119
  cleaned="$(printf '%s\n' "$cleaned" | grep -Ev \
96
- '^[[:space:]]*$|^[[:space:]]*(build ·|◇ Doctor warnings)[[:space:]]*$|^[[:space:]]*openclaw message send --channel[[:space:]]+|^[[:space:]]*Sent via Telegram|^[[:space:]]*\[[0-9]{1,3}m|^[[:space:]]*\[(telegram|discord|slack|whatsapp|signal|irc|matrix|line|mattermost|teams)\]|autoSelectFamily=|dnsResultOrder=|^[[:space:]]*[│┌┐└┘├┤┬┴┼─═╭╮╰╯]+[[:space:]]*$|^[[:space:]]*\$[[:space:]]*\[[0-9]{1,3}m')"
120
+ '^[[:space:]]*$|^[[:space:]]*(build[[:space:]]*·|◇[[:space:]]+Doctor warnings)[[:space:]]*$|^[[:space:]]*◇[[: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:]]*$')"
121
+
122
+ marked="$(extract_last_marked_block "$cleaned")"
123
+ if [ -n "$marked" ]; then
124
+ cleaned="$marked"
125
+ fi
126
+
127
+ cleaned="$(printf '%s\n' "$cleaned" | sed -E 's/^🔗[[:space:]]*//')"
97
128
 
98
129
  printf '%s' "$(trim_text "$cleaned")"
99
130
  }
100
131
 
132
+ sentence_case_first() {
133
+ local text="$1"
134
+ printf '%s' "$text" | awk '
135
+ BEGIN { done = 0 }
136
+ {
137
+ if (done) { print; next }
138
+ line = $0
139
+ for (i = 1; i <= length(line); i++) {
140
+ ch = substr(line, i, 1)
141
+ if (ch ~ /[a-z]/) {
142
+ pre = substr(line, 1, i - 1)
143
+ post = substr(line, i + 1)
144
+ line = pre toupper(ch) post
145
+ done = 1
146
+ break
147
+ } else if (ch ~ /[A-Z]/) {
148
+ done = 1
149
+ break
150
+ }
151
+ }
152
+ print line
153
+ }'
154
+ }
155
+
156
+ apply_reply_style() {
157
+ local text="$1"
158
+ text="$(trim_text "$text")"
159
+ [ -z "$text" ] && { printf '%s' "$text"; return; }
160
+ text="$(sentence_case_first "$text")"
161
+ printf '%s' "$text"
162
+ }
163
+
101
164
  run_with_timeout() {
102
165
  local mode="$1"
103
166
  local prompt="$2"
@@ -140,10 +203,16 @@ run_with_timeout() {
140
203
  cd "$WORKSPACE" || exit 1
141
204
  FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
142
205
 
206
+ if ! acquire_bridge_lock; then
207
+ openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "Bridge is still processing a previous request. Please retry in a moment."
208
+ printf '[%s] /cc lock timeout after %ss\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$LOCK_WAIT_SEC"
209
+ exit 0
210
+ fi
211
+
143
212
  run_result="$(run_with_timeout --continue "$FULL_MSG")"
144
213
  rc="$(printf '%s' "$run_result" | head -n 1)"
145
- output="$(printf '%s' "$run_result" | tail -n +2)"
146
- output="$(sanitize_output "$output")"
214
+ raw_output="$(printf '%s' "$run_result" | tail -n +2)"
215
+ output="$(sanitize_output "$raw_output")"
147
216
 
148
217
  if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
149
218
  output="OpenCode timed out after ${RUN_TIMEOUT_SEC}s. Task may still be running. Try waiting a bit or send a follow-up."
@@ -153,7 +222,13 @@ run_with_timeout() {
153
222
  output="OpenCode ran, but returned a non-informative echo. Please retry with a more specific prompt."
154
223
  fi
155
224
 
156
- openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
225
+ output="$(apply_reply_style "$output")"
226
+
227
+ if has_external_delivery_success "$raw_output"; then
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
157
232
 
158
233
  ended_at=$(date +%s)
159
234
  elapsed=$((ended_at - started_at))
@@ -39,4 +39,6 @@ The format is: `[CHANNEL:ID] actual message`
39
39
  - For follow-up status questions such as "have you created it?", do not answer with only yes/no.
40
40
  Always include short status details and the run command.
41
41
  - Keep tone clear, proactive, and helpful. Prefer concise but complete responses.
42
+ - Use proper sentence case and punctuation. Start the first sentence with an uppercase letter.
43
+ - Avoid slang-only openings like "hey!" or "yep!" without context; provide a complete helpful sentence.
42
44
  - Length guidance: trivial questions can be 1-2 lines; implementation results should usually be 4-10 lines.