opentmux 1.3.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 +131 -0
- package/dist/bin/opentmux.js +418 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +841 -0
- package/dist/scripts/install.js +138 -0
- package/dist/scripts/update-plugins.js +139 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# opentmux
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/opentmux)
|
|
4
|
+
[](https://www.npmjs.com/package/opentmux)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
An [OpenCode](https://opencode.ai) plugin that provides **smart tmux integration** for viewing agent execution in real-time. Automatically spawns panes, streams output, and manages your terminal workspace.
|
|
8
|
+
|
|
9
|
+
- **Agent-Agnostic**: Works with oh-my-opencode, omoc-slim, or vanilla OpenCode.
|
|
10
|
+
- **Cross-Platform**: Full support for **macOS**, **Linux**, and **Windows** (via PowerShell or WSL).
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
*Note: The demo shows the "sisyphus" agent from [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode), but this plugin supports ALL OpenCode subagents.*
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### π€ Let an LLM do it (Recommended)
|
|
18
|
+
|
|
19
|
+
Paste this into any LLM agent (OpenCode, Claude Code, Cursor, etc.):
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
Install the opentmux plugin by following: https://raw.githubusercontent.com/AnganSamadder/opentmux/main/README.md
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
<details>
|
|
26
|
+
<summary><b>Instructions for LLM Agents</b></summary>
|
|
27
|
+
|
|
28
|
+
To install this plugin, please perform the following steps for the user:
|
|
29
|
+
|
|
30
|
+
1. **Install the NPM package globally:**
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g opentmux
|
|
33
|
+
```
|
|
34
|
+
*Note: This installer automatically configures the user's shell (Bash/Zsh/Fish/PowerShell) to use the smart wrapper.*
|
|
35
|
+
|
|
36
|
+
2. **Configure OpenCode:**
|
|
37
|
+
Add `"opentmux"` to the `plugin` array in the user's config file (usually `~/.config/opencode/opencode.json`).
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"plugin": [
|
|
42
|
+
"opentmux"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. **Verify & Finalize:**
|
|
48
|
+
* Check if `tmux` is installed on the system. If not, advise the user to install it (e.g., `brew install tmux`, `sudo apt install tmux`, or `winget install tmux`).
|
|
49
|
+
* Tell the user: **"Installation complete! Please restart your terminal and run `opencode` to see the tmux integration in action."**
|
|
50
|
+
|
|
51
|
+
</details>
|
|
52
|
+
|
|
53
|
+
### π€ For Humans (Manual)
|
|
54
|
+
|
|
55
|
+
1. **Install via NPM:**
|
|
56
|
+
```bash
|
|
57
|
+
npm install -g opentmux
|
|
58
|
+
```
|
|
59
|
+
*The installer automatically configures your shell to use the smart wrapper.*
|
|
60
|
+
|
|
61
|
+
2. **Enable the Plugin:**
|
|
62
|
+
Add `"opentmux"` to your `~/.config/opencode/opencode.json`:
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"plugin": [
|
|
66
|
+
"opentmux"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
3. **Run OpenCode:**
|
|
72
|
+
Restart your terminal and type `opencode`. The plugin handles the rest!
|
|
73
|
+
|
|
74
|
+
## π οΈ Development
|
|
75
|
+
|
|
76
|
+
For contributors working on this plugin locally, see [LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md) for setup instructions.
|
|
77
|
+
|
|
78
|
+
## β¨ Features
|
|
79
|
+
|
|
80
|
+
- **Automatic Tmux Pane Spawning**: When any agent starts, automatically spawns a tmux pane
|
|
81
|
+
- **Live Streaming**: Each pane runs `opencode attach` to show real-time agent output
|
|
82
|
+
- **Auto-Cleanup**: Panes automatically close when agents complete
|
|
83
|
+
- **Configurable Layout**: Support multiple tmux layouts (`main-vertical`, `tiled`, etc.)
|
|
84
|
+
- **Multi-Port Support**: Automatically finds available ports (4096-4106) when running multiple instances
|
|
85
|
+
- **Smart Wrapper**: Automatically detects if you are in tmux; if not, launches a session for you.
|
|
86
|
+
|
|
87
|
+
## βοΈ Configuration
|
|
88
|
+
|
|
89
|
+
You can customize behavior by creating `~/.config/opencode/opentmux.json`:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"enabled": true,
|
|
94
|
+
"port": 4096,
|
|
95
|
+
"layout": "main-vertical",
|
|
96
|
+
"main_pane_size": 60,
|
|
97
|
+
"auto_close": true
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Option | Type | Default | Description |
|
|
102
|
+
|--------|------|---------|-------------|
|
|
103
|
+
| `enabled` | boolean | `true` | Enable/disable the plugin |
|
|
104
|
+
| `port` | number | `4096` | OpenCode server port |
|
|
105
|
+
| `layout` | string | `"main-vertical"` | Tmux layout: `main-horizontal`, `main-vertical`, `tiled`, etc. |
|
|
106
|
+
| `main_pane_size` | number | `60` | Size of main pane (20-80%) |
|
|
107
|
+
| `auto_close` | boolean | `true` | Auto-close panes when sessions complete |
|
|
108
|
+
|
|
109
|
+
## β Troubleshooting
|
|
110
|
+
|
|
111
|
+
### Panes Not Spawning
|
|
112
|
+
1. Verify you're inside tmux: `echo $TMUX`
|
|
113
|
+
2. Check tmux is installed: `which tmux` (or `where tmux` on Windows)
|
|
114
|
+
3. Check logs: `cat /tmp/opentmux.log`
|
|
115
|
+
|
|
116
|
+
### Server Not Found
|
|
117
|
+
Make sure OpenCode is started with the `--port` flag matching your config (the wrapper does this automatically).
|
|
118
|
+
|
|
119
|
+
## πΊοΈ Roadmap
|
|
120
|
+
|
|
121
|
+
The following features are planned for future releases:
|
|
122
|
+
- **Glow Integration**: Support for [Glow](https://github.com/charmbracelet/glow) to render markdown beautifully in spawned panes.
|
|
123
|
+
- **Neovim Quick-Launch**: Direct integration to launch Neovim at the agent's current working directory.
|
|
124
|
+
- **Enhanced Customization**: More options for pane positioning, colors, and persistent layouts.
|
|
125
|
+
|
|
126
|
+
## π License
|
|
127
|
+
|
|
128
|
+
MIT
|
|
129
|
+
|
|
130
|
+
## π Acknowledgements
|
|
131
|
+
This project extracts and improves upon the tmux session management from [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) by alvinunreal.
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/opentmux.ts
|
|
4
|
+
import { spawn, execSync } from "child_process";
|
|
5
|
+
import { createServer } from "net";
|
|
6
|
+
import { env, platform, exit, argv } from "process";
|
|
7
|
+
import { existsSync, appendFileSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
var OPENCODE_PORT_START = parseInt(env.OPENCODE_PORT || "4096", 10);
|
|
12
|
+
var OPENCODE_PORT_MAX = OPENCODE_PORT_START + 10;
|
|
13
|
+
var LOG_FILE = "/tmp/opentmux.log";
|
|
14
|
+
var HEALTH_TIMEOUT_MS = 1e3;
|
|
15
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
var __dirname = dirname(__filename);
|
|
17
|
+
function log(...args) {
|
|
18
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
19
|
+
const message = `[${timestamp}] ${args.join(" ")}
|
|
20
|
+
`;
|
|
21
|
+
try {
|
|
22
|
+
appendFileSync(LOG_FILE, message);
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function spawnPluginUpdater() {
|
|
27
|
+
if (env.OPENCODE_TMUX_DISABLE_UPDATES === "1") return;
|
|
28
|
+
const updaterPath = join(__dirname, "../scripts/update-plugins.js");
|
|
29
|
+
if (!existsSync(updaterPath)) return;
|
|
30
|
+
try {
|
|
31
|
+
const child = spawn(
|
|
32
|
+
process.execPath,
|
|
33
|
+
[updaterPath],
|
|
34
|
+
{
|
|
35
|
+
stdio: "ignore",
|
|
36
|
+
detached: true,
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
OPENCODE_TMUX_UPDATE: "1"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
child.unref();
|
|
44
|
+
} catch (error) {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function findOpencodeBin() {
|
|
48
|
+
try {
|
|
49
|
+
const cmd = platform === "win32" ? "where opencode" : "which -a opencode";
|
|
50
|
+
const output = execSync(cmd, { encoding: "utf-8" }).trim().split("\n");
|
|
51
|
+
const currentScript = argv[1];
|
|
52
|
+
for (const bin of output) {
|
|
53
|
+
const normalizedBin = bin.trim();
|
|
54
|
+
if (normalizedBin.includes("opentmux") || normalizedBin === currentScript) continue;
|
|
55
|
+
if (normalizedBin) return normalizedBin;
|
|
56
|
+
}
|
|
57
|
+
} catch (e) {
|
|
58
|
+
}
|
|
59
|
+
const commonPaths = [
|
|
60
|
+
join(homedir(), ".opencode", "bin", platform === "win32" ? "opencode.exe" : "opencode"),
|
|
61
|
+
join(homedir(), "AppData", "Local", "opencode", "bin", "opencode.exe"),
|
|
62
|
+
"/usr/local/bin/opencode",
|
|
63
|
+
"/usr/bin/opencode"
|
|
64
|
+
];
|
|
65
|
+
for (const p of commonPaths) {
|
|
66
|
+
if (existsSync(p)) return p;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function checkPort(port) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
const server = createServer();
|
|
73
|
+
server.listen(port, "127.0.0.1");
|
|
74
|
+
server.on("listening", () => {
|
|
75
|
+
server.close();
|
|
76
|
+
resolve(true);
|
|
77
|
+
});
|
|
78
|
+
server.on("error", () => {
|
|
79
|
+
resolve(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function isProcessAlive(pid) {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function safeExec(command) {
|
|
92
|
+
try {
|
|
93
|
+
const output = execSync(command, {
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
96
|
+
});
|
|
97
|
+
return output.trim();
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function getTmuxPanePids() {
|
|
103
|
+
if (!hasTmux()) return /* @__PURE__ */ new Set();
|
|
104
|
+
const output = safeExec("tmux list-panes -a -F '#{pane_pid}'");
|
|
105
|
+
if (!output) return /* @__PURE__ */ new Set();
|
|
106
|
+
const pids = output.split("\n").map((value) => Number.parseInt(value.trim(), 10)).filter((value) => Number.isFinite(value));
|
|
107
|
+
return new Set(pids);
|
|
108
|
+
}
|
|
109
|
+
async function isOpencodeHealthy(port) {
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeout = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
|
|
112
|
+
const healthUrl = `http://127.0.0.1:${port}/health`;
|
|
113
|
+
try {
|
|
114
|
+
const response = await fetch(healthUrl, { signal: controller.signal }).catch(
|
|
115
|
+
() => null
|
|
116
|
+
);
|
|
117
|
+
return response?.ok ?? false;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
} finally {
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function getListeningPids(port) {
|
|
125
|
+
if (platform === "win32") return [];
|
|
126
|
+
const output = safeExec(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
|
|
127
|
+
if (!output) return [];
|
|
128
|
+
return output.split("\n").map((value) => Number.parseInt(value.trim(), 10)).filter((value) => Number.isFinite(value));
|
|
129
|
+
}
|
|
130
|
+
function getProcessCommand(pid) {
|
|
131
|
+
const output = safeExec(`ps -p ${pid} -o command=`);
|
|
132
|
+
return output && output.length > 0 ? output : null;
|
|
133
|
+
}
|
|
134
|
+
function getProcessStat(pid) {
|
|
135
|
+
const output = safeExec(`ps -p ${pid} -o stat=`);
|
|
136
|
+
return output && output.length > 0 ? output.trim() : null;
|
|
137
|
+
}
|
|
138
|
+
function getProcessTty(pid) {
|
|
139
|
+
const output = safeExec(`ps -p ${pid} -o tty=`);
|
|
140
|
+
return output && output.length > 0 ? output.trim() : null;
|
|
141
|
+
}
|
|
142
|
+
function getTtyProcessIds(tty) {
|
|
143
|
+
const output = safeExec(`ps -t ${tty} -o pid=`);
|
|
144
|
+
if (!output) return [];
|
|
145
|
+
return output.split("\n").map((value) => Number.parseInt(value.trim(), 10)).filter((value) => Number.isFinite(value));
|
|
146
|
+
}
|
|
147
|
+
function hasOtherTtyProcesses(tty, pid) {
|
|
148
|
+
if (!tty || tty === "?" || tty === "??") return false;
|
|
149
|
+
const ttyPids = getTtyProcessIds(tty);
|
|
150
|
+
return ttyPids.some((ttyPid) => ttyPid !== pid);
|
|
151
|
+
}
|
|
152
|
+
function getParentPid(pid) {
|
|
153
|
+
const output = safeExec(`ps -p ${pid} -o ppid=`);
|
|
154
|
+
if (!output) return null;
|
|
155
|
+
const value = Number.parseInt(output.trim(), 10);
|
|
156
|
+
return Number.isFinite(value) ? value : null;
|
|
157
|
+
}
|
|
158
|
+
function isDescendantOf(pid, ancestors) {
|
|
159
|
+
let current = pid;
|
|
160
|
+
const visited = /* @__PURE__ */ new Set();
|
|
161
|
+
while (current > 1 && !visited.has(current)) {
|
|
162
|
+
if (ancestors.has(current)) return true;
|
|
163
|
+
visited.add(current);
|
|
164
|
+
const parent = getParentPid(current);
|
|
165
|
+
if (!parent || parent <= 1) return false;
|
|
166
|
+
current = parent;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
function isForegroundProcess(pid) {
|
|
171
|
+
const stat = safeExec(`ps -p ${pid} -o stat=`);
|
|
172
|
+
if (!stat) return false;
|
|
173
|
+
return stat.includes("+");
|
|
174
|
+
}
|
|
175
|
+
function killZombieClients() {
|
|
176
|
+
if (platform === "win32") return;
|
|
177
|
+
log("Scanning for zombie opencode clients...");
|
|
178
|
+
const output = safeExec("ps -A -o pid,ppid,command");
|
|
179
|
+
if (!output) return;
|
|
180
|
+
const lines = output.split("\n").slice(1);
|
|
181
|
+
const currentPid = process.pid;
|
|
182
|
+
const parentPid = process.ppid;
|
|
183
|
+
const wrapperCount = lines.filter((line) => {
|
|
184
|
+
const trimmed = line.trim();
|
|
185
|
+
if (!trimmed) return false;
|
|
186
|
+
const match = trimmed.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
187
|
+
if (!match) return false;
|
|
188
|
+
const command = match[3];
|
|
189
|
+
return (command.includes("opentmux.ts") || command.includes("bin/opentmux")) && !command.includes("ps ");
|
|
190
|
+
}).length;
|
|
191
|
+
if (wrapperCount > 1) {
|
|
192
|
+
log(`Active sessions detected (${wrapperCount} wrappers), skipping zombie cleanup.`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
const trimmed = line.trim();
|
|
197
|
+
if (!trimmed) continue;
|
|
198
|
+
const match = trimmed.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
199
|
+
if (!match) continue;
|
|
200
|
+
const pid = parseInt(match[1], 10);
|
|
201
|
+
const ppid = parseInt(match[2], 10);
|
|
202
|
+
const command = match[3];
|
|
203
|
+
if (pid === currentPid || pid === parentPid) continue;
|
|
204
|
+
if (command.includes("opencode") && command.includes("attach") && command.includes("--session")) {
|
|
205
|
+
if (command.includes("opencode-agent-tmux") || command.includes("opentmux")) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
log(`Found zombie client: PID ${pid}, PPID ${ppid}, CMD: ${command}`);
|
|
209
|
+
log(`Sending SIGKILL to zombie PID ${pid}`);
|
|
210
|
+
try {
|
|
211
|
+
process.kill(pid, "SIGKILL");
|
|
212
|
+
} catch (err) {
|
|
213
|
+
log(`Failed to kill PID ${pid}: ${err instanceof Error ? err.message : String(err)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function tryReclaimPort(port, tmuxPanePids) {
|
|
219
|
+
if (platform === "win32") return false;
|
|
220
|
+
const healthy = await isOpencodeHealthy(port);
|
|
221
|
+
if (healthy) return false;
|
|
222
|
+
const pids = getListeningPids(port);
|
|
223
|
+
log(
|
|
224
|
+
"Port scan:",
|
|
225
|
+
port.toString(),
|
|
226
|
+
"healthy",
|
|
227
|
+
String(healthy),
|
|
228
|
+
"pids",
|
|
229
|
+
pids.length > 0 ? pids.join(",") : "none"
|
|
230
|
+
);
|
|
231
|
+
if (pids.length === 0) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
let attemptedKill = false;
|
|
235
|
+
for (const pid of pids) {
|
|
236
|
+
const command = getProcessCommand(pid);
|
|
237
|
+
const tty = getProcessTty(pid);
|
|
238
|
+
const stat = getProcessStat(pid);
|
|
239
|
+
const hasTtyPeers = hasOtherTtyProcesses(tty, pid);
|
|
240
|
+
const inTmux = tmuxPanePids.size > 0 && isDescendantOf(pid, tmuxPanePids);
|
|
241
|
+
log(
|
|
242
|
+
"Port process:",
|
|
243
|
+
port.toString(),
|
|
244
|
+
"pid",
|
|
245
|
+
pid.toString(),
|
|
246
|
+
"tty",
|
|
247
|
+
tty ?? "unknown",
|
|
248
|
+
"stat",
|
|
249
|
+
stat ?? "unknown",
|
|
250
|
+
"tmux",
|
|
251
|
+
String(inTmux),
|
|
252
|
+
"ttyPeers",
|
|
253
|
+
String(hasTtyPeers),
|
|
254
|
+
"command",
|
|
255
|
+
command ?? "unknown"
|
|
256
|
+
);
|
|
257
|
+
if (command && command.includes("opencode")) {
|
|
258
|
+
if (inTmux) {
|
|
259
|
+
log("Port owned by tmux process, skipping:", port.toString(), pid.toString());
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (hasTtyPeers) {
|
|
263
|
+
log("Port owned by active tty process, skipping:", port.toString(), pid.toString());
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (isForegroundProcess(pid)) {
|
|
267
|
+
log("Port owned by potentially busy foreground process, skipping:", port.toString(), pid.toString());
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
log("Attempting to stop stale or non-opencode process:", port.toString(), pid.toString());
|
|
272
|
+
attemptedKill = true;
|
|
273
|
+
try {
|
|
274
|
+
process.kill(pid, "SIGTERM");
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!attemptedKill) return false;
|
|
279
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
280
|
+
for (const pid of pids) {
|
|
281
|
+
if (isProcessAlive(pid)) {
|
|
282
|
+
log("Process still alive, sending SIGKILL:", port.toString(), pid.toString());
|
|
283
|
+
try {
|
|
284
|
+
process.kill(pid, "SIGKILL");
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
290
|
+
return checkPort(port);
|
|
291
|
+
}
|
|
292
|
+
async function findAvailablePort() {
|
|
293
|
+
let tmuxPanePids = null;
|
|
294
|
+
for (let port = OPENCODE_PORT_START; port <= OPENCODE_PORT_MAX; port++) {
|
|
295
|
+
if (await checkPort(port)) return port;
|
|
296
|
+
if (!tmuxPanePids) {
|
|
297
|
+
tmuxPanePids = getTmuxPanePids();
|
|
298
|
+
}
|
|
299
|
+
const reclaimed = await tryReclaimPort(port, tmuxPanePids);
|
|
300
|
+
if (reclaimed && await checkPort(port)) return port;
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
function hasTmux() {
|
|
305
|
+
try {
|
|
306
|
+
execSync("tmux -V", { stdio: "ignore" });
|
|
307
|
+
return true;
|
|
308
|
+
} catch (e) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function main() {
|
|
313
|
+
const args = argv.slice(2);
|
|
314
|
+
const isCliCommand = args.length > 0 && (["auth", "config", "plugins", "update", "completion", "stats"].includes(args[0]) || ["--version", "-v", "--help", "-h"].includes(args[0]) || args.includes("--print-logs") || args.includes("--log-level"));
|
|
315
|
+
if (isCliCommand) {
|
|
316
|
+
const opencodeBin2 = findOpencodeBin();
|
|
317
|
+
if (!opencodeBin2) {
|
|
318
|
+
console.error(
|
|
319
|
+
'Error: Could not find "opencode" binary in PATH or common locations.'
|
|
320
|
+
);
|
|
321
|
+
exit(1);
|
|
322
|
+
}
|
|
323
|
+
const bypassArgs = [...args];
|
|
324
|
+
const hasPrintLogs = args.includes("--print-logs");
|
|
325
|
+
if (!hasPrintLogs && !args.some((arg) => arg.startsWith("--log-level"))) {
|
|
326
|
+
bypassArgs.push("--log-level", "ERROR");
|
|
327
|
+
}
|
|
328
|
+
const child = spawn(opencodeBin2, bypassArgs, {
|
|
329
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
330
|
+
env: process.env
|
|
331
|
+
});
|
|
332
|
+
child.stderr?.on("data", (data) => {
|
|
333
|
+
const lines = data.toString().split("\n");
|
|
334
|
+
const filtered = lines.filter(
|
|
335
|
+
(line) => !/^INFO\s+.*service=models\.dev.*refreshing/.test(line)
|
|
336
|
+
);
|
|
337
|
+
process.stderr.write(filtered.join("\n"));
|
|
338
|
+
});
|
|
339
|
+
child.on("close", (code) => {
|
|
340
|
+
exit(code ?? 0);
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
log("=== OpenCode Tmux Wrapper Started ===");
|
|
345
|
+
log("Process argv:", JSON.stringify(argv));
|
|
346
|
+
log("Current directory:", process.cwd());
|
|
347
|
+
const opencodeBin = findOpencodeBin();
|
|
348
|
+
log("Found opencode binary:", opencodeBin);
|
|
349
|
+
if (!opencodeBin) {
|
|
350
|
+
console.error('Error: Could not find "opencode" binary in PATH or common locations.');
|
|
351
|
+
log("ERROR: opencode binary not found");
|
|
352
|
+
exit(1);
|
|
353
|
+
}
|
|
354
|
+
spawnPluginUpdater();
|
|
355
|
+
killZombieClients();
|
|
356
|
+
const port = await findAvailablePort();
|
|
357
|
+
log("Found available port:", port);
|
|
358
|
+
if (!port) {
|
|
359
|
+
console.error("Error: No available ports found in range 4096-4106.");
|
|
360
|
+
log("ERROR: No available ports");
|
|
361
|
+
exit(1);
|
|
362
|
+
}
|
|
363
|
+
const env2 = { ...process.env };
|
|
364
|
+
env2.OPENCODE_PORT = port.toString();
|
|
365
|
+
log("User args:", JSON.stringify(args));
|
|
366
|
+
const childArgs = ["--port", port.toString(), ...args];
|
|
367
|
+
log("Final childArgs:", JSON.stringify(childArgs));
|
|
368
|
+
const inTmux = !!env2.TMUX;
|
|
369
|
+
const tmuxAvailable = hasTmux();
|
|
370
|
+
log("In tmux?", inTmux);
|
|
371
|
+
log("Tmux available?", tmuxAvailable);
|
|
372
|
+
if (inTmux || !tmuxAvailable) {
|
|
373
|
+
log("Running directly (in tmux or no tmux available)");
|
|
374
|
+
const child = spawn(opencodeBin, childArgs, { stdio: "inherit", env: env2 });
|
|
375
|
+
child.on("error", (err) => {
|
|
376
|
+
log("ERROR spawning child:", err.message);
|
|
377
|
+
});
|
|
378
|
+
child.on("close", (code) => {
|
|
379
|
+
log("Child exited with code:", code);
|
|
380
|
+
exit(code ?? 0);
|
|
381
|
+
});
|
|
382
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
383
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
384
|
+
} else {
|
|
385
|
+
console.log("\u{1F680} Launching tmux session...");
|
|
386
|
+
log("Launching tmux session");
|
|
387
|
+
const escapedBin = opencodeBin.includes(" ") ? `'${opencodeBin}'` : opencodeBin;
|
|
388
|
+
const escapedArgs = childArgs.map((arg) => {
|
|
389
|
+
if (arg.includes(" ") || arg.includes('"') || arg.includes("'")) {
|
|
390
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
391
|
+
}
|
|
392
|
+
return arg;
|
|
393
|
+
});
|
|
394
|
+
const shellCommand = `${escapedBin} ${escapedArgs.join(" ")}; EXIT_CODE=$?; if [ $EXIT_CODE -ne 0 ] && [ $EXIT_CODE -ne 130 ] && [ $EXIT_CODE -ne 133 ] && [ $EXIT_CODE -ne 143 ]; then echo "Exit code: $EXIT_CODE"; echo "Press Enter to close..."; read; fi`;
|
|
395
|
+
log("Shell command for tmux:", shellCommand);
|
|
396
|
+
const tmuxArgs = [
|
|
397
|
+
"new-session",
|
|
398
|
+
"-c",
|
|
399
|
+
process.cwd(),
|
|
400
|
+
// Use current working directory
|
|
401
|
+
shellCommand
|
|
402
|
+
];
|
|
403
|
+
log("Tmux args:", JSON.stringify(tmuxArgs));
|
|
404
|
+
const child = spawn("tmux", tmuxArgs, { stdio: "inherit", env: env2 });
|
|
405
|
+
child.on("error", (err) => {
|
|
406
|
+
log("ERROR spawning tmux:", err.message);
|
|
407
|
+
});
|
|
408
|
+
child.on("close", (code) => {
|
|
409
|
+
log("Tmux exited with code:", code);
|
|
410
|
+
exit(code ?? 0);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
main().catch((err) => {
|
|
415
|
+
log("FATAL ERROR:", err.message, err.stack);
|
|
416
|
+
console.error(err);
|
|
417
|
+
exit(1);
|
|
418
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
interface PluginInput {
|
|
4
|
+
directory: string;
|
|
5
|
+
serverUrl?: URL | string;
|
|
6
|
+
client: {
|
|
7
|
+
session: {
|
|
8
|
+
status(): Promise<{
|
|
9
|
+
data?: Record<string, {
|
|
10
|
+
type: string;
|
|
11
|
+
}>;
|
|
12
|
+
}>;
|
|
13
|
+
subscribe(callback: (event: {
|
|
14
|
+
type: string;
|
|
15
|
+
properties?: unknown;
|
|
16
|
+
}) => void): () => void;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
interface PluginOutput {
|
|
21
|
+
name: string;
|
|
22
|
+
event?: (input: {
|
|
23
|
+
event: {
|
|
24
|
+
type: string;
|
|
25
|
+
properties?: unknown;
|
|
26
|
+
};
|
|
27
|
+
}) => Promise<void>;
|
|
28
|
+
tool?: Record<string, unknown>;
|
|
29
|
+
config?: unknown;
|
|
30
|
+
}
|
|
31
|
+
type Plugin = (ctx: PluginInput) => Promise<PluginOutput>;
|
|
32
|
+
|
|
33
|
+
declare const TmuxLayoutSchema: z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical", "dynamic-vertical"]>;
|
|
34
|
+
type TmuxLayout = z.infer<typeof TmuxLayoutSchema>;
|
|
35
|
+
declare const TmuxConfigSchema: z.ZodObject<{
|
|
36
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
37
|
+
layout: z.ZodDefault<z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical", "dynamic-vertical"]>>;
|
|
38
|
+
main_pane_size: z.ZodDefault<z.ZodNumber>;
|
|
39
|
+
max_agents_per_column: z.ZodDefault<z.ZodNumber>;
|
|
40
|
+
}, "strip", z.ZodTypeAny, {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
layout: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | "dynamic-vertical";
|
|
43
|
+
main_pane_size: number;
|
|
44
|
+
max_agents_per_column: number;
|
|
45
|
+
}, {
|
|
46
|
+
enabled?: boolean | undefined;
|
|
47
|
+
layout?: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | "dynamic-vertical" | undefined;
|
|
48
|
+
main_pane_size?: number | undefined;
|
|
49
|
+
max_agents_per_column?: number | undefined;
|
|
50
|
+
}>;
|
|
51
|
+
type TmuxConfig = z.infer<typeof TmuxConfigSchema>;
|
|
52
|
+
declare const PluginConfigSchema: z.ZodObject<{
|
|
53
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
54
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
55
|
+
layout: z.ZodDefault<z.ZodEnum<["main-horizontal", "main-vertical", "tiled", "even-horizontal", "even-vertical", "dynamic-vertical"]>>;
|
|
56
|
+
main_pane_size: z.ZodDefault<z.ZodNumber>;
|
|
57
|
+
max_agents_per_column: z.ZodDefault<z.ZodNumber>;
|
|
58
|
+
auto_close: z.ZodDefault<z.ZodBoolean>;
|
|
59
|
+
}, "strip", z.ZodTypeAny, {
|
|
60
|
+
enabled: boolean;
|
|
61
|
+
port: number;
|
|
62
|
+
layout: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | "dynamic-vertical";
|
|
63
|
+
main_pane_size: number;
|
|
64
|
+
max_agents_per_column: number;
|
|
65
|
+
auto_close: boolean;
|
|
66
|
+
}, {
|
|
67
|
+
enabled?: boolean | undefined;
|
|
68
|
+
port?: number | undefined;
|
|
69
|
+
layout?: "main-horizontal" | "main-vertical" | "tiled" | "even-horizontal" | "even-vertical" | "dynamic-vertical" | undefined;
|
|
70
|
+
main_pane_size?: number | undefined;
|
|
71
|
+
max_agents_per_column?: number | undefined;
|
|
72
|
+
auto_close?: boolean | undefined;
|
|
73
|
+
}>;
|
|
74
|
+
type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
75
|
+
|
|
76
|
+
declare const OpencodeTmux: Plugin;
|
|
77
|
+
|
|
78
|
+
export { type PluginConfig, type TmuxConfig, type TmuxLayout, OpencodeTmux as default };
|