claude-remote-cli 2.12.1 → 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.
@@ -11,7 +11,7 @@
11
11
  <meta name="apple-mobile-web-app-capable" content="yes" />
12
12
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
13
13
  <meta name="theme-color" content="#1a1a1a" />
14
- <script type="module" crossorigin src="/assets/index-BBAKirMm.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-YKIeThK-.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-nIPDa7NP.css">
16
16
  </head>
17
17
  <body>
@@ -24,7 +24,13 @@ function generateTmuxSessionName(displayName, id) {
24
24
  function resolveTmuxSpawn(command, args, tmuxSessionName) {
25
25
  return {
26
26
  command: 'tmux',
27
- args: ['-u', 'new-session', '-s', tmuxSessionName, '--', command, ...args],
27
+ args: [
28
+ '-u', 'new-session', '-s', tmuxSessionName, '--', command, ...args,
29
+ // ';' tokens are tmux command separators — parsed at the top level before
30
+ // dispatching to new-session, not passed as argv to `command`.
31
+ ';', 'set', 'set-clipboard', 'on',
32
+ ';', 'set', 'allow-passthrough', 'on',
33
+ ],
28
34
  };
29
35
  }
30
36
  // In-memory registry: id -> Session
@@ -79,6 +85,7 @@ function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktre
79
85
  idle: false,
80
86
  useTmux,
81
87
  tmuxSessionName,
88
+ onPtyReplacedCallbacks: [],
82
89
  };
83
90
  sessions.set(id, session);
84
91
  // Load existing metadata to preserve a previously-set displayName
@@ -130,23 +137,42 @@ function create({ type, agent = 'claude', repoName, repoPath, cwd, root, worktre
130
137
  // If continue args failed quickly, retry without them
131
138
  if (canRetry && (Date.now() - spawnTime) < 3000 && exitCode !== 0) {
132
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';
133
141
  scrollback.length = 0;
134
142
  scrollbackBytes = 0;
143
+ scrollback.push(retryNotice);
144
+ scrollbackBytes = retryNotice.length;
135
145
  let retryCommand = resolvedCommand;
136
146
  let retrySpawnArgs = retryArgs;
137
147
  if (useTmux && tmuxSessionName) {
138
- const tmux = resolveTmuxSpawn(resolvedCommand, retryArgs, tmuxSessionName);
148
+ const retryTmuxName = tmuxSessionName + '-retry';
149
+ session.tmuxSessionName = retryTmuxName;
150
+ const tmux = resolveTmuxSpawn(resolvedCommand, retryArgs, retryTmuxName);
139
151
  retryCommand = tmux.command;
140
152
  retrySpawnArgs = tmux.args;
141
153
  }
142
- const retryPty = pty.spawn(retryCommand, retrySpawnArgs, {
143
- name: 'xterm-256color',
144
- cols,
145
- rows,
146
- cwd: cwd || repoPath,
147
- env,
148
- });
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
+ }
149
173
  session.pty = retryPty;
174
+ for (const cb of session.onPtyReplacedCallbacks)
175
+ cb(retryPty);
150
176
  attachHandlers(retryPty, false);
151
177
  return;
152
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) => {
@@ -269,7 +269,11 @@ describe('sessions', () => {
269
269
  const result = resolveTmuxSpawn('claude', ['--continue'], 'test-session');
270
270
  assert.deepStrictEqual(result, {
271
271
  command: 'tmux',
272
- args: ['-u', 'new-session', '-s', 'test-session', '--', 'claude', '--continue'],
272
+ args: [
273
+ '-u', 'new-session', '-s', 'test-session', '--', 'claude', '--continue',
274
+ ';', 'set', 'set-clipboard', 'on',
275
+ ';', 'set', 'allow-passthrough', 'on',
276
+ ],
273
277
  });
274
278
  });
275
279
  it('generateTmuxSessionName has crc- prefix', () => {
@@ -370,4 +374,36 @@ describe('sessions', () => {
370
374
  assert.strictEqual(session.useTmux, false);
371
375
  assert.strictEqual(session.tmuxSessionName, '');
372
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
+ });
373
409
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "2.12.1",
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",