@lucascouts/claude-agent-tui 0.1.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.
Files changed (112) hide show
  1. package/LICENSE +191 -0
  2. package/NOTICE +14 -0
  3. package/README.md +50 -0
  4. package/dist/acp-agent.d.ts +594 -0
  5. package/dist/acp-agent.d.ts.map +1 -0
  6. package/dist/acp-agent.js +2139 -0
  7. package/dist/ansi-mirror.d.ts +42 -0
  8. package/dist/ansi-mirror.d.ts.map +1 -0
  9. package/dist/ansi-mirror.js +61 -0
  10. package/dist/besteffort.d.ts +44 -0
  11. package/dist/besteffort.d.ts.map +1 -0
  12. package/dist/besteffort.js +100 -0
  13. package/dist/billing/entrypoint-guard.d.ts +97 -0
  14. package/dist/billing/entrypoint-guard.d.ts.map +1 -0
  15. package/dist/billing/entrypoint-guard.js +166 -0
  16. package/dist/claude-path.d.ts +12 -0
  17. package/dist/claude-path.d.ts.map +1 -0
  18. package/dist/claude-path.js +61 -0
  19. package/dist/diff-enriched-reader.d.ts +41 -0
  20. package/dist/diff-enriched-reader.d.ts.map +1 -0
  21. package/dist/diff-enriched-reader.js +106 -0
  22. package/dist/diff-source.d.ts +104 -0
  23. package/dist/diff-source.d.ts.map +1 -0
  24. package/dist/diff-source.js +164 -0
  25. package/dist/end-of-turn.d.ts +172 -0
  26. package/dist/end-of-turn.d.ts.map +1 -0
  27. package/dist/end-of-turn.js +415 -0
  28. package/dist/engine-lifecycle.d.ts +222 -0
  29. package/dist/engine-lifecycle.d.ts.map +1 -0
  30. package/dist/engine-lifecycle.js +236 -0
  31. package/dist/engine-pty.d.ts +143 -0
  32. package/dist/engine-pty.d.ts.map +1 -0
  33. package/dist/engine-pty.js +222 -0
  34. package/dist/engine-watcher.d.ts +83 -0
  35. package/dist/engine-watcher.d.ts.map +1 -0
  36. package/dist/engine-watcher.js +173 -0
  37. package/dist/engine.d.ts +30 -0
  38. package/dist/engine.d.ts.map +1 -0
  39. package/dist/engine.js +34 -0
  40. package/dist/event-switch.d.ts +164 -0
  41. package/dist/event-switch.d.ts.map +1 -0
  42. package/dist/event-switch.js +206 -0
  43. package/dist/gate/port.d.ts +38 -0
  44. package/dist/gate/port.d.ts.map +1 -0
  45. package/dist/gate/port.js +126 -0
  46. package/dist/gate/settings-writer.d.ts +130 -0
  47. package/dist/gate/settings-writer.d.ts.map +1 -0
  48. package/dist/gate/settings-writer.js +349 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +106 -0
  52. package/dist/jsonl.d.ts +267 -0
  53. package/dist/jsonl.d.ts.map +1 -0
  54. package/dist/jsonl.js +527 -0
  55. package/dist/lib.d.ts +6 -0
  56. package/dist/lib.d.ts.map +1 -0
  57. package/dist/lib.js +5 -0
  58. package/dist/linearize.d.ts +219 -0
  59. package/dist/linearize.d.ts.map +1 -0
  60. package/dist/linearize.js +444 -0
  61. package/dist/live-diff-env.d.ts +7 -0
  62. package/dist/live-diff-env.d.ts.map +1 -0
  63. package/dist/live-diff-env.js +18 -0
  64. package/dist/live-subagent-env.d.ts +7 -0
  65. package/dist/live-subagent-env.d.ts.map +1 -0
  66. package/dist/live-subagent-env.js +19 -0
  67. package/dist/permissions/allow-inject.d.ts +67 -0
  68. package/dist/permissions/allow-inject.d.ts.map +1 -0
  69. package/dist/permissions/allow-inject.js +85 -0
  70. package/dist/permissions/deny.d.ts +60 -0
  71. package/dist/permissions/deny.d.ts.map +1 -0
  72. package/dist/permissions/deny.js +81 -0
  73. package/dist/permissions/gate-wiring.d.ts +112 -0
  74. package/dist/permissions/gate-wiring.d.ts.map +1 -0
  75. package/dist/permissions/gate-wiring.js +350 -0
  76. package/dist/permissions/hook-server.d.ts +72 -0
  77. package/dist/permissions/hook-server.d.ts.map +1 -0
  78. package/dist/permissions/hook-server.js +179 -0
  79. package/dist/permissions/permission-mode.d.ts +67 -0
  80. package/dist/permissions/permission-mode.d.ts.map +1 -0
  81. package/dist/permissions/permission-mode.js +100 -0
  82. package/dist/permissions/request-permission.d.ts +102 -0
  83. package/dist/permissions/request-permission.d.ts.map +1 -0
  84. package/dist/permissions/request-permission.js +124 -0
  85. package/dist/settings.d.ts +68 -0
  86. package/dist/settings.d.ts.map +1 -0
  87. package/dist/settings.js +182 -0
  88. package/dist/stop-reason-map.d.ts +17 -0
  89. package/dist/stop-reason-map.d.ts.map +1 -0
  90. package/dist/stop-reason-map.js +33 -0
  91. package/dist/subagent-source.d.ts +63 -0
  92. package/dist/subagent-source.d.ts.map +1 -0
  93. package/dist/subagent-source.js +132 -0
  94. package/dist/subagent-watcher.d.ts +40 -0
  95. package/dist/subagent-watcher.d.ts.map +1 -0
  96. package/dist/subagent-watcher.js +108 -0
  97. package/dist/tools.d.ts +119 -0
  98. package/dist/tools.d.ts.map +1 -0
  99. package/dist/tools.js +729 -0
  100. package/dist/usage-env.d.ts +7 -0
  101. package/dist/usage-env.d.ts.map +1 -0
  102. package/dist/usage-env.js +16 -0
  103. package/dist/usage.d.ts +54 -0
  104. package/dist/usage.d.ts.map +1 -0
  105. package/dist/usage.js +53 -0
  106. package/dist/utils.d.ts +16 -0
  107. package/dist/utils.d.ts.map +1 -0
  108. package/dist/utils.js +83 -0
  109. package/dist/zed-register.d.ts +26 -0
  110. package/dist/zed-register.d.ts.map +1 -0
  111. package/dist/zed-register.js +106 -0
  112. package/package.json +79 -0
@@ -0,0 +1,42 @@
1
+ import type { IPty, IDisposable } from "node-pty";
2
+ /**
3
+ * The live-view sink: receives each raw PTY chunk verbatim. This is the OUTPUT-ONLY surface a
4
+ * host may pipe to a terminal view for §9 "is the prompt alive?" gating visibility. It is a sink,
5
+ * not a source — there is deliberately NO return channel by which a consumer could write back into
6
+ * engine/session state. Consumers MUST NOT parse this stream back into session state (§2): state
7
+ * comes only from the JSONL tail.
8
+ */
9
+ export type AnsiChunkSink = (chunk: string) => void;
10
+ /** Options for {@link attachAnsiMirror}. */
11
+ export interface AnsiMirrorOptions {
12
+ /**
13
+ * Master flag — OFF by default (§5/§9). When false/absent, NO `p.onData` listener is attached
14
+ * and the engine path is byte-for-byte the read-only Degrau-1 behaviour. Only when true AND a
15
+ * sink is supplied is the cosmetic tap subscribed.
16
+ */
17
+ enabled?: boolean;
18
+ /**
19
+ * The live-view sink that receives each raw chunk verbatim. The tap is a no-op (and no listener
20
+ * is attached) when this is absent, so the mirror cannot accidentally subscribe with nowhere to
21
+ * forward to.
22
+ */
23
+ onAnsiChunk?: AnsiChunkSink;
24
+ }
25
+ /**
26
+ * Conditionally tap the PTY's `p.onData` stream for the cosmetic live ANSI mirror (§5).
27
+ *
28
+ * When the mirror is ENABLED (flag on AND a sink supplied), subscribes a stateless `p.onData`
29
+ * listener that forwards each raw chunk VERBATIM to the sink — no parsing, no ANSI decoding, no
30
+ * state accumulation, no `SessionUpdate`, no billing — and returns the `IDisposable` so the caller
31
+ * can detach it on teardown.
32
+ *
33
+ * When the mirror is DISABLED (the default — flag off OR no sink), `p.onData` is NEVER touched and
34
+ * the function returns `undefined`. This is the load-bearing guarantee that the OFF path is
35
+ * byte-for-byte the read-only Degrau-1 engine path: a disabled mirror attaches no listener at all.
36
+ *
37
+ * The mirror is structurally subordinate to the JSONL tail (§2): it is one-way (bytes → sink) and
38
+ * feeds NOTHING into the structural path. Toggling it on/off does not change the structural event
39
+ * sequence the JSONL tail produces.
40
+ */
41
+ export declare function attachAnsiMirror(p: Pick<IPty, "onData">, opts?: AnsiMirrorOptions): IDisposable | undefined;
42
+ //# sourceMappingURL=ansi-mirror.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ansi-mirror.d.ts","sourceRoot":"","sources":["../src/ansi-mirror.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;AAEpD,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;OAIG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EACvB,IAAI,GAAE,iBAAsB,GAC3B,WAAW,GAAG,SAAS,CAgBzB"}
@@ -0,0 +1,61 @@
1
+ // === §5 Espelho ANSI ao vivo (opcional) — cosmetic live PTY mirror (story 035) ===
2
+ //
3
+ // Optional, flag-gated tap on the story-013/014 PTY's `p.onData(chunk => …)` stream that
4
+ // forwards the RAW TUI bytes to a "live-view" sink, for the §5 "ao vivo" effect ONLY. Its sole
5
+ // purpose is the §9 v1 gating-visibility surface: a human can render this stream to confirm the
6
+ // real `claude` prompt is alive and drawing in-TUI, WITHOUT it ever becoming a structural input.
7
+ //
8
+ // ── tail-as-source-of-truth fence (§2 ordering) ──────────────────────────────────────────────
9
+ // This mirror is STRICTLY subordinate to the JSONL tail (stories 015/016/017). It:
10
+ // • holds NO parsed state — it is stateless and one-way (bytes → sink, nothing read back);
11
+ // • emits NO ACP `SessionUpdate` — it never calls the tail watcher, the event switch, the
12
+ // translators, or any `SessionUpdate` producer (structure/state come ONLY from the JSONL);
13
+ // • does NOT decode/parse ANSI — the byte stream is interleaved/partial across chunks, so any
14
+ // parse attempt would corrupt state; it forwards the chunk verbatim and nothing else;
15
+ // • touches NO billing and has NO IO side-effect beyond the single sink call.
16
+ // ANY future code that derives session state from `onData` bytes is a §2 violation by construction.
17
+ //
18
+ // ── OFF is byte-for-byte the read-only Degrau-1 path (story 023) ─────────────────────────────
19
+ // The flag is OFF by default. When disabled, {@link attachAnsiMirror} returns WITHOUT calling
20
+ // `p.onData` at all — no listener is ever attached — so the engine path is identical to the
21
+ // read-only Degrau-1 behaviour. Enabling/disabling the mirror changes NOTHING in the structural
22
+ // output: the JSONL tail produces the identical event sequence with the mirror on or off.
23
+ //
24
+ // ── §7 caveat: streamEventToAcpNotifications stays UNWIRED in v1 ──────────────────────────────
25
+ // `streamEventToAcpNotifications` (`content_block_delta` deltas) is left without a source in the
26
+ // JSONL (which is per-block granular, §7); it would ONLY be viable layered ON TOP of this cosmetic
27
+ // mirror. But this mirror has NO ordering or structural guarantees (structure still comes from the
28
+ // JSONL tail per §2), so wiring deltas onto it is DELIBERATELY left undone in v1. This module
29
+ // implements NO delta-to-`SessionUpdate` translation, and no such wiring exists in this path.
30
+ /**
31
+ * Conditionally tap the PTY's `p.onData` stream for the cosmetic live ANSI mirror (§5).
32
+ *
33
+ * When the mirror is ENABLED (flag on AND a sink supplied), subscribes a stateless `p.onData`
34
+ * listener that forwards each raw chunk VERBATIM to the sink — no parsing, no ANSI decoding, no
35
+ * state accumulation, no `SessionUpdate`, no billing — and returns the `IDisposable` so the caller
36
+ * can detach it on teardown.
37
+ *
38
+ * When the mirror is DISABLED (the default — flag off OR no sink), `p.onData` is NEVER touched and
39
+ * the function returns `undefined`. This is the load-bearing guarantee that the OFF path is
40
+ * byte-for-byte the read-only Degrau-1 engine path: a disabled mirror attaches no listener at all.
41
+ *
42
+ * The mirror is structurally subordinate to the JSONL tail (§2): it is one-way (bytes → sink) and
43
+ * feeds NOTHING into the structural path. Toggling it on/off does not change the structural event
44
+ * sequence the JSONL tail produces.
45
+ */
46
+ export function attachAnsiMirror(p, opts = {}) {
47
+ // OFF path (default): do NOT touch p.onData. No listener is attached → byte-for-byte the
48
+ // read-only Degrau-1 path. The `onAnsiChunk` guard means an enabled-but-sinkless config is also
49
+ // a no-op (it would have nowhere to forward to), never a dangling subscription.
50
+ if (!opts.enabled || !opts.onAnsiChunk) {
51
+ return undefined;
52
+ }
53
+ const sink = opts.onAnsiChunk;
54
+ // ON path: forward the raw chunk verbatim. STATELESS and one-way — bytes → sink ONLY. This
55
+ // handler deliberately does NOT call the tail watcher (015), the event switch (016), the
56
+ // translators, or any `SessionUpdate` producer (§2). Deriving any state from `chunk` here would
57
+ // be a §2 violation: structure/state come solely from the JSONL tail.
58
+ return p.onData((chunk) => {
59
+ sink(chunk);
60
+ });
61
+ }
@@ -0,0 +1,44 @@
1
+ import { type AgentSideConnection, type SessionNotification } from "@agentclientprotocol/sdk";
2
+ import { type ContentBlockParam } from "@anthropic-ai/sdk/resources";
3
+ import { BetaContentBlock, BetaRawContentBlockDelta } from "@anthropic-ai/sdk/resources/beta.mjs";
4
+ import { toAcpNotifications, type Logger, type ToolUseCache } from "./acp-agent.js";
5
+ /**
6
+ * Per-surface enable flags for the convenience translation. Each surface is gated independently so
7
+ * the orchestrator can, e.g., keep `plan` while suppressing `image` (or disable both entirely).
8
+ */
9
+ export interface ConvenienceFlags {
10
+ /** Emit `plan` updates derived from `TodoWrite` tool_use blocks when true. */
11
+ plan: boolean;
12
+ /** Emit `image` content updates derived from `image` blocks when true. */
13
+ image: boolean;
14
+ }
15
+ /**
16
+ * A CONVENIENCE block is one whose translation produces a best-effort surface (NOT part of the core
17
+ * text/tool thread): an `image` content block, OR a `TodoWrite` tool_use (which routes to `plan`).
18
+ * Everything else (text, thinking, other tool_use, tool_result, …) is CORE.
19
+ */
20
+ export declare function isConvenienceBlock(block: any): boolean;
21
+ type ContentBlock = ContentBlockParam | BetaContentBlock | BetaRawContentBlockDelta;
22
+ type TranslateFn = typeof toAcpNotifications;
23
+ /**
24
+ * Translate `content` with the plan/image convenience surface gated behind a best-effort flag.
25
+ *
26
+ * CORE pass (always, never gated): the non-convenience blocks are translated via the REAL
27
+ * `toAcpNotifications` (`registerHooks: false` to stay synchronous, mirroring the §7 seam tests). A
28
+ * convenience failure can NEVER touch these updates — they are produced before the gated pass runs.
29
+ *
30
+ * CONVENIENCE pass (flag-gated + try/catch): only when at least one surface flag is on AND there is
31
+ * at least one convenience block, the convenience subset is translated (via the injectable
32
+ * `opts.translate`, default the real translator) inside a try/catch. Surviving updates are filtered
33
+ * to the enabled surfaces and appended after the core updates. On a thrown error OR an unusable
34
+ * (non-array) result, a drift note is logged via `logger.error(...)` and that surface is SKIPPED
35
+ * (nothing appended) — the error is NEVER rethrown, so the live thread is never broken.
36
+ *
37
+ * Signature mirrors `toAcpNotifications`'s real parameter list/types (acp-agent.ts:2866), with the
38
+ * required `flags` and the optional `opts.translate` test seam appended.
39
+ */
40
+ export declare function translateBestEffort(content: string | ContentBlock[], role: "assistant" | "user", sessionId: string, cache: ToolUseCache, client: AgentSideConnection, logger: Logger, flags: ConvenienceFlags, opts?: {
41
+ translate?: TranslateFn;
42
+ }): SessionNotification[];
43
+ export {};
44
+ //# sourceMappingURL=besteffort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"besteffort.d.ts","sourceRoot":"","sources":["../src/besteffort.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC9F,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACzB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,KAAK,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEpF;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,IAAI,EAAE,OAAO,CAAC;IACd,0EAA0E;IAC1E,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAKtD;AAED,KAAK,YAAY,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,wBAAwB,CAAC;AACpF,KAAK,WAAW,GAAG,OAAO,kBAAkB,CAAC;AA8C7C;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,GAAG,YAAY,EAAE,EAChC,IAAI,EAAE,WAAW,GAAG,MAAM,EAC1B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,mBAAmB,EAC3B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,EACvB,IAAI,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,WAAW,CAAA;CAAE,GACjC,mBAAmB,EAAE,CA0CvB"}
@@ -0,0 +1,100 @@
1
+ import { toAcpNotifications } from "./acp-agent.js";
2
+ /**
3
+ * A CONVENIENCE block is one whose translation produces a best-effort surface (NOT part of the core
4
+ * text/tool thread): an `image` content block, OR a `TodoWrite` tool_use (which routes to `plan`).
5
+ * Everything else (text, thinking, other tool_use, tool_result, …) is CORE.
6
+ */
7
+ export function isConvenienceBlock(block) {
8
+ return (block?.type === "image" ||
9
+ (block?.type === "tool_use" && block?.name === "TodoWrite"));
10
+ }
11
+ /**
12
+ * Split `content` into [core, convenience] block subsets using `isConvenienceBlock`. A non-array /
13
+ * string `content` (the bare-string `toAcpNotifications` fast-path) has no per-block convenience
14
+ * surface, so it is treated wholly as CORE (returned via the core subset, empty convenience subset).
15
+ */
16
+ function partitionBlocks(content) {
17
+ if (typeof content === "string" || !Array.isArray(content)) {
18
+ return { core: content, convenience: [] };
19
+ }
20
+ const core = [];
21
+ const convenience = [];
22
+ for (const block of content) {
23
+ if (isConvenienceBlock(block)) {
24
+ convenience.push(block);
25
+ }
26
+ else {
27
+ core.push(block);
28
+ }
29
+ }
30
+ return { core, convenience };
31
+ }
32
+ /**
33
+ * Keep only the convenience updates whose surface flag is on. A `plan` update survives iff
34
+ * `flags.plan`; an `image`-content update survives iff `flags.image`. Anything else the convenience
35
+ * pass might emit is dropped (the convenience subset only ever contains image/TodoWrite blocks).
36
+ */
37
+ function filterConvenienceUpdates(updates, flags) {
38
+ return updates.filter((n) => {
39
+ const update = n?.update;
40
+ if (update?.sessionUpdate === "plan") {
41
+ return flags.plan;
42
+ }
43
+ if (update?.content?.type === "image") {
44
+ return flags.image;
45
+ }
46
+ return false;
47
+ });
48
+ }
49
+ /**
50
+ * Translate `content` with the plan/image convenience surface gated behind a best-effort flag.
51
+ *
52
+ * CORE pass (always, never gated): the non-convenience blocks are translated via the REAL
53
+ * `toAcpNotifications` (`registerHooks: false` to stay synchronous, mirroring the §7 seam tests). A
54
+ * convenience failure can NEVER touch these updates — they are produced before the gated pass runs.
55
+ *
56
+ * CONVENIENCE pass (flag-gated + try/catch): only when at least one surface flag is on AND there is
57
+ * at least one convenience block, the convenience subset is translated (via the injectable
58
+ * `opts.translate`, default the real translator) inside a try/catch. Surviving updates are filtered
59
+ * to the enabled surfaces and appended after the core updates. On a thrown error OR an unusable
60
+ * (non-array) result, a drift note is logged via `logger.error(...)` and that surface is SKIPPED
61
+ * (nothing appended) — the error is NEVER rethrown, so the live thread is never broken.
62
+ *
63
+ * Signature mirrors `toAcpNotifications`'s real parameter list/types (acp-agent.ts:2866), with the
64
+ * required `flags` and the optional `opts.translate` test seam appended.
65
+ */
66
+ export function translateBestEffort(content, role, sessionId, cache, client, logger, flags, opts) {
67
+ const { core, convenience } = partitionBlocks(content);
68
+ // CORE pass — ALWAYS the real translator, ALWAYS emitted. Outside any try/catch so a convenience
69
+ // failure provably cannot affect it.
70
+ const coreUpdates = toAcpNotifications(core, role, sessionId, cache, client, logger, {
71
+ registerHooks: false,
72
+ });
73
+ // CONVENIENCE pass — gated by the flags AND the presence of convenience blocks. Skipped entirely
74
+ // when both surfaces are off or there is nothing convenient to translate.
75
+ const anyFlagOn = flags.plan || flags.image;
76
+ if (!anyFlagOn || convenience.length === 0) {
77
+ return coreUpdates;
78
+ }
79
+ const translate = opts?.translate ?? toAcpNotifications;
80
+ try {
81
+ const raw = translate(convenience, role, sessionId, cache, client, logger, {
82
+ registerHooks: false,
83
+ });
84
+ // Guard against an unusable result (e.g. a stub returning undefined/non-array): treat as a
85
+ // best-effort miss rather than letting a downstream `.filter` throw.
86
+ if (!Array.isArray(raw)) {
87
+ logger.error(`[claude-agent-acp] best-effort convenience translation yielded an unusable (non-array) result for session ${sessionId}; skipping the plan/image surface`);
88
+ return coreUpdates;
89
+ }
90
+ const convenienceUpdates = filterConvenienceUpdates(raw, flags);
91
+ // Core first, then the enabled convenience surfaces appended (ordering trade-off above).
92
+ return [...coreUpdates, ...convenienceUpdates];
93
+ }
94
+ catch (err) {
95
+ // CONTAINMENT: a thrown convenience translation is caught here, logged as a drift note, and
96
+ // swallowed. The core stream (already computed) is returned intact — the turn/thread survives.
97
+ logger.error(`[claude-agent-acp] best-effort convenience translation threw for session ${sessionId}; skipping the plan/image surface (thread preserved): ${err instanceof Error ? err.message : String(err)}`);
98
+ return coreUpdates;
99
+ }
100
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * §10 SUBSCRIPTION set (OK) — client labels that bill on the Claude subscription.
3
+ * Source: doc §10, `claude` binary 2.1.159.
4
+ */
5
+ export declare const SUBSCRIPTION_ENTRYPOINTS: readonly ["cli", "claude-vscode", "vscode", "jetbrains", "firstParty", "local-agent"];
6
+ /**
7
+ * §10 CREDIT set (ABORT) — client labels that bill on API credit (Agent SDK / headless `-p`).
8
+ * Source: doc §10, `claude` binary 2.1.159.
9
+ */
10
+ export declare const CREDIT_ENTRYPOINTS: readonly ["sdk-ts", "sdk-py", "sdk-cli", "print"];
11
+ export type EntrypointClass = "subscription" | "credit" | "unknown";
12
+ /**
13
+ * Classify a client-emitted `entrypoint` self-label into its §10 billing class.
14
+ *
15
+ * @param entrypoint the raw `entrypoint` value read from a message event.
16
+ * @returns `'subscription'` for any label in {@link SUBSCRIPTION_ENTRYPOINTS}; `'credit'` for any
17
+ * label in {@link CREDIT_ENTRYPOINTS}; `'unknown'` for any other label (conservative — NEVER
18
+ * classifies an unrecognized label as `'subscription'`).
19
+ */
20
+ export declare function classifyEntrypoint(entrypoint: string): EntrypointClass;
21
+ /**
22
+ * Structural shape of one watched message event. Mirrors the story-015 watcher's
23
+ * `SessionMessage` (`{ uuid?: string; [k: string]: unknown }`) plus the two fields this guard
24
+ * reads. Kept LOCAL & structural on purpose — importing `engine-watcher` would pull in node-pty
25
+ * and friends, and this module must stay light and pure.
26
+ */
27
+ export interface WatchedMessage {
28
+ type?: string;
29
+ entrypoint?: string;
30
+ sessionId?: string;
31
+ [k: string]: unknown;
32
+ }
33
+ /**
34
+ * The decision {@link inspectEvent} returns for one watched event:
35
+ * - `skip` — a lightweight (non-message) type; nothing to classify.
36
+ * - `allow` — an `assistant`/`user` event whose entrypoint is in the §10 SUBSCRIPTION set.
37
+ * - `abort` — an `assistant`/`user` event whose entrypoint is credit OR unknown (incl. missing).
38
+ */
39
+ export type GuardDecision = {
40
+ action: "skip";
41
+ } | {
42
+ action: "allow";
43
+ entrypoint: string;
44
+ entrypointClass: "subscription";
45
+ } | {
46
+ action: "abort";
47
+ entrypoint: string;
48
+ entrypointClass: "credit" | "unknown";
49
+ };
50
+ /**
51
+ * Pure decision for one watched message event (see module note). Does not mutate `event` and has
52
+ * no side effects.
53
+ *
54
+ * @param event one event delivered by the story-015 watcher.
55
+ * @returns `{ action: 'skip' }` for non-message (lightweight) types; otherwise an `allow`/`abort`
56
+ * decision carrying the read `entrypoint` and its §10 class. A missing/empty `.entrypoint` on an
57
+ * `assistant`/`user` event classifies as `'unknown'` → `abort` (never allowed).
58
+ */
59
+ export declare function inspectEvent(event: WatchedMessage): GuardDecision;
60
+ /**
61
+ * Side-effect sink injected into {@link guardEvent}. Kept as an interface (not real I/O) so the guard
62
+ * is pure w.r.t. globals and testable; story 023 wires these to the engine-lifecycle stop.
63
+ */
64
+ export interface GuardHooks {
65
+ /** Loud, unambiguous alert sink. The message MUST carry the offending entrypoint value AND session id. */
66
+ alert(message: string): void;
67
+ /** Stop the session (wired to engine-lifecycle stop in story 023). Invoked once per violation. */
68
+ stopSession(info: {
69
+ entrypoint: string;
70
+ sessionId?: string;
71
+ entrypointClass: "credit" | "unknown";
72
+ }): void;
73
+ }
74
+ /**
75
+ * Apply {@link inspectEvent}'s decision for one watched event against the injected {@link GuardHooks}.
76
+ *
77
+ * @param event one event delivered by the story-015 watcher. NEVER mutated.
78
+ * @param hooks the alert/stop sink (the ONLY channel for side effects).
79
+ * @returns the same {@link GuardDecision} `inspectEvent` produced. On `'abort'` (credit OR unknown),
80
+ * first {@link GuardHooks.alert | alerts} with a loud message carrying the offending entrypoint value
81
+ * AND session id, then {@link GuardHooks.stopSession | stops the session}; on `'allow'`/`'skip'` it
82
+ * returns with no side effect. Does NOT mutate, default, or overwrite `event.entrypoint` (§10).
83
+ */
84
+ export declare function guardEvent(event: WatchedMessage, hooks: GuardHooks): GuardDecision;
85
+ /**
86
+ * Fail-fast assertion that NO billing-sensitive env key survived into the env about to be passed to
87
+ * spawn (§10 post-condition for {@link buildSanitizedEnv}). Read-only: never deletes or mutates `env`.
88
+ *
89
+ * @param env the spawn env (process-env shape — values may be `undefined`).
90
+ * @throws {Error} if any key is present (value not `undefined`) that is exactly
91
+ * `CLAUDE_CODE_ENTRYPOINT` OR starts with `CLAUDE_AGENT_SDK_`. The message NAMES every leaked key
92
+ * and states the spawn must be refused (these would silently bill the run as API credit — R1, §10).
93
+ * A key set to `undefined` is treated as absent (not a leak). `CLAUDECODE` is NOT checked here (it
94
+ * is the spawn helper's anti-recusa concern, distinct from this billing check — §10).
95
+ */
96
+ export declare function assertCleanBillingEnv(env: Record<string, string | undefined>): void;
97
+ //# sourceMappingURL=entrypoint-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entrypoint-guard.d.ts","sourceRoot":"","sources":["../../src/billing/entrypoint-guard.ts"],"names":[],"mappings":"AAsCA;;;GAGG;AACH,eAAO,MAAM,wBAAwB,uFAO3B,CAAC;AAEX;;;GAGG;AACH,eAAO,MAAM,kBAAkB,mDAKrB,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEpE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,CAQtE;AAeD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,cAAc,CAAA;CAAE,GACxE;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,QAAQ,GAAG,SAAS,CAAA;CAAE,CAAC;AAEnF;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,aAAa,CAUjE;AAkBD;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,0GAA0G;IAC1G,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kGAAkG;IAClG,WAAW,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,QAAQ,GAAG,SAAS,CAAA;KAAE,GAAG,IAAI,CAAC;CAC5G;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,GAAG,aAAa,CAkBlF;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,IAAI,CAenF"}
@@ -0,0 +1,166 @@
1
+ // Story 022 — entrypoint billing guard-rail.
2
+ //
3
+ // ── GOOD-FAITH LIMITS (this guard-rail is a SIGNAL, never a lever) ─────────────────────────────────
4
+ // Sources: doc §10 "Auditoria de billing — entrypoint"; §0 TL;DR point 3; Appendix row
5
+ // "`entrypoint` como prova de billing"; experiments/DEGRAU0-RESULTS.md (E1).
6
+ //
7
+ // (a) `entrypoint` is a CLIENT SELF-LABEL, written verbatim from an env var without checking the
8
+ // real mode. It is a GOOD-FAITH SIGNAL — NOT contractual proof that the run billed on the Claude
9
+ // subscription, and NOT a contract published by Anthropic. Billing is decided server-side by the
10
+ // headers the client sends; we do not capture TLS, so this field corroborates, it does not prove.
11
+ // (b) This code NEVER forces or spoofs `entrypoint=cli`. It only READS and REACTS — rewriting the
12
+ // value toward 'cli' would be spoofing-with-enforcement (the OpenClaw case) = evasion. `guardEvent`
13
+ // leaves the observed value untouched and aborts on a credit-class label instead.
14
+ // (c) The subscription GUARANTEE comes from RUNNING THE REAL TUI with a sanitized env — the E1
15
+ // keystone (experiments/DEGRAU0-RESULTS.md: a clean-env PTY writes `entrypoint=='cli'`
16
+ // ORGANICALLY), with billing decided server-side. This guard-rail only DETECTS a drift into
17
+ // credit (sdk-*/print) after the fact; it must never manufacture the subscription posture.
18
+ // ───────────────────────────────────────────────────────────────────────────────────────────────────
19
+ //
20
+ // Story 022 / Task 1.1 — subscription/credit taxonomy + classifier for the runtime
21
+ // entrypoint billing guard-rail (doc §10 "Auditoria de billing — entrypoint").
22
+ //
23
+ // WHAT THIS IS: the centralized §10 taxonomy (two frozen label sets) plus `classifyEntrypoint`,
24
+ // a PURE function mapping a client-emitted `entrypoint` self-label to a billing class:
25
+ // - SUBSCRIPTION (OK): the TUI / first-party clients that bill on the Claude subscription.
26
+ // - CREDIT (ABORT): the Agent SDK / headless `-p` clients that bill on API credit.
27
+ // - 'unknown': any label in NEITHER set. It MUST be treated conservatively by the caller
28
+ // (task 2.2) and MUST NEVER fall through to 'subscription' — an unrecognized or newly
29
+ // introduced label is not proof of subscription billing.
30
+ //
31
+ // WHY a centralized, version-noted constant (doc §10): the taxonomy was reverse-engineered from
32
+ // the `claude` binary 2.1.159 and the `entrypoint` field is the client's own self-label (written
33
+ // verbatim from an env var, not a published contract). It may drift between 2.1.x versions, so the
34
+ // sets live in ONE place with the source version cited below — a binary bump shows up in review.
35
+ //
36
+ // SOURCE: doc §10 "Auditoria de billing — entrypoint", reverse-engineered from `claude` binary
37
+ // 2.1.159. If the binary version changes, re-audit these two sets against the new binary.
38
+ /**
39
+ * §10 SUBSCRIPTION set (OK) — client labels that bill on the Claude subscription.
40
+ * Source: doc §10, `claude` binary 2.1.159.
41
+ */
42
+ export const SUBSCRIPTION_ENTRYPOINTS = [
43
+ "cli",
44
+ "claude-vscode",
45
+ "vscode",
46
+ "jetbrains",
47
+ "firstParty",
48
+ "local-agent",
49
+ ];
50
+ /**
51
+ * §10 CREDIT set (ABORT) — client labels that bill on API credit (Agent SDK / headless `-p`).
52
+ * Source: doc §10, `claude` binary 2.1.159.
53
+ */
54
+ export const CREDIT_ENTRYPOINTS = [
55
+ "sdk-ts",
56
+ "sdk-py",
57
+ "sdk-cli",
58
+ "print",
59
+ ];
60
+ /**
61
+ * Classify a client-emitted `entrypoint` self-label into its §10 billing class.
62
+ *
63
+ * @param entrypoint the raw `entrypoint` value read from a message event.
64
+ * @returns `'subscription'` for any label in {@link SUBSCRIPTION_ENTRYPOINTS}; `'credit'` for any
65
+ * label in {@link CREDIT_ENTRYPOINTS}; `'unknown'` for any other label (conservative — NEVER
66
+ * classifies an unrecognized label as `'subscription'`).
67
+ */
68
+ export function classifyEntrypoint(entrypoint) {
69
+ if (SUBSCRIPTION_ENTRYPOINTS.includes(entrypoint)) {
70
+ return "subscription";
71
+ }
72
+ if (CREDIT_ENTRYPOINTS.includes(entrypoint)) {
73
+ return "credit";
74
+ }
75
+ return "unknown";
76
+ }
77
+ /**
78
+ * Pure decision for one watched message event (see module note). Does not mutate `event` and has
79
+ * no side effects.
80
+ *
81
+ * @param event one event delivered by the story-015 watcher.
82
+ * @returns `{ action: 'skip' }` for non-message (lightweight) types; otherwise an `allow`/`abort`
83
+ * decision carrying the read `entrypoint` and its §10 class. A missing/empty `.entrypoint` on an
84
+ * `assistant`/`user` event classifies as `'unknown'` → `abort` (never allowed).
85
+ */
86
+ export function inspectEvent(event) {
87
+ if (event.type !== "assistant" && event.type !== "user") {
88
+ return { action: "skip" };
89
+ }
90
+ const entrypoint = event.entrypoint ?? "";
91
+ const entrypointClass = classifyEntrypoint(entrypoint);
92
+ if (entrypointClass === "subscription") {
93
+ return { action: "allow", entrypoint, entrypointClass };
94
+ }
95
+ return { action: "abort", entrypoint, entrypointClass };
96
+ }
97
+ /**
98
+ * Apply {@link inspectEvent}'s decision for one watched event against the injected {@link GuardHooks}.
99
+ *
100
+ * @param event one event delivered by the story-015 watcher. NEVER mutated.
101
+ * @param hooks the alert/stop sink (the ONLY channel for side effects).
102
+ * @returns the same {@link GuardDecision} `inspectEvent` produced. On `'abort'` (credit OR unknown),
103
+ * first {@link GuardHooks.alert | alerts} with a loud message carrying the offending entrypoint value
104
+ * AND session id, then {@link GuardHooks.stopSession | stops the session}; on `'allow'`/`'skip'` it
105
+ * returns with no side effect. Does NOT mutate, default, or overwrite `event.entrypoint` (§10).
106
+ */
107
+ export function guardEvent(event, hooks) {
108
+ const decision = inspectEvent(event);
109
+ if (decision.action !== "abort") {
110
+ // 'allow' (subscription) and 'skip' (lightweight) continue with no side effect.
111
+ return decision;
112
+ }
113
+ const message = `[billing §10] BLOCKED ${decision.entrypointClass}-class entrypoint "${decision.entrypoint}" ` +
114
+ `on session "${event.sessionId ?? "(unknown)"}": this client bills on API credit, not the ` +
115
+ `Claude subscription. Stopping the session to avoid silent credit billing — the entrypoint is ` +
116
+ `NOT being rewritten (forcing it to 'cli' would be evasion).`;
117
+ hooks.alert(message);
118
+ hooks.stopSession({
119
+ entrypoint: decision.entrypoint,
120
+ sessionId: event.sessionId,
121
+ entrypointClass: decision.entrypointClass,
122
+ });
123
+ return decision;
124
+ }
125
+ // Task 3.1 — startup self-check: fail fast on leaked SDK env BEFORE spawn.
126
+ //
127
+ // `assertCleanBillingEnv` is the POST-CONDITION assertion for the spawn helper's env-sanitize
128
+ // (`buildSanitizedEnv` in engine-pty.ts, which DELETES the billing-sensitive keys). It does NOT
129
+ // delete-and-continue — deletion is the helper's job; this is the read-only check that it worked.
130
+ // Inheriting any of these keys from the parent Node/ACP process would silently bill the spawned
131
+ // TUI as API credit (R1, §10) BEFORE any message event is produced, so this must run BEFORE spawn.
132
+ //
133
+ // SCOPE (the billing subset of engine-pty's FORBIDDEN_BILLING_VARS): the exact key
134
+ // `CLAUDE_CODE_ENTRYPOINT` plus the whole `CLAUDE_AGENT_SDK_*` family. The family is matched by
135
+ // PREFIX (not an enumerated list) on purpose: it covers today's `CLAUDE_AGENT_SDK_VERSION` /
136
+ // `CLAUDE_AGENT_SDK_CLIENT_APP` AND catches any future `CLAUDE_AGENT_SDK_*` sibling the helper's
137
+ // fixed deletion list might miss.
138
+ //
139
+ // `CLAUDECODE` is deliberately NOT checked here. It IS one of engine-pty's FORBIDDEN_BILLING_VARS,
140
+ // but it is the spawn helper's ANTI-RECUSA (anti-nesting) concern — it makes the nested `claude` TUI
141
+ // refuse to start — which is distinct from this BILLING check (§10). Folding it in here would
142
+ // conflate the two guard-rails, so it stays out of the collection logic.
143
+ //
144
+ // PURITY: read-only. Does NOT delete keys and does NOT mutate `env`.
145
+ /**
146
+ * Fail-fast assertion that NO billing-sensitive env key survived into the env about to be passed to
147
+ * spawn (§10 post-condition for {@link buildSanitizedEnv}). Read-only: never deletes or mutates `env`.
148
+ *
149
+ * @param env the spawn env (process-env shape — values may be `undefined`).
150
+ * @throws {Error} if any key is present (value not `undefined`) that is exactly
151
+ * `CLAUDE_CODE_ENTRYPOINT` OR starts with `CLAUDE_AGENT_SDK_`. The message NAMES every leaked key
152
+ * and states the spawn must be refused (these would silently bill the run as API credit — R1, §10).
153
+ * A key set to `undefined` is treated as absent (not a leak). `CLAUDECODE` is NOT checked here (it
154
+ * is the spawn helper's anti-recusa concern, distinct from this billing check — §10).
155
+ */
156
+ export function assertCleanBillingEnv(env) {
157
+ const leaked = Object.keys(env).filter((key) => env[key] !== undefined &&
158
+ (key === "CLAUDE_CODE_ENTRYPOINT" || key.startsWith("CLAUDE_AGENT_SDK_")));
159
+ if (leaked.length === 0) {
160
+ return;
161
+ }
162
+ throw new Error(`[billing §10] REFUSING TO SPAWN: ${leaked.length} billing-sensitive env key(s) leaked into the ` +
163
+ `spawn env: ${leaked.join(", ")}. These would silently bill the spawned TUI as API credit ` +
164
+ `(R1) instead of the Claude subscription. The spawn-env sanitize must strip them BEFORE spawn — ` +
165
+ `aborting rather than launch a credit-billed run.`);
166
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolve an absolute path to an executable subscription `claude` binary.
3
+ *
4
+ * Resolution order:
5
+ * 1. `claude` found on `process.env.PATH` (which-style scan).
6
+ * 2. The documented native-binary path under the vscode extension.
7
+ *
8
+ * @returns absolute path to an executable `claude`.
9
+ * @throws Error naming both attempted locations if neither is executable.
10
+ */
11
+ export declare function resolveClaudePath(): string;
12
+ //# sourceMappingURL=claude-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-path.d.ts","sourceRoot":"","sources":["../src/claude-path.ts"],"names":[],"mappings":"AAsCA;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAyB1C"}
@@ -0,0 +1,61 @@
1
+ // Resolves the subscription `claude` binary from PATH (story 012, R1/R1.2).
2
+ //
3
+ // The fork must drive the user's *subscription* claude (resolved from PATH),
4
+ // NOT the binary embedded in @anthropic-ai/claude-agent-sdk — the SDK-embedded
5
+ // path bills as credit, while the PATH `claude` bills as signature
6
+ // (`entrypoint == 'cli'`, the E1 keystone — experiments/DEGRAU0-RESULTS.md).
7
+ // This helper is ported 1:1 from the E1-validated experiments/lib/claude-path.ts,
8
+ // preserving its resolution order: PATH first, then the documented native-binary
9
+ // fallback (IMPLEMENTACAO-FORK-ACP §17), else throw naming BOTH attempted
10
+ // locations (R1.3).
11
+ //
12
+ // Dependency-free on purpose: only node: builtins (FORK.md pins node-pty + the
13
+ // SDK as the only runtime deps; this adds none).
14
+ import { accessSync, constants } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { delimiter, join } from "node:path";
17
+ // Documented native-binary fallback (IMPLEMENTACAO-FORK-ACP §17), for claude
18
+ // 2.1.159 (frozen base doc anchor, §3). `~` is expanded via os.homedir.
19
+ const FALLBACK_RELATIVE_PATH = join(".vscode", "extensions", "anthropic.claude-code-2.1.159-linux-x64", "resources", "native-binary", "claude");
20
+ /** True if `p` exists and is executable by the current process. */
21
+ function isExecutable(p) {
22
+ try {
23
+ accessSync(p, constants.X_OK);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Resolve an absolute path to an executable subscription `claude` binary.
32
+ *
33
+ * Resolution order:
34
+ * 1. `claude` found on `process.env.PATH` (which-style scan).
35
+ * 2. The documented native-binary path under the vscode extension.
36
+ *
37
+ * @returns absolute path to an executable `claude`.
38
+ * @throws Error naming both attempted locations if neither is executable.
39
+ */
40
+ export function resolveClaudePath() {
41
+ // 1. PATH lookup: scan each PATH entry for an executable `claude`.
42
+ const pathEnv = process.env.PATH ?? "";
43
+ const pathEntries = pathEnv.split(delimiter).filter((entry) => entry.length > 0);
44
+ for (const entry of pathEntries) {
45
+ const candidate = join(entry, "claude");
46
+ if (isExecutable(candidate)) {
47
+ return candidate;
48
+ }
49
+ }
50
+ // 2. Documented native-binary fallback (~ expanded via os.homedir).
51
+ const fallback = join(homedir(), FALLBACK_RELATIVE_PATH);
52
+ if (isExecutable(fallback)) {
53
+ return fallback;
54
+ }
55
+ // 3. Neither exists: throw naming BOTH attempted locations.
56
+ throw new Error(`Could not resolve an executable "claude" binary. Tried: ` +
57
+ `(1) PATH lookup over ${pathEntries.length} entr${pathEntries.length === 1 ? "y" : "ies"} ` +
58
+ `("${pathEnv}"), and ` +
59
+ `(2) the documented native-binary fallback "${fallback}". ` +
60
+ `Install the Claude Code subscription CLI or ensure "claude" is on PATH.`);
61
+ }
@@ -0,0 +1,41 @@
1
+ import type { GetMessages } from "./engine-watcher.js";
2
+ /**
3
+ * Build a `uuid → toolUseResult` index from raw JSONL transcript lines.
4
+ *
5
+ * Only records that ARE plain objects carrying BOTH a string `uuid` AND a defined `toolUseResult`
6
+ * enter the map — a record without a `toolUseResult` is intentionally absent so the matching message is
7
+ * returned unchanged (R1.2). Empty/whitespace lines and malformed JSON are skipped silently (robustness:
8
+ * a torn or partially-written final line must never throw and abort the hydration).
9
+ *
10
+ * @param rawLines the transcript's newline-split lines (READ-ONLY; never mutated).
11
+ * @returns a map from message uuid to its raw `toolUseResult` value (possibly empty).
12
+ */
13
+ export declare function toolUseResultMap(rawLines: string[]): Map<string, unknown>;
14
+ /** Construction options for {@link createDiffEnrichedReader} — both seams injectable for tests. */
15
+ export interface DiffEnrichedReaderOptions {
16
+ /**
17
+ * Locate the session's transcript file(s). Defaults to the in-fork {@link defaultFindTranscript}
18
+ * (a billing-free recursive glob of `<sessionId>.jsonl` under `~/.claude/projects`). First match.
19
+ */
20
+ findTranscript?: (sessionId: string) => string[];
21
+ /**
22
+ * Read a transcript file into its newline-split lines. Defaults to a synchronous
23
+ * `readFileSync(path,'utf8').split('\n')`. Injected so tests can stub the raw read (and simulate an
24
+ * unreadable transcript for the R1.3 fallback) without touching the filesystem.
25
+ */
26
+ readRawLines?: (path: string) => string[];
27
+ }
28
+ /**
29
+ * Wrap a base `getSessionMessages`-shaped reader so each returned message gains its `toolUseResult`
30
+ * from the raw transcript, matched by `uuid`. This is PURE hydration: ordering and identity come from
31
+ * `base`; only the top-level `toolUseResult` key is added (exactly where the diff block reads it,
32
+ * `turn.message.toolUseResult`). Billing-free — the only extra cost is one `fs` read of the transcript
33
+ * located from the `sessionId` (R4.2). On ANY locate/read/parse failure, or when no record carries a
34
+ * `toolUseResult`, the plain reduced messages are returned unchanged (R1.3) and NO patch is fabricated.
35
+ *
36
+ * @param base the underlying reader (the SDK `getSessionMessages`, or an injected stub).
37
+ * @param opts injectable {@link DiffEnrichedReaderOptions} seams (defaults are production behaviour).
38
+ * @returns a {@link GetMessages} that yields the base messages with `toolUseResult` hydrated by uuid.
39
+ */
40
+ export declare function createDiffEnrichedReader(base: GetMessages, opts?: DiffEnrichedReaderOptions): GetMessages;
41
+ //# sourceMappingURL=diff-enriched-reader.d.ts.map