openclaw-opencode-bridge 2.0.8 → 2.1.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-opencode-bridge",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
4
4
  "description": "Bridge OpenClaw messaging channels to OpenCode via tmux persistent sessions",
5
5
  "main": "./lib/onboard.js",
6
6
  "bin": {
package/plugin/index.ts CHANGED
@@ -12,7 +12,8 @@ const SCRIPT_MAP: Record<string, string> = {
12
12
  };
13
13
 
14
14
  const REQUIRES_ARG = new Set(["cc", "ccn"]);
15
- const EXEC_TIMEOUT = 120_000;
15
+ const EXEC_TIMEOUT = 15_000;
16
+ const SUPPRESSION_WINDOW = 15_000;
16
17
 
17
18
  const DELIVERY_MSG = "🔗 OpenCode will reply shortly.";
18
19
 
@@ -57,15 +58,6 @@ export default function register(api: OpenClawPluginApi) {
57
58
  const script = SCRIPT_MAP[command];
58
59
  if (!script) return;
59
60
 
60
- // Set flag for before_prompt_build to consume
61
- pendingBridgeCommand = true;
62
- // Also set suppression timer as safety net
63
- bridgeSuppressUntil = Date.now() + EXEC_TIMEOUT + 5_000;
64
-
65
- api.logger.debug?.(
66
- `[opencode-bridge] message_received: command=${command}, pendingBridgeCommand=true`,
67
- );
68
-
69
61
  const arg = match[2].trim();
70
62
 
71
63
  if (REQUIRES_ARG.has(command) && !arg) {
@@ -73,17 +65,32 @@ export default function register(api: OpenClawPluginApi) {
73
65
  return;
74
66
  }
75
67
 
68
+ // Set flag for before_prompt_build to consume
69
+ pendingBridgeCommand = true;
70
+ // Keep suppression narrow to this turn to avoid cross-message blocking.
71
+ bridgeSuppressUntil = Date.now() + SUPPRESSION_WINDOW;
72
+
73
+ api.logger.debug?.(
74
+ `[opencode-bridge] message_received: command=${command}, pendingBridgeCommand=true`,
75
+ );
76
+
76
77
  const scriptPath = `${scriptsDir}/${script}`;
77
78
  const args = arg ? [arg] : [];
79
+ const startedAt = Date.now();
78
80
 
79
81
  execFile(
80
82
  scriptPath,
81
83
  args,
82
84
  { timeout: EXEC_TIMEOUT },
83
85
  (error, _stdout, stderr) => {
86
+ const elapsedMs = Date.now() - startedAt;
84
87
  if (error) {
85
88
  api.logger.error?.(
86
- `[opencode-bridge] ${script} failed: ${stderr?.trim() || error.message}`,
89
+ `[opencode-bridge] ${script} failed after ${elapsedMs}ms: ${stderr?.trim() || error.message}`,
90
+ );
91
+ } else {
92
+ api.logger.debug?.(
93
+ `[opencode-bridge] ${script} queued/completed in ${elapsedMs}ms`,
87
94
  );
88
95
  }
89
96
  },
@@ -102,7 +109,7 @@ export default function register(api: OpenClawPluginApi) {
102
109
 
103
110
  if (shouldSuppress) {
104
111
  pendingBridgeCommand = false;
105
- bridgeSuppressUntil = Date.now() + EXEC_TIMEOUT + 5_000;
112
+ bridgeSuppressUntil = Date.now() + SUPPRESSION_WINDOW;
106
113
  return { systemPrompt: SILENT_PROMPT, prependContext: SILENT_PROMPT };
107
114
  } else {
108
115
  // Clear suppression for non-bridge messages
@@ -119,6 +126,8 @@ export default function register(api: OpenClawPluginApi) {
119
126
  );
120
127
 
121
128
  if (suppressing) {
129
+ // One-shot override for the intercepted bridge message.
130
+ bridgeSuppressUntil = 0;
122
131
  return { content: DELIVERY_MSG, cancel: false };
123
132
  }
124
133
  });
@@ -1,22 +1,84 @@
1
1
  #!/bin/bash
2
- # bridge-version: 3
3
- # Start fresh session and send instruction
2
+ # bridge-version: 4
3
+ # Start fresh session asynchronously and send instruction
4
4
  MSG="$1"
5
5
  OPENCODE="{{OPENCODE_BIN}}"
6
6
  CHANNEL="{{CHANNEL}}"
7
7
  TARGET="{{TARGET_ID}}"
8
8
  WORKSPACE="{{WORKSPACE}}"
9
+ RUN_TIMEOUT_SEC=45
10
+ LOG_FILE="/tmp/opencode-bridge-send.log"
9
11
 
10
12
  if [ -z "$MSG" ]; then
11
13
  echo "ERROR: No message provided"
12
14
  exit 1
13
15
  fi
14
16
 
15
- cd "$WORKSPACE"
16
- FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
17
+ trim_text() {
18
+ local value="$1"
19
+ value="${value#"${value%%[![:space:]]*}"}"
20
+ value="${value%"${value##*[![:space:]]}"}"
21
+ printf '%s' "$value"
22
+ }
17
23
 
18
- OUTPUT=$("$OPENCODE" run --fork "$FULL_MSG" 2>&1)
24
+ normalize_text() {
25
+ printf '%s' "$1" \
26
+ | tr '[:upper:]' '[:lower:]' \
27
+ | sed -E 's/^🔗[[:space:]]*//; s/^["'\''`]+|["'\''`]+$//g; s/[[:space:]]+/ /g; s/^[[:space:]]+|[[:space:]]+$//g'
28
+ }
19
29
 
20
- openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$OUTPUT"
30
+ is_trivial_echo() {
31
+ local output_norm message_norm
32
+ output_norm="$(normalize_text "$1")"
33
+ message_norm="$(normalize_text "$2")"
34
+ [ -n "$message_norm" ] && [ "$output_norm" = "$message_norm" ]
35
+ }
21
36
 
22
- echo "✅ OpenCode response sent."
37
+ run_with_timeout() {
38
+ local mode="$1"
39
+ local prompt="$2"
40
+ local output rc
41
+
42
+ if command -v timeout >/dev/null 2>&1; then
43
+ output=$(timeout "${RUN_TIMEOUT_SEC}s" "$OPENCODE" run "$mode" "$prompt" 2>&1)
44
+ rc=$?
45
+ elif command -v gtimeout >/dev/null 2>&1; then
46
+ output=$(gtimeout "${RUN_TIMEOUT_SEC}s" "$OPENCODE" run "$mode" "$prompt" 2>&1)
47
+ rc=$?
48
+ else
49
+ output=$("$OPENCODE" run "$mode" "$prompt" 2>&1)
50
+ rc=$?
51
+ fi
52
+
53
+ printf '%s\n' "$rc"
54
+ printf '%s' "$output"
55
+ }
56
+
57
+ (
58
+ started_at=$(date +%s)
59
+ cd "$WORKSPACE" || exit 1
60
+ FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
61
+
62
+ run_result="$(run_with_timeout --fork "$FULL_MSG")"
63
+ rc="$(printf '%s' "$run_result" | head -n 1)"
64
+ output="$(printf '%s' "$run_result" | tail -n +2)"
65
+
66
+ output="$(printf '%s\n' "$output" | sed -E '/^[[:space:]]*openclaw message send --channel[[:space:]]+/d')"
67
+ output="$(trim_text "$output")"
68
+
69
+ if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
70
+ output="OpenCode timed out after ${RUN_TIMEOUT_SEC}s. Please retry with a narrower request."
71
+ elif [ -z "$output" ]; then
72
+ output="OpenCode finished, but returned an empty response."
73
+ elif is_trivial_echo "$output" "$MSG"; then
74
+ output="OpenCode ran, but returned a non-informative echo. Please retry with a more specific prompt."
75
+ fi
76
+
77
+ openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
78
+
79
+ ended_at=$(date +%s)
80
+ elapsed=$((ended_at - started_at))
81
+ printf '[%s] /ccn completed in %ss (exit=%s)\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$elapsed" "$rc"
82
+ ) >>"$LOG_FILE" 2>&1 &
83
+
84
+ echo "✅ OpenCode new session queued."
@@ -1,22 +1,84 @@
1
1
  #!/bin/bash
2
- # bridge-version: 3
3
- # Send instruction to OpenCode and relay response to telegram
2
+ # bridge-version: 4
3
+ # Dispatch instruction to OpenCode asynchronously and relay response
4
4
  MSG="$1"
5
5
  OPENCODE="{{OPENCODE_BIN}}"
6
6
  CHANNEL="{{CHANNEL}}"
7
7
  TARGET="{{TARGET_ID}}"
8
8
  WORKSPACE="{{WORKSPACE}}"
9
+ RUN_TIMEOUT_SEC=45
10
+ LOG_FILE="/tmp/opencode-bridge-send.log"
9
11
 
10
12
  if [ -z "$MSG" ]; then
11
13
  echo "ERROR: No message provided"
12
14
  exit 1
13
15
  fi
14
16
 
15
- cd "$WORKSPACE"
16
- FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
17
+ trim_text() {
18
+ local value="$1"
19
+ value="${value#"${value%%[![:space:]]*}"}"
20
+ value="${value%"${value##*[![:space:]]}"}"
21
+ printf '%s' "$value"
22
+ }
17
23
 
18
- OUTPUT=$("$OPENCODE" run --continue "$FULL_MSG" 2>&1)
24
+ normalize_text() {
25
+ printf '%s' "$1" \
26
+ | tr '[:upper:]' '[:lower:]' \
27
+ | sed -E 's/^🔗[[:space:]]*//; s/^["'\''`]+|["'\''`]+$//g; s/[[:space:]]+/ /g; s/^[[:space:]]+|[[:space:]]+$//g'
28
+ }
19
29
 
20
- openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$OUTPUT"
30
+ is_trivial_echo() {
31
+ local output_norm message_norm
32
+ output_norm="$(normalize_text "$1")"
33
+ message_norm="$(normalize_text "$2")"
34
+ [ -n "$message_norm" ] && [ "$output_norm" = "$message_norm" ]
35
+ }
21
36
 
22
- echo "✅ OpenCode response sent."
37
+ run_with_timeout() {
38
+ local mode="$1"
39
+ local prompt="$2"
40
+ local output rc
41
+
42
+ if command -v timeout >/dev/null 2>&1; then
43
+ output=$(timeout "${RUN_TIMEOUT_SEC}s" "$OPENCODE" run "$mode" "$prompt" 2>&1)
44
+ rc=$?
45
+ elif command -v gtimeout >/dev/null 2>&1; then
46
+ output=$(gtimeout "${RUN_TIMEOUT_SEC}s" "$OPENCODE" run "$mode" "$prompt" 2>&1)
47
+ rc=$?
48
+ else
49
+ output=$("$OPENCODE" run "$mode" "$prompt" 2>&1)
50
+ rc=$?
51
+ fi
52
+
53
+ printf '%s\n' "$rc"
54
+ printf '%s' "$output"
55
+ }
56
+
57
+ (
58
+ started_at=$(date +%s)
59
+ cd "$WORKSPACE" || exit 1
60
+ FULL_MSG="[${CHANNEL}:${TARGET}] $MSG"
61
+
62
+ run_result="$(run_with_timeout --continue "$FULL_MSG")"
63
+ rc="$(printf '%s' "$run_result" | head -n 1)"
64
+ output="$(printf '%s' "$run_result" | tail -n +2)"
65
+
66
+ output="$(printf '%s\n' "$output" | sed -E '/^[[:space:]]*openclaw message send --channel[[:space:]]+/d')"
67
+ output="$(trim_text "$output")"
68
+
69
+ if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
70
+ output="OpenCode timed out after ${RUN_TIMEOUT_SEC}s. Please retry with a narrower request."
71
+ elif [ -z "$output" ]; then
72
+ output="OpenCode finished, but returned an empty response."
73
+ elif is_trivial_echo "$output" "$MSG"; then
74
+ output="OpenCode ran, but returned a non-informative echo. Please retry with a more specific prompt."
75
+ fi
76
+
77
+ openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$output"
78
+
79
+ ended_at=$(date +%s)
80
+ elapsed=$((ended_at - started_at))
81
+ printf '[%s] /cc completed in %ss (exit=%s)\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$elapsed" "$rc"
82
+ ) >>"$LOG_FILE" 2>&1 &
83
+
84
+ echo "✅ OpenCode request queued."
@@ -1,11 +1,12 @@
1
1
  #!/bin/bash
2
- # bridge-version: 4
3
- # Keep OpenCode server alive (runs every 30s via daemon)
4
- # Safe: idempotent, flock-protected (with fallback)
2
+ # bridge-version: 5
3
+ # Keep OpenCode server running in background (for faster message processing)
4
+ # Daemon runs every 30s to ensure server stays alive
5
5
 
6
6
  OPENCODE="{{OPENCODE_BIN}}"
7
7
  WORKSPACE="{{WORKSPACE}}"
8
8
  LOCK_FILE="/tmp/opencode-session.lock"
9
+ PID_FILE="/tmp/opencode-session.pid"
9
10
  TIMEOUT=10
10
11
 
11
12
  # Use flock for lock protection if available
@@ -25,10 +26,17 @@ fi
25
26
 
26
27
  cd "$WORKSPACE"
27
28
 
28
- # Start OpenCode in server mode if not running
29
- if ! pgrep -f "opencode serve" > /dev/null 2>&1; then
30
- "$OPENCODE" serve &
31
- sleep 2
29
+ # Check if server is already running
30
+ if [ -f "$PID_FILE" ]; then
31
+ OLD_PID=$(cat "$PID_FILE")
32
+ if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
33
+ exit 0
34
+ fi
32
35
  fi
33
36
 
37
+ # Start OpenCode server in background
38
+ nohup "$OPENCODE" serve > /tmp/opencode-serve.log 2>&1 &
39
+ echo $! > "$PID_FILE"
40
+ sleep 3
41
+
34
42
  exit 0
@@ -2,23 +2,28 @@
2
2
 
3
3
  ## Bot Mode
4
4
 
5
- When a message starts with `[{CHANNEL}:{ID}]` prefix, you **MUST** follow this procedure.
5
+ When a message starts with `[{CHANNEL}:{ID}]` prefix, you are being used as a bot.
6
+
7
+ ### Message Format
8
+
9
+ The format is: `[CHANNEL:ID] actual message`
10
+
11
+ **Examples:**
12
+ - `[telegram:850961516] hello` → Channel=telegram, ID=850961516, Message="hello"
13
+ - `[discord:123456] build a website` → Channel=discord, ID=123456, Message="build a website"
14
+
15
+ **IMPORTANT:** The prefix `[channel:id]` is metadata. You should ONLY respond to the actual message, NOT repeat the prefix or the user's name in your response.
6
16
 
7
17
  ### Procedure
8
18
 
9
- 1. Extract `{CHANNEL}` and `{ID}` from the prefix
10
- 2. Perform the task described after the prefix
11
- 3. **After completion, send the response via:**
12
- ```bash
13
- openclaw message send --channel {CHANNEL} --target {ID} -m '🔗 response'
14
- ```
15
- 4. **Every message MUST start with 🔗** — this identifies you as OpenCode
16
- 5. Split messages over 4000 characters into multiple sends (each starts with 🔗)
17
-
18
- ### Response transmission is mandatory (never skip)
19
-
20
- - **Every `[{CHANNEL}:{ID}]` message MUST receive a response via the channel.**
21
- - Regardless of success, failure, or error, you must always send the result.
22
- - Ending a turn without sending is **strictly forbidden**.
23
- - If the task failed, send the failure reason. If it succeeded, send the result.
24
- - If you did not run `openclaw message send`, the task is **NOT** complete.
19
+ 1. Extract `{CHANNEL}` and `{ID}` from the `[...]` prefix
20
+ 2. Ignore everything before `] ` that is metadata, not user content
21
+ 3. Perform the task described in the actual message
22
+ 4. Return only the assistant answer text
23
+
24
+ ### Output rules
25
+
26
+ - Do not output or suggest `openclaw message send` commands.
27
+ - Do not include the `[channel:id]` prefix in your response.
28
+ - Do not prepend delivery markers such as `🔗` unless explicitly requested by the user.
29
+ - If the task fails, return a direct failure reason in plain text.