claude-remote-cli 3.1.1 → 3.3.0

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/server/ws.js CHANGED
@@ -2,15 +2,10 @@ import { WebSocketServer } from 'ws';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
4
  import * as sessions from './sessions.js';
5
- import { onSdkEvent, sendMessage as sdkSendMessage, handlePermission as sdkHandlePermission } from './sdk-handler.js';
6
5
  import { writeMeta } from './config.js';
7
6
  const execFileAsync = promisify(execFile);
8
- const BACKPRESSURE_HIGH = 1024 * 1024; // 1MB
9
- const BACKPRESSURE_LOW = 512 * 1024; // 512KB
10
7
  const BRANCH_POLL_INTERVAL_MS = 3000;
11
8
  const BRANCH_POLL_MAX_ATTEMPTS = 10;
12
- 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.`;
13
- const SDK_BRANCH_RENAME_INSTRUCTION = `Before responding to my message, first ${RENAME_CORE} After renaming, proceed with my request normally.\n\n`;
14
9
  function startBranchWatcher(session, broadcastEvent, cfgPath) {
15
10
  const originalBranch = session.branchName;
16
11
  let attempts = 0;
@@ -88,7 +83,7 @@ function setupWebSocket(server, authenticatedTokens, watcher, configPath) {
88
83
  });
89
84
  return;
90
85
  }
91
- // PTY/SDK channel: /ws/:sessionId
86
+ // PTY channel: /ws/:sessionId
92
87
  const match = request.url && request.url.match(/^\/ws\/([a-f0-9]+)$/);
93
88
  if (!match) {
94
89
  socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
@@ -112,15 +107,6 @@ function setupWebSocket(server, authenticatedTokens, watcher, configPath) {
112
107
  const session = sessionMap.get(ws);
113
108
  if (!session)
114
109
  return;
115
- if (session.mode === 'sdk') {
116
- handleSdkConnection(ws, session);
117
- return;
118
- }
119
- // PTY mode — existing behavior
120
- if (session.mode !== 'pty') {
121
- ws.close(1008, 'Session mode does not support PTY streaming');
122
- return;
123
- }
124
110
  const ptySession = session;
125
111
  let dataDisposable = null;
126
112
  let exitDisposable = null;
@@ -190,87 +176,12 @@ function setupWebSocket(server, authenticatedTokens, watcher, configPath) {
190
176
  ptySession.onPtyReplacedCallbacks.splice(idx, 1);
191
177
  });
192
178
  });
193
- function handleSdkConnection(ws, session) {
194
- // Send session info
195
- const sessionInfo = JSON.stringify({
196
- type: 'session_info',
197
- mode: 'sdk',
198
- sessionId: session.id,
199
- });
200
- if (ws.readyState === ws.OPEN)
201
- ws.send(sessionInfo);
202
- // Replay stored events (send as-is — client expects raw SdkEvent shape)
203
- for (const event of session.events) {
204
- if (ws.readyState !== ws.OPEN)
205
- break;
206
- ws.send(JSON.stringify(event));
207
- }
208
- // Subscribe to live events with backpressure
209
- let paused = false;
210
- const unsubscribe = onSdkEvent(session.id, (event) => {
211
- if (ws.readyState !== ws.OPEN)
212
- return;
213
- // Backpressure check
214
- if (ws.bufferedAmount > BACKPRESSURE_HIGH) {
215
- paused = true;
216
- return;
217
- }
218
- ws.send(JSON.stringify(event));
219
- });
220
- // Periodically check if we can resume
221
- const backpressureInterval = setInterval(() => {
222
- if (paused && ws.bufferedAmount < BACKPRESSURE_LOW) {
223
- paused = false;
224
- }
225
- }, 100);
226
- // Handle incoming messages
227
- ws.on('message', (msg) => {
228
- const str = msg.toString();
229
- try {
230
- const parsed = JSON.parse(str);
231
- if (parsed.type === 'message' && typeof parsed.text === 'string') {
232
- if (parsed.text.length > 100_000)
233
- return;
234
- if (session.needsBranchRename) {
235
- session.needsBranchRename = false;
236
- sdkSendMessage(session.id, SDK_BRANCH_RENAME_INSTRUCTION + parsed.text);
237
- if (configPath)
238
- startBranchWatcher(session, broadcastEvent, configPath);
239
- }
240
- else {
241
- sdkSendMessage(session.id, parsed.text);
242
- }
243
- return;
244
- }
245
- if (parsed.type === 'permission' && typeof parsed.requestId === 'string' && typeof parsed.approved === 'boolean') {
246
- sdkHandlePermission(session.id, parsed.requestId, parsed.approved);
247
- return;
248
- }
249
- if (parsed.type === 'resize' && typeof parsed.cols === 'number' && typeof parsed.rows === 'number') {
250
- // TODO: wire up companion shell — currently open_companion message is unhandled server-side
251
- return;
252
- }
253
- if (parsed.type === 'open_companion') {
254
- // TODO: spawn companion PTY in session CWD and relay via terminal_data/terminal_exit frames
255
- return;
256
- }
257
- }
258
- catch (_) {
259
- // Not JSON — ignore for SDK sessions
260
- }
261
- });
262
- ws.on('close', () => {
263
- unsubscribe();
264
- clearInterval(backpressureInterval);
265
- });
266
- ws.on('error', () => {
267
- unsubscribe();
268
- clearInterval(backpressureInterval);
269
- });
270
- }
271
179
  sessions.onIdleChange((sessionId, idle) => {
272
180
  broadcastEvent('session-idle-changed', { sessionId, idle });
273
181
  });
182
+ sessions.onSessionEnd((sessionId, repoPath, branchName) => {
183
+ broadcastEvent('session-ended', { sessionId, repoPath, branchName });
184
+ });
274
185
  return { wss, broadcastEvent };
275
186
  }
276
187
  export { setupWebSocket };
@@ -7,6 +7,7 @@ describe('derivePrAction', () => {
7
7
  commitsAhead: 0,
8
8
  prState: null,
9
9
  ciPassing: 0, ciFailing: 0, ciPending: 0, ciTotal: 0,
10
+ mergeable: null, unresolvedCommentCount: 0,
10
11
  };
11
12
  const action = derivePrAction(input);
12
13
  assert.equal(action.type, 'none');
@@ -18,6 +19,7 @@ describe('derivePrAction', () => {
18
19
  commitsAhead: 3,
19
20
  prState: null,
20
21
  ciPassing: 0, ciFailing: 0, ciPending: 0, ciTotal: 0,
22
+ mergeable: null, unresolvedCommentCount: 0,
21
23
  };
22
24
  const action = derivePrAction(input);
23
25
  assert.equal(action.type, 'create-pr');
@@ -29,31 +31,34 @@ describe('derivePrAction', () => {
29
31
  commitsAhead: 5,
30
32
  prState: 'DRAFT',
31
33
  ciPassing: 0, ciFailing: 0, ciPending: 0, ciTotal: 0,
34
+ mergeable: null, unresolvedCommentCount: 0,
32
35
  };
33
36
  const action = derivePrAction(input);
34
37
  assert.equal(action.type, 'ready-for-review');
35
38
  assert.equal(action.color, 'muted');
36
39
  assert.equal(action.label, 'Ready for Review');
37
40
  });
38
- it('returns code-review for open PR with all CI passing', () => {
41
+ it('returns review-pr for open PR with all CI passing', () => {
39
42
  const input = {
40
43
  commitsAhead: 2,
41
44
  prState: 'OPEN',
42
45
  ciPassing: 5, ciFailing: 0, ciPending: 0, ciTotal: 5,
46
+ mergeable: 'MERGEABLE', unresolvedCommentCount: 0,
43
47
  };
44
48
  const action = derivePrAction(input);
45
- assert.equal(action.type, 'code-review');
49
+ assert.equal(action.type, 'review-pr');
46
50
  assert.equal(action.color, 'success');
47
- assert.equal(action.label, 'Code Review');
51
+ assert.equal(action.label, 'Review PR');
48
52
  });
49
- it('returns code-review for open PR with no CI checks', () => {
53
+ it('returns review-pr for open PR with no CI checks', () => {
50
54
  const input = {
51
55
  commitsAhead: 1,
52
56
  prState: 'OPEN',
53
57
  ciPassing: 0, ciFailing: 0, ciPending: 0, ciTotal: 0,
58
+ mergeable: 'MERGEABLE', unresolvedCommentCount: 0,
54
59
  };
55
60
  const action = derivePrAction(input);
56
- assert.equal(action.type, 'code-review');
61
+ assert.equal(action.type, 'review-pr');
57
62
  assert.equal(action.color, 'success');
58
63
  });
59
64
  it('returns fix-errors for open PR with failing CI', () => {
@@ -61,6 +66,7 @@ describe('derivePrAction', () => {
61
66
  commitsAhead: 2,
62
67
  prState: 'OPEN',
63
68
  ciPassing: 6, ciFailing: 2, ciPending: 0, ciTotal: 8,
69
+ mergeable: 'MERGEABLE', unresolvedCommentCount: 0,
64
70
  };
65
71
  const action = derivePrAction(input);
66
72
  assert.equal(action.type, 'fix-errors');
@@ -72,6 +78,7 @@ describe('derivePrAction', () => {
72
78
  commitsAhead: 1,
73
79
  prState: 'OPEN',
74
80
  ciPassing: 3, ciFailing: 0, ciPending: 2, ciTotal: 5,
81
+ mergeable: 'MERGEABLE', unresolvedCommentCount: 0,
75
82
  };
76
83
  const action = derivePrAction(input);
77
84
  assert.equal(action.type, 'checks-running');
@@ -83,6 +90,7 @@ describe('derivePrAction', () => {
83
90
  commitsAhead: 1,
84
91
  prState: 'OPEN',
85
92
  ciPassing: 3, ciFailing: 1, ciPending: 1, ciTotal: 5,
93
+ mergeable: 'MERGEABLE', unresolvedCommentCount: 0,
86
94
  };
87
95
  const action = derivePrAction(input);
88
96
  assert.equal(action.type, 'fix-errors');
@@ -93,6 +101,7 @@ describe('derivePrAction', () => {
93
101
  commitsAhead: 0,
94
102
  prState: 'MERGED',
95
103
  ciPassing: 5, ciFailing: 0, ciPending: 0, ciTotal: 5,
104
+ mergeable: null, unresolvedCommentCount: 0,
96
105
  };
97
106
  const action = derivePrAction(input);
98
107
  assert.equal(action.type, 'archive-merged');
@@ -104,38 +113,85 @@ describe('derivePrAction', () => {
104
113
  commitsAhead: 0,
105
114
  prState: 'CLOSED',
106
115
  ciPassing: 0, ciFailing: 0, ciPending: 0, ciTotal: 0,
116
+ mergeable: null, unresolvedCommentCount: 0,
107
117
  };
108
118
  const action = derivePrAction(input);
109
119
  assert.equal(action.type, 'archive-closed');
110
120
  assert.equal(action.color, 'muted');
111
121
  assert.equal(action.label, 'Archive');
112
122
  });
123
+ it('returns fix-conflicts for open PR with CONFLICTING mergeable', () => {
124
+ const input = {
125
+ commitsAhead: 2,
126
+ prState: 'OPEN',
127
+ ciPassing: 0, ciFailing: 0, ciPending: 0, ciTotal: 0,
128
+ mergeable: 'CONFLICTING', unresolvedCommentCount: 0,
129
+ };
130
+ const action = derivePrAction(input);
131
+ assert.equal(action.type, 'fix-conflicts');
132
+ assert.equal(action.color, 'error');
133
+ assert.equal(action.label, 'Fix Conflicts');
134
+ });
135
+ it('prioritizes fix-conflicts over fix-errors', () => {
136
+ const input = {
137
+ commitsAhead: 2,
138
+ prState: 'OPEN',
139
+ ciPassing: 0, ciFailing: 3, ciPending: 0, ciTotal: 3,
140
+ mergeable: 'CONFLICTING', unresolvedCommentCount: 0,
141
+ };
142
+ const action = derivePrAction(input);
143
+ assert.equal(action.type, 'fix-conflicts');
144
+ });
145
+ it('returns resolve-comments when unresolved comments > 0 and CI passing', () => {
146
+ const input = {
147
+ commitsAhead: 1,
148
+ prState: 'OPEN',
149
+ ciPassing: 5, ciFailing: 0, ciPending: 0, ciTotal: 5,
150
+ mergeable: 'MERGEABLE', unresolvedCommentCount: 3,
151
+ };
152
+ const action = derivePrAction(input);
153
+ assert.equal(action.type, 'resolve-comments');
154
+ assert.equal(action.color, 'accent');
155
+ assert.equal(action.label, 'Resolve Comments (3)');
156
+ });
113
157
  });
114
158
  describe('getActionPrompt', () => {
115
159
  it('returns prompt for create-pr', () => {
116
- const prompt = getActionPrompt({ type: 'create-pr', color: 'accent', label: 'Create PR' }, 'feat/my-feature');
160
+ const prompt = getActionPrompt({ type: 'create-pr', color: 'accent', label: 'Create PR' }, { branchName: 'feat/my-feature' });
117
161
  assert.ok(prompt);
118
162
  assert.ok(prompt.includes('feat/my-feature'));
119
163
  assert.ok(prompt.includes('pull request'));
120
164
  });
121
165
  it('returns prompt for fix-errors', () => {
122
- const prompt = getActionPrompt({ type: 'fix-errors', color: 'error', label: 'Fix Errors 2/8' }, 'bugfix/auth');
166
+ const prompt = getActionPrompt({ type: 'fix-errors', color: 'error', label: 'Fix Errors 2/8' }, { branchName: 'bugfix/auth' });
123
167
  assert.ok(prompt);
124
168
  assert.ok(prompt.includes('bugfix/auth'));
125
169
  assert.ok(prompt.includes('failing'));
126
170
  });
127
- it('returns prompt for code-review', () => {
128
- const prompt = getActionPrompt({ type: 'code-review', color: 'success', label: 'Code Review' }, 'main');
171
+ it('returns prompt for review-pr', () => {
172
+ const prompt = getActionPrompt({ type: 'review-pr', color: 'success', label: 'Review PR' }, { branchName: 'main', prNumber: 42 });
129
173
  assert.ok(prompt);
130
174
  assert.ok(prompt.includes('Review'));
131
175
  });
176
+ it('returns prompt for fix-conflicts', () => {
177
+ const prompt = getActionPrompt({ type: 'fix-conflicts', color: 'error', label: 'Fix Conflicts' }, { branchName: 'feat/foo', baseBranch: 'main' });
178
+ assert.ok(prompt);
179
+ assert.ok(prompt.includes('main'));
180
+ assert.ok(prompt.includes('conflict'));
181
+ });
182
+ it('returns prompt for resolve-comments', () => {
183
+ const prompt = getActionPrompt({ type: 'resolve-comments', color: 'accent', label: 'Resolve Comments (3)' }, { branchName: 'feat/foo', prNumber: 7, unresolvedCommentCount: 3 });
184
+ assert.ok(prompt);
185
+ assert.ok(prompt.includes('3'));
186
+ assert.ok(prompt.includes('#7'));
187
+ });
132
188
  it('returns null for archive actions', () => {
133
- assert.equal(getActionPrompt({ type: 'archive-merged', color: 'merged', label: 'Archive' }, 'main'), null);
134
- assert.equal(getActionPrompt({ type: 'archive-closed', color: 'muted', label: 'Archive' }, 'main'), null);
189
+ assert.equal(getActionPrompt({ type: 'archive-merged', color: 'merged', label: 'Archive' }, { branchName: 'main' }), null);
190
+ assert.equal(getActionPrompt({ type: 'archive-closed', color: 'muted', label: 'Archive' }, { branchName: 'main' }), null);
135
191
  });
136
192
  it('returns null for none and checks-running', () => {
137
- assert.equal(getActionPrompt({ type: 'none', color: 'none', label: '' }, 'main'), null);
138
- assert.equal(getActionPrompt({ type: 'checks-running', color: 'warning', label: 'Checks Running...' }, 'main'), null);
193
+ assert.equal(getActionPrompt({ type: 'none', color: 'none', label: '' }, { branchName: 'main' }), null);
194
+ assert.equal(getActionPrompt({ type: 'checks-running', color: 'warning', label: 'Checks Running...' }, { branchName: 'main' }), null);
139
195
  });
140
196
  });
141
197
  describe('getStatusCssVar', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "3.1.1",
3
+ "version": "3.3.0",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",
@@ -42,7 +42,6 @@
42
42
  "license": "MIT",
43
43
  "author": "Donovan Yohan",
44
44
  "dependencies": {
45
- "@anthropic-ai/claude-agent-sdk": "^0.2.77",
46
45
  "@tanstack/svelte-query": "^6.0.18",
47
46
  "@xterm/addon-fit": "^0.11.0",
48
47
  "@xterm/xterm": "^6.0.0",