hoolix 0.0.1-beta.19

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 (263) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +337 -0
  3. package/STABILITY.md +109 -0
  4. package/bin/hoolix.js +50 -0
  5. package/bin/mcp-portal.js +12 -0
  6. package/bin/postinstall.js +61 -0
  7. package/dist/app/contracts.d.ts +105 -0
  8. package/dist/app/contracts.d.ts.map +1 -0
  9. package/dist/app/contracts.js +2 -0
  10. package/dist/app/contracts.js.map +1 -0
  11. package/dist/app/events.d.ts +13 -0
  12. package/dist/app/events.d.ts.map +1 -0
  13. package/dist/app/events.js +13 -0
  14. package/dist/app/events.js.map +1 -0
  15. package/dist/app/services/analytics.d.ts +42 -0
  16. package/dist/app/services/analytics.d.ts.map +1 -0
  17. package/dist/app/services/analytics.js +106 -0
  18. package/dist/app/services/analytics.js.map +1 -0
  19. package/dist/app/services/catalog.d.ts +14 -0
  20. package/dist/app/services/catalog.d.ts.map +1 -0
  21. package/dist/app/services/catalog.js +26 -0
  22. package/dist/app/services/catalog.js.map +1 -0
  23. package/dist/app/services/credentials.d.ts +42 -0
  24. package/dist/app/services/credentials.d.ts.map +1 -0
  25. package/dist/app/services/credentials.js +143 -0
  26. package/dist/app/services/credentials.js.map +1 -0
  27. package/dist/app/services/servers.d.ts +15 -0
  28. package/dist/app/services/servers.d.ts.map +1 -0
  29. package/dist/app/services/servers.js +445 -0
  30. package/dist/app/services/servers.js.map +1 -0
  31. package/dist/catalog/community.d.ts +19 -0
  32. package/dist/catalog/community.d.ts.map +1 -0
  33. package/dist/catalog/community.js +53 -0
  34. package/dist/catalog/community.js.map +1 -0
  35. package/dist/catalog/templates.d.ts +436 -0
  36. package/dist/catalog/templates.d.ts.map +1 -0
  37. package/dist/catalog/templates.js +489 -0
  38. package/dist/catalog/templates.js.map +1 -0
  39. package/dist/commands/audit.d.ts +2 -0
  40. package/dist/commands/audit.d.ts.map +1 -0
  41. package/dist/commands/audit.js +122 -0
  42. package/dist/commands/audit.js.map +1 -0
  43. package/dist/commands/bundle.d.ts +11 -0
  44. package/dist/commands/bundle.d.ts.map +1 -0
  45. package/dist/commands/bundle.js +299 -0
  46. package/dist/commands/bundle.js.map +1 -0
  47. package/dist/commands/clients.d.ts +6 -0
  48. package/dist/commands/clients.d.ts.map +1 -0
  49. package/dist/commands/clients.js +242 -0
  50. package/dist/commands/clients.js.map +1 -0
  51. package/dist/commands/completion.d.ts +18 -0
  52. package/dist/commands/completion.d.ts.map +1 -0
  53. package/dist/commands/completion.js +495 -0
  54. package/dist/commands/completion.js.map +1 -0
  55. package/dist/commands/connect.d.ts +10 -0
  56. package/dist/commands/connect.d.ts.map +1 -0
  57. package/dist/commands/connect.js +463 -0
  58. package/dist/commands/connect.js.map +1 -0
  59. package/dist/commands/create.d.ts +2 -0
  60. package/dist/commands/create.d.ts.map +1 -0
  61. package/dist/commands/create.js +607 -0
  62. package/dist/commands/create.js.map +1 -0
  63. package/dist/commands/delete.d.ts +2 -0
  64. package/dist/commands/delete.d.ts.map +1 -0
  65. package/dist/commands/delete.js +44 -0
  66. package/dist/commands/delete.js.map +1 -0
  67. package/dist/commands/doctor.d.ts +2 -0
  68. package/dist/commands/doctor.d.ts.map +1 -0
  69. package/dist/commands/doctor.js +259 -0
  70. package/dist/commands/doctor.js.map +1 -0
  71. package/dist/commands/export.d.ts +2 -0
  72. package/dist/commands/export.d.ts.map +1 -0
  73. package/dist/commands/export.js +93 -0
  74. package/dist/commands/export.js.map +1 -0
  75. package/dist/commands/gui.d.ts +2 -0
  76. package/dist/commands/gui.d.ts.map +1 -0
  77. package/dist/commands/gui.js +19 -0
  78. package/dist/commands/gui.js.map +1 -0
  79. package/dist/commands/import.d.ts +2 -0
  80. package/dist/commands/import.d.ts.map +1 -0
  81. package/dist/commands/import.js +102 -0
  82. package/dist/commands/import.js.map +1 -0
  83. package/dist/commands/info.d.ts +2 -0
  84. package/dist/commands/info.d.ts.map +1 -0
  85. package/dist/commands/info.js +151 -0
  86. package/dist/commands/info.js.map +1 -0
  87. package/dist/commands/list.d.ts +2 -0
  88. package/dist/commands/list.d.ts.map +1 -0
  89. package/dist/commands/list.js +90 -0
  90. package/dist/commands/list.js.map +1 -0
  91. package/dist/commands/reindex.d.ts +2 -0
  92. package/dist/commands/reindex.d.ts.map +1 -0
  93. package/dist/commands/reindex.js +186 -0
  94. package/dist/commands/reindex.js.map +1 -0
  95. package/dist/commands/rotate.d.ts +2 -0
  96. package/dist/commands/rotate.d.ts.map +1 -0
  97. package/dist/commands/rotate.js +67 -0
  98. package/dist/commands/rotate.js.map +1 -0
  99. package/dist/commands/secrets.d.ts +10 -0
  100. package/dist/commands/secrets.d.ts.map +1 -0
  101. package/dist/commands/secrets.js +293 -0
  102. package/dist/commands/secrets.js.map +1 -0
  103. package/dist/commands/start.d.ts +2 -0
  104. package/dist/commands/start.d.ts.map +1 -0
  105. package/dist/commands/start.js +234 -0
  106. package/dist/commands/start.js.map +1 -0
  107. package/dist/commands/stats.d.ts +2 -0
  108. package/dist/commands/stats.d.ts.map +1 -0
  109. package/dist/commands/stats.js +220 -0
  110. package/dist/commands/stats.js.map +1 -0
  111. package/dist/commands/stop.d.ts +2 -0
  112. package/dist/commands/stop.d.ts.map +1 -0
  113. package/dist/commands/stop.js +24 -0
  114. package/dist/commands/stop.js.map +1 -0
  115. package/dist/commands/templates.d.ts +2 -0
  116. package/dist/commands/templates.d.ts.map +1 -0
  117. package/dist/commands/templates.js +168 -0
  118. package/dist/commands/templates.js.map +1 -0
  119. package/dist/commands/trial.d.ts +2 -0
  120. package/dist/commands/trial.d.ts.map +1 -0
  121. package/dist/commands/trial.js +61 -0
  122. package/dist/commands/trial.js.map +1 -0
  123. package/dist/commands/uninstall.d.ts +2 -0
  124. package/dist/commands/uninstall.d.ts.map +1 -0
  125. package/dist/commands/uninstall.js +114 -0
  126. package/dist/commands/uninstall.js.map +1 -0
  127. package/dist/commands/update.d.ts +2 -0
  128. package/dist/commands/update.d.ts.map +1 -0
  129. package/dist/commands/update.js +104 -0
  130. package/dist/commands/update.js.map +1 -0
  131. package/dist/commands/verify.d.ts +2 -0
  132. package/dist/commands/verify.d.ts.map +1 -0
  133. package/dist/commands/verify.js +301 -0
  134. package/dist/commands/verify.js.map +1 -0
  135. package/dist/core/config.d.ts +25 -0
  136. package/dist/core/config.d.ts.map +1 -0
  137. package/dist/core/config.js +54 -0
  138. package/dist/core/config.js.map +1 -0
  139. package/dist/core/errors.d.ts +25 -0
  140. package/dist/core/errors.d.ts.map +1 -0
  141. package/dist/core/errors.js +53 -0
  142. package/dist/core/errors.js.map +1 -0
  143. package/dist/core/logger.d.ts +3 -0
  144. package/dist/core/logger.d.ts.map +1 -0
  145. package/dist/core/logger.js +12 -0
  146. package/dist/core/logger.js.map +1 -0
  147. package/dist/core/paths.d.ts +15 -0
  148. package/dist/core/paths.d.ts.map +1 -0
  149. package/dist/core/paths.js +51 -0
  150. package/dist/core/paths.js.map +1 -0
  151. package/dist/core/registry.d.ts +474 -0
  152. package/dist/core/registry.d.ts.map +1 -0
  153. package/dist/core/registry.js +186 -0
  154. package/dist/core/registry.js.map +1 -0
  155. package/dist/core/updater.d.ts +16 -0
  156. package/dist/core/updater.d.ts.map +1 -0
  157. package/dist/core/updater.js +317 -0
  158. package/dist/core/updater.js.map +1 -0
  159. package/dist/core/version.d.ts +2 -0
  160. package/dist/core/version.d.ts.map +1 -0
  161. package/dist/core/version.js +3 -0
  162. package/dist/core/version.js.map +1 -0
  163. package/dist/index.d.ts +13 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +270 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/ingestion/chunker.d.ts +13 -0
  168. package/dist/ingestion/chunker.d.ts.map +1 -0
  169. package/dist/ingestion/chunker.js +107 -0
  170. package/dist/ingestion/chunker.js.map +1 -0
  171. package/dist/ingestion/cleaners.d.ts +10 -0
  172. package/dist/ingestion/cleaners.d.ts.map +1 -0
  173. package/dist/ingestion/cleaners.js +61 -0
  174. package/dist/ingestion/cleaners.js.map +1 -0
  175. package/dist/ingestion/detectors.d.ts +5 -0
  176. package/dist/ingestion/detectors.d.ts.map +1 -0
  177. package/dist/ingestion/detectors.js +25 -0
  178. package/dist/ingestion/detectors.js.map +1 -0
  179. package/dist/ingestion/fetchers.d.ts +38 -0
  180. package/dist/ingestion/fetchers.d.ts.map +1 -0
  181. package/dist/ingestion/fetchers.js +296 -0
  182. package/dist/ingestion/fetchers.js.map +1 -0
  183. package/dist/ingestion/github.d.ts +60 -0
  184. package/dist/ingestion/github.d.ts.map +1 -0
  185. package/dist/ingestion/github.js +314 -0
  186. package/dist/ingestion/github.js.map +1 -0
  187. package/dist/ingestion/pipeline.d.ts +3 -0
  188. package/dist/ingestion/pipeline.d.ts.map +1 -0
  189. package/dist/ingestion/pipeline.js +160 -0
  190. package/dist/ingestion/pipeline.js.map +1 -0
  191. package/dist/ingestion/types.d.ts +51 -0
  192. package/dist/ingestion/types.d.ts.map +1 -0
  193. package/dist/ingestion/types.js +2 -0
  194. package/dist/ingestion/types.js.map +1 -0
  195. package/dist/lib/auth.d.ts +2 -0
  196. package/dist/lib/auth.d.ts.map +1 -0
  197. package/dist/lib/auth.js +6 -0
  198. package/dist/lib/auth.js.map +1 -0
  199. package/dist/lib/embedding.d.ts +10 -0
  200. package/dist/lib/embedding.d.ts.map +1 -0
  201. package/dist/lib/embedding.js +21 -0
  202. package/dist/lib/embedding.js.map +1 -0
  203. package/dist/mcp/host.d.ts +16 -0
  204. package/dist/mcp/host.d.ts.map +1 -0
  205. package/dist/mcp/host.js +307 -0
  206. package/dist/mcp/host.js.map +1 -0
  207. package/dist/mcp/proxy-host.d.ts +25 -0
  208. package/dist/mcp/proxy-host.d.ts.map +1 -0
  209. package/dist/mcp/proxy-host.js +393 -0
  210. package/dist/mcp/proxy-host.js.map +1 -0
  211. package/dist/mcp/stdio-host.d.ts +19 -0
  212. package/dist/mcp/stdio-host.d.ts.map +1 -0
  213. package/dist/mcp/stdio-host.js +175 -0
  214. package/dist/mcp/stdio-host.js.map +1 -0
  215. package/dist/process/manager.d.ts +74 -0
  216. package/dist/process/manager.d.ts.map +1 -0
  217. package/dist/process/manager.js +322 -0
  218. package/dist/process/manager.js.map +1 -0
  219. package/dist/rag/models.d.ts +30 -0
  220. package/dist/rag/models.d.ts.map +1 -0
  221. package/dist/rag/models.js +30 -0
  222. package/dist/rag/models.js.map +1 -0
  223. package/dist/rag/store.d.ts +63 -0
  224. package/dist/rag/store.d.ts.map +1 -0
  225. package/dist/rag/store.js +505 -0
  226. package/dist/rag/store.js.map +1 -0
  227. package/dist/rag/types.d.ts +56 -0
  228. package/dist/rag/types.d.ts.map +1 -0
  229. package/dist/rag/types.js +2 -0
  230. package/dist/rag/types.js.map +1 -0
  231. package/dist/sources/plugins.d.ts +25 -0
  232. package/dist/sources/plugins.d.ts.map +1 -0
  233. package/dist/sources/plugins.js +55 -0
  234. package/dist/sources/plugins.js.map +1 -0
  235. package/dist/sources/registry.d.ts +19 -0
  236. package/dist/sources/registry.d.ts.map +1 -0
  237. package/dist/sources/registry.js +183 -0
  238. package/dist/sources/registry.js.map +1 -0
  239. package/dist/sources/types.d.ts +361 -0
  240. package/dist/sources/types.d.ts.map +1 -0
  241. package/dist/sources/types.js +59 -0
  242. package/dist/sources/types.js.map +1 -0
  243. package/dist/tui/index.d.ts +22 -0
  244. package/dist/tui/index.d.ts.map +1 -0
  245. package/dist/tui/index.js +711 -0
  246. package/dist/tui/index.js.map +1 -0
  247. package/dist/ui/format.d.ts +27 -0
  248. package/dist/ui/format.d.ts.map +1 -0
  249. package/dist/ui/format.js +80 -0
  250. package/dist/ui/format.js.map +1 -0
  251. package/dist/ui/help.d.ts +2 -0
  252. package/dist/ui/help.d.ts.map +1 -0
  253. package/dist/ui/help.js +106 -0
  254. package/dist/ui/help.js.map +1 -0
  255. package/dist/web/assets.d.ts +2 -0
  256. package/dist/web/assets.d.ts.map +1 -0
  257. package/dist/web/assets.js +659 -0
  258. package/dist/web/assets.js.map +1 -0
  259. package/dist/web/server.d.ts +9 -0
  260. package/dist/web/server.d.ts.map +1 -0
  261. package/dist/web/server.js +349 -0
  262. package/dist/web/server.js.map +1 -0
  263. package/package.json +105 -0
@@ -0,0 +1,711 @@
1
+ /**
2
+ * Hoolix TUI — polished pure-Node terminal dashboard.
3
+ *
4
+ * Layout:
5
+ * ┌─ header (brand + stats + version) ────────────────────────────────────┐
6
+ * │ Server list (left) │ Detail panel (right) │
7
+ * │ ▶ 1 slug [●RUN :3456] │ Name My Docs │
8
+ * │ 2 other [○STOP] │ Source https://... │
9
+ * ├─────────────────────────────┴──────────────────────────────────────────┤
10
+ * │ Log tail (last N lines of host.log for selected server) │
11
+ * ├────────────────────────────────────────────────────────────────────────┤
12
+ * │ Key help │
13
+ * │ Action status │
14
+ * └────────────────────────────────────────────────────────────────────────┘
15
+ *
16
+ * Constraints (from AGENTS.md):
17
+ * - Pure Node, no Ink/React
18
+ * - Dynamically imported only (TTY/raw-mode guard in index.ts)
19
+ * - Never console.log from library code — only here in the TUI layer
20
+ */
21
+ import fs from 'fs-extra';
22
+ import path from 'node:path';
23
+ import { execSync } from 'node:child_process';
24
+ import { listServers, getServerMetadata } from '../core/registry.js';
25
+ import { serverManager } from '../process/manager.js';
26
+ import { getServerDir } from '../core/paths.js';
27
+ import { logger } from '../core/logger.js';
28
+ import { VERSION } from '../core/version.js';
29
+ import { getServerSourceLabel, reindexServer, verifyServer } from '../app/services/servers.js';
30
+ import { loadCredentials, interpolateRunConfig } from '../app/services/credentials.js';
31
+ import { getTemplate } from '../app/services/catalog.js';
32
+ // ── ANSI helpers ────────────────────────────────────────────────────────────
33
+ const A = {
34
+ reset: '\x1b[0m',
35
+ bold: '\x1b[1m',
36
+ dim: '\x1b[2m',
37
+ green: '\x1b[32m',
38
+ cyan: '\x1b[36m',
39
+ yellow: '\x1b[33m',
40
+ red: '\x1b[31m',
41
+ gray: '\x1b[90m',
42
+ white: '\x1b[97m',
43
+ bgSelected: '\x1b[48;2;15;52;80m', // dark steel-blue for selection
44
+ clear: '\x1b[2J\x1b[H',
45
+ // Brand accent matches CLI ui.accent (hex #7dd3fc → closest 256-color: 117)
46
+ brand: '\x1b[38;5;117m',
47
+ };
48
+ // ── Box-drawing ──────────────────────────────────────────────────────────────
49
+ const B = {
50
+ h: '─', v: '│',
51
+ tl: '┌', tr: '┐', bl: '└', br: '┘',
52
+ ml: '├', mr: '┤', mt: '┬', mb: '┴',
53
+ run: '●',
54
+ stop: '○',
55
+ sel: '▶',
56
+ dot: '·',
57
+ };
58
+ // ── Helpers ──────────────────────────────────────────────────────────────────
59
+ function clip(s, max) {
60
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
61
+ }
62
+ function pad(s, w) {
63
+ return s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length);
64
+ }
65
+ function maskKey(value, visible = 6) {
66
+ if (!value)
67
+ return '';
68
+ if (value.length <= visible * 2)
69
+ return `${value.slice(0, 2)}...`;
70
+ return `${value.slice(0, visible)}...${value.slice(-visible)}`;
71
+ }
72
+ function freshnessLabel(lastUpdatedAt) {
73
+ const updated = Date.parse(lastUpdatedAt);
74
+ if (!Number.isFinite(updated))
75
+ return 'unknown';
76
+ const ageDays = Math.max(0, Math.floor((Date.now() - updated) / 86400000));
77
+ if (ageDays === 0)
78
+ return 'today';
79
+ if (ageDays === 1)
80
+ return '1d old';
81
+ if (ageDays < 14)
82
+ return `${ageDays}d old`;
83
+ if (ageDays < 30)
84
+ return `${ageDays}d (aging)`;
85
+ return `${ageDays}d (stale!)`;
86
+ }
87
+ async function readLastLogLines(slug, n = 6) {
88
+ try {
89
+ const content = await fs.readFile(path.join(getServerDir(slug), 'host.log'), 'utf8');
90
+ const lines = content.trim().split(/\r?\n/);
91
+ return lines.slice(-n).filter(Boolean);
92
+ }
93
+ catch {
94
+ return ['(no host.log yet — start the server to see logs)'];
95
+ }
96
+ }
97
+ async function copyToClipboard(text) {
98
+ try {
99
+ if (process.platform === 'win32') {
100
+ execSync('clip', { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
101
+ return true;
102
+ }
103
+ if (process.platform === 'darwin') {
104
+ execSync('pbcopy', { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
105
+ return true;
106
+ }
107
+ try {
108
+ execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
109
+ return true;
110
+ }
111
+ catch { }
112
+ execSync('wl-copy', { input: text, stdio: ['pipe', 'ignore', 'ignore'] });
113
+ return true;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ // ── Renderer ─────────────────────────────────────────────────────────────────
120
+ const LOG_LINES = 5;
121
+ const MIN_WIDTH = 72;
122
+ const MIN_HEIGHT = 18;
123
+ function buildFrame(state) {
124
+ const W = Math.max(MIN_WIDTH, process.stdout.columns || 80);
125
+ const H = Math.max(MIN_HEIGHT, process.stdout.rows || 24);
126
+ // Column layout: left panel ~40% of width
127
+ const innerW = W - 2; // inside the outer border
128
+ const leftW = Math.min(40, Math.floor(W * 0.42));
129
+ const rightW = innerW - leftW - 1; // −1 for the column divider │
130
+ const out = [];
131
+ // ── Header ────────────────────────────────────────────────────────────────
132
+ const runningCount = Object.values(state.statuses).filter((s) => s.running).length;
133
+ const statsStr = `${state.servers.length} server${state.servers.length !== 1 ? 's' : ''} · ${runningCount} running`;
134
+ const verStr = `v${VERSION}`;
135
+ const brandStr = `◆ hoolix`;
136
+ // Visible header content: ' brandStr statsStr '
137
+ const headerPad = innerW - brandStr.length - 2 - statsStr.length - verStr.length - 2;
138
+ const headerLine = ` ${A.brand}${A.bold}${brandStr}${A.reset} ${A.dim}${statsStr}${A.reset}` +
139
+ ' '.repeat(Math.max(1, headerPad)) +
140
+ `${A.dim}${verStr}${A.reset} `;
141
+ out.push(`${B.tl}${B.h.repeat(innerW)}${B.tr}`);
142
+ out.push(`${B.v}${headerLine}${B.v}`);
143
+ // ── Column divider (after header) ─────────────────────────────────────────
144
+ out.push(`${B.ml}${B.h.repeat(leftW)}${B.mt}${B.h.repeat(rightW)}${B.mr}`);
145
+ // ── Main area: calculate how many rows we have ────────────────────────────
146
+ // Fixed lines: 3 (top border + header + col-divider)
147
+ // + 1 (mid-divider between main and log)
148
+ // + LOG_LINES
149
+ // + 1 (log divider)
150
+ // + 1 (key help)
151
+ // + 1 (action status)
152
+ // + 1 (bottom border)
153
+ const fixedRows = 3 + 1 + LOG_LINES + 1 + 1 + 1 + 1;
154
+ const mainRows = Math.max(3, H - fixedRows);
155
+ // ── Build left-column lines (visible text + colored text tracked separately)
156
+ const leftRaw = [];
157
+ const leftColored = [];
158
+ if (state.servers.length === 0) {
159
+ const r1 = pad(' No servers yet.', leftW);
160
+ const r2 = pad('', leftW);
161
+ const r3 = pad(` ${B.dot} t copy trial command`, leftW);
162
+ const r4 = pad(` ${B.dot} n copy create command`, leftW);
163
+ leftRaw.push(r1, r2, r3, r4);
164
+ leftColored.push(` ${A.dim}No servers yet.${A.reset}` + ' '.repeat(Math.max(0, leftW - 16)), r2, ` ${A.cyan}${B.dot}${A.reset} ${A.dim}t${A.reset} copy trial command` + ' '.repeat(Math.max(0, leftW - 21)), ` ${A.cyan}${B.dot}${A.reset} ${A.dim}n${A.reset} copy create command` + ' '.repeat(Math.max(0, leftW - 22)));
165
+ }
166
+ else {
167
+ for (let i = 0; i < state.servers.length && leftRaw.length < mainRows; i++) {
168
+ const s = state.servers[i];
169
+ const st = state.statuses[s.slug] || { running: false };
170
+ const isSel = i === state.selectedIndex;
171
+ const numStr = `${i + 1}`.padStart(2);
172
+ const dotChar = st.running ? B.run : B.stop;
173
+ const slug = clip(s.slug, leftW - 16).padEnd(Math.min(18, leftW - 16));
174
+ const portStr = st.running && st.port ? `:${st.port}` : '';
175
+ const portPad = portStr.padEnd(7);
176
+ // Raw visible line (for width measurement)
177
+ const rawLine = ` ${isSel ? B.sel : ' '} ${numStr} ${dotChar} ${slug} ${portPad}`;
178
+ const rawPadded = pad(rawLine, leftW);
179
+ leftRaw.push(rawPadded);
180
+ // Colored version
181
+ const selArrow = isSel ? `${A.brand}${B.sel}${A.reset}` : ' ';
182
+ const dotColor = st.running ? `${A.green}${dotChar}${A.reset}` : `${A.gray}${dotChar}${A.reset}`;
183
+ const portColor = st.running && portStr ? `${A.green}${portPad}${A.reset}` : `${A.dim}${portPad}${A.reset}`;
184
+ let colored = ` ${selArrow} ${A.dim}${numStr}${A.reset} ${dotColor} ${slug} ${portColor}`;
185
+ if (isSel) {
186
+ // bg-highlight the entire row
187
+ colored = `${A.bgSelected}${colored}${' '.repeat(Math.max(0, leftW - rawLine.length))}${A.reset}`;
188
+ }
189
+ else {
190
+ colored += ' '.repeat(Math.max(0, leftW - rawLine.length));
191
+ }
192
+ leftColored.push(colored);
193
+ }
194
+ }
195
+ // Pad left column to mainRows
196
+ while (leftRaw.length < mainRows) {
197
+ leftRaw.push(' '.repeat(leftW));
198
+ leftColored.push(' '.repeat(leftW));
199
+ }
200
+ // ── Build right-column lines ───────────────────────────────────────────────
201
+ const rightRaw = [];
202
+ const rightColored = [];
203
+ const sel = state.servers[state.selectedIndex];
204
+ if (sel) {
205
+ const st = state.statuses[sel.slug] || { running: false };
206
+ const isMcpServer = sel.serverKind === 'mcp-server';
207
+ const addRow = (label, value, valueColor = (v) => v) => {
208
+ const l = pad(label, 12);
209
+ const v = clip(value, rightW - 14);
210
+ const vPad = pad(v, rightW - 14);
211
+ rightRaw.push(` ${l} ${vPad}`);
212
+ rightColored.push(` ${A.dim}${l}${A.reset} ${valueColor(vPad)}`);
213
+ };
214
+ const blank = () => {
215
+ rightRaw.push('');
216
+ rightColored.push('');
217
+ };
218
+ addRow('Name', sel.name || sel.slug);
219
+ addRow('Slug', sel.slug);
220
+ if (isMcpServer) {
221
+ // ── mcp-server kind detail ──────────────────────────────────────────
222
+ const templateId = sel.definition?.template?.id ?? '—';
223
+ const templateName = sel.definition?.template?.name ?? templateId;
224
+ const credKeys = sel.credentialKeys ?? [];
225
+ const isProxied = st.running && st.mode === 'proxy';
226
+ addRow('Kind', 'mcp-server');
227
+ addRow('Template', clip(templateName, rightW - 14));
228
+ if (isProxied) {
229
+ addRow('Transport', 'proxy (HTTP)');
230
+ const proxyUrl = `http://127.0.0.1:${st.port}/mcp`;
231
+ addRow('Status', `running (proxy on :${st.port})`, (v) => `${A.green}${v}${A.reset}`);
232
+ rightRaw.push(` ${pad('Proxy URL', 12)} ${clip(proxyUrl, rightW - 14)}`);
233
+ rightColored.push(` ${A.dim}${pad('Proxy URL', 12)}${A.reset} ${A.cyan}${clip(proxyUrl, rightW - 14)}${A.reset}`);
234
+ }
235
+ else if (st.running) {
236
+ addRow('Transport', 'stdio');
237
+ addRow('Status', 'running (stdio)', (v) => `${A.green}${v}${A.reset}`);
238
+ }
239
+ else {
240
+ addRow('Transport', 'stdio');
241
+ addRow('Status', 'stopped (c to copy config)', (v) => `${A.gray}${v}${A.reset}`);
242
+ }
243
+ if (credKeys.length > 0) {
244
+ addRow('Credentials', `${credKeys.length} stored`);
245
+ }
246
+ else {
247
+ addRow('Credentials', 'none stored');
248
+ }
249
+ blank();
250
+ if (isProxied) {
251
+ const hintLine = pad(` ${B.dot} c copy HTTP config · x update secrets · s stop proxy`, rightW);
252
+ rightRaw.push(hintLine);
253
+ rightColored.push(` ${A.dim}${B.dot} Press ${A.reset}${A.cyan}c${A.reset}${A.dim} HTTP config` +
254
+ ` · ${A.reset}${A.cyan}x${A.reset}${A.dim} secrets` +
255
+ ` · ${A.reset}${A.cyan}s${A.reset}${A.dim} stop proxy${A.reset}` +
256
+ ' '.repeat(Math.max(0, rightW - 50)));
257
+ }
258
+ else {
259
+ const hintLine = pad(` ${B.dot} c copy stdio config · x update secrets · s—n/a`, rightW);
260
+ rightRaw.push(hintLine);
261
+ rightColored.push(` ${A.dim}${B.dot} Press ${A.reset}${A.cyan}c${A.reset}${A.dim} copy config` +
262
+ ` · ${A.reset}${A.cyan}x${A.reset}${A.dim} secrets` +
263
+ ` · s—n/a${A.reset}` +
264
+ ' '.repeat(Math.max(0, rightW - 46)));
265
+ }
266
+ }
267
+ else {
268
+ // ── docs-rag kind detail (existing) ────────────────────────────────
269
+ addRow((sel.definition?.sources.length ?? 1) > 1 ? 'Sources' : 'Source', (sel.definition?.sources.length ?? 1) > 1 ? getServerSourceLabel(sel) : (sel.sourceUrl || '—'));
270
+ addRow('Chunks', sel.chunkCount.toLocaleString());
271
+ addRow('Index', sel.embeddingModel === 'fuse' ? 'Fuse.js' : `Hybrid (${sel.embeddingModel})`);
272
+ if (sel.definition?.template)
273
+ addRow('Template', sel.definition.template.name);
274
+ addRow('Fresh', freshnessLabel(sel.lastUpdatedAt));
275
+ addRow('Status', st.running ? `running on :${st.port || '?'}` : 'stopped', (v) => st.running ? `${A.green}${v}${A.reset}` : `${A.gray}${v}${A.reset}`);
276
+ if (st.running) {
277
+ blank();
278
+ const mcpUrl = `http://127.0.0.1:${st.port}/mcp`;
279
+ const urlLine = clip(`URL ${mcpUrl}`, rightW - 2);
280
+ rightRaw.push(` ${pad(urlLine, rightW - 2)}`);
281
+ rightColored.push(` ${A.dim}URL ${A.reset}${A.cyan}${clip(mcpUrl, rightW - 7)}${A.reset}`);
282
+ let authKeyStr = '—';
283
+ try {
284
+ authKeyStr = maskKey(sel.authKey || '');
285
+ }
286
+ catch { }
287
+ addRow('Auth', `Bearer ${authKeyStr}`);
288
+ }
289
+ blank();
290
+ const hintLine = pad(` ${B.dot} Press c to copy MCP config to clipboard`, rightW);
291
+ rightRaw.push(hintLine);
292
+ rightColored.push(` ${A.dim}${B.dot} Press ${A.reset}${A.cyan}c${A.reset}${A.dim} to copy MCP config to clipboard${A.reset}` + ' '.repeat(Math.max(0, rightW - 42)));
293
+ }
294
+ }
295
+ else if (state.servers.length === 0) {
296
+ const lines = [
297
+ ' Start here:',
298
+ '',
299
+ ` 1. hoolix install filesystem /Users/me/projects --yes`,
300
+ ` 2. hoolix install github-api --yes (token prompted)`,
301
+ ` 3. hoolix install brave-search --yes`,
302
+ ` 4. hoolix create "Docs" --url https://.../llms.txt --yes`,
303
+ ` 5. hoolix connect my-files --client claude`,
304
+ ` 6. hoolix client status`,
305
+ '',
306
+ ` ${B.dot} Press t for templates · n for create command.`,
307
+ ];
308
+ for (const l of lines) {
309
+ rightRaw.push(pad(l, rightW));
310
+ rightColored.push(`${A.dim}${pad(l, rightW)}${A.reset}`);
311
+ }
312
+ }
313
+ // Pad right column to mainRows
314
+ while (rightRaw.length < mainRows) {
315
+ rightRaw.push('');
316
+ rightColored.push('');
317
+ }
318
+ // ── Merge columns into rows ────────────────────────────────────────────────
319
+ for (let i = 0; i < mainRows; i++) {
320
+ const lRaw = leftRaw[i] || '';
321
+ const rRaw = rightRaw[i] || '';
322
+ const lCol = leftColored[i] || '';
323
+ const rCol = rightColored[i] || '';
324
+ // Pad raw widths, then substitute colored
325
+ const lPad = lCol + ' '.repeat(Math.max(0, leftW - lRaw.length));
326
+ const rPad = rCol + ' '.repeat(Math.max(0, rightW - rRaw.length));
327
+ out.push(`${B.v}${lPad}${B.v}${rPad}${B.v}`);
328
+ }
329
+ // ── Log divider + tail ────────────────────────────────────────────────────
330
+ const logTitle = sel ? ` Log: ${sel.slug} ` : ' Log ';
331
+ const logLeft = B.h.repeat(3) + logTitle;
332
+ const logRight = B.h.repeat(Math.max(0, innerW - logLeft.length));
333
+ out.push(`${B.ml}${logLeft}${logRight}${B.mr}`);
334
+ for (let i = 0; i < LOG_LINES; i++) {
335
+ const line = state.logTail[i] || '';
336
+ // Strip ANSI from log lines to avoid layout corruption
337
+ const clean = line.replace(/\x1b\[[0-9;]*m/g, '');
338
+ const displayed = clip(clean, innerW - 2);
339
+ const padded = pad(` ${displayed}`, innerW);
340
+ out.push(`${B.v}${A.dim}${padded}${A.reset}${B.v}`);
341
+ }
342
+ // ── Key help bar ──────────────────────────────────────────────────────────
343
+ out.push(`${B.ml}${B.h.repeat(innerW)}${B.mr}`);
344
+ const keyHelp = '↑↓/1-9 select · s start/stop · v verify · c connect · x reindex/secrets · n new · t templates · r refresh · q quit';
345
+ const helpLine = pad(` ${keyHelp}`, innerW);
346
+ out.push(`${B.v}${A.dim}${helpLine}${A.reset}${B.v}`);
347
+ // ── Action status line ────────────────────────────────────────────────────
348
+ const actionRaw = state.actionMsg
349
+ ? clip(state.actionMsg.replace(/\n/g, ' '), innerW - 3)
350
+ : '';
351
+ const actionColor = state.actionIsError ? A.red : A.yellow;
352
+ const actionPad = state.actionMsg
353
+ ? `${A.bold}${actionColor} › ${actionRaw}${A.reset}` + ' '.repeat(Math.max(0, innerW - actionRaw.length - 3))
354
+ : ' '.repeat(innerW);
355
+ out.push(`${B.v}${actionPad}${B.v}`);
356
+ out.push(`${B.bl}${B.h.repeat(innerW)}${B.br}`);
357
+ return A.clear + out.join('\n') + '\n';
358
+ }
359
+ // ── State management ──────────────────────────────────────────────────────────
360
+ async function refresh(state) {
361
+ try {
362
+ state.servers = await listServers();
363
+ state.statuses = {};
364
+ for (const s of state.servers) {
365
+ try {
366
+ state.statuses[s.slug] = await serverManager.getStatus(s.slug);
367
+ }
368
+ catch {
369
+ state.statuses[s.slug] = { running: false };
370
+ }
371
+ }
372
+ state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.servers.length - 1));
373
+ const sel = state.servers[state.selectedIndex];
374
+ state.logTail = sel ? await readLastLogLines(sel.slug) : [];
375
+ }
376
+ catch (e) {
377
+ logger.debug('TUI refresh error', e?.message);
378
+ }
379
+ }
380
+ function render(state) {
381
+ process.stdout.write(buildFrame(state));
382
+ }
383
+ function setAction(state, msg, isError = false) {
384
+ state.actionMsg = msg;
385
+ state.actionIsError = isError;
386
+ }
387
+ // ── Key handlers ──────────────────────────────────────────────────────────────
388
+ async function handleKey(key, state) {
389
+ const { servers, statuses } = state;
390
+ const noServers = servers.length === 0;
391
+ const slug = servers[state.selectedIndex]?.slug;
392
+ // ── Navigation ────────────────────────────────────────────────────────────
393
+ if (/^[1-9]$/.test(key)) {
394
+ const idx = parseInt(key, 10) - 1;
395
+ if (idx < servers.length) {
396
+ state.selectedIndex = idx;
397
+ state.logTail = await readLastLogLines(servers[idx].slug);
398
+ }
399
+ return;
400
+ }
401
+ if (key === '' && !noServers) { // ↑
402
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
403
+ state.logTail = await readLastLogLines(servers[state.selectedIndex].slug);
404
+ return;
405
+ }
406
+ if (key === '' && !noServers) { // ↓
407
+ state.selectedIndex = Math.min(servers.length - 1, state.selectedIndex + 1);
408
+ state.logTail = await readLastLogLines(servers[state.selectedIndex].slug);
409
+ return;
410
+ }
411
+ // ── Global actions ────────────────────────────────────────────────────────
412
+ if (key.toLowerCase() === 'r') {
413
+ setAction(state, 'Refreshing…');
414
+ await refresh(state);
415
+ setAction(state, null);
416
+ return;
417
+ }
418
+ if (key.toLowerCase() === 'n') {
419
+ const cmd = 'hoolix create "My Docs" --url https://example.com/llms.txt --yes';
420
+ const copied = await copyToClipboard(cmd);
421
+ setAction(state, copied ? `Copied: ${cmd}` : `Run: ${cmd}`);
422
+ setTimeout(() => { setAction(state, null); render(state); }, 3000);
423
+ return;
424
+ }
425
+ if (key.toLowerCase() === 't') {
426
+ const cmd = noServers ? 'hoolix trial' : 'hoolix templates list';
427
+ const copied = await copyToClipboard(cmd);
428
+ setAction(state, copied ? `Copied: ${cmd}` : `Run: ${cmd}`);
429
+ setTimeout(() => { setAction(state, null); render(state); }, 3000);
430
+ return;
431
+ }
432
+ // ── Server actions (require a selected server) ────────────────────────────
433
+ if (!slug)
434
+ return;
435
+ if (key.toLowerCase() === 's') {
436
+ // mcp-server kind: stop proxy if running, otherwise hint
437
+ if (servers[state.selectedIndex]?.serverKind === 'mcp-server') {
438
+ const mcpSt = statuses[slug] || { running: false };
439
+ if (mcpSt.running && mcpSt.mode === 'proxy') {
440
+ setAction(state, `Stopping proxy for ${slug}…`);
441
+ render(state);
442
+ try {
443
+ await serverManager.stop(slug);
444
+ setAction(state, `Proxy stopped: ${slug}`);
445
+ }
446
+ catch (e) {
447
+ setAction(state, `Stop failed: ${e?.message || e}`, true);
448
+ }
449
+ await refresh(state);
450
+ setTimeout(() => { setAction(state, null); render(state); }, 2500);
451
+ return;
452
+ }
453
+ setAction(state, `${slug} uses stdio — press c to copy config, or run: hoolix start ${slug} --proxy`);
454
+ setTimeout(() => { setAction(state, null); render(state); }, 3500);
455
+ return;
456
+ }
457
+ const st = statuses[slug] || { running: false };
458
+ if (st.running) {
459
+ setAction(state, `Stopping ${slug}…`);
460
+ render(state);
461
+ try {
462
+ await serverManager.stop(slug);
463
+ setAction(state, `Stopped ${slug}`);
464
+ }
465
+ catch (e) {
466
+ setAction(state, `Stop failed: ${e?.message || e}`, true);
467
+ }
468
+ }
469
+ else {
470
+ setAction(state, `Starting ${slug}…`);
471
+ render(state);
472
+ try {
473
+ const meta = await getServerMetadata(slug);
474
+ const port = 3456 + Math.floor(Math.random() * 400);
475
+ const result = await serverManager.start(slug, { port, authKey: meta.authKey });
476
+ setAction(state, `Started ${slug} on :${result.port}`);
477
+ }
478
+ catch (e) {
479
+ setAction(state, `Start failed: ${e?.message || e}`, true);
480
+ }
481
+ }
482
+ await refresh(state);
483
+ setTimeout(() => { setAction(state, null); render(state); }, 2500);
484
+ return;
485
+ }
486
+ if (key.toLowerCase() === 'v') {
487
+ // mcp-server kind: quick credential count check instead of RAG verify
488
+ if (servers[state.selectedIndex]?.serverKind === 'mcp-server') {
489
+ try {
490
+ const creds = await loadCredentials(slug);
491
+ const count = Object.keys(creds).length;
492
+ setAction(state, `${slug}: mcp-server — ${count} credential(s) stored. Run: hoolix verify ${slug}`);
493
+ }
494
+ catch (e) {
495
+ setAction(state, `${slug}: mcp-server verify — ${e?.message || e}`, true);
496
+ }
497
+ setTimeout(() => { setAction(state, null); render(state); }, 3500);
498
+ return;
499
+ }
500
+ setAction(state, `Verifying ${slug}…`);
501
+ render(state);
502
+ try {
503
+ const report = await verifyServer(slug, ['overview']);
504
+ const top = report.samples[0]?.results[0];
505
+ if (top) {
506
+ setAction(state, `✓ Verify ok — top result: ${top.metadata.url || top.metadata.title || 'hit'}`);
507
+ }
508
+ else {
509
+ setAction(state, 'Verify: no results (index may be empty)', true);
510
+ }
511
+ }
512
+ catch (e) {
513
+ setAction(state, `Verify failed: ${e?.message || e}`, true);
514
+ }
515
+ setTimeout(() => { setAction(state, null); render(state); }, 3500);
516
+ return;
517
+ }
518
+ if (key.toLowerCase() === 'c') {
519
+ try {
520
+ const meta = await getServerMetadata(slug);
521
+ if (meta.serverKind === 'mcp-server') {
522
+ // If proxy is running, emit HTTP config; otherwise stdio config
523
+ const mcpSt = statuses[slug] || {};
524
+ if (mcpSt.running && mcpSt.mode === 'proxy' && mcpSt.port) {
525
+ // Proxy mode: HTTP streamable config
526
+ const payload = {
527
+ mcpServers: {
528
+ [slug]: {
529
+ type: 'streamable-http',
530
+ url: `http://127.0.0.1:${mcpSt.port}/mcp`,
531
+ headers: { Authorization: `Bearer ${meta.authKey}` },
532
+ },
533
+ },
534
+ };
535
+ const copied = await copyToClipboard(JSON.stringify(payload, null, 2));
536
+ const maskedKey = maskKey(meta.authKey);
537
+ setAction(state, copied
538
+ ? `✓ Copied HTTP proxy config for ${slug} (key: ${maskedKey})`
539
+ : `Run: hoolix connect ${slug} --json`);
540
+ }
541
+ else {
542
+ // Build stdio config: load credentials + interpolate run config
543
+ const templateId = meta.definition?.template?.id;
544
+ const template = templateId ? await getTemplate(templateId).catch(() => null) : null;
545
+ const credentials = await loadCredentials(slug);
546
+ const templateInputs = (meta.definition?.template?.inputs ?? {});
547
+ const substitutions = { ...templateInputs, ...credentials };
548
+ const runConfig = template?.server ? interpolateRunConfig(template.server, substitutions) : null;
549
+ const entry = runConfig
550
+ ? {
551
+ command: runConfig.command,
552
+ args: runConfig.args,
553
+ ...(Object.keys(runConfig.env).length > 0 ? { env: runConfig.env } : {}),
554
+ }
555
+ : { command: 'hoolix', args: ['connect', slug] };
556
+ const payload = { mcpServers: { [slug]: entry } };
557
+ const copied = await copyToClipboard(JSON.stringify(payload, null, 2));
558
+ setAction(state, copied
559
+ ? `✓ Copied stdio config for ${slug}`
560
+ : `Run: hoolix connect ${slug} --json`);
561
+ }
562
+ }
563
+ else {
564
+ // docs-rag: existing HTTP streamable flow
565
+ const st = statuses[slug] || {};
566
+ const port = st.port || 3456;
567
+ const payload = {
568
+ mcpServers: {
569
+ [slug]: {
570
+ type: 'streamable-http',
571
+ url: `http://127.0.0.1:${port}/mcp`,
572
+ headers: { Authorization: `Bearer ${meta.authKey}` },
573
+ },
574
+ },
575
+ };
576
+ const copied = await copyToClipboard(JSON.stringify(payload, null, 2));
577
+ const maskedKey = maskKey(meta.authKey);
578
+ setAction(state, copied
579
+ ? `✓ Copied MCP config for ${slug} (key: ${maskedKey})`
580
+ : `MCP config for ${slug} — copy manually from \`hoolix connect ${slug} --json\``);
581
+ }
582
+ }
583
+ catch (e) {
584
+ setAction(state, `Failed to build config: ${e?.message || e}`, true);
585
+ }
586
+ setTimeout(() => { setAction(state, null); render(state); }, 3500);
587
+ return;
588
+ }
589
+ if (key.toLowerCase() === 'x') {
590
+ // mcp-server kind: redirect to secrets instead of reindex
591
+ if (servers[state.selectedIndex]?.serverKind === 'mcp-server') {
592
+ const cmd = `hoolix secrets set ${slug} <key> <value>`;
593
+ await copyToClipboard(`hoolix secrets list ${slug}`);
594
+ setAction(state, `mcp-server — to rotate credentials: ${cmd}`);
595
+ setTimeout(() => { setAction(state, null); render(state); }, 4000);
596
+ return;
597
+ }
598
+ setAction(state, `Re-indexing ${slug}… (this may take a while)`);
599
+ render(state);
600
+ try {
601
+ const meta = await getServerMetadata(slug);
602
+ if (!meta.sourceUrl)
603
+ throw new Error('No sourceUrl recorded — cannot reindex');
604
+ const result = await reindexServer({
605
+ slug,
606
+ embeddingModel: meta.embeddingModel || 'fuse',
607
+ maxChunks: 6000,
608
+ maxPages: 80,
609
+ });
610
+ setAction(state, `✓ Reindexed ${slug} — ${result.ingestion.stats.totalChunks} chunks`);
611
+ }
612
+ catch (e) {
613
+ setAction(state, `Reindex failed: ${e?.message || e}`, true);
614
+ }
615
+ await refresh(state);
616
+ setTimeout(() => { setAction(state, null); render(state); }, 3000);
617
+ return;
618
+ }
619
+ }
620
+ // ── Main entry ────────────────────────────────────────────────────────────────
621
+ export async function launchTUI() {
622
+ const testMode = process.env.MCP_PORTAL_TUI_TEST_MODE === '1';
623
+ if (testMode) {
624
+ process.stdout.write('hoolix TUI\n');
625
+ }
626
+ if (!testMode && (process.env.CI || !process.stdout.isTTY || !process.stdin.isTTY)) {
627
+ console.log('hoolix TUI requires an interactive TTY. Use CLI commands instead: hoolix --help');
628
+ return;
629
+ }
630
+ // Final raw-mode probe
631
+ let probeOk = false;
632
+ try {
633
+ if (process.stdin.isTTY) {
634
+ process.stdin.setRawMode(true);
635
+ process.stdin.setRawMode(false);
636
+ probeOk = true;
637
+ }
638
+ }
639
+ catch {
640
+ probeOk = false;
641
+ }
642
+ if (!testMode && !probeOk) {
643
+ console.log('TUI requires a terminal that supports raw mode. Falling back — use CLI commands.');
644
+ return;
645
+ }
646
+ const state = {
647
+ servers: [],
648
+ statuses: {},
649
+ selectedIndex: 0,
650
+ logTail: [],
651
+ actionMsg: null,
652
+ actionIsError: false,
653
+ };
654
+ let poll = null;
655
+ let dataHandler = null;
656
+ function cleanup() {
657
+ if (poll) {
658
+ clearInterval(poll);
659
+ poll = null;
660
+ }
661
+ if (dataHandler) {
662
+ process.stdin.removeListener('data', dataHandler);
663
+ dataHandler = null;
664
+ }
665
+ try {
666
+ process.stdin.setRawMode(false);
667
+ }
668
+ catch { }
669
+ process.stdin.pause();
670
+ }
671
+ // Initial load
672
+ await refresh(state);
673
+ render(state);
674
+ const onInput = async (raw) => {
675
+ // Quit on Ctrl-C or q
676
+ if (raw === '' || raw.toLowerCase() === 'q') {
677
+ cleanup();
678
+ process.stdout.write(A.clear);
679
+ process.exit(0);
680
+ }
681
+ await handleKey(raw, state);
682
+ render(state);
683
+ };
684
+ if (testMode) {
685
+ const scriptedKeys = (process.env.MCP_PORTAL_TUI_KEYS || 'r,q').split(',').map((k) => k.trim()).filter(Boolean);
686
+ for (const key of scriptedKeys) {
687
+ if (key.toLowerCase() === 'q')
688
+ break;
689
+ await onInput(key);
690
+ }
691
+ cleanup();
692
+ return;
693
+ }
694
+ dataHandler = (chunk) => { onInput(chunk).catch(() => { }); };
695
+ process.stdin.setRawMode(true);
696
+ process.stdin.resume();
697
+ process.stdin.setEncoding('utf8');
698
+ process.stdin.on('data', dataHandler);
699
+ // Periodic status + log refresh
700
+ poll = setInterval(async () => {
701
+ await refresh(state);
702
+ if (!state.actionMsg)
703
+ render(state);
704
+ }, 2000);
705
+ // Handle terminal resize
706
+ process.stdout.on('resize', () => render(state));
707
+ const onSig = () => { cleanup(); process.stdout.write(A.clear); process.exit(0); };
708
+ process.once('SIGINT', onSig);
709
+ process.once('SIGTERM', onSig);
710
+ }
711
+ //# sourceMappingURL=index.js.map