runtape 0.2.0 → 0.3.0

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## Status
8
8
 
9
- v0.1MVP. The CLI captures Claude Code sessions via hooks and streams them to a Runtape backend.
9
+ v0.3Adds subagent (Task tool) capture. The full internal session of every spawned agent — its user prompt, assistant turns with model + token usage, and tool calls is now recorded as nested children of the parent `Agent` step.
10
10
 
11
11
  ## Install
12
12
 
@@ -44,11 +44,12 @@ If you prefer the granular commands instead of the wizard, `runtape login` and `
44
44
 
45
45
  ```
46
46
  ~/.runtape/
47
- config.json # api_key (chmod 600), server_url
48
- buffer/<session_id>.ndjson # pending events
49
- seq/<session_id> # monotonic sequence counter
50
- flusher.pid # daemon PID
51
- flusher.log # daemon log (append-only)
47
+ config.json # api_key (chmod 600), server_url
48
+ buffer/<session_id>.ndjson # pending events
49
+ seq/<session_id> # monotonic sequence counter
50
+ transcript/<session_id> # uuids of assistant turns already emitted (v0.2+)
51
+ flusher.pid # daemon PID
52
+ flusher.log # daemon log (append-only)
52
53
  ```
53
54
 
54
55
  Override the home dir with `RUNTAPE_HOME` (useful for tests). Override the server with `RUNTAPE_API_URL` or `runtape login --server-url <url>`.
@@ -63,6 +64,36 @@ import { RuntapeEvent, IngestionRequest } from "runtape/types";
63
64
 
64
65
  The schemas live in `src/types.ts`. The package's `exports` map points TypeScript at the source file (no build step required for type consumers) and Node at the compiled `dist/types.js`.
65
66
 
67
+ ## Changelog
68
+
69
+ ### 0.3.0 — 2026-05-15
70
+
71
+ - **Subagent capture.** On `SubagentStop`, the CLI now reads the subagent's `agent_transcript_path` and synthesizes the full internal event stream — user prompt, assistant turns (with model + usage), tool attempts and calls. Every synthesized event carries `agent_tool_use_id` so the server resolves it as a child of the parent `Agent` step. The dashboard renders these inline as nested rows under the Agent.
72
+ - **`agent_tool_use_id` envelope field.** Optional on every event type. Server-side, when set, it overrides the open-Agent temporal heuristic and resolves the parent step directly.
73
+
74
+ ### 0.2.1 — 2026-05-15
75
+
76
+ - **Skip `<synthetic>` turns.** Claude Code emits placeholder assistant messages with `model: "<synthetic>"` (compaction markers, internal state) that always have zero usage. The transcript scanner now drops them so the dashboard doesn't show empty no-cost turns.
77
+
78
+ ### 0.2.0 — 2026-05-15
79
+
80
+ - **Model + token usage per turn.** The CLI now reads the Claude Code transcript JSONL on `PostToolUse` / `Stop` / `SubagentStop` and emits a new `assistant_turn` event for every assistant message, carrying `model`, `input_tokens`, `output_tokens`, `cache_read_tokens`, and `cache_creation_tokens`. The dashboard surfaces a per-turn model + cost pill and a run-level total.
81
+ - **Tool error surfacing.** `tool_call` events now carry `is_error` + `error_message` (derived from the tool response shape — Bash exits, Edit/Write rejections, `is_error` content blocks, interrupts). Errored tools are visible in the run timeline without expanding the step.
82
+ - **State.** Adds `~/.runtape/transcript/<session_id>` to track which assistant message uuids have already been emitted (idempotent scans across hook fires).
83
+
84
+ To upgrade:
85
+
86
+ ```bash
87
+ npm install -g runtape@latest
88
+ runtape install # safe to re-run; refreshes the hook entries
89
+ ```
90
+
91
+ No changes to the existing hook commands or config file format.
92
+
93
+ ### 0.1.x
94
+
95
+ Initial MVP. Hook-based capture of Claude Code sessions (`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStop`), buffered to disk and flushed by a detached daemon.
96
+
66
97
  ## Open source
67
98
 
68
99
  MIT licensed. Audit exactly what's captured. The Runtape backend (dashboard, ingestion API) is closed-source SaaS at [runtape.dev](https://runtape.dev).
@@ -70,7 +101,7 @@ MIT licensed. Audit exactly what's captured. The Runtape backend (dashboard, ing
70
101
  ## Repos
71
102
 
72
103
  - This repo (`runtape`) — the open-source CLI
73
- - `runtape-mcp` — MCP server for Runtape (coming v0.2)
104
+ - `runtape-mcp` — MCP server for Runtape (planned)
74
105
 
75
106
  ## License
76
107
 
@@ -9,7 +9,8 @@ var ClaudeHookBase = z.object({
9
9
  });
10
10
  var CliAugment = z.object({
11
11
  wall_ts: z.string().datetime(),
12
- sequence: z.number().int().nonnegative()
12
+ sequence: z.number().int().nonnegative(),
13
+ agent_tool_use_id: z.string().min(1).optional()
13
14
  });
14
15
  var SessionStartEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
15
16
  type: z.literal("session_start"),
@@ -95,4 +96,4 @@ export {
95
96
  IngestionRequest,
96
97
  IngestionResponse
97
98
  };
98
- //# sourceMappingURL=chunk-PMFKNOJA.js.map
99
+ //# sourceMappingURL=chunk-PXZACW6S.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["import { z } from 'zod';\n\n// Every Claude Code hook event carries these envelope fields.\nconst ClaudeHookBase = z.object({\n session_id: z.string().min(1),\n transcript_path: z.string().min(1),\n cwd: z.string().min(1),\n hook_event_name: z.string().min(1),\n permission_mode: z.string().optional(),\n});\n\n// CLI-augmented envelope adds monotonic ordering + wall-clock timestamp.\n// agent_tool_use_id is set when the CLI is synthesizing events from a\n// subagent transcript — it carries the parent Agent step's tool_use_id so\n// the server can resolve parent_step_id deterministically instead of\n// relying on the temporal open-stack heuristic. Optional so the field is\n// absent for normal top-level events.\nconst CliAugment = z.object({\n wall_ts: z.string().datetime(),\n sequence: z.number().int().nonnegative(),\n agent_tool_use_id: z.string().min(1).optional(),\n});\n\nexport const SessionStartEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('session_start'),\n source: z.string(),\n});\n\nexport const UserPromptEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('user_prompt'),\n prompt: z.string(),\n});\n\nexport const ToolAttemptEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('tool_attempt'),\n tool_name: z.string().min(1),\n tool_input: z.unknown(),\n tool_use_id: z.string().min(1),\n});\n\nexport const ToolCallEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('tool_call'),\n tool_name: z.string().min(1),\n tool_input: z.unknown(),\n tool_response: z.unknown(),\n tool_use_id: z.string().min(1),\n duration_ms: z.number().nonnegative(),\n // Set by the CLI when the tool_response signals an error (Bash non-zero\n // exit, Edit/Write rejection, tool_response.is_error, etc.). Optional so\n // older CLI versions stay forward-compatible.\n is_error: z.boolean().optional(),\n error_message: z.string().optional(),\n});\n\n// Emitted by the CLI on Stop / PostToolUse after scanning the Claude Code\n// transcript JSONL. One event per assistant message we haven't seen yet,\n// keyed by message_uuid so re-deliveries dedupe at the server. Carries the\n// model identifier and token usage for that turn — the only place either\n// datum is available in Claude Code's emit chain.\nexport const AssistantTurnEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('assistant_turn'),\n message_uuid: z.string().min(1),\n model: z.string().min(1),\n input_tokens: z.number().int().nonnegative(),\n output_tokens: z.number().int().nonnegative(),\n cache_read_tokens: z.number().int().nonnegative().default(0),\n cache_creation_tokens: z.number().int().nonnegative().default(0),\n text: z.string().optional(),\n});\n\nexport const SubagentEndEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('subagent_end'),\n agent_id: z.string().min(1),\n agent_type: z.string().min(1),\n agent_transcript_path: z.string().min(1),\n last_assistant_message: z.string(),\n stop_hook_active: z.boolean().optional(),\n});\n\nexport const SessionEndEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('session_end'),\n last_assistant_message: z.string().optional(),\n stop_hook_active: z.boolean().optional(),\n});\n\nexport const RuntapeEvent = z.discriminatedUnion('type', [\n SessionStartEvent,\n UserPromptEvent,\n ToolAttemptEvent,\n ToolCallEvent,\n AssistantTurnEvent,\n SubagentEndEvent,\n SessionEndEvent,\n]);\n\nexport type RuntapeEvent = z.infer<typeof RuntapeEvent>;\n\n// POST /v1/events body — a batch of up to 100 events.\nexport const IngestionRequest = z.object({\n events: z.array(RuntapeEvent).min(1).max(100),\n});\n\nexport type IngestionRequest = z.infer<typeof IngestionRequest>;\n\n// Response shape (server returns 200 on accepted, 400 on Zod failure, 401 on bad auth).\nexport const IngestionResponse = z.object({\n accepted: z.number().int().nonnegative(),\n errors: z\n .array(\n z.object({\n index: z.number().int().nonnegative(),\n reason: z.string(),\n }),\n )\n .default([]),\n});\n\nexport type IngestionResponse = z.infer<typeof IngestionResponse>;\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  RuntapeEvent
3
- } from "./chunk-PMFKNOJA.js";
3
+ } from "./chunk-PXZACW6S.js";
4
4
 
5
5
  // src/index.ts
6
6
  import { readFileSync } from "fs";
7
7
  import { fileURLToPath as fileURLToPath2 } from "url";
8
- import { dirname as dirname7, join as join2 } from "path";
8
+ import { dirname as dirname8, join as join2 } from "path";
9
9
  import { Command } from "commander";
10
10
 
11
11
  // src/commands/login.ts
@@ -549,7 +549,7 @@ async function readNewAssistantTurns(sessionId, transcriptPath) {
549
549
  if (uuid === "" || seen.has(uuid)) continue;
550
550
  const msg = parsed.message ?? {};
551
551
  const model = typeof msg.model === "string" ? msg.model : "";
552
- if (model === "") continue;
552
+ if (model === "" || model === "<synthetic>") continue;
553
553
  const usage = msg.usage ?? {};
554
554
  turns.push({
555
555
  message_uuid: uuid,
@@ -583,6 +583,157 @@ async function persistCursor(sessionId, seen, newlyEmitted) {
583
583
  await writeFile5(file, trimmed.join("\n") + "\n");
584
584
  }
585
585
 
586
+ // src/lib/subagent-transcript.ts
587
+ import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
588
+ import { dirname as dirname6 } from "path";
589
+ function asInt2(x) {
590
+ if (typeof x === "number" && Number.isFinite(x) && x >= 0) return Math.trunc(x);
591
+ return 0;
592
+ }
593
+ function textFromBlocks(content) {
594
+ if (!Array.isArray(content)) return typeof content === "string" ? content : void 0;
595
+ const parts = [];
596
+ for (const block of content) {
597
+ if (block && typeof block === "object" && "type" in block) {
598
+ const b = block;
599
+ if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
600
+ }
601
+ }
602
+ const joined = parts.join("\n").trim();
603
+ return joined === "" ? void 0 : joined;
604
+ }
605
+ function summarizeToolResult(content) {
606
+ if (!Array.isArray(content)) {
607
+ if (typeof content === "string") return { is_error: false };
608
+ return { is_error: false };
609
+ }
610
+ let isError = false;
611
+ const texts = [];
612
+ for (const block of content) {
613
+ if (block && typeof block === "object") {
614
+ const b = block;
615
+ if (b.is_error === true) isError = true;
616
+ if (typeof b.text === "string") texts.push(b.text);
617
+ }
618
+ }
619
+ if (isError && texts.length > 0) return { is_error: true, error_message: texts.join("\n").slice(0, 500) };
620
+ if (isError) return { is_error: true };
621
+ return { is_error: false };
622
+ }
623
+ async function readNewSubagentEvents(parentSessionId, agentToolUseId, transcriptPath) {
624
+ const seen = await readCursor2(parentSessionId, agentToolUseId);
625
+ let raw;
626
+ try {
627
+ raw = await readFile6(transcriptPath, "utf8");
628
+ } catch {
629
+ return { emits: [], seen };
630
+ }
631
+ const emits = [];
632
+ const toolNameByUseId = /* @__PURE__ */ new Map();
633
+ for (const line of raw.split("\n")) {
634
+ const trimmed = line.trim();
635
+ if (trimmed === "") continue;
636
+ let parsed;
637
+ try {
638
+ parsed = JSON.parse(trimmed);
639
+ } catch {
640
+ continue;
641
+ }
642
+ const uuid = typeof parsed.uuid === "string" ? parsed.uuid : "";
643
+ if (uuid === "" || seen.has(uuid)) continue;
644
+ const msg = parsed.message ?? {};
645
+ if (parsed.type === "user") {
646
+ const content = msg.content;
647
+ const hasToolResults = Array.isArray(content) && content.some(
648
+ (b) => b && typeof b === "object" && b.type === "tool_result"
649
+ );
650
+ if (hasToolResults) {
651
+ for (const block of content) {
652
+ if (!block || typeof block !== "object") continue;
653
+ const b = block;
654
+ if (b.type !== "tool_result") continue;
655
+ const tuid = typeof b.tool_use_id === "string" ? b.tool_use_id : "";
656
+ if (tuid === "") continue;
657
+ const { is_error, error_message } = summarizeToolResult(b.content);
658
+ emits.push({
659
+ kind: "tool_call",
660
+ uuid: `${uuid}:${tuid}`,
661
+ tool_use_id: tuid,
662
+ tool_name: toolNameByUseId.get(tuid) ?? "unknown",
663
+ tool_response: b.content,
664
+ is_error,
665
+ error_message
666
+ });
667
+ }
668
+ } else {
669
+ const text = textFromBlocks(content);
670
+ if (text !== void 0) {
671
+ emits.push({ kind: "user_prompt", uuid, prompt: text });
672
+ }
673
+ }
674
+ continue;
675
+ }
676
+ if (parsed.type === "assistant") {
677
+ const model = typeof msg.model === "string" ? msg.model : "";
678
+ if (model === "" || model === "<synthetic>") continue;
679
+ const usage = msg.usage ?? {};
680
+ emits.push({
681
+ kind: "assistant_turn",
682
+ uuid,
683
+ message_uuid: uuid,
684
+ model,
685
+ input_tokens: asInt2(usage.input_tokens),
686
+ output_tokens: asInt2(usage.output_tokens),
687
+ cache_read_tokens: asInt2(usage.cache_read_input_tokens),
688
+ cache_creation_tokens: asInt2(usage.cache_creation_input_tokens),
689
+ text: textFromBlocks(msg.content)
690
+ });
691
+ if (Array.isArray(msg.content)) {
692
+ for (const block of msg.content) {
693
+ if (!block || typeof block !== "object") continue;
694
+ const b = block;
695
+ if (b.type !== "tool_use") continue;
696
+ const tuid = typeof b.id === "string" ? b.id : "";
697
+ const tname = typeof b.name === "string" ? b.name : "";
698
+ if (tuid === "" || tname === "") continue;
699
+ toolNameByUseId.set(tuid, tname);
700
+ emits.push({
701
+ kind: "tool_attempt",
702
+ uuid: `${uuid}:${tuid}`,
703
+ tool_use_id: tuid,
704
+ tool_name: tname,
705
+ tool_input: b.input
706
+ });
707
+ }
708
+ }
709
+ continue;
710
+ }
711
+ }
712
+ return { emits, seen };
713
+ }
714
+ var CURSOR_KEEP2 = 500;
715
+ async function readCursor2(parentSessionId, agentToolUseId) {
716
+ try {
717
+ const raw = await readFile6(cursorFile(parentSessionId, agentToolUseId), "utf8");
718
+ return new Set(
719
+ raw.split("\n").map((s) => s.trim()).filter((s) => s !== "")
720
+ );
721
+ } catch {
722
+ return /* @__PURE__ */ new Set();
723
+ }
724
+ }
725
+ function cursorFile(parentSessionId, agentToolUseId) {
726
+ return `${paths.transcriptDir}/subagent-${parentSessionId}-${agentToolUseId}`;
727
+ }
728
+ async function persistSubagentCursor(parentSessionId, agentToolUseId, seen, newlyEmitted) {
729
+ for (const u of newlyEmitted) seen.add(u);
730
+ const file = cursorFile(parentSessionId, agentToolUseId);
731
+ await mkdir6(dirname6(file), { recursive: true });
732
+ const arr = [...seen];
733
+ const trimmed = arr.length > CURSOR_KEEP2 ? arr.slice(arr.length - CURSOR_KEEP2) : arr;
734
+ await writeFile6(file, trimmed.join("\n") + "\n");
735
+ }
736
+
586
737
  // src/commands/push.ts
587
738
  async function readStdin() {
588
739
  if (process.stdin.isTTY) return "";
@@ -668,6 +819,76 @@ async function pushCommand(opts) {
668
819
  }
669
820
  } catch (err) {
670
821
  process.stderr.write(`runtape: transcript scan failed: ${err instanceof Error ? err.message : String(err)}
822
+ `);
823
+ }
824
+ }
825
+ }
826
+ if (opts.event === "SubagentStop") {
827
+ const subTranscript = typeof payload.agent_transcript_path === "string" ? payload.agent_transcript_path : "";
828
+ const agentToolUseId = typeof payload.agent_id === "string" ? payload.agent_id : "";
829
+ if (subTranscript !== "" && agentToolUseId !== "") {
830
+ try {
831
+ const { emits, seen } = await readNewSubagentEvents(sessionId, agentToolUseId, subTranscript);
832
+ const newlyEmitted = [];
833
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
834
+ for (const e of emits) {
835
+ const seq = await nextSequence(sessionId);
836
+ const baseEnvelope = {
837
+ session_id: sessionId,
838
+ transcript_path: subTranscript,
839
+ cwd,
840
+ hook_event_name: opts.event,
841
+ permission_mode: typeof payload.permission_mode === "string" ? payload.permission_mode : void 0,
842
+ wall_ts: (/* @__PURE__ */ new Date()).toISOString(),
843
+ sequence: seq,
844
+ agent_tool_use_id: agentToolUseId
845
+ };
846
+ let ev = null;
847
+ if (e.kind === "user_prompt") {
848
+ ev = { ...baseEnvelope, type: "user_prompt", prompt: e.prompt };
849
+ } else if (e.kind === "assistant_turn") {
850
+ ev = {
851
+ ...baseEnvelope,
852
+ type: "assistant_turn",
853
+ message_uuid: e.message_uuid,
854
+ model: e.model,
855
+ input_tokens: e.input_tokens,
856
+ output_tokens: e.output_tokens,
857
+ cache_read_tokens: e.cache_read_tokens,
858
+ cache_creation_tokens: e.cache_creation_tokens,
859
+ text: e.text
860
+ };
861
+ } else if (e.kind === "tool_attempt") {
862
+ ev = {
863
+ ...baseEnvelope,
864
+ type: "tool_attempt",
865
+ tool_name: e.tool_name,
866
+ tool_input: e.tool_input,
867
+ tool_use_id: e.tool_use_id
868
+ };
869
+ } else if (e.kind === "tool_call") {
870
+ ev = {
871
+ ...baseEnvelope,
872
+ type: "tool_call",
873
+ tool_name: e.tool_name,
874
+ tool_input: null,
875
+ tool_response: e.tool_response,
876
+ tool_use_id: e.tool_use_id,
877
+ duration_ms: 0,
878
+ is_error: e.is_error,
879
+ error_message: e.error_message
880
+ };
881
+ }
882
+ if (ev) {
883
+ await appendEvent(sessionId, ev);
884
+ newlyEmitted.push(e.uuid);
885
+ }
886
+ }
887
+ if (newlyEmitted.length > 0) {
888
+ await persistSubagentCursor(sessionId, agentToolUseId, seen, newlyEmitted);
889
+ }
890
+ } catch (err) {
891
+ process.stderr.write(`runtape: subagent transcript scan failed: ${err instanceof Error ? err.message : String(err)}
671
892
  `);
672
893
  }
673
894
  }
@@ -682,10 +903,10 @@ async function pushCommand(opts) {
682
903
  }
683
904
 
684
905
  // src/commands/status.ts
685
- import { readFile as readFile6 } from "fs/promises";
906
+ import { readFile as readFile7 } from "fs/promises";
686
907
  async function readFlusherPid() {
687
908
  try {
688
- const raw = await readFile6(paths.flusherPid, "utf8");
909
+ const raw = await readFile7(paths.flusherPid, "utf8");
689
910
  const n = Number.parseInt(raw.trim(), 10);
690
911
  return Number.isFinite(n) ? n : null;
691
912
  } catch (err) {
@@ -873,15 +1094,15 @@ Step 3/3 \u2014 Claude Code hooks
873
1094
  }
874
1095
 
875
1096
  // src/lib/flusher.ts
876
- import { appendFile as appendFile2, mkdir as mkdir6, readFile as readFile7, unlink as unlink3, writeFile as writeFile6 } from "fs/promises";
877
- import { dirname as dirname6 } from "path";
1097
+ import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile8, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
1098
+ import { dirname as dirname7 } from "path";
878
1099
  var POLL_INTERVAL_MS = 1500;
879
1100
  var IDLE_EXIT_MS = 3e4;
880
1101
  var BATCH_MAX = 100;
881
1102
  var BACKOFF_STEPS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 6e4];
882
1103
  async function log(line) {
883
1104
  try {
884
- await mkdir6(dirname6(paths.flusherLog), { recursive: true });
1105
+ await mkdir7(dirname7(paths.flusherLog), { recursive: true });
885
1106
  await appendFile2(paths.flusherLog, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
886
1107
  `);
887
1108
  } catch {
@@ -898,9 +1119,9 @@ async function isProcessAlive(pid) {
898
1119
  }
899
1120
  }
900
1121
  async function acquirePidLock() {
901
- await mkdir6(dirname6(paths.flusherPid), { recursive: true });
1122
+ await mkdir7(dirname7(paths.flusherPid), { recursive: true });
902
1123
  try {
903
- const existing = await readFile7(paths.flusherPid, "utf8");
1124
+ const existing = await readFile8(paths.flusherPid, "utf8");
904
1125
  const pid = Number.parseInt(existing.trim(), 10);
905
1126
  if (Number.isFinite(pid) && await isProcessAlive(pid)) {
906
1127
  return false;
@@ -908,7 +1129,7 @@ async function acquirePidLock() {
908
1129
  } catch (err) {
909
1130
  if (err.code !== "ENOENT") throw err;
910
1131
  }
911
- await writeFile6(paths.flusherPid, String(process.pid));
1132
+ await writeFile7(paths.flusherPid, String(process.pid));
912
1133
  return true;
913
1134
  }
914
1135
  async function releasePidLock() {
@@ -993,7 +1214,7 @@ async function runFlusher() {
993
1214
  }
994
1215
 
995
1216
  // src/index.ts
996
- var pkgPath = join2(dirname7(fileURLToPath2(import.meta.url)), "..", "package.json");
1217
+ var pkgPath = join2(dirname8(fileURLToPath2(import.meta.url)), "..", "package.json");
997
1218
  var PKG_VERSION = JSON.parse(readFileSync(pkgPath, "utf8")).version;
998
1219
  if (process.argv.includes("--internal-flusher")) {
999
1220
  void runFlusher().then(