claude-remote-cli 3.0.6 → 3.0.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/dist/bin/claude-remote-cli.js +3 -0
- package/dist/frontend/assets/{index-BBvs0auR.js → index-De_IzAmR.js} +17 -17
- package/dist/frontend/assets/{index-CVH0jxa8.css → index-yTmvRrnt.css} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/server/index.js +75 -6
- package/dist/server/pty-handler.js +216 -0
- package/dist/server/push.js +54 -3
- package/dist/server/sdk-handler.js +539 -0
- package/dist/server/sessions.js +191 -263
- package/dist/server/types.js +13 -0
- package/dist/server/ws.js +159 -32
- package/dist/test/branch-rename.test.js +28 -0
- package/dist/test/sessions.test.js +23 -7
- package/package.json +2 -1
package/dist/server/ws.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
2
4
|
import * as sessions from './sessions.js';
|
|
5
|
+
import { onSdkEvent, sendMessage as sdkSendMessage, handlePermission as sdkHandlePermission } from './sdk-handler.js';
|
|
6
|
+
import { writeMeta } from './config.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const BACKPRESSURE_HIGH = 1024 * 1024; // 1MB
|
|
9
|
+
const BACKPRESSURE_LOW = 512 * 1024; // 512KB
|
|
10
|
+
const RENAME_CORE = `rename the current git branch using \`git branch -m <new-name>\` to a short, descriptive kebab-case name based on the task I'm asking about. Do not include any ticket numbers or prefixes.`;
|
|
11
|
+
// Prepended to the user's first message in SDK mode
|
|
12
|
+
const SDK_BRANCH_RENAME_INSTRUCTION = `Before responding to my message, first ${RENAME_CORE} After renaming, proceed with my request normally.\n\n`;
|
|
13
|
+
// Sent as a standalone first message in PTY mode, before the user types
|
|
14
|
+
const PTY_BRANCH_RENAME_INSTRUCTION = `When I send my next message, before responding to it, first ${RENAME_CORE} After renaming, proceed with my request normally. Reply with only "Ready." and nothing else.`;
|
|
3
15
|
function parseCookies(cookieHeader) {
|
|
4
16
|
const cookies = {};
|
|
5
17
|
if (!cookieHeader)
|
|
@@ -14,7 +26,39 @@ function parseCookies(cookieHeader) {
|
|
|
14
26
|
});
|
|
15
27
|
return cookies;
|
|
16
28
|
}
|
|
17
|
-
|
|
29
|
+
const BRANCH_POLL_INTERVAL_MS = 3000;
|
|
30
|
+
const BRANCH_POLL_MAX_ATTEMPTS = 10;
|
|
31
|
+
function startBranchWatcher(session, broadcastEvent, configPath) {
|
|
32
|
+
const originalBranch = session.branchName;
|
|
33
|
+
let attempts = 0;
|
|
34
|
+
const timer = setInterval(async () => {
|
|
35
|
+
attempts++;
|
|
36
|
+
if (attempts > BRANCH_POLL_MAX_ATTEMPTS) {
|
|
37
|
+
clearInterval(timer);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: session.cwd });
|
|
42
|
+
const currentBranch = stdout.trim();
|
|
43
|
+
if (currentBranch && currentBranch !== originalBranch) {
|
|
44
|
+
clearInterval(timer);
|
|
45
|
+
session.branchName = currentBranch;
|
|
46
|
+
session.displayName = currentBranch;
|
|
47
|
+
broadcastEvent('session-renamed', { sessionId: session.id, branchName: currentBranch, displayName: currentBranch });
|
|
48
|
+
writeMeta(configPath, {
|
|
49
|
+
worktreePath: session.repoPath,
|
|
50
|
+
displayName: currentBranch,
|
|
51
|
+
lastActivity: new Date().toISOString(),
|
|
52
|
+
branchName: currentBranch,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// git command failed — session cwd may not exist yet, retry
|
|
58
|
+
}
|
|
59
|
+
}, BRANCH_POLL_INTERVAL_MS);
|
|
60
|
+
}
|
|
61
|
+
function setupWebSocket(server, authenticatedTokens, watcher, configPath) {
|
|
18
62
|
const wss = new WebSocketServer({ noServer: true });
|
|
19
63
|
const eventClients = new Set();
|
|
20
64
|
function broadcastEvent(type, data) {
|
|
@@ -47,7 +91,7 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
|
|
|
47
91
|
});
|
|
48
92
|
return;
|
|
49
93
|
}
|
|
50
|
-
// PTY channel: /ws/:sessionId
|
|
94
|
+
// PTY/SDK channel: /ws/:sessionId
|
|
51
95
|
const match = request.url && request.url.match(/^\/ws\/([a-f0-9]+)$/);
|
|
52
96
|
if (!match) {
|
|
53
97
|
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
@@ -71,6 +115,16 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
|
|
|
71
115
|
const session = sessionMap.get(ws);
|
|
72
116
|
if (!session)
|
|
73
117
|
return;
|
|
118
|
+
if (session.mode === 'sdk') {
|
|
119
|
+
handleSdkConnection(ws, session);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// PTY mode — existing behavior
|
|
123
|
+
if (session.mode !== 'pty') {
|
|
124
|
+
ws.close(1008, 'Session mode does not support PTY streaming');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const ptySession = session;
|
|
74
128
|
let dataDisposable = null;
|
|
75
129
|
let exitDisposable = null;
|
|
76
130
|
function attachToPty(ptyProcess) {
|
|
@@ -78,7 +132,7 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
|
|
|
78
132
|
dataDisposable?.dispose();
|
|
79
133
|
exitDisposable?.dispose();
|
|
80
134
|
// Replay scrollback
|
|
81
|
-
for (const chunk of
|
|
135
|
+
for (const chunk of ptySession.scrollback) {
|
|
82
136
|
if (ws.readyState === ws.OPEN)
|
|
83
137
|
ws.send(chunk);
|
|
84
138
|
}
|
|
@@ -91,52 +145,125 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
|
|
|
91
145
|
ws.close(1000);
|
|
92
146
|
});
|
|
93
147
|
}
|
|
94
|
-
attachToPty(
|
|
148
|
+
attachToPty(ptySession.pty);
|
|
149
|
+
// For PTY sessions needing branch rename, send the rename instruction once Claude CLI is ready.
|
|
150
|
+
// We watch for PTY idle (Claude shows its prompt and waits for input) as the trigger.
|
|
151
|
+
let pendingIdleHandler = null;
|
|
152
|
+
if (ptySession.needsBranchRename) {
|
|
153
|
+
ptySession.needsBranchRename = false;
|
|
154
|
+
const idleHandler = (sessionId, idle) => {
|
|
155
|
+
if (idle && sessionId === ptySession.id) {
|
|
156
|
+
sessions.offIdleChange(idleHandler);
|
|
157
|
+
pendingIdleHandler = null;
|
|
158
|
+
ptySession.pty.write(PTY_BRANCH_RENAME_INSTRUCTION + '\r');
|
|
159
|
+
startBranchWatcher(ptySession, broadcastEvent, configPath);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
pendingIdleHandler = idleHandler;
|
|
163
|
+
sessions.onIdleChange(idleHandler);
|
|
164
|
+
}
|
|
95
165
|
const ptyReplacedHandler = (newPty) => attachToPty(newPty);
|
|
96
|
-
|
|
166
|
+
ptySession.onPtyReplacedCallbacks.push(ptyReplacedHandler);
|
|
97
167
|
ws.on('message', (msg) => {
|
|
98
168
|
const str = msg.toString();
|
|
99
169
|
try {
|
|
100
170
|
const parsed = JSON.parse(str);
|
|
101
171
|
if (parsed.type === 'resize' && parsed.cols && parsed.rows) {
|
|
102
|
-
sessions.resize(
|
|
172
|
+
sessions.resize(ptySession.id, parsed.cols, parsed.rows);
|
|
103
173
|
return;
|
|
104
174
|
}
|
|
105
175
|
}
|
|
106
176
|
catch (_) { }
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
if (!session._renameBuffer)
|
|
110
|
-
session._renameBuffer = '';
|
|
111
|
-
const enterIndex = str.indexOf('\r');
|
|
112
|
-
if (enterIndex === -1) {
|
|
113
|
-
// No Enter yet — buffer and pass through so the user sees echo
|
|
114
|
-
session._renameBuffer += str;
|
|
115
|
-
session.pty.write(str);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
// Enter detected — inject rename prompt before the user's message
|
|
119
|
-
const buffered = session._renameBuffer;
|
|
120
|
-
const beforeEnter = buffered + str.slice(0, enterIndex);
|
|
121
|
-
const afterEnter = str.slice(enterIndex); // includes the \r
|
|
122
|
-
const renamePrompt = `Before doing anything else, rename the current git branch using \`git branch -m <new-name>\`. Choose a short, descriptive kebab-case branch name based on the task below.${session.branchRenamePrompt ? ' User preferences: ' + session.branchRenamePrompt : ''} Do not ask for confirmation — just rename and proceed.\n\n`;
|
|
123
|
-
const clearLine = '\x15'; // Ctrl+U clears the current input line
|
|
124
|
-
session.pty.write(clearLine + renamePrompt + beforeEnter + afterEnter);
|
|
125
|
-
session.needsBranchRename = false;
|
|
126
|
-
delete session._renameBuffer;
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
// Use session.pty dynamically so writes go to current PTY
|
|
130
|
-
session.pty.write(str);
|
|
177
|
+
// Use ptySession.pty dynamically so writes go to current PTY
|
|
178
|
+
ptySession.pty.write(str);
|
|
131
179
|
});
|
|
132
180
|
ws.on('close', () => {
|
|
133
181
|
dataDisposable?.dispose();
|
|
134
182
|
exitDisposable?.dispose();
|
|
135
|
-
|
|
183
|
+
if (pendingIdleHandler)
|
|
184
|
+
sessions.offIdleChange(pendingIdleHandler);
|
|
185
|
+
const idx = ptySession.onPtyReplacedCallbacks.indexOf(ptyReplacedHandler);
|
|
136
186
|
if (idx !== -1)
|
|
137
|
-
|
|
187
|
+
ptySession.onPtyReplacedCallbacks.splice(idx, 1);
|
|
138
188
|
});
|
|
139
189
|
});
|
|
190
|
+
function handleSdkConnection(ws, session) {
|
|
191
|
+
// Send session info
|
|
192
|
+
const sessionInfo = JSON.stringify({
|
|
193
|
+
type: 'session_info',
|
|
194
|
+
mode: 'sdk',
|
|
195
|
+
sessionId: session.id,
|
|
196
|
+
});
|
|
197
|
+
if (ws.readyState === ws.OPEN)
|
|
198
|
+
ws.send(sessionInfo);
|
|
199
|
+
// Replay stored events (send as-is — client expects raw SdkEvent shape)
|
|
200
|
+
for (const event of session.events) {
|
|
201
|
+
if (ws.readyState !== ws.OPEN)
|
|
202
|
+
break;
|
|
203
|
+
ws.send(JSON.stringify(event));
|
|
204
|
+
}
|
|
205
|
+
// Subscribe to live events with backpressure
|
|
206
|
+
let paused = false;
|
|
207
|
+
const unsubscribe = onSdkEvent(session.id, (event) => {
|
|
208
|
+
if (ws.readyState !== ws.OPEN)
|
|
209
|
+
return;
|
|
210
|
+
// Backpressure check
|
|
211
|
+
if (ws.bufferedAmount > BACKPRESSURE_HIGH) {
|
|
212
|
+
paused = true;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
ws.send(JSON.stringify(event));
|
|
216
|
+
});
|
|
217
|
+
// Periodically check if we can resume
|
|
218
|
+
const backpressureInterval = setInterval(() => {
|
|
219
|
+
if (paused && ws.bufferedAmount < BACKPRESSURE_LOW) {
|
|
220
|
+
paused = false;
|
|
221
|
+
}
|
|
222
|
+
}, 100);
|
|
223
|
+
// Handle incoming messages
|
|
224
|
+
ws.on('message', (msg) => {
|
|
225
|
+
const str = msg.toString();
|
|
226
|
+
try {
|
|
227
|
+
const parsed = JSON.parse(str);
|
|
228
|
+
if (parsed.type === 'message' && typeof parsed.text === 'string') {
|
|
229
|
+
if (parsed.text.length > 100_000)
|
|
230
|
+
return;
|
|
231
|
+
if (session.needsBranchRename) {
|
|
232
|
+
session.needsBranchRename = false;
|
|
233
|
+
sdkSendMessage(session.id, SDK_BRANCH_RENAME_INSTRUCTION + parsed.text);
|
|
234
|
+
startBranchWatcher(session, broadcastEvent, configPath);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
sdkSendMessage(session.id, parsed.text);
|
|
238
|
+
}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (parsed.type === 'permission' && typeof parsed.requestId === 'string' && typeof parsed.approved === 'boolean') {
|
|
242
|
+
sdkHandlePermission(session.id, parsed.requestId, parsed.approved);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (parsed.type === 'resize' && typeof parsed.cols === 'number' && typeof parsed.rows === 'number') {
|
|
246
|
+
// TODO: wire up companion shell — currently open_companion message is unhandled server-side
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (parsed.type === 'open_companion') {
|
|
250
|
+
// TODO: spawn companion PTY in session CWD and relay via terminal_data/terminal_exit frames
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (_) {
|
|
255
|
+
// Not JSON — ignore for SDK sessions
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
ws.on('close', () => {
|
|
259
|
+
unsubscribe();
|
|
260
|
+
clearInterval(backpressureInterval);
|
|
261
|
+
});
|
|
262
|
+
ws.on('error', () => {
|
|
263
|
+
unsubscribe();
|
|
264
|
+
clearInterval(backpressureInterval);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
140
267
|
sessions.onIdleChange((sessionId, idle) => {
|
|
141
268
|
broadcastEvent('session-idle-changed', { sessionId, idle });
|
|
142
269
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { MOUNTAIN_NAMES } from '../server/types.js';
|
|
4
|
+
describe('MOUNTAIN_NAMES', () => {
|
|
5
|
+
test('contains 30 mountain names', () => {
|
|
6
|
+
assert.equal(MOUNTAIN_NAMES.length, 30);
|
|
7
|
+
});
|
|
8
|
+
test('all names are lowercase kebab-case', () => {
|
|
9
|
+
for (const name of MOUNTAIN_NAMES) {
|
|
10
|
+
assert.match(name, /^[a-z][a-z0-9-]*$/, `Mountain name "${name}" is not kebab-case`);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
test('no duplicate names', () => {
|
|
14
|
+
const unique = new Set(MOUNTAIN_NAMES);
|
|
15
|
+
assert.equal(unique.size, MOUNTAIN_NAMES.length);
|
|
16
|
+
});
|
|
17
|
+
test('cycling wraps around at array length', () => {
|
|
18
|
+
let idx = 28;
|
|
19
|
+
const name1 = MOUNTAIN_NAMES[idx % MOUNTAIN_NAMES.length];
|
|
20
|
+
idx++;
|
|
21
|
+
const name2 = MOUNTAIN_NAMES[idx % MOUNTAIN_NAMES.length];
|
|
22
|
+
idx++;
|
|
23
|
+
const name3 = MOUNTAIN_NAMES[idx % MOUNTAIN_NAMES.length];
|
|
24
|
+
assert.equal(name1, 'whitney');
|
|
25
|
+
assert.equal(name2, 'hood');
|
|
26
|
+
assert.equal(name3, 'everest'); // wraps back to start
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -60,6 +60,7 @@ describe('sessions', () => {
|
|
|
60
60
|
assert.ok(session, 'should return the session');
|
|
61
61
|
assert.strictEqual(session.id, result.id);
|
|
62
62
|
assert.strictEqual(session.repoName, 'test-repo');
|
|
63
|
+
assert.strictEqual(session.mode, 'pty');
|
|
63
64
|
assert.ok(session.pty, 'get should include the pty object');
|
|
64
65
|
});
|
|
65
66
|
it('get returns undefined for nonexistent id', () => {
|
|
@@ -100,8 +101,10 @@ describe('sessions', () => {
|
|
|
100
101
|
createdIds.push(result.id);
|
|
101
102
|
const session = sessions.get(result.id);
|
|
102
103
|
assert.ok(session);
|
|
104
|
+
assert.strictEqual(session.mode, 'pty');
|
|
105
|
+
const ptySession = session;
|
|
103
106
|
let output = '';
|
|
104
|
-
|
|
107
|
+
ptySession.pty.onData((data) => {
|
|
105
108
|
output += data;
|
|
106
109
|
if (output.includes('hello')) {
|
|
107
110
|
done();
|
|
@@ -388,9 +391,11 @@ describe('sessions', () => {
|
|
|
388
391
|
createdIds.push(result.id);
|
|
389
392
|
const session = sessions.get(result.id);
|
|
390
393
|
assert.ok(session);
|
|
391
|
-
session.
|
|
394
|
+
assert.strictEqual(session.mode, 'pty');
|
|
395
|
+
const ptySession = session;
|
|
396
|
+
ptySession.onPtyReplacedCallbacks.push((newPty) => {
|
|
392
397
|
assert.ok(newPty, 'should receive new PTY');
|
|
393
|
-
assert.strictEqual(
|
|
398
|
+
assert.strictEqual(ptySession.pty, newPty, 'session.pty should be updated to new PTY');
|
|
394
399
|
done();
|
|
395
400
|
});
|
|
396
401
|
});
|
|
@@ -404,7 +409,9 @@ describe('sessions', () => {
|
|
|
404
409
|
createdIds.push(result.id);
|
|
405
410
|
const session = sessions.get(result.id);
|
|
406
411
|
assert.ok(session);
|
|
407
|
-
session.
|
|
412
|
+
assert.strictEqual(session.mode, 'pty');
|
|
413
|
+
const ptySession = session;
|
|
414
|
+
ptySession.onPtyReplacedCallbacks.push(() => {
|
|
408
415
|
const stillExists = sessions.get(result.id);
|
|
409
416
|
assert.ok(stillExists, 'session should still exist after retry');
|
|
410
417
|
done();
|
|
@@ -420,9 +427,11 @@ describe('sessions', () => {
|
|
|
420
427
|
createdIds.push(result.id);
|
|
421
428
|
const session = sessions.get(result.id);
|
|
422
429
|
assert.ok(session);
|
|
423
|
-
session.
|
|
430
|
+
assert.strictEqual(session.mode, 'pty');
|
|
431
|
+
const ptySession = session;
|
|
432
|
+
ptySession.onPtyReplacedCallbacks.push((newPty) => {
|
|
424
433
|
assert.ok(newPty, 'should receive new PTY even with exit code 0');
|
|
425
|
-
assert.strictEqual(
|
|
434
|
+
assert.strictEqual(ptySession.pty, newPty, 'session.pty should be updated');
|
|
426
435
|
const stillExists = sessions.get(result.id);
|
|
427
436
|
assert.ok(stillExists, 'session should still exist after retry');
|
|
428
437
|
done();
|
|
@@ -452,6 +461,7 @@ describe('sessions', () => {
|
|
|
452
461
|
createdIds.push(result.id);
|
|
453
462
|
const session = sessions.get(result.id);
|
|
454
463
|
assert.ok(session);
|
|
464
|
+
assert.strictEqual(session.mode, 'pty');
|
|
455
465
|
assert.ok(session.scrollback.length >= 1);
|
|
456
466
|
assert.strictEqual(session.scrollback[0], 'prior output\r\n');
|
|
457
467
|
});
|
|
@@ -489,6 +499,7 @@ describe('session persistence', () => {
|
|
|
489
499
|
// Manually push some scrollback
|
|
490
500
|
const session = sessions.get(s.id);
|
|
491
501
|
assert.ok(session);
|
|
502
|
+
assert.strictEqual(session.mode, 'pty');
|
|
492
503
|
session.scrollback.push('hello world');
|
|
493
504
|
serializeAll(configDir);
|
|
494
505
|
// Check pending-sessions.json
|
|
@@ -519,6 +530,7 @@ describe('session persistence', () => {
|
|
|
519
530
|
const originalId = s.id;
|
|
520
531
|
const session = sessions.get(originalId);
|
|
521
532
|
assert.ok(session);
|
|
533
|
+
assert.strictEqual(session.mode, 'pty');
|
|
522
534
|
session.scrollback.push('saved output');
|
|
523
535
|
serializeAll(configDir);
|
|
524
536
|
// Kill the original session
|
|
@@ -533,6 +545,7 @@ describe('session persistence', () => {
|
|
|
533
545
|
assert.strictEqual(restoredSession.repoPath, '/tmp');
|
|
534
546
|
assert.strictEqual(restoredSession.displayName, 'my-session');
|
|
535
547
|
// Scrollback should be restored
|
|
548
|
+
assert.strictEqual(restoredSession.mode, 'pty');
|
|
536
549
|
assert.ok(restoredSession.scrollback.length >= 1);
|
|
537
550
|
assert.strictEqual(restoredSession.scrollback[0], 'saved output');
|
|
538
551
|
// pending-sessions.json should be cleaned up
|
|
@@ -597,7 +610,7 @@ describe('session persistence', () => {
|
|
|
597
610
|
lastActivity: new Date().toISOString(),
|
|
598
611
|
useTmux: true,
|
|
599
612
|
tmuxSessionName: 'crc-my-session-tmux-tes',
|
|
600
|
-
customCommand:
|
|
613
|
+
customCommand: '/bin/cat', // Use /bin/cat to avoid spawning real claude binary in test
|
|
601
614
|
cwd: '/tmp',
|
|
602
615
|
}],
|
|
603
616
|
};
|
|
@@ -606,6 +619,7 @@ describe('session persistence', () => {
|
|
|
606
619
|
assert.strictEqual(restored, 1);
|
|
607
620
|
const session = sessions.get('tmux-test-id');
|
|
608
621
|
assert.ok(session, 'restored session should exist');
|
|
622
|
+
assert.strictEqual(session.mode, 'pty');
|
|
609
623
|
assert.strictEqual(session.tmuxSessionName, 'crc-my-session-tmux-tes', 'tmuxSessionName should be preserved from serialized data');
|
|
610
624
|
});
|
|
611
625
|
it('restored session remains in list after PTY exits (disconnected status)', async () => {
|
|
@@ -705,6 +719,7 @@ describe('session persistence', () => {
|
|
|
705
719
|
// Verify tmux session name survived the round trip
|
|
706
720
|
const restoredTmux = sessions.get('tmux-roundtrip-id');
|
|
707
721
|
assert.ok(restoredTmux);
|
|
722
|
+
assert.strictEqual(restoredTmux.mode, 'pty');
|
|
708
723
|
assert.strictEqual(restoredTmux.tmuxSessionName, 'crc-tmux-session-tmux-rou');
|
|
709
724
|
assert.strictEqual(restoredTmux.displayName, 'Tmux Session');
|
|
710
725
|
});
|
|
@@ -719,6 +734,7 @@ describe('session persistence', () => {
|
|
|
719
734
|
});
|
|
720
735
|
const session = sessions.get(s.id);
|
|
721
736
|
assert.ok(session);
|
|
737
|
+
assert.strictEqual(session.mode, 'pty');
|
|
722
738
|
session.scrollback.push('important output');
|
|
723
739
|
serializeAll(configDir);
|
|
724
740
|
// Kill after serialize (mimics gracefulShutdown sequence)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-remote-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9",
|
|
4
4
|
"description": "Remote web interface for Claude Code CLI sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server/index.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"author": "Donovan Yohan",
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.77",
|
|
45
46
|
"@tanstack/svelte-query": "^6.0.18",
|
|
46
47
|
"@xterm/addon-fit": "^0.11.0",
|
|
47
48
|
"@xterm/xterm": "^6.0.0",
|