runtape 0.2.0 → 0.4.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 +38 -7
- package/dist/{chunk-PMFKNOJA.js → chunk-LGAXYSLN.js} +10 -3
- package/dist/chunk-LGAXYSLN.js.map +1 -0
- package/dist/index.js +242 -13
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +210 -5
- package/dist/types.js +3 -1
- package/package.json +1 -1
- package/dist/chunk-PMFKNOJA.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Status
|
|
8
8
|
|
|
9
|
-
v0.
|
|
9
|
+
v0.3 — Adds 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
|
|
48
|
-
buffer/<session_id>.ndjson
|
|
49
|
-
seq/<session_id>
|
|
50
|
-
|
|
51
|
-
flusher.
|
|
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 (
|
|
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"),
|
|
@@ -61,6 +62,10 @@ var SessionEndEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
|
|
|
61
62
|
last_assistant_message: z.string().optional(),
|
|
62
63
|
stop_hook_active: z.boolean().optional()
|
|
63
64
|
});
|
|
65
|
+
var SessionCloseEvent = ClaudeHookBase.extend(CliAugment.shape).extend({
|
|
66
|
+
type: z.literal("session_close"),
|
|
67
|
+
reason: z.string().optional()
|
|
68
|
+
});
|
|
64
69
|
var RuntapeEvent = z.discriminatedUnion("type", [
|
|
65
70
|
SessionStartEvent,
|
|
66
71
|
UserPromptEvent,
|
|
@@ -68,7 +73,8 @@ var RuntapeEvent = z.discriminatedUnion("type", [
|
|
|
68
73
|
ToolCallEvent,
|
|
69
74
|
AssistantTurnEvent,
|
|
70
75
|
SubagentEndEvent,
|
|
71
|
-
SessionEndEvent
|
|
76
|
+
SessionEndEvent,
|
|
77
|
+
SessionCloseEvent
|
|
72
78
|
]);
|
|
73
79
|
var IngestionRequest = z.object({
|
|
74
80
|
events: z.array(RuntapeEvent).min(1).max(100)
|
|
@@ -91,8 +97,9 @@ export {
|
|
|
91
97
|
AssistantTurnEvent,
|
|
92
98
|
SubagentEndEvent,
|
|
93
99
|
SessionEndEvent,
|
|
100
|
+
SessionCloseEvent,
|
|
94
101
|
RuntapeEvent,
|
|
95
102
|
IngestionRequest,
|
|
96
103
|
IngestionResponse
|
|
97
104
|
};
|
|
98
|
-
//# sourceMappingURL=chunk-
|
|
105
|
+
//# sourceMappingURL=chunk-LGAXYSLN.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\n// Stop hook (fires every turn). Despite the literal name 'session_end', this\n// is \"turn end\" semantically. Kept for backward compatibility with CLI <= 0.3.x.\n// New CLIs still emit this; the server interprets it as \"run is idle\".\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\n// SessionEnd hook (fires once when Claude Code actually closes the session).\n// Server uses this to promote the run from 'idle' to 'ended'.\nexport const SessionCloseEvent = ClaudeHookBase.extend(CliAugment.shape).extend({\n type: z.literal('session_close'),\n reason: z.string().optional(),\n});\n\nexport const RuntapeEvent = z.discriminatedUnion('type', [\n SessionStartEvent,\n UserPromptEvent,\n ToolAttemptEvent,\n ToolCallEvent,\n AssistantTurnEvent,\n SubagentEndEvent,\n SessionEndEvent,\n SessionCloseEvent,\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":";AAAA,SAAS,SAAS;AAGlB,IAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACjC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACrB,iBAAiB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACjC,iBAAiB,EAAE,OAAO,EAAE,SAAS;AACvC,CAAC;AAQD,IAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,SAAS,EAAE,OAAO,EAAE,SAAS;AAAA,EAC7B,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACvC,mBAAmB,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAChD,CAAC;AAEM,IAAM,oBAAoB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC9E,MAAM,EAAE,QAAQ,eAAe;AAAA,EAC/B,QAAQ,EAAE,OAAO;AACnB,CAAC;AAEM,IAAM,kBAAkB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC5E,MAAM,EAAE,QAAQ,aAAa;AAAA,EAC7B,QAAQ,EAAE,OAAO;AACnB,CAAC;AAEM,IAAM,mBAAmB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC7E,MAAM,EAAE,QAAQ,cAAc;AAAA,EAC9B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,YAAY,EAAE,QAAQ;AAAA,EACtB,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAC/B,CAAC;AAEM,IAAM,gBAAgB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC1E,MAAM,EAAE,QAAQ,WAAW;AAAA,EAC3B,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,YAAY,EAAE,QAAQ;AAAA,EACtB,eAAe,EAAE,QAAQ;AAAA,EACzB,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC7B,aAAa,EAAE,OAAO,EAAE,YAAY;AAAA;AAAA;AAAA;AAAA,EAIpC,UAAU,EAAE,QAAQ,EAAE,SAAS;AAAA,EAC/B,eAAe,EAAE,OAAO,EAAE,SAAS;AACrC,CAAC;AAOM,IAAM,qBAAqB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC/E,MAAM,EAAE,QAAQ,gBAAgB;AAAA,EAChC,cAAc,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC9B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvB,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC3C,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EAC5C,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;AAAA,EAC3D,uBAAuB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC;AAAA,EAC/D,MAAM,EAAE,OAAO,EAAE,SAAS;AAC5B,CAAC;AAEM,IAAM,mBAAmB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC7E,MAAM,EAAE,QAAQ,cAAc;AAAA,EAC9B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC5B,uBAAuB,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvC,wBAAwB,EAAE,OAAO;AAAA,EACjC,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AACzC,CAAC;AAKM,IAAM,kBAAkB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC5E,MAAM,EAAE,QAAQ,aAAa;AAAA,EAC7B,wBAAwB,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5C,kBAAkB,EAAE,QAAQ,EAAE,SAAS;AACzC,CAAC;AAIM,IAAM,oBAAoB,eAAe,OAAO,WAAW,KAAK,EAAE,OAAO;AAAA,EAC9E,MAAM,EAAE,QAAQ,eAAe;AAAA,EAC/B,QAAQ,EAAE,OAAO,EAAE,SAAS;AAC9B,CAAC;AAEM,IAAM,eAAe,EAAE,mBAAmB,QAAQ;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAKM,IAAM,mBAAmB,EAAE,OAAO;AAAA,EACvC,QAAQ,EAAE,MAAM,YAAY,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;AAC9C,CAAC;AAKM,IAAM,oBAAoB,EAAE,OAAO;AAAA,EACxC,UAAU,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,EACvC,QAAQ,EACL;AAAA,IACC,EAAE,OAAO;AAAA,MACP,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY;AAAA,MACpC,QAAQ,EAAE,OAAO;AAAA,IACnB,CAAC;AAAA,EACH,EACC,QAAQ,CAAC,CAAC;AACf,CAAC;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RuntapeEvent
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-LGAXYSLN.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 dirname8, join as join2 } from "path";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/login.ts
|
|
@@ -185,7 +185,8 @@ var SUPPORTED_HOOKS = [
|
|
|
185
185
|
"PreToolUse",
|
|
186
186
|
"PostToolUse",
|
|
187
187
|
"Stop",
|
|
188
|
-
"SubagentStop"
|
|
188
|
+
"SubagentStop",
|
|
189
|
+
"SessionEnd"
|
|
189
190
|
];
|
|
190
191
|
function inspectToolResponse(tool_response) {
|
|
191
192
|
if (!tool_response || typeof tool_response !== "object") return { is_error: false };
|
|
@@ -261,6 +262,13 @@ function mapHookPayload(hookName, payload, augment) {
|
|
|
261
262
|
stop_hook_active: payload.stop_hook_active
|
|
262
263
|
};
|
|
263
264
|
break;
|
|
265
|
+
case "SessionEnd":
|
|
266
|
+
candidate = {
|
|
267
|
+
...base,
|
|
268
|
+
type: "session_close",
|
|
269
|
+
reason: payload.reason
|
|
270
|
+
};
|
|
271
|
+
break;
|
|
264
272
|
case "SubagentStop":
|
|
265
273
|
candidate = {
|
|
266
274
|
...base,
|
|
@@ -549,7 +557,7 @@ async function readNewAssistantTurns(sessionId, transcriptPath) {
|
|
|
549
557
|
if (uuid === "" || seen.has(uuid)) continue;
|
|
550
558
|
const msg = parsed.message ?? {};
|
|
551
559
|
const model = typeof msg.model === "string" ? msg.model : "";
|
|
552
|
-
if (model === "") continue;
|
|
560
|
+
if (model === "" || model === "<synthetic>") continue;
|
|
553
561
|
const usage = msg.usage ?? {};
|
|
554
562
|
turns.push({
|
|
555
563
|
message_uuid: uuid,
|
|
@@ -583,6 +591,157 @@ async function persistCursor(sessionId, seen, newlyEmitted) {
|
|
|
583
591
|
await writeFile5(file, trimmed.join("\n") + "\n");
|
|
584
592
|
}
|
|
585
593
|
|
|
594
|
+
// src/lib/subagent-transcript.ts
|
|
595
|
+
import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
596
|
+
import { dirname as dirname6 } from "path";
|
|
597
|
+
function asInt2(x) {
|
|
598
|
+
if (typeof x === "number" && Number.isFinite(x) && x >= 0) return Math.trunc(x);
|
|
599
|
+
return 0;
|
|
600
|
+
}
|
|
601
|
+
function textFromBlocks(content) {
|
|
602
|
+
if (!Array.isArray(content)) return typeof content === "string" ? content : void 0;
|
|
603
|
+
const parts = [];
|
|
604
|
+
for (const block of content) {
|
|
605
|
+
if (block && typeof block === "object" && "type" in block) {
|
|
606
|
+
const b = block;
|
|
607
|
+
if (b.type === "text" && typeof b.text === "string") parts.push(b.text);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
const joined = parts.join("\n").trim();
|
|
611
|
+
return joined === "" ? void 0 : joined;
|
|
612
|
+
}
|
|
613
|
+
function summarizeToolResult(content) {
|
|
614
|
+
if (!Array.isArray(content)) {
|
|
615
|
+
if (typeof content === "string") return { is_error: false };
|
|
616
|
+
return { is_error: false };
|
|
617
|
+
}
|
|
618
|
+
let isError = false;
|
|
619
|
+
const texts = [];
|
|
620
|
+
for (const block of content) {
|
|
621
|
+
if (block && typeof block === "object") {
|
|
622
|
+
const b = block;
|
|
623
|
+
if (b.is_error === true) isError = true;
|
|
624
|
+
if (typeof b.text === "string") texts.push(b.text);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (isError && texts.length > 0) return { is_error: true, error_message: texts.join("\n").slice(0, 500) };
|
|
628
|
+
if (isError) return { is_error: true };
|
|
629
|
+
return { is_error: false };
|
|
630
|
+
}
|
|
631
|
+
async function readNewSubagentEvents(parentSessionId, agentToolUseId, transcriptPath) {
|
|
632
|
+
const seen = await readCursor2(parentSessionId, agentToolUseId);
|
|
633
|
+
let raw;
|
|
634
|
+
try {
|
|
635
|
+
raw = await readFile6(transcriptPath, "utf8");
|
|
636
|
+
} catch {
|
|
637
|
+
return { emits: [], seen };
|
|
638
|
+
}
|
|
639
|
+
const emits = [];
|
|
640
|
+
const toolNameByUseId = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const line of raw.split("\n")) {
|
|
642
|
+
const trimmed = line.trim();
|
|
643
|
+
if (trimmed === "") continue;
|
|
644
|
+
let parsed;
|
|
645
|
+
try {
|
|
646
|
+
parsed = JSON.parse(trimmed);
|
|
647
|
+
} catch {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const uuid = typeof parsed.uuid === "string" ? parsed.uuid : "";
|
|
651
|
+
if (uuid === "" || seen.has(uuid)) continue;
|
|
652
|
+
const msg = parsed.message ?? {};
|
|
653
|
+
if (parsed.type === "user") {
|
|
654
|
+
const content = msg.content;
|
|
655
|
+
const hasToolResults = Array.isArray(content) && content.some(
|
|
656
|
+
(b) => b && typeof b === "object" && b.type === "tool_result"
|
|
657
|
+
);
|
|
658
|
+
if (hasToolResults) {
|
|
659
|
+
for (const block of content) {
|
|
660
|
+
if (!block || typeof block !== "object") continue;
|
|
661
|
+
const b = block;
|
|
662
|
+
if (b.type !== "tool_result") continue;
|
|
663
|
+
const tuid = typeof b.tool_use_id === "string" ? b.tool_use_id : "";
|
|
664
|
+
if (tuid === "") continue;
|
|
665
|
+
const { is_error, error_message } = summarizeToolResult(b.content);
|
|
666
|
+
emits.push({
|
|
667
|
+
kind: "tool_call",
|
|
668
|
+
uuid: `${uuid}:${tuid}`,
|
|
669
|
+
tool_use_id: tuid,
|
|
670
|
+
tool_name: toolNameByUseId.get(tuid) ?? "unknown",
|
|
671
|
+
tool_response: b.content,
|
|
672
|
+
is_error,
|
|
673
|
+
error_message
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
const text = textFromBlocks(content);
|
|
678
|
+
if (text !== void 0) {
|
|
679
|
+
emits.push({ kind: "user_prompt", uuid, prompt: text });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (parsed.type === "assistant") {
|
|
685
|
+
const model = typeof msg.model === "string" ? msg.model : "";
|
|
686
|
+
if (model === "" || model === "<synthetic>") continue;
|
|
687
|
+
const usage = msg.usage ?? {};
|
|
688
|
+
emits.push({
|
|
689
|
+
kind: "assistant_turn",
|
|
690
|
+
uuid,
|
|
691
|
+
message_uuid: uuid,
|
|
692
|
+
model,
|
|
693
|
+
input_tokens: asInt2(usage.input_tokens),
|
|
694
|
+
output_tokens: asInt2(usage.output_tokens),
|
|
695
|
+
cache_read_tokens: asInt2(usage.cache_read_input_tokens),
|
|
696
|
+
cache_creation_tokens: asInt2(usage.cache_creation_input_tokens),
|
|
697
|
+
text: textFromBlocks(msg.content)
|
|
698
|
+
});
|
|
699
|
+
if (Array.isArray(msg.content)) {
|
|
700
|
+
for (const block of msg.content) {
|
|
701
|
+
if (!block || typeof block !== "object") continue;
|
|
702
|
+
const b = block;
|
|
703
|
+
if (b.type !== "tool_use") continue;
|
|
704
|
+
const tuid = typeof b.id === "string" ? b.id : "";
|
|
705
|
+
const tname = typeof b.name === "string" ? b.name : "";
|
|
706
|
+
if (tuid === "" || tname === "") continue;
|
|
707
|
+
toolNameByUseId.set(tuid, tname);
|
|
708
|
+
emits.push({
|
|
709
|
+
kind: "tool_attempt",
|
|
710
|
+
uuid: `${uuid}:${tuid}`,
|
|
711
|
+
tool_use_id: tuid,
|
|
712
|
+
tool_name: tname,
|
|
713
|
+
tool_input: b.input
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return { emits, seen };
|
|
721
|
+
}
|
|
722
|
+
var CURSOR_KEEP2 = 500;
|
|
723
|
+
async function readCursor2(parentSessionId, agentToolUseId) {
|
|
724
|
+
try {
|
|
725
|
+
const raw = await readFile6(cursorFile(parentSessionId, agentToolUseId), "utf8");
|
|
726
|
+
return new Set(
|
|
727
|
+
raw.split("\n").map((s) => s.trim()).filter((s) => s !== "")
|
|
728
|
+
);
|
|
729
|
+
} catch {
|
|
730
|
+
return /* @__PURE__ */ new Set();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function cursorFile(parentSessionId, agentToolUseId) {
|
|
734
|
+
return `${paths.transcriptDir}/subagent-${parentSessionId}-${agentToolUseId}`;
|
|
735
|
+
}
|
|
736
|
+
async function persistSubagentCursor(parentSessionId, agentToolUseId, seen, newlyEmitted) {
|
|
737
|
+
for (const u of newlyEmitted) seen.add(u);
|
|
738
|
+
const file = cursorFile(parentSessionId, agentToolUseId);
|
|
739
|
+
await mkdir6(dirname6(file), { recursive: true });
|
|
740
|
+
const arr = [...seen];
|
|
741
|
+
const trimmed = arr.length > CURSOR_KEEP2 ? arr.slice(arr.length - CURSOR_KEEP2) : arr;
|
|
742
|
+
await writeFile6(file, trimmed.join("\n") + "\n");
|
|
743
|
+
}
|
|
744
|
+
|
|
586
745
|
// src/commands/push.ts
|
|
587
746
|
async function readStdin() {
|
|
588
747
|
if (process.stdin.isTTY) return "";
|
|
@@ -668,6 +827,76 @@ async function pushCommand(opts) {
|
|
|
668
827
|
}
|
|
669
828
|
} catch (err) {
|
|
670
829
|
process.stderr.write(`runtape: transcript scan failed: ${err instanceof Error ? err.message : String(err)}
|
|
830
|
+
`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (opts.event === "SubagentStop") {
|
|
835
|
+
const subTranscript = typeof payload.agent_transcript_path === "string" ? payload.agent_transcript_path : "";
|
|
836
|
+
const agentToolUseId = typeof payload.agent_id === "string" ? payload.agent_id : "";
|
|
837
|
+
if (subTranscript !== "" && agentToolUseId !== "") {
|
|
838
|
+
try {
|
|
839
|
+
const { emits, seen } = await readNewSubagentEvents(sessionId, agentToolUseId, subTranscript);
|
|
840
|
+
const newlyEmitted = [];
|
|
841
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
|
|
842
|
+
for (const e of emits) {
|
|
843
|
+
const seq = await nextSequence(sessionId);
|
|
844
|
+
const baseEnvelope = {
|
|
845
|
+
session_id: sessionId,
|
|
846
|
+
transcript_path: subTranscript,
|
|
847
|
+
cwd,
|
|
848
|
+
hook_event_name: opts.event,
|
|
849
|
+
permission_mode: typeof payload.permission_mode === "string" ? payload.permission_mode : void 0,
|
|
850
|
+
wall_ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
851
|
+
sequence: seq,
|
|
852
|
+
agent_tool_use_id: agentToolUseId
|
|
853
|
+
};
|
|
854
|
+
let ev = null;
|
|
855
|
+
if (e.kind === "user_prompt") {
|
|
856
|
+
ev = { ...baseEnvelope, type: "user_prompt", prompt: e.prompt };
|
|
857
|
+
} else if (e.kind === "assistant_turn") {
|
|
858
|
+
ev = {
|
|
859
|
+
...baseEnvelope,
|
|
860
|
+
type: "assistant_turn",
|
|
861
|
+
message_uuid: e.message_uuid,
|
|
862
|
+
model: e.model,
|
|
863
|
+
input_tokens: e.input_tokens,
|
|
864
|
+
output_tokens: e.output_tokens,
|
|
865
|
+
cache_read_tokens: e.cache_read_tokens,
|
|
866
|
+
cache_creation_tokens: e.cache_creation_tokens,
|
|
867
|
+
text: e.text
|
|
868
|
+
};
|
|
869
|
+
} else if (e.kind === "tool_attempt") {
|
|
870
|
+
ev = {
|
|
871
|
+
...baseEnvelope,
|
|
872
|
+
type: "tool_attempt",
|
|
873
|
+
tool_name: e.tool_name,
|
|
874
|
+
tool_input: e.tool_input,
|
|
875
|
+
tool_use_id: e.tool_use_id
|
|
876
|
+
};
|
|
877
|
+
} else if (e.kind === "tool_call") {
|
|
878
|
+
ev = {
|
|
879
|
+
...baseEnvelope,
|
|
880
|
+
type: "tool_call",
|
|
881
|
+
tool_name: e.tool_name,
|
|
882
|
+
tool_input: null,
|
|
883
|
+
tool_response: e.tool_response,
|
|
884
|
+
tool_use_id: e.tool_use_id,
|
|
885
|
+
duration_ms: 0,
|
|
886
|
+
is_error: e.is_error,
|
|
887
|
+
error_message: e.error_message
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
if (ev) {
|
|
891
|
+
await appendEvent(sessionId, ev);
|
|
892
|
+
newlyEmitted.push(e.uuid);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (newlyEmitted.length > 0) {
|
|
896
|
+
await persistSubagentCursor(sessionId, agentToolUseId, seen, newlyEmitted);
|
|
897
|
+
}
|
|
898
|
+
} catch (err) {
|
|
899
|
+
process.stderr.write(`runtape: subagent transcript scan failed: ${err instanceof Error ? err.message : String(err)}
|
|
671
900
|
`);
|
|
672
901
|
}
|
|
673
902
|
}
|
|
@@ -682,10 +911,10 @@ async function pushCommand(opts) {
|
|
|
682
911
|
}
|
|
683
912
|
|
|
684
913
|
// src/commands/status.ts
|
|
685
|
-
import { readFile as
|
|
914
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
686
915
|
async function readFlusherPid() {
|
|
687
916
|
try {
|
|
688
|
-
const raw = await
|
|
917
|
+
const raw = await readFile7(paths.flusherPid, "utf8");
|
|
689
918
|
const n = Number.parseInt(raw.trim(), 10);
|
|
690
919
|
return Number.isFinite(n) ? n : null;
|
|
691
920
|
} catch (err) {
|
|
@@ -873,15 +1102,15 @@ Step 3/3 \u2014 Claude Code hooks
|
|
|
873
1102
|
}
|
|
874
1103
|
|
|
875
1104
|
// src/lib/flusher.ts
|
|
876
|
-
import { appendFile as appendFile2, mkdir as
|
|
877
|
-
import { dirname as
|
|
1105
|
+
import { appendFile as appendFile2, mkdir as mkdir7, readFile as readFile8, unlink as unlink3, writeFile as writeFile7 } from "fs/promises";
|
|
1106
|
+
import { dirname as dirname7 } from "path";
|
|
878
1107
|
var POLL_INTERVAL_MS = 1500;
|
|
879
1108
|
var IDLE_EXIT_MS = 3e4;
|
|
880
1109
|
var BATCH_MAX = 100;
|
|
881
1110
|
var BACKOFF_STEPS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 6e4];
|
|
882
1111
|
async function log(line) {
|
|
883
1112
|
try {
|
|
884
|
-
await
|
|
1113
|
+
await mkdir7(dirname7(paths.flusherLog), { recursive: true });
|
|
885
1114
|
await appendFile2(paths.flusherLog, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
886
1115
|
`);
|
|
887
1116
|
} catch {
|
|
@@ -898,9 +1127,9 @@ async function isProcessAlive(pid) {
|
|
|
898
1127
|
}
|
|
899
1128
|
}
|
|
900
1129
|
async function acquirePidLock() {
|
|
901
|
-
await
|
|
1130
|
+
await mkdir7(dirname7(paths.flusherPid), { recursive: true });
|
|
902
1131
|
try {
|
|
903
|
-
const existing = await
|
|
1132
|
+
const existing = await readFile8(paths.flusherPid, "utf8");
|
|
904
1133
|
const pid = Number.parseInt(existing.trim(), 10);
|
|
905
1134
|
if (Number.isFinite(pid) && await isProcessAlive(pid)) {
|
|
906
1135
|
return false;
|
|
@@ -908,7 +1137,7 @@ async function acquirePidLock() {
|
|
|
908
1137
|
} catch (err) {
|
|
909
1138
|
if (err.code !== "ENOENT") throw err;
|
|
910
1139
|
}
|
|
911
|
-
await
|
|
1140
|
+
await writeFile7(paths.flusherPid, String(process.pid));
|
|
912
1141
|
return true;
|
|
913
1142
|
}
|
|
914
1143
|
async function releasePidLock() {
|
|
@@ -993,7 +1222,7 @@ async function runFlusher() {
|
|
|
993
1222
|
}
|
|
994
1223
|
|
|
995
1224
|
// src/index.ts
|
|
996
|
-
var pkgPath = join2(
|
|
1225
|
+
var pkgPath = join2(dirname8(fileURLToPath2(import.meta.url)), "..", "package.json");
|
|
997
1226
|
var PKG_VERSION = JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
998
1227
|
if (process.argv.includes("--internal-flusher")) {
|
|
999
1228
|
void runFlusher().then(
|