@descix/cli 1.0.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 +95 -0
- package/SERVICE_README_sdk.md +399 -0
- package/agent-assets/guides/README.md +61 -0
- package/agent-assets/guides/dynamic-site-pattern.md +125 -0
- package/agent-assets/guides/knowledge-only-pattern.md +93 -0
- package/agent-assets/guides/microservice-pattern.md +152 -0
- package/agent-assets/guides/static-site-pattern.md +111 -0
- package/agent-assets/instructions/app-asset-validation.md +477 -0
- package/agent-assets/instructions/onboarding-flow.md +408 -0
- package/agent-assets/instructions/standard-folder-structure.md +125 -0
- package/agent-assets/reference/app-creation.md +381 -0
- package/agent-assets/reference/architecture.md +406 -0
- package/agent-assets/reference/local-dev.md +138 -0
- package/agent-assets/reference/mcp-integration.md +424 -0
- package/agent-assets/reference/workspace-config.md +415 -0
- package/agent-assets/rules/descix_mcp.template.mdc +243 -0
- package/bin/descix.js +3930 -0
- package/bin/mcp-server.js +382 -0
- package/descix-cli-1.0.0.tgz +0 -0
- package/lib/agent-files.js +162 -0
- package/lib/api-client.js +458 -0
- package/lib/auth-guard.js +67 -0
- package/lib/commands/app-wizard.js +627 -0
- package/lib/commands/auth.js +407 -0
- package/lib/commands/buy.js +207 -0
- package/lib/commands/clone.js +91 -0
- package/lib/commands/config.js +155 -0
- package/lib/commands/doctor.js +277 -0
- package/lib/commands/folder.js +291 -0
- package/lib/commands/init.js +257 -0
- package/lib/commands/kb.js +654 -0
- package/lib/commands/mcp.js +273 -0
- package/lib/commands/serve.js +12 -0
- package/lib/commands/status.js +239 -0
- package/lib/commands/update.js +653 -0
- package/lib/core/Chunker.js +610 -0
- package/lib/core/Hydrator.js +801 -0
- package/lib/core/PathContext.js +335 -0
- package/lib/core/Syncer.js +310 -0
- package/lib/core/index.js +32 -0
- package/lib/global-config.js +86 -0
- package/lib/google-storage-adc.js +511 -0
- package/lib/wallet-file.js +232 -0
- package/lib/wizard/setup.js +502 -0
- package/lib/workspace-config.js +712 -0
- package/lib/workspace-utils.js +126 -0
- package/package.json +41 -0
- package/templates/agent-claude.md +226 -0
- package/templates/agent-cline.md +133 -0
- package/templates/agent-copilot.md +133 -0
- package/templates/agent-cursor.md +133 -0
- package/templates/descix_mcp.mdc +122 -0
- package/templates/drive/README.md +42 -0
- package/templates/drive/app/assets/app_description.md +13 -0
- package/templates/drive/app/assets/icon.png +0 -0
- package/templates/drive/app/assets/system_instructions.md +17 -0
- package/templates/drive/app/kb/General/README.md +25 -0
- package/templates/drive/community/Apps/.gitkeep +4 -0
- package/templates/drive/community/community_assets/community_description.md +15 -0
- package/templates/drive/community/community_assets/icon.png +0 -0
- package/templates/drive_templates/README.md +104 -0
- package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
- package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
- package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
- package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
- package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
- package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
- package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
- package/templates/scaffolds/README.md +62 -0
- package/templates/scaffolds/microservice/Dockerfile +19 -0
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
- package/templates/scaffolds/microservice/app.js +50 -0
- package/templates/scaffolds/microservice/app.yaml +8 -0
- package/templates/scaffolds/microservice/defaults-config.json +5 -0
- package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
- package/templates/scaffolds/microservice/manifest.json +49 -0
- package/templates/scaffolds/microservice/package-lock.json +2645 -0
- package/templates/scaffolds/microservice/package.json +29 -0
- package/templates/scaffolds/microservice/scripts/register.js +55 -0
- package/templates/scaffolds/microservice/services/apiFront.js +75 -0
- package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
- package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
- package/templates/scaffolds/microservice/services/utils.js +247 -0
- package/templates/scaffolds/microservice/setup-schema.json +18 -0
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
- package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
- package/templates/scaffolds/site/README.md +51 -0
- package/templates/scaffolds/site/app.js +22 -0
- package/templates/scaffolds/site/index.html +42 -0
- package/templates/scaffolds/site/styles.css +123 -0
- package/test-hydration.js +150 -0
- package/vendor/mcp/transports/http-sse-transport.js +196 -0
- package/vendor/mcp/transports/index.js +15 -0
- package/vendor/mcp/transports/stdio-transport.js +118 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeSciX HTTP-Only API Client
|
|
3
|
+
*
|
|
4
|
+
* Self-contained HTTP client for CLI/MCP.
|
|
5
|
+
* No service imports - all communication via HTTP.
|
|
6
|
+
* Uses PathContext for workspace/config resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import axios from 'axios';
|
|
11
|
+
import { PathContext } from './core/PathContext.js';
|
|
12
|
+
|
|
13
|
+
export class DeSciXApiClient {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.baseUrl = options.baseUrl || null; // Will be loaded async
|
|
16
|
+
this.credentials = null;
|
|
17
|
+
this.workspaceRoot = options.workspaceRoot || null; // Set by initialize()
|
|
18
|
+
this._pathContext = null; // Cached PathContext
|
|
19
|
+
this._initialized = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the client by finding workspace root and loading config
|
|
24
|
+
* This is the primary initialization method - call before any API operations
|
|
25
|
+
*
|
|
26
|
+
* Uses PathContext for unified path resolution.
|
|
27
|
+
*
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
*/
|
|
30
|
+
async initialize() {
|
|
31
|
+
if (this._initialized) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Step 1: Try to load PathContext (will find IDE marker and load workspace.json)
|
|
36
|
+
this._pathContext = await PathContext.tryLoad();
|
|
37
|
+
|
|
38
|
+
if (this._pathContext) {
|
|
39
|
+
this.workspaceRoot = this._pathContext.getWorkspaceRoot();
|
|
40
|
+
} else {
|
|
41
|
+
this.workspaceRoot = null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 2: Set API URL from PathContext config or environment (only if not already set)
|
|
45
|
+
if (!this.baseUrl) {
|
|
46
|
+
this.baseUrl = await this.detectApiUrl();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Step 3: Load credentials from known wallet location
|
|
50
|
+
await this.loadCredentials();
|
|
51
|
+
|
|
52
|
+
this._initialized = true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Detect API URL from workspace config or environment
|
|
57
|
+
* @returns {Promise<string>} API base URL
|
|
58
|
+
*/
|
|
59
|
+
async detectApiUrl() {
|
|
60
|
+
const PRODUCTION_URL = 'https://descix.net';
|
|
61
|
+
|
|
62
|
+
// Check environment variable first (highest priority)
|
|
63
|
+
if (process.env.DESCIX_API_URL) {
|
|
64
|
+
return process.env.DESCIX_API_URL;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Use PathContext config if loaded (but only if it has an explicit URL, not the production default)
|
|
68
|
+
if (this._pathContext) {
|
|
69
|
+
const apiUrl = this._pathContext.getApiUrl();
|
|
70
|
+
if (apiUrl && apiUrl !== PRODUCTION_URL) {
|
|
71
|
+
return apiUrl;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check global config (set by `descix login --dev` or `descix config set-url`)
|
|
76
|
+
try {
|
|
77
|
+
const { GlobalConfig } = await import('./global-config.js');
|
|
78
|
+
const gc = await GlobalConfig.load();
|
|
79
|
+
if (gc.api_url && gc.api_url !== PRODUCTION_URL) return gc.api_url;
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
// Default to production
|
|
83
|
+
return PRODUCTION_URL;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Ensure client is initialized (workspace root found, config loaded)
|
|
88
|
+
* @returns {Promise<void>}
|
|
89
|
+
*/
|
|
90
|
+
async ensureInitialized() {
|
|
91
|
+
if (!this._initialized) {
|
|
92
|
+
await this.initialize();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Ensure baseUrl is loaded (calls initialize if needed)
|
|
98
|
+
* @returns {Promise<string>} Base URL
|
|
99
|
+
*/
|
|
100
|
+
async ensureBaseUrl() {
|
|
101
|
+
await this.ensureInitialized();
|
|
102
|
+
return this.baseUrl;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build request body for API call
|
|
107
|
+
* @param {string} command - Command name
|
|
108
|
+
* @param {Object} params - Command parameters
|
|
109
|
+
* @param {Object} credentials - Optional credentials override
|
|
110
|
+
* @returns {Object} Request body
|
|
111
|
+
*/
|
|
112
|
+
buildRequestBody(command, params = {}, credentials = null) {
|
|
113
|
+
const creds = credentials || this.credentials || {};
|
|
114
|
+
|
|
115
|
+
const requestBody = {
|
|
116
|
+
command,
|
|
117
|
+
params,
|
|
118
|
+
user_id: creds.userId || null,
|
|
119
|
+
access_token: creds.accessToken || null,
|
|
120
|
+
wallet_address: creds.walletAddress || null,
|
|
121
|
+
signature: creds.signature || null,
|
|
122
|
+
guild_id: params.guild_id || null
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (params.streaming) {
|
|
126
|
+
requestBody.streaming = true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return requestBody;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Ensure we have a valid session by calling reconnect_by_wallet if needed
|
|
134
|
+
* This is called automatically before authenticated API calls
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
*/
|
|
137
|
+
async ensureSession() {
|
|
138
|
+
// If we already have an access token, assume it's valid (will retry if expired)
|
|
139
|
+
if (this.credentials?.accessToken) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If we have wallet credentials but no session, get one via reconnect_by_wallet
|
|
144
|
+
if (this.credentials?.walletAddress && this.credentials?.signature) {
|
|
145
|
+
try {
|
|
146
|
+
// Use invokeRaw to avoid session check recursion
|
|
147
|
+
// We pass skipSessionCheck: true to invokeRaw to be extra safe
|
|
148
|
+
const result = await this.invokeRaw('reconnect_by_wallet', {
|
|
149
|
+
wallet_address: this.credentials.walletAddress,
|
|
150
|
+
signature: this.credentials.signature
|
|
151
|
+
}, null, { skipSessionCheck: true });
|
|
152
|
+
|
|
153
|
+
if (result.status === 'OK' && result.message?.sessionInfo?.access_token) {
|
|
154
|
+
this.credentials.accessToken = result.message.sessionInfo.access_token;
|
|
155
|
+
// Also update userId if returned (in case it was missing)
|
|
156
|
+
if (result.message.sessionInfo.id) {
|
|
157
|
+
this.credentials.userId = result.message.sessionInfo.id;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Save updated credentials to wallet file
|
|
161
|
+
try {
|
|
162
|
+
const { WalletFileManager } = await import('./wallet-file.js');
|
|
163
|
+
const walletPath = await WalletFileManager.findWalletFile(this.workspaceRoot || process.cwd());
|
|
164
|
+
if (walletPath) {
|
|
165
|
+
const walletData = await WalletFileManager.loadWalletFile(walletPath);
|
|
166
|
+
if (walletData) {
|
|
167
|
+
walletData.sessionToken = this.credentials.accessToken;
|
|
168
|
+
walletData.userId = this.credentials.userId;
|
|
169
|
+
walletData.expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
170
|
+
await WalletFileManager.saveWalletFile(walletPath, walletData);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (saveErr) {
|
|
174
|
+
console.error('[ApiClient] Failed to save refreshed session to wallet file:', saveErr.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// If reconnect fails, continue anyway - the actual request will fail with proper error
|
|
179
|
+
console.error('[ApiClient] Failed to create session via wallet:', error.message);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Invoke a command via HTTP (raw, no session management)
|
|
186
|
+
* Used internally for auth commands to avoid recursion
|
|
187
|
+
* @param {string} command - Command name
|
|
188
|
+
* @param {Object} params - Command parameters
|
|
189
|
+
* @param {Object} credentials - Optional credentials override
|
|
190
|
+
* @returns {Promise<Object>} Response data
|
|
191
|
+
*/
|
|
192
|
+
async invokeRaw(command, params = {}, credentials = null, options = {}) {
|
|
193
|
+
console.log(`[ApiClient] invoking ${command}...`);
|
|
194
|
+
await this.ensureBaseUrl();
|
|
195
|
+
console.log(`[ApiClient] Base URL: ${this.baseUrl}`);
|
|
196
|
+
|
|
197
|
+
const requestBody = this.buildRequestBody(command, params, credentials);
|
|
198
|
+
if (options.skipSessionCheck) {
|
|
199
|
+
requestBody.access_token = null; // Force guest-like call for refresh
|
|
200
|
+
}
|
|
201
|
+
const url = `${this.baseUrl}/apifront/`;
|
|
202
|
+
console.log(`[ApiClient] POST ${url}`);
|
|
203
|
+
|
|
204
|
+
const axiosConfig = {
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
timeout: 300000
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (this.baseUrl.includes('localhost') || this.baseUrl.includes('127.0.0.1')) {
|
|
210
|
+
const https = await import('https');
|
|
211
|
+
axiosConfig.httpsAgent = new https.Agent({
|
|
212
|
+
rejectUnauthorized: false
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const response = await axios.post(url, requestBody, axiosConfig);
|
|
218
|
+
console.log(`[ApiClient] Response status: ${response.status}`);
|
|
219
|
+
const data = response.data;
|
|
220
|
+
|
|
221
|
+
if (data.status !== 'OK') {
|
|
222
|
+
const error = new Error(data.message || 'API request failed');
|
|
223
|
+
error.status = data.status;
|
|
224
|
+
error.response = data;
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return data;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`[ApiClient] Error invoking ${command}:`, error.message);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Invoke a command via HTTP with automatic session management
|
|
237
|
+
* @param {string} command - Command name
|
|
238
|
+
* @param {Object} params - Command parameters
|
|
239
|
+
* @param {Object} options - Optional { credentials, allowGuest, skipSessionCheck }
|
|
240
|
+
* @returns {Promise<Object>} Response data
|
|
241
|
+
*/
|
|
242
|
+
async invoke(command, params = {}, options = {}) {
|
|
243
|
+
// Ensure baseUrl is loaded
|
|
244
|
+
await this.ensureBaseUrl();
|
|
245
|
+
|
|
246
|
+
// Load credentials if not provided
|
|
247
|
+
let credentials = options.credentials || this.credentials;
|
|
248
|
+
if (!credentials && !options.allowGuest) {
|
|
249
|
+
credentials = await this.loadCredentials();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Ensure we have a session (unless this is a guest command or session check is skipped)
|
|
253
|
+
if (!options.allowGuest && !options.skipSessionCheck) {
|
|
254
|
+
await this.ensureSession();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Build request with potentially updated credentials (after ensureSession)
|
|
258
|
+
const requestBody = this.buildRequestBody(command, params, this.credentials || credentials);
|
|
259
|
+
|
|
260
|
+
// If we are triggering device login, we MUST NOT pass an expired session token
|
|
261
|
+
// or the backend will reject it before we can even request a new code.
|
|
262
|
+
if (command === 'device_request_login' || command === 'device_check_status') {
|
|
263
|
+
requestBody.access_token = null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const url = `${this.baseUrl}/apifront/`;
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// For local development with self-signed certs, disable SSL verification
|
|
270
|
+
const axiosConfig = {
|
|
271
|
+
headers: { 'Content-Type': 'application/json' },
|
|
272
|
+
timeout: 300000 // 5 minute timeout for file uploads with vectorization
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Disable SSL verification for localhost (development only)
|
|
276
|
+
if (this.baseUrl.includes('localhost') || this.baseUrl.includes('127.0.0.1')) {
|
|
277
|
+
const https = await import('https');
|
|
278
|
+
axiosConfig.httpsAgent = new https.Agent({
|
|
279
|
+
rejectUnauthorized: false
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const response = await axios.post(url, requestBody, axiosConfig);
|
|
284
|
+
|
|
285
|
+
const data = response.data;
|
|
286
|
+
|
|
287
|
+
if (data.status !== 'OK') {
|
|
288
|
+
const error = new Error(data.message || 'API request failed');
|
|
289
|
+
error.status = data.status;
|
|
290
|
+
error.response = data;
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return data;
|
|
295
|
+
} catch (error) {
|
|
296
|
+
// Check if this is an auth failure that we can retry
|
|
297
|
+
const isAuthError = error.response?.status === 401 ||
|
|
298
|
+
error.response?.auth_status === 'AUTH_FAILED' ||
|
|
299
|
+
error.message?.includes('session') ||
|
|
300
|
+
error.message?.includes('expired');
|
|
301
|
+
|
|
302
|
+
// If auth failed and we have wallet credentials, try to refresh session once
|
|
303
|
+
if (isAuthError &&
|
|
304
|
+
this.credentials?.walletAddress &&
|
|
305
|
+
this.credentials?.signature &&
|
|
306
|
+
!options._retried) {
|
|
307
|
+
|
|
308
|
+
// Clear existing access token
|
|
309
|
+
if (this.credentials) {
|
|
310
|
+
this.credentials.accessToken = null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Try to get new session
|
|
314
|
+
await this.ensureSession();
|
|
315
|
+
|
|
316
|
+
// If we got a new token, retry the original request
|
|
317
|
+
if (this.credentials?.accessToken) {
|
|
318
|
+
return this.invoke(command, params, { ...options, _retried: true });
|
|
319
|
+
} else if (command !== 'device_request_login' && command !== 'device_check_status') {
|
|
320
|
+
// If refresh failed (e.g. signature invalid or user not found),
|
|
321
|
+
// we should trigger device login if this is a CLI environment
|
|
322
|
+
console.log('[ApiClient] Session refresh failed, triggering device login...');
|
|
323
|
+
const { loginDevice } = await import('./commands/auth.js');
|
|
324
|
+
await loginDevice({ url: this.baseUrl, workspaceRoot: this.workspaceRoot });
|
|
325
|
+
|
|
326
|
+
// After login, credentials should be updated in the wallet file.
|
|
327
|
+
// Reload them and retry the request one last time.
|
|
328
|
+
this.credentials = null;
|
|
329
|
+
await this.loadCredentials();
|
|
330
|
+
if (this.credentials?.accessToken) {
|
|
331
|
+
return this.invoke(command, params, { ...options, _retried: true });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (error.response) {
|
|
337
|
+
// Axios error with response
|
|
338
|
+
const errorData = error.response.data || { message: 'HTTP request failed' };
|
|
339
|
+
const apiError = new Error(errorData.message || `HTTP ${error.response.status}`);
|
|
340
|
+
apiError.status = error.response.status;
|
|
341
|
+
apiError.response = errorData;
|
|
342
|
+
throw apiError;
|
|
343
|
+
} else if (error.code === 'ECONNREFUSED') {
|
|
344
|
+
throw new Error(`Cannot connect to ${this.baseUrl}. Is the backend server running?`);
|
|
345
|
+
} else if (error.code === 'ETIMEDOUT') {
|
|
346
|
+
throw new Error(`Request to ${this.baseUrl} timed out. Please try again.`);
|
|
347
|
+
} else {
|
|
348
|
+
// Network or other error
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Load credentials from wallet file at workspace root
|
|
356
|
+
* Wallet is ALWAYS at {workspaceRoot}/.descix/wallet.json
|
|
357
|
+
* @returns {Promise<Object|null>} Credentials object or null
|
|
358
|
+
*/
|
|
359
|
+
async loadCredentials() {
|
|
360
|
+
if (this.credentials) {
|
|
361
|
+
return this.credentials;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Use PathContext's workspace root if available
|
|
365
|
+
if (!this.workspaceRoot && this._pathContext) {
|
|
366
|
+
this.workspaceRoot = this._pathContext.getWorkspaceRoot();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// If still no workspace root, try: PathContext (IDE markers) → workspace.json → wallet.json
|
|
370
|
+
if (!this.workspaceRoot) {
|
|
371
|
+
const ctx = await PathContext.tryLoad();
|
|
372
|
+
if (ctx) {
|
|
373
|
+
this.workspaceRoot = ctx.getWorkspaceRoot();
|
|
374
|
+
this._pathContext = ctx;
|
|
375
|
+
} else {
|
|
376
|
+
const { WorkspaceConfig } = await import('./workspace-config.js');
|
|
377
|
+
this.workspaceRoot = await WorkspaceConfig.findWorkspaceRoot();
|
|
378
|
+
}
|
|
379
|
+
// Final fallback: walk up from cwd looking for .descix/wallet.json
|
|
380
|
+
if (!this.workspaceRoot) {
|
|
381
|
+
const fs = await import('fs/promises');
|
|
382
|
+
let dir = path.resolve(process.cwd());
|
|
383
|
+
const root = path.parse(dir).root;
|
|
384
|
+
while (dir !== root) {
|
|
385
|
+
try {
|
|
386
|
+
await fs.access(path.join(dir, '.descix', 'wallet.json'));
|
|
387
|
+
this.workspaceRoot = dir;
|
|
388
|
+
break;
|
|
389
|
+
} catch { /* not here, keep walking */ }
|
|
390
|
+
const parent = path.dirname(dir);
|
|
391
|
+
if (parent === dir) break;
|
|
392
|
+
dir = parent;
|
|
393
|
+
}
|
|
394
|
+
if (!this.workspaceRoot) return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const { WalletFileManager } = await import('./wallet-file.js');
|
|
400
|
+
|
|
401
|
+
// Load wallet from known location: {workspaceRoot}/.descix/wallet.json
|
|
402
|
+
const walletData = await WalletFileManager.loadFromWorkspace(this.workspaceRoot);
|
|
403
|
+
if (!walletData) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.credentials = {
|
|
408
|
+
userId: walletData.userId || null,
|
|
409
|
+
accessToken: walletData.sessionToken || null,
|
|
410
|
+
walletAddress: walletData.walletAddress || null,
|
|
411
|
+
signature: walletData.signature || null,
|
|
412
|
+
communityId: walletData.communityId || null,
|
|
413
|
+
tokenSymbol: walletData.tokenSymbol || null
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
return this.credentials;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('[ApiClient] Error loading credentials:', error.message);
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Set credentials manually
|
|
425
|
+
* @param {Object} credentials - { userId, accessToken, walletAddress, signature, ... }
|
|
426
|
+
*/
|
|
427
|
+
setCredentials(credentials) {
|
|
428
|
+
this.credentials = credentials;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Check if credentials are loaded and valid
|
|
433
|
+
* @returns {boolean}
|
|
434
|
+
*/
|
|
435
|
+
hasCredentials() {
|
|
436
|
+
return !!(this.credentials?.userId || this.credentials?.walletAddress);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Get the PathContext instance (if loaded)
|
|
441
|
+
* Useful for commands that need path resolution
|
|
442
|
+
* @returns {PathContext|null}
|
|
443
|
+
*/
|
|
444
|
+
getPathContext() {
|
|
445
|
+
return this._pathContext;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Create a default API client instance
|
|
451
|
+
* @param {Object} options - Client options
|
|
452
|
+
* @returns {DeSciXApiClient}
|
|
453
|
+
*/
|
|
454
|
+
export function createApiClient(options = {}) {
|
|
455
|
+
return new DeSciXApiClient(options);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export default DeSciXApiClient;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication Guard
|
|
3
|
+
*
|
|
4
|
+
* Enforces authentication requirement for CLI/MCP commands.
|
|
5
|
+
* Unlike the PWA which allows guest browsing, CLI and MCP require authentication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { DeSciXApiClient } from './api-client.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Require authentication before executing a command
|
|
13
|
+
* @param {DeSciXApiClient} apiClient - API client instance
|
|
14
|
+
* @returns {Promise<Object>} User information if authenticated
|
|
15
|
+
*/
|
|
16
|
+
export async function requireAuth(apiClient) {
|
|
17
|
+
// Load credentials from wallet file
|
|
18
|
+
await apiClient.loadCredentials();
|
|
19
|
+
|
|
20
|
+
if (!apiClient.hasCredentials()) {
|
|
21
|
+
console.error(chalk.red('\n Authentication required.\n'));
|
|
22
|
+
console.error(chalk.white(' Please sign up with DeSciX to use this feature.\n'));
|
|
23
|
+
console.error(chalk.cyan(' Run: descix login\n'));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Validate session is still active by calling check_staked_status via HTTP
|
|
28
|
+
try {
|
|
29
|
+
const response = await apiClient.invoke('check_staked_status', {}, { allowGuest: false });
|
|
30
|
+
if (response.status === 'OK' && response.message) {
|
|
31
|
+
return response.message;
|
|
32
|
+
}
|
|
33
|
+
throw new Error('Invalid session');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error.message.includes('expired') ||
|
|
36
|
+
error.message.includes('invalid') ||
|
|
37
|
+
error.message.includes('Authentication required') ||
|
|
38
|
+
error.message.includes('User session invalid')) {
|
|
39
|
+
console.error(chalk.yellow('\n Session expired or invalid.\n'));
|
|
40
|
+
console.error(chalk.white(' Please sign in again.\n'));
|
|
41
|
+
console.error(chalk.cyan(' Run: descix login\n'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
// Other errors (network, etc.) - rethrow
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if user is authenticated (non-blocking)
|
|
51
|
+
* @param {DeSciXApiClient} apiClient - API client instance
|
|
52
|
+
* @returns {Promise<boolean>} True if authenticated
|
|
53
|
+
*/
|
|
54
|
+
export async function isAuthenticated(apiClient) {
|
|
55
|
+
try {
|
|
56
|
+
await apiClient.loadCredentials();
|
|
57
|
+
if (!apiClient.hasCredentials()) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// Try a lightweight check via HTTP
|
|
61
|
+
const response = await apiClient.invoke('check_staked_status', {}, { allowGuest: false });
|
|
62
|
+
return response.status === 'OK';
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|