@cordfuse/llmux 0.11.0 → 0.12.1
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 +155 -75
- package/dist/index.js +3268 -78
- package/package.json +17 -4
- package/src/cli.ts +100 -0
- package/src/{client.ts → client/client.ts} +3 -3
- package/src/daemon/agents.ts +193 -0
- package/src/daemon/auth-store.ts +85 -0
- package/src/daemon/config.ts +77 -0
- package/src/daemon/handlers.ts +414 -0
- package/src/daemon/net.ts +113 -0
- package/src/daemon/state.ts +78 -0
- package/src/daemon/tmux.ts +117 -0
- package/src/daemon/token.ts +13 -0
- package/src/daemon/web/server.ts +2277 -0
- package/src/index.ts +386 -37
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export interface TmuxSession {
|
|
4
|
+
name: string;
|
|
5
|
+
windows: number;
|
|
6
|
+
attached: boolean;
|
|
7
|
+
created: Date;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TMUX_FORMAT = '#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}';
|
|
11
|
+
|
|
12
|
+
/** Verify tmux is installed; throws if not. */
|
|
13
|
+
export function requireTmux(): void {
|
|
14
|
+
const r = spawnSync('tmux', ['-V'], { stdio: 'pipe' });
|
|
15
|
+
if (r.status !== 0) {
|
|
16
|
+
throw new Error('tmux is required but was not found on PATH');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function listSessions(): TmuxSession[] {
|
|
21
|
+
const r = spawnSync('tmux', ['list-sessions', '-F', TMUX_FORMAT], { stdio: 'pipe' });
|
|
22
|
+
// exit 1 with empty stderr = no server running = no sessions
|
|
23
|
+
if (r.status !== 0) return [];
|
|
24
|
+
return r.stdout
|
|
25
|
+
.toString()
|
|
26
|
+
.trim()
|
|
27
|
+
.split('\n')
|
|
28
|
+
.filter(Boolean)
|
|
29
|
+
.map((line) => {
|
|
30
|
+
const [name, windows, attached, created] = line.split('\t');
|
|
31
|
+
return {
|
|
32
|
+
name: name ?? '',
|
|
33
|
+
windows: Number(windows ?? '0'),
|
|
34
|
+
attached: attached === '1',
|
|
35
|
+
created: new Date(Number(created ?? '0') * 1000),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hasSession(name: string): boolean {
|
|
41
|
+
const r = spawnSync('tmux', ['has-session', '-t', `=${name}`], { stdio: 'pipe' });
|
|
42
|
+
return r.status === 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface NewSessionOptions {
|
|
46
|
+
name: string;
|
|
47
|
+
command: string;
|
|
48
|
+
cwd?: string;
|
|
49
|
+
env?: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function newSession(opts: NewSessionOptions): void {
|
|
53
|
+
if (hasSession(opts.name)) {
|
|
54
|
+
throw new Error(`tmux session "${opts.name}" already exists`);
|
|
55
|
+
}
|
|
56
|
+
const args: string[] = ['new-session', '-d', '-s', opts.name];
|
|
57
|
+
if (opts.cwd) args.push('-c', opts.cwd);
|
|
58
|
+
args.push(opts.command);
|
|
59
|
+
const env = opts.env ? { ...process.env, ...opts.env } : process.env;
|
|
60
|
+
const r = spawnSync('tmux', args, { stdio: 'pipe', env });
|
|
61
|
+
if (r.status !== 0) {
|
|
62
|
+
throw new Error(`tmux new-session failed: ${r.stderr.toString().trim() || `exit ${r.status}`}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Send literal text to a session's active pane, optionally followed by Enter.
|
|
68
|
+
* Uses `-l` for literal (no key-name translation), then a separate Enter to
|
|
69
|
+
* actually submit when requested.
|
|
70
|
+
*/
|
|
71
|
+
export function sendKeys(name: string, text: string, opts: { enter?: boolean } = {}): void {
|
|
72
|
+
if (!hasSession(name)) {
|
|
73
|
+
throw new Error(`tmux session "${name}" not found`);
|
|
74
|
+
}
|
|
75
|
+
const literal = spawnSync('tmux', ['send-keys', '-t', name, '-l', text], { stdio: 'pipe' });
|
|
76
|
+
if (literal.status !== 0) {
|
|
77
|
+
throw new Error(`tmux send-keys failed: ${literal.stderr.toString().trim() || `exit ${literal.status}`}`);
|
|
78
|
+
}
|
|
79
|
+
if (opts.enter) {
|
|
80
|
+
const enter = spawnSync('tmux', ['send-keys', '-t', name, 'Enter'], { stdio: 'pipe' });
|
|
81
|
+
if (enter.status !== 0) {
|
|
82
|
+
throw new Error(`tmux send-keys Enter failed: ${enter.stderr.toString().trim() || `exit ${enter.status}`}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function killSession(name: string): void {
|
|
88
|
+
if (!hasSession(name)) return;
|
|
89
|
+
const r = spawnSync('tmux', ['kill-session', '-t', name], { stdio: 'pipe' });
|
|
90
|
+
if (r.status !== 0) {
|
|
91
|
+
throw new Error(`tmux kill-session failed: ${r.stderr.toString().trim() || `exit ${r.status}`}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function renameSession(oldName: string, newName: string): void {
|
|
96
|
+
if (!hasSession(oldName)) return;
|
|
97
|
+
const r = spawnSync('tmux', ['rename-session', '-t', oldName, newName], { stdio: 'pipe' });
|
|
98
|
+
if (r.status !== 0) {
|
|
99
|
+
throw new Error(`tmux rename-session failed: ${r.stderr.toString().trim() || `exit ${r.status}`}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Attach interactively. Inside a tmux client → `switch-client`. Outside → `attach`.
|
|
105
|
+
* Inherits the controlling TTY so the user takes over the terminal.
|
|
106
|
+
*/
|
|
107
|
+
export function attachOrSwitch(name: string): void {
|
|
108
|
+
if (!hasSession(name)) {
|
|
109
|
+
throw new Error(`tmux session "${name}" not found`);
|
|
110
|
+
}
|
|
111
|
+
const inTmux = Boolean(process.env.TMUX);
|
|
112
|
+
const verb = inTmux ? 'switch-client' : 'attach-session';
|
|
113
|
+
const r = spawnSync('tmux', [verb, '-t', name], { stdio: 'inherit' });
|
|
114
|
+
if (r.status !== 0) {
|
|
115
|
+
throw new Error(`tmux ${verb} exited with status ${r.status}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const TOKEN_PREFIX = 'sas_';
|
|
4
|
+
|
|
5
|
+
/** Generate a fresh SAS token: `sas_<43-char-base64url>`. */
|
|
6
|
+
export function generateToken(): string {
|
|
7
|
+
return TOKEN_PREFIX + randomBytes(32).toString('base64url');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Stable short id for a token: 8 chars after the `sas_` prefix. */
|
|
11
|
+
export function tokenId(token: string): string {
|
|
12
|
+
return token.startsWith(TOKEN_PREFIX) ? token.slice(TOKEN_PREFIX.length, TOKEN_PREFIX.length + 8) : token.slice(0, 8);
|
|
13
|
+
}
|