@respan/cli 0.6.9 → 0.7.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.
@@ -340,7 +340,7 @@ function toOtlpPayload(spans) {
340
340
  })
341
341
  },
342
342
  scopeSpans: [{
343
- scope: { name: "respan-cli-hooks", version: "0.5.3" },
343
+ scope: { name: "respan-cli-hooks", version: "0.7.0" },
344
344
  spans: otlpSpans
345
345
  }]
346
346
  }]
@@ -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);
@@ -339,7 +340,7 @@ function toOtlpPayload(spans) {
339
340
  })
340
341
  },
341
342
  scopeSpans: [{
342
- scope: { name: "respan-cli-hooks", version: "0.5.3" },
343
+ scope: { name: "respan-cli-hooks", version: "0.7.0" },
343
344
  spans: otlpSpans
344
345
  }]
345
346
  }]
@@ -699,50 +700,18 @@ function findLatestSessionFile() {
699
700
  return null;
700
701
  }
701
702
  }
702
- async function main() {
703
+ async function mainWorker() {
703
704
  const scriptStart = Date.now();
704
- debug("Codex hook started");
705
- if (process.argv.length < 3) {
706
- debug("No argument provided (expected JSON payload in argv[2])");
707
- process.exit(0);
708
- }
709
- let payload;
710
- try {
711
- payload = JSON.parse(process.argv[2]);
712
- } catch (e) {
713
- debug(`Invalid JSON in argv[2]: ${e}`);
714
- process.exit(0);
715
- }
716
- const eventType = String(payload.type ?? "");
717
- if (eventType !== "agent-turn-complete") {
718
- debug(`Ignoring event type: ${eventType}`);
719
- process.exit(0);
720
- }
721
- let sessionId = String(payload["thread-id"] ?? "");
722
- if (!sessionId) {
723
- debug("No thread-id in notify payload");
724
- process.exit(0);
725
- }
726
- debug(`Processing notify: type=${eventType}, session=${sessionId}`);
705
+ debug("Worker started");
706
+ const sessionId = process.env._RESPAN_CODEX_SESSION;
707
+ const sessionFile = process.env._RESPAN_CODEX_FILE;
708
+ const cwd = process.env._RESPAN_CODEX_CWD ?? "";
727
709
  const creds = resolveCredentials();
728
710
  if (!creds) {
729
- log("ERROR", "No API key found. Run: respan auth login");
730
- process.exit(0);
731
- }
732
- let sessionFile = findSessionFile(sessionId);
733
- if (!sessionFile) {
734
- const latest = findLatestSessionFile();
735
- if (latest) {
736
- sessionId = latest.sessionId;
737
- sessionFile = latest.sessionFile;
738
- } else {
739
- debug("No session file found");
740
- process.exit(0);
741
- }
711
+ log("ERROR", "No API key");
712
+ return;
742
713
  }
743
- const cwd = String(payload.cwd ?? "");
744
714
  const config = cwd ? loadRespanConfig(path2.join(cwd, ".codex", "respan.json")) : null;
745
- if (config) debug(`Loaded respan.json config from ${cwd}`);
746
715
  const maxAttempts = 3;
747
716
  let turns = 0;
748
717
  try {
@@ -774,20 +743,73 @@ async function main() {
774
743
  }
775
744
  if (turns > 0) break;
776
745
  if (attempt < maxAttempts - 1) {
777
- const delay = 500 * (attempt + 1);
778
- debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
779
- await new Promise((r) => setTimeout(r, delay));
746
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
780
747
  }
781
748
  }
782
749
  const duration = (Date.now() - scriptStart) / 1e3;
783
750
  log("INFO", `Processed ${turns} turns in ${duration.toFixed(1)}s`);
784
- if (duration > 180) log("WARN", `Hook took ${duration.toFixed(1)}s (>3min)`);
785
751
  } catch (e) {
786
752
  log("ERROR", `Failed to process session: ${e}`);
787
- if (DEBUG_MODE) debug(String(e.stack ?? e));
788
753
  }
789
754
  }
790
- main().catch((e) => {
791
- log("ERROR", `Hook crashed: ${e}`);
792
- process.exit(1);
793
- });
755
+ function main() {
756
+ if (process.env._RESPAN_CODEX_WORKER === "1") {
757
+ mainWorker().catch((e) => log("ERROR", `Worker crashed: ${e}`));
758
+ return;
759
+ }
760
+ debug("Codex hook started");
761
+ if (process.argv.length < 3) {
762
+ debug("No argument provided");
763
+ process.exit(0);
764
+ }
765
+ let payload;
766
+ try {
767
+ payload = JSON.parse(process.argv[2]);
768
+ } catch (e) {
769
+ debug(`Invalid JSON in argv[2]: ${e}`);
770
+ process.exit(0);
771
+ }
772
+ const eventType = String(payload.type ?? "");
773
+ if (eventType !== "agent-turn-complete") {
774
+ debug(`Ignoring event type: ${eventType}`);
775
+ process.exit(0);
776
+ }
777
+ let sessionId = String(payload["thread-id"] ?? "");
778
+ if (!sessionId) {
779
+ debug("No thread-id in notify payload");
780
+ process.exit(0);
781
+ }
782
+ let sessionFile = findSessionFile(sessionId);
783
+ if (!sessionFile) {
784
+ const latest = findLatestSessionFile();
785
+ if (latest) {
786
+ sessionId = latest.sessionId;
787
+ sessionFile = latest.sessionFile;
788
+ } else {
789
+ debug("No session file found");
790
+ process.exit(0);
791
+ }
792
+ }
793
+ const cwd = String(payload.cwd ?? "");
794
+ debug(`Forking worker for session: ${sessionId}`);
795
+ try {
796
+ const scriptPath = __filename || process.argv[1];
797
+ const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
798
+ env: {
799
+ ...process.env,
800
+ _RESPAN_CODEX_WORKER: "1",
801
+ _RESPAN_CODEX_SESSION: sessionId,
802
+ _RESPAN_CODEX_FILE: sessionFile,
803
+ _RESPAN_CODEX_CWD: cwd
804
+ },
805
+ stdio: "ignore",
806
+ detached: true
807
+ });
808
+ child.unref();
809
+ debug("Worker launched");
810
+ } catch (e) {
811
+ log("ERROR", `Failed to fork worker: ${e}`);
812
+ }
813
+ process.exit(0);
814
+ }
815
+ main();
@@ -16,6 +16,7 @@
16
16
  import * as fs from 'node:fs';
17
17
  import * as os from 'node:os';
18
18
  import * as path from 'node:path';
19
+ import { execFile } from 'node:child_process';
19
20
  import { initLogging, log, debug, resolveCredentials, loadRespanConfig, loadState, saveState, acquireLock, sendSpans, addDefaultsToAll, resolveSpanFields, buildMetadata, nowISO, latencySeconds, truncate, } from './shared.js';
20
21
  // ── Config ────────────────────────────────────────────────────────
21
22
  const STATE_DIR = path.join(os.homedir(), '.codex', 'state');
@@ -363,57 +364,18 @@ function findLatestSessionFile() {
363
364
  }
364
365
  }
365
366
  // ── Main ──────────────────────────────────────────────────────────
366
- async function main() {
367
+ async function mainWorker() {
367
368
  const scriptStart = Date.now();
368
- debug('Codex hook started');
369
- // Parse notify payload from argv[2] (argv[0]=node, argv[1]=script)
370
- if (process.argv.length < 3) {
371
- debug('No argument provided (expected JSON payload in argv[2])');
372
- process.exit(0);
373
- }
374
- let payload;
375
- try {
376
- payload = JSON.parse(process.argv[2]);
377
- }
378
- catch (e) {
379
- debug(`Invalid JSON in argv[2]: ${e}`);
380
- process.exit(0);
381
- }
382
- const eventType = String(payload.type ?? '');
383
- if (eventType !== 'agent-turn-complete') {
384
- debug(`Ignoring event type: ${eventType}`);
385
- process.exit(0);
386
- }
387
- let sessionId = String(payload['thread-id'] ?? '');
388
- if (!sessionId) {
389
- debug('No thread-id in notify payload');
390
- process.exit(0);
391
- }
392
- debug(`Processing notify: type=${eventType}, session=${sessionId}`);
369
+ debug('Worker started');
370
+ const sessionId = process.env._RESPAN_CODEX_SESSION;
371
+ const sessionFile = process.env._RESPAN_CODEX_FILE;
372
+ const cwd = process.env._RESPAN_CODEX_CWD ?? '';
393
373
  const creds = resolveCredentials();
394
374
  if (!creds) {
395
- log('ERROR', 'No API key found. Run: respan auth login');
396
- process.exit(0);
397
- }
398
- // Find session file
399
- let sessionFile = findSessionFile(sessionId);
400
- if (!sessionFile) {
401
- const latest = findLatestSessionFile();
402
- if (latest) {
403
- sessionId = latest.sessionId;
404
- sessionFile = latest.sessionFile;
405
- }
406
- else {
407
- debug('No session file found');
408
- process.exit(0);
409
- }
375
+ log('ERROR', 'No API key');
376
+ return;
410
377
  }
411
- // Load config
412
- const cwd = String(payload.cwd ?? '');
413
378
  const config = cwd ? loadRespanConfig(path.join(cwd, '.codex', 'respan.json')) : null;
414
- if (config)
415
- debug(`Loaded respan.json config from ${cwd}`);
416
- // Process with retry
417
379
  const maxAttempts = 3;
418
380
  let turns = 0;
419
381
  try {
@@ -447,23 +409,80 @@ async function main() {
447
409
  if (turns > 0)
448
410
  break;
449
411
  if (attempt < maxAttempts - 1) {
450
- const delay = 500 * (attempt + 1);
451
- debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
452
- await new Promise((r) => setTimeout(r, delay));
412
+ await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
453
413
  }
454
414
  }
455
415
  const duration = (Date.now() - scriptStart) / 1000;
456
416
  log('INFO', `Processed ${turns} turns in ${duration.toFixed(1)}s`);
457
- if (duration > 180)
458
- log('WARN', `Hook took ${duration.toFixed(1)}s (>3min)`);
459
417
  }
460
418
  catch (e) {
461
419
  log('ERROR', `Failed to process session: ${e}`);
462
- if (DEBUG_MODE)
463
- debug(String(e.stack ?? e));
464
420
  }
465
421
  }
466
- main().catch((e) => {
467
- log('ERROR', `Hook crashed: ${e}`);
468
- process.exit(1);
469
- });
422
+ function main() {
423
+ // Worker mode: re-invoked as detached subprocess
424
+ if (process.env._RESPAN_CODEX_WORKER === '1') {
425
+ mainWorker().catch((e) => log('ERROR', `Worker crashed: ${e}`));
426
+ return;
427
+ }
428
+ debug('Codex hook started');
429
+ if (process.argv.length < 3) {
430
+ debug('No argument provided');
431
+ process.exit(0);
432
+ }
433
+ let payload;
434
+ try {
435
+ payload = JSON.parse(process.argv[2]);
436
+ }
437
+ catch (e) {
438
+ debug(`Invalid JSON in argv[2]: ${e}`);
439
+ process.exit(0);
440
+ }
441
+ const eventType = String(payload.type ?? '');
442
+ if (eventType !== 'agent-turn-complete') {
443
+ debug(`Ignoring event type: ${eventType}`);
444
+ process.exit(0);
445
+ }
446
+ let sessionId = String(payload['thread-id'] ?? '');
447
+ if (!sessionId) {
448
+ debug('No thread-id in notify payload');
449
+ process.exit(0);
450
+ }
451
+ // Find session file
452
+ let sessionFile = findSessionFile(sessionId);
453
+ if (!sessionFile) {
454
+ const latest = findLatestSessionFile();
455
+ if (latest) {
456
+ sessionId = latest.sessionId;
457
+ sessionFile = latest.sessionFile;
458
+ }
459
+ else {
460
+ debug('No session file found');
461
+ process.exit(0);
462
+ }
463
+ }
464
+ // Fork self as detached worker so Codex CLI doesn't block
465
+ const cwd = String(payload.cwd ?? '');
466
+ debug(`Forking worker for session: ${sessionId}`);
467
+ try {
468
+ const scriptPath = __filename || process.argv[1];
469
+ const child = execFile('node', [scriptPath], {
470
+ env: {
471
+ ...process.env,
472
+ _RESPAN_CODEX_WORKER: '1',
473
+ _RESPAN_CODEX_SESSION: sessionId,
474
+ _RESPAN_CODEX_FILE: sessionFile,
475
+ _RESPAN_CODEX_CWD: cwd,
476
+ },
477
+ stdio: 'ignore',
478
+ detached: true,
479
+ });
480
+ child.unref();
481
+ debug('Worker launched');
482
+ }
483
+ catch (e) {
484
+ log('ERROR', `Failed to fork worker: ${e}`);
485
+ }
486
+ process.exit(0);
487
+ }
488
+ main();
@@ -318,7 +318,7 @@ function toOtlpPayload(spans) {
318
318
  })
319
319
  },
320
320
  scopeSpans: [{
321
- scope: { name: "respan-cli-hooks", version: "0.5.3" },
321
+ scope: { name: "respan-cli-hooks", version: "0.7.0" },
322
322
  spans: otlpSpans
323
323
  }]
324
324
  }]
@@ -438,6 +438,34 @@ function detectModel(hookData) {
438
438
  const llmReq = hookData.llm_request ?? {};
439
439
  return String(llmReq.model ?? "") || "gemini-cli";
440
440
  }
441
+ function buildToolSpan(detail, idx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime) {
442
+ const toolName = detail?.name ?? "";
443
+ const toolArgs = detail?.args ?? detail?.input ?? {};
444
+ const toolOutput = detail?.output ?? "";
445
+ const displayName = toolName ? toolDisplayName(toolName) : `Call ${idx + 1}`;
446
+ const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
447
+ const toolMeta = {};
448
+ if (toolName) toolMeta.tool_name = toolName;
449
+ if (detail?.error) toolMeta.error = detail.error;
450
+ const toolStart = detail?.start_time ?? beginTime;
451
+ const toolEnd = detail?.end_time ?? endTime;
452
+ const toolLat = latencySeconds(toolStart, toolEnd);
453
+ return {
454
+ trace_unique_id: traceUniqueId,
455
+ span_unique_id: `gcli_${safeId}_${turnTs}_tool_${idx + 1}`,
456
+ span_parent_id: rootSpanId,
457
+ span_name: `Tool: ${displayName}`,
458
+ span_workflow_name: workflowName,
459
+ span_path: toolName ? `tool_${toolName}` : "tool_call",
460
+ provider_id: "",
461
+ metadata: toolMeta,
462
+ input: toolInputStr,
463
+ output: truncate(toolOutput, MAX_CHARS),
464
+ timestamp: toolEnd,
465
+ start_time: toolStart,
466
+ ...toolLat !== void 0 ? { latency: toolLat } : {}
467
+ };
468
+ }
441
469
  function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurns, toolDetails, thoughtsTokens, textRounds, roundStartTimes) {
442
470
  const spans = [];
443
471
  const sessionId = String(hookData.session_id ?? "");
@@ -519,33 +547,7 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
519
547
  }
520
548
  if (r < rounds.length - 1) {
521
549
  while (toolIdx < toolDetails.length) {
522
- const detail = toolDetails[toolIdx];
523
- const toolName = detail?.name ?? "";
524
- const toolArgs = detail?.args ?? detail?.input ?? {};
525
- const toolOutput = detail?.output ?? "";
526
- const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
527
- const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
528
- const toolMeta = {};
529
- if (toolName) toolMeta.tool_name = toolName;
530
- if (detail?.error) toolMeta.error = detail.error;
531
- const toolStart = detail?.start_time ?? beginTime;
532
- const toolEnd = detail?.end_time ?? endTime;
533
- const toolLat = latencySeconds(toolStart, toolEnd);
534
- spans.push({
535
- trace_unique_id: traceUniqueId,
536
- span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
537
- span_parent_id: rootSpanId,
538
- span_name: `Tool: ${displayName}`,
539
- span_workflow_name: workflowName,
540
- span_path: toolName ? `tool_${toolName}` : "tool_call",
541
- provider_id: "",
542
- metadata: toolMeta,
543
- input: toolInputStr,
544
- output: truncate(toolOutput, MAX_CHARS),
545
- timestamp: toolEnd,
546
- start_time: toolStart,
547
- ...toolLat !== void 0 ? { latency: toolLat } : {}
548
- });
550
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
549
551
  toolIdx++;
550
552
  const nextDetail = toolDetails[toolIdx];
551
553
  if (nextDetail && roundStarts[r + 1] && nextDetail.start_time && nextDetail.start_time > roundStarts[r + 1]) break;
@@ -553,33 +555,7 @@ function buildSpans(hookData, outputText, tokens, config, startTimeIso, toolTurn
553
555
  }
554
556
  }
555
557
  while (toolIdx < toolDetails.length) {
556
- const detail = toolDetails[toolIdx];
557
- const toolName = detail?.name ?? "";
558
- const toolArgs = detail?.args ?? detail?.input ?? {};
559
- const toolOutput = detail?.output ?? "";
560
- const displayName = toolName ? toolDisplayName(toolName) : `Call ${toolIdx + 1}`;
561
- const toolInputStr = toolName ? formatToolInput(toolName, toolArgs) : "";
562
- const toolMeta = {};
563
- if (toolName) toolMeta.tool_name = toolName;
564
- if (detail?.error) toolMeta.error = detail.error;
565
- const toolStart = detail?.start_time ?? beginTime;
566
- const toolEnd = detail?.end_time ?? endTime;
567
- const toolLat = latencySeconds(toolStart, toolEnd);
568
- spans.push({
569
- trace_unique_id: traceUniqueId,
570
- span_unique_id: `gcli_${safeId}_${turnTs}_tool_${toolIdx + 1}`,
571
- span_parent_id: rootSpanId,
572
- span_name: `Tool: ${displayName}`,
573
- span_workflow_name: workflowName,
574
- span_path: toolName ? `tool_${toolName}` : "tool_call",
575
- provider_id: "",
576
- metadata: toolMeta,
577
- input: toolInputStr,
578
- output: truncate(toolOutput, MAX_CHARS),
579
- timestamp: toolEnd,
580
- start_time: toolStart,
581
- ...toolLat !== void 0 ? { latency: toolLat } : {}
582
- });
558
+ spans.push(buildToolSpan(toolDetails[toolIdx], toolIdx, traceUniqueId, rootSpanId, safeId, turnTs, workflowName, beginTime, endTime));
583
559
  toolIdx++;
584
560
  }
585
561
  if (thoughtsTokens > 0) {
@@ -716,7 +692,6 @@ function processBeforeTool(hookData) {
716
692
  pending.push({ name: toolName, input: toolInput, start_time: nowISO() });
717
693
  state.pending_tools = pending;
718
694
  state.send_version = (state.send_version ?? 0) + 1;
719
- state.tool_turns = (state.tool_turns ?? 0) + 1;
720
695
  saveStreamState(sessionId, state);
721
696
  }
722
697
  function processAfterTool(hookData) {
@@ -873,35 +848,30 @@ function processChunk(hookData) {
873
848
  debug(`Delayed send (version=${state.send_version}, delay=${SEND_DELAY}s), ${state.accumulated_text.length} chars`);
874
849
  launchDelayedSend(sessionId, state.send_version, spans, creds.apiKey, creds.baseUrl);
875
850
  }
876
- function mainWorker(raw) {
851
+ function processChunkInWorker(dataFile) {
877
852
  try {
853
+ const raw = fs2.readFileSync(dataFile, "utf-8");
854
+ fs2.unlinkSync(dataFile);
878
855
  if (!raw.trim()) return;
879
856
  const hookData = JSON.parse(raw);
880
- const event = String(hookData.hook_event_name ?? "");
881
857
  const unlock = acquireLock(LOCK_PATH);
882
858
  try {
883
- if (event === "BeforeTool") {
884
- processBeforeTool(hookData);
885
- } else if (event === "AfterTool") {
886
- processAfterTool(hookData);
887
- } else {
888
- processChunk(hookData);
889
- }
859
+ processChunk(hookData);
890
860
  } finally {
891
861
  unlock?.();
892
862
  }
893
863
  } catch (e) {
894
- if (e instanceof SyntaxError) {
895
- log("ERROR", `Invalid JSON from stdin: ${e}`);
896
- } else {
897
- log("ERROR", `Hook error: ${e}`);
864
+ log("ERROR", `Worker error: ${e}`);
865
+ try {
866
+ fs2.unlinkSync(dataFile);
867
+ } catch {
898
868
  }
899
869
  }
900
870
  }
901
871
  function main() {
902
872
  if (process.env._RESPAN_GEM_WORKER === "1") {
903
- const raw2 = process.env._RESPAN_GEM_DATA ?? "";
904
- mainWorker(raw2);
873
+ const dataFile = process.env._RESPAN_GEM_FILE ?? "";
874
+ if (dataFile) processChunkInWorker(dataFile);
905
875
  return;
906
876
  }
907
877
  let raw = "";
@@ -914,15 +884,34 @@ function main() {
914
884
  process.exit(0);
915
885
  }
916
886
  try {
917
- const scriptPath = __filename || process.argv[1];
918
- const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
919
- env: { ...process.env, _RESPAN_GEM_WORKER: "1", _RESPAN_GEM_DATA: raw },
920
- stdio: "ignore",
921
- detached: true
922
- });
923
- child.unref();
887
+ const hookData = JSON.parse(raw);
888
+ const event = String(hookData.hook_event_name ?? "");
889
+ if (event === "BeforeTool" || event === "AfterTool") {
890
+ const unlock = acquireLock(LOCK_PATH);
891
+ try {
892
+ if (event === "BeforeTool") processBeforeTool(hookData);
893
+ else processAfterTool(hookData);
894
+ } finally {
895
+ unlock?.();
896
+ }
897
+ } else {
898
+ const dataFile = path2.join(STATE_DIR, `respan_chunk_${process.pid}.json`);
899
+ fs2.mkdirSync(STATE_DIR, { recursive: true });
900
+ fs2.writeFileSync(dataFile, raw);
901
+ try {
902
+ const scriptPath = __filename || process.argv[1];
903
+ const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
904
+ env: { ...process.env, _RESPAN_GEM_WORKER: "1", _RESPAN_GEM_FILE: dataFile },
905
+ stdio: "ignore",
906
+ detached: true
907
+ });
908
+ child.unref();
909
+ } catch (e) {
910
+ processChunkInWorker(dataFile);
911
+ }
912
+ }
924
913
  } catch (e) {
925
- mainWorker(raw);
914
+ log("ERROR", `Hook error: ${e}`);
926
915
  }
927
916
  process.exit(0);
928
917
  }