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/frontend/assets/index-BNLfnaOa.css +32 -0
- package/dist/frontend/assets/index-ggOT9Hda.js +47 -0
- package/dist/frontend/index.html +2 -2
- package/dist/server/git.js +55 -2
- package/dist/server/index.js +20 -63
- package/dist/server/pty-handler.js +2 -0
- package/dist/server/push.js +1 -32
- package/dist/server/sessions.js +98 -133
- package/dist/server/types.js +1 -1
- package/dist/server/workspaces.js +8 -2
- package/dist/server/ws.js +4 -93
- package/dist/test/pr-state.test.js +69 -13
- package/package.json +1 -2
- package/dist/frontend/assets/index-BRH8jV0L.js +0 -47
- package/dist/frontend/assets/index-w5wJhB5f.css +0 -32
- package/dist/server/sdk-handler.js +0 -539
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
|
|
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
|
|
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, '
|
|
49
|
+
assert.equal(action.type, 'review-pr');
|
|
46
50
|
assert.equal(action.color, 'success');
|
|
47
|
-
assert.equal(action.label, '
|
|
51
|
+
assert.equal(action.label, 'Review PR');
|
|
48
52
|
});
|
|
49
|
-
it('returns
|
|
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, '
|
|
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
|
|
128
|
-
const prompt = getActionPrompt({ type: '
|
|
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.
|
|
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",
|