claude-remote 0.1.4 → 0.1.6

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 +50 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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
@@ -121,6 +121,7 @@ let switchWatcher = null;
121
121
  let expectingSwitch = false;
122
122
  let expectingSwitchTimer = null;
123
123
  let tailRemainder = Buffer.alloc(0);
124
+ let tailCatchingUp = false; // true while reading historical transcript content
124
125
  const isTTY = process.stdin.isTTY && process.stdout.isTTY;
125
126
  const LEGACY_REPLAY_DELAY_MS = 1500;
126
127
  const IMAGE_UPLOAD_TTL_MS = 15 * 60 * 1000;
@@ -196,16 +197,37 @@ function maybeAttachHookSession(data, source) {
196
197
  const target = resolveHookTranscript(data);
197
198
  if (!target) return;
198
199
 
200
+ // Already attached to this exact session — no-op
199
201
  if (currentSessionId === target.sessionId && transcriptPath &&
200
202
  normalizeFsPath(transcriptPath) === normalizeFsPath(target.full)) {
201
203
  return;
202
204
  }
203
205
 
204
- // session-start is authoritative always allow it to switch sessions.
205
- // pre-tool-use is opportunistic only accept if expecting a switch.
206
- if (currentSessionId && currentSessionId !== target.sessionId && !expectingSwitch && source !== 'session-start') {
207
- log(`Ignored hook session from ${source}: ${target.sessionId} (current=${currentSessionId})`);
208
- return;
206
+ // Switching to a different session apply source-specific guards.
207
+ if (currentSessionId && currentSessionId !== target.sessionId && !expectingSwitch) {
208
+ const targetHasContent = fileLooksLikeTranscript(target.full);
209
+
210
+ if (source === 'session-start') {
211
+ // --resume triggers two session-start hooks in unpredictable order.
212
+ // Don't switch away from a transcript with conversation content to an
213
+ // empty one — the one with content is the real resumed session.
214
+ const currentHasContent = transcriptPath && fileLooksLikeTranscript(transcriptPath);
215
+ if (currentHasContent && !targetHasContent) {
216
+ log(`Ignored hook session from ${source}: ${target.sessionId} (current has content, target empty)`);
217
+ return;
218
+ }
219
+ } else if (source === 'pre-tool-use') {
220
+ // pre-tool-use is the most reliable signal — it comes from the actually
221
+ // running Claude process. Accept it if the transcript has content.
222
+ if (!targetHasContent) {
223
+ log(`Ignored hook session from ${source}: ${target.sessionId} (no conversation content)`);
224
+ return;
225
+ }
226
+ } else {
227
+ // Unknown source — block unexpected switches
228
+ log(`Ignored hook session from ${source}: ${target.sessionId} (current=${currentSessionId})`);
229
+ return;
230
+ }
209
231
  }
210
232
 
211
233
  log(`Hook session attached from ${source}: ${target.sessionId}`);
@@ -826,7 +848,16 @@ function attachTranscript(target, startOffset = 0) {
826
848
  eventBuffer = [];
827
849
  eventSeq = 0;
828
850
 
829
- log(`Transcript attached: ${currentSessionId} (offset=${transcriptOffset})`);
851
+ // If transcript file already has content, mark as catching up so we don't
852
+ // broadcast working_started for historical user messages.
853
+ try {
854
+ const stat = fs.statSync(transcriptPath);
855
+ tailCatchingUp = stat.size > transcriptOffset;
856
+ } catch {
857
+ tailCatchingUp = false;
858
+ }
859
+
860
+ log(`Transcript attached: ${currentSessionId} (offset=${transcriptOffset} catchUp=${tailCatchingUp})`);
830
861
  broadcast({
831
862
  type: 'transcript_ready',
832
863
  transcript: transcriptPath,
@@ -886,7 +917,14 @@ function startTailing() {
886
917
  if (!transcriptPath) return;
887
918
  try {
888
919
  const stat = fs.statSync(transcriptPath);
889
- if (stat.size <= transcriptOffset) return;
920
+ if (stat.size <= transcriptOffset) {
921
+ // Caught up to file end — initial catch-up phase is over
922
+ if (tailCatchingUp) {
923
+ tailCatchingUp = false;
924
+ log('Tail catch-up complete, live mode');
925
+ }
926
+ return;
927
+ }
890
928
 
891
929
  const fd = fs.openSync(transcriptPath, 'r');
892
930
  const buf = Buffer.alloc(stat.size - transcriptOffset);
@@ -905,7 +943,11 @@ function startTailing() {
905
943
  const event = JSON.parse(line);
906
944
  // Detect /clear from JSONL events (covers terminal direct input)
907
945
  if (event.type === 'user' || (event.message && event.message.role === 'user')) {
908
- broadcast({ type: 'working_started' });
946
+ // Only broadcast working_started for live (new) user messages,
947
+ // not for historical events during catch-up.
948
+ if (!tailCatchingUp) {
949
+ broadcast({ type: 'working_started' });
950
+ }
909
951
  const content = event.message && event.message.content;
910
952
  if (typeof content === 'string' && /^\/clear\s*$/i.test(content.trim())) {
911
953
  markExpectingSwitch();