tmuxes 0.1.7 → 0.1.9
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/.node-version +1 -0
- package/.nvmrc +1 -0
- package/.tmp-npm-cache/_cacache/content-v2/sha512/43/27/5e000b8b9c56a6ccc66f709485499f4304e2cb1982582ba571321c07b3ef56fcabd2c671898cc8003365a0485b6fd8e73e7b17b073cec0f7d1628c1a99df +0 -0
- package/.tmp-npm-cache/_cacache/content-v2/sha512/51/cf/4301295d74559ed494bae160d54d8741077f89faebb311882ac065019246951e7b53f3dcb913793c42b331e14c7070c4810c3cdc27a427d103a7db4614e0 +0 -0
- package/.tmp-npm-cache/_cacache/content-v2/sha512/c3/4d/d68a454a916e74c2617f586fbf770981b33811d667c2547eb0e9fc21938f4ee7e98f1ceee4bde8ad8815b5f6efe21b60eee798837d68f51a3340d7e5bb7a +0 -0
- package/.tmp-npm-cache/_cacache/content-v2/sha512/fe/40/2abfbefc96299e8bf714aa91d62607190ae299e102cf5933db2e2904640d65d25d67dbbb6fa2ddc92a17f00b9dbfdf2e37487f67d96ec36c64a285b59a7d +0 -0
- package/.tmp-npm-cache/_cacache/index-v5/27/fe/81a3de6ce7ae3d1e41a3421de20c5629998c4ee5d0ffe2037630f03b03b2 +4 -0
- package/.tmp-npm-cache/_cacache/index-v5/65/22/dd66711f62681fce09aabb2357a2907b4a0c778ac5227c4baf9603fd86e8 +4 -0
- package/.tmp-npm-cache/_update-notifier-last-checked +0 -0
- package/AGENTS.md +15 -0
- package/CLAUDE.md +3 -0
- package/LICENSE +21 -21
- package/README.en.md +304 -0
- package/README.md +301 -289
- package/SECURITY.md +31 -0
- package/{public → client}/index.html +12 -13
- package/client/package.json +29 -0
- package/client/src/App.tsx +123 -0
- package/client/src/activity.ts +5 -0
- package/client/src/api.ts +130 -0
- package/client/src/attention.tsx +157 -0
- package/client/src/components/FileExplorer.tsx +156 -0
- package/client/src/components/FileViewer.tsx +194 -0
- package/client/src/components/SessionRow.tsx +108 -0
- package/client/src/components/SessionTree.tsx +197 -0
- package/client/src/components/SettingsButton.tsx +122 -0
- package/client/src/components/Sidebar.tsx +96 -0
- package/client/src/components/StatusBanner.tsx +31 -0
- package/client/src/components/TargetGroup.tsx +275 -0
- package/client/src/components/TerminalPanel.tsx +192 -0
- package/client/src/folders.ts +245 -0
- package/client/src/hooks/useTerminal.ts +67 -0
- package/client/src/hooks/useTmuxSocket.ts +65 -0
- package/client/src/i18n.ts +213 -0
- package/client/src/main.tsx +17 -0
- package/client/src/settings.tsx +87 -0
- package/client/src/styles.css +723 -0
- package/client/src/types.ts +93 -0
- package/client/src/util.ts +65 -0
- package/client/tsconfig.json +13 -0
- package/client/vite.config.ts +15 -0
- package/fig/fig1.png +0 -0
- package/package.json +28 -61
- package/scripts/prepack.mjs +35 -0
- package/{bin → server/bin}/tmuxes.js +36 -36
- package/server/package.json +61 -0
- package/server/src/agentHooks.ts +120 -0
- package/server/src/agentOutput.ts +36 -0
- package/server/src/agentState.ts +70 -0
- package/server/src/config.ts +31 -0
- package/server/src/exe.ts +34 -0
- package/server/src/exec.ts +61 -0
- package/server/src/files.ts +330 -0
- package/server/src/foldersStore.ts +114 -0
- package/server/src/index.ts +114 -0
- package/server/src/logger.ts +16 -0
- package/{dist/monitor.js → server/src/monitor.ts} +10 -9
- package/server/src/openBrowser.ts +28 -0
- package/{dist/platform.js → server/src/platform.ts} +4 -5
- package/server/src/rest/router.ts +290 -0
- package/server/src/targetCommand.ts +79 -0
- package/server/src/targets.ts +152 -0
- package/server/src/tmux/builder.ts +198 -0
- package/server/src/tmux/formats.ts +95 -0
- package/server/src/tmux/sessions.ts +204 -0
- package/server/src/validate.ts +79 -0
- package/server/src/windowsSsh.ts +239 -0
- package/server/src/winshell/manager.ts +296 -0
- package/server/src/ws/protocol.ts +15 -0
- package/server/src/ws/sshState.ts +36 -0
- package/server/src/ws/terminalSession.ts +207 -0
- package/server/src/ws/wsServer.ts +153 -0
- package/server/src/wsl.ts +38 -0
- package/server/test/agentHooks.test.ts +66 -0
- package/server/test/agentOutput.test.ts +26 -0
- package/server/test/agentState.test.ts +24 -0
- package/server/test/builder.test.ts +162 -0
- package/server/test/files.test.ts +81 -0
- package/server/test/formats.test.ts +123 -0
- package/server/test/monitor.test.ts +25 -0
- package/server/test/validate.test.ts +71 -0
- package/server/test/wsl.test.ts +18 -0
- package/server/tsconfig.json +9 -0
- package/server/vitest.config.ts +12 -0
- package/start.cmd +30 -0
- package/start.command +20 -0
- package/start.sh +20 -0
- package/tsconfig.base.json +19 -0
- package/dist/agentHooks.js +0 -91
- package/dist/agentHooks.js.map +0 -1
- package/dist/agentOutput.js +0 -30
- package/dist/agentOutput.js.map +0 -1
- package/dist/agentState.js +0 -45
- package/dist/agentState.js.map +0 -1
- package/dist/config.js +0 -32
- package/dist/config.js.map +0 -1
- package/dist/exe.js +0 -37
- package/dist/exe.js.map +0 -1
- package/dist/exec.js +0 -43
- package/dist/exec.js.map +0 -1
- package/dist/files.js +0 -243
- package/dist/files.js.map +0 -1
- package/dist/foldersStore.js +0 -103
- package/dist/foldersStore.js.map +0 -1
- package/dist/index.js +0 -117
- package/dist/index.js.map +0 -1
- package/dist/logger.js +0 -16
- package/dist/logger.js.map +0 -1
- package/dist/monitor.js.map +0 -1
- package/dist/openBrowser.js +0 -31
- package/dist/openBrowser.js.map +0 -1
- package/dist/platform.js.map +0 -1
- package/dist/rest/router.js +0 -190
- package/dist/rest/router.js.map +0 -1
- package/dist/targetCommand.js +0 -41
- package/dist/targetCommand.js.map +0 -1
- package/dist/targets.js +0 -131
- package/dist/targets.js.map +0 -1
- package/dist/tmux/builder.js +0 -173
- package/dist/tmux/builder.js.map +0 -1
- package/dist/tmux/formats.js +0 -61
- package/dist/tmux/formats.js.map +0 -1
- package/dist/tmux/sessions.js +0 -157
- package/dist/tmux/sessions.js.map +0 -1
- package/dist/validate.js +0 -65
- package/dist/validate.js.map +0 -1
- package/dist/winshell/manager.js +0 -267
- package/dist/winshell/manager.js.map +0 -1
- package/dist/ws/protocol.js +0 -4
- package/dist/ws/protocol.js.map +0 -1
- package/dist/ws/sshState.js +0 -35
- package/dist/ws/sshState.js.map +0 -1
- package/dist/ws/terminalSession.js +0 -204
- package/dist/ws/terminalSession.js.map +0 -1
- package/dist/ws/wsServer.js +0 -151
- package/dist/ws/wsServer.js.map +0 -1
- package/dist/wsl.js +0 -35
- package/dist/wsl.js.map +0 -1
- package/public/assets/index-BpVrfoZw.js +0 -44
- package/public/assets/index-D_X5SnGx.css +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Shared types — mirror of the server's REST + WS shapes. */
|
|
2
|
+
|
|
3
|
+
export interface Target {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: 'local' | 'ssh' | 'wsl' | 'winlocal';
|
|
6
|
+
label: string;
|
|
7
|
+
host?: string;
|
|
8
|
+
user?: string;
|
|
9
|
+
port?: number;
|
|
10
|
+
distro?: string;
|
|
11
|
+
/** Launchable shells (winlocal target only). */
|
|
12
|
+
shells?: { id: string; label: string }[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SessionInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
windows: number;
|
|
18
|
+
attached: boolean;
|
|
19
|
+
/** unix epoch seconds */
|
|
20
|
+
created: number;
|
|
21
|
+
/** epoch seconds from tmux / native shell */
|
|
22
|
+
lastActivity?: number;
|
|
23
|
+
/** recognized agent whose hooks are driving status */
|
|
24
|
+
agentKind?: AgentKind;
|
|
25
|
+
/** current lifecycle state from agent hooks */
|
|
26
|
+
agentState?: AgentState;
|
|
27
|
+
/** why this session is asking for attention */
|
|
28
|
+
attentionReason?: AttentionReason;
|
|
29
|
+
/** hook event that last updated the state */
|
|
30
|
+
agentEvent?: string;
|
|
31
|
+
/** monotonic-ish event token for client edge detection */
|
|
32
|
+
agentNonce?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type AgentKind = 'claude' | 'codex';
|
|
36
|
+
export type AgentState = 'running' | 'waiting' | 'idle';
|
|
37
|
+
export type AttentionReason = 'decision' | 'done' | 'error';
|
|
38
|
+
export type LaunchAgent = 'claude' | 'codex';
|
|
39
|
+
|
|
40
|
+
export interface WindowInfo {
|
|
41
|
+
index: number;
|
|
42
|
+
name: string;
|
|
43
|
+
panes: number;
|
|
44
|
+
active: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface FileEntry {
|
|
48
|
+
name: string;
|
|
49
|
+
type: 'dir' | 'file';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FilePreview {
|
|
53
|
+
path: string;
|
|
54
|
+
content: string;
|
|
55
|
+
truncated: boolean;
|
|
56
|
+
binary: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SessionDirectory {
|
|
60
|
+
cwd: string;
|
|
61
|
+
path: string;
|
|
62
|
+
entries: FileEntry[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A file opened in the right-hand viewer. */
|
|
66
|
+
export interface OpenFile {
|
|
67
|
+
targetId: string;
|
|
68
|
+
session: string;
|
|
69
|
+
path: string;
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Selection {
|
|
74
|
+
targetId: string;
|
|
75
|
+
session: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type SshState = 'hostkey' | 'authfail' | 'refused' | 'timeout';
|
|
79
|
+
|
|
80
|
+
export type ServerControl =
|
|
81
|
+
| { type: 'ready'; target: string; session: string }
|
|
82
|
+
| { type: 'ssh'; state: SshState; message: string }
|
|
83
|
+
| { type: 'error'; message: string }
|
|
84
|
+
| { type: 'exit'; code: number | null }
|
|
85
|
+
| { type: 'pong' };
|
|
86
|
+
|
|
87
|
+
/** UI-facing connection status for the terminal panel. */
|
|
88
|
+
export type ConnStatus =
|
|
89
|
+
| { kind: 'connecting' }
|
|
90
|
+
| { kind: 'connected' }
|
|
91
|
+
| { kind: 'ssh'; message: string }
|
|
92
|
+
| { kind: 'disconnected'; message: string }
|
|
93
|
+
| { kind: 'error'; message: string };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Mirror of the server's session-name allowlist (no '.'/':' — tmux delimiters). */
|
|
2
|
+
export const SESSION_NAME_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
3
|
+
|
|
4
|
+
export function isValidSessionName(name: string): boolean {
|
|
5
|
+
return SESSION_NAME_RE.test(name);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// ---------- POSIX-ish path helpers (targets are always unix-like) ----------
|
|
9
|
+
|
|
10
|
+
export function joinPath(dir: string, name: string): string {
|
|
11
|
+
return dir.endsWith('/') ? dir + name : `${dir}/${name}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parentPath(path: string): string {
|
|
15
|
+
const trimmed = path.replace(/\/+$/, '');
|
|
16
|
+
const idx = trimmed.lastIndexOf('/');
|
|
17
|
+
if (idx <= 0) return '/';
|
|
18
|
+
return trimmed.slice(0, idx);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function basename(path: string): string {
|
|
22
|
+
const trimmed = path.replace(/\/+$/, '');
|
|
23
|
+
return trimmed.slice(trimmed.lastIndexOf('/') + 1) || '/';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TEXT_EXTENSIONS = new Set([
|
|
27
|
+
'txt', 'text', 'md', 'markdown', 'rst', 'log', 'csv', 'tsv',
|
|
28
|
+
'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'json', 'jsonc', 'map',
|
|
29
|
+
'py', 'pyi', 'rb', 'go', 'rs', 'java', 'kt', 'kts', 'scala', 'clj', 'cljs',
|
|
30
|
+
'c', 'h', 'cc', 'cpp', 'cxx', 'hpp', 'hh', 'cs', 'm', 'mm', 'swift',
|
|
31
|
+
'php', 'pl', 'pm', 'lua', 'r', 'jl', 'dart', 'ex', 'exs', 'erl', 'hrl', 'hs', 'ml', 'mli',
|
|
32
|
+
'sh', 'bash', 'zsh', 'fish', 'ps1', 'bat', 'cmd',
|
|
33
|
+
'html', 'htm', 'xml', 'svg', 'css', 'scss', 'sass', 'less', 'vue', 'svelte',
|
|
34
|
+
'sql', 'graphql', 'gql', 'proto',
|
|
35
|
+
'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', 'env', 'properties', 'lock',
|
|
36
|
+
'gitignore', 'gitattributes', 'dockerignore', 'editorconfig', 'diff', 'patch',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const TEXT_FILENAMES = new Set([
|
|
40
|
+
'dockerfile', 'makefile', 'readme', 'license', 'licence', 'changelog',
|
|
41
|
+
'authors', 'notice', 'copying', 'procfile', '.gitignore', '.env',
|
|
42
|
+
'.bashrc', '.zshrc', '.profile', '.vimrc', '.editorconfig',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Heuristic: is this filename openable as text/code in the viewer? */
|
|
46
|
+
export function isTextFile(name: string): boolean {
|
|
47
|
+
const lower = name.toLowerCase();
|
|
48
|
+
if (TEXT_FILENAMES.has(lower)) return true;
|
|
49
|
+
const dot = lower.lastIndexOf('.');
|
|
50
|
+
if (dot < 0) return false; // no extension and not a known name → treat as non-text
|
|
51
|
+
return TEXT_EXTENSIONS.has(lower.slice(dot + 1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Compact relative time, e.g. "刚刚", "5m", "3h", "2d". */
|
|
55
|
+
export function ago(epochSeconds: number, nowMs: number, language: 'zh' | 'en' = 'zh'): string {
|
|
56
|
+
if (!epochSeconds) return '';
|
|
57
|
+
const sec = Math.max(0, Math.floor(nowMs / 1000 - epochSeconds));
|
|
58
|
+
if (sec < 10) return language === 'zh' ? '刚刚' : 'just now';
|
|
59
|
+
if (sec < 60) return `${sec}s`;
|
|
60
|
+
const min = Math.floor(sec / 60);
|
|
61
|
+
if (min < 60) return `${min}m`;
|
|
62
|
+
const hr = Math.floor(min / 60);
|
|
63
|
+
if (hr < 24) return `${hr}h`;
|
|
64
|
+
return `${Math.floor(hr / 24)}d`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"types": ["vite/client"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["src", "vite.config.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
const API_TARGET = 'http://127.0.0.1:7420';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
server: {
|
|
9
|
+
port: 5173,
|
|
10
|
+
proxy: {
|
|
11
|
+
'/api': { target: API_TARGET, changeOrigin: true },
|
|
12
|
+
'/ws': { target: API_TARGET, ws: true, changeOrigin: true },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
package/fig/fig1.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,61 +1,28 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "tmuxes",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
],
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"tmuxes": "bin/tmuxes.js"
|
|
30
|
-
},
|
|
31
|
-
"engines": {
|
|
32
|
-
"node": ">=22.12.0 <23"
|
|
33
|
-
},
|
|
34
|
-
"files": [
|
|
35
|
-
"dist",
|
|
36
|
-
"public",
|
|
37
|
-
"bin",
|
|
38
|
-
"README.md",
|
|
39
|
-
"LICENSE"
|
|
40
|
-
],
|
|
41
|
-
"scripts": {
|
|
42
|
-
"dev": "tsx watch src/index.ts",
|
|
43
|
-
"build": "tsc -p tsconfig.json",
|
|
44
|
-
"start": "node dist/index.js",
|
|
45
|
-
"test": "vitest run",
|
|
46
|
-
"prepack": "node ../scripts/prepack.mjs"
|
|
47
|
-
},
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"express": "^5.2.1",
|
|
50
|
-
"node-pty": "1.1.0",
|
|
51
|
-
"ws": "^8.21.0"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@types/express": "^5.0.0",
|
|
55
|
-
"@types/node": "^22.10.0",
|
|
56
|
-
"@types/ws": "^8.5.13",
|
|
57
|
-
"tsx": "^4.19.2",
|
|
58
|
-
"typescript": "^5.7.2",
|
|
59
|
-
"vitest": "^2.1.8"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "tmuxes",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Dev monorepo for tmuxes — the publishable package lives in server/ (name: tmuxes).",
|
|
7
|
+
"packageManager": "npm@10.9.7",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.12.0 <23"
|
|
10
|
+
},
|
|
11
|
+
"workspaces": [
|
|
12
|
+
"server",
|
|
13
|
+
"client"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "concurrently -n server,client -c blue,green \"npm:dev:server\" \"npm:dev:client\"",
|
|
17
|
+
"dev:server": "npm --workspace server run dev",
|
|
18
|
+
"dev:client": "npm --workspace client run dev",
|
|
19
|
+
"build": "npm --workspace client run build && npm --workspace server run build",
|
|
20
|
+
"start": "npm --workspace server run start",
|
|
21
|
+
"test": "npm --workspace server run test",
|
|
22
|
+
"pack:check": "npm pack --workspace server --dry-run",
|
|
23
|
+
"release": "npm publish --workspace server"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"concurrently": "^9.1.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Runs as the `tmuxes` package's prepack (npm pack / npm publish).
|
|
2
|
+
// Builds the client + server and bundles the built client into server/public
|
|
3
|
+
// so the published package is self-contained.
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { cpSync, rmSync, existsSync } from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url))); // scripts/.. = repo root
|
|
10
|
+
const log = (m) => console.log(`[prepack] ${m}`);
|
|
11
|
+
|
|
12
|
+
log('building client + server…');
|
|
13
|
+
rmSync(join(root, 'server', 'dist'), { recursive: true, force: true });
|
|
14
|
+
execSync('npm run build', { cwd: root, stdio: 'inherit' });
|
|
15
|
+
|
|
16
|
+
const clientDist = join(root, 'client', 'dist');
|
|
17
|
+
if (!existsSync(clientDist)) {
|
|
18
|
+
console.error('[prepack] client/dist missing after build');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const publicDir = join(root, 'server', 'public');
|
|
23
|
+
rmSync(publicDir, { recursive: true, force: true });
|
|
24
|
+
cpSync(clientDist, publicDir, { recursive: true });
|
|
25
|
+
log('bundled client → server/public');
|
|
26
|
+
|
|
27
|
+
for (const file of ['README.md', 'LICENSE']) {
|
|
28
|
+
const src = join(root, file);
|
|
29
|
+
if (existsSync(src)) {
|
|
30
|
+
cpSync(src, join(root, 'server', file));
|
|
31
|
+
log(`copied ${file}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
log('done');
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// tmuxes — web UI to manage tmux sessions (local / SSH / WSL).
|
|
3
|
-
// Parses a couple of flags, sets the env the server reads, then launches it.
|
|
4
|
-
|
|
5
|
-
const args = process.argv.slice(2);
|
|
6
|
-
|
|
7
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
-
console.log(`tmuxes — one browser tab to run and supervise tmux sessions (local · SSH · WSL)
|
|
9
|
-
|
|
10
|
-
Usage:
|
|
11
|
-
tmuxes [options]
|
|
12
|
-
|
|
13
|
-
Options:
|
|
14
|
-
--port <n> Port to listen on (default 7420, env TMUXES_PORT)
|
|
15
|
-
--no-open Do not open the browser
|
|
16
|
-
-h, --help Show this help
|
|
17
|
-
|
|
18
|
-
Binds to 127.0.0.1 only (no-auth local UI). Then open http://127.0.0.1:7420
|
|
19
|
-
(opens automatically unless --no-open). Requires tmux on the host you connect to.`);
|
|
20
|
-
process.exit(0);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function flag(name) {
|
|
24
|
-
const i = args.indexOf(name);
|
|
25
|
-
return i >= 0 ? args[i + 1] : undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const port = flag('--port');
|
|
29
|
-
if (port) process.env.TMUXES_PORT = port;
|
|
30
|
-
|
|
31
|
-
// Open the browser by default (the "one-click run" experience), unless the
|
|
32
|
-
// user opted out or already set TMUXES_OPEN.
|
|
33
|
-
if (!args.includes('--no-open') && process.env.TMUXES_OPEN === undefined) {
|
|
34
|
-
process.env.TMUXES_OPEN = '1';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
await import('../dist/index.js');
|
|
2
|
+
// tmuxes — web UI to manage tmux sessions (local / SSH / WSL).
|
|
3
|
+
// Parses a couple of flags, sets the env the server reads, then launches it.
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
8
|
+
console.log(`tmuxes — one browser tab to run and supervise tmux sessions (local · SSH · WSL)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
tmuxes [options]
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--port <n> Port to listen on (default 7420, env TMUXES_PORT)
|
|
15
|
+
--no-open Do not open the browser
|
|
16
|
+
-h, --help Show this help
|
|
17
|
+
|
|
18
|
+
Binds to 127.0.0.1 only (no-auth local UI). Then open http://127.0.0.1:7420
|
|
19
|
+
(opens automatically unless --no-open). Requires tmux on the host you connect to.`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function flag(name) {
|
|
24
|
+
const i = args.indexOf(name);
|
|
25
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const port = flag('--port');
|
|
29
|
+
if (port) process.env.TMUXES_PORT = port;
|
|
30
|
+
|
|
31
|
+
// Open the browser by default (the "one-click run" experience), unless the
|
|
32
|
+
// user opted out or already set TMUXES_OPEN.
|
|
33
|
+
if (!args.includes('--no-open') && process.env.TMUXES_OPEN === undefined) {
|
|
34
|
+
process.env.TMUXES_OPEN = '1';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await import('../dist/index.js');
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tmuxes",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Web UI to run and supervise many CLI coding agents in tmux — local, SSH, and WSL.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tmux",
|
|
7
|
+
"terminal",
|
|
8
|
+
"xterm",
|
|
9
|
+
"ssh",
|
|
10
|
+
"wsl",
|
|
11
|
+
"web-terminal",
|
|
12
|
+
"node-pty",
|
|
13
|
+
"cli-agents"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "f1974939505 (https://github.com/f1974939505)",
|
|
17
|
+
"homepage": "https://github.com/f1974939505/tmuxes#readme",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/f1974939505/tmuxes.git",
|
|
21
|
+
"directory": "server"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/f1974939505/tmuxes/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"tmuxes": "bin/tmuxes.js"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=22.12.0 <23"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"public",
|
|
37
|
+
"bin",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"dev": "tsx watch src/index.ts",
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"start": "node dist/index.js",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"prepack": "node ../scripts/prepack.mjs"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"express": "^5.2.1",
|
|
50
|
+
"node-pty": "1.1.0",
|
|
51
|
+
"ws": "^8.21.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/express": "^5.0.0",
|
|
55
|
+
"@types/node": "^22.10.0",
|
|
56
|
+
"@types/ws": "^8.5.13",
|
|
57
|
+
"tsx": "^4.19.2",
|
|
58
|
+
"typescript": "^5.7.2",
|
|
59
|
+
"vitest": "^2.1.8"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentHookCommand,
|
|
3
|
+
type AgentKind,
|
|
4
|
+
type AgentState,
|
|
5
|
+
type AttentionReason,
|
|
6
|
+
} from './agentState.js';
|
|
7
|
+
|
|
8
|
+
interface AugmentedCommand {
|
|
9
|
+
command: string;
|
|
10
|
+
kind?: AgentKind;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isDisabled(): boolean {
|
|
14
|
+
const v = process.env.TMUXES_NO_AUTOHOOK;
|
|
15
|
+
return v === '1' || v === 'true';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function baseName(token: string): string {
|
|
19
|
+
return (token.split(/[\\/]/).pop() || '').toLowerCase().replace(/\.(exe|cmd|bat)$/, '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function detectAgentKind(command: string): AgentKind | undefined {
|
|
23
|
+
if (isDisabled()) return undefined;
|
|
24
|
+
const trimmed = command.trim();
|
|
25
|
+
if (!trimmed) return undefined;
|
|
26
|
+
|
|
27
|
+
const m = /^(\S+)(\s+[\s\S]*)?$/.exec(trimmed);
|
|
28
|
+
if (!m) return undefined;
|
|
29
|
+
switch (baseName(m[1])) {
|
|
30
|
+
case 'claude':
|
|
31
|
+
return 'claude';
|
|
32
|
+
case 'codex':
|
|
33
|
+
return 'codex';
|
|
34
|
+
default:
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hook(
|
|
40
|
+
kind: AgentKind,
|
|
41
|
+
state: AgentState,
|
|
42
|
+
reason: AttentionReason | '',
|
|
43
|
+
event: string,
|
|
44
|
+
) {
|
|
45
|
+
return { type: 'command', command: agentHookCommand(kind, state, reason, event) };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function claudeSettings(): string {
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
hooks: {
|
|
51
|
+
UserPromptSubmit: [{ hooks: [hook('claude', 'running', '', 'UserPromptSubmit')] }],
|
|
52
|
+
PreToolUse: [{ matcher: '', hooks: [hook('claude', 'running', '', 'PreToolUse')] }],
|
|
53
|
+
PostToolUse: [{ matcher: '', hooks: [hook('claude', 'running', '', 'PostToolUse')] }],
|
|
54
|
+
PermissionRequest: [
|
|
55
|
+
{ matcher: '', hooks: [hook('claude', 'waiting', 'decision', 'PermissionRequest')] },
|
|
56
|
+
],
|
|
57
|
+
Notification: [
|
|
58
|
+
{
|
|
59
|
+
matcher: 'permission_prompt',
|
|
60
|
+
hooks: [hook('claude', 'waiting', 'decision', 'Notification.permission_prompt')],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
matcher: 'elicitation_dialog',
|
|
64
|
+
hooks: [hook('claude', 'waiting', 'decision', 'Notification.elicitation_dialog')],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
Stop: [{ hooks: [hook('claude', 'idle', 'done', 'Stop')] }],
|
|
68
|
+
StopFailure: [{ hooks: [hook('claude', 'idle', 'error', 'StopFailure')] }],
|
|
69
|
+
SessionEnd: [{ hooks: [hook('claude', 'idle', 'done', 'SessionEnd')] }],
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function tomlString(v: string): string {
|
|
75
|
+
return `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function codexHookConfig(
|
|
79
|
+
event: string,
|
|
80
|
+
cmd: string,
|
|
81
|
+
opts: { matcher?: string } = {},
|
|
82
|
+
): string {
|
|
83
|
+
const matcher = opts.matcher === undefined ? '' : `matcher=${tomlString(opts.matcher)},`;
|
|
84
|
+
return `hooks.${event}=[{${matcher}hooks=[{type="command",command=${tomlString(cmd)}}]}]`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function codexConfigArgs(): string {
|
|
88
|
+
const configs = [
|
|
89
|
+
codexHookConfig('UserPromptSubmit', agentHookCommand('codex', 'running', '', 'UserPromptSubmit')),
|
|
90
|
+
codexHookConfig('PreToolUse', agentHookCommand('codex', 'running', '', 'PreToolUse'), {
|
|
91
|
+
matcher: '',
|
|
92
|
+
}),
|
|
93
|
+
codexHookConfig('PostToolUse', agentHookCommand('codex', 'running', '', 'PostToolUse'), {
|
|
94
|
+
matcher: '',
|
|
95
|
+
}),
|
|
96
|
+
codexHookConfig(
|
|
97
|
+
'PermissionRequest',
|
|
98
|
+
agentHookCommand('codex', 'waiting', 'decision', 'PermissionRequest'),
|
|
99
|
+
{ matcher: '' },
|
|
100
|
+
),
|
|
101
|
+
codexHookConfig('Stop', agentHookCommand('codex', 'idle', 'done', 'Stop')),
|
|
102
|
+
];
|
|
103
|
+
return configs.map((c) => `-c '${c}'`).join(' ');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function augmentAgentCommand(command: string): AugmentedCommand {
|
|
107
|
+
const kind = detectAgentKind(command);
|
|
108
|
+
if (!kind) return { command };
|
|
109
|
+
|
|
110
|
+
const trimmed = command.trim();
|
|
111
|
+
const m = /^(\S+)(\s+[\s\S]*)?$/.exec(trimmed);
|
|
112
|
+
if (!m) return { command };
|
|
113
|
+
const prog = m[1];
|
|
114
|
+
const rest = m[2] ?? '';
|
|
115
|
+
|
|
116
|
+
if (kind === 'claude') {
|
|
117
|
+
return { kind, command: `${prog} --settings '${claudeSettings()}'${rest}` };
|
|
118
|
+
}
|
|
119
|
+
return { kind, command: `${prog} ${codexConfigArgs()}${rest}` };
|
|
120
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AgentKind } from './agentState.js';
|
|
2
|
+
|
|
3
|
+
interface TerminalErrorPattern {
|
|
4
|
+
event: string;
|
|
5
|
+
kinds?: AgentKind[];
|
|
6
|
+
re: RegExp;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const ERROR_PATTERNS: TerminalErrorPattern[] = [
|
|
10
|
+
{
|
|
11
|
+
event: 'CodexStreamDisconnected',
|
|
12
|
+
kinds: ['codex'],
|
|
13
|
+
re: /stream disconnected before completion.*error sending request for url.*\/codex\/responses/i,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
event: 'StreamDisconnected',
|
|
17
|
+
re: /stream disconnected before completion/i,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function normalizeTerminalText(text: string): string {
|
|
22
|
+
return text
|
|
23
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
24
|
+
.replace(/\s+/g, ' ')
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function classifyAgentTerminalError(text: string, kind: AgentKind): string | undefined {
|
|
29
|
+
const normalized = normalizeTerminalText(text);
|
|
30
|
+
if (!normalized) return undefined;
|
|
31
|
+
for (const pattern of ERROR_PATTERNS) {
|
|
32
|
+
if (pattern.kinds && !pattern.kinds.includes(kind)) continue;
|
|
33
|
+
if (pattern.re.test(normalized)) return pattern.event;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|