@aragon-agent/cli 0.6.3 → 0.6.7

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 (138) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +203 -13
  3. package/dist/agent/controller.d.ts +158 -2
  4. package/dist/agent/controller.js +400 -1
  5. package/dist/agent/reducer.d.ts +102 -19
  6. package/dist/agent/reducer.js +162 -71
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/agent/usage.d.ts +34 -0
  10. package/dist/agent/usage.js +43 -0
  11. package/dist/cli.js +132 -23
  12. package/dist/commands/builtins.js +101 -1
  13. package/dist/compaction/child.js +5 -0
  14. package/dist/compaction/command.js +14 -1
  15. package/dist/compaction/compactor.d.ts +94 -24
  16. package/dist/compaction/compactor.js +247 -98
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +77 -0
  24. package/dist/compaction/wiring.d.ts +56 -0
  25. package/dist/compaction/wiring.js +107 -25
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.d.ts +1 -1
  28. package/dist/config/env.js +63 -2
  29. package/dist/config/load.d.ts +28 -0
  30. package/dist/config/load.js +83 -1
  31. package/dist/config/schema.d.ts +129 -0
  32. package/dist/config/schema.js +69 -0
  33. package/dist/config/store.js +47 -1
  34. package/dist/exec/events.d.ts +138 -1
  35. package/dist/exec/events.js +42 -0
  36. package/dist/exec/index.js +43 -6
  37. package/dist/exec/progress-reporter.d.ts +23 -0
  38. package/dist/exec/progress-reporter.js +88 -0
  39. package/dist/exec/runner.d.ts +34 -0
  40. package/dist/exec/runner.js +137 -25
  41. package/dist/fast/reviewer.d.ts +19 -1
  42. package/dist/fast/reviewer.js +22 -0
  43. package/dist/fast/types.d.ts +15 -0
  44. package/dist/fast/wiring.d.ts +10 -1
  45. package/dist/fast/wiring.js +18 -1
  46. package/dist/input/limits.d.ts +72 -0
  47. package/dist/input/limits.js +63 -0
  48. package/dist/input/paste-parse.d.ts +71 -0
  49. package/dist/input/paste-parse.js +95 -0
  50. package/dist/input/stdin-filter.d.ts +81 -0
  51. package/dist/input/stdin-filter.js +379 -0
  52. package/dist/logging/install.d.ts +2 -0
  53. package/dist/logging/install.js +53 -1
  54. package/dist/proc/classify.d.ts +52 -0
  55. package/dist/proc/classify.js +143 -0
  56. package/dist/proc/kill-tree.d.ts +42 -0
  57. package/dist/proc/kill-tree.js +119 -0
  58. package/dist/proc/limits.d.ts +84 -0
  59. package/dist/proc/limits.js +84 -0
  60. package/dist/proc/log-ring.d.ts +68 -0
  61. package/dist/proc/log-ring.js +102 -0
  62. package/dist/proc/prompt.d.ts +24 -0
  63. package/dist/proc/prompt.js +49 -0
  64. package/dist/proc/readiness.d.ts +102 -0
  65. package/dist/proc/readiness.js +219 -0
  66. package/dist/proc/supervisor.d.ts +103 -0
  67. package/dist/proc/supervisor.js +465 -0
  68. package/dist/proc/types.d.ts +175 -0
  69. package/dist/proc/types.js +15 -0
  70. package/dist/session/persist.js +36 -1
  71. package/dist/todo/limits.d.ts +4 -17
  72. package/dist/todo/limits.js +4 -17
  73. package/dist/todo/panel-rows.d.ts +8 -6
  74. package/dist/todo/panel-rows.js +19 -6
  75. package/dist/tools/bash-tool.d.ts +51 -2
  76. package/dist/tools/bash-tool.js +321 -51
  77. package/dist/tools/fs-tools.d.ts +17 -0
  78. package/dist/tools/index.d.ts +55 -2
  79. package/dist/tools/index.js +39 -1
  80. package/dist/tools/proc-tools.d.ts +25 -0
  81. package/dist/tools/proc-tools.js +102 -0
  82. package/dist/ui/App.d.ts +15 -2
  83. package/dist/ui/App.js +330 -69
  84. package/dist/ui/AutocompletePopup.d.ts +3 -1
  85. package/dist/ui/AutocompletePopup.js +9 -11
  86. package/dist/ui/BottomStatusRow.d.ts +1 -1
  87. package/dist/ui/Composer.d.ts +56 -1
  88. package/dist/ui/Composer.js +50 -6
  89. package/dist/ui/PromptInput.d.ts +52 -3
  90. package/dist/ui/PromptInput.js +201 -55
  91. package/dist/ui/StatusBar.d.ts +40 -15
  92. package/dist/ui/StatusBar.js +51 -9
  93. package/dist/ui/TeamPanel.d.ts +3 -1
  94. package/dist/ui/TeamPanel.js +13 -9
  95. package/dist/ui/TodoPanel.d.ts +5 -5
  96. package/dist/ui/TodoPanel.js +56 -24
  97. package/dist/ui/Transcript.js +29 -1
  98. package/dist/ui/UpdateLine.d.ts +1 -1
  99. package/dist/ui/capabilities.js +5 -5
  100. package/dist/ui/composer-limits.d.ts +31 -0
  101. package/dist/ui/composer-limits.js +50 -0
  102. package/dist/ui/composer-rows.d.ts +73 -0
  103. package/dist/ui/composer-rows.js +201 -0
  104. package/dist/ui/editor-reducer.d.ts +37 -0
  105. package/dist/ui/editor-reducer.js +88 -12
  106. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  107. package/dist/ui/entries/ServiceCard.js +116 -0
  108. package/dist/ui/entries/UserEntry.d.ts +17 -3
  109. package/dist/ui/entries/UserEntry.js +34 -5
  110. package/dist/ui/exit-snapshot.d.ts +9 -0
  111. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  112. package/dist/ui/layout/autocomplete.d.ts +18 -0
  113. package/dist/ui/layout/autocomplete.js +26 -0
  114. package/dist/ui/layout/budget.d.ts +34 -6
  115. package/dist/ui/layout/budget.js +32 -7
  116. package/dist/ui/layout/rail.d.ts +4 -53
  117. package/dist/ui/layout/rail.js +10 -62
  118. package/dist/ui/layout/team-panel.d.ts +18 -0
  119. package/dist/ui/layout/team-panel.js +22 -0
  120. package/dist/ui/layout/todo-layout.d.ts +24 -0
  121. package/dist/ui/layout/todo-layout.js +27 -0
  122. package/dist/ui/layout/virtual-window.js +51 -1
  123. package/dist/ui/overlays/HelpOverlay.js +3 -0
  124. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  125. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  126. package/dist/ui/overlays/SettingsScreen.js +14 -1
  127. package/dist/ui/paste-frames.d.ts +87 -0
  128. package/dist/ui/paste-frames.js +112 -0
  129. package/dist/ui/paste-tokens.d.ts +90 -0
  130. package/dist/ui/paste-tokens.js +147 -0
  131. package/dist/ui/screen.d.ts +16 -0
  132. package/dist/ui/screen.js +45 -2
  133. package/dist/ui/selection/selection-controller.d.ts +1 -1
  134. package/dist/ui/transcript-text.js +55 -2
  135. package/dist/ui/use-wheel-routing.d.ts +1 -1
  136. package/package.json +2 -2
  137. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  138. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -53,6 +53,9 @@ export function offFastStatus() {
53
53
  tier: { ok: false, reason: 'disabled' },
54
54
  snapshot: {
55
55
  live: false,
56
+ // A tier that never existed was never switched off by its reviewer, so
57
+ // `false` here is the truth and not a placeholder.
58
+ selfDisabled: false,
56
59
  model: '',
57
60
  sameAsMain: false,
58
61
  reviews: 0,
@@ -113,6 +116,9 @@ export class FastWiring {
113
116
  available: () => this.available(),
114
117
  getApiKey: deps.getApiKey,
115
118
  emit: (event) => this.emit(event),
119
+ // The reviewer announces its own self-disable with a `tier_changed`, and
120
+ // only the wiring can build the snapshot that carries it.
121
+ snapshot: () => this.snapshot(),
116
122
  notify: deps.notify,
117
123
  });
118
124
  }
@@ -158,7 +164,16 @@ export class FastWiring {
158
164
  isEnabled() {
159
165
  return this.enabled;
160
166
  }
161
- /** §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header. */
167
+ /**
168
+ * §3.3's live predicate. THE THIRD TERM IS THE POINT - see the header.
169
+ *
170
+ * `FastReviewer.isSelfDisabled()` is DELIBERATELY NOT a fourth term. Folding
171
+ * it in would look tidier and would break something else: `delegationAvailable()`
172
+ * is `available() && config.fast.delegate === true`, so three consecutive
173
+ * REVIEW failures would silently switch DELEGATION off as well - turning a
174
+ * diagnostic signal into a behaviour change for every user. The two facts are
175
+ * reported separately on `FastSnapshot` instead (`live` / `selfDisabled`).
176
+ */
162
177
  available() {
163
178
  return this.registered && this.enabled && this.tier.ok;
164
179
  }
@@ -272,6 +287,8 @@ export class FastWiring {
272
287
  const ref = this.tier.ok ? this.tier.ref : null;
273
288
  return {
274
289
  live: this.available(),
290
+ // ORTHOGONAL TO `live`, never derivable from it (see `FastSnapshot`).
291
+ selfDisabled: this.reviewer.isSelfDisabled(),
275
292
  model: ref ? ref.modelId : '',
276
293
  sameAsMain: this.tier.ok ? this.tier.sameAsMain : false,
277
294
  // THE NUMERATOR, THE DENOMINATOR AND THE FLAG ARE READ IN ONE EXPRESSION,
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Structural bounds for paste handling (tui-paste-handling D-10 / section 8).
3
+ *
4
+ * THESE ARE STRUCTURAL, NOT POLICY. The package convention recorded in
5
+ * `.claude-index/index.md` is that `limits.ts` carries the numbers an
6
+ * implementation needs to stay bounded, while `config/schema.ts` carries the
7
+ * preferences a user is allowed to have. The only user-facing key this feature
8
+ * adds is the boolean `paste`; a tunable collapse threshold is a preference
9
+ * nobody has asked for, and a kill switch is what a bug report actually needs.
10
+ *
11
+ * The two framing constants below are the only bytes that cross from the stdin
12
+ * filter into the composer, so they are load-bearing in a way a threshold is
13
+ * not: see `PASTE_OPEN`.
14
+ */
15
+ /**
16
+ * Ceiling on ONE paste. Above it the body is dropped and the user is told,
17
+ * never truncated (D-11): a truncated paste loses data and looks like success.
18
+ */
19
+ export declare const PASTE_MAX_BYTES: number;
20
+ /** Ceiling on every live payload in one draft, summed. Above it a paste is refused. */
21
+ export declare const PASTE_DRAFT_MAX_BYTES: number;
22
+ /** Ceiling on collapsed blocks in one draft. Above it a paste is refused. */
23
+ export declare const PASTE_MAX_BLOCKS = 32;
24
+ /**
25
+ * Tier 2's single-line rule: a chunk of at least this many printable characters
26
+ * with no line break is a paste.
27
+ *
28
+ * Safe because coalescing a keyboard burst into 25 characters inside one
29
+ * event-loop turn needs roughly 1 500 WPM. It is a statement about what a
30
+ * KEYBOARD can emit, not about what text looks like.
31
+ */
32
+ export declare const PASTE_MIN_BURST_CHARS = 25;
33
+ /** Tier 2 coalescing window. Ordinary typing never opens it, so never pays it. */
34
+ export declare const PASTE_BURST_MS = 15;
35
+ /**
36
+ * How long an unterminated Tier 1 body is held before it is force-flushed.
37
+ *
38
+ * I-4: an unterminated `\x1b[200~` must never be able to wedge keyboard input,
39
+ * which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
40
+ */
41
+ export declare const PASTE_ASSEMBLY_MAX_MS = 2000;
42
+ /**
43
+ * The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
44
+ *
45
+ * ON THE SAME STREAM, NOT A SIDE CHANNEL. `wrapper.write(text)` is delivered
46
+ * asynchronously; a synchronous side-channel emit would apply the paste BEFORE
47
+ * text that preceded it in the same chunk. Framing preserves order by
48
+ * construction.
49
+ *
50
+ * NUL is the delimiter because no terminal delivers it for any key, and because
51
+ * `sanitisePaste` strips every NUL out of every payload -- so a payload can
52
+ * never forge a frame boundary.
53
+ */
54
+ export declare const PASTE_OPEN = "\0[";
55
+ export declare const PASTE_CLOSE = "\0]";
56
+ /**
57
+ * The one channel from the stdin filter to `App`'s notice dispatch (P1-3 / I-15).
58
+ *
59
+ * The filter is constructed in `cli.tsx` BEFORE `render()`, so it has no
60
+ * `dispatch`. Its only other outlet is the logger, and a limit that writes only
61
+ * to a log file is indistinguishable -- from the user's chair -- from a paste
62
+ * that silently did nothing, which is exactly what D-11 exists to rule out.
63
+ *
64
+ * Same late-bound shape as `selectionBridge` / `updateBridge` / `terminalBridge`:
65
+ * `cli.tsx` creates the object, `App` assigns `notify` on mount and clears it on
66
+ * unmount, and the filter calls `bridge.notify?.(...)`.
67
+ */
68
+ export interface PasteBridge {
69
+ notify: ((level: 'warn' | 'error', text: string) => void) | null;
70
+ }
71
+ /** Human-readable byte size for the refusal toast (`4.1 MB`, `812.0 KB`, `7 B`). */
72
+ export declare function formatPasteSize(bytes: number): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Structural bounds for paste handling (tui-paste-handling D-10 / section 8).
3
+ *
4
+ * THESE ARE STRUCTURAL, NOT POLICY. The package convention recorded in
5
+ * `.claude-index/index.md` is that `limits.ts` carries the numbers an
6
+ * implementation needs to stay bounded, while `config/schema.ts` carries the
7
+ * preferences a user is allowed to have. The only user-facing key this feature
8
+ * adds is the boolean `paste`; a tunable collapse threshold is a preference
9
+ * nobody has asked for, and a kill switch is what a bug report actually needs.
10
+ *
11
+ * The two framing constants below are the only bytes that cross from the stdin
12
+ * filter into the composer, so they are load-bearing in a way a threshold is
13
+ * not: see `PASTE_OPEN`.
14
+ */
15
+ /**
16
+ * Ceiling on ONE paste. Above it the body is dropped and the user is told,
17
+ * never truncated (D-11): a truncated paste loses data and looks like success.
18
+ */
19
+ export const PASTE_MAX_BYTES = 2 * 1024 * 1024;
20
+ /** Ceiling on every live payload in one draft, summed. Above it a paste is refused. */
21
+ export const PASTE_DRAFT_MAX_BYTES = 8 * 1024 * 1024;
22
+ /** Ceiling on collapsed blocks in one draft. Above it a paste is refused. */
23
+ export const PASTE_MAX_BLOCKS = 32;
24
+ /**
25
+ * Tier 2's single-line rule: a chunk of at least this many printable characters
26
+ * with no line break is a paste.
27
+ *
28
+ * Safe because coalescing a keyboard burst into 25 characters inside one
29
+ * event-loop turn needs roughly 1 500 WPM. It is a statement about what a
30
+ * KEYBOARD can emit, not about what text looks like.
31
+ */
32
+ export const PASTE_MIN_BURST_CHARS = 25;
33
+ /** Tier 2 coalescing window. Ordinary typing never opens it, so never pays it. */
34
+ export const PASTE_BURST_MS = 15;
35
+ /**
36
+ * How long an unterminated Tier 1 body is held before it is force-flushed.
37
+ *
38
+ * I-4: an unterminated `\x1b[200~` must never be able to wedge keyboard input,
39
+ * which is the rule `MAX_PENDING_MOUSE_CHARS` already encodes for mouse reports.
40
+ */
41
+ export const PASTE_ASSEMBLY_MAX_MS = 2_000;
42
+ /**
43
+ * The inline frame the filter writes into the stream Ink reads (D-3 / I-5).
44
+ *
45
+ * ON THE SAME STREAM, NOT A SIDE CHANNEL. `wrapper.write(text)` is delivered
46
+ * asynchronously; a synchronous side-channel emit would apply the paste BEFORE
47
+ * text that preceded it in the same chunk. Framing preserves order by
48
+ * construction.
49
+ *
50
+ * NUL is the delimiter because no terminal delivers it for any key, and because
51
+ * `sanitisePaste` strips every NUL out of every payload -- so a payload can
52
+ * never forge a frame boundary.
53
+ */
54
+ export const PASTE_OPEN = '\u0000[';
55
+ export const PASTE_CLOSE = '\u0000]';
56
+ /** Human-readable byte size for the refusal toast (`4.1 MB`, `812.0 KB`, `7 B`). */
57
+ export function formatPasteSize(bytes) {
58
+ if (bytes >= 1024 * 1024)
59
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
60
+ if (bytes >= 1024)
61
+ return `${(bytes / 1024).toFixed(1)} KB`;
62
+ return `${bytes} B`;
63
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Paste recognition primitives (tui-paste-handling section 5.1) -- pure,
3
+ * stream-free, no timers.
4
+ *
5
+ * WHY THIS LIVES BELOW INK. `ink/build/components/App.js:131-138` reads stdin on
6
+ * `'readable'` and emits each chunk as one `'input'` event;
7
+ * `parse-keypress.js:145-148` maps the exact string `'\r'` to `name: 'return'`,
8
+ * which `PromptInput` reads as "send the message". A 15-line paste on a Windows
9
+ * console routinely arrives as a dozen chunks that are exactly `"\r"`, and each
10
+ * one SUBMITS THE DRAFT AS IT STOOD. No component-level heuristic can undo a
11
+ * `key.return` that has already fired, so recognition has to happen on the byte
12
+ * stream, in front of Ink -- the same lesson `mouse-events.ts` records.
13
+ *
14
+ * Two tiers feed one framing (D-4). Tier 1 is bracketed paste and is exact;
15
+ * Tier 2 is a heuristic on chunk SHAPE for the terminals that ignore
16
+ * `DECSET 2004`. Both are about what a keyboard can physically emit.
17
+ */
18
+ /** `DECSET 2004` wraps a paste in these two markers. */
19
+ export declare const PASTE_BEGIN_MARK = "\u001B[200~";
20
+ export declare const PASTE_END_MARK = "\u001B[201~";
21
+ /** The longest tail worth holding back while a marker might still complete. */
22
+ export declare const PASTE_MARK_LENGTH: number;
23
+ /**
24
+ * True when `s` is a PROPER prefix of either paste marker (never a whole one).
25
+ *
26
+ * Both markers are held, in every mode, whether or not THIS process enabled DEC
27
+ * 2004 (I-16): the user's shell, `tmux` or a wrapper script may have left it on,
28
+ * and an unconsumed marker is a literal `[200~` typed into the draft -- the
29
+ * exact failure the feature exists to prevent.
30
+ */
31
+ export declare function isPastePrefix(s: string): boolean;
32
+ /**
33
+ * Length of the trailing run of `text` that is a proper prefix of a paste
34
+ * marker, or 0. Shortest match wins, so only the bytes that could still become a
35
+ * marker are held back.
36
+ */
37
+ export declare function trailingPastePrefixLength(text: string): number;
38
+ /**
39
+ * Tier 2: is this chunk a paste, or keystrokes?
40
+ *
41
+ * The PRIMARY rule -- "longer than one character AND contains a line break" --
42
+ * is exactly the shape defect A needs, so the two coincide: any chunk that could
43
+ * submit the draft by accident is classified as a paste. A lone `\r` is the
44
+ * Enter key and stays `'keys'`.
45
+ *
46
+ * The SECONDARY rule needs `PASTE_MIN_BURST_CHARS` printable characters in one
47
+ * chunk. It excludes chunks carrying any C0 byte but TAB, which is what keeps an
48
+ * arrow-key burst (`\x1b[B\x1b[B...`, and every wheel notch translated by
49
+ * alternate scroll) out of the paste path.
50
+ */
51
+ export declare function classifyChunk(text: string, minBurstChars?: number): 'paste' | 'keys';
52
+ /**
53
+ * Make a payload safe to put in a buffer and draw through `<Text>` (G2).
54
+ *
55
+ * 1. CRLF and lone CR both become exactly ONE `\n` -- terminals send CR for a
56
+ * newline in pasted text, and a lone LF is dropped by Ink's key parser
57
+ * entirely, so both have to be normalised here or a pasted line break either
58
+ * submits the message or silently vanishes.
59
+ * 2. Every C0 byte except `\n` and `\t` is removed. Ink writes `<Text>` children
60
+ * to the terminal without escaping anything, so a surviving `\r` overwrites
61
+ * the row it is drawn on and a surviving `\x1b` repaints the frame's colours.
62
+ * 3. DEL and every C1 byte (U+0080..U+009F) go too, and that is NOT padding:
63
+ * U+009B is the single-byte CSI and U+0085 is NEL, and a terminal that decodes
64
+ * them acts on them exactly as it acts on `\x1b[`. A file pasted out of a
65
+ * non-UTF-8 editor walks straight through a "strip C0" rule.
66
+ * 4. TAB is KEPT. Indentation is content, and it is what the model receives.
67
+ *
68
+ * Applied to PAYLOAD ONLY, never to keystrokes, so `Ctrl+A` and Escape keep
69
+ * working. Non-ASCII text (CJK, emoji) is untouched.
70
+ */
71
+ export declare function sanitisePaste(raw: string): string;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Paste recognition primitives (tui-paste-handling section 5.1) -- pure,
3
+ * stream-free, no timers.
4
+ *
5
+ * WHY THIS LIVES BELOW INK. `ink/build/components/App.js:131-138` reads stdin on
6
+ * `'readable'` and emits each chunk as one `'input'` event;
7
+ * `parse-keypress.js:145-148` maps the exact string `'\r'` to `name: 'return'`,
8
+ * which `PromptInput` reads as "send the message". A 15-line paste on a Windows
9
+ * console routinely arrives as a dozen chunks that are exactly `"\r"`, and each
10
+ * one SUBMITS THE DRAFT AS IT STOOD. No component-level heuristic can undo a
11
+ * `key.return` that has already fired, so recognition has to happen on the byte
12
+ * stream, in front of Ink -- the same lesson `mouse-events.ts` records.
13
+ *
14
+ * Two tiers feed one framing (D-4). Tier 1 is bracketed paste and is exact;
15
+ * Tier 2 is a heuristic on chunk SHAPE for the terminals that ignore
16
+ * `DECSET 2004`. Both are about what a keyboard can physically emit.
17
+ */
18
+ import { PASTE_MIN_BURST_CHARS } from './limits.js';
19
+ /** `DECSET 2004` wraps a paste in these two markers. */
20
+ export const PASTE_BEGIN_MARK = '\x1b[200~';
21
+ export const PASTE_END_MARK = '\x1b[201~';
22
+ /** The longest tail worth holding back while a marker might still complete. */
23
+ export const PASTE_MARK_LENGTH = PASTE_BEGIN_MARK.length;
24
+ /**
25
+ * True when `s` is a PROPER prefix of either paste marker (never a whole one).
26
+ *
27
+ * Both markers are held, in every mode, whether or not THIS process enabled DEC
28
+ * 2004 (I-16): the user's shell, `tmux` or a wrapper script may have left it on,
29
+ * and an unconsumed marker is a literal `[200~` typed into the draft -- the
30
+ * exact failure the feature exists to prevent.
31
+ */
32
+ export function isPastePrefix(s) {
33
+ if (s.length === 0 || s.length >= PASTE_MARK_LENGTH)
34
+ return false;
35
+ return PASTE_BEGIN_MARK.startsWith(s) || PASTE_END_MARK.startsWith(s);
36
+ }
37
+ /**
38
+ * Length of the trailing run of `text` that is a proper prefix of a paste
39
+ * marker, or 0. Shortest match wins, so only the bytes that could still become a
40
+ * marker are held back.
41
+ */
42
+ export function trailingPastePrefixLength(text) {
43
+ const max = Math.min(text.length, PASTE_MARK_LENGTH - 1);
44
+ for (let k = 1; k <= max; k += 1) {
45
+ if (isPastePrefix(text.slice(text.length - k)))
46
+ return k;
47
+ }
48
+ return 0;
49
+ }
50
+ /** Any C0 control byte other than TAB -- ESC, BEL, NUL, CR, LF included. */
51
+ const HAS_CONTROL_EXCEPT_TAB = /[\u0000-\u0008\u000A-\u001F]/;
52
+ /**
53
+ * Tier 2: is this chunk a paste, or keystrokes?
54
+ *
55
+ * The PRIMARY rule -- "longer than one character AND contains a line break" --
56
+ * is exactly the shape defect A needs, so the two coincide: any chunk that could
57
+ * submit the draft by accident is classified as a paste. A lone `\r` is the
58
+ * Enter key and stays `'keys'`.
59
+ *
60
+ * The SECONDARY rule needs `PASTE_MIN_BURST_CHARS` printable characters in one
61
+ * chunk. It excludes chunks carrying any C0 byte but TAB, which is what keeps an
62
+ * arrow-key burst (`\x1b[B\x1b[B...`, and every wheel notch translated by
63
+ * alternate scroll) out of the paste path.
64
+ */
65
+ export function classifyChunk(text, minBurstChars = PASTE_MIN_BURST_CHARS) {
66
+ if (text.length > 1 && /[\r\n]/.test(text))
67
+ return 'paste';
68
+ if (text.length >= minBurstChars && !HAS_CONTROL_EXCEPT_TAB.test(text))
69
+ return 'paste';
70
+ return 'keys';
71
+ }
72
+ /**
73
+ * Make a payload safe to put in a buffer and draw through `<Text>` (G2).
74
+ *
75
+ * 1. CRLF and lone CR both become exactly ONE `\n` -- terminals send CR for a
76
+ * newline in pasted text, and a lone LF is dropped by Ink's key parser
77
+ * entirely, so both have to be normalised here or a pasted line break either
78
+ * submits the message or silently vanishes.
79
+ * 2. Every C0 byte except `\n` and `\t` is removed. Ink writes `<Text>` children
80
+ * to the terminal without escaping anything, so a surviving `\r` overwrites
81
+ * the row it is drawn on and a surviving `\x1b` repaints the frame's colours.
82
+ * 3. DEL and every C1 byte (U+0080..U+009F) go too, and that is NOT padding:
83
+ * U+009B is the single-byte CSI and U+0085 is NEL, and a terminal that decodes
84
+ * them acts on them exactly as it acts on `\x1b[`. A file pasted out of a
85
+ * non-UTF-8 editor walks straight through a "strip C0" rule.
86
+ * 4. TAB is KEPT. Indentation is content, and it is what the model receives.
87
+ *
88
+ * Applied to PAYLOAD ONLY, never to keystrokes, so `Ctrl+A` and Escape keep
89
+ * working. Non-ASCII text (CJK, emoji) is untouched.
90
+ */
91
+ export function sanitisePaste(raw) {
92
+ return raw
93
+ .replace(/\r\n?/g, '\n')
94
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
95
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Stdin filter (mouse-wheel-region-routing section 4.2 / 5.2, extended by
3
+ * tui-paste-handling section 5.1).
4
+ *
5
+ * WHY A WRAPPER AND NOT A SECOND LISTENER. Ink 5.2.1 owns stdin in a way that
6
+ * forbids a second reader: `components/App.js` calls `setEncoding('utf8')`,
7
+ * `ref()`, `setRawMode(true)`, subscribes to `'readable'` and drains with
8
+ * `read()`. Attaching our own `'data'` listener to the same `process.stdin`
9
+ * would switch the stream into flowing mode and RACE Ink's `read()` for chunks
10
+ * -- some keystrokes would reach Ink, some would not, non-deterministically
11
+ * (D-3). So the filter REPLACES the stream Ink is given: it reads the real
12
+ * stdin, strips every mouse report, frames every paste, and writes the
13
+ * remainder into a `PassThrough` that carries the surface Ink requires.
14
+ *
15
+ * ONE CHANNEL, NOT TWO, AND THAT NOW COVERS PASTES AS WELL (D-2 / D-3). A second
16
+ * chained filter would need a second `PassThrough` and would reopen the ordering
17
+ * hazard this note closes; a side-channel paste emit would apply the payload
18
+ * BEFORE text that preceded it in the same chunk, because `wrapper.write` is
19
+ * asynchronous and a synchronous emit is not. The paste therefore travels
20
+ * INLINE, wrapped in `PASTE_OPEN` / `PASTE_CLOSE` (I-5).
21
+ *
22
+ * ORDERING IS AN INVARIANT, NOT A CONVENTION (I-8). The caller must build this
23
+ * filter BEFORE anything writes `\x1b[?1000h` or `\x1b[?2004h`. Reporting or
24
+ * bracketed paste enabled while Ink still holds the raw stream does not degrade
25
+ * the feature -- it types `[<0;12;5M` or `[200~` into the user's message, which
26
+ * is strictly worse than the bug being fixed.
27
+ *
28
+ * THE HANDLE ANSWERS EXACTLY ONE QUESTION: "is a stream wrapped?" (I-11 / D-17).
29
+ * It stopped being the answer to "is the mouse on?" the moment paste could build
30
+ * a filter on its own -- `paste` defaults to `true`, so `aragon --no-mouse`
31
+ * builds one. `cli.tsx` reads its own `mouseOn` / `pasteOn` booleans; this module
32
+ * is told which features it is running and never derives them.
33
+ */
34
+ import { type MouseEvent } from './mouse-events.js';
35
+ import { type PasteBridge } from './limits.js';
36
+ export interface MouseSource {
37
+ /**
38
+ * Returns an unsubscribe function. Multiple subscribers are allowed -- the
39
+ * wheel router and (since tui-selection-and-scroll-follow) the selection
40
+ * controller both take this one channel, and each ignores the kinds it does
41
+ * not own.
42
+ *
43
+ * With `features.mouse` false the channel exists and stays SILENT forever, so
44
+ * a subscriber written against it does not have to know which features are on.
45
+ */
46
+ subscribe(listener: (event: MouseEvent) => void): () => void;
47
+ }
48
+ /**
49
+ * Which stream-level features this filter is running.
50
+ *
51
+ * TOLD, NEVER DERIVED (D-17 / P0-1). With `{ mouse: false, paste: true }` no
52
+ * `MouseEvent` is ever emitted and `splitMouseEvents` is not called at all: a
53
+ * `--no-mouse` session never wrote `?1000h`, so no real report can arrive, and
54
+ * running the parser anyway could only ever EAT pasted bytes that happen to look
55
+ * like one. With `{ mouse: true, paste: false }` this is byte-for-byte the
56
+ * v0.6.3 mouse filter.
57
+ */
58
+ export interface StdinFilterFeatures {
59
+ /** Parse and strip SGR / X10 mouse reports, and emit `MouseEvent`s. */
60
+ readonly mouse: boolean;
61
+ /** Recognise pastes and deliver them framed. */
62
+ readonly paste: boolean;
63
+ }
64
+ export interface StdinFilter {
65
+ /** Hand this to Ink: `render(<App/>, { stdin: filter.stdin })`. */
66
+ readonly stdin: NodeJS.ReadStream;
67
+ readonly source: MouseSource;
68
+ /** Idempotent. Detaches from the real stdin and clears every timer (I-7). */
69
+ dispose(): void;
70
+ }
71
+ export declare function createStdinFilter(real: NodeJS.ReadStream, features: StdinFilterFeatures, bridge?: PasteBridge, onWarn?: (reason: string) => void): StdinFilter;
72
+ /**
73
+ * Build a filter, or `null` if construction throws.
74
+ *
75
+ * The `null` return is the whole point (P0-1): a construction failure must
76
+ * degrade to "the wheel is inert and pastes are keystrokes" -- a supported,
77
+ * tested rung of the fail-safe ladder -- rather than leaving `?1000h` or `?2004h`
78
+ * on with no filter in front of Ink. `cli.tsx` derives BOTH `mouseOn` and
79
+ * `pasteOn` from the returned handle for exactly that reason.
80
+ */
81
+ export declare function tryCreateStdinFilter(real: NodeJS.ReadStream, features: StdinFilterFeatures, onError?: (reason: string) => void, bridge?: PasteBridge): StdinFilter | null;