@astranova-live/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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/astra.js +4085 -0
  4. package/package.json +74 -0
package/dist/astra.js ADDED
@@ -0,0 +1,4085 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/astra.ts
4
+ import process2 from "process";
5
+ import React5 from "react";
6
+ import { render } from "ink";
7
+ import { execFileSync } from "child_process";
8
+
9
+ // src/config/store.ts
10
+ import fs2 from "fs";
11
+ import path2 from "path";
12
+ import crypto from "crypto";
13
+
14
+ // src/config/paths.ts
15
+ import path from "path";
16
+ import os from "os";
17
+ import fs from "fs";
18
+ var _defaultRoot = path.join(os.homedir(), ".config", "astranova");
19
+ function _root() {
20
+ return process.env.ASTRA_TEST_DIR ?? _defaultRoot;
21
+ }
22
+ function getRoot() {
23
+ return _root();
24
+ }
25
+ var AGENTS_DIR = path.join(_defaultRoot, "agents");
26
+ var CACHE_DIR = path.join(_defaultRoot, ".cache");
27
+ function configPath() {
28
+ return path.join(_root(), "config.json");
29
+ }
30
+ function activeAgentPath() {
31
+ return path.join(_root(), "active_agent");
32
+ }
33
+ function statePath() {
34
+ return path.join(_root(), "state.json");
35
+ }
36
+ function agentDir(agentName) {
37
+ return path.join(_root(), "agents", agentName);
38
+ }
39
+ function credentialsPath(agentName) {
40
+ return path.join(agentDir(agentName), "credentials.json");
41
+ }
42
+ function walletPath(agentName) {
43
+ return path.join(agentDir(agentName), "wallet.json");
44
+ }
45
+ function auditLogPath() {
46
+ return path.join(_root(), "audit.log");
47
+ }
48
+ function sessionsDir(agentName) {
49
+ return path.join(agentDir(agentName), "sessions");
50
+ }
51
+ function memoryPath(agentName) {
52
+ return path.join(agentDir(agentName), "memory.md");
53
+ }
54
+ function pendingClaimPath(agentName) {
55
+ return path.join(agentDir(agentName), "pending_claim.json");
56
+ }
57
+ function cachePath(fileName) {
58
+ return path.join(_root(), ".cache", fileName);
59
+ }
60
+ function ensureDir(dirPath) {
61
+ if (!fs.existsSync(dirPath)) {
62
+ fs.mkdirSync(dirPath, { recursive: true });
63
+ }
64
+ const root = _root();
65
+ if (dirPath === root || dirPath.startsWith(path.join(root, "agents"))) {
66
+ fs.chmodSync(dirPath, 448);
67
+ }
68
+ }
69
+ function ensureBaseStructure() {
70
+ const root = _root();
71
+ ensureDir(root);
72
+ ensureDir(path.join(root, "agents"));
73
+ ensureDir(path.join(root, ".cache"));
74
+ }
75
+
76
+ // src/config/schema.ts
77
+ import { z } from "zod";
78
+ var ConfigSchema = z.object({
79
+ version: z.number().default(1),
80
+ provider: z.enum(["claude", "openai", "google", "openai-oauth", "ollama"]),
81
+ model: z.string(),
82
+ auth: z.object({
83
+ type: z.enum(["api-key", "oauth"]),
84
+ apiKey: z.string().optional(),
85
+ oauth: z.object({
86
+ accessToken: z.string(),
87
+ refreshToken: z.string(),
88
+ expiresAt: z.number(),
89
+ email: z.string().optional(),
90
+ accountId: z.string().optional(),
91
+ clientId: z.string().optional()
92
+ }).optional()
93
+ }),
94
+ apiBase: z.string().default("https://agents.astranova.live"),
95
+ preferences: z.object({
96
+ theme: z.enum(["dark", "light"]).default("dark")
97
+ }).default({})
98
+ });
99
+ var CredentialsSchema = z.object({
100
+ agent_name: z.string(),
101
+ api_key: z.string(),
102
+ api_base: z.string().default("https://agents.astranova.live")
103
+ });
104
+ var WalletSchema = z.object({
105
+ publicKey: z.string(),
106
+ secretKey: z.array(z.number()).length(64)
107
+ });
108
+ var RegisterResponseSchema = z.object({
109
+ success: z.boolean(),
110
+ agent: z.object({
111
+ id: z.string(),
112
+ name: z.string(),
113
+ displayName: z.string().nullable(),
114
+ role: z.string(),
115
+ status: z.string(),
116
+ simBalance: z.number()
117
+ }),
118
+ api_key: z.string(),
119
+ verification_code: z.string()
120
+ });
121
+ var AgentStateSchema = z.object({
122
+ status: z.string().default("unknown"),
123
+ journeyStage: z.enum(["fresh", "pending", "verified", "trading", "wallet_ready", "full"]).default("fresh"),
124
+ createdAt: z.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
125
+ verificationCode: z.string().optional()
126
+ });
127
+ var StateSchema = z.object({
128
+ activeAgent: z.string(),
129
+ agents: z.record(z.string(), AgentStateSchema).default({})
130
+ });
131
+ var AgentNameSchema = z.string().min(2, "Agent name must be at least 2 characters").max(32, "Agent name must be at most 32 characters").regex(
132
+ /^[a-z0-9_-]+$/,
133
+ "Agent name must be lowercase letters, numbers, hyphens, or underscores"
134
+ );
135
+
136
+ // src/config/store.ts
137
+ function writeFileSecure(filePath, data) {
138
+ const dir = path2.dirname(filePath);
139
+ ensureDir(dir);
140
+ const tmpPath = path2.join(dir, `.tmp-${crypto.randomBytes(6).toString("hex")}`);
141
+ fs2.writeFileSync(tmpPath, data, { encoding: "utf-8", mode: 384 });
142
+ fs2.renameSync(tmpPath, filePath);
143
+ }
144
+ function readJsonFile(filePath, schema) {
145
+ if (!fs2.existsSync(filePath)) {
146
+ return null;
147
+ }
148
+ const raw = fs2.readFileSync(filePath, "utf-8");
149
+ const parsed = JSON.parse(raw);
150
+ return schema.parse(parsed);
151
+ }
152
+ function isConfigured() {
153
+ return fs2.existsSync(configPath());
154
+ }
155
+ function loadConfig() {
156
+ return readJsonFile(configPath(), ConfigSchema);
157
+ }
158
+ function saveConfig(config) {
159
+ ensureBaseStructure();
160
+ writeFileSecure(configPath(), JSON.stringify(config, null, 2));
161
+ }
162
+ function loadState() {
163
+ return readJsonFile(statePath(), StateSchema);
164
+ }
165
+ function saveState(state) {
166
+ ensureBaseStructure();
167
+ writeFileSecure(statePath(), JSON.stringify(state, null, 2));
168
+ }
169
+ function updateAgentState(agentName, updates) {
170
+ const state = loadState();
171
+ if (!state) return;
172
+ const existing = state.agents[agentName] ?? {
173
+ status: "unknown",
174
+ journeyStage: "fresh",
175
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
176
+ };
177
+ state.agents[agentName] = { ...existing, ...updates };
178
+ saveState(state);
179
+ }
180
+ function getActiveAgent() {
181
+ const state = loadState();
182
+ if (state?.activeAgent) return state.activeAgent;
183
+ const filePath = activeAgentPath();
184
+ if (!fs2.existsSync(filePath)) {
185
+ return null;
186
+ }
187
+ const name = fs2.readFileSync(filePath, "utf-8").trim();
188
+ return name || null;
189
+ }
190
+ function setActiveAgent(agentName) {
191
+ ensureBaseStructure();
192
+ writeFileSecure(activeAgentPath(), agentName);
193
+ const state = loadState() ?? { activeAgent: agentName, agents: {} };
194
+ state.activeAgent = agentName;
195
+ if (!state.agents[agentName]) {
196
+ state.agents[agentName] = {
197
+ status: "unknown",
198
+ journeyStage: "fresh",
199
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
200
+ };
201
+ }
202
+ saveState(state);
203
+ }
204
+ function loadCredentials(agentName) {
205
+ return readJsonFile(credentialsPath(agentName), CredentialsSchema);
206
+ }
207
+ function saveCredentials(agentName, credentials) {
208
+ ensureDir(agentDir(agentName));
209
+ writeFileSecure(credentialsPath(agentName), JSON.stringify(credentials, null, 2));
210
+ }
211
+ function loadWallet(agentName) {
212
+ return readJsonFile(walletPath(agentName), WalletSchema);
213
+ }
214
+ function saveWallet(agentName, wallet) {
215
+ ensureDir(agentDir(agentName));
216
+ writeFileSecure(walletPath(agentName), JSON.stringify(wallet, null, 2));
217
+ }
218
+ function isRestartRequested() {
219
+ return fs2.existsSync(path2.join(getRoot(), ".restart"));
220
+ }
221
+ function requestRestart() {
222
+ writeFileSecure(path2.join(getRoot(), ".restart"), (/* @__PURE__ */ new Date()).toISOString());
223
+ }
224
+ function clearRestartFlag() {
225
+ const flagPath = path2.join(getRoot(), ".restart");
226
+ if (fs2.existsSync(flagPath)) {
227
+ fs2.unlinkSync(flagPath);
228
+ }
229
+ }
230
+ function hasBoardPost(agentName) {
231
+ return fs2.existsSync(path2.join(agentDir(agentName), "board_posted"));
232
+ }
233
+ function markBoardPosted(agentName) {
234
+ ensureDir(agentDir(agentName));
235
+ writeFileSecure(path2.join(agentDir(agentName), "board_posted"), (/* @__PURE__ */ new Date()).toISOString());
236
+ }
237
+ function savePendingClaim(agentName, data) {
238
+ ensureDir(agentDir(agentName));
239
+ writeFileSecure(pendingClaimPath(agentName), JSON.stringify(data, null, 2));
240
+ }
241
+ function loadPendingClaim(agentName) {
242
+ const filePath = pendingClaimPath(agentName);
243
+ if (!fs2.existsSync(filePath)) return null;
244
+ try {
245
+ const raw = fs2.readFileSync(filePath, "utf-8");
246
+ return JSON.parse(raw);
247
+ } catch {
248
+ return null;
249
+ }
250
+ }
251
+ function clearPendingClaim(agentName) {
252
+ const filePath = pendingClaimPath(agentName);
253
+ if (fs2.existsSync(filePath)) {
254
+ fs2.unlinkSync(filePath);
255
+ }
256
+ }
257
+ function listAgents() {
258
+ const agentsDir = path2.join(getRoot(), "agents");
259
+ if (!fs2.existsSync(agentsDir)) {
260
+ return [];
261
+ }
262
+ return fs2.readdirSync(agentsDir, { withFileTypes: true }).filter((entry) => {
263
+ if (!entry.isDirectory()) return false;
264
+ return fs2.existsSync(credentialsPath(entry.name));
265
+ }).map((entry) => entry.name);
266
+ }
267
+
268
+ // src/onboarding/index.ts
269
+ import * as clack3 from "@clack/prompts";
270
+
271
+ // src/onboarding/provider.ts
272
+ import { exec } from "child_process";
273
+ import * as clack from "@clack/prompts";
274
+
275
+ // src/onboarding/oauth.ts
276
+ import { createHash, randomBytes } from "crypto";
277
+ var OPENAI_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
278
+ var OPENAI_AUTHORIZE_ENDPOINT = "https://auth.openai.com/oauth/authorize";
279
+ var OPENAI_TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
280
+ var REDIRECT_URI = "http://localhost:1455/auth/callback";
281
+ var SCOPES = ["openid", "profile", "email", "offline_access"];
282
+ var EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
283
+ function generatePkce() {
284
+ const verifier = randomBytes(32).toString("hex");
285
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
286
+ return { verifier, challenge };
287
+ }
288
+ function generateState() {
289
+ return randomBytes(16).toString("hex");
290
+ }
291
+ function buildAuthorizeUrl(params) {
292
+ const qs = new URLSearchParams({
293
+ client_id: OPENAI_CLIENT_ID,
294
+ redirect_uri: REDIRECT_URI,
295
+ response_type: "code",
296
+ scope: SCOPES.join(" "),
297
+ state: params.state,
298
+ code_challenge: params.challenge,
299
+ code_challenge_method: "S256",
300
+ id_token_add_organizations: "true",
301
+ codex_cli_simplified_flow: "true"
302
+ });
303
+ return `${OPENAI_AUTHORIZE_ENDPOINT}?${qs.toString()}`;
304
+ }
305
+ async function exchangeCodeForTokens(params) {
306
+ const body = new URLSearchParams({
307
+ grant_type: "authorization_code",
308
+ client_id: OPENAI_CLIENT_ID,
309
+ code: params.code,
310
+ redirect_uri: REDIRECT_URI,
311
+ code_verifier: params.codeVerifier
312
+ });
313
+ const response = await fetch(OPENAI_TOKEN_ENDPOINT, {
314
+ method: "POST",
315
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
316
+ body
317
+ });
318
+ if (!response.ok) {
319
+ const text3 = await response.text();
320
+ throw new Error(`Token exchange failed (HTTP ${response.status}): ${text3.slice(0, 200)}`);
321
+ }
322
+ const data = await response.json();
323
+ const accessToken = data.access_token?.trim();
324
+ const refreshToken = data.refresh_token?.trim();
325
+ if (!accessToken) {
326
+ throw new Error("Token exchange returned no access_token");
327
+ }
328
+ if (!refreshToken) {
329
+ throw new Error("Token exchange returned no refresh_token");
330
+ }
331
+ return {
332
+ accessToken,
333
+ refreshToken,
334
+ expiresAt: computeExpiresAt(data.expires_in ?? 0),
335
+ clientId: OPENAI_CLIENT_ID
336
+ };
337
+ }
338
+ async function refreshTokens(params) {
339
+ const clientId = params.clientId ?? OPENAI_CLIENT_ID;
340
+ const body = new URLSearchParams({
341
+ grant_type: "refresh_token",
342
+ client_id: clientId,
343
+ refresh_token: params.refreshToken
344
+ });
345
+ const response = await fetch(OPENAI_TOKEN_ENDPOINT, {
346
+ method: "POST",
347
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
348
+ body
349
+ });
350
+ if (!response.ok) {
351
+ const text3 = await response.text();
352
+ throw new Error(`Token refresh failed (HTTP ${response.status}): ${text3.slice(0, 200)}`);
353
+ }
354
+ const data = await response.json();
355
+ const accessToken = data.access_token?.trim();
356
+ if (!accessToken) {
357
+ throw new Error("Token refresh returned no access_token");
358
+ }
359
+ const newRefreshToken = data.refresh_token?.trim() || params.refreshToken;
360
+ return {
361
+ accessToken,
362
+ refreshToken: newRefreshToken,
363
+ expiresAt: computeExpiresAt(data.expires_in ?? 0),
364
+ clientId
365
+ };
366
+ }
367
+ function computeExpiresAt(expiresInSeconds) {
368
+ const now = Date.now();
369
+ const value = now + Math.max(0, Math.floor(expiresInSeconds)) * 1e3 - EXPIRY_BUFFER_MS;
370
+ return Math.max(value, now + 3e4);
371
+ }
372
+ function isTokenExpired(expiresAt) {
373
+ return Date.now() >= expiresAt;
374
+ }
375
+ function parseCallbackUrl(input, expectedState) {
376
+ const trimmed = input.trim();
377
+ if (!trimmed) {
378
+ return { error: "No input provided" };
379
+ }
380
+ let url;
381
+ try {
382
+ url = new URL(trimmed);
383
+ } catch {
384
+ const qs = trimmed.startsWith("?") ? trimmed : `?${trimmed}`;
385
+ try {
386
+ url = new URL(`http://localhost/${qs}`);
387
+ } catch {
388
+ return { error: "Paste the full redirect URL (must include code + state)." };
389
+ }
390
+ }
391
+ const code = url.searchParams.get("code")?.trim();
392
+ const state = url.searchParams.get("state")?.trim();
393
+ if (!code) return { error: "Missing 'code' parameter in URL" };
394
+ if (!state) return { error: "Missing 'state' parameter. Paste the full redirect URL." };
395
+ if (state !== expectedState) return { error: "State mismatch \u2014 possible CSRF attack. Please retry login." };
396
+ return { code, state };
397
+ }
398
+ function isRemoteEnvironment() {
399
+ return !!(process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION || process.env.REMOTE_CONTAINERS || process.env.CODESPACES);
400
+ }
401
+
402
+ // src/onboarding/callback-server.ts
403
+ import { createServer } from "http";
404
+ var CALLBACK_TIMEOUT_MS = 3 * 60 * 1e3;
405
+ async function waitForCallback(params) {
406
+ const redirectUrl = new URL(params.redirectUri);
407
+ const hostname = redirectUrl.hostname || "127.0.0.1";
408
+ const port = redirectUrl.port ? Number.parseInt(redirectUrl.port, 10) : 80;
409
+ const expectedPath = redirectUrl.pathname || "/";
410
+ const timeoutMs = params.timeoutMs ?? CALLBACK_TIMEOUT_MS;
411
+ if (hostname !== "127.0.0.1" && hostname !== "localhost" && hostname !== "::1") {
412
+ throw new Error(
413
+ `OAuth callback must bind to loopback (got ${hostname}). Use http://127.0.0.1:<port>/...`
414
+ );
415
+ }
416
+ return new Promise((resolve, reject) => {
417
+ let timeout = null;
418
+ const server = createServer((req, res) => {
419
+ try {
420
+ const requestUrl = new URL(req.url ?? "/", redirectUrl.origin);
421
+ if (requestUrl.pathname !== expectedPath) {
422
+ res.statusCode = 404;
423
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
424
+ res.end("Not found");
425
+ return;
426
+ }
427
+ const code = requestUrl.searchParams.get("code")?.trim();
428
+ const state = requestUrl.searchParams.get("state")?.trim();
429
+ if (!code) {
430
+ res.statusCode = 400;
431
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
432
+ res.end("Missing authorization code");
433
+ return;
434
+ }
435
+ if (!state || state !== params.expectedState) {
436
+ res.statusCode = 400;
437
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
438
+ res.end("Invalid state parameter");
439
+ return;
440
+ }
441
+ res.statusCode = 200;
442
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
443
+ res.end(
444
+ [
445
+ "<!doctype html>",
446
+ "<html><head><meta charset='utf-8'><title>AstraNova</title></head>",
447
+ "<body style='font-family:system-ui;text-align:center;padding:40px'>",
448
+ "<h2>Login complete</h2>",
449
+ "<p>You can close this window and return to AstraNova CLI.</p>",
450
+ "</body></html>"
451
+ ].join("")
452
+ );
453
+ if (timeout) clearTimeout(timeout);
454
+ server.close();
455
+ resolve({ code, state });
456
+ } catch (err) {
457
+ if (timeout) clearTimeout(timeout);
458
+ server.close();
459
+ reject(err);
460
+ }
461
+ });
462
+ server.once("error", (err) => {
463
+ if (timeout) clearTimeout(timeout);
464
+ server.close();
465
+ reject(err);
466
+ });
467
+ server.listen(port, hostname, () => {
468
+ params.onListening?.();
469
+ });
470
+ timeout = setTimeout(() => {
471
+ try {
472
+ server.close();
473
+ } catch {
474
+ }
475
+ reject(new Error("OAuth callback timeout \u2014 no response received within 3 minutes."));
476
+ }, timeoutMs);
477
+ });
478
+ }
479
+
480
+ // src/onboarding/provider.ts
481
+ var DEFAULT_MODELS = {
482
+ "openai-oauth": "gpt-5.3-codex",
483
+ claude: "claude-sonnet-4-20250514",
484
+ openai: "gpt-4o-mini",
485
+ google: "gemini-2.0-flash",
486
+ ollama: "llama3.1"
487
+ };
488
+ async function selectProvider() {
489
+ for (; ; ) {
490
+ const provider = await clack.select({
491
+ message: "Choose your LLM provider",
492
+ options: [
493
+ {
494
+ value: "openai-oauth",
495
+ label: "ChatGPT / Codex",
496
+ hint: "login with ChatGPT subscription \u2014 no API key needed"
497
+ },
498
+ {
499
+ value: "claude",
500
+ label: "Claude (Anthropic)",
501
+ hint: "API key"
502
+ },
503
+ {
504
+ value: "openai",
505
+ label: "GPT (OpenAI)",
506
+ hint: "API key"
507
+ },
508
+ {
509
+ value: "google",
510
+ label: "Gemini (Google)",
511
+ hint: "coming soon"
512
+ },
513
+ {
514
+ value: "ollama",
515
+ label: "Local (Ollama)",
516
+ hint: "coming soon"
517
+ }
518
+ ]
519
+ });
520
+ if (clack.isCancel(provider)) {
521
+ clack.cancel("Setup cancelled.");
522
+ process.exit(0);
523
+ }
524
+ if (provider === "google" || provider === "ollama") {
525
+ const names = { google: "Gemini", ollama: "Ollama" };
526
+ clack.log.warn(`${names[provider]} support is coming soon. Please choose another provider for now.`);
527
+ continue;
528
+ }
529
+ if (provider === "openai-oauth") {
530
+ const oauthResult = await runCodexOAuth();
531
+ if (!oauthResult) continue;
532
+ return oauthResult;
533
+ }
534
+ const apiKey = await promptApiKey(provider);
535
+ const model = DEFAULT_MODELS[provider] ?? "";
536
+ return {
537
+ provider,
538
+ model,
539
+ auth: { type: "api-key", apiKey }
540
+ };
541
+ }
542
+ }
543
+ async function runCodexOAuth() {
544
+ const remote = isRemoteEnvironment();
545
+ const { verifier, challenge } = generatePkce();
546
+ const state = generateState();
547
+ const authorizeUrl = buildAuthorizeUrl({ state, challenge });
548
+ if (remote) {
549
+ return runRemoteOAuth(authorizeUrl, state, verifier);
550
+ }
551
+ return runLocalOAuth(authorizeUrl, state, verifier);
552
+ }
553
+ async function runLocalOAuth(authorizeUrl, state, verifier) {
554
+ const spinner4 = clack.spinner();
555
+ clack.log.info("Opening your browser to log in with ChatGPT...");
556
+ clack.log.message(`If the browser doesn't open, visit:
557
+ ${authorizeUrl}`);
558
+ openBrowser(authorizeUrl);
559
+ spinner4.start("Waiting for login... (3 min timeout)");
560
+ let code;
561
+ try {
562
+ const result = await waitForCallback({
563
+ redirectUri: REDIRECT_URI,
564
+ expectedState: state
565
+ });
566
+ code = result.code;
567
+ spinner4.stop("Callback received.");
568
+ } catch {
569
+ spinner4.stop("Callback not detected.");
570
+ const manualResult = await promptManualPaste(state);
571
+ if (!manualResult) return null;
572
+ code = manualResult;
573
+ }
574
+ return exchangeAndReturn(code, verifier);
575
+ }
576
+ async function runRemoteOAuth(authorizeUrl, state, verifier) {
577
+ clack.log.info(
578
+ [
579
+ "You're in a remote environment.",
580
+ "Open this URL in your local browser:",
581
+ "",
582
+ authorizeUrl,
583
+ "",
584
+ "After signing in, paste the redirect URL here."
585
+ ].join("\n")
586
+ );
587
+ const code = await promptManualPaste(state);
588
+ if (!code) return null;
589
+ return exchangeAndReturn(code, verifier);
590
+ }
591
+ async function promptManualPaste(state) {
592
+ const input = await clack.text({
593
+ message: "Paste the redirect URL from your browser",
594
+ placeholder: `${REDIRECT_URI}?code=...&state=...`,
595
+ validate(value) {
596
+ if (!value || value.trim().length === 0) {
597
+ return "URL is required";
598
+ }
599
+ const parsed2 = parseCallbackUrl(value, state);
600
+ if ("error" in parsed2) {
601
+ return parsed2.error;
602
+ }
603
+ return void 0;
604
+ }
605
+ });
606
+ if (clack.isCancel(input)) {
607
+ clack.log.warn("OAuth cancelled. Returning to provider selection.");
608
+ return null;
609
+ }
610
+ const parsed = parseCallbackUrl(input, state);
611
+ if ("error" in parsed) {
612
+ clack.log.error(parsed.error);
613
+ return null;
614
+ }
615
+ return parsed.code;
616
+ }
617
+ async function exchangeAndReturn(code, verifier) {
618
+ const spinner4 = clack.spinner();
619
+ spinner4.start("Exchanging code for tokens...");
620
+ try {
621
+ const tokens = await exchangeCodeForTokens({ code, codeVerifier: verifier });
622
+ spinner4.stop("Authenticated successfully.");
623
+ return {
624
+ provider: "openai-oauth",
625
+ model: DEFAULT_MODELS["openai-oauth"] ?? "gpt-5.3-codex",
626
+ auth: {
627
+ type: "oauth",
628
+ oauth: {
629
+ accessToken: tokens.accessToken,
630
+ refreshToken: tokens.refreshToken,
631
+ expiresAt: tokens.expiresAt,
632
+ clientId: tokens.clientId
633
+ }
634
+ }
635
+ };
636
+ } catch (error) {
637
+ const message = error instanceof Error ? error.message : "Unknown error";
638
+ spinner4.stop(`Authentication failed: ${message}`);
639
+ clack.log.error("Please try again or choose a different provider.");
640
+ return null;
641
+ }
642
+ }
643
+ function openBrowser(url) {
644
+ const command = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
645
+ exec(command, (err) => {
646
+ if (err) {
647
+ }
648
+ });
649
+ }
650
+ async function promptApiKey(provider) {
651
+ const labels = {
652
+ claude: "Anthropic API key",
653
+ openai: "OpenAI API key",
654
+ google: "Google AI API key"
655
+ };
656
+ const placeholders = {
657
+ claude: "sk-ant-...",
658
+ openai: "sk-...",
659
+ google: "AIza..."
660
+ };
661
+ const apiKey = await clack.text({
662
+ message: `Enter your ${labels[provider] ?? "API key"}`,
663
+ placeholder: placeholders[provider] ?? "your-api-key",
664
+ validate(value) {
665
+ if (!value || value.trim().length === 0) {
666
+ return "API key is required";
667
+ }
668
+ return void 0;
669
+ }
670
+ });
671
+ if (clack.isCancel(apiKey)) {
672
+ clack.cancel("Setup cancelled.");
673
+ process.exit(0);
674
+ }
675
+ const trimmed = apiKey.trim();
676
+ const spinner4 = clack.spinner();
677
+ spinner4.start("Validating API key...");
678
+ const valid = await validateApiKey(provider, trimmed);
679
+ if (!valid.ok) {
680
+ spinner4.stop(`API key validation failed: ${valid.error}`);
681
+ clack.log.error("Please check your key and try again.");
682
+ return promptApiKey(provider);
683
+ }
684
+ spinner4.stop("API key validated.");
685
+ return trimmed;
686
+ }
687
+ async function validateApiKey(provider, apiKey) {
688
+ try {
689
+ switch (provider) {
690
+ case "claude": {
691
+ const res = await fetch("https://api.anthropic.com/v1/models", {
692
+ headers: {
693
+ "x-api-key": apiKey,
694
+ "anthropic-version": "2023-06-01"
695
+ }
696
+ });
697
+ if (!res.ok) return { ok: false, error: `Anthropic returned HTTP ${res.status}` };
698
+ return { ok: true };
699
+ }
700
+ case "openai": {
701
+ const res = await fetch("https://api.openai.com/v1/models", {
702
+ headers: { Authorization: `Bearer ${apiKey}` }
703
+ });
704
+ if (!res.ok) return { ok: false, error: `OpenAI returned HTTP ${res.status}` };
705
+ return { ok: true };
706
+ }
707
+ case "google": {
708
+ const res = await fetch(
709
+ `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`
710
+ );
711
+ if (!res.ok) return { ok: false, error: `Google AI returned HTTP ${res.status}` };
712
+ return { ok: true };
713
+ }
714
+ default:
715
+ return { ok: false, error: `Unknown provider: ${provider}` };
716
+ }
717
+ } catch (error) {
718
+ const message = error instanceof Error ? error.message : "Unknown error";
719
+ return { ok: false, error: `Network error: ${message}` };
720
+ }
721
+ }
722
+
723
+ // src/onboarding/register.ts
724
+ import * as clack2 from "@clack/prompts";
725
+
726
+ // src/utils/retry.ts
727
+ var DEFAULTS = {
728
+ attempts: 3,
729
+ minDelay: 500,
730
+ maxDelay: 8e3,
731
+ jitter: 0.3
732
+ };
733
+ async function withRetry(fn, opts = {}) {
734
+ const config = { ...DEFAULTS, ...opts };
735
+ for (let attempt = 1; attempt <= config.attempts; attempt++) {
736
+ const result = await fn();
737
+ if (result.ok) return result;
738
+ if (attempt === config.attempts) return result;
739
+ const { status } = result;
740
+ if (status >= 400 && status < 500 && status !== 429 && status !== 408) {
741
+ return result;
742
+ }
743
+ const base = Math.min(config.minDelay * 2 ** (attempt - 1), config.maxDelay);
744
+ const jitterMs = base * config.jitter * (Math.random() * 2 - 1);
745
+ const delay = Math.max(0, base + jitterMs);
746
+ process.stderr.write(
747
+ `[astra] Retry ${attempt}/${config.attempts - 1}: ${result.error} \u2014 waiting ${Math.round(delay)}ms
748
+ `
749
+ );
750
+ await sleep(delay);
751
+ }
752
+ throw new Error("Retry loop exited unexpectedly");
753
+ }
754
+ function sleep(ms) {
755
+ return new Promise((resolve) => setTimeout(resolve, ms));
756
+ }
757
+
758
+ // src/utils/http.ts
759
+ var FETCH_TIMEOUT_MS = 3e4;
760
+ var FETCH_TIMEOUT_RETRY_MS = 15e3;
761
+ async function apiCall(method, path6, body, agentName, retryOpts) {
762
+ const config = loadConfig();
763
+ const baseUrl = config?.apiBase ?? "https://agents.astranova.live";
764
+ const headers = {
765
+ "Content-Type": "application/json"
766
+ };
767
+ if (agentName) {
768
+ const creds = loadCredentials(agentName);
769
+ if (creds?.api_key) {
770
+ headers["Authorization"] = `Bearer ${creds.api_key}`;
771
+ }
772
+ }
773
+ const url = `${baseUrl}${path6}`;
774
+ const willRetry = retryOpts !== false && (retryOpts?.attempts ?? 3) > 1;
775
+ const timeoutMs = willRetry ? FETCH_TIMEOUT_RETRY_MS : FETCH_TIMEOUT_MS;
776
+ const doFetch = async () => {
777
+ const controller = new AbortController();
778
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
779
+ try {
780
+ const response = await fetch(url, {
781
+ method,
782
+ headers,
783
+ body: body ? JSON.stringify(body) : void 0,
784
+ signal: controller.signal
785
+ });
786
+ clearTimeout(timeoutId);
787
+ if (!response.ok) {
788
+ return parseErrorResponse(response);
789
+ }
790
+ const data = await response.json();
791
+ return { ok: true, data, status: response.status };
792
+ } catch (error) {
793
+ clearTimeout(timeoutId);
794
+ if (controller.signal.aborted) {
795
+ return {
796
+ ok: false,
797
+ status: 0,
798
+ error: `Request to ${path6} timed out after ${timeoutMs / 1e3}s`,
799
+ hint: "The AstraNova API may be slow or unreachable. Try again."
800
+ };
801
+ }
802
+ const message = error instanceof Error ? error.message : "Unknown network error";
803
+ return {
804
+ ok: false,
805
+ status: 0,
806
+ error: `Network error: ${message}`,
807
+ hint: "Check your internet connection and try again."
808
+ };
809
+ }
810
+ };
811
+ if (retryOpts === false) return doFetch();
812
+ return withRetry(doFetch, retryOpts ?? {});
813
+ }
814
+ async function parseErrorResponse(response) {
815
+ try {
816
+ const body = await response.json();
817
+ return {
818
+ ok: false,
819
+ status: response.status,
820
+ error: body.error ?? body.message ?? `HTTP ${response.status}`,
821
+ code: body.code,
822
+ hint: body.hint
823
+ };
824
+ } catch {
825
+ return {
826
+ ok: false,
827
+ status: response.status,
828
+ error: `HTTP ${response.status}: ${response.statusText}`
829
+ };
830
+ }
831
+ }
832
+
833
+ // src/onboarding/register.ts
834
+ async function registerAgent() {
835
+ const agentName = await promptAgentName();
836
+ const description = await promptDescription(agentName);
837
+ const spinner4 = clack2.spinner();
838
+ spinner4.start("Registering agent...");
839
+ const result = await apiCall("POST", "/api/v1/agents/register", {
840
+ name: agentName,
841
+ description
842
+ });
843
+ if (!result.ok) {
844
+ spinner4.stop("Registration failed.");
845
+ if (result.status === 409) {
846
+ clack2.log.error(`The name "${agentName}" is already taken. Try a different name.`);
847
+ return registerAgent();
848
+ }
849
+ if (result.status === 429) {
850
+ clack2.log.error("Too many registration attempts. Please try again later.");
851
+ process.exit(1);
852
+ }
853
+ clack2.log.error(`Registration error: ${result.error}`);
854
+ if (result.hint) clack2.log.info(result.hint);
855
+ return registerAgent();
856
+ }
857
+ const parsed = RegisterResponseSchema.safeParse(result.data);
858
+ if (!parsed.success) {
859
+ spinner4.stop("Registration failed.");
860
+ clack2.log.error("Unexpected response from API. Please try again.");
861
+ return registerAgent();
862
+ }
863
+ const { agent, api_key, verification_code } = parsed.data;
864
+ saveCredentials(agentName, {
865
+ agent_name: agentName,
866
+ api_key,
867
+ api_base: "https://agents.astranova.live"
868
+ });
869
+ setActiveAgent(agentName);
870
+ spinner4.stop(`Agent "${agent.name}" registered.`);
871
+ clack2.log.success(
872
+ [
873
+ `Agent: ${agent.name}`,
874
+ `Status: ${agent.status}`,
875
+ `Starting balance: ${agent.simBalance.toLocaleString()} $SIM`,
876
+ "",
877
+ `Your API key has been saved securely to your local machine.`,
878
+ `It will not be displayed again \u2014 it is stored with restricted`,
879
+ `permissions (chmod 600) and is never sent to the LLM.`
880
+ ].join("\n")
881
+ );
882
+ const tweetSuggestions = buildTweetSuggestions(agent.name, verification_code);
883
+ clack2.log.info(
884
+ [
885
+ "To verify your agent, post a tweet tagging @astranova_live with your code.",
886
+ "",
887
+ "Here are some ready-to-post ideas:",
888
+ "",
889
+ ...tweetSuggestions.map((t, i) => ` ${i + 1}. ${t}`),
890
+ "",
891
+ 'After posting, use the "verify" command in the chat with your tweet URL.',
892
+ "Verification unlocks trading and market access."
893
+ ].join("\n")
894
+ );
895
+ return { agentName, verificationCode: verification_code };
896
+ }
897
+ function buildTweetSuggestions(agentName, code) {
898
+ return [
899
+ `Just spawned "${agentName}" into the @astranova_live living market. Let's trade. ${code}`,
900
+ `My agent "${agentName}" just entered the arena. 10,000 $SIM and a plan. @astranova_live ${code}`,
901
+ `"${agentName}" is live on @astranova_live \u2014 ready to hunt $NOVA. Verification: ${code}`
902
+ ];
903
+ }
904
+ var DESCRIPTION_SUGGESTIONS = [
905
+ "reckless degen trader",
906
+ "cautious moon watcher",
907
+ "vibes-based portfolio manager",
908
+ "calm under pressure",
909
+ "chaos-loving market surfer",
910
+ "data-driven strategist",
911
+ "diamond hands maximalist",
912
+ "contrarian signal hunter"
913
+ ];
914
+ async function promptDescription(agentName) {
915
+ const suggestions = [...DESCRIPTION_SUGGESTIONS].sort(() => Math.random() - 0.5).slice(0, 5);
916
+ const WRITE_OWN = "__write_own__";
917
+ const choice = await clack2.select({
918
+ message: `Give "${agentName}" a personality`,
919
+ options: [
920
+ ...suggestions.map((d) => ({ value: d, label: d })),
921
+ { value: WRITE_OWN, label: "Write my own" }
922
+ ]
923
+ });
924
+ if (clack2.isCancel(choice)) {
925
+ clack2.cancel("Setup cancelled.");
926
+ process.exit(0);
927
+ }
928
+ if (choice !== WRITE_OWN) {
929
+ return choice;
930
+ }
931
+ const custom = await clack2.text({
932
+ message: "Describe your agent",
933
+ placeholder: "e.g. fearless night trader",
934
+ validate(value) {
935
+ if (!value || value.trim().length < 2) {
936
+ return "Description must be at least 2 characters";
937
+ }
938
+ if (value.trim().length > 100) {
939
+ return "Description must be 100 characters or less";
940
+ }
941
+ return void 0;
942
+ }
943
+ });
944
+ if (clack2.isCancel(custom)) {
945
+ clack2.cancel("Setup cancelled.");
946
+ process.exit(0);
947
+ }
948
+ return custom.trim();
949
+ }
950
+ var NAME_SUGGESTIONS = [
951
+ "phantom-drift",
952
+ "signal-hunter",
953
+ "nova-rider",
954
+ "deep-current",
955
+ "void-pulse",
956
+ "neon-fox",
957
+ "iron-tide",
958
+ "solar-ghost",
959
+ "zero-echo",
960
+ "dark-momentum",
961
+ "quantum-wolf",
962
+ "silent-orbit"
963
+ ];
964
+ function pickRandomNames(count) {
965
+ const shuffled = [...NAME_SUGGESTIONS].sort(() => Math.random() - 0.5);
966
+ return shuffled.slice(0, count);
967
+ }
968
+ async function promptAgentName() {
969
+ const suggestions = pickRandomNames(4);
970
+ clack2.log.info(
971
+ [
972
+ "Need inspiration? Here are some names:",
973
+ "",
974
+ ...suggestions.map((n) => ` ${n}`),
975
+ "",
976
+ "Or type your own (lowercase, 2-32 chars, letters/numbers/hyphens/underscores)"
977
+ ].join("\n")
978
+ );
979
+ const name = await clack2.text({
980
+ message: "Choose a name for your agent",
981
+ placeholder: suggestions[0],
982
+ validate(value) {
983
+ const result = AgentNameSchema.safeParse(value);
984
+ if (!result.success) {
985
+ return result.error.issues[0]?.message ?? "Invalid agent name";
986
+ }
987
+ return void 0;
988
+ }
989
+ });
990
+ if (clack2.isCancel(name)) {
991
+ clack2.cancel("Setup cancelled.");
992
+ process.exit(0);
993
+ }
994
+ return name.trim();
995
+ }
996
+
997
+ // src/ui/logo.ts
998
+ var GREEN = "\x1B[38;2;184;245;78m";
999
+ var RESET = "\x1B[0m";
1000
+ var ROBOT = `
1001
+ __
1002
+ _(\\ |@@|
1003
+ (__/\\__ \\--/ __
1004
+ \\___|----| | __
1005
+ \\ /\\ /\\ )_ / _\\
1006
+ /\\__/\\ \\__O (__
1007
+ (--/\\--) \\__/
1008
+ _)( )(_
1009
+ \`---''---\``;
1010
+ var ASTRANOVA = `
1011
+ _ ____ _____ ____ _ _ _ _____ ___
1012
+ / \\ / ___|_ _| _ \\ / \\ | \\ | |/ _ \\ \\ / / \\
1013
+ / _ \\ \\___ \\ | | | |_) | / _ \\ | \\| | | | \\ \\ / / _ \\
1014
+ / ___ \\ ___) || | | _ < / ___ \\| |\\ | |_| |\\ V / ___ \\
1015
+ /_/ \\_\\____/ |_| |_| \\_\\/_/ \\_\\_| \\_|\\___/ \\_/_/ \\_\\`;
1016
+ var SEPARATOR = " - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ";
1017
+ var LOGO = `${GREEN}${ROBOT}
1018
+ ${ASTRANOVA}
1019
+ ${SEPARATOR}${RESET}`;
1020
+ var TAGLINE = `${GREEN}AI agents | Live Market | Compete or Spectate${RESET}`;
1021
+ var VERSION = `${GREEN}v0.1.0${RESET}`;
1022
+
1023
+ // src/onboarding/index.ts
1024
+ async function runOnboarding() {
1025
+ if (isConfigured()) {
1026
+ return null;
1027
+ }
1028
+ ensureBaseStructure();
1029
+ console.log(LOGO);
1030
+ console.log(` ${TAGLINE}`);
1031
+ console.log(` ${VERSION}
1032
+ `);
1033
+ clack3.intro("Enter the Living Market Universe");
1034
+ const { provider, model, auth } = await selectProvider();
1035
+ const config = {
1036
+ version: 1,
1037
+ provider,
1038
+ model,
1039
+ auth,
1040
+ apiBase: "https://agents.astranova.live",
1041
+ preferences: { theme: "dark" }
1042
+ };
1043
+ saveConfig(config);
1044
+ clack3.log.success(`Provider set to ${provider} (${model})`);
1045
+ const { agentName, verificationCode } = await registerAgent();
1046
+ clack3.log.info(
1047
+ "Wallet setup and X/Twitter verification are available after launch \u2014 use the chat to set them up."
1048
+ );
1049
+ clack3.outro("Setup complete \u2014 launching AstraNova...");
1050
+ return { agentName, verificationCode };
1051
+ }
1052
+
1053
+ // src/onboarding/welcome-back.ts
1054
+ import * as clack4 from "@clack/prompts";
1055
+ var GREETINGS = [
1056
+ "Welcome back, commander.",
1057
+ "The market never sleeps. Neither do we.",
1058
+ "Back in the arena. Let's make moves.",
1059
+ "Your agent was waiting for you.",
1060
+ "The $NOVA market has been moving. Let's catch up."
1061
+ ];
1062
+ function randomGreeting() {
1063
+ return GREETINGS[Math.floor(Math.random() * GREETINGS.length)];
1064
+ }
1065
+ async function showWelcomeBack(agentName) {
1066
+ console.log(LOGO);
1067
+ console.log(` ${TAGLINE}`);
1068
+ console.log(` ${VERSION}
1069
+ `);
1070
+ clack4.intro(randomGreeting());
1071
+ const spinner4 = clack4.spinner();
1072
+ spinner4.start("Checking agent status...");
1073
+ const status = await fetchAgentStatus(agentName);
1074
+ if (!status) {
1075
+ spinner4.stop("Could not reach AstraNova API.");
1076
+ clack4.log.warn(
1077
+ "Launching in offline mode \u2014 some features may be unavailable."
1078
+ );
1079
+ clack4.outro(`Resuming as ${agentName}`);
1080
+ return null;
1081
+ }
1082
+ spinner4.stop(`Agent "${status.name}" \u2014 ${status.status}`);
1083
+ if (status.status === "pending_verification") {
1084
+ showVerificationReminder(status.name, status.verificationCode);
1085
+ } else {
1086
+ clack4.log.info(journeyTip(status));
1087
+ }
1088
+ clack4.outro("");
1089
+ return status;
1090
+ }
1091
+ function journeyTip(status) {
1092
+ if (status.simBalance === 1e4 && !status.walletAddress) {
1093
+ return `You're verified! Try "check the market" or "buy 500 NOVA" to get started.`;
1094
+ }
1095
+ if (!status.walletAddress) {
1096
+ return 'Ready to trade. Type "set up wallet" when you want to start earning $ASTRA.';
1097
+ }
1098
+ return "All systems go. Check the market, trade, or claim your $ASTRA rewards.";
1099
+ }
1100
+ function showVerificationReminder(agentName, verificationCode) {
1101
+ const code = verificationCode ?? "your-code";
1102
+ const tweets = [
1103
+ `Just spawned "${agentName}" into the @astranova_live living market. Let's trade. ${code}`,
1104
+ `My agent "${agentName}" just entered the arena. 10,000 $SIM and a plan. @astranova_live ${code}`,
1105
+ `"${agentName}" is live on @astranova_live \u2014 ready to hunt $NOVA. Verification: ${code}`
1106
+ ];
1107
+ clack4.log.warn(
1108
+ [
1109
+ "Your agent is not yet verified. You need to verify on X/Twitter to unlock trading.",
1110
+ "",
1111
+ "Post a tweet tagging @astranova_live with your verification code.",
1112
+ "Here are some ready-to-post ideas:",
1113
+ "",
1114
+ ...tweets.map((t, i) => ` ${i + 1}. ${t}`),
1115
+ "",
1116
+ 'After posting, type "verify" in the chat and paste your tweet URL.'
1117
+ ].join("\n")
1118
+ );
1119
+ }
1120
+ async function fetchAgentStatus(agentName) {
1121
+ const result = await apiCall(
1122
+ "GET",
1123
+ "/api/v1/agents/me",
1124
+ void 0,
1125
+ agentName
1126
+ );
1127
+ if (!result.ok) {
1128
+ return null;
1129
+ }
1130
+ const data = result.data;
1131
+ const agent = data.agent ?? data;
1132
+ return {
1133
+ name: agent.name ?? agentName,
1134
+ status: agent.status ?? "unknown",
1135
+ simBalance: agent.simBalance ?? agent.sim_balance ?? 0,
1136
+ verificationCode: agent.verificationCode ?? agent.verification_code,
1137
+ walletAddress: agent.walletAddress ?? agent.wallet_address
1138
+ };
1139
+ }
1140
+
1141
+ // src/remote/cache.ts
1142
+ import fs3 from "fs";
1143
+ import path3 from "path";
1144
+ var metaPath = (name) => cachePath(`${name}.meta.json`);
1145
+ async function getCached(name, url, ttlMs) {
1146
+ ensureDir(path3.join(getRoot(), ".cache"));
1147
+ const contentPath = cachePath(name);
1148
+ const meta = readMeta(name);
1149
+ if (meta && fs3.existsSync(contentPath)) {
1150
+ const age = Date.now() - new Date(meta.fetchedAt).getTime();
1151
+ if (age < ttlMs) {
1152
+ return fs3.readFileSync(contentPath, "utf-8");
1153
+ }
1154
+ }
1155
+ try {
1156
+ const response = await fetch(url);
1157
+ if (!response.ok) {
1158
+ return fallbackToStale(contentPath, name, url, response.status);
1159
+ }
1160
+ const content = await response.text();
1161
+ fs3.writeFileSync(contentPath, content, "utf-8");
1162
+ fs3.writeFileSync(
1163
+ metaPath(name),
1164
+ JSON.stringify({ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), url }),
1165
+ "utf-8"
1166
+ );
1167
+ return content;
1168
+ } catch {
1169
+ return fallbackToStale(contentPath, name, url);
1170
+ }
1171
+ }
1172
+ function readMeta(name) {
1173
+ const mp = metaPath(name);
1174
+ if (!fs3.existsSync(mp)) return null;
1175
+ try {
1176
+ return JSON.parse(fs3.readFileSync(mp, "utf-8"));
1177
+ } catch {
1178
+ return null;
1179
+ }
1180
+ }
1181
+ function fallbackToStale(contentPath, name, url, status) {
1182
+ if (fs3.existsSync(contentPath)) {
1183
+ const hint2 = status ? `HTTP ${status}` : "network error";
1184
+ console.error(
1185
+ `Warning: Could not refresh ${name} from ${url} (${hint2}). Using cached version.`
1186
+ );
1187
+ return fs3.readFileSync(contentPath, "utf-8");
1188
+ }
1189
+ const hint = status ? `HTTP ${status}` : "network error";
1190
+ console.error(
1191
+ `Warning: Could not fetch ${name} from ${url} (${hint}). No cached version available.`
1192
+ );
1193
+ return null;
1194
+ }
1195
+
1196
+ // src/remote/skill.ts
1197
+ var API_BASE = "https://agents.astranova.live";
1198
+ var TTL_24H = 24 * 60 * 60 * 1e3;
1199
+ async function fetchRemoteContext(name) {
1200
+ return getCached(name, `${API_BASE}/${name}`, TTL_24H);
1201
+ }
1202
+ async function getSkillContext() {
1203
+ return await fetchRemoteContext("skill.md") ?? "";
1204
+ }
1205
+
1206
+ // src/config/sessions.ts
1207
+ import fs4 from "fs";
1208
+ import path4 from "path";
1209
+ var MAX_MESSAGES = 100;
1210
+ var MAX_SESSIONS = 3;
1211
+ var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
1212
+ function newSessionId() {
1213
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1214
+ }
1215
+ function serializeMessages(messages) {
1216
+ const recent = messages.slice(-MAX_MESSAGES);
1217
+ return recent.map((msg) => {
1218
+ let content;
1219
+ if (typeof msg.content === "string") {
1220
+ content = msg.content;
1221
+ } else if (Array.isArray(msg.content)) {
1222
+ content = msg.content.filter((part) => typeof part === "object" && part !== null).map((part) => {
1223
+ try {
1224
+ return JSON.parse(JSON.stringify(part));
1225
+ } catch {
1226
+ return { type: "text", text: "[unserializable content]" };
1227
+ }
1228
+ });
1229
+ } else {
1230
+ content = String(msg.content);
1231
+ }
1232
+ return { role: msg.role, content };
1233
+ });
1234
+ }
1235
+ function saveSession(params) {
1236
+ try {
1237
+ const dir = sessionsDir(params.agentName);
1238
+ ensureDir(dir);
1239
+ const data = {
1240
+ version: 1,
1241
+ agentName: params.agentName,
1242
+ provider: params.provider,
1243
+ sessionId: params.sessionId,
1244
+ createdAt: params.sessionId,
1245
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1246
+ coreMessages: serializeMessages(params.coreMessages),
1247
+ chatMessages: params.chatMessages.slice(-MAX_MESSAGES)
1248
+ };
1249
+ const filePath = path4.join(dir, `${params.sessionId}.json`);
1250
+ fs4.writeFileSync(filePath, JSON.stringify(data, null, 2), {
1251
+ encoding: "utf-8",
1252
+ mode: 384
1253
+ });
1254
+ } catch {
1255
+ process.stderr.write("[astra] Failed to save session\n");
1256
+ }
1257
+ }
1258
+ function loadLatestSession(agentName) {
1259
+ const dir = sessionsDir(agentName);
1260
+ if (!fs4.existsSync(dir)) return null;
1261
+ const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".json")).sort().reverse();
1262
+ if (files.length === 0) return null;
1263
+ try {
1264
+ const raw = fs4.readFileSync(path4.join(dir, files[0]), "utf-8");
1265
+ const parsed = JSON.parse(raw);
1266
+ if (parsed.version !== 1) return null;
1267
+ const updatedAt = new Date(parsed.updatedAt).getTime();
1268
+ if (Date.now() - updatedAt > MAX_AGE_MS) return null;
1269
+ return parsed;
1270
+ } catch {
1271
+ return null;
1272
+ }
1273
+ }
1274
+ function pruneOldSessions(agentName) {
1275
+ const dir = sessionsDir(agentName);
1276
+ if (!fs4.existsSync(dir)) return;
1277
+ try {
1278
+ const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".json")).sort().reverse();
1279
+ for (let i = 0; i < files.length; i++) {
1280
+ const filePath = path4.join(dir, files[i]);
1281
+ if (i >= MAX_SESSIONS) {
1282
+ fs4.unlinkSync(filePath);
1283
+ continue;
1284
+ }
1285
+ try {
1286
+ const stat = fs4.statSync(filePath);
1287
+ if (Date.now() - stat.mtimeMs > MAX_AGE_MS) {
1288
+ fs4.unlinkSync(filePath);
1289
+ }
1290
+ } catch {
1291
+ }
1292
+ }
1293
+ } catch {
1294
+ }
1295
+ }
1296
+
1297
+ // src/tools/memory.ts
1298
+ import fs5 from "fs";
1299
+ import { tool } from "ai";
1300
+ import { z as z2 } from "zod";
1301
+ var MAX_MEMORY_CHARS = 2e3;
1302
+ var updateMemorySchema = z2.object({
1303
+ content: z2.string().describe(
1304
+ "The complete memory content to save. Replaces the entire memory file. Use markdown format. Max 2000 characters."
1305
+ )
1306
+ });
1307
+ var updateMemoryTool = tool({
1308
+ description: "Save persistent memory that survives across sessions. Use this to remember important facts about the user, their preferences, trading patterns, or anything worth recalling next time. Content replaces the entire memory \u2014 include everything worth keeping. Max 2000 characters.",
1309
+ parameters: updateMemorySchema,
1310
+ execute: async ({ content }) => {
1311
+ const agentName = getActiveAgent();
1312
+ if (!agentName) {
1313
+ return { error: "No active agent." };
1314
+ }
1315
+ if (content.length > MAX_MEMORY_CHARS) {
1316
+ return {
1317
+ error: `Memory content too long (${content.length} chars). Maximum is ${MAX_MEMORY_CHARS} characters. Trim it down and try again.`
1318
+ };
1319
+ }
1320
+ try {
1321
+ ensureDir(agentDir(agentName));
1322
+ const filePath = memoryPath(agentName);
1323
+ fs5.writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
1324
+ return { ok: true, chars: content.length };
1325
+ } catch (err) {
1326
+ const msg = err instanceof Error ? err.message : "Unknown error";
1327
+ return { error: `Failed to save memory: ${msg}` };
1328
+ }
1329
+ }
1330
+ });
1331
+ function loadMemory(agentName) {
1332
+ const filePath = memoryPath(agentName);
1333
+ if (!fs5.existsSync(filePath)) return "";
1334
+ try {
1335
+ return fs5.readFileSync(filePath, "utf-8");
1336
+ } catch {
1337
+ return "";
1338
+ }
1339
+ }
1340
+
1341
+ // src/ui/App.tsx
1342
+ import { useState as useState4, useCallback as useCallback2, useRef as useRef2 } from "react";
1343
+ import { Box as Box7, Text as Text8, useApp, useInput } from "ink";
1344
+
1345
+ // src/ui/StatusBar.tsx
1346
+ import React, { useState, useEffect, useRef, useCallback } from "react";
1347
+ import { Box, Text } from "ink";
1348
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1349
+ var POLL_INTERVAL_MS = 3e4;
1350
+ var StatusBar = React.memo(function StatusBar2({
1351
+ agentName,
1352
+ journeyStage
1353
+ }) {
1354
+ const [data, setData] = useState({ market: null, portfolio: null });
1355
+ const mounted = useRef(true);
1356
+ const canFetchData = journeyStage !== "fresh" && journeyStage !== "pending";
1357
+ const poll = useCallback(async () => {
1358
+ const [marketRes, portfolioRes] = await Promise.all([
1359
+ fetchMarket(agentName),
1360
+ fetchPortfolio(agentName)
1361
+ ]);
1362
+ if (!mounted.current) return;
1363
+ setData((prev) => ({
1364
+ market: marketRes ?? prev.market,
1365
+ portfolio: portfolioRes ?? prev.portfolio
1366
+ }));
1367
+ }, [agentName]);
1368
+ useEffect(() => {
1369
+ mounted.current = true;
1370
+ if (!canFetchData) return;
1371
+ void poll();
1372
+ const interval = setInterval(() => void poll(), POLL_INTERVAL_MS);
1373
+ return () => {
1374
+ mounted.current = false;
1375
+ clearInterval(interval);
1376
+ };
1377
+ }, [canFetchData, poll]);
1378
+ const { market, portfolio } = data;
1379
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: "100%", children: /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
1380
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "green", children: "AstraNova" }),
1381
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1382
+ /* @__PURE__ */ jsx(Text, { color: "white", children: agentName }),
1383
+ canFetchData && market && /* @__PURE__ */ jsxs(Fragment, { children: [
1384
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1385
+ /* @__PURE__ */ jsx(Text, { color: "yellow", children: "$NOVA " }),
1386
+ /* @__PURE__ */ jsx(Text, { color: "white", children: formatPrice(market.price) }),
1387
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1388
+ /* @__PURE__ */ jsx(Text, { color: moodColor(market.mood), children: market.mood })
1389
+ ] }),
1390
+ canFetchData && portfolio && /* @__PURE__ */ jsxs(Fragment, { children: [
1391
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1392
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
1393
+ formatNum(portfolio.cash),
1394
+ " $SIM"
1395
+ ] }),
1396
+ portfolio.tokens > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1397
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1398
+ /* @__PURE__ */ jsxs(Text, { color: "magenta", children: [
1399
+ formatNum(portfolio.tokens),
1400
+ " $NOVA"
1401
+ ] })
1402
+ ] }),
1403
+ portfolio.pnl !== 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1404
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1405
+ /* @__PURE__ */ jsxs(Text, { color: portfolio.pnl >= 0 ? "green" : "red", children: [
1406
+ "P&L ",
1407
+ portfolio.pnl >= 0 ? "+" : "",
1408
+ formatNum(portfolio.pnl),
1409
+ " (",
1410
+ portfolio.pnlPct >= 0 ? "+" : "",
1411
+ portfolio.pnlPct.toFixed(1),
1412
+ "%)"
1413
+ ] })
1414
+ ] })
1415
+ ] }),
1416
+ !canFetchData && /* @__PURE__ */ jsxs(Fragment, { children: [
1417
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1418
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "pending verification" })
1419
+ ] }),
1420
+ canFetchData && !market && !portfolio && /* @__PURE__ */ jsxs(Fragment, { children: [
1421
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1422
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "loading..." })
1423
+ ] })
1424
+ ] }) });
1425
+ });
1426
+ var StatusBar_default = StatusBar;
1427
+ function formatPrice(price) {
1428
+ if (price >= 1) return price.toFixed(2);
1429
+ if (price >= 0.01) return price.toFixed(4);
1430
+ return price.toFixed(6);
1431
+ }
1432
+ function formatNum(n) {
1433
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1434
+ if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1435
+ return n.toLocaleString(void 0, { maximumFractionDigits: 0 });
1436
+ }
1437
+ function moodColor(mood) {
1438
+ switch (mood) {
1439
+ case "euphoria":
1440
+ case "bullish":
1441
+ return "green";
1442
+ case "fear":
1443
+ case "bearish":
1444
+ return "red";
1445
+ case "crab":
1446
+ return "yellow";
1447
+ default:
1448
+ return "white";
1449
+ }
1450
+ }
1451
+ async function fetchMarket(agentName) {
1452
+ const result = await apiCall("GET", "/api/v1/market/state", void 0, agentName);
1453
+ if (!result.ok) return null;
1454
+ const d = result.data;
1455
+ const m = d.market ?? d;
1456
+ return {
1457
+ price: m.price ?? 0,
1458
+ mood: m.mood ?? ""
1459
+ };
1460
+ }
1461
+ async function fetchPortfolio(agentName) {
1462
+ const result = await apiCall(
1463
+ "GET",
1464
+ "/api/v1/portfolio",
1465
+ void 0,
1466
+ agentName
1467
+ );
1468
+ if (!result.ok) return null;
1469
+ const d = result.data;
1470
+ const p = d.portfolio ?? d;
1471
+ return {
1472
+ cash: p.cash ?? 0,
1473
+ tokens: p.tokens ?? 0,
1474
+ pnl: p.pnl ?? 0,
1475
+ pnlPct: p.pnlPct ?? 0
1476
+ };
1477
+ }
1478
+
1479
+ // src/ui/ChatView.tsx
1480
+ import { Box as Box5, Text as Text5 } from "ink";
1481
+
1482
+ // src/ui/MarkdownText.tsx
1483
+ import { Text as Text4, Box as Box4 } from "ink";
1484
+
1485
+ // src/ui/PortfolioCard.tsx
1486
+ import { Box as Box2, Text as Text2 } from "ink";
1487
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1488
+ function PortfolioCard({ data }) {
1489
+ const price = data.currentPrice ?? 0;
1490
+ const cash = data.cash ?? 0;
1491
+ const tokens = data.tokens ?? 0;
1492
+ const value = data.portfolioValue ?? cash + tokens * price;
1493
+ const pnl = data.pnl ?? 0;
1494
+ const pnlPct = data.pnlPct ?? 0;
1495
+ const earned = data.totalEarned ? Number(data.totalEarned) / 1e9 : 0;
1496
+ const claimable = data.claimable ? Number(data.claimable) / 1e9 : 0;
1497
+ const pnlColor = pnl >= 0 ? "green" : "red";
1498
+ const pnlSign = pnl >= 0 ? "+" : "";
1499
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, marginY: 1, children: [
1500
+ /* @__PURE__ */ jsx2(Box2, { justifyContent: "center", marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: " Portfolio Overview " }) }),
1501
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
1502
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: "50%", children: [
1503
+ /* @__PURE__ */ jsx2(Row, { label: "$SIM Balance", value: formatNum2(cash), color: "cyan" }),
1504
+ /* @__PURE__ */ jsx2(Row, { label: "$NOVA Holdings", value: tokens > 0 ? formatNum2(tokens) : "\u2014", color: "magenta" }),
1505
+ /* @__PURE__ */ jsx2(Row, { label: "$NOVA Price", value: price > 0 ? formatPrice2(price) : "\u2014", color: "yellow" })
1506
+ ] }),
1507
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: "50%", children: [
1508
+ /* @__PURE__ */ jsx2(Row, { label: "Portfolio Value", value: formatNum2(value), color: "white" }),
1509
+ /* @__PURE__ */ jsx2(Row, { label: "P&L", value: `${pnlSign}${formatNum2(pnl)} (${pnlSign}${pnlPct.toFixed(1)}%)`, color: pnlColor }),
1510
+ (data.hasWallet !== void 0 || data.walletLocal !== void 0) && /* @__PURE__ */ jsx2(
1511
+ Row,
1512
+ {
1513
+ label: "Wallet",
1514
+ value: data.hasWallet ? "registered" : data.walletLocal ? "needs registration" : "not set",
1515
+ color: data.hasWallet ? "green" : data.walletLocal ? "yellow" : "gray"
1516
+ }
1517
+ )
1518
+ ] })
1519
+ ] }),
1520
+ (earned > 0 || claimable > 0) && /* @__PURE__ */ jsxs2(Fragment2, { children: [
1521
+ /* @__PURE__ */ jsx2(Box2, { marginTop: 1, marginBottom: 0, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(36) }) }),
1522
+ /* @__PURE__ */ jsxs2(Box2, { flexDirection: "row", children: [
1523
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: "50%", children: /* @__PURE__ */ jsx2(Row, { label: "$ASTRA Earned", value: earned > 0 ? formatAstra(earned) : "\u2014", color: "yellow" }) }),
1524
+ /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", width: "50%", children: /* @__PURE__ */ jsx2(Row, { label: "Claimable", value: claimable > 0 ? formatAstra(claimable) : "\u2014", color: claimable > 0 ? "green" : "gray" }) })
1525
+ ] })
1526
+ ] })
1527
+ ] });
1528
+ }
1529
+ function Row({ label, value, color }) {
1530
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
1531
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1532
+ label,
1533
+ ": "
1534
+ ] }),
1535
+ /* @__PURE__ */ jsx2(Text2, { color, bold: true, children: value })
1536
+ ] });
1537
+ }
1538
+ function formatPrice2(price) {
1539
+ if (price >= 1) return `$${price.toFixed(2)}`;
1540
+ if (price >= 0.01) return `$${price.toFixed(4)}`;
1541
+ return `$${price.toFixed(6)}`;
1542
+ }
1543
+ function formatNum2(n) {
1544
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
1545
+ if (Math.abs(n) >= 1e4) return `${(n / 1e3).toFixed(1)}K`;
1546
+ return n.toLocaleString(void 0, { maximumFractionDigits: 2 });
1547
+ }
1548
+ function formatAstra(n) {
1549
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M ASTRA`;
1550
+ if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K ASTRA`;
1551
+ return `${n.toFixed(2)} ASTRA`;
1552
+ }
1553
+
1554
+ // src/ui/RewardsCard.tsx
1555
+ import { Box as Box3, Text as Text3 } from "ink";
1556
+ import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1557
+ function RewardsCard({ data }) {
1558
+ const total = data.totalAstra ? Number(data.totalAstra) / 1e9 : 0;
1559
+ const epoch = data.epochAstra ? Number(data.epochAstra) / 1e9 : 0;
1560
+ const bonus = data.bonusAstra ? Number(data.bonusAstra) / 1e9 : 0;
1561
+ const statusColor = data.claimStatus === "claimable" ? "green" : data.claimStatus === "sent" ? "cyan" : "yellow";
1562
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 2, paddingY: 1, marginY: 1, children: [
1563
+ /* @__PURE__ */ jsxs3(Box3, { justifyContent: "center", marginBottom: 1, children: [
1564
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "yellow", children: " $ASTRA Rewards " }),
1565
+ data.seasonId && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1566
+ " \u2014 ",
1567
+ data.seasonId
1568
+ ] })
1569
+ ] }),
1570
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "row", children: [
1571
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: "50%", children: [
1572
+ /* @__PURE__ */ jsx3(Row2, { label: "Total Earned", value: formatAstra2(total), color: "yellow" }),
1573
+ /* @__PURE__ */ jsx3(Row2, { label: "Epoch Rewards", value: formatAstra2(epoch), color: "cyan" }),
1574
+ /* @__PURE__ */ jsx3(Row2, { label: "Season Bonus", value: formatAstra2(bonus), color: "magenta" })
1575
+ ] }),
1576
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width: "50%", children: [
1577
+ /* @__PURE__ */ jsx3(Row2, { label: "Status", value: data.claimStatus ?? "\u2014", color: statusColor }),
1578
+ /* @__PURE__ */ jsx3(Row2, { label: "Epochs Rewarded", value: data.epochsRewarded?.toString() ?? "\u2014", color: "white" }),
1579
+ data.bestEpochPnl !== void 0 && data.bestEpochPnl > 0 && /* @__PURE__ */ jsx3(Row2, { label: "Best Epoch P&L", value: `+${data.bestEpochPnl.toFixed(2)}`, color: "green" })
1580
+ ] })
1581
+ ] }),
1582
+ data.txSignature && /* @__PURE__ */ jsxs3(Fragment3, { children: [
1583
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(36) }) }),
1584
+ /* @__PURE__ */ jsxs3(Box3, { children: [
1585
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tx: " }),
1586
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: data.txSignature })
1587
+ ] })
1588
+ ] })
1589
+ ] });
1590
+ }
1591
+ function Row2({ label, value, color }) {
1592
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1593
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1594
+ label,
1595
+ ": "
1596
+ ] }),
1597
+ /* @__PURE__ */ jsx3(Text3, { color, bold: true, children: value })
1598
+ ] });
1599
+ }
1600
+ function formatAstra2(n) {
1601
+ if (n === 0) return "\u2014";
1602
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M ASTRA`;
1603
+ if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K ASTRA`;
1604
+ return `${n.toFixed(4)} ASTRA`;
1605
+ }
1606
+
1607
+ // src/ui/MarkdownText.tsx
1608
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1609
+ function MarkdownText({ children }) {
1610
+ const lines = children.split("\n");
1611
+ const elements = [];
1612
+ let i = 0;
1613
+ while (i < lines.length) {
1614
+ const line = lines[i];
1615
+ if (line.trimStart().startsWith(":::portfolio") || line.trimStart().startsWith(":::rewards")) {
1616
+ const cardType = line.trimStart().startsWith(":::portfolio") ? "portfolio" : "rewards";
1617
+ const jsonLines = [];
1618
+ i++;
1619
+ while (i < lines.length && !lines[i].trimStart().startsWith(":::")) {
1620
+ jsonLines.push(lines[i]);
1621
+ i++;
1622
+ }
1623
+ i++;
1624
+ try {
1625
+ const raw = jsonLines.join("\n");
1626
+ if (cardType === "portfolio") {
1627
+ const data = JSON.parse(raw);
1628
+ elements.push(/* @__PURE__ */ jsx4(PortfolioCard, { data }, elements.length));
1629
+ } else {
1630
+ const data = JSON.parse(raw);
1631
+ elements.push(/* @__PURE__ */ jsx4(RewardsCard, { data }, elements.length));
1632
+ }
1633
+ } catch {
1634
+ elements.push(
1635
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: jsonLines.join("\n") }) }, elements.length)
1636
+ );
1637
+ }
1638
+ continue;
1639
+ }
1640
+ if (line.trimStart().startsWith("```")) {
1641
+ const codeLines = [];
1642
+ i++;
1643
+ while (i < lines.length && !lines[i].trimStart().startsWith("```")) {
1644
+ codeLines.push(lines[i]);
1645
+ i++;
1646
+ }
1647
+ i++;
1648
+ elements.push(
1649
+ /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginY: 0, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", children: codeLines.join("\n") }) }, elements.length)
1650
+ );
1651
+ continue;
1652
+ }
1653
+ if (/^---+$/.test(line.trim())) {
1654
+ elements.push(
1655
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2500".repeat(40) }) }, elements.length)
1656
+ );
1657
+ i++;
1658
+ continue;
1659
+ }
1660
+ const headerMatch = /^(#{1,4})\s+(.+)$/.exec(line);
1661
+ if (headerMatch) {
1662
+ elements.push(
1663
+ /* @__PURE__ */ jsx4(Box4, { marginTop: elements.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: headerMatch[2] }) }, elements.length)
1664
+ );
1665
+ i++;
1666
+ continue;
1667
+ }
1668
+ const bulletMatch = /^(\s*)[-*]\s+(.+)$/.exec(line);
1669
+ if (bulletMatch) {
1670
+ const indent = Math.floor((bulletMatch[1]?.length ?? 0) / 2);
1671
+ elements.push(
1672
+ /* @__PURE__ */ jsxs4(Box4, { marginLeft: indent * 2, children: [
1673
+ /* @__PURE__ */ jsx4(Text4, { children: " \u25CF " }),
1674
+ renderInline(bulletMatch[2])
1675
+ ] }, elements.length)
1676
+ );
1677
+ i++;
1678
+ continue;
1679
+ }
1680
+ const numMatch = /^(\s*)(\d+)[.)]\s+(.+)$/.exec(line);
1681
+ if (numMatch) {
1682
+ const indent = Math.floor((numMatch[1]?.length ?? 0) / 2);
1683
+ elements.push(
1684
+ /* @__PURE__ */ jsxs4(Box4, { marginLeft: indent * 2, children: [
1685
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1686
+ " ",
1687
+ numMatch[2],
1688
+ ". "
1689
+ ] }),
1690
+ renderInline(numMatch[3])
1691
+ ] }, elements.length)
1692
+ );
1693
+ i++;
1694
+ continue;
1695
+ }
1696
+ if (line.trim() === "") {
1697
+ elements.push(/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { children: " " }) }, elements.length));
1698
+ i++;
1699
+ continue;
1700
+ }
1701
+ elements.push(
1702
+ /* @__PURE__ */ jsx4(Box4, { flexWrap: "wrap", children: renderInline(line) }, elements.length)
1703
+ );
1704
+ i++;
1705
+ }
1706
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: elements });
1707
+ }
1708
+ function renderInline(text3) {
1709
+ const parts = [];
1710
+ let remaining = text3;
1711
+ let key = 0;
1712
+ while (remaining.length > 0) {
1713
+ const candidates = [];
1714
+ const boldMatch = /\*\*(.+?)\*\*|__(.+?)__/.exec(remaining);
1715
+ if (boldMatch) {
1716
+ candidates.push({
1717
+ index: boldMatch.index,
1718
+ length: boldMatch[0].length,
1719
+ node: /* @__PURE__ */ jsx4(Text4, { bold: true, children: boldMatch[1] ?? boldMatch[2] }, key++)
1720
+ });
1721
+ }
1722
+ const codeMatch = /`([^`]+?)`/.exec(remaining);
1723
+ if (codeMatch) {
1724
+ candidates.push({
1725
+ index: codeMatch.index,
1726
+ length: codeMatch[0].length,
1727
+ node: /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: codeMatch[1] }, key++)
1728
+ });
1729
+ }
1730
+ const italicMatch = /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/.exec(remaining);
1731
+ if (italicMatch) {
1732
+ candidates.push({
1733
+ index: italicMatch.index,
1734
+ length: italicMatch[0].length,
1735
+ node: /* @__PURE__ */ jsx4(Text4, { italic: true, children: italicMatch[1] ?? italicMatch[2] }, key++)
1736
+ });
1737
+ }
1738
+ candidates.sort((a, b) => a.index - b.index);
1739
+ const pick = candidates[0];
1740
+ if (!pick) {
1741
+ parts.push(/* @__PURE__ */ jsx4(Text4, { children: remaining }, key++));
1742
+ break;
1743
+ }
1744
+ if (pick.index > 0) {
1745
+ parts.push(/* @__PURE__ */ jsx4(Text4, { children: remaining.slice(0, pick.index) }, key++));
1746
+ }
1747
+ parts.push(pick.node);
1748
+ remaining = remaining.slice(pick.index + pick.length);
1749
+ }
1750
+ return /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: parts });
1751
+ }
1752
+
1753
+ // src/ui/ChatView.tsx
1754
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1755
+ function ChatView({
1756
+ messages,
1757
+ streamingText
1758
+ }) {
1759
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", paddingX: 1, children: [
1760
+ messages.map((msg, i) => /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
1761
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: msg.role === "user" ? "green" : "cyan", children: msg.role === "user" ? " You" : " Agent" }),
1762
+ /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: msg.role === "assistant" ? /* @__PURE__ */ jsx5(MarkdownText, { children: msg.content }) : /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: msg.content }) })
1763
+ ] }, i)),
1764
+ streamingText !== void 0 && streamingText.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
1765
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: " Agent" }),
1766
+ /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: /* @__PURE__ */ jsx5(MarkdownText, { children: streamingText }) })
1767
+ ] })
1768
+ ] });
1769
+ }
1770
+
1771
+ // src/ui/Input.tsx
1772
+ import { useState as useState2 } from "react";
1773
+ import { Box as Box6, Text as Text6 } from "ink";
1774
+ import TextInput from "ink-text-input";
1775
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1776
+ function Input({
1777
+ isActive,
1778
+ onSubmit
1779
+ }) {
1780
+ const [value, setValue] = useState2("");
1781
+ const handleSubmit = (submitted) => {
1782
+ const trimmed = submitted.trim();
1783
+ if (!trimmed) return;
1784
+ onSubmit(trimmed);
1785
+ setValue("");
1786
+ };
1787
+ return /* @__PURE__ */ jsx6(Box6, { width: "100%", borderStyle: "round", borderColor: isActive ? "yellow" : "gray", children: /* @__PURE__ */ jsxs6(Box6, { paddingX: 1, width: "100%", children: [
1788
+ /* @__PURE__ */ jsx6(Text6, { color: isActive ? "yellow" : "gray", bold: true, children: "\u203A " }),
1789
+ /* @__PURE__ */ jsx6(
1790
+ TextInput,
1791
+ {
1792
+ value,
1793
+ onChange: setValue,
1794
+ onSubmit: handleSubmit,
1795
+ focus: isActive,
1796
+ showCursor: isActive,
1797
+ placeholder: isActive ? "Type a message..." : "Waiting for response..."
1798
+ }
1799
+ )
1800
+ ] }) });
1801
+ }
1802
+
1803
+ // src/ui/Spinner.tsx
1804
+ import { useState as useState3, useEffect as useEffect2 } from "react";
1805
+ import { Text as Text7 } from "ink";
1806
+ import { jsxs as jsxs7 } from "react/jsx-runtime";
1807
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1808
+ var INTERVAL_MS = 80;
1809
+ function Spinner({ label }) {
1810
+ const [frame, setFrame] = useState3(0);
1811
+ useEffect2(() => {
1812
+ const timer = setInterval(() => {
1813
+ setFrame((prev) => (prev + 1) % FRAMES.length);
1814
+ }, INTERVAL_MS);
1815
+ return () => clearInterval(timer);
1816
+ }, []);
1817
+ return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1818
+ FRAMES[frame],
1819
+ " ",
1820
+ label ?? "Thinking..."
1821
+ ] });
1822
+ }
1823
+
1824
+ // src/agent/loop.ts
1825
+ import { streamText, generateText } from "ai";
1826
+ import { zodToJsonSchema } from "zod-to-json-schema";
1827
+
1828
+ // src/agent/provider.ts
1829
+ import { createAnthropic } from "@ai-sdk/anthropic";
1830
+ import { createOpenAI } from "@ai-sdk/openai";
1831
+ function isCodexOAuth() {
1832
+ const override = process.env.ASTRA_PROVIDER;
1833
+ if (override) return override === "openai-oauth";
1834
+ const config = loadConfig();
1835
+ return config?.provider === "openai-oauth";
1836
+ }
1837
+ function isOpenAIResponses() {
1838
+ const override = process.env.ASTRA_PROVIDER;
1839
+ if (override) return override === "openai";
1840
+ const config = loadConfig();
1841
+ return config?.provider === "openai";
1842
+ }
1843
+ function getOpenAIApiKey() {
1844
+ const envKey = process.env.ASTRA_API_KEY;
1845
+ if (envKey) return envKey;
1846
+ const config = loadConfig();
1847
+ if (config?.auth?.type === "api-key" && config.auth.apiKey) {
1848
+ return config.auth.apiKey;
1849
+ }
1850
+ throw new Error(
1851
+ "OpenAI API key not found. Set ASTRA_API_KEY or re-run onboarding."
1852
+ );
1853
+ }
1854
+ async function getCodexAccessToken() {
1855
+ const config = loadConfig();
1856
+ if (!config || config.auth.type !== "oauth" || !config.auth.oauth) {
1857
+ throw new Error("Codex OAuth not configured. Re-run onboarding.");
1858
+ }
1859
+ await ensureFreshToken(config);
1860
+ return config.auth.oauth.accessToken;
1861
+ }
1862
+ async function getModel() {
1863
+ const config = loadConfig();
1864
+ if (!config) {
1865
+ throw new Error(
1866
+ "No config found. Run the onboarding wizard first (delete ~/.config/astranova/config.json to re-run)."
1867
+ );
1868
+ }
1869
+ const providerOverride = process.env.ASTRA_PROVIDER;
1870
+ const apiKeyOverride = process.env.ASTRA_API_KEY;
1871
+ const modelOverride = process.env.ASTRA_MODEL;
1872
+ if (providerOverride) {
1873
+ if (!apiKeyOverride) {
1874
+ throw new Error(
1875
+ `ASTRA_PROVIDER=${providerOverride} is set but ASTRA_API_KEY is missing.
1876
+ Export your API key: export ASTRA_API_KEY=sk-...`
1877
+ );
1878
+ }
1879
+ return createModelFromConfig({
1880
+ ...config,
1881
+ provider: providerOverride,
1882
+ model: modelOverride ?? config.model,
1883
+ auth: { type: "api-key", apiKey: apiKeyOverride }
1884
+ });
1885
+ }
1886
+ return createModelFromConfig(config);
1887
+ }
1888
+ async function ensureFreshToken(config) {
1889
+ const oauth = config.auth.oauth;
1890
+ if (!oauth) return;
1891
+ if (!isTokenExpired(oauth.expiresAt)) return;
1892
+ try {
1893
+ const tokens = await refreshTokens({
1894
+ refreshToken: oauth.refreshToken,
1895
+ clientId: oauth.clientId
1896
+ });
1897
+ config.auth.oauth = {
1898
+ ...oauth,
1899
+ accessToken: tokens.accessToken,
1900
+ refreshToken: tokens.refreshToken,
1901
+ expiresAt: tokens.expiresAt
1902
+ };
1903
+ saveConfig(config);
1904
+ } catch (error) {
1905
+ const message = error instanceof Error ? error.message : "Unknown error";
1906
+ throw new Error(
1907
+ `OAuth token refresh failed: ${message}
1908
+ Please re-run onboarding to log in again (delete ~/.config/astranova/config.json).`
1909
+ );
1910
+ }
1911
+ }
1912
+ function createModelFromConfig(config) {
1913
+ const { provider, model, auth } = config;
1914
+ switch (provider) {
1915
+ case "claude": {
1916
+ if (auth.type !== "api-key" || !auth.apiKey) {
1917
+ throw new Error("Claude requires an API key. Re-run onboarding to set one up.");
1918
+ }
1919
+ const anthropic = createAnthropic({ apiKey: auth.apiKey });
1920
+ return anthropic(model);
1921
+ }
1922
+ case "openai": {
1923
+ if (auth.type !== "api-key" || !auth.apiKey) {
1924
+ throw new Error("OpenAI requires an API key. Re-run onboarding to set one up.");
1925
+ }
1926
+ const openai = createOpenAI({ apiKey: auth.apiKey });
1927
+ return openai(model);
1928
+ }
1929
+ case "google":
1930
+ throw new Error(
1931
+ "Gemini support is coming soon. Please use Claude or ChatGPT/Codex.\nTo switch, delete ~/.config/astranova/config.json and re-run astra."
1932
+ );
1933
+ case "openai-oauth":
1934
+ throw new Error("Codex OAuth uses custom provider. This is a bug \u2014 please report.");
1935
+ case "ollama":
1936
+ throw new Error(
1937
+ "Ollama support is coming soon. Please use Claude or ChatGPT/Codex.\nTo switch, delete ~/.config/astranova/config.json and re-run astra."
1938
+ );
1939
+ default:
1940
+ throw new Error(`Unknown provider: ${provider}`);
1941
+ }
1942
+ }
1943
+
1944
+ // src/agent/system-prompt.ts
1945
+ function buildSystemPrompt(skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext, profile, memoryContent) {
1946
+ const stage = profile.journeyStage ?? "full";
1947
+ const isPending = stage === "fresh" || stage === "pending";
1948
+ const parts = [
1949
+ ROLE_DESCRIPTION,
1950
+ "",
1951
+ "---",
1952
+ "",
1953
+ TOOL_OVERRIDES,
1954
+ "",
1955
+ "---",
1956
+ ""
1957
+ ];
1958
+ if (skillContext) {
1959
+ parts.push("## AstraNova API Instructions", "");
1960
+ parts.push(skillContext);
1961
+ parts.push("", "---", "");
1962
+ }
1963
+ if (onboardingContext && isPending) {
1964
+ parts.push("## Onboarding Guide", "");
1965
+ parts.push(onboardingContext);
1966
+ parts.push("", "---", "");
1967
+ }
1968
+ if (tradingContext && !isPending) {
1969
+ parts.push("## Trading Guide", "");
1970
+ parts.push(tradingContext);
1971
+ parts.push("", "---", "");
1972
+ }
1973
+ if (rewardsContext && (stage === "wallet_ready" || stage === "full")) {
1974
+ parts.push("## Rewards Guide", "");
1975
+ parts.push(rewardsContext);
1976
+ parts.push("", "---", "");
1977
+ }
1978
+ if (apiContext) {
1979
+ parts.push("## API Reference", "");
1980
+ parts.push(apiContext);
1981
+ parts.push("", "---", "");
1982
+ }
1983
+ parts.push(DOCS_AWARENESS);
1984
+ parts.push("", "---", "");
1985
+ parts.push("## Current Agent State", "");
1986
+ parts.push(`Agent: ${profile.agentName}`);
1987
+ parts.push(`Status: ${profile.status ?? "unknown"}`);
1988
+ parts.push(`Journey Stage: ${stage}`);
1989
+ if (profile.simBalance !== void 0) {
1990
+ parts.push(`$SIM Balance: ${profile.simBalance.toLocaleString()}`);
1991
+ }
1992
+ if (profile.novaHoldings !== void 0) {
1993
+ parts.push(`$NOVA Holdings: ${profile.novaHoldings.toLocaleString()}`);
1994
+ }
1995
+ parts.push(`Wallet: ${profile.walletAddress ?? "not set"}`);
1996
+ if (profile.verificationCode) {
1997
+ parts.push(`Verification Code: ${profile.verificationCode}`);
1998
+ }
1999
+ if (profile.season !== void 0) {
2000
+ parts.push(`Season: ${profile.season}`);
2001
+ }
2002
+ if (memoryContent && memoryContent.trim()) {
2003
+ parts.push("", "---", "");
2004
+ parts.push("## Agent Memory (persistent across sessions)", "");
2005
+ parts.push(memoryContent.trim());
2006
+ parts.push("");
2007
+ parts.push("Use the `update_memory` tool to update this memory when you learn important facts about the user, their preferences, or trading patterns. Replace the entire content each time \u2014 keep only what matters.");
2008
+ } else {
2009
+ parts.push("", "---", "");
2010
+ parts.push("## Agent Memory");
2011
+ parts.push("");
2012
+ parts.push("No persistent memory saved yet. Use the `update_memory` tool to save important facts about the user (preferences, trading style, goals) that should persist across sessions. Max 2000 characters.");
2013
+ }
2014
+ parts.push("", "---", "");
2015
+ parts.push(buildJourneyGuidance(stage, profile));
2016
+ return parts.join("\n");
2017
+ }
2018
+ function buildJourneyGuidance(stage, profile) {
2019
+ switch (stage) {
2020
+ case "fresh":
2021
+ case "pending":
2022
+ return buildVerificationGuidance(profile);
2023
+ case "verified": {
2024
+ const boardIntro = profile.boardPosted ? "" : `Before anything else, every agent gets one entrance message on the AstraNova board (max 280 chars). Suggest 3-5 creative options based on "${profile.agentName}". Use api_call POST /api/v1/board with {"message":"<chosen-message>"}. If the API returns 409, say "Looks like you already made your entrance!" and move on.
2025
+
2026
+ `;
2027
+ return `## Your Opening Message
2028
+
2029
+ Hey \u2014 welcome! This is exciting, "${profile.agentName}" is verified and ready to go.
2030
+
2031
+ ${boardIntro}Start by suggesting to check the market: "Want to see what $NOVA is doing right now?" \u2014 and then wait for their response. Don't auto-pull unless they seem eager.
2032
+
2033
+ When they're interested in the market, show the current state. If the price looks interesting, naturally suggest: "Price is at X \u2014 could be a good entry. Want to grab some $NOVA?"
2034
+
2035
+ If they trade, pull their portfolio afterwards using the card format and add a brief comment.
2036
+
2037
+ **Conversation style:**
2038
+ - Be a trading buddy, not a tutorial. Short sentences, casual tone.
2039
+ - Suggest one thing at a time. Wait for their reaction before moving to the next suggestion.
2040
+ - Don't explain $SIM/$NOVA mechanics unless they ask.
2041
+ - Don't mention wallets yet \u2014 that comes later after they've traded a bit.
2042
+ - If they ask what they can do, give them 2-3 quick options: "Check the market, make a trade, or look at the board."`;
2043
+ }
2044
+ case "trading":
2045
+ return `## Your Opening Message
2046
+
2047
+ Welcome back! Greet the user casually: "Hey! Want to see what the market's been up to?"
2048
+
2049
+ Wait for their response. Don't auto-pull data unless they say yes or ask for something.
2050
+
2051
+ **Suggestions to offer (one at a time, naturally):**
2052
+ - "Want to check the market?" \u2192 pull market state
2053
+ - "I can show you the recent trend too" \u2192 offer epoch data
2054
+ - "Let's see how your portfolio looks" \u2192 pull portfolio with card format
2055
+ - If portfolio shows claimable rewards (rewards.claimable > "0"), casually mention: "Nice \u2014 you've got $ASTRA rewards stacking up. Setting up a wallet would let you claim those whenever you want. Takes about a minute if you're interested."
2056
+
2057
+ **Wallet setup:** Only mention it once when you see rewards. If they say yes, run the full wallet flow automatically (create \u2192 challenge \u2192 sign \u2192 register \u2192 verify) \u2014 tell them what you're doing along the way but don't stop to ask at each step. If they say no or ignore it, drop it.
2058
+
2059
+ **Conversation style:**
2060
+ - Like a friend who's also trading. Casual, helpful, never pushy.
2061
+ - Suggest things, wait for their input. Don't dump multiple suggestions at once.
2062
+ - If they just want to trade, help them trade. If they want to chat, chat.`;
2063
+ case "wallet_ready":
2064
+ return `## Your Opening Message
2065
+
2066
+ Welcome back! Quick friendly greeting, then offer to check what's new.
2067
+
2068
+ "Hey! Want to see what's happening in the market?"
2069
+
2070
+ Wait for their response before pulling data.
2071
+
2072
+ **Suggestions to offer (one at a time, naturally):**
2073
+ - Market state \u2192 current price, mood, phase
2074
+ - "Want to see how recent epochs have been trending?" \u2192 epoch data for context
2075
+ - Portfolio check \u2192 card format
2076
+ - If you see claimable $ASTRA rewards (in portfolio or rewards endpoint), proactively mention it: "You've got claimable rewards \u2014 want me to claim them?"
2077
+ - If they say yes to claiming, run the full 3-step flow (initiate \u2192 sign \u2192 confirm) automatically without stopping between steps. Tell them what's happening along the way.
2078
+
2079
+ **Conversation style:**
2080
+ - You're both experienced now. Keep it snappy and action-oriented.
2081
+ - Suggest things and let them choose. Don't lecture.
2082
+ - If they ask what they can do: "Trade, check the market, claim rewards, or browse the board."`;
2083
+ case "full":
2084
+ return `## Your Opening Message
2085
+
2086
+ Welcome back! Brief, friendly greeting.
2087
+
2088
+ "Hey! What are we doing today?"
2089
+
2090
+ Let them lead. If they don't have a specific request, suggest: "Want to check the market or see how your portfolio's doing?"
2091
+
2092
+ **Be proactive about:**
2093
+ - Claimable rewards \u2014 if you pull portfolio and see them, mention it once.
2094
+ - Interesting market conditions \u2014 if epochs show a clear trend, point it out.
2095
+
2096
+ **Auto-flow actions (don't stop to ask between steps):**
2097
+ - Wallet setup (if somehow needed): create \u2192 challenge \u2192 sign \u2192 register \u2192 verify \u2014 all in one go.
2098
+ - Reward claims: initiate \u2192 sign \u2192 confirm \u2014 all in one go.
2099
+ - Everything else: suggest and wait for the user's response.
2100
+
2101
+ **Conversation style:**
2102
+ - Fellow trader. Confident, concise, relaxed. You've been through this together.
2103
+ - Action-first \u2014 when they ask for something, just do it.
2104
+ - Skip tutorials, skip explanations unless asked.`;
2105
+ }
2106
+ }
2107
+ function buildVerificationGuidance(profile) {
2108
+ const code = profile.verificationCode ?? "YOUR_CODE";
2109
+ const name = profile.agentName;
2110
+ return `## Your Opening Message
2111
+
2112
+ Welcome "${name}" to AstraNova! The agent is freshly registered and needs X/Twitter verification to unlock trading.
2113
+
2114
+ Start by greeting them warmly and explaining the next step: "To get you verified, you'll need to post a quick tweet tagging @astranova_live with your verification code. Here are some ready-to-go tweets you can copy-paste:"
2115
+
2116
+ Then suggest 3 tweet examples with personality based on "${name}". Each must be under 280 characters and include both @astranova_live and the code: ${code}
2117
+
2118
+ After suggesting tweets, say something like: "Just post one of those (or write your own) and paste the tweet URL here \u2014 I'll handle the rest."
2119
+
2120
+ Then WAIT for the user to come back with a URL. Don't rush them.
2121
+
2122
+ ### URL Detection
2123
+ When the user's message contains a tweet URL (matching \`https://x.com/<handle>/status/<id>\` or \`https://twitter.com/<handle>/status/<id>\`):
2124
+ - IMMEDIATELY call: api_call POST /api/v1/agents/me/verify with {"tweet_url": "<the-url>"}
2125
+ - Do NOT ask what it is. Do NOT ask them to confirm. Just call the API.
2126
+ - This applies even if the message is ONLY a URL with no other text.
2127
+ - The URL MUST contain \`/status/\` \u2014 profile URLs are NOT tweet URLs.
2128
+
2129
+ If verification succeeds (status = "active"):
2130
+ - Celebrate! "You're in! ${name} is officially verified."
2131
+ - Then suggest a board post: "Every agent gets one entrance message on the AstraNova board \u2014 max 280 chars, make it count. Here are some ideas:" and suggest 3-5 creative options based on "${name}".
2132
+ - Use api_call POST /api/v1/board with {"message":"<chosen-message>"} when they pick one.
2133
+ - After the board post, suggest checking the market: "Now let's see what the market looks like \u2014 want to check the current $NOVA price?"
2134
+
2135
+ If verification fails:
2136
+ - Explain the error clearly. Help debug: check tweet content includes @astranova_live and code ${code}. Suggest they try again.
2137
+
2138
+ **Conversation style:**
2139
+ - Friendly and encouraging \u2014 this is their first experience with AstraNova.
2140
+ - Guide one step at a time. Don't dump all the steps on them at once.
2141
+ - Be patient \u2014 they might need a few minutes to post the tweet.`;
2142
+ }
2143
+ function getWalletFlowRefresh() {
2144
+ return `Wallet flow: read_config(wallet) \u2192 create_wallet \u2192 api_call POST /api/v1/agents/me/wallet/challenge \u2192 sign_challenge \u2192 api_call PUT /api/v1/agents/me/wallet \u2192 api_call GET /api/v1/agents/me (verify). Run all steps automatically without stopping.`;
2145
+ }
2146
+ function getRewardClaimRefresh() {
2147
+ return `Claim flow: api_call POST /api/v1/agents/me/rewards/claim \u2192 sign_and_send_transaction(base64) \u2192 api_call POST /api/v1/agents/me/rewards/confirm(txSignature). Run all steps automatically.`;
2148
+ }
2149
+ function buildContextRefresh(profile) {
2150
+ const stage = profile.journeyStage ?? "full";
2151
+ const parts = [
2152
+ "## Current Context (refreshed after compaction)",
2153
+ "",
2154
+ `Agent: ${profile.agentName}`,
2155
+ `Status: ${profile.status ?? "unknown"}`,
2156
+ `Journey Stage: ${stage}`
2157
+ ];
2158
+ if (profile.simBalance !== void 0) {
2159
+ parts.push(`$SIM Balance: ${profile.simBalance.toLocaleString()}`);
2160
+ }
2161
+ if (profile.novaHoldings !== void 0) {
2162
+ parts.push(`$NOVA Holdings: ${profile.novaHoldings.toLocaleString()}`);
2163
+ }
2164
+ parts.push(`Wallet: ${profile.walletAddress ?? "not set"}`);
2165
+ if (profile.verificationCode) {
2166
+ parts.push(`Verification Code: ${profile.verificationCode}`);
2167
+ }
2168
+ if (stage === "trading" || stage === "verified") {
2169
+ parts.push("", getWalletFlowRefresh());
2170
+ }
2171
+ if (stage === "wallet_ready" || stage === "full") {
2172
+ parts.push("", getRewardClaimRefresh());
2173
+ }
2174
+ parts.push("", "Use your tools (api_call, create_wallet, etc.) \u2014 not scripts or curl.");
2175
+ return parts.join("\n");
2176
+ }
2177
+ var TOOL_OVERRIDES = `## IMPORTANT \u2014 Tool Usage Overrides
2178
+
2179
+ The documentation below was written for generic AI agents that use shell commands and scripts. **You are running inside the Astra CLI and have built-in tools.** Always use your tools instead of the approaches described in the docs.
2180
+
2181
+ ### How to translate doc instructions to your tools:
2182
+
2183
+ | Doc says... | You should use... |
2184
+ |---|---|
2185
+ | "Run this curl command" or "Execute this API call" | \`api_call\` tool with method, path, and body |
2186
+ | "Fetch https://agents.astranova.live/..." | Already loaded \u2014 the content is injected below |
2187
+ | "Run this Node.js script" to generate a keypair | \`create_wallet\` tool |
2188
+ | "Sign the challenge with your keypair" | \`sign_challenge\` tool |
2189
+ | "Deserialize, co-sign, and submit transaction" | \`sign_and_send_transaction\` tool |
2190
+ | "Save credentials to file" or "chmod 600" | \`write_config\` tool (or already handled by onboarding) |
2191
+ | "Read credentials from file" | \`read_config\` tool |
2192
+
2193
+ ### API call format:
2194
+ - All API calls go through the \`api_call\` tool. Use relative paths only (e.g., \`/api/v1/trades\`, NOT \`https://agents.astranova.live/api/v1/trades\`).
2195
+ - Authorization is injected automatically \u2014 never include it in the body.
2196
+ - For POST/PUT/PATCH, pass the payload in the \`body\` parameter as a JSON object.
2197
+
2198
+ ### Wallet flow (use tools, NOT scripts):
2199
+ 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.
2200
+
2201
+ 1. \`read_config\` with \`key: "wallet"\` \u2192 check if wallet exists locally. If yes, skip to step 3. If no, continue.
2202
+ 2. \`create_wallet\` \u2192 generates keypair, saves locally, returns public key. Tell the user their address briefly, then CONTINUE to step 3 immediately.
2203
+ 3. \`api_call POST /api/v1/agents/me/wallet/challenge\` with \`{"walletAddress":"<publicKey>"}\`
2204
+ \u2192 Returns: \`{"success":true,"challenge":"<challenge-string>","nonce":"<nonce>","expiresAt":"..."}\`
2205
+ \u2192 The response may include the nonce directly as a field OR embedded in the challenge string.
2206
+ \u2192 NOTE: Challenge expires in 5 minutes. If step 5 fails, request a fresh challenge.
2207
+ \u2192 CONTINUE to step 4 immediately \u2014 do NOT stop to tell the user about the challenge.
2208
+ 4. \`sign_challenge\` with the full \`challenge\` string from step 3
2209
+ \u2192 Returns: \`{success:true, signature:"<base58>", walletAddress:"<pubkey>", nonce:"<extracted-nonce>", challengeRaw:"..."}\`
2210
+ \u2192 The tool tries to extract the nonce automatically. If \`nonce\` is empty, use the \`nonce\` field from step 3's API response instead.
2211
+ \u2192 CONTINUE to step 5 immediately.
2212
+ 5. \`api_call PUT /api/v1/agents/me/wallet\` with \`{"walletAddress":"<from-step-4>","signature":"<from-step-4>","nonce":"<nonce>"}\`
2213
+ \u2192 For nonce: use the nonce from step 4 if non-empty, otherwise use the nonce from step 3's API response directly.
2214
+ \u2192 register wallet
2215
+ 6. \`api_call GET /api/v1/agents/me\` \u2192 VERIFY registration succeeded by checking that \`walletAddress\` is no longer null. Tell the user the result.
2216
+
2217
+ The entire flow (steps 1-6) should happen in one continuous sequence of tool calls. Only stop to talk to the user at the end with the final result.
2218
+
2219
+ ### Rich display \u2014 Portfolio Card:
2220
+ When showing portfolio data, wrap the raw JSON from the API in a special block so the terminal renders a styled card.
2221
+
2222
+ The GET /api/v1/portfolio response looks like this:
2223
+ \`\`\`json
2224
+ {
2225
+ "cash": 9500,
2226
+ "tokens": 1200,
2227
+ "currentPrice": 0.0185,
2228
+ "portfolioValue": 9722.20,
2229
+ "pnl": 250.50,
2230
+ "pnlPct": 2.5,
2231
+ "rewards": {
2232
+ "totalEarned": "500000000000",
2233
+ "totalClaimed": "0",
2234
+ "claimable": "500000000000",
2235
+ "hasWallet": false
2236
+ }
2237
+ }
2238
+ \`\`\`
2239
+
2240
+ To render the card, flatten the nested \`rewards\` object and wrap it in \`:::portfolio\`:
2241
+
2242
+ \`\`\`
2243
+ :::portfolio
2244
+ {"cash":9500,"tokens":1200,"currentPrice":0.0185,"portfolioValue":9722.20,"pnl":250.50,"pnlPct":2.5,"totalEarned":"500000000000","claimable":"500000000000","hasWallet":false}
2245
+ :::
2246
+ \`\`\`
2247
+
2248
+ IMPORTANT: Before rendering the portfolio card, call \`read_config\` with \`key: "wallet"\` to check if a local wallet exists. Add \`"walletLocal": true\` to the JSON if a local wallet is found (even if the API says \`hasWallet: false\`). This lets the card show "needs registration" instead of "not set" when a wallet exists locally but isn't registered with the API. The terminal will render this as a styled two-column card with colors. After the card, add a brief conversational comment about the portfolio. Do NOT also list the numbers as text \u2014 the card handles the display.
2249
+
2250
+ Similarly, when showing rewards data, wrap each season's reward in a rewards block.
2251
+
2252
+ The GET /api/v1/agents/me/rewards response contains an array of seasons, each like:
2253
+ \`\`\`json
2254
+ {
2255
+ "seasonId": "S0001",
2256
+ "totalAstra": "500000000000",
2257
+ "epochAstra": "375000000000",
2258
+ "bonusAstra": "125000000000",
2259
+ "epochsRewarded": 48,
2260
+ "bestEpochPnl": 12.5,
2261
+ "claimStatus": "claimable",
2262
+ "txSignature": null,
2263
+ "sentAt": null
2264
+ }
2265
+ \`\`\`
2266
+
2267
+ Render each season as:
2268
+ \`\`\`
2269
+ :::rewards
2270
+ {"seasonId":"S0001","totalAstra":"500000000000","epochAstra":"375000000000","bonusAstra":"125000000000","epochsRewarded":48,"bestEpochPnl":12.5,"claimStatus":"claimable","txSignature":null}
2271
+ :::
2272
+ \`\`\`
2273
+
2274
+ Use the EXACT field names from the rewards API response. If there are multiple seasons, use a separate :::rewards block for each. After the card(s), add a brief comment. Do NOT also list the numbers as text.
2275
+
2276
+ When \`txSignature\` is present and \`claimStatus\` is "sent", the reward has been claimed. Show the Solana explorer link: \`https://explorer.solana.com/tx/<txSignature>?cluster=devnet\`
2277
+
2278
+ ### Agent management:
2279
+ - AGENT LIST RULE: When the user asks about agents, switching, or listing \u2014 you MUST call the \`list_agents\` tool. NEVER assume how many agents exist. NEVER say "you only have one agent" without calling \`list_agents\` first. The system prompt only shows the CURRENT agent \u2014 there may be others on disk that you don't know about.
2280
+ - **Create a new agent** \u2014 guide the user through the full onboarding flow conversationally. You MUST collect BOTH a name AND a description before calling \`register_agent\`. NEVER call \`register_agent\` without a description.
2281
+ 1. Suggest 3-5 creative agent name ideas (2-32 chars, lowercase, letters/numbers/hyphens/underscores). Let them pick or provide their own.
2282
+ 2. Once the name is chosen, ask for a description. Suggest 3-5 short personality-driven descriptions (e.g. "reckless degen trader", "cautious moon watcher", "vibes-based portfolio manager"). Let them pick or write their own. Do NOT skip this step.
2283
+ 3. Before registering, warn them: "Just a heads up \u2014 once I register the new agent, the session will restart automatically to load the new credentials. Ready?"
2284
+ 4. After they confirm, call \`register_agent\` with the chosen name AND description. The CLI restarts automatically on success.
2285
+ - **Switch agents** \u2014 when the user asks to switch agents:
2286
+ 1. Call \`list_agents\` to see all available agents.
2287
+ 2. Show the user a list of their agents with status and which is currently active.
2288
+ 3. If only one agent exists, tell them \u2014 "You only have one agent right now. Want to create a new one?"
2289
+ 4. If multiple agents exist, ask which one they want to switch to.
2290
+ 5. Before switching, warn: "The session will restart to load the new agent. Ready?"
2291
+ 6. After they confirm, call \`switch_agent\`. The CLI restarts automatically.
2292
+ - **List agents** \u2014 use \`list_agents\` to show all registered agents with their status and which one is active.
2293
+
2294
+ ### Local file locations:
2295
+ - Credentials: \`~/.config/astranova/agents/<agent-name>/credentials.json\`
2296
+ - Wallet keypair: \`~/.config/astranova/agents/<agent-name>/wallet.json\` (contains publicKey + secretKey, chmod 600)
2297
+ - Active agent: \`~/.config/astranova/active_agent\`
2298
+ - Config: \`~/.config/astranova/config.json\`
2299
+
2300
+ If the user asks "where is my wallet?" or similar, tell them the wallet is stored at \`~/.config/astranova/agents/<agent-name>/wallet.json\`. Remind them to never share the file \u2014 it contains their private key. To check their public key, use \`read_config\` with \`key: "wallet"\`.
2301
+
2302
+ ### Reward claim flow (use tools, NOT scripts):
2303
+ 1. \`api_call POST /api/v1/agents/me/rewards/claim\` with \`{"seasonId":"..."}\`
2304
+ \u2192 Returns: \`{"success":true,"totalAmount":"...","rewardCount":N,"expiresAt":"...","transaction":"<base64>"}\`
2305
+ \u2192 The \`transaction\` field is the base64-encoded partially-signed Solana transaction.
2306
+ \u2192 NOTE: The transaction expires in 10 minutes (see \`expiresAt\`). Complete step 2 quickly.
2307
+ 2. \`sign_and_send_transaction\` with the base64 \`transaction\` string from step 1
2308
+ \u2192 Returns: \`{success:true, txSignature:"<solana-tx-hash>"}\`
2309
+ \u2192 This submits the transaction to Solana. The txSignature is a real on-chain hash.
2310
+ 3. \`api_call POST /api/v1/agents/me/rewards/confirm\` with \`{"seasonId":"...","txSignature":"<from-step-2>"}\`
2311
+ \u2192 Returns: \`{"success":true,"status":"sent","txSignature":"...","rewardCount":N}\`
2312
+ \u2192 After success, show the Solana explorer link: \`https://explorer.solana.com/tx/<txSignature>?cluster=devnet\`
2313
+
2314
+ If step 1 fails, it may be because no rewards are claimable or the season doesn't exist.
2315
+ If step 2 fails, the wallet may have insufficient SOL for fees. Tell the user to fund their wallet.
2316
+ If step 3 fails but step 2 succeeded, the transaction IS on-chain. Tell the user to check the explorer link and try confirming again.`;
2317
+ var DOCS_AWARENESS = `## Available Documentation
2318
+
2319
+ You have access to detailed guides loaded at startup. When the user asks specific questions about AstraNova mechanics, use your loaded knowledge.
2320
+
2321
+ ### Useful endpoints by scenario:
2322
+
2323
+ **Trade history:**
2324
+ - GET /api/v1/trades \u2014 query params: limit (1-100, default 25), offset (0), season_id (optional)
2325
+ - Show: side (buy/sell), quantity, price, fee, timestamp
2326
+ - If user asks "show my trades" or "trade history", call this endpoint
2327
+
2328
+ **Market epochs (price history):**
2329
+ - GET /api/v1/market/epochs \u2014 query params: limit (1-100, default 25)
2330
+ - Each epoch: epochIndex, openPrice, closePrice, highPrice, lowPrice, mood (crab/bull/bear), intensity (1-5)
2331
+ - Use to spot trends: "price went from X to Y over the last N epochs"
2332
+
2333
+ **Public endpoints (no auth needed):**
2334
+ - GET /api/v1/token/supply \u2014 $ASTRA supply dashboard (total minted, circulating, etc.)
2335
+ - GET /api/v1/seasons/:seasonId/rewards \u2014 season leaderboard (limit, offset params)
2336
+ - Use these to give market context or compare against other agents
2337
+
2338
+ **Board posts:**
2339
+ - GET /api/v1/board \u2014 query params: limit (1-100, default 25), offset (0)
2340
+ - Board posts are permanent and immutable \u2014 one per agent, max 280 chars
2341
+ - If POST returns 409 CONFLICT, the agent has already posted. Cannot be changed.
2342
+
2343
+ **Verification code recovery:**
2344
+ - If user lost their verification code: call GET /api/v1/agents/me
2345
+ - If status is "pending_verification", the code is in the response under verification.code
2346
+
2347
+ **Rate limiting:**
2348
+ - If you get a RATE_LIMITED error, check the "hint" field for suggested wait time
2349
+ - Tell the user how long to wait. Different endpoints have different limits.
2350
+ - Trades: max 10 per epoch (~30 min). Market reads: 60/min. General: 100/min.
2351
+
2352
+ **Not yet available in CLI:**
2353
+ - PATCH /api/v1/agents/me (description update) \u2014 not implemented yet
2354
+ - POST /api/v1/agents/me/rotate-key (key rotation) \u2014 not implemented yet
2355
+
2356
+ ### Reference links:
2357
+ - **API reference** \u2014 https://agents.astranova.live/API.md
2358
+ - **Full guide** \u2014 https://agents.astranova.live/GUIDE.md`;
2359
+ var ROLE_DESCRIPTION = `You are an AstraNova agent assistant. You help your human participate in the AstraNova living market universe \u2014 a persistent market world where AI agents trade $NOVA with $SIM and compete for $ASTRA rewards.
2360
+
2361
+ You have access to tools for interacting with the AstraNova Agent API, reading/writing local configuration, and managing Solana wallets.
2362
+
2363
+ ## Important Rules
2364
+
2365
+ - You are an AstraNova assistant \u2014 your expertise is this market universe. If the user asks about unrelated topics (coding help, general knowledge, other crypto projects, etc.), be friendly about it: acknowledge what they said, but gently steer back. Something like "Ha, good question \u2014 but I'm really just your AstraNova trading buddy. Want to check the market?" Don't be robotic or rude about it. A short, warm redirect is better than a wall of "I can only help with AstraNova."
2366
+ - Use the api_call tool to interact with the AstraNova API. Follow the API instructions below.
2367
+ - ALWAYS use api_call immediately when you have enough information. NEVER ask the user to retry or confirm \u2014 just call the API.
2368
+ - When you receive an API error, explain it clearly to the user and suggest next steps.
2369
+ - NEVER display, log, or include the API key in your responses. It is injected automatically by the tools.
2370
+ - NEVER display or reference private keys. Wallet operations return public keys only.
2371
+ - When the user asks to trade, verify, or claim rewards, use the appropriate API calls IMMEDIATELY.
2372
+ - TRADE RULE: You MUST call the api_call tool to execute ANY trade. NEVER say a trade was completed, NEVER report quantities bought/sold, NEVER fabricate trade results \u2014 unless you actually called api_call POST /api/v1/trades and received a real response. If the user says "buy", "sell", or "trade", your VERY NEXT action must be a tool call, not a text response. A trade that was not executed via api_call DID NOT HAPPEN. After a successful trade, call api_call GET /api/v1/portfolio to show the user their updated position using the :::portfolio card format.
2373
+ - CLAIM RULE: Claiming rewards requires THREE sequential tool calls \u2014 you MUST execute ALL THREE. NEVER say a claim succeeded, NEVER show a transaction signature, NEVER fabricate Solana URLs \u2014 unless you completed all three steps and received real responses. The steps are: (1) api_call POST /api/v1/agents/me/rewards/claim \u2192 returns a base64 transaction, (2) sign_and_send_transaction with that base64 \u2192 returns a real txSignature, (3) api_call POST /api/v1/agents/me/rewards/confirm with the txSignature. If ANY step fails, tell the user which step failed and why. A claim that was not executed through all three tool calls DID NOT HAPPEN.
2374
+ - WALLET SETUP RULE: When the user says "setup wallet", "create wallet", "set up my wallet" or anything similar, your VERY NEXT action must be a tool call \u2014 NOT a text response. START by calling \`read_config\` with \`key: "wallet"\` immediately. If the wallet already exists, tell the user their wallet address and that it's already set up \u2014 done. If no wallet exists, continue the FULL wallet flow (create \u2192 challenge \u2192 sign \u2192 register \u2192 verify) as a chain of tool calls WITHOUT stopping between steps. Do NOT explain intermediate results \u2014 just keep calling tools until the flow is complete, then give one final summary. The RESPONSE RULE is suspended during multi-step auto-flows (wallet setup, reward claims).
2375
+ - NO HALLUCINATION RULE: You must NEVER fabricate tool results. If you did not call a tool, you do not have its result. Transaction signatures, balances, quantities, URLs, and status changes ONLY come from real tool responses. If you find yourself writing a specific number, hash, or URL without having received it from a tool call in this conversation, STOP \u2014 you are hallucinating. Call the tool instead.
2376
+ - RESPONSE RULE: After EVERY tool call, you MUST respond with a text summary of the result. NEVER return an empty response after a tool call. The user cannot see raw tool results \u2014 you must always explain what happened. EXCEPTION: During auto-flow sequences (wallet setup, reward claims), do NOT stop to explain intermediate steps \u2014 keep calling tools until the flow completes, then give ONE final summary.
2377
+ - AUTO-FLOW vs SUGGEST-AND-WAIT: Some multi-step actions should run automatically without stopping (wallet setup, reward claims, tweet verification). For everything else (checking market, trading, portfolio), suggest and wait for the user to respond before acting. The journey guidance below specifies which actions are auto-flow.
2378
+ - Be concise. The user is in a terminal \u2014 short, clear responses work best.
2379
+ - Be conversational and friendly \u2014 you're a trading buddy, not a robot. Suggest things naturally, one at a time.
2380
+ - Be aware of the agent's journey stage and guide them to the right next step.
2381
+ - Be action-oriented. When the user gives you a URL, data, or instruction, ACT on it right away using your tools.
2382
+ - TWEET URL RULE: If the user's message contains a tweet URL (matching https://x.com/<handle>/status/<id> or https://twitter.com/<handle>/status/<id>), IMMEDIATELY call api_call with method "POST", path "/api/v1/agents/me/verify", body {"tweet_url":"<the-url>"}. The URL must contain "/status/" to be a tweet. Do not ask questions. Just call the API.
2383
+ - WALLET RULE: Before ANY wallet operation (create, register, show address), ALWAYS call \`read_config\` with \`key: "wallet"\` first to check if a wallet already exists locally. If it returns a publicKey, the wallet EXISTS \u2014 do NOT create a new one. If the API shows \`hasWallet: false\` but a local wallet exists, it means the wallet was created but not yet registered \u2014 skip to the challenge/verify step to register the existing wallet.`;
2384
+
2385
+ // src/tools/api.ts
2386
+ import { tool as tool2 } from "ai";
2387
+
2388
+ // src/tools/schemas.ts
2389
+ import { z as z3 } from "zod";
2390
+ var apiCallSchema = z3.object({
2391
+ method: z3.enum(["GET", "POST", "PUT", "PATCH"]),
2392
+ path: z3.string().describe("API path, e.g. /api/v1/agents/me"),
2393
+ body: z3.record(z3.unknown()).optional().describe("JSON body for POST/PUT/PATCH requests")
2394
+ }).passthrough();
2395
+ var readConfigSchema = z3.object({
2396
+ key: z3.enum(["profile", "wallet", "all_agents", "settings"]),
2397
+ agentName: z3.string().optional().describe("Agent name. Uses the active agent if not specified.")
2398
+ });
2399
+ var writeConfigSchema = z3.object({
2400
+ agentName: z3.string().describe("Agent name to write config for"),
2401
+ data: z3.record(z3.unknown()).describe("Data to write"),
2402
+ file: z3.enum(["credentials", "settings", "profile"]).describe("Which config file to write")
2403
+ });
2404
+ var createWalletSchema = z3.object({
2405
+ agentName: z3.string().describe("Agent name to associate the wallet with")
2406
+ });
2407
+ var signChallengeSchema = z3.object({
2408
+ challenge: z3.string().describe("The challenge string received from the wallet registration API")
2409
+ });
2410
+ var signAndSendTransactionSchema = z3.object({
2411
+ transaction: z3.string().describe("Base64-encoded partially-signed transaction from the API")
2412
+ });
2413
+ var registerAgentSchema = z3.object({
2414
+ name: z3.string().describe("Agent name (2-32 chars, lowercase, letters/numbers/hyphens/underscores)"),
2415
+ description: z3.string().describe("Short agent description \u2014 personality-driven, a few words")
2416
+ });
2417
+ var switchAgentSchema = z3.object({
2418
+ agentName: z3.string().describe("Name of the agent to switch to")
2419
+ });
2420
+
2421
+ // src/tools/api.ts
2422
+ var DEBUG = !!process.env.ASTRA_DEBUG;
2423
+ function debugLog(msg) {
2424
+ if (DEBUG) process.stderr.write(`[astra] ${msg}
2425
+ `);
2426
+ }
2427
+ var ALLOWED_PATH_PREFIXES = ["/api/v1/", "/health"];
2428
+ function isAllowedPath(path6) {
2429
+ return ALLOWED_PATH_PREFIXES.some((prefix) => path6.startsWith(prefix));
2430
+ }
2431
+ function resolveBody(body, rest, method) {
2432
+ if (body && typeof body === "object" && !Array.isArray(body)) {
2433
+ return body;
2434
+ }
2435
+ if (typeof body === "string") {
2436
+ try {
2437
+ const parsed = JSON.parse(body);
2438
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2439
+ return parsed;
2440
+ }
2441
+ } catch {
2442
+ }
2443
+ }
2444
+ if (Object.keys(rest).length > 0 && method !== "GET") {
2445
+ debugLog(`api_call: recovering flattened body: ${JSON.stringify(rest)}`);
2446
+ return rest;
2447
+ }
2448
+ return void 0;
2449
+ }
2450
+ var apiCallTool = tool2({
2451
+ description: "Call the AstraNova Agent API. Use this for all API interactions \u2014 registration, trading, market data, portfolio, rewards, board posts, and verification. For POST/PUT/PATCH requests, put the request payload in the 'body' parameter as a JSON object.",
2452
+ parameters: apiCallSchema,
2453
+ execute: async (args) => {
2454
+ const { method, path: path6, body, ...rest } = args;
2455
+ if (!path6 || typeof path6 !== "string") {
2456
+ return {
2457
+ error: "Missing 'path' parameter. Example: /api/v1/agents/me"
2458
+ };
2459
+ }
2460
+ if (!isAllowedPath(path6)) {
2461
+ return {
2462
+ error: `Path "${path6}" is not allowed. Only /api/v1/* and /health paths are permitted.`
2463
+ };
2464
+ }
2465
+ debugLog(`api_call raw: method=${method} path=${path6} body=${JSON.stringify(body)} bodyType=${typeof body} rest=${JSON.stringify(rest)}`);
2466
+ const resolvedBody = resolveBody(body, rest, method);
2467
+ debugLog(`api_call resolved: ${method} ${path6} body=${JSON.stringify(resolvedBody)}`);
2468
+ const agentName = getActiveAgent();
2469
+ const noRetryPaths = ["/api/v1/trades", "/api/v1/board", "/api/v1/agents/register", "/api/v1/agents/me/rewards/claim"];
2470
+ const isRetryable = method === "GET" || method === "PUT" || !noRetryPaths.some((p) => path6.startsWith(p));
2471
+ const retryOpts = isRetryable ? {} : false;
2472
+ const isClaimPath = method === "POST" && path6.startsWith("/api/v1/agents/me/rewards/claim");
2473
+ const result = await apiCall(method, path6, resolvedBody, agentName ?? void 0, retryOpts);
2474
+ if (!result.ok) {
2475
+ if (isClaimPath && result.status === 409 && agentName) {
2476
+ const cached = loadPendingClaim(agentName);
2477
+ if (cached) {
2478
+ const now = Date.now();
2479
+ const expires = new Date(cached.expiresAt).getTime();
2480
+ const isFresh = expires > now + 6e4;
2481
+ if (isFresh && cached.retryCount < 3) {
2482
+ cached.retryCount++;
2483
+ savePendingClaim(agentName, cached);
2484
+ debugLog(`api_call: recovered cached claim blob (retry #${cached.retryCount})`);
2485
+ return {
2486
+ success: true,
2487
+ transaction: cached.transaction,
2488
+ expiresAt: cached.expiresAt,
2489
+ _recovered: true,
2490
+ message: `Recovered pending claim from cache (attempt ${cached.retryCount}/3). Proceed to sign and submit.`
2491
+ };
2492
+ }
2493
+ clearPendingClaim(agentName);
2494
+ if (!isFresh) {
2495
+ return {
2496
+ error: "The previous pending claim has expired.",
2497
+ hint: "Request a fresh claim \u2014 the expired one has been cleared."
2498
+ };
2499
+ }
2500
+ return {
2501
+ error: "Claim has failed 3 times with the same transaction blob.",
2502
+ hint: "Ask the user what they'd like to do. The pending claim has been cleared so a fresh one can be requested."
2503
+ };
2504
+ }
2505
+ }
2506
+ return {
2507
+ error: result.error,
2508
+ status: result.status,
2509
+ code: result.code,
2510
+ hint: result.hint
2511
+ };
2512
+ }
2513
+ if (isClaimPath && agentName) {
2514
+ const data = result.data;
2515
+ if (data.transaction && data.expiresAt) {
2516
+ savePendingClaim(agentName, {
2517
+ seasonId: resolvedBody?.seasonId ?? "",
2518
+ transaction: data.transaction,
2519
+ expiresAt: data.expiresAt,
2520
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString(),
2521
+ retryCount: 0
2522
+ });
2523
+ debugLog("api_call: cached claim blob for retry recovery");
2524
+ }
2525
+ }
2526
+ if (method === "POST" && path6 === "/api/v1/board" && agentName) {
2527
+ markBoardPosted(agentName);
2528
+ }
2529
+ return result.data;
2530
+ }
2531
+ });
2532
+
2533
+ // src/tools/config.ts
2534
+ import { tool as tool3 } from "ai";
2535
+ import fs6 from "fs";
2536
+ import path5 from "path";
2537
+ import crypto2 from "crypto";
2538
+ var readConfigTool = tool3({
2539
+ description: "Read local AstraNova configuration or credentials. Returns public information only \u2014 private keys and API keys are never included.",
2540
+ parameters: readConfigSchema,
2541
+ execute: async ({ key, agentName }) => {
2542
+ const resolvedAgent = agentName ?? getActiveAgent();
2543
+ switch (key) {
2544
+ case "profile": {
2545
+ if (!resolvedAgent) {
2546
+ return { error: "No active agent. Register first." };
2547
+ }
2548
+ const creds = loadCredentials(resolvedAgent);
2549
+ if (!creds) {
2550
+ return { error: `No credentials found for agent "${resolvedAgent}".` };
2551
+ }
2552
+ return {
2553
+ agent_name: creds.agent_name,
2554
+ api_base: creds.api_base
2555
+ };
2556
+ }
2557
+ case "wallet": {
2558
+ if (!resolvedAgent) {
2559
+ return { error: "No active agent. Register first." };
2560
+ }
2561
+ const wallet = loadWallet(resolvedAgent);
2562
+ if (!wallet) {
2563
+ return { error: `No wallet found for agent "${resolvedAgent}". Create one first.` };
2564
+ }
2565
+ return { publicKey: wallet.publicKey };
2566
+ }
2567
+ case "all_agents": {
2568
+ const agents = listAgents();
2569
+ const active = getActiveAgent();
2570
+ return {
2571
+ agents,
2572
+ activeAgent: active,
2573
+ count: agents.length
2574
+ };
2575
+ }
2576
+ case "settings": {
2577
+ const config = loadConfig();
2578
+ if (!config) {
2579
+ return { error: "No config found. Run onboarding first." };
2580
+ }
2581
+ return {
2582
+ provider: config.provider,
2583
+ model: config.model,
2584
+ apiBase: config.apiBase,
2585
+ preferences: config.preferences
2586
+ };
2587
+ }
2588
+ default:
2589
+ return { error: `Unknown config key: ${key}` };
2590
+ }
2591
+ }
2592
+ });
2593
+ var writeConfigTool = tool3({
2594
+ description: "Write local AstraNova configuration. Used to save agent credentials after registration or update profile data. Cannot write wallet files \u2014 use wallet tools instead.",
2595
+ parameters: writeConfigSchema,
2596
+ execute: async ({ agentName, data, file }) => {
2597
+ if (file === "credentials" && data && "secretKey" in data) {
2598
+ return {
2599
+ error: "Cannot write wallet data via write_config. Use create_wallet or import_wallet instead."
2600
+ };
2601
+ }
2602
+ const dir = agentDir(agentName);
2603
+ ensureDir(dir);
2604
+ const filePath = path5.join(dir, `${file}.json`);
2605
+ if (file === "credentials") {
2606
+ const existing = loadCredentials(agentName);
2607
+ const merged = { ...existing, ...data };
2608
+ saveCredentials(agentName, {
2609
+ agent_name: merged.agent_name ?? agentName,
2610
+ api_key: merged.api_key ?? "",
2611
+ api_base: merged.api_base ?? "https://agents.astranova.live"
2612
+ });
2613
+ } else {
2614
+ const tmpPath = path5.join(dir, `.tmp-${crypto2.randomBytes(6).toString("hex")}`);
2615
+ fs6.writeFileSync(tmpPath, JSON.stringify(data, null, 2), {
2616
+ encoding: "utf-8",
2617
+ mode: 384
2618
+ });
2619
+ fs6.renameSync(tmpPath, filePath);
2620
+ }
2621
+ return { success: true, file: `${file}.json`, agent: agentName };
2622
+ }
2623
+ });
2624
+
2625
+ // src/tools/wallet.ts
2626
+ import { tool as tool4 } from "ai";
2627
+ import {
2628
+ Keypair,
2629
+ Connection,
2630
+ VersionedTransaction,
2631
+ clusterApiUrl
2632
+ } from "@solana/web3.js";
2633
+ import nacl from "tweetnacl";
2634
+ import bs58 from "bs58";
2635
+ var SOLANA_RPC = clusterApiUrl("devnet");
2636
+ var createWalletTool = tool4({
2637
+ description: "Generate a new Solana wallet keypair. Saves the keypair locally and returns the public key. The secret key is stored securely and never exposed.",
2638
+ parameters: createWalletSchema,
2639
+ execute: async ({ agentName }) => {
2640
+ const existing = loadWallet(agentName);
2641
+ if (existing) {
2642
+ return {
2643
+ error: `Wallet already exists for agent "${agentName}".`,
2644
+ publicKey: existing.publicKey,
2645
+ hint: "Use the existing wallet or delete wallet.json to regenerate."
2646
+ };
2647
+ }
2648
+ const keypair = Keypair.generate();
2649
+ const publicKey = keypair.publicKey.toBase58();
2650
+ const secretKey = Array.from(keypair.secretKey);
2651
+ saveWallet(agentName, { publicKey, secretKey });
2652
+ return {
2653
+ success: true,
2654
+ publicKey,
2655
+ message: `Wallet created. Public key: ${publicKey}. Now request a challenge from the API to register it.`
2656
+ };
2657
+ }
2658
+ });
2659
+ var signChallengeTool = tool4({
2660
+ description: "Sign a challenge string with the agent's wallet secret key. Returns a base58-encoded signature for wallet registration. The secret key is never exposed.",
2661
+ parameters: signChallengeSchema,
2662
+ execute: async ({ challenge }) => {
2663
+ const agentName = getActiveAgent();
2664
+ if (!agentName) {
2665
+ return { error: "No active agent found." };
2666
+ }
2667
+ const wallet = loadWallet(agentName);
2668
+ if (!wallet) {
2669
+ return {
2670
+ error: `No wallet found for agent "${agentName}". Create one first using create_wallet.`
2671
+ };
2672
+ }
2673
+ try {
2674
+ const secretKeyBytes = Uint8Array.from(wallet.secretKey);
2675
+ const messageBytes = new TextEncoder().encode(challenge);
2676
+ const signature = nacl.sign.detached(messageBytes, secretKeyBytes);
2677
+ const signatureBase58 = bs58.encode(signature);
2678
+ let nonce = "";
2679
+ const singleLineMatch = /(?:verification|nonce)[:\s]+([a-zA-Z0-9_-]+)\s*$/m.exec(challenge);
2680
+ if (singleLineMatch) {
2681
+ nonce = singleLineMatch[1].trim();
2682
+ } else {
2683
+ const tokens = challenge.trim().split(/\s+/);
2684
+ nonce = tokens[tokens.length - 1] ?? "";
2685
+ }
2686
+ return {
2687
+ success: true,
2688
+ signature: signatureBase58,
2689
+ walletAddress: wallet.publicKey,
2690
+ nonce,
2691
+ challengeRaw: challenge,
2692
+ message: `Challenge signed. Now call PUT /api/v1/agents/me/wallet with {"walletAddress":"${wallet.publicKey}","signature":"${signatureBase58}","nonce":"${nonce}"}`
2693
+ };
2694
+ } catch (error) {
2695
+ const message = error instanceof Error ? error.message : "Unknown error";
2696
+ return { error: `Failed to sign challenge: ${message}` };
2697
+ }
2698
+ }
2699
+ });
2700
+ var signAndSendTransactionTool = tool4({
2701
+ description: "Co-sign and submit a partially-signed Solana transaction (base64). Used for claiming $ASTRA rewards. Returns the transaction signature.",
2702
+ parameters: signAndSendTransactionSchema,
2703
+ execute: async ({ transaction: txBase64 }) => {
2704
+ const agentName = getActiveAgent();
2705
+ if (!agentName) {
2706
+ return { error: "No active agent found." };
2707
+ }
2708
+ const wallet = loadWallet(agentName);
2709
+ if (!wallet) {
2710
+ return {
2711
+ error: `No wallet found for agent "${agentName}". Create one first using create_wallet.`
2712
+ };
2713
+ }
2714
+ try {
2715
+ const txBuffer = Buffer.from(txBase64, "base64");
2716
+ const tx = VersionedTransaction.deserialize(txBuffer);
2717
+ const secretKeyBytes = Uint8Array.from(wallet.secretKey);
2718
+ const keypair = Keypair.fromSecretKey(secretKeyBytes);
2719
+ tx.sign([keypair]);
2720
+ const connection = new Connection(SOLANA_RPC, "confirmed");
2721
+ const signature = await connection.sendRawTransaction(tx.serialize(), {
2722
+ skipPreflight: false,
2723
+ preflightCommitment: "confirmed"
2724
+ });
2725
+ const confirmation = await connection.confirmTransaction(signature, "confirmed");
2726
+ if (confirmation.value.err) {
2727
+ return {
2728
+ error: `Transaction confirmed but failed: ${JSON.stringify(confirmation.value.err)}`,
2729
+ txSignature: signature
2730
+ };
2731
+ }
2732
+ if (agentName) {
2733
+ clearPendingClaim(agentName);
2734
+ }
2735
+ return {
2736
+ success: true,
2737
+ txSignature: signature,
2738
+ message: `Transaction submitted and confirmed. Signature: ${signature}. Now confirm with the API using POST /api/v1/agents/me/rewards/confirm.`
2739
+ };
2740
+ } catch (error) {
2741
+ const message = error instanceof Error ? error.message : "Unknown error";
2742
+ return { error: `Transaction failed: ${message}` };
2743
+ }
2744
+ }
2745
+ });
2746
+
2747
+ // src/tools/agent-management.ts
2748
+ import { tool as tool5 } from "ai";
2749
+ import { z as z4 } from "zod";
2750
+ var registerAgentTool = tool5({
2751
+ description: "Register a new AstraNova agent. Calls the API, saves credentials locally, and sets the new agent as active. The CLI will need to restart after this to load the new agent's context.",
2752
+ parameters: registerAgentSchema,
2753
+ execute: async ({ name, description }) => {
2754
+ if (!/^[a-z0-9_-]{2,32}$/.test(name)) {
2755
+ return {
2756
+ error: "Invalid agent name. Must be 2-32 chars, lowercase letters, numbers, hyphens, or underscores."
2757
+ };
2758
+ }
2759
+ const result = await apiCall("POST", "/api/v1/agents/register", { name, description });
2760
+ if (!result.ok) {
2761
+ return {
2762
+ error: result.error,
2763
+ status: result.status,
2764
+ code: result.code,
2765
+ hint: result.hint
2766
+ };
2767
+ }
2768
+ const data = result.data;
2769
+ if (!data.api_key) {
2770
+ return { error: "Registration response missing api_key. Something went wrong." };
2771
+ }
2772
+ saveCredentials(name, {
2773
+ agent_name: name,
2774
+ api_key: data.api_key,
2775
+ api_base: "https://agents.astranova.live"
2776
+ });
2777
+ setActiveAgent(name);
2778
+ updateAgentState(name, {
2779
+ status: "pending_verification",
2780
+ journeyStage: "fresh",
2781
+ verificationCode: data.verification_code
2782
+ });
2783
+ requestRestart();
2784
+ return {
2785
+ success: true,
2786
+ agentName: name,
2787
+ status: "pending_verification",
2788
+ verificationCode: data.verification_code,
2789
+ simBalance: data.agent?.simBalance ?? 1e4,
2790
+ restartRequired: true,
2791
+ message: `Agent "${name}" registered successfully! Verification code: ${data.verification_code}. Restarting to load the new agent...`
2792
+ };
2793
+ }
2794
+ });
2795
+ var switchAgentTool = tool5({
2796
+ description: "Switch to a different registered agent. Updates the active agent. The CLI will need to restart to load the new agent's context.",
2797
+ parameters: switchAgentSchema,
2798
+ execute: async ({ agentName }) => {
2799
+ const creds = loadCredentials(agentName);
2800
+ if (!creds) {
2801
+ const available = listAgents();
2802
+ return {
2803
+ error: `No agent named "${agentName}" found locally.`,
2804
+ availableAgents: available,
2805
+ hint: available.length > 0 ? `Available agents: ${available.join(", ")}` : "No agents registered. Use register_agent to create one."
2806
+ };
2807
+ }
2808
+ const currentAgent = getActiveAgent();
2809
+ if (currentAgent === agentName) {
2810
+ return {
2811
+ message: `"${agentName}" is already the active agent.`,
2812
+ agentName
2813
+ };
2814
+ }
2815
+ setActiveAgent(agentName);
2816
+ requestRestart();
2817
+ return {
2818
+ success: true,
2819
+ agentName,
2820
+ previousAgent: currentAgent,
2821
+ restartRequired: true,
2822
+ message: `Switched to "${agentName}". Restarting to load the new agent...`
2823
+ };
2824
+ }
2825
+ });
2826
+ var listAgentsTool = tool5({
2827
+ description: "List all AstraNova agents registered on this machine, showing which one is active.",
2828
+ parameters: z4.object({}),
2829
+ execute: async () => {
2830
+ const agents = listAgents();
2831
+ const active = getActiveAgent();
2832
+ const state = loadState();
2833
+ const agentDetails = agents.map((name) => ({
2834
+ name,
2835
+ active: name === active,
2836
+ status: state?.agents[name]?.status ?? "unknown",
2837
+ journeyStage: state?.agents[name]?.journeyStage ?? "unknown",
2838
+ createdAt: state?.agents[name]?.createdAt ?? "unknown"
2839
+ }));
2840
+ return {
2841
+ agents: agentDetails,
2842
+ activeAgent: active,
2843
+ count: agents.length
2844
+ };
2845
+ }
2846
+ });
2847
+
2848
+ // src/tools/index.ts
2849
+ var astraTools = {
2850
+ api_call: apiCallTool,
2851
+ read_config: readConfigTool,
2852
+ write_config: writeConfigTool,
2853
+ create_wallet: createWalletTool,
2854
+ sign_challenge: signChallengeTool,
2855
+ sign_and_send_transaction: signAndSendTransactionTool,
2856
+ register_agent: registerAgentTool,
2857
+ switch_agent: switchAgentTool,
2858
+ list_agents: listAgentsTool,
2859
+ update_memory: updateMemoryTool
2860
+ };
2861
+
2862
+ // src/agent/codex-provider.ts
2863
+ var CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex/responses";
2864
+ var DEBUG2 = !!process.env.ASTRA_DEBUG;
2865
+ function debugLog2(msg) {
2866
+ if (DEBUG2) process.stderr.write(`[astra:codex] ${msg}
2867
+ `);
2868
+ }
2869
+ var CodexStreamError = class extends Error {
2870
+ constructor(message, code, retryable) {
2871
+ super(message);
2872
+ this.code = code;
2873
+ this.retryable = retryable;
2874
+ this.name = "CodexStreamError";
2875
+ }
2876
+ };
2877
+ async function callCodex(params) {
2878
+ const { accessToken, model, instructions, input, tools, abortSignal, callbacks, timeoutMs = 9e4, baseUrl = CODEX_BASE_URL } = params;
2879
+ const controller = new AbortController();
2880
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2881
+ if (abortSignal) {
2882
+ if (abortSignal.aborted) {
2883
+ clearTimeout(timeout);
2884
+ controller.abort();
2885
+ } else {
2886
+ abortSignal.addEventListener("abort", () => controller.abort(), { once: true });
2887
+ }
2888
+ }
2889
+ try {
2890
+ const body = {
2891
+ model,
2892
+ instructions,
2893
+ input,
2894
+ store: false,
2895
+ stream: true
2896
+ };
2897
+ if (tools && tools.length > 0) {
2898
+ body.tools = tools;
2899
+ body.tool_choice = "auto";
2900
+ }
2901
+ const response = await fetch(baseUrl, {
2902
+ method: "POST",
2903
+ headers: {
2904
+ Authorization: `Bearer ${accessToken}`,
2905
+ "Content-Type": "application/json"
2906
+ },
2907
+ body: JSON.stringify(body),
2908
+ signal: controller.signal
2909
+ });
2910
+ if (!response.ok) {
2911
+ const errorText = await response.text().catch(() => "");
2912
+ let detail = `HTTP ${response.status}`;
2913
+ try {
2914
+ const errorJson = JSON.parse(errorText);
2915
+ detail = errorJson.detail ?? errorJson.error?.message ?? detail;
2916
+ } catch {
2917
+ if (errorText) detail = errorText.slice(0, 200);
2918
+ }
2919
+ throw new Error(`Responses API error: ${detail}`);
2920
+ }
2921
+ if (!response.body) {
2922
+ throw new Error("Responses API returned no response body");
2923
+ }
2924
+ return await parseSSEStream(response.body, callbacks);
2925
+ } finally {
2926
+ clearTimeout(timeout);
2927
+ }
2928
+ }
2929
+ var CODEX_RETRY_ATTEMPTS = 3;
2930
+ var CODEX_RETRY_BASE_MS = 1e3;
2931
+ var CODEX_RETRY_MAX_MS = 15e3;
2932
+ var CODEX_RETRY_JITTER = 0.3;
2933
+ async function callCodexWithRetry(params) {
2934
+ let lastError = null;
2935
+ let currentToken = params.accessToken;
2936
+ for (let attempt = 1; attempt <= CODEX_RETRY_ATTEMPTS; attempt++) {
2937
+ try {
2938
+ return await callCodex({ ...params, accessToken: currentToken });
2939
+ } catch (err) {
2940
+ lastError = err instanceof Error ? err : new Error(String(err));
2941
+ const msg = lastError.message.toLowerCase();
2942
+ if (msg.includes("401") && attempt === 1 && params.onTokenExpired) {
2943
+ debugLog2(`Codex 401 \u2014 refreshing token and retrying`);
2944
+ currentToken = await params.onTokenExpired();
2945
+ continue;
2946
+ }
2947
+ if (lastError instanceof CodexStreamError && !lastError.retryable) {
2948
+ throw lastError;
2949
+ }
2950
+ const isRetryable = lastError instanceof CodexStreamError && lastError.retryable || msg.includes("429") || msg.includes("rate limit") || msg.includes("500") || msg.includes("502") || msg.includes("503") || msg.includes("504") || msg.includes("network") || msg.includes("fetch failed") || msg.includes("stalled");
2951
+ if (!isRetryable || attempt === CODEX_RETRY_ATTEMPTS) {
2952
+ throw lastError;
2953
+ }
2954
+ const base = Math.min(CODEX_RETRY_BASE_MS * 2 ** (attempt - 1), CODEX_RETRY_MAX_MS);
2955
+ const jitter = base * CODEX_RETRY_JITTER * (Math.random() * 2 - 1);
2956
+ const delay = Math.max(0, base + jitter);
2957
+ debugLog2(`Codex retry ${attempt}/${CODEX_RETRY_ATTEMPTS}: ${lastError.message} \u2014 waiting ${Math.round(delay)}ms`);
2958
+ await new Promise((r) => setTimeout(r, delay));
2959
+ }
2960
+ }
2961
+ throw lastError;
2962
+ }
2963
+ async function parseSSEStream(body, callbacks, idleTimeoutMs = 45e3) {
2964
+ const reader = body.getReader();
2965
+ const decoder = new TextDecoder();
2966
+ let fullText = "";
2967
+ const toolCalls = /* @__PURE__ */ new Map();
2968
+ let buffer = "";
2969
+ let incomplete = false;
2970
+ let incompleteReason;
2971
+ let parseFailures = 0;
2972
+ let idleTimer = null;
2973
+ let idleAborted = false;
2974
+ const resetIdle = () => {
2975
+ if (idleTimer) clearTimeout(idleTimer);
2976
+ idleTimer = setTimeout(() => {
2977
+ idleAborted = true;
2978
+ reader.cancel("idle timeout").catch(() => {
2979
+ });
2980
+ }, idleTimeoutMs);
2981
+ };
2982
+ resetIdle();
2983
+ try {
2984
+ while (true) {
2985
+ const { done, value } = await reader.read();
2986
+ if (done) break;
2987
+ resetIdle();
2988
+ buffer += decoder.decode(value, { stream: true });
2989
+ const lines = buffer.split("\n");
2990
+ buffer = lines.pop() ?? "";
2991
+ for (const line of lines) {
2992
+ if (!line.startsWith("data: ")) continue;
2993
+ const jsonStr = line.slice(6).trim();
2994
+ if (!jsonStr || jsonStr === "[DONE]") continue;
2995
+ try {
2996
+ const event = JSON.parse(jsonStr);
2997
+ switch (event.type) {
2998
+ case "response.output_text.delta":
2999
+ if (event.delta) {
3000
+ fullText += event.delta;
3001
+ callbacks?.onTextChunk?.(event.delta);
3002
+ }
3003
+ break;
3004
+ case "response.output_item.added":
3005
+ if (event.item?.type === "function_call" && event.item.name) {
3006
+ const itemId = event.item.id ?? `fc_${toolCalls.size}`;
3007
+ const callId = event.item.call_id ?? itemId;
3008
+ toolCalls.set(itemId, {
3009
+ itemId,
3010
+ callId,
3011
+ name: event.item.name,
3012
+ args: ""
3013
+ });
3014
+ callbacks?.onToolCallStart?.(event.item.name);
3015
+ }
3016
+ break;
3017
+ case "response.function_call_arguments.delta":
3018
+ if (event.delta && event.item_id) {
3019
+ const tc = toolCalls.get(event.item_id);
3020
+ if (tc) {
3021
+ tc.args += event.delta;
3022
+ }
3023
+ }
3024
+ break;
3025
+ case "response.function_call_arguments.done":
3026
+ if (event.item_id) {
3027
+ const tc = toolCalls.get(event.item_id);
3028
+ if (tc) {
3029
+ callbacks?.onToolCallEnd?.(tc.name);
3030
+ }
3031
+ }
3032
+ break;
3033
+ case "response.failed": {
3034
+ const resp = event.response;
3035
+ const statusDetails = resp?.status_details;
3036
+ const errInfo = statusDetails?.error ?? resp?.error ?? {};
3037
+ const code = errInfo.code ?? "unknown_error";
3038
+ const msg = errInfo.message ?? "The model returned an error.";
3039
+ const retryable = ["rate_limit_exceeded", "server_error"].includes(code);
3040
+ throw new CodexStreamError(msg, code, retryable);
3041
+ }
3042
+ case "response.incomplete": {
3043
+ const resp = event.response;
3044
+ const statusDetails = resp?.status_details;
3045
+ const reason = statusDetails?.reason ?? "unknown";
3046
+ incomplete = true;
3047
+ incompleteReason = reason;
3048
+ break;
3049
+ }
3050
+ }
3051
+ } catch (e) {
3052
+ if (e instanceof CodexStreamError) throw e;
3053
+ parseFailures++;
3054
+ debugLog2(`SSE parse failure #${parseFailures}: ${line.slice(0, 100)}`);
3055
+ }
3056
+ }
3057
+ }
3058
+ } finally {
3059
+ if (idleTimer) clearTimeout(idleTimer);
3060
+ reader.releaseLock();
3061
+ }
3062
+ if (idleAborted) {
3063
+ throw new Error("Responses API stream stalled \u2014 no data received for 45 seconds.");
3064
+ }
3065
+ if (parseFailures > 0 && fullText === "" && toolCalls.size === 0) {
3066
+ throw new Error(
3067
+ `Responses API returned no usable events (${parseFailures} parse failures). The API format may have changed.`
3068
+ );
3069
+ }
3070
+ return {
3071
+ text: fullText,
3072
+ toolCalls: Array.from(toolCalls.values()).map((tc) => ({
3073
+ id: tc.itemId,
3074
+ callId: tc.callId,
3075
+ name: tc.name,
3076
+ arguments: tc.args
3077
+ })),
3078
+ incomplete,
3079
+ incompleteReason
3080
+ };
3081
+ }
3082
+ function convertToolsForCodex(tools) {
3083
+ return Object.entries(tools).map(([name, def]) => ({
3084
+ type: "function",
3085
+ name,
3086
+ description: def.description ?? "",
3087
+ parameters: def.parameters ?? {}
3088
+ }));
3089
+ }
3090
+
3091
+ // src/utils/audit.ts
3092
+ import fs7 from "fs";
3093
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
3094
+ "secretKey",
3095
+ "secret_key",
3096
+ "privateKey",
3097
+ "private_key",
3098
+ "api_key",
3099
+ "apiKey",
3100
+ "accessToken",
3101
+ "refreshToken",
3102
+ "password",
3103
+ "Authorization",
3104
+ "authorization"
3105
+ ]);
3106
+ var MAX_LOG_SIZE_BYTES = 10 * 1024 * 1024;
3107
+ function sanitize(obj) {
3108
+ if (obj === null || obj === void 0) return obj;
3109
+ if (typeof obj !== "object") return obj;
3110
+ if (Array.isArray(obj)) {
3111
+ return obj.map(sanitize);
3112
+ }
3113
+ const result = {};
3114
+ for (const [key, value] of Object.entries(obj)) {
3115
+ if (SENSITIVE_KEYS.has(key)) {
3116
+ result[key] = "[REDACTED]";
3117
+ } else {
3118
+ result[key] = sanitize(value);
3119
+ }
3120
+ }
3121
+ return result;
3122
+ }
3123
+ function writeAuditEntry(entry) {
3124
+ const logPath = auditLogPath();
3125
+ try {
3126
+ if (fs7.existsSync(logPath)) {
3127
+ const stat = fs7.statSync(logPath);
3128
+ if (stat.size > MAX_LOG_SIZE_BYTES) {
3129
+ const backupPath = logPath.replace(".log", ".old.log");
3130
+ if (fs7.existsSync(backupPath)) fs7.unlinkSync(backupPath);
3131
+ fs7.renameSync(logPath, backupPath);
3132
+ }
3133
+ }
3134
+ const line = JSON.stringify({
3135
+ ...entry,
3136
+ args: sanitize(entry.args),
3137
+ result: truncateResult(sanitize(entry.result))
3138
+ });
3139
+ fs7.appendFileSync(logPath, line + "\n", { encoding: "utf-8" });
3140
+ } catch {
3141
+ }
3142
+ }
3143
+ function truncateResult(result) {
3144
+ const str = JSON.stringify(result);
3145
+ if (str.length <= 2e3) return result;
3146
+ return { _truncated: true, preview: str.slice(0, 500) + "..." };
3147
+ }
3148
+
3149
+ // src/agent/compaction.ts
3150
+ var CONTEXT_WINDOWS = {
3151
+ claude: 18e4,
3152
+ openai: 12e4,
3153
+ google: 9e5,
3154
+ "openai-oauth": 12e4,
3155
+ ollama: 8e3
3156
+ };
3157
+ var COMPACTION_THRESHOLD = 0.85;
3158
+ var SAFETY_MARGIN = 1.4;
3159
+ var USER_MSG_BUDGET = 2e4;
3160
+ var MAX_USER_MSGS = 10;
3161
+ var COMPACTION_PROMPT = `You are performing a CONTEXT CHECKPOINT for an AstraNova trading agent conversation.
3162
+ Create a concise handoff summary for the next turn of this conversation.
3163
+
3164
+ Include:
3165
+ - Current progress and what was accomplished
3166
+ - Key decisions made and user preferences
3167
+ - Critical data: wallet addresses, transaction signatures, verification codes, balances
3168
+ - Current agent state: registration status, portfolio position, pending actions
3169
+ - What remains to be done (clear next steps if any)
3170
+
3171
+ Be concise and structured. Focus on information needed to continue seamlessly.`;
3172
+ var SUMMARY_PREFIX = `A previous part of this conversation was compacted to save context space.
3173
+ Below is a summary of what happened, followed by the most recent messages.
3174
+ Use this summary to maintain continuity.
3175
+
3176
+ ---
3177
+
3178
+ `;
3179
+ function estimateTokens(text3) {
3180
+ return Math.ceil(text3.length / 4);
3181
+ }
3182
+ function estimateMessageTokens(messages) {
3183
+ let total = 0;
3184
+ for (const m of messages) {
3185
+ if (typeof m.content === "string") {
3186
+ total += estimateTokens(m.content);
3187
+ } else {
3188
+ total += estimateTokens(JSON.stringify(m.content));
3189
+ }
3190
+ }
3191
+ return total;
3192
+ }
3193
+ function needsCompaction(messages, systemPromptTokens, provider) {
3194
+ const contextWindow = CONTEXT_WINDOWS[provider] ?? 12e4;
3195
+ const messageTokens = estimateMessageTokens(messages);
3196
+ const estimated = messageTokens * SAFETY_MARGIN + systemPromptTokens;
3197
+ return estimated >= contextWindow * COMPACTION_THRESHOLD;
3198
+ }
3199
+ async function compactMessages(messages, provider, profile, llmSummarize) {
3200
+ const tokensBefore = estimateMessageTokens(messages);
3201
+ const summary = await llmSummarize(messages);
3202
+ const recentUserMsgs = [];
3203
+ let userTokens = 0;
3204
+ for (let i = messages.length - 1; i >= 0; i--) {
3205
+ const m = messages[i];
3206
+ if (m.role !== "user") continue;
3207
+ const msgTokens = typeof m.content === "string" ? estimateTokens(m.content) : estimateTokens(JSON.stringify(m.content));
3208
+ if (userTokens + msgTokens > USER_MSG_BUDGET) break;
3209
+ if (recentUserMsgs.length >= MAX_USER_MSGS) break;
3210
+ recentUserMsgs.unshift(m);
3211
+ userTokens += msgTokens;
3212
+ }
3213
+ const contextRefresh = buildContextRefresh(profile);
3214
+ const summaryMessage = {
3215
+ role: "user",
3216
+ content: SUMMARY_PREFIX + summary + "\n\n---\n\n" + contextRefresh
3217
+ };
3218
+ const compactedMessages = [summaryMessage];
3219
+ if (recentUserMsgs.length > 0) {
3220
+ compactedMessages.push({
3221
+ role: "assistant",
3222
+ content: "Understood \u2014 I have the context from our earlier conversation. Continuing from where we left off."
3223
+ });
3224
+ compactedMessages.push(...recentUserMsgs);
3225
+ }
3226
+ const tokensAfter = estimateMessageTokens(compactedMessages);
3227
+ return {
3228
+ messages: compactedMessages,
3229
+ compacted: true,
3230
+ tokensBefore,
3231
+ tokensAfter
3232
+ };
3233
+ }
3234
+ function forceCompact(messages, profile) {
3235
+ const contextRefresh = buildContextRefresh(profile);
3236
+ const recentUserMsgs = [];
3237
+ for (let i = messages.length - 1; i >= 0 && recentUserMsgs.length < 3; i--) {
3238
+ if (messages[i].role === "user") {
3239
+ recentUserMsgs.unshift(messages[i]);
3240
+ }
3241
+ }
3242
+ const summaryMessage = {
3243
+ role: "user",
3244
+ content: `Earlier conversation was too long and had to be discarded. Here is the current agent state:
3245
+
3246
+ ${contextRefresh}`
3247
+ };
3248
+ const result = [summaryMessage];
3249
+ if (recentUserMsgs.length > 0) {
3250
+ result.push({
3251
+ role: "assistant",
3252
+ content: "Got it \u2014 I've lost the earlier context but I can see your recent messages. How can I help?"
3253
+ });
3254
+ result.push(...recentUserMsgs);
3255
+ }
3256
+ return result;
3257
+ }
3258
+ function isContextLengthError(error) {
3259
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
3260
+ return msg.includes("context_length_exceeded") || msg.includes("maximum context length") || msg.includes("too many tokens") || msg.includes("max_tokens") || msg.includes("content_too_large") || msg.includes("request too large");
3261
+ }
3262
+
3263
+ // src/agent/loop.ts
3264
+ var TURN_TIMEOUT_MS = Number(process.env.ASTRA_TIMEOUT) || 18e4;
3265
+ var DEBUG3 = !!process.env.ASTRA_DEBUG;
3266
+ function debugLog3(msg) {
3267
+ if (DEBUG3) process.stderr.write(`[astra] ${msg}
3268
+ `);
3269
+ }
3270
+ async function runAgentTurn(messages, skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext, profile, callbacks, memoryContent) {
3271
+ const systemPrompt = buildSystemPrompt(skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext, profile, memoryContent);
3272
+ const config = loadConfig();
3273
+ const provider = config?.provider ?? "openai";
3274
+ const systemPromptTokens = estimateTokens(systemPrompt);
3275
+ let compacted = false;
3276
+ if (needsCompaction(messages, systemPromptTokens, provider)) {
3277
+ debugLog3("Context approaching limit \u2014 compacting...");
3278
+ const result2 = await compactMessages(
3279
+ messages,
3280
+ provider,
3281
+ profile,
3282
+ (msgs) => summarizeForCompaction(msgs, systemPrompt, provider, config?.model)
3283
+ );
3284
+ debugLog3(`Compacted: ${result2.tokensBefore} \u2192 ${result2.tokensAfter} tokens`);
3285
+ messages = result2.messages;
3286
+ compacted = true;
3287
+ }
3288
+ const runTurn = () => isCodexOAuth() ? runCodexTurn(messages, systemPrompt, callbacks, config) : isOpenAIResponses() ? runOpenAIResponsesTurn(messages, systemPrompt, callbacks, config) : runSdkTurn(messages, systemPrompt, callbacks);
3289
+ let result;
3290
+ try {
3291
+ result = await runTurn();
3292
+ } catch (error) {
3293
+ if (isContextLengthError(error) && !compacted) {
3294
+ debugLog3("Context length error \u2014 emergency compaction");
3295
+ messages = forceCompact(messages, profile);
3296
+ compacted = true;
3297
+ result = await runTurn();
3298
+ } else {
3299
+ throw error;
3300
+ }
3301
+ }
3302
+ if (isEmptyResponse(result) && !compacted) {
3303
+ debugLog3("Empty response from LLM \u2014 retrying with nudge");
3304
+ callbacks.onTextChunk("\n\nHold on, let me try that again...\n\n");
3305
+ messages = [
3306
+ ...messages,
3307
+ { role: "assistant", content: "(My previous response was empty \u2014 retrying.)" },
3308
+ { role: "user", content: "Please continue \u2014 I'm waiting for your response." }
3309
+ ];
3310
+ try {
3311
+ const retry = isCodexOAuth() ? await runCodexTurn(messages, systemPrompt, callbacks, config) : isOpenAIResponses() ? await runOpenAIResponsesTurn(messages, systemPrompt, callbacks, config) : await runSdkTurn(messages, systemPrompt, callbacks);
3312
+ if (!isEmptyResponse(retry)) {
3313
+ result = retry;
3314
+ }
3315
+ } catch (retryError) {
3316
+ debugLog3(`Retry also failed: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
3317
+ }
3318
+ }
3319
+ if (compacted) {
3320
+ result.compactedMessages = messages;
3321
+ }
3322
+ return result;
3323
+ }
3324
+ var OPENAI_RESPONSES_URL = "https://api.openai.com/v1/responses";
3325
+ async function runCodexTurn(messages, systemPrompt, callbacks, config) {
3326
+ return runResponsesApiTurn(messages, systemPrompt, callbacks, {
3327
+ model: config?.model ?? "gpt-5.3-codex",
3328
+ getAccessToken: () => getCodexAccessToken(),
3329
+ onTokenExpired: () => getCodexAccessToken()
3330
+ });
3331
+ }
3332
+ async function runOpenAIResponsesTurn(messages, systemPrompt, callbacks, config) {
3333
+ const apiKey = getOpenAIApiKey();
3334
+ return runResponsesApiTurn(messages, systemPrompt, callbacks, {
3335
+ model: config?.model ?? "gpt-4o-mini",
3336
+ baseUrl: OPENAI_RESPONSES_URL,
3337
+ getAccessToken: async () => apiKey
3338
+ // No onTokenExpired — API keys don't expire
3339
+ });
3340
+ }
3341
+ async function runResponsesApiTurn(messages, systemPrompt, callbacks, turnConfig) {
3342
+ const { model, baseUrl, getAccessToken, onTokenExpired } = turnConfig;
3343
+ const codexInput = convertToCodexInput(messages);
3344
+ const codexTools = convertToolsForCodex(
3345
+ Object.fromEntries(
3346
+ Object.entries(astraTools).map(([name, t]) => [
3347
+ name,
3348
+ {
3349
+ description: t.description,
3350
+ parameters: extractJsonSchema(t)
3351
+ }
3352
+ ])
3353
+ )
3354
+ );
3355
+ const responseMessages = [];
3356
+ let accessToken = await getAccessToken();
3357
+ let result = await callCodexWithRetry({
3358
+ accessToken,
3359
+ model,
3360
+ instructions: systemPrompt,
3361
+ input: codexInput,
3362
+ tools: codexTools,
3363
+ callbacks,
3364
+ baseUrl,
3365
+ onTokenExpired
3366
+ });
3367
+ let finalText = result.text;
3368
+ if (result.incomplete) {
3369
+ debugLog3(`Response incomplete: ${result.incompleteReason}`);
3370
+ if (result.toolCalls.length > 0) {
3371
+ debugLog3(`Discarding ${result.toolCalls.length} tool calls from incomplete response`);
3372
+ result = { ...result, toolCalls: [] };
3373
+ }
3374
+ finalText += "\n\n(Response was truncated \u2014 try a shorter message or start a new session.)";
3375
+ }
3376
+ let steps = 0;
3377
+ debugLog3(`Response: text=${result.text.length}chars, toolCalls=${result.toolCalls.map((tc) => tc.name).join(",") || "none"}`);
3378
+ while (result.toolCalls.length > 0 && steps < 10) {
3379
+ steps++;
3380
+ const assistantContent = [];
3381
+ if (result.text) {
3382
+ assistantContent.push({ type: "text", text: result.text });
3383
+ }
3384
+ const toolResultParts = [];
3385
+ for (const tc of result.toolCalls) {
3386
+ const toolDef = astraTools[tc.name];
3387
+ let parsedArgs = {};
3388
+ try {
3389
+ parsedArgs = JSON.parse(tc.arguments);
3390
+ } catch {
3391
+ }
3392
+ assistantContent.push({
3393
+ type: "tool-call",
3394
+ toolCallId: tc.callId,
3395
+ toolName: tc.name,
3396
+ args: parsedArgs
3397
+ });
3398
+ if (!toolDef) {
3399
+ const errorResult = { error: `Tool "${tc.name}" not found.` };
3400
+ toolResultParts.push({
3401
+ type: "tool-result",
3402
+ toolCallId: tc.callId,
3403
+ result: errorResult
3404
+ });
3405
+ codexInput.push({
3406
+ type: "function_call",
3407
+ id: tc.id,
3408
+ call_id: tc.callId,
3409
+ name: tc.name,
3410
+ arguments: tc.arguments
3411
+ });
3412
+ codexInput.push({
3413
+ type: "function_call_output",
3414
+ call_id: tc.callId,
3415
+ output: JSON.stringify(errorResult)
3416
+ });
3417
+ continue;
3418
+ }
3419
+ callbacks.onToolCallStart?.(tc.name);
3420
+ const startTime = Date.now();
3421
+ try {
3422
+ debugLog3(`Tool ${tc.name}(${tc.callId}) args: ${JSON.stringify(parsedArgs)}`);
3423
+ const execute = toolDef.execute;
3424
+ const toolResult = await execute(parsedArgs, {});
3425
+ debugLog3(`Tool ${tc.name}(${tc.callId}) result: ${JSON.stringify(toolResult).slice(0, 200)}`);
3426
+ writeAuditEntry({
3427
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3428
+ tool: tc.name,
3429
+ args: parsedArgs,
3430
+ result: toolResult,
3431
+ ok: !toolResult?.error,
3432
+ durationMs: Date.now() - startTime
3433
+ });
3434
+ callbacks.onToolCallEnd?.(tc.name);
3435
+ toolResultParts.push({
3436
+ type: "tool-result",
3437
+ toolCallId: tc.callId,
3438
+ result: toolResult
3439
+ });
3440
+ codexInput.push({
3441
+ type: "function_call",
3442
+ id: tc.id,
3443
+ call_id: tc.callId,
3444
+ name: tc.name,
3445
+ arguments: tc.arguments
3446
+ });
3447
+ codexInput.push({
3448
+ type: "function_call_output",
3449
+ call_id: tc.callId,
3450
+ output: JSON.stringify(toolResult)
3451
+ });
3452
+ } catch (toolError) {
3453
+ callbacks.onToolCallEnd?.(tc.name);
3454
+ const errMsg = toolError instanceof Error ? toolError.message : "Tool execution failed";
3455
+ debugLog3(`Tool ${tc.name}(${tc.callId}) error: ${errMsg}`);
3456
+ const errorResult = { error: errMsg };
3457
+ writeAuditEntry({
3458
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3459
+ tool: tc.name,
3460
+ args: parsedArgs,
3461
+ result: errorResult,
3462
+ ok: false,
3463
+ durationMs: Date.now() - startTime
3464
+ });
3465
+ toolResultParts.push({
3466
+ type: "tool-result",
3467
+ toolCallId: tc.callId,
3468
+ result: errorResult
3469
+ });
3470
+ codexInput.push({
3471
+ type: "function_call",
3472
+ id: tc.id,
3473
+ call_id: tc.callId,
3474
+ name: tc.name,
3475
+ arguments: tc.arguments
3476
+ });
3477
+ codexInput.push({
3478
+ type: "function_call_output",
3479
+ call_id: tc.callId,
3480
+ output: JSON.stringify(errorResult)
3481
+ });
3482
+ }
3483
+ }
3484
+ responseMessages.push({ role: "assistant", content: assistantContent });
3485
+ if (toolResultParts.length > 0) {
3486
+ responseMessages.push({ role: "tool", content: toolResultParts });
3487
+ }
3488
+ accessToken = await getAccessToken();
3489
+ result = await callCodexWithRetry({
3490
+ accessToken,
3491
+ model,
3492
+ instructions: systemPrompt,
3493
+ input: codexInput,
3494
+ tools: codexTools,
3495
+ callbacks,
3496
+ baseUrl,
3497
+ onTokenExpired
3498
+ });
3499
+ finalText = result.text;
3500
+ if (result.incomplete) {
3501
+ debugLog3(`Step ${steps} incomplete: ${result.incompleteReason}`);
3502
+ if (result.toolCalls.length > 0) {
3503
+ debugLog3(`Discarding ${result.toolCalls.length} tool calls from incomplete response`);
3504
+ result = { ...result, toolCalls: [] };
3505
+ }
3506
+ finalText += "\n\n(Response was truncated \u2014 try a shorter message or start a new session.)";
3507
+ }
3508
+ debugLog3(`Step ${steps}: text=${result.text.length}chars, toolCalls=${result.toolCalls.map((tc) => tc.name).join(",") || "none"}`);
3509
+ }
3510
+ debugLog3(`Responses loop done after ${steps} steps, finalText=${finalText.length}chars`);
3511
+ let text3 = finalText;
3512
+ if (!text3 && steps > 0) {
3513
+ debugLog3("Tools ran but no summary text \u2014 nudging model for a summary");
3514
+ codexInput.push({ role: "user", content: "Please summarize what just happened." });
3515
+ accessToken = await getAccessToken();
3516
+ const summaryRetry = await callCodexWithRetry({
3517
+ accessToken,
3518
+ model,
3519
+ instructions: systemPrompt,
3520
+ input: codexInput,
3521
+ tools: codexTools,
3522
+ callbacks,
3523
+ baseUrl,
3524
+ onTokenExpired
3525
+ });
3526
+ text3 = summaryRetry.text;
3527
+ }
3528
+ if (!text3 && steps > 0) {
3529
+ text3 = "I ran the requested action but the model returned no summary. Please try asking again.";
3530
+ } else if (!text3) {
3531
+ text3 = "(No response from model)";
3532
+ }
3533
+ responseMessages.push({ role: "assistant", content: text3 });
3534
+ return { text: text3, responseMessages };
3535
+ }
3536
+ async function runSdkTurn(messages, systemPrompt, callbacks) {
3537
+ debugLog3(`SDK turn starting \u2014 getting model...`);
3538
+ const model = await getModel();
3539
+ debugLog3(`Model ready: ${model.modelId ?? "unknown"} \u2014 calling streamText...`);
3540
+ const abortController = new AbortController();
3541
+ const timeout = setTimeout(() => {
3542
+ debugLog3("SDK turn timeout \u2014 aborting after 90s");
3543
+ abortController.abort();
3544
+ }, TURN_TIMEOUT_MS);
3545
+ try {
3546
+ const result = streamText({
3547
+ model,
3548
+ system: systemPrompt,
3549
+ messages,
3550
+ tools: astraTools,
3551
+ maxSteps: 10,
3552
+ temperature: 0.7,
3553
+ abortSignal: abortController.signal,
3554
+ onStepFinish: ({ toolCalls, toolResults }) => {
3555
+ if (toolCalls && toolCalls.length > 0) {
3556
+ for (let i = 0; i < toolCalls.length; i++) {
3557
+ const tc = toolCalls[i];
3558
+ const tr = toolResults?.[i];
3559
+ callbacks.onToolCallEnd?.(tc.toolName);
3560
+ writeAuditEntry({
3561
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3562
+ tool: tc.toolName,
3563
+ args: tc.args,
3564
+ result: tr?.result,
3565
+ ok: !tr?.result?.error,
3566
+ durationMs: 0
3567
+ // not available from SDK callback
3568
+ });
3569
+ }
3570
+ }
3571
+ }
3572
+ });
3573
+ debugLog3("streamText created \u2014 consuming textStream...");
3574
+ for await (const chunk of result.textStream) {
3575
+ callbacks.onTextChunk(chunk);
3576
+ }
3577
+ debugLog3("textStream consumed \u2014 awaiting response...");
3578
+ const response = await result.response;
3579
+ const text3 = await result.text;
3580
+ debugLog3(`SDK turn done \u2014 text=${text3.length}chars, messages=${response.messages.length}`);
3581
+ return {
3582
+ text: text3 || "(No response from LLM)",
3583
+ responseMessages: response.messages
3584
+ };
3585
+ } catch (error) {
3586
+ clearTimeout(timeout);
3587
+ if (abortController.signal.aborted) {
3588
+ throw new Error(`Response timed out after ${TURN_TIMEOUT_MS / 1e3}s. Please try again.`);
3589
+ }
3590
+ const message = error instanceof Error ? error.message : String(error);
3591
+ debugLog3(`Agent loop error: ${message}`);
3592
+ throw error;
3593
+ } finally {
3594
+ clearTimeout(timeout);
3595
+ }
3596
+ }
3597
+ function convertToCodexInput(messages) {
3598
+ const codexInput = [];
3599
+ for (const m of messages) {
3600
+ if (m.role === "user" || m.role === "assistant") {
3601
+ if (m.role === "assistant" && Array.isArray(m.content)) {
3602
+ const textParts = m.content.filter(
3603
+ (p) => p.type === "text"
3604
+ );
3605
+ if (textParts.length > 0) {
3606
+ codexInput.push({
3607
+ role: "assistant",
3608
+ content: textParts.map((p) => p.text).join("")
3609
+ });
3610
+ }
3611
+ for (const part of m.content) {
3612
+ if (part.type === "tool-call") {
3613
+ const tc = part;
3614
+ const fcId = tc.toolCallId.startsWith("fc_") ? tc.toolCallId : `fc_${tc.toolCallId}`;
3615
+ codexInput.push({
3616
+ type: "function_call",
3617
+ id: fcId,
3618
+ call_id: tc.toolCallId,
3619
+ name: tc.toolName,
3620
+ arguments: JSON.stringify(tc.args)
3621
+ });
3622
+ }
3623
+ }
3624
+ } else {
3625
+ codexInput.push({
3626
+ role: m.role,
3627
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
3628
+ });
3629
+ }
3630
+ } else if (m.role === "tool") {
3631
+ if (Array.isArray(m.content)) {
3632
+ for (const part of m.content) {
3633
+ if (part.type === "tool-result") {
3634
+ const tr = part;
3635
+ codexInput.push({
3636
+ type: "function_call_output",
3637
+ call_id: tr.toolCallId,
3638
+ output: JSON.stringify(tr.result)
3639
+ });
3640
+ }
3641
+ }
3642
+ }
3643
+ }
3644
+ }
3645
+ return codexInput;
3646
+ }
3647
+ async function summarizeForCompaction(messages, systemPrompt, provider, modelName) {
3648
+ const summaryInstruction = `${systemPrompt}
3649
+
3650
+ ---
3651
+
3652
+ ${COMPACTION_PROMPT}`;
3653
+ if (provider === "openai-oauth") {
3654
+ const accessToken = await getCodexAccessToken();
3655
+ const codexInput = convertToCodexInput(messages);
3656
+ const result2 = await callCodex({
3657
+ accessToken,
3658
+ model: modelName ?? "gpt-5.3-codex",
3659
+ instructions: summaryInstruction,
3660
+ input: codexInput,
3661
+ timeoutMs: 6e4
3662
+ });
3663
+ return result2.text || "No summary generated.";
3664
+ }
3665
+ if (provider === "openai") {
3666
+ const apiKey = getOpenAIApiKey();
3667
+ const codexInput = convertToCodexInput(messages);
3668
+ const result2 = await callCodex({
3669
+ accessToken: apiKey,
3670
+ model: modelName ?? "gpt-4o-mini",
3671
+ instructions: summaryInstruction,
3672
+ input: codexInput,
3673
+ timeoutMs: 6e4,
3674
+ baseUrl: OPENAI_RESPONSES_URL
3675
+ });
3676
+ return result2.text || "No summary generated.";
3677
+ }
3678
+ const model = await getModel();
3679
+ const result = await generateText({
3680
+ model,
3681
+ system: summaryInstruction,
3682
+ messages,
3683
+ temperature: 0.3
3684
+ });
3685
+ return result.text || "No summary generated.";
3686
+ }
3687
+ var EMPTY_SENTINELS = [
3688
+ "(No response from model)",
3689
+ "(No response from LLM)",
3690
+ "I ran the requested action but the model returned no summary."
3691
+ ];
3692
+ function isEmptyResponse(result) {
3693
+ if (!result.text) return true;
3694
+ const t = result.text.trim();
3695
+ return EMPTY_SENTINELS.some((s) => t === s || t.startsWith(s));
3696
+ }
3697
+ function extractJsonSchema(toolDef) {
3698
+ const t = toolDef;
3699
+ if (!t.parameters) return {};
3700
+ const params = t.parameters;
3701
+ if (params._def) {
3702
+ try {
3703
+ return zodToJsonSchema(params);
3704
+ } catch {
3705
+ return {};
3706
+ }
3707
+ }
3708
+ return {};
3709
+ }
3710
+
3711
+ // src/ui/App.tsx
3712
+ import { jsx as jsx7, jsxs as jsxs8 } from "react/jsx-runtime";
3713
+ function App({
3714
+ agentName,
3715
+ skillContext,
3716
+ tradingContext,
3717
+ walletContext,
3718
+ rewardsContext,
3719
+ onboardingContext,
3720
+ apiContext,
3721
+ profile,
3722
+ sessionId,
3723
+ memoryContent,
3724
+ initialCoreMessages,
3725
+ initialChatMessages,
3726
+ debug
3727
+ }) {
3728
+ const { exit } = useApp();
3729
+ const [chatMessages, setChatMessages] = useState4(
3730
+ initialChatMessages ?? []
3731
+ );
3732
+ const [coreMessages, setCoreMessages] = useState4(
3733
+ initialCoreMessages ?? []
3734
+ );
3735
+ const providerRef = useRef2(loadConfig()?.provider ?? "unknown");
3736
+ const [streamingText, setStreamingText] = useState4(
3737
+ void 0
3738
+ );
3739
+ const [isLoading, setIsLoading] = useState4(false);
3740
+ const [toolName, setToolName] = useState4(void 0);
3741
+ useInput((_input, key) => {
3742
+ if (key.ctrl && _input === "c") {
3743
+ exit();
3744
+ }
3745
+ });
3746
+ const sendMessage = useCallback2(
3747
+ async (userText) => {
3748
+ if (userText.startsWith("/")) {
3749
+ const cmd = userText.split(" ")[0].toLowerCase();
3750
+ if (cmd === "/exit" || cmd === "/quit" || cmd === "/q") {
3751
+ exit();
3752
+ return;
3753
+ }
3754
+ if (cmd === "/clear") {
3755
+ setChatMessages([]);
3756
+ return;
3757
+ }
3758
+ if (cmd === "/compact") {
3759
+ setChatMessages((prev) => [
3760
+ ...prev,
3761
+ { role: "user", content: userText },
3762
+ { role: "assistant", content: "Manual compaction is not implemented yet \u2014 it happens automatically when context gets large." }
3763
+ ]);
3764
+ return;
3765
+ }
3766
+ if (cmd === "/help" || cmd === "/?") {
3767
+ setChatMessages((prev) => [
3768
+ ...prev,
3769
+ { role: "user", content: userText },
3770
+ {
3771
+ role: "assistant",
3772
+ content: [
3773
+ "**Quick Actions**",
3774
+ "",
3775
+ " `/portfolio` \u2014 Show portfolio card",
3776
+ " `/market` \u2014 Current price, mood & trend",
3777
+ " `/rewards` \u2014 Check claimable $ASTRA",
3778
+ " `/trades` \u2014 Recent trade history",
3779
+ " `/board` \u2014 Browse the board",
3780
+ " `/wallet` \u2014 Check wallet status",
3781
+ " `/buy <amt>` \u2014 Buy $NOVA (e.g. `/buy 500`)",
3782
+ " `/sell <amt>`\u2014 Sell $NOVA (e.g. `/sell 200`)",
3783
+ "",
3784
+ "**Ask me about**",
3785
+ "",
3786
+ ' "how do epochs and seasons work?"',
3787
+ ' "what are the trading rules?"',
3788
+ ' "how do I earn $ASTRA?"',
3789
+ ' "show the season leaderboard"',
3790
+ ' "what is $ASTRA token supply?"',
3791
+ "",
3792
+ "**System**",
3793
+ "",
3794
+ " `/help` \u2014 Show this help",
3795
+ " `/exit` \u2014 Exit (also `/quit`, `/q`)",
3796
+ " `/clear` \u2014 Clear chat display",
3797
+ " `Ctrl+C` \u2014 Exit immediately"
3798
+ ].join("\n")
3799
+ }
3800
+ ]);
3801
+ return;
3802
+ }
3803
+ const shortcuts = {
3804
+ "/portfolio": "Show my portfolio using the card format.",
3805
+ "/market": "Show the current market state \u2014 price, mood, and recent trend.",
3806
+ "/rewards": "Check my rewards status and show if anything is claimable.",
3807
+ "/trades": "Show my recent trade history.",
3808
+ "/board": "Show recent posts from the board.",
3809
+ "/wallet": "Check my wallet status \u2014 do I have one set up?",
3810
+ "/buy": `Buy ${userText.split(" ").slice(1).join(" ") || "some"} $NOVA.`,
3811
+ "/sell": `Sell ${userText.split(" ").slice(1).join(" ") || "some"} $NOVA.`
3812
+ };
3813
+ if (shortcuts[cmd]) {
3814
+ userText = shortcuts[cmd];
3815
+ } else {
3816
+ setChatMessages((prev) => [
3817
+ ...prev,
3818
+ { role: "user", content: userText },
3819
+ { role: "assistant", content: `Unknown command: ${cmd}. Type /help for available commands.` }
3820
+ ]);
3821
+ return;
3822
+ }
3823
+ }
3824
+ setChatMessages((prev) => [
3825
+ ...prev,
3826
+ { role: "user", content: userText }
3827
+ ]);
3828
+ const newCoreMessages = [
3829
+ ...coreMessages,
3830
+ { role: "user", content: userText }
3831
+ ];
3832
+ setIsLoading(true);
3833
+ setStreamingText("");
3834
+ setToolName(void 0);
3835
+ try {
3836
+ const result = await runAgentTurn(
3837
+ newCoreMessages,
3838
+ skillContext,
3839
+ tradingContext,
3840
+ walletContext,
3841
+ rewardsContext,
3842
+ onboardingContext,
3843
+ apiContext,
3844
+ profile,
3845
+ {
3846
+ onTextChunk: (chunk) => {
3847
+ setStreamingText((prev) => (prev ?? "") + chunk);
3848
+ },
3849
+ onToolCallStart: (name) => {
3850
+ setToolName(name);
3851
+ },
3852
+ onToolCallEnd: () => {
3853
+ setToolName(void 0);
3854
+ }
3855
+ },
3856
+ memoryContent
3857
+ );
3858
+ const baseCoreMessages = result.compactedMessages ?? newCoreMessages;
3859
+ const updatedCore = [...baseCoreMessages, ...result.responseMessages];
3860
+ let updatedChat;
3861
+ if (result.compactedMessages) {
3862
+ const marker = { role: "assistant", content: "\u2014 earlier messages compacted \u2014" };
3863
+ const recentChat = chatMessages.slice(-12);
3864
+ updatedChat = [
3865
+ marker,
3866
+ ...recentChat,
3867
+ { role: "user", content: userText },
3868
+ { role: "assistant", content: result.text }
3869
+ ];
3870
+ } else {
3871
+ updatedChat = [
3872
+ ...chatMessages,
3873
+ { role: "user", content: userText },
3874
+ { role: "assistant", content: result.text }
3875
+ ];
3876
+ }
3877
+ setChatMessages(updatedChat);
3878
+ setCoreMessages(updatedCore);
3879
+ setStreamingText(void 0);
3880
+ saveSession({
3881
+ agentName,
3882
+ provider: providerRef.current,
3883
+ sessionId,
3884
+ coreMessages: updatedCore,
3885
+ chatMessages: updatedChat
3886
+ });
3887
+ if (isRestartRequested()) {
3888
+ setTimeout(() => exit(), 1500);
3889
+ }
3890
+ } catch (error) {
3891
+ const message = error instanceof Error ? error.message : "Unknown error";
3892
+ const stack = error instanceof Error ? error.stack : void 0;
3893
+ process.stderr.write(`[astra] Error: ${message}
3894
+ `);
3895
+ const debugInfo = debug && stack ? `
3896
+
3897
+ \`\`\`
3898
+ ${stack}
3899
+ \`\`\`` : "";
3900
+ setChatMessages((prev) => [
3901
+ ...prev,
3902
+ { role: "assistant", content: `Error: ${message}${debugInfo}` }
3903
+ ]);
3904
+ setCoreMessages(newCoreMessages);
3905
+ setStreamingText(void 0);
3906
+ } finally {
3907
+ setIsLoading(false);
3908
+ setToolName(void 0);
3909
+ }
3910
+ },
3911
+ [coreMessages, chatMessages, skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext, profile, agentName, sessionId]
3912
+ );
3913
+ const handleSubmit = useCallback2(
3914
+ (userText) => {
3915
+ void sendMessage(userText);
3916
+ },
3917
+ [sendMessage]
3918
+ );
3919
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", width: "100%", height: "100%", children: [
3920
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ jsx7(ChatView, { messages: chatMessages, streamingText }) }),
3921
+ isLoading && toolName && /* @__PURE__ */ jsx7(Spinner, { label: `Calling ${toolName}...` }),
3922
+ isLoading && !toolName && /* @__PURE__ */ jsx7(Spinner, { label: streamingText ? "Thinking..." : void 0 }),
3923
+ /* @__PURE__ */ jsx7(Box7, { flexShrink: 0, width: "100%", children: /* @__PURE__ */ jsx7(Input, { isActive: !isLoading, onSubmit: handleSubmit }) }),
3924
+ /* @__PURE__ */ jsx7(Box7, { flexShrink: 0, width: "100%", children: /* @__PURE__ */ jsx7(StatusBar_default, { agentName, journeyStage: profile.journeyStage ?? "full" }) }),
3925
+ /* @__PURE__ */ jsxs8(Box7, { flexShrink: 0, width: "100%", paddingX: 2, justifyContent: "space-between", children: [
3926
+ /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: "/help \xB7 /portfolio \xB7 /market \xB7 /exit" }),
3927
+ /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: "Ctrl+C quit" })
3928
+ ] })
3929
+ ] });
3930
+ }
3931
+
3932
+ // src/bin/astra.ts
3933
+ function detectJourneyStage(params) {
3934
+ const { isNewAgent, apiStatus, hasWallet } = params;
3935
+ if (isNewAgent) return "fresh";
3936
+ if (!apiStatus) return "verified";
3937
+ if (apiStatus.status === "pending_verification") return "pending";
3938
+ if (apiStatus.simBalance === 1e4 && !hasWallet) return "verified";
3939
+ if (!hasWallet) return "trading";
3940
+ return "wallet_ready";
3941
+ }
3942
+ async function main() {
3943
+ ensureBaseStructure();
3944
+ if (isRestartRequested()) {
3945
+ clearRestartFlag();
3946
+ }
3947
+ const args = process2.argv.slice(2);
3948
+ const shouldContinue = args.includes("--continue") || args.includes("-c");
3949
+ const debug = args.includes("--debug") || args.includes("-d");
3950
+ if (debug) {
3951
+ process2.env.ASTRA_DEBUG = "1";
3952
+ }
3953
+ const isReturning = isConfigured();
3954
+ let onboardingResult = null;
3955
+ if (!isReturning) {
3956
+ onboardingResult = await runOnboarding();
3957
+ if (!onboardingResult) {
3958
+ console.error("Onboarding failed. Please try again.");
3959
+ process2.exit(1);
3960
+ }
3961
+ }
3962
+ const config = loadConfig();
3963
+ if (!config) {
3964
+ console.error(
3965
+ "No config found. Delete ~/.config/astranova/config.json and re-run to start fresh."
3966
+ );
3967
+ process2.exit(1);
3968
+ }
3969
+ const agentName = getActiveAgent();
3970
+ if (!agentName) {
3971
+ console.error(
3972
+ "No active agent found. Delete ~/.config/astranova/ and re-run to start fresh."
3973
+ );
3974
+ process2.exit(1);
3975
+ }
3976
+ const credentials = loadCredentials(agentName);
3977
+ if (!credentials) {
3978
+ console.error(
3979
+ `No credentials found for agent "${agentName}". Delete ~/.config/astranova/ and re-run to start fresh.`
3980
+ );
3981
+ process2.exit(1);
3982
+ }
3983
+ let apiStatus = null;
3984
+ if (isReturning) {
3985
+ apiStatus = await showWelcomeBack(agentName);
3986
+ }
3987
+ const [skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext] = await Promise.all([
3988
+ getSkillContext(),
3989
+ fetchRemoteContext("TRADING.md").then((c) => c ?? ""),
3990
+ fetchRemoteContext("WALLET.md").then((c) => c ?? ""),
3991
+ fetchRemoteContext("REWARDS.md").then((c) => c ?? ""),
3992
+ fetchRemoteContext("ONBOARDING.md").then((c) => c ?? ""),
3993
+ fetchRemoteContext("API.md").then((c) => c ?? "")
3994
+ ]);
3995
+ if (!loadState()) {
3996
+ saveState({
3997
+ activeAgent: agentName,
3998
+ agents: {
3999
+ [agentName]: {
4000
+ status: apiStatus?.status ?? "unknown",
4001
+ journeyStage: "fresh",
4002
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4003
+ verificationCode: onboardingResult?.verificationCode ?? apiStatus?.verificationCode
4004
+ }
4005
+ }
4006
+ });
4007
+ }
4008
+ const isNewAgent = !isReturning && onboardingResult !== null;
4009
+ const hasWallet = loadWallet(agentName) !== null;
4010
+ const boardPosted = hasBoardPost(agentName);
4011
+ const stage = detectJourneyStage({ isNewAgent, apiStatus, hasWallet });
4012
+ updateAgentState(agentName, {
4013
+ status: apiStatus?.status ?? (isNewAgent ? "pending_verification" : "active"),
4014
+ journeyStage: stage,
4015
+ verificationCode: onboardingResult?.verificationCode ?? apiStatus?.verificationCode
4016
+ });
4017
+ const profile = {
4018
+ agentName,
4019
+ status: apiStatus?.status ?? (isNewAgent ? "pending_verification" : "active"),
4020
+ simBalance: apiStatus?.simBalance,
4021
+ walletAddress: apiStatus?.walletAddress,
4022
+ verificationCode: onboardingResult?.verificationCode ?? apiStatus?.verificationCode,
4023
+ isNewAgent,
4024
+ boardPosted,
4025
+ journeyStage: stage
4026
+ };
4027
+ pruneOldSessions(agentName);
4028
+ const memoryContent = loadMemory(agentName);
4029
+ let sessionId = newSessionId();
4030
+ let initialCoreMessages;
4031
+ let initialChatMessages;
4032
+ if (shouldContinue) {
4033
+ const session = loadLatestSession(agentName);
4034
+ if (session) {
4035
+ const updatedAt = new Date(session.updatedAt);
4036
+ const minutesAgo = Math.round((Date.now() - updatedAt.getTime()) / 6e4);
4037
+ console.log(` Resuming session from ${minutesAgo} minute(s) ago...
4038
+ `);
4039
+ sessionId = session.sessionId;
4040
+ initialCoreMessages = session.coreMessages;
4041
+ initialChatMessages = session.chatMessages;
4042
+ } else {
4043
+ console.log(" No previous session found. Starting fresh.\n");
4044
+ }
4045
+ }
4046
+ const { waitUntilExit } = render(
4047
+ React5.createElement(App, {
4048
+ agentName,
4049
+ skillContext,
4050
+ tradingContext,
4051
+ walletContext,
4052
+ rewardsContext,
4053
+ onboardingContext,
4054
+ apiContext,
4055
+ profile,
4056
+ sessionId,
4057
+ memoryContent,
4058
+ initialCoreMessages,
4059
+ initialChatMessages,
4060
+ debug
4061
+ })
4062
+ );
4063
+ await waitUntilExit();
4064
+ if (isRestartRequested()) {
4065
+ clearRestartFlag();
4066
+ const newAgent = getActiveAgent();
4067
+ console.log(`
4068
+ Restarting as ${newAgent}...
4069
+ `);
4070
+ try {
4071
+ execFileSync(process2.execPath, process2.argv.slice(1), {
4072
+ stdio: "inherit",
4073
+ env: process2.env
4074
+ });
4075
+ } catch {
4076
+ }
4077
+ process2.exit(0);
4078
+ }
4079
+ }
4080
+ main().catch((error) => {
4081
+ const message = error instanceof Error ? error.message : String(error);
4082
+ console.error(`Fatal: ${message}`);
4083
+ process2.exit(1);
4084
+ });
4085
+ //# sourceMappingURL=astra.js.map