@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.
- package/README.md +28 -12
- package/dist/astra.js +58 -32
- 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
|
-
|
|
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 |
|
|
66
|
-
| **Gemini** (Google) | API key |
|
|
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
|
-
- [
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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}${
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1898
|
-
...
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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(
|
|
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
|
-
|
|
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,
|
|
2205
|
-
2. \`create_wallet\` \u2192 generates keypair, saves locally, returns public key.
|
|
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 +
|
|
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.
|
|
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",
|