@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,83 @@
1
+ import type { SessionWatcher } from "./engine-lifecycle.js";
2
+ import type { TranscriptTail, TailOptions } from "./jsonl.js";
3
+ /**
4
+ * A conversation message as returned by `getSessionMessages`. We rely ONLY on `.uuid` here (the
5
+ * dedupe key); everything else is forwarded opaque. Typed as an optional `uuid` superset so the
6
+ * injectable seam structurally accepts both the real SDK `SessionMessage` (whose `uuid` is a
7
+ * required `string`) and lightweight test fixtures.
8
+ */
9
+ export type SessionMessage = {
10
+ uuid?: string;
11
+ [k: string]: unknown;
12
+ };
13
+ /**
14
+ * Injectable seam matching the SDK `getSessionMessages` signature (the pure, billing-free reader).
15
+ * May be sync or async — the orchestrator awaits the result either way and then performs the
16
+ * check-add-emit synchronously (see {@link createJsonlWatcher}). Defaults to the SDK function.
17
+ */
18
+ export type GetMessages = (sessionId: string, opts?: {
19
+ dir?: string;
20
+ }) => Promise<SessionMessage[]> | SessionMessage[];
21
+ /** Default debounce window (ms) coalescing a burst of live write signals into one re-read. */
22
+ export declare const DEFAULT_WATCHER_DEBOUNCE_MS = 50;
23
+ /** Construction options for {@link createJsonlWatcher}. */
24
+ export interface JsonlWatcherOptions {
25
+ /** The session id; equals the transcript filename basename. Passed to `getSessionMessages`. */
26
+ sessionId: string;
27
+ /** Resolved transcript path (from Task 1 `locateTranscript`) — the file the tail watches. */
28
+ transcriptPath: string;
29
+ /** Runtime cwd (from Task 1 `readCwdFromInside`) → passed as `{ dir }` to `getSessionMessages`. */
30
+ dir?: string;
31
+ /** Sink: invoked once per NEW (not-yet-seen-by-uuid) message, in `getSessionMessages` order. */
32
+ onEvent: (message: SessionMessage) => void;
33
+ /** Injected for tests; defaults to the SDK `getSessionMessages`. */
34
+ getMessages?: GetMessages;
35
+ /** Injected for tests; defaults to {@link tailTranscript} from ./jsonl.js. */
36
+ tail?: (path: string, opts: TailOptions) => TranscriptTail;
37
+ /** Debounce window (ms) for the live write signal; defaults to {@link DEFAULT_WATCHER_DEBOUNCE_MS}. */
38
+ debounceMs?: number;
39
+ /**
40
+ * Injected for deterministic debounce in tests; returns a CANCEL fn. Defaults to a
41
+ * setTimeout/clearTimeout pair. Tests pass a controllable scheduler (capture fn + manual flush)
42
+ * so the debounce never depends on real timer timing.
43
+ */
44
+ schedule?: (fn: () => void, ms: number) => () => void;
45
+ }
46
+ /**
47
+ * The concrete per-session watcher: it satisfies the story-014 {@link SessionWatcher} seam (so a
48
+ * {@link SessionEngine} can tear it down on cleanup) AND exposes the story-024 end-of-turn hook.
49
+ */
50
+ export interface JsonlWatcher extends SessionWatcher {
51
+ /**
52
+ * Tear down the tail, cancel any pending debounce, and set a stopped flag so an in-flight async
53
+ * re-read does not emit after stop. Idempotent (the {@link SessionWatcher} contract).
54
+ */
55
+ stop(): void;
56
+ /**
57
+ * Story-024 end-of-turn signal: cancel any pending debounce and force ONE final re-read
58
+ * IMMEDIATELY so the just-completed turn is fully ingested. This watcher only CONSUMES the
59
+ * signal — it does NOT detect end-of-turn (that is story 024).
60
+ */
61
+ notifyEndOfTurn(): void;
62
+ }
63
+ /**
64
+ * Create the read-only JSONL tail watcher for one session (story 015, R4 + R5).
65
+ *
66
+ * Wiring: `tailTranscript(transcriptPath, { onRecord: () => scheduleReread() })`. Each complete
67
+ * line the tail forwards is treated PURELY as a debounced write signal — never as the event source.
68
+ * After `debounceMs` of quiet, exactly one re-read runs: it calls `getMessages(sessionId, { dir })`,
69
+ * then SYNCHRONOUSLY (no `await` between messages) walks the returned array and, for each message
70
+ * whose `uuid` is not yet in `seen`, adds it to `seen` and forwards it to `onEvent` — in the SDK's
71
+ * chronological order, unchanged (R5.3). `notifyEndOfTurn()` bypasses the debounce and forces one
72
+ * immediate final re-read.
73
+ *
74
+ * Re-reads are SERIALIZED (never concurrent): a `reading` flag plus a coalesced `pendingReread`
75
+ * flag guarantee that a signal arriving mid-read schedules exactly one more re-read after the
76
+ * current one settles. Because the check-add-emit runs synchronously after the await, exactly-once
77
+ * holds even if re-reads overlap logically (R4.1, R4.2, R5.2).
78
+ *
79
+ * @param opts session id, transcript path, dir, the onEvent sink, and injectable seams.
80
+ * @returns a {@link JsonlWatcher} (a {@link SessionWatcher} plus `notifyEndOfTurn`).
81
+ */
82
+ export declare function createJsonlWatcher(opts: JsonlWatcherOptions): JsonlWatcher;
83
+ //# sourceMappingURL=engine-watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-watcher.d.ts","sourceRoot":"","sources":["../src/engine-watcher.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAErE;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,CACxB,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACpB,OAAO,CAAC,cAAc,EAAE,CAAC,GAAG,cAAc,EAAE,CAAC;AAElD,8FAA8F;AAC9F,eAAO,MAAM,2BAA2B,KAAK,CAAC;AAE9C,2DAA2D;AAC3D,MAAM,WAAW,mBAAmB;IAClC,+FAA+F;IAC/F,SAAS,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,cAAc,EAAE,MAAM,CAAC;IACvB,mGAAmG;IACnG,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC;IAC3C,oEAAoE;IACpE,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,8EAA8E;IAC9E,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,KAAK,cAAc,CAAC;IAC3D,uGAAuG;IACvG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,MAAM,IAAI,CAAC;CACvD;AAED;;;GAGG;AACH,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD;;;OAGG;IACH,IAAI,IAAI,IAAI,CAAC;IACb;;;;OAIG;IACH,eAAe,IAAI,IAAI,CAAC;CACzB;AAQD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,mBAAmB,GAAG,YAAY,CAyH1E"}
@@ -0,0 +1,173 @@
1
+ // === §6 JSONL tail watcher orchestrator (story 015, Task Group 3) =============================
2
+ //
3
+ // BINDING DECISION E5 "REUSE-live" (story.md R5, constraint 1): this watcher does NOT hand-parse
4
+ // `message.content`. It WRAPS the SDK's pure, billing-free reader `getSessionMessages(sessionId,
5
+ // { dir })` and RE-INVOKES it (a) on a debounced write SIGNAL (live streaming) and (b) on a forced
6
+ // final re-read when the story-024 end-of-turn signal fires. There is exactly ONE parser — the
7
+ // SDK's — and we surface its parentUuid-linked chronological order UNCHANGED (R5.3; linearization
8
+ // of sidechains/forks is deferred to story 017).
9
+ //
10
+ // Division of labour:
11
+ // - `tailTranscript` (Task 2, jsonl.ts) is the TRIGGER: its newline-atomic line reader + residue
12
+ // buffer decide *when* a new COMPLETE line has landed. We use each forwarded record only as a
13
+ // write SIGNAL — never as the event source. A torn ~630 KB image append is therefore withheld
14
+ // by the residue buffer until its `\n` arrives, so no signal fires for a half-written line.
15
+ // - `getSessionMessages` is the SOURCE: every re-read returns the full monotonic ordered superset.
16
+ // - the per-`uuid` `Set` is the EMIT-ONCE guard: across re-reads of the growing superset, each
17
+ // message is forwarded to `onEvent` at most once, preserving the SDK array order (R4.1, R4.2).
18
+ //
19
+ // The story-024 end-of-turn detector (terminal `stop_reason` + 200 ms quiescence) is NOT
20
+ // implemented here — this watcher only CONSUMES its signal via `notifyEndOfTurn()` as the
21
+ // forced-final-re-read cadence.
22
+ import { tailTranscript } from "./jsonl.js";
23
+ /** Default debounce window (ms) coalescing a burst of live write signals into one re-read. */
24
+ export const DEFAULT_WATCHER_DEBOUNCE_MS = 50;
25
+ /** Default debounce scheduler: fire `fn` after `ms` via setTimeout; the returned fn clears it. */
26
+ function defaultSchedule(fn, ms) {
27
+ const handle = setTimeout(fn, ms);
28
+ return () => clearTimeout(handle);
29
+ }
30
+ /**
31
+ * Create the read-only JSONL tail watcher for one session (story 015, R4 + R5).
32
+ *
33
+ * Wiring: `tailTranscript(transcriptPath, { onRecord: () => scheduleReread() })`. Each complete
34
+ * line the tail forwards is treated PURELY as a debounced write signal — never as the event source.
35
+ * After `debounceMs` of quiet, exactly one re-read runs: it calls `getMessages(sessionId, { dir })`,
36
+ * then SYNCHRONOUSLY (no `await` between messages) walks the returned array and, for each message
37
+ * whose `uuid` is not yet in `seen`, adds it to `seen` and forwards it to `onEvent` — in the SDK's
38
+ * chronological order, unchanged (R5.3). `notifyEndOfTurn()` bypasses the debounce and forces one
39
+ * immediate final re-read.
40
+ *
41
+ * Re-reads are SERIALIZED (never concurrent): a `reading` flag plus a coalesced `pendingReread`
42
+ * flag guarantee that a signal arriving mid-read schedules exactly one more re-read after the
43
+ * current one settles. Because the check-add-emit runs synchronously after the await, exactly-once
44
+ * holds even if re-reads overlap logically (R4.1, R4.2, R5.2).
45
+ *
46
+ * @param opts session id, transcript path, dir, the onEvent sink, and injectable seams.
47
+ * @returns a {@link JsonlWatcher} (a {@link SessionWatcher} plus `notifyEndOfTurn`).
48
+ */
49
+ export function createJsonlWatcher(opts) {
50
+ const { sessionId, transcriptPath, dir, onEvent, getMessages, tail = tailTranscript, debounceMs = DEFAULT_WATCHER_DEBOUNCE_MS, schedule = defaultSchedule, } = opts;
51
+ // The SINGLE source of events: the SDK reader (or an injected stub). No second parser exists.
52
+ const readMessages = getMessages ?? defaultGetMessages;
53
+ // The emit-once guard. Dedupe key is `message.uuid`; survives across every live re-read so the
54
+ // growing superset re-emits nothing already forwarded (R4.1).
55
+ const seen = new Set();
56
+ // Lifecycle / serialization flags.
57
+ let stopped = false; // set by stop(): suppress any further (incl. in-flight) emits + re-reads.
58
+ let reading = false; // a re-read's async body is in flight — do not start a second concurrently.
59
+ let pendingReread = false; // a signal arrived mid-read — run exactly ONE more re-read after.
60
+ let cancelDebounce; // cancels the armed debounce timer, if any.
61
+ /**
62
+ * Run ONE re-read: await the SDK reader, then SYNCHRONOUSLY (no await between messages) emit each
63
+ * not-yet-seen message in array order. The post-await synchrony is what makes exactly-once hold
64
+ * even if two re-reads were logically requested — only the FIRST `seen.add(uuid)` lets `onEvent`
65
+ * fire for that uuid. Serialized by the `reading` flag; a mid-read signal sets `pendingReread`
66
+ * and we loop once more after settling.
67
+ */
68
+ const runReread = async () => {
69
+ if (stopped)
70
+ return;
71
+ if (reading) {
72
+ // A re-read is already in flight; coalesce this request into a single follow-up pass.
73
+ pendingReread = true;
74
+ return;
75
+ }
76
+ reading = true;
77
+ try {
78
+ do {
79
+ pendingReread = false;
80
+ // The ONLY parse of the transcript — the SDK's pure reader. May be sync or async; await
81
+ // handles both. After this await we must NOT await again before the emit loop, so the
82
+ // check-add-emit stays atomic w.r.t. the microtask queue (no dedupe race across re-reads).
83
+ let messages;
84
+ try {
85
+ messages = await readMessages(sessionId, { dir });
86
+ }
87
+ catch {
88
+ // Transient SDK read failure — SWALLOW it (as the scheduleReread comment promises): emit
89
+ // nothing this cycle, so a failing read never rejects an unhandled promise nor tears the
90
+ // watcher down. A later write/end-of-turn signal triggers a fresh re-read; if a signal
91
+ // arrived mid-read (`pendingReread`), the loop condition retries once, else we settle.
92
+ continue;
93
+ }
94
+ // stop() may have fired during the await — do not emit after teardown (R: stop is final).
95
+ if (stopped)
96
+ return;
97
+ for (const message of messages) {
98
+ const { uuid } = message;
99
+ if (typeof uuid === "string") {
100
+ // Already forwarded on an earlier (sub)superset re-read → skip (R4.1, R4.2).
101
+ if (seen.has(uuid))
102
+ continue;
103
+ seen.add(uuid);
104
+ }
105
+ // No `uuid` (should not happen in-corpus): we cannot dedupe it, so we emit it rather than
106
+ // silently drop a real event. Documented fallback — prefer emit-once-if-keyed over loss.
107
+ onEvent(message);
108
+ }
109
+ // If a write/end-of-turn signal arrived mid-read, `pendingReread` is set: loop ONE more
110
+ // time (coalesced) so we never miss the latest superset, but never run two reads at once.
111
+ } while (pendingReread && !stopped);
112
+ }
113
+ finally {
114
+ reading = false;
115
+ }
116
+ };
117
+ /**
118
+ * Debounced live path: (re)arm the debounce timer; after `debounceMs` of quiet it fires exactly
119
+ * one re-read. Coalesces a burst of write signals into a single read. A no-op once stopped.
120
+ */
121
+ const scheduleReread = () => {
122
+ if (stopped)
123
+ return;
124
+ cancelDebounce?.();
125
+ cancelDebounce = schedule(() => {
126
+ cancelDebounce = undefined;
127
+ // Fire-and-forget: runReread serializes internally; errors are swallowed so a transient SDK
128
+ // read failure never rejects an unhandled promise nor tears the watcher down.
129
+ void runReread();
130
+ }, debounceMs);
131
+ };
132
+ // Wire the tail. Each complete-line record is ONLY a write signal (its payload is unused as the
133
+ // event source) — the event source is `getMessages`. The tail's residue buffer guarantees a torn
134
+ // append fires no signal until its `\n` lands (so no re-read sees a half-written line).
135
+ const transcriptTail = tail(transcriptPath, {
136
+ onRecord: () => scheduleReread(),
137
+ });
138
+ return {
139
+ notifyEndOfTurn() {
140
+ if (stopped)
141
+ return;
142
+ // Forced FINAL re-read: bypass the debounce (cancel any pending live timer) and read NOW so
143
+ // the just-completed turn is fully ingested. runReread serializes with any in-flight read.
144
+ cancelDebounce?.();
145
+ cancelDebounce = undefined;
146
+ void runReread();
147
+ },
148
+ stop() {
149
+ if (stopped)
150
+ return; // idempotent (SessionWatcher contract).
151
+ stopped = true; // suppress emits from any in-flight async re-read past this point.
152
+ cancelDebounce?.();
153
+ cancelDebounce = undefined;
154
+ pendingReread = false;
155
+ try {
156
+ transcriptTail.stop();
157
+ }
158
+ catch {
159
+ /* tail already stopped / transient — ignore */
160
+ }
161
+ },
162
+ };
163
+ }
164
+ /**
165
+ * Default {@link GetMessages}: the SDK's pure, billing-free `getSessionMessages`. Imported lazily
166
+ * (dynamic import) so this module — and the deterministic unit tests, which always inject a stub —
167
+ * never force-loads the SDK at module-eval time. Returns `[]` if the SDK import fails (defer rather
168
+ * than throw); in production the seam is the real SDK function.
169
+ */
170
+ async function defaultGetMessages(sessionId, opts) {
171
+ const sdk = await import("@anthropic-ai/claude-agent-sdk");
172
+ return sdk.getSessionMessages(sessionId, opts);
173
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The engine boundary the ACP layer delegates session/turn work to. The methods
3
+ * mirror the CUT symbols in SEAM-MAP.md (`createSession`, `prompt` → 023). The
4
+ * real implementation (PTY spawn + JSONL tail) arrives in 013–015/023; story 011
5
+ * ships only this contract plus the no-op stub below. Params/returns are `unknown`
6
+ * on purpose — the concrete shapes are defined by the 023 rewrite, not pinned here.
7
+ */
8
+ export interface Engine {
9
+ /** Discriminator for diagnostics/tests (e.g. "stub" vs the future "pty"). */
10
+ readonly kind: string;
11
+ /** Create an engine-backed session. CUT in 011 (→ 023). */
12
+ createSession(params: unknown): Promise<unknown>;
13
+ /** Run one prompt turn. CUT in 011 (→ 023). */
14
+ prompt(params: unknown): Promise<unknown>;
15
+ }
16
+ /** Error message thrown when 011's READ-ONLY stub is asked to do real engine work. */
17
+ export declare const ENGINE_NOT_IMPLEMENTED_011 = "engine not implemented in 011 (read-only Degrau 1)";
18
+ /**
19
+ * No-op engine: boots cleanly, performs no SDK/PTY work, and rejects any real engine
20
+ * call with a legible error. Temporary scaffolding — replaced by the PTY engine in
21
+ * stories 013–015/023.
22
+ */
23
+ export declare class StubEngine implements Engine {
24
+ readonly kind = "stub";
25
+ createSession(): Promise<never>;
26
+ prompt(): Promise<never>;
27
+ }
28
+ /** Factory for the default story-011 engine (the no-op stub). */
29
+ export declare function createStubEngine(): Engine;
30
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,MAAM,WAAW,MAAM;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,+CAA+C;IAC/C,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3C;AAED,sFAAsF;AACtF,eAAO,MAAM,0BAA0B,uDAAuD,CAAC;AAE/F;;;;GAIG;AACH,qBAAa,UAAW,YAAW,MAAM;IACvC,QAAQ,CAAC,IAAI,UAAU;IAEjB,aAAa,IAAI,OAAO,CAAC,KAAK,CAAC;IAI/B,MAAM,IAAI,OAAO,CAAC,KAAK,CAAC;CAG/B;AAED,iEAAiE;AACjE,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
package/dist/engine.js ADDED
@@ -0,0 +1,34 @@
1
+ // === SEAM(011): engine boundary — temporary no-op stub ===
2
+ // Story 011 inserts a TEMPORARY no-op engine at the reuse/rewrite seam mapped by
3
+ // story 010 (SEAM-MAP.md): the SDK-coupled `createSession()` + the ~590-line
4
+ // `prompt()` loop are CUT (→ story 023, the core rewrite). This module lets the ACP
5
+ // agent boot end-to-end WITHOUT the credit-billing SDK `query()` path while the real
6
+ // PTY + JSONL-tail engine is built in stories 013–015/023.
7
+ //
8
+ // It imports NOTHING — in particular nothing from `@anthropic-ai/claude-agent-sdk`
9
+ // — so no billed path is reachable through the engine seam (story 011 Task 2.2).
10
+ // Degrau 1 is READ-ONLY: `session/prompt` is not wired ACP-side, so the stub's work
11
+ // methods are never reached in normal operation; if they ever are, they reject
12
+ // loudly rather than silently no-op, keeping failures legible.
13
+ /** Error message thrown when 011's READ-ONLY stub is asked to do real engine work. */
14
+ export const ENGINE_NOT_IMPLEMENTED_011 = "engine not implemented in 011 (read-only Degrau 1)";
15
+ /**
16
+ * No-op engine: boots cleanly, performs no SDK/PTY work, and rejects any real engine
17
+ * call with a legible error. Temporary scaffolding — replaced by the PTY engine in
18
+ * stories 013–015/023.
19
+ */
20
+ export class StubEngine {
21
+ constructor() {
22
+ this.kind = "stub";
23
+ }
24
+ async createSession() {
25
+ throw new Error(ENGINE_NOT_IMPLEMENTED_011);
26
+ }
27
+ async prompt() {
28
+ throw new Error(ENGINE_NOT_IMPLEMENTED_011);
29
+ }
30
+ }
31
+ /** Factory for the default story-011 engine (the no-op stub). */
32
+ export function createStubEngine() {
33
+ return new StubEngine();
34
+ }
@@ -0,0 +1,164 @@
1
+ import type { JsonlEvent } from "./jsonl.js";
2
+ /** A §6 `text` block — the only block {@link normaliseContent} ever synthesises (from a raw string). */
3
+ export interface TextBlock {
4
+ type: "text";
5
+ text: string;
6
+ }
7
+ /** A §6 `thinking` block. `signature` is optional (present on signed extended-thinking blocks). */
8
+ export interface ThinkingBlock {
9
+ type: "thinking";
10
+ thinking: string;
11
+ signature?: string;
12
+ }
13
+ /**
14
+ * A §6 `tool_use` block. `caller` is the §6 extra story 019 reads to attribute the call; it is
15
+ * PRESERVED here (never dropped) and OPTIONAL — its absence is the normal case, not drift.
16
+ */
17
+ export interface ToolUseBlock {
18
+ type: "tool_use";
19
+ id?: string;
20
+ name?: string;
21
+ input?: unknown;
22
+ caller?: unknown;
23
+ }
24
+ /**
25
+ * A §6 `tool_result` block. Two §6 facts story 019 depends on are modelled here:
26
+ * - `content` is a raw STRING **or** an array of {@link ContentBlock}s — story 016 preserves
27
+ * whichever form arrives UNTRANSFORMED (the nested `tool_reference` sub-block lives inside the
28
+ * array form). Story 019's unmodified `toolUpdateFromToolResult` consumes both forms.
29
+ * - `is_error` may be `null` (not just `boolean`), per §6/§7.
30
+ * `tool_reference` is the §6 extra that may also appear; it is PRESERVED and OPTIONAL.
31
+ */
32
+ export interface ToolResultBlock {
33
+ type: "tool_result";
34
+ tool_use_id?: string;
35
+ content?: string | ContentBlock[];
36
+ is_error?: boolean | null;
37
+ tool_reference?: unknown;
38
+ }
39
+ /** A §6 `image` block. `source` is preserved opaquely (story 019 owns image handling). */
40
+ export interface ImageBlock {
41
+ type: "image";
42
+ source?: unknown;
43
+ }
44
+ /**
45
+ * The §6 content-block union story 016 carries through {@link normaliseContent} to the §7
46
+ * translators. The trailing catch-all member keeps the union OPEN: any `{ type, ... }` object is
47
+ * structurally assignable, so the array form of `message.content` passes through as
48
+ * {@link ContentBlock}[] without losing unmodelled blocks or arbitrary extra fields (R6).
49
+ */
50
+ export type ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlock | ImageBlock | {
51
+ type: string;
52
+ [key: string]: unknown;
53
+ };
54
+ /**
55
+ * A drift-telemetry record handed to {@link ClassifyOptions.onDrift}. Two flavours, discriminated by
56
+ * {@link DriftRecord.kind}:
57
+ * - `"unknown-type"` — `event.type` is NOT a documented §6 type; it reached the DEFAULT branch and
58
+ * was logged-and-ignored (no routing, no SessionUpdate). `fields` carries any unrecognised
59
+ * top-level keys on the event (possibly empty).
60
+ * - `"unknown-fields"` — a KNOWN (routed) type carried unexpected top-level field(s); `fields`
61
+ * names them. Routing is UNCHANGED — this is observational only.
62
+ * Telemetry is billing-free and side-effect-free beyond logging.
63
+ */
64
+ export interface DriftRecord {
65
+ /** Which flavour of drift this is — an unknown `.type`, or unknown field(s) on a known type. */
66
+ kind: "unknown-type" | "unknown-fields";
67
+ /** The event type — the unrecognised type (unknown-type) or the routed type (unknown-fields). */
68
+ type: string;
69
+ /** The unrecognised top-level keys on the event (possibly empty for an unknown-type with none). */
70
+ fields: string[];
71
+ /** The event's `version` when present (e.g. the `claude` version that introduced the drift). */
72
+ version?: string;
73
+ /** The source event that drifted, carried through for downstream diagnostics. */
74
+ event?: JsonlEvent;
75
+ }
76
+ /** Options for {@link classifyEvent}. */
77
+ export interface ClassifyOptions {
78
+ /**
79
+ * Injectable drift sink — invoked (a) when an event's `.type` is NOT a documented type and falls
80
+ * through to the DEFAULT branch (an `unknown-type` record), and (b) when a KNOWN, routed type
81
+ * carries unexpected top-level field(s) (an `unknown-fields` record). The injectable-seam pattern
82
+ * mirrors ./jsonl.ts so the telemetry is unit-testable without globals.
83
+ *
84
+ * When omitted, drift is logged via {@link defaultDriftSink} to STDERR (never stdout — stdout is
85
+ * the ACP protocol channel). A fully-documented known event emits NOTHING.
86
+ */
87
+ onDrift?: (record: DriftRecord) => void;
88
+ }
89
+ /** The two content-bearing roles — the only `type`s routed to the §7 translator content path. */
90
+ type ContentType = "assistant" | "user";
91
+ /**
92
+ * Classification of a content-bearing event (`assistant` / `user`). Routed to the content-block
93
+ * dispatch path consumed by the §7 translators.
94
+ */
95
+ export interface ContentClassification {
96
+ class: "content";
97
+ /** The content-bearing type, echoed for the consumer. */
98
+ type: ContentType;
99
+ /** The message role — equal to {@link ContentClassification.type} for these two types. */
100
+ role: ContentType;
101
+ /**
102
+ * The NORMALISED content-block array (Task 1.3): `message.content` verbatim when it is an array,
103
+ * a raw string wrapped as `[{ type: 'text', text }]`, else `[]`. Always a real array so the §7
104
+ * translators can `.map`/index without tripping on the string form. Typed as {@link ContentBlock}[]
105
+ * (Task 1.4) so stories 018/019 read modelled blocks; the open catch-all union member keeps the §6
106
+ * extras (`tool_use.caller`, `tool_result.tool_reference`) and any unmodelled block surviving.
107
+ */
108
+ content: ContentBlock[];
109
+ /** The source typed event, carried through (universal fields preserved for downstream §7). */
110
+ event: JsonlEvent;
111
+ }
112
+ /**
113
+ * Classification of a recognised lifecycle/metadata event — one of the 13 documented non-content
114
+ * types. Recognised and classified, but it NEVER reaches a content translator.
115
+ */
116
+ export interface LifecycleClassification {
117
+ class: "lifecycle";
118
+ /** The lifecycle/metadata type, echoed for the consumer. */
119
+ type: string;
120
+ /** The source typed event, carried through. */
121
+ event: JsonlEvent;
122
+ }
123
+ /**
124
+ * Classification of an UNRECOGNISED event — one whose `.type` is not documented and fell through to
125
+ * the DEFAULT branch. The DEFAULT arm emits an `unknown-type` drift record through `onDrift` before
126
+ * returning this marker; the marker itself carries NO content, so it produces no ACP SessionUpdate.
127
+ */
128
+ export interface DriftClassification {
129
+ class: "drift";
130
+ /** The unrecognised type, echoed for the consumer. */
131
+ type: string;
132
+ /** The source typed event, carried through. */
133
+ event: JsonlEvent;
134
+ }
135
+ /** Discriminated union over the `class` field — the result of {@link classifyEvent}. */
136
+ export type EventClassification = ContentClassification | LifecycleClassification | DriftClassification;
137
+ /**
138
+ * Route a story-015 typed {@link JsonlEvent} by its `.type` to the appropriate classification.
139
+ *
140
+ * The two content-bearing types are dispatched to the content-block path consumed by the §7
141
+ * translators; the 13 documented lifecycle/metadata types (`system`, `result`, `started`, `mode`,
142
+ * `permission-mode`, `file-history-snapshot`, `attachment`, `queue-operation`, `last-prompt`,
143
+ * `ai-title`, `pr-link`, `agent-name`, and the post-`/compact` `summary`) are classified as
144
+ * recognised, NON-content events that neither crash nor reach a content translator. Every documented
145
+ * type has its OWN explicit `case` arm (no membership check) so drift against the DEFAULT branch is
146
+ * observable.
147
+ *
148
+ * No custom parsing is performed — the event already arrives typed from story 015's `projectEvent`;
149
+ * this function ONLY dispatches on `event.type`.
150
+ *
151
+ * Drift telemetry (R1.2, R3, R6): an UNRECOGNISED `.type` is a LOGGED, NON-FATAL drop — it emits an
152
+ * `unknown-type` record through `onDrift`, then returns a `drift` classification WITHOUT throwing and
153
+ * WITHOUT producing any ACP SessionUpdate. A KNOWN, routed type carrying unexpected top-level field(s)
154
+ * additionally emits an `unknown-fields` record (its routing is UNCHANGED). When `opts.onDrift` is
155
+ * omitted, drift is logged to STDERR via {@link defaultDriftSink}. Telemetry is billing-free and
156
+ * side-effect-free beyond logging, so the read path never assumes a fixed `.type` set.
157
+ *
158
+ * @param event the typed event projected by story 015's `projectEvent`.
159
+ * @param opts injectable `onDrift` seam; defaults to the stderr {@link defaultDriftSink}.
160
+ * @returns the {@link EventClassification} for the event.
161
+ */
162
+ export declare function classifyEvent(event: JsonlEvent, opts?: ClassifyOptions): EventClassification;
163
+ export {};
164
+ //# sourceMappingURL=event-switch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-switch.d.ts","sourceRoot":"","sources":["../src/event-switch.ts"],"names":[],"mappings":"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"}