@tenex-chat/backend 0.9.4 → 0.9.6

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 (148) hide show
  1. package/README.md +5 -1
  2. package/dist/daemon-wrapper.cjs +47 -0
  3. package/dist/index.js +59268 -0
  4. package/dist/wrapper.js +171 -0
  5. package/package.json +19 -27
  6. package/src/agents/AgentRegistry.ts +9 -7
  7. package/src/agents/AgentStorage.ts +24 -1
  8. package/src/agents/agent-installer.ts +6 -0
  9. package/src/agents/agent-loader.ts +7 -2
  10. package/src/agents/constants.ts +10 -2
  11. package/src/agents/execution/AgentExecutor.ts +35 -6
  12. package/src/agents/execution/StreamCallbacks.ts +53 -13
  13. package/src/agents/execution/StreamExecutionHandler.ts +110 -16
  14. package/src/agents/execution/StreamSetup.ts +19 -9
  15. package/src/agents/execution/ToolEventHandlers.ts +112 -0
  16. package/src/agents/role-categories.ts +53 -0
  17. package/src/agents/types/runtime.ts +7 -0
  18. package/src/agents/types/storage.ts +7 -0
  19. package/src/commands/agent/import/openclaw-distiller.ts +63 -7
  20. package/src/commands/agent/import/openclaw-reader.ts +54 -0
  21. package/src/commands/agent/import/openclaw.ts +120 -29
  22. package/src/commands/agent/index.ts +83 -2
  23. package/src/commands/setup/display.ts +123 -0
  24. package/src/commands/setup/embed.ts +13 -13
  25. package/src/commands/setup/global-system-prompt.ts +15 -17
  26. package/src/commands/setup/image.ts +17 -20
  27. package/src/commands/setup/interactive.ts +37 -20
  28. package/src/commands/setup/llm.ts +12 -7
  29. package/src/commands/setup/onboarding.ts +1580 -248
  30. package/src/commands/setup/providers.ts +3 -3
  31. package/src/conversations/ConversationStore.ts +23 -2
  32. package/src/conversations/MessageBuilder.ts +51 -73
  33. package/src/conversations/formatters/utils/conversation-transcript-formatter.ts +425 -0
  34. package/src/conversations/search/embeddings/ConversationEmbeddingService.ts +40 -98
  35. package/src/conversations/search/embeddings/ConversationIndexingJob.ts +40 -52
  36. package/src/conversations/services/ConversationSummarizer.ts +1 -2
  37. package/src/conversations/types.ts +11 -0
  38. package/src/daemon/Daemon.ts +78 -57
  39. package/src/daemon/ProjectRuntime.ts +6 -12
  40. package/src/daemon/SubscriptionManager.ts +13 -0
  41. package/src/daemon/index.ts +0 -1
  42. package/src/event-handler/index.ts +1 -0
  43. package/src/index.ts +20 -1
  44. package/src/llm/ChunkHandler.ts +1 -1
  45. package/src/llm/FinishHandler.ts +28 -4
  46. package/src/llm/LLMConfigEditor.ts +218 -106
  47. package/src/llm/index.ts +0 -4
  48. package/src/llm/meta/MetaModelResolver.ts +3 -18
  49. package/src/llm/middleware/message-sanitizer.ts +153 -0
  50. package/src/llm/providers/ollama-models.ts +0 -38
  51. package/src/llm/service.ts +50 -15
  52. package/src/llm/types.ts +0 -12
  53. package/src/llm/utils/ConfigurationManager.ts +88 -465
  54. package/src/llm/utils/ConfigurationTester.ts +42 -185
  55. package/src/llm/utils/ModelSelector.ts +156 -92
  56. package/src/llm/utils/ProviderConfigUI.ts +10 -141
  57. package/src/llm/utils/models-dev-cache.ts +102 -23
  58. package/src/llm/utils/provider-select-prompt.ts +284 -0
  59. package/src/llm/utils/provider-setup.ts +81 -34
  60. package/src/llm/utils/variant-list-prompt.ts +361 -0
  61. package/src/nostr/AgentEventDecoder.ts +1 -0
  62. package/src/nostr/AgentEventEncoder.ts +37 -0
  63. package/src/nostr/AgentProfilePublisher.ts +13 -0
  64. package/src/nostr/AgentPublisher.ts +26 -0
  65. package/src/nostr/kinds.ts +1 -0
  66. package/src/nostr/ndkClient.ts +4 -1
  67. package/src/nostr/types.ts +12 -0
  68. package/src/prompts/fragments/25-rag-instructions.ts +22 -21
  69. package/src/prompts/fragments/31-agents-md-guidance.ts +7 -21
  70. package/src/prompts/fragments/index.ts +2 -0
  71. package/src/prompts/utils/systemPromptBuilder.ts +18 -28
  72. package/src/services/AgentDefinitionMonitor.ts +8 -0
  73. package/src/services/ConfigService.ts +34 -0
  74. package/src/services/PubkeyService.ts +7 -1
  75. package/src/services/compression/CompressionService.ts +133 -74
  76. package/src/services/compression/compression-utils.ts +110 -19
  77. package/src/services/config/types.ts +0 -6
  78. package/src/services/dispatch/AgentDispatchService.ts +79 -0
  79. package/src/services/intervention/InterventionService.ts +78 -5
  80. package/src/services/nip46/Nip46SigningService.ts +30 -1
  81. package/src/services/projects/ProjectContext.ts +8 -6
  82. package/src/services/rag/RAGCollectionRegistry.ts +199 -0
  83. package/src/services/rag/RAGDatabaseService.ts +2 -7
  84. package/src/services/rag/RAGOperations.ts +25 -45
  85. package/src/services/rag/RAGService.ts +0 -31
  86. package/src/services/rag/RagSubscriptionService.ts +71 -122
  87. package/src/services/rag/rag-utils.ts +13 -0
  88. package/src/services/ral/RALRegistry.ts +25 -184
  89. package/src/services/reports/ReportEmbeddingService.ts +63 -113
  90. package/src/services/search/UnifiedSearchService.ts +115 -4
  91. package/src/services/search/index.ts +1 -0
  92. package/src/services/search/projectFilter.ts +20 -4
  93. package/src/services/search/providers/ConversationSearchProvider.ts +1 -0
  94. package/src/services/search/providers/GenericCollectionSearchProvider.ts +81 -0
  95. package/src/services/search/providers/LessonSearchProvider.ts +1 -8
  96. package/src/services/search/providers/ReportSearchProvider.ts +1 -0
  97. package/src/services/search/types.ts +24 -3
  98. package/src/services/trust-pubkeys/SystemPubkeyListService.ts +148 -0
  99. package/src/services/trust-pubkeys/TrustPubkeyService.ts +70 -9
  100. package/src/telemetry/setup.ts +2 -13
  101. package/src/tools/implementations/ask.ts +3 -3
  102. package/src/tools/implementations/conversation_get.ts +28 -268
  103. package/src/tools/implementations/fs_grep.ts +6 -6
  104. package/src/tools/implementations/fs_read.ts +2 -0
  105. package/src/tools/implementations/fs_write.ts +2 -0
  106. package/src/tools/implementations/learn.ts +38 -50
  107. package/src/tools/implementations/rag_add_documents.ts +6 -4
  108. package/src/tools/implementations/rag_create_collection.ts +37 -4
  109. package/src/tools/implementations/rag_delete_collection.ts +9 -0
  110. package/src/tools/implementations/{search.ts → rag_search.ts} +31 -25
  111. package/src/tools/registry.ts +7 -8
  112. package/src/tools/types.ts +11 -2
  113. package/src/tools/utils/transcript-args.ts +13 -0
  114. package/src/utils/cli-theme.ts +13 -0
  115. package/src/utils/logger.ts +55 -0
  116. package/src/utils/metadataKeys.ts +17 -0
  117. package/src/utils/sqlEscaping.ts +39 -0
  118. package/src/wrapper.ts +7 -3
  119. package/dist/src/index.js +0 -46778
  120. package/dist/tenex-backend-wrapper.cjs +0 -3
  121. package/src/agents/execution/constants.ts +0 -16
  122. package/src/agents/execution/index.ts +0 -3
  123. package/src/agents/index.ts +0 -4
  124. package/src/commands/agent.ts +0 -215
  125. package/src/conversations/formatters/DelegationXmlFormatter.ts +0 -64
  126. package/src/conversations/formatters/index.ts +0 -9
  127. package/src/conversations/index.ts +0 -2
  128. package/src/conversations/utils/content-utils.ts +0 -69
  129. package/src/daemon/UnixSocketTransport.ts +0 -318
  130. package/src/event-handler/newConversation.ts +0 -165
  131. package/src/events/NDKProjectStatus.ts +0 -384
  132. package/src/events/index.ts +0 -4
  133. package/src/lib/json-parser.ts +0 -30
  134. package/src/llm/RecordingState.ts +0 -37
  135. package/src/llm/StreamPublisher.ts +0 -40
  136. package/src/llm/middleware/flight-recorder.ts +0 -188
  137. package/src/llm/utils/claudeCodePromptCompiler.ts +0 -141
  138. package/src/nostr/constants.ts +0 -38
  139. package/src/prompts/core/index.ts +0 -3
  140. package/src/prompts/index.ts +0 -21
  141. package/src/services/image/index.ts +0 -12
  142. package/src/services/status/index.ts +0 -11
  143. package/src/telemetry/diagnostics.ts +0 -27
  144. package/src/tools/implementations/rag_query.ts +0 -107
  145. package/src/types/index.ts +0 -46
  146. package/src/utils/agentFetcher.ts +0 -107
  147. package/src/utils/conversation-utils.ts +0 -1
  148. package/src/utils/process.ts +0 -49
@@ -1,71 +1,52 @@
1
1
  import type { CompleteEvent, ContentEvent, StreamErrorEvent } from "@/llm/types";
2
2
  import { config as configService } from "@/services/ConfigService";
3
3
  import type { TenexLLMs } from "@/services/config/types";
4
- import { isMetaModelConfiguration } from "@/services/config/types";
5
- import chalk from "chalk";
6
- import inquirer from "inquirer";
7
- import { z } from "zod";
8
4
  import { llmServiceFactory } from "../LLMServiceFactory";
9
5
 
10
- /**
11
- * Extended type for editor use - includes providers
12
- */
13
6
  type TenexLLMsWithProviders = TenexLLMs & {
14
7
  providers: Record<string, { apiKey: string | string[] }>;
15
8
  };
16
9
 
17
- /**
18
- * Tests LLM configurations
19
- */
20
- export class ConfigurationTester {
21
- static async test(llmsConfig: TenexLLMsWithProviders): Promise<void> {
22
- const configNames = Object.keys(llmsConfig.configurations);
10
+ export type TestResult = { success: true } | { success: false; error: string };
11
+
12
+ function silenceConsole(): () => void {
13
+ const origLog = console.log;
14
+ const origWarn = console.warn;
15
+ const origError = console.error;
16
+ const origInfo = console.info;
17
+ const noop = () => {};
18
+ console.log = noop;
19
+ console.warn = noop;
20
+ console.error = noop;
21
+ console.info = noop;
22
+ return () => {
23
+ console.log = origLog;
24
+ console.warn = origWarn;
25
+ console.error = origError;
26
+ console.info = origInfo;
27
+ };
28
+ }
23
29
 
24
- if (configNames.length === 0) {
25
- console.log(chalk.yellow("⚠️ No configurations to test"));
26
- return;
30
+ export class ConfigurationTester {
31
+ /**
32
+ * Run a silent test against a configuration. Returns pass/fail result.
33
+ * All console output is suppressed during the test.
34
+ */
35
+ static async runTest(llmsConfig: TenexLLMsWithProviders, configName: string): Promise<TestResult> {
36
+ if (!llmsConfig.configurations[configName]) {
37
+ return { success: false, error: "configuration not found" };
27
38
  }
28
39
 
29
- const { name } = await inquirer.prompt([
30
- {
31
- type: "select",
32
- name: "name",
33
- message: "Select configuration to test:",
34
- choices: configNames.map((n) => {
35
- const cfg = llmsConfig.configurations[n];
36
- const isMeta = isMetaModelConfiguration(cfg);
37
- const label = n === llmsConfig.default ? `${n} (default)` : n;
38
- return {
39
- name: isMeta ? `${label} [meta model]` : label,
40
- value: n,
41
- };
42
- }),
43
- },
44
- ]);
40
+ const restoreConsole = silenceConsole();
45
41
 
46
42
  try {
47
- // Load full config first (needed for getLLMConfig and MCP server configs)
48
43
  await configService.loadConfig();
44
+ const llmConfig = configService.getLLMConfig(configName);
49
45
 
50
- // Use getLLMConfig to resolve meta models to their default variant
51
- const llmConfig = configService.getLLMConfig(name);
52
- const rawConfig = llmsConfig.configurations[name];
53
- const isMeta = isMetaModelConfiguration(rawConfig);
54
-
55
- console.log(chalk.yellow(`\nTesting configuration "${name}"${isMeta ? " (meta model - using default variant)" : ""}...`));
56
- console.log(chalk.gray(`Provider: ${llmConfig.provider}, Model: ${llmConfig.model}`));
57
-
58
- // Initialize providers before testing
59
46
  await llmServiceFactory.initializeProviders(llmsConfig.providers);
60
-
61
- // Create the service using the factory
62
47
  const service = llmServiceFactory.createService(llmConfig);
63
48
 
64
- console.log(chalk.cyan("📡 Sending test message..."));
65
- const handleContent = (event: ContentEvent): void => {
66
- process.stdout.write(chalk.cyan(event.delta));
67
- };
68
- service.on("content", handleContent);
49
+ service.on("content", (_event: ContentEvent) => {});
69
50
 
70
51
  const completePromise = new Promise<CompleteEvent>((resolve) => {
71
52
  service.once("complete", resolve);
@@ -76,154 +57,30 @@ export class ConfigurationTester {
76
57
  });
77
58
  });
78
59
 
79
- const completion = Promise.race([completePromise, errorPromise]);
80
-
81
- console.log(chalk.white("Response: "));
82
- const [, completeEvent] = await Promise.all([
60
+ await Promise.all([
83
61
  service.stream(
84
62
  [{ role: "user", content: "Say 'Hello, TENEX!' in exactly those words." }],
85
63
  {}
86
64
  ),
87
- completion,
65
+ Promise.race([completePromise, errorPromise]),
88
66
  ]);
89
67
 
90
- process.stdout.write("\n");
91
- console.log(chalk.green("✅ Test successful!"));
92
-
93
- // Show usage stats if available
94
- if (completeEvent.usage) {
95
- const usage = completeEvent.usage;
96
- const inputTokens = usage.inputTokens ?? "?";
97
- const outputTokens = usage.outputTokens ?? "?";
98
- const totalTokens = usage.totalTokens ?? "?";
99
- console.log(
100
- chalk.gray(`\nTokens: ${inputTokens} + ${outputTokens} = ${totalTokens}`)
101
- );
102
- }
103
-
104
- await new Promise((resolve) => setTimeout(resolve, 2000));
105
- } catch (error: unknown) {
106
- console.log(chalk.red("\n❌ Test failed!"));
107
-
108
- const errorMessage = error instanceof Error ? error.message : String(error);
109
- if (errorMessage) {
110
- console.log(chalk.red(`Error: ${errorMessage}`));
111
- }
112
-
113
- // Check for common issues
114
- if (errorMessage?.includes("401") || errorMessage?.includes("Unauthorized")) {
115
- console.log(chalk.yellow("\n💡 Invalid or expired API key"));
116
- } else if (errorMessage?.includes("404")) {
117
- console.log(chalk.yellow(`\n💡 Model for configuration '${name}' may not be available`));
118
- } else if (errorMessage?.includes("rate limit")) {
119
- console.log(chalk.yellow("\n💡 Rate limit hit. Please wait and try again"));
120
- }
121
-
122
- await new Promise((resolve) => setTimeout(resolve, 3000));
123
- }
124
- }
125
-
126
- /**
127
- * Test a configuration for summarization using generateObject
128
- */
129
- static async testSummarization(llmsConfig: TenexLLMsWithProviders, configName: string): Promise<void> {
130
- const rawConfig = llmsConfig.configurations[configName];
131
- if (!rawConfig) {
132
- console.log(chalk.red(`❌ Configuration "${configName}" not found`));
133
- return;
134
- }
135
-
136
- // Use getLLMConfig to resolve meta models to their default variant
137
- const llmConfig = configService.getLLMConfig(configName);
138
- const isMeta = isMetaModelConfiguration(rawConfig);
139
-
140
- console.log(chalk.yellow(`\nTesting summarization with "${configName}"${isMeta ? " (meta model - using default variant)" : ""}...`));
141
- console.log(chalk.gray(`Provider: ${llmConfig.provider}, Model: ${llmConfig.model}`));
142
-
143
- // Schema that mimics what we'd use for kind 513 summaries
144
- const SummarySchema = z.object({
145
- title: z.string().describe("A brief title for the summary"),
146
- summary: z.string().describe("A concise summary of the conversation"),
147
- keyPoints: z.array(z.string()).describe("Key points from the conversation"),
148
- });
149
-
150
- try {
151
- // Load full config (needed for MCP server configs in agent providers)
152
- await configService.loadConfig();
153
-
154
- // Initialize providers before testing
155
- await llmServiceFactory.initializeProviders(llmsConfig.providers);
156
-
157
- // Create the service using the factory
158
- const service = llmServiceFactory.createService(llmConfig);
159
-
160
- console.log(chalk.cyan("📡 Testing generateObject..."));
161
-
162
- const testConversation = `
163
- User: I need help setting up authentication for my web app.
164
- Assistant: I can help with that. What authentication method are you considering?
165
- User: I'm thinking OAuth with Google and GitHub.
166
- Assistant: Great choice. OAuth is secure and user-friendly. Let me outline the steps...
167
- `;
168
-
169
- const result = await service.generateObject(
170
- [
171
- {
172
- role: "system",
173
- content:
174
- "You are a helpful assistant that summarizes conversations. Generate a structured summary.",
175
- },
176
- {
177
- role: "user",
178
- content: `Summarize this conversation:\n${testConversation}`,
179
- },
180
- ],
181
- SummarySchema
182
- );
183
-
184
- console.log(chalk.green("\n✅ generateObject test successful!"));
185
- console.log(chalk.white("\nGenerated summary:"));
186
- console.log(chalk.cyan(` Title: ${result.object.title}`));
187
- console.log(chalk.cyan(` Summary: ${result.object.summary}`));
188
- console.log(chalk.cyan(" Key Points:"));
189
- for (const point of result.object.keyPoints) {
190
- console.log(chalk.cyan(` • ${point}`));
191
- }
192
-
193
- // Show usage stats if available
194
- if (result.usage) {
195
- const { inputTokens, outputTokens, totalTokens } = result.usage;
196
- console.log(chalk.gray(`\nTokens: ${inputTokens} + ${outputTokens} = ${totalTokens}`));
197
- }
198
-
199
- await new Promise((resolve) => setTimeout(resolve, 2000));
68
+ return { success: true };
200
69
  } catch (error: unknown) {
201
- console.log(chalk.red("\n❌ generateObject test failed!"));
202
-
203
70
  const errorMessage = error instanceof Error ? error.message : String(error);
204
- if (errorMessage) {
205
- console.log(chalk.red(`Error: ${errorMessage}`));
206
- }
207
-
208
- // Check for common issues
71
+ let hint = errorMessage;
209
72
  if (errorMessage?.includes("401") || errorMessage?.includes("Unauthorized")) {
210
- console.log(chalk.yellow("\n💡 Invalid or expired API key"));
73
+ hint = "invalid or expired API key";
211
74
  } else if (errorMessage?.includes("404")) {
212
- console.log(chalk.yellow(`\n💡 Model '${llmConfig.model}' may not be available`));
75
+ hint = "model not available";
213
76
  } else if (errorMessage?.includes("rate limit")) {
214
- console.log(chalk.yellow("\n💡 Rate limit hit. Please wait and try again"));
215
- } else if (
216
- errorMessage?.includes("structured output") ||
217
- errorMessage?.includes("json")
218
- ) {
219
- console.log(
220
- chalk.yellow(
221
- "\n💡 This model may not support structured output (generateObject)"
222
- )
223
- );
77
+ hint = "rate limited";
224
78
  }
225
-
226
- await new Promise((resolve) => setTimeout(resolve, 3000));
79
+ return { success: false, error: hint };
80
+ } finally {
81
+ // Delay restore so async logger stragglers are swallowed
82
+ await new Promise((resolve) => setTimeout(resolve, 200));
83
+ restoreConsole();
227
84
  }
228
85
  }
229
86
  }
@@ -1,7 +1,9 @@
1
1
  import chalk from "chalk";
2
2
  import inquirer from "inquirer";
3
- import { fetchOllamaModels, getPopularOllamaModels } from "../providers/ollama-models";
3
+ import { amber, inquirerTheme } from "@/utils/cli-theme";
4
+ import { fetchOllamaModels } from "../providers/ollama-models";
4
5
  import { fetchOpenRouterModels, getPopularModels } from "../providers/openrouter-models";
6
+ import { ensureCacheLoaded, getProviderModels } from "./models-dev-cache";
5
7
 
6
8
  /**
7
9
  * Utility class for interactive model selection
@@ -9,7 +11,7 @@ import { fetchOpenRouterModels, getPopularModels } from "../providers/openrouter
9
11
  */
10
12
  export class ModelSelector {
11
13
  /**
12
- * Select an Ollama model interactively
14
+ * Select an Ollama model interactively with fuzzy search
13
15
  */
14
16
  static async selectOllamaModel(currentModel?: string): Promise<string> {
15
17
  console.log(chalk.gray("Fetching available Ollama models..."));
@@ -19,23 +21,38 @@ export class ModelSelector {
19
21
  if (ollamaModels.length > 0) {
20
22
  console.log(chalk.green(`✓ Found ${ollamaModels.length} installed models`));
21
23
 
22
- const choices = [
24
+ const allChoices = [
23
25
  ...ollamaModels.map((m) => ({
24
26
  name: `${m.name} ${chalk.gray(`(${m.size})`)}`,
25
27
  value: m.name,
28
+ short: m.name,
26
29
  })),
27
- new inquirer.Separator(),
28
- { name: chalk.cyan("→ Type model name manually"), value: "__manual__" },
30
+ { name: chalk.cyan("→ Type model name manually"), value: "__manual__", short: "manual" },
29
31
  ];
30
32
 
31
33
  const { selectedModel } = await inquirer.prompt([
32
34
  {
33
- type: "select",
35
+ type: "search",
34
36
  name: "selectedModel",
35
37
  message: "Select model:",
36
- choices,
38
+ source: (term: string | undefined) => {
39
+ if (!term) return allChoices;
40
+ const lower = term.toLowerCase();
41
+ const filtered = allChoices.filter(
42
+ (c) =>
43
+ c.value === "__manual__" ||
44
+ c.value.toLowerCase().includes(lower)
45
+ );
46
+ return filtered;
47
+ },
37
48
  default: currentModel,
38
- pageSize: 15,
49
+ theme: {
50
+ ...inquirerTheme,
51
+ style: {
52
+ ...inquirerTheme.style,
53
+ searchTerm: (text: string) => amber(text || chalk.gray("Search models...")),
54
+ },
55
+ },
39
56
  },
40
57
  ]);
41
58
 
@@ -45,44 +62,13 @@ export class ModelSelector {
45
62
 
46
63
  return selectedModel;
47
64
  }
48
- console.log(chalk.yellow("⚠️ No Ollama models found. Make sure Ollama is running."));
49
- console.log(chalk.gray("Showing popular models (you'll need to pull them first)."));
50
-
51
- const popular = getPopularOllamaModels();
52
- const choices = [];
53
- for (const [category, models] of Object.entries(popular)) {
54
- choices.push(new inquirer.Separator(`--- ${category} ---`));
55
- choices.push(
56
- ...models.map((m) => ({
57
- name: m,
58
- value: m,
59
- }))
60
- );
61
- }
62
-
63
- choices.push(new inquirer.Separator());
64
- choices.push({ name: chalk.cyan("→ Type model name manually"), value: "__manual__" });
65
-
66
- const { selectedModel } = await inquirer.prompt([
67
- {
68
- type: "select",
69
- name: "selectedModel",
70
- message: "Select model:",
71
- default: currentModel,
72
- choices,
73
- pageSize: 15,
74
- },
75
- ]);
76
-
77
- if (selectedModel === "__manual__") {
78
- return await ModelSelector.promptManualModel(currentModel || "llama3.1:8b");
79
- }
80
-
81
- return selectedModel;
65
+ console.log(amber("⚠️ No local Ollama models were discovered."));
66
+ console.log(chalk.gray("Check `ollama list` and run `ollama pull <model>` if needed."));
67
+ return await ModelSelector.promptManualModel(currentModel || "llama3.1:8b");
82
68
  }
83
69
 
84
70
  /**
85
- * Select an OpenRouter model interactively
71
+ * Select an OpenRouter model interactively with fuzzy search
86
72
  */
87
73
  static async selectOpenRouterModel(currentModel?: string): Promise<string> {
88
74
  console.log(chalk.gray("Fetching available OpenRouter models..."));
@@ -92,51 +78,42 @@ export class ModelSelector {
92
78
  if (openRouterModels.length > 0) {
93
79
  console.log(chalk.green(`✓ Found ${openRouterModels.length} available models`));
94
80
 
95
- // Group models by provider
96
- const modelsByProvider: Record<string, typeof openRouterModels> = {};
97
- for (const model of openRouterModels) {
98
- const provider = model.id.split("/")[0] || "other";
99
- if (!modelsByProvider[provider]) {
100
- modelsByProvider[provider] = [];
101
- }
102
- modelsByProvider[provider].push(model);
103
- }
81
+ const allChoices = openRouterModels.map((model) => {
82
+ const pricing = `$${model.pricing.prompt}/$${model.pricing.completion}/1M`;
83
+ const context = `${Math.round(model.context_length / 1000)}k`;
84
+ const freeTag = model.id.endsWith(":free") ? chalk.green(" [FREE]") : "";
104
85
 
105
- // Build choices
106
- const choices = [];
107
- const sortedProviders = Object.keys(modelsByProvider).sort();
108
-
109
- for (const provider of sortedProviders) {
110
- choices.push(
111
- new inquirer.Separator(chalk.yellow(`--- ${provider.toUpperCase()} ---`))
112
- );
113
- const providerModels = modelsByProvider[provider];
114
-
115
- for (const model of providerModels) {
116
- const pricing = `$${model.pricing.prompt}/$${model.pricing.completion}/1M`;
117
- const context = `${Math.round(model.context_length / 1000)}k`;
118
- const freeTag = model.id.endsWith(":free") ? chalk.green(" [FREE]") : "";
119
-
120
- choices.push({
121
- name: `${model.id}${freeTag} ${chalk.gray(`- ${context} context, ${pricing}`)}`,
122
- value: model.id,
123
- short: model.id,
124
- });
125
- }
126
- }
86
+ return {
87
+ name: `${model.id}${freeTag} ${chalk.gray(`- ${context} ctx, ${pricing}`)}`,
88
+ value: model.id,
89
+ short: model.id,
90
+ };
91
+ });
127
92
 
128
- choices.push(new inquirer.Separator());
129
- choices.push({ name: chalk.cyan("→ Type model ID manually"), value: "__manual__" });
93
+ allChoices.push({ name: chalk.cyan("→ Type model ID manually"), value: "__manual__", short: "manual" });
130
94
 
131
95
  const { selectedModel } = await inquirer.prompt([
132
96
  {
133
- type: "select",
97
+ type: "search",
134
98
  name: "selectedModel",
135
99
  message: "Select model:",
136
- choices,
100
+ source: (term: string | undefined) => {
101
+ if (!term) return allChoices;
102
+ const lower = term.toLowerCase();
103
+ return allChoices.filter(
104
+ (c) =>
105
+ c.value === "__manual__" ||
106
+ c.value.toLowerCase().includes(lower)
107
+ );
108
+ },
137
109
  default: currentModel,
138
- pageSize: 20,
139
- loop: false,
110
+ theme: {
111
+ ...inquirerTheme,
112
+ style: {
113
+ ...inquirerTheme.style,
114
+ searchTerm: (text: string) => amber(text || chalk.gray("Search models...")),
115
+ },
116
+ },
140
117
  },
141
118
  ]);
142
119
 
@@ -146,7 +123,7 @@ export class ModelSelector {
146
123
 
147
124
  return selectedModel;
148
125
  }
149
- console.log(chalk.yellow("⚠️ Failed to fetch models from OpenRouter API"));
126
+ console.log(amber("⚠️ Failed to fetch models from OpenRouter API"));
150
127
  console.log(
151
128
  chalk.gray("You can still enter a model ID manually or select from popular models.")
152
129
  );
@@ -160,30 +137,44 @@ export class ModelSelector {
160
137
  { name: "Quick select from popular models", value: "quick" },
161
138
  { name: "Type model ID manually", value: "manual" },
162
139
  ],
140
+ theme: inquirerTheme,
163
141
  },
164
142
  ]);
165
143
 
166
144
  if (selectionMethod === "quick") {
167
145
  const popular = getPopularModels();
168
- const choices = [];
146
+ const popularChoices: Array<{ name: string; value: string; short: string }> = [];
169
147
  for (const [category, models] of Object.entries(popular)) {
170
- choices.push(new inquirer.Separator(`--- ${category} ---`));
171
- choices.push(
172
- ...models.map((m) => ({
173
- name: m,
148
+ for (const m of models) {
149
+ popularChoices.push({
150
+ name: `${m} ${chalk.gray(`(${category})`)}`,
174
151
  value: m,
175
- }))
176
- );
152
+ short: m,
153
+ });
154
+ }
177
155
  }
178
156
 
179
157
  const { selectedModel } = await inquirer.prompt([
180
158
  {
181
- type: "select",
159
+ type: "search",
182
160
  name: "selectedModel",
183
161
  message: "Select model:",
184
- default: currentModel,
185
- choices,
186
- pageSize: 15,
162
+ source: (term: string | undefined) => {
163
+ if (!term) return popularChoices;
164
+ const lower = term.toLowerCase();
165
+ return popularChoices.filter(
166
+ (c) =>
167
+ c.value.toLowerCase().includes(lower) ||
168
+ c.name.toLowerCase().includes(lower)
169
+ );
170
+ },
171
+ theme: {
172
+ ...inquirerTheme,
173
+ style: {
174
+ ...inquirerTheme.style,
175
+ searchTerm: (text: string) => amber(text || chalk.gray("Search models...")),
176
+ },
177
+ },
187
178
  },
188
179
  ]);
189
180
  return selectedModel;
@@ -191,6 +182,78 @@ export class ModelSelector {
191
182
  return await ModelSelector.promptManualModel(currentModel || "openai/gpt-4");
192
183
  }
193
184
 
185
+ /**
186
+ * Select a model from models.dev data (for Anthropic, OpenAI, etc.)
187
+ * Returns { id, name } so callers can use the human name for config naming.
188
+ */
189
+ static async selectModelsDevModel(
190
+ provider: string,
191
+ defaultModel?: string
192
+ ): Promise<{ id: string; name: string }> {
193
+ await ensureCacheLoaded();
194
+ const models = getProviderModels(provider);
195
+
196
+ if (models.length > 0) {
197
+ const allChoices = [
198
+ ...models.map((m) => {
199
+ const ctx = m.limit?.context
200
+ ? `${Math.round(m.limit.context / 1000)}k ctx`
201
+ : "";
202
+ const pricing = m.cost
203
+ ? `$${m.cost.input}/$${m.cost.output}/M`
204
+ : "";
205
+ const meta = [ctx, pricing].filter(Boolean).join(", ");
206
+
207
+ return {
208
+ name: `${m.name} ${chalk.gray(`(${m.id})`)} ${chalk.gray(meta ? `- ${meta}` : "")}`,
209
+ value: m.id,
210
+ short: m.id,
211
+ humanName: m.name,
212
+ };
213
+ }),
214
+ { name: chalk.cyan("→ Type model ID manually"), value: "__manual__", short: "manual", humanName: "" },
215
+ ];
216
+
217
+ const { selectedModel } = await inquirer.prompt([
218
+ {
219
+ type: "search",
220
+ name: "selectedModel",
221
+ message: "Select model:",
222
+ source: (term: string | undefined) => {
223
+ if (!term) return allChoices;
224
+ const lower = term.toLowerCase();
225
+ return allChoices.filter(
226
+ (c) =>
227
+ c.value === "__manual__" ||
228
+ c.value.toLowerCase().includes(lower) ||
229
+ c.humanName.toLowerCase().includes(lower)
230
+ );
231
+ },
232
+ default: defaultModel,
233
+ theme: {
234
+ ...inquirerTheme,
235
+ style: {
236
+ ...inquirerTheme.style,
237
+ searchTerm: (text: string) => amber(text || chalk.gray("Search models...")),
238
+ },
239
+ },
240
+ },
241
+ ]);
242
+
243
+ if (selectedModel === "__manual__") {
244
+ const id = await ModelSelector.promptManualModel(defaultModel || "");
245
+ return { id, name: id };
246
+ }
247
+
248
+ const selected = allChoices.find((c) => c.value === selectedModel);
249
+ return { id: selectedModel, name: selected?.humanName || selectedModel };
250
+ }
251
+
252
+ // No models.dev data available — fall back to manual
253
+ const id = await ModelSelector.promptManualModel(defaultModel || "");
254
+ return { id, name: id };
255
+ }
256
+
194
257
  /**
195
258
  * Prompt for manual model entry
196
259
  */
@@ -205,6 +268,7 @@ export class ModelSelector {
205
268
  if (!input.trim()) return "Model name is required";
206
269
  return true;
207
270
  },
271
+ theme: inquirerTheme,
208
272
  },
209
273
  ]);
210
274
  return inputModel;