@tenex-chat/backend 0.9.5 → 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 -46790
  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 -235
  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
@@ -301,26 +301,23 @@ export class ProjectRuntime {
301
301
 
302
302
  // Stop status publisher
303
303
  if (this.statusPublisher) {
304
- process.stdout.write(chalk.gray(" Stopping status publisher..."));
304
+ logger.info(`[ProjectRuntime] Stopping status publisher: ${this.projectId}`);
305
305
  await this.statusPublisher.stopPublishing();
306
306
  this.statusPublisher = null;
307
- console.log(chalk.gray(" done"));
308
307
  }
309
308
 
310
309
  // Stop operations status publisher
311
310
  if (this.operationsStatusPublisher) {
312
- process.stdout.write(chalk.gray(" Stopping operations status..."));
311
+ logger.info(`[ProjectRuntime] Stopping operations status: ${this.projectId}`);
313
312
  this.operationsStatusPublisher.stop();
314
313
  this.operationsStatusPublisher = null;
315
- console.log(chalk.gray(" done"));
316
314
  }
317
315
 
318
316
  // Cleanup event handler
319
317
  if (this.eventHandler) {
320
- process.stdout.write(chalk.gray(" Cleaning up event handler..."));
318
+ logger.info(`[ProjectRuntime] Cleaning up event handler: ${this.projectId}`);
321
319
  await this.eventHandler.cleanup();
322
320
  this.eventHandler = null;
323
- console.log(chalk.gray(" done"));
324
321
  }
325
322
 
326
323
  // Shutdown MCP subscription service
@@ -332,20 +329,17 @@ export class ProjectRuntime {
332
329
  }
333
330
 
334
331
  // Save conversation state
335
- process.stdout.write(chalk.gray(" Saving conversations..."));
332
+ logger.info(`[ProjectRuntime] Saving conversations: ${this.projectId}`);
336
333
  await ConversationStore.cleanup();
337
- console.log(chalk.gray(" done"));
338
334
 
339
335
  // Reset local report store
340
- process.stdout.write(chalk.gray(" Resetting report store..."));
336
+ logger.info(`[ProjectRuntime] Resetting report store: ${this.projectId}`);
341
337
  this.localReportStore.reset();
342
- console.log(chalk.gray(" done"));
343
338
 
344
339
  // Release our reference to the prefix KV store (but don't close it -
345
340
  // it's a daemon-global resource that outlives individual project runtimes)
346
- process.stdout.write(chalk.gray(" Releasing storage..."));
341
+ logger.info(`[ProjectRuntime] Releasing storage: ${this.projectId}`);
347
342
  await prefixKVStore.close();
348
- console.log(chalk.gray(" done"));
349
343
 
350
344
  // Clear context
351
345
  this.context = null;
@@ -288,6 +288,19 @@ export class SubscriptionManager {
288
288
  eventId: event.id,
289
289
  eventKind: event.kind,
290
290
  });
291
+ logger.writeToWarnLog({
292
+ timestamp: new Date().toISOString(),
293
+ level: "error",
294
+ component: "SubscriptionManager",
295
+ message: "Error handling incoming Nostr event",
296
+ context: {
297
+ eventId: event.id,
298
+ eventKind: event.kind,
299
+ pubkey: event.pubkey,
300
+ },
301
+ error: error instanceof Error ? error.message : String(error),
302
+ stack: error instanceof Error ? error.stack : undefined,
303
+ });
291
304
  }
292
305
  }
293
306
 
@@ -6,4 +6,3 @@
6
6
  */
7
7
 
8
8
  export { getDaemon } from "./Daemon";
9
- export { UnixSocketTransport } from "./UnixSocketTransport";
@@ -57,6 +57,7 @@ const IGNORED_EVENT_KINDS = [
57
57
  NDKKind.Contacts,
58
58
  NDKKind.TenexProjectStatus,
59
59
  NDKKind.TenexOperationsStatus,
60
+ NDKKind.TenexStreamTextDelta,
60
61
  ];
61
62
 
62
63
  export class EventHandler {
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
10
  import { join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
11
12
  import { initializeTelemetry } from "@/telemetry/setup";
12
13
 
13
14
  /**
@@ -25,6 +26,24 @@ interface TelemetryConfig {
25
26
  endpoint: string;
26
27
  }
27
28
 
29
+ function getCliVersion(): string {
30
+ if (process.env.npm_package_version) {
31
+ return process.env.npm_package_version;
32
+ }
33
+
34
+ try {
35
+ const packageJsonPath = fileURLToPath(new URL("../package.json", import.meta.url));
36
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
37
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
38
+ return packageJson.version;
39
+ }
40
+ } catch {
41
+ // Fall through to default when package metadata is unavailable.
42
+ }
43
+
44
+ return "0.0.0";
45
+ }
46
+
28
47
  function getTelemetryConfig(): TelemetryConfig {
29
48
  const configPath = join(getBasePath(), "config.json");
30
49
  const defaults: TelemetryConfig = {
@@ -89,7 +108,7 @@ async function main(): Promise<void> {
89
108
  program
90
109
  .name("tenex")
91
110
  .description("TENEX Command Line Interface")
92
- .version(process.env.npm_package_version || "0.8.0");
111
+ .version(getCliVersion());
93
112
 
94
113
  // Register subcommands
95
114
  program.addCommand(daemonCommand);
@@ -40,7 +40,7 @@ export class ChunkHandler {
40
40
  return;
41
41
  }
42
42
 
43
- // Emit raw-chunk event for consumers (e.g., local streaming)
43
+ // Emit raw-chunk event for low-level stream observers.
44
44
  logger.debug("[LLMService] emitting raw-chunk", { chunkType: chunk.type });
45
45
  this.emitter.emit("raw-chunk", { chunk: event.chunk });
46
46
 
@@ -56,24 +56,32 @@ export function createFinishHandler(
56
56
  // This creates intentional duplication (conversation + completion both have same text)
57
57
  // but ensures delegated agents receive the full response via p-tag on completion.
58
58
  //
59
- // Three-level fallback for finalMessage:
60
- // 1. Use cachedContent if available (normal case)
61
- // 2. Use e.text if cachedContent is empty AND e.text is non-empty (fallback when content was already published)
62
- // 3. Use error message if both are empty (edge case where no content was captured)
59
+ // Multi-level fallback for finalMessage:
60
+ // 1. Use cachedContent if available (normal case: text is still buffered)
61
+ // 2. Use e.text if non-empty (text was already published via chunk-type-change,
62
+ // but we still re-publish for delegations - see comment above)
63
+ // 3. Use accumulated steps text if non-empty (handles multi-step flows where
64
+ // the final step has no text - e.g., last step processed a tool result and
65
+ // stopped immediately; e.text = finalStep.text which is "" in that case,
66
+ // but the real response text is in an earlier step)
67
+ // 4. Use error message if all sources are empty
63
68
  const ERROR_FALLBACK_MESSAGE =
64
69
  "There was an error capturing the work done, please review the conversation for the results";
65
70
 
66
71
  const cachedContent = state.getCachedContent();
67
72
  const text = e.text ?? "";
73
+ const stepsText = e.steps.reduce((acc, step) => acc + step.text, "");
68
74
 
69
75
  const fallbackLevel =
70
76
  cachedContent.length > 0 ? "cached" :
71
77
  text.length > 0 ? "text" :
78
+ stepsText.length > 0 ? "steps" :
72
79
  "error";
73
80
 
74
81
  const finalMessage =
75
82
  fallbackLevel === "cached" ? cachedContent :
76
83
  fallbackLevel === "text" ? text :
84
+ fallbackLevel === "steps" ? stepsText :
77
85
  ERROR_FALLBACK_MESSAGE;
78
86
 
79
87
  const usedFallbackToText = fallbackLevel === "text";
@@ -108,6 +116,8 @@ export function createFinishHandler(
108
116
  "complete.message_length": finalMessage.length,
109
117
  "complete.cached_content_length": cachedContent.length,
110
118
  "complete.e_text_length": text.length,
119
+ "complete.steps_text_length": stepsText.length,
120
+ "complete.fallback_level": fallbackLevel,
111
121
  "complete.used_fallback_to_e_text": usedFallbackToText,
112
122
  "complete.used_error_fallback": usedErrorFallback,
113
123
  "complete.usage_input_tokens": usage.inputTokens,
@@ -173,6 +183,20 @@ export function createFinishHandler(
173
183
  logger.error("[LLMService] Error in onFinish handler", {
174
184
  error: error instanceof Error ? error.message : String(error),
175
185
  });
186
+ logger.writeToWarnLog({
187
+ timestamp: new Date().toISOString(),
188
+ level: "error",
189
+ component: "FinishHandler",
190
+ message: "Error in LLM onFinish handler",
191
+ context: {
192
+ provider: config.provider,
193
+ model: config.model,
194
+ finishReason: e.finishReason,
195
+ stepsCount: e.steps.length,
196
+ },
197
+ error: error instanceof Error ? error.message : String(error),
198
+ stack: error instanceof Error ? error.stack : undefined,
199
+ });
176
200
  throw error;
177
201
  }
178
202
  };
@@ -1,137 +1,255 @@
1
1
  import { config } from "@/services/ConfigService";
2
2
  import type { TenexLLMs } from "@/services/config/types";
3
+ import {
4
+ createPrompt,
5
+ useState,
6
+ useEffect,
7
+ useRef,
8
+ useKeypress,
9
+ usePrefix,
10
+ isEnterKey,
11
+ isUpKey,
12
+ isDownKey,
13
+ makeTheme,
14
+ } from "@inquirer/core";
15
+ import { cursorHide } from "@inquirer/ansi";
3
16
  import chalk from "chalk";
4
- import inquirer from "inquirer";
17
+ import { inquirerTheme } from "@/utils/cli-theme";
18
+ import * as display from "@/commands/setup/display";
5
19
  import { llmServiceFactory } from "./LLMServiceFactory";
6
20
  import { ConfigurationManager } from "./utils/ConfigurationManager";
7
21
  import { ConfigurationTester } from "./utils/ConfigurationTester";
22
+ import type { TestResult } from "./utils/ConfigurationTester";
8
23
  import { ProviderConfigUI } from "./utils/ProviderConfigUI";
9
- import { runProviderSetup } from "./utils/provider-setup";
10
24
 
11
- /**
12
- * Internal type used by editor to work with providers
13
- * Merges providers with llms for internal convenience
14
- */
15
25
  type LLMConfigWithProviders = TenexLLMs & {
16
26
  providers: Record<string, { apiKey: string | string[] }>;
17
27
  };
18
28
 
19
- /**
20
- * LLM Configuration Editor - Simple menu orchestrator
21
- * Note: LLM configs are now global only (no project-level llms.json)
22
- */
23
- export class LLMConfigEditor {
24
- constructor() {}
29
+ type ListItem = { name: string; value: string; configName?: string };
30
+ type ActionItem = { name: string; value: string; key: string };
25
31
 
26
- async showMainMenu(): Promise<void> {
27
- const llmsConfig = await this.loadConfig();
32
+ type MenuConfig = {
33
+ message: string;
34
+ items: ListItem[];
35
+ actions: ActionItem[];
36
+ onTest?: (configName: string) => Promise<TestResult>;
37
+ };
38
+
39
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
40
+
41
+ const menuTheme = {
42
+ icon: { cursor: inquirerTheme.icon.cursor },
43
+ style: {
44
+ highlight: inquirerTheme.style.highlight,
45
+ },
46
+ };
47
+
48
+ const selectWithFooter = createPrompt<string, MenuConfig>((config, done) => {
49
+ const { items, actions } = config;
50
+ const theme = makeTheme(menuTheme);
51
+ // items + actions + Done
52
+ const doneIndex = items.length + actions.length;
53
+ const totalNavigable = doneIndex + 1;
54
+
55
+ const [active, setActive] = useState(0);
56
+ const resultsRef = useRef<Record<string, TestResult>>({});
57
+ const [testing, setTesting] = useState<string | null>(null);
58
+ const [spinnerFrame, setSpinnerFrame] = useState(0);
59
+ const prefix = usePrefix({ status: "idle", theme });
60
+
61
+ const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
62
+
63
+ useEffect(() => {
64
+ if (testing && !timerRef.current) {
65
+ timerRef.current = setInterval(() => {
66
+ setSpinnerFrame(spinnerFrame + 1);
67
+ }, 80);
68
+ }
69
+ if (!testing && timerRef.current) {
70
+ clearInterval(timerRef.current);
71
+ timerRef.current = null;
72
+ }
73
+ return () => {
74
+ if (timerRef.current) {
75
+ clearInterval(timerRef.current);
76
+ timerRef.current = null;
77
+ }
78
+ };
79
+ }, [testing != null, spinnerFrame]);
80
+
81
+ useKeypress((key, rl) => {
82
+ if (testing) return;
28
83
 
29
- console.log(chalk.cyan("\n=== LLM Configuration ===\n"));
30
- ProviderConfigUI.displayCurrentConfig(llmsConfig);
31
-
32
- const { action } = await inquirer.prompt([
33
- {
34
- type: "select",
35
- name: "action",
36
- message: "What would you like to do?",
37
- choices: [
38
- { name: "Add new configuration", value: "add" },
39
- { name: "Create meta model", value: "addMeta" },
40
- { name: "Delete configuration", value: "delete" },
41
- {
42
- name: `Default agents' model: ${llmsConfig.default || "none"}`,
43
- value: "default",
44
- },
45
- {
46
- name: `Summarization model: ${llmsConfig.summarization || "none"}`,
47
- value: "summarization",
48
- },
49
- {
50
- name: `Supervision model: ${llmsConfig.supervision || "none"}`,
51
- value: "supervision",
52
- },
53
- {
54
- name: `Search model: ${llmsConfig.search || "none"}`,
55
- value: "search",
56
- },
57
- {
58
- name: `Prompt compilation model: ${llmsConfig.promptCompilation || "none"}`,
59
- value: "promptCompilation",
60
- },
61
- {
62
- name: `Compression model: ${llmsConfig.compression || "none"}`,
63
- value: "compression",
64
- },
65
- { name: "Test configuration", value: "test" },
66
- { name: "Exit", value: "exit" },
67
- ],
68
- },
69
- ]);
70
-
71
- if (action === "exit") process.exit(0);
72
-
73
- if (action === "test") {
74
- await ConfigurationTester.test(llmsConfig);
84
+ if (isEnterKey(key)) {
85
+ if (active < items.length) {
86
+ done(items[active]!.value);
87
+ } else if (active < doneIndex) {
88
+ done(actions[active - items.length]!.value);
89
+ } else {
90
+ done("done");
91
+ }
92
+ } else if (isUpKey(key) || isDownKey(key)) {
93
+ rl.clearLine(0);
94
+ const offset = isUpKey(key) ? -1 : 1;
95
+ setActive((active + offset + totalNavigable) % totalNavigable);
96
+ } else if (key.name === "t" && active < items.length) {
97
+ const item = items[active];
98
+ if (item?.configName && config.onTest) {
99
+ if (resultsRef.current[item.configName]) return;
100
+ setTesting(item.configName);
101
+ config.onTest(item.configName).then((result) => {
102
+ resultsRef.current[item.configName!] = result;
103
+ setTesting(null);
104
+ });
105
+ }
106
+ } else if (key.name === "d" && active < items.length) {
107
+ const configValue = items[active]?.value;
108
+ if (configValue?.startsWith("config:")) {
109
+ const configName = configValue.slice("config:".length);
110
+ done(`delete:${configName}`);
111
+ }
75
112
  } else {
76
- // All other actions use ConfigurationManager
77
- if (action === "add") await ConfigurationManager.add(llmsConfig);
78
- if (action === "addMeta") await ConfigurationManager.addMetaModel(llmsConfig);
79
- if (action === "delete") await ConfigurationManager.delete(llmsConfig);
80
- if (action === "default") await ConfigurationManager.setDefault(llmsConfig);
81
- if (action === "summarization") await ConfigurationManager.setSummarizationModel(llmsConfig);
82
- if (action === "supervision") await ConfigurationManager.setSupervisionModel(llmsConfig);
83
- if (action === "search") await ConfigurationManager.setSearchModel(llmsConfig);
84
- if (action === "promptCompilation") await ConfigurationManager.setPromptCompilationModel(llmsConfig);
85
- if (action === "compression") await ConfigurationManager.setCompressionModel(llmsConfig);
86
- await this.saveConfig(llmsConfig);
113
+ const match = actions.find((a) => a.key === key.name);
114
+ if (match) {
115
+ done(match.value);
116
+ }
87
117
  }
118
+ });
88
119
 
89
- await this.showMainMenu();
120
+ const message = theme.style.message(config.message, "idle");
121
+ const cursor = theme.icon.cursor;
122
+ const lines: string[] = [];
123
+
124
+ lines.push(`${prefix} ${message}`);
125
+
126
+ if (items.length === 0) {
127
+ lines.push(chalk.dim(" No configurations yet"));
128
+ } else {
129
+ for (let i = 0; i < items.length; i++) {
130
+ const item = items[i]!;
131
+ const isActive = i === active;
132
+ const pfx = isActive ? `${cursor} ` : " ";
133
+ const color = isActive ? theme.style.highlight : (x: string) => x;
134
+ const name = item.configName;
135
+
136
+ if (name && testing === name) {
137
+ const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
138
+ lines.push(`${pfx}${chalk.yellow(frame)} ${color(item.name)}`);
139
+ } else {
140
+ const result = name ? resultsRef.current[name] : undefined;
141
+ if (result) {
142
+ const icon = result.success ? chalk.green("✓") : chalk.red("✗");
143
+ const errorHint = !result.success ? ` ${chalk.dim(result.error)}` : "";
144
+ lines.push(`${pfx}${icon} ${color(item.name)}${errorHint}`);
145
+ } else {
146
+ lines.push(`${pfx} ${color(item.name)}`);
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ lines.push(` ${"─".repeat(40)}`);
153
+
154
+ for (let i = 0; i < actions.length; i++) {
155
+ const action = actions[i]!;
156
+ const idx = items.length + i;
157
+ const isActive = active === idx;
158
+ const pfx = isActive ? `${cursor} ` : " ";
159
+ lines.push(`${pfx}${chalk.cyan(action.name)}`);
90
160
  }
91
161
 
92
- async runOnboardingFlow(): Promise<void> {
93
- console.log(chalk.green("\n🚀 Welcome to TENEX LLM Setup!\n"));
162
+ const donePfx = active === doneIndex ? `${cursor} ` : " ";
163
+ lines.push(`${donePfx}${display.doneLabel()}`);
94
164
 
165
+ const helpParts = [
166
+ `${chalk.bold("↑↓")} ${chalk.dim("navigate")}`,
167
+ `${chalk.bold("⏎")} ${chalk.dim("select")}`,
168
+ `${chalk.bold("t")} ${chalk.dim("test")}`,
169
+ `${chalk.bold("d")} ${chalk.dim("delete")}`,
170
+ ];
171
+ lines.push(chalk.dim(` ${helpParts.join(chalk.dim(" • "))}`));
172
+
173
+ return `${lines.join("\n")}${cursorHide}`;
174
+ });
175
+
176
+ export class LLMConfigEditor {
177
+ private advanced: boolean;
178
+
179
+ constructor(options: { advanced?: boolean } = {}) {
180
+ this.advanced = options.advanced ?? false;
181
+ }
182
+
183
+ async showMainMenu(): Promise<void> {
95
184
  const llmsConfig = await this.loadConfig();
96
- const globalPath = config.getGlobalPath();
97
185
 
98
- // Step 1: Configure providers
99
- console.log(chalk.cyan("Step 1: Configure Provider API Keys"));
100
- const existingProviders = await config.loadTenexProviders(globalPath);
101
- const updatedProviders = await runProviderSetup(existingProviders);
102
- llmsConfig.providers = updatedProviders.providers;
103
- await this.saveConfig(llmsConfig);
186
+ display.blank();
187
+ display.step(0, 0, "LLM Configuration");
188
+ ProviderConfigUI.displayProviders(llmsConfig);
104
189
 
105
- // Step 2: Create first configuration
106
- console.log(chalk.cyan("\nStep 2: Create Your First Configuration"));
107
- await ConfigurationManager.add(llmsConfig, true);
108
- await this.saveConfig(llmsConfig);
190
+ const configNames = Object.keys(llmsConfig.configurations);
191
+ const items: ListItem[] = configNames.map((name) => {
192
+ const cfg = llmsConfig.configurations[name];
193
+ const detail =
194
+ cfg.provider === "meta"
195
+ ? `multi-modal, ${Object.keys((cfg as { variants: Record<string, unknown> }).variants).length} variants`
196
+ : `${"model" in cfg ? cfg.model : "unknown"}`;
197
+ return {
198
+ name: `${name} ${chalk.dim(detail)}`,
199
+ value: `config:${name}`,
200
+ configName: name,
201
+ };
202
+ });
203
+
204
+ const actions: ActionItem[] = [
205
+ { name: `Add new configuration ${chalk.dim("(a)")}`, value: "add", key: "a" },
206
+ { name: `Add multi-modal configuration ${chalk.dim("(m)")}`, value: "addMultiModal", key: "m" },
207
+ ];
208
+
209
+ const action = await selectWithFooter({
210
+ message: "Configurations",
211
+ items,
212
+ actions,
213
+ onTest: (configName) => ConfigurationTester.runTest(llmsConfig, configName),
214
+ });
109
215
 
110
- // Step 3: Offer to test
111
- const { shouldTest } = await inquirer.prompt([
112
- {
113
- type: "confirm",
114
- name: "shouldTest",
115
- message: "Would you like to test your configuration?",
116
- default: true,
117
- },
118
- ]);
119
-
120
- if (shouldTest) {
121
- await ConfigurationTester.test(llmsConfig);
216
+ if (action.startsWith("delete:")) {
217
+ const configName = action.slice("delete:".length);
218
+ await this.deleteConfig(llmsConfig, configName);
219
+ } else if (action === "add") {
220
+ await ConfigurationManager.add(llmsConfig, this.advanced);
221
+ await this.saveConfig(llmsConfig);
222
+ } else if (action === "addMultiModal") {
223
+ await ConfigurationManager.addMultiModal(llmsConfig);
224
+ await this.saveConfig(llmsConfig);
225
+ } else if (action === "done") {
226
+ return;
122
227
  }
123
228
 
124
- console.log(chalk.green("\n✅ LLM configuration complete!"));
229
+ await this.showMainMenu();
230
+ }
231
+
232
+ private async deleteConfig(llmsConfig: LLMConfigWithProviders, configName: string): Promise<void> {
233
+ delete llmsConfig.configurations[configName];
234
+
235
+ if (llmsConfig.default === configName) {
236
+ const remaining = Object.keys(llmsConfig.configurations);
237
+ llmsConfig.default = remaining.length > 0 ? remaining[0] : undefined;
238
+ if (llmsConfig.default) {
239
+ display.hint(`Default changed to "${llmsConfig.default}"`);
240
+ }
241
+ }
242
+
243
+ display.success(`Configuration "${configName}" deleted`);
244
+ await this.saveConfig(llmsConfig);
125
245
  }
126
246
 
127
247
  private async loadConfig(): Promise<LLMConfigWithProviders> {
128
248
  const globalPath = config.getGlobalPath();
129
249
 
130
- // Load providers and llms separately
131
250
  const providersConfig = await config.loadTenexProviders(globalPath);
132
251
  const llmsConfig = await config.loadTenexLLMs(globalPath);
133
252
 
134
- // Merge for internal editor use
135
253
  return {
136
254
  ...llmsConfig,
137
255
  providers: providersConfig.providers,
@@ -139,16 +257,10 @@ export class LLMConfigEditor {
139
257
  }
140
258
 
141
259
  private async saveConfig(llmsConfig: LLMConfigWithProviders): Promise<void> {
142
- // Split providers and llms for separate storage
143
260
  const { providers, ...llmsWithoutProviders } = llmsConfig;
144
261
 
145
- // Save providers to providers.json
146
262
  await config.saveGlobalProviders({ providers });
147
-
148
- // Save llms to llms.json
149
263
  await config.saveGlobalLLMs(llmsWithoutProviders as TenexLLMs);
150
-
151
- // Re-initialize factory with updated providers
152
264
  await llmServiceFactory.initializeProviders(providers);
153
265
  }
154
266
  }
package/src/llm/index.ts CHANGED
@@ -4,9 +4,5 @@ export { LLMService } from "./service";
4
4
  // Export factory
5
5
  export { LLMServiceFactory, llmServiceFactory } from "./LLMServiceFactory";
6
6
 
7
- // Export stream publisher
8
- export { StreamPublisher, streamPublisher, type StreamTransport } from "./StreamPublisher";
9
- export type { LocalStreamChunk } from "./types";
10
-
11
7
  // Export types
12
8
  export * from "./types";
@@ -134,8 +134,8 @@ export class MetaModelResolver {
134
134
  }
135
135
 
136
136
  /**
137
- * Select the winning variant from a set of matches using tier-based resolution.
138
- * Highest tier wins. If tiers are equal, first match wins.
137
+ * Select the winning variant from a set of matches using position-based resolution.
138
+ * First keyword match wins (earliest position in message).
139
139
  */
140
140
  private static selectWinningVariant(
141
141
  matches: Array<{ keyword: string; variantName: string; variant: MetaModelVariant; position: number }>
@@ -144,16 +144,7 @@ export class MetaModelResolver {
144
144
  return null;
145
145
  }
146
146
 
147
- // Sort by tier (descending), then by position (ascending for first match)
148
- const sorted = [...matches].sort((a, b) => {
149
- const tierA = a.variant.tier ?? 0;
150
- const tierB = b.variant.tier ?? 0;
151
- if (tierB !== tierA) {
152
- return tierB - tierA; // Higher tier wins
153
- }
154
- return a.position - b.position; // Earlier position wins if tiers equal
155
- });
156
-
147
+ const sorted = [...matches].sort((a, b) => a.position - b.position);
157
148
  return sorted[0];
158
149
  }
159
150
 
@@ -256,7 +247,6 @@ export class MetaModelResolver {
256
247
  logger.debug("[MetaModelResolver] Resolved variant", {
257
248
  variantName: winner.variantName,
258
249
  matchedKeywords,
259
- tier: winner.variant.tier ?? 0,
260
250
  configName: winner.variant.model,
261
251
  });
262
252
 
@@ -314,11 +304,6 @@ export class MetaModelResolver {
314
304
  static generateSystemPromptFragment(config: MetaModelConfiguration): string {
315
305
  const lines: string[] = [];
316
306
 
317
- if (config.description) {
318
- lines.push(config.description);
319
- lines.push("");
320
- }
321
-
322
307
  lines.push("You have access to the following models via change_model() tool:");
323
308
 
324
309
  for (const [variantName, variant] of Object.entries(config.variants)) {