@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.
- package/README.md +28 -12
- package/dist/astra.js +91 -42
- 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(
|
|
@@ -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);
|
|
@@ -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
|
-
|
|
3557
|
-
|
|
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
|
-
|
|
3590
|
-
|
|
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(`
|
|
3647
|
+
debugLog3(`SDK turn error: ${message}`);
|
|
3607
3648
|
throw error;
|
|
3608
3649
|
} finally {
|
|
3609
|
-
clearTimeout(
|
|
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.
|
|
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",
|