nova-terminal-assistant 0.1.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.

Potentially problematic release.


This version of nova-terminal-assistant might be problematic. Click here for more details.

Files changed (192) hide show
  1. package/README.md +358 -0
  2. package/bin/nova +38 -0
  3. package/bin/nova.js +12 -0
  4. package/package.json +67 -0
  5. package/src/cli/commands/SmartCompletion.ts +458 -0
  6. package/src/cli/index.ts +5 -0
  7. package/src/cli/startup/IFlowRepl.ts +212 -0
  8. package/src/cli/startup/InkBasedRepl.ts +1056 -0
  9. package/src/cli/startup/InteractiveRepl.ts +2833 -0
  10. package/src/cli/startup/NovaApp.ts +1861 -0
  11. package/src/cli/startup/index.ts +4 -0
  12. package/src/cli/startup/parseArgs.ts +293 -0
  13. package/src/cli/test-modules.ts +27 -0
  14. package/src/cli/ui/IFlowDropdown.ts +425 -0
  15. package/src/cli/ui/ModernReplUI.ts +276 -0
  16. package/src/cli/ui/SimpleSelector2.ts +215 -0
  17. package/src/cli/ui/components/ConfirmDialog.ts +176 -0
  18. package/src/cli/ui/components/ErrorPanel.ts +364 -0
  19. package/src/cli/ui/components/InkAppRunner.tsx +67 -0
  20. package/src/cli/ui/components/InkComponents.tsx +613 -0
  21. package/src/cli/ui/components/NovaInkApp.tsx +312 -0
  22. package/src/cli/ui/components/ProgressBar.ts +177 -0
  23. package/src/cli/ui/components/ProgressIndicator.ts +298 -0
  24. package/src/cli/ui/components/QuickActions.ts +396 -0
  25. package/src/cli/ui/components/SimpleErrorPanel.ts +231 -0
  26. package/src/cli/ui/components/StatusBar.ts +194 -0
  27. package/src/cli/ui/components/ThinkingBlockRenderer.ts +401 -0
  28. package/src/cli/ui/components/index.ts +27 -0
  29. package/src/cli/ui/ink-prototype.tsx +347 -0
  30. package/src/cli/utils/CliUI.ts +336 -0
  31. package/src/cli/utils/CompletionHelper.ts +388 -0
  32. package/src/cli/utils/EnhancedCompleter.test.ts +226 -0
  33. package/src/cli/utils/EnhancedCompleter.ts +513 -0
  34. package/src/cli/utils/ErrorEnhancer.ts +429 -0
  35. package/src/cli/utils/OutputFormatter.ts +193 -0
  36. package/src/cli/utils/index.ts +9 -0
  37. package/src/core/agents/AgentOrchestrator.ts +515 -0
  38. package/src/core/agents/index.ts +17 -0
  39. package/src/core/audit/AuditLogger.ts +509 -0
  40. package/src/core/audit/index.ts +11 -0
  41. package/src/core/auth/AuthManager.d.ts.map +1 -0
  42. package/src/core/auth/AuthManager.ts +138 -0
  43. package/src/core/auth/index.d.ts.map +1 -0
  44. package/src/core/auth/index.ts +2 -0
  45. package/src/core/config/ConfigManager.d.ts.map +1 -0
  46. package/src/core/config/ConfigManager.test.ts +183 -0
  47. package/src/core/config/ConfigManager.ts +1219 -0
  48. package/src/core/config/index.d.ts.map +1 -0
  49. package/src/core/config/index.ts +1 -0
  50. package/src/core/context/ContextBuilder.d.ts.map +1 -0
  51. package/src/core/context/ContextBuilder.ts +171 -0
  52. package/src/core/context/ContextCompressor.d.ts.map +1 -0
  53. package/src/core/context/ContextCompressor.ts +642 -0
  54. package/src/core/context/LayeredMemoryManager.ts +657 -0
  55. package/src/core/context/MemoryDiscovery.d.ts.map +1 -0
  56. package/src/core/context/MemoryDiscovery.ts +175 -0
  57. package/src/core/context/defaultSystemPrompt.d.ts.map +1 -0
  58. package/src/core/context/defaultSystemPrompt.ts +35 -0
  59. package/src/core/context/index.d.ts.map +1 -0
  60. package/src/core/context/index.ts +22 -0
  61. package/src/core/extensions/SkillGenerator.ts +421 -0
  62. package/src/core/extensions/SkillInstaller.d.ts.map +1 -0
  63. package/src/core/extensions/SkillInstaller.ts +257 -0
  64. package/src/core/extensions/SkillRegistry.d.ts.map +1 -0
  65. package/src/core/extensions/SkillRegistry.ts +361 -0
  66. package/src/core/extensions/SkillValidator.ts +525 -0
  67. package/src/core/extensions/index.ts +15 -0
  68. package/src/core/index.d.ts.map +1 -0
  69. package/src/core/index.ts +42 -0
  70. package/src/core/mcp/McpManager.d.ts.map +1 -0
  71. package/src/core/mcp/McpManager.ts +632 -0
  72. package/src/core/mcp/index.d.ts.map +1 -0
  73. package/src/core/mcp/index.ts +2 -0
  74. package/src/core/model/ModelClient.d.ts.map +1 -0
  75. package/src/core/model/ModelClient.ts +217 -0
  76. package/src/core/model/ModelConnectionTester.ts +363 -0
  77. package/src/core/model/ModelValidator.ts +348 -0
  78. package/src/core/model/index.d.ts.map +1 -0
  79. package/src/core/model/index.ts +6 -0
  80. package/src/core/model/providers/AnthropicProvider.d.ts.map +1 -0
  81. package/src/core/model/providers/AnthropicProvider.ts +279 -0
  82. package/src/core/model/providers/CodingPlanProvider.d.ts.map +1 -0
  83. package/src/core/model/providers/CodingPlanProvider.ts +210 -0
  84. package/src/core/model/providers/OllamaCloudProvider.d.ts.map +1 -0
  85. package/src/core/model/providers/OllamaCloudProvider.ts +405 -0
  86. package/src/core/model/providers/OllamaManager.d.ts.map +1 -0
  87. package/src/core/model/providers/OllamaManager.ts +201 -0
  88. package/src/core/model/providers/OllamaProvider.d.ts.map +1 -0
  89. package/src/core/model/providers/OllamaProvider.ts +73 -0
  90. package/src/core/model/providers/OpenAICompatibleProvider.d.ts.map +1 -0
  91. package/src/core/model/providers/OpenAICompatibleProvider.ts +327 -0
  92. package/src/core/model/providers/OpenAIProvider.d.ts.map +1 -0
  93. package/src/core/model/providers/OpenAIProvider.ts +29 -0
  94. package/src/core/model/providers/index.d.ts.map +1 -0
  95. package/src/core/model/providers/index.ts +12 -0
  96. package/src/core/model/types.d.ts.map +1 -0
  97. package/src/core/model/types.ts +77 -0
  98. package/src/core/security/ApprovalManager.d.ts.map +1 -0
  99. package/src/core/security/ApprovalManager.ts +174 -0
  100. package/src/core/security/FileFilter.d.ts.map +1 -0
  101. package/src/core/security/FileFilter.ts +141 -0
  102. package/src/core/security/HookExecutor.d.ts.map +1 -0
  103. package/src/core/security/HookExecutor.ts +178 -0
  104. package/src/core/security/SandboxExecutor.ts +447 -0
  105. package/src/core/security/index.d.ts.map +1 -0
  106. package/src/core/security/index.ts +8 -0
  107. package/src/core/session/AgentLoop.d.ts.map +1 -0
  108. package/src/core/session/AgentLoop.ts +501 -0
  109. package/src/core/session/SessionManager.d.ts.map +1 -0
  110. package/src/core/session/SessionManager.test.ts +183 -0
  111. package/src/core/session/SessionManager.ts +460 -0
  112. package/src/core/session/index.d.ts.map +1 -0
  113. package/src/core/session/index.ts +3 -0
  114. package/src/core/telemetry/Telemetry.d.ts.map +1 -0
  115. package/src/core/telemetry/Telemetry.ts +90 -0
  116. package/src/core/telemetry/TelemetryService.ts +531 -0
  117. package/src/core/telemetry/index.d.ts.map +1 -0
  118. package/src/core/telemetry/index.ts +12 -0
  119. package/src/core/testing/AutoFixer.ts +385 -0
  120. package/src/core/testing/ErrorAnalyzer.ts +499 -0
  121. package/src/core/testing/TestRunner.ts +265 -0
  122. package/src/core/testing/agent-cli-tests.ts +538 -0
  123. package/src/core/testing/index.ts +11 -0
  124. package/src/core/tools/ToolRegistry.d.ts.map +1 -0
  125. package/src/core/tools/ToolRegistry.test.ts +206 -0
  126. package/src/core/tools/ToolRegistry.ts +260 -0
  127. package/src/core/tools/impl/EditFileTool.d.ts.map +1 -0
  128. package/src/core/tools/impl/EditFileTool.ts +97 -0
  129. package/src/core/tools/impl/ListDirectoryTool.d.ts.map +1 -0
  130. package/src/core/tools/impl/ListDirectoryTool.ts +142 -0
  131. package/src/core/tools/impl/MemoryTool.d.ts.map +1 -0
  132. package/src/core/tools/impl/MemoryTool.ts +102 -0
  133. package/src/core/tools/impl/ReadFileTool.d.ts.map +1 -0
  134. package/src/core/tools/impl/ReadFileTool.ts +58 -0
  135. package/src/core/tools/impl/SearchContentTool.d.ts.map +1 -0
  136. package/src/core/tools/impl/SearchContentTool.ts +94 -0
  137. package/src/core/tools/impl/SearchFileTool.d.ts.map +1 -0
  138. package/src/core/tools/impl/SearchFileTool.ts +61 -0
  139. package/src/core/tools/impl/ShellTool.d.ts.map +1 -0
  140. package/src/core/tools/impl/ShellTool.ts +118 -0
  141. package/src/core/tools/impl/TaskTool.d.ts.map +1 -0
  142. package/src/core/tools/impl/TaskTool.ts +207 -0
  143. package/src/core/tools/impl/TodoTool.d.ts.map +1 -0
  144. package/src/core/tools/impl/TodoTool.ts +122 -0
  145. package/src/core/tools/impl/WebFetchTool.d.ts.map +1 -0
  146. package/src/core/tools/impl/WebFetchTool.ts +103 -0
  147. package/src/core/tools/impl/WebSearchTool.d.ts.map +1 -0
  148. package/src/core/tools/impl/WebSearchTool.ts +89 -0
  149. package/src/core/tools/impl/WriteFileTool.d.ts.map +1 -0
  150. package/src/core/tools/impl/WriteFileTool.ts +49 -0
  151. package/src/core/tools/impl/index.d.ts.map +1 -0
  152. package/src/core/tools/impl/index.ts +16 -0
  153. package/src/core/tools/index.d.ts.map +1 -0
  154. package/src/core/tools/index.ts +7 -0
  155. package/src/core/tools/schemas/execution.d.ts.map +1 -0
  156. package/src/core/tools/schemas/execution.ts +42 -0
  157. package/src/core/tools/schemas/file.d.ts.map +1 -0
  158. package/src/core/tools/schemas/file.ts +119 -0
  159. package/src/core/tools/schemas/index.d.ts.map +1 -0
  160. package/src/core/tools/schemas/index.ts +11 -0
  161. package/src/core/tools/schemas/memory.d.ts.map +1 -0
  162. package/src/core/tools/schemas/memory.ts +52 -0
  163. package/src/core/tools/schemas/orchestration.d.ts.map +1 -0
  164. package/src/core/tools/schemas/orchestration.ts +44 -0
  165. package/src/core/tools/schemas/search.d.ts.map +1 -0
  166. package/src/core/tools/schemas/search.ts +112 -0
  167. package/src/core/tools/schemas/todo.d.ts.map +1 -0
  168. package/src/core/tools/schemas/todo.ts +32 -0
  169. package/src/core/tools/schemas/web.d.ts.map +1 -0
  170. package/src/core/tools/schemas/web.ts +86 -0
  171. package/src/core/types/config.d.ts.map +1 -0
  172. package/src/core/types/config.ts +200 -0
  173. package/src/core/types/errors.d.ts.map +1 -0
  174. package/src/core/types/errors.ts +204 -0
  175. package/src/core/types/index.d.ts.map +1 -0
  176. package/src/core/types/index.ts +8 -0
  177. package/src/core/types/session.d.ts.map +1 -0
  178. package/src/core/types/session.ts +216 -0
  179. package/src/core/types/tools.d.ts.map +1 -0
  180. package/src/core/types/tools.ts +157 -0
  181. package/src/core/utils/CheckpointManager.d.ts.map +1 -0
  182. package/src/core/utils/CheckpointManager.ts +327 -0
  183. package/src/core/utils/Logger.d.ts.map +1 -0
  184. package/src/core/utils/Logger.ts +98 -0
  185. package/src/core/utils/RetryManager.ts +471 -0
  186. package/src/core/utils/TokenCounter.d.ts.map +1 -0
  187. package/src/core/utils/TokenCounter.ts +414 -0
  188. package/src/core/utils/VectorMemoryStore.ts +440 -0
  189. package/src/core/utils/helpers.d.ts.map +1 -0
  190. package/src/core/utils/helpers.ts +89 -0
  191. package/src/core/utils/index.d.ts.map +1 -0
  192. package/src/core/utils/index.ts +19 -0
@@ -0,0 +1,217 @@
1
+ // ============================================================================
2
+ // ModelClient - Unified LLM client abstraction
3
+ // ============================================================================
4
+
5
+ import type {
6
+ Message,
7
+ ContentBlock,
8
+ ToolUseContent,
9
+ TextContent,
10
+ ToolCallId,
11
+ SessionId,
12
+ } from '../types/session.js';
13
+ import type { ToolDefinition } from '../types/tools.js';
14
+ import { ModelError, RateLimitError, NovaError } from '../types/errors.js';
15
+ import { AnthropicProvider } from './providers/AnthropicProvider.js';
16
+ import { OpenAIProvider } from './providers/OpenAIProvider.js';
17
+ import { OllamaProvider } from './providers/OllamaProvider.js';
18
+ import { OllamaCloudProvider } from './providers/OllamaCloudProvider.js';
19
+ import { CodingPlanProvider, type CodingPlanPlatform } from './providers/CodingPlanProvider.js';
20
+ import type { ModelProvider, ModelRequestOptions, ModelResponse, StreamEvent } from './types.js';
21
+
22
+ export interface CreateModelClientOptions {
23
+ provider: 'anthropic' | 'openai' | 'azure' | 'ollama' | 'ollama-cloud' | 'coding-plan' | 'custom';
24
+ apiKey?: string;
25
+ baseUrl?: string;
26
+ model: string;
27
+ maxTokens?: number;
28
+ temperature?: number;
29
+ organizationId?: string;
30
+ apiVersion?: string;
31
+ /** Coding Plan platform (required when provider is 'coding-plan') */
32
+ codingPlanPlatform?: CodingPlanPlatform;
33
+ }
34
+
35
+ export class ModelClient {
36
+ private provider: ModelProvider;
37
+ private model: string;
38
+ private maxTokens: number;
39
+ private temperature: number;
40
+
41
+ constructor(options: CreateModelClientOptions) {
42
+ this.model = options.model;
43
+ this.maxTokens = options.maxTokens ?? 4096;
44
+ this.temperature = options.temperature ?? 0.7;
45
+
46
+ switch (options.provider) {
47
+ case 'anthropic':
48
+ if (!options.apiKey) {
49
+ throw new NovaError('API key is required for Anthropic provider', 'AUTH_ERROR');
50
+ }
51
+ this.provider = new AnthropicProvider({
52
+ apiKey: options.apiKey,
53
+ baseUrl: options.baseUrl,
54
+ model: options.model,
55
+ apiVersion: options.apiVersion,
56
+ });
57
+ break;
58
+
59
+ case 'openai':
60
+ if (!options.apiKey) {
61
+ throw new NovaError('API key is required for OpenAI provider', 'AUTH_ERROR');
62
+ }
63
+ this.provider = new OpenAIProvider({
64
+ apiKey: options.apiKey,
65
+ baseUrl: options.baseUrl,
66
+ organizationId: options.organizationId,
67
+ model: options.model,
68
+ });
69
+ break;
70
+
71
+ case 'azure':
72
+ if (!options.apiKey) {
73
+ throw new NovaError('API key is required for Azure OpenAI provider', 'AUTH_ERROR');
74
+ }
75
+ // Azure uses OpenAI-compatible SDK with custom baseUrl and apiVersion
76
+ this.provider = new OpenAIProvider({
77
+ apiKey: options.apiKey,
78
+ baseUrl: options.baseUrl,
79
+ organizationId: options.organizationId,
80
+ model: options.model,
81
+ });
82
+ break;
83
+
84
+ case 'ollama':
85
+ // Ollama doesn't require an API key; uses localhost by default
86
+ this.provider = new OllamaProvider({
87
+ baseUrl: options.baseUrl || 'http://localhost:11434',
88
+ model: options.model,
89
+ });
90
+ break;
91
+
92
+ case 'ollama-cloud':
93
+ // Ollama Cloud: hosted models via ollama.com with API key
94
+ if (!options.apiKey) {
95
+ throw new NovaError('API key is required for Ollama Cloud provider. Run: nova auth set ollama-cloud', 'AUTH_ERROR');
96
+ }
97
+ this.provider = new OllamaCloudProvider({
98
+ apiKey: options.apiKey,
99
+ baseUrl: options.baseUrl || 'https://ollama.com',
100
+ model: options.model,
101
+ });
102
+ break;
103
+
104
+ case 'coding-plan':
105
+ // Coding Plan: Chinese AI coding platforms (Alibaba, Tencent, Volcengine, etc.)
106
+ if (!options.apiKey) {
107
+ throw new NovaError('API key is required for Coding Plan provider. Get your key from the platform console.', 'AUTH_ERROR');
108
+ }
109
+ if (!options.codingPlanPlatform) {
110
+ throw new NovaError('codingPlanPlatform is required for Coding Plan provider. Options: alibaba, tencent, volcengine, baidu, kimi, zhipu, minimax', 'AUTH_ERROR');
111
+ }
112
+ this.provider = new CodingPlanProvider({
113
+ platform: options.codingPlanPlatform,
114
+ apiKey: options.apiKey,
115
+ model: options.model,
116
+ customBaseUrl: options.baseUrl,
117
+ });
118
+ break;
119
+
120
+ case 'custom':
121
+ // Custom provider: uses OpenAI-compatible SDK with user-provided baseUrl + apiKey
122
+ // This covers DeepSeek, Google Gemini (OpenAI compat), Together AI, Groq, etc.
123
+ this.provider = new OpenAIProvider({
124
+ apiKey: options.apiKey || 'no-key-required',
125
+ baseUrl: options.baseUrl,
126
+ model: options.model,
127
+ });
128
+ break;
129
+
130
+ default:
131
+ throw new NovaError(`Provider "${options.provider}" is not supported`, 'NOT_IMPLEMENTED');
132
+ }
133
+ }
134
+
135
+ /** Send a non-streaming completion request */
136
+ async complete(
137
+ messages: Message[],
138
+ tools: ToolDefinition[],
139
+ sessionId: SessionId,
140
+ options?: Partial<ModelRequestOptions>
141
+ ): Promise<ModelResponse> {
142
+ try {
143
+ const requestOpts: ModelRequestOptions = {
144
+ model: this.model,
145
+ maxTokens: options?.maxTokens ?? this.maxTokens,
146
+ temperature: options?.temperature ?? this.temperature,
147
+ tools,
148
+ sessionId,
149
+ stopSequences: options?.stopSequences,
150
+ systemPrompt: options?.systemPrompt,
151
+ };
152
+
153
+ return await this.provider.complete(messages, requestOpts);
154
+ } catch (err) {
155
+ if (err instanceof NovaError) throw err;
156
+ throw new ModelError(
157
+ `Model request failed: ${(err as Error).message}`,
158
+ undefined,
159
+ this.provider.name,
160
+ { originalError: err }
161
+ );
162
+ }
163
+ }
164
+
165
+ /** Send a streaming completion request */
166
+ async *stream(
167
+ messages: Message[],
168
+ tools: ToolDefinition[],
169
+ sessionId: SessionId,
170
+ options?: Partial<ModelRequestOptions>
171
+ ): AsyncGenerator<StreamEvent> {
172
+ try {
173
+ const requestOpts: ModelRequestOptions = {
174
+ model: this.model,
175
+ maxTokens: options?.maxTokens ?? this.maxTokens,
176
+ temperature: options?.temperature ?? this.temperature,
177
+ tools,
178
+ sessionId,
179
+ stopSequences: options?.stopSequences,
180
+ systemPrompt: options?.systemPrompt,
181
+ };
182
+
183
+ yield* this.provider.stream(messages, requestOpts);
184
+ } catch (err) {
185
+ if (err instanceof NovaError) throw err;
186
+ const error = new ModelError(
187
+ `Model stream failed: ${(err as Error).message}`,
188
+ undefined,
189
+ this.provider.name,
190
+ { originalError: err }
191
+ );
192
+ yield { type: 'error', error };
193
+ }
194
+ }
195
+
196
+ /** Count tokens for a message array (approximate) */
197
+ async countTokens(messages: Message[]): Promise<number> {
198
+ return this.provider.countTokens(messages);
199
+ }
200
+
201
+ /** Get the current model name */
202
+ getModel(): string {
203
+ return this.model;
204
+ }
205
+
206
+ /** Get the provider name */
207
+ getProviderName(): string {
208
+ return this.provider.name;
209
+ }
210
+
211
+ /** Update model parameters */
212
+ updateOptions(options: { maxTokens?: number; temperature?: number; model?: string }): void {
213
+ if (options.maxTokens !== undefined) this.maxTokens = options.maxTokens;
214
+ if (options.temperature !== undefined) this.temperature = options.temperature;
215
+ if (options.model !== undefined) this.model = options.model;
216
+ }
217
+ }
@@ -0,0 +1,363 @@
1
+ // ============================================================================
2
+ // ModelConnectionTester - Test model provider connectivity
3
+ // ============================================================================
4
+
5
+ import type { AuthManager } from '../auth/AuthManager.js';
6
+ import type { ConfigManager } from '../config/ConfigManager.js';
7
+ import { OllamaManager } from './providers/OllamaManager.js';
8
+
9
+ export interface ModelConnectionStatus {
10
+ provider: string;
11
+ model: string;
12
+ status: 'configured' | 'unconfigured' | 'available' | 'unavailable' | 'error';
13
+ message?: string;
14
+ latency?: number;
15
+ }
16
+
17
+ export interface ProviderConnectionStatus {
18
+ provider: string;
19
+ type: string;
20
+ status: 'configured' | 'unconfigured' | 'partial' | 'unavailable' | 'error';
21
+ hasCredentials: boolean;
22
+ models: ModelConnectionStatus[];
23
+ message?: string;
24
+ latency?: number;
25
+ }
26
+
27
+ /**
28
+ * Test connectivity for model providers
29
+ */
30
+ export class ModelConnectionTester {
31
+ private authManager: AuthManager;
32
+ private configManager: ConfigManager;
33
+ private cache: Map<string, ProviderConnectionStatus> = new Map();
34
+ private cacheTimeout = 60000; // 1 minute cache
35
+ private lastTestTime = 0;
36
+
37
+ constructor(authManager: AuthManager, configManager: ConfigManager) {
38
+ this.authManager = authManager;
39
+ this.configManager = configManager;
40
+ }
41
+
42
+ /**
43
+ * Test all providers and return their status
44
+ */
45
+ async testAllProviders(): Promise<ProviderConnectionStatus[]> {
46
+ const config = this.configManager.getConfig();
47
+ const results: ProviderConnectionStatus[] = [];
48
+
49
+ for (const [providerName, providerConfig] of Object.entries(config.models.providers)) {
50
+ const status = await this.testProvider(providerName, providerConfig.type);
51
+ results.push(status);
52
+ }
53
+
54
+ this.lastTestTime = Date.now();
55
+ return results;
56
+ }
57
+
58
+ /**
59
+ * Test a single provider
60
+ */
61
+ async testProvider(providerName: string, providerType: string): Promise<ProviderConnectionStatus> {
62
+ // Check cache
63
+ const cached = this.cache.get(providerName);
64
+ if (cached && Date.now() - this.lastTestTime < this.cacheTimeout) {
65
+ return cached;
66
+ }
67
+
68
+ const config = this.configManager.getConfig();
69
+ const providerConfig = config.models.providers[providerName];
70
+
71
+ if (!providerConfig) {
72
+ return {
73
+ provider: providerName,
74
+ type: providerType,
75
+ status: 'unconfigured',
76
+ hasCredentials: false,
77
+ models: [],
78
+ message: 'Provider not found in configuration',
79
+ };
80
+ }
81
+
82
+ const hasCredentials = this.checkCredentials(providerName, providerType);
83
+ const models: ModelConnectionStatus[] = [];
84
+ let providerStatus: ProviderConnectionStatus['status'] = 'unconfigured';
85
+ let message: string | undefined;
86
+ let latency: number | undefined;
87
+
88
+ // For Ollama, test actual connectivity
89
+ if (providerType === 'ollama') {
90
+ const startTime = Date.now();
91
+ try {
92
+ const ollamaCreds = this.authManager.getCredentials('ollama');
93
+ const baseUrl = ollamaCreds?.baseUrl || process.env.OLLAMA_HOST || 'http://localhost:11434';
94
+ const manager = new OllamaManager(baseUrl);
95
+ const isRunning = await manager.ping();
96
+ latency = Date.now() - startTime;
97
+
98
+ if (isRunning) {
99
+ providerStatus = 'configured';
100
+ message = `Running at ${baseUrl}`;
101
+
102
+ // List available models
103
+ const ollamaModels = await manager.listModels();
104
+ for (const model of ollamaModels) {
105
+ models.push({
106
+ provider: providerName,
107
+ model: model.name,
108
+ status: 'available',
109
+ latency,
110
+ });
111
+ }
112
+ } else {
113
+ providerStatus = 'unavailable';
114
+ message = `Ollama not running at ${baseUrl}`;
115
+ }
116
+ } catch (err) {
117
+ providerStatus = 'error';
118
+ message = `Failed to connect: ${(err as Error).message}`;
119
+ }
120
+ }
121
+ // For ollama-cloud, check credentials
122
+ else if (providerType === 'ollama-cloud') {
123
+ if (hasCredentials) {
124
+ providerStatus = 'configured';
125
+ message = 'API key configured';
126
+
127
+ // Add models from config
128
+ for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
129
+ models.push({
130
+ provider: providerName,
131
+ model: modelId,
132
+ status: 'configured',
133
+ });
134
+ }
135
+ } else {
136
+ providerStatus = 'unconfigured';
137
+ message = 'No API key configured';
138
+ }
139
+ }
140
+ // For other providers (anthropic, openai, etc.)
141
+ else {
142
+ if (hasCredentials) {
143
+ // Try to actually test the connection
144
+ const startTime = Date.now();
145
+ const connectionTest = await this.testApiConnection(providerName, providerConfig);
146
+ latency = Date.now() - startTime;
147
+
148
+ if (connectionTest.success) {
149
+ providerStatus = 'available';
150
+ message = connectionTest.message || `Connected (${latency}ms)`;
151
+ } else {
152
+ providerStatus = 'error';
153
+ message = connectionTest.message || 'Connection failed';
154
+ }
155
+
156
+ // Add models from config with individual status
157
+ for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
158
+ models.push({
159
+ provider: providerName,
160
+ model: modelId,
161
+ status: connectionTest.success ? 'available' : 'error',
162
+ message: connectionTest.success ? undefined : connectionTest.message,
163
+ latency: connectionTest.success ? latency : undefined,
164
+ });
165
+ }
166
+ } else {
167
+ providerStatus = 'unconfigured';
168
+ message = 'No API key configured';
169
+
170
+ // Still list models but mark as unconfigured
171
+ for (const [modelId, modelConfig] of Object.entries(providerConfig.models)) {
172
+ models.push({
173
+ provider: providerName,
174
+ model: modelId,
175
+ status: 'unconfigured',
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ const result: ProviderConnectionStatus = {
182
+ provider: providerName,
183
+ type: providerType,
184
+ status: providerStatus,
185
+ hasCredentials,
186
+ models,
187
+ message,
188
+ latency,
189
+ };
190
+
191
+ this.cache.set(providerName, result);
192
+ return result;
193
+ }
194
+
195
+ /**
196
+ * Quick check if a provider has credentials
197
+ */
198
+ checkCredentials(providerName: string, providerType: string): boolean {
199
+ // Ollama local doesn't need credentials
200
+ if (providerType === 'ollama') {
201
+ return true;
202
+ }
203
+
204
+ // Check auth manager
205
+ if (this.authManager.hasCredentials(providerName)) {
206
+ return true;
207
+ }
208
+
209
+ // Check for common env vars
210
+ const envKeyMap: Record<string, string> = {
211
+ anthropic: 'ANTHROPIC_API_KEY',
212
+ openai: 'OPENAI_API_KEY',
213
+ google: 'GOOGLE_API_KEY',
214
+ deepseek: 'DEEPSEEK_API_KEY',
215
+ qwen: 'DASHSCOPE_API_KEY',
216
+ glm: 'ZHIPU_API_KEY',
217
+ moonshot: 'MOONSHOT_API_KEY',
218
+ baichuan: 'BAICHUAN_API_KEY',
219
+ yi: 'YI_API_KEY',
220
+ minimax: 'MINIMAX_API_KEY',
221
+ groq: 'GROQ_API_KEY',
222
+ mistral: 'MISTRAL_API_KEY',
223
+ together: 'TOGETHER_API_KEY',
224
+ perplexity: 'PERPLEXITY_API_KEY',
225
+ siliconflow: 'SILICONFLOW_API_KEY',
226
+ 'ollama-cloud': 'OLLAMA_CLOUD_API_KEY',
227
+ };
228
+
229
+ const envKey = envKeyMap[providerName] || envKeyMap[providerType] || `${providerName.toUpperCase()}_API_KEY`;
230
+ return !!process.env[envKey];
231
+ }
232
+
233
+ /**
234
+ * Get cached status without testing
235
+ */
236
+ getCachedStatus(providerName: string): ProviderConnectionStatus | undefined {
237
+ return this.cache.get(providerName);
238
+ }
239
+
240
+ /**
241
+ * Clear cache
242
+ */
243
+ clearCache(): void {
244
+ this.cache.clear();
245
+ this.lastTestTime = 0;
246
+ }
247
+
248
+ /**
249
+ * Test a specific model's connectivity (for providers that support it)
250
+ */
251
+ async testModel(providerName: string, modelId: string): Promise<ModelConnectionStatus> {
252
+ const config = this.configManager.getConfig();
253
+ const providerConfig = config.models.providers[providerName];
254
+
255
+ if (!providerConfig) {
256
+ return {
257
+ provider: providerName,
258
+ model: modelId,
259
+ status: 'unconfigured',
260
+ message: 'Provider not found',
261
+ };
262
+ }
263
+
264
+ const hasCredentials = this.checkCredentials(providerName, providerConfig.type);
265
+
266
+ if (!hasCredentials && providerConfig.type !== 'ollama') {
267
+ return {
268
+ provider: providerName,
269
+ model: modelId,
270
+ status: 'unconfigured',
271
+ message: 'No API key configured',
272
+ };
273
+ }
274
+
275
+ // For Ollama, check if model is installed
276
+ if (providerConfig.type === 'ollama') {
277
+ try {
278
+ const ollamaCreds = this.authManager.getCredentials('ollama');
279
+ const baseUrl = ollamaCreds?.baseUrl || process.env.OLLAMA_HOST || 'http://localhost:11434';
280
+ const manager = new OllamaManager(baseUrl);
281
+
282
+ const isRunning = await manager.ping();
283
+ if (!isRunning) {
284
+ return {
285
+ provider: providerName,
286
+ model: modelId,
287
+ status: 'unavailable',
288
+ message: 'Ollama not running',
289
+ };
290
+ }
291
+
292
+ const modelInfo = await manager.showModel(modelId);
293
+ return {
294
+ provider: providerName,
295
+ model: modelId,
296
+ status: 'available',
297
+ message: modelInfo.details?.parameter_size || 'Available',
298
+ };
299
+ } catch (err) {
300
+ return {
301
+ provider: providerName,
302
+ model: modelId,
303
+ status: 'unavailable',
304
+ message: `Model not found: ${(err as Error).message}`,
305
+ };
306
+ }
307
+ }
308
+
309
+ // For other providers, we just check credentials
310
+ return {
311
+ provider: providerName,
312
+ model: modelId,
313
+ status: hasCredentials ? 'configured' : 'unconfigured',
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Test actual API connection for a provider
319
+ */
320
+ private async testApiConnection(
321
+ providerName: string,
322
+ providerConfig: any
323
+ ): Promise<{ success: boolean; message?: string }> {
324
+ try {
325
+ const creds = this.authManager.getCredentials(providerName);
326
+ const apiKey = creds?.apiKey || process.env[`${providerName.toUpperCase()}_API_KEY`];
327
+ const baseUrl = creds?.baseUrl || providerConfig.baseUrl;
328
+
329
+ if (!apiKey) {
330
+ return { success: false, message: 'No API key available' };
331
+ }
332
+
333
+ // For OpenAI-compatible providers, try to list models
334
+ if (providerConfig.type === 'custom' || providerConfig.type === 'openai' ||
335
+ providerConfig.baseUrl?.includes('openai') || providerConfig.baseUrl) {
336
+ try {
337
+ const { OpenAICompatibleProvider } = await import('./providers/OpenAICompatibleProvider.js');
338
+ const tester = new (class extends OpenAICompatibleProvider {
339
+ constructor() {
340
+ super({ apiKey, baseUrl, model: 'probe' });
341
+ }
342
+ get name() { return providerName; }
343
+ })();
344
+
345
+ const models = await tester.listModels();
346
+ if (models.length > 0) {
347
+ return { success: true, message: `${models.length} models available` };
348
+ }
349
+ return { success: true, message: 'Connected' };
350
+ } catch (err) {
351
+ // If listModels fails, try a simple completion as fallback
352
+ return { success: false, message: `API error: ${(err as Error).message}` };
353
+ }
354
+ }
355
+
356
+ // For other providers (Anthropic, etc.), just check credentials for now
357
+ // They could be tested with specific provider SDK calls
358
+ return { success: true, message: 'API key configured' };
359
+ } catch (err) {
360
+ return { success: false, message: `Connection failed: ${(err as Error).message}` };
361
+ }
362
+ }
363
+ }