claude-remote-cli 3.2.0 → 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.
@@ -11,8 +11,8 @@
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-CzJETpYJ.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-H6FwqbR7.css">
14
+ <script type="module" crossorigin src="/assets/index-ggOT9Hda.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BNLfnaOa.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
@@ -257,4 +257,19 @@ async function getUnresolvedCommentCount(repoPath, prNumber, options = {}) {
257
257
  return 0;
258
258
  }
259
259
  }
260
- export { listBranches, normalizeBranchNames, getActivityFeed, getCiStatus, getPrForBranch, getUnresolvedCommentCount, switchBranch, getCommitsAhead, getCurrentBranch, };
260
+ async function getWorkingTreeDiff(repoPath, exec = execFileAsync) {
261
+ try {
262
+ const { stdout } = await exec('git', ['diff', '--shortstat'], { cwd: repoPath, timeout: 5000 });
263
+ // Output like: " 3 files changed, 55 insertions(+), 12 deletions(-)"
264
+ const insertions = stdout.match(/(\d+) insertion/);
265
+ const deletions = stdout.match(/(\d+) deletion/);
266
+ return {
267
+ additions: insertions?.[1] ? parseInt(insertions[1], 10) : 0,
268
+ deletions: deletions?.[1] ? parseInt(deletions[1], 10) : 0,
269
+ };
270
+ }
271
+ catch {
272
+ return { additions: 0, deletions: 0 };
273
+ }
274
+ }
275
+ export { listBranches, normalizeBranchNames, getActivityFeed, getCiStatus, getPrForBranch, getUnresolvedCommentCount, switchBranch, getCommitsAhead, getCurrentBranch, getWorkingTreeDiff, };
@@ -11,7 +11,7 @@ import cookieParser from 'cookie-parser';
11
11
  import { loadConfig, saveConfig, DEFAULTS, readMeta, writeMeta, deleteMeta, ensureMetaDir } from './config.js';
12
12
  import * as auth from './auth.js';
13
13
  import * as sessions from './sessions.js';
14
- import { AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS, serializeAll, restoreFromDisk, activeTmuxSessionNames, startSdkIdleSweep, stopSdkIdleSweep } from './sessions.js';
14
+ import { AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS, serializeAll, restoreFromDisk, activeTmuxSessionNames, getSessionMeta, getAllSessionMeta, populateMetaCache } from './sessions.js';
15
15
  import { setupWebSocket } from './ws.js';
16
16
  import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from './watcher.js';
17
17
  import { isInstalled as serviceIsInstalled } from './service.js';
@@ -134,12 +134,6 @@ async function main() {
134
134
  config.port = parseInt(process.env.CLAUDE_REMOTE_PORT, 10);
135
135
  if (process.env.CLAUDE_REMOTE_HOST)
136
136
  config.host = process.env.CLAUDE_REMOTE_HOST;
137
- // Enable SDK debug logging if requested
138
- if (process.env.CLAUDE_REMOTE_DEBUG_LOG === '1' || config.debugLog) {
139
- const { enableDebugLog } = await import('./sdk-handler.js');
140
- enableDebugLog(true);
141
- console.log('SDK debug logging enabled → ~/.config/claude-remote-cli/debug/');
142
- }
143
137
  push.ensureVapidKeys(config, CONFIG_PATH, saveConfig);
144
138
  if (!config.pinHash) {
145
139
  const pin = await promptPin('Set up a PIN for claude-remote-cli:');
@@ -216,6 +210,8 @@ async function main() {
216
210
  if (restoredCount > 0) {
217
211
  console.log(`Restored ${restoredCount} session(s) from previous update.`);
218
212
  }
213
+ // Populate session metadata cache in background (non-blocking)
214
+ populateMetaCache().catch(() => { });
219
215
  // Push notifications on session idle
220
216
  sessions.onIdleChange((sessionId, idle) => {
221
217
  if (idle) {
@@ -259,6 +255,22 @@ async function main() {
259
255
  app.get('/sessions', requireAuth, (_req, res) => {
260
256
  res.json(sessions.list());
261
257
  });
258
+ // GET /sessions/meta — bulk metadata for all sessions (cached)
259
+ app.get('/sessions/meta', requireAuth, (_req, res) => {
260
+ res.json(getAllSessionMeta());
261
+ });
262
+ // GET /sessions/:id/meta — individual session metadata
263
+ app.get('/sessions/:id/meta', requireAuth, async (req, res) => {
264
+ const id = req.params.id ?? '';
265
+ const refresh = req.query.refresh === 'true';
266
+ try {
267
+ const meta = await getSessionMeta(id, refresh);
268
+ res.json(meta ?? { prNumber: null, additions: 0, deletions: 0, fetchedAt: new Date().toISOString() });
269
+ }
270
+ catch {
271
+ res.json({ prNumber: null, additions: 0, deletions: 0, fetchedAt: new Date().toISOString() });
272
+ }
273
+ });
262
274
  // GET /branches?repo=<path> — list local and remote branches for a repo
263
275
  app.get('/branches', requireAuth, async (req, res) => {
264
276
  const repoPath = typeof req.query.repo === 'string' ? req.query.repo : undefined;
@@ -732,58 +744,6 @@ async function main() {
732
744
  res.status(404).json({ error: 'Session not found' });
733
745
  }
734
746
  });
735
- // POST /sessions/:id/message — send message to SDK session
736
- app.post('/sessions/:id/message', requireAuth, (req, res) => {
737
- const id = req.params['id'];
738
- const { text } = req.body;
739
- if (!text) {
740
- res.status(400).json({ error: 'text is required' });
741
- return;
742
- }
743
- const session = sessions.get(id);
744
- if (!session) {
745
- res.status(404).json({ error: 'Session not found' });
746
- return;
747
- }
748
- if (session.mode !== 'sdk') {
749
- res.status(400).json({ error: 'Session is not an SDK session — use WebSocket for PTY sessions' });
750
- return;
751
- }
752
- try {
753
- sessions.write(id, text);
754
- res.json({ ok: true });
755
- }
756
- catch (err) {
757
- const message = err instanceof Error ? err.message : 'Failed to send message';
758
- res.status(500).json({ error: message });
759
- }
760
- });
761
- // POST /sessions/:id/permission — handle permission approval for SDK session
762
- app.post('/sessions/:id/permission', requireAuth, (req, res) => {
763
- const id = req.params['id'];
764
- const { requestId, approved } = req.body;
765
- if (!requestId || typeof approved !== 'boolean') {
766
- res.status(400).json({ error: 'requestId and approved are required' });
767
- return;
768
- }
769
- const session = sessions.get(id);
770
- if (!session) {
771
- res.status(404).json({ error: 'Session not found' });
772
- return;
773
- }
774
- if (session.mode !== 'sdk') {
775
- res.status(400).json({ error: 'Session is not an SDK session' });
776
- return;
777
- }
778
- try {
779
- sessions.handlePermission(id, requestId, approved);
780
- res.json({ ok: true });
781
- }
782
- catch (err) {
783
- const message = err instanceof Error ? err.message : 'Failed to handle permission';
784
- res.status(500).json({ error: message });
785
- }
786
- });
787
747
  // PATCH /sessions/:id — update displayName and persist to metadata
788
748
  app.patch('/sessions/:id', requireAuth, (req, res) => {
789
749
  const { displayName } = req.body;
@@ -891,15 +851,12 @@ async function main() {
891
851
  catch {
892
852
  // tmux not installed or no sessions — ignore
893
853
  }
894
- // Start SDK idle sweep
895
- startSdkIdleSweep();
896
854
  function gracefulShutdown() {
897
855
  server.close();
898
- stopSdkIdleSweep();
899
856
  // Serialize sessions to disk BEFORE killing them
900
857
  const configDir = path.dirname(CONFIG_PATH);
901
858
  serializeAll(configDir);
902
- // Kill all active sessions (PTY + tmux + SDK)
859
+ // Kill all active sessions (PTY + tmux)
903
860
  for (const s of sessions.list()) {
904
861
  try {
905
862
  sessions.kill(s.id);
@@ -40,33 +40,6 @@ export function removeSession(sessionId) {
40
40
  entry.sessionIds.delete(sessionId);
41
41
  }
42
42
  }
43
- export function enrichNotification(event) {
44
- try {
45
- switch (event.type) {
46
- case 'tool_call': {
47
- const action = event.toolName || 'use a tool';
48
- const target = event.path || (event.toolInput && typeof event.toolInput === 'object'
49
- ? event.toolInput.file_path || event.toolInput.command || ''
50
- : '');
51
- const msg = target
52
- ? `Claude wants to ${action} ${target}`
53
- : `Claude wants to ${action}`;
54
- return msg.slice(0, 200);
55
- }
56
- case 'turn_completed':
57
- return 'Claude finished';
58
- case 'error': {
59
- const brief = (event.text || 'unknown error').slice(0, 150);
60
- return `Claude hit an error: ${brief}`;
61
- }
62
- default:
63
- return 'Claude is waiting for your input';
64
- }
65
- }
66
- catch {
67
- return 'Claude is waiting for your input';
68
- }
69
- }
70
43
  function truncatePayload(payload) {
71
44
  if (payload.length <= MAX_PAYLOAD_SIZE)
72
45
  return payload;
@@ -85,19 +58,15 @@ function truncatePayload(payload) {
85
58
  }
86
59
  return payload.slice(0, MAX_PAYLOAD_SIZE);
87
60
  }
88
- export function notifySessionIdle(sessionId, session, sdkEvent) {
61
+ export function notifySessionIdle(sessionId, session) {
89
62
  if (!vapidPublicKey)
90
63
  return;
91
- const enrichedMessage = sdkEvent ? enrichNotification(sdkEvent) : undefined;
92
64
  const payloadObj = {
93
65
  type: 'session-attention',
94
66
  sessionId,
95
67
  displayName: session.displayName,
96
68
  sessionType: session.type,
97
69
  };
98
- if (enrichedMessage) {
99
- payloadObj.enrichedMessage = enrichedMessage;
100
- }
101
70
  const payload = truncatePayload(JSON.stringify(payloadObj));
102
71
  for (const [endpoint, entry] of subscriptions) {
103
72
  if (!entry.sessionIds.has(sessionId))
@@ -5,14 +5,13 @@ import { execFile } from 'node:child_process';
5
5
  import { promisify } from 'node:util';
6
6
  import { AGENT_COMMANDS, AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS } from './types.js';
7
7
  import { createPtySession } from './pty-handler.js';
8
- import { createSdkSession, killSdkSession, sendMessage as sdkSendMessage, handlePermission as sdkHandlePermission, serializeSdkSession, restoreSdkSession } from './sdk-handler.js';
8
+ import { getPrForBranch, getWorkingTreeDiff } from './git.js';
9
9
  const execFileAsync = promisify(execFile);
10
10
  const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
11
- const SDK_IDLE_CHECK_INTERVAL_MS = 60 * 1000; // 60 seconds
12
- const SDK_MAX_IDLE_MS = 30 * 60 * 1000; // 30 minutes
13
- const SDK_MAX_IDLE_SESSIONS = 5;
14
11
  // In-memory registry: id -> Session
15
12
  const sessions = new Map();
13
+ // Session metadata cache: session ID or worktree path -> SessionMeta
14
+ const metaCache = new Map();
16
15
  let terminalCounter = 0;
17
16
  const idleChangeCallbacks = [];
18
17
  function onIdleChange(cb) {
@@ -28,26 +27,7 @@ function fireSessionEnd(sessionId, repoPath, branchName) {
28
27
  }
29
28
  function create({ id: providedId, type, agent = 'claude', repoName, repoPath, cwd, root, worktreeName, branchName, displayName, command, args = [], cols = 80, rows = 24, configPath, useTmux: paramUseTmux, tmuxSessionName: paramTmuxSessionName, initialScrollback, restored: paramRestored, needsBranchRename: paramNeedsBranchRename, branchRenamePrompt: paramBranchRenamePrompt }) {
30
29
  const id = providedId || crypto.randomBytes(8).toString('hex');
31
- // Dispatch: if agent is claude, no custom command, try SDK first
32
- if (agent === 'claude' && !command) {
33
- const sdkResult = createSdkSession({
34
- id,
35
- type,
36
- agent,
37
- repoName,
38
- repoPath,
39
- cwd,
40
- root,
41
- worktreeName,
42
- branchName,
43
- displayName,
44
- }, sessions, idleChangeCallbacks);
45
- if (!('fallback' in sdkResult)) {
46
- return { ...sdkResult.result, pid: undefined, needsBranchRename: false };
47
- }
48
- // SDK init failed — fall through to PTY
49
- }
50
- // PTY path: codex, terminal, custom command, or SDK fallback
30
+ // PTY path
51
31
  const ptyParams = {
52
32
  id,
53
33
  type,
@@ -99,10 +79,10 @@ function list() {
99
79
  idle: s.idle,
100
80
  cwd: s.cwd,
101
81
  customCommand: s.customCommand,
102
- useTmux: s.mode === 'pty' ? s.useTmux : false,
103
- tmuxSessionName: s.mode === 'pty' ? s.tmuxSessionName : '',
82
+ useTmux: s.useTmux,
83
+ tmuxSessionName: s.tmuxSessionName,
104
84
  status: s.status,
105
- needsBranchRename: s.mode === 'pty' ? !!s.needsBranchRename : false,
85
+ needsBranchRename: !!s.needsBranchRename,
106
86
  }))
107
87
  .sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
108
88
  }
@@ -118,26 +98,21 @@ function kill(id) {
118
98
  if (!session) {
119
99
  throw new Error(`Session not found: ${id}`);
120
100
  }
121
- if (session.mode === 'pty') {
122
- try {
123
- session.pty.kill('SIGTERM');
124
- }
125
- catch {
126
- // PTY may already be dead (e.g. disconnected sessions) — still delete from registry
127
- }
128
- if (session.tmuxSessionName) {
129
- execFile('tmux', ['kill-session', '-t', session.tmuxSessionName], () => { });
130
- }
101
+ try {
102
+ session.pty.kill('SIGTERM');
131
103
  }
132
- else if (session.mode === 'sdk') {
133
- killSdkSession(id);
104
+ catch {
105
+ // PTY may already be dead (e.g. disconnected sessions) — still delete from registry
106
+ }
107
+ if (session.tmuxSessionName) {
108
+ execFile('tmux', ['kill-session', '-t', session.tmuxSessionName], () => { });
134
109
  }
135
110
  fireSessionEnd(id, session.repoPath, session.branchName);
136
111
  sessions.delete(id);
137
112
  }
138
113
  function killAllTmuxSessions() {
139
114
  for (const session of sessions.values()) {
140
- if (session.mode === 'pty' && session.tmuxSessionName) {
115
+ if (session.tmuxSessionName) {
141
116
  execFile('tmux', ['kill-session', '-t', session.tmuxSessionName], () => { });
142
117
  }
143
118
  }
@@ -147,25 +122,14 @@ function resize(id, cols, rows) {
147
122
  if (!session) {
148
123
  throw new Error(`Session not found: ${id}`);
149
124
  }
150
- if (session.mode === 'pty') {
151
- session.pty.resize(cols, rows);
152
- }
153
- // SDK sessions don't support resize (no PTY)
125
+ session.pty.resize(cols, rows);
154
126
  }
155
127
  function write(id, data) {
156
128
  const session = sessions.get(id);
157
129
  if (!session) {
158
130
  throw new Error(`Session not found: ${id}`);
159
131
  }
160
- if (session.mode === 'pty') {
161
- session.pty.write(data);
162
- }
163
- else if (session.mode === 'sdk') {
164
- sdkSendMessage(id, data);
165
- }
166
- }
167
- function handlePermission(id, requestId, approved) {
168
- sdkHandlePermission(id, requestId, approved);
132
+ session.pty.write(data);
169
133
  }
170
134
  function findRepoSession(repoPath) {
171
135
  return list().find((s) => s.type === 'repo' && s.repoPath === repoPath);
@@ -177,39 +141,32 @@ function serializeAll(configDir) {
177
141
  const scrollbackDirPath = path.join(configDir, 'scrollback');
178
142
  fs.mkdirSync(scrollbackDirPath, { recursive: true });
179
143
  const serializedPty = [];
180
- const serializedSdk = [];
181
144
  for (const session of sessions.values()) {
182
- if (session.mode === 'pty') {
183
- // Write scrollback to disk
184
- const scrollbackPath = path.join(scrollbackDirPath, session.id + '.buf');
185
- fs.writeFileSync(scrollbackPath, session.scrollback.join(''), 'utf-8');
186
- serializedPty.push({
187
- id: session.id,
188
- type: session.type,
189
- agent: session.agent,
190
- root: session.root,
191
- repoName: session.repoName,
192
- repoPath: session.repoPath,
193
- worktreeName: session.worktreeName,
194
- branchName: session.branchName,
195
- displayName: session.displayName,
196
- createdAt: session.createdAt,
197
- lastActivity: session.lastActivity,
198
- useTmux: session.useTmux,
199
- tmuxSessionName: session.tmuxSessionName,
200
- customCommand: session.customCommand,
201
- cwd: session.cwd,
202
- });
203
- }
204
- else if (session.mode === 'sdk') {
205
- serializedSdk.push(serializeSdkSession(session));
206
- }
145
+ // Write scrollback to disk
146
+ const scrollbackPath = path.join(scrollbackDirPath, session.id + '.buf');
147
+ fs.writeFileSync(scrollbackPath, session.scrollback.join(''), 'utf-8');
148
+ serializedPty.push({
149
+ id: session.id,
150
+ type: session.type,
151
+ agent: session.agent,
152
+ root: session.root,
153
+ repoName: session.repoName,
154
+ repoPath: session.repoPath,
155
+ worktreeName: session.worktreeName,
156
+ branchName: session.branchName,
157
+ displayName: session.displayName,
158
+ createdAt: session.createdAt,
159
+ lastActivity: session.lastActivity,
160
+ useTmux: session.useTmux,
161
+ tmuxSessionName: session.tmuxSessionName,
162
+ customCommand: session.customCommand,
163
+ cwd: session.cwd,
164
+ });
207
165
  }
208
166
  const pending = {
209
167
  version: 1,
210
168
  timestamp: new Date().toISOString(),
211
169
  sessions: serializedPty,
212
- sdkSessions: serializedSdk.length > 0 ? serializedSdk : undefined,
213
170
  };
214
171
  fs.writeFileSync(path.join(configDir, 'pending-sessions.json'), JSON.stringify(pending, null, 2), 'utf-8');
215
172
  }
@@ -309,18 +266,6 @@ async function restoreFromDisk(configDir) {
309
266
  }
310
267
  catch { /* ignore */ }
311
268
  }
312
- // Restore SDK sessions (as disconnected — they can't resume a live process)
313
- if (pending.sdkSessions) {
314
- for (const sdkData of pending.sdkSessions) {
315
- try {
316
- restoreSdkSession(sdkData, sessions);
317
- restored++;
318
- }
319
- catch {
320
- console.error(`Failed to restore SDK session ${sdkData.id} (${sdkData.displayName})`);
321
- }
322
- }
323
- }
324
269
  // Clean up
325
270
  try {
326
271
  fs.unlinkSync(pendingPath);
@@ -336,55 +281,66 @@ async function restoreFromDisk(configDir) {
336
281
  function activeTmuxSessionNames() {
337
282
  const names = new Set();
338
283
  for (const session of sessions.values()) {
339
- if (session.mode === 'pty' && session.tmuxSessionName)
284
+ if (session.tmuxSessionName)
340
285
  names.add(session.tmuxSessionName);
341
286
  }
342
287
  return names;
343
288
  }
344
- // SDK idle sweep: check every 60s, terminate SDK sessions idle > 30min, max 5 idle
345
- let sdkIdleSweepTimer = null;
346
- function startSdkIdleSweep() {
347
- if (sdkIdleSweepTimer)
348
- return;
349
- sdkIdleSweepTimer = setInterval(() => {
350
- const now = Date.now();
351
- const sdkSessions = [];
352
- for (const session of sessions.values()) {
353
- if (session.mode === 'sdk') {
354
- sdkSessions.push(session);
355
- }
356
- }
357
- // Terminate sessions idle > 30 minutes
358
- for (const session of sdkSessions) {
359
- const lastActivity = new Date(session.lastActivity).getTime();
360
- if (session.idle && (now - lastActivity) > SDK_MAX_IDLE_MS) {
361
- console.log(`SDK idle sweep: terminating session ${session.id} (${session.displayName}) — idle for ${Math.round((now - lastActivity) / 60000)}min`);
362
- try {
363
- kill(session.id);
364
- }
365
- catch { /* already dead */ }
366
- }
367
- }
368
- // LRU eviction: if more than 5 idle SDK sessions remain, evict oldest
369
- const idleSdkSessions = Array.from(sessions.values())
370
- .filter((s) => s.mode === 'sdk' && s.idle)
371
- .sort((a, b) => a.lastActivity.localeCompare(b.lastActivity));
372
- while (idleSdkSessions.length > SDK_MAX_IDLE_SESSIONS) {
373
- const oldest = idleSdkSessions.shift();
374
- console.log(`SDK idle sweep: evicting session ${oldest.id} (${oldest.displayName}) — LRU`);
375
- try {
376
- kill(oldest.id);
289
+ async function fetchMetaForSession(session) {
290
+ const repoPath = session.repoPath;
291
+ const branch = session.branchName;
292
+ let prNumber = null;
293
+ let additions = 0;
294
+ let deletions = 0;
295
+ if (branch) {
296
+ try {
297
+ const pr = await getPrForBranch(repoPath, branch);
298
+ if (pr) {
299
+ prNumber = pr.number;
300
+ additions = pr.additions;
301
+ deletions = pr.deletions;
377
302
  }
378
- catch { /* already dead */ }
379
303
  }
380
- }, SDK_IDLE_CHECK_INTERVAL_MS);
304
+ catch { /* gh CLI unavailable */ }
305
+ }
306
+ // Fallback to working tree diff if no PR data
307
+ if (additions === 0 && deletions === 0) {
308
+ const diff = await getWorkingTreeDiff(repoPath);
309
+ additions = diff.additions;
310
+ deletions = diff.deletions;
311
+ }
312
+ return { prNumber, additions, deletions, fetchedAt: new Date().toISOString() };
381
313
  }
382
- function stopSdkIdleSweep() {
383
- if (sdkIdleSweepTimer) {
384
- clearInterval(sdkIdleSweepTimer);
385
- sdkIdleSweepTimer = null;
314
+ async function getSessionMeta(id, refresh = false) {
315
+ if (!refresh && metaCache.has(id))
316
+ return metaCache.get(id);
317
+ const session = sessions.get(id);
318
+ if (!session)
319
+ return metaCache.get(id) ?? null;
320
+ const summary = list().find(s => s.id === id);
321
+ if (!summary)
322
+ return null;
323
+ const meta = await fetchMetaForSession(summary);
324
+ metaCache.set(id, meta);
325
+ return meta;
326
+ }
327
+ function getAllSessionMeta() {
328
+ const result = {};
329
+ for (const [key, meta] of metaCache) {
330
+ result[key] = meta;
386
331
  }
332
+ return result;
333
+ }
334
+ // Populate cache for all active sessions (called on startup or refresh)
335
+ async function populateMetaCache() {
336
+ const allSessions = list();
337
+ await Promise.allSettled(allSessions.map(async (s) => {
338
+ if (!metaCache.has(s.id)) {
339
+ const meta = await fetchMetaForSession(s);
340
+ metaCache.set(s.id, meta);
341
+ }
342
+ }));
387
343
  }
388
344
  // Re-export pty-handler utilities for backward compatibility
389
345
  export { generateTmuxSessionName, resolveTmuxSpawn } from './pty-handler.js';
390
- export { create, get, list, kill, killAllTmuxSessions, resize, updateDisplayName, write, handlePermission, onIdleChange, onSessionEnd, fireSessionEnd, findRepoSession, nextTerminalName, serializeAll, restoreFromDisk, activeTmuxSessionNames, startSdkIdleSweep, stopSdkIdleSweep, AGENT_COMMANDS, AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS };
346
+ export { create, get, list, kill, killAllTmuxSessions, resize, updateDisplayName, write, onIdleChange, onSessionEnd, fireSessionEnd, findRepoSession, nextTerminalName, serializeAll, restoreFromDisk, activeTmuxSessionNames, getSessionMeta, getAllSessionMeta, populateMetaCache, AGENT_COMMANDS, AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS };
@@ -1,4 +1,4 @@
1
- // Agent command records (shared by PTY and SDK handlers)
1
+ // Agent command records
2
2
  export const AGENT_COMMANDS = {
3
3
  claude: 'claude',
4
4
  codex: 'codex',
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,84 +176,6 @@ 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
  });