@respan/cli 0.6.3 → 0.6.5
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/dist/hooks/claude-code.cjs +44 -25
- package/dist/hooks/claude-code.js +47 -31
- package/dist/hooks/gemini-cli.cjs +5 -10
- package/dist/hooks/gemini-cli.js +11 -12
- package/oclif.manifest.json +797 -797
- package/package.json +1 -1
|
@@ -26,6 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
var fs2 = __toESM(require("node:fs"), 1);
|
|
27
27
|
var os2 = __toESM(require("node:os"), 1);
|
|
28
28
|
var path2 = __toESM(require("node:path"), 1);
|
|
29
|
+
var import_node_child_process = require("node:child_process");
|
|
29
30
|
|
|
30
31
|
// src/hooks/shared.ts
|
|
31
32
|
var fs = __toESM(require("node:fs"), 1);
|
|
@@ -870,24 +871,16 @@ function findLatestTranscript() {
|
|
|
870
871
|
return null;
|
|
871
872
|
}
|
|
872
873
|
}
|
|
873
|
-
async function
|
|
874
|
+
async function mainWorker() {
|
|
874
875
|
const scriptStart = Date.now();
|
|
875
|
-
debug("
|
|
876
|
-
if ((process.env.TRACE_TO_RESPAN ?? "").toLowerCase() !== "true") {
|
|
877
|
-
debug("Tracing disabled (TRACE_TO_RESPAN != true)");
|
|
878
|
-
process.exit(0);
|
|
879
|
-
}
|
|
876
|
+
debug("Worker started");
|
|
880
877
|
const creds = resolveCredentials();
|
|
881
878
|
if (!creds) {
|
|
882
879
|
log("ERROR", "No API key found. Run: respan auth login");
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
const payload = readStdinPayload() ?? findLatestTranscript();
|
|
886
|
-
if (!payload) {
|
|
887
|
-
debug("No transcript file found");
|
|
888
|
-
process.exit(0);
|
|
880
|
+
return;
|
|
889
881
|
}
|
|
890
|
-
const
|
|
882
|
+
const sessionId = process.env._RESPAN_SESSION_ID;
|
|
883
|
+
const transcriptPath = process.env._RESPAN_TRANSCRIPT_PATH;
|
|
891
884
|
debug(`Processing session: ${sessionId}`);
|
|
892
885
|
let config = null;
|
|
893
886
|
try {
|
|
@@ -907,10 +900,8 @@ async function main() {
|
|
|
907
900
|
}
|
|
908
901
|
if (cwd) {
|
|
909
902
|
config = loadRespanConfig(path2.join(cwd, ".claude", "respan.json"));
|
|
910
|
-
debug(`Loaded respan.json config from ${cwd}`);
|
|
911
903
|
}
|
|
912
|
-
} catch
|
|
913
|
-
debug(`Failed to load config: ${e}`);
|
|
904
|
+
} catch {
|
|
914
905
|
}
|
|
915
906
|
const maxAttempts = 3;
|
|
916
907
|
let turns = 0;
|
|
@@ -932,20 +923,48 @@ async function main() {
|
|
|
932
923
|
}
|
|
933
924
|
if (turns > 0) break;
|
|
934
925
|
if (attempt < maxAttempts - 1) {
|
|
935
|
-
|
|
936
|
-
debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
|
|
937
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
926
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
938
927
|
}
|
|
939
928
|
}
|
|
940
929
|
const duration = (Date.now() - scriptStart) / 1e3;
|
|
941
930
|
log("INFO", `Processed ${turns} turns in ${duration.toFixed(1)}s`);
|
|
942
|
-
if (duration > 180) log("WARN", `Hook took ${duration.toFixed(1)}s (>3min)`);
|
|
943
931
|
} catch (e) {
|
|
944
932
|
log("ERROR", `Failed to process transcript: ${e}`);
|
|
945
|
-
if (DEBUG_MODE) debug(String(e.stack ?? e));
|
|
946
933
|
}
|
|
947
934
|
}
|
|
948
|
-
main()
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
935
|
+
function main() {
|
|
936
|
+
if (process.env._RESPAN_WORKER === "1") {
|
|
937
|
+
mainWorker().catch((e) => log("ERROR", `Worker crashed: ${e}`));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
debug("Hook started");
|
|
941
|
+
if ((process.env.TRACE_TO_RESPAN ?? "").toLowerCase() !== "true") {
|
|
942
|
+
debug("Tracing disabled (TRACE_TO_RESPAN != true)");
|
|
943
|
+
process.exit(0);
|
|
944
|
+
}
|
|
945
|
+
const payload = readStdinPayload() ?? findLatestTranscript();
|
|
946
|
+
if (!payload) {
|
|
947
|
+
debug("No transcript file found");
|
|
948
|
+
process.exit(0);
|
|
949
|
+
}
|
|
950
|
+
const { sessionId, transcriptPath } = payload;
|
|
951
|
+
debug(`Forking worker for session: ${sessionId}`);
|
|
952
|
+
try {
|
|
953
|
+
const scriptPath = __filename || process.argv[1];
|
|
954
|
+
const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
|
|
955
|
+
env: {
|
|
956
|
+
...process.env,
|
|
957
|
+
_RESPAN_WORKER: "1",
|
|
958
|
+
_RESPAN_SESSION_ID: sessionId,
|
|
959
|
+
_RESPAN_TRANSCRIPT_PATH: transcriptPath
|
|
960
|
+
},
|
|
961
|
+
stdio: "ignore",
|
|
962
|
+
detached: true
|
|
963
|
+
});
|
|
964
|
+
child.unref();
|
|
965
|
+
debug("Worker launched");
|
|
966
|
+
} catch (e) {
|
|
967
|
+
log("ERROR", `Failed to fork worker: ${e}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
main();
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import * as os from 'node:os';
|
|
16
16
|
import * as path from 'node:path';
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
17
18
|
import { initLogging, log, debug, resolveCredentials, loadRespanConfig, loadState, saveState, acquireLock, sendSpans, addDefaultsToAll, resolveSpanFields, buildMetadata, nowISO, latencySeconds, truncate, } from './shared.js';
|
|
18
19
|
// ── Config ────────────────────────────────────────────────────────
|
|
19
20
|
const STATE_DIR = path.join(os.homedir(), '.claude', 'state');
|
|
@@ -550,27 +551,17 @@ function findLatestTranscript() {
|
|
|
550
551
|
}
|
|
551
552
|
}
|
|
552
553
|
// ── Main ──────────────────────────────────────────────────────────
|
|
553
|
-
async function
|
|
554
|
+
async function mainWorker() {
|
|
554
555
|
const scriptStart = Date.now();
|
|
555
|
-
debug('
|
|
556
|
-
if ((process.env.TRACE_TO_RESPAN ?? '').toLowerCase() !== 'true') {
|
|
557
|
-
debug('Tracing disabled (TRACE_TO_RESPAN != true)');
|
|
558
|
-
process.exit(0);
|
|
559
|
-
}
|
|
556
|
+
debug('Worker started');
|
|
560
557
|
const creds = resolveCredentials();
|
|
561
558
|
if (!creds) {
|
|
562
559
|
log('ERROR', 'No API key found. Run: respan auth login');
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
// Find transcript
|
|
566
|
-
const payload = readStdinPayload() ?? findLatestTranscript();
|
|
567
|
-
if (!payload) {
|
|
568
|
-
debug('No transcript file found');
|
|
569
|
-
process.exit(0);
|
|
560
|
+
return;
|
|
570
561
|
}
|
|
571
|
-
const
|
|
562
|
+
const sessionId = process.env._RESPAN_SESSION_ID;
|
|
563
|
+
const transcriptPath = process.env._RESPAN_TRANSCRIPT_PATH;
|
|
572
564
|
debug(`Processing session: ${sessionId}`);
|
|
573
|
-
// Load respan.json config from project directory
|
|
574
565
|
let config = null;
|
|
575
566
|
try {
|
|
576
567
|
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
@@ -590,13 +581,9 @@ async function main() {
|
|
|
590
581
|
}
|
|
591
582
|
if (cwd) {
|
|
592
583
|
config = loadRespanConfig(path.join(cwd, '.claude', 'respan.json'));
|
|
593
|
-
debug(`Loaded respan.json config from ${cwd}`);
|
|
594
584
|
}
|
|
595
585
|
}
|
|
596
|
-
catch
|
|
597
|
-
debug(`Failed to load config: ${e}`);
|
|
598
|
-
}
|
|
599
|
-
// Process with retry
|
|
586
|
+
catch { }
|
|
600
587
|
const maxAttempts = 3;
|
|
601
588
|
let turns = 0;
|
|
602
589
|
try {
|
|
@@ -619,23 +606,52 @@ async function main() {
|
|
|
619
606
|
if (turns > 0)
|
|
620
607
|
break;
|
|
621
608
|
if (attempt < maxAttempts - 1) {
|
|
622
|
-
|
|
623
|
-
debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
|
|
624
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
609
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
625
610
|
}
|
|
626
611
|
}
|
|
627
612
|
const duration = (Date.now() - scriptStart) / 1000;
|
|
628
613
|
log('INFO', `Processed ${turns} turns in ${duration.toFixed(1)}s`);
|
|
629
|
-
if (duration > 180)
|
|
630
|
-
log('WARN', `Hook took ${duration.toFixed(1)}s (>3min)`);
|
|
631
614
|
}
|
|
632
615
|
catch (e) {
|
|
633
616
|
log('ERROR', `Failed to process transcript: ${e}`);
|
|
634
|
-
if (DEBUG_MODE)
|
|
635
|
-
debug(String(e.stack ?? e));
|
|
636
617
|
}
|
|
637
618
|
}
|
|
638
|
-
main()
|
|
639
|
-
|
|
640
|
-
process.
|
|
641
|
-
});
|
|
619
|
+
function main() {
|
|
620
|
+
// Worker mode: re-invoked as detached subprocess
|
|
621
|
+
if (process.env._RESPAN_WORKER === '1') {
|
|
622
|
+
mainWorker().catch((e) => log('ERROR', `Worker crashed: ${e}`));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
debug('Hook started');
|
|
626
|
+
if ((process.env.TRACE_TO_RESPAN ?? '').toLowerCase() !== 'true') {
|
|
627
|
+
debug('Tracing disabled (TRACE_TO_RESPAN != true)');
|
|
628
|
+
process.exit(0);
|
|
629
|
+
}
|
|
630
|
+
const payload = readStdinPayload() ?? findLatestTranscript();
|
|
631
|
+
if (!payload) {
|
|
632
|
+
debug('No transcript file found');
|
|
633
|
+
process.exit(0);
|
|
634
|
+
}
|
|
635
|
+
// Fork self as detached worker so Claude Code doesn't block
|
|
636
|
+
const { sessionId, transcriptPath } = payload;
|
|
637
|
+
debug(`Forking worker for session: ${sessionId}`);
|
|
638
|
+
try {
|
|
639
|
+
const scriptPath = __filename || process.argv[1];
|
|
640
|
+
const child = execFile('node', [scriptPath], {
|
|
641
|
+
env: {
|
|
642
|
+
...process.env,
|
|
643
|
+
_RESPAN_WORKER: '1',
|
|
644
|
+
_RESPAN_SESSION_ID: sessionId,
|
|
645
|
+
_RESPAN_TRANSCRIPT_PATH: transcriptPath,
|
|
646
|
+
},
|
|
647
|
+
stdio: 'ignore',
|
|
648
|
+
detached: true,
|
|
649
|
+
});
|
|
650
|
+
child.unref();
|
|
651
|
+
debug('Worker launched');
|
|
652
|
+
}
|
|
653
|
+
catch (e) {
|
|
654
|
+
log('ERROR', `Failed to fork worker: ${e}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
main();
|
|
@@ -665,7 +665,6 @@ function processBeforeTool(hookData) {
|
|
|
665
665
|
pending.push({ name: toolName, input: toolInput, start_time: nowISO() });
|
|
666
666
|
state.pending_tools = pending;
|
|
667
667
|
saveStreamState(sessionId, state);
|
|
668
|
-
process.stdout.write("{}\n");
|
|
669
668
|
}
|
|
670
669
|
function processAfterTool(hookData) {
|
|
671
670
|
const sessionId = String(hookData.session_id ?? "unknown");
|
|
@@ -690,7 +689,6 @@ function processAfterTool(hookData) {
|
|
|
690
689
|
state.pending_tools = pending;
|
|
691
690
|
state.tool_details = completed;
|
|
692
691
|
saveStreamState(sessionId, state);
|
|
693
|
-
process.stdout.write("{}\n");
|
|
694
692
|
}
|
|
695
693
|
function processChunk(hookData) {
|
|
696
694
|
const sessionId = String(hookData.session_id ?? "unknown");
|
|
@@ -771,12 +769,12 @@ function processChunk(hookData) {
|
|
|
771
769
|
}
|
|
772
770
|
saveStreamState(sessionId, state);
|
|
773
771
|
debug(`Tool call via response parts (finish=${finishReason}), tool_turns=${state.tool_turns}`);
|
|
774
|
-
process.stdout.write("{}\n");
|
|
775
772
|
return;
|
|
776
773
|
}
|
|
774
|
+
const hasPendingTools = (state.pending_tools ?? []).length > 0;
|
|
775
|
+
const hadToolsThisTurn = (state.tool_turns ?? 0) > 0 || hasPendingTools;
|
|
777
776
|
const hasNewText = state.accumulated_text.length > (state.last_send_text_len ?? 0);
|
|
778
|
-
const shouldSend =
|
|
779
|
-
process.stdout.write("{}\n");
|
|
777
|
+
const shouldSend = hasNewText && state.accumulated_text && (isFinished || !hadToolsThisTurn && !toolCallDetected && !chunkText);
|
|
780
778
|
if (!shouldSend) {
|
|
781
779
|
if (toolCallDetected) saveStreamState(sessionId, state);
|
|
782
780
|
return;
|
|
@@ -817,10 +815,8 @@ function processChunk(hookData) {
|
|
|
817
815
|
function main() {
|
|
818
816
|
try {
|
|
819
817
|
const raw = fs2.readFileSync(0, "utf-8");
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
818
|
+
process.stdout.write("{}\n");
|
|
819
|
+
if (!raw.trim()) return;
|
|
824
820
|
const hookData = JSON.parse(raw);
|
|
825
821
|
const event = String(hookData.hook_event_name ?? "");
|
|
826
822
|
const unlock = acquireLock(LOCK_PATH);
|
|
@@ -841,7 +837,6 @@ function main() {
|
|
|
841
837
|
} else {
|
|
842
838
|
log("ERROR", `Hook error: ${e}`);
|
|
843
839
|
}
|
|
844
|
-
process.stdout.write("{}\n");
|
|
845
840
|
}
|
|
846
841
|
}
|
|
847
842
|
main();
|
package/dist/hooks/gemini-cli.js
CHANGED
|
@@ -388,7 +388,6 @@ function processBeforeTool(hookData) {
|
|
|
388
388
|
pending.push({ name: toolName, input: toolInput, start_time: nowISO() });
|
|
389
389
|
state.pending_tools = pending;
|
|
390
390
|
saveStreamState(sessionId, state);
|
|
391
|
-
process.stdout.write('{}\n');
|
|
392
391
|
}
|
|
393
392
|
function processAfterTool(hookData) {
|
|
394
393
|
const sessionId = String(hookData.session_id ?? 'unknown');
|
|
@@ -415,7 +414,6 @@ function processAfterTool(hookData) {
|
|
|
415
414
|
state.pending_tools = pending;
|
|
416
415
|
state.tool_details = completed;
|
|
417
416
|
saveStreamState(sessionId, state);
|
|
418
|
-
process.stdout.write('{}\n');
|
|
419
417
|
}
|
|
420
418
|
// ── AfterModel chunk processing ──────────────────────────────────
|
|
421
419
|
function processChunk(hookData) {
|
|
@@ -507,16 +505,18 @@ function processChunk(hookData) {
|
|
|
507
505
|
}
|
|
508
506
|
saveStreamState(sessionId, state);
|
|
509
507
|
debug(`Tool call via response parts (finish=${finishReason}), tool_turns=${state.tool_turns}`);
|
|
510
|
-
process.stdout.write('{}\n');
|
|
511
508
|
return;
|
|
512
509
|
}
|
|
513
|
-
// Detect completion and send
|
|
510
|
+
// Detect completion and send.
|
|
511
|
+
// If tools have been used this turn, only send on STOP to avoid
|
|
512
|
+
// splitting a multi-tool turn into separate traces.
|
|
513
|
+
const hasPendingTools = (state.pending_tools ?? []).length > 0;
|
|
514
|
+
const hadToolsThisTurn = (state.tool_turns ?? 0) > 0 || hasPendingTools;
|
|
514
515
|
const hasNewText = state.accumulated_text.length > (state.last_send_text_len ?? 0);
|
|
515
|
-
const shouldSend = (
|
|
516
|
-
&& hasNewText
|
|
516
|
+
const shouldSend = (hasNewText
|
|
517
517
|
&& state.accumulated_text
|
|
518
|
-
&& (
|
|
519
|
-
|
|
518
|
+
&& (isFinished
|
|
519
|
+
|| (!hadToolsThisTurn && !toolCallDetected && !chunkText)));
|
|
520
520
|
if (!shouldSend) {
|
|
521
521
|
if (toolCallDetected)
|
|
522
522
|
saveStreamState(sessionId, state);
|
|
@@ -552,10 +552,10 @@ function processChunk(hookData) {
|
|
|
552
552
|
function main() {
|
|
553
553
|
try {
|
|
554
554
|
const raw = fs.readFileSync(0, 'utf-8');
|
|
555
|
-
|
|
556
|
-
|
|
555
|
+
// Respond to Gemini CLI immediately so it doesn't block
|
|
556
|
+
process.stdout.write('{}\n');
|
|
557
|
+
if (!raw.trim())
|
|
557
558
|
return;
|
|
558
|
-
}
|
|
559
559
|
const hookData = JSON.parse(raw);
|
|
560
560
|
const event = String(hookData.hook_event_name ?? '');
|
|
561
561
|
const unlock = acquireLock(LOCK_PATH);
|
|
@@ -581,7 +581,6 @@ function main() {
|
|
|
581
581
|
else {
|
|
582
582
|
log('ERROR', `Hook error: ${e}`);
|
|
583
583
|
}
|
|
584
|
-
process.stdout.write('{}\n');
|
|
585
584
|
}
|
|
586
585
|
}
|
|
587
586
|
main();
|