claude-remote-cli 2.15.16 → 3.0.3
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-BgOmCV-k.css +32 -0
- package/dist/frontend/assets/index-CKQHbnTN.js +47 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server/index.js +66 -2
- package/dist/server/pty-handler.js +214 -0
- package/dist/server/push.js +54 -3
- package/dist/server/sdk-handler.js +536 -0
- package/dist/server/sessions.js +183 -230
- package/dist/server/types.js +13 -1
- package/dist/server/ws.js +92 -9
- package/dist/test/sessions.test.js +175 -6
- package/package.json +3 -2
- package/dist/frontend/assets/index-DQ-fMetm.js +0 -47
- package/dist/frontend/assets/index-XlU0yxtO.css +0 -32
|
@@ -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
|
|
@@ -577,4 +590,160 @@ describe('session persistence', () => {
|
|
|
577
590
|
const restored = await restoreFromDisk(configDir);
|
|
578
591
|
assert.strictEqual(restored, 0);
|
|
579
592
|
});
|
|
593
|
+
it('restoreFromDisk preserves tmuxSessionName for tmux sessions', async () => {
|
|
594
|
+
const configDir = createTmpDir();
|
|
595
|
+
// Write a pending file with a tmux session
|
|
596
|
+
const pending = {
|
|
597
|
+
version: 1,
|
|
598
|
+
timestamp: new Date().toISOString(),
|
|
599
|
+
sessions: [{
|
|
600
|
+
id: 'tmux-test-id',
|
|
601
|
+
type: 'worktree',
|
|
602
|
+
agent: 'claude',
|
|
603
|
+
root: '',
|
|
604
|
+
repoName: 'test-repo',
|
|
605
|
+
repoPath: '/tmp',
|
|
606
|
+
worktreeName: 'my-wt',
|
|
607
|
+
branchName: 'my-branch',
|
|
608
|
+
displayName: 'my-session',
|
|
609
|
+
createdAt: new Date().toISOString(),
|
|
610
|
+
lastActivity: new Date().toISOString(),
|
|
611
|
+
useTmux: true,
|
|
612
|
+
tmuxSessionName: 'crc-my-session-tmux-tes',
|
|
613
|
+
customCommand: '/bin/cat', // Use /bin/cat to avoid spawning real claude binary in test
|
|
614
|
+
cwd: '/tmp',
|
|
615
|
+
}],
|
|
616
|
+
};
|
|
617
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
618
|
+
const restored = await restoreFromDisk(configDir);
|
|
619
|
+
assert.strictEqual(restored, 1);
|
|
620
|
+
const session = sessions.get('tmux-test-id');
|
|
621
|
+
assert.ok(session, 'restored session should exist');
|
|
622
|
+
assert.strictEqual(session.mode, 'pty');
|
|
623
|
+
assert.strictEqual(session.tmuxSessionName, 'crc-my-session-tmux-tes', 'tmuxSessionName should be preserved from serialized data');
|
|
624
|
+
});
|
|
625
|
+
it('restored session remains in list after PTY exits (disconnected status)', async () => {
|
|
626
|
+
const configDir = createTmpDir();
|
|
627
|
+
const pending = {
|
|
628
|
+
version: 1,
|
|
629
|
+
timestamp: new Date().toISOString(),
|
|
630
|
+
sessions: [{
|
|
631
|
+
id: 'restore-exit-test',
|
|
632
|
+
type: 'worktree',
|
|
633
|
+
agent: 'claude',
|
|
634
|
+
root: '',
|
|
635
|
+
repoName: 'test-repo',
|
|
636
|
+
repoPath: '/tmp',
|
|
637
|
+
worktreeName: 'my-wt',
|
|
638
|
+
branchName: 'my-branch',
|
|
639
|
+
displayName: 'restored-session',
|
|
640
|
+
createdAt: new Date().toISOString(),
|
|
641
|
+
lastActivity: new Date().toISOString(),
|
|
642
|
+
useTmux: false,
|
|
643
|
+
tmuxSessionName: '',
|
|
644
|
+
customCommand: '/bin/false',
|
|
645
|
+
cwd: '/tmp',
|
|
646
|
+
}],
|
|
647
|
+
};
|
|
648
|
+
fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending));
|
|
649
|
+
await restoreFromDisk(configDir);
|
|
650
|
+
// Wait for PTY to exit
|
|
651
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
652
|
+
// Session should still be in the list with disconnected status
|
|
653
|
+
const list = sessions.list();
|
|
654
|
+
const found = list.find(s => s.id === 'restore-exit-test');
|
|
655
|
+
assert.ok(found, 'restored session should remain in list after PTY exit');
|
|
656
|
+
assert.strictEqual(found.status, 'disconnected');
|
|
657
|
+
});
|
|
658
|
+
it('full serialize-restore round trip preserves all session fields including tmuxSessionName', async () => {
|
|
659
|
+
const configDir = createTmpDir();
|
|
660
|
+
// Create sessions of different types
|
|
661
|
+
const repo = sessions.create({
|
|
662
|
+
type: 'repo',
|
|
663
|
+
repoName: 'my-repo',
|
|
664
|
+
repoPath: '/tmp/repo',
|
|
665
|
+
command: '/bin/cat',
|
|
666
|
+
args: [],
|
|
667
|
+
displayName: 'My Repo',
|
|
668
|
+
});
|
|
669
|
+
const terminal = sessions.create({
|
|
670
|
+
type: 'terminal',
|
|
671
|
+
repoPath: '/tmp',
|
|
672
|
+
command: '/bin/sh',
|
|
673
|
+
args: [],
|
|
674
|
+
displayName: 'Terminal 1',
|
|
675
|
+
});
|
|
676
|
+
// Serialize all
|
|
677
|
+
serializeAll(configDir);
|
|
678
|
+
// Kill originals
|
|
679
|
+
sessions.kill(repo.id);
|
|
680
|
+
sessions.kill(terminal.id);
|
|
681
|
+
assert.strictEqual(sessions.list().length, 0);
|
|
682
|
+
// Also inject a tmux-style session into the pending file to test tmuxSessionName round-trip.
|
|
683
|
+
// Use customCommand so restore spawns that instead of claude --continue (which would exit instantly).
|
|
684
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
685
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
686
|
+
pending.sessions.push({
|
|
687
|
+
id: 'tmux-roundtrip-id',
|
|
688
|
+
type: 'worktree',
|
|
689
|
+
agent: 'claude',
|
|
690
|
+
root: '',
|
|
691
|
+
repoName: 'tmux-repo',
|
|
692
|
+
repoPath: '/tmp',
|
|
693
|
+
worktreeName: 'tmux-wt',
|
|
694
|
+
branchName: 'feat/tmux',
|
|
695
|
+
displayName: 'Tmux Session',
|
|
696
|
+
createdAt: new Date().toISOString(),
|
|
697
|
+
lastActivity: new Date().toISOString(),
|
|
698
|
+
useTmux: true,
|
|
699
|
+
tmuxSessionName: 'crc-tmux-session-tmux-rou',
|
|
700
|
+
customCommand: '/bin/cat',
|
|
701
|
+
cwd: '/tmp',
|
|
702
|
+
});
|
|
703
|
+
fs.writeFileSync(pendingPath, JSON.stringify(pending));
|
|
704
|
+
// Restore
|
|
705
|
+
const restored = await restoreFromDisk(configDir);
|
|
706
|
+
assert.strictEqual(restored, 3);
|
|
707
|
+
// Verify all sessions exist
|
|
708
|
+
const list = sessions.list();
|
|
709
|
+
assert.strictEqual(list.length, 3);
|
|
710
|
+
const restoredRepo = list.find(s => s.id === repo.id);
|
|
711
|
+
assert.ok(restoredRepo);
|
|
712
|
+
assert.strictEqual(restoredRepo.type, 'repo');
|
|
713
|
+
assert.strictEqual(restoredRepo.displayName, 'My Repo');
|
|
714
|
+
assert.strictEqual(restoredRepo.status, 'active');
|
|
715
|
+
const restoredTerminal = list.find(s => s.id === terminal.id);
|
|
716
|
+
assert.ok(restoredTerminal);
|
|
717
|
+
assert.strictEqual(restoredTerminal.type, 'terminal');
|
|
718
|
+
assert.strictEqual(restoredTerminal.displayName, 'Terminal 1');
|
|
719
|
+
// Verify tmux session name survived the round trip
|
|
720
|
+
const restoredTmux = sessions.get('tmux-roundtrip-id');
|
|
721
|
+
assert.ok(restoredTmux);
|
|
722
|
+
assert.strictEqual(restoredTmux.mode, 'pty');
|
|
723
|
+
assert.strictEqual(restoredTmux.tmuxSessionName, 'crc-tmux-session-tmux-rou');
|
|
724
|
+
assert.strictEqual(restoredTmux.displayName, 'Tmux Session');
|
|
725
|
+
});
|
|
726
|
+
it('serializeAll captures session state before kill', () => {
|
|
727
|
+
const configDir = createTmpDir();
|
|
728
|
+
const s = sessions.create({
|
|
729
|
+
repoName: 'test-repo',
|
|
730
|
+
repoPath: '/tmp',
|
|
731
|
+
command: '/bin/cat',
|
|
732
|
+
args: [],
|
|
733
|
+
displayName: 'before-kill',
|
|
734
|
+
});
|
|
735
|
+
const session = sessions.get(s.id);
|
|
736
|
+
assert.ok(session);
|
|
737
|
+
assert.strictEqual(session.mode, 'pty');
|
|
738
|
+
session.scrollback.push('important output');
|
|
739
|
+
serializeAll(configDir);
|
|
740
|
+
// Kill after serialize (mimics gracefulShutdown sequence)
|
|
741
|
+
sessions.kill(s.id);
|
|
742
|
+
// Verify data is on disk
|
|
743
|
+
const pendingPath = path.join(configDir, 'pending-sessions.json');
|
|
744
|
+
assert.ok(fs.existsSync(pendingPath));
|
|
745
|
+
const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
|
|
746
|
+
assert.strictEqual(pending.sessions.length, 1);
|
|
747
|
+
assert.strictEqual(pending.sessions[0].displayName, 'before-kill');
|
|
748
|
+
});
|
|
580
749
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-remote-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "Remote web interface for Claude Code CLI sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"build:frontend": "svelte-check --workspace frontend && vite build --config frontend/vite.config.ts",
|
|
19
19
|
"dev": "vite --config frontend/vite.config.ts",
|
|
20
20
|
"start": "tsc && svelte-check --workspace frontend && vite build --config frontend/vite.config.ts && node dist/server/index.js",
|
|
21
|
-
"test": "svelte-check --workspace frontend && tsc -p tsconfig.test.json && node --test dist/test/*.test.js",
|
|
21
|
+
"test": "svelte-check --workspace frontend && tsc -p tsconfig.test.json && node --test --test-force-exit dist/test/*.test.js",
|
|
22
22
|
"prepublishOnly": "npm run build",
|
|
23
23
|
"postinstall": "chmod +x node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper 2>/dev/null || true"
|
|
24
24
|
},
|
|
@@ -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",
|