claude-remote-cli 3.0.2 → 3.0.4
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/dist/bin/claude-remote-cli.js +0 -3
- package/dist/frontend/assets/index-Bw4iKQrv.css +32 -0
- package/dist/frontend/assets/index-C9kPfx3H.js +47 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server/config.js +22 -0
- package/dist/server/git.js +193 -1
- package/dist/server/index.js +51 -292
- package/dist/server/push.js +3 -54
- package/dist/server/sessions.js +265 -180
- package/dist/server/types.js +7 -13
- package/dist/server/workspaces.js +448 -0
- package/dist/server/ws.js +31 -92
- package/dist/test/pr-state.test.js +164 -0
- package/dist/test/pull-requests.test.js +3 -3
- package/dist/test/sessions.test.js +7 -23
- package/package.json +1 -2
- package/dist/frontend/assets/index-Bz_R9N9S.css +0 -32
- package/dist/frontend/assets/index-Yv6LVq28.js +0 -47
- package/dist/server/pty-handler.js +0 -214
- package/dist/server/sdk-handler.js +0 -536
package/dist/server/sessions.js
CHANGED
|
@@ -1,93 +1,260 @@
|
|
|
1
|
+
import pty from 'node-pty';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
import { execFile } from 'node:child_process';
|
|
5
7
|
import { promisify } from 'node:util';
|
|
6
|
-
import {
|
|
7
|
-
import { createPtySession } from './pty-handler.js';
|
|
8
|
-
import { createSdkSession, killSdkSession, sendMessage as sdkSendMessage, handlePermission as sdkHandlePermission, serializeSdkSession, restoreSdkSession } from './sdk-handler.js';
|
|
8
|
+
import { readMeta, writeMeta } from './config.js';
|
|
9
9
|
const execFileAsync = promisify(execFile);
|
|
10
|
+
const AGENT_COMMANDS = {
|
|
11
|
+
claude: 'claude',
|
|
12
|
+
codex: 'codex',
|
|
13
|
+
};
|
|
14
|
+
const AGENT_CONTINUE_ARGS = {
|
|
15
|
+
claude: ['--continue'],
|
|
16
|
+
codex: ['resume', '--last'],
|
|
17
|
+
};
|
|
18
|
+
const AGENT_YOLO_ARGS = {
|
|
19
|
+
claude: ['--dangerously-skip-permissions'],
|
|
20
|
+
codex: ['--full-auto'],
|
|
21
|
+
};
|
|
10
22
|
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
23
|
+
function generateTmuxSessionName(displayName, id) {
|
|
24
|
+
const sanitized = displayName.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-+/g, '-').slice(0, 30);
|
|
25
|
+
return `crc-${sanitized}-${id.slice(0, 8)}`;
|
|
26
|
+
}
|
|
27
|
+
function resolveTmuxSpawn(command, args, tmuxSessionName) {
|
|
28
|
+
return {
|
|
29
|
+
command: 'tmux',
|
|
30
|
+
args: [
|
|
31
|
+
'-u', 'new-session', '-s', tmuxSessionName, '--', command, ...args,
|
|
32
|
+
// ';' tokens are tmux command separators — parsed at the top level before
|
|
33
|
+
// dispatching to new-session, not passed as argv to `command`.
|
|
34
|
+
';', 'set', 'set-clipboard', 'on',
|
|
35
|
+
';', 'set', 'allow-passthrough', 'on',
|
|
36
|
+
';', 'set', 'mode-keys', 'vi',
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
14
40
|
// In-memory registry: id -> Session
|
|
15
41
|
const sessions = new Map();
|
|
42
|
+
const IDLE_TIMEOUT_MS = 5000;
|
|
16
43
|
let terminalCounter = 0;
|
|
17
44
|
const idleChangeCallbacks = [];
|
|
18
45
|
function onIdleChange(cb) {
|
|
19
46
|
idleChangeCallbacks.push(cb);
|
|
20
47
|
}
|
|
21
|
-
function create({ id: providedId, type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath, useTmux: paramUseTmux, tmuxSessionName: paramTmuxSessionName, initialScrollback, restored: paramRestored }) {
|
|
48
|
+
function create({ id: providedId, type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath, useTmux: paramUseTmux, tmuxSessionName: paramTmuxSessionName, initialScrollback, restored: paramRestored, needsBranchRename: paramNeedsBranchRename, branchRenamePrompt: paramBranchRenamePrompt }) {
|
|
22
49
|
const id = providedId || crypto.randomBytes(8).toString('hex');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}, sessions, idleChangeCallbacks);
|
|
37
|
-
if (!('fallback' in sdkResult)) {
|
|
38
|
-
return { ...sdkResult.result, pid: undefined };
|
|
39
|
-
}
|
|
40
|
-
// SDK init failed — fall through to PTY
|
|
50
|
+
const createdAt = new Date().toISOString();
|
|
51
|
+
const resolvedCommand = command || AGENT_COMMANDS[agent];
|
|
52
|
+
// Strip CLAUDECODE env var to allow spawning claude inside a claude-managed server
|
|
53
|
+
const env = Object.assign({}, process.env);
|
|
54
|
+
delete env.CLAUDECODE;
|
|
55
|
+
const useTmux = !command && !!paramUseTmux;
|
|
56
|
+
let spawnCommand = resolvedCommand;
|
|
57
|
+
let spawnArgs = args;
|
|
58
|
+
const tmuxSessionName = paramTmuxSessionName || (useTmux ? generateTmuxSessionName(displayName || repoName || 'session', id) : '');
|
|
59
|
+
if (useTmux) {
|
|
60
|
+
const tmux = resolveTmuxSpawn(resolvedCommand, args, tmuxSessionName);
|
|
61
|
+
spawnCommand = tmux.command;
|
|
62
|
+
spawnArgs = tmux.args;
|
|
41
63
|
}
|
|
42
|
-
//
|
|
43
|
-
|
|
64
|
+
// Wrap the spawn command to trap SIGPIPE in the child shell.
|
|
65
|
+
// Without this, piped bash commands (e.g. `cmd | grep | tail`) run by
|
|
66
|
+
// Claude Code inside the PTY can generate SIGPIPE when the reading end
|
|
67
|
+
// of the pipe closes, which propagates up and kills the PTY session.
|
|
68
|
+
// Wrapping with `trap '' PIPE; exec ...` makes the shell ignore SIGPIPE
|
|
69
|
+
// before exec'ing the agent, so the agent inherits SIG_IGN for PIPE.
|
|
70
|
+
const wrappedCommand = '/bin/bash';
|
|
71
|
+
const wrappedArgs = ['-c', `trap '' PIPE; exec ${spawnCommand} ${spawnArgs.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`];
|
|
72
|
+
const ptyProcess = pty.spawn(wrappedCommand, wrappedArgs, {
|
|
73
|
+
name: 'xterm-256color',
|
|
74
|
+
cols,
|
|
75
|
+
rows,
|
|
76
|
+
cwd: cwd || repoPath,
|
|
77
|
+
env,
|
|
78
|
+
});
|
|
79
|
+
// Scrollback buffer: stores all PTY output so we can replay on WebSocket (re)connect
|
|
80
|
+
const scrollback = initialScrollback ? [...initialScrollback] : [];
|
|
81
|
+
let scrollbackBytes = initialScrollback ? initialScrollback.reduce((sum, s) => sum + s.length, 0) : 0;
|
|
82
|
+
const MAX_SCROLLBACK = 256 * 1024; // 256KB max
|
|
83
|
+
const resolvedCwd = cwd || repoPath;
|
|
84
|
+
const session = {
|
|
44
85
|
id,
|
|
45
|
-
type,
|
|
86
|
+
type: type || 'worktree',
|
|
46
87
|
agent,
|
|
47
|
-
|
|
88
|
+
root: root || '',
|
|
89
|
+
repoName: repoName || '',
|
|
48
90
|
repoPath,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
worktreeName,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
useTmux
|
|
60
|
-
tmuxSessionName
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
worktreeName: worktreeName || '',
|
|
92
|
+
branchName: branchName || worktreeName || '',
|
|
93
|
+
displayName: displayName || worktreeName || repoName || '',
|
|
94
|
+
pty: ptyProcess,
|
|
95
|
+
createdAt,
|
|
96
|
+
lastActivity: createdAt,
|
|
97
|
+
scrollback,
|
|
98
|
+
idle: false,
|
|
99
|
+
cwd: resolvedCwd,
|
|
100
|
+
customCommand: command || null,
|
|
101
|
+
useTmux,
|
|
102
|
+
tmuxSessionName,
|
|
103
|
+
onPtyReplacedCallbacks: [],
|
|
104
|
+
status: 'active',
|
|
105
|
+
restored: paramRestored || false,
|
|
106
|
+
needsBranchRename: paramNeedsBranchRename || false,
|
|
107
|
+
branchRenamePrompt: paramBranchRenamePrompt || '',
|
|
63
108
|
};
|
|
64
|
-
|
|
65
|
-
|
|
109
|
+
sessions.set(id, session);
|
|
110
|
+
// Load existing metadata to preserve a previously-set displayName
|
|
111
|
+
if (configPath && worktreeName) {
|
|
112
|
+
const existing = readMeta(configPath, repoPath);
|
|
113
|
+
if (existing && existing.displayName) {
|
|
114
|
+
session.displayName = existing.displayName;
|
|
115
|
+
}
|
|
116
|
+
writeMeta(configPath, { worktreePath: repoPath, displayName: session.displayName, lastActivity: createdAt });
|
|
117
|
+
}
|
|
118
|
+
let metaFlushTimer = null;
|
|
119
|
+
let idleTimer = null;
|
|
120
|
+
function resetIdleTimer() {
|
|
121
|
+
if (session.idle) {
|
|
122
|
+
session.idle = false;
|
|
123
|
+
for (const cb of idleChangeCallbacks)
|
|
124
|
+
cb(session.id, false);
|
|
125
|
+
}
|
|
126
|
+
if (idleTimer)
|
|
127
|
+
clearTimeout(idleTimer);
|
|
128
|
+
idleTimer = setTimeout(() => {
|
|
129
|
+
if (!session.idle) {
|
|
130
|
+
session.idle = true;
|
|
131
|
+
for (const cb of idleChangeCallbacks)
|
|
132
|
+
cb(session.id, true);
|
|
133
|
+
}
|
|
134
|
+
}, IDLE_TIMEOUT_MS);
|
|
135
|
+
}
|
|
136
|
+
const continueArgs = AGENT_CONTINUE_ARGS[agent];
|
|
137
|
+
function attachHandlers(proc, canRetry) {
|
|
138
|
+
const spawnTime = Date.now();
|
|
139
|
+
// Clear restored flag after 3s of running — means the PTY is healthy
|
|
140
|
+
const restoredClearTimer = session.restored ? setTimeout(() => { session.restored = false; }, 3000) : null;
|
|
141
|
+
proc.onData((data) => {
|
|
142
|
+
session.lastActivity = new Date().toISOString();
|
|
143
|
+
resetIdleTimer();
|
|
144
|
+
scrollback.push(data);
|
|
145
|
+
scrollbackBytes += data.length;
|
|
146
|
+
// Trim oldest entries if over limit
|
|
147
|
+
while (scrollbackBytes > MAX_SCROLLBACK && scrollback.length > 1) {
|
|
148
|
+
scrollbackBytes -= scrollback.shift().length;
|
|
149
|
+
}
|
|
150
|
+
if (configPath && worktreeName && !metaFlushTimer) {
|
|
151
|
+
metaFlushTimer = setTimeout(() => {
|
|
152
|
+
metaFlushTimer = null;
|
|
153
|
+
writeMeta(configPath, { worktreePath: repoPath, displayName: session.displayName, lastActivity: session.lastActivity });
|
|
154
|
+
}, 5000);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
proc.onExit(() => {
|
|
158
|
+
// If continue args failed quickly, retry without them.
|
|
159
|
+
// Exit code is intentionally not checked: tmux wrapping exits 0 even
|
|
160
|
+
// when the inner command (e.g. claude --continue) fails, because the
|
|
161
|
+
// tmux client doesn't propagate inner exit codes. The 3-second window
|
|
162
|
+
// is the primary heuristic — no user quits a session that fast.
|
|
163
|
+
if (canRetry && (Date.now() - spawnTime) < 3000) {
|
|
164
|
+
const retryArgs = args.filter(a => !continueArgs.includes(a));
|
|
165
|
+
const retryNotice = '\r\n[claude-remote-cli] --continue not available; starting new session...\r\n';
|
|
166
|
+
scrollback.length = 0;
|
|
167
|
+
scrollbackBytes = 0;
|
|
168
|
+
scrollback.push(retryNotice);
|
|
169
|
+
scrollbackBytes = retryNotice.length;
|
|
170
|
+
let retryCommand = resolvedCommand;
|
|
171
|
+
let retrySpawnArgs = retryArgs;
|
|
172
|
+
if (useTmux && tmuxSessionName) {
|
|
173
|
+
const retryTmuxName = tmuxSessionName + '-retry';
|
|
174
|
+
session.tmuxSessionName = retryTmuxName;
|
|
175
|
+
const tmux = resolveTmuxSpawn(resolvedCommand, retryArgs, retryTmuxName);
|
|
176
|
+
retryCommand = tmux.command;
|
|
177
|
+
retrySpawnArgs = tmux.args;
|
|
178
|
+
}
|
|
179
|
+
let retryPty;
|
|
180
|
+
try {
|
|
181
|
+
// Wrap retry spawn with SIGPIPE trap (same as initial spawn)
|
|
182
|
+
const retryWrapped = ['-c', `trap '' PIPE; exec ${retryCommand} ${retrySpawnArgs.map(a => `'${a.replace(/'/g, "'\\''")}'`).join(' ')}`];
|
|
183
|
+
retryPty = pty.spawn('/bin/bash', retryWrapped, {
|
|
184
|
+
name: 'xterm-256color',
|
|
185
|
+
cols,
|
|
186
|
+
rows,
|
|
187
|
+
cwd: cwd || repoPath,
|
|
188
|
+
env,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// Retry spawn failed — fall through to normal exit cleanup
|
|
193
|
+
if (restoredClearTimer)
|
|
194
|
+
clearTimeout(restoredClearTimer);
|
|
195
|
+
if (idleTimer)
|
|
196
|
+
clearTimeout(idleTimer);
|
|
197
|
+
if (metaFlushTimer)
|
|
198
|
+
clearTimeout(metaFlushTimer);
|
|
199
|
+
sessions.delete(id);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
session.pty = retryPty;
|
|
203
|
+
for (const cb of session.onPtyReplacedCallbacks)
|
|
204
|
+
cb(retryPty);
|
|
205
|
+
attachHandlers(retryPty, false);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (restoredClearTimer)
|
|
209
|
+
clearTimeout(restoredClearTimer);
|
|
210
|
+
// If PTY exited and this is a restored session, mark disconnected rather than delete
|
|
211
|
+
if (session.restored) {
|
|
212
|
+
session.status = 'disconnected';
|
|
213
|
+
session.restored = false; // clear so user-initiated kills can delete normally
|
|
214
|
+
if (idleTimer)
|
|
215
|
+
clearTimeout(idleTimer);
|
|
216
|
+
if (metaFlushTimer)
|
|
217
|
+
clearTimeout(metaFlushTimer);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (idleTimer)
|
|
221
|
+
clearTimeout(idleTimer);
|
|
222
|
+
if (metaFlushTimer)
|
|
223
|
+
clearTimeout(metaFlushTimer);
|
|
224
|
+
if (configPath && worktreeName) {
|
|
225
|
+
writeMeta(configPath, { worktreePath: repoPath, displayName: session.displayName, lastActivity: session.lastActivity });
|
|
226
|
+
}
|
|
227
|
+
sessions.delete(id);
|
|
228
|
+
const tmpDir = path.join(os.tmpdir(), 'claude-remote-cli', id);
|
|
229
|
+
fs.rm(tmpDir, { recursive: true, force: true }, () => { });
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
attachHandlers(ptyProcess, continueArgs.some(a => args.includes(a)));
|
|
233
|
+
return { id, type: session.type, agent: session.agent, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, branchName: session.branchName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false, cwd: resolvedCwd, customCommand: command || null, useTmux, tmuxSessionName, status: 'active' };
|
|
66
234
|
}
|
|
67
235
|
function get(id) {
|
|
68
236
|
return sessions.get(id);
|
|
69
237
|
}
|
|
70
238
|
function list() {
|
|
71
239
|
return Array.from(sessions.values())
|
|
72
|
-
.map((
|
|
73
|
-
id
|
|
74
|
-
type
|
|
75
|
-
agent
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
status: s.status,
|
|
240
|
+
.map(({ id, type, agent, root, repoName, repoPath, worktreeName, branchName, displayName, createdAt, lastActivity, idle, cwd, customCommand, useTmux, tmuxSessionName, status }) => ({
|
|
241
|
+
id,
|
|
242
|
+
type,
|
|
243
|
+
agent,
|
|
244
|
+
root,
|
|
245
|
+
repoName,
|
|
246
|
+
repoPath,
|
|
247
|
+
worktreeName,
|
|
248
|
+
branchName,
|
|
249
|
+
displayName,
|
|
250
|
+
createdAt,
|
|
251
|
+
lastActivity,
|
|
252
|
+
idle,
|
|
253
|
+
cwd,
|
|
254
|
+
customCommand,
|
|
255
|
+
useTmux,
|
|
256
|
+
tmuxSessionName,
|
|
257
|
+
status,
|
|
91
258
|
}))
|
|
92
259
|
.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
|
|
93
260
|
}
|
|
@@ -103,25 +270,20 @@ function kill(id) {
|
|
|
103
270
|
if (!session) {
|
|
104
271
|
throw new Error(`Session not found: ${id}`);
|
|
105
272
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
session.pty.kill('SIGTERM');
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// PTY may already be dead (e.g. disconnected sessions) — still delete from registry
|
|
112
|
-
}
|
|
113
|
-
if (session.tmuxSessionName) {
|
|
114
|
-
execFile('tmux', ['kill-session', '-t', session.tmuxSessionName], () => { });
|
|
115
|
-
}
|
|
273
|
+
try {
|
|
274
|
+
session.pty.kill('SIGTERM');
|
|
116
275
|
}
|
|
117
|
-
|
|
118
|
-
|
|
276
|
+
catch {
|
|
277
|
+
// PTY may already be dead (e.g. disconnected sessions) — still delete from registry
|
|
278
|
+
}
|
|
279
|
+
if (session.tmuxSessionName) {
|
|
280
|
+
execFile('tmux', ['kill-session', '-t', session.tmuxSessionName], () => { });
|
|
119
281
|
}
|
|
120
282
|
sessions.delete(id);
|
|
121
283
|
}
|
|
122
284
|
function killAllTmuxSessions() {
|
|
123
285
|
for (const session of sessions.values()) {
|
|
124
|
-
if (session.
|
|
286
|
+
if (session.tmuxSessionName) {
|
|
125
287
|
execFile('tmux', ['kill-session', '-t', session.tmuxSessionName], () => { });
|
|
126
288
|
}
|
|
127
289
|
}
|
|
@@ -131,25 +293,14 @@ function resize(id, cols, rows) {
|
|
|
131
293
|
if (!session) {
|
|
132
294
|
throw new Error(`Session not found: ${id}`);
|
|
133
295
|
}
|
|
134
|
-
|
|
135
|
-
session.pty.resize(cols, rows);
|
|
136
|
-
}
|
|
137
|
-
// SDK sessions don't support resize (no PTY)
|
|
296
|
+
session.pty.resize(cols, rows);
|
|
138
297
|
}
|
|
139
298
|
function write(id, data) {
|
|
140
299
|
const session = sessions.get(id);
|
|
141
300
|
if (!session) {
|
|
142
301
|
throw new Error(`Session not found: ${id}`);
|
|
143
302
|
}
|
|
144
|
-
|
|
145
|
-
session.pty.write(data);
|
|
146
|
-
}
|
|
147
|
-
else if (session.mode === 'sdk') {
|
|
148
|
-
sdkSendMessage(id, data);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function handlePermission(id, requestId, approved) {
|
|
152
|
-
sdkHandlePermission(id, requestId, approved);
|
|
303
|
+
session.pty.write(data);
|
|
153
304
|
}
|
|
154
305
|
function findRepoSession(repoPath) {
|
|
155
306
|
return list().find((s) => s.type === 'repo' && s.repoPath === repoPath);
|
|
@@ -160,40 +311,33 @@ function nextTerminalName() {
|
|
|
160
311
|
function serializeAll(configDir) {
|
|
161
312
|
const scrollbackDirPath = path.join(configDir, 'scrollback');
|
|
162
313
|
fs.mkdirSync(scrollbackDirPath, { recursive: true });
|
|
163
|
-
const
|
|
164
|
-
const serializedSdk = [];
|
|
314
|
+
const serialized = [];
|
|
165
315
|
for (const session of sessions.values()) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
else if (session.mode === 'sdk') {
|
|
189
|
-
serializedSdk.push(serializeSdkSession(session));
|
|
190
|
-
}
|
|
316
|
+
// Write scrollback to disk
|
|
317
|
+
const scrollbackPath = path.join(scrollbackDirPath, session.id + '.buf');
|
|
318
|
+
fs.writeFileSync(scrollbackPath, session.scrollback.join(''), 'utf-8');
|
|
319
|
+
serialized.push({
|
|
320
|
+
id: session.id,
|
|
321
|
+
type: session.type,
|
|
322
|
+
agent: session.agent,
|
|
323
|
+
root: session.root,
|
|
324
|
+
repoName: session.repoName,
|
|
325
|
+
repoPath: session.repoPath,
|
|
326
|
+
worktreeName: session.worktreeName,
|
|
327
|
+
branchName: session.branchName,
|
|
328
|
+
displayName: session.displayName,
|
|
329
|
+
createdAt: session.createdAt,
|
|
330
|
+
lastActivity: session.lastActivity,
|
|
331
|
+
useTmux: session.useTmux,
|
|
332
|
+
tmuxSessionName: session.tmuxSessionName,
|
|
333
|
+
customCommand: session.customCommand,
|
|
334
|
+
cwd: session.cwd,
|
|
335
|
+
});
|
|
191
336
|
}
|
|
192
337
|
const pending = {
|
|
193
338
|
version: 1,
|
|
194
339
|
timestamp: new Date().toISOString(),
|
|
195
|
-
sessions:
|
|
196
|
-
sdkSessions: serializedSdk.length > 0 ? serializedSdk : undefined,
|
|
340
|
+
sessions: serialized,
|
|
197
341
|
};
|
|
198
342
|
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending, null, 2), 'utf-8');
|
|
199
343
|
}
|
|
@@ -216,7 +360,6 @@ async function restoreFromDisk(configDir) {
|
|
|
216
360
|
}
|
|
217
361
|
const scrollbackDirPath = path.join(configDir, 'scrollback');
|
|
218
362
|
let restored = 0;
|
|
219
|
-
// Restore PTY sessions
|
|
220
363
|
for (const s of pending.sessions) {
|
|
221
364
|
// Load scrollback from disk
|
|
222
365
|
let initialScrollback;
|
|
@@ -293,18 +436,6 @@ async function restoreFromDisk(configDir) {
|
|
|
293
436
|
}
|
|
294
437
|
catch { /* ignore */ }
|
|
295
438
|
}
|
|
296
|
-
// Restore SDK sessions (as disconnected — they can't resume a live process)
|
|
297
|
-
if (pending.sdkSessions) {
|
|
298
|
-
for (const sdkData of pending.sdkSessions) {
|
|
299
|
-
try {
|
|
300
|
-
restoreSdkSession(sdkData, sessions);
|
|
301
|
-
restored++;
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
console.error(`Failed to restore SDK session ${sdkData.id} (${sdkData.displayName})`);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
439
|
// Clean up
|
|
309
440
|
try {
|
|
310
441
|
fs.unlinkSync(pendingPath);
|
|
@@ -320,55 +451,9 @@ async function restoreFromDisk(configDir) {
|
|
|
320
451
|
function activeTmuxSessionNames() {
|
|
321
452
|
const names = new Set();
|
|
322
453
|
for (const session of sessions.values()) {
|
|
323
|
-
if (session.
|
|
454
|
+
if (session.tmuxSessionName)
|
|
324
455
|
names.add(session.tmuxSessionName);
|
|
325
456
|
}
|
|
326
457
|
return names;
|
|
327
458
|
}
|
|
328
|
-
|
|
329
|
-
let sdkIdleSweepTimer = null;
|
|
330
|
-
function startSdkIdleSweep() {
|
|
331
|
-
if (sdkIdleSweepTimer)
|
|
332
|
-
return;
|
|
333
|
-
sdkIdleSweepTimer = setInterval(() => {
|
|
334
|
-
const now = Date.now();
|
|
335
|
-
const sdkSessions = [];
|
|
336
|
-
for (const session of sessions.values()) {
|
|
337
|
-
if (session.mode === 'sdk') {
|
|
338
|
-
sdkSessions.push(session);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
// Terminate sessions idle > 30 minutes
|
|
342
|
-
for (const session of sdkSessions) {
|
|
343
|
-
const lastActivity = new Date(session.lastActivity).getTime();
|
|
344
|
-
if (session.idle && (now - lastActivity) > SDK_MAX_IDLE_MS) {
|
|
345
|
-
console.log(`SDK idle sweep: terminating session ${session.id} (${session.displayName}) — idle for ${Math.round((now - lastActivity) / 60000)}min`);
|
|
346
|
-
try {
|
|
347
|
-
kill(session.id);
|
|
348
|
-
}
|
|
349
|
-
catch { /* already dead */ }
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// LRU eviction: if more than 5 idle SDK sessions remain, evict oldest
|
|
353
|
-
const idleSdkSessions = Array.from(sessions.values())
|
|
354
|
-
.filter((s) => s.mode === 'sdk' && s.idle)
|
|
355
|
-
.sort((a, b) => a.lastActivity.localeCompare(b.lastActivity));
|
|
356
|
-
while (idleSdkSessions.length > SDK_MAX_IDLE_SESSIONS) {
|
|
357
|
-
const oldest = idleSdkSessions.shift();
|
|
358
|
-
console.log(`SDK idle sweep: evicting session ${oldest.id} (${oldest.displayName}) — LRU`);
|
|
359
|
-
try {
|
|
360
|
-
kill(oldest.id);
|
|
361
|
-
}
|
|
362
|
-
catch { /* already dead */ }
|
|
363
|
-
}
|
|
364
|
-
}, SDK_IDLE_CHECK_INTERVAL_MS);
|
|
365
|
-
}
|
|
366
|
-
function stopSdkIdleSweep() {
|
|
367
|
-
if (sdkIdleSweepTimer) {
|
|
368
|
-
clearInterval(sdkIdleSweepTimer);
|
|
369
|
-
sdkIdleSweepTimer = null;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
// Re-export pty-handler utilities for backward compatibility
|
|
373
|
-
export { generateTmuxSessionName, resolveTmuxSpawn } from './pty-handler.js';
|
|
374
|
-
export { create, get, list, kill, killAllTmuxSessions, resize, updateDisplayName, write, handlePermission, onIdleChange, findRepoSession, nextTerminalName, serializeAll, restoreFromDisk, activeTmuxSessionNames, startSdkIdleSweep, stopSdkIdleSweep, AGENT_COMMANDS, AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS };
|
|
459
|
+
export { create, get, list, kill, killAllTmuxSessions, resize, updateDisplayName, write, onIdleChange, findRepoSession, nextTerminalName, serializeAll, restoreFromDisk, activeTmuxSessionNames, AGENT_COMMANDS, AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS, resolveTmuxSpawn, generateTmuxSessionName };
|
package/dist/server/types.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
codex: ['resume', '--last'],
|
|
9
|
-
};
|
|
10
|
-
export const AGENT_YOLO_ARGS = {
|
|
11
|
-
claude: ['--dangerously-skip-permissions'],
|
|
12
|
-
codex: ['--full-auto'],
|
|
13
|
-
};
|
|
1
|
+
export const MOUNTAIN_NAMES = [
|
|
2
|
+
'everest', 'kilimanjaro', 'denali', 'fuji', 'rainier', 'matterhorn',
|
|
3
|
+
'elbrus', 'aconcagua', 'kangchenjunga', 'lhotse', 'makalu', 'cho-oyu',
|
|
4
|
+
'dhaulagiri', 'manaslu', 'annapurna', 'nanga-parbat', 'olympus',
|
|
5
|
+
'mont-blanc', 'k2', 'vinson', 'erebus', 'logan', 'puncak-jaya',
|
|
6
|
+
'wilhelm', 'cook', 'ararat', 'etna', 'shasta', 'whitney', 'hood',
|
|
7
|
+
];
|