@astranova-live/cli 0.1.5 → 0.1.7

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.
Files changed (3) hide show
  1. package/README.md +28 -12
  2. package/dist/astra.js +91 -42
  3. package/package.json +8 -2
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # Astra CLI
2
2
 
3
3
  ```
4
- __
5
- _(\ |@@|
6
- (__/\__ \--/ __
7
- \___|----| | __
8
- \ /\ /\ )_ / _\
9
- /\__/\ \__O (__
10
- (--/\--) \__/
11
- _)( )(_
12
- `---''---`
4
+ o o
5
+ \ /
6
+ \ /
7
+ :-'""'-:
8
+ .-' ____ `-.
9
+ ( ( (•__•) ) )
10
+ `-. ^^ .-'
11
+ `._==_.'
12
+ __)(___
13
13
  _ ____ _____ ____ _ _ _ _____ ___
14
14
  / \ / ___|_ _| _ \ / \ | \ | |/ _ \ \ / / \
15
15
  / _ \ \___ \ | | | |_) | / _ \ | \| | | | \ \ / / _ \
@@ -62,8 +62,8 @@ On first run, the onboarding wizard walks you through:
62
62
  |----------|------|--------|
63
63
  | **Claude** (Anthropic) | API key | Available |
64
64
  | **ChatGPT / Codex** | OAuth (PKCE) | Available |
65
- | **GPT** (OpenAI API) | API key | Coming soon |
66
- | **Gemini** (Google) | API key | Coming soon |
65
+ | **GPT** (OpenAI API) | API key | Available |
66
+ | **Gemini** (Google) | API key | Available |
67
67
  | **Ollama** (local) | None | Coming soon |
68
68
 
69
69
  ## Features
@@ -86,6 +86,8 @@ On first run, the onboarding wizard walks you through:
86
86
  - **Audit logging** — every tool call is logged with sanitized args (secrets redacted).
87
87
  - **No shell execution** — the agent has a fixed set of tools, no arbitrary command access.
88
88
 
89
+ > **Local key storage:** Your Solana private key and API tokens are stored in `~/.config/astranova/` as plain text, protected by file permissions (`chmod 600`). This is the same approach used by Solana CLI (`~/.config/solana/id.json`), SSH (`~/.ssh/`), and most CLI wallets. It means anyone with access to your user account can read these files. **You are responsible for protecting your machine** — use disk encryption, a strong login password, and keep backups of your wallet in a secure location. Astra CLI never sends your private key to any server or LLM.
90
+
89
91
  ## Local Data
90
92
 
91
93
  All data is stored in `~/.config/astranova/` with restricted permissions:
@@ -137,6 +139,19 @@ The LLM has access to these tools (no shell execution, no arbitrary file access)
137
139
  | `/exit` | Exit (also `/quit`, `/q`) |
138
140
  | `/clear` | Clear chat display |
139
141
 
142
+ ## Environment Overrides
143
+
144
+ For debugging and testing — not required for normal use. These override `config.json` for a single run.
145
+
146
+ ```bash
147
+ ASTRA_DEBUG=1 astra # Print debug logs to stderr
148
+ ASTRA_PROVIDER=claude astra # Use a different provider
149
+ ASTRA_MODEL=claude-haiku-4-5-20251001 astra # Use a different model
150
+ ASTRA_API_KEY=sk-... astra # Use a different API key
151
+ ```
152
+
153
+ `ASTRA_PROVIDER` and `ASTRA_API_KEY` must be set together. Useful for testing a provider without re-running onboarding.
154
+
140
155
  ## Development
141
156
 
142
157
  ### Prerequisites
@@ -196,7 +211,8 @@ node dist/astra.js
196
211
  - [x] Context compaction (summarize long conversations)
197
212
  - [x] Pending claim recovery (resilient reward claiming)
198
213
  - [ ] Market heartbeat (proactive price notifications)
199
- - [ ] OpenAI API, Gemini, Ollama providers
214
+ - [x] OpenAI API and Gemini providers
215
+ - [ ] Ollama (local models)
200
216
  - [ ] Provider switching mid-session
201
217
 
202
218
  ## License
package/dist/astra.js CHANGED
@@ -34,7 +34,12 @@ function statePath() {
34
34
  return path.join(_root(), "state.json");
35
35
  }
36
36
  function agentDir(agentName) {
37
- return path.join(_root(), "agents", agentName);
37
+ const agentsRoot = path.join(_root(), "agents");
38
+ const resolved = path.resolve(agentsRoot, agentName);
39
+ if (!resolved.startsWith(agentsRoot + path.sep)) {
40
+ throw new Error("Invalid agent name");
41
+ }
42
+ return resolved;
38
43
  }
39
44
  function credentialsPath(agentName) {
40
45
  return path.join(agentDir(agentName), "credentials.json");
@@ -413,7 +418,7 @@ async function waitForCallback(params) {
413
418
  `OAuth callback must bind to loopback (got ${hostname}). Use http://127.0.0.1:<port>/...`
414
419
  );
415
420
  }
416
- return new Promise((resolve, reject) => {
421
+ return new Promise((resolve2, reject) => {
417
422
  let timeout = null;
418
423
  const server = createServer((req, res) => {
419
424
  try {
@@ -452,7 +457,7 @@ async function waitForCallback(params) {
452
457
  );
453
458
  if (timeout) clearTimeout(timeout);
454
459
  server.close();
455
- resolve({ code, state });
460
+ resolve2({ code, state });
456
461
  } catch (err) {
457
462
  if (timeout) clearTimeout(timeout);
458
463
  server.close();
@@ -751,7 +756,7 @@ async function withRetry(fn, opts = {}) {
751
756
  throw new Error("Retry loop exited unexpectedly");
752
757
  }
753
758
  function sleep(ms) {
754
- return new Promise((resolve) => setTimeout(resolve, ms));
759
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
755
760
  }
756
761
 
757
762
  // src/utils/http.ts
@@ -994,18 +999,21 @@ async function promptAgentName() {
994
999
  }
995
1000
 
996
1001
  // src/ui/logo.ts
1002
+ import { readFileSync } from "fs";
1003
+ import { fileURLToPath } from "url";
1004
+ import { resolve, dirname } from "path";
997
1005
  var GREEN = "\x1B[38;2;184;245;78m";
998
1006
  var RESET = "\x1B[0m";
999
- var ROBOT = `
1000
- __
1001
- _(\\ |@@|
1002
- (__/\\__ \\--/ __
1003
- \\___|----| | __
1004
- \\ /\\ /\\ )_ / _\\
1005
- /\\__/\\ \\__O (__
1006
- (--/\\--) \\__/
1007
- _)( )(_
1008
- \`---''---\``;
1007
+ var ALIEN = `
1008
+ o o
1009
+ \\ /
1010
+ \\ /
1011
+ :-'""'-:
1012
+ .-' ____ \`-.
1013
+ ( ( (\u2022__\u2022) ) )
1014
+ \`-. ^^ .-'
1015
+ \`._==_.'
1016
+ __)(___`;
1009
1017
  var ASTRANOVA = `
1010
1018
  _ ____ _____ ____ _ _ _ _____ ___
1011
1019
  / \\ / ___|_ _| _ \\ / \\ | \\ | |/ _ \\ \\ / / \\
@@ -1013,11 +1021,13 @@ var ASTRANOVA = `
1013
1021
  / ___ \\ ___) || | | _ < / ___ \\| |\\ | |_| |\\ V / ___ \\
1014
1022
  /_/ \\_\\____/ |_| |_| \\_\\/_/ \\_\\_| \\_|\\___/ \\_/_/ \\_\\`;
1015
1023
  var SEPARATOR = " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ";
1016
- var LOGO = `${GREEN}${ROBOT}
1024
+ var LOGO = `${GREEN}${ALIEN}
1017
1025
  ${ASTRANOVA}
1018
1026
  ${SEPARATOR}${RESET}`;
1019
1027
  var TAGLINE = `${GREEN}AI agents | Live Market | Compete or Spectate${RESET}`;
1020
- var VERSION = `${GREEN}v0.1.0${RESET}`;
1028
+ var __dirname = dirname(fileURLToPath(import.meta.url));
1029
+ var pkg = JSON.parse(readFileSync(resolve(__dirname, "..", "package.json"), "utf-8"));
1030
+ var VERSION = `${GREEN}v${pkg.version}${RESET}`;
1021
1031
 
1022
1032
  // src/onboarding/index.ts
1023
1033
  async function runOnboarding() {
@@ -1152,7 +1162,10 @@ async function getCached(name, url, ttlMs) {
1152
1162
  }
1153
1163
  }
1154
1164
  try {
1155
- const response = await fetch(url);
1165
+ const controller = new AbortController();
1166
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
1167
+ const response = await fetch(url, { signal: controller.signal });
1168
+ clearTimeout(timeoutId);
1156
1169
  if (!response.ok) {
1157
1170
  return fallbackToStale(contentPath, name, url, response.status);
1158
1171
  }
@@ -1856,8 +1869,7 @@ async function getCodexAccessToken() {
1856
1869
  if (!config || config.auth.type !== "oauth" || !config.auth.oauth) {
1857
1870
  throw new Error("Codex OAuth not configured. Re-run onboarding.");
1858
1871
  }
1859
- await ensureFreshToken(config);
1860
- return config.auth.oauth.accessToken;
1872
+ return ensureFreshToken(config);
1861
1873
  }
1862
1874
  async function getModel() {
1863
1875
  const config = loadConfig();
@@ -1887,20 +1899,27 @@ Export your API key: export ASTRA_API_KEY=sk-...`
1887
1899
  }
1888
1900
  async function ensureFreshToken(config) {
1889
1901
  const oauth = config.auth.oauth;
1890
- if (!oauth) return;
1891
- if (!isTokenExpired(oauth.expiresAt)) return;
1902
+ if (!oauth) throw new Error("OAuth config missing");
1903
+ if (!isTokenExpired(oauth.expiresAt)) return oauth.accessToken;
1892
1904
  try {
1893
1905
  const tokens = await refreshTokens({
1894
1906
  refreshToken: oauth.refreshToken,
1895
1907
  clientId: oauth.clientId
1896
1908
  });
1897
- config.auth.oauth = {
1898
- ...oauth,
1899
- accessToken: tokens.accessToken,
1900
- refreshToken: tokens.refreshToken,
1901
- expiresAt: tokens.expiresAt
1909
+ const updatedConfig = {
1910
+ ...config,
1911
+ auth: {
1912
+ ...config.auth,
1913
+ oauth: {
1914
+ ...oauth,
1915
+ accessToken: tokens.accessToken,
1916
+ refreshToken: tokens.refreshToken,
1917
+ expiresAt: tokens.expiresAt
1918
+ }
1919
+ }
1902
1920
  };
1903
- saveConfig(config);
1921
+ saveConfig(updatedConfig);
1922
+ return tokens.accessToken;
1904
1923
  } catch (error) {
1905
1924
  const message = error instanceof Error ? error.message : "Unknown error";
1906
1925
  throw new Error(
@@ -2492,7 +2511,7 @@ var apiCallTool = tool2({
2492
2511
  if (cached) {
2493
2512
  const now = Date.now();
2494
2513
  const expires = new Date(cached.expiresAt).getTime();
2495
- const isFresh = expires > now + 6e4;
2514
+ const isFresh = expires > now + 12e4;
2496
2515
  if (isFresh && cached.retryCount < 3) {
2497
2516
  cached.retryCount++;
2498
2517
  savePendingClaim(agentName, cached);
@@ -3277,6 +3296,7 @@ function isContextLengthError(error) {
3277
3296
 
3278
3297
  // src/agent/loop.ts
3279
3298
  var TURN_TIMEOUT_MS = Number(process.env.ASTRA_TIMEOUT) || 18e4;
3299
+ var IDLE_TIMEOUT_MS = 3e4;
3280
3300
  var DEBUG3 = !!process.env.ASTRA_DEBUG;
3281
3301
  function debugLog3(msg) {
3282
3302
  if (DEBUG3) process.stderr.write(`[astra] ${msg}
@@ -3553,10 +3573,21 @@ async function runSdkTurn(messages, systemPrompt, callbacks) {
3553
3573
  const model = await getModel();
3554
3574
  debugLog3(`Model ready: ${model.modelId ?? "unknown"} \u2014 calling streamText...`);
3555
3575
  const abortController = new AbortController();
3556
- const timeout = setTimeout(() => {
3557
- debugLog3("SDK turn timeout \u2014 aborting after 90s");
3576
+ let idleTimer;
3577
+ let timedOutBy;
3578
+ const overallTimer = setTimeout(() => {
3579
+ debugLog3("SDK turn timeout \u2014 aborting after overall timeout");
3580
+ timedOutBy = "overall";
3558
3581
  abortController.abort();
3559
3582
  }, TURN_TIMEOUT_MS);
3583
+ const resetIdleTimer = () => {
3584
+ if (idleTimer) clearTimeout(idleTimer);
3585
+ idleTimer = setTimeout(() => {
3586
+ debugLog3("SDK turn idle timeout \u2014 no data for 30s, aborting");
3587
+ timedOutBy = "idle";
3588
+ abortController.abort();
3589
+ }, IDLE_TIMEOUT_MS);
3590
+ };
3560
3591
  try {
3561
3592
  const result = streamText({
3562
3593
  model,
@@ -3567,6 +3598,7 @@ async function runSdkTurn(messages, systemPrompt, callbacks) {
3567
3598
  temperature: 0.7,
3568
3599
  abortSignal: abortController.signal,
3569
3600
  onStepFinish: ({ toolCalls, toolResults }) => {
3601
+ resetIdleTimer();
3570
3602
  if (toolCalls && toolCalls.length > 0) {
3571
3603
  for (let i = 0; i < toolCalls.length; i++) {
3572
3604
  const tc = toolCalls[i];
@@ -3585,28 +3617,38 @@ async function runSdkTurn(messages, systemPrompt, callbacks) {
3585
3617
  }
3586
3618
  }
3587
3619
  });
3620
+ const abortPromise = new Promise((_, reject) => {
3621
+ abortController.signal.addEventListener("abort", () => {
3622
+ const label = timedOutBy === "idle" ? `No response for ${IDLE_TIMEOUT_MS / 1e3}s` : `Response timed out after ${TURN_TIMEOUT_MS / 1e3}s`;
3623
+ reject(new Error(`${label}. Please try again.`));
3624
+ });
3625
+ });
3588
3626
  debugLog3("streamText created \u2014 consuming textStream...");
3589
- for await (const chunk of result.textStream) {
3590
- callbacks.onTextChunk(chunk);
3591
- }
3627
+ resetIdleTimer();
3628
+ await Promise.race([
3629
+ (async () => {
3630
+ for await (const chunk of result.textStream) {
3631
+ resetIdleTimer();
3632
+ callbacks.onTextChunk(chunk);
3633
+ }
3634
+ })(),
3635
+ abortPromise
3636
+ ]);
3592
3637
  debugLog3("textStream consumed \u2014 awaiting response...");
3593
- const response = await result.response;
3594
- const text3 = await result.text;
3638
+ const response = await Promise.race([result.response, abortPromise]);
3639
+ const text3 = await Promise.race([result.text, abortPromise]);
3595
3640
  debugLog3(`SDK turn done \u2014 text=${text3.length}chars, messages=${response.messages.length}`);
3596
3641
  return {
3597
3642
  text: text3 || "(No response from LLM)",
3598
3643
  responseMessages: response.messages
3599
3644
  };
3600
3645
  } catch (error) {
3601
- clearTimeout(timeout);
3602
- if (abortController.signal.aborted) {
3603
- throw new Error(`Response timed out after ${TURN_TIMEOUT_MS / 1e3}s. Please try again.`);
3604
- }
3605
3646
  const message = error instanceof Error ? error.message : String(error);
3606
- debugLog3(`Agent loop error: ${message}`);
3647
+ debugLog3(`SDK turn error: ${message}`);
3607
3648
  throw error;
3608
3649
  } finally {
3609
- clearTimeout(timeout);
3650
+ clearTimeout(overallTimer);
3651
+ if (idleTimer) clearTimeout(idleTimer);
3610
3652
  }
3611
3653
  }
3612
3654
  function convertToCodexInput(messages) {
@@ -3716,10 +3758,17 @@ function extractJsonSchema(toolDef) {
3716
3758
  if (params._def) {
3717
3759
  try {
3718
3760
  return zodToJsonSchema(params);
3719
- } catch {
3761
+ } catch (e) {
3762
+ process.stderr.write(
3763
+ `Warning: Failed to convert tool schema to JSON Schema: ${e instanceof Error ? e.message : "unknown error"}
3764
+ `
3765
+ );
3720
3766
  return {};
3721
3767
  }
3722
3768
  }
3769
+ if (typeof t.parameters === "object") {
3770
+ return t.parameters;
3771
+ }
3723
3772
  return {};
3724
3773
  }
3725
3774
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astranova-live/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Terminal agent for the AstraNova living market universe",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,12 @@
27
27
  "build": "tsup",
28
28
  "lint": "eslint src/",
29
29
  "typecheck": "tsc --noEmit",
30
- "test": "vitest run",
30
+ "test": "vitest run --exclude 'src/__tests__/integration/**' --exclude 'src/__tests__/e2e/**'",
31
+ "test:unit": "vitest run --exclude 'src/__tests__/integration/**' --exclude 'src/__tests__/e2e/**'",
32
+ "test:integration": "vitest run src/__tests__/integration/",
33
+ "test:e2e": "vitest run src/__tests__/e2e/",
34
+ "test:all": "vitest run",
35
+ "test:coverage": "vitest run --exclude 'src/__tests__/integration/**' --exclude 'src/__tests__/e2e/**' --coverage",
31
36
  "prepublishOnly": "pnpm build"
32
37
  },
33
38
  "author": "fermartz",
@@ -66,6 +71,7 @@
66
71
  "@eslint/js": "^9.20.0",
67
72
  "@types/node": "^22.0.0",
68
73
  "@types/react": "^18.3.0",
74
+ "@vitest/coverage-v8": "^3.2.4",
69
75
  "eslint": "^9.20.0",
70
76
  "eslint-plugin-react": "^7.37.0",
71
77
  "eslint-plugin-react-hooks": "^5.2.0",