claude-remote-cli 2.15.16 → 3.0.2

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.
@@ -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
- session.pty.onData((data) => {
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.onPtyReplacedCallbacks.push((newPty) => {
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(session.pty, newPty, 'session.pty should be updated to new PTY');
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.onPtyReplacedCallbacks.push(() => {
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.onPtyReplacedCallbacks.push((newPty) => {
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(session.pty, newPty, 'session.pty should be updated');
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": "2.15.16",
3
+ "version": "3.0.2",
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",