claude-remote-cli 2.13.0 → 2.13.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.
@@ -85,6 +85,7 @@ function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktre
85
85
  idle: false,
86
86
  useTmux,
87
87
  tmuxSessionName,
88
+ onPtyReplacedCallbacks: [],
88
89
  };
89
90
  sessions.set(id, session);
90
91
  // Load existing metadata to preserve a previously-set displayName
@@ -136,23 +137,42 @@ function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktre
136
137
  // If continue args failed quickly, retry without them
137
138
  if (canRetry && (Date.now() - spawnTime) < 3000 && exitCode !== 0) {
138
139
  const retryArgs = args.filter(a => !continueArgs.includes(a));
140
+ const retryNotice = '\r\n[claude-remote-cli] --continue not available; starting new session...\r\n';
139
141
  scrollback.length = 0;
140
142
  scrollbackBytes = 0;
143
+ scrollback.push(retryNotice);
144
+ scrollbackBytes = retryNotice.length;
141
145
  let retryCommand = resolvedCommand;
142
146
  let retrySpawnArgs = retryArgs;
143
147
  if (useTmux && tmuxSessionName) {
144
- const tmux = resolveTmuxSpawn(resolvedCommand, retryArgs, tmuxSessionName);
148
+ const retryTmuxName = tmuxSessionName + '-retry';
149
+ session.tmuxSessionName = retryTmuxName;
150
+ const tmux = resolveTmuxSpawn(resolvedCommand, retryArgs, retryTmuxName);
145
151
  retryCommand = tmux.command;
146
152
  retrySpawnArgs = tmux.args;
147
153
  }
148
- const retryPty = pty.spawn(retryCommand, retrySpawnArgs, {
149
- name: 'xterm-256color',
150
- cols,
151
- rows,
152
- cwd: cwd || repoPath,
153
- env,
154
- });
154
+ let retryPty;
155
+ try {
156
+ retryPty = pty.spawn(retryCommand, retrySpawnArgs, {
157
+ name: 'xterm-256color',
158
+ cols,
159
+ rows,
160
+ cwd: cwd || repoPath,
161
+ env,
162
+ });
163
+ }
164
+ catch {
165
+ // Retry spawn failed — fall through to normal exit cleanup
166
+ if (idleTimer)
167
+ clearTimeout(idleTimer);
168
+ if (metaFlushTimer)
169
+ clearTimeout(metaFlushTimer);
170
+ sessions.delete(id);
171
+ return;
172
+ }
155
173
  session.pty = retryPty;
174
+ for (const cb of session.onPtyReplacedCallbacks)
175
+ cb(retryPty);
156
176
  attachHandlers(retryPty, false);
157
177
  return;
158
178
  }
package/dist/server/ws.js CHANGED
@@ -71,15 +71,29 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
71
71
  const session = sessionMap.get(ws);
72
72
  if (!session)
73
73
  return;
74
- const ptyProcess = session.pty;
75
- for (const chunk of session.scrollback) {
76
- ws.send(chunk);
77
- }
78
- const dataHandler = ptyProcess.onData((data) => {
79
- if (ws.readyState === ws.OPEN) {
80
- ws.send(data);
74
+ let dataDisposable = null;
75
+ let exitDisposable = null;
76
+ function attachToPty(ptyProcess) {
77
+ // Dispose previous handlers
78
+ dataDisposable?.dispose();
79
+ exitDisposable?.dispose();
80
+ // Replay scrollback
81
+ for (const chunk of session.scrollback) {
82
+ if (ws.readyState === ws.OPEN)
83
+ ws.send(chunk);
81
84
  }
82
- });
85
+ dataDisposable = ptyProcess.onData((data) => {
86
+ if (ws.readyState === ws.OPEN)
87
+ ws.send(data);
88
+ });
89
+ exitDisposable = ptyProcess.onExit(() => {
90
+ if (ws.readyState === ws.OPEN)
91
+ ws.close(1000);
92
+ });
93
+ }
94
+ attachToPty(session.pty);
95
+ const ptyReplacedHandler = (newPty) => attachToPty(newPty);
96
+ session.onPtyReplacedCallbacks.push(ptyReplacedHandler);
83
97
  ws.on('message', (msg) => {
84
98
  const str = msg.toString();
85
99
  try {
@@ -90,15 +104,15 @@ function setupWebSocket(server, authenticatedTokens, watcher) {
90
104
  }
91
105
  }
92
106
  catch (_) { }
93
- ptyProcess.write(str);
107
+ // Use session.pty dynamically so writes go to current PTY
108
+ session.pty.write(str);
94
109
  });
95
110
  ws.on('close', () => {
96
- dataHandler.dispose();
97
- });
98
- ptyProcess.onExit(() => {
99
- if (ws.readyState === ws.OPEN) {
100
- ws.close(1000);
101
- }
111
+ dataDisposable?.dispose();
112
+ exitDisposable?.dispose();
113
+ const idx = session.onPtyReplacedCallbacks.indexOf(ptyReplacedHandler);
114
+ if (idx !== -1)
115
+ session.onPtyReplacedCallbacks.splice(idx, 1);
102
116
  });
103
117
  });
104
118
  sessions.onIdleChange((sessionId, idle) => {
@@ -374,4 +374,36 @@ describe('sessions', () => {
374
374
  assert.strictEqual(session.useTmux, false);
375
375
  assert.strictEqual(session.tmuxSessionName, '');
376
376
  });
377
+ it('calls onPtyReplaced when continue-arg process fails quickly', (_, done) => {
378
+ const result = sessions.create({
379
+ repoName: 'test-repo',
380
+ repoPath: '/tmp',
381
+ command: '/bin/false',
382
+ args: [...sessions.AGENT_CONTINUE_ARGS.claude],
383
+ });
384
+ createdIds.push(result.id);
385
+ const session = sessions.get(result.id);
386
+ assert.ok(session);
387
+ session.onPtyReplacedCallbacks.push((newPty) => {
388
+ assert.ok(newPty, 'should receive new PTY');
389
+ assert.strictEqual(session.pty, newPty, 'session.pty should be updated to new PTY');
390
+ done();
391
+ });
392
+ });
393
+ it('session survives after continue-arg retry', (_, done) => {
394
+ const result = sessions.create({
395
+ repoName: 'test-repo',
396
+ repoPath: '/tmp',
397
+ command: '/bin/false',
398
+ args: [...sessions.AGENT_CONTINUE_ARGS.claude],
399
+ });
400
+ createdIds.push(result.id);
401
+ const session = sessions.get(result.id);
402
+ assert.ok(session);
403
+ session.onPtyReplacedCallbacks.push(() => {
404
+ const stillExists = sessions.get(result.id);
405
+ assert.ok(stillExists, 'session should still exist after retry');
406
+ done();
407
+ });
408
+ });
377
409
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",