openclaw-opencode-bridge 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEMO_1.png +0 -0
- package/DEMO_2.png +0 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/lib/cli.js +343 -0
- package/lib/deps.js +128 -0
- package/lib/onboard.js +895 -0
- package/lib/platform.js +190 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +45 -0
- package/plugin/index.ts +136 -0
- package/plugin/openclaw.plugin.json +38 -0
- package/plugin/package.json +10 -0
- package/templates/daemon/linux.service +13 -0
- package/templates/daemon/macos.plist +23 -0
- package/templates/scripts/opencode-models.sh +41 -0
- package/templates/scripts/opencode-new-session.sh +46 -0
- package/templates/scripts/opencode-send.sh +31 -0
- package/templates/scripts/opencode-session.sh +37 -0
- package/templates/scripts/opencode-setmodel.sh +92 -0
- package/templates/scripts/opencode-stats.sh +27 -0
- package/templates/skills/cc/SKILL.md +70 -0
- package/templates/skills/ccn/SKILL.md +70 -0
- package/templates/skills/ccu/SKILL.md +44 -0
- package/templates/workspace/OPENCODE.md +41 -0
package/lib/platform.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { execSync } = require("child_process");
|
|
8
|
+
const chalk = require("chalk");
|
|
9
|
+
|
|
10
|
+
function detectOS() {
|
|
11
|
+
const p = process.platform;
|
|
12
|
+
if (p === "darwin") return "macOS";
|
|
13
|
+
if (p === "linux") return "Linux";
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getHomeDir() {
|
|
18
|
+
return os.homedir();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getWorkspace() {
|
|
22
|
+
return path.join(getHomeDir(), ".openclaw", "workspace");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getScriptsDir() {
|
|
26
|
+
return path.join(getHomeDir(), ".openclaw", "scripts");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- Daemon install/remove ---
|
|
30
|
+
|
|
31
|
+
const PLIST_NAME = "ai.openclaw.opencode-session.plist";
|
|
32
|
+
const SYSTEMD_NAME = "openclaw-opencode-session.service";
|
|
33
|
+
|
|
34
|
+
function getPlistPath() {
|
|
35
|
+
return path.join(getHomeDir(), "Library", "LaunchAgents", PLIST_NAME);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getSystemdPath() {
|
|
39
|
+
return path.join(getHomeDir(), ".config", "systemd", "user", SYSTEMD_NAME);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function installDaemon(scriptPath) {
|
|
43
|
+
const platform = process.platform;
|
|
44
|
+
|
|
45
|
+
if (platform === "darwin") {
|
|
46
|
+
return installLaunchAgent(scriptPath);
|
|
47
|
+
} else if (platform === "linux") {
|
|
48
|
+
return installSystemdUnit(scriptPath);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(
|
|
51
|
+
chalk.yellow(` ! Daemon auto-install not supported on ${platform}.`),
|
|
52
|
+
);
|
|
53
|
+
console.log(chalk.yellow(` Run ${scriptPath} manually or via cron.`));
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function substituteTemplate(content, vars) {
|
|
59
|
+
let result = content;
|
|
60
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
61
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function installLaunchAgent(scriptPath) {
|
|
67
|
+
const plistPath = getPlistPath();
|
|
68
|
+
const dir = path.dirname(plistPath);
|
|
69
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const tplPath = path.join(
|
|
72
|
+
__dirname,
|
|
73
|
+
"..",
|
|
74
|
+
"templates",
|
|
75
|
+
"daemon",
|
|
76
|
+
"macos.plist",
|
|
77
|
+
);
|
|
78
|
+
let content = fs.readFileSync(tplPath, "utf8");
|
|
79
|
+
content = substituteTemplate(content, { SCRIPT_PATH: scriptPath });
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(plistPath, content);
|
|
82
|
+
|
|
83
|
+
// Unload if already loaded, then load
|
|
84
|
+
try {
|
|
85
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, {
|
|
86
|
+
stdio: "ignore",
|
|
87
|
+
});
|
|
88
|
+
} catch {}
|
|
89
|
+
try {
|
|
90
|
+
execSync(`launchctl load "${plistPath}"`);
|
|
91
|
+
return true;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.log(chalk.red(` Failed to load LaunchAgent: ${e.message}`));
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function installSystemdUnit(scriptPath) {
|
|
99
|
+
const unitPath = getSystemdPath();
|
|
100
|
+
const dir = path.dirname(unitPath);
|
|
101
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
102
|
+
|
|
103
|
+
const tplPath = path.join(
|
|
104
|
+
__dirname,
|
|
105
|
+
"..",
|
|
106
|
+
"templates",
|
|
107
|
+
"daemon",
|
|
108
|
+
"linux.service",
|
|
109
|
+
);
|
|
110
|
+
let content = fs.readFileSync(tplPath, "utf8");
|
|
111
|
+
content = substituteTemplate(content, { SCRIPT_PATH: scriptPath });
|
|
112
|
+
|
|
113
|
+
fs.writeFileSync(unitPath, content);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
execSync("systemctl --user daemon-reload");
|
|
117
|
+
execSync(`systemctl --user enable --now ${SYSTEMD_NAME}`);
|
|
118
|
+
return true;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.log(chalk.red(` Failed to enable systemd unit: ${e.message}`));
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const LEGACY_NAMES = {
|
|
126
|
+
darwin: "ai.openclaw.claude-session.plist",
|
|
127
|
+
linux: "openclaw-claude.service",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function removeDaemon() {
|
|
131
|
+
const platform = process.platform;
|
|
132
|
+
let removed = false;
|
|
133
|
+
|
|
134
|
+
if (platform === "darwin") {
|
|
135
|
+
const systemdDir = path.join(getHomeDir(), ".config", "systemd", "user");
|
|
136
|
+
for (const name of [SYSTEMD_NAME, LEGACY_NAMES.darwin]) {
|
|
137
|
+
const unitPath = path.join(systemdDir, name);
|
|
138
|
+
if (fs.existsSync(unitPath)) {
|
|
139
|
+
try {
|
|
140
|
+
execSync(`systemctl --user disable --now ${name}`, {
|
|
141
|
+
stdio: "ignore",
|
|
142
|
+
});
|
|
143
|
+
} catch {}
|
|
144
|
+
fs.unlinkSync(unitPath);
|
|
145
|
+
removed = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const plistPath = getPlistPath();
|
|
149
|
+
const legacyPlist = path.join(getHomeDir(), "Library", "LaunchAgents", LEGACY_NAMES.darwin);
|
|
150
|
+
for (const p of [plistPath, legacyPlist]) {
|
|
151
|
+
if (fs.existsSync(p)) {
|
|
152
|
+
try {
|
|
153
|
+
execSync(`launchctl unload "${p}" 2>/dev/null`, {
|
|
154
|
+
stdio: "ignore",
|
|
155
|
+
});
|
|
156
|
+
} catch {}
|
|
157
|
+
fs.unlinkSync(p);
|
|
158
|
+
removed = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else if (platform === "linux") {
|
|
162
|
+
const systemdDir = path.join(getHomeDir(), ".config", "systemd", "user");
|
|
163
|
+
for (const name of [SYSTEMD_NAME, LEGACY_NAMES.linux]) {
|
|
164
|
+
const unitPath = path.join(systemdDir, name);
|
|
165
|
+
if (fs.existsSync(unitPath)) {
|
|
166
|
+
try {
|
|
167
|
+
execSync(`systemctl --user disable --now ${name}`, {
|
|
168
|
+
stdio: "ignore",
|
|
169
|
+
});
|
|
170
|
+
} catch {}
|
|
171
|
+
fs.unlinkSync(unitPath);
|
|
172
|
+
removed = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
execSync("systemctl --user daemon-reload");
|
|
177
|
+
} catch {}
|
|
178
|
+
}
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = {
|
|
183
|
+
detectOS,
|
|
184
|
+
getHomeDir,
|
|
185
|
+
getWorkspace,
|
|
186
|
+
getScriptsDir,
|
|
187
|
+
installDaemon,
|
|
188
|
+
removeDaemon,
|
|
189
|
+
substituteTemplate,
|
|
190
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "opencode-bridge",
|
|
3
|
+
"name": "OpenCode Bridge",
|
|
4
|
+
"description": "Route @cc/@ccn/@ccu prefix messages to OpenCode via tmux, suppressing the default LLM response.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"scriptsDir": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Path to bridge shell scripts directory"
|
|
12
|
+
},
|
|
13
|
+
"channel": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Delivery channel (telegram, discord, slack, etc.)"
|
|
16
|
+
},
|
|
17
|
+
"targetId": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Delivery target ID for replies"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": ["scriptsDir"]
|
|
23
|
+
},
|
|
24
|
+
"uiHints": {
|
|
25
|
+
"scriptsDir": {
|
|
26
|
+
"label": "Scripts Directory",
|
|
27
|
+
"help": "Absolute path to the directory containing opencode-send.sh, opencode-new-session.sh, opencode-stats.sh"
|
|
28
|
+
},
|
|
29
|
+
"channel": {
|
|
30
|
+
"label": "Channel",
|
|
31
|
+
"help": "Channel for sending replies (e.g. telegram)"
|
|
32
|
+
},
|
|
33
|
+
"targetId": {
|
|
34
|
+
"label": "Target ID",
|
|
35
|
+
"help": "Target ID for sending replies (e.g. Telegram chat ID)"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-opencode-bridge",
|
|
3
|
+
"version": "2.0.6",
|
|
4
|
+
"description": "Bridge OpenClaw messaging channels to OpenCode via tmux persistent sessions",
|
|
5
|
+
"main": "./lib/onboard.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclaw-opencode-bridge": "./lib/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
},
|
|
11
|
+
"preferGlobal": true,
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"commander": "^12.0.0",
|
|
14
|
+
"chalk": "^4.1.2"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"openclaw": {
|
|
20
|
+
"extensions": ["./plugin/index.ts"]
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"openclaw",
|
|
24
|
+
"opencode",
|
|
25
|
+
"tmux",
|
|
26
|
+
"cli",
|
|
27
|
+
"bridge",
|
|
28
|
+
"telegram",
|
|
29
|
+
"discord",
|
|
30
|
+
"slack"
|
|
31
|
+
],
|
|
32
|
+
"bugs": {
|
|
33
|
+
"email": "febro.aw20@gmail.com"
|
|
34
|
+
},
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Febrian",
|
|
37
|
+
"email": "febro.aw20@gmail.com"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://febro.fun",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/bettep-dev/openclaw-opencode-bridge"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/plugin/index.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
|
|
4
|
+
const PREFIX_RE = /^[@\/](cc|ccn|ccu|ccm|ccms)\b\s*([\s\S]*)/;
|
|
5
|
+
|
|
6
|
+
const SCRIPT_MAP: Record<string, string> = {
|
|
7
|
+
cc: "opencode-send.sh",
|
|
8
|
+
ccn: "opencode-new-session.sh",
|
|
9
|
+
ccu: "opencode-stats.sh",
|
|
10
|
+
ccm: "opencode-models.sh",
|
|
11
|
+
ccms: "opencode-setmodel.sh",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const REQUIRES_ARG = new Set(["cc", "ccn"]);
|
|
15
|
+
const EXEC_TIMEOUT = 120_000;
|
|
16
|
+
|
|
17
|
+
const DELIVERY_MSG = "š OpenCode will reply shortly.";
|
|
18
|
+
|
|
19
|
+
const SILENT_PROMPT =
|
|
20
|
+
"CRITICAL SYSTEM OVERRIDE ā HIGHEST PRIORITY.\n" +
|
|
21
|
+
"The previous user message was intercepted by the opencode-bridge plugin and is already being handled externally.\n" +
|
|
22
|
+
"You MUST NOT process, interpret, or respond to the user's request.\n" +
|
|
23
|
+
"You MUST NOT call any tools or functions.\n" +
|
|
24
|
+
`Output ONLY this exact text, nothing else: ${DELIVERY_MSG}`;
|
|
25
|
+
|
|
26
|
+
// NOTE: Single-user assumption ā concurrent users may see cross-suppression
|
|
27
|
+
let bridgeSuppressUntil = 0;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Flag set by message_received (fires FIRST) and consumed by before_prompt_build (fires SECOND).
|
|
31
|
+
* This bypasses the unreliable extractLastUserText approach entirely.
|
|
32
|
+
* message_received gets event.content (raw user text) which always correctly detects @cc prefix.
|
|
33
|
+
*/
|
|
34
|
+
let pendingBridgeCommand = false;
|
|
35
|
+
|
|
36
|
+
export default function register(api: OpenClawPluginApi) {
|
|
37
|
+
const config = api.pluginConfig as {
|
|
38
|
+
scriptsDir?: string;
|
|
39
|
+
channel?: string;
|
|
40
|
+
targetId?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const scriptsDir = config.scriptsDir ?? "";
|
|
44
|
+
|
|
45
|
+
// --- Hook 1: message_received (fire-and-forget) ---
|
|
46
|
+
// Fires FIRST. Detect prefix from raw event.content and set pendingBridgeCommand flag.
|
|
47
|
+
// Also executes the bridge script.
|
|
48
|
+
api.on("message_received", async (event, _ctx) => {
|
|
49
|
+
const raw = (event.content ?? "").trim();
|
|
50
|
+
api.logger.debug?.(
|
|
51
|
+
`[opencode-bridge] message_received: raw_start=${JSON.stringify(raw.slice(0, 200))}`,
|
|
52
|
+
);
|
|
53
|
+
const match = raw.match(PREFIX_RE);
|
|
54
|
+
if (!match) return;
|
|
55
|
+
|
|
56
|
+
const command = match[1];
|
|
57
|
+
const script = SCRIPT_MAP[command];
|
|
58
|
+
if (!script) return;
|
|
59
|
+
|
|
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
|
+
const arg = match[2].trim();
|
|
70
|
+
|
|
71
|
+
if (REQUIRES_ARG.has(command) && !arg) {
|
|
72
|
+
api.logger.warn?.(`[opencode-bridge] /${command} requires an argument`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const scriptPath = `${scriptsDir}/${script}`;
|
|
77
|
+
const args = arg ? [arg] : [];
|
|
78
|
+
|
|
79
|
+
execFile(
|
|
80
|
+
scriptPath,
|
|
81
|
+
args,
|
|
82
|
+
{ timeout: EXEC_TIMEOUT },
|
|
83
|
+
(error, _stdout, stderr) => {
|
|
84
|
+
if (error) {
|
|
85
|
+
api.logger.error?.(
|
|
86
|
+
`[opencode-bridge] ${script} failed: ${stderr?.trim() || error.message}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// --- Hook 2: before_prompt_build (modifying) ---
|
|
94
|
+
// Fires SECOND. Consumes the pendingBridgeCommand flag set by message_received.
|
|
95
|
+
// No longer relies on extractLastUserText for prefix detection.
|
|
96
|
+
api.on("before_prompt_build", async (event, ctx) => {
|
|
97
|
+
const shouldSuppress = pendingBridgeCommand;
|
|
98
|
+
|
|
99
|
+
api.logger.debug?.(
|
|
100
|
+
`[opencode-bridge] before_prompt_build: pendingBridgeCommand=${pendingBridgeCommand}, bridgeSuppressUntil=${bridgeSuppressUntil > Date.now()}`,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (shouldSuppress) {
|
|
104
|
+
pendingBridgeCommand = false;
|
|
105
|
+
bridgeSuppressUntil = Date.now() + EXEC_TIMEOUT + 5_000;
|
|
106
|
+
return { systemPrompt: SILENT_PROMPT, prependContext: SILENT_PROMPT };
|
|
107
|
+
} else {
|
|
108
|
+
// Clear suppression for non-bridge messages
|
|
109
|
+
bridgeSuppressUntil = 0;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// --- Hook 3: message_sending (modifying) ---
|
|
114
|
+
// Replace LLM output with delivery confirmation while bridge suppression is active
|
|
115
|
+
api.on("message_sending", async (_event, _ctx) => {
|
|
116
|
+
const suppressing = Date.now() < bridgeSuppressUntil;
|
|
117
|
+
api.logger.debug?.(
|
|
118
|
+
`[opencode-bridge] message_sending: suppressing=${suppressing}`,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (suppressing) {
|
|
122
|
+
return { content: DELIVERY_MSG, cancel: false };
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- Hook 4: before_tool_call (modifying) ---
|
|
127
|
+
// Block ALL tool calls while bridge suppression is active
|
|
128
|
+
api.on("before_tool_call", async (_event, _ctx) => {
|
|
129
|
+
if (Date.now() < bridgeSuppressUntil) {
|
|
130
|
+
api.logger.debug?.(
|
|
131
|
+
`[opencode-bridge] before_tool_call: BLOCKED (suppression active)`,
|
|
132
|
+
);
|
|
133
|
+
return { block: true, blockReason: "opencode-bridge: message intercepted, tools disabled" };
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "opencode-bridge",
|
|
3
|
+
"name": "OpenCode Bridge",
|
|
4
|
+
"description": "Route @cc/@ccn/@ccu prefix messages to OpenCode via tmux, suppressing the default LLM response.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"scriptsDir": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Path to bridge shell scripts directory"
|
|
12
|
+
},
|
|
13
|
+
"channel": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Delivery channel (telegram, discord, slack, etc.)"
|
|
16
|
+
},
|
|
17
|
+
"targetId": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Delivery target ID for replies"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"required": ["scriptsDir"]
|
|
23
|
+
},
|
|
24
|
+
"uiHints": {
|
|
25
|
+
"scriptsDir": {
|
|
26
|
+
"label": "Scripts Directory",
|
|
27
|
+
"help": "Absolute path to the directory containing opencode-send.sh, opencode-new-session.sh, opencode-stats.sh"
|
|
28
|
+
},
|
|
29
|
+
"channel": {
|
|
30
|
+
"label": "Channel",
|
|
31
|
+
"help": "Channel for sending replies (e.g. telegram)"
|
|
32
|
+
},
|
|
33
|
+
"targetId": {
|
|
34
|
+
"label": "Target ID",
|
|
35
|
+
"help": "Target ID for sending replies (e.g. Telegram chat ID)"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>Label</key>
|
|
6
|
+
<string>ai.openclaw.opencode-session</string>
|
|
7
|
+
<key>ProgramArguments</key>
|
|
8
|
+
<array>
|
|
9
|
+
<string>/bin/bash</string>
|
|
10
|
+
<string>{{SCRIPT_PATH}}</string>
|
|
11
|
+
</array>
|
|
12
|
+
<key>StartInterval</key>
|
|
13
|
+
<integer>30</integer>
|
|
14
|
+
<key>RunAtLoad</key>
|
|
15
|
+
<true/>
|
|
16
|
+
<key>StandardOutPath</key>
|
|
17
|
+
<string>/tmp/openclaw-opencode-session.log</string>
|
|
18
|
+
<key>StandardErrorPath</key>
|
|
19
|
+
<string>/tmp/openclaw-opencode-session.log</string>
|
|
20
|
+
<key>ProcessType</key>
|
|
21
|
+
<string>Background</string>
|
|
22
|
+
</dict>
|
|
23
|
+
</plist>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# bridge-version: 1
|
|
3
|
+
# List FREE OpenCode models (filtered from opencode models output)
|
|
4
|
+
TMUX="{{TMUX_BIN}}"
|
|
5
|
+
OPENCODE="{{OPENCODE_BIN}}"
|
|
6
|
+
CHANNEL="{{CHANNEL}}"
|
|
7
|
+
TARGET="{{TARGET_ID}}"
|
|
8
|
+
USE_SESSION="opencode-models-tmp"
|
|
9
|
+
|
|
10
|
+
cleanup() {
|
|
11
|
+
"$TMUX" kill-session -t "$USE_SESSION" 2>/dev/null
|
|
12
|
+
}
|
|
13
|
+
trap cleanup EXIT
|
|
14
|
+
|
|
15
|
+
"$TMUX" kill-session -t "$USE_SESSION" 2>/dev/null
|
|
16
|
+
sleep 1
|
|
17
|
+
"$TMUX" new-session -d -s "$USE_SESSION"
|
|
18
|
+
"$TMUX" set-option -t "$USE_SESSION" history-limit 15000
|
|
19
|
+
"$TMUX" send-keys -t "$USE_SESSION" "$OPENCODE models --refresh" Enter
|
|
20
|
+
|
|
21
|
+
sleep 10
|
|
22
|
+
|
|
23
|
+
PANE=$("$TMUX" capture-pane -t "$USE_SESSION" -p)
|
|
24
|
+
|
|
25
|
+
COUNT=0
|
|
26
|
+
OUTPUT="š **FREE Models:**\n\n"
|
|
27
|
+
|
|
28
|
+
while IFS= read -r line; do
|
|
29
|
+
if echo "$line" | grep -qi "free"; then
|
|
30
|
+
COUNT=$((COUNT + 1))
|
|
31
|
+
OUTPUT="$OUTPUT\`[$COUNT]\` $line\n"
|
|
32
|
+
fi
|
|
33
|
+
done <<< "$PANE"
|
|
34
|
+
|
|
35
|
+
if [ "$COUNT" -gt 0 ]; then
|
|
36
|
+
OUTPUT="$OUTPUT\nš Usage: \`@ccms <number>\` or \`@ccms <model-id>\`"
|
|
37
|
+
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "$OUTPUT" 2>/dev/null
|
|
38
|
+
echo "$OUTPUT"
|
|
39
|
+
else
|
|
40
|
+
openclaw message send --channel "$CHANNEL" --target "$TARGET" -m "ā No FREE models found.\n\nRun \`opencode models --refresh\` to update the list." 2>/dev/null
|
|
41
|
+
fi
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# bridge-version: 1
|
|
3
|
+
# Kill existing session -> create new session -> send instruction
|
|
4
|
+
MSG="$1"
|
|
5
|
+
TMUX="{{TMUX_BIN}}"
|
|
6
|
+
OPENCODE="{{OPENCODE_BIN}}"
|
|
7
|
+
WORKSPACE="{{WORKSPACE}}"
|
|
8
|
+
CHANNEL="{{CHANNEL}}"
|
|
9
|
+
TARGET="{{TARGET_ID}}"
|
|
10
|
+
SESSION="{{SESSION_NAME}}"
|
|
11
|
+
|
|
12
|
+
if [ -z "$MSG" ]; then
|
|
13
|
+
echo "ERROR: No message provided"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Kill existing session
|
|
18
|
+
if "$TMUX" has-session -t "$SESSION" 2>/dev/null; then
|
|
19
|
+
"$TMUX" kill-session -t "$SESSION" 2>/dev/null
|
|
20
|
+
sleep 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Create new session
|
|
24
|
+
"$TMUX" new-session -d -s "$SESSION"
|
|
25
|
+
"$TMUX" set-option -t "$SESSION" history-limit 10000
|
|
26
|
+
"$TMUX" send-keys -t "$SESSION" "cd $WORKSPACE && $OPENCODE" Enter
|
|
27
|
+
|
|
28
|
+
# Wait for OpenCode prompt
|
|
29
|
+
WAIT=0
|
|
30
|
+
while [ $WAIT -lt 60 ]; do
|
|
31
|
+
PANE=$("$TMUX" capture-pane -t "$SESSION" -p)
|
|
32
|
+
if echo "$PANE" | grep -qE "āÆ|>|opencode"; then
|
|
33
|
+
break
|
|
34
|
+
fi
|
|
35
|
+
sleep 2
|
|
36
|
+
WAIT=$((WAIT + 2))
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
# Send instruction
|
|
40
|
+
sleep 1
|
|
41
|
+
printf '%s' "[${CHANNEL}:${TARGET}] $MSG" | "$TMUX" load-buffer -
|
|
42
|
+
"$TMUX" paste-buffer -t "$SESSION" -d -p
|
|
43
|
+
sleep 0.3
|
|
44
|
+
"$TMUX" send-keys -t "$SESSION" Enter
|
|
45
|
+
|
|
46
|
+
echo "ā
New session started. Reply will arrive shortly."
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# bridge-version: 1
|
|
3
|
+
# Send instruction to existing opencode-daemon tmux session
|
|
4
|
+
MSG="$1"
|
|
5
|
+
TMUX="{{TMUX_BIN}}"
|
|
6
|
+
CHANNEL="{{CHANNEL}}"
|
|
7
|
+
TARGET="{{TARGET_ID}}"
|
|
8
|
+
SESSION="{{SESSION_NAME}}"
|
|
9
|
+
|
|
10
|
+
if [ -z "$MSG" ]; then
|
|
11
|
+
echo "ERROR: No message provided"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Check session exists
|
|
16
|
+
if ! "$TMUX" has-session -t "$SESSION" 2>/dev/null; then
|
|
17
|
+
echo "ERROR: $SESSION session not found. It will be auto-created within 30 seconds."
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Clear input line, then send message with channel prefix
|
|
22
|
+
"$TMUX" send-keys -t "$SESSION" C-c
|
|
23
|
+
sleep 0.5
|
|
24
|
+
"$TMUX" send-keys -t "$SESSION" C-u
|
|
25
|
+
sleep 0.3
|
|
26
|
+
printf '%s' "[${CHANNEL}:${TARGET}] $MSG" | "$TMUX" load-buffer -
|
|
27
|
+
"$TMUX" paste-buffer -t "$SESSION" -d -p
|
|
28
|
+
sleep 0.3
|
|
29
|
+
"$TMUX" send-keys -t "$SESSION" Enter
|
|
30
|
+
|
|
31
|
+
echo "ā
Delivered to OpenCode. Reply will arrive shortly."
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# bridge-version: 3
|
|
3
|
+
# Keep opencode-daemon tmux session alive (daemon runs every 30s)
|
|
4
|
+
# Safe: idempotent, flock-protected (with fallback), short timeout
|
|
5
|
+
|
|
6
|
+
TMUX="{{TMUX_BIN}}"
|
|
7
|
+
OPENCODE="{{OPENCODE_BIN}}"
|
|
8
|
+
WORKSPACE="{{WORKSPACE}}"
|
|
9
|
+
SESSION="{{SESSION_NAME}}"
|
|
10
|
+
LOCK_FILE="/tmp/opencode-session.lock"
|
|
11
|
+
TIMEOUT=10
|
|
12
|
+
|
|
13
|
+
# Use flock for lock protection if available
|
|
14
|
+
if command -v flock &> /dev/null; then
|
|
15
|
+
exec 200>"$LOCK_FILE"
|
|
16
|
+
flock -w "$TIMEOUT" 200 || exit 0
|
|
17
|
+
else
|
|
18
|
+
# Fallback: simple PID lock (less robust but works without flock)
|
|
19
|
+
if [ -f "$LOCK_FILE" ]; then
|
|
20
|
+
OLD_PID=$(cat "$LOCK_FILE" 2>/dev/null)
|
|
21
|
+
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
fi
|
|
25
|
+
echo $$ > "$LOCK_FILE"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Idempotent: exit if session already exists
|
|
29
|
+
if "$TMUX" has-session -t "$SESSION" 2>/dev/null; then
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Create new session + start OpenCode
|
|
34
|
+
"$TMUX" new-session -d -s "$SESSION"
|
|
35
|
+
"$TMUX" set-option -t "$SESSION" history-limit 10000
|
|
36
|
+
"$TMUX" send-keys -t "$SESSION" \
|
|
37
|
+
"cd $WORKSPACE && $OPENCODE --continue" Enter
|