centaurus-cli 3.1.2 → 3.1.3
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.
- package/dist/cli-adapter.js +4 -2
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/models.js +2 -0
- package/dist/config/models.js.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/services/ai-service-client.js +3 -2
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.js.map +1 -1
- package/dist/utils/git-stats.js +7 -5
- package/dist/utils/git-stats.js.map +1 -1
- package/package.json +1 -1
package/dist/config/models.js
CHANGED
|
@@ -6,6 +6,7 @@ let fetchPromise = null;
|
|
|
6
6
|
const FALLBACK_CONFIG = {
|
|
7
7
|
models: [
|
|
8
8
|
{
|
|
9
|
+
uid: "gemini-2-5-flash",
|
|
9
10
|
id: "gemini-2.5-flash",
|
|
10
11
|
name: "Gemini 2.5 Flash",
|
|
11
12
|
description: "Fast and efficient",
|
|
@@ -67,6 +68,7 @@ function getModelContextWindowSync(modelNameOrId) {
|
|
|
67
68
|
const nameLower = modelNameOrId.toLowerCase();
|
|
68
69
|
if (nameLower.includes("kimi")) return 128e3;
|
|
69
70
|
if (nameLower.includes("glm")) return 128e3;
|
|
71
|
+
if (nameLower.includes("minimax")) return 128e3;
|
|
70
72
|
if (nameLower.includes("gemini")) return 1e6;
|
|
71
73
|
return 1e6;
|
|
72
74
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config/models.ts"],"sourcesContent":["/**\r\n * Supported AI models\r\n * Fetched from backend API with caching\r\n */\r\n\r\nimport { apiClient, ModelConfig, ModelsConfig } from '../services/api-client.js';\r\nimport { logWarning } from '../utils/logger.js';\r\nimport { quickLog } from '../utils/conversation-logger.js';\r\n\r\n// Re-export types\r\nexport type { ModelConfig, ModelsConfig };\r\n\r\n/**\r\n * Cached models configuration\r\n * Fetched once per session from backend\r\n */\r\nlet cachedConfig: ModelsConfig | null = null;\r\nlet fetchPromise: Promise<ModelsConfig> | null = null;\r\n\r\n/**\r\n * Default fallback configuration when backend is unavailable\r\n */\r\nconst FALLBACK_CONFIG: ModelsConfig = {\r\n models: [\r\n {\r\n id: 'gemini-2.5-flash',\r\n name: 'Gemini 2.5 Flash',\r\n description: 'Fast and efficient',\r\n provider: 'google',\r\n contextWindow: 1000000,\r\n region: 'us-central1',\r\n supportsThinking: true,\r\n thinkingConfig: { thinking_config: { include_thoughts: true } },\r\n generationConfig: {\r\n temperature: 0.1,\r\n topP: 0.95,\r\n maxOutputTokens: 16384\r\n }\r\n }\r\n ],\r\n defaultModel: 'gemini-2.5-flash'\r\n};\r\n\r\n/**\r\n * Fetch models configuration from backend (with caching)\r\n * This is the main entry point for getting model config\r\n */\r\nexport async function fetchModelsConfig(): Promise<ModelsConfig> {\r\n // Return cached config if available\r\n if (cachedConfig) {\r\n return cachedConfig;\r\n }\r\n\r\n // If a fetch is already in progress, wait for it\r\n if (fetchPromise) {\r\n return fetchPromise;\r\n }\r\n\r\n // Start fetching from backend\r\n fetchPromise = (async () => {\r\n try {\r\n const config = await apiClient.getModelsConfig();\r\n cachedConfig = config;\r\n return config;\r\n } catch (error) {\r\n // On error, use fallback config\r\n logWarning('Failed to fetch models config from backend, using fallback');\r\n cachedConfig = FALLBACK_CONFIG;\r\n return FALLBACK_CONFIG;\r\n } finally {\r\n fetchPromise = null;\r\n }\r\n })();\r\n\r\n return fetchPromise;\r\n}\r\n\r\n/**\r\n * Clear cached config (useful for refreshing)\r\n */\r\nexport function clearModelsCache(): void {\r\n cachedConfig = null;\r\n fetchPromise = null;\r\n}\r\n\r\n/**\r\n * Get context window size for a model SYNCHRONOUSLY from cache\r\n * This is useful for UI components that need immediate values\r\n * Returns fallback defaults if cache is not yet populated\r\n * @param modelNameOrId - Model name or ID (case-insensitive search)\r\n * @returns Context window size in tokens\r\n */\r\nexport function getModelContextWindowSync(modelNameOrId: string): number {\r\n // DEBUG: Log cache state to file\r\n quickLog(`[${new Date().toISOString()}] [getModelContextWindowSync] Looking up: \"${modelNameOrId}\", cache populated: ${!!cachedConfig}, models in cache: ${cachedConfig?.models?.length || 0}\\n`);\r\n\r\n // Try to find in cache first\r\n if (cachedConfig) {\r\n const model = cachedConfig.models.find(m =>\r\n m.name.toLowerCase() === modelNameOrId.toLowerCase() ||\r\n m.id.toLowerCase() === modelNameOrId.toLowerCase()\r\n );\r\n if (model) {\r\n quickLog(`[${new Date().toISOString()}] [getModelContextWindowSync] Found model \"${model.name}\" with contextWindow: ${model.contextWindow}\\n`);\r\n return model.contextWindow;\r\n }\r\n quickLog(`[${new Date().toISOString()}] [getModelContextWindowSync] Model NOT found in cache. Available: ${cachedConfig.models.map(m => m.name).join(', ')}\\n`);\r\n }\r\n\r\n // Fallback defaults based on known patterns\r\n const nameLower = modelNameOrId.toLowerCase();\r\n if (nameLower.includes('kimi')) return 128000;\r\n if (nameLower.includes('glm')) return 128000;\r\n if (nameLower.includes('gemini')) return 1000000;\r\n return 1000000; // Default 1M tokens\r\n}\r\n\r\n\r\n/**\r\n * Get all model configurations (async)\r\n * @returns Promise resolving to array of all model configurations\r\n */\r\nexport async function getAllModelConfigs(): Promise<ModelConfig[]> {\r\n const config = await fetchModelsConfig();\r\n return config.models;\r\n}\r\n\r\n/**\r\n * Get model configuration by ID (async)\r\n * @param modelId - Model ID\r\n * @returns Promise resolving to model configuration or undefined\r\n */\r\nexport async function getModelConfig(modelId: string): Promise<ModelConfig | undefined> {\r\n const config = await fetchModelsConfig();\r\n return config.models.find(m => m.id === modelId);\r\n}\r\n\r\n/**\r\n * Get model configuration by ID and name (for duplicate model IDs with different configs)\r\n * @param modelId - Model ID\r\n * @param modelName - Model display name\r\n * @returns Promise resolving to model configuration or undefined\r\n */\r\nexport async function getModelConfigByIdAndName(modelId: string, modelName: string): Promise<ModelConfig | undefined> {\r\n const config = await fetchModelsConfig();\r\n return config.models.find(m => m.id === modelId && m.name === modelName);\r\n}\r\n\r\n/**\r\n * Get default model ID (async)\r\n * @returns Promise resolving to default model ID\r\n */\r\nexport async function getDefaultModel(): Promise<string> {\r\n const config = await fetchModelsConfig();\r\n return config.defaultModel;\r\n}\r\n\r\n/**\r\n * Get list of supported model IDs (unique) (async)\r\n * Excludes internal models that are not meant to be user-selectable\r\n * @returns Promise resolving to array of unique model IDs\r\n */\r\nexport async function getSupportedModels(): Promise<string[]> {\r\n const config = await fetchModelsConfig();\r\n // Backend already filters allowFrontendDisplay: false, but we check here too for robustness\r\n const userFacingModels = config.models.filter(m => m.allowFrontendDisplay !== false);\r\n return Array.from(new Set(userFacingModels.map(m => m.id)));\r\n}\r\n\r\n/**\r\n * Validate if a model name is supported (async)\r\n * @param model - Model name to validate\r\n * @returns Promise resolving to true if model is supported\r\n */\r\nexport async function isValidModel(model: string): Promise<boolean> {\r\n const models = await getSupportedModels();\r\n return models.includes(model);\r\n}\r\n\r\n/**\r\n * Get a user-friendly error message for invalid models (async)\r\n * @param invalidModel - The invalid model name\r\n * @returns Promise resolving to error message with list of valid models\r\n */\r\nexport async function getInvalidModelError(invalidModel: string): Promise<string> {\r\n const config = await fetchModelsConfig();\r\n // Only show user-facing models\r\n const userFacingModels = config.models.filter(m => m.allowFrontendDisplay !== false);\r\n return `Invalid model: ${invalidModel}\\n\\n` +\r\n `Supported models:\\n` +\r\n userFacingModels.map(m => ` - ${m.id} (${m.name})`).join('\\n') +\r\n `\\n\\nUse /model to select from available models.`;\r\n}\r\n\r\n/**\r\n * Get model display name with description (async)\r\n * @param model - Model ID\r\n * @returns Promise resolving to display name with description\r\n */\r\nexport async function getModelDisplayName(model: string): Promise<string> {\r\n const config = await getModelConfig(model);\r\n if (config) {\r\n return `${config.name} - ${config.description}`;\r\n }\r\n return model;\r\n}\r\n\r\n/**\r\n * Get context window size for a model (async)\r\n * @param model - Model ID\r\n * @returns Promise resolving to context window size in tokens\r\n */\r\nexport async function getModelContextWindow(model: string): Promise<number> {\r\n const config = await getModelConfig(model);\r\n return config?.contextWindow || 2000000; // Default 2M tokens\r\n}\r\n\r\n/**\r\n * Check if model supports thinking (async)\r\n * @param model - Model ID\r\n * @returns Promise resolving to true if model supports thinking\r\n */\r\nexport async function modelSupportsThinking(model: string): Promise<boolean> {\r\n const config = await getModelConfig(model);\r\n return config?.supportsThinking || false;\r\n}\r\n\r\n\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/config/models.ts"],"sourcesContent":["/**\r\n * Supported AI models\r\n * Fetched from backend API with caching\r\n */\r\n\r\nimport { apiClient, ModelConfig, ModelsConfig } from '../services/api-client.js';\r\nimport { logWarning } from '../utils/logger.js';\r\nimport { quickLog } from '../utils/conversation-logger.js';\r\n\r\n// Re-export types\r\nexport type { ModelConfig, ModelsConfig };\r\n\r\n/**\r\n * Cached models configuration\r\n * Fetched once per session from backend\r\n */\r\nlet cachedConfig: ModelsConfig | null = null;\r\nlet fetchPromise: Promise<ModelsConfig> | null = null;\r\n\r\n/**\r\n * Default fallback configuration when backend is unavailable\r\n */\r\nconst FALLBACK_CONFIG: ModelsConfig = {\r\n models: [\r\n {\r\n uid: 'gemini-2-5-flash',\r\n id: 'gemini-2.5-flash',\r\n name: 'Gemini 2.5 Flash',\r\n description: 'Fast and efficient',\r\n provider: 'google',\r\n contextWindow: 1000000,\r\n region: 'us-central1',\r\n supportsThinking: true,\r\n thinkingConfig: { thinking_config: { include_thoughts: true } },\r\n generationConfig: {\r\n temperature: 0.1,\r\n topP: 0.95,\r\n maxOutputTokens: 16384\r\n }\r\n }\r\n ],\r\n defaultModel: 'gemini-2.5-flash'\r\n};\r\n\r\n/**\r\n * Fetch models configuration from backend (with caching)\r\n * This is the main entry point for getting model config\r\n */\r\nexport async function fetchModelsConfig(): Promise<ModelsConfig> {\r\n // Return cached config if available\r\n if (cachedConfig) {\r\n return cachedConfig;\r\n }\r\n\r\n // If a fetch is already in progress, wait for it\r\n if (fetchPromise) {\r\n return fetchPromise;\r\n }\r\n\r\n // Start fetching from backend\r\n fetchPromise = (async () => {\r\n try {\r\n const config = await apiClient.getModelsConfig();\r\n cachedConfig = config;\r\n return config;\r\n } catch (error) {\r\n // On error, use fallback config\r\n logWarning('Failed to fetch models config from backend, using fallback');\r\n cachedConfig = FALLBACK_CONFIG;\r\n return FALLBACK_CONFIG;\r\n } finally {\r\n fetchPromise = null;\r\n }\r\n })();\r\n\r\n return fetchPromise;\r\n}\r\n\r\n/**\r\n * Clear cached config (useful for refreshing)\r\n */\r\nexport function clearModelsCache(): void {\r\n cachedConfig = null;\r\n fetchPromise = null;\r\n}\r\n\r\n/**\r\n * Get context window size for a model SYNCHRONOUSLY from cache\r\n * This is useful for UI components that need immediate values\r\n * Returns fallback defaults if cache is not yet populated\r\n * @param modelNameOrId - Model name or ID (case-insensitive search)\r\n * @returns Context window size in tokens\r\n */\r\nexport function getModelContextWindowSync(modelNameOrId: string): number {\r\n // DEBUG: Log cache state to file\r\n quickLog(`[${new Date().toISOString()}] [getModelContextWindowSync] Looking up: \"${modelNameOrId}\", cache populated: ${!!cachedConfig}, models in cache: ${cachedConfig?.models?.length || 0}\\n`);\r\n\r\n // Try to find in cache first\r\n if (cachedConfig) {\r\n const model = cachedConfig.models.find(m =>\r\n m.name.toLowerCase() === modelNameOrId.toLowerCase() ||\r\n m.id.toLowerCase() === modelNameOrId.toLowerCase()\r\n );\r\n if (model) {\r\n quickLog(`[${new Date().toISOString()}] [getModelContextWindowSync] Found model \"${model.name}\" with contextWindow: ${model.contextWindow}\\n`);\r\n return model.contextWindow;\r\n }\r\n quickLog(`[${new Date().toISOString()}] [getModelContextWindowSync] Model NOT found in cache. Available: ${cachedConfig.models.map(m => m.name).join(', ')}\\n`);\r\n }\r\n\r\n // Fallback defaults based on known patterns\r\n const nameLower = modelNameOrId.toLowerCase();\r\n if (nameLower.includes('kimi')) return 128000;\r\n if (nameLower.includes('glm')) return 128000;\r\n if (nameLower.includes('minimax')) return 128000;\r\n if (nameLower.includes('gemini')) return 1000000;\r\n return 1000000; // Default 1M tokens\r\n}\r\n\r\n\r\n/**\r\n * Get all model configurations (async)\r\n * @returns Promise resolving to array of all model configurations\r\n */\r\nexport async function getAllModelConfigs(): Promise<ModelConfig[]> {\r\n const config = await fetchModelsConfig();\r\n return config.models;\r\n}\r\n\r\n/**\r\n * Get model configuration by ID (async)\r\n * @param modelId - Model ID\r\n * @returns Promise resolving to model configuration or undefined\r\n */\r\nexport async function getModelConfig(modelId: string): Promise<ModelConfig | undefined> {\r\n const config = await fetchModelsConfig();\r\n return config.models.find(m => m.id === modelId);\r\n}\r\n\r\n/**\r\n * Get model configuration by ID and name (for duplicate model IDs with different configs)\r\n * @param modelId - Model ID\r\n * @param modelName - Model display name\r\n * @returns Promise resolving to model configuration or undefined\r\n */\r\nexport async function getModelConfigByIdAndName(modelId: string, modelName: string): Promise<ModelConfig | undefined> {\r\n const config = await fetchModelsConfig();\r\n return config.models.find(m => m.id === modelId && m.name === modelName);\r\n}\r\n\r\n/**\r\n * Get default model ID (async)\r\n * @returns Promise resolving to default model ID\r\n */\r\nexport async function getDefaultModel(): Promise<string> {\r\n const config = await fetchModelsConfig();\r\n return config.defaultModel;\r\n}\r\n\r\n/**\r\n * Get list of supported model IDs (unique) (async)\r\n * Excludes internal models that are not meant to be user-selectable\r\n * @returns Promise resolving to array of unique model IDs\r\n */\r\nexport async function getSupportedModels(): Promise<string[]> {\r\n const config = await fetchModelsConfig();\r\n // Backend already filters allowFrontendDisplay: false, but we check here too for robustness\r\n const userFacingModels = config.models.filter(m => m.allowFrontendDisplay !== false);\r\n return Array.from(new Set(userFacingModels.map(m => m.id)));\r\n}\r\n\r\n/**\r\n * Validate if a model name is supported (async)\r\n * @param model - Model name to validate\r\n * @returns Promise resolving to true if model is supported\r\n */\r\nexport async function isValidModel(model: string): Promise<boolean> {\r\n const models = await getSupportedModels();\r\n return models.includes(model);\r\n}\r\n\r\n/**\r\n * Get a user-friendly error message for invalid models (async)\r\n * @param invalidModel - The invalid model name\r\n * @returns Promise resolving to error message with list of valid models\r\n */\r\nexport async function getInvalidModelError(invalidModel: string): Promise<string> {\r\n const config = await fetchModelsConfig();\r\n // Only show user-facing models\r\n const userFacingModels = config.models.filter(m => m.allowFrontendDisplay !== false);\r\n return `Invalid model: ${invalidModel}\\n\\n` +\r\n `Supported models:\\n` +\r\n userFacingModels.map(m => ` - ${m.id} (${m.name})`).join('\\n') +\r\n `\\n\\nUse /model to select from available models.`;\r\n}\r\n\r\n/**\r\n * Get model display name with description (async)\r\n * @param model - Model ID\r\n * @returns Promise resolving to display name with description\r\n */\r\nexport async function getModelDisplayName(model: string): Promise<string> {\r\n const config = await getModelConfig(model);\r\n if (config) {\r\n return `${config.name} - ${config.description}`;\r\n }\r\n return model;\r\n}\r\n\r\n/**\r\n * Get context window size for a model (async)\r\n * @param model - Model ID\r\n * @returns Promise resolving to context window size in tokens\r\n */\r\nexport async function getModelContextWindow(model: string): Promise<number> {\r\n const config = await getModelConfig(model);\r\n return config?.contextWindow || 2000000; // Default 2M tokens\r\n}\r\n\r\n/**\r\n * Check if model supports thinking (async)\r\n * @param model - Model ID\r\n * @returns Promise resolving to true if model supports thinking\r\n */\r\nexport async function modelSupportsThinking(model: string): Promise<boolean> {\r\n const config = await getModelConfig(model);\r\n return config?.supportsThinking || false;\r\n}\r\n\r\n\r\n"],"mappings":"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","names":[]}
|
package/dist/config/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/config/types.ts"],"sourcesContent":["export interface SubshellConfig {\r\n enabled?: boolean;\r\n ssh?: {\r\n enabled?: boolean;\r\n defaultAuthMethod?: 'password' | 'key';\r\n keyPath?: string;\r\n timeout?: number;\r\n };\r\n wsl?: {\r\n enabled?: boolean;\r\n defaultDistribution?: string;\r\n timeout?: number;\r\n };\r\n docker?: {\r\n enabled?: boolean;\r\n timeout?: number;\r\n };\r\n commandTimeout?: number;\r\n stateQueryTimeout?: number;\r\n reconnectAttempts?: number;\r\n reconnectBackoff?: number;\r\n}\r\n\r\nexport interface Config {\r\n model: string;\r\n modelName?: string;
|
|
1
|
+
{"version":3,"sources":["../../src/config/types.ts"],"sourcesContent":["export interface SubshellConfig {\r\n enabled?: boolean;\r\n ssh?: {\r\n enabled?: boolean;\r\n defaultAuthMethod?: 'password' | 'key';\r\n keyPath?: string;\r\n timeout?: number;\r\n };\r\n wsl?: {\r\n enabled?: boolean;\r\n defaultDistribution?: string;\r\n timeout?: number;\r\n };\r\n docker?: {\r\n enabled?: boolean;\r\n timeout?: number;\r\n };\r\n commandTimeout?: number;\r\n stateQueryTimeout?: number;\r\n reconnectAttempts?: number;\r\n reconnectBackoff?: number;\r\n}\r\n\r\nexport interface Config {\r\n model: string;\r\n modelUid?: string; // Unique uid for the selected model entry (e.g. \"claude-opus-4-6-thinking\")\r\n modelName?: string; // Display name of the selected model\r\n isLocalModel?: boolean; // True if using a local Ollama model, false for cloud models\r\n autoApprove?: boolean;\r\n subshell?: SubshellConfig;\r\n enhancedQuality?: boolean;\r\n externalThinking?: boolean; // Enable external thinking mode (show <thinking> tags)\r\n autonomousMode?: boolean; // Enable autonomous mode (Silent Operator with task_complete)\r\n aiAutoSuggest?: boolean; // Enable AI-powered command auto-suggestions\r\n}\r\n\r\nexport const DEFAULT_CONFIG: Partial<Config> = {\r\n model: 'gemini-2.5-flash',\r\n autoApprove: false,\r\n enhancedQuality: true, // Enable enhanced quality features by default\r\n externalThinking: false, // Disable external thinking by default (internal reasoning)\r\n autonomousMode: false, // Disable autonomous mode by default (for backward compatibility)\r\n aiAutoSuggest: false, // Disable AI auto-suggest by default\r\n subshell: {\r\n enabled: true,\r\n ssh: {\r\n enabled: true,\r\n defaultAuthMethod: 'key',\r\n timeout: 30000,\r\n },\r\n wsl: {\r\n enabled: true,\r\n timeout: 30000,\r\n },\r\n docker: {\r\n enabled: true,\r\n timeout: 30000,\r\n },\r\n commandTimeout: 30000,\r\n stateQueryTimeout: 10000,\r\n reconnectAttempts: 3,\r\n reconnectBackoff: 1000,\r\n },\r\n};\r\n"],"mappings":"AAoCO,MAAM,iBAAkC;AAAA,EAC7C,OAAO;AAAA,EACP,aAAa;AAAA,EACb,iBAAiB;AAAA;AAAA,EACjB,kBAAkB;AAAA;AAAA,EAClB,gBAAgB;AAAA;AAAA,EAChB,eAAe;AAAA;AAAA,EACf,UAAU;AAAA,IACR,SAAS;AAAA,IACT,KAAK;AAAA,MACH,SAAS;AAAA,MACT,mBAAmB;AAAA,MACnB,SAAS;AAAA,IACX;AAAA,IACA,KAAK;AAAA,MACH,SAAS;AAAA,MACT,SAAS;AAAA,IACX;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,SAAS;AAAA,IACX;AAAA,IACA,gBAAgB;AAAA,IAChB,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,kBAAkB;AAAA,EACpB;AACF;","names":[]}
|
|
@@ -37,7 +37,7 @@ class AIServiceClient {
|
|
|
37
37
|
* @param mode - Optional mode (default, plan, command)
|
|
38
38
|
* @yields Stream chunks (text, tool calls, done, or error events)
|
|
39
39
|
*/
|
|
40
|
-
async *streamChat(model, messages, tools, environmentContext, mode, thinkingConfig, abortSignal) {
|
|
40
|
+
async *streamChat(model, messages, tools, environmentContext, mode, thinkingConfig, abortSignal, modelName, modelUid) {
|
|
41
41
|
if (this.isUsingLocalModel()) {
|
|
42
42
|
const localModel = this.getCurrentModel();
|
|
43
43
|
const supportsTools = OllamaService.modelSupportsTools(localModel);
|
|
@@ -56,11 +56,12 @@ class AIServiceClient {
|
|
|
56
56
|
}
|
|
57
57
|
const payload = {
|
|
58
58
|
model,
|
|
59
|
+
modelUid,
|
|
60
|
+
modelName,
|
|
59
61
|
messages,
|
|
60
62
|
tools,
|
|
61
63
|
stream: true,
|
|
62
64
|
clientType: "cli",
|
|
63
|
-
// Tell backend this is a CLI request
|
|
64
65
|
environmentContext,
|
|
65
66
|
mode,
|
|
66
67
|
thinkingConfig
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/ai-service-client.ts"],"sourcesContent":["/**\r\n * AI Service Client\r\n * \r\n * Handles communication with the backend AI proxy service for streaming\r\n * AI chat requests. Replaces direct Gemini SDK usage in the CLI.\r\n */\r\n\r\nimport { apiClient } from './api-client.js';\r\nimport type { ToolSchema } from '../tools/types.js';\r\nimport type { EnvironmentContext } from '../types/index.js';\r\nimport { readFileSync, existsSync } from 'fs';\r\nimport { join } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logWarning } from '../utils/logger.js';\r\nimport { ollamaService, OllamaChatMessage, OllamaTool, OllamaService, OllamaToolCall } from './ollama-service.js';\r\nimport { quickLog } from '../utils/conversation-logger.js';\r\n\r\n/**\r\n * Message format for AI chat requests\r\n */\r\nexport interface Message {\r\n role: 'system' | 'user' | 'assistant' | 'tool';\r\n content: string;\r\n tool_call_id?: string; // For tool messages - references the tool call being responded to\r\n tool_calls?: ToolCall[]; // For assistant messages - tool calls made by the assistant\r\n thinking?: string; // For assistant messages - thinking/reasoning content from the AI (kept only for most recent turn)\r\n thinkingSignature?: string; // For assistant messages - signature for Claude thinking blocks (must be passed back)\r\n}\r\n\r\n/**\r\n * Tool call from AI model\r\n */\r\nexport interface ToolCall {\r\n id: string;\r\n name: string;\r\n arguments: Record<string, any>;\r\n thoughtSignature?: string; // For Gemini thinking models - must be passed back\r\n}\r\n\r\n/**\r\n * Stream chunk types from backend\r\n */\r\nexport interface TextChunk {\r\n type: 'text';\r\n content: string;\r\n}\r\n\r\nexport interface ToolCallChunk {\r\n type: 'tool_call';\r\n toolCall: ToolCall;\r\n}\r\n\r\nexport interface DoneChunk {\r\n type: 'done';\r\n}\r\n\r\nexport interface ErrorChunk {\r\n type: 'error';\r\n message: string;\r\n code: string;\r\n}\r\n\r\nexport interface ThoughtChunk {\r\n type: 'thought';\r\n content: string;\r\n}\r\n\r\nexport interface ThinkingSignatureChunk {\r\n type: 'thinking_signature';\r\n signature: string;\r\n}\r\n\r\nexport interface FileDescriptionsChunk {\r\n type: 'file_descriptions';\r\n descriptions: Record<string, { description: string; filename: string; mimeType: string }>;\r\n}\r\n\r\nexport type StreamChunk = TextChunk | ToolCallChunk | DoneChunk | ErrorChunk | ThoughtChunk | ThinkingSignatureChunk | FileDescriptionsChunk;\r\n\r\n/**\r\n * Chat request payload\r\n */\r\ninterface ChatRequest {\r\n model: string;\r\n messages: Message[];\r\n tools: ToolSchema[];\r\n stream: boolean;\r\n clientType?: string; // Client type (cli, desktop, etc.)\r\n environmentContext?: EnvironmentContext;\r\n mode?: string;\r\n thinkingConfig?: Record<string, any>;\r\n}\r\n\r\n/**\r\n * AI Service Client for streaming chat requests to backend\r\n */\r\nexport class AIServiceClient {\r\n private baseURL: string;\r\n private maxRetries: number = 5;\r\n private retryDelay: number = 1000; // Start with 1 second\r\n private maxRetryDelay: number = 30000;\r\n private maxRetryAfterDelay: number = 60000;\r\n private highDemandMessage: string = 'Very high demand right now. Please wait and retry after some time.';\r\n\r\n constructor() {\r\n // Don't set baseURL yet - lazy load it when first used\r\n // This allows environment variables to be loaded first\r\n this.baseURL = '';\r\n }\r\n\r\n /**\r\n * Get the base URL for API requests\r\n * Lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getBaseURL(): string {\r\n if (!this.baseURL) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n this.baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n }\r\n return this.baseURL;\r\n }\r\n\r\n /**\r\n * Stream chat request to backend AI proxy\r\n * \r\n * @param model - The AI model to use (e.g., 'gemini-2.5-flash')\r\n * @param messages - Conversation history including system, user, assistant, and tool messages\r\n * @param tools - Available tool schemas for the AI to use\r\n * @param environmentContext - Optional environment context (OS, shell, cwd, etc.)\r\n * @param mode - Optional mode (default, plan, command)\r\n * @yields Stream chunks (text, tool calls, done, or error events)\r\n */\r\n async *streamChat(\r\n model: string,\r\n messages: Message[],\r\n tools: ToolSchema[],\r\n environmentContext?: EnvironmentContext,\r\n mode?: string,\r\n thinkingConfig?: Record<string, any>,\r\n abortSignal?: AbortSignal\r\n ): AsyncGenerator<StreamChunk, void, unknown> {\r\n // Check if using a local Ollama model\r\n if (this.isUsingLocalModel()) {\r\n const localModel = this.getCurrentModel();\r\n const supportsTools = OllamaService.modelSupportsTools(localModel);\r\n\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Routing to local Ollama model: ${localModel}, supportsTools: ${supportsTools}\\n`);\r\n\r\n // Show warning only if tools are provided but model doesn't support them\r\n if (tools.length > 0 && !supportsTools) {\r\n yield {\r\n type: 'text',\r\n content: `⚠️ Note: Model \"${localModel}\" does not support tool calling. Running in text-only mode.\\n\\n`,\r\n };\r\n }\r\n\r\n // Route to local Ollama with tools\r\n yield* this.streamLocalChat(localModel, messages, tools);\r\n return;\r\n }\r\n\r\n // Build request payload for cloud backend\r\n const payload: ChatRequest = {\r\n model,\r\n messages,\r\n tools,\r\n stream: true,\r\n clientType: 'cli', // Tell backend this is a CLI request\r\n environmentContext,\r\n mode,\r\n thinkingConfig,\r\n };\r\n\r\n // Get authentication token from api client\r\n if (!apiClient.isAuthenticated()) {\r\n yield {\r\n type: 'error',\r\n message: 'Authentication required. Please sign in.',\r\n code: 'AUTH_REQUIRED',\r\n };\r\n return;\r\n }\r\n\r\n // Retry logic for transient errors\r\n let lastError: ErrorChunk | null = null;\r\n\r\n for (let attempt = 0; attempt < this.maxRetries; attempt++) {\r\n try {\r\n // Make fetch request to backend AI endpoint\r\n // NOTE: We intentionally do NOT set a timeout here because:\r\n // 1. Tool execution happens during streaming (in-stream execution)\r\n // 2. Interactive commands may require user input for arbitrary duration\r\n // 3. The only timeout should be explicit user cancellation via abortSignal\r\n // Connection issues will manifest as network errors, not timeouts\r\n\r\n const response = await fetch(`${this.getBaseURL()}/chat/completions`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${this.getSessionToken()}`,\r\n },\r\n body: JSON.stringify(payload),\r\n signal: abortSignal, // Only abort when user explicitly cancels\r\n });\r\n\r\n\r\n\r\n // Check for HTTP errors\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n const parsedHttpError = this.parseHttpError(response.status, response.statusText, errorText);\r\n\r\n // Handle specific error codes\r\n if (response.status === 401) {\r\n yield {\r\n type: 'error',\r\n message: 'Session expired. Please sign in again.',\r\n code: 'AUTH_REQUIRED',\r\n };\r\n return;\r\n }\r\n\r\n lastError = parsedHttpError;\r\n\r\n if (this.isRetryableError(parsedHttpError.code, parsedHttpError.message, response.status) && attempt < this.maxRetries - 1) {\r\n const retryAfterMs = this.getRetryAfterMs(response.headers);\r\n await this.sleep(this.getRetryDelayMs(attempt, retryAfterMs));\r\n continue;\r\n }\r\n\r\n if (this.isRateLimitError(parsedHttpError.code, parsedHttpError.message, response.status)) {\r\n yield this.createHighDemandError();\r\n return;\r\n }\r\n\r\n yield parsedHttpError;\r\n return;\r\n }\r\n\r\n // Parse SSE stream\r\n // We iterate manually to catch retryable errors from the stream\r\n let contentReceived = false;\r\n let shouldRetry = false;\r\n\r\n for await (const chunk of this.parseSSEStream(response.body)) {\r\n if (chunk.type === 'error') {\r\n const normalizedChunk = this.normalizeErrorChunk(chunk);\r\n\r\n // Check if this is a retryable error (like rate limit) AND we haven't sent partial content yet\r\n if (this.isRetryableError(normalizedChunk.code, normalizedChunk.message) && !contentReceived) {\r\n lastError = normalizedChunk;\r\n shouldRetry = true;\r\n break; // Break the stream loop to trigger retry\r\n }\r\n\r\n // Not retryable or content already sent - yield error and stop\r\n yield normalizedChunk;\r\n return;\r\n }\r\n\r\n // Mark that we've received content\r\n if (chunk.type !== 'done' && chunk.type !== 'thought' && chunk.type !== 'thinking_signature' && chunk.type !== 'file_descriptions') {\r\n contentReceived = true;\r\n }\r\n\r\n yield chunk;\r\n }\r\n\r\n // If stream finished cleanly (without error chunk), we are done\r\n if (!shouldRetry) {\r\n return;\r\n }\r\n\r\n // If we need to retry, check attempts and wait\r\n if (attempt < this.maxRetries - 1) {\r\n // We're going to retry, so we need to wait for the backoff period\r\n await this.sleep(this.getRetryDelayMs(attempt));\r\n continue;\r\n }\r\n\r\n // If we've exhausted retries, we'll fall through to the end of the loop \r\n // and yield the lastError below\r\n\r\n } catch (error: any) {\r\n lastError = this.normalizeThrownError(error);\r\n const wasIntentionalAbort = (error?.name === 'AbortError' || error?.name === 'TimeoutError') && !!abortSignal?.aborted;\r\n\r\n if (wasIntentionalAbort) {\r\n break;\r\n }\r\n\r\n // Retry for transient errors\r\n if (this.isRetryableError(lastError.code, lastError.message) && attempt < this.maxRetries - 1) {\r\n await this.sleep(this.getRetryDelayMs(attempt));\r\n continue;\r\n }\r\n\r\n // If not retryable or max retries reached, yield error and return\r\n break;\r\n }\r\n }\r\n\r\n // If we get here, we've exhausted retries\r\n if (lastError) {\r\n if (this.isRateLimitError(lastError.code, lastError.message)) {\r\n yield this.createHighDemandError();\r\n return;\r\n }\r\n yield lastError;\r\n }\r\n }\r\n\r\n /**\r\n * Create a normalized high-demand error response.\r\n */\r\n private createHighDemandError(): ErrorChunk {\r\n return {\r\n type: 'error',\r\n message: this.highDemandMessage,\r\n code: 'RATE_LIMIT',\r\n };\r\n }\r\n\r\n /**\r\n * Parse backend HTTP error body into a normalized error chunk.\r\n */\r\n private parseHttpError(status: number, statusText: string, errorText: string): ErrorChunk {\r\n let errorMessage = `HTTP ${status}: ${statusText}`;\r\n let errorCode = 'HTTP_ERROR';\r\n\r\n if (errorText) {\r\n try {\r\n const parsed = JSON.parse(errorText);\r\n const rootError = Array.isArray(parsed) ? parsed[0]?.error ?? parsed[0] : parsed?.error ?? parsed;\r\n\r\n if (rootError) {\r\n if (typeof rootError.message === 'string' && rootError.message.trim()) {\r\n errorMessage = rootError.message;\r\n }\r\n if (typeof rootError.code === 'string' || typeof rootError.code === 'number') {\r\n errorCode = String(rootError.code);\r\n } else if (typeof rootError.status === 'string') {\r\n errorCode = rootError.status;\r\n }\r\n }\r\n } catch {\r\n errorMessage = errorText;\r\n }\r\n }\r\n\r\n if (status === 504) {\r\n return {\r\n type: 'error',\r\n message: 'Request timed out. Please try again.',\r\n code: 'TIMEOUT',\r\n };\r\n }\r\n\r\n if (this.isRateLimitError(errorCode, errorMessage, status)) {\r\n return this.createHighDemandError();\r\n }\r\n\r\n return {\r\n type: 'error',\r\n message: errorMessage,\r\n code: errorCode,\r\n };\r\n }\r\n\r\n /**\r\n * Normalize streamed error chunks to stable internal codes/messages.\r\n */\r\n private normalizeErrorChunk(chunk: ErrorChunk): ErrorChunk {\r\n const normalizedCode = chunk.code ? String(chunk.code) : 'UNKNOWN_ERROR';\r\n const normalizedMessage = chunk.message || 'Unknown error occurred';\r\n\r\n if (this.isRateLimitError(normalizedCode, normalizedMessage)) {\r\n return this.createHighDemandError();\r\n }\r\n\r\n if (normalizedCode === '504') {\r\n return {\r\n type: 'error',\r\n message: 'Request timed out. Please try again.',\r\n code: 'TIMEOUT',\r\n };\r\n }\r\n\r\n return {\r\n type: 'error',\r\n message: normalizedMessage,\r\n code: normalizedCode,\r\n };\r\n }\r\n\r\n /**\r\n * Normalize thrown errors from fetch/network/runtime paths.\r\n */\r\n private normalizeThrownError(error: any): ErrorChunk {\r\n const rawMessage = error?.message || 'Unknown error occurred';\r\n const rawCode = error?.code ? String(error.code) : 'UNKNOWN_ERROR';\r\n\r\n if (this.isRateLimitError(rawCode, rawMessage, error?.status)) {\r\n return this.createHighDemandError();\r\n }\r\n\r\n if (error?.name === 'TypeError' && rawMessage.includes('fetch')) {\r\n return {\r\n type: 'error',\r\n message: 'Backend service is unreachable. Please check your connection.',\r\n code: 'NETWORK_ERROR',\r\n };\r\n }\r\n\r\n if (error?.name === 'AbortError' || error?.name === 'TimeoutError') {\r\n return {\r\n type: 'error',\r\n message: 'Request timed out. Please try again.',\r\n code: 'TIMEOUT',\r\n };\r\n }\r\n\r\n return {\r\n type: 'error',\r\n message: rawMessage,\r\n code: rawCode,\r\n };\r\n }\r\n\r\n /**\r\n * Check if an error is a rate-limit/resource-exhausted signal.\r\n */\r\n private isRateLimitError(code?: string, message: string = '', status?: number): boolean {\r\n const normalizedCode = (code || '').toString().toLowerCase();\r\n const normalizedMessage = message.toLowerCase();\r\n\r\n return status === 429 ||\r\n normalizedCode === '429' ||\r\n normalizedCode === '8' ||\r\n normalizedCode === 'rate_limit' ||\r\n normalizedCode === 'resource_exhausted' ||\r\n normalizedMessage.includes('rate limit') ||\r\n normalizedMessage.includes('too many requests') ||\r\n normalizedMessage.includes('quota') ||\r\n normalizedMessage.includes('resource exhausted') ||\r\n normalizedMessage.includes('resource_exhausted') ||\r\n normalizedMessage.includes('maas api returned 429');\r\n }\r\n\r\n /**\r\n * Check if HTTP status is retryable.\r\n */\r\n private isRetryableStatus(status?: number): boolean {\r\n if (typeof status !== 'number') {\r\n return false;\r\n }\r\n return [408, 425, 429, 500, 502, 503, 504].includes(status);\r\n }\r\n\r\n /**\r\n * Check if an error should be retried.\r\n */\r\n private isRetryableError(code: string, message: string = '', status?: number): boolean {\r\n if (this.isRateLimitError(code, message, status)) {\r\n return true;\r\n }\r\n\r\n if (this.isRetryableStatus(status)) {\r\n return true;\r\n }\r\n\r\n const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'UNKNOWN_ERROR', 'RATE_LIMIT'];\r\n if (retryableCodes.includes(code)) {\r\n return true;\r\n }\r\n\r\n const normalizedMessage = message.toLowerCase();\r\n return normalizedMessage.includes('timeout') ||\r\n normalizedMessage.includes('timed out') ||\r\n normalizedMessage.includes('temporarily unavailable') ||\r\n normalizedMessage.includes('network error');\r\n }\r\n\r\n /**\r\n * Parse Retry-After from response headers.\r\n */\r\n private getRetryAfterMs(headers: Headers): number | undefined {\r\n const retryAfterValue = headers.get('retry-after');\r\n if (!retryAfterValue) {\r\n return undefined;\r\n }\r\n\r\n const seconds = Number(retryAfterValue);\r\n if (Number.isFinite(seconds) && seconds >= 0) {\r\n return seconds * 1000;\r\n }\r\n\r\n const retryAfterDate = Date.parse(retryAfterValue);\r\n if (!Number.isNaN(retryAfterDate)) {\r\n return Math.max(0, retryAfterDate - Date.now());\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n /**\r\n * Compute retry delay with exponential backoff + jitter.\r\n */\r\n private getRetryDelayMs(attempt: number, retryAfterMs?: number): number {\r\n const exponentialDelay = this.retryDelay * Math.pow(2, attempt);\r\n const cappedDelay = Math.min(exponentialDelay, this.maxRetryDelay);\r\n const jitteredDelay = Math.round(Math.min(cappedDelay * (0.5 + Math.random()), this.maxRetryDelay));\r\n\r\n if (retryAfterMs === undefined) {\r\n return jitteredDelay;\r\n }\r\n\r\n const cappedRetryAfter = Math.min(Math.max(0, retryAfterMs), this.maxRetryAfterDelay);\r\n return Math.max(jitteredDelay, cappedRetryAfter);\r\n }\r\n\r\n /**\r\n * Sleep for specified milliseconds\r\n */\r\n private sleep(ms: number): Promise<void> {\r\n return new Promise(resolve => setTimeout(resolve, ms));\r\n }\r\n\r\n /**\r\n * Get session token from apiClient\r\n * This is a workaround since sessionToken is private\r\n */\r\n private getSessionToken(): string {\r\n // Read session token from the same location apiClient uses\r\n const configPath = join(homedir(), '.centaurus', 'session.json');\r\n\r\n try {\r\n if (existsSync(configPath)) {\r\n const data = readFileSync(configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n return session.sessionToken || '';\r\n }\r\n } catch (error) {\r\n // Return empty string if unable to read\r\n }\r\n\r\n return '';\r\n }\r\n\r\n /**\r\n * Check if the current configuration is using a local Ollama model\r\n */\r\n isUsingLocalModel(): boolean {\r\n const configPath = join(homedir(), '.centaurus', 'config.json');\r\n try {\r\n if (existsSync(configPath)) {\r\n const data = readFileSync(configPath, 'utf-8');\r\n const config = JSON.parse(data);\r\n return config.isLocalModel === true;\r\n }\r\n } catch (error) {\r\n // Return false if unable to read\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * Get the current model name from config\r\n */\r\n private getCurrentModel(): string {\r\n const configPath = join(homedir(), '.centaurus', 'config.json');\r\n try {\r\n if (existsSync(configPath)) {\r\n const data = readFileSync(configPath, 'utf-8');\r\n const config = JSON.parse(data);\r\n return config.model || 'gemini-2.5-flash';\r\n }\r\n } catch (error) {\r\n // Return default if unable to read\r\n }\r\n return 'gemini-2.5-flash';\r\n }\r\n\r\n /**\r\n * Convert internal message format to Ollama format (with tool support)\r\n */\r\n private convertToOllamaMessages(messages: Message[]): OllamaChatMessage[] {\r\n return messages.map(msg => {\r\n if (msg.role === 'tool') {\r\n // Tool results - convert to Ollama's tool message format\r\n return {\r\n role: 'tool' as const,\r\n content: msg.content,\r\n tool_name: msg.tool_call_id || 'unknown', // Use tool_call_id as tool_name\r\n };\r\n }\r\n\r\n if (msg.role === 'assistant' && msg.tool_calls?.length) {\r\n // Assistant message with tool calls\r\n return {\r\n role: 'assistant' as const,\r\n content: msg.content || '',\r\n tool_calls: msg.tool_calls.map(tc => ({\r\n function: {\r\n name: tc.name,\r\n arguments: tc.arguments,\r\n }\r\n })),\r\n };\r\n }\r\n\r\n // Regular message\r\n return {\r\n role: msg.role as 'system' | 'user' | 'assistant',\r\n content: msg.content,\r\n };\r\n });\r\n }\r\n\r\n /**\r\n * Convert ToolSchema array to Ollama tool format\r\n */\r\n private convertToolsToOllamaFormat(tools: ToolSchema[]): OllamaTool[] {\r\n return tools.map(tool => ({\r\n type: 'function' as const,\r\n function: {\r\n name: tool.name,\r\n description: tool.description,\r\n parameters: {\r\n type: 'object' as const,\r\n properties: tool.parameters.properties,\r\n required: tool.parameters.required,\r\n },\r\n },\r\n }));\r\n }\r\n\r\n /**\r\n * Stream chat request to local Ollama instance with tool calling support\r\n * \r\n * @param model - The local Ollama model to use (e.g., 'llama3.2:latest')\r\n * @param messages - Conversation history\r\n * @param tools - Available tools (will be converted to Ollama format)\r\n * @yields Stream chunks (text, tool_call, or done events)\r\n */\r\n async *streamLocalChat(\r\n model: string,\r\n messages: Message[],\r\n tools: ToolSchema[] = []\r\n ): AsyncGenerator<StreamChunk, void, unknown> {\r\n try {\r\n const supportsTools = OllamaService.modelSupportsTools(model);\r\n const effectiveTools = supportsTools ? tools : [];\r\n\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Starting local chat with model: ${model}, tools: ${effectiveTools.length}, supportsTools: ${supportsTools}\\n`);\r\n\r\n // Convert messages to Ollama format\r\n const ollamaMessages = this.convertToOllamaMessages(messages);\r\n\r\n // Convert tools to Ollama format if model supports them\r\n const ollamaTools = effectiveTools.length > 0\r\n ? this.convertToolsToOllamaFormat(effectiveTools)\r\n : undefined;\r\n\r\n // Send request to Ollama with tools\r\n const response = await ollamaService.sendChatMessage(model, ollamaMessages, ollamaTools);\r\n\r\n // Check for tool calls in response\r\n if (response.message?.tool_calls?.length) {\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Received ${response.message.tool_calls.length} tool calls from Ollama\\n`);\r\n\r\n // Yield each tool call\r\n for (const toolCall of response.message.tool_calls) {\r\n yield {\r\n type: 'tool_call',\r\n toolCall: {\r\n id: `ollama-${Date.now()}-${toolCall.function.name}`, // Generate unique ID\r\n name: toolCall.function.name,\r\n arguments: toolCall.function.arguments,\r\n },\r\n };\r\n }\r\n\r\n // Signal completion (tool execution will happen in cli-adapter)\r\n yield { type: 'done' };\r\n return;\r\n }\r\n\r\n // No tool calls - yield text response\r\n if (response.message.content) {\r\n yield {\r\n type: 'text',\r\n content: response.message.content,\r\n };\r\n }\r\n\r\n // Signal completion\r\n yield { type: 'done' };\r\n\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Local chat completed\\n`);\r\n } catch (error: any) {\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Local chat error: ${error.message}\\n`);\r\n yield {\r\n type: 'error',\r\n message: error.message || 'Failed to communicate with Ollama',\r\n code: 'OLLAMA_ERROR',\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Parse Server-Sent Events stream from response body\r\n * \r\n * @param body - ReadableStream from fetch response\r\n * @yields Parsed stream chunks\r\n */\r\n private async *parseSSEStream(\r\n body: ReadableStream<Uint8Array>\r\n ): AsyncGenerator<StreamChunk, void, unknown> {\r\n const reader = body.getReader();\r\n const decoder = new TextDecoder();\r\n let buffer = '';\r\n\r\n try {\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n\r\n if (done) {\r\n // Process any remaining data in buffer before exiting\r\n if (buffer.trim()) {\r\n // Split remaining buffer and process\r\n const remainingLines = buffer.split('\\n');\r\n for (const line of remainingLines) {\r\n if (line.startsWith('data: ')) {\r\n const dataStr = line.slice(6);\r\n if (dataStr.trim()) {\r\n try {\r\n const chunk = JSON.parse(dataStr) as StreamChunk;\r\n yield chunk;\r\n } catch (error) {\r\n // Skip malformed JSON\r\n logWarning(`Failed to parse SSE data (in buffer): ${dataStr}`);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n break;\r\n }\r\n\r\n // Decode chunk and add to buffer\r\n buffer += decoder.decode(value, { stream: true });\r\n\r\n // Process complete lines in buffer\r\n const lines = buffer.split('\\n');\r\n\r\n // Keep the last incomplete line in buffer\r\n buffer = lines.pop() || '';\r\n\r\n for (const line of lines) {\r\n // SSE format: \"data: {json}\"\r\n if (line.startsWith('data: ')) {\r\n const dataStr = line.slice(6); // Remove \"data: \" prefix\r\n\r\n // Skip empty data lines\r\n if (!dataStr.trim()) {\r\n continue;\r\n }\r\n\r\n try {\r\n const chunk = JSON.parse(dataStr) as StreamChunk;\r\n yield chunk;\r\n\r\n // Stop if we receive a done or error event\r\n if (chunk.type === 'done' || chunk.type === 'error') {\r\n return;\r\n }\r\n } catch (error) {\r\n // Skip malformed JSON\r\n logWarning(`Failed to parse SSE data: ${dataStr}`);\r\n }\r\n }\r\n // SSE event type line: \"event: chunk\"\r\n // We don't need to process these separately since the data contains the type\r\n }\r\n }\r\n } finally {\r\n reader.releaseLock();\r\n }\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const aiServiceClient = new AIServiceClient();\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/services/ai-service-client.ts"],"sourcesContent":["/**\r\n * AI Service Client\r\n * \r\n * Handles communication with the backend AI proxy service for streaming\r\n * AI chat requests. Replaces direct Gemini SDK usage in the CLI.\r\n */\r\n\r\nimport { apiClient } from './api-client.js';\r\nimport type { ToolSchema } from '../tools/types.js';\r\nimport type { EnvironmentContext } from '../types/index.js';\r\nimport { readFileSync, existsSync } from 'fs';\r\nimport { join } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logWarning } from '../utils/logger.js';\r\nimport { ollamaService, OllamaChatMessage, OllamaTool, OllamaService, OllamaToolCall } from './ollama-service.js';\r\nimport { quickLog } from '../utils/conversation-logger.js';\r\n\r\n/**\r\n * Message format for AI chat requests\r\n */\r\nexport interface Message {\r\n role: 'system' | 'user' | 'assistant' | 'tool';\r\n content: string;\r\n tool_call_id?: string; // For tool messages - references the tool call being responded to\r\n tool_calls?: ToolCall[]; // For assistant messages - tool calls made by the assistant\r\n thinking?: string; // For assistant messages - thinking/reasoning content from the AI (kept only for most recent turn)\r\n thinkingSignature?: string; // For assistant messages - signature for Claude thinking blocks (must be passed back)\r\n}\r\n\r\n/**\r\n * Tool call from AI model\r\n */\r\nexport interface ToolCall {\r\n id: string;\r\n name: string;\r\n arguments: Record<string, any>;\r\n thoughtSignature?: string; // For Gemini thinking models - must be passed back\r\n}\r\n\r\n/**\r\n * Stream chunk types from backend\r\n */\r\nexport interface TextChunk {\r\n type: 'text';\r\n content: string;\r\n}\r\n\r\nexport interface ToolCallChunk {\r\n type: 'tool_call';\r\n toolCall: ToolCall;\r\n}\r\n\r\nexport interface DoneChunk {\r\n type: 'done';\r\n}\r\n\r\nexport interface ErrorChunk {\r\n type: 'error';\r\n message: string;\r\n code: string;\r\n}\r\n\r\nexport interface ThoughtChunk {\r\n type: 'thought';\r\n content: string;\r\n}\r\n\r\nexport interface ThinkingSignatureChunk {\r\n type: 'thinking_signature';\r\n signature: string;\r\n}\r\n\r\nexport interface FileDescriptionsChunk {\r\n type: 'file_descriptions';\r\n descriptions: Record<string, { description: string; filename: string; mimeType: string }>;\r\n}\r\n\r\nexport type StreamChunk = TextChunk | ToolCallChunk | DoneChunk | ErrorChunk | ThoughtChunk | ThinkingSignatureChunk | FileDescriptionsChunk;\r\n\r\n/**\r\n * Chat request payload\r\n */\r\ninterface ChatRequest {\r\n model: string;\r\n modelUid?: string; // Unique uid per model entry — used by backend to find exact display name\r\n modelName?: string; // Display name (legacy fallback)\r\n messages: Message[];\r\n tools: ToolSchema[];\r\n stream: boolean;\r\n clientType?: string;\r\n environmentContext?: EnvironmentContext;\r\n mode?: string;\r\n thinkingConfig?: Record<string, any>;\r\n}\r\n\r\n/**\r\n * AI Service Client for streaming chat requests to backend\r\n */\r\nexport class AIServiceClient {\r\n private baseURL: string;\r\n private maxRetries: number = 5;\r\n private retryDelay: number = 1000; // Start with 1 second\r\n private maxRetryDelay: number = 30000;\r\n private maxRetryAfterDelay: number = 60000;\r\n private highDemandMessage: string = 'Very high demand right now. Please wait and retry after some time.';\r\n\r\n constructor() {\r\n // Don't set baseURL yet - lazy load it when first used\r\n // This allows environment variables to be loaded first\r\n this.baseURL = '';\r\n }\r\n\r\n /**\r\n * Get the base URL for API requests\r\n * Lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getBaseURL(): string {\r\n if (!this.baseURL) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n this.baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n }\r\n return this.baseURL;\r\n }\r\n\r\n /**\r\n * Stream chat request to backend AI proxy\r\n * \r\n * @param model - The AI model to use (e.g., 'gemini-2.5-flash')\r\n * @param messages - Conversation history including system, user, assistant, and tool messages\r\n * @param tools - Available tool schemas for the AI to use\r\n * @param environmentContext - Optional environment context (OS, shell, cwd, etc.)\r\n * @param mode - Optional mode (default, plan, command)\r\n * @yields Stream chunks (text, tool calls, done, or error events)\r\n */\r\n async *streamChat(\r\n model: string,\r\n messages: Message[],\r\n tools: ToolSchema[],\r\n environmentContext?: EnvironmentContext,\r\n mode?: string,\r\n thinkingConfig?: Record<string, any>,\r\n abortSignal?: AbortSignal,\r\n modelName?: string,\r\n modelUid?: string\r\n ): AsyncGenerator<StreamChunk, void, unknown> {\r\n // Check if using a local Ollama model\r\n if (this.isUsingLocalModel()) {\r\n const localModel = this.getCurrentModel();\r\n const supportsTools = OllamaService.modelSupportsTools(localModel);\r\n\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Routing to local Ollama model: ${localModel}, supportsTools: ${supportsTools}\\n`);\r\n\r\n // Show warning only if tools are provided but model doesn't support them\r\n if (tools.length > 0 && !supportsTools) {\r\n yield {\r\n type: 'text',\r\n content: `⚠️ Note: Model \"${localModel}\" does not support tool calling. Running in text-only mode.\\n\\n`,\r\n };\r\n }\r\n\r\n // Route to local Ollama with tools\r\n yield* this.streamLocalChat(localModel, messages, tools);\r\n return;\r\n }\r\n\r\n // Build request payload for cloud backend\r\n const payload: ChatRequest = {\r\n model,\r\n modelUid,\r\n modelName,\r\n messages,\r\n tools,\r\n stream: true,\r\n clientType: 'cli',\r\n environmentContext,\r\n mode,\r\n thinkingConfig,\r\n };\r\n\r\n // Get authentication token from api client\r\n if (!apiClient.isAuthenticated()) {\r\n yield {\r\n type: 'error',\r\n message: 'Authentication required. Please sign in.',\r\n code: 'AUTH_REQUIRED',\r\n };\r\n return;\r\n }\r\n\r\n // Retry logic for transient errors\r\n let lastError: ErrorChunk | null = null;\r\n\r\n for (let attempt = 0; attempt < this.maxRetries; attempt++) {\r\n try {\r\n // Make fetch request to backend AI endpoint\r\n // NOTE: We intentionally do NOT set a timeout here because:\r\n // 1. Tool execution happens during streaming (in-stream execution)\r\n // 2. Interactive commands may require user input for arbitrary duration\r\n // 3. The only timeout should be explicit user cancellation via abortSignal\r\n // Connection issues will manifest as network errors, not timeouts\r\n\r\n const response = await fetch(`${this.getBaseURL()}/chat/completions`, {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n 'Authorization': `Bearer ${this.getSessionToken()}`,\r\n },\r\n body: JSON.stringify(payload),\r\n signal: abortSignal, // Only abort when user explicitly cancels\r\n });\r\n\r\n\r\n\r\n // Check for HTTP errors\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n const parsedHttpError = this.parseHttpError(response.status, response.statusText, errorText);\r\n\r\n // Handle specific error codes\r\n if (response.status === 401) {\r\n yield {\r\n type: 'error',\r\n message: 'Session expired. Please sign in again.',\r\n code: 'AUTH_REQUIRED',\r\n };\r\n return;\r\n }\r\n\r\n lastError = parsedHttpError;\r\n\r\n if (this.isRetryableError(parsedHttpError.code, parsedHttpError.message, response.status) && attempt < this.maxRetries - 1) {\r\n const retryAfterMs = this.getRetryAfterMs(response.headers);\r\n await this.sleep(this.getRetryDelayMs(attempt, retryAfterMs));\r\n continue;\r\n }\r\n\r\n if (this.isRateLimitError(parsedHttpError.code, parsedHttpError.message, response.status)) {\r\n yield this.createHighDemandError();\r\n return;\r\n }\r\n\r\n yield parsedHttpError;\r\n return;\r\n }\r\n\r\n // Parse SSE stream\r\n // We iterate manually to catch retryable errors from the stream\r\n let contentReceived = false;\r\n let shouldRetry = false;\r\n\r\n for await (const chunk of this.parseSSEStream(response.body)) {\r\n if (chunk.type === 'error') {\r\n const normalizedChunk = this.normalizeErrorChunk(chunk);\r\n\r\n // Check if this is a retryable error (like rate limit) AND we haven't sent partial content yet\r\n if (this.isRetryableError(normalizedChunk.code, normalizedChunk.message) && !contentReceived) {\r\n lastError = normalizedChunk;\r\n shouldRetry = true;\r\n break; // Break the stream loop to trigger retry\r\n }\r\n\r\n // Not retryable or content already sent - yield error and stop\r\n yield normalizedChunk;\r\n return;\r\n }\r\n\r\n // Mark that we've received content\r\n if (chunk.type !== 'done' && chunk.type !== 'thought' && chunk.type !== 'thinking_signature' && chunk.type !== 'file_descriptions') {\r\n contentReceived = true;\r\n }\r\n\r\n yield chunk;\r\n }\r\n\r\n // If stream finished cleanly (without error chunk), we are done\r\n if (!shouldRetry) {\r\n return;\r\n }\r\n\r\n // If we need to retry, check attempts and wait\r\n if (attempt < this.maxRetries - 1) {\r\n // We're going to retry, so we need to wait for the backoff period\r\n await this.sleep(this.getRetryDelayMs(attempt));\r\n continue;\r\n }\r\n\r\n // If we've exhausted retries, we'll fall through to the end of the loop \r\n // and yield the lastError below\r\n\r\n } catch (error: any) {\r\n lastError = this.normalizeThrownError(error);\r\n const wasIntentionalAbort = (error?.name === 'AbortError' || error?.name === 'TimeoutError') && !!abortSignal?.aborted;\r\n\r\n if (wasIntentionalAbort) {\r\n break;\r\n }\r\n\r\n // Retry for transient errors\r\n if (this.isRetryableError(lastError.code, lastError.message) && attempt < this.maxRetries - 1) {\r\n await this.sleep(this.getRetryDelayMs(attempt));\r\n continue;\r\n }\r\n\r\n // If not retryable or max retries reached, yield error and return\r\n break;\r\n }\r\n }\r\n\r\n // If we get here, we've exhausted retries\r\n if (lastError) {\r\n if (this.isRateLimitError(lastError.code, lastError.message)) {\r\n yield this.createHighDemandError();\r\n return;\r\n }\r\n yield lastError;\r\n }\r\n }\r\n\r\n /**\r\n * Create a normalized high-demand error response.\r\n */\r\n private createHighDemandError(): ErrorChunk {\r\n return {\r\n type: 'error',\r\n message: this.highDemandMessage,\r\n code: 'RATE_LIMIT',\r\n };\r\n }\r\n\r\n /**\r\n * Parse backend HTTP error body into a normalized error chunk.\r\n */\r\n private parseHttpError(status: number, statusText: string, errorText: string): ErrorChunk {\r\n let errorMessage = `HTTP ${status}: ${statusText}`;\r\n let errorCode = 'HTTP_ERROR';\r\n\r\n if (errorText) {\r\n try {\r\n const parsed = JSON.parse(errorText);\r\n const rootError = Array.isArray(parsed) ? parsed[0]?.error ?? parsed[0] : parsed?.error ?? parsed;\r\n\r\n if (rootError) {\r\n if (typeof rootError.message === 'string' && rootError.message.trim()) {\r\n errorMessage = rootError.message;\r\n }\r\n if (typeof rootError.code === 'string' || typeof rootError.code === 'number') {\r\n errorCode = String(rootError.code);\r\n } else if (typeof rootError.status === 'string') {\r\n errorCode = rootError.status;\r\n }\r\n }\r\n } catch {\r\n errorMessage = errorText;\r\n }\r\n }\r\n\r\n if (status === 504) {\r\n return {\r\n type: 'error',\r\n message: 'Request timed out. Please try again.',\r\n code: 'TIMEOUT',\r\n };\r\n }\r\n\r\n if (this.isRateLimitError(errorCode, errorMessage, status)) {\r\n return this.createHighDemandError();\r\n }\r\n\r\n return {\r\n type: 'error',\r\n message: errorMessage,\r\n code: errorCode,\r\n };\r\n }\r\n\r\n /**\r\n * Normalize streamed error chunks to stable internal codes/messages.\r\n */\r\n private normalizeErrorChunk(chunk: ErrorChunk): ErrorChunk {\r\n const normalizedCode = chunk.code ? String(chunk.code) : 'UNKNOWN_ERROR';\r\n const normalizedMessage = chunk.message || 'Unknown error occurred';\r\n\r\n if (this.isRateLimitError(normalizedCode, normalizedMessage)) {\r\n return this.createHighDemandError();\r\n }\r\n\r\n if (normalizedCode === '504') {\r\n return {\r\n type: 'error',\r\n message: 'Request timed out. Please try again.',\r\n code: 'TIMEOUT',\r\n };\r\n }\r\n\r\n return {\r\n type: 'error',\r\n message: normalizedMessage,\r\n code: normalizedCode,\r\n };\r\n }\r\n\r\n /**\r\n * Normalize thrown errors from fetch/network/runtime paths.\r\n */\r\n private normalizeThrownError(error: any): ErrorChunk {\r\n const rawMessage = error?.message || 'Unknown error occurred';\r\n const rawCode = error?.code ? String(error.code) : 'UNKNOWN_ERROR';\r\n\r\n if (this.isRateLimitError(rawCode, rawMessage, error?.status)) {\r\n return this.createHighDemandError();\r\n }\r\n\r\n if (error?.name === 'TypeError' && rawMessage.includes('fetch')) {\r\n return {\r\n type: 'error',\r\n message: 'Backend service is unreachable. Please check your connection.',\r\n code: 'NETWORK_ERROR',\r\n };\r\n }\r\n\r\n if (error?.name === 'AbortError' || error?.name === 'TimeoutError') {\r\n return {\r\n type: 'error',\r\n message: 'Request timed out. Please try again.',\r\n code: 'TIMEOUT',\r\n };\r\n }\r\n\r\n return {\r\n type: 'error',\r\n message: rawMessage,\r\n code: rawCode,\r\n };\r\n }\r\n\r\n /**\r\n * Check if an error is a rate-limit/resource-exhausted signal.\r\n */\r\n private isRateLimitError(code?: string, message: string = '', status?: number): boolean {\r\n const normalizedCode = (code || '').toString().toLowerCase();\r\n const normalizedMessage = message.toLowerCase();\r\n\r\n return status === 429 ||\r\n normalizedCode === '429' ||\r\n normalizedCode === '8' ||\r\n normalizedCode === 'rate_limit' ||\r\n normalizedCode === 'resource_exhausted' ||\r\n normalizedMessage.includes('rate limit') ||\r\n normalizedMessage.includes('too many requests') ||\r\n normalizedMessage.includes('quota') ||\r\n normalizedMessage.includes('resource exhausted') ||\r\n normalizedMessage.includes('resource_exhausted') ||\r\n normalizedMessage.includes('maas api returned 429');\r\n }\r\n\r\n /**\r\n * Check if HTTP status is retryable.\r\n */\r\n private isRetryableStatus(status?: number): boolean {\r\n if (typeof status !== 'number') {\r\n return false;\r\n }\r\n return [408, 425, 429, 500, 502, 503, 504].includes(status);\r\n }\r\n\r\n /**\r\n * Check if an error should be retried.\r\n */\r\n private isRetryableError(code: string, message: string = '', status?: number): boolean {\r\n if (this.isRateLimitError(code, message, status)) {\r\n return true;\r\n }\r\n\r\n if (this.isRetryableStatus(status)) {\r\n return true;\r\n }\r\n\r\n const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'UNKNOWN_ERROR', 'RATE_LIMIT'];\r\n if (retryableCodes.includes(code)) {\r\n return true;\r\n }\r\n\r\n const normalizedMessage = message.toLowerCase();\r\n return normalizedMessage.includes('timeout') ||\r\n normalizedMessage.includes('timed out') ||\r\n normalizedMessage.includes('temporarily unavailable') ||\r\n normalizedMessage.includes('network error');\r\n }\r\n\r\n /**\r\n * Parse Retry-After from response headers.\r\n */\r\n private getRetryAfterMs(headers: Headers): number | undefined {\r\n const retryAfterValue = headers.get('retry-after');\r\n if (!retryAfterValue) {\r\n return undefined;\r\n }\r\n\r\n const seconds = Number(retryAfterValue);\r\n if (Number.isFinite(seconds) && seconds >= 0) {\r\n return seconds * 1000;\r\n }\r\n\r\n const retryAfterDate = Date.parse(retryAfterValue);\r\n if (!Number.isNaN(retryAfterDate)) {\r\n return Math.max(0, retryAfterDate - Date.now());\r\n }\r\n\r\n return undefined;\r\n }\r\n\r\n /**\r\n * Compute retry delay with exponential backoff + jitter.\r\n */\r\n private getRetryDelayMs(attempt: number, retryAfterMs?: number): number {\r\n const exponentialDelay = this.retryDelay * Math.pow(2, attempt);\r\n const cappedDelay = Math.min(exponentialDelay, this.maxRetryDelay);\r\n const jitteredDelay = Math.round(Math.min(cappedDelay * (0.5 + Math.random()), this.maxRetryDelay));\r\n\r\n if (retryAfterMs === undefined) {\r\n return jitteredDelay;\r\n }\r\n\r\n const cappedRetryAfter = Math.min(Math.max(0, retryAfterMs), this.maxRetryAfterDelay);\r\n return Math.max(jitteredDelay, cappedRetryAfter);\r\n }\r\n\r\n /**\r\n * Sleep for specified milliseconds\r\n */\r\n private sleep(ms: number): Promise<void> {\r\n return new Promise(resolve => setTimeout(resolve, ms));\r\n }\r\n\r\n /**\r\n * Get session token from apiClient\r\n * This is a workaround since sessionToken is private\r\n */\r\n private getSessionToken(): string {\r\n // Read session token from the same location apiClient uses\r\n const configPath = join(homedir(), '.centaurus', 'session.json');\r\n\r\n try {\r\n if (existsSync(configPath)) {\r\n const data = readFileSync(configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n return session.sessionToken || '';\r\n }\r\n } catch (error) {\r\n // Return empty string if unable to read\r\n }\r\n\r\n return '';\r\n }\r\n\r\n /**\r\n * Check if the current configuration is using a local Ollama model\r\n */\r\n isUsingLocalModel(): boolean {\r\n const configPath = join(homedir(), '.centaurus', 'config.json');\r\n try {\r\n if (existsSync(configPath)) {\r\n const data = readFileSync(configPath, 'utf-8');\r\n const config = JSON.parse(data);\r\n return config.isLocalModel === true;\r\n }\r\n } catch (error) {\r\n // Return false if unable to read\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * Get the current model name from config\r\n */\r\n private getCurrentModel(): string {\r\n const configPath = join(homedir(), '.centaurus', 'config.json');\r\n try {\r\n if (existsSync(configPath)) {\r\n const data = readFileSync(configPath, 'utf-8');\r\n const config = JSON.parse(data);\r\n return config.model || 'gemini-2.5-flash';\r\n }\r\n } catch (error) {\r\n // Return default if unable to read\r\n }\r\n return 'gemini-2.5-flash';\r\n }\r\n\r\n /**\r\n * Convert internal message format to Ollama format (with tool support)\r\n */\r\n private convertToOllamaMessages(messages: Message[]): OllamaChatMessage[] {\r\n return messages.map(msg => {\r\n if (msg.role === 'tool') {\r\n // Tool results - convert to Ollama's tool message format\r\n return {\r\n role: 'tool' as const,\r\n content: msg.content,\r\n tool_name: msg.tool_call_id || 'unknown', // Use tool_call_id as tool_name\r\n };\r\n }\r\n\r\n if (msg.role === 'assistant' && msg.tool_calls?.length) {\r\n // Assistant message with tool calls\r\n return {\r\n role: 'assistant' as const,\r\n content: msg.content || '',\r\n tool_calls: msg.tool_calls.map(tc => ({\r\n function: {\r\n name: tc.name,\r\n arguments: tc.arguments,\r\n }\r\n })),\r\n };\r\n }\r\n\r\n // Regular message\r\n return {\r\n role: msg.role as 'system' | 'user' | 'assistant',\r\n content: msg.content,\r\n };\r\n });\r\n }\r\n\r\n /**\r\n * Convert ToolSchema array to Ollama tool format\r\n */\r\n private convertToolsToOllamaFormat(tools: ToolSchema[]): OllamaTool[] {\r\n return tools.map(tool => ({\r\n type: 'function' as const,\r\n function: {\r\n name: tool.name,\r\n description: tool.description,\r\n parameters: {\r\n type: 'object' as const,\r\n properties: tool.parameters.properties,\r\n required: tool.parameters.required,\r\n },\r\n },\r\n }));\r\n }\r\n\r\n /**\r\n * Stream chat request to local Ollama instance with tool calling support\r\n * \r\n * @param model - The local Ollama model to use (e.g., 'llama3.2:latest')\r\n * @param messages - Conversation history\r\n * @param tools - Available tools (will be converted to Ollama format)\r\n * @yields Stream chunks (text, tool_call, or done events)\r\n */\r\n async *streamLocalChat(\r\n model: string,\r\n messages: Message[],\r\n tools: ToolSchema[] = []\r\n ): AsyncGenerator<StreamChunk, void, unknown> {\r\n try {\r\n const supportsTools = OllamaService.modelSupportsTools(model);\r\n const effectiveTools = supportsTools ? tools : [];\r\n\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Starting local chat with model: ${model}, tools: ${effectiveTools.length}, supportsTools: ${supportsTools}\\n`);\r\n\r\n // Convert messages to Ollama format\r\n const ollamaMessages = this.convertToOllamaMessages(messages);\r\n\r\n // Convert tools to Ollama format if model supports them\r\n const ollamaTools = effectiveTools.length > 0\r\n ? this.convertToolsToOllamaFormat(effectiveTools)\r\n : undefined;\r\n\r\n // Send request to Ollama with tools\r\n const response = await ollamaService.sendChatMessage(model, ollamaMessages, ollamaTools);\r\n\r\n // Check for tool calls in response\r\n if (response.message?.tool_calls?.length) {\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Received ${response.message.tool_calls.length} tool calls from Ollama\\n`);\r\n\r\n // Yield each tool call\r\n for (const toolCall of response.message.tool_calls) {\r\n yield {\r\n type: 'tool_call',\r\n toolCall: {\r\n id: `ollama-${Date.now()}-${toolCall.function.name}`, // Generate unique ID\r\n name: toolCall.function.name,\r\n arguments: toolCall.function.arguments,\r\n },\r\n };\r\n }\r\n\r\n // Signal completion (tool execution will happen in cli-adapter)\r\n yield { type: 'done' };\r\n return;\r\n }\r\n\r\n // No tool calls - yield text response\r\n if (response.message.content) {\r\n yield {\r\n type: 'text',\r\n content: response.message.content,\r\n };\r\n }\r\n\r\n // Signal completion\r\n yield { type: 'done' };\r\n\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Local chat completed\\n`);\r\n } catch (error: any) {\r\n quickLog(`[${new Date().toISOString()}] [AIServiceClient] Local chat error: ${error.message}\\n`);\r\n yield {\r\n type: 'error',\r\n message: error.message || 'Failed to communicate with Ollama',\r\n code: 'OLLAMA_ERROR',\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Parse Server-Sent Events stream from response body\r\n * \r\n * @param body - ReadableStream from fetch response\r\n * @yields Parsed stream chunks\r\n */\r\n private async *parseSSEStream(\r\n body: ReadableStream<Uint8Array>\r\n ): AsyncGenerator<StreamChunk, void, unknown> {\r\n const reader = body.getReader();\r\n const decoder = new TextDecoder();\r\n let buffer = '';\r\n\r\n try {\r\n while (true) {\r\n const { done, value } = await reader.read();\r\n\r\n if (done) {\r\n // Process any remaining data in buffer before exiting\r\n if (buffer.trim()) {\r\n // Split remaining buffer and process\r\n const remainingLines = buffer.split('\\n');\r\n for (const line of remainingLines) {\r\n if (line.startsWith('data: ')) {\r\n const dataStr = line.slice(6);\r\n if (dataStr.trim()) {\r\n try {\r\n const chunk = JSON.parse(dataStr) as StreamChunk;\r\n yield chunk;\r\n } catch (error) {\r\n // Skip malformed JSON\r\n logWarning(`Failed to parse SSE data (in buffer): ${dataStr}`);\r\n }\r\n }\r\n }\r\n }\r\n }\r\n break;\r\n }\r\n\r\n // Decode chunk and add to buffer\r\n buffer += decoder.decode(value, { stream: true });\r\n\r\n // Process complete lines in buffer\r\n const lines = buffer.split('\\n');\r\n\r\n // Keep the last incomplete line in buffer\r\n buffer = lines.pop() || '';\r\n\r\n for (const line of lines) {\r\n // SSE format: \"data: {json}\"\r\n if (line.startsWith('data: ')) {\r\n const dataStr = line.slice(6); // Remove \"data: \" prefix\r\n\r\n // Skip empty data lines\r\n if (!dataStr.trim()) {\r\n continue;\r\n }\r\n\r\n try {\r\n const chunk = JSON.parse(dataStr) as StreamChunk;\r\n yield chunk;\r\n\r\n // Stop if we receive a done or error event\r\n if (chunk.type === 'done' || chunk.type === 'error') {\r\n return;\r\n }\r\n } catch (error) {\r\n // Skip malformed JSON\r\n logWarning(`Failed to parse SSE data: ${dataStr}`);\r\n }\r\n }\r\n // SSE event type line: \"event: chunk\"\r\n // We don't need to process these separately since the data contains the type\r\n }\r\n }\r\n } finally {\r\n reader.releaseLock();\r\n }\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const aiServiceClient = new AIServiceClient();\r\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/api-client.ts"],"sourcesContent":["/**\r\n * API Client Service for Centaurus CLI\r\n * \r\n * Handles all communication with the backend REST API including:\r\n * - Authentication and session management\r\n * - Conversation and message operations\r\n * - User settings management\r\n * - API key storage and retrieval\r\n */\r\n\r\nimport axios, { AxiosInstance, AxiosError } from 'axios';\r\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logError } from '../utils/logger.js';\r\n\r\n/**\r\n * Type definitions for API requests and responses\r\n */\r\n\r\n// Authentication types\r\ninterface GoogleAuthInitResponse {\r\n authUrl: string;\r\n state: string;\r\n}\r\n\r\ninterface AuthResponse {\r\n sessionToken: string;\r\n expiresAt: string;\r\n user: {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n };\r\n}\r\n\r\ninterface UserProfile {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n createdAt: string;\r\n}\r\n\r\n// Conversation types\r\ninterface CreateConversationRequest {\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags?: string[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Conversation {\r\n id: string;\r\n userId: string;\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags: string[];\r\n isPinned: boolean;\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n updatedAt: string;\r\n archivedAt?: string;\r\n}\r\n\r\ninterface UpdateConversationRequest {\r\n title?: string;\r\n tags?: string[];\r\n isPinned?: boolean;\r\n metadata?: Record<string, any>;\r\n}\r\n\r\n// Message types\r\ninterface CreateMessageRequest {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType?: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Message {\r\n id: string;\r\n conversationId: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n editedAt?: string;\r\n}\r\n\r\n// Settings types\r\ninterface UserSettings {\r\n defaultModel?: string;\r\n defaultProvider?: string;\r\n theme?: string;\r\n autoSave?: boolean;\r\n planMode?: boolean;\r\n [key: string]: any;\r\n}\r\n\r\n// Models configuration types\r\nexport interface ModelConfig {\r\n id: string;\r\n name: string;\r\n description: string;\r\n provider: string;\r\n contextWindow: number;\r\n region: string;\r\n supportsThinking: boolean;\r\n thinkingConfig?: Record<string, any>;\r\n generationConfig?: {\r\n temperature?: number;\r\n topP?: number;\r\n topK?: number;\r\n maxOutputTokens?: number;\r\n };\r\n allowFrontendDisplay?: boolean; // If false, model is hidden from frontend model picker but valid for API use\r\n}\r\n\r\nexport interface ModelsConfig {\r\n models: ModelConfig[];\r\n defaultModel: string;\r\n}\r\n\r\n// API Response wrapper\r\ninterface ApiResponse<T = any> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: any;\r\n };\r\n meta?: {\r\n page?: number;\r\n limit?: number;\r\n total?: number;\r\n };\r\n}\r\n\r\n/**\r\n * API Client class for communicating with the backend service\r\n */\r\nclass ApiClient {\r\n private client: AxiosInstance | null = null;\r\n private sessionToken: string | null = null;\r\n private configPath: string;\r\n private configDir: string;\r\n private cachedUser: UserProfile | null = null;\r\n\r\n constructor() {\r\n // Set up session storage path: ~/.centaurus/session.json\r\n this.configDir = join(homedir(), '.centaurus');\r\n this.configPath = join(this.configDir, 'session.json');\r\n\r\n // Load existing session if available\r\n this.loadSession();\r\n\r\n // Don't create axios client yet - wait until first use\r\n // This allows environment variables to be loaded first\r\n }\r\n\r\n /**\r\n * Helper to extract data from API response safely\r\n */\r\n private extractData<T>(response: { data: ApiResponse<T> }): T {\r\n if (response.data.data === undefined || response.data.data === null) {\r\n throw new Error('API response missing expected data payload');\r\n }\r\n return response.data.data as T;\r\n }\r\n\r\n /**\r\n * Get or create the axios client instance\r\n * This is lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getClient(): AxiosInstance {\r\n if (!this.client) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n\r\n this.client = axios.create({\r\n baseURL,\r\n timeout: 30000,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n });\r\n\r\n // Request interceptor: Add Authorization header if session token exists\r\n this.getClient().interceptors.request.use(\r\n (config) => {\r\n if (this.sessionToken) {\r\n config.headers.Authorization = `Bearer ${this.sessionToken}`;\r\n }\r\n return config;\r\n },\r\n (error) => {\r\n return Promise.reject(error);\r\n }\r\n );\r\n\r\n // Response interceptor: Handle 401 errors (expired/invalid session)\r\n this.getClient().interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (error.response?.status === 401) {\r\n // Clear invalid session\r\n this.clearSession();\r\n\r\n // Create a more user-friendly error\r\n const authError = new Error('Session expired. Please sign in again.');\r\n authError.name = 'AuthenticationError';\r\n throw authError;\r\n }\r\n\r\n // For other errors, extract message from API response if available\r\n if (error.response?.data) {\r\n const apiError = error.response.data as ApiResponse;\r\n if (apiError.error) {\r\n const customError = new Error(apiError.error.message);\r\n customError.name = apiError.error.code;\r\n throw customError;\r\n }\r\n }\r\n\r\n throw error;\r\n }\r\n );\r\n }\r\n\r\n return this.client;\r\n }\r\n\r\n /**\r\n * Load session token from local config file\r\n */\r\n private loadSession(): void {\r\n try {\r\n if (existsSync(this.configPath)) {\r\n const data = readFileSync(this.configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n this.sessionToken = session.sessionToken || null;\r\n }\r\n } catch (error) {\r\n // If there's any error reading the session, just start fresh\r\n this.sessionToken = null;\r\n }\r\n }\r\n\r\n /**\r\n * Save session token to local config file\r\n */\r\n private saveSession(token: string, expiresAt?: string): void {\r\n try {\r\n // Ensure config directory exists\r\n if (!existsSync(this.configDir)) {\r\n mkdirSync(this.configDir, { recursive: true, mode: 0o700 });\r\n }\r\n\r\n // Save session data\r\n const sessionData = {\r\n sessionToken: token,\r\n expiresAt: expiresAt || null,\r\n savedAt: new Date().toISOString(),\r\n };\r\n\r\n writeFileSync(this.configPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n this.sessionToken = token;\r\n } catch (error) {\r\n logError('Failed to save session', error as Error);\r\n throw new Error('Failed to save session locally');\r\n }\r\n }\r\n\r\n /**\r\n * Clear session token from memory and local storage\r\n */\r\n private clearSession(): void {\r\n this.sessionToken = null;\r\n this.cachedUser = null;\r\n try {\r\n if (existsSync(this.configPath)) {\r\n unlinkSync(this.configPath);\r\n }\r\n } catch (error) {\r\n // Ignore errors when clearing session\r\n }\r\n }\r\n\r\n /**\r\n * Check if user is authenticated\r\n */\r\n public isAuthenticated(): boolean {\r\n return this.sessionToken !== null;\r\n }\r\n\r\n // ==================== Authentication Methods ====================\r\n\r\n /**\r\n * Initialize Google OAuth flow\r\n * @param redirectUri - The URI to redirect to after OAuth\r\n * @returns OAuth URL and state parameter\r\n */\r\n async initGoogleAuth(redirectUri: string): Promise<GoogleAuthInitResponse> {\r\n const response = await this.getClient().post<ApiResponse<GoogleAuthInitResponse>>(\r\n '/auth/google/init',\r\n { redirectUri }\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Complete Google OAuth authentication\r\n * @param code - Authorization code from Google\r\n * @param state - State parameter for CSRF protection\r\n * @returns Session token and user information\r\n */\r\n async authenticate(code: string, state: string): Promise<AuthResponse> {\r\n const response = await this.getClient().post<ApiResponse<AuthResponse>>(\r\n '/auth/google/callback',\r\n { code, state }\r\n );\r\n\r\n const authData = this.extractData(response);\r\n this.saveSession(authData.sessionToken, authData.expiresAt);\r\n\r\n return authData;\r\n }\r\n\r\n /**\r\n * Set session token directly (used when receiving token from web app)\r\n * @param sessionToken - The session token to save\r\n * @param user - User information\r\n */\r\n setSessionToken(sessionToken: string, user?: any): void {\r\n // Calculate expiration (30 days from now)\r\n const expiresAt = new Date();\r\n expiresAt.setDate(expiresAt.getDate() + 30);\r\n\r\n this.saveSession(sessionToken, expiresAt.toISOString());\r\n }\r\n\r\n /**\r\n * Refresh the current session token\r\n * @returns New session token and expiration\r\n */\r\n async refreshSession(): Promise<{ sessionToken: string; expiresAt: string }> {\r\n const response = await this.getClient().post<ApiResponse<{ sessionToken: string; expiresAt: string }>>(\r\n '/auth/refresh'\r\n );\r\n\r\n const refreshData = this.extractData(response);\r\n this.saveSession(refreshData.sessionToken, refreshData.expiresAt);\r\n\r\n return refreshData;\r\n }\r\n\r\n /**\r\n * Logout and invalidate current session\r\n */\r\n async logout(): Promise<void> {\r\n try {\r\n await this.getClient().post('/auth/logout');\r\n } finally {\r\n // Always clear local session, even if API call fails\r\n this.clearSession();\r\n }\r\n }\r\n\r\n /**\r\n * Get current authenticated user profile\r\n * @returns User profile information\r\n */\r\n async getCurrentUser(): Promise<UserProfile> {\r\n const response = await this.getClient().get<ApiResponse<UserProfile>>('/auth/me');\r\n const user = this.extractData(response);\r\n this.cachedUser = user;\r\n return user;\r\n }\r\n\r\n getCachedUser(): UserProfile | null {\r\n return this.cachedUser;\r\n }\r\n\r\n // ==================== Conversation Methods ====================\r\n\r\n /**\r\n * Create a new conversation\r\n * @param data - Conversation creation parameters\r\n * @returns Created conversation\r\n */\r\n async createConversation(data: CreateConversationRequest): Promise<Conversation> {\r\n const response = await this.getClient().post<ApiResponse<Conversation>>(\r\n '/threads',\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all conversations for the authenticated user\r\n * @param params - Pagination and filter parameters\r\n * @returns List of conversations with pagination metadata\r\n */\r\n async getConversations(params?: {\r\n page?: number;\r\n limit?: number;\r\n includeArchived?: boolean;\r\n tags?: string[];\r\n }): Promise<ApiResponse<Conversation[]>> {\r\n const queryParams: any = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 20,\r\n };\r\n\r\n if (params?.includeArchived !== undefined) {\r\n queryParams.includeArchived = params.includeArchived;\r\n }\r\n\r\n if (params?.tags && params.tags.length > 0) {\r\n queryParams.tags = params.tags.join(',');\r\n }\r\n\r\n const response = await this.getClient().get<ApiResponse<Conversation[]>>(\r\n '/threads',\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n /**\r\n * Get a specific conversation by ID\r\n * @param conversationId - The conversation ID\r\n * @returns Conversation details\r\n */\r\n async getConversation(conversationId: string): Promise<Conversation> {\r\n const response = await this.getClient().get<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update a conversation\r\n * @param conversationId - The conversation ID\r\n * @param data - Fields to update\r\n * @returns Updated conversation\r\n */\r\n async updateConversation(\r\n conversationId: string,\r\n data: UpdateConversationRequest\r\n ): Promise<Conversation> {\r\n const response = await this.getClient().put<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`,\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete (archive) a conversation\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversation(conversationId: string): Promise<void> {\r\n await this.getClient().delete(`/threads/${conversationId}`);\r\n }\r\n\r\n // ==================== Message Methods ====================\r\n\r\n /**\r\n * Add a message to a conversation\r\n * @param conversationId - The conversation ID\r\n * @param message - Message data\r\n * @returns Created message\r\n */\r\n async addMessage(\r\n conversationId: string,\r\n message: CreateMessageRequest\r\n ): Promise<Message> {\r\n const response = await this.getClient().post<ApiResponse<Message>>(\r\n `/threads/${conversationId}/messages`,\r\n message\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all messages for a conversation\r\n * @param conversationId - The conversation ID\r\n * @param params - Pagination parameters\r\n * @returns List of messages with pagination metadata\r\n */\r\n async getMessages(\r\n conversationId: string,\r\n params?: { page?: number; limit?: number }\r\n ): Promise<ApiResponse<Message[]>> {\r\n const queryParams = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 50,\r\n };\r\n\r\n const response = await this.getClient().get<ApiResponse<Message[]>>(\r\n `/threads/${conversationId}/messages`,\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n // ==================== Settings Methods ====================\r\n\r\n /**\r\n * Get user settings\r\n * @returns User settings object\r\n */\r\n async getSettings(): Promise<UserSettings> {\r\n const response = await this.getClient().get<ApiResponse<UserSettings>>('/settings');\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update user settings\r\n * @param settings - Settings to update (partial update supported)\r\n * @returns Updated settings\r\n */\r\n async updateSettings(settings: Partial<UserSettings>): Promise<UserSettings> {\r\n const response = await this.getClient().put<ApiResponse<UserSettings>>(\r\n '/settings',\r\n settings\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Models Configuration Methods ====================\r\n\r\n /**\r\n * Get available AI models configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Models configuration including all model variants and default model\r\n */\r\n async getModelsConfig(): Promise<ModelsConfig> {\r\n const response = await this.getClient().get<ApiResponse<ModelsConfig>>('/models');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Rate Limits Configuration Methods ====================\r\n\r\n /**\r\n * Get rate limits configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Rate limits configuration including session quotas\r\n */\r\n async getRateLimitsConfig(): Promise<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }> {\r\n const response = await this.getClient().get<ApiResponse<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }>>('/rate-limits/session');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Classification Methods ====================\r\n\r\n /**\r\n * Classify user input to determine if it's a terminal command or AI message\r\n * @param text - Input text to classify\r\n * @returns Mode prediction: 'terminal' or 'ai'\r\n */\r\n async classifyInput(text: string): Promise<'terminal' | 'ai'> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ mode: 'terminal' | 'ai' }>>(\r\n '/classify',\r\n { text }\r\n );\r\n return response.data.data?.mode || 'ai';\r\n } catch (error) {\r\n // On error, return default 'ai' mode - silent fallback\r\n return 'ai';\r\n }\r\n }\r\n\r\n // ==================== File Upload Methods ====================\r\n\r\n /**\r\n * Upload a file to the backend for AI processing\r\n * @param conversationId - The conversation ID\r\n * @param fileName - Original file name\r\n * @param fileType - MIME type\r\n * @param fileData - Base64 encoded file data\r\n * @returns Upload result with gcsUri for Vertex AI\r\n */\r\n async uploadFile(\r\n conversationId: string,\r\n fileName: string,\r\n fileType: string,\r\n fileData: string\r\n ): Promise<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }>>('/files', { conversationId, fileName, fileType, mimeType: fileType, fileData, clientType: 'cli' });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete all files for a conversation from storage (Supabase and GCS)\r\n * Call this when deleting a conversation to clean up associated images\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversationFiles(conversationId: string): Promise<void> {\r\n try {\r\n await this.getClient().delete(`/files/by-thread/${conversationId}`);\r\n } catch (error) {\r\n // Silently fail - files might not exist or user might not be authenticated\r\n // This is a cleanup operation, so we don't want to block chat deletion\r\n }\r\n }\r\n\r\n // ==================== Sync Methods ====================\r\n\r\n /**\r\n * Upload sync data (combined chat history and config)\r\n * @param syncData - The combined data to sync\r\n * @returns Upload result with version info\r\n */\r\n async uploadSyncData(syncData: any): Promise<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }>>('/sync', { syncData });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get sync data for the current user\r\n * @returns Sync data or null if not found\r\n */\r\n async getSyncData(): Promise<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n } | null> {\r\n try {\r\n const response = await this.getClient().get<ApiResponse<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n }>>('/sync');\r\n return this.extractData(response);\r\n } catch (error: any) {\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Delete sync data for the current user\r\n */\r\n async deleteSyncData(): Promise<void> {\r\n await this.getClient().delete('/sync');\r\n }\r\n\r\n // ==================== Token Counting Methods ====================\r\n\r\n /**\r\n * Count tokens for a given model and messages\r\n * Uses backend's accurate token counting (Vertex AI countTokens API)\r\n * @param model - Model name (e.g., gemini-2.5-flash)\r\n * @param messages - Array of conversation messages\r\n * @returns Total token count including system prompt\r\n */\r\n async countTokens(model: string, messages: any[]): Promise<number> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ tokenCount: number; model: string }>>(\r\n '/chat/token-count',\r\n { model, messages }\r\n );\r\n return response.data.data?.tokenCount || 0;\r\n } catch (error) {\r\n logError('Failed to count tokens via API', error as Error);\r\n\r\n // Fallback to character-based estimation\r\n const totalCharacters = messages.reduce((sum, msg) => {\r\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\r\n return sum + content.length;\r\n }, 0);\r\n\r\n // Add system prompt estimate (roughly 14000 characters)\r\n // Use 1 token ≈ 4 characters for Gemini models\r\n return Math.ceil((totalCharacters + 14000) / 4);\r\n }\r\n }\r\n\r\n // ==================== Health Check ====================\r\n\r\n /**\r\n * Check backend service health\r\n * @returns Health status information\r\n */\r\n async healthCheck(): Promise<{\r\n status: string;\r\n timestamp: string;\r\n database: string;\r\n version: string;\r\n }> {\r\n // Health endpoint is at root level, not under /api\r\n // So we need to construct the full URL manually\r\n // Use build config for URL (frozen at compile time)\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n const healthURL = baseURL.replace('/v1', '/health');\r\n\r\n const response = await axios.get<ApiResponse<any>>(healthURL);\r\n return this.extractData(response);\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const apiClient = new ApiClient();\r\n\r\n// Export types for use in other modules\r\nexport type {\r\n GoogleAuthInitResponse,\r\n AuthResponse,\r\n UserProfile,\r\n CreateConversationRequest,\r\n Conversation,\r\n UpdateConversationRequest,\r\n CreateMessageRequest,\r\n Message,\r\n UserSettings,\r\n ApiResponse,\r\n};\r\n\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/services/api-client.ts"],"sourcesContent":["/**\r\n * API Client Service for Centaurus CLI\r\n * \r\n * Handles all communication with the backend REST API including:\r\n * - Authentication and session management\r\n * - Conversation and message operations\r\n * - User settings management\r\n * - API key storage and retrieval\r\n */\r\n\r\nimport axios, { AxiosInstance, AxiosError } from 'axios';\r\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';\r\nimport { join, dirname } from 'path';\r\nimport { homedir } from 'os';\r\nimport { IS_DEV_BUILD, DEV_BACKEND_URL, PRODUCTION_BACKEND_URL } from '../config/build-config.js';\r\nimport { logError } from '../utils/logger.js';\r\n\r\n/**\r\n * Type definitions for API requests and responses\r\n */\r\n\r\n// Authentication types\r\ninterface GoogleAuthInitResponse {\r\n authUrl: string;\r\n state: string;\r\n}\r\n\r\ninterface AuthResponse {\r\n sessionToken: string;\r\n expiresAt: string;\r\n user: {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n };\r\n}\r\n\r\ninterface UserProfile {\r\n id: string;\r\n email: string;\r\n fullName: string;\r\n avatarUrl?: string;\r\n createdAt: string;\r\n}\r\n\r\n// Conversation types\r\ninterface CreateConversationRequest {\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags?: string[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Conversation {\r\n id: string;\r\n userId: string;\r\n title: string;\r\n modelUsed: string;\r\n provider: string;\r\n workingDirectory?: string;\r\n tags: string[];\r\n isPinned: boolean;\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n updatedAt: string;\r\n archivedAt?: string;\r\n}\r\n\r\ninterface UpdateConversationRequest {\r\n title?: string;\r\n tags?: string[];\r\n isPinned?: boolean;\r\n metadata?: Record<string, any>;\r\n}\r\n\r\n// Message types\r\ninterface CreateMessageRequest {\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType?: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata?: Record<string, any>;\r\n}\r\n\r\ninterface Message {\r\n id: string;\r\n conversationId: string;\r\n role: 'user' | 'assistant' | 'system' | 'tool';\r\n content: string;\r\n contentType: 'text' | 'code' | 'markdown' | 'file' | 'image';\r\n tokensUsed?: number;\r\n toolCalls?: any[];\r\n attachments?: any[];\r\n metadata: Record<string, any>;\r\n createdAt: string;\r\n editedAt?: string;\r\n}\r\n\r\n// Settings types\r\ninterface UserSettings {\r\n defaultModel?: string;\r\n defaultProvider?: string;\r\n theme?: string;\r\n autoSave?: boolean;\r\n planMode?: boolean;\r\n [key: string]: any;\r\n}\r\n\r\n// Models configuration types\r\nexport interface ModelConfig {\r\n uid: string; // Unique identifier per entry (e.g. \"claude-opus-4-6-thinking\")\r\n id: string; // Actual API model identifier (may be shared across entries)\r\n name: string;\r\n description: string;\r\n provider: string;\r\n contextWindow: number;\r\n region: string;\r\n supportsThinking: boolean;\r\n thinkingConfig?: Record<string, any>;\r\n generationConfig?: {\r\n temperature?: number;\r\n topP?: number;\r\n topK?: number;\r\n maxOutputTokens?: number;\r\n };\r\n allowFrontendDisplay?: boolean; // If false, model is hidden from frontend model picker but valid for API use\r\n}\r\n\r\nexport interface ModelsConfig {\r\n models: ModelConfig[];\r\n defaultModel: string;\r\n}\r\n\r\n// API Response wrapper\r\ninterface ApiResponse<T = any> {\r\n success: boolean;\r\n data?: T;\r\n error?: {\r\n code: string;\r\n message: string;\r\n details?: any;\r\n };\r\n meta?: {\r\n page?: number;\r\n limit?: number;\r\n total?: number;\r\n };\r\n}\r\n\r\n/**\r\n * API Client class for communicating with the backend service\r\n */\r\nclass ApiClient {\r\n private client: AxiosInstance | null = null;\r\n private sessionToken: string | null = null;\r\n private configPath: string;\r\n private configDir: string;\r\n private cachedUser: UserProfile | null = null;\r\n\r\n constructor() {\r\n // Set up session storage path: ~/.centaurus/session.json\r\n this.configDir = join(homedir(), '.centaurus');\r\n this.configPath = join(this.configDir, 'session.json');\r\n\r\n // Load existing session if available\r\n this.loadSession();\r\n\r\n // Don't create axios client yet - wait until first use\r\n // This allows environment variables to be loaded first\r\n }\r\n\r\n /**\r\n * Helper to extract data from API response safely\r\n */\r\n private extractData<T>(response: { data: ApiResponse<T> }): T {\r\n if (response.data.data === undefined || response.data.data === null) {\r\n throw new Error('API response missing expected data payload');\r\n }\r\n return response.data.data as T;\r\n }\r\n\r\n /**\r\n * Get or create the axios client instance\r\n * This is lazy-loaded to ensure environment variables are loaded first\r\n */\r\n private getClient(): AxiosInstance {\r\n if (!this.client) {\r\n // Import build config - values frozen at compile time for security\r\n // SECURITY: This prevents malicious .env files from overriding production URLs\r\n\r\n // Use production URL in production builds, localhost only in dev builds\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n\r\n this.client = axios.create({\r\n baseURL,\r\n timeout: 30000,\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n });\r\n\r\n // Request interceptor: Add Authorization header if session token exists\r\n this.getClient().interceptors.request.use(\r\n (config) => {\r\n if (this.sessionToken) {\r\n config.headers.Authorization = `Bearer ${this.sessionToken}`;\r\n }\r\n return config;\r\n },\r\n (error) => {\r\n return Promise.reject(error);\r\n }\r\n );\r\n\r\n // Response interceptor: Handle 401 errors (expired/invalid session)\r\n this.getClient().interceptors.response.use(\r\n (response) => response,\r\n async (error: AxiosError) => {\r\n if (error.response?.status === 401) {\r\n // Clear invalid session\r\n this.clearSession();\r\n\r\n // Create a more user-friendly error\r\n const authError = new Error('Session expired. Please sign in again.');\r\n authError.name = 'AuthenticationError';\r\n throw authError;\r\n }\r\n\r\n // For other errors, extract message from API response if available\r\n if (error.response?.data) {\r\n const apiError = error.response.data as ApiResponse;\r\n if (apiError.error) {\r\n const customError = new Error(apiError.error.message);\r\n customError.name = apiError.error.code;\r\n throw customError;\r\n }\r\n }\r\n\r\n throw error;\r\n }\r\n );\r\n }\r\n\r\n return this.client;\r\n }\r\n\r\n /**\r\n * Load session token from local config file\r\n */\r\n private loadSession(): void {\r\n try {\r\n if (existsSync(this.configPath)) {\r\n const data = readFileSync(this.configPath, 'utf-8');\r\n const session = JSON.parse(data);\r\n this.sessionToken = session.sessionToken || null;\r\n }\r\n } catch (error) {\r\n // If there's any error reading the session, just start fresh\r\n this.sessionToken = null;\r\n }\r\n }\r\n\r\n /**\r\n * Save session token to local config file\r\n */\r\n private saveSession(token: string, expiresAt?: string): void {\r\n try {\r\n // Ensure config directory exists\r\n if (!existsSync(this.configDir)) {\r\n mkdirSync(this.configDir, { recursive: true, mode: 0o700 });\r\n }\r\n\r\n // Save session data\r\n const sessionData = {\r\n sessionToken: token,\r\n expiresAt: expiresAt || null,\r\n savedAt: new Date().toISOString(),\r\n };\r\n\r\n writeFileSync(this.configPath, JSON.stringify(sessionData, null, 2), { encoding: 'utf-8', mode: 0o600 });\r\n this.sessionToken = token;\r\n } catch (error) {\r\n logError('Failed to save session', error as Error);\r\n throw new Error('Failed to save session locally');\r\n }\r\n }\r\n\r\n /**\r\n * Clear session token from memory and local storage\r\n */\r\n private clearSession(): void {\r\n this.sessionToken = null;\r\n this.cachedUser = null;\r\n try {\r\n if (existsSync(this.configPath)) {\r\n unlinkSync(this.configPath);\r\n }\r\n } catch (error) {\r\n // Ignore errors when clearing session\r\n }\r\n }\r\n\r\n /**\r\n * Check if user is authenticated\r\n */\r\n public isAuthenticated(): boolean {\r\n return this.sessionToken !== null;\r\n }\r\n\r\n // ==================== Authentication Methods ====================\r\n\r\n /**\r\n * Initialize Google OAuth flow\r\n * @param redirectUri - The URI to redirect to after OAuth\r\n * @returns OAuth URL and state parameter\r\n */\r\n async initGoogleAuth(redirectUri: string): Promise<GoogleAuthInitResponse> {\r\n const response = await this.getClient().post<ApiResponse<GoogleAuthInitResponse>>(\r\n '/auth/google/init',\r\n { redirectUri }\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Complete Google OAuth authentication\r\n * @param code - Authorization code from Google\r\n * @param state - State parameter for CSRF protection\r\n * @returns Session token and user information\r\n */\r\n async authenticate(code: string, state: string): Promise<AuthResponse> {\r\n const response = await this.getClient().post<ApiResponse<AuthResponse>>(\r\n '/auth/google/callback',\r\n { code, state }\r\n );\r\n\r\n const authData = this.extractData(response);\r\n this.saveSession(authData.sessionToken, authData.expiresAt);\r\n\r\n return authData;\r\n }\r\n\r\n /**\r\n * Set session token directly (used when receiving token from web app)\r\n * @param sessionToken - The session token to save\r\n * @param user - User information\r\n */\r\n setSessionToken(sessionToken: string, user?: any): void {\r\n // Calculate expiration (30 days from now)\r\n const expiresAt = new Date();\r\n expiresAt.setDate(expiresAt.getDate() + 30);\r\n\r\n this.saveSession(sessionToken, expiresAt.toISOString());\r\n }\r\n\r\n /**\r\n * Refresh the current session token\r\n * @returns New session token and expiration\r\n */\r\n async refreshSession(): Promise<{ sessionToken: string; expiresAt: string }> {\r\n const response = await this.getClient().post<ApiResponse<{ sessionToken: string; expiresAt: string }>>(\r\n '/auth/refresh'\r\n );\r\n\r\n const refreshData = this.extractData(response);\r\n this.saveSession(refreshData.sessionToken, refreshData.expiresAt);\r\n\r\n return refreshData;\r\n }\r\n\r\n /**\r\n * Logout and invalidate current session\r\n */\r\n async logout(): Promise<void> {\r\n try {\r\n await this.getClient().post('/auth/logout');\r\n } finally {\r\n // Always clear local session, even if API call fails\r\n this.clearSession();\r\n }\r\n }\r\n\r\n /**\r\n * Get current authenticated user profile\r\n * @returns User profile information\r\n */\r\n async getCurrentUser(): Promise<UserProfile> {\r\n const response = await this.getClient().get<ApiResponse<UserProfile>>('/auth/me');\r\n const user = this.extractData(response);\r\n this.cachedUser = user;\r\n return user;\r\n }\r\n\r\n getCachedUser(): UserProfile | null {\r\n return this.cachedUser;\r\n }\r\n\r\n // ==================== Conversation Methods ====================\r\n\r\n /**\r\n * Create a new conversation\r\n * @param data - Conversation creation parameters\r\n * @returns Created conversation\r\n */\r\n async createConversation(data: CreateConversationRequest): Promise<Conversation> {\r\n const response = await this.getClient().post<ApiResponse<Conversation>>(\r\n '/threads',\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all conversations for the authenticated user\r\n * @param params - Pagination and filter parameters\r\n * @returns List of conversations with pagination metadata\r\n */\r\n async getConversations(params?: {\r\n page?: number;\r\n limit?: number;\r\n includeArchived?: boolean;\r\n tags?: string[];\r\n }): Promise<ApiResponse<Conversation[]>> {\r\n const queryParams: any = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 20,\r\n };\r\n\r\n if (params?.includeArchived !== undefined) {\r\n queryParams.includeArchived = params.includeArchived;\r\n }\r\n\r\n if (params?.tags && params.tags.length > 0) {\r\n queryParams.tags = params.tags.join(',');\r\n }\r\n\r\n const response = await this.getClient().get<ApiResponse<Conversation[]>>(\r\n '/threads',\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n /**\r\n * Get a specific conversation by ID\r\n * @param conversationId - The conversation ID\r\n * @returns Conversation details\r\n */\r\n async getConversation(conversationId: string): Promise<Conversation> {\r\n const response = await this.getClient().get<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update a conversation\r\n * @param conversationId - The conversation ID\r\n * @param data - Fields to update\r\n * @returns Updated conversation\r\n */\r\n async updateConversation(\r\n conversationId: string,\r\n data: UpdateConversationRequest\r\n ): Promise<Conversation> {\r\n const response = await this.getClient().put<ApiResponse<Conversation>>(\r\n `/threads/${conversationId}`,\r\n data\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete (archive) a conversation\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversation(conversationId: string): Promise<void> {\r\n await this.getClient().delete(`/threads/${conversationId}`);\r\n }\r\n\r\n // ==================== Message Methods ====================\r\n\r\n /**\r\n * Add a message to a conversation\r\n * @param conversationId - The conversation ID\r\n * @param message - Message data\r\n * @returns Created message\r\n */\r\n async addMessage(\r\n conversationId: string,\r\n message: CreateMessageRequest\r\n ): Promise<Message> {\r\n const response = await this.getClient().post<ApiResponse<Message>>(\r\n `/threads/${conversationId}/messages`,\r\n message\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get all messages for a conversation\r\n * @param conversationId - The conversation ID\r\n * @param params - Pagination parameters\r\n * @returns List of messages with pagination metadata\r\n */\r\n async getMessages(\r\n conversationId: string,\r\n params?: { page?: number; limit?: number }\r\n ): Promise<ApiResponse<Message[]>> {\r\n const queryParams = {\r\n page: params?.page || 1,\r\n limit: params?.limit || 50,\r\n };\r\n\r\n const response = await this.getClient().get<ApiResponse<Message[]>>(\r\n `/threads/${conversationId}/messages`,\r\n { params: queryParams }\r\n );\r\n\r\n return response.data;\r\n }\r\n\r\n // ==================== Settings Methods ====================\r\n\r\n /**\r\n * Get user settings\r\n * @returns User settings object\r\n */\r\n async getSettings(): Promise<UserSettings> {\r\n const response = await this.getClient().get<ApiResponse<UserSettings>>('/settings');\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Update user settings\r\n * @param settings - Settings to update (partial update supported)\r\n * @returns Updated settings\r\n */\r\n async updateSettings(settings: Partial<UserSettings>): Promise<UserSettings> {\r\n const response = await this.getClient().put<ApiResponse<UserSettings>>(\r\n '/settings',\r\n settings\r\n );\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Models Configuration Methods ====================\r\n\r\n /**\r\n * Get available AI models configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Models configuration including all model variants and default model\r\n */\r\n async getModelsConfig(): Promise<ModelsConfig> {\r\n const response = await this.getClient().get<ApiResponse<ModelsConfig>>('/models');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Rate Limits Configuration Methods ====================\r\n\r\n /**\r\n * Get rate limits configuration from backend\r\n * This is a public endpoint (no auth required)\r\n * @returns Rate limits configuration including session quotas\r\n */\r\n async getRateLimitsConfig(): Promise<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }> {\r\n const response = await this.getClient().get<ApiResponse<{\r\n maxMessagesPerSession: number;\r\n sessionDurationHours: number;\r\n warningThreshold: number;\r\n defaultPlan: string;\r\n }>>('/rate-limits/session');\r\n return this.extractData(response);\r\n }\r\n\r\n // ==================== Classification Methods ====================\r\n\r\n /**\r\n * Classify user input to determine if it's a terminal command or AI message\r\n * @param text - Input text to classify\r\n * @returns Mode prediction: 'terminal' or 'ai'\r\n */\r\n async classifyInput(text: string): Promise<'terminal' | 'ai'> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ mode: 'terminal' | 'ai' }>>(\r\n '/classify',\r\n { text }\r\n );\r\n return response.data.data?.mode || 'ai';\r\n } catch (error) {\r\n // On error, return default 'ai' mode - silent fallback\r\n return 'ai';\r\n }\r\n }\r\n\r\n // ==================== File Upload Methods ====================\r\n\r\n /**\r\n * Upload a file to the backend for AI processing\r\n * @param conversationId - The conversation ID\r\n * @param fileName - Original file name\r\n * @param fileType - MIME type\r\n * @param fileData - Base64 encoded file data\r\n * @returns Upload result with gcsUri for Vertex AI\r\n */\r\n async uploadFile(\r\n conversationId: string,\r\n fileName: string,\r\n fileType: string,\r\n fileData: string\r\n ): Promise<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n storagePath: string;\r\n publicUrl: string;\r\n fileName: string;\r\n fileType: string;\r\n fileSize: number;\r\n gcsUri?: string;\r\n gcsPath?: string;\r\n }>>('/files', { conversationId, fileName, fileType, mimeType: fileType, fileData, clientType: 'cli' });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Delete all files for a conversation from storage (Supabase and GCS)\r\n * Call this when deleting a conversation to clean up associated images\r\n * @param conversationId - The conversation ID\r\n */\r\n async deleteConversationFiles(conversationId: string): Promise<void> {\r\n try {\r\n await this.getClient().delete(`/files/by-thread/${conversationId}`);\r\n } catch (error) {\r\n // Silently fail - files might not exist or user might not be authenticated\r\n // This is a cleanup operation, so we don't want to block chat deletion\r\n }\r\n }\r\n\r\n // ==================== Sync Methods ====================\r\n\r\n /**\r\n * Upload sync data (combined chat history and config)\r\n * @param syncData - The combined data to sync\r\n * @returns Upload result with version info\r\n */\r\n async uploadSyncData(syncData: any): Promise<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }> {\r\n const response = await this.getClient().post<ApiResponse<{\r\n id: string;\r\n version: number;\r\n updatedAt: string;\r\n }>>('/sync', { syncData });\r\n return this.extractData(response);\r\n }\r\n\r\n /**\r\n * Get sync data for the current user\r\n * @returns Sync data or null if not found\r\n */\r\n async getSyncData(): Promise<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n } | null> {\r\n try {\r\n const response = await this.getClient().get<ApiResponse<{\r\n id: string;\r\n syncData: any;\r\n version: number;\r\n createdAt: string;\r\n updatedAt: string;\r\n }>>('/sync');\r\n return this.extractData(response);\r\n } catch (error: any) {\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n throw error;\r\n }\r\n }\r\n\r\n /**\r\n * Delete sync data for the current user\r\n */\r\n async deleteSyncData(): Promise<void> {\r\n await this.getClient().delete('/sync');\r\n }\r\n\r\n // ==================== Token Counting Methods ====================\r\n\r\n /**\r\n * Count tokens for a given model and messages\r\n * Uses backend's accurate token counting (Vertex AI countTokens API)\r\n * @param model - Model name (e.g., gemini-2.5-flash)\r\n * @param messages - Array of conversation messages\r\n * @returns Total token count including system prompt\r\n */\r\n async countTokens(model: string, messages: any[]): Promise<number> {\r\n try {\r\n const response = await this.getClient().post<ApiResponse<{ tokenCount: number; model: string }>>(\r\n '/chat/token-count',\r\n { model, messages }\r\n );\r\n return response.data.data?.tokenCount || 0;\r\n } catch (error) {\r\n logError('Failed to count tokens via API', error as Error);\r\n\r\n // Fallback to character-based estimation\r\n const totalCharacters = messages.reduce((sum, msg) => {\r\n const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);\r\n return sum + content.length;\r\n }, 0);\r\n\r\n // Add system prompt estimate (roughly 14000 characters)\r\n // Use 1 token ≈ 4 characters for Gemini models\r\n return Math.ceil((totalCharacters + 14000) / 4);\r\n }\r\n }\r\n\r\n // ==================== Health Check ====================\r\n\r\n /**\r\n * Check backend service health\r\n * @returns Health status information\r\n */\r\n async healthCheck(): Promise<{\r\n status: string;\r\n timestamp: string;\r\n database: string;\r\n version: string;\r\n }> {\r\n // Health endpoint is at root level, not under /api\r\n // So we need to construct the full URL manually\r\n // Use build config for URL (frozen at compile time)\r\n const baseURL = IS_DEV_BUILD ? DEV_BACKEND_URL : PRODUCTION_BACKEND_URL;\r\n const healthURL = baseURL.replace('/v1', '/health');\r\n\r\n const response = await axios.get<ApiResponse<any>>(healthURL);\r\n return this.extractData(response);\r\n }\r\n}\r\n\r\n// Export singleton instance\r\nexport const apiClient = new ApiClient();\r\n\r\n// Export types for use in other modules\r\nexport type {\r\n GoogleAuthInitResponse,\r\n AuthResponse,\r\n UserProfile,\r\n CreateConversationRequest,\r\n Conversation,\r\n UpdateConversationRequest,\r\n CreateMessageRequest,\r\n Message,\r\n UserSettings,\r\n ApiResponse,\r\n};\r\n\r\n"],"mappings":"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","names":[]}
|
package/dist/utils/git-stats.js
CHANGED
|
@@ -66,11 +66,13 @@ function getGitDiffStats(cwd) {
|
|
|
66
66
|
return defaultResult;
|
|
67
67
|
}
|
|
68
68
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
69
|
+
let hasHead = false;
|
|
70
|
+
try {
|
|
71
|
+
execSync("git rev-parse --verify HEAD", { cwd, stdio: "ignore" });
|
|
72
|
+
hasHead = true;
|
|
73
|
+
} catch {
|
|
74
|
+
hasHead = false;
|
|
75
|
+
}
|
|
74
76
|
const trackedNumstatCommand = hasHead ? "git diff --numstat --no-ext-diff HEAD" : "git diff --numstat --no-ext-diff --cached";
|
|
75
77
|
const trackedOutput = execSync(trackedNumstatCommand, {
|
|
76
78
|
cwd,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/git-stats.ts"],"sourcesContent":["import { execSync } from 'child_process';\r\nimport * as fs from 'fs';\r\nimport * as path from 'path';\r\n\r\nexport interface GitDiffStats {\r\n isGitRepo: boolean;\r\n additions: number;\r\n deletions: number;\r\n filesChanged: number;\r\n}\r\n\r\nfunction escapeForDoubleQuotedShell(value: string): string {\r\n return value\r\n .replace(/\\\\/g, '\\\\\\\\')\r\n .replace(/\"/g, '\\\\\"')\r\n .replace(/\\$/g, '\\\\$')\r\n .replace(/`/g, '\\\\`');\r\n}\r\n\r\nfunction countLinesInText(content: string): number {\r\n if (!content) return 0;\r\n if (content.includes('\\u0000')) return 0;\r\n\r\n let newlineCount = 0;\r\n for (let i = 0; i < content.length; i += 1) {\r\n if (content.charCodeAt(i) === 10) {\r\n newlineCount += 1;\r\n }\r\n }\r\n\r\n return content.endsWith('\\n') ? newlineCount : newlineCount + 1;\r\n}\r\n\r\nfunction countLinesInBuffer(buffer: Buffer): number {\r\n if (buffer.length === 0) return 0;\r\n\r\n let newlineCount = 0;\r\n for (let i = 0; i < buffer.length; i += 1) {\r\n const byte = buffer[i];\r\n if (byte === 0) return 0; // Treat binary files as 0-line additions (matches git numstat behavior better).\r\n if (byte === 10) newlineCount += 1;\r\n }\r\n\r\n return buffer[buffer.length - 1] === 10 ? newlineCount : newlineCount + 1;\r\n}\r\n\r\nfunction parseNumstat(output: string): { additions: number; deletions: number; filesChanged: number } {\r\n let additions = 0;\r\n let deletions = 0;\r\n let filesChanged = 0;\r\n\r\n for (const rawLine of output.split('\\n')) {\r\n if (!rawLine.trim()) continue;\r\n\r\n // Format: \"<additions>\\t<deletions>\\t<path>\"\r\n const parts = rawLine.split('\\t');\r\n if (parts.length < 3) continue;\r\n\r\n const addPart = parts[0].trim();\r\n const delPart = parts[1].trim();\r\n\r\n const parsedAdds = addPart === '-' ? 0 : parseInt(addPart, 10);\r\n const parsedDels = delPart === '-' ? 0 : parseInt(delPart, 10);\r\n\r\n additions += Number.isFinite(parsedAdds) ? parsedAdds : 0;\r\n deletions += Number.isFinite(parsedDels) ? parsedDels : 0;\r\n filesChanged += 1;\r\n }\r\n\r\n return { additions, deletions, filesChanged };\r\n}\r\n\r\n/**\r\n * Check if a directory is inside a git repository\r\n */\r\nexport function isGitRepository(cwd: string): boolean {\r\n try {\r\n execSync('git rev-parse --git-dir', {\r\n cwd,\r\n encoding: 'utf-8',\r\n stdio: ['pipe', 'pipe', 'ignore'],\r\n });\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Get git diff statistics for a directory\r\n * Returns insertions and deletions compared to the working tree\r\n */\r\nexport function getGitDiffStats(cwd: string): GitDiffStats {\r\n const defaultResult: GitDiffStats = {\r\n isGitRepo: false,\r\n additions: 0,\r\n deletions: 0,\r\n filesChanged: 0,\r\n };\r\n\r\n // First check if it's a git repository\r\n if (!isGitRepository(cwd)) {\r\n return defaultResult;\r\n }\r\n\r\n try {\r\n const hasHead = execSync('git rev-parse --verify HEAD >/dev/null 2>&1 && echo 1 || echo 0', {\r\n cwd,\r\n encoding: 'utf-8',\r\n stdio: ['pipe', 'pipe', 'ignore'],\r\n }).trim() === '1';\r\n\r\n // Track both staged and unstaged net changes against HEAD when available.\r\n const trackedNumstatCommand = hasHead\r\n ? 'git diff --numstat --no-ext-diff HEAD'\r\n : 'git diff --numstat --no-ext-diff --cached';\r\n const trackedOutput = execSync(trackedNumstatCommand, {\r\n cwd,\r\n encoding: 'utf-8',\r\n stdio: ['pipe', 'pipe', 'ignore'],\r\n }).trim();\r\n const trackedStats = parseNumstat(trackedOutput);\r\n\r\n // Include untracked files (critical for brand-new repos and newly created files).\r\n const untrackedOutput = execSync('git ls-files --others --exclude-standard -z', {\r\n cwd,\r\n encoding: 'utf-8',\r\n stdio: ['pipe', 'pipe', 'ignore'],\r\n });\r\n const untrackedFiles = untrackedOutput\r\n ? untrackedOutput.split('\\u0000').map(file => file.trim()).filter(Boolean)\r\n : [];\r\n\r\n let untrackedAdditions = 0;\r\n for (const relPath of untrackedFiles) {\r\n const absolutePath = path.join(cwd, relPath);\r\n try {\r\n const stat = fs.statSync(absolutePath);\r\n if (!stat.isFile()) continue;\r\n\r\n const buffer = fs.readFileSync(absolutePath);\r\n untrackedAdditions += countLinesInBuffer(buffer);\r\n } catch {\r\n // Skip unreadable/unavailable files.\r\n }\r\n }\r\n\r\n return {\r\n isGitRepo: true,\r\n additions: trackedStats.additions + untrackedAdditions,\r\n deletions: trackedStats.deletions,\r\n filesChanged: trackedStats.filesChanged + untrackedFiles.length,\r\n };\r\n } catch (error) {\r\n // Git command failed, return default\r\n return {\r\n isGitRepo: true,\r\n additions: 0,\r\n deletions: 0,\r\n filesChanged: 0,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Get git diff stats for a remote directory via SSH/WSL/Docker handler\r\n */\r\nexport async function getRemoteGitDiffStats(\r\n handler: {\r\n executeCommand: (cmd: string) => Promise<{ stdout: string; stderr?: string; exitCode: number }>;\r\n readFile?: (path: string) => Promise<string>;\r\n },\r\n cwd: string\r\n): Promise<GitDiffStats> {\r\n const defaultResult: GitDiffStats = {\r\n isGitRepo: false,\r\n additions: 0,\r\n deletions: 0,\r\n filesChanged: 0,\r\n };\r\n\r\n try {\r\n const escapedCwd = escapeForDoubleQuotedShell(cwd);\r\n\r\n // Check if it's a git repository (do not trust exitCode alone for all handlers).\r\n const checkResult = await handler.executeCommand(`git -C \"${escapedCwd}\" rev-parse --is-inside-work-tree 2>/dev/null`);\r\n if (checkResult.stdout.trim() !== 'true') {\r\n return defaultResult;\r\n }\r\n\r\n const hasHeadResult = await handler.executeCommand(`git -C \"${escapedCwd}\" rev-parse --verify HEAD >/dev/null 2>/dev/null && echo 1 || echo 0`);\r\n const hasHead = hasHeadResult.stdout.trim() === '1';\r\n\r\n const trackedDiffCommand = hasHead\r\n ? `git -C \"${escapedCwd}\" diff --numstat --no-ext-diff HEAD 2>/dev/null`\r\n : `git -C \"${escapedCwd}\" diff --numstat --no-ext-diff --cached 2>/dev/null`;\r\n const trackedResult = await handler.executeCommand(trackedDiffCommand);\r\n const trackedStats = parseNumstat(trackedResult.stdout.trim());\r\n\r\n const untrackedListResult = await handler.executeCommand(`git -C \"${escapedCwd}\" ls-files --others --exclude-standard -z 2>/dev/null`);\r\n const untrackedFiles = untrackedListResult.stdout\r\n .split('\\u0000')\r\n .map(line => line.trim())\r\n .filter(Boolean);\r\n\r\n let untrackedAdditions = 0;\r\n for (const relPath of untrackedFiles) {\r\n const remotePath = `${cwd.replace(/\\/$/, '')}/${relPath}`;\r\n\r\n try {\r\n if (typeof handler.readFile === 'function') {\r\n const content = await handler.readFile(remotePath);\r\n untrackedAdditions += countLinesInText(content);\r\n continue;\r\n }\r\n } catch {\r\n // Fall back to wc below.\r\n }\r\n\r\n try {\r\n const escapedPath = escapeForDoubleQuotedShell(remotePath);\r\n const lineCountResult = await handler.executeCommand(`awk 'END { print NR }' \"${escapedPath}\" 2>/dev/null || echo 0`);\r\n const lineCount = parseInt(lineCountResult.stdout.trim(), 10);\r\n if (Number.isFinite(lineCount)) {\r\n untrackedAdditions += lineCount;\r\n }\r\n } catch {\r\n // Ignore unreadable files.\r\n }\r\n }\r\n\r\n return {\r\n isGitRepo: true,\r\n additions: trackedStats.additions + untrackedAdditions,\r\n deletions: trackedStats.deletions,\r\n filesChanged: trackedStats.filesChanged + untrackedFiles.length,\r\n };\r\n } catch (error) {\r\n return defaultResult;\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/git-stats.ts"],"sourcesContent":["import { execSync } from 'child_process';\r\nimport * as fs from 'fs';\r\nimport * as path from 'path';\r\n\r\nexport interface GitDiffStats {\r\n isGitRepo: boolean;\r\n additions: number;\r\n deletions: number;\r\n filesChanged: number;\r\n}\r\n\r\nfunction escapeForDoubleQuotedShell(value: string): string {\r\n return value\r\n .replace(/\\\\/g, '\\\\\\\\')\r\n .replace(/\"/g, '\\\\\"')\r\n .replace(/\\$/g, '\\\\$')\r\n .replace(/`/g, '\\\\`');\r\n}\r\n\r\nfunction countLinesInText(content: string): number {\r\n if (!content) return 0;\r\n if (content.includes('\\u0000')) return 0;\r\n\r\n let newlineCount = 0;\r\n for (let i = 0; i < content.length; i += 1) {\r\n if (content.charCodeAt(i) === 10) {\r\n newlineCount += 1;\r\n }\r\n }\r\n\r\n return content.endsWith('\\n') ? newlineCount : newlineCount + 1;\r\n}\r\n\r\nfunction countLinesInBuffer(buffer: Buffer): number {\r\n if (buffer.length === 0) return 0;\r\n\r\n let newlineCount = 0;\r\n for (let i = 0; i < buffer.length; i += 1) {\r\n const byte = buffer[i];\r\n if (byte === 0) return 0; // Treat binary files as 0-line additions (matches git numstat behavior better).\r\n if (byte === 10) newlineCount += 1;\r\n }\r\n\r\n return buffer[buffer.length - 1] === 10 ? newlineCount : newlineCount + 1;\r\n}\r\n\r\nfunction parseNumstat(output: string): { additions: number; deletions: number; filesChanged: number } {\r\n let additions = 0;\r\n let deletions = 0;\r\n let filesChanged = 0;\r\n\r\n for (const rawLine of output.split('\\n')) {\r\n if (!rawLine.trim()) continue;\r\n\r\n // Format: \"<additions>\\t<deletions>\\t<path>\"\r\n const parts = rawLine.split('\\t');\r\n if (parts.length < 3) continue;\r\n\r\n const addPart = parts[0].trim();\r\n const delPart = parts[1].trim();\r\n\r\n const parsedAdds = addPart === '-' ? 0 : parseInt(addPart, 10);\r\n const parsedDels = delPart === '-' ? 0 : parseInt(delPart, 10);\r\n\r\n additions += Number.isFinite(parsedAdds) ? parsedAdds : 0;\r\n deletions += Number.isFinite(parsedDels) ? parsedDels : 0;\r\n filesChanged += 1;\r\n }\r\n\r\n return { additions, deletions, filesChanged };\r\n}\r\n\r\n/**\r\n * Check if a directory is inside a git repository\r\n */\r\nexport function isGitRepository(cwd: string): boolean {\r\n try {\r\n execSync('git rev-parse --git-dir', {\r\n cwd,\r\n encoding: 'utf-8',\r\n stdio: ['pipe', 'pipe', 'ignore'],\r\n });\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Get git diff statistics for a directory\r\n * Returns insertions and deletions compared to the working tree\r\n */\r\nexport function getGitDiffStats(cwd: string): GitDiffStats {\r\n const defaultResult: GitDiffStats = {\r\n isGitRepo: false,\r\n additions: 0,\r\n deletions: 0,\r\n filesChanged: 0,\r\n };\r\n\r\n // First check if it's a git repository\r\n if (!isGitRepository(cwd)) {\r\n return defaultResult;\r\n }\r\n\r\n try {\r\n // Determine whether HEAD exists using a try/catch instead of shell redirections\r\n // so the check works on Windows (cmd.exe doesn't understand >/dev/null).\r\n let hasHead = false;\r\n try {\r\n execSync('git rev-parse --verify HEAD', { cwd, stdio: 'ignore' });\r\n hasHead = true;\r\n } catch {\r\n hasHead = false;\r\n }\r\n\r\n // Track both staged and unstaged net changes against HEAD when available.\r\n const trackedNumstatCommand = hasHead\r\n ? 'git diff --numstat --no-ext-diff HEAD'\r\n : 'git diff --numstat --no-ext-diff --cached';\r\n const trackedOutput = execSync(trackedNumstatCommand, {\r\n cwd,\r\n encoding: 'utf-8',\r\n stdio: ['pipe', 'pipe', 'ignore'],\r\n }).trim();\r\n const trackedStats = parseNumstat(trackedOutput);\r\n\r\n // Include untracked files (critical for brand-new repos and newly created files).\r\n const untrackedOutput = execSync('git ls-files --others --exclude-standard -z', {\r\n cwd,\r\n encoding: 'utf-8',\r\n stdio: ['pipe', 'pipe', 'ignore'],\r\n });\r\n const untrackedFiles = untrackedOutput\r\n ? untrackedOutput.split('\\u0000').map(file => file.trim()).filter(Boolean)\r\n : [];\r\n\r\n let untrackedAdditions = 0;\r\n for (const relPath of untrackedFiles) {\r\n const absolutePath = path.join(cwd, relPath);\r\n try {\r\n const stat = fs.statSync(absolutePath);\r\n if (!stat.isFile()) continue;\r\n\r\n const buffer = fs.readFileSync(absolutePath);\r\n untrackedAdditions += countLinesInBuffer(buffer);\r\n } catch {\r\n // Skip unreadable/unavailable files.\r\n }\r\n }\r\n\r\n return {\r\n isGitRepo: true,\r\n additions: trackedStats.additions + untrackedAdditions,\r\n deletions: trackedStats.deletions,\r\n filesChanged: trackedStats.filesChanged + untrackedFiles.length,\r\n };\r\n } catch (error) {\r\n // Git command failed, return default\r\n return {\r\n isGitRepo: true,\r\n additions: 0,\r\n deletions: 0,\r\n filesChanged: 0,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Get git diff stats for a remote directory via SSH/WSL/Docker handler\r\n */\r\nexport async function getRemoteGitDiffStats(\r\n handler: {\r\n executeCommand: (cmd: string) => Promise<{ stdout: string; stderr?: string; exitCode: number }>;\r\n readFile?: (path: string) => Promise<string>;\r\n },\r\n cwd: string\r\n): Promise<GitDiffStats> {\r\n const defaultResult: GitDiffStats = {\r\n isGitRepo: false,\r\n additions: 0,\r\n deletions: 0,\r\n filesChanged: 0,\r\n };\r\n\r\n try {\r\n const escapedCwd = escapeForDoubleQuotedShell(cwd);\r\n\r\n // Check if it's a git repository (do not trust exitCode alone for all handlers).\r\n const checkResult = await handler.executeCommand(`git -C \"${escapedCwd}\" rev-parse --is-inside-work-tree 2>/dev/null`);\r\n if (checkResult.stdout.trim() !== 'true') {\r\n return defaultResult;\r\n }\r\n\r\n const hasHeadResult = await handler.executeCommand(`git -C \"${escapedCwd}\" rev-parse --verify HEAD >/dev/null 2>/dev/null && echo 1 || echo 0`);\r\n const hasHead = hasHeadResult.stdout.trim() === '1';\r\n\r\n const trackedDiffCommand = hasHead\r\n ? `git -C \"${escapedCwd}\" diff --numstat --no-ext-diff HEAD 2>/dev/null`\r\n : `git -C \"${escapedCwd}\" diff --numstat --no-ext-diff --cached 2>/dev/null`;\r\n const trackedResult = await handler.executeCommand(trackedDiffCommand);\r\n const trackedStats = parseNumstat(trackedResult.stdout.trim());\r\n\r\n const untrackedListResult = await handler.executeCommand(`git -C \"${escapedCwd}\" ls-files --others --exclude-standard -z 2>/dev/null`);\r\n const untrackedFiles = untrackedListResult.stdout\r\n .split('\\u0000')\r\n .map(line => line.trim())\r\n .filter(Boolean);\r\n\r\n let untrackedAdditions = 0;\r\n for (const relPath of untrackedFiles) {\r\n const remotePath = `${cwd.replace(/\\/$/, '')}/${relPath}`;\r\n\r\n try {\r\n if (typeof handler.readFile === 'function') {\r\n const content = await handler.readFile(remotePath);\r\n untrackedAdditions += countLinesInText(content);\r\n continue;\r\n }\r\n } catch {\r\n // Fall back to wc below.\r\n }\r\n\r\n try {\r\n const escapedPath = escapeForDoubleQuotedShell(remotePath);\r\n const lineCountResult = await handler.executeCommand(`awk 'END { print NR }' \"${escapedPath}\" 2>/dev/null || echo 0`);\r\n const lineCount = parseInt(lineCountResult.stdout.trim(), 10);\r\n if (Number.isFinite(lineCount)) {\r\n untrackedAdditions += lineCount;\r\n }\r\n } catch {\r\n // Ignore unreadable files.\r\n }\r\n }\r\n\r\n return {\r\n isGitRepo: true,\r\n additions: trackedStats.additions + untrackedAdditions,\r\n deletions: trackedStats.deletions,\r\n filesChanged: trackedStats.filesChanged + untrackedFiles.length,\r\n };\r\n } catch (error) {\r\n return defaultResult;\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|