moderado 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/dist/args.d.ts +26 -0
  4. package/dist/args.d.ts.map +1 -0
  5. package/dist/args.js +108 -0
  6. package/dist/args.js.map +1 -0
  7. package/dist/commands/chat.d.ts +121 -0
  8. package/dist/commands/chat.d.ts.map +1 -0
  9. package/dist/commands/chat.js +1484 -0
  10. package/dist/commands/chat.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +3 -0
  12. package/dist/commands/doctor.d.ts.map +1 -0
  13. package/dist/commands/doctor.js +61 -0
  14. package/dist/commands/doctor.js.map +1 -0
  15. package/dist/commands/init.d.ts +27 -0
  16. package/dist/commands/init.d.ts.map +1 -0
  17. package/dist/commands/init.js +172 -0
  18. package/dist/commands/init.js.map +1 -0
  19. package/dist/commands/interactive_menu.d.ts +3 -0
  20. package/dist/commands/interactive_menu.d.ts.map +1 -0
  21. package/dist/commands/interactive_menu.js +98 -0
  22. package/dist/commands/interactive_menu.js.map +1 -0
  23. package/dist/commands/models.d.ts +8 -0
  24. package/dist/commands/models.d.ts.map +1 -0
  25. package/dist/commands/models.js +81 -0
  26. package/dist/commands/models.js.map +1 -0
  27. package/dist/commands/run.d.ts +3 -0
  28. package/dist/commands/run.d.ts.map +1 -0
  29. package/dist/commands/run.js +140 -0
  30. package/dist/commands/run.js.map +1 -0
  31. package/dist/config.d.ts +44 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +195 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/credentials.d.ts +14 -0
  36. package/dist/credentials.d.ts.map +1 -0
  37. package/dist/credentials.js +23 -0
  38. package/dist/credentials.js.map +1 -0
  39. package/dist/diagnostic_log.d.ts +2 -0
  40. package/dist/diagnostic_log.d.ts.map +1 -0
  41. package/dist/diagnostic_log.js +21 -0
  42. package/dist/diagnostic_log.js.map +1 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +83 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/model_pricing.d.ts +4 -0
  48. package/dist/model_pricing.d.ts.map +1 -0
  49. package/dist/model_pricing.js +10 -0
  50. package/dist/model_pricing.js.map +1 -0
  51. package/dist/sessions.d.ts +203 -0
  52. package/dist/sessions.d.ts.map +1 -0
  53. package/dist/sessions.js +82 -0
  54. package/dist/sessions.js.map +1 -0
  55. package/dist/ui/box.d.ts +14 -0
  56. package/dist/ui/box.d.ts.map +1 -0
  57. package/dist/ui/box.js +52 -0
  58. package/dist/ui/box.js.map +1 -0
  59. package/dist/ui/file_mentions.d.ts +38 -0
  60. package/dist/ui/file_mentions.d.ts.map +1 -0
  61. package/dist/ui/file_mentions.js +119 -0
  62. package/dist/ui/file_mentions.js.map +1 -0
  63. package/dist/ui/help_modal.d.ts +2 -0
  64. package/dist/ui/help_modal.d.ts.map +1 -0
  65. package/dist/ui/help_modal.js +101 -0
  66. package/dist/ui/help_modal.js.map +1 -0
  67. package/dist/ui/model_selector.d.ts +48 -0
  68. package/dist/ui/model_selector.d.ts.map +1 -0
  69. package/dist/ui/model_selector.js +829 -0
  70. package/dist/ui/model_selector.js.map +1 -0
  71. package/dist/ui/popup.d.ts +78 -0
  72. package/dist/ui/popup.d.ts.map +1 -0
  73. package/dist/ui/popup.js +274 -0
  74. package/dist/ui/popup.js.map +1 -0
  75. package/dist/ui/prompt.d.ts +21 -0
  76. package/dist/ui/prompt.d.ts.map +1 -0
  77. package/dist/ui/prompt.js +128 -0
  78. package/dist/ui/prompt.js.map +1 -0
  79. package/dist/ui/provider_connect.d.ts +33 -0
  80. package/dist/ui/provider_connect.d.ts.map +1 -0
  81. package/dist/ui/provider_connect.js +175 -0
  82. package/dist/ui/provider_connect.js.map +1 -0
  83. package/dist/ui/renderer.d.ts +19 -0
  84. package/dist/ui/renderer.d.ts.map +1 -0
  85. package/dist/ui/renderer.js +147 -0
  86. package/dist/ui/renderer.js.map +1 -0
  87. package/dist/ui/session_screen.d.ts +32 -0
  88. package/dist/ui/session_screen.d.ts.map +1 -0
  89. package/dist/ui/session_screen.js +148 -0
  90. package/dist/ui/session_screen.js.map +1 -0
  91. package/dist/ui/session_terminal.d.ts +46 -0
  92. package/dist/ui/session_terminal.d.ts.map +1 -0
  93. package/dist/ui/session_terminal.js +342 -0
  94. package/dist/ui/session_terminal.js.map +1 -0
  95. package/dist/ui/terminal_approval.d.ts +14 -0
  96. package/dist/ui/terminal_approval.d.ts.map +1 -0
  97. package/dist/ui/terminal_approval.js +98 -0
  98. package/dist/ui/terminal_approval.js.map +1 -0
  99. package/dist/ui/welcome.d.ts +130 -0
  100. package/dist/ui/welcome.d.ts.map +1 -0
  101. package/dist/ui/welcome.js +990 -0
  102. package/dist/ui/welcome.js.map +1 -0
  103. package/dist/vendor/contracts/approvals.d.ts +91 -0
  104. package/dist/vendor/contracts/approvals.d.ts.map +1 -0
  105. package/dist/vendor/contracts/approvals.js +22 -0
  106. package/dist/vendor/contracts/approvals.js.map +1 -0
  107. package/dist/vendor/contracts/events.d.ts +1215 -0
  108. package/dist/vendor/contracts/events.d.ts.map +1 -0
  109. package/dist/vendor/contracts/events.js +94 -0
  110. package/dist/vendor/contracts/events.js.map +1 -0
  111. package/dist/vendor/contracts/index.d.ts +7 -0
  112. package/dist/vendor/contracts/index.d.ts.map +1 -0
  113. package/dist/vendor/contracts/index.js +7 -0
  114. package/dist/vendor/contracts/index.js.map +1 -0
  115. package/dist/vendor/contracts/messages.d.ts +181 -0
  116. package/dist/vendor/contracts/messages.d.ts.map +1 -0
  117. package/dist/vendor/contracts/messages.js +40 -0
  118. package/dist/vendor/contracts/messages.js.map +1 -0
  119. package/dist/vendor/contracts/models.d.ts +117 -0
  120. package/dist/vendor/contracts/models.d.ts.map +1 -0
  121. package/dist/vendor/contracts/models.js +37 -0
  122. package/dist/vendor/contracts/models.js.map +1 -0
  123. package/dist/vendor/contracts/provider.d.ts +121 -0
  124. package/dist/vendor/contracts/provider.d.ts.map +1 -0
  125. package/dist/vendor/contracts/provider.js +64 -0
  126. package/dist/vendor/contracts/provider.js.map +1 -0
  127. package/dist/vendor/contracts/tools.d.ts +305 -0
  128. package/dist/vendor/contracts/tools.d.ts.map +1 -0
  129. package/dist/vendor/contracts/tools.js +65 -0
  130. package/dist/vendor/contracts/tools.js.map +1 -0
  131. package/dist/vendor/core/agent.d.ts +39 -0
  132. package/dist/vendor/core/agent.d.ts.map +1 -0
  133. package/dist/vendor/core/agent.js +710 -0
  134. package/dist/vendor/core/agent.js.map +1 -0
  135. package/dist/vendor/core/host_event_stream.d.ts +9 -0
  136. package/dist/vendor/core/host_event_stream.d.ts.map +1 -0
  137. package/dist/vendor/core/host_event_stream.js +12 -0
  138. package/dist/vendor/core/host_event_stream.js.map +1 -0
  139. package/dist/vendor/core/index.d.ts +7 -0
  140. package/dist/vendor/core/index.d.ts.map +1 -0
  141. package/dist/vendor/core/index.js +7 -0
  142. package/dist/vendor/core/index.js.map +1 -0
  143. package/dist/vendor/core/policy.d.ts +19 -0
  144. package/dist/vendor/core/policy.d.ts.map +1 -0
  145. package/dist/vendor/core/policy.js +31 -0
  146. package/dist/vendor/core/policy.js.map +1 -0
  147. package/dist/vendor/core/router.d.ts +26 -0
  148. package/dist/vendor/core/router.d.ts.map +1 -0
  149. package/dist/vendor/core/router.js +222 -0
  150. package/dist/vendor/core/router.js.map +1 -0
  151. package/dist/vendor/core/subagent.d.ts +29 -0
  152. package/dist/vendor/core/subagent.d.ts.map +1 -0
  153. package/dist/vendor/core/subagent.js +66 -0
  154. package/dist/vendor/core/subagent.js.map +1 -0
  155. package/dist/vendor/core/think_filter.d.ts +14 -0
  156. package/dist/vendor/core/think_filter.d.ts.map +1 -0
  157. package/dist/vendor/core/think_filter.js +136 -0
  158. package/dist/vendor/core/think_filter.js.map +1 -0
  159. package/dist/vendor/providers/fake/fake_provider.d.ts +16 -0
  160. package/dist/vendor/providers/fake/fake_provider.d.ts.map +1 -0
  161. package/dist/vendor/providers/fake/fake_provider.js +89 -0
  162. package/dist/vendor/providers/fake/fake_provider.js.map +1 -0
  163. package/dist/vendor/providers/index.d.ts +6 -0
  164. package/dist/vendor/providers/index.d.ts.map +1 -0
  165. package/dist/vendor/providers/index.js +6 -0
  166. package/dist/vendor/providers/index.js.map +1 -0
  167. package/dist/vendor/providers/model_discovery.d.ts +28 -0
  168. package/dist/vendor/providers/model_discovery.d.ts.map +1 -0
  169. package/dist/vendor/providers/model_discovery.js +106 -0
  170. package/dist/vendor/providers/model_discovery.js.map +1 -0
  171. package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts +23 -0
  172. package/dist/vendor/providers/nvidia/nvidia_adapter.d.ts.map +1 -0
  173. package/dist/vendor/providers/nvidia/nvidia_adapter.js +197 -0
  174. package/dist/vendor/providers/nvidia/nvidia_adapter.js.map +1 -0
  175. package/dist/vendor/providers/nvidia/sse_parser.d.ts +3 -0
  176. package/dist/vendor/providers/nvidia/sse_parser.d.ts.map +1 -0
  177. package/dist/vendor/providers/nvidia/sse_parser.js +84 -0
  178. package/dist/vendor/providers/nvidia/sse_parser.js.map +1 -0
  179. package/dist/vendor/providers/openai_compatible_adapter.d.ts +10 -0
  180. package/dist/vendor/providers/openai_compatible_adapter.d.ts.map +1 -0
  181. package/dist/vendor/providers/openai_compatible_adapter.js +8 -0
  182. package/dist/vendor/providers/openai_compatible_adapter.js.map +1 -0
  183. package/dist/vendor/tools/checkpoints.d.ts +27 -0
  184. package/dist/vendor/tools/checkpoints.d.ts.map +1 -0
  185. package/dist/vendor/tools/checkpoints.js +145 -0
  186. package/dist/vendor/tools/checkpoints.js.map +1 -0
  187. package/dist/vendor/tools/diagnostics.d.ts +11 -0
  188. package/dist/vendor/tools/diagnostics.d.ts.map +1 -0
  189. package/dist/vendor/tools/diagnostics.js +16 -0
  190. package/dist/vendor/tools/diagnostics.js.map +1 -0
  191. package/dist/vendor/tools/diff.d.ts +7 -0
  192. package/dist/vendor/tools/diff.d.ts.map +1 -0
  193. package/dist/vendor/tools/diff.js +48 -0
  194. package/dist/vendor/tools/diff.js.map +1 -0
  195. package/dist/vendor/tools/errors.d.ts +17 -0
  196. package/dist/vendor/tools/errors.d.ts.map +1 -0
  197. package/dist/vendor/tools/errors.js +33 -0
  198. package/dist/vendor/tools/errors.js.map +1 -0
  199. package/dist/vendor/tools/git_workspace.d.ts +14 -0
  200. package/dist/vendor/tools/git_workspace.d.ts.map +1 -0
  201. package/dist/vendor/tools/git_workspace.js +42 -0
  202. package/dist/vendor/tools/git_workspace.js.map +1 -0
  203. package/dist/vendor/tools/index.d.ts +22 -0
  204. package/dist/vendor/tools/index.d.ts.map +1 -0
  205. package/dist/vendor/tools/index.js +22 -0
  206. package/dist/vendor/tools/index.js.map +1 -0
  207. package/dist/vendor/tools/jail.d.ts +17 -0
  208. package/dist/vendor/tools/jail.d.ts.map +1 -0
  209. package/dist/vendor/tools/jail.js +221 -0
  210. package/dist/vendor/tools/jail.js.map +1 -0
  211. package/dist/vendor/tools/lsp.d.ts +3 -0
  212. package/dist/vendor/tools/lsp.d.ts.map +1 -0
  213. package/dist/vendor/tools/lsp.js +43 -0
  214. package/dist/vendor/tools/lsp.js.map +1 -0
  215. package/dist/vendor/tools/mcp.d.ts +9 -0
  216. package/dist/vendor/tools/mcp.d.ts.map +1 -0
  217. package/dist/vendor/tools/mcp.js +54 -0
  218. package/dist/vendor/tools/mcp.js.map +1 -0
  219. package/dist/vendor/tools/mcp_adapter.d.ts +15 -0
  220. package/dist/vendor/tools/mcp_adapter.d.ts.map +1 -0
  221. package/dist/vendor/tools/mcp_adapter.js +29 -0
  222. package/dist/vendor/tools/mcp_adapter.js.map +1 -0
  223. package/dist/vendor/tools/registry.d.ts +10 -0
  224. package/dist/vendor/tools/registry.d.ts.map +1 -0
  225. package/dist/vendor/tools/registry.js +126 -0
  226. package/dist/vendor/tools/registry.js.map +1 -0
  227. package/dist/vendor/tools/tools/apply_patch.d.ts +3 -0
  228. package/dist/vendor/tools/tools/apply_patch.d.ts.map +1 -0
  229. package/dist/vendor/tools/tools/apply_patch.js +49 -0
  230. package/dist/vendor/tools/tools/apply_patch.js.map +1 -0
  231. package/dist/vendor/tools/tools/edit_file.d.ts +3 -0
  232. package/dist/vendor/tools/tools/edit_file.d.ts.map +1 -0
  233. package/dist/vendor/tools/tools/edit_file.js +71 -0
  234. package/dist/vendor/tools/tools/edit_file.js.map +1 -0
  235. package/dist/vendor/tools/tools/git_diff.d.ts +3 -0
  236. package/dist/vendor/tools/tools/git_diff.d.ts.map +1 -0
  237. package/dist/vendor/tools/tools/git_diff.js +91 -0
  238. package/dist/vendor/tools/tools/git_diff.js.map +1 -0
  239. package/dist/vendor/tools/tools/language_intelligence.d.ts +4 -0
  240. package/dist/vendor/tools/tools/language_intelligence.d.ts.map +1 -0
  241. package/dist/vendor/tools/tools/language_intelligence.js +13 -0
  242. package/dist/vendor/tools/tools/language_intelligence.js.map +1 -0
  243. package/dist/vendor/tools/tools/list_files.d.ts +9 -0
  244. package/dist/vendor/tools/tools/list_files.d.ts.map +1 -0
  245. package/dist/vendor/tools/tools/list_files.js +165 -0
  246. package/dist/vendor/tools/tools/list_files.js.map +1 -0
  247. package/dist/vendor/tools/tools/read_file.d.ts +3 -0
  248. package/dist/vendor/tools/tools/read_file.d.ts.map +1 -0
  249. package/dist/vendor/tools/tools/read_file.js +72 -0
  250. package/dist/vendor/tools/tools/read_file.js.map +1 -0
  251. package/dist/vendor/tools/tools/run_command.d.ts +12 -0
  252. package/dist/vendor/tools/tools/run_command.d.ts.map +1 -0
  253. package/dist/vendor/tools/tools/run_command.js +421 -0
  254. package/dist/vendor/tools/tools/run_command.js.map +1 -0
  255. package/dist/vendor/tools/tools/run_diagnostics.d.ts +3 -0
  256. package/dist/vendor/tools/tools/run_diagnostics.d.ts.map +1 -0
  257. package/dist/vendor/tools/tools/run_diagnostics.js +11 -0
  258. package/dist/vendor/tools/tools/run_diagnostics.js.map +1 -0
  259. package/dist/vendor/tools/tools/search_files.d.ts +3 -0
  260. package/dist/vendor/tools/tools/search_files.d.ts.map +1 -0
  261. package/dist/vendor/tools/tools/search_files.js +120 -0
  262. package/dist/vendor/tools/tools/search_files.js.map +1 -0
  263. package/dist/vendor/tools/tools/web_search.d.ts +46 -0
  264. package/dist/vendor/tools/tools/web_search.d.ts.map +1 -0
  265. package/dist/vendor/tools/tools/web_search.js +187 -0
  266. package/dist/vendor/tools/tools/web_search.js.map +1 -0
  267. package/dist/vendor/tools/tools/write_file.d.ts +3 -0
  268. package/dist/vendor/tools/tools/write_file.d.ts.map +1 -0
  269. package/dist/vendor/tools/tools/write_file.js +52 -0
  270. package/dist/vendor/tools/tools/write_file.js.map +1 -0
  271. package/dist/windows_credentials.d.ts +15 -0
  272. package/dist/windows_credentials.d.ts.map +1 -0
  273. package/dist/windows_credentials.js +23 -0
  274. package/dist/windows_credentials.js.map +1 -0
  275. package/package.json +28 -0
@@ -0,0 +1,829 @@
1
+ import { NvidiaAdapter } from '../vendor/providers/index.js';
2
+ import { Router } from '../vendor/core/index.js';
3
+ import { askQuestion, askSelect, askModalChoice } from './prompt.js';
4
+ import { renderBoxLines, layerPromptBox, selectListPopup, selectConfirmPopup, } from './popup.js';
5
+ import { loadConfig, saveConfig, resolveApiKey } from '../config.js';
6
+ let cachedInventory = null;
7
+ export function buildModelConnectionRequiredItems() {
8
+ return [
9
+ {
10
+ label: 'Connect NVIDIA NIM first',
11
+ value: 'connect',
12
+ description: 'Use /connect to add an NVIDIA NIM API key, then choose a model here.',
13
+ },
14
+ {
15
+ label: 'Close Window',
16
+ value: 'close',
17
+ description: 'Return to the welcome window without changing anything.',
18
+ },
19
+ ];
20
+ }
21
+ export async function showModelConnectionRequired(drawFrame, signal) {
22
+ await selectListPopup('Model Selection Window', buildModelConnectionRequiredItems(), {
23
+ drawFrame,
24
+ signal,
25
+ hint: 'Enter or Esc close',
26
+ });
27
+ }
28
+ export function buildCompatibleModelMenuItems(providerName, models, currentModel, allModelsFree = false) {
29
+ const items = [];
30
+ if (allModelsFree && models.length > 0) {
31
+ items.push({
32
+ label: 'Browse available models',
33
+ value: 'browse',
34
+ tag: `${models.length} Free`,
35
+ description: `Filter the live ${providerName} free model catalog.`,
36
+ });
37
+ }
38
+ else {
39
+ const freeCount = models.filter(isFreeCompatibleModel).length;
40
+ if (freeCount > 0) {
41
+ items.push({ label: 'Browse Free Models', value: 'free', tag: `${freeCount} Free`, description: `No-cost ${providerName} endpoints.` });
42
+ }
43
+ }
44
+ items.push({
45
+ label: 'Enter a model ID',
46
+ value: 'custom',
47
+ tag: 'Free',
48
+ description: `Use any ${providerName} model ID, including one not returned by discovery.`,
49
+ });
50
+ if (currentModel) {
51
+ items.push({
52
+ label: 'Keep Current Model',
53
+ value: 'keep',
54
+ tag: currentModel,
55
+ description: 'Close the window without switching models.',
56
+ });
57
+ }
58
+ items.push({
59
+ label: 'Cancel & Close Window',
60
+ value: 'cancel',
61
+ description: 'No changes will be made.',
62
+ });
63
+ return items;
64
+ }
65
+ function isFreeCompatibleModel(model) {
66
+ const prices = Object.values(model.pricing ?? {});
67
+ return prices.length > 0 && prices.every((price) => Number.isFinite(Number(price)) && Number(price) === 0);
68
+ }
69
+ function compatibleModelPopupItem(model, allModelsFree, providerName) {
70
+ const isFree = allModelsFree || isFreeCompatibleModel(model);
71
+ const price = model.pricing?.prompt;
72
+ return {
73
+ label: model.id,
74
+ value: model.id,
75
+ tag: 'Free',
76
+ description: isFree
77
+ ? `${providerName} free model`
78
+ : price
79
+ ? `${providerName} model · input $${Number(price) * 1_000_000}/M tokens`
80
+ : `${providerName} free model`,
81
+ };
82
+ }
83
+ export async function selectCompatibleModelOverlay(options) {
84
+ const { apiKey, baseUrl, providerId, providerName, currentModel, allModelsFree, signal, drawFrame } = options;
85
+ let models = [];
86
+ try {
87
+ layerPromptBox(drawFrame, `\x1b[36mQuerying ${providerName} model catalog...\x1b[0m`);
88
+ const provider = new NvidiaAdapter({ apiKey, baseUrl, providerId, providerName });
89
+ models = (await provider.discoverModels(signal))
90
+ .map((model) => ({ id: model.id, pricing: model.pricing }))
91
+ .sort((a, b) => a.id.localeCompare(b.id));
92
+ }
93
+ catch {
94
+ // Explicit model entry remains available when a provider does not expose /models.
95
+ }
96
+ const picked = await selectListPopup(`${providerName} Free Models`, buildCompatibleModelMenuItems(providerName, models, currentModel, allModelsFree), { drawFrame, signal, pageSize: 8, hint: '↑↓ navigate · Enter select · Esc close' });
97
+ if (picked === null || picked === 'keep' || picked === 'cancel')
98
+ return currentModel;
99
+ if (picked === 'browse' || picked === 'free') {
100
+ const visibleModels = allModelsFree ? models : models.filter(isFreeCompatibleModel);
101
+ return (await selectListPopup(`${providerName} Free Models`, visibleModels.map((model) => compatibleModelPopupItem(model, allModelsFree ?? false, providerName)), { drawFrame, signal, filterable: true, pageSize: 10, hint: 'type to filter · ↑↓ navigate · Enter select · Esc close' })) ?? currentModel;
102
+ }
103
+ return askCompatibleModelId(providerName, currentModel, drawFrame, signal);
104
+ }
105
+ async function askCompatibleModelId(providerName, currentModel, drawFrame, signal) {
106
+ const stdin = process.stdin;
107
+ let value = currentModel ?? '';
108
+ const redraw = () => drawFrame(renderBoxLines(`${providerName} Models`, [
109
+ '\x1b[1;38;5;75mModel ID\x1b[0m',
110
+ '',
111
+ ` \x1b[1;38;5;75m❯\x1b[0m ${value}\x1b[7m \x1b[0m`,
112
+ '',
113
+ '\x1b[38;5;244mEnter save · Esc cancel\x1b[0m',
114
+ ], 64));
115
+ return new Promise((resolve) => {
116
+ const finish = (modelId) => {
117
+ stdin.removeListener('keypress', onKeypress);
118
+ signal?.removeEventListener('abort', onAbort);
119
+ resolve(modelId);
120
+ };
121
+ const onAbort = () => finish(undefined);
122
+ const onKeypress = (str, key) => {
123
+ if (key?.name === 'escape' || (key?.ctrl && key.name === 'c'))
124
+ return finish(undefined);
125
+ if (key?.name === 'return' || key?.name === 'enter')
126
+ return finish(value.trim() || undefined);
127
+ if (key?.name === 'backspace') {
128
+ value = value.slice(0, -1);
129
+ redraw();
130
+ return;
131
+ }
132
+ if (str && str.length === 1 && str.charCodeAt(0) >= 32 && !key?.ctrl && !key?.meta) {
133
+ value += str;
134
+ redraw();
135
+ }
136
+ };
137
+ if (signal?.aborted)
138
+ return finish(undefined);
139
+ signal?.addEventListener('abort', onAbort, { once: true });
140
+ stdin.on('keypress', onKeypress);
141
+ redraw();
142
+ });
143
+ }
144
+ export function buildOverlayMenuItems(freeModelIds, freeCount, paidCount, activeModel) {
145
+ const popular = [
146
+ { label: 'Nemotron 3 Ultra Free', match: (id) => /nemotron.*3.*ultra/i.test(id) },
147
+ { label: 'Nemotron 3.5 Lightning Free', match: (id) => /nemotron.*3.*5.*lightning|nemotron.*lightning/i.test(id) },
148
+ { label: 'z-ai/glm-5.3-flash', match: (id) => id.toLowerCase() === 'z-ai/glm-5.3-flash' },
149
+ ];
150
+ const items = popular.flatMap(({ label, match }) => {
151
+ const id = freeModelIds.find(match);
152
+ return id ? [{ label, value: `model:${id}`, tag: 'Free', description: id }] : [];
153
+ });
154
+ items.push({ label: 'Browse Free Models', value: 'free', tag: `${freeCount} Free`, description: 'Scan the live NVIDIA NIM free catalog' }, { label: 'Browse Paid Models', value: 'paid', tag: `${paidCount} Paid`, description: 'Scan paid and frontier NVIDIA NIM models' }, { label: 'Search Full Catalog', value: 'search', description: 'Type to filter every discovered NIM model' }, { label: 'Auto Routing', value: 'auto', tag: 'Recommended', description: 'Use free-first automatic routing' });
155
+ if (activeModel)
156
+ items.push({ label: 'Keep Current Model', value: 'keep', tag: activeModel, description: 'Close without changing models' });
157
+ items.push({ label: 'Cancel & Close Window', value: 'cancel', description: 'No changes · Esc also closes this window' });
158
+ return items;
159
+ }
160
+ export async function selectModelInteractive(options = {}) {
161
+ const isTty = Boolean(process.stdout.isTTY && process.stdin.isTTY);
162
+ if (isTty) {
163
+ process.stdout.write('\x1b[?1049h\x1b[?25l\x1b[H\x1b[2J');
164
+ }
165
+ try {
166
+ return await executeModelSelection(options);
167
+ }
168
+ finally {
169
+ if (isTty) {
170
+ process.stdout.write('\x1b[?25h\x1b[?1049l');
171
+ }
172
+ }
173
+ }
174
+ /**
175
+ * Same model selection flow as selectModelInteractive, but rendered as a
176
+ * floating popup window layered on top of the main app window (background
177
+ * stays as-is). Used by the TUI /model popup via the drawFrame callback.
178
+ */
179
+ export async function selectModelOverlay(options = {}) {
180
+ // No alternate screen — the caller is responsible for repainting the background.
181
+ return executeModelSelection(options);
182
+ }
183
+ async function executeModelSelection(options = {}) {
184
+ const signal = options.signal;
185
+ const apiKey = options.apiKey ?? resolveApiKey();
186
+ const config = loadConfig();
187
+ const activeModel = options.currentModel ?? config.defaultModel;
188
+ if (signal?.aborted) {
189
+ return {
190
+ modelId: activeModel,
191
+ allowPaid: config.allowPaid ?? false,
192
+ allowUnknown: config.allowUnknown ?? false,
193
+ savedAsDefault: false,
194
+ };
195
+ }
196
+ const provider = new NvidiaAdapter({ apiKey });
197
+ const router = new Router();
198
+ let discoveredEntries = [];
199
+ if (cachedInventory && cachedInventory.length > 0) {
200
+ discoveredEntries = cachedInventory;
201
+ }
202
+ else {
203
+ if (options.drawFrame) {
204
+ // Layer mode: show the query status inside the popup window itself.
205
+ layerPromptBox(options.drawFrame, '\x1b[36mQuerying NVIDIA NIM model catalog...\x1b[0m');
206
+ }
207
+ else {
208
+ process.stdout.write('\n\x1b[36mQuerying NVIDIA NIM model catalog...\x1b[0m\n');
209
+ }
210
+ try {
211
+ discoveredEntries = await provider.discoverModels(signal);
212
+ cachedInventory = discoveredEntries;
213
+ }
214
+ catch {
215
+ discoveredEntries = [
216
+ { id: 'nvidia/llama-3.1-nemotron-70b-instruct' },
217
+ { id: 'meta/llama-3.3-70b-instruct' },
218
+ { id: 'meta/llama-3.2-90b-vision-instruct' },
219
+ { id: 'meta/llama-3.2-11b-vision-instruct' },
220
+ { id: 'meta/llama-3.1-70b-instruct' },
221
+ { id: 'mistralai/mixtral-8x7b-instruct-v0.1' },
222
+ { id: 'mistralai/mistral-large-2-instruct' },
223
+ { id: 'deepseek-ai/deepseek-v4-flash-0731' },
224
+ { id: 'deepseek-ai/deepseek-coder-6.7b-instruct' },
225
+ { id: 'google/codegemma-7b' },
226
+ { id: 'ibm/granite-34b-code-instruct' },
227
+ { id: 'openai/gpt-oss-20b' },
228
+ { id: 'z-ai/glm-5.3' },
229
+ { id: 'z-ai/glm-5.3-flash' },
230
+ ];
231
+ }
232
+ }
233
+ const freeModels = [];
234
+ const paidModels = [];
235
+ for (const entry of discoveredEntries) {
236
+ const meta = router.classifyModel(entry.id);
237
+ if (meta.accessTier === 'free_trial' || meta.accessTier === 'local') {
238
+ let desc = 'Free trial endpoint';
239
+ if (entry.id.includes('nemotron-70b'))
240
+ desc = 'NVIDIA flagship 70B reasoning & code model';
241
+ else if (entry.id.includes('llama-3.3-70b'))
242
+ desc = 'Meta 70B frontier instruction model';
243
+ else if (entry.id.includes('llama-3.2-90b'))
244
+ desc = 'High-capacity multimodal reasoning';
245
+ else if (entry.id.includes('llama-3.2-11b'))
246
+ desc = 'Fast lightweight vision instruction model';
247
+ else if (entry.id.includes('mixtral'))
248
+ desc = 'MoE model with tool calling support';
249
+ freeModels.push({ id: entry.id, desc });
250
+ }
251
+ else {
252
+ let desc = meta.accessTier === 'paid' ? 'Paid model' : 'Unclassified NIM endpoint';
253
+ if (entry.id.includes('glm'))
254
+ desc = 'Zhipu AI GLM multilingual model';
255
+ else if (entry.id.includes('mistral-large'))
256
+ desc = 'Flagship frontier reasoning & coding model';
257
+ else if (entry.id.includes('deepseek'))
258
+ desc = 'Specialized high-efficiency coding model';
259
+ else if (entry.id.includes('codestral'))
260
+ desc = 'Mistral code generation specialist';
261
+ else if (entry.id.includes('codegemma'))
262
+ desc = 'Google code intelligence model';
263
+ paidModels.push({ id: entry.id, desc });
264
+ }
265
+ }
266
+ // Sort free models with 70B reasoning models first
267
+ freeModels.sort((a, b) => {
268
+ const scoreA = a.id.includes('nemotron-70b') || a.id.includes('llama-3.3-70b') ? 2 : 1;
269
+ const scoreB = b.id.includes('nemotron-70b') || b.id.includes('llama-3.3-70b') ? 2 : 1;
270
+ return scoreB - scoreA;
271
+ });
272
+ const menuLines = [
273
+ `\x1b[1;38;5;75m[1]\x1b[0m Search by keyword \x1b[38;5;244m(e.g. "glm", "flash", "llama")\x1b[0m`,
274
+ `\x1b[1;38;5;114m[2]\x1b[0m Free Trial Models \x1b[38;5;244m(${freeModels.length} models ready)\x1b[0m`,
275
+ `\x1b[1;38;5;222m[3]\x1b[0m Paid & Frontier Models \x1b[38;5;244m(${paidModels.length} models)\x1b[0m`,
276
+ `\x1b[1;38;5;141m[4]\x1b[0m Complete Catalog \x1b[38;5;244m(${discoveredEntries.length} models)\x1b[0m`,
277
+ `\x1b[1;38;5;250m[5]\x1b[0m Auto Routing \x1b[38;5;244m(Free-first recommended)\x1b[0m`,
278
+ ];
279
+ if (activeModel) {
280
+ menuLines.push('---');
281
+ menuLines.push(`\x1b[1;38;5;39m[6]\x1b[0m Keep Current \x1b[38;5;141m${activeModel}\x1b[0m`);
282
+ }
283
+ menuLines.push(`\x1b[1;38;5;245m[q]\x1b[0m Cancel & Close Window \x1b[38;5;244m(No changes)\x1b[0m`);
284
+ const cols = process.stdout.columns || 80;
285
+ const boxWidth = Math.min(Math.max(62, Math.min(cols - 4, 72)), cols);
286
+ const popupLines = renderBoxLines('Model Selection Window', menuLines, boxWidth);
287
+ const layer = options.drawFrame;
288
+ if (options.drawFrame) {
289
+ // Popup window mode: Cline/OpenCode-style interactive list window layered
290
+ // on top of the main app window (background stays as-is underneath).
291
+ const menuItems = [
292
+ {
293
+ label: 'Search by keyword',
294
+ value: 'search',
295
+ description: 'Type-to-filter across the full NVIDIA NIM catalog',
296
+ },
297
+ {
298
+ label: 'Free Trial Models',
299
+ value: 'free',
300
+ tag: `${freeModels.length}`,
301
+ description: 'Free trial endpoints — ready to use, no cost',
302
+ },
303
+ {
304
+ label: 'Paid & Frontier Models',
305
+ value: 'paid',
306
+ tag: `${paidModels.length}`,
307
+ description: 'Flagship and paid-tier models',
308
+ },
309
+ {
310
+ label: 'Complete Catalog',
311
+ value: 'all',
312
+ tag: `${discoveredEntries.length}`,
313
+ description: 'Every discovered NVIDIA NIM endpoint',
314
+ },
315
+ {
316
+ label: 'Auto Routing',
317
+ value: 'auto',
318
+ description: 'Free-first recommended routing',
319
+ },
320
+ ];
321
+ if (activeModel) {
322
+ menuItems.push({
323
+ label: 'Keep Current',
324
+ value: 'keep',
325
+ tag: activeModel,
326
+ description: 'Close the window without switching models',
327
+ });
328
+ }
329
+ menuItems.push({
330
+ label: 'Cancel & Close Window',
331
+ value: 'cancel',
332
+ description: 'No changes will be made',
333
+ });
334
+ menuItems.splice(0, menuItems.length, ...buildOverlayMenuItems(freeModels.map((model) => model.id), freeModels.length, paidModels.length, activeModel));
335
+ const picked = await selectListPopup('Model Selection Window', menuItems, {
336
+ signal,
337
+ drawFrame: options.drawFrame,
338
+ pageSize: 9,
339
+ hint: '↑↓ navigate · Enter select · Esc close',
340
+ });
341
+ if (picked === null || picked === 'keep' || picked === 'cancel') {
342
+ return {
343
+ modelId: activeModel,
344
+ allowPaid: config.allowPaid ?? false,
345
+ allowUnknown: config.allowUnknown ?? false,
346
+ savedAsDefault: false,
347
+ };
348
+ }
349
+ let chosenModelId = undefined;
350
+ let isPaid = false;
351
+ if (picked.startsWith('model:')) {
352
+ chosenModelId = picked.slice('model:'.length);
353
+ isPaid = false;
354
+ }
355
+ else if (picked === 'auto') {
356
+ chosenModelId = 'auto';
357
+ isPaid = false;
358
+ }
359
+ else if (picked === 'free') {
360
+ chosenModelId = await pickFromList('Choose Free Model:', freeModels.map((m) => m.id), router, signal, layer);
361
+ isPaid = false;
362
+ }
363
+ else if (picked === 'paid') {
364
+ chosenModelId = await browseOrSearchList('Paid & Frontier Models:', paidModels.map((m) => m.id), router, signal, layer);
365
+ isPaid = true;
366
+ }
367
+ else if (picked === 'all') {
368
+ chosenModelId = await browseOrSearchList('All NVIDIA NIM Models:', discoveredEntries.map((m) => m.id), router, signal, layer);
369
+ if (chosenModelId) {
370
+ const meta = router.classifyModel(chosenModelId);
371
+ isPaid = meta.accessTier === 'paid' || meta.accessTier === 'unknown';
372
+ }
373
+ }
374
+ else {
375
+ // Search by keyword: interactive type-to-filter popup over the catalog.
376
+ chosenModelId = await searchAndSelect('', discoveredEntries.map((m) => m.id), router, signal, layer);
377
+ if (chosenModelId) {
378
+ const meta = router.classifyModel(chosenModelId);
379
+ isPaid = meta.accessTier === 'paid' || meta.accessTier === 'unknown';
380
+ }
381
+ }
382
+ if (!chosenModelId || chosenModelId === activeModel) {
383
+ return {
384
+ modelId: activeModel,
385
+ allowPaid: config.allowPaid ?? false,
386
+ allowUnknown: config.allowUnknown ?? false,
387
+ savedAsDefault: false,
388
+ };
389
+ }
390
+ let savedAsDefault = false;
391
+ if (options.saveSelectionByDefault !== false && chosenModelId) {
392
+ const shouldSave = await selectConfirmPopup('Save Default Model', [
393
+ `\x1b[32m✔ Selected model:\x1b[0m \x1b[1m${chosenModelId}\x1b[0m`,
394
+ 'Save as persistent default in ~/.moderado/config.json?',
395
+ ], { signal, drawFrame: options.drawFrame });
396
+ if (shouldSave === true) {
397
+ saveConfig({
398
+ defaultModel: chosenModelId,
399
+ allowPaid: isPaid ? true : config.allowPaid,
400
+ allowUnknown: isPaid ? true : config.allowUnknown,
401
+ });
402
+ layerPromptBox(options.drawFrame, `\x1b[32m✔ Saved ${chosenModelId} as default model.\x1b[0m`);
403
+ savedAsDefault = true;
404
+ }
405
+ }
406
+ return {
407
+ modelId: chosenModelId,
408
+ allowPaid: isPaid || (config.allowPaid ?? false),
409
+ allowUnknown: isPaid || (config.allowUnknown ?? false),
410
+ savedAsDefault,
411
+ };
412
+ }
413
+ // ── Standalone setup mode: inline number-menu flow (unchanged) ────────────
414
+ const leftPad = Math.max(0, Math.floor((cols - boxWidth) / 2));
415
+ const pad = ' '.repeat(leftPad);
416
+ process.stdout.write('\n' + popupLines.map((l) => pad + l).join('\n') + '\n');
417
+ const maxOption = activeModel ? 6 : 5;
418
+ const promptMsg = (options.drawFrame ? '\n' : '') +
419
+ `\x1b[38;5;244mSelect [1-${maxOption}], [Esc]/[Enter] to close, or type keyword:\x1b[0m `;
420
+ const input = await askModalChoice(promptMsg, { signal });
421
+ let chosenModelId = undefined;
422
+ let isPaid = false;
423
+ const normalized = input.trim().toLowerCase();
424
+ if (normalized === 'q' || normalized === 'cancel' || normalized === 'exit' || normalized === '' || (normalized === '6' && activeModel)) {
425
+ return {
426
+ modelId: activeModel,
427
+ allowPaid: config.allowPaid ?? false,
428
+ allowUnknown: config.allowUnknown ?? false,
429
+ savedAsDefault: false,
430
+ };
431
+ }
432
+ else if (normalized === '5' || normalized === 'auto') {
433
+ chosenModelId = 'auto';
434
+ isPaid = false;
435
+ }
436
+ else if (normalized === '2' || normalized === 'free') {
437
+ // Browse all free models
438
+ chosenModelId = await pickFromList('Choose Free Model:', freeModels.map((m) => m.id), router, signal, layer);
439
+ isPaid = false;
440
+ }
441
+ else if (normalized === '3' || normalized === 'paid') {
442
+ // Browse paid models with search or list
443
+ chosenModelId = await browseOrSearchList('Paid & Frontier Models:', paidModels.map((m) => m.id), router, signal, layer);
444
+ isPaid = true;
445
+ }
446
+ else if (normalized === '4' || normalized === 'all') {
447
+ // Browse all models with search or list
448
+ chosenModelId = await browseOrSearchList('All NVIDIA NIM Models:', discoveredEntries.map((m) => m.id), router, signal, layer);
449
+ if (chosenModelId) {
450
+ const meta = router.classifyModel(chosenModelId);
451
+ isPaid = meta.accessTier === 'paid' || meta.accessTier === 'unknown';
452
+ }
453
+ }
454
+ else {
455
+ // Keyword search or direct selection (e.g. "1", "glm", "flash", "z-ai/glm-5.3-flash")
456
+ let searchTerm = input.trim();
457
+ if (searchTerm === '1' || !searchTerm) {
458
+ if (layer) {
459
+ layerPromptBox(layer, 'Enter search term or model ID (e.g. "glm", "flash", "llama"):');
460
+ searchTerm = await askModalChoice('\n> ', { signal });
461
+ }
462
+ else {
463
+ searchTerm = await askQuestion('Enter search term or model ID (e.g. "glm", "flash", "llama"): ', { signal });
464
+ }
465
+ }
466
+ if (!searchTerm) {
467
+ return {
468
+ modelId: activeModel,
469
+ allowPaid: config.allowPaid ?? false,
470
+ allowUnknown: config.allowUnknown ?? false,
471
+ savedAsDefault: false,
472
+ };
473
+ }
474
+ else {
475
+ chosenModelId = await searchAndSelect(searchTerm, discoveredEntries.map((m) => m.id), router, signal, layer);
476
+ if (chosenModelId) {
477
+ const meta = router.classifyModel(chosenModelId);
478
+ isPaid = meta.accessTier === 'paid' || meta.accessTier === 'unknown';
479
+ }
480
+ }
481
+ }
482
+ if (!chosenModelId || chosenModelId === activeModel) {
483
+ return {
484
+ modelId: activeModel,
485
+ allowPaid: config.allowPaid ?? false,
486
+ allowUnknown: config.allowUnknown ?? false,
487
+ savedAsDefault: false,
488
+ };
489
+ }
490
+ let savedAsDefault = false;
491
+ if (options.saveSelectionByDefault !== false && chosenModelId) {
492
+ if (layer) {
493
+ layerPromptBox(layer, [
494
+ `\x1b[32m✔ Selected model:\x1b[0m \x1b[1m${chosenModelId}\x1b[0m`,
495
+ `Save as persistent default model in ~/.moderado/config.json? [Y/n]`,
496
+ ]);
497
+ }
498
+ else {
499
+ process.stdout.write(`\n\x1b[32m✔ Selected model:\x1b[0m \x1b[1m${chosenModelId}\x1b[0m\n`);
500
+ }
501
+ const shouldSave = layer
502
+ ? await askModalChoice('\n> ', { signal })
503
+ : await askQuestion(`Save ${chosenModelId} as persistent default model in ~/.moderado/config.json? [Y/n]: `, { signal });
504
+ if (shouldSave.toLowerCase() !== 'n' && shouldSave.toLowerCase() !== 'no') {
505
+ saveConfig({
506
+ defaultModel: chosenModelId,
507
+ allowPaid: isPaid ? true : config.allowPaid,
508
+ allowUnknown: isPaid ? true : config.allowUnknown,
509
+ });
510
+ if (layer) {
511
+ layerPromptBox(layer, `\x1b[32m✔ Saved ${chosenModelId} as default model.\x1b[0m`);
512
+ }
513
+ else {
514
+ process.stdout.write(`\x1b[32m✔ Saved ${chosenModelId} as default model.\x1b[0m\n\n`);
515
+ }
516
+ savedAsDefault = true;
517
+ }
518
+ }
519
+ return {
520
+ modelId: chosenModelId,
521
+ allowPaid: isPaid || (config.allowPaid ?? false),
522
+ allowUnknown: isPaid || (config.allowUnknown ?? false),
523
+ savedAsDefault,
524
+ };
525
+ }
526
+ async function searchAndSelect(initialQuery, allModelIds, router, signal, layer) {
527
+ // Layer mode: Cline-style interactive type-to-filter popup. The filter is
528
+ // built into the list window itself; Esc opens a retry/custom-ID/cancel
529
+ // menu so the "custom Model ID" escape hatch is preserved.
530
+ if (layer) {
531
+ const tier = (id) => {
532
+ const meta = router.classifyModel(id);
533
+ return meta.accessTier === 'free_trial' ? 'Free' : meta.accessTier === 'paid' ? 'Paid' : 'NIM';
534
+ };
535
+ while (!signal?.aborted) {
536
+ const items = allModelIds.map((id) => ({
537
+ label: id,
538
+ value: id,
539
+ tag: tier(id),
540
+ description: tier(id) === 'Free' ? 'Free trial endpoint' : tier(id) === 'Paid' ? 'Paid model' : 'Unclassified NIM endpoint',
541
+ }));
542
+ const picked = await selectListPopup('Search Models', items, {
543
+ signal,
544
+ drawFrame: layer,
545
+ filterable: true,
546
+ initialFilter: initialQuery,
547
+ pageSize: 10,
548
+ hint: 'type to filter · ↑↓ navigate · Enter select · Esc menu',
549
+ });
550
+ if (picked !== null)
551
+ return picked;
552
+ // Esc: retry with a new keyword, use a custom ID, or cancel.
553
+ const action = await selectListPopup('Search Models', [
554
+ { label: 'Try another search keyword', value: 'retry', description: 'Reopen the filter popup' },
555
+ { label: 'Cancel search', value: 'cancel', description: 'Go back without switching' },
556
+ ], { signal, drawFrame: layer, pageSize: 5 });
557
+ if (action === 'retry')
558
+ continue;
559
+ return '';
560
+ }
561
+ return '';
562
+ }
563
+ let currentQuery = initialQuery;
564
+ const rows = process.stdout.rows || 24;
565
+ const maxList = Math.max(5, Math.min(15, rows - 12));
566
+ while (!signal?.aborted) {
567
+ const q = currentQuery.trim().toLowerCase();
568
+ // Check exact match first
569
+ const exact = allModelIds.find((id) => id.toLowerCase() === q);
570
+ if (exact) {
571
+ const meta = router.classifyModel(exact);
572
+ if (layer) {
573
+ layerPromptBox(layer, [
574
+ `Selected: \x1b[1m${exact}\x1b[0m [${meta.accessTier}]`,
575
+ 'Confirm? [Y/n, or type new keyword]',
576
+ ]);
577
+ }
578
+ else {
579
+ process.stdout.write(`\nSelected: ${exact} [${meta.accessTier}]. Confirm? [Y/n, or type new keyword]: `);
580
+ }
581
+ const conf = layer ? await askModalChoice('\n> ', { signal }) : await askQuestion('', { signal });
582
+ const confNorm = conf.trim().toLowerCase();
583
+ if (!confNorm || confNorm === 'y' || confNorm === 'yes') {
584
+ return exact;
585
+ }
586
+ if (confNorm !== 'n' && confNorm !== 'no') {
587
+ currentQuery = conf.trim();
588
+ continue;
589
+ }
590
+ }
591
+ // Filter matching models
592
+ const matches = allModelIds.filter((id) => id.toLowerCase().includes(q));
593
+ if (matches.length === 0) {
594
+ if (layer) {
595
+ layerPromptBox(layer, [
596
+ `\x1b[33mNo catalog model found matching "${currentQuery}".\x1b[0m`,
597
+ '---',
598
+ ' [1] Try another search keyword',
599
+ ` [2] Use "${currentQuery}" as custom Model ID`,
600
+ ' [3] Cancel search',
601
+ ]);
602
+ }
603
+ else {
604
+ process.stdout.write(`\n\x1b[33mNo catalog model found matching "${currentQuery}".\x1b[0m\n`);
605
+ process.stdout.write(' \x1b[1m[1]\x1b[0m Try another search keyword\n');
606
+ process.stdout.write(` \x1b[1m[2]\x1b[0m Use "${currentQuery}" as custom Model ID\n`);
607
+ process.stdout.write(' \x1b[1m[3]\x1b[0m Cancel search\n');
608
+ }
609
+ const action = layer
610
+ ? await askModalChoice('\n> ', { signal })
611
+ : await askQuestion('Select [1-3] or type a new search keyword: ', { signal });
612
+ const actNorm = action.trim().toLowerCase();
613
+ if (actNorm === '2') {
614
+ return currentQuery.trim();
615
+ }
616
+ else if (actNorm === '3' || actNorm === 'cancel' || actNorm === 'exit' || actNorm === 'q') {
617
+ return '';
618
+ }
619
+ else if (actNorm === '1') {
620
+ const next = await askFlowText(layer, 'Enter new search keyword: ', { signal });
621
+ if (!next)
622
+ return '';
623
+ currentQuery = next;
624
+ continue;
625
+ }
626
+ else if (action.trim()) {
627
+ currentQuery = action.trim();
628
+ continue;
629
+ }
630
+ return '';
631
+ }
632
+ if (matches.length === 1) {
633
+ const meta = router.classifyModel(matches[0]);
634
+ if (layer) {
635
+ layerPromptBox(layer, [
636
+ `\x1b[32mFound 1 matching model:\x1b[0m \x1b[1m${matches[0]}\x1b[0m [${meta.accessTier}]`,
637
+ 'Select this model? [Y/n, or type a new keyword]',
638
+ ]);
639
+ }
640
+ else {
641
+ process.stdout.write(`\n\x1b[32mFound 1 matching model:\x1b[0m \x1b[1m${matches[0]}\x1b[0m [${meta.accessTier}]\n`);
642
+ }
643
+ const conf = layer ? await askModalChoice('\n> ', { signal }) : await askQuestion('', { signal });
644
+ const confNorm = conf.trim().toLowerCase();
645
+ if (!confNorm || confNorm === 'y' || confNorm === 'yes') {
646
+ return matches[0];
647
+ }
648
+ if (confNorm !== 'n' && confNorm !== 'no') {
649
+ currentQuery = conf.trim();
650
+ continue;
651
+ }
652
+ const next = await askFlowText(layer, 'Enter new search keyword (or press Enter to cancel): ', { signal });
653
+ if (!next)
654
+ return '';
655
+ currentQuery = next;
656
+ continue;
657
+ }
658
+ // Multiple matches
659
+ if (layer) {
660
+ const lines = [`\x1b[1mFound ${matches.length} models matching "${currentQuery}":\x1b[0m`, '---'];
661
+ for (let i = 0; i < Math.min(matches.length, maxList); i++) {
662
+ const meta = router.classifyModel(matches[i]);
663
+ const tag = meta.accessTier === 'free_trial' ? 'Free Trial' : 'Paid';
664
+ lines.push(` \x1b[1m[${i + 1}]\x1b[0m ${matches[i]} \x1b[36m[${tag}]\x1b[0m`);
665
+ }
666
+ if (matches.length > maxList) {
667
+ lines.push(` \x1b[90m... and ${matches.length - maxList} more — refine the keyword\x1b[0m`);
668
+ }
669
+ lines.push(' [s] Search again with a different keyword');
670
+ lines.push(' [b] Back to main menu');
671
+ layerPromptBox(layer, lines);
672
+ }
673
+ else {
674
+ process.stdout.write(`\n\x1b[1mFound ${matches.length} models matching "${currentQuery}":\x1b[0m\n`);
675
+ process.stdout.write('\x1b[90m─────────────────────────────────────────────────────────────\x1b[0m\n');
676
+ for (let i = 0; i < matches.length; i++) {
677
+ const m = matches[i];
678
+ const meta = router.classifyModel(m);
679
+ const tag = meta.accessTier === 'free_trial' ? 'Free Trial' : 'Paid';
680
+ process.stdout.write(` \x1b[1m[${i + 1}]\x1b[0m ${m} \x1b[36m[${tag}]\x1b[0m\n`);
681
+ }
682
+ process.stdout.write(' \x1b[1m[s]\x1b[0m Search again with a different keyword\n');
683
+ process.stdout.write(' \x1b[1m[b]\x1b[0m Back to main menu\n');
684
+ process.stdout.write('\x1b[90m─────────────────────────────────────────────────────────────\x1b[0m\n');
685
+ }
686
+ const choice = layer
687
+ ? await askModalChoice('\n> ', { signal })
688
+ : await askQuestion(`Select [1-${matches.length}], "s" to search again, or type a new keyword: `, { signal });
689
+ const choiceNorm = choice.trim().toLowerCase();
690
+ if (choiceNorm === 'b' || choiceNorm === 'back' || choiceNorm === 'q') {
691
+ return '';
692
+ }
693
+ if (choiceNorm === 's' || choiceNorm === 'search') {
694
+ const next = await askFlowText(layer, 'Enter new search keyword: ', { signal });
695
+ if (!next)
696
+ return '';
697
+ currentQuery = next;
698
+ continue;
699
+ }
700
+ const num = parseInt(choice, 10);
701
+ if (!isNaN(num) && num >= 1 && num <= (layer ? Math.min(matches.length, maxList) : matches.length)) {
702
+ return matches[num - 1];
703
+ }
704
+ if (choice.trim()) {
705
+ currentQuery = choice.trim();
706
+ continue;
707
+ }
708
+ return '';
709
+ }
710
+ return '';
711
+ }
712
+ async function browseOrSearchList(title, modelIds, router, signal, layer) {
713
+ if (layer) {
714
+ // Layer mode: Cline-style interactive filter popup — type-to-filter is
715
+ // built into the list window itself.
716
+ const items = modelIds.map((id) => {
717
+ const meta = router.classifyModel(id);
718
+ const tier = meta.accessTier;
719
+ return {
720
+ label: id,
721
+ value: id,
722
+ tag: tier === 'free_trial' ? 'Free' : tier === 'paid' ? 'Paid' : 'NIM',
723
+ description: tier === 'free_trial'
724
+ ? 'Free trial endpoint'
725
+ : tier === 'paid'
726
+ ? 'Paid model'
727
+ : 'Unclassified NIM endpoint',
728
+ };
729
+ });
730
+ return (await selectListPopup(title, items, {
731
+ signal,
732
+ drawFrame: layer,
733
+ filterable: true,
734
+ pageSize: 10,
735
+ })) ?? '';
736
+ }
737
+ process.stdout.write(`\n\x1b[1m${title}\x1b[0m (${modelIds.length} models)\n`);
738
+ const filter = await askModalChoice('Filter by keyword (e.g. "glm", "flash", "coder"), [Esc] to cancel, or press Enter to list all: ', { signal });
739
+ if (filter === 'q')
740
+ return '';
741
+ if (filter.trim()) {
742
+ return searchAndSelect(filter.trim(), modelIds, router, signal);
743
+ }
744
+ return pickFromList(title, modelIds, router, signal);
745
+ }
746
+ /**
747
+ * Text input that works in both modes: inside the popup layer (raw keys, the
748
+ * prompt box above is redrawn by the layer) or inline (line-based readline).
749
+ */
750
+ async function askFlowText(layer, promptMsg, options) {
751
+ if (layer) {
752
+ layerPromptBox(layer, promptMsg.replace(/:\s*$/, ':'));
753
+ return askModalChoice('\n> ', options);
754
+ }
755
+ return askQuestion(promptMsg, options);
756
+ }
757
+ async function pickFromList(title, modelIds, router, signal, layer) {
758
+ const choices = modelIds.map((id) => {
759
+ const meta = router.classifyModel(id);
760
+ const tag = meta.accessTier === 'free_trial' ? 'Free' : meta.accessTier === 'paid' ? 'Paid' : 'Unclassified';
761
+ return {
762
+ label: id,
763
+ value: id,
764
+ tag,
765
+ };
766
+ });
767
+ if (layer) {
768
+ // Layer mode: Cline-style interactive list popup with type-to-filter.
769
+ const items = choices.map((c) => ({
770
+ label: c.label,
771
+ value: c.value,
772
+ tag: c.tag,
773
+ description: c.tag === 'Free'
774
+ ? 'Free trial endpoint'
775
+ : c.tag === 'Paid'
776
+ ? 'Paid model'
777
+ : 'Unclassified NIM endpoint',
778
+ }));
779
+ return (await selectListPopup(title, items, {
780
+ signal,
781
+ drawFrame: layer,
782
+ filterable: true,
783
+ pageSize: 10,
784
+ })) ?? '';
785
+ }
786
+ // If list is large (> 25), show in pages of 20
787
+ if (choices.length > 25) {
788
+ let page = 0;
789
+ const pageSize = 20;
790
+ const totalPages = Math.ceil(choices.length / pageSize);
791
+ while (page < totalPages) {
792
+ const pageChoices = choices.slice(page * pageSize, (page + 1) * pageSize);
793
+ process.stdout.write(`\n\x1b[1m${title}\x1b[0m (Page ${page + 1} of ${totalPages})\n`);
794
+ process.stdout.write('\x1b[90m─────────────────────────────────────────────────────────────\x1b[0m\n');
795
+ for (let i = 0; i < pageChoices.length; i++) {
796
+ const item = pageChoices[i];
797
+ const num = `[${i + 1}]`;
798
+ const tag = item.tag ? ` \x1b[36m[${item.tag}]\x1b[0m` : '';
799
+ process.stdout.write(` \x1b[1m${num}\x1b[0m ${item.label}${tag}\n`);
800
+ }
801
+ process.stdout.write('\x1b[90m─────────────────────────────────────────────────────────────\x1b[0m\n');
802
+ const promptMsg = page + 1 < totalPages
803
+ ? `Select [1-${pageChoices.length}], [Esc] to cancel, or press Enter for next page: `
804
+ : `Select [1-${pageChoices.length}] or [Esc] to cancel: `;
805
+ const answer = await askModalChoice(promptMsg, { signal });
806
+ if (answer === 'q')
807
+ return '';
808
+ if (!answer && page + 1 < totalPages) {
809
+ page++;
810
+ continue;
811
+ }
812
+ const num = parseInt(answer, 10);
813
+ if (!isNaN(num) && num >= 1 && num <= pageChoices.length) {
814
+ return pageChoices[num - 1].value;
815
+ }
816
+ // Check if user typed a model name/keyword directly
817
+ if (answer.trim()) {
818
+ const matched = modelIds.find((m) => m.toLowerCase().includes(answer.trim().toLowerCase()));
819
+ if (matched)
820
+ return matched;
821
+ }
822
+ break;
823
+ }
824
+ return '';
825
+ }
826
+ const pick = await askSelect(title, choices, 0, { signal });
827
+ return pick.value;
828
+ }
829
+ //# sourceMappingURL=model_selector.js.map