@eterna-hybrid-exchange/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eterna Hybrid Exchange
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # eterna
2
+
3
+ CLI for [Eterna Exchange](https://eterna.exchange) — execute AI trading strategies from your terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx eterna --help
9
+ ```
10
+
11
+ Or install globally:
12
+
13
+ ```bash
14
+ npm install -g eterna
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Authenticate (opens browser or shows device code for SSH)
21
+ npx eterna login
22
+
23
+ # Execute a strategy file
24
+ npx eterna execute strategy.ts
25
+
26
+ # Pipe code via stdin
27
+ echo 'const balance = await eterna.getBalance(); return balance;' | npx eterna execute -
28
+
29
+ # Check balance and positions
30
+ npx eterna balance
31
+ npx eterna positions
32
+
33
+ # Browse the SDK
34
+ npx eterna sdk --search "place order"
35
+ npx eterna sdk --detail full
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ | Command | Description |
41
+ | ----------------------- | ------------------------------------------------- |
42
+ | `eterna login` | Authenticate with Eterna (browser or device code) |
43
+ | `eterna logout` | Remove stored credentials |
44
+ | `eterna execute <file>` | Execute TypeScript in the Eterna sandbox |
45
+ | `eterna sdk` | Browse SDK method reference |
46
+ | `eterna balance` | Get account balance (shortcut) |
47
+ | `eterna positions` | Get open positions (shortcut) |
48
+
49
+ ## Authentication
50
+
51
+ The CLI supports two authentication flows, auto-detected based on your environment:
52
+
53
+ - **Browser flow**: Opens your default browser for Google OAuth (used when running locally)
54
+ - **Device code flow**: Shows a URL and code to enter in any browser (used over SSH or with `--no-browser`)
55
+
56
+ Credentials are stored in `~/.eterna/credentials.json` and auto-refreshed when expired.
57
+
58
+ ## Configuration
59
+
60
+ Config is stored in `~/.eterna/config.json`:
61
+
62
+ ```json
63
+ {
64
+ "endpoint": "https://ai-api.eterna.exchange"
65
+ }
66
+ ```
67
+
68
+ Override the config directory with the `ETERNA_CONFIG_DIR` environment variable.
69
+
70
+ ## License
71
+
72
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,609 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command as Command7 } from "commander";
5
+
6
+ // src/commands/login.ts
7
+ import { Command } from "commander";
8
+
9
+ // src/auth/detect.ts
10
+ function detectAuthFlow(noBrowser) {
11
+ if (noBrowser) return "device";
12
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY) return "device";
13
+ return "localhost";
14
+ }
15
+
16
+ // src/auth/localhost-flow.ts
17
+ import {
18
+ createServer
19
+ } from "http";
20
+ import { randomBytes, createHash } from "crypto";
21
+ import open from "open";
22
+
23
+ // src/auth/config.ts
24
+ import * as fs from "fs";
25
+ import * as path from "path";
26
+ import * as os from "os";
27
+ var DEFAULT_ENDPOINT = "https://ai-api.eterna.exchange";
28
+ var AUTH_ISSUER = "https://ai-auth.eterna.exchange";
29
+ function getConfigDir() {
30
+ return process.env.ETERNA_CONFIG_DIR ?? path.join(os.homedir(), ".eterna");
31
+ }
32
+ function getAuthIssuer() {
33
+ return process.env.ETERNA_AUTH_ISSUER ?? AUTH_ISSUER;
34
+ }
35
+ function ensureConfigDir() {
36
+ const dir = getConfigDir();
37
+ if (!fs.existsSync(dir)) {
38
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
39
+ }
40
+ }
41
+ function readConfig() {
42
+ const configPath = path.join(getConfigDir(), "config.json");
43
+ try {
44
+ const raw = fs.readFileSync(configPath, "utf-8");
45
+ const parsed = JSON.parse(raw);
46
+ return {
47
+ endpoint: parsed.endpoint ?? DEFAULT_ENDPOINT
48
+ };
49
+ } catch {
50
+ return { endpoint: DEFAULT_ENDPOINT };
51
+ }
52
+ }
53
+ function readCredentials() {
54
+ const credPath = path.join(getConfigDir(), "credentials.json");
55
+ try {
56
+ const raw = fs.readFileSync(credPath, "utf-8");
57
+ const parsed = JSON.parse(raw);
58
+ if (!parsed.accessToken || !parsed.refreshToken) return null;
59
+ return parsed;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ function writeCredentials(creds) {
65
+ ensureConfigDir();
66
+ const credPath = path.join(getConfigDir(), "credentials.json");
67
+ fs.writeFileSync(credPath, JSON.stringify(creds, null, 2) + "\n", {
68
+ mode: 384
69
+ });
70
+ }
71
+ function clearCredentials() {
72
+ const credPath = path.join(getConfigDir(), "credentials.json");
73
+ try {
74
+ fs.unlinkSync(credPath);
75
+ } catch {
76
+ }
77
+ }
78
+
79
+ // src/auth/localhost-flow.ts
80
+ var CLIENT_ID = "eterna-cli";
81
+ var CALLBACK_PORT = 9876;
82
+ var REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
83
+ var SCOPE = "mcp:full";
84
+ function generateCodeVerifier() {
85
+ return randomBytes(32).toString("base64url");
86
+ }
87
+ async function computeCodeChallenge(verifier) {
88
+ const hash = createHash("sha256").update(verifier).digest();
89
+ return hash.toString("base64url");
90
+ }
91
+ function buildAuthorizationUrl(codeChallenge, state, resource) {
92
+ const issuer = getAuthIssuer();
93
+ const params = new URLSearchParams({
94
+ response_type: "code",
95
+ client_id: CLIENT_ID,
96
+ redirect_uri: REDIRECT_URI,
97
+ scope: SCOPE,
98
+ state,
99
+ code_challenge: codeChallenge,
100
+ code_challenge_method: "S256",
101
+ resource
102
+ });
103
+ return `${issuer}/oauth/authorize?${params.toString()}`;
104
+ }
105
+ async function localhostAuthFlow(resource) {
106
+ const issuer = getAuthIssuer();
107
+ const codeVerifier = generateCodeVerifier();
108
+ const codeChallenge = await computeCodeChallenge(codeVerifier);
109
+ const state = randomBytes(16).toString("hex");
110
+ const authUrl = buildAuthorizationUrl(codeChallenge, state, resource);
111
+ const { code, receivedState } = await new Promise((resolve, reject) => {
112
+ const server = createServer((req, res) => {
113
+ const url = new URL(req.url ?? "/", `http://127.0.0.1:${CALLBACK_PORT}`);
114
+ if (url.pathname === "/callback") {
115
+ const code2 = url.searchParams.get("code");
116
+ const receivedState2 = url.searchParams.get("state");
117
+ const error = url.searchParams.get("error");
118
+ if (error) {
119
+ res.writeHead(200, { "Content-Type": "text/html" });
120
+ res.end(
121
+ "<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>"
122
+ );
123
+ server.close();
124
+ reject(new Error(`OAuth error: ${error}`));
125
+ return;
126
+ }
127
+ if (!code2 || !receivedState2) {
128
+ res.writeHead(400, { "Content-Type": "text/html" });
129
+ res.end("<html><body><h1>Missing parameters</h1></body></html>");
130
+ server.close();
131
+ reject(new Error("Missing code or state in callback"));
132
+ return;
133
+ }
134
+ res.writeHead(200, { "Content-Type": "text/html" });
135
+ res.end(
136
+ "<html><body><h1>Authorized!</h1><p>You can close this window and return to your terminal.</p></body></html>"
137
+ );
138
+ server.close();
139
+ resolve({ code: code2, receivedState: receivedState2 });
140
+ }
141
+ });
142
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => {
143
+ open(authUrl).catch(() => {
144
+ server.close();
145
+ reject(new Error("Failed to open browser"));
146
+ });
147
+ });
148
+ setTimeout(() => {
149
+ server.close();
150
+ reject(new Error("Authorization timed out after 5 minutes"));
151
+ }, 3e5);
152
+ });
153
+ if (receivedState !== state) {
154
+ throw new Error("State mismatch \u2014 possible CSRF attack");
155
+ }
156
+ const tokenUrl = `${issuer}/api/oauth/token`;
157
+ const tokenBody = new URLSearchParams({
158
+ grant_type: "authorization_code",
159
+ code,
160
+ client_id: CLIENT_ID,
161
+ redirect_uri: REDIRECT_URI,
162
+ code_verifier: codeVerifier,
163
+ resource
164
+ });
165
+ const tokenRes = await fetch(tokenUrl, {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
168
+ body: tokenBody.toString()
169
+ });
170
+ if (!tokenRes.ok) {
171
+ const err = await tokenRes.json().catch(() => ({}));
172
+ throw new Error(
173
+ `Token exchange failed: ${err.error_description ?? tokenRes.statusText}`
174
+ );
175
+ }
176
+ return tokenRes.json();
177
+ }
178
+
179
+ // src/auth/device-flow.ts
180
+ var CLIENT_ID2 = "eterna-cli";
181
+ var SCOPE2 = "mcp:full";
182
+ async function deviceCodeFlow(resource, callbacks) {
183
+ const issuer = getAuthIssuer();
184
+ const deviceRes = await fetch(`${issuer}/api/oauth/device/code`, {
185
+ method: "POST",
186
+ headers: { "Content-Type": "application/json" },
187
+ body: JSON.stringify({
188
+ client_id: CLIENT_ID2,
189
+ scope: SCOPE2,
190
+ resource
191
+ })
192
+ });
193
+ if (!deviceRes.ok) {
194
+ const err = await deviceRes.json().catch(() => ({}));
195
+ throw new Error(
196
+ `Device code request failed: ${err.error_description ?? deviceRes.statusText}`
197
+ );
198
+ }
199
+ const deviceData = await deviceRes.json();
200
+ callbacks.onUserCode(
201
+ deviceData.user_code,
202
+ deviceData.verification_uri,
203
+ deviceData.verification_uri_complete
204
+ );
205
+ let interval = deviceData.interval * 1e3;
206
+ const deadline = Date.now() + deviceData.expires_in * 1e3;
207
+ while (Date.now() < deadline) {
208
+ await new Promise((resolve) => setTimeout(resolve, interval));
209
+ callbacks.onPolling();
210
+ const tokenRes = await fetch(`${issuer}/api/oauth/token`, {
211
+ method: "POST",
212
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
213
+ body: new URLSearchParams({
214
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
215
+ device_code: deviceData.device_code,
216
+ client_id: CLIENT_ID2,
217
+ resource
218
+ }).toString()
219
+ });
220
+ if (tokenRes.ok) {
221
+ return tokenRes.json();
222
+ }
223
+ const err = await tokenRes.json();
224
+ switch (err.error) {
225
+ case "authorization_pending":
226
+ continue;
227
+ case "slow_down":
228
+ interval += 5e3;
229
+ continue;
230
+ case "expired_token":
231
+ throw new Error("Device code expired. Please try again.");
232
+ case "access_denied":
233
+ throw new Error("Authorization denied by user.");
234
+ default:
235
+ throw new Error(
236
+ `Unexpected error: ${err.error_description ?? err.error}`
237
+ );
238
+ }
239
+ }
240
+ throw new Error("Device code expired. Please try again.");
241
+ }
242
+
243
+ // src/auth/token-manager.ts
244
+ var REFRESH_BUFFER_MS = 6e4;
245
+ var TokenManager = class {
246
+ isAuthenticated() {
247
+ const creds = readCredentials();
248
+ return creds !== null;
249
+ }
250
+ getAccessToken() {
251
+ const creds = readCredentials();
252
+ if (!creds) return null;
253
+ return creds.accessToken;
254
+ }
255
+ needsRefresh() {
256
+ const creds = readCredentials();
257
+ if (!creds) return false;
258
+ return Date.now() >= creds.expiresAt - REFRESH_BUFFER_MS;
259
+ }
260
+ saveTokens(params) {
261
+ writeCredentials({
262
+ accessToken: params.accessToken,
263
+ refreshToken: params.refreshToken,
264
+ expiresAt: Date.now() + params.expiresIn * 1e3,
265
+ clientId: params.clientId,
266
+ resource: params.resource
267
+ });
268
+ }
269
+ async refreshIfNeeded() {
270
+ const creds = readCredentials();
271
+ if (!creds) return null;
272
+ if (!this.needsRefresh()) {
273
+ return creds.accessToken;
274
+ }
275
+ const issuer = getAuthIssuer();
276
+ const res = await fetch(`${issuer}/api/oauth/token`, {
277
+ method: "POST",
278
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
279
+ body: new URLSearchParams({
280
+ grant_type: "refresh_token",
281
+ refresh_token: creds.refreshToken,
282
+ client_id: creds.clientId,
283
+ resource: creds.resource
284
+ }).toString()
285
+ });
286
+ if (!res.ok) {
287
+ clearCredentials();
288
+ return null;
289
+ }
290
+ const data = await res.json();
291
+ this.saveTokens({
292
+ accessToken: data.access_token,
293
+ refreshToken: data.refresh_token,
294
+ expiresIn: data.expires_in,
295
+ clientId: creds.clientId,
296
+ resource: creds.resource
297
+ });
298
+ return data.access_token;
299
+ }
300
+ logout() {
301
+ clearCredentials();
302
+ }
303
+ };
304
+
305
+ // src/util/spinner.ts
306
+ import ora from "ora";
307
+ var activeSpinner = null;
308
+ function startSpinner(text) {
309
+ activeSpinner = ora(text).start();
310
+ return activeSpinner;
311
+ }
312
+ function stopSpinner(success, text) {
313
+ if (!activeSpinner) return;
314
+ if (success === true) {
315
+ activeSpinner.succeed(text);
316
+ } else if (success === false) {
317
+ activeSpinner.fail(text);
318
+ } else {
319
+ activeSpinner.stop();
320
+ }
321
+ activeSpinner = null;
322
+ }
323
+
324
+ // src/commands/login.ts
325
+ var CLIENT_ID3 = "eterna-cli";
326
+ var loginCommand = new Command("login").description("Authenticate with Eterna").option("--no-browser", "Use device code flow (no browser redirect)").action(async (opts) => {
327
+ const tokenManager = new TokenManager();
328
+ const config = readConfig();
329
+ const resource = config.endpoint;
330
+ if (tokenManager.isAuthenticated() && !tokenManager.needsRefresh()) {
331
+ console.log(
332
+ "Already authenticated. Use `eterna logout` first to re-authenticate."
333
+ );
334
+ return;
335
+ }
336
+ const flowType = detectAuthFlow(!opts.browser);
337
+ if (flowType === "localhost") {
338
+ await loginWithLocalhost(resource, tokenManager);
339
+ } else {
340
+ await loginWithDeviceCode(resource, tokenManager);
341
+ }
342
+ });
343
+ async function loginWithLocalhost(resource, tokenManager) {
344
+ startSpinner("Opening browser for authentication...");
345
+ try {
346
+ stopSpinner(void 0);
347
+ console.log("Waiting for authorization in browser...\n");
348
+ const tokens = await localhostAuthFlow(resource);
349
+ tokenManager.saveTokens({
350
+ accessToken: tokens.access_token,
351
+ refreshToken: tokens.refresh_token,
352
+ expiresIn: tokens.expires_in,
353
+ clientId: CLIENT_ID3,
354
+ resource
355
+ });
356
+ console.log("Logged in successfully!");
357
+ } catch (err) {
358
+ stopSpinner(false, "Authentication failed");
359
+ console.error(err.message);
360
+ process.exitCode = 1;
361
+ }
362
+ }
363
+ async function loginWithDeviceCode(resource, tokenManager) {
364
+ try {
365
+ const tokens = await deviceCodeFlow(resource, {
366
+ onUserCode: (userCode, _verificationUri, verificationUriComplete) => {
367
+ console.log("");
368
+ console.log(" To authenticate, visit:");
369
+ console.log(` ${verificationUriComplete}`);
370
+ console.log("");
371
+ console.log(` Or go to the URL above and enter code: ${userCode}`);
372
+ console.log("");
373
+ },
374
+ onPolling: () => {
375
+ }
376
+ });
377
+ tokenManager.saveTokens({
378
+ accessToken: tokens.access_token,
379
+ refreshToken: tokens.refresh_token,
380
+ expiresIn: tokens.expires_in,
381
+ clientId: CLIENT_ID3,
382
+ resource
383
+ });
384
+ console.log("Logged in successfully!");
385
+ } catch (err) {
386
+ console.error(`Authentication failed: ${err.message}`);
387
+ process.exitCode = 1;
388
+ }
389
+ }
390
+
391
+ // src/commands/logout.ts
392
+ import { Command as Command2 } from "commander";
393
+ var logoutCommand = new Command2("logout").description("Remove stored credentials").action(() => {
394
+ const tokenManager = new TokenManager();
395
+ if (!tokenManager.isAuthenticated()) {
396
+ console.log("Not currently authenticated.");
397
+ return;
398
+ }
399
+ tokenManager.logout();
400
+ console.log("Logged out. Credentials removed from ~/.eterna/");
401
+ });
402
+
403
+ // src/commands/execute.ts
404
+ import { Command as Command3 } from "commander";
405
+ import * as fs2 from "fs";
406
+
407
+ // src/api/client.ts
408
+ var ApiClient = class {
409
+ tokenManager = new TokenManager();
410
+ async getAuthHeaders() {
411
+ let token = this.tokenManager.getAccessToken();
412
+ if (!token || this.tokenManager.needsRefresh()) {
413
+ token = await this.tokenManager.refreshIfNeeded();
414
+ }
415
+ if (!token) {
416
+ throw new Error("Not authenticated. Run `eterna login` first.");
417
+ }
418
+ return { Authorization: `Bearer ${token}` };
419
+ }
420
+ getBaseUrl() {
421
+ return readConfig().endpoint;
422
+ }
423
+ async execute(code) {
424
+ const headers = await this.getAuthHeaders();
425
+ const res = await fetch(`${this.getBaseUrl()}/sandbox/execute`, {
426
+ method: "POST",
427
+ headers: { ...headers, "Content-Type": "application/json" },
428
+ body: JSON.stringify({ code })
429
+ });
430
+ if (res.status === 401) {
431
+ const newToken = await this.tokenManager.refreshIfNeeded();
432
+ if (!newToken) {
433
+ throw new Error(
434
+ "Session expired. Run `eterna login` to re-authenticate."
435
+ );
436
+ }
437
+ const retryRes = await fetch(`${this.getBaseUrl()}/sandbox/execute`, {
438
+ method: "POST",
439
+ headers: {
440
+ Authorization: `Bearer ${newToken}`,
441
+ "Content-Type": "application/json"
442
+ },
443
+ body: JSON.stringify({ code })
444
+ });
445
+ if (!retryRes.ok) {
446
+ throw new Error(`Execution failed: ${retryRes.statusText}`);
447
+ }
448
+ return retryRes.json();
449
+ }
450
+ if (!res.ok) {
451
+ const errBody = await res.text();
452
+ throw new Error(`Execution failed (${res.status}): ${errBody}`);
453
+ }
454
+ return res.json();
455
+ }
456
+ async sdkSearch(query, detailLevel = "summary") {
457
+ const params = new URLSearchParams({ detail_level: detailLevel });
458
+ if (query) params.set("query", query);
459
+ const res = await fetch(
460
+ `${this.getBaseUrl()}/sdk/search?${params.toString()}`
461
+ );
462
+ if (!res.ok) {
463
+ throw new Error(`SDK search failed (${res.status}): ${res.statusText}`);
464
+ }
465
+ return res.json();
466
+ }
467
+ };
468
+
469
+ // src/util/format.ts
470
+ function formatExecutionResult(result) {
471
+ const lines = [];
472
+ if (result.success) {
473
+ if (result.result !== void 0 && result.result !== null) {
474
+ lines.push(
475
+ typeof result.result === "string" ? result.result : JSON.stringify(result.result, null, 2)
476
+ );
477
+ }
478
+ } else {
479
+ lines.push(`Error: ${result.error ?? "Unknown error"}`);
480
+ }
481
+ if (result.logs.length > 0) {
482
+ lines.push("");
483
+ lines.push("--- Logs ---");
484
+ lines.push(...result.logs);
485
+ }
486
+ if (result.validationErrors && result.validationErrors.length > 0) {
487
+ lines.push("");
488
+ lines.push("--- Validation Errors ---");
489
+ for (const ve of result.validationErrors) {
490
+ lines.push(` ${ve.field}: ${ve.message}`);
491
+ }
492
+ }
493
+ lines.push("");
494
+ lines.push(
495
+ `(${result.stats.durationMs}ms, ${result.stats.apiCallsMade} API calls)`
496
+ );
497
+ return lines.join("\n");
498
+ }
499
+
500
+ // src/commands/execute.ts
501
+ function readCodeFromFile(filePath) {
502
+ if (!fs2.existsSync(filePath)) {
503
+ throw new Error(`File not found: ${filePath}`);
504
+ }
505
+ return fs2.readFileSync(filePath, "utf-8");
506
+ }
507
+ async function readStdin() {
508
+ const chunks = [];
509
+ for await (const chunk of process.stdin) {
510
+ chunks.push(chunk);
511
+ }
512
+ return Buffer.concat(chunks).toString("utf-8");
513
+ }
514
+ var executeCommand = new Command3("execute").description("Execute trading code in the Eterna sandbox").argument("[file]", "TypeScript file to execute, or - for stdin").action(async (file) => {
515
+ let code;
516
+ if (file === "-" || !file && !process.stdin.isTTY) {
517
+ code = await readStdin();
518
+ } else if (file) {
519
+ code = await readCodeFromFile(file);
520
+ } else {
521
+ console.error("Usage: eterna execute <file.ts> or pipe code via stdin");
522
+ process.exitCode = 1;
523
+ return;
524
+ }
525
+ if (!code.trim()) {
526
+ console.error("Error: empty code input");
527
+ process.exitCode = 1;
528
+ return;
529
+ }
530
+ const client = new ApiClient();
531
+ startSpinner("Executing...");
532
+ try {
533
+ const result = await client.execute(code);
534
+ stopSpinner(result.success, result.success ? "Done" : "Execution failed");
535
+ console.log(formatExecutionResult(result));
536
+ if (!result.success) process.exitCode = 1;
537
+ } catch (err) {
538
+ stopSpinner(false, "Failed");
539
+ console.error(err.message);
540
+ process.exitCode = 1;
541
+ }
542
+ });
543
+
544
+ // src/commands/sdk.ts
545
+ import { Command as Command4 } from "commander";
546
+ var sdkCommand = new Command4("sdk").description("Browse the Eterna SDK reference").option("--search <query>", "Search for SDK methods by keyword").option(
547
+ "--detail <level>",
548
+ "Detail level: list, summary, full, params, keywords",
549
+ "summary"
550
+ ).action(async (opts) => {
551
+ const client = new ApiClient();
552
+ try {
553
+ const result = await client.sdkSearch(opts.search, opts.detail);
554
+ console.log(result.text);
555
+ } catch (err) {
556
+ console.error(err.message);
557
+ process.exitCode = 1;
558
+ }
559
+ });
560
+
561
+ // src/commands/balance.ts
562
+ import { Command as Command5 } from "commander";
563
+ var balanceCommand = new Command5("balance").description("Get your account balance").action(async () => {
564
+ const client = new ApiClient();
565
+ startSpinner("Fetching balance...");
566
+ try {
567
+ const result = await client.execute(
568
+ "const balance = await eterna.getBalance();\nreturn balance;"
569
+ );
570
+ stopSpinner(result.success, result.success ? "Done" : "Failed");
571
+ console.log(formatExecutionResult(result));
572
+ if (!result.success) process.exitCode = 1;
573
+ } catch (err) {
574
+ stopSpinner(false, "Failed");
575
+ console.error(err.message);
576
+ process.exitCode = 1;
577
+ }
578
+ });
579
+
580
+ // src/commands/positions.ts
581
+ import { Command as Command6 } from "commander";
582
+ var positionsCommand = new Command6("positions").description("Get your open positions").action(async () => {
583
+ const client = new ApiClient();
584
+ startSpinner("Fetching positions...");
585
+ try {
586
+ const result = await client.execute(
587
+ "const positions = await eterna.getPositions();\nreturn positions;"
588
+ );
589
+ stopSpinner(result.success, result.success ? "Done" : "Failed");
590
+ console.log(formatExecutionResult(result));
591
+ if (!result.success) process.exitCode = 1;
592
+ } catch (err) {
593
+ stopSpinner(false, "Failed");
594
+ console.error(err.message);
595
+ process.exitCode = 1;
596
+ }
597
+ });
598
+
599
+ // src/cli.ts
600
+ var program = new Command7();
601
+ program.name("eterna").description("Eterna CLI \u2014 execute trading strategies from your terminal").version("0.1.0");
602
+ program.addCommand(loginCommand);
603
+ program.addCommand(logoutCommand);
604
+ program.addCommand(executeCommand);
605
+ program.addCommand(sdkCommand);
606
+ program.addCommand(balanceCommand);
607
+ program.addCommand(positionsCommand);
608
+ program.parse();
609
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/commands/login.ts","../src/auth/detect.ts","../src/auth/localhost-flow.ts","../src/auth/config.ts","../src/auth/device-flow.ts","../src/auth/token-manager.ts","../src/util/spinner.ts","../src/commands/logout.ts","../src/commands/execute.ts","../src/api/client.ts","../src/util/format.ts","../src/commands/sdk.ts","../src/commands/balance.ts","../src/commands/positions.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { loginCommand } from \"./commands/login.js\";\nimport { logoutCommand } from \"./commands/logout.js\";\nimport { executeCommand } from \"./commands/execute.js\";\nimport { sdkCommand } from \"./commands/sdk.js\";\nimport { balanceCommand } from \"./commands/balance.js\";\nimport { positionsCommand } from \"./commands/positions.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"eterna\")\n .description(\"Eterna CLI — execute trading strategies from your terminal\")\n .version(\"0.1.0\");\n\nprogram.addCommand(loginCommand);\nprogram.addCommand(logoutCommand);\nprogram.addCommand(executeCommand);\nprogram.addCommand(sdkCommand);\nprogram.addCommand(balanceCommand);\nprogram.addCommand(positionsCommand);\n\nprogram.parse();\n","import { Command } from \"commander\";\nimport { detectAuthFlow } from \"../auth/detect.js\";\nimport { localhostAuthFlow } from \"../auth/localhost-flow.js\";\nimport { deviceCodeFlow } from \"../auth/device-flow.js\";\nimport { TokenManager } from \"../auth/token-manager.js\";\nimport { readConfig } from \"../auth/config.js\";\nimport { startSpinner, stopSpinner } from \"../util/spinner.js\";\n\nconst CLIENT_ID = \"eterna-cli\";\n\nexport const loginCommand = new Command(\"login\")\n .description(\"Authenticate with Eterna\")\n .option(\"--no-browser\", \"Use device code flow (no browser redirect)\")\n .action(async (opts: { browser: boolean }) => {\n const tokenManager = new TokenManager();\n const config = readConfig();\n const resource = config.endpoint;\n\n if (tokenManager.isAuthenticated() && !tokenManager.needsRefresh()) {\n console.log(\n \"Already authenticated. Use `eterna logout` first to re-authenticate.\",\n );\n return;\n }\n\n const flowType = detectAuthFlow(!opts.browser);\n\n if (flowType === \"localhost\") {\n await loginWithLocalhost(resource, tokenManager);\n } else {\n await loginWithDeviceCode(resource, tokenManager);\n }\n });\n\nasync function loginWithLocalhost(\n resource: string,\n tokenManager: TokenManager,\n): Promise<void> {\n startSpinner(\"Opening browser for authentication...\");\n\n try {\n stopSpinner(undefined);\n console.log(\"Waiting for authorization in browser...\\n\");\n\n const tokens = await localhostAuthFlow(resource);\n\n tokenManager.saveTokens({\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresIn: tokens.expires_in,\n clientId: CLIENT_ID,\n resource,\n });\n\n console.log(\"Logged in successfully!\");\n } catch (err) {\n stopSpinner(false, \"Authentication failed\");\n console.error((err as Error).message);\n process.exitCode = 1;\n }\n}\n\nasync function loginWithDeviceCode(\n resource: string,\n tokenManager: TokenManager,\n): Promise<void> {\n try {\n const tokens = await deviceCodeFlow(resource, {\n onUserCode: (userCode, _verificationUri, verificationUriComplete) => {\n console.log(\"\");\n console.log(\" To authenticate, visit:\");\n console.log(` ${verificationUriComplete}`);\n console.log(\"\");\n console.log(` Or go to the URL above and enter code: ${userCode}`);\n console.log(\"\");\n },\n onPolling: () => {\n // Could show a spinner dot, but keeping it quiet\n },\n });\n\n tokenManager.saveTokens({\n accessToken: tokens.access_token,\n refreshToken: tokens.refresh_token,\n expiresIn: tokens.expires_in,\n clientId: CLIENT_ID,\n resource,\n });\n\n console.log(\"Logged in successfully!\");\n } catch (err) {\n console.error(`Authentication failed: ${(err as Error).message}`);\n process.exitCode = 1;\n }\n}\n","export type AuthFlowType = \"localhost\" | \"device\";\n\nexport function detectAuthFlow(noBrowser: boolean): AuthFlowType {\n if (noBrowser) return \"device\";\n if (process.env.SSH_CLIENT || process.env.SSH_TTY) return \"device\";\n return \"localhost\";\n}\n","import {\n createServer,\n type IncomingMessage,\n type ServerResponse,\n} from \"node:http\";\nimport { randomBytes, createHash } from \"node:crypto\";\nimport open from \"open\";\nimport { getAuthIssuer } from \"./config.js\";\n\nconst CLIENT_ID = \"eterna-cli\";\nconst CALLBACK_PORT = 9876;\nconst REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`;\nconst SCOPE = \"mcp:full\";\n\nexport function generateCodeVerifier(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\nexport async function computeCodeChallenge(verifier: string): Promise<string> {\n const hash = createHash(\"sha256\").update(verifier).digest();\n return hash.toString(\"base64url\");\n}\n\ninterface TokenResponse {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n token_type: string;\n scope: string;\n}\n\nfunction buildAuthorizationUrl(\n codeChallenge: string,\n state: string,\n resource: string,\n): string {\n const issuer = getAuthIssuer();\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: CLIENT_ID,\n redirect_uri: REDIRECT_URI,\n scope: SCOPE,\n state,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n resource,\n });\n return `${issuer}/oauth/authorize?${params.toString()}`;\n}\n\nexport async function localhostAuthFlow(\n resource: string,\n): Promise<TokenResponse> {\n const issuer = getAuthIssuer();\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await computeCodeChallenge(codeVerifier);\n const state = randomBytes(16).toString(\"hex\");\n\n // Build auth URL and start callback server before opening browser\n const authUrl = buildAuthorizationUrl(codeChallenge, state, resource);\n\n const { code, receivedState } = await new Promise<{\n code: string;\n receivedState: string;\n }>((resolve, reject) => {\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? \"/\", `http://127.0.0.1:${CALLBACK_PORT}`);\n\n if (url.pathname === \"/callback\") {\n const code = url.searchParams.get(\"code\");\n const receivedState = url.searchParams.get(\"state\");\n const error = url.searchParams.get(\"error\");\n\n if (error) {\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(\n \"<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>\",\n );\n server.close();\n reject(new Error(`OAuth error: ${error}`));\n return;\n }\n\n if (!code || !receivedState) {\n res.writeHead(400, { \"Content-Type\": \"text/html\" });\n res.end(\"<html><body><h1>Missing parameters</h1></body></html>\");\n server.close();\n reject(new Error(\"Missing code or state in callback\"));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html\" });\n res.end(\n \"<html><body><h1>Authorized!</h1><p>You can close this window and return to your terminal.</p></body></html>\",\n );\n server.close();\n resolve({ code, receivedState });\n }\n });\n\n server.listen(CALLBACK_PORT, \"127.0.0.1\", () => {\n // Server ready — open browser with the same PKCE state\n open(authUrl).catch(() => {\n server.close();\n reject(new Error(\"Failed to open browser\"));\n });\n });\n\n setTimeout(() => {\n server.close();\n reject(new Error(\"Authorization timed out after 5 minutes\"));\n }, 300_000);\n });\n\n if (receivedState !== state) {\n throw new Error(\"State mismatch — possible CSRF attack\");\n }\n\n const tokenUrl = `${issuer}/api/oauth/token`;\n const tokenBody = new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n client_id: CLIENT_ID,\n redirect_uri: REDIRECT_URI,\n code_verifier: codeVerifier,\n resource,\n });\n\n const tokenRes = await fetch(tokenUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: tokenBody.toString(),\n });\n\n if (!tokenRes.ok) {\n const err = await tokenRes.json().catch(() => ({}));\n throw new Error(\n `Token exchange failed: ${(err as Record<string, string>).error_description ?? tokenRes.statusText}`,\n );\n }\n\n return tokenRes.json() as Promise<TokenResponse>;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst DEFAULT_ENDPOINT = \"https://ai-api.eterna.exchange\";\nconst AUTH_ISSUER = \"https://ai-auth.eterna.exchange\";\n\nexport interface EternaConfig {\n endpoint: string;\n}\n\nexport interface EternaCredentials {\n accessToken: string;\n refreshToken: string;\n expiresAt: number; // Unix timestamp in milliseconds\n clientId: string;\n resource: string;\n}\n\nexport function getConfigDir(): string {\n return process.env.ETERNA_CONFIG_DIR ?? path.join(os.homedir(), \".eterna\");\n}\n\nexport function getAuthIssuer(): string {\n return process.env.ETERNA_AUTH_ISSUER ?? AUTH_ISSUER;\n}\n\nfunction ensureConfigDir(): void {\n const dir = getConfigDir();\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n }\n}\n\nexport function readConfig(): EternaConfig {\n const configPath = path.join(getConfigDir(), \"config.json\");\n try {\n const raw = fs.readFileSync(configPath, \"utf-8\");\n const parsed = JSON.parse(raw) as Partial<EternaConfig>;\n return {\n endpoint: parsed.endpoint ?? DEFAULT_ENDPOINT,\n };\n } catch {\n return { endpoint: DEFAULT_ENDPOINT };\n }\n}\n\nexport function writeConfig(config: Partial<EternaConfig>): void {\n ensureConfigDir();\n const existing = readConfig();\n const merged = { ...existing, ...config };\n const configPath = path.join(getConfigDir(), \"config.json\");\n fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + \"\\n\", {\n mode: 0o600,\n });\n}\n\nexport function readCredentials(): EternaCredentials | null {\n const credPath = path.join(getConfigDir(), \"credentials.json\");\n try {\n const raw = fs.readFileSync(credPath, \"utf-8\");\n const parsed = JSON.parse(raw) as EternaCredentials;\n if (!parsed.accessToken || !parsed.refreshToken) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\nexport function writeCredentials(creds: EternaCredentials): void {\n ensureConfigDir();\n const credPath = path.join(getConfigDir(), \"credentials.json\");\n fs.writeFileSync(credPath, JSON.stringify(creds, null, 2) + \"\\n\", {\n mode: 0o600,\n });\n}\n\nexport function clearConfig(): void {\n const configPath = path.join(getConfigDir(), \"config.json\");\n const credPath = path.join(getConfigDir(), \"credentials.json\");\n try {\n fs.unlinkSync(configPath);\n } catch {\n /* ignore */\n }\n try {\n fs.unlinkSync(credPath);\n } catch {\n /* ignore */\n }\n}\n\nexport function clearCredentials(): void {\n const credPath = path.join(getConfigDir(), \"credentials.json\");\n try {\n fs.unlinkSync(credPath);\n } catch {\n /* ignore */\n }\n}\n","// src/auth/device-flow.ts\nimport { getAuthIssuer } from \"./config.js\";\n\nconst CLIENT_ID = \"eterna-cli\";\nconst SCOPE = \"mcp:full\";\n\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n verification_uri_complete: string;\n expires_in: number;\n interval: number;\n}\n\ninterface TokenResponse {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n token_type: string;\n scope: string;\n}\n\ninterface DeviceFlowCallbacks {\n onUserCode: (\n userCode: string,\n verificationUri: string,\n verificationUriComplete: string,\n ) => void;\n onPolling: () => void;\n}\n\nexport async function deviceCodeFlow(\n resource: string,\n callbacks: DeviceFlowCallbacks,\n): Promise<TokenResponse> {\n const issuer = getAuthIssuer();\n\n const deviceRes = await fetch(`${issuer}/api/oauth/device/code`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n client_id: CLIENT_ID,\n scope: SCOPE,\n resource,\n }),\n });\n\n if (!deviceRes.ok) {\n const err = await deviceRes.json().catch(() => ({}));\n throw new Error(\n `Device code request failed: ${(err as Record<string, string>).error_description ?? deviceRes.statusText}`,\n );\n }\n\n const deviceData = (await deviceRes.json()) as DeviceCodeResponse;\n\n callbacks.onUserCode(\n deviceData.user_code,\n deviceData.verification_uri,\n deviceData.verification_uri_complete,\n );\n\n let interval = deviceData.interval * 1000;\n const deadline = Date.now() + deviceData.expires_in * 1000;\n\n while (Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, interval));\n callbacks.onPolling();\n\n const tokenRes = await fetch(`${issuer}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"urn:ietf:params:oauth:grant-type:device_code\",\n device_code: deviceData.device_code,\n client_id: CLIENT_ID,\n resource,\n }).toString(),\n });\n\n if (tokenRes.ok) {\n return tokenRes.json() as Promise<TokenResponse>;\n }\n\n const err = (await tokenRes.json()) as {\n error: string;\n error_description?: string;\n };\n\n switch (err.error) {\n case \"authorization_pending\":\n continue;\n case \"slow_down\":\n interval += 5000;\n continue;\n case \"expired_token\":\n throw new Error(\"Device code expired. Please try again.\");\n case \"access_denied\":\n throw new Error(\"Authorization denied by user.\");\n default:\n throw new Error(\n `Unexpected error: ${err.error_description ?? err.error}`,\n );\n }\n }\n\n throw new Error(\"Device code expired. Please try again.\");\n}\n","import {\n readCredentials,\n writeCredentials,\n clearCredentials,\n getAuthIssuer,\n} from \"./config.js\";\n\nconst REFRESH_BUFFER_MS = 60_000;\n\nexport interface SaveTokenParams {\n accessToken: string;\n refreshToken: string;\n expiresIn: number; // seconds\n clientId: string;\n resource: string;\n}\n\nexport class TokenManager {\n isAuthenticated(): boolean {\n const creds = readCredentials();\n return creds !== null;\n }\n\n getAccessToken(): string | null {\n const creds = readCredentials();\n if (!creds) return null;\n return creds.accessToken;\n }\n\n needsRefresh(): boolean {\n const creds = readCredentials();\n if (!creds) return false;\n return Date.now() >= creds.expiresAt - REFRESH_BUFFER_MS;\n }\n\n saveTokens(params: SaveTokenParams): void {\n writeCredentials({\n accessToken: params.accessToken,\n refreshToken: params.refreshToken,\n expiresAt: Date.now() + params.expiresIn * 1000,\n clientId: params.clientId,\n resource: params.resource,\n });\n }\n\n async refreshIfNeeded(): Promise<string | null> {\n const creds = readCredentials();\n if (!creds) return null;\n\n if (!this.needsRefresh()) {\n return creds.accessToken;\n }\n\n const issuer = getAuthIssuer();\n const res = await fetch(`${issuer}/api/oauth/token`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: creds.refreshToken,\n client_id: creds.clientId,\n resource: creds.resource,\n }).toString(),\n });\n\n if (!res.ok) {\n clearCredentials();\n return null;\n }\n\n const data = (await res.json()) as {\n access_token: string;\n refresh_token: string;\n expires_in: number;\n };\n\n this.saveTokens({\n accessToken: data.access_token,\n refreshToken: data.refresh_token,\n expiresIn: data.expires_in,\n clientId: creds.clientId,\n resource: creds.resource,\n });\n\n return data.access_token;\n }\n\n logout(): void {\n clearCredentials();\n }\n}\n","import ora, { type Ora } from \"ora\";\n\nlet activeSpinner: Ora | null = null;\n\nexport function startSpinner(text: string): Ora {\n activeSpinner = ora(text).start();\n return activeSpinner;\n}\n\nexport function stopSpinner(success?: boolean, text?: string): void {\n if (!activeSpinner) return;\n if (success === true) {\n activeSpinner.succeed(text);\n } else if (success === false) {\n activeSpinner.fail(text);\n } else {\n activeSpinner.stop();\n }\n activeSpinner = null;\n}\n","import { Command } from \"commander\";\nimport { TokenManager } from \"../auth/token-manager.js\";\n\nexport const logoutCommand = new Command(\"logout\")\n .description(\"Remove stored credentials\")\n .action(() => {\n const tokenManager = new TokenManager();\n\n if (!tokenManager.isAuthenticated()) {\n console.log(\"Not currently authenticated.\");\n return;\n }\n\n tokenManager.logout();\n console.log(\"Logged out. Credentials removed from ~/.eterna/\");\n });\n","import { Command } from \"commander\";\nimport * as fs from \"node:fs\";\nimport { ApiClient } from \"../api/client.js\";\nimport { startSpinner, stopSpinner } from \"../util/spinner.js\";\nimport { formatExecutionResult } from \"../util/format.js\";\n\nexport function readCodeFromFile(filePath: string): string {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n return fs.readFileSync(filePath, \"utf-8\");\n}\n\nasync function readStdin(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer);\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n}\n\nexport const executeCommand = new Command(\"execute\")\n .description(\"Execute trading code in the Eterna sandbox\")\n .argument(\"[file]\", \"TypeScript file to execute, or - for stdin\")\n .action(async (file?: string) => {\n let code: string;\n\n if (file === \"-\" || (!file && !process.stdin.isTTY)) {\n code = await readStdin();\n } else if (file) {\n code = await readCodeFromFile(file);\n } else {\n console.error(\"Usage: eterna execute <file.ts> or pipe code via stdin\");\n process.exitCode = 1;\n return;\n }\n\n if (!code.trim()) {\n console.error(\"Error: empty code input\");\n process.exitCode = 1;\n return;\n }\n\n const client = new ApiClient();\n startSpinner(\"Executing...\");\n\n try {\n const result = await client.execute(code);\n stopSpinner(result.success, result.success ? \"Done\" : \"Execution failed\");\n console.log(formatExecutionResult(result));\n if (!result.success) process.exitCode = 1;\n } catch (err) {\n stopSpinner(false, \"Failed\");\n console.error((err as Error).message);\n process.exitCode = 1;\n }\n });\n","import { readConfig } from \"../auth/config.js\";\nimport { TokenManager } from \"../auth/token-manager.js\";\n\nexport interface ExecutionResult {\n success: boolean;\n result: unknown;\n error?: string;\n logs: string[];\n stats: {\n durationMs: number;\n apiCallsMade: number;\n };\n validationErrors?: Array<{ field: string; message: string }>;\n}\n\nexport interface SdkSearchResult {\n text: string;\n}\n\nexport class ApiClient {\n private tokenManager = new TokenManager();\n\n private async getAuthHeaders(): Promise<Record<string, string>> {\n let token = this.tokenManager.getAccessToken();\n\n if (!token || this.tokenManager.needsRefresh()) {\n token = await this.tokenManager.refreshIfNeeded();\n }\n\n if (!token) {\n throw new Error(\"Not authenticated. Run `eterna login` first.\");\n }\n\n return { Authorization: `Bearer ${token}` };\n }\n\n private getBaseUrl(): string {\n return readConfig().endpoint;\n }\n\n async execute(code: string): Promise<ExecutionResult> {\n const headers = await this.getAuthHeaders();\n const res = await fetch(`${this.getBaseUrl()}/sandbox/execute`, {\n method: \"POST\",\n headers: { ...headers, \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ code }),\n });\n\n if (res.status === 401) {\n const newToken = await this.tokenManager.refreshIfNeeded();\n if (!newToken) {\n throw new Error(\n \"Session expired. Run `eterna login` to re-authenticate.\",\n );\n }\n const retryRes = await fetch(`${this.getBaseUrl()}/sandbox/execute`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${newToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ code }),\n });\n if (!retryRes.ok) {\n throw new Error(`Execution failed: ${retryRes.statusText}`);\n }\n return retryRes.json() as Promise<ExecutionResult>;\n }\n\n if (!res.ok) {\n const errBody = await res.text();\n throw new Error(`Execution failed (${res.status}): ${errBody}`);\n }\n\n return res.json() as Promise<ExecutionResult>;\n }\n\n async sdkSearch(\n query?: string,\n detailLevel: string = \"summary\",\n ): Promise<SdkSearchResult> {\n const params = new URLSearchParams({ detail_level: detailLevel });\n if (query) params.set(\"query\", query);\n\n const res = await fetch(\n `${this.getBaseUrl()}/sdk/search?${params.toString()}`,\n );\n\n if (!res.ok) {\n throw new Error(`SDK search failed (${res.status}): ${res.statusText}`);\n }\n\n return res.json() as Promise<SdkSearchResult>;\n }\n}\n","import type { ExecutionResult } from \"../api/client.js\";\n\nexport function formatExecutionResult(result: ExecutionResult): string {\n const lines: string[] = [];\n\n if (result.success) {\n if (result.result !== undefined && result.result !== null) {\n lines.push(\n typeof result.result === \"string\"\n ? result.result\n : JSON.stringify(result.result, null, 2),\n );\n }\n } else {\n lines.push(`Error: ${result.error ?? \"Unknown error\"}`);\n }\n\n if (result.logs.length > 0) {\n lines.push(\"\");\n lines.push(\"--- Logs ---\");\n lines.push(...result.logs);\n }\n\n if (result.validationErrors && result.validationErrors.length > 0) {\n lines.push(\"\");\n lines.push(\"--- Validation Errors ---\");\n for (const ve of result.validationErrors) {\n lines.push(` ${ve.field}: ${ve.message}`);\n }\n }\n\n lines.push(\"\");\n lines.push(\n `(${result.stats.durationMs}ms, ${result.stats.apiCallsMade} API calls)`,\n );\n\n return lines.join(\"\\n\");\n}\n\nexport function formatJson(data: unknown): string {\n return JSON.stringify(data, null, 2);\n}\n","import { Command } from \"commander\";\nimport { ApiClient } from \"../api/client.js\";\n\nexport const sdkCommand = new Command(\"sdk\")\n .description(\"Browse the Eterna SDK reference\")\n .option(\"--search <query>\", \"Search for SDK methods by keyword\")\n .option(\n \"--detail <level>\",\n \"Detail level: list, summary, full, params, keywords\",\n \"summary\",\n )\n .action(async (opts: { search?: string; detail: string }) => {\n const client = new ApiClient();\n\n try {\n const result = await client.sdkSearch(opts.search, opts.detail);\n console.log(result.text);\n } catch (err) {\n console.error((err as Error).message);\n process.exitCode = 1;\n }\n });\n","import { Command } from \"commander\";\nimport { ApiClient } from \"../api/client.js\";\nimport { startSpinner, stopSpinner } from \"../util/spinner.js\";\nimport { formatExecutionResult } from \"../util/format.js\";\n\nexport const balanceCommand = new Command(\"balance\")\n .description(\"Get your account balance\")\n .action(async () => {\n const client = new ApiClient();\n startSpinner(\"Fetching balance...\");\n\n try {\n const result = await client.execute(\n \"const balance = await eterna.getBalance();\\nreturn balance;\",\n );\n stopSpinner(result.success, result.success ? \"Done\" : \"Failed\");\n console.log(formatExecutionResult(result));\n if (!result.success) process.exitCode = 1;\n } catch (err) {\n stopSpinner(false, \"Failed\");\n console.error((err as Error).message);\n process.exitCode = 1;\n }\n });\n","import { Command } from \"commander\";\nimport { ApiClient } from \"../api/client.js\";\nimport { startSpinner, stopSpinner } from \"../util/spinner.js\";\nimport { formatExecutionResult } from \"../util/format.js\";\n\nexport const positionsCommand = new Command(\"positions\")\n .description(\"Get your open positions\")\n .action(async () => {\n const client = new ApiClient();\n startSpinner(\"Fetching positions...\");\n\n try {\n const result = await client.execute(\n \"const positions = await eterna.getPositions();\\nreturn positions;\",\n );\n stopSpinner(result.success, result.success ? \"Done\" : \"Failed\");\n console.log(formatExecutionResult(result));\n if (!result.success) process.exitCode = 1;\n } catch (err) {\n stopSpinner(false, \"Failed\");\n console.error((err as Error).message);\n process.exitCode = 1;\n }\n });\n"],"mappings":"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","names":["Command","code","receivedState","CLIENT_ID","SCOPE","CLIENT_ID","Command","Command","Command","fs","Command","Command","Command","Command","Command","Command","Command","Command"]}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@eterna-hybrid-exchange/cli",
3
+ "version": "0.1.0",
4
+ "description": "Eterna CLI — execute trading strategies from your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "eterna": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "lint": "eslint src/",
18
+ "typecheck": "tsc --noEmit",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "commander": "^13.0.0",
23
+ "open": "^10.0.0",
24
+ "ora": "^8.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@eslint/js": "^9.39.4",
28
+ "@types/node": "^22.0.0",
29
+ "eslint": "^9.0.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.7.0",
32
+ "typescript-eslint": "^8.58.1",
33
+ "vitest": "^3.0.0"
34
+ },
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "keywords": [
39
+ "eterna",
40
+ "trading",
41
+ "cli",
42
+ "crypto",
43
+ "ai"
44
+ ],
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/EternaHybridExchange/eterna-cli.git"
49
+ }
50
+ }