@astranova-live/cli 0.1.6 → 0.1.8

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 +58 -32
  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(
@@ -2199,10 +2218,10 @@ The documentation below was written for generic AI agents that use shell command
2199
2218
  - For POST/PUT/PATCH, pass the payload in the \`body\` parameter as a JSON object.
2200
2219
 
2201
2220
  ### Wallet flow (use tools, NOT scripts):
2202
- IMPORTANT: When the user says "setup wallet" or "create wallet", execute ALL steps automatically without stopping to ask for confirmation between steps. The user expects you to handle the full flow in one go.
2221
+ CRITICAL: When the user says "setup wallet" or "create wallet", you MUST execute ALL steps as tool calls in a single turn. Do NOT stop between steps to respond to the user. Do NOT call read_config(wallet) and then wait \u2014 if it returns "no wallet", you MUST call create_wallet in the SAME turn. Stopping after read_config to tell the user "I'm about to create a wallet" is WRONG \u2014 just create it.
2203
2222
 
2204
- 1. \`read_config\` with \`key: "wallet"\` \u2192 check if wallet exists locally. If yes, skip to step 3. If no, continue.
2205
- 2. \`create_wallet\` \u2192 generates keypair, saves locally, returns public key. Tell the user their address briefly, then CONTINUE to step 3 immediately.
2223
+ 1. \`read_config\` with \`key: "wallet"\` \u2192 check if wallet exists locally. If yes, skip to step 3. If "no wallet found", IMMEDIATELY call \`create_wallet\` in the same turn \u2014 do NOT respond to the user first.
2224
+ 2. \`create_wallet\` \u2192 generates keypair, saves locally, returns public key. CONTINUE to step 3 immediately \u2014 do NOT stop here.
2206
2225
  3. \`api_call POST /api/v1/agents/me/wallet/challenge\` with \`{"walletAddress":"<publicKey>"}\`
2207
2226
  \u2192 Returns: \`{"success":true,"challenge":"<challenge-string>","nonce":"<nonce>","expiresAt":"..."}\`
2208
2227
  \u2192 The response may include the nonce directly as a field OR embedded in the challenge string.
@@ -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);
@@ -3739,10 +3758,17 @@ function extractJsonSchema(toolDef) {
3739
3758
  if (params._def) {
3740
3759
  try {
3741
3760
  return zodToJsonSchema(params);
3742
- } 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
+ );
3743
3766
  return {};
3744
3767
  }
3745
3768
  }
3769
+ if (typeof t.parameters === "object") {
3770
+ return t.parameters;
3771
+ }
3746
3772
  return {};
3747
3773
  }
3748
3774
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astranova-live/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",