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.
- package/package.json +1 -1
- package/server.js +64 -6
package/package.json
CHANGED
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
|
|
554
|
-
if (
|
|
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 (!
|
|
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
|
|
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 && !
|
|
1024
|
+
if (!tailCatchingUp && !slashCommand) {
|
|
968
1025
|
broadcast({ type: 'working_started' });
|
|
969
1026
|
}
|
|
970
|
-
if (
|
|
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
|
|