@towry/mcp 0.1.7 → 0.3.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/README.md +13 -126
- package/dist/checkpoints.d.ts +17 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +358 -0
- package/dist/checkpoints.js.map +1 -0
- package/dist/checkpoints.test.d.ts +2 -0
- package/dist/checkpoints.test.d.ts.map +1 -0
- package/dist/checkpoints.test.js +277 -0
- package/dist/checkpoints.test.js.map +1 -0
- package/dist/checkpoints.validation.test.d.ts +2 -0
- package/dist/checkpoints.validation.test.d.ts.map +1 -0
- package/dist/checkpoints.validation.test.js +28 -0
- package/dist/checkpoints.validation.test.js.map +1 -0
- package/dist/index.d.ts +3 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -557
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/tmux.d.ts +0 -128
- package/dist/tmux.d.ts.map +0 -1
- package/dist/tmux.js +0 -797
- package/dist/tmux.js.map +0 -1
package/dist/tmux.js
DELETED
|
@@ -1,797 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tmux CLI Controller - TypeScript port
|
|
3
|
-
*
|
|
4
|
-
* Provides tmux pane management without external Python dependency.
|
|
5
|
-
* Auto-detects whether running inside tmux or not.
|
|
6
|
-
*
|
|
7
|
-
* Keywords: tmux, pane-management, terminal-multiplexer
|
|
8
|
-
*/
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
|
-
import * as crypto from "node:crypto";
|
|
11
|
-
import * as fs from "node:fs";
|
|
12
|
-
import * as path from "node:path";
|
|
13
|
-
// Unit Separator - safe delimiter for tmux format strings (pane_title etc. may contain |)
|
|
14
|
-
const TMUX_SEP = "\x1f";
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// Helpers
|
|
17
|
-
// ============================================================================
|
|
18
|
-
/**
|
|
19
|
-
* Get the full command line of a pane's foreground process via its PID.
|
|
20
|
-
* Looks up the first child process of the shell, falls back to the given default.
|
|
21
|
-
*/
|
|
22
|
-
async function getFullCommand(panePid, fallback) {
|
|
23
|
-
if (!panePid)
|
|
24
|
-
return fallback;
|
|
25
|
-
try {
|
|
26
|
-
const proc = spawn("pgrep", ["-P", panePid], {
|
|
27
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
-
shell: false,
|
|
29
|
-
});
|
|
30
|
-
let stdout = "";
|
|
31
|
-
proc.stdout.on("data", (d) => (stdout += d.toString()));
|
|
32
|
-
await new Promise((resolve) => proc.on("close", () => resolve()));
|
|
33
|
-
const childPid = stdout.trim().split("\n")[0];
|
|
34
|
-
if (!childPid)
|
|
35
|
-
return fallback;
|
|
36
|
-
const ps = spawn("ps", ["-o", "command=", "-p", childPid], {
|
|
37
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
-
shell: false,
|
|
39
|
-
});
|
|
40
|
-
let cmd = "";
|
|
41
|
-
ps.stdout.on("data", (d) => (cmd += d.toString()));
|
|
42
|
-
await new Promise((resolve) => ps.on("close", () => resolve()));
|
|
43
|
-
const trimmed = cmd.trim();
|
|
44
|
-
if (!trimmed)
|
|
45
|
-
return fallback;
|
|
46
|
-
// Strip nix store prefix for readability: /nix/store/...-name/bin/foo -> foo
|
|
47
|
-
return trimmed.replace(/\/nix\/store\/[^/]+-[^/]+\/bin\//g, "");
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return fallback;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
// ============================================================================
|
|
54
|
-
// Tmux Command Runner
|
|
55
|
-
// ============================================================================
|
|
56
|
-
async function runTmux(args) {
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
const proc = spawn("tmux", args, {
|
|
59
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
-
shell: false,
|
|
61
|
-
});
|
|
62
|
-
let stdout = "";
|
|
63
|
-
proc.stdout.on("data", (d) => (stdout += d.toString()));
|
|
64
|
-
proc.on("close", (code) => resolve({ stdout: stdout.trim(), exitCode: code ?? 0 }));
|
|
65
|
-
proc.on("error", () => resolve({ stdout: "", exitCode: 1 }));
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Validate that a path is absolute and exists.
|
|
70
|
-
* Returns { valid: true } or { valid: false, error: string }
|
|
71
|
-
*/
|
|
72
|
-
function validateCwd(cwd) {
|
|
73
|
-
if (!cwd)
|
|
74
|
-
return { valid: true };
|
|
75
|
-
if (!path.isAbsolute(cwd)) {
|
|
76
|
-
return { valid: false, error: `cwd must be an absolute path, got: ${cwd}` };
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
const stat = fs.statSync(cwd);
|
|
80
|
-
if (!stat.isDirectory()) {
|
|
81
|
-
return { valid: false, error: `cwd is not a directory: ${cwd}` };
|
|
82
|
-
}
|
|
83
|
-
return { valid: true };
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return { valid: false, error: `cwd does not exist: ${cwd}` };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// ============================================================================
|
|
90
|
-
// TmuxCLIController - For use inside tmux
|
|
91
|
-
// ============================================================================
|
|
92
|
-
export class TmuxCLIController {
|
|
93
|
-
targetPane = null;
|
|
94
|
-
async getCurrentSession() {
|
|
95
|
-
const { stdout, exitCode } = await runTmux(["display-message", "-p", "#{session_name}"]);
|
|
96
|
-
return exitCode === 0 ? stdout : null;
|
|
97
|
-
}
|
|
98
|
-
async getCurrentWindow() {
|
|
99
|
-
const { stdout, exitCode } = await runTmux(["display-message", "-p", "#{window_name}"]);
|
|
100
|
-
return exitCode === 0 ? stdout : null;
|
|
101
|
-
}
|
|
102
|
-
async getCurrentPane() {
|
|
103
|
-
const { stdout, exitCode } = await runTmux(["display-message", "-p", "#{pane_id}"]);
|
|
104
|
-
return exitCode === 0 ? stdout : null;
|
|
105
|
-
}
|
|
106
|
-
async getCurrentPaneIndex() {
|
|
107
|
-
const { stdout, exitCode } = await runTmux(["display-message", "-p", "#{pane_index}"]);
|
|
108
|
-
return exitCode === 0 ? stdout : null;
|
|
109
|
-
}
|
|
110
|
-
async formatPaneIdentifier(paneId) {
|
|
111
|
-
if (!paneId)
|
|
112
|
-
return paneId;
|
|
113
|
-
try {
|
|
114
|
-
const [session, window, pane] = await Promise.all([
|
|
115
|
-
runTmux(["display-message", "-t", paneId, "-p", "#{session_name}"]),
|
|
116
|
-
runTmux(["display-message", "-t", paneId, "-p", "#{window_index}"]),
|
|
117
|
-
runTmux(["display-message", "-t", paneId, "-p", "#{pane_index}"]),
|
|
118
|
-
]);
|
|
119
|
-
if (session.exitCode === 0 &&
|
|
120
|
-
window.exitCode === 0 &&
|
|
121
|
-
pane.exitCode === 0 &&
|
|
122
|
-
session.stdout &&
|
|
123
|
-
window.stdout &&
|
|
124
|
-
pane.stdout) {
|
|
125
|
-
return `${session.stdout}:${window.stdout}.${pane.stdout}`;
|
|
126
|
-
}
|
|
127
|
-
return paneId;
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return paneId;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
async resolvePaneIdentifier(identifier) {
|
|
134
|
-
if (!identifier)
|
|
135
|
-
return null;
|
|
136
|
-
identifier = String(identifier);
|
|
137
|
-
// Already a pane ID
|
|
138
|
-
if (identifier.startsWith("%"))
|
|
139
|
-
return identifier;
|
|
140
|
-
// Numeric index
|
|
141
|
-
if (/^\d+$/.test(identifier)) {
|
|
142
|
-
const panes = await this.listPanes();
|
|
143
|
-
const pane = panes.find((p) => p.index === identifier);
|
|
144
|
-
return pane?.id ?? null;
|
|
145
|
-
}
|
|
146
|
-
// session:window.pane format
|
|
147
|
-
if (identifier.includes(":") && identifier.includes(".")) {
|
|
148
|
-
try {
|
|
149
|
-
const { stdout, exitCode } = await runTmux([
|
|
150
|
-
"display-message",
|
|
151
|
-
"-t",
|
|
152
|
-
identifier,
|
|
153
|
-
"-p",
|
|
154
|
-
"#{pane_id}",
|
|
155
|
-
]);
|
|
156
|
-
return exitCode === 0 ? stdout : null;
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
async getCurrentWindowId() {
|
|
165
|
-
const currentPane = process.env.TMUX_PANE;
|
|
166
|
-
if (currentPane) {
|
|
167
|
-
const { stdout, exitCode } = await runTmux([
|
|
168
|
-
"display-message",
|
|
169
|
-
"-t",
|
|
170
|
-
currentPane,
|
|
171
|
-
"-p",
|
|
172
|
-
"#{window_id}",
|
|
173
|
-
]);
|
|
174
|
-
return exitCode === 0 ? stdout : null;
|
|
175
|
-
}
|
|
176
|
-
const { stdout, exitCode } = await runTmux(["display-message", "-p", "#{window_id}"]);
|
|
177
|
-
return exitCode === 0 ? stdout : null;
|
|
178
|
-
}
|
|
179
|
-
async listPanes(session) {
|
|
180
|
-
// Use Unit Separator (\x1f) as delimiter to avoid conflicts with pane_title content
|
|
181
|
-
// Default to current session if no session specified
|
|
182
|
-
const targetSession = session || (await this.getCurrentSession());
|
|
183
|
-
if (!targetSession)
|
|
184
|
-
return [];
|
|
185
|
-
const format = `#{pane_id}${TMUX_SEP}#{pane_index}${TMUX_SEP}#{pane_title}${TMUX_SEP}#{pane_active}${TMUX_SEP}#{pane_width}x#{pane_height}${TMUX_SEP}#{pane_current_command}${TMUX_SEP}#{window_index}${TMUX_SEP}#{window_name}${TMUX_SEP}#{pane_pid}${TMUX_SEP}#{pane_current_path}`;
|
|
186
|
-
const { stdout, exitCode } = await runTmux([
|
|
187
|
-
"list-panes",
|
|
188
|
-
"-s",
|
|
189
|
-
"-t",
|
|
190
|
-
targetSession,
|
|
191
|
-
"-F",
|
|
192
|
-
format,
|
|
193
|
-
]);
|
|
194
|
-
if (exitCode !== 0)
|
|
195
|
-
return [];
|
|
196
|
-
const panes = [];
|
|
197
|
-
for (const line of stdout.split("\n")) {
|
|
198
|
-
if (!line)
|
|
199
|
-
continue;
|
|
200
|
-
const parts = line.split(TMUX_SEP);
|
|
201
|
-
const paneId = parts[0];
|
|
202
|
-
const shortCmd = parts[5] || "";
|
|
203
|
-
const panePid = parts[8] || "";
|
|
204
|
-
const cwd = parts[9] || "";
|
|
205
|
-
panes.push({
|
|
206
|
-
id: paneId,
|
|
207
|
-
index: parts[1],
|
|
208
|
-
title: parts[2],
|
|
209
|
-
active: parts[3] === "1",
|
|
210
|
-
size: parts[4],
|
|
211
|
-
command: await getFullCommand(panePid, shortCmd),
|
|
212
|
-
formatted_id: await this.formatPaneIdentifier(paneId),
|
|
213
|
-
cwd,
|
|
214
|
-
window_index: parts[6],
|
|
215
|
-
window_name: parts[7],
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
return panes;
|
|
219
|
-
}
|
|
220
|
-
async listSessions() {
|
|
221
|
-
const { stdout, exitCode } = await runTmux([
|
|
222
|
-
"list-sessions",
|
|
223
|
-
"-F",
|
|
224
|
-
`#{session_name}${TMUX_SEP}#{session_id}${TMUX_SEP}#{session_windows}${TMUX_SEP}#{session_attached}${TMUX_SEP}#{session_created}`,
|
|
225
|
-
]);
|
|
226
|
-
if (exitCode !== 0)
|
|
227
|
-
return [];
|
|
228
|
-
const current = await this.getCurrentSession();
|
|
229
|
-
const sessions = [];
|
|
230
|
-
for (const line of stdout.split("\n")) {
|
|
231
|
-
if (!line)
|
|
232
|
-
continue;
|
|
233
|
-
const parts = line.split(TMUX_SEP);
|
|
234
|
-
sessions.push({
|
|
235
|
-
name: parts[0],
|
|
236
|
-
id: parts[1],
|
|
237
|
-
windows: parts[2],
|
|
238
|
-
attached: parts[3] === "1",
|
|
239
|
-
created: parts[4],
|
|
240
|
-
current: parts[0] === current,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
return sessions;
|
|
244
|
-
}
|
|
245
|
-
async createSession(name, command, cwd) {
|
|
246
|
-
const cmd = ["new-session", "-s", name, "-d", "-P", "-F", "#{session_name}"];
|
|
247
|
-
if (cwd)
|
|
248
|
-
cmd.push("-c", cwd);
|
|
249
|
-
if (command)
|
|
250
|
-
cmd.push(command);
|
|
251
|
-
const { stdout, exitCode } = await runTmux(cmd);
|
|
252
|
-
return exitCode === 0 ? stdout : null;
|
|
253
|
-
}
|
|
254
|
-
async killSession(name) {
|
|
255
|
-
const current = await this.getCurrentSession();
|
|
256
|
-
if (current && name === current) {
|
|
257
|
-
throw new Error(`Cannot kill current session '${name}'`);
|
|
258
|
-
}
|
|
259
|
-
const { exitCode } = await runTmux(["kill-session", "-t", name]);
|
|
260
|
-
return exitCode === 0;
|
|
261
|
-
}
|
|
262
|
-
async createWindow(options = {}) {
|
|
263
|
-
const { name, command, session, cwd, detached = true } = options;
|
|
264
|
-
const targetSession = session || (await this.getCurrentSession());
|
|
265
|
-
const cmd = ["new-window", "-P", "-F", "#{pane_id}"];
|
|
266
|
-
if (detached)
|
|
267
|
-
cmd.push("-d");
|
|
268
|
-
if (targetSession)
|
|
269
|
-
cmd.push("-t", targetSession);
|
|
270
|
-
if (cwd)
|
|
271
|
-
cmd.push("-c", cwd);
|
|
272
|
-
if (name)
|
|
273
|
-
cmd.push("-n", name);
|
|
274
|
-
if (command)
|
|
275
|
-
cmd.push(command);
|
|
276
|
-
const { stdout, exitCode } = await runTmux(cmd);
|
|
277
|
-
return exitCode === 0 && stdout ? stdout : null;
|
|
278
|
-
}
|
|
279
|
-
async createPane(vertical = true, size, startCommand) {
|
|
280
|
-
const currentWindowId = await this.getCurrentWindowId();
|
|
281
|
-
const baseCmd = ["split-window"];
|
|
282
|
-
if (currentWindowId)
|
|
283
|
-
baseCmd.push("-t", currentWindowId);
|
|
284
|
-
baseCmd.push(vertical ? "-h" : "-v");
|
|
285
|
-
const sizeFlags = size ? [["-l", `${size}%`], ["-p", String(size)]] : [[]];
|
|
286
|
-
for (const sizeFlag of sizeFlags) {
|
|
287
|
-
const cmd = [...baseCmd];
|
|
288
|
-
if (sizeFlag.length)
|
|
289
|
-
cmd.push(...sizeFlag);
|
|
290
|
-
cmd.push("-P", "-F", "#{pane_id}");
|
|
291
|
-
if (startCommand)
|
|
292
|
-
cmd.push(startCommand);
|
|
293
|
-
const { stdout, exitCode } = await runTmux(cmd);
|
|
294
|
-
if (exitCode === 0 && stdout && stdout.startsWith("%")) {
|
|
295
|
-
this.targetPane = stdout;
|
|
296
|
-
return stdout;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
selectPane(paneId, paneIndex) {
|
|
302
|
-
if (paneId) {
|
|
303
|
-
this.targetPane = paneId;
|
|
304
|
-
}
|
|
305
|
-
else if (paneIndex !== undefined) {
|
|
306
|
-
// Will be resolved later
|
|
307
|
-
this.targetPane = String(paneIndex);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
async sendKeys(text, paneId, enter = true, delayEnter = true) {
|
|
311
|
-
const target = paneId || this.targetPane;
|
|
312
|
-
if (!target)
|
|
313
|
-
throw new Error("No target pane specified");
|
|
314
|
-
if (enter && delayEnter) {
|
|
315
|
-
await runTmux(["send-keys", "-l", "-t", target, text]);
|
|
316
|
-
const delay = typeof delayEnter === "number" ? delayEnter : 0.1;
|
|
317
|
-
await new Promise((r) => setTimeout(r, delay * 1000));
|
|
318
|
-
await runTmux(["send-keys", "-t", target, "Enter"]);
|
|
319
|
-
}
|
|
320
|
-
else {
|
|
321
|
-
const cmd = ["send-keys", "-l", "-t", target, text];
|
|
322
|
-
if (enter)
|
|
323
|
-
cmd.push("Enter");
|
|
324
|
-
await runTmux(cmd);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
async capturePane(paneId, lines) {
|
|
328
|
-
const target = paneId || this.targetPane;
|
|
329
|
-
if (!target)
|
|
330
|
-
throw new Error("No target pane specified");
|
|
331
|
-
const cmd = ["capture-pane", "-t", target, "-p"];
|
|
332
|
-
if (lines)
|
|
333
|
-
cmd.push("-S", `-${lines}`);
|
|
334
|
-
const { stdout } = await runTmux(cmd);
|
|
335
|
-
return stdout;
|
|
336
|
-
}
|
|
337
|
-
async waitForIdle(paneId, idleTime = 2.0, checkInterval = 0.5, timeout) {
|
|
338
|
-
const target = paneId || this.targetPane;
|
|
339
|
-
if (!target)
|
|
340
|
-
throw new Error("No target pane specified");
|
|
341
|
-
const startTime = Date.now();
|
|
342
|
-
let lastChangeTime = Date.now();
|
|
343
|
-
let lastHash = "";
|
|
344
|
-
while (true) {
|
|
345
|
-
if (timeout && Date.now() - startTime > timeout * 1000)
|
|
346
|
-
return false;
|
|
347
|
-
const content = await this.capturePane(target);
|
|
348
|
-
const contentHash = crypto.createHash("md5").update(content).digest("hex");
|
|
349
|
-
if (contentHash !== lastHash) {
|
|
350
|
-
lastHash = contentHash;
|
|
351
|
-
lastChangeTime = Date.now();
|
|
352
|
-
}
|
|
353
|
-
else if (Date.now() - lastChangeTime >= idleTime * 1000) {
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
await new Promise((r) => setTimeout(r, checkInterval * 1000));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
async killPane(paneId) {
|
|
360
|
-
const target = paneId || this.targetPane;
|
|
361
|
-
if (!target)
|
|
362
|
-
throw new Error("No target pane specified");
|
|
363
|
-
if (paneId) {
|
|
364
|
-
const currentPane = await this.getCurrentPane();
|
|
365
|
-
if (currentPane && target === currentPane) {
|
|
366
|
-
throw new Error("Cannot kill own pane!");
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
await runTmux(["kill-pane", "-t", target]);
|
|
370
|
-
if (target === this.targetPane) {
|
|
371
|
-
this.targetPane = null;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
async sendInterrupt(paneId) {
|
|
375
|
-
const target = paneId || this.targetPane;
|
|
376
|
-
if (!target)
|
|
377
|
-
throw new Error("No target pane specified");
|
|
378
|
-
await runTmux(["send-keys", "-t", target, "C-c"]);
|
|
379
|
-
}
|
|
380
|
-
async sendEscape(paneId) {
|
|
381
|
-
const target = paneId || this.targetPane;
|
|
382
|
-
if (!target)
|
|
383
|
-
throw new Error("No target pane specified");
|
|
384
|
-
await runTmux(["send-keys", "-t", target, "Escape"]);
|
|
385
|
-
}
|
|
386
|
-
async launchCli(command, vertical = true, size = 50) {
|
|
387
|
-
const paneId = await this.createPane(vertical, size, command);
|
|
388
|
-
if (paneId) {
|
|
389
|
-
const formattedId = await this.formatPaneIdentifier(paneId);
|
|
390
|
-
return { paneId, formattedId };
|
|
391
|
-
}
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
// ============================================================================
|
|
396
|
-
// RemoteTmuxController - For use outside tmux
|
|
397
|
-
// ============================================================================
|
|
398
|
-
export class RemoteTmuxController {
|
|
399
|
-
sessionName;
|
|
400
|
-
targetWindow = null;
|
|
401
|
-
constructor(sessionName = "remote-cli-session") {
|
|
402
|
-
this.sessionName = sessionName;
|
|
403
|
-
}
|
|
404
|
-
async ensureSession() {
|
|
405
|
-
const { exitCode } = await runTmux(["has-session", "-t", this.sessionName]);
|
|
406
|
-
if (exitCode !== 0) {
|
|
407
|
-
await runTmux([
|
|
408
|
-
"new-session",
|
|
409
|
-
"-d",
|
|
410
|
-
"-s",
|
|
411
|
-
this.sessionName,
|
|
412
|
-
"-P",
|
|
413
|
-
"-F",
|
|
414
|
-
"#{session_name}",
|
|
415
|
-
]);
|
|
416
|
-
this.targetWindow = `${this.sessionName}:0`;
|
|
417
|
-
}
|
|
418
|
-
else if (!this.targetWindow) {
|
|
419
|
-
const { stdout, exitCode: code2 } = await runTmux([
|
|
420
|
-
"display-message",
|
|
421
|
-
"-p",
|
|
422
|
-
"-t",
|
|
423
|
-
this.sessionName,
|
|
424
|
-
"#{session_name}:#{window_index}",
|
|
425
|
-
]);
|
|
426
|
-
if (code2 === 0 && stdout) {
|
|
427
|
-
this.targetWindow = stdout;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
async windowTarget(pane) {
|
|
432
|
-
await this.ensureSession();
|
|
433
|
-
if (!pane) {
|
|
434
|
-
if (this.targetWindow)
|
|
435
|
-
return this.targetWindow;
|
|
436
|
-
const { stdout, exitCode } = await runTmux([
|
|
437
|
-
"display-message",
|
|
438
|
-
"-p",
|
|
439
|
-
"-t",
|
|
440
|
-
this.sessionName,
|
|
441
|
-
"#{session_name}:#{window_index}",
|
|
442
|
-
]);
|
|
443
|
-
if (exitCode === 0 && stdout) {
|
|
444
|
-
this.targetWindow = stdout;
|
|
445
|
-
return stdout;
|
|
446
|
-
}
|
|
447
|
-
return `${this.sessionName}:0`;
|
|
448
|
-
}
|
|
449
|
-
if (/^\d+$/.test(pane)) {
|
|
450
|
-
return `${this.sessionName}:${pane}`;
|
|
451
|
-
}
|
|
452
|
-
return pane;
|
|
453
|
-
}
|
|
454
|
-
async listPanes() {
|
|
455
|
-
await this.ensureSession();
|
|
456
|
-
const { stdout, exitCode } = await runTmux([
|
|
457
|
-
"list-windows",
|
|
458
|
-
"-t",
|
|
459
|
-
this.sessionName,
|
|
460
|
-
"-F",
|
|
461
|
-
`#{window_index}${TMUX_SEP}#{window_name}${TMUX_SEP}#{window_active}${TMUX_SEP}#{window_width}x#{window_height}`,
|
|
462
|
-
]);
|
|
463
|
-
if (exitCode !== 0 || !stdout)
|
|
464
|
-
return [];
|
|
465
|
-
const windows = [];
|
|
466
|
-
for (const line of stdout.split("\n")) {
|
|
467
|
-
if (!line)
|
|
468
|
-
continue;
|
|
469
|
-
const [idx, name, active, size] = line.split(TMUX_SEP);
|
|
470
|
-
windows.push({
|
|
471
|
-
id: `${this.sessionName}:${idx}`,
|
|
472
|
-
index: idx,
|
|
473
|
-
title: name,
|
|
474
|
-
active: active === "1",
|
|
475
|
-
size,
|
|
476
|
-
command: "",
|
|
477
|
-
formatted_id: `${this.sessionName}:${idx}`,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
return windows;
|
|
481
|
-
}
|
|
482
|
-
async launchCli(command, name) {
|
|
483
|
-
await this.ensureSession();
|
|
484
|
-
const args = [
|
|
485
|
-
"new-window",
|
|
486
|
-
"-t",
|
|
487
|
-
this.sessionName,
|
|
488
|
-
"-P",
|
|
489
|
-
"-F",
|
|
490
|
-
"#{session_name}:#{window_index}",
|
|
491
|
-
];
|
|
492
|
-
if (name)
|
|
493
|
-
args.push("-n", name);
|
|
494
|
-
if (command)
|
|
495
|
-
args.push(command);
|
|
496
|
-
const { stdout, exitCode } = await runTmux(args);
|
|
497
|
-
if (exitCode === 0 && stdout) {
|
|
498
|
-
this.targetWindow = stdout;
|
|
499
|
-
return stdout;
|
|
500
|
-
}
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
async sendKeys(text, paneId, enter = true, delayEnter = true) {
|
|
504
|
-
if (!text)
|
|
505
|
-
return;
|
|
506
|
-
const target = await this.windowTarget(paneId);
|
|
507
|
-
if (enter && delayEnter) {
|
|
508
|
-
await runTmux(["send-keys", "-l", "-t", target, text]);
|
|
509
|
-
const delay = typeof delayEnter === "number" ? delayEnter : 0.1;
|
|
510
|
-
await new Promise((r) => setTimeout(r, delay * 1000));
|
|
511
|
-
await runTmux(["send-keys", "-t", target, "Enter"]);
|
|
512
|
-
}
|
|
513
|
-
else {
|
|
514
|
-
const args = ["send-keys", "-l", "-t", target, text];
|
|
515
|
-
if (enter)
|
|
516
|
-
args.push("Enter");
|
|
517
|
-
await runTmux(args);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
async capturePane(paneId, lines) {
|
|
521
|
-
const target = await this.windowTarget(paneId);
|
|
522
|
-
const args = ["capture-pane", "-t", target, "-p"];
|
|
523
|
-
if (lines)
|
|
524
|
-
args.push("-S", `-${lines}`);
|
|
525
|
-
const { stdout } = await runTmux(args);
|
|
526
|
-
return stdout;
|
|
527
|
-
}
|
|
528
|
-
async waitForIdle(paneId, idleTime = 2.0, checkInterval = 0.5, timeout) {
|
|
529
|
-
const target = await this.windowTarget(paneId);
|
|
530
|
-
const startTime = Date.now();
|
|
531
|
-
let lastChange = Date.now();
|
|
532
|
-
let lastHash = "";
|
|
533
|
-
while (true) {
|
|
534
|
-
if (timeout && Date.now() - startTime > timeout * 1000)
|
|
535
|
-
return false;
|
|
536
|
-
const { stdout } = await runTmux(["capture-pane", "-t", target, "-p"]);
|
|
537
|
-
const h = crypto.createHash("md5").update(stdout).digest("hex");
|
|
538
|
-
if (h !== lastHash) {
|
|
539
|
-
lastHash = h;
|
|
540
|
-
lastChange = Date.now();
|
|
541
|
-
}
|
|
542
|
-
else if (Date.now() - lastChange >= idleTime * 1000) {
|
|
543
|
-
return true;
|
|
544
|
-
}
|
|
545
|
-
await new Promise((r) => setTimeout(r, checkInterval * 1000));
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
async sendInterrupt(paneId) {
|
|
549
|
-
const target = await this.windowTarget(paneId);
|
|
550
|
-
await runTmux(["send-keys", "-t", target, "C-c"]);
|
|
551
|
-
}
|
|
552
|
-
async sendEscape(paneId) {
|
|
553
|
-
const target = await this.windowTarget(paneId);
|
|
554
|
-
await runTmux(["send-keys", "-t", target, "Escape"]);
|
|
555
|
-
}
|
|
556
|
-
async killWindow(windowId) {
|
|
557
|
-
const target = await this.windowTarget(windowId);
|
|
558
|
-
await runTmux(["kill-window", "-t", target]);
|
|
559
|
-
if (this.targetWindow === target) {
|
|
560
|
-
this.targetWindow = null;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
async attachSession() {
|
|
564
|
-
await this.ensureSession();
|
|
565
|
-
spawn("tmux", ["attach-session", "-t", this.sessionName], {
|
|
566
|
-
stdio: "inherit",
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
async cleanupSession() {
|
|
570
|
-
await runTmux(["kill-session", "-t", this.sessionName]);
|
|
571
|
-
this.targetWindow = null;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
// ============================================================================
|
|
575
|
-
// Unified CLI Interface
|
|
576
|
-
// ============================================================================
|
|
577
|
-
export class TmuxCLI {
|
|
578
|
-
controller;
|
|
579
|
-
mode;
|
|
580
|
-
inTmux;
|
|
581
|
-
constructor(session) {
|
|
582
|
-
this.inTmux = Boolean(process.env.TMUX);
|
|
583
|
-
if (this.inTmux) {
|
|
584
|
-
this.controller = new TmuxCLIController();
|
|
585
|
-
this.mode = "local";
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
this.controller = new RemoteTmuxController(session || "remote-cli-session");
|
|
589
|
-
this.mode = "remote";
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
async listTmuxPanes(session) {
|
|
593
|
-
if (this.mode === "local") {
|
|
594
|
-
const ctrl = this.controller;
|
|
595
|
-
return ctrl.listPanes(session);
|
|
596
|
-
}
|
|
597
|
-
else {
|
|
598
|
-
const ctrl = this.controller;
|
|
599
|
-
// Remote mode doesn't support session filter, just list windows
|
|
600
|
-
return ctrl.listPanes();
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
async listTmuxSessions() {
|
|
604
|
-
if (this.mode === "local") {
|
|
605
|
-
const ctrl = this.controller;
|
|
606
|
-
return ctrl.listSessions();
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
// Remote mode: just return the managed session info
|
|
610
|
-
const ctrl = this.controller;
|
|
611
|
-
await ctrl.ensureSession();
|
|
612
|
-
return [
|
|
613
|
-
{
|
|
614
|
-
name: ctrl.sessionName,
|
|
615
|
-
id: "$0",
|
|
616
|
-
windows: "1",
|
|
617
|
-
attached: true,
|
|
618
|
-
created: "",
|
|
619
|
-
current: true,
|
|
620
|
-
},
|
|
621
|
-
];
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
async createTmuxSession(name, command, cwd) {
|
|
625
|
-
const validation = validateCwd(cwd);
|
|
626
|
-
if (!validation.valid) {
|
|
627
|
-
throw new Error(validation.error);
|
|
628
|
-
}
|
|
629
|
-
if (this.mode === "local") {
|
|
630
|
-
const ctrl = this.controller;
|
|
631
|
-
return ctrl.createSession(name, command, cwd);
|
|
632
|
-
}
|
|
633
|
-
else {
|
|
634
|
-
// Remote mode already manages its own session
|
|
635
|
-
throw new Error("create_session not available in remote mode");
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
async killTmuxSession(name) {
|
|
639
|
-
if (this.mode === "local") {
|
|
640
|
-
const ctrl = this.controller;
|
|
641
|
-
return ctrl.killSession(name);
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
throw new Error("kill_session not available in remote mode");
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
async createTmuxWindow(options = {}) {
|
|
648
|
-
const validation = validateCwd(options.cwd);
|
|
649
|
-
if (!validation.valid) {
|
|
650
|
-
throw new Error(validation.error);
|
|
651
|
-
}
|
|
652
|
-
if (this.mode === "local") {
|
|
653
|
-
const ctrl = this.controller;
|
|
654
|
-
return ctrl.createWindow(options);
|
|
655
|
-
}
|
|
656
|
-
else {
|
|
657
|
-
const ctrl = this.controller;
|
|
658
|
-
return ctrl.launchCli(options.command || "", options.name);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
async launchTmuxPane(command, options = {}) {
|
|
662
|
-
const { vertical = true, size = 50, name } = options;
|
|
663
|
-
if (this.mode === "local") {
|
|
664
|
-
const ctrl = this.controller;
|
|
665
|
-
const result = await ctrl.launchCli(command, vertical, size);
|
|
666
|
-
return result?.paneId ?? null;
|
|
667
|
-
}
|
|
668
|
-
else {
|
|
669
|
-
const ctrl = this.controller;
|
|
670
|
-
return ctrl.launchCli(command, name);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
async sendTmuxKeys(text, options = {}) {
|
|
674
|
-
const { pane, enter = true, delayEnter = true } = options;
|
|
675
|
-
if (this.mode === "local") {
|
|
676
|
-
const ctrl = this.controller;
|
|
677
|
-
if (pane) {
|
|
678
|
-
const resolved = await ctrl.resolvePaneIdentifier(pane);
|
|
679
|
-
if (!resolved)
|
|
680
|
-
throw new Error(`Could not resolve pane identifier: ${pane}`);
|
|
681
|
-
ctrl.selectPane(resolved);
|
|
682
|
-
}
|
|
683
|
-
await ctrl.sendKeys(text, undefined, enter, delayEnter);
|
|
684
|
-
}
|
|
685
|
-
else {
|
|
686
|
-
const ctrl = this.controller;
|
|
687
|
-
await ctrl.sendKeys(text, pane, enter, delayEnter);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
async captureTmuxPane(options = {}) {
|
|
691
|
-
const { pane, lines } = options;
|
|
692
|
-
if (this.mode === "local") {
|
|
693
|
-
const ctrl = this.controller;
|
|
694
|
-
if (pane) {
|
|
695
|
-
const resolved = await ctrl.resolvePaneIdentifier(pane);
|
|
696
|
-
if (!resolved)
|
|
697
|
-
throw new Error(`Could not resolve pane identifier: ${pane}`);
|
|
698
|
-
ctrl.selectPane(resolved);
|
|
699
|
-
}
|
|
700
|
-
return ctrl.capturePane(undefined, lines);
|
|
701
|
-
}
|
|
702
|
-
else {
|
|
703
|
-
const ctrl = this.controller;
|
|
704
|
-
return ctrl.capturePane(pane, lines);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
async interruptTmuxPane(pane) {
|
|
708
|
-
if (this.mode === "local") {
|
|
709
|
-
const ctrl = this.controller;
|
|
710
|
-
if (pane) {
|
|
711
|
-
const resolved = await ctrl.resolvePaneIdentifier(pane);
|
|
712
|
-
if (!resolved)
|
|
713
|
-
throw new Error(`Could not resolve pane identifier: ${pane}`);
|
|
714
|
-
ctrl.selectPane(resolved);
|
|
715
|
-
}
|
|
716
|
-
await ctrl.sendInterrupt();
|
|
717
|
-
}
|
|
718
|
-
else {
|
|
719
|
-
const ctrl = this.controller;
|
|
720
|
-
await ctrl.sendInterrupt(pane);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
async escapeTmuxPane(pane) {
|
|
724
|
-
if (this.mode === "local") {
|
|
725
|
-
const ctrl = this.controller;
|
|
726
|
-
if (pane) {
|
|
727
|
-
const resolved = await ctrl.resolvePaneIdentifier(pane);
|
|
728
|
-
if (!resolved)
|
|
729
|
-
throw new Error(`Could not resolve pane identifier: ${pane}`);
|
|
730
|
-
ctrl.selectPane(resolved);
|
|
731
|
-
}
|
|
732
|
-
await ctrl.sendEscape();
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
const ctrl = this.controller;
|
|
736
|
-
await ctrl.sendEscape(pane);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
async killTmuxPane(pane) {
|
|
740
|
-
if (this.mode === "local") {
|
|
741
|
-
const ctrl = this.controller;
|
|
742
|
-
if (pane) {
|
|
743
|
-
const resolved = await ctrl.resolvePaneIdentifier(pane);
|
|
744
|
-
if (!resolved)
|
|
745
|
-
throw new Error(`Could not resolve pane identifier: ${pane}`);
|
|
746
|
-
ctrl.selectPane(resolved);
|
|
747
|
-
}
|
|
748
|
-
await ctrl.killPane();
|
|
749
|
-
}
|
|
750
|
-
else {
|
|
751
|
-
const ctrl = this.controller;
|
|
752
|
-
await ctrl.killWindow(pane);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
async waitTmuxPaneIdle(options = {}) {
|
|
756
|
-
const { pane, idleTime = 2.0, timeout } = options;
|
|
757
|
-
if (this.mode === "local") {
|
|
758
|
-
const ctrl = this.controller;
|
|
759
|
-
if (pane) {
|
|
760
|
-
const resolved = await ctrl.resolvePaneIdentifier(pane);
|
|
761
|
-
if (!resolved)
|
|
762
|
-
throw new Error(`Could not resolve pane identifier: ${pane}`);
|
|
763
|
-
ctrl.selectPane(resolved);
|
|
764
|
-
}
|
|
765
|
-
return ctrl.waitForIdle(undefined, idleTime, 0.5, timeout);
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
const ctrl = this.controller;
|
|
769
|
-
return ctrl.waitForIdle(pane, idleTime, 0.5, timeout);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
// ============================================================================
|
|
774
|
-
// Helper Functions (for MCP tools)
|
|
775
|
-
// ============================================================================
|
|
776
|
-
export async function getCurrentPaneInfo() {
|
|
777
|
-
const paneId = process.env.TMUX_PANE;
|
|
778
|
-
if (!paneId)
|
|
779
|
-
return { id: "", title: "" };
|
|
780
|
-
const { stdout } = await runTmux(["display-message", "-p", "-t", paneId, "#{pane_title}"]);
|
|
781
|
-
return { id: paneId, title: stdout };
|
|
782
|
-
}
|
|
783
|
-
export function shellQuote(s) {
|
|
784
|
-
return `'${s.replace(/'/g, `"'"'"'`)}'`;
|
|
785
|
-
}
|
|
786
|
-
export function parseLaunchPaneIds(stdout) {
|
|
787
|
-
const newFormatMatch = stdout.match(/in pane\s+(%\d+)\s+\(([^)]+)\)/);
|
|
788
|
-
if (newFormatMatch) {
|
|
789
|
-
return { paneId: newFormatMatch[1], paneFormattedId: newFormatMatch[2] };
|
|
790
|
-
}
|
|
791
|
-
const rawIdMatch = stdout.match(/in pane\s+(%\d+)/);
|
|
792
|
-
const paneId = rawIdMatch?.[1];
|
|
793
|
-
const formattedPaneMatch = stdout.match(/in pane\s+([^\s]+)$/);
|
|
794
|
-
const paneFormattedId = formattedPaneMatch?.[1];
|
|
795
|
-
return { paneId, paneFormattedId };
|
|
796
|
-
}
|
|
797
|
-
//# sourceMappingURL=tmux.js.map
|