opencode-agora 0.3.0 → 0.4.1

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 (280) hide show
  1. package/README.md +86 -255
  2. package/dist/atomic-write.d.ts +10 -0
  3. package/dist/atomic-write.d.ts.map +1 -0
  4. package/dist/atomic-write.js +23 -0
  5. package/dist/atomic-write.js.map +1 -0
  6. package/dist/auth/refresh.d.ts +17 -0
  7. package/dist/auth/refresh.d.ts.map +1 -0
  8. package/dist/auth/refresh.js +50 -0
  9. package/dist/auth/refresh.js.map +1 -0
  10. package/dist/cli/app.d.ts +13 -18
  11. package/dist/cli/app.d.ts.map +1 -1
  12. package/dist/cli/app.js +184 -1187
  13. package/dist/cli/app.js.map +1 -1
  14. package/dist/cli/chat-renderer.d.ts +31 -0
  15. package/dist/cli/chat-renderer.d.ts.map +1 -0
  16. package/dist/cli/chat-renderer.js +275 -0
  17. package/dist/cli/chat-renderer.js.map +1 -0
  18. package/dist/cli/commands/browse.d.ts +4 -0
  19. package/dist/cli/commands/browse.d.ts.map +1 -0
  20. package/dist/cli/commands/browse.js +80 -0
  21. package/dist/cli/commands/browse.js.map +1 -0
  22. package/dist/cli/commands/chat.d.ts +4 -0
  23. package/dist/cli/commands/chat.d.ts.map +1 -0
  24. package/dist/cli/commands/chat.js +125 -0
  25. package/dist/cli/commands/chat.js.map +1 -0
  26. package/dist/cli/commands/community.d.ts +12 -0
  27. package/dist/cli/commands/community.d.ts.map +1 -0
  28. package/dist/cli/commands/community.js +453 -0
  29. package/dist/cli/commands/community.js.map +1 -0
  30. package/dist/cli/commands/export.d.ts +3 -0
  31. package/dist/cli/commands/export.d.ts.map +1 -0
  32. package/dist/cli/commands/export.js +108 -0
  33. package/dist/cli/commands/export.js.map +1 -0
  34. package/dist/cli/commands/init.d.ts +4 -0
  35. package/dist/cli/commands/init.d.ts.map +1 -0
  36. package/dist/cli/commands/init.js +299 -0
  37. package/dist/cli/commands/init.js.map +1 -0
  38. package/dist/cli/commands/learn.d.ts +4 -0
  39. package/dist/cli/commands/learn.d.ts.map +1 -0
  40. package/dist/cli/commands/learn.js +62 -0
  41. package/dist/cli/commands/learn.js.map +1 -0
  42. package/dist/cli/commands/marketplace.d.ts +9 -0
  43. package/dist/cli/commands/marketplace.d.ts.map +1 -0
  44. package/dist/cli/commands/marketplace.js +321 -0
  45. package/dist/cli/commands/marketplace.js.map +1 -0
  46. package/dist/cli/commands/notify.d.ts +3 -0
  47. package/dist/cli/commands/notify.d.ts.map +1 -0
  48. package/dist/cli/commands/notify.js +59 -0
  49. package/dist/cli/commands/notify.js.map +1 -0
  50. package/dist/cli/commands/operations.d.ts +16 -0
  51. package/dist/cli/commands/operations.d.ts.map +1 -0
  52. package/dist/cli/commands/operations.js +1006 -0
  53. package/dist/cli/commands/operations.js.map +1 -0
  54. package/dist/cli/commands/ping.d.ts +3 -0
  55. package/dist/cli/commands/ping.d.ts.map +1 -0
  56. package/dist/cli/commands/ping.js +56 -0
  57. package/dist/cli/commands/ping.js.map +1 -0
  58. package/dist/cli/commands/today.d.ts +3 -0
  59. package/dist/cli/commands/today.d.ts.map +1 -0
  60. package/dist/cli/commands/today.js +142 -0
  61. package/dist/cli/commands/today.js.map +1 -0
  62. package/dist/cli/commands/types.d.ts +5 -0
  63. package/dist/cli/commands/types.d.ts.map +1 -0
  64. package/dist/cli/commands/types.js +2 -0
  65. package/dist/cli/commands/types.js.map +1 -0
  66. package/dist/cli/commands/watch.d.ts +3 -0
  67. package/dist/cli/commands/watch.d.ts.map +1 -0
  68. package/dist/cli/commands/watch.js +41 -0
  69. package/dist/cli/commands/watch.js.map +1 -0
  70. package/dist/cli/commands/welcome.d.ts +3 -0
  71. package/dist/cli/commands/welcome.d.ts.map +1 -0
  72. package/dist/cli/commands/welcome.js +97 -0
  73. package/dist/cli/commands/welcome.js.map +1 -0
  74. package/dist/cli/commands-meta.d.ts +21 -0
  75. package/dist/cli/commands-meta.d.ts.map +1 -0
  76. package/dist/cli/commands-meta.js +828 -0
  77. package/dist/cli/commands-meta.js.map +1 -0
  78. package/dist/cli/completions-gen.d.ts +2 -0
  79. package/dist/cli/completions-gen.d.ts.map +1 -0
  80. package/dist/cli/completions-gen.js +195 -0
  81. package/dist/cli/completions-gen.js.map +1 -0
  82. package/dist/cli/completions.d.ts +18 -0
  83. package/dist/cli/completions.d.ts.map +1 -0
  84. package/dist/cli/completions.js +227 -0
  85. package/dist/cli/completions.js.map +1 -0
  86. package/dist/cli/flags.d.ts +19 -0
  87. package/dist/cli/flags.d.ts.map +1 -0
  88. package/dist/cli/flags.js +91 -0
  89. package/dist/cli/flags.js.map +1 -0
  90. package/dist/cli/format.d.ts +19 -0
  91. package/dist/cli/format.d.ts.map +1 -0
  92. package/dist/cli/format.js +249 -0
  93. package/dist/cli/format.js.map +1 -0
  94. package/dist/cli/helpers.d.ts +95 -0
  95. package/dist/cli/helpers.d.ts.map +1 -0
  96. package/dist/cli/helpers.js +301 -0
  97. package/dist/cli/helpers.js.map +1 -0
  98. package/dist/cli/mcp-server.d.ts +4 -0
  99. package/dist/cli/mcp-server.d.ts.map +1 -0
  100. package/dist/cli/mcp-server.js +277 -0
  101. package/dist/cli/mcp-server.js.map +1 -0
  102. package/dist/cli/menu.d.ts +7 -0
  103. package/dist/cli/menu.d.ts.map +1 -0
  104. package/dist/cli/menu.js +172 -0
  105. package/dist/cli/menu.js.map +1 -0
  106. package/dist/cli/pages/community.d.ts +9 -0
  107. package/dist/cli/pages/community.d.ts.map +1 -0
  108. package/dist/cli/pages/community.js +1094 -0
  109. package/dist/cli/pages/community.js.map +1 -0
  110. package/dist/cli/pages/helpers.d.ts +37 -0
  111. package/dist/cli/pages/helpers.d.ts.map +1 -0
  112. package/dist/cli/pages/helpers.js +98 -0
  113. package/dist/cli/pages/helpers.js.map +1 -0
  114. package/dist/cli/pages/home.d.ts +4 -0
  115. package/dist/cli/pages/home.d.ts.map +1 -0
  116. package/dist/cli/pages/home.js +231 -0
  117. package/dist/cli/pages/home.js.map +1 -0
  118. package/dist/cli/pages/marketplace.d.ts +5 -0
  119. package/dist/cli/pages/marketplace.d.ts.map +1 -0
  120. package/dist/cli/pages/marketplace.js +583 -0
  121. package/dist/cli/pages/marketplace.js.map +1 -0
  122. package/dist/cli/pages/news.d.ts +31 -0
  123. package/dist/cli/pages/news.d.ts.map +1 -0
  124. package/dist/cli/pages/news.js +688 -0
  125. package/dist/cli/pages/news.js.map +1 -0
  126. package/dist/cli/pages/settings.d.ts +3 -0
  127. package/dist/cli/pages/settings.d.ts.map +1 -0
  128. package/dist/cli/pages/settings.js +296 -0
  129. package/dist/cli/pages/settings.js.map +1 -0
  130. package/dist/cli/pages/types.d.ts +67 -0
  131. package/dist/cli/pages/types.d.ts.map +1 -0
  132. package/dist/cli/pages/types.js +2 -0
  133. package/dist/cli/pages/types.js.map +1 -0
  134. package/dist/cli/prompter.d.ts +135 -0
  135. package/dist/cli/prompter.d.ts.map +1 -0
  136. package/dist/cli/prompter.js +710 -0
  137. package/dist/cli/prompter.js.map +1 -0
  138. package/dist/cli/shell.d.ts +23 -0
  139. package/dist/cli/shell.d.ts.map +1 -0
  140. package/dist/cli/shell.js +1106 -0
  141. package/dist/cli/shell.js.map +1 -0
  142. package/dist/cli/tui.d.ts +7 -0
  143. package/dist/cli/tui.d.ts.map +1 -0
  144. package/dist/cli/tui.js +419 -0
  145. package/dist/cli/tui.js.map +1 -0
  146. package/dist/cli.js +1 -1
  147. package/dist/cli.js.map +1 -1
  148. package/dist/commands.d.ts +14 -0
  149. package/dist/commands.d.ts.map +1 -0
  150. package/dist/commands.js +28 -0
  151. package/dist/commands.js.map +1 -0
  152. package/dist/community/client.d.ts +84 -0
  153. package/dist/community/client.d.ts.map +1 -0
  154. package/dist/community/client.js +340 -0
  155. package/dist/community/client.js.map +1 -0
  156. package/dist/community/search.d.ts +25 -0
  157. package/dist/community/search.d.ts.map +1 -0
  158. package/dist/community/search.js +62 -0
  159. package/dist/community/search.js.map +1 -0
  160. package/dist/community/types.d.ts +71 -0
  161. package/dist/community/types.d.ts.map +1 -0
  162. package/dist/community/types.js +11 -0
  163. package/dist/community/types.js.map +1 -0
  164. package/dist/config.d.ts +1 -7
  165. package/dist/config.d.ts.map +1 -1
  166. package/dist/config.js +0 -32
  167. package/dist/config.js.map +1 -1
  168. package/dist/data.d.ts +1 -1
  169. package/dist/data.d.ts.map +1 -1
  170. package/dist/data.js +778 -40
  171. package/dist/data.js.map +1 -1
  172. package/dist/format.d.ts +5 -39
  173. package/dist/format.d.ts.map +1 -1
  174. package/dist/format.js +5 -120
  175. package/dist/format.js.map +1 -1
  176. package/dist/history.d.ts +13 -0
  177. package/dist/history.d.ts.map +1 -0
  178. package/dist/history.js +37 -0
  179. package/dist/history.js.map +1 -0
  180. package/dist/hubs/cache.d.ts +6 -0
  181. package/dist/hubs/cache.d.ts.map +1 -0
  182. package/dist/hubs/cache.js +46 -0
  183. package/dist/hubs/cache.js.map +1 -0
  184. package/dist/hubs/enrichment.d.ts +43 -0
  185. package/dist/hubs/enrichment.d.ts.map +1 -0
  186. package/dist/hubs/enrichment.js +239 -0
  187. package/dist/hubs/enrichment.js.map +1 -0
  188. package/dist/hubs/github.d.ts +12 -0
  189. package/dist/hubs/github.d.ts.map +1 -0
  190. package/dist/hubs/github.js +54 -0
  191. package/dist/hubs/github.js.map +1 -0
  192. package/dist/hubs/huggingface.d.ts +27 -0
  193. package/dist/hubs/huggingface.d.ts.map +1 -0
  194. package/dist/hubs/huggingface.js +88 -0
  195. package/dist/hubs/huggingface.js.map +1 -0
  196. package/dist/hubs/quality.d.ts +26 -0
  197. package/dist/hubs/quality.d.ts.map +1 -0
  198. package/dist/hubs/quality.js +57 -0
  199. package/dist/hubs/quality.js.map +1 -0
  200. package/dist/hubs/types.d.ts +30 -0
  201. package/dist/hubs/types.d.ts.map +1 -0
  202. package/dist/hubs/types.js +2 -0
  203. package/dist/hubs/types.js.map +1 -0
  204. package/dist/index.d.ts.map +1 -1
  205. package/dist/index.js +188 -224
  206. package/dist/index.js.map +1 -1
  207. package/dist/init.d.ts.map +1 -1
  208. package/dist/init.js +6 -11
  209. package/dist/init.js.map +1 -1
  210. package/dist/live.d.ts +14 -0
  211. package/dist/live.d.ts.map +1 -1
  212. package/dist/live.js +35 -3
  213. package/dist/live.js.map +1 -1
  214. package/dist/marketplace.d.ts +25 -3
  215. package/dist/marketplace.d.ts.map +1 -1
  216. package/dist/marketplace.js +279 -22
  217. package/dist/marketplace.js.map +1 -1
  218. package/dist/news/cache.d.ts +13 -0
  219. package/dist/news/cache.d.ts.map +1 -0
  220. package/dist/news/cache.js +66 -0
  221. package/dist/news/cache.js.map +1 -0
  222. package/dist/news/score.d.ts +4 -0
  223. package/dist/news/score.d.ts.map +1 -0
  224. package/dist/news/score.js +43 -0
  225. package/dist/news/score.js.map +1 -0
  226. package/dist/news/sources/arxiv.d.ts +9 -0
  227. package/dist/news/sources/arxiv.d.ts.map +1 -0
  228. package/dist/news/sources/arxiv.js +107 -0
  229. package/dist/news/sources/arxiv.js.map +1 -0
  230. package/dist/news/sources/github-trending.d.ts +9 -0
  231. package/dist/news/sources/github-trending.d.ts.map +1 -0
  232. package/dist/news/sources/github-trending.js +97 -0
  233. package/dist/news/sources/github-trending.js.map +1 -0
  234. package/dist/news/sources/hn.d.ts +9 -0
  235. package/dist/news/sources/hn.d.ts.map +1 -0
  236. package/dist/news/sources/hn.js +57 -0
  237. package/dist/news/sources/hn.js.map +1 -0
  238. package/dist/news/sources/reddit.d.ts +9 -0
  239. package/dist/news/sources/reddit.d.ts.map +1 -0
  240. package/dist/news/sources/reddit.js +69 -0
  241. package/dist/news/sources/reddit.js.map +1 -0
  242. package/dist/news/sources/rss.d.ts +13 -0
  243. package/dist/news/sources/rss.d.ts.map +1 -0
  244. package/dist/news/sources/rss.js +14 -0
  245. package/dist/news/sources/rss.js.map +1 -0
  246. package/dist/news/types.d.ts +42 -0
  247. package/dist/news/types.d.ts.map +1 -0
  248. package/dist/news/types.js +56 -0
  249. package/dist/news/types.js.map +1 -0
  250. package/dist/preferences.d.ts +14 -0
  251. package/dist/preferences.d.ts.map +1 -0
  252. package/dist/preferences.js +31 -0
  253. package/dist/preferences.js.map +1 -0
  254. package/dist/settings.d.ts +26 -0
  255. package/dist/settings.d.ts.map +1 -0
  256. package/dist/settings.js +251 -0
  257. package/dist/settings.js.map +1 -0
  258. package/dist/state.d.ts +9 -2
  259. package/dist/state.d.ts.map +1 -1
  260. package/dist/state.js +41 -19
  261. package/dist/state.js.map +1 -1
  262. package/dist/transcript.d.ts +28 -0
  263. package/dist/transcript.d.ts.map +1 -0
  264. package/dist/transcript.js +79 -0
  265. package/dist/transcript.js.map +1 -0
  266. package/dist/types.d.ts +19 -1
  267. package/dist/types.d.ts.map +1 -1
  268. package/dist/ui.d.ts +157 -0
  269. package/dist/ui.d.ts.map +1 -0
  270. package/dist/ui.js +296 -0
  271. package/dist/ui.js.map +1 -0
  272. package/package.json +11 -9
  273. package/dist/api.d.ts +0 -72
  274. package/dist/api.d.ts.map +0 -1
  275. package/dist/api.js +0 -109
  276. package/dist/api.js.map +0 -1
  277. package/dist/logger.d.ts +0 -20
  278. package/dist/logger.d.ts.map +0 -1
  279. package/dist/logger.js +0 -59
  280. package/dist/logger.js.map +0 -1
@@ -0,0 +1,710 @@
1
+ /**
2
+ * Raw-mode line editor with tab completion, ghost-text suggestions, and
3
+ * history navigation. The public surface is readLine(); the internal state
4
+ * machine is exposed via applyKeyEvent() for unit testing without I/O.
5
+ */
6
+ export function makeInitialState(history) {
7
+ return {
8
+ line: '',
9
+ cursor: 0,
10
+ history,
11
+ historyIndex: history.length,
12
+ draft: '',
13
+ ghost: null,
14
+ mode: 'normal',
15
+ searchQuery: '',
16
+ searchIndex: -1,
17
+ tabCycle: null
18
+ };
19
+ }
20
+ // ── Pure state machine ──────────────────────────────────────────────────────
21
+ export function applyKeyEvent(state, event, opts = {}) {
22
+ // Reverse-i-search mode has its own dispatch
23
+ if (state.mode === 'reverse-search') {
24
+ return applySearchEvent(state, event);
25
+ }
26
+ switch (event.kind) {
27
+ case 'ctrl-c':
28
+ return { state, output: { kind: 'abort' } };
29
+ case 'ctrl-d': {
30
+ if (state.line === '') {
31
+ return { state, output: { kind: 'eof' } };
32
+ }
33
+ // Forward delete
34
+ const chars = Array.from(state.line);
35
+ if (state.cursor < chars.length) {
36
+ chars.splice(state.cursor, 1);
37
+ const next = withGhost({ ...state, line: chars.join('') }, opts);
38
+ return { state: next };
39
+ }
40
+ return { state };
41
+ }
42
+ case 'enter': {
43
+ const next = { ...state, ghost: null, tabCycle: null };
44
+ return { state: next, output: { kind: 'line', value: state.line } };
45
+ }
46
+ case 'char': {
47
+ const chars = Array.from(state.line);
48
+ chars.splice(state.cursor, 0, ...Array.from(event.data));
49
+ const newCursor = state.cursor + Array.from(event.data).length;
50
+ const newLine = chars.join('');
51
+ const next = withGhost({ ...state, line: newLine, cursor: newCursor, tabCycle: null }, opts);
52
+ // Auto-complete: show slash-command completions as soon as '/' is typed
53
+ if (newLine.startsWith('/') && opts.completer) {
54
+ const result = opts.completer(newLine, newCursor);
55
+ if (result.matches.length > 0) {
56
+ return {
57
+ state: next,
58
+ sideEffect: 'show-completions',
59
+ completionsToShow: result.matches.slice(0, 6)
60
+ };
61
+ }
62
+ }
63
+ return { state: next };
64
+ }
65
+ case 'backspace': {
66
+ if (state.cursor === 0)
67
+ return { state };
68
+ const chars = Array.from(state.line);
69
+ chars.splice(state.cursor - 1, 1);
70
+ const newLine = chars.join('');
71
+ const next = withGhost({ ...state, line: newLine, cursor: state.cursor - 1, tabCycle: null }, opts);
72
+ // Auto-complete: update slash-command completions when backspacing inside a slash prefix
73
+ if (newLine.startsWith('/') && opts.completer) {
74
+ const result = opts.completer(newLine, Math.min(newLine.length, state.cursor - 1));
75
+ if (result.matches.length > 0) {
76
+ return {
77
+ state: next,
78
+ sideEffect: 'show-completions',
79
+ completionsToShow: result.matches.slice(0, 6)
80
+ };
81
+ }
82
+ }
83
+ return { state: next };
84
+ }
85
+ case 'left': {
86
+ const next = { ...state, cursor: Math.max(0, state.cursor - 1), tabCycle: null };
87
+ return { state: next };
88
+ }
89
+ case 'right': {
90
+ const chars = Array.from(state.line);
91
+ if (state.cursor >= chars.length && state.ghost) {
92
+ // Accept ghost
93
+ const newLine = state.line + state.ghost;
94
+ const next = withGhost({ ...state, line: newLine, cursor: newLine.length, ghost: null, tabCycle: null }, opts);
95
+ return { state: next };
96
+ }
97
+ const next = {
98
+ ...state,
99
+ cursor: Math.min(chars.length, state.cursor + 1),
100
+ tabCycle: null
101
+ };
102
+ return { state: next };
103
+ }
104
+ case 'ctrl-f': {
105
+ const chars = Array.from(state.line);
106
+ if (state.cursor >= chars.length && state.ghost) {
107
+ const newLine = state.line + state.ghost;
108
+ const next = withGhost({ ...state, line: newLine, cursor: newLine.length, ghost: null, tabCycle: null }, opts);
109
+ return { state: next };
110
+ }
111
+ const next = {
112
+ ...state,
113
+ cursor: Math.min(chars.length, state.cursor + 1),
114
+ tabCycle: null
115
+ };
116
+ return { state: next };
117
+ }
118
+ case 'home': {
119
+ return { state: { ...state, cursor: 0, tabCycle: null } };
120
+ }
121
+ case 'end': {
122
+ return { state: { ...state, cursor: Array.from(state.line).length, tabCycle: null } };
123
+ }
124
+ case 'up': {
125
+ if (state.historyIndex <= 0)
126
+ return { state };
127
+ const saveDraft = state.historyIndex === state.history.length ? state.line : state.draft;
128
+ const newIndex = state.historyIndex - 1;
129
+ const newLine = state.history[newIndex] ?? '';
130
+ const next = withGhost({
131
+ ...state,
132
+ line: newLine,
133
+ cursor: Array.from(newLine).length,
134
+ historyIndex: newIndex,
135
+ draft: saveDraft,
136
+ tabCycle: null
137
+ }, opts);
138
+ return { state: next };
139
+ }
140
+ case 'down': {
141
+ if (state.historyIndex >= state.history.length)
142
+ return { state };
143
+ const newIndex = state.historyIndex + 1;
144
+ const newLine = newIndex === state.history.length ? state.draft : (state.history[newIndex] ?? '');
145
+ const next = withGhost({
146
+ ...state,
147
+ line: newLine,
148
+ cursor: Array.from(newLine).length,
149
+ historyIndex: newIndex,
150
+ tabCycle: null
151
+ }, opts);
152
+ return { state: next };
153
+ }
154
+ case 'ctrl-l': {
155
+ return { state, sideEffect: 'clear-screen' };
156
+ }
157
+ case 'ctrl-r': {
158
+ const searchIdx = findPrevMatch(state.history, state.searchQuery, state.history.length - 1);
159
+ return {
160
+ state: {
161
+ ...state,
162
+ mode: 'reverse-search',
163
+ searchQuery: '',
164
+ searchIndex: searchIdx,
165
+ tabCycle: null
166
+ }
167
+ };
168
+ }
169
+ case 'ctrl-w': {
170
+ // Delete word backward
171
+ const chars = Array.from(state.line);
172
+ let pos = state.cursor;
173
+ // Skip trailing spaces
174
+ while (pos > 0 && chars[pos - 1] === ' ')
175
+ pos -= 1;
176
+ // Delete word chars
177
+ while (pos > 0 && chars[pos - 1] !== ' ')
178
+ pos -= 1;
179
+ chars.splice(pos, state.cursor - pos);
180
+ const next = withGhost({ ...state, line: chars.join(''), cursor: pos, tabCycle: null }, opts);
181
+ return { state: next };
182
+ }
183
+ case 'ctrl-u': {
184
+ const chars = Array.from(state.line);
185
+ chars.splice(0, state.cursor);
186
+ const next = withGhost({ ...state, line: chars.join(''), cursor: 0, tabCycle: null }, opts);
187
+ return { state: next };
188
+ }
189
+ case 'ctrl-k': {
190
+ const chars = Array.from(state.line);
191
+ const newLine = chars.slice(0, state.cursor).join('');
192
+ const next = withGhost({ ...state, line: newLine, tabCycle: null }, opts);
193
+ return { state: next };
194
+ }
195
+ case 'esc': {
196
+ return { state: { ...state, ghost: null, tabCycle: null } };
197
+ }
198
+ case 'tab': {
199
+ if (!opts.completer)
200
+ return { state };
201
+ // If there's an active tabCycle, cycle through its stored matches
202
+ if (state.tabCycle && state.tabCycle.matches.length > 1) {
203
+ const nextIndex = (state.tabCycle.index + 1) % state.tabCycle.matches.length;
204
+ const match = state.tabCycle.matches[nextIndex];
205
+ const newLine = spliceMatch(state.line, state.tabCycle.replaceFrom, state.cursor, match);
206
+ const next = withGhost({
207
+ ...state,
208
+ line: newLine,
209
+ cursor: state.tabCycle.replaceFrom + Array.from(match).length,
210
+ tabCycle: { ...state.tabCycle, index: nextIndex }
211
+ }, opts);
212
+ return { state: next };
213
+ }
214
+ const result = opts.completer(state.line, state.cursor);
215
+ if (result.matches.length === 0)
216
+ return { state };
217
+ if (result.matches.length === 1) {
218
+ const match = result.matches[0];
219
+ const newLine = spliceMatch(state.line, result.replaceFrom, state.cursor, match);
220
+ const newCursor = result.replaceFrom + Array.from(match).length;
221
+ const next = withGhost({ ...state, line: newLine, cursor: newCursor, tabCycle: null }, opts);
222
+ return { state: next };
223
+ }
224
+ // Multiple matches: show list, replace with longest common prefix
225
+ const lcp = longestCommonPrefix(result.matches);
226
+ const newLine = spliceMatch(state.line, result.replaceFrom, state.cursor, lcp);
227
+ const newCursor = result.replaceFrom + Array.from(lcp).length;
228
+ const next = withGhost({
229
+ ...state,
230
+ line: newLine,
231
+ cursor: newCursor,
232
+ tabCycle: { matches: result.matches, index: -1, replaceFrom: result.replaceFrom }
233
+ }, opts);
234
+ return {
235
+ state: next,
236
+ sideEffect: 'show-completions',
237
+ completionsToShow: result.matches.slice(0, 6)
238
+ };
239
+ }
240
+ default:
241
+ return { state };
242
+ }
243
+ }
244
+ // ── Reverse-i-search ─────────────────────────────────────────────────────────
245
+ function applySearchEvent(state, event) {
246
+ if (event.kind === 'esc') {
247
+ return {
248
+ state: {
249
+ ...state,
250
+ mode: 'normal',
251
+ searchQuery: '',
252
+ searchIndex: -1
253
+ }
254
+ };
255
+ }
256
+ if (event.kind === 'enter') {
257
+ const matched = state.searchIndex >= 0 ? (state.history[state.searchIndex] ?? '') : state.line;
258
+ return {
259
+ state: {
260
+ ...state,
261
+ mode: 'normal',
262
+ line: matched,
263
+ cursor: Array.from(matched).length,
264
+ searchQuery: '',
265
+ searchIndex: -1
266
+ },
267
+ output: { kind: 'line', value: matched }
268
+ };
269
+ }
270
+ if (event.kind === 'ctrl-r') {
271
+ // Cycle to older match
272
+ const startFrom = state.searchIndex > 0 ? state.searchIndex - 1 : state.history.length - 1;
273
+ const nextIdx = findPrevMatch(state.history, state.searchQuery, startFrom);
274
+ return { state: { ...state, searchIndex: nextIdx } };
275
+ }
276
+ if (event.kind === 'ctrl-c') {
277
+ return {
278
+ state: { ...state, mode: 'normal', searchQuery: '', searchIndex: -1 },
279
+ output: { kind: 'abort' }
280
+ };
281
+ }
282
+ if (event.kind === 'backspace') {
283
+ const newQuery = state.searchQuery.slice(0, -1);
284
+ const idx = findPrevMatch(state.history, newQuery, state.history.length - 1);
285
+ return { state: { ...state, searchQuery: newQuery, searchIndex: idx } };
286
+ }
287
+ if (event.kind === 'char') {
288
+ const newQuery = state.searchQuery + event.data;
289
+ const idx = findPrevMatch(state.history, newQuery, state.history.length - 1);
290
+ return { state: { ...state, searchQuery: newQuery, searchIndex: idx } };
291
+ }
292
+ return { state };
293
+ }
294
+ function findPrevMatch(history, query, startFrom) {
295
+ if (!query)
296
+ return -1;
297
+ for (let i = startFrom; i >= 0; i -= 1) {
298
+ if (history[i].includes(query))
299
+ return i;
300
+ }
301
+ return -1;
302
+ }
303
+ // ── Helpers ──────────────────────────────────────────────────────────────────
304
+ function withGhost(state, opts) {
305
+ if (!opts.ghostSuggester)
306
+ return { ...state, ghost: null };
307
+ const ghost = opts.ghostSuggester(state.line, state.history);
308
+ return { ...state, ghost };
309
+ }
310
+ function spliceMatch(line, replaceFrom, cursor, match) {
311
+ const chars = Array.from(line);
312
+ chars.splice(replaceFrom, cursor - replaceFrom, ...Array.from(match));
313
+ return chars.join('');
314
+ }
315
+ function longestCommonPrefix(strs) {
316
+ if (strs.length === 0)
317
+ return '';
318
+ let prefix = strs[0];
319
+ for (let i = 1; i < strs.length; i += 1) {
320
+ while (!strs[i].startsWith(prefix)) {
321
+ prefix = prefix.slice(0, -1);
322
+ if (!prefix)
323
+ return '';
324
+ }
325
+ }
326
+ return prefix;
327
+ }
328
+ // ── Rendering helpers ────────────────────────────────────────────────────────
329
+ /** Visible cell width of a string with ANSI CSI escape sequences stripped. */
330
+ export function visibleWidth(s) {
331
+ // eslint-disable-next-line no-control-regex
332
+ return Array.from(s.replace(/\x1b\[[0-9;]*m/g, '')).length;
333
+ }
334
+ const EMPTY_FRAME_POSITION = { totalRows: 0, cursorRow: 0, promptRows: 0 };
335
+ /**
336
+ * Returns the ANSI bytes that paint a fresh prompt frame and the position
337
+ * the new frame occupies. Pass `width` (terminal columns) and `prev` (the
338
+ * previous frame's position) so the renderer can erase exactly the rows
339
+ * that were previously drawn — including any that wrapped past the
340
+ * declared width. With `width=Infinity` (default) the wrapping math
341
+ * collapses to legacy single-row behaviour and the only redraw side-effect
342
+ * is the leading `\r\x1b[K` that clears the current row.
343
+ */
344
+ export function renderPromptFrame(state, prompt, footer, width = Infinity, prev = EMPTY_FRAME_POSITION) {
345
+ const safeWidth = Number.isFinite(width) && width > 0 ? width : Infinity;
346
+ const wrapRows = (text) => {
347
+ if (!Number.isFinite(safeWidth))
348
+ return 1;
349
+ const w = visibleWidth(text);
350
+ return Math.max(1, Math.ceil(Math.max(1, w) / safeWidth));
351
+ };
352
+ const chars = Array.from(state.line);
353
+ const before = chars.slice(0, state.cursor).join('');
354
+ const after = chars.slice(state.cursor).join('');
355
+ const ghostAnsi = state.ghost ? `\x1b[2m${state.ghost}\x1b[0m` : '';
356
+ const suffix = after + ghostAnsi;
357
+ const promptVisible = visibleWidth(prompt);
358
+ const beforeLen = Array.from(before).length;
359
+ const afterLen = Array.from(after).length;
360
+ const ghostLen = Array.from(state.ghost ?? '').length;
361
+ const promptTotalVisible = promptVisible + beforeLen + afterLen + ghostLen;
362
+ const promptRows = Number.isFinite(safeWidth)
363
+ ? Math.max(1, Math.ceil(Math.max(1, promptTotalVisible) / safeWidth))
364
+ : 1;
365
+ const cursorLogicalCol = promptVisible + beforeLen;
366
+ const cursorRow = Number.isFinite(safeWidth) ? Math.floor(cursorLogicalCol / safeWidth) : 0;
367
+ const cursorCol = Number.isFinite(safeWidth) ? cursorLogicalCol % safeWidth : cursorLogicalCol;
368
+ const footerLines = footer ? footer.split('\n') : [];
369
+ const footerRowsPhysical = footerLines.reduce((sum, line) => sum + wrapRows(line), 0);
370
+ const totalRows = promptRows + footerRowsPhysical;
371
+ let frame = '';
372
+ if (prev.totalRows > 0) {
373
+ // Erase the previous frame: go to its top-left corner, then clear from
374
+ // there to the end of the screen. This wipes any wrapped continuation
375
+ // rows that single-row `\r\x1b[K` would have missed.
376
+ frame += '\r';
377
+ if (prev.cursorRow > 0)
378
+ frame += `\x1b[${prev.cursorRow}A`;
379
+ frame += '\x1b[J';
380
+ }
381
+ else {
382
+ // First render: assume the row we're on may have stale content and clear it.
383
+ frame += '\r\x1b[K';
384
+ }
385
+ frame += prompt + before + suffix;
386
+ if (footer) {
387
+ for (const line of footerLines) {
388
+ frame += '\n\r\x1b[K' + line;
389
+ }
390
+ }
391
+ // After writing, terminal cursor sits at the end of the last printed text.
392
+ // `\r` brings it to col 0 of the row it ended up on (the last physical row
393
+ // of the last logical line). Move up to the target cursor row, then right.
394
+ frame += '\r';
395
+ const endRow = totalRows - 1;
396
+ const moveUp = endRow - cursorRow;
397
+ if (moveUp > 0)
398
+ frame += `\x1b[${moveUp}A`;
399
+ if (cursorCol > 0)
400
+ frame += `\x1b[${cursorCol}C`;
401
+ return {
402
+ frame,
403
+ position: { totalRows, cursorRow, promptRows }
404
+ };
405
+ }
406
+ function renderSearch(state, _prompt) {
407
+ const matched = state.searchIndex >= 0 ? (state.history[state.searchIndex] ?? '') : '';
408
+ return `\r\x1b[K(reverse-i-search)\`${state.searchQuery}\`: ${matched}`;
409
+ }
410
+ // ── Main readLine ────────────────────────────────────────────────────────────
411
+ export async function readLine(opts) {
412
+ const out = opts.out ?? process.stdout;
413
+ const inp = opts.in ?? process.stdin;
414
+ return new Promise((resolve) => {
415
+ let state = makeInitialState(opts.history);
416
+ // Compute initial ghost
417
+ if (opts.ghostSuggester) {
418
+ state = { ...state, ghost: opts.ghostSuggester('', opts.history) };
419
+ }
420
+ // Emit initial prompt (with suffix computed for empty initial line)
421
+ out.write(opts.prompt + (opts.promptSuffix ? opts.promptSuffix('') : ''));
422
+ // Accumulated escape sequence buffer
423
+ let escBuf = '';
424
+ let escTimer = null;
425
+ // Paste detection: when characters arrive faster than 50ms apart,
426
+ // treat newlines as literal inserts instead of submission.
427
+ let lastCharTime = 0;
428
+ let pasteMode = false;
429
+ function flushEsc() {
430
+ if (escTimer) {
431
+ clearTimeout(escTimer);
432
+ escTimer = null;
433
+ }
434
+ if (!escBuf)
435
+ return;
436
+ // If the buffer is just a bare \x1b, it means a new escape byte
437
+ // interrupted an incomplete sequence. Discard silently instead of
438
+ // dispatching a false Esc keypress that could dismiss ghost suggestions.
439
+ if (escBuf === '\x1b') {
440
+ escBuf = '';
441
+ return;
442
+ }
443
+ const seq = escBuf;
444
+ escBuf = '';
445
+ dispatchEscSequence(seq);
446
+ }
447
+ function dispatchEscSequence(seq) {
448
+ // Arrow keys and navigation
449
+ if (seq === '\x1b[A' || seq === '\x1bOA')
450
+ return dispatchEvent({ kind: 'up' });
451
+ if (seq === '\x1b[B' || seq === '\x1bOB')
452
+ return dispatchEvent({ kind: 'down' });
453
+ if (seq === '\x1b[C' || seq === '\x1bOC')
454
+ return dispatchEvent({ kind: 'right' });
455
+ if (seq === '\x1b[D' || seq === '\x1bOD')
456
+ return dispatchEvent({ kind: 'left' });
457
+ if (seq === '\x1b[H' || seq === '\x1bOH')
458
+ return dispatchEvent({ kind: 'home' });
459
+ if (seq === '\x1b[F' || seq === '\x1bOF')
460
+ return dispatchEvent({ kind: 'end' });
461
+ // Bare Esc
462
+ if (seq === '\x1b')
463
+ return dispatchEvent({ kind: 'esc' });
464
+ }
465
+ // Position of the last redraw; threaded into the next call so we can
466
+ // clear exactly the rows we drew (including wrapped continuations).
467
+ let framePos = EMPTY_FRAME_POSITION;
468
+ // Completion hints to show in the footer area on next redraw
469
+ let pendingCompletions = null;
470
+ function dispatchEvent(event) {
471
+ const result = applyKeyEvent(state, event, {
472
+ completer: opts.completer,
473
+ ghostSuggester: opts.ghostSuggester
474
+ });
475
+ state = result.state;
476
+ if (result.sideEffect === 'clear-screen') {
477
+ out.write('\x1b[2J\x1b[H');
478
+ }
479
+ if (result.sideEffect === 'show-completions' && result.completionsToShow) {
480
+ pendingCompletions = result.completionsToShow;
481
+ }
482
+ redraw();
483
+ if (result.output) {
484
+ cleanup();
485
+ resolve(result.output);
486
+ }
487
+ }
488
+ function redraw() {
489
+ if (state.mode === 'reverse-search') {
490
+ framePos = EMPTY_FRAME_POSITION;
491
+ pendingCompletions = null;
492
+ out.write(renderSearch(state, opts.prompt));
493
+ }
494
+ else {
495
+ const suffix = opts.promptSuffix ? opts.promptSuffix(state.line) : '';
496
+ const footerBase = opts.footer ? opts.footer(state.line) : '';
497
+ const parts = [footerBase];
498
+ if (pendingCompletions && pendingCompletions.length > 0) {
499
+ parts.push(`\x1b[2m${pendingCompletions.slice(0, 6).join(' · ')}\x1b[0m`);
500
+ pendingCompletions = null;
501
+ }
502
+ const footer = parts.filter(Boolean).join('\n');
503
+ const width = typeof out.columns === 'number'
504
+ ? out.columns
505
+ : Infinity;
506
+ const result = renderPromptFrame(state, opts.prompt + suffix, footer, width, framePos);
507
+ framePos = result.position;
508
+ out.write(result.frame);
509
+ }
510
+ }
511
+ function onData(chunk) {
512
+ // Defensive: if a previous consumer left stdin in string-encoding mode
513
+ // (e.g. setEncoding('utf8') uncleared), `chunk` is a string and
514
+ // `[...string]` yields chars, not bytes. Normalise to Buffer so the
515
+ // byte-level switch logic below stays correct.
516
+ const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
517
+ const bytes = [...buf];
518
+ let i = 0;
519
+ // If more than 200ms since last character data, exit paste mode
520
+ if (pasteMode && lastCharTime > 0 && Date.now() - lastCharTime > 200) {
521
+ pasteMode = false;
522
+ }
523
+ while (i < bytes.length) {
524
+ const b = bytes[i];
525
+ // Escape or continuation of escape sequence
526
+ if (b === 0x1b || escBuf) {
527
+ flushEsc();
528
+ // Start new escape sequence
529
+ escBuf = String.fromCharCode(b);
530
+ // Collect more bytes greedily (up to 8)
531
+ let j = i + 1;
532
+ while (j < bytes.length && j - i < 8) {
533
+ const nb = bytes[j];
534
+ // Stop at printable ASCII that signals end of sequence (letters, ~)
535
+ escBuf += String.fromCharCode(nb);
536
+ j += 1;
537
+ if (nb >= 0x40 &&
538
+ nb <= 0x7e // final byte of CSI
539
+ ) {
540
+ // Don't break on '[' (0x5B) — it introduces CSI sequences,
541
+ // so there must be at least one more byte coming.
542
+ if (nb === 0x5b)
543
+ continue;
544
+ break;
545
+ }
546
+ }
547
+ i = j;
548
+ // Dispatch immediately if we have a complete sequence
549
+ if (escBuf.length > 1) {
550
+ const seq = escBuf;
551
+ escBuf = '';
552
+ dispatchEscSequence(seq);
553
+ }
554
+ else {
555
+ // Wait a moment for follow-up bytes (Esc alone vs. Esc[...)
556
+ escTimer = setTimeout(() => {
557
+ const s = escBuf;
558
+ escBuf = '';
559
+ dispatchEscSequence(s || '\x1b');
560
+ }, 10);
561
+ }
562
+ continue;
563
+ }
564
+ // Control codes
565
+ if (b === 0x03) {
566
+ dispatchEvent({ kind: 'ctrl-c' });
567
+ i++;
568
+ continue;
569
+ }
570
+ if (b === 0x04) {
571
+ dispatchEvent({ kind: 'ctrl-d' });
572
+ i++;
573
+ continue;
574
+ }
575
+ if (b === 0x01) {
576
+ dispatchEvent({ kind: 'home' });
577
+ i++;
578
+ continue;
579
+ }
580
+ if (b === 0x05) {
581
+ dispatchEvent({ kind: 'end' });
582
+ i++;
583
+ continue;
584
+ }
585
+ if (b === 0x06) {
586
+ dispatchEvent({ kind: 'ctrl-f' });
587
+ i++;
588
+ continue;
589
+ }
590
+ if (b === 0x07) {
591
+ i++;
592
+ continue;
593
+ } // bell, ignore
594
+ if (b === 0x08 || b === 0x7f) {
595
+ dispatchEvent({ kind: 'backspace' });
596
+ i++;
597
+ continue;
598
+ }
599
+ if (b === 0x09) {
600
+ dispatchEvent({ kind: 'tab' });
601
+ i++;
602
+ continue;
603
+ }
604
+ if (b === 0x0a || b === 0x0d) {
605
+ // In paste mode, insert literal newline instead of submitting
606
+ if (pasteMode) {
607
+ const chars = Array.from(state.line);
608
+ chars.splice(state.cursor, 0, '\n');
609
+ const newLine = chars.join('');
610
+ state = withGhost({ ...state, line: newLine, cursor: state.cursor + 1, tabCycle: null }, { ghostSuggester: opts.ghostSuggester });
611
+ redraw();
612
+ i++;
613
+ continue;
614
+ }
615
+ dispatchEvent({ kind: 'enter' });
616
+ i++;
617
+ continue;
618
+ }
619
+ if (b === 0x0b) {
620
+ dispatchEvent({ kind: 'ctrl-k' });
621
+ i++;
622
+ continue;
623
+ }
624
+ if (b === 0x0c) {
625
+ dispatchEvent({ kind: 'ctrl-l' });
626
+ i++;
627
+ continue;
628
+ }
629
+ if (b === 0x12) {
630
+ dispatchEvent({ kind: 'ctrl-r' });
631
+ i++;
632
+ continue;
633
+ }
634
+ if (b === 0x15) {
635
+ dispatchEvent({ kind: 'ctrl-u' });
636
+ i++;
637
+ continue;
638
+ }
639
+ if (b === 0x17) {
640
+ dispatchEvent({ kind: 'ctrl-w' });
641
+ i++;
642
+ continue;
643
+ }
644
+ // Paste detection: if data chunks arrive < 50ms apart, enter paste mode
645
+ const now = Date.now();
646
+ if (now - lastCharTime < 50 && lastCharTime > 0) {
647
+ pasteMode = true;
648
+ }
649
+ lastCharTime = now;
650
+ // Printable ASCII or UTF-8 multi-byte: batch until next control
651
+ let printable = '';
652
+ while (i < bytes.length) {
653
+ const cb = bytes[i];
654
+ if (cb < 0x20 || cb === 0x7f)
655
+ break;
656
+ if (cb === 0x1b)
657
+ break;
658
+ // UTF-8 multi-byte: pass through
659
+ printable += String.fromCharCode(cb);
660
+ i++;
661
+ }
662
+ if (printable) {
663
+ dispatchEvent({ kind: 'char', data: printable });
664
+ }
665
+ }
666
+ }
667
+ function cleanup() {
668
+ inp.removeListener('data', onData);
669
+ inp.pause();
670
+ if (escTimer)
671
+ clearTimeout(escTimer);
672
+ if (framePos.totalRows > 0) {
673
+ // Wipe the whole frame, re-print the prompt line without footer or
674
+ // ghost text so the submitted input stays visible in the scrollback,
675
+ // and park the cursor on the row immediately below.
676
+ const suffix = opts.promptSuffix ? opts.promptSuffix(state.line) : '';
677
+ let seq = '\r';
678
+ if (framePos.cursorRow > 0)
679
+ seq += `\x1b[${framePos.cursorRow}A`;
680
+ seq += '\x1b[J';
681
+ seq += opts.prompt + suffix + state.line + '\r\n';
682
+ out.write(seq);
683
+ }
684
+ else {
685
+ out.write('\n');
686
+ }
687
+ framePos = EMPTY_FRAME_POSITION;
688
+ if (inp.setRawMode) {
689
+ try {
690
+ inp.setRawMode(false);
691
+ }
692
+ catch {
693
+ /* ignore */
694
+ }
695
+ }
696
+ }
697
+ // Enter raw mode
698
+ if (inp.setRawMode) {
699
+ try {
700
+ inp.setRawMode(true);
701
+ }
702
+ catch {
703
+ /* ignore if not a TTY */
704
+ }
705
+ }
706
+ inp.resume();
707
+ inp.on('data', onData);
708
+ });
709
+ }
710
+ //# sourceMappingURL=prompter.js.map