@respan/cli 0.6.4 → 0.6.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.
@@ -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 main() {
874
+ async function mainWorker() {
874
875
  const scriptStart = Date.now();
875
- debug("Hook started");
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
- process.exit(0);
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 { sessionId, transcriptPath } = payload;
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 (e) {
913
- debug(`Failed to load config: ${e}`);
904
+ } catch {
914
905
  }
915
906
  const maxAttempts = 3;
916
907
  let turns = 0;
@@ -932,20 +923,49 @@ async function main() {
932
923
  }
933
924
  if (turns > 0) break;
934
925
  if (attempt < maxAttempts - 1) {
935
- const delay = 500 * (attempt + 1);
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().catch((e) => {
949
- log("ERROR", `Hook crashed: ${e}`);
950
- process.exit(1);
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
+ process.exit(0);
970
+ }
971
+ 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 main() {
554
+ async function mainWorker() {
554
555
  const scriptStart = Date.now();
555
- debug('Hook started');
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
- process.exit(0);
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 { sessionId, transcriptPath } = payload;
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 (e) {
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,54 @@ async function main() {
619
606
  if (turns > 0)
620
607
  break;
621
608
  if (attempt < maxAttempts - 1) {
622
- const delay = 500 * (attempt + 1);
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().catch((e) => {
639
- log('ERROR', `Hook crashed: ${e}`);
640
- process.exit(1);
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
+ // Exit immediately so Claude Code doesn't block
657
+ process.exit(0);
658
+ }
659
+ 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,14 +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
  }
777
774
  const hasPendingTools = (state.pending_tools ?? []).length > 0;
778
775
  const hadToolsThisTurn = (state.tool_turns ?? 0) > 0 || hasPendingTools;
779
776
  const hasNewText = state.accumulated_text.length > (state.last_send_text_len ?? 0);
780
777
  const shouldSend = hasNewText && state.accumulated_text && (isFinished || !hadToolsThisTurn && !toolCallDetected && !chunkText);
781
- process.stdout.write("{}\n");
782
778
  if (!shouldSend) {
783
779
  if (toolCallDetected) saveStreamState(sessionId, state);
784
780
  return;
@@ -819,10 +815,8 @@ function processChunk(hookData) {
819
815
  function main() {
820
816
  try {
821
817
  const raw = fs2.readFileSync(0, "utf-8");
822
- if (!raw.trim()) {
823
- process.stdout.write("{}\n");
824
- return;
825
- }
818
+ process.stdout.write("{}\n");
819
+ if (!raw.trim()) return;
826
820
  const hookData = JSON.parse(raw);
827
821
  const event = String(hookData.hook_event_name ?? "");
828
822
  const unlock = acquireLock(LOCK_PATH);
@@ -843,7 +837,6 @@ function main() {
843
837
  } else {
844
838
  log("ERROR", `Hook error: ${e}`);
845
839
  }
846
- process.stdout.write("{}\n");
847
840
  }
848
841
  }
849
842
  main();
@@ -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,7 +505,6 @@ 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
510
  // Detect completion and send.
@@ -520,7 +517,6 @@ function processChunk(hookData) {
520
517
  && state.accumulated_text
521
518
  && (isFinished
522
519
  || (!hadToolsThisTurn && !toolCallDetected && !chunkText)));
523
- process.stdout.write('{}\n');
524
520
  if (!shouldSend) {
525
521
  if (toolCallDetected)
526
522
  saveStreamState(sessionId, state);
@@ -556,10 +552,10 @@ function processChunk(hookData) {
556
552
  function main() {
557
553
  try {
558
554
  const raw = fs.readFileSync(0, 'utf-8');
559
- if (!raw.trim()) {
560
- process.stdout.write('{}\n');
555
+ // Respond to Gemini CLI immediately so it doesn't block
556
+ process.stdout.write('{}\n');
557
+ if (!raw.trim())
561
558
  return;
562
- }
563
559
  const hookData = JSON.parse(raw);
564
560
  const event = String(hookData.hook_event_name ?? '');
565
561
  const unlock = acquireLock(LOCK_PATH);
@@ -585,7 +581,6 @@ function main() {
585
581
  else {
586
582
  log('ERROR', `Hook error: ${e}`);
587
583
  }
588
- process.stdout.write('{}\n');
589
584
  }
590
585
  }
591
586
  main();