claude-remote 0.1.10 → 0.2.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +64 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote",
3
- "version": "0.1.10",
3
+ "version": "0.2.1",
4
4
  "description": "Remote control bridge for Claude Code REPL - drive from phone/WebUI",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -120,6 +120,7 @@ let tailTimer = null;
120
120
  let switchWatcher = null;
121
121
  let expectingSwitch = false;
122
122
  let expectingSwitchTimer = null;
123
+ let pendingSwitchTarget = null;
123
124
  let tailRemainder = Buffer.alloc(0);
124
125
  let tailCatchingUp = false; // true while reading historical transcript content
125
126
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
@@ -214,6 +215,10 @@ function maybeAttachHookSession(data, source) {
214
215
  if (currentSessionId && !expectingSwitch) {
215
216
  const currentHasContent = transcriptPath && fileLooksLikeTranscript(transcriptPath);
216
217
  if (!targetHasContent || currentHasContent) {
218
+ if (currentSessionId !== target.sessionId) {
219
+ pendingSwitchTarget = { ...target, seenAt: Date.now(), source };
220
+ log(`Queued pending session-start: ${target.sessionId} (current=${currentSessionId} currentHasContent=${currentHasContent} targetHasContent=${targetHasContent})`);
221
+ }
217
222
  log(`Ignored session-start: ${target.sessionId} (current=${currentSessionId} currentHasContent=${currentHasContent} targetHasContent=${targetHasContent})`);
218
223
  return;
219
224
  }
@@ -238,6 +243,31 @@ function maybeAttachHookSession(data, source) {
238
243
  attachTranscript({ full: target.full }, 0);
239
244
  }
240
245
 
246
+ function maybeAttachPendingSwitchTarget(reason, requireReady = true) {
247
+ if (!pendingSwitchTarget) return false;
248
+ if ((Date.now() - pendingSwitchTarget.seenAt) > 15000) {
249
+ log(`Dropped stale pending switch target: ${pendingSwitchTarget.sessionId}`);
250
+ pendingSwitchTarget = null;
251
+ return false;
252
+ }
253
+ if (pendingSwitchTarget.sessionId === currentSessionId) {
254
+ pendingSwitchTarget = null;
255
+ return false;
256
+ }
257
+
258
+ if (requireReady && !fileLooksLikeTranscript(pendingSwitchTarget.full)) {
259
+ return false;
260
+ }
261
+
262
+ const target = pendingSwitchTarget;
263
+ pendingSwitchTarget = null;
264
+ log(`Attaching pending switch target from ${reason}: ${target.sessionId}`);
265
+ if (tailTimer) { clearInterval(tailTimer); tailTimer = null; }
266
+ if (switchWatcher) { clearInterval(switchWatcher); switchWatcher = null; }
267
+ attachTranscript({ full: target.full }, 0);
268
+ return true;
269
+ }
270
+
241
271
  // ============================================================
242
272
  // 1. Static file server
243
273
  // ============================================================
@@ -550,14 +580,14 @@ wss.on('connection', (ws, req) => {
550
580
  if (claudeProc) {
551
581
  const text = msg.text;
552
582
  log(`Chat input → PTY: "${text.substring(0, 80)}"`);
553
- const isClear = /^\/clear\s*$/i.test(text.trim());
554
- if (isClear) {
583
+ const slashCommand = extractSlashCommand(text);
584
+ if (slashCommand === '/clear') {
555
585
  markExpectingSwitch();
556
586
  }
557
587
  // Slash commands (e.g. /clear, /help, /compact) are internal CLI
558
588
  // commands, not AI turns — the stop hook will never fire, so don't
559
589
  // enter the waiting state.
560
- if (!text.trim().startsWith('/')) {
590
+ if (!slashCommand) {
561
591
  broadcast({ type: 'working_started' });
562
592
  }
563
593
  claudeProc.write(text);
@@ -850,9 +880,34 @@ function fileLooksLikeTranscript(filePath) {
850
880
  return false;
851
881
  }
852
882
 
883
+ function flattenUserContent(content) {
884
+ if (typeof content === 'string') return content;
885
+ if (!Array.isArray(content)) return '';
886
+ return content.map(block => {
887
+ if (!block || typeof block !== 'object') return '';
888
+ if (typeof block.text === 'string') return block.text;
889
+ if (typeof block.content === 'string') return block.content;
890
+ return '';
891
+ }).filter(Boolean).join('\n');
892
+ }
893
+
894
+ function extractSlashCommand(content) {
895
+ const text = flattenUserContent(content).trim();
896
+ if (!text) return '';
897
+
898
+ const commandTagMatch = text.match(/<command-name>\s*(\/[^\s<]+)\s*<\/command-name>/i);
899
+ if (commandTagMatch) return commandTagMatch[1].trim().toLowerCase();
900
+
901
+ const inlineMatch = text.match(/^(\/\S+)/);
902
+ return inlineMatch ? inlineMatch[1].trim().toLowerCase() : '';
903
+ }
904
+
853
905
  function attachTranscript(target, startOffset = 0) {
854
906
  transcriptPath = target.full;
855
907
  currentSessionId = path.basename(transcriptPath, '.jsonl');
908
+ if (pendingSwitchTarget && pendingSwitchTarget.sessionId === currentSessionId) {
909
+ pendingSwitchTarget = null;
910
+ }
856
911
  transcriptOffset = Math.max(0, startOffset);
857
912
  tailRemainder = Buffer.alloc(0);
858
913
  eventBuffer = [];
@@ -893,6 +948,7 @@ function markExpectingSwitch() {
893
948
  log('Expecting-switch flag expired (no new transcript found)');
894
949
  }, 15000);
895
950
  log('Expecting session switch (/clear detected)');
951
+ if (maybeAttachPendingSwitchTarget('markExpectingSwitch')) return;
896
952
  }
897
953
 
898
954
  function startSwitchWatcher() {
@@ -930,6 +986,7 @@ function startSwitchWatcher() {
930
986
  function startTailing() {
931
987
  tailRemainder = Buffer.alloc(0);
932
988
  tailTimer = setInterval(() => {
989
+ if (maybeAttachPendingSwitchTarget('tail_pending_target')) return;
933
990
  if (!transcriptPath) return;
934
991
  try {
935
992
  const stat = fs.statSync(transcriptPath);
@@ -960,14 +1017,14 @@ function startTailing() {
960
1017
  // Detect /clear from JSONL events (covers terminal direct input)
961
1018
  if (event.type === 'user' || (event.message && event.message.role === 'user')) {
962
1019
  const content = event.message && event.message.content;
963
- const isSlashCmd = typeof content === 'string' && content.trim().startsWith('/');
1020
+ const slashCommand = extractSlashCommand(content);
964
1021
  // Only broadcast working_started for live (new) user messages,
965
1022
  // not for historical events during catch-up, and not for slash
966
1023
  // commands (which are CLI commands, not AI turns).
967
- if (!tailCatchingUp && !isSlashCmd) {
1024
+ if (!tailCatchingUp && !slashCommand) {
968
1025
  broadcast({ type: 'working_started' });
969
1026
  }
970
- if (typeof content === 'string' && /^\/clear\s*$/i.test(content.trim())) {
1027
+ if (slashCommand === '/clear') {
971
1028
  markExpectingSwitch();
972
1029
  }
973
1030
  }
@@ -1017,6 +1074,7 @@ function stopTailing() {
1017
1074
  if (switchWatcher) { clearInterval(switchWatcher); switchWatcher = null; }
1018
1075
  if (expectingSwitchTimer) { clearTimeout(expectingSwitchTimer); expectingSwitchTimer = null; }
1019
1076
  expectingSwitch = false;
1077
+ pendingSwitchTarget = null;
1020
1078
  tailRemainder = Buffer.alloc(0);
1021
1079
  }
1022
1080