@adhdev/daemon-core 0.9.82-rc.15 → 0.9.82-rc.151

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 (182) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +5 -1
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +104 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
  11. package/dist/commands/handler.d.ts +66 -0
  12. package/dist/commands/router.d.ts +22 -0
  13. package/dist/config/chat-history.d.ts +5 -0
  14. package/dist/config/config.d.ts +5 -0
  15. package/dist/config/mesh-config.d.ts +68 -1
  16. package/dist/git/git-commands.d.ts +5 -1
  17. package/dist/index.d.ts +20 -6
  18. package/dist/index.js +13153 -3031
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +13099 -3023
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/installer.d.ts +1 -4
  23. package/dist/ipc/local-ipc-server.d.ts +91 -0
  24. package/dist/launch.d.ts +1 -1
  25. package/dist/logging/async-batch-writer.d.ts +10 -0
  26. package/dist/mesh/beads-db.d.ts +72 -0
  27. package/dist/mesh/contracts.d.ts +164 -0
  28. package/dist/mesh/coordinator-registry.d.ts +25 -0
  29. package/dist/mesh/mesh-active-work.d.ts +90 -0
  30. package/dist/mesh/mesh-events.d.ts +77 -5
  31. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  32. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  33. package/dist/mesh/mesh-ledger.d.ts +58 -1
  34. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  35. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  36. package/dist/mesh/preview-freshness.d.ts +18 -0
  37. package/dist/mesh/refine-config.d.ts +193 -0
  38. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  39. package/dist/providers/approval-utils.d.ts +9 -0
  40. package/dist/providers/cli-provider-instance.d.ts +6 -1
  41. package/dist/providers/contracts.d.ts +50 -5
  42. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  43. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  44. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  45. package/dist/providers/native-history/index.d.ts +11 -0
  46. package/dist/providers/provider-loader.d.ts +19 -1
  47. package/dist/providers/read-chat-contract.d.ts +29 -0
  48. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  49. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  50. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  51. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
  52. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  53. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  54. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  55. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  56. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  57. package/dist/providers/sdk/v1/index.d.ts +30 -0
  58. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  59. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  60. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  61. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  62. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  63. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  64. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  65. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  66. package/dist/providers/transcript-v2.d.ts +176 -0
  67. package/dist/repo-mesh-types.d.ts +67 -0
  68. package/dist/shared-types.d.ts +12 -0
  69. package/dist/status/reporter.d.ts +2 -0
  70. package/dist/status/snapshot.d.ts +1 -0
  71. package/dist/types.d.ts +5 -0
  72. package/package.json +6 -2
  73. package/src/boot/daemon-lifecycle.ts +17 -10
  74. package/src/boot/process-hardening.ts +89 -0
  75. package/src/chat/source-machine.ts +534 -0
  76. package/src/chat/source-resolver.ts +0 -0
  77. package/src/chat/subscription-updates.ts +14 -1
  78. package/src/cli-adapter-types.d.ts +1 -0
  79. package/src/cli-adapter-types.ts +3 -1
  80. package/src/cli-adapters/cli-script-runner.ts +421 -0
  81. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  82. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  83. package/src/cli-adapters/provider-cli-adapter.ts +664 -1137
  84. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  85. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  86. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  87. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  88. package/src/cli-adapters/provider-cli-shared.ts +57 -11
  89. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  90. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  91. package/src/commands/chat-commands.ts +1451 -52
  92. package/src/commands/cli-manager.ts +184 -3
  93. package/src/commands/handler.ts +547 -2
  94. package/src/commands/mesh-coordinator.ts +13 -143
  95. package/src/commands/router.ts +3273 -428
  96. package/src/config/chat-history.ts +79 -24
  97. package/src/config/config.ts +12 -0
  98. package/src/config/mesh-config.ts +249 -2
  99. package/src/config/recent-activity.ts +8 -2
  100. package/src/daemon/dev-cli-debug.ts +10 -1
  101. package/src/detection/ide-detector.ts +26 -16
  102. package/src/git/git-commands.ts +17 -5
  103. package/src/git/git-worktree.ts +8 -1
  104. package/src/index.ts +72 -5
  105. package/src/installer.d.ts +1 -1
  106. package/src/installer.ts +8 -6
  107. package/src/ipc/local-ipc-server.ts +278 -0
  108. package/src/launch.d.ts +1 -1
  109. package/src/launch.ts +37 -28
  110. package/src/logging/async-batch-writer.ts +55 -0
  111. package/src/logging/logger.ts +2 -1
  112. package/src/mesh/beads-db.ts +479 -0
  113. package/src/mesh/contracts.ts +329 -0
  114. package/src/mesh/coordinator-prompt.ts +40 -22
  115. package/src/mesh/coordinator-registry.ts +75 -0
  116. package/src/mesh/mesh-active-work.ts +437 -0
  117. package/src/mesh/mesh-events.ts +820 -61
  118. package/src/mesh/mesh-fast-forward.ts +430 -0
  119. package/src/mesh/mesh-host-ownership.ts +73 -0
  120. package/src/mesh/mesh-ledger.ts +457 -104
  121. package/src/mesh/mesh-refine-status.ts +144 -0
  122. package/src/mesh/mesh-work-queue.ts +216 -158
  123. package/src/mesh/preview-freshness.ts +118 -0
  124. package/src/mesh/refine-config.ts +366 -0
  125. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  126. package/src/providers/approval-utils.ts +39 -5
  127. package/src/providers/chat-message-normalization.ts +4 -11
  128. package/src/providers/cli-provider-instance.ts +380 -44
  129. package/src/providers/contracts.ts +52 -5
  130. package/src/providers/ide-provider-instance.ts +17 -3
  131. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  132. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  133. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  134. package/src/providers/native-history/index.ts +23 -0
  135. package/src/providers/provider-loader.ts +294 -28
  136. package/src/providers/provider-schema.ts +31 -13
  137. package/src/providers/read-chat-contract.ts +76 -16
  138. package/src/providers/sdk/README.md +49 -0
  139. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  140. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  141. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  142. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
  143. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  144. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  145. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  146. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  147. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  148. package/src/providers/sdk/v1/index.ts +152 -0
  149. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  150. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  151. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  152. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +444 -0
  153. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  154. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  155. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  156. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  157. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  158. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  159. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  160. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  161. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
  162. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  163. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  164. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  165. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  166. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  167. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  168. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  169. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  170. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  171. package/src/providers/sdk/v1/validators/index.ts +19 -0
  172. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  173. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  174. package/src/providers/transcript-v2.ts +567 -0
  175. package/src/providers/version-archive.ts +38 -20
  176. package/src/repo-mesh-types.ts +77 -0
  177. package/src/shared-types.ts +9 -0
  178. package/src/status/builders.ts +23 -6
  179. package/src/status/reporter.ts +15 -0
  180. package/src/status/snapshot.ts +35 -11
  181. package/src/system/host-memory.ts +29 -12
  182. package/src/types.ts +5 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * buildParseApprovalFromSquash
3
+ *
4
+ * Companion to buildParseApprovalFromTui. Handles the squashed-modal case:
5
+ * the terminal has re-flowed the approval frame so the rendered text is a
6
+ * single run of lowercase-alnum bytes (e.g. `doyoutrustthecontentsofthisdirectory…1yescontinue2noquit`).
7
+ *
8
+ * Author declares:
9
+ * - `cues` — compact substrings/regexes that identify the modal
10
+ * - `footers` — optional compact substrings that must also be present
11
+ * - `buttonRules` — compact→labels mappings used to recover button labels
12
+ *
13
+ * Returns the same shape as buildParseApprovalFromTui — a CliApprovalModal
14
+ * (message + buttons) or null when no rule matches. Caller composes the two
15
+ * builders: try the regular modal first, then fall back to squash. Codex
16
+ * needs both because at narrow widths the terminal renders the squash form,
17
+ * and at wider widths it renders the regular framed form.
18
+ */
19
+
20
+ import type {
21
+ CliApprovalInput,
22
+ CliApprovalModal,
23
+ CliParseApprovalFn,
24
+ } from '../../types/cli/index.js';
25
+
26
+ // ─── Spec mirrors the JSON schema ─────────────────────────────────────
27
+
28
+ interface SquashCueSpec {
29
+ compact: string;
30
+ kind?: 'substring' | 'regex';
31
+ label?: string;
32
+ messageWhenMatched?: string;
33
+ }
34
+
35
+ interface SquashFooterSpec {
36
+ compact: string;
37
+ kind?: 'substring' | 'regex';
38
+ }
39
+
40
+ interface SquashButtonRuleSpec {
41
+ compact: string;
42
+ kind?: 'substring' | 'regex';
43
+ labels: string[];
44
+ }
45
+
46
+ export interface ApprovalSquashSpec {
47
+ $schema: 'adhdev:tui/approval-squash@1';
48
+ cues: SquashCueSpec[];
49
+ footers?: SquashFooterSpec[];
50
+ buttonRules: SquashButtonRuleSpec[];
51
+ scope?: 'tail-window' | 'whole-screen';
52
+ tailWindowLines?: number;
53
+ }
54
+
55
+ // ─── Internals ────────────────────────────────────────────────────────
56
+
57
+ const COMPACT_RE = /[^a-z0-9]+/g;
58
+
59
+ /**
60
+ * The canonical normalisation. Identical to codex-cli's `compactText`:
61
+ * lowercase, then strip everything that is not a-z or 0-9.
62
+ *
63
+ * Exported for tests and for the (rare) provider that wants to compose this
64
+ * with extra normalisation steps.
65
+ */
66
+ export function compactText(value: string): string {
67
+ return String(value || '').toLowerCase().replace(COMPACT_RE, '');
68
+ }
69
+
70
+ function compileRegex(source: string, flags = 'i'): RegExp {
71
+ try { return new RegExp(source, flags); }
72
+ catch (e) { throw new Error(`Invalid regex /${source}/${flags}: ${(e as Error).message}`); }
73
+ }
74
+
75
+ function matchSubstringOrRegex(
76
+ needle: string,
77
+ kind: 'substring' | 'regex' | undefined,
78
+ haystack: string,
79
+ ): boolean {
80
+ if (kind === 'regex') return compileRegex(needle).test(haystack);
81
+ return haystack.includes(needle);
82
+ }
83
+
84
+ function scopeText(spec: ApprovalSquashSpec, input: CliApprovalInput): string {
85
+ const scope = spec.scope ?? 'tail-window';
86
+ const candidates = [
87
+ String(input.screenText || ''),
88
+ String(input.rawBuffer || ''),
89
+ String(input.buffer || ''),
90
+ String(input.tail || ''),
91
+ ].filter(Boolean);
92
+ const joined = candidates.join('\n');
93
+ if (scope === 'whole-screen') return joined;
94
+ const tailLines = spec.tailWindowLines ?? 24;
95
+ return joined.split('\n').slice(-tailLines).join('\n');
96
+ }
97
+
98
+ // ─── Public builder ───────────────────────────────────────────────────
99
+
100
+ export function buildParseApprovalFromSquash(spec: ApprovalSquashSpec): CliParseApprovalFn {
101
+ if (!Array.isArray(spec.cues) || spec.cues.length === 0) {
102
+ throw new Error('approval-squash spec must declare at least one cue.');
103
+ }
104
+ if (!Array.isArray(spec.buttonRules) || spec.buttonRules.length === 0) {
105
+ throw new Error('approval-squash spec must declare at least one button rule.');
106
+ }
107
+
108
+ return function parseApproval(input: CliApprovalInput): CliApprovalModal | null {
109
+ const text = scopeText(spec, input);
110
+ if (!text) return null;
111
+ const compact = compactText(text);
112
+ if (!compact) return null;
113
+
114
+ // 1) Find the first matching cue. Its messageWhenMatched (if present) is
115
+ // the human-readable modal message; otherwise we fall back to a label
116
+ // or empty string.
117
+ let matchedCue: SquashCueSpec | null = null;
118
+ for (const cue of spec.cues) {
119
+ if (matchSubstringOrRegex(cue.compact, cue.kind, compact)) {
120
+ matchedCue = cue;
121
+ break;
122
+ }
123
+ }
124
+ if (!matchedCue) return null;
125
+
126
+ // 2) If footers are declared, at least one must also match.
127
+ if (spec.footers && spec.footers.length > 0) {
128
+ const anyFooter = spec.footers.some((f) =>
129
+ matchSubstringOrRegex(f.compact, f.kind, compact),
130
+ );
131
+ if (!anyFooter) return null;
132
+ }
133
+
134
+ // 3) Find a button rule whose compact pattern is present in the compacted
135
+ // text. First match wins.
136
+ let labels: string[] | null = null;
137
+ for (const rule of spec.buttonRules) {
138
+ if (matchSubstringOrRegex(rule.compact, rule.kind, compact)) {
139
+ labels = rule.labels;
140
+ break;
141
+ }
142
+ }
143
+ if (!labels || labels.length === 0) return null;
144
+
145
+ const message = matchedCue.messageWhenMatched
146
+ || matchedCue.label
147
+ || '';
148
+
149
+ return { message, buttons: [...labels] };
150
+ };
151
+ }
152
+
153
+ // Internal exports for tests.
154
+ export const __internal = {
155
+ compactText,
156
+ matchSubstringOrRegex,
157
+ scopeText,
158
+ };
@@ -0,0 +1,245 @@
1
+ /**
2
+ * buildParseApprovalFromTui
3
+ *
4
+ * Turns a `tui/modal@1` block into a runtime
5
+ * `(input: CliApprovalInput) => CliApprovalModal | null` function.
6
+ *
7
+ * Contract rules enforced (sprint-2026-06):
8
+ * - Returns null when the question pattern is not visible.
9
+ * - Returns null when the button list is empty or shorter than minButtons.
10
+ * - Button labels are raw text; never rewritten (e.g. "Yes, and don't ask
11
+ * again for X" stays verbatim).
12
+ * - Modal scope respects between-last-two-separators / window-around-question
13
+ * / whole-screen to avoid false-positives from numbered lists in
14
+ * assistant prose.
15
+ */
16
+
17
+ import type {
18
+ CliApprovalInput,
19
+ CliApprovalModal,
20
+ CliParseApprovalFn,
21
+ } from '../../types/cli/index.js';
22
+ import {
23
+ applyVisibleRegion,
24
+ type VisibleRegionSpec,
25
+ } from './visible-region.js';
26
+
27
+ // ─── Spec shapes ───────────────────────────────────────────────────────
28
+
29
+ interface ModalQuestionVariant {
30
+ regex: string;
31
+ flags?: string;
32
+ label?: string;
33
+ }
34
+
35
+ interface ModalContextHeader {
36
+ regex: string;
37
+ flags?: string;
38
+ }
39
+
40
+ export interface ModalTuiSpec {
41
+ $schema: 'adhdev:tui/modal@1';
42
+ questionPattern: string;
43
+ questionFlags?: string;
44
+ questionVariants?: ModalQuestionVariant[];
45
+ buttonPattern: string;
46
+ buttonFlags?: string;
47
+ /**
48
+ * Optional fallback for terminals that render all options on a single line
49
+ * (e.g. Antigravity feedback survey: `[0] skip [1] yes [2] no [3] still using`).
50
+ * When per-line button extraction collects fewer than `minButtons`, the
51
+ * builder makes a second pass with this regex (global match, capture group 1
52
+ * is the label).
53
+ */
54
+ inlineButtonPattern?: string;
55
+ inlineButtonFlags?: string;
56
+ scope?: 'between-last-two-separators' | 'window-around-question' | 'whole-screen';
57
+ scopeWindowLines?: number;
58
+ contextHeader?: ModalContextHeader;
59
+ continuationLines?: boolean;
60
+ minButtons?: number;
61
+ }
62
+
63
+ // ─── Helpers ───────────────────────────────────────────────────────────
64
+
65
+ const SEPARATOR_RE = /^(?:─|━|═|━){10,}\s*$/;
66
+
67
+ function compile(re: string, flags?: string): RegExp {
68
+ try {
69
+ return new RegExp(re, flags);
70
+ } catch (e) {
71
+ throw new Error(`Invalid regex /${re}/${flags ?? ''}: ${(e as Error).message}`);
72
+ }
73
+ }
74
+
75
+ function findQuestionLineIndex(
76
+ spec: ModalTuiSpec,
77
+ lines: string[],
78
+ ): { index: number; matchedSource: 'primary' | string } | null {
79
+ const primary = compile(spec.questionPattern, spec.questionFlags ?? 'i');
80
+ // Search bottom-up to prefer the most recent modal.
81
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
82
+ if (primary.test(lines[i])) return { index: i, matchedSource: 'primary' };
83
+ }
84
+ for (const variant of spec.questionVariants ?? []) {
85
+ const re = compile(variant.regex, variant.flags ?? 'i');
86
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
87
+ if (re.test(lines[i])) return { index: i, matchedSource: variant.label ?? 'variant' };
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function scopeLines(
94
+ spec: ModalTuiSpec,
95
+ lines: string[],
96
+ questionIndex: number,
97
+ ): { start: number; end: number } {
98
+ const scope = spec.scope ?? 'between-last-two-separators';
99
+ if (scope === 'whole-screen') {
100
+ return { start: 0, end: lines.length };
101
+ }
102
+ if (scope === 'window-around-question') {
103
+ const window = spec.scopeWindowLines ?? 16;
104
+ return {
105
+ start: Math.max(0, questionIndex - 2),
106
+ end: Math.min(lines.length, questionIndex + window),
107
+ };
108
+ }
109
+ // between-last-two-separators
110
+ let lastSep = -1;
111
+ let prevSep = -1;
112
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
113
+ if (SEPARATOR_RE.test(lines[i].trim())) {
114
+ if (lastSep < 0) lastSep = i;
115
+ else if (prevSep < 0) {
116
+ prevSep = i;
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ if (lastSep >= 0 && prevSep >= 0) {
122
+ return { start: prevSep, end: lastSep + 1 };
123
+ }
124
+ // Fallback: window around question line.
125
+ return {
126
+ start: Math.max(0, questionIndex - 4),
127
+ end: Math.min(lines.length, questionIndex + 16),
128
+ };
129
+ }
130
+
131
+ function extractButtons(
132
+ spec: ModalTuiSpec,
133
+ lines: string[],
134
+ windowStart: number,
135
+ windowEnd: number,
136
+ ): string[] {
137
+ const buttonRe = compile(spec.buttonPattern, spec.buttonFlags ?? 'm');
138
+ const out: string[] = [];
139
+ let i = windowStart;
140
+ while (i < windowEnd) {
141
+ const line = lines[i];
142
+ const m = buttonRe.exec(line);
143
+ if (m && m[1]) {
144
+ let label = m[1].trim();
145
+ // Continuation lines: when enabled, append indented lines below until
146
+ // the next button or blank.
147
+ if (spec.continuationLines) {
148
+ let j = i + 1;
149
+ while (j < windowEnd) {
150
+ const next = lines[j];
151
+ if (!next.trim()) break;
152
+ if (buttonRe.test(next)) break;
153
+ // continuation must be indented (not at column 0 like a new section).
154
+ if (!/^\s+/.test(next)) break;
155
+ label += ' ' + next.trim();
156
+ j += 1;
157
+ }
158
+ i = j;
159
+ } else {
160
+ i += 1;
161
+ }
162
+ out.push(label);
163
+ } else {
164
+ i += 1;
165
+ }
166
+ }
167
+ return out;
168
+ }
169
+
170
+ function buildMessage(
171
+ spec: ModalTuiSpec,
172
+ lines: string[],
173
+ questionIndex: number,
174
+ windowStart: number,
175
+ windowEnd: number,
176
+ ): string {
177
+ const messageParts: string[] = [];
178
+ if (spec.contextHeader) {
179
+ const ctxRe = compile(spec.contextHeader.regex, (spec.contextHeader.flags ?? 'i') + 'm');
180
+ const haystack = lines.slice(windowStart, windowEnd).join('\n');
181
+ const m = ctxRe.exec(haystack);
182
+ if (m) messageParts.push(m[1] ? m[1].trim() : m[0].trim());
183
+ }
184
+ messageParts.push(lines[questionIndex].trim());
185
+ return messageParts.filter(Boolean).join(' — ');
186
+ }
187
+
188
+ // ─── Public builder ────────────────────────────────────────────────────
189
+
190
+ function extractInlineButtons(
191
+ spec: ModalTuiSpec,
192
+ lines: string[],
193
+ windowStart: number,
194
+ windowEnd: number,
195
+ ): string[] {
196
+ if (!spec.inlineButtonPattern) return [];
197
+ // Always force the `g` flag so we can iterate all matches in the line.
198
+ const declaredFlags = spec.inlineButtonFlags ?? 'gi';
199
+ const flags = declaredFlags.includes('g') ? declaredFlags : declaredFlags + 'g';
200
+ const re = compile(spec.inlineButtonPattern, flags);
201
+ const out: string[] = [];
202
+ for (let i = windowStart; i < windowEnd; i += 1) {
203
+ let m: RegExpExecArray | null;
204
+ re.lastIndex = 0;
205
+ while ((m = re.exec(lines[i])) !== null) {
206
+ const label = (m[1] ?? m[0]).trim();
207
+ if (label && !out.includes(label)) out.push(label);
208
+ if (m.index === re.lastIndex) re.lastIndex += 1; // safety against zero-width matches
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+
214
+ export function buildParseApprovalFromTui(
215
+ spec: ModalTuiSpec,
216
+ visibleRegion?: VisibleRegionSpec,
217
+ ): CliParseApprovalFn {
218
+ const minButtons = spec.minButtons ?? 2;
219
+
220
+ return function parseApproval(input: CliApprovalInput): CliApprovalModal | null {
221
+ const rawText = input.screenText ?? input.buffer ?? '';
222
+ if (!rawText) return null;
223
+ const text = visibleRegion ? applyVisibleRegion(visibleRegion, rawText) : rawText;
224
+ const lines = text.split('\n');
225
+ const question = findQuestionLineIndex(spec, lines);
226
+ if (!question) return null;
227
+ const { start, end } = scopeLines(spec, lines, question.index);
228
+ if (question.index < start || question.index >= end) return null;
229
+ let buttons = extractButtons(spec, lines, question.index + 1, end);
230
+ // Inline fallback: when per-line extraction came up short, the terminal
231
+ // may have rendered every option on the same line ("[0] skip [1] yes …").
232
+ if (buttons.length < minButtons && spec.inlineButtonPattern) {
233
+ buttons = extractInlineButtons(spec, lines, question.index, end);
234
+ }
235
+ if (buttons.length < minButtons) return null;
236
+ const message = buildMessage(spec, lines, question.index, start, end);
237
+ return { message, buttons };
238
+ };
239
+ }
240
+
241
+ export const __internal = {
242
+ findQuestionLineIndex,
243
+ scopeLines,
244
+ extractButtons,
245
+ };
@@ -0,0 +1,276 @@
1
+ /**
2
+ * buildParseSessionFromTui — synthesize a parseSession function from the
3
+ * manifest tui block alone.
4
+ *
5
+ * Composition:
6
+ * 1. detectStatus from tui.spinner/modal/settledPrompt/dispatchOrder
7
+ * (delegated to buildDetectStatusFromTui)
8
+ * 2. parseApproval from tui.modal (delegated to buildParseApprovalFromTui)
9
+ * 3. message extraction from tui.transcriptPty (this file)
10
+ *
11
+ * The result has the same shape as the legacy parse_output:
12
+ * { status, messages, activeModal, transcriptAuthority?, coverage?, providerSessionId? }
13
+ *
14
+ * Identity stamping (providerUnitKey, bubbleId, sequence, bubbleState) is
15
+ * applied by the caller, mirroring _shared/parse_session.js. We export the
16
+ * stamper as `normalizeMessageIdentity` for reuse.
17
+ */
18
+
19
+ import { buildDetectStatusFromTui, type DetectStatusTuiSpec } from './detect-status.js'
20
+ import { buildParseApprovalFromTui, type ModalTuiSpec } from './parse-approval.js'
21
+ import * as crypto from 'node:crypto'
22
+
23
+ // ─── Schema types ────────────────────────────────
24
+
25
+ export interface PrefixSpec {
26
+ regex: string
27
+ flags?: string
28
+ }
29
+
30
+ export interface ToolPrefixSpec extends PrefixSpec {
31
+ skip?: boolean
32
+ }
33
+
34
+ export interface ChromePatternSpec {
35
+ regex: string
36
+ flags?: string
37
+ label?: string
38
+ }
39
+
40
+ export interface TranscriptPtySpec {
41
+ $schema?: 'adhdev:tui/transcript-pty@1'
42
+ assistantPrefix: PrefixSpec
43
+ userPrefix?: PrefixSpec
44
+ toolPrefix?: ToolPrefixSpec
45
+ continuationLine?: { indented?: boolean }
46
+ chromePatterns?: ChromePatternSpec[]
47
+ stripLeadingChrome?: boolean
48
+ scope?: 'screen' | 'buffer' | 'tail'
49
+ }
50
+
51
+ export interface SessionIdExtractionSpec {
52
+ $schema?: 'adhdev:tui/session-id-extraction@1'
53
+ regex: string
54
+ flags?: string
55
+ scope?: 'screen' | 'tail' | 'buffer'
56
+ label?: string
57
+ }
58
+
59
+ export interface ParseSessionTuiSpec {
60
+ spinner?: any
61
+ settledPrompt?: any
62
+ modal?: ModalTuiSpec
63
+ dispatchOrder?: any
64
+ transcriptPty: TranscriptPtySpec
65
+ sessionIdExtraction?: SessionIdExtractionSpec
66
+ }
67
+
68
+ // ─── Output shape ────────────────────────────────
69
+
70
+ export interface SynthesizedMessage {
71
+ role: 'user' | 'assistant'
72
+ kind?: 'standard' | 'tool'
73
+ content: string
74
+ receivedAt?: number
75
+ }
76
+
77
+ export interface SynthesizedSession {
78
+ status: string | null
79
+ messages: SynthesizedMessage[]
80
+ activeModal: { message: string; buttons: string[] } | null
81
+ modal: { message: string; buttons: string[] } | null
82
+ parsedStatus: string | null
83
+ transcriptAuthority?: 'provider' | 'daemon'
84
+ coverage?: 'full' | 'tail' | 'current-turn'
85
+ providerSessionId?: string
86
+ }
87
+
88
+ // ─── ANSI / line helpers ─────────────────────────
89
+
90
+ const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g
91
+ const OSC_RE = /\x1b\][^\x07\x1b\n]*(?:\x07|\x1b\\|(?=\n|$))/g
92
+
93
+ function stripAnsi(text: string): string {
94
+ return String(text || '')
95
+ .replace(/\x1b\[(\d*)C/g, (_m, n) => ' '.repeat(Math.max(1, Number(n) || 1)))
96
+ .replace(/\x1b\[\d*D/g, '')
97
+ .replace(ANSI_RE, '')
98
+ .replace(OSC_RE, '')
99
+ .replace(/\x1b[P^_X][\s\S]*?(?:\x07|\x1b\\)/g, '')
100
+ .replace(/\x1b(?:[@-Z\\-_])/g, '')
101
+ }
102
+
103
+ function splitLines(text: string): string[] {
104
+ return stripAnsi(text)
105
+ .replace(//g, '')
106
+ .split(/\r?\n/)
107
+ .map(l => l.replace(/\s+$/, ''))
108
+ }
109
+
110
+ function pickInputText(input: any, scope: TranscriptPtySpec['scope']): string {
111
+ if (!input) return ''
112
+ if (scope === 'screen') return String(input.screenText || input.screen?.text || '')
113
+ if (scope === 'tail') return String(input.tail || input.recentBuffer || input.buffer || '')
114
+ return String(input.buffer || input.rawBuffer || input.screenText || '')
115
+ }
116
+
117
+ // ─── Identity stamping (mirrors _shared/parse_session.js) ─────
118
+
119
+ function stableHash(value: string): string {
120
+ return crypto.createHash('sha1').update(String(value || '')).digest('hex').slice(0, 12)
121
+ }
122
+
123
+ export function normalizeMessageIdentity<T extends { role?: string; kind?: string; content?: string }>(
124
+ messages: T[],
125
+ status: string,
126
+ ): Array<T & {
127
+ providerUnitKey: string
128
+ bubbleId: string
129
+ sequence: number
130
+ _turnKey: string
131
+ bubbleState: 'streaming' | 'final'
132
+ }> {
133
+ const list = Array.isArray(messages) ? messages : []
134
+ let turnIndex = -1
135
+ return list.map((message, index) => {
136
+ const role = message?.role || 'assistant'
137
+ const kind = message?.kind || 'standard'
138
+ const content = typeof message?.content === 'string' ? message.content : ''
139
+ if (role === 'user' || turnIndex < 0) turnIndex += 1
140
+ const seed = [role, kind, '', index, content].join('\n')
141
+ const providerUnitKey = `v2-pty:${role}:${kind}:${index}:${stableHash(seed)}`
142
+ const bubbleId = `bubble:${providerUnitKey}`
143
+ const turnKey = `turn:${turnIndex}`
144
+ const isStreamingTail = status === 'generating' && role === 'assistant' && index === list.length - 1
145
+ return {
146
+ ...message,
147
+ providerUnitKey,
148
+ bubbleId,
149
+ sequence: index,
150
+ _turnKey: turnKey,
151
+ bubbleState: isStreamingTail ? 'streaming' as const : 'final' as const,
152
+ }
153
+ })
154
+ }
155
+
156
+ // ─── Main builder ────────────────────────────────
157
+
158
+ export function buildParseSessionFromTui(spec: ParseSessionTuiSpec): (input: any) => SynthesizedSession {
159
+ if (!spec.transcriptPty) {
160
+ throw new Error('buildParseSessionFromTui: spec.transcriptPty is required')
161
+ }
162
+
163
+ // Compile regexes once
164
+ const assistantRe = new RegExp(spec.transcriptPty.assistantPrefix.regex, spec.transcriptPty.assistantPrefix.flags || '')
165
+ const userRe = spec.transcriptPty.userPrefix
166
+ ? new RegExp(spec.transcriptPty.userPrefix.regex, spec.transcriptPty.userPrefix.flags || '')
167
+ : null
168
+ const toolRe = spec.transcriptPty.toolPrefix
169
+ ? new RegExp(spec.transcriptPty.toolPrefix.regex, spec.transcriptPty.toolPrefix.flags || '')
170
+ : null
171
+ const toolSkip = spec.transcriptPty.toolPrefix?.skip ?? false
172
+ const chromeRes = (spec.transcriptPty.chromePatterns || []).map(p =>
173
+ new RegExp(p.regex, p.flags || ''))
174
+ const requireIndentForContinuation = spec.transcriptPty.continuationLine?.indented ?? false
175
+ const stripLeadingChrome = spec.transcriptPty.stripLeadingChrome ?? true
176
+ const scope = spec.transcriptPty.scope ?? 'buffer'
177
+
178
+ // Compose detect + approval helpers if their inputs are present.
179
+ const detectStatus = (spec.spinner || spec.settledPrompt || spec.modal || spec.dispatchOrder)
180
+ ? buildDetectStatusFromTui({
181
+ spinner: spec.spinner,
182
+ settledPrompt: spec.settledPrompt,
183
+ modal: spec.modal,
184
+ dispatchOrder: spec.dispatchOrder,
185
+ })
186
+ : () => null
187
+ const parseApproval = spec.modal ? buildParseApprovalFromTui(spec.modal) : () => null
188
+
189
+ // Optional session-id extraction (tui/session-id-extraction@1). Compile once
190
+ // so each parseSession call is a single regex.exec on the chosen scope.
191
+ const sessionIdRe = spec.sessionIdExtraction
192
+ ? new RegExp(
193
+ spec.sessionIdExtraction.regex,
194
+ spec.sessionIdExtraction.flags ?? 'i',
195
+ )
196
+ : null
197
+ const sessionIdScope: TranscriptPtySpec['scope'] = spec.sessionIdExtraction?.scope ?? 'tail'
198
+
199
+ return function parseSession(input: any): SynthesizedSession {
200
+ // status + modal first
201
+ const status = detectStatus(input as any) ?? 'idle'
202
+ const modal = parseApproval(input as any)
203
+
204
+ const text = pickInputText(input, scope)
205
+ const lines = splitLines(text)
206
+
207
+ const messages: SynthesizedMessage[] = []
208
+ let seenFirstRoleLine = !stripLeadingChrome
209
+
210
+ for (const raw of lines) {
211
+ const line = raw
212
+ if (line.trim() === '') {
213
+ // Blank line: ends streaming continuation but doesn't add a message.
214
+ continue
215
+ }
216
+
217
+ // Chrome filter
218
+ let isChrome = false
219
+ for (const cre of chromeRes) {
220
+ if (cre.test(line)) { isChrome = true; break }
221
+ }
222
+ if (isChrome) continue
223
+
224
+ // Role detection
225
+ const userMatch = userRe ? line.match(userRe) : null
226
+ const toolMatch = toolRe ? line.match(toolRe) : null
227
+ const assistMatch = line.match(assistantRe)
228
+
229
+ if (userMatch) {
230
+ seenFirstRoleLine = true
231
+ const content = (userMatch[1] ?? userMatch[0]).trim()
232
+ if (content) messages.push({ role: 'user', kind: 'standard', content })
233
+ continue
234
+ }
235
+ if (toolMatch) {
236
+ seenFirstRoleLine = true
237
+ if (toolSkip) continue
238
+ const content = (toolMatch[1] ?? toolMatch[0]).trim()
239
+ if (content) messages.push({ role: 'assistant', kind: 'tool', content })
240
+ continue
241
+ }
242
+ if (assistMatch) {
243
+ seenFirstRoleLine = true
244
+ const content = (assistMatch[1] ?? assistMatch[0]).trim()
245
+ if (content) messages.push({ role: 'assistant', kind: 'standard', content })
246
+ continue
247
+ }
248
+
249
+ if (!seenFirstRoleLine) continue
250
+
251
+ // Continuation rule
252
+ if (requireIndentForContinuation && !/^\s/.test(line)) continue
253
+ const last = messages[messages.length - 1]
254
+ if (!last) continue
255
+ const cont = line.replace(/^\s+/, '')
256
+ if (cont) last.content = last.content ? `${last.content}\n${cont}` : cont
257
+ }
258
+
259
+ const result: SynthesizedSession = {
260
+ status,
261
+ messages,
262
+ activeModal: modal,
263
+ modal,
264
+ parsedStatus: status,
265
+ }
266
+
267
+ if (sessionIdRe) {
268
+ const haystack = stripAnsi(pickInputText(input, sessionIdScope))
269
+ const match = haystack.match(sessionIdRe)
270
+ const captured = match?.[1]?.trim()
271
+ if (captured) result.providerSessionId = captured
272
+ }
273
+
274
+ return result
275
+ }
276
+ }