runtape 0.1.1 → 0.2.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/dist/{chunk-Q4RSBPGN.js → chunk-PMFKNOJA.js} +19 -2
- package/dist/chunk-PMFKNOJA.js.map +1 -0
- package/dist/index.js +162 -15
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +252 -47
- package/dist/types.js +3 -1
- package/package.json +1 -1
- package/dist/chunk-Q4RSBPGN.js.map +0 -1
|
@@ -31,7 +31,22 @@ var ToolCallEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
|
|
|
31
31
|
tool_input: z.unknown(),
|
|
32
32
|
tool_response: z.unknown(),
|
|
33
33
|
tool_use_id: z.string().min(1),
|
|
34
|
-
duration_ms: z.number().nonnegative()
|
|
34
|
+
duration_ms: z.number().nonnegative(),
|
|
35
|
+
// Set by the CLI when the tool_response signals an error (Bash non-zero
|
|
36
|
+
// exit, Edit/Write rejection, tool_response.is_error, etc.). Optional so
|
|
37
|
+
// older CLI versions stay forward-compatible.
|
|
38
|
+
is_error: z.boolean().optional(),
|
|
39
|
+
error_message: z.string().optional()
|
|
40
|
+
});
|
|
41
|
+
var AssistantTurnEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
|
|
42
|
+
type: z.literal("assistant_turn"),
|
|
43
|
+
message_uuid: z.string().min(1),
|
|
44
|
+
model: z.string().min(1),
|
|
45
|
+
input_tokens: z.number().int().nonnegative(),
|
|
46
|
+
output_tokens: z.number().int().nonnegative(),
|
|
47
|
+
cache_read_tokens: z.number().int().nonnegative().default(0),
|
|
48
|
+
cache_creation_tokens: z.number().int().nonnegative().default(0),
|
|
49
|
+
text: z.string().optional()
|
|
35
50
|
});
|
|
36
51
|
var SubagentEndEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
|
|
37
52
|
type: z.literal("subagent_end"),
|
|
@@ -51,6 +66,7 @@ var RuntapeEvent = z.discriminatedUnion("type", [
|
|
|
51
66
|
UserPromptEvent,
|
|
52
67
|
ToolAttemptEvent,
|
|
53
68
|
ToolCallEvent,
|
|
69
|
+
AssistantTurnEvent,
|
|
54
70
|
SubagentEndEvent,
|
|
55
71
|
SessionEndEvent
|
|
56
72
|
]);
|
|
@@ -72,10 +88,11 @@ export {
|
|
|
72
88
|
UserPromptEvent,
|
|
73
89
|
ToolAttemptEvent,
|
|
74
90
|
ToolCallEvent,
|
|
91
|
+
AssistantTurnEvent,
|
|
75
92
|
SubagentEndEvent,
|
|
76
93
|
SessionEndEvent,
|
|
77
94
|
RuntapeEvent,
|
|
78
95
|
IngestionRequest,
|
|
79
96
|
IngestionResponse
|
|
80
97
|
};
|
|
81
|
-
//# sourceMappingURL=chunk-
|
|
98
|
+
//# sourceMappingURL=chunk-PMFKNOJA.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.\nconst CliAugment = z.object({\n wall_ts: z.string().datetime(),\n sequence: z.number().int().nonnegative(),\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-
|
|
3
|
+
} from "./chunk-PMFKNOJA.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
|
|
8
|
+
import { dirname as dirname7, join as join2 } from "path";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/login.ts
|
|
@@ -30,6 +30,11 @@ var paths = {
|
|
|
30
30
|
flusherLog: join(RUNTAPE_HOME, "flusher.log"),
|
|
31
31
|
bufferFile: (sessionId) => join(RUNTAPE_HOME, "buffer", `${sessionId}.ndjson`),
|
|
32
32
|
seqFile: (sessionId) => join(RUNTAPE_HOME, "seq", sessionId),
|
|
33
|
+
// Per-session marker recording the last assistant message uuid we've
|
|
34
|
+
// emitted from the transcript. Lets transcript scans be idempotent across
|
|
35
|
+
// hook invocations without re-reading + re-emitting everything.
|
|
36
|
+
transcriptCursorFile: (sessionId) => join(RUNTAPE_HOME, "transcript", sessionId),
|
|
37
|
+
transcriptDir: join(RUNTAPE_HOME, "transcript"),
|
|
33
38
|
claudeSettings: (scope) => scope === "user" ? join(homedir(), ".claude", "settings.json") : join(process.cwd(), ".claude", "settings.json"),
|
|
34
39
|
claudeSettingsBackup: (scope) => scope === "user" ? join(homedir(), ".claude", "settings.json.runtape-backup") : join(process.cwd(), ".claude", "settings.json.runtape-backup")
|
|
35
40
|
};
|
|
@@ -182,6 +187,30 @@ var SUPPORTED_HOOKS = [
|
|
|
182
187
|
"Stop",
|
|
183
188
|
"SubagentStop"
|
|
184
189
|
];
|
|
190
|
+
function inspectToolResponse(tool_response) {
|
|
191
|
+
if (!tool_response || typeof tool_response !== "object") return { is_error: false };
|
|
192
|
+
const r = tool_response;
|
|
193
|
+
if (r.is_error === true) {
|
|
194
|
+
const content = r.content;
|
|
195
|
+
if (Array.isArray(content)) {
|
|
196
|
+
for (const block of content) {
|
|
197
|
+
if (block && typeof block === "object" && "text" in block) {
|
|
198
|
+
const t = block.text;
|
|
199
|
+
if (typeof t === "string") return { is_error: true, error_message: t };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (typeof r.message === "string") return { is_error: true, error_message: r.message };
|
|
204
|
+
return { is_error: true };
|
|
205
|
+
}
|
|
206
|
+
if (typeof r.error === "string" && r.error.trim() !== "") {
|
|
207
|
+
return { is_error: true, error_message: r.error };
|
|
208
|
+
}
|
|
209
|
+
if (r.interrupted === true) {
|
|
210
|
+
return { is_error: true, error_message: "Interrupted" };
|
|
211
|
+
}
|
|
212
|
+
return { is_error: false };
|
|
213
|
+
}
|
|
185
214
|
function mapHookPayload(hookName, payload, augment) {
|
|
186
215
|
const base = {
|
|
187
216
|
session_id: payload.session_id,
|
|
@@ -209,7 +238,8 @@ function mapHookPayload(hookName, payload, augment) {
|
|
|
209
238
|
tool_use_id: payload.tool_use_id
|
|
210
239
|
};
|
|
211
240
|
break;
|
|
212
|
-
case "PostToolUse":
|
|
241
|
+
case "PostToolUse": {
|
|
242
|
+
const err = inspectToolResponse(payload.tool_response);
|
|
213
243
|
candidate = {
|
|
214
244
|
...base,
|
|
215
245
|
type: "tool_call",
|
|
@@ -217,9 +247,12 @@ function mapHookPayload(hookName, payload, augment) {
|
|
|
217
247
|
tool_input: payload.tool_input,
|
|
218
248
|
tool_response: payload.tool_response,
|
|
219
249
|
tool_use_id: payload.tool_use_id,
|
|
220
|
-
duration_ms: payload.duration_ms
|
|
250
|
+
duration_ms: payload.duration_ms,
|
|
251
|
+
is_error: err.is_error,
|
|
252
|
+
error_message: err.error_message
|
|
221
253
|
};
|
|
222
254
|
break;
|
|
255
|
+
}
|
|
223
256
|
case "Stop":
|
|
224
257
|
candidate = {
|
|
225
258
|
...base,
|
|
@@ -474,6 +507,82 @@ async function bufferMtimeMs(sessionId) {
|
|
|
474
507
|
}
|
|
475
508
|
}
|
|
476
509
|
|
|
510
|
+
// src/lib/transcript.ts
|
|
511
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
512
|
+
import { dirname as dirname5 } from "path";
|
|
513
|
+
function asInt(x) {
|
|
514
|
+
if (typeof x === "number" && Number.isFinite(x) && x >= 0) return Math.trunc(x);
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
function extractAssistantText(content) {
|
|
518
|
+
if (!Array.isArray(content)) return void 0;
|
|
519
|
+
const parts = [];
|
|
520
|
+
for (const block of content) {
|
|
521
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
522
|
+
const b = block;
|
|
523
|
+
if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const joined = parts.join("\n").trim();
|
|
527
|
+
return joined === "" ? void 0 : joined;
|
|
528
|
+
}
|
|
529
|
+
async function readNewAssistantTurns(sessionId, transcriptPath) {
|
|
530
|
+
const seen = await readCursor(sessionId);
|
|
531
|
+
let raw;
|
|
532
|
+
try {
|
|
533
|
+
raw = await readFile5(transcriptPath, "utf8");
|
|
534
|
+
} catch {
|
|
535
|
+
return { turns: [], seen };
|
|
536
|
+
}
|
|
537
|
+
const turns = [];
|
|
538
|
+
for (const line of raw.split("\n")) {
|
|
539
|
+
const trimmed = line.trim();
|
|
540
|
+
if (trimmed === "") continue;
|
|
541
|
+
let parsed;
|
|
542
|
+
try {
|
|
543
|
+
parsed = JSON.parse(trimmed);
|
|
544
|
+
} catch {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (parsed.type !== "assistant") continue;
|
|
548
|
+
const uuid = typeof parsed.uuid === "string" ? parsed.uuid : "";
|
|
549
|
+
if (uuid === "" || seen.has(uuid)) continue;
|
|
550
|
+
const msg = parsed.message ?? {};
|
|
551
|
+
const model = typeof msg.model === "string" ? msg.model : "";
|
|
552
|
+
if (model === "") continue;
|
|
553
|
+
const usage = msg.usage ?? {};
|
|
554
|
+
turns.push({
|
|
555
|
+
message_uuid: uuid,
|
|
556
|
+
model,
|
|
557
|
+
input_tokens: asInt(usage.input_tokens),
|
|
558
|
+
output_tokens: asInt(usage.output_tokens),
|
|
559
|
+
cache_read_tokens: asInt(usage.cache_read_input_tokens),
|
|
560
|
+
cache_creation_tokens: asInt(usage.cache_creation_input_tokens),
|
|
561
|
+
text: extractAssistantText(msg.content)
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return { turns, seen };
|
|
565
|
+
}
|
|
566
|
+
var CURSOR_KEEP = 200;
|
|
567
|
+
async function readCursor(sessionId) {
|
|
568
|
+
try {
|
|
569
|
+
const raw = await readFile5(paths.transcriptCursorFile(sessionId), "utf8");
|
|
570
|
+
return new Set(
|
|
571
|
+
raw.split("\n").map((s) => s.trim()).filter((s) => s !== "")
|
|
572
|
+
);
|
|
573
|
+
} catch {
|
|
574
|
+
return /* @__PURE__ */ new Set();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function persistCursor(sessionId, seen, newlyEmitted) {
|
|
578
|
+
for (const u of newlyEmitted) seen.add(u);
|
|
579
|
+
const file = paths.transcriptCursorFile(sessionId);
|
|
580
|
+
await mkdir5(dirname5(file), { recursive: true });
|
|
581
|
+
const arr = [...seen];
|
|
582
|
+
const trimmed = arr.length > CURSOR_KEEP ? arr.slice(arr.length - CURSOR_KEEP) : arr;
|
|
583
|
+
await writeFile5(file, trimmed.join("\n") + "\n");
|
|
584
|
+
}
|
|
585
|
+
|
|
477
586
|
// src/commands/push.ts
|
|
478
587
|
async function readStdin() {
|
|
479
588
|
if (process.stdin.isTTY) return "";
|
|
@@ -526,6 +635,43 @@ async function pushCommand(opts) {
|
|
|
526
635
|
return 0;
|
|
527
636
|
}
|
|
528
637
|
await appendEvent(sessionId, result.event);
|
|
638
|
+
if (opts.event === "PostToolUse" || opts.event === "Stop" || opts.event === "SubagentStop") {
|
|
639
|
+
const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : "";
|
|
640
|
+
if (transcriptPath !== "") {
|
|
641
|
+
try {
|
|
642
|
+
const { turns, seen } = await readNewAssistantTurns(sessionId, transcriptPath);
|
|
643
|
+
const newlyEmitted = [];
|
|
644
|
+
for (const t of turns) {
|
|
645
|
+
const seq = await nextSequence(sessionId);
|
|
646
|
+
const ev = {
|
|
647
|
+
type: "assistant_turn",
|
|
648
|
+
session_id: sessionId,
|
|
649
|
+
transcript_path: transcriptPath,
|
|
650
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : "",
|
|
651
|
+
hook_event_name: opts.event,
|
|
652
|
+
permission_mode: typeof payload.permission_mode === "string" ? payload.permission_mode : void 0,
|
|
653
|
+
wall_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
654
|
+
sequence: seq,
|
|
655
|
+
message_uuid: t.message_uuid,
|
|
656
|
+
model: t.model,
|
|
657
|
+
input_tokens: t.input_tokens,
|
|
658
|
+
output_tokens: t.output_tokens,
|
|
659
|
+
cache_read_tokens: t.cache_read_tokens,
|
|
660
|
+
cache_creation_tokens: t.cache_creation_tokens,
|
|
661
|
+
text: t.text
|
|
662
|
+
};
|
|
663
|
+
await appendEvent(sessionId, ev);
|
|
664
|
+
newlyEmitted.push(t.message_uuid);
|
|
665
|
+
}
|
|
666
|
+
if (newlyEmitted.length > 0) {
|
|
667
|
+
await persistCursor(sessionId, seen, newlyEmitted);
|
|
668
|
+
}
|
|
669
|
+
} catch (err) {
|
|
670
|
+
process.stderr.write(`runtape: transcript scan failed: ${err instanceof Error ? err.message : String(err)}
|
|
671
|
+
`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
529
675
|
spawnFlusher(resolveCliBinPath());
|
|
530
676
|
return 0;
|
|
531
677
|
} catch (err) {
|
|
@@ -536,10 +682,10 @@ async function pushCommand(opts) {
|
|
|
536
682
|
}
|
|
537
683
|
|
|
538
684
|
// src/commands/status.ts
|
|
539
|
-
import { readFile as
|
|
685
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
540
686
|
async function readFlusherPid() {
|
|
541
687
|
try {
|
|
542
|
-
const raw = await
|
|
688
|
+
const raw = await readFile6(paths.flusherPid, "utf8");
|
|
543
689
|
const n = Number.parseInt(raw.trim(), 10);
|
|
544
690
|
return Number.isFinite(n) ? n : null;
|
|
545
691
|
} catch (err) {
|
|
@@ -639,7 +785,8 @@ async function setupCommand(opts) {
|
|
|
639
785
|
const rl = createInterface3({ input: input3, output: output3 });
|
|
640
786
|
try {
|
|
641
787
|
process.stdout.write("\nRuntape setup\n");
|
|
642
|
-
process.stdout.write("Let's get your Claude Code runs captured.\n
|
|
788
|
+
process.stdout.write("Let's get your Claude Code runs captured.\n");
|
|
789
|
+
process.stdout.write("(Press Enter at any prompt to accept the default shown in [brackets].)\n\n");
|
|
643
790
|
const existing = await readConfig();
|
|
644
791
|
if (existing) {
|
|
645
792
|
process.stdout.write(`Already logged in to ${existing.server_url}
|
|
@@ -655,7 +802,7 @@ async function setupCommand(opts) {
|
|
|
655
802
|
const suggestedUrl = defaultServerUrl();
|
|
656
803
|
process.stdout.write(`Step 1/3 \u2014 Backend
|
|
657
804
|
`);
|
|
658
|
-
const urlInput = (await rl.question(`Server URL [${suggestedUrl}]: `)).trim();
|
|
805
|
+
const urlInput = (await rl.question(`Server URL [${suggestedUrl}] (Enter to use this): `)).trim();
|
|
659
806
|
const serverUrl = urlInput === "" ? suggestedUrl : urlInput;
|
|
660
807
|
const dashboardUrl = `${serverUrl.replace(/\/$/, "")}/dashboard`;
|
|
661
808
|
process.stdout.write(`
|
|
@@ -726,15 +873,15 @@ Step 3/3 \u2014 Claude Code hooks
|
|
|
726
873
|
}
|
|
727
874
|
|
|
728
875
|
// src/lib/flusher.ts
|
|
729
|
-
import { appendFile as appendFile2, mkdir as
|
|
730
|
-
import { dirname as
|
|
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";
|
|
731
878
|
var POLL_INTERVAL_MS = 1500;
|
|
732
879
|
var IDLE_EXIT_MS = 3e4;
|
|
733
880
|
var BATCH_MAX = 100;
|
|
734
881
|
var BACKOFF_STEPS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 6e4];
|
|
735
882
|
async function log(line) {
|
|
736
883
|
try {
|
|
737
|
-
await
|
|
884
|
+
await mkdir6(dirname6(paths.flusherLog), { recursive: true });
|
|
738
885
|
await appendFile2(paths.flusherLog, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
739
886
|
`);
|
|
740
887
|
} catch {
|
|
@@ -751,9 +898,9 @@ async function isProcessAlive(pid) {
|
|
|
751
898
|
}
|
|
752
899
|
}
|
|
753
900
|
async function acquirePidLock() {
|
|
754
|
-
await
|
|
901
|
+
await mkdir6(dirname6(paths.flusherPid), { recursive: true });
|
|
755
902
|
try {
|
|
756
|
-
const existing = await
|
|
903
|
+
const existing = await readFile7(paths.flusherPid, "utf8");
|
|
757
904
|
const pid = Number.parseInt(existing.trim(), 10);
|
|
758
905
|
if (Number.isFinite(pid) && await isProcessAlive(pid)) {
|
|
759
906
|
return false;
|
|
@@ -761,7 +908,7 @@ async function acquirePidLock() {
|
|
|
761
908
|
} catch (err) {
|
|
762
909
|
if (err.code !== "ENOENT") throw err;
|
|
763
910
|
}
|
|
764
|
-
await
|
|
911
|
+
await writeFile6(paths.flusherPid, String(process.pid));
|
|
765
912
|
return true;
|
|
766
913
|
}
|
|
767
914
|
async function releasePidLock() {
|
|
@@ -846,7 +993,7 @@ async function runFlusher() {
|
|
|
846
993
|
}
|
|
847
994
|
|
|
848
995
|
// src/index.ts
|
|
849
|
-
var pkgPath = join2(
|
|
996
|
+
var pkgPath = join2(dirname7(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
850
997
|
var PKG_VERSION = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
851
998
|
if (process.argv.includes("--internal-flusher")) {
|
|
852
999
|
void runFlusher().then(
|