claude-remote-cli 3.2.0 → 3.3.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.
@@ -1,539 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { query as sdkQuery } from '@anthropic-ai/claude-agent-sdk';
6
- const MAX_EVENTS = 2000;
7
- const IDLE_TIMEOUT_MS = 5000;
8
- // Controller for streaming user messages into an SDK query
9
- class SdkInputController {
10
- resolveNext = null;
11
- queue = [];
12
- done = false;
13
- push(msg) {
14
- if (this.done)
15
- return;
16
- if (this.resolveNext) {
17
- const resolve = this.resolveNext;
18
- this.resolveNext = null;
19
- resolve({ value: msg, done: false });
20
- }
21
- else {
22
- this.queue.push(msg);
23
- }
24
- }
25
- close() {
26
- this.done = true;
27
- if (this.resolveNext) {
28
- const resolve = this.resolveNext;
29
- this.resolveNext = null;
30
- resolve({ value: undefined, done: true });
31
- }
32
- }
33
- [Symbol.asyncIterator]() {
34
- return {
35
- next: () => {
36
- if (this.queue.length > 0) {
37
- return Promise.resolve({ value: this.queue.shift(), done: false });
38
- }
39
- if (this.done) {
40
- return Promise.resolve({ value: undefined, done: true });
41
- }
42
- return new Promise((resolve) => {
43
- this.resolveNext = resolve;
44
- });
45
- },
46
- };
47
- }
48
- }
49
- const runtimeStates = new Map();
50
- // Debug log state
51
- let debugLogEnabled = false;
52
- const DEBUG_DIR = path.join(os.homedir(), '.config', 'claude-remote-cli', 'debug');
53
- const MAX_DEBUG_FILE_SIZE = 10 * 1024 * 1024; // 10MB
54
- const DEBUG_FILE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
55
- export function enableDebugLog(enabled) {
56
- debugLogEnabled = enabled;
57
- if (enabled) {
58
- fs.mkdirSync(DEBUG_DIR, { recursive: true });
59
- cleanupOldDebugFiles();
60
- }
61
- }
62
- function cleanupOldDebugFiles() {
63
- try {
64
- const files = fs.readdirSync(DEBUG_DIR);
65
- const now = Date.now();
66
- for (const file of files) {
67
- if (!file.endsWith('.jsonl'))
68
- continue;
69
- const filePath = path.join(DEBUG_DIR, file);
70
- try {
71
- const stat = fs.statSync(filePath);
72
- if (now - stat.mtimeMs > DEBUG_FILE_MAX_AGE_MS) {
73
- fs.unlinkSync(filePath);
74
- }
75
- }
76
- catch {
77
- // ignore individual file errors
78
- }
79
- }
80
- }
81
- catch {
82
- // ignore if directory doesn't exist yet
83
- }
84
- }
85
- // Async write queue to avoid blocking the event loop on debug log writes
86
- const debugWriteQueue = new Map();
87
- let debugFlushPending = false;
88
- function flushDebugWrites() {
89
- if (debugFlushPending)
90
- return;
91
- debugFlushPending = true;
92
- queueMicrotask(() => {
93
- debugFlushPending = false;
94
- for (const [filePath, lines] of debugWriteQueue) {
95
- const data = lines.join('');
96
- debugWriteQueue.delete(filePath);
97
- fs.appendFile(filePath, data, 'utf-8', () => { });
98
- }
99
- });
100
- }
101
- function debugLogEvent(sessionId, event) {
102
- if (!debugLogEnabled)
103
- return;
104
- try {
105
- const filePath = path.join(DEBUG_DIR, `${sessionId}.jsonl`);
106
- const line = JSON.stringify({ ...event, _logged: new Date().toISOString() }) + '\n';
107
- // Async rotation check (best-effort, runs periodically)
108
- fs.stat(filePath, (err, stat) => {
109
- if (!err && stat.size > MAX_DEBUG_FILE_SIZE) {
110
- const rotatedPath = filePath + '.1';
111
- fs.unlink(rotatedPath, () => {
112
- fs.rename(filePath, rotatedPath, () => { });
113
- });
114
- }
115
- });
116
- // Queue the write
117
- const existing = debugWriteQueue.get(filePath);
118
- if (existing) {
119
- existing.push(line);
120
- }
121
- else {
122
- debugWriteQueue.set(filePath, [line]);
123
- }
124
- flushDebugWrites();
125
- }
126
- catch {
127
- // debug logging should never crash the server
128
- }
129
- }
130
- const FILE_EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
131
- function extractAssistantEvents(msg, timestamp) {
132
- const events = [];
133
- const content = msg.message.content;
134
- const textParts = [];
135
- const thinkingParts = [];
136
- for (const block of content) {
137
- if (block.type === 'text' && block.text) {
138
- textParts.push(block.text);
139
- }
140
- else if (block.type === 'thinking' && block.thinking) {
141
- thinkingParts.push(block.thinking);
142
- }
143
- else if (block.type === 'tool_use' && block.name && block.id) {
144
- const input = (block.input || {});
145
- const isFileEdit = FILE_EDIT_TOOLS.has(block.name);
146
- const filePath = input.file_path || input.path;
147
- if (isFileEdit) {
148
- const evt = {
149
- type: 'file_change',
150
- id: block.id,
151
- toolName: block.name,
152
- toolInput: input,
153
- timestamp,
154
- };
155
- if (filePath)
156
- evt.path = filePath;
157
- events.push(evt);
158
- }
159
- else {
160
- events.push({
161
- type: 'tool_call',
162
- id: block.id,
163
- toolName: block.name,
164
- toolInput: input,
165
- timestamp,
166
- });
167
- }
168
- }
169
- }
170
- if (thinkingParts.length > 0) {
171
- // Prepend thinking before text/tool events
172
- events.unshift({
173
- type: 'reasoning',
174
- text: thinkingParts.join('\n'),
175
- timestamp,
176
- });
177
- }
178
- if (textParts.length > 0) {
179
- // Insert text event after thinking but before tool events
180
- const insertIdx = thinkingParts.length > 0 ? 1 : 0;
181
- events.splice(insertIdx, 0, {
182
- type: 'agent_message',
183
- id: msg.uuid,
184
- text: textParts.join(''),
185
- timestamp,
186
- });
187
- }
188
- return events;
189
- }
190
- function mapSdkMessageAll(msg) {
191
- const timestamp = new Date().toISOString();
192
- const events = [];
193
- if (msg.type === 'system' && msg.subtype === 'init') {
194
- events.push({
195
- type: 'session_started',
196
- id: msg.session_id,
197
- timestamp,
198
- });
199
- return events;
200
- }
201
- if (msg.type === 'assistant') {
202
- return extractAssistantEvents(msg, timestamp);
203
- }
204
- if (msg.type === 'result') {
205
- if (msg.subtype === 'success') {
206
- events.push({
207
- type: 'turn_completed',
208
- usage: {
209
- input_tokens: msg.usage.input_tokens,
210
- output_tokens: msg.usage.output_tokens,
211
- },
212
- timestamp,
213
- });
214
- }
215
- else {
216
- events.push({
217
- type: 'error',
218
- text: msg.errors.join('; '),
219
- timestamp,
220
- });
221
- }
222
- return events;
223
- }
224
- return events;
225
- }
226
- function addEvent(session, event, state) {
227
- session.events.push(event);
228
- // FIFO cap
229
- while (session.events.length > MAX_EVENTS) {
230
- session.events.shift();
231
- }
232
- // Notify listeners
233
- for (const listener of state.eventListeners) {
234
- try {
235
- listener(event);
236
- }
237
- catch {
238
- // listeners should not crash the handler
239
- }
240
- }
241
- // Debug log
242
- debugLogEvent(session.id, event);
243
- }
244
- function resetIdleTimer(session, state, idleChangeCallbacks) {
245
- if (session.idle) {
246
- session.idle = false;
247
- for (const cb of idleChangeCallbacks)
248
- cb(session.id, false);
249
- }
250
- if (state.idleTimer)
251
- clearTimeout(state.idleTimer);
252
- state.idleTimer = setTimeout(() => {
253
- if (!session.idle) {
254
- session.idle = true;
255
- for (const cb of idleChangeCallbacks)
256
- cb(session.id, true);
257
- }
258
- }, IDLE_TIMEOUT_MS);
259
- }
260
- export function createSdkSession(params, sessionsMap, idleChangeCallbacks) {
261
- const { id, type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, prompt, } = params;
262
- const createdAt = new Date().toISOString();
263
- const resolvedCwd = cwd || repoPath;
264
- // Strip sensitive env vars from child process
265
- const env = Object.assign({}, process.env);
266
- delete env.CLAUDECODE;
267
- // Strip server-internal env vars
268
- for (const key of Object.keys(env)) {
269
- if (key.startsWith('VAPID_') || key === 'PIN_HASH') {
270
- delete env[key];
271
- }
272
- }
273
- const abortController = new AbortController();
274
- const inputController = new SdkInputController();
275
- // Permission queue for canUseTool callbacks
276
- const permissionQueue = new Map();
277
- const session = {
278
- id,
279
- type: type || 'worktree',
280
- agent,
281
- mode: 'sdk',
282
- root: root || '',
283
- repoName: repoName || '',
284
- repoPath,
285
- worktreeName: worktreeName || '',
286
- branchName: branchName || worktreeName || '',
287
- displayName: displayName || worktreeName || repoName || '',
288
- createdAt,
289
- lastActivity: createdAt,
290
- idle: false,
291
- cwd: resolvedCwd,
292
- customCommand: null,
293
- status: 'active',
294
- needsBranchRename: false,
295
- events: [],
296
- sdkSessionId: null,
297
- tokenUsage: { input: 0, output: 0 },
298
- estimatedCost: 0,
299
- };
300
- const state = {
301
- query: null,
302
- abortController,
303
- permissionQueue,
304
- eventListeners: [],
305
- inputController,
306
- idleTimer: null,
307
- };
308
- // Try to create the SDK query
309
- let q;
310
- try {
311
- q = sdkQuery({
312
- prompt: inputController,
313
- options: {
314
- abortController,
315
- cwd: resolvedCwd,
316
- env,
317
- canUseTool: async (toolName, input, options) => {
318
- const requestId = options.toolUseID || crypto.randomBytes(8).toString('hex');
319
- // Emit a tool_call event for the permission request
320
- const permEvent = {
321
- type: 'tool_call',
322
- id: requestId,
323
- toolName,
324
- toolInput: input,
325
- status: 'pending',
326
- text: options.title || `Claude wants to use ${toolName}`,
327
- timestamp: new Date().toISOString(),
328
- };
329
- addEvent(session, permEvent, state);
330
- return new Promise((resolve, reject) => {
331
- permissionQueue.set(requestId, { resolve, reject });
332
- // Clean up on abort
333
- options.signal.addEventListener('abort', () => {
334
- permissionQueue.delete(requestId);
335
- reject(new Error('Permission request aborted'));
336
- }, { once: true });
337
- });
338
- },
339
- },
340
- });
341
- state.query = q;
342
- }
343
- catch (err) {
344
- console.warn('SDK init failed, falling back to PTY:', err instanceof Error ? err.message : String(err));
345
- return { fallback: true };
346
- }
347
- sessionsMap.set(id, session);
348
- runtimeStates.set(id, state);
349
- // Send initial prompt if provided
350
- if (prompt) {
351
- inputController.push({
352
- type: 'user',
353
- message: { role: 'user', content: prompt },
354
- parent_tool_use_id: null,
355
- session_id: id,
356
- });
357
- }
358
- // Start consuming the event stream in the background
359
- void (async () => {
360
- try {
361
- for await (const msg of q) {
362
- session.lastActivity = new Date().toISOString();
363
- resetIdleTimer(session, state, idleChangeCallbacks);
364
- const events = mapSdkMessageAll(msg);
365
- for (const event of events) {
366
- addEvent(session, event, state);
367
- // Track token usage
368
- if (event.type === 'turn_completed' && event.usage) {
369
- session.tokenUsage.input += event.usage.input_tokens;
370
- session.tokenUsage.output += event.usage.output_tokens;
371
- }
372
- // Track session ID
373
- if (event.type === 'session_started' && event.id) {
374
- session.sdkSessionId = event.id;
375
- }
376
- }
377
- }
378
- }
379
- catch (err) {
380
- const errorEvent = {
381
- type: 'error',
382
- text: err instanceof Error ? err.message : 'SDK stream error',
383
- timestamp: new Date().toISOString(),
384
- };
385
- addEvent(session, errorEvent, state);
386
- }
387
- })();
388
- const result = {
389
- id,
390
- type: session.type,
391
- agent: session.agent,
392
- mode: 'sdk',
393
- root: session.root,
394
- repoName: session.repoName,
395
- repoPath,
396
- worktreeName: session.worktreeName,
397
- branchName: session.branchName,
398
- displayName: session.displayName,
399
- createdAt,
400
- lastActivity: createdAt,
401
- idle: false,
402
- cwd: resolvedCwd,
403
- customCommand: null,
404
- useTmux: false,
405
- tmuxSessionName: '',
406
- status: 'active',
407
- needsBranchRename: false,
408
- };
409
- return { session, result };
410
- }
411
- export function sendMessage(sessionId, text) {
412
- const state = runtimeStates.get(sessionId);
413
- if (!state || !state.inputController) {
414
- throw new Error(`SDK session not found or not active: ${sessionId}`);
415
- }
416
- state.inputController.push({
417
- type: 'user',
418
- message: { role: 'user', content: text },
419
- parent_tool_use_id: null,
420
- session_id: sessionId,
421
- });
422
- }
423
- export function handlePermission(sessionId, requestId, approved) {
424
- const state = runtimeStates.get(sessionId);
425
- if (!state) {
426
- throw new Error(`SDK session not found: ${sessionId}`);
427
- }
428
- const pending = state.permissionQueue.get(requestId);
429
- if (!pending) {
430
- throw new Error(`No pending permission request: ${requestId}`);
431
- }
432
- state.permissionQueue.delete(requestId);
433
- if (approved) {
434
- pending.resolve({ behavior: 'allow' });
435
- }
436
- else {
437
- pending.resolve({ behavior: 'deny', message: 'User denied permission' });
438
- }
439
- }
440
- export function interruptSession(sessionId) {
441
- const state = runtimeStates.get(sessionId);
442
- if (!state) {
443
- throw new Error(`SDK session not found: ${sessionId}`);
444
- }
445
- if (state.query) {
446
- void state.query.interrupt().catch(() => {
447
- // If interrupt fails, abort
448
- state.abortController.abort();
449
- });
450
- }
451
- }
452
- export function killSdkSession(sessionId) {
453
- const state = runtimeStates.get(sessionId);
454
- if (!state)
455
- return;
456
- // Reject all pending permission requests
457
- for (const [, pending] of state.permissionQueue) {
458
- pending.reject(new Error('Session killed'));
459
- }
460
- state.permissionQueue.clear();
461
- // Close input stream
462
- if (state.inputController) {
463
- state.inputController.close();
464
- }
465
- // Close the query
466
- if (state.query) {
467
- state.query.close();
468
- }
469
- // Abort
470
- state.abortController.abort();
471
- // Clear idle timer
472
- if (state.idleTimer)
473
- clearTimeout(state.idleTimer);
474
- // Clean up runtime state
475
- runtimeStates.delete(sessionId);
476
- }
477
- export function onSdkEvent(sessionId, callback) {
478
- const state = runtimeStates.get(sessionId);
479
- if (!state) {
480
- return () => { };
481
- }
482
- state.eventListeners.push(callback);
483
- return () => {
484
- const idx = state.eventListeners.indexOf(callback);
485
- if (idx !== -1)
486
- state.eventListeners.splice(idx, 1);
487
- };
488
- }
489
- export function hasSdkRuntime(sessionId) {
490
- return runtimeStates.has(sessionId);
491
- }
492
- export function serializeSdkSession(session) {
493
- return {
494
- id: session.id,
495
- type: session.type,
496
- agent: session.agent,
497
- mode: 'sdk',
498
- root: session.root,
499
- repoName: session.repoName,
500
- repoPath: session.repoPath,
501
- worktreeName: session.worktreeName,
502
- branchName: session.branchName,
503
- displayName: session.displayName,
504
- createdAt: session.createdAt,
505
- lastActivity: session.lastActivity,
506
- cwd: session.cwd,
507
- sdkSessionId: session.sdkSessionId,
508
- tokenUsage: { ...session.tokenUsage },
509
- estimatedCost: session.estimatedCost,
510
- events: session.events.slice(-100), // Keep last 100 events for restore
511
- };
512
- }
513
- export function restoreSdkSession(serialized, sessionsMap) {
514
- const session = {
515
- id: serialized.id,
516
- type: serialized.type,
517
- agent: serialized.agent,
518
- mode: 'sdk',
519
- root: serialized.root,
520
- repoName: serialized.repoName,
521
- repoPath: serialized.repoPath,
522
- worktreeName: serialized.worktreeName,
523
- branchName: serialized.branchName,
524
- displayName: serialized.displayName,
525
- createdAt: serialized.createdAt,
526
- lastActivity: serialized.lastActivity,
527
- idle: true,
528
- cwd: serialized.cwd,
529
- customCommand: null,
530
- status: 'disconnected',
531
- needsBranchRename: false,
532
- events: serialized.events || [],
533
- sdkSessionId: serialized.sdkSessionId,
534
- tokenUsage: serialized.tokenUsage || { input: 0, output: 0 },
535
- estimatedCost: serialized.estimatedCost || 0,
536
- };
537
- sessionsMap.set(session.id, session);
538
- return session;
539
- }