centaurus-cli 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -1
- package/dist/cli-adapter.d.ts +41 -2
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +407 -79
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/types.d.ts +23 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +20 -0
- package/dist/config/types.js.map +1 -1
- package/dist/context/__tests__/command-detector.test.d.ts +14 -0
- package/dist/context/__tests__/command-detector.test.d.ts.map +1 -0
- package/dist/context/__tests__/command-detector.test.js +318 -0
- package/dist/context/__tests__/command-detector.test.js.map +1 -0
- package/dist/context/__tests__/context-manager.test.d.ts +16 -0
- package/dist/context/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/context/__tests__/context-manager.test.js +375 -0
- package/dist/context/__tests__/context-manager.test.js.map +1 -0
- package/dist/context/__tests__/error-handling.test.d.ts +15 -0
- package/dist/context/__tests__/error-handling.test.d.ts.map +1 -0
- package/dist/context/__tests__/error-handling.test.js +447 -0
- package/dist/context/__tests__/error-handling.test.js.map +1 -0
- package/dist/context/command-detector.d.ts +50 -0
- package/dist/context/command-detector.d.ts.map +1 -0
- package/dist/context/command-detector.js +72 -0
- package/dist/context/command-detector.js.map +1 -0
- package/dist/context/context-manager.d.ts +144 -0
- package/dist/context/context-manager.d.ts.map +1 -0
- package/dist/context/context-manager.js +487 -0
- package/dist/context/context-manager.js.map +1 -0
- package/dist/context/handlers/__tests__/docker-handler.test.d.ts +13 -0
- package/dist/context/handlers/__tests__/docker-handler.test.d.ts.map +1 -0
- package/dist/context/handlers/__tests__/docker-handler.test.js +285 -0
- package/dist/context/handlers/__tests__/docker-handler.test.js.map +1 -0
- package/dist/context/handlers/__tests__/ssh-handler.test.d.ts +13 -0
- package/dist/context/handlers/__tests__/ssh-handler.test.d.ts.map +1 -0
- package/dist/context/handlers/__tests__/ssh-handler.test.js +251 -0
- package/dist/context/handlers/__tests__/ssh-handler.test.js.map +1 -0
- package/dist/context/handlers/__tests__/wsl-handler.test.d.ts +7 -0
- package/dist/context/handlers/__tests__/wsl-handler.test.d.ts.map +1 -0
- package/dist/context/handlers/__tests__/wsl-handler.test.js +331 -0
- package/dist/context/handlers/__tests__/wsl-handler.test.js.map +1 -0
- package/dist/context/handlers/docker-handler.d.ts +111 -0
- package/dist/context/handlers/docker-handler.d.ts.map +1 -0
- package/dist/context/handlers/docker-handler.js +439 -0
- package/dist/context/handlers/docker-handler.js.map +1 -0
- package/dist/context/handlers/ssh-handler.d.ts +120 -0
- package/dist/context/handlers/ssh-handler.d.ts.map +1 -0
- package/dist/context/handlers/ssh-handler.js +523 -0
- package/dist/context/handlers/ssh-handler.js.map +1 -0
- package/dist/context/handlers/wsl-handler.d.ts +128 -0
- package/dist/context/handlers/wsl-handler.d.ts.map +1 -0
- package/dist/context/handlers/wsl-handler.js +590 -0
- package/dist/context/handlers/wsl-handler.js.map +1 -0
- package/dist/context/index.d.ts +8 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +7 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/subshell-handler.d.ts +130 -0
- package/dist/context/subshell-handler.d.ts.map +1 -0
- package/dist/context/subshell-handler.js +5 -0
- package/dist/context/subshell-handler.js.map +1 -0
- package/dist/context/types.d.ts +70 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/context/types.js +34 -0
- package/dist/context/types.js.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/services/__tests__/ai-context-injector.test.d.ts +15 -0
- package/dist/services/__tests__/ai-context-injector.test.d.ts.map +1 -0
- package/dist/services/__tests__/ai-context-injector.test.js +326 -0
- package/dist/services/__tests__/ai-context-injector.test.js.map +1 -0
- package/dist/services/ai-context-injector.d.ts +41 -0
- package/dist/services/ai-context-injector.d.ts.map +1 -0
- package/dist/services/ai-context-injector.js +97 -0
- package/dist/services/ai-context-injector.js.map +1 -0
- package/dist/services/ai-service-client.d.ts +4 -1
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +6 -2
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.js +1 -1
- package/dist/services/api-client.js.map +1 -1
- package/dist/src/context/types.js +27 -0
- package/dist/src/services/ai-context-injector.js +96 -0
- package/dist/src/services/ai-service-client.js +270 -0
- package/dist/src/services/api-client.js +349 -0
- package/dist/src/tools/types.js +1 -0
- package/dist/src/types/index.js +1 -0
- package/dist/test/context/types.js +27 -0
- package/dist/test/services/__tests__/ai-context-injector.test.js +325 -0
- package/dist/test/services/ai-context-injector.js +96 -0
- package/dist/test/services/ai-service-client.js +270 -0
- package/dist/test/services/api-client.js +349 -0
- package/dist/test/tools/types.js +1 -0
- package/dist/test/types/index.js +1 -0
- package/dist/test-ai-context-injector.js +97 -0
- package/dist/test-ssh-handler.d.ts +8 -0
- package/dist/test-ssh-handler.d.ts.map +1 -0
- package/dist/test-ssh-handler.js +198 -0
- package/dist/test-ssh-handler.js.map +1 -0
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +123 -46
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +115 -48
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/types.d.ts +1 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/web-search.js +2 -2
- package/dist/tools/web-search.js.map +1 -1
- package/dist/types/index.d.ts +41 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/components/App.d.ts +3 -0
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +213 -46
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/Breadcrumbs.d.ts +12 -0
- package/dist/ui/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/ui/components/Breadcrumbs.js +62 -0
- package/dist/ui/components/Breadcrumbs.js.map +1 -0
- package/dist/ui/components/CodeBlock.js +1 -1
- package/dist/ui/components/CodeBlock.js.map +1 -1
- package/dist/ui/components/DiffViewer.js +1 -1
- package/dist/ui/components/DiffViewer.js.map +1 -1
- package/dist/ui/components/FileViewerScreen.d.ts +14 -0
- package/dist/ui/components/FileViewerScreen.d.ts.map +1 -0
- package/dist/ui/components/FileViewerScreen.js +74 -0
- package/dist/ui/components/FileViewerScreen.js.map +1 -0
- package/dist/ui/components/InputBox.d.ts +2 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +85 -41
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +3 -28
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/PasswordPrompt.d.ts +9 -0
- package/dist/ui/components/PasswordPrompt.d.ts.map +1 -0
- package/dist/ui/components/PasswordPrompt.js +20 -0
- package/dist/ui/components/PasswordPrompt.js.map +1 -0
- package/dist/ui/components/StatusBar.d.ts +2 -0
- package/dist/ui/components/StatusBar.d.ts.map +1 -1
- package/dist/ui/components/StatusBar.js +36 -1
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +13 -24
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.d.ts +10 -0
- package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -0
- package/dist/ui/components/VersionUpdatePrompt.js +41 -0
- package/dist/ui/components/VersionUpdatePrompt.js.map +1 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +38 -10
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/version-checker.d.ts +14 -0
- package/dist/utils/version-checker.d.ts.map +1 -0
- package/dist/utils/version-checker.js +63 -0
- package/dist/utils/version-checker.js.map +1 -0
- package/package.json +71 -69
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Service Client
|
|
3
|
+
*
|
|
4
|
+
* Handles communication with the backend AI proxy service for streaming
|
|
5
|
+
* AI chat requests. Replaces direct Gemini SDK usage in the CLI.
|
|
6
|
+
*/
|
|
7
|
+
import { apiClient } from './api-client.js';
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
/**
|
|
12
|
+
* AI Service Client for streaming chat requests to backend
|
|
13
|
+
*/
|
|
14
|
+
export class AIServiceClient {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.maxRetries = 3;
|
|
17
|
+
this.retryDelay = 1000; // Start with 1 second
|
|
18
|
+
// Don't set baseURL yet - lazy load it when first used
|
|
19
|
+
// This allows environment variables to be loaded first
|
|
20
|
+
this.baseURL = '';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get the base URL for API requests
|
|
24
|
+
* Lazy-loaded to ensure environment variables are loaded first
|
|
25
|
+
*/
|
|
26
|
+
getBaseURL() {
|
|
27
|
+
if (!this.baseURL) {
|
|
28
|
+
// Use production URL by default, only use localhost in development mode
|
|
29
|
+
this.baseURL = process.env.DEV_MODE === 'true'
|
|
30
|
+
? 'http://localhost:3002/api'
|
|
31
|
+
: (process.env.BACKEND_URL || 'https://centaurus-backend-354715948975.asia-south1.run.app/api');
|
|
32
|
+
}
|
|
33
|
+
return this.baseURL;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Stream chat request to backend AI proxy
|
|
37
|
+
*
|
|
38
|
+
* @param model - The AI model to use (e.g., 'gemini-2.5-flash')
|
|
39
|
+
* @param messages - Conversation history including system, user, assistant, and tool messages
|
|
40
|
+
* @param tools - Available tool schemas for the AI to use
|
|
41
|
+
* @param environmentContext - Optional environment context (OS, shell, cwd, etc.)
|
|
42
|
+
* @param mode - Optional mode (default, plan, command)
|
|
43
|
+
* @yields Stream chunks (text, tool calls, done, or error events)
|
|
44
|
+
*/
|
|
45
|
+
async *streamChat(model, messages, tools, environmentContext, mode) {
|
|
46
|
+
// Build request payload
|
|
47
|
+
const payload = {
|
|
48
|
+
model,
|
|
49
|
+
messages,
|
|
50
|
+
tools,
|
|
51
|
+
stream: true,
|
|
52
|
+
environmentContext,
|
|
53
|
+
mode,
|
|
54
|
+
};
|
|
55
|
+
// Get authentication token from api client
|
|
56
|
+
if (!apiClient.isAuthenticated()) {
|
|
57
|
+
yield {
|
|
58
|
+
type: 'error',
|
|
59
|
+
message: 'Authentication required. Please sign in.',
|
|
60
|
+
code: 'AUTH_REQUIRED',
|
|
61
|
+
};
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Retry logic for transient errors
|
|
65
|
+
let lastError = null;
|
|
66
|
+
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
// Make fetch request to backend AI endpoint
|
|
69
|
+
const response = await fetch(`${this.getBaseURL()}/ai/chat`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Authorization': `Bearer ${this.getSessionToken()}`,
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(payload),
|
|
76
|
+
signal: AbortSignal.timeout(60000), // 60 second timeout
|
|
77
|
+
});
|
|
78
|
+
// Check for HTTP errors
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const errorText = await response.text();
|
|
81
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
82
|
+
let errorCode = 'HTTP_ERROR';
|
|
83
|
+
try {
|
|
84
|
+
const errorData = JSON.parse(errorText);
|
|
85
|
+
if (errorData.error) {
|
|
86
|
+
errorMessage = errorData.error.message || errorMessage;
|
|
87
|
+
errorCode = errorData.error.code || errorCode;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// If response is not JSON, use the text as message
|
|
92
|
+
if (errorText) {
|
|
93
|
+
errorMessage = errorText;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Handle specific error codes
|
|
97
|
+
if (response.status === 401) {
|
|
98
|
+
yield {
|
|
99
|
+
type: 'error',
|
|
100
|
+
message: 'Session expired. Please sign in again.',
|
|
101
|
+
code: 'AUTH_REQUIRED',
|
|
102
|
+
};
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (response.status === 429) {
|
|
106
|
+
// Rate limit - don't retry immediately
|
|
107
|
+
yield {
|
|
108
|
+
type: 'error',
|
|
109
|
+
message: 'Service temporarily unavailable due to high demand. Please try again in a moment.',
|
|
110
|
+
code: 'RATE_LIMIT',
|
|
111
|
+
};
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (response.status === 504) {
|
|
115
|
+
// Timeout error - retryable
|
|
116
|
+
lastError = {
|
|
117
|
+
type: 'error',
|
|
118
|
+
message: 'Request timed out. Retrying...',
|
|
119
|
+
code: 'TIMEOUT',
|
|
120
|
+
};
|
|
121
|
+
if (attempt < this.maxRetries - 1) {
|
|
122
|
+
await this.sleep(this.retryDelay * Math.pow(2, attempt));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// For other errors, yield and return
|
|
127
|
+
yield {
|
|
128
|
+
type: 'error',
|
|
129
|
+
message: errorMessage,
|
|
130
|
+
code: errorCode,
|
|
131
|
+
};
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Check if response body exists
|
|
135
|
+
if (!response.body) {
|
|
136
|
+
yield {
|
|
137
|
+
type: 'error',
|
|
138
|
+
message: 'No response body from backend',
|
|
139
|
+
code: 'NO_RESPONSE_BODY',
|
|
140
|
+
};
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Parse SSE stream - if successful, we're done
|
|
144
|
+
yield* this.parseSSEStream(response.body);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
// Handle network errors
|
|
149
|
+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
150
|
+
lastError = {
|
|
151
|
+
type: 'error',
|
|
152
|
+
message: 'Backend service is unreachable. Please check your connection.',
|
|
153
|
+
code: 'NETWORK_ERROR',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
else if (error.name === 'AbortError' || error.name === 'TimeoutError') {
|
|
157
|
+
lastError = {
|
|
158
|
+
type: 'error',
|
|
159
|
+
message: 'Request timed out. Please try again.',
|
|
160
|
+
code: 'TIMEOUT',
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
lastError = {
|
|
165
|
+
type: 'error',
|
|
166
|
+
message: error.message || 'Unknown error occurred',
|
|
167
|
+
code: error.code || 'UNKNOWN_ERROR',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
// Retry for transient errors
|
|
171
|
+
if (this.isRetryableError(lastError.code) && attempt < this.maxRetries - 1) {
|
|
172
|
+
await this.sleep(this.retryDelay * Math.pow(2, attempt));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// If not retryable or max retries reached, yield error and return
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// If we get here, we've exhausted retries
|
|
180
|
+
if (lastError) {
|
|
181
|
+
yield lastError;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Check if an error code is retryable
|
|
186
|
+
*/
|
|
187
|
+
isRetryableError(code) {
|
|
188
|
+
const retryableCodes = ['NETWORK_ERROR', 'TIMEOUT', 'UNKNOWN_ERROR'];
|
|
189
|
+
return retryableCodes.includes(code);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Sleep for specified milliseconds
|
|
193
|
+
*/
|
|
194
|
+
sleep(ms) {
|
|
195
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get session token from apiClient
|
|
199
|
+
* This is a workaround since sessionToken is private
|
|
200
|
+
*/
|
|
201
|
+
getSessionToken() {
|
|
202
|
+
// Read session token from the same location apiClient uses
|
|
203
|
+
const configPath = join(homedir(), '.centaurus', 'session.json');
|
|
204
|
+
try {
|
|
205
|
+
if (existsSync(configPath)) {
|
|
206
|
+
const data = readFileSync(configPath, 'utf-8');
|
|
207
|
+
const session = JSON.parse(data);
|
|
208
|
+
return session.sessionToken || '';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
// Return empty string if unable to read
|
|
213
|
+
}
|
|
214
|
+
return '';
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Parse Server-Sent Events stream from response body
|
|
218
|
+
*
|
|
219
|
+
* @param body - ReadableStream from fetch response
|
|
220
|
+
* @yields Parsed stream chunks
|
|
221
|
+
*/
|
|
222
|
+
async *parseSSEStream(body) {
|
|
223
|
+
const reader = body.getReader();
|
|
224
|
+
const decoder = new TextDecoder();
|
|
225
|
+
let buffer = '';
|
|
226
|
+
try {
|
|
227
|
+
while (true) {
|
|
228
|
+
const { done, value } = await reader.read();
|
|
229
|
+
if (done) {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
// Decode chunk and add to buffer
|
|
233
|
+
buffer += decoder.decode(value, { stream: true });
|
|
234
|
+
// Process complete lines in buffer
|
|
235
|
+
const lines = buffer.split('\n');
|
|
236
|
+
// Keep the last incomplete line in buffer
|
|
237
|
+
buffer = lines.pop() || '';
|
|
238
|
+
for (const line of lines) {
|
|
239
|
+
// SSE format: "data: {json}"
|
|
240
|
+
if (line.startsWith('data: ')) {
|
|
241
|
+
const dataStr = line.slice(6); // Remove "data: " prefix
|
|
242
|
+
// Skip empty data lines
|
|
243
|
+
if (!dataStr.trim()) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const chunk = JSON.parse(dataStr);
|
|
248
|
+
yield chunk;
|
|
249
|
+
// Stop if we receive a done or error event
|
|
250
|
+
if (chunk.type === 'done' || chunk.type === 'error') {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
// Skip malformed JSON
|
|
256
|
+
console.error('Failed to parse SSE data:', dataStr);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// SSE event type line: "event: chunk"
|
|
260
|
+
// We don't need to process these separately since the data contains the type
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
reader.releaseLock();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Export singleton instance
|
|
270
|
+
export const aiServiceClient = new AIServiceClient();
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client Service for Centaurus CLI
|
|
3
|
+
*
|
|
4
|
+
* Handles all communication with the backend REST API including:
|
|
5
|
+
* - Authentication and session management
|
|
6
|
+
* - Conversation and message operations
|
|
7
|
+
* - User settings management
|
|
8
|
+
* - API key storage and retrieval
|
|
9
|
+
*/
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
/**
|
|
15
|
+
* API Client class for communicating with the backend service
|
|
16
|
+
*/
|
|
17
|
+
class ApiClient {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.client = null;
|
|
20
|
+
this.sessionToken = null;
|
|
21
|
+
// Set up session storage path: ~/.centaurus/session.json
|
|
22
|
+
this.configDir = join(homedir(), '.centaurus');
|
|
23
|
+
this.configPath = join(this.configDir, 'session.json');
|
|
24
|
+
// Load existing session if available
|
|
25
|
+
this.loadSession();
|
|
26
|
+
// Don't create axios client yet - wait until first use
|
|
27
|
+
// This allows environment variables to be loaded first
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get or create the axios client instance
|
|
31
|
+
* This is lazy-loaded to ensure environment variables are loaded first
|
|
32
|
+
*/
|
|
33
|
+
getClient() {
|
|
34
|
+
if (!this.client) {
|
|
35
|
+
// Create axios instance with base configuration
|
|
36
|
+
// Use production URL by default, only use localhost in development mode
|
|
37
|
+
const baseURL = process.env.DEV_MODE === 'true'
|
|
38
|
+
? 'http://localhost:3002/api'
|
|
39
|
+
: (process.env.BACKEND_URL || 'https://centaurus-backend-354715948975.asia-south1.run.app/api');
|
|
40
|
+
this.client = axios.create({
|
|
41
|
+
baseURL,
|
|
42
|
+
timeout: 30000,
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
// Request interceptor: Add Authorization header if session token exists
|
|
48
|
+
this.getClient().interceptors.request.use((config) => {
|
|
49
|
+
if (this.sessionToken) {
|
|
50
|
+
config.headers.Authorization = `Bearer ${this.sessionToken}`;
|
|
51
|
+
}
|
|
52
|
+
return config;
|
|
53
|
+
}, (error) => {
|
|
54
|
+
return Promise.reject(error);
|
|
55
|
+
});
|
|
56
|
+
// Response interceptor: Handle 401 errors (expired/invalid session)
|
|
57
|
+
this.getClient().interceptors.response.use((response) => response, async (error) => {
|
|
58
|
+
if (error.response?.status === 401) {
|
|
59
|
+
// Clear invalid session
|
|
60
|
+
this.clearSession();
|
|
61
|
+
// Create a more user-friendly error
|
|
62
|
+
const authError = new Error('Session expired. Please sign in again.');
|
|
63
|
+
authError.name = 'AuthenticationError';
|
|
64
|
+
throw authError;
|
|
65
|
+
}
|
|
66
|
+
// For other errors, extract message from API response if available
|
|
67
|
+
if (error.response?.data) {
|
|
68
|
+
const apiError = error.response.data;
|
|
69
|
+
if (apiError.error) {
|
|
70
|
+
const customError = new Error(apiError.error.message);
|
|
71
|
+
customError.name = apiError.error.code;
|
|
72
|
+
throw customError;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return this.client;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Load session token from local config file
|
|
82
|
+
*/
|
|
83
|
+
loadSession() {
|
|
84
|
+
try {
|
|
85
|
+
if (existsSync(this.configPath)) {
|
|
86
|
+
const data = readFileSync(this.configPath, 'utf-8');
|
|
87
|
+
const session = JSON.parse(data);
|
|
88
|
+
this.sessionToken = session.sessionToken || null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// If there's any error reading the session, just start fresh
|
|
93
|
+
this.sessionToken = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Save session token to local config file
|
|
98
|
+
*/
|
|
99
|
+
saveSession(token, expiresAt) {
|
|
100
|
+
try {
|
|
101
|
+
// Ensure config directory exists
|
|
102
|
+
if (!existsSync(this.configDir)) {
|
|
103
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
// Save session data
|
|
106
|
+
const sessionData = {
|
|
107
|
+
sessionToken: token,
|
|
108
|
+
expiresAt: expiresAt || null,
|
|
109
|
+
savedAt: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
writeFileSync(this.configPath, JSON.stringify(sessionData, null, 2), 'utf-8');
|
|
112
|
+
this.sessionToken = token;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Failed to save session:', error);
|
|
116
|
+
throw new Error('Failed to save session locally');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Clear session token from memory and local storage
|
|
121
|
+
*/
|
|
122
|
+
clearSession() {
|
|
123
|
+
this.sessionToken = null;
|
|
124
|
+
try {
|
|
125
|
+
if (existsSync(this.configPath)) {
|
|
126
|
+
unlinkSync(this.configPath);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
// Ignore errors when clearing session
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Check if user is authenticated
|
|
135
|
+
*/
|
|
136
|
+
isAuthenticated() {
|
|
137
|
+
return this.sessionToken !== null;
|
|
138
|
+
}
|
|
139
|
+
// ==================== Authentication Methods ====================
|
|
140
|
+
/**
|
|
141
|
+
* Initialize Google OAuth flow
|
|
142
|
+
* @param redirectUri - The URI to redirect to after OAuth
|
|
143
|
+
* @returns OAuth URL and state parameter
|
|
144
|
+
*/
|
|
145
|
+
async initGoogleAuth(redirectUri) {
|
|
146
|
+
const response = await this.getClient().post('/auth/google/init', { redirectUri });
|
|
147
|
+
return response.data.data;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Complete Google OAuth authentication
|
|
151
|
+
* @param code - Authorization code from Google
|
|
152
|
+
* @param state - State parameter for CSRF protection
|
|
153
|
+
* @returns Session token and user information
|
|
154
|
+
*/
|
|
155
|
+
async authenticate(code, state) {
|
|
156
|
+
const response = await this.getClient().post('/auth/google/callback', { code, state });
|
|
157
|
+
const authData = response.data.data;
|
|
158
|
+
this.saveSession(authData.sessionToken, authData.expiresAt);
|
|
159
|
+
return authData;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Set session token directly (used when receiving token from web app)
|
|
163
|
+
* @param sessionToken - The session token to save
|
|
164
|
+
* @param user - User information
|
|
165
|
+
*/
|
|
166
|
+
setSessionToken(sessionToken, user) {
|
|
167
|
+
// Calculate expiration (30 days from now)
|
|
168
|
+
const expiresAt = new Date();
|
|
169
|
+
expiresAt.setDate(expiresAt.getDate() + 30);
|
|
170
|
+
this.saveSession(sessionToken, expiresAt.toISOString());
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Refresh the current session token
|
|
174
|
+
* @returns New session token and expiration
|
|
175
|
+
*/
|
|
176
|
+
async refreshSession() {
|
|
177
|
+
const response = await this.getClient().post('/auth/refresh');
|
|
178
|
+
const refreshData = response.data.data;
|
|
179
|
+
this.saveSession(refreshData.sessionToken, refreshData.expiresAt);
|
|
180
|
+
return refreshData;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Logout and invalidate current session
|
|
184
|
+
*/
|
|
185
|
+
async logout() {
|
|
186
|
+
try {
|
|
187
|
+
await this.getClient().post('/auth/logout');
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
// Always clear local session, even if API call fails
|
|
191
|
+
this.clearSession();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get current authenticated user profile
|
|
196
|
+
* @returns User profile information
|
|
197
|
+
*/
|
|
198
|
+
async getCurrentUser() {
|
|
199
|
+
const response = await this.getClient().get('/auth/me');
|
|
200
|
+
return response.data.data;
|
|
201
|
+
}
|
|
202
|
+
// ==================== Conversation Methods ====================
|
|
203
|
+
/**
|
|
204
|
+
* Create a new conversation
|
|
205
|
+
* @param data - Conversation creation parameters
|
|
206
|
+
* @returns Created conversation
|
|
207
|
+
*/
|
|
208
|
+
async createConversation(data) {
|
|
209
|
+
const response = await this.getClient().post('/conversations', data);
|
|
210
|
+
return response.data.data;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get all conversations for the authenticated user
|
|
214
|
+
* @param params - Pagination and filter parameters
|
|
215
|
+
* @returns List of conversations with pagination metadata
|
|
216
|
+
*/
|
|
217
|
+
async getConversations(params) {
|
|
218
|
+
const queryParams = {
|
|
219
|
+
page: params?.page || 1,
|
|
220
|
+
limit: params?.limit || 20,
|
|
221
|
+
};
|
|
222
|
+
if (params?.includeArchived !== undefined) {
|
|
223
|
+
queryParams.includeArchived = params.includeArchived;
|
|
224
|
+
}
|
|
225
|
+
if (params?.tags && params.tags.length > 0) {
|
|
226
|
+
queryParams.tags = params.tags.join(',');
|
|
227
|
+
}
|
|
228
|
+
const response = await this.getClient().get('/conversations', { params: queryParams });
|
|
229
|
+
return response.data;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get a specific conversation by ID
|
|
233
|
+
* @param conversationId - The conversation ID
|
|
234
|
+
* @returns Conversation details
|
|
235
|
+
*/
|
|
236
|
+
async getConversation(conversationId) {
|
|
237
|
+
const response = await this.getClient().get(`/conversations/${conversationId}`);
|
|
238
|
+
return response.data.data;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Update a conversation
|
|
242
|
+
* @param conversationId - The conversation ID
|
|
243
|
+
* @param data - Fields to update
|
|
244
|
+
* @returns Updated conversation
|
|
245
|
+
*/
|
|
246
|
+
async updateConversation(conversationId, data) {
|
|
247
|
+
const response = await this.getClient().put(`/conversations/${conversationId}`, data);
|
|
248
|
+
return response.data.data;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Delete (archive) a conversation
|
|
252
|
+
* @param conversationId - The conversation ID
|
|
253
|
+
*/
|
|
254
|
+
async deleteConversation(conversationId) {
|
|
255
|
+
await this.getClient().delete(`/conversations/${conversationId}`);
|
|
256
|
+
}
|
|
257
|
+
// ==================== Message Methods ====================
|
|
258
|
+
/**
|
|
259
|
+
* Add a message to a conversation
|
|
260
|
+
* @param conversationId - The conversation ID
|
|
261
|
+
* @param message - Message data
|
|
262
|
+
* @returns Created message
|
|
263
|
+
*/
|
|
264
|
+
async addMessage(conversationId, message) {
|
|
265
|
+
const response = await this.getClient().post(`/conversations/${conversationId}/messages`, message);
|
|
266
|
+
return response.data.data;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Get all messages for a conversation
|
|
270
|
+
* @param conversationId - The conversation ID
|
|
271
|
+
* @param params - Pagination parameters
|
|
272
|
+
* @returns List of messages with pagination metadata
|
|
273
|
+
*/
|
|
274
|
+
async getMessages(conversationId, params) {
|
|
275
|
+
const queryParams = {
|
|
276
|
+
page: params?.page || 1,
|
|
277
|
+
limit: params?.limit || 50,
|
|
278
|
+
};
|
|
279
|
+
const response = await this.getClient().get(`/conversations/${conversationId}/messages`, { params: queryParams });
|
|
280
|
+
return response.data;
|
|
281
|
+
}
|
|
282
|
+
// ==================== Settings Methods ====================
|
|
283
|
+
/**
|
|
284
|
+
* Get user settings
|
|
285
|
+
* @returns User settings object
|
|
286
|
+
*/
|
|
287
|
+
async getSettings() {
|
|
288
|
+
const response = await this.getClient().get('/settings');
|
|
289
|
+
return response.data.data;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Update user settings
|
|
293
|
+
* @param settings - Settings to update (partial update supported)
|
|
294
|
+
* @returns Updated settings
|
|
295
|
+
*/
|
|
296
|
+
async updateSettings(settings) {
|
|
297
|
+
const response = await this.getClient().put('/settings', settings);
|
|
298
|
+
return response.data.data;
|
|
299
|
+
}
|
|
300
|
+
// ==================== API Key Methods ====================
|
|
301
|
+
/**
|
|
302
|
+
* Store an encrypted API key
|
|
303
|
+
* @param data - API key data including provider, name, and key value
|
|
304
|
+
* @returns API key metadata (without the actual key)
|
|
305
|
+
*/
|
|
306
|
+
async storeApiKey(data) {
|
|
307
|
+
const response = await this.getClient().post('/api-keys', data);
|
|
308
|
+
return response.data.data;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Get all API keys (metadata only, not actual keys)
|
|
312
|
+
* @returns List of API key metadata
|
|
313
|
+
*/
|
|
314
|
+
async getApiKeys() {
|
|
315
|
+
const response = await this.getClient().get('/api-keys');
|
|
316
|
+
return response.data.data;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Decrypt and retrieve an API key for a specific provider
|
|
320
|
+
* @param provider - The provider name
|
|
321
|
+
* @returns Decrypted API key
|
|
322
|
+
*/
|
|
323
|
+
async decryptApiKey(provider) {
|
|
324
|
+
const response = await this.getClient().get(`/api-keys/${provider}/decrypt`);
|
|
325
|
+
return response.data.data;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Delete an API key
|
|
329
|
+
* @param keyId - The API key ID
|
|
330
|
+
*/
|
|
331
|
+
async deleteApiKey(keyId) {
|
|
332
|
+
await this.getClient().delete(`/api-keys/${keyId}`);
|
|
333
|
+
}
|
|
334
|
+
// ==================== Health Check ====================
|
|
335
|
+
/**
|
|
336
|
+
* Check backend service health
|
|
337
|
+
* @returns Health status information
|
|
338
|
+
*/
|
|
339
|
+
async healthCheck() {
|
|
340
|
+
// Health endpoint is at root level, not under /api
|
|
341
|
+
// So we need to construct the full URL manually
|
|
342
|
+
const baseURL = process.env.BACKEND_API_URL || 'http://localhost:3000/api';
|
|
343
|
+
const healthURL = baseURL.replace('/api', '/health');
|
|
344
|
+
const response = await axios.get(healthURL);
|
|
345
|
+
return response.data.data;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Export singleton instance
|
|
349
|
+
export const apiClient = new ApiClient();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the subshell context management system
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when a subshell connection fails
|
|
6
|
+
*/
|
|
7
|
+
export class SubshellConnectionError extends Error {
|
|
8
|
+
constructor(type, reason, recoverable) {
|
|
9
|
+
super(`${type} connection failed: ${reason}`);
|
|
10
|
+
this.type = type;
|
|
11
|
+
this.reason = reason;
|
|
12
|
+
this.recoverable = recoverable;
|
|
13
|
+
this.name = 'SubshellConnectionError';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown when a subshell command execution fails
|
|
18
|
+
*/
|
|
19
|
+
export class SubshellExecutionError extends Error {
|
|
20
|
+
constructor(command, reason, exitCode) {
|
|
21
|
+
super(`Command execution failed: ${reason}`);
|
|
22
|
+
this.command = command;
|
|
23
|
+
this.reason = reason;
|
|
24
|
+
this.exitCode = exitCode;
|
|
25
|
+
this.name = 'SubshellExecutionError';
|
|
26
|
+
}
|
|
27
|
+
}
|