@astranova-live/cli 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +19 -2
  2. package/dist/astra.js +1511 -1085
  3. package/package.json +1 -1
package/dist/astra.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin/astra.ts
4
- import process2 from "process";
4
+ import process4 from "process";
5
5
  import React5 from "react";
6
6
  import { render } from "ink";
7
7
  import { execFileSync } from "child_process";
@@ -62,6 +62,15 @@ function pendingClaimPath(agentName) {
62
62
  function epochBudgetPath(agentName) {
63
63
  return path.join(agentDir(agentName), "epoch_budget.json");
64
64
  }
65
+ function strategyPath(agentName) {
66
+ return path.join(agentDir(agentName), "strategy.md");
67
+ }
68
+ function autopilotLogPath(agentName) {
69
+ return path.join(agentDir(agentName), "autopilot.log");
70
+ }
71
+ function daemonPidPath(agentName) {
72
+ return path.join(agentDir(agentName), "daemon.pid");
73
+ }
65
74
  function cachePath(fileName) {
66
75
  return path.join(_root(), ".cache", fileName);
67
76
  }
@@ -103,10 +112,11 @@ var ConfigSchema = z.object({
103
112
  preferences: z.object({
104
113
  theme: z.enum(["dark", "light"]).default("dark")
105
114
  }).default({}),
115
+ // Kept as optional for backward compat — autopilot config is now per-agent in state.json.
106
116
  autopilot: z.object({
107
117
  mode: z.enum(["off", "semi", "full"]).default("off"),
108
118
  intervalMs: z.number().min(6e4).max(36e5).default(3e5)
109
- }).default({ mode: "off", intervalMs: 3e5 })
119
+ }).optional()
110
120
  });
111
121
  var CredentialsSchema = z.object({
112
122
  agent_name: z.string(),
@@ -130,11 +140,16 @@ var RegisterResponseSchema = z.object({
130
140
  api_key: z.string(),
131
141
  verification_code: z.string()
132
142
  });
143
+ var AgentAutopilotSchema = z.object({
144
+ mode: z.enum(["off", "semi", "full"]).default("off"),
145
+ intervalMs: z.number().min(6e4).max(36e5).default(3e5)
146
+ });
133
147
  var AgentStateSchema = z.object({
134
148
  status: z.string().default("unknown"),
135
149
  journeyStage: z.enum(["fresh", "pending", "verified", "trading", "wallet_ready", "full"]).default("fresh"),
136
150
  createdAt: z.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
137
- verificationCode: z.string().optional()
151
+ verificationCode: z.string().optional(),
152
+ autopilot: AgentAutopilotSchema.optional()
138
153
  });
139
154
  var StateSchema = z.object({
140
155
  activeAgent: z.string(),
@@ -172,13 +187,19 @@ function saveConfig(config) {
172
187
  writeFileSecure(configPath(), JSON.stringify(config, null, 2));
173
188
  }
174
189
  function loadAutopilotConfig() {
190
+ const agentName = getActiveAgent();
191
+ if (agentName) {
192
+ const state = loadState();
193
+ const agentAutopilot = state?.agents[agentName]?.autopilot;
194
+ if (agentAutopilot) return agentAutopilot;
195
+ }
175
196
  const config = loadConfig();
176
197
  return config?.autopilot ?? { mode: "off", intervalMs: 3e5 };
177
198
  }
178
199
  function saveAutopilotConfig(autopilot) {
179
- const config = loadConfig();
180
- if (!config) return;
181
- saveConfig({ ...config, autopilot });
200
+ const agentName = getActiveAgent();
201
+ if (!agentName) return;
202
+ updateAgentState(agentName, { autopilot });
182
203
  }
183
204
  function loadState() {
184
205
  return readJsonFile(statePath(), StateSchema);
@@ -291,6 +312,59 @@ function clearPendingClaim(agentName) {
291
312
  fs2.unlinkSync(filePath);
292
313
  }
293
314
  }
315
+ function appendAutopilotLog(agentName, entry) {
316
+ try {
317
+ ensureDir(agentDir(agentName));
318
+ fs2.appendFileSync(autopilotLogPath(agentName), JSON.stringify(entry) + "\n", {
319
+ encoding: "utf-8"
320
+ });
321
+ } catch {
322
+ }
323
+ }
324
+ function loadAutopilotLogSince(agentName, since) {
325
+ const filePath = autopilotLogPath(agentName);
326
+ if (!fs2.existsSync(filePath)) return [];
327
+ try {
328
+ const raw = fs2.readFileSync(filePath, "utf-8");
329
+ const entries = [];
330
+ for (const line of raw.split("\n")) {
331
+ if (!line.trim()) continue;
332
+ try {
333
+ const entry = JSON.parse(line);
334
+ if (!since || new Date(entry.ts) > since) {
335
+ entries.push(entry);
336
+ }
337
+ } catch {
338
+ }
339
+ }
340
+ return entries;
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+ function loadDaemonPid(agentName) {
346
+ const filePath = daemonPidPath(agentName);
347
+ if (!fs2.existsSync(filePath)) return null;
348
+ try {
349
+ const pid = parseInt(fs2.readFileSync(filePath, "utf-8").trim(), 10);
350
+ return isNaN(pid) ? null : pid;
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+ function saveDaemonPid(agentName, pid) {
356
+ ensureDir(agentDir(agentName));
357
+ writeFileSecure(daemonPidPath(agentName), String(pid));
358
+ }
359
+ function clearDaemonPid(agentName) {
360
+ const filePath = daemonPidPath(agentName);
361
+ if (fs2.existsSync(filePath)) {
362
+ try {
363
+ fs2.unlinkSync(filePath);
364
+ } catch {
365
+ }
366
+ }
367
+ }
294
368
  function listAgents() {
295
369
  const agentsDir = path2.join(getRoot(), "agents");
296
370
  if (!fs2.existsSync(agentsDir)) {
@@ -1390,772 +1464,300 @@ function loadMemory(agentName) {
1390
1464
  }
1391
1465
  }
1392
1466
 
1393
- // src/ui/App.tsx
1394
- import { useState as useState4, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect3 } from "react";
1395
- import { Box as Box7, Text as Text8, useApp, useInput } from "ink";
1396
-
1397
- // src/ui/StatusBar.tsx
1398
- import React, { useState, useEffect, useRef, useCallback } from "react";
1399
- import { Box, Text } from "ink";
1400
-
1401
- // src/autopilot/scheduler.ts
1402
- var EPOCH_BUDGET = 10;
1403
- var BUDGET_BUFFER = 2;
1404
- var SEMI_TRIGGER_MSG = "AUTOPILOT CHECK: Analyze market and propose a trade if signal is clear. Ask me to confirm before executing.";
1405
- var FULL_TRIGGER_MSG = "AUTOPILOT CHECK: Analyze market and execute a trade if signal is clear. If uncertain, skip. Keep response to 2-3 lines max.";
1406
- function buildAutopilotTrigger(mode) {
1407
- switch (mode) {
1408
- case "semi":
1409
- return SEMI_TRIGGER_MSG;
1410
- case "full":
1411
- return FULL_TRIGGER_MSG;
1412
- case "off":
1413
- return null;
1467
+ // src/tools/strategy.ts
1468
+ import fs6 from "fs";
1469
+ import { tool as tool2 } from "ai";
1470
+ import { z as z3 } from "zod";
1471
+ var MAX_STRATEGY_CHARS = 4e3;
1472
+ var writeStrategyTool = tool2({
1473
+ description: "Save the agent's trading strategy to disk. Call this after completing the guided strategy creation conversation. Replaces the entire strategy file. Max 4000 characters.",
1474
+ parameters: z3.object({
1475
+ content: z3.string().describe(
1476
+ "The complete trading strategy in markdown format. Replaces the entire strategy file. Max 4000 characters. Use clear sections: approach, buy conditions, sell conditions, position sizing, risk limits."
1477
+ )
1478
+ }),
1479
+ execute: async ({ content }) => {
1480
+ const agentName = getActiveAgent();
1481
+ if (!agentName) return { error: "No active agent." };
1482
+ if (content.length > MAX_STRATEGY_CHARS) {
1483
+ return {
1484
+ error: `Strategy content too long (${content.length} chars). Maximum is ${MAX_STRATEGY_CHARS} characters. Condense it.`
1485
+ };
1486
+ }
1487
+ try {
1488
+ ensureDir(agentDir(agentName));
1489
+ fs6.writeFileSync(strategyPath(agentName), content, { encoding: "utf-8", mode: 384 });
1490
+ return { ok: true, chars: content.length };
1491
+ } catch (err) {
1492
+ const msg = err instanceof Error ? err.message : "Unknown error";
1493
+ return { error: `Failed to save strategy: ${msg}` };
1494
+ }
1414
1495
  }
1415
- }
1416
- function formatInterval(ms) {
1417
- const minutes = Math.round(ms / 6e4);
1418
- return `${minutes}m`;
1419
- }
1420
- function parseInterval(input) {
1421
- const match = input.match(/^(\d+)m$/);
1422
- if (!match) return null;
1423
- const minutes = parseInt(match[1], 10);
1424
- if (minutes < 1 || minutes > 60) return null;
1425
- return minutes * 6e4;
1426
- }
1427
-
1428
- // src/ui/StatusBar.tsx
1429
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1430
- var POLL_INTERVAL_MS = 6e4;
1431
- var StatusBar = React.memo(function StatusBar2({
1432
- agentName,
1433
- journeyStage,
1434
- autopilotMode = "off",
1435
- autopilotIntervalMs = 3e5,
1436
- onEpochChange
1437
- }) {
1438
- const [data, setData] = useState({ market: null, portfolio: null });
1439
- const mounted = useRef(true);
1440
- const canFetchData = journeyStage !== "fresh" && journeyStage !== "pending";
1441
- const poll = useCallback(async () => {
1442
- const [marketRes, portfolioRes] = await Promise.all([
1443
- fetchMarket(agentName),
1444
- fetchPortfolio(agentName)
1445
- ]);
1446
- if (!mounted.current) return;
1447
- setData((prev) => ({
1448
- market: marketRes ?? prev.market,
1449
- portfolio: portfolioRes ?? prev.portfolio
1450
- }));
1451
- if (marketRes && onEpochChange) {
1452
- onEpochChange(marketRes.epochId);
1496
+ });
1497
+ var readStrategyTool = tool2({
1498
+ description: "Read the agent's current trading strategy from disk. Use when the user asks about their strategy, or to show the strategy before editing it.",
1499
+ parameters: z3.object({}),
1500
+ execute: async () => {
1501
+ const agentName = getActiveAgent();
1502
+ if (!agentName) return { error: "No active agent." };
1503
+ const filePath = strategyPath(agentName);
1504
+ if (!fs6.existsSync(filePath)) {
1505
+ return {
1506
+ error: "No trading strategy found for this agent. Guide the user through strategy creation \u2014 suggest `/strategy setup`."
1507
+ };
1453
1508
  }
1454
- }, [agentName, onEpochChange]);
1455
- useEffect(() => {
1456
- mounted.current = true;
1457
- if (!canFetchData) return;
1458
- void poll();
1459
- const interval = setInterval(() => void poll(), POLL_INTERVAL_MS);
1460
- return () => {
1461
- mounted.current = false;
1462
- clearInterval(interval);
1463
- };
1464
- }, [canFetchData, poll]);
1465
- const { market, portfolio } = data;
1466
- const apActive = autopilotMode !== "off";
1467
- return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: "100%", children: /* @__PURE__ */ jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [
1468
- /* @__PURE__ */ jsxs(Box, { children: [
1469
- /* @__PURE__ */ jsx(Text, { bold: true, color: "#00ff00", children: "AstraNova" }),
1470
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1471
- /* @__PURE__ */ jsx(Text, { color: "#ff8800", children: agentName }),
1472
- canFetchData && market && /* @__PURE__ */ jsxs(Fragment, { children: [
1473
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1474
- /* @__PURE__ */ jsx(Text, { color: "#ffff00", children: "$NOVA " }),
1475
- /* @__PURE__ */ jsx(Text, { color: "white", children: formatPrice(market.price) }),
1476
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1477
- /* @__PURE__ */ jsx(Text, { color: moodColor(market.mood), children: market.mood })
1478
- ] }),
1479
- canFetchData && portfolio && /* @__PURE__ */ jsxs(Fragment, { children: [
1480
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1481
- /* @__PURE__ */ jsxs(Text, { color: "#00ffff", children: [
1482
- formatNum(portfolio.cash),
1483
- " $SIM"
1484
- ] }),
1485
- portfolio.tokens > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1486
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1487
- /* @__PURE__ */ jsxs(Text, { color: "#ff00ff", children: [
1488
- formatNum(portfolio.tokens),
1489
- " $NOVA"
1490
- ] })
1491
- ] }),
1492
- portfolio.pnl !== 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
1493
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1494
- /* @__PURE__ */ jsxs(Text, { color: portfolio.pnl >= 0 ? "#00ff00" : "#ff4444", children: [
1495
- "P&L ",
1496
- portfolio.pnl >= 0 ? "+" : "",
1497
- formatNum(portfolio.pnl),
1498
- " (",
1499
- portfolio.pnlPct >= 0 ? "+" : "",
1500
- portfolio.pnlPct.toFixed(1),
1501
- "%)"
1502
- ] })
1503
- ] }),
1504
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1505
- /* @__PURE__ */ jsxs(Text, { color: "#e2f902", children: [
1506
- "Net ",
1507
- formatNum(portfolio.portfolioValue)
1508
- ] })
1509
- ] }),
1510
- !canFetchData && /* @__PURE__ */ jsxs(Fragment, { children: [
1511
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1512
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "pending verification" })
1513
- ] }),
1514
- canFetchData && !market && !portfolio && /* @__PURE__ */ jsxs(Fragment, { children: [
1515
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
1516
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "loading..." })
1517
- ] })
1518
- ] }),
1519
- apActive && /* @__PURE__ */ jsxs(Text, { color: "#00ff00", children: [
1520
- "AP: \u25CF ",
1521
- autopilotMode.toUpperCase(),
1522
- " ",
1523
- formatInterval(autopilotIntervalMs)
1524
- ] })
1525
- ] }) });
1509
+ try {
1510
+ const content = fs6.readFileSync(filePath, "utf-8");
1511
+ return { ok: true, content };
1512
+ } catch (err) {
1513
+ const msg = err instanceof Error ? err.message : "Unknown error";
1514
+ return { error: `Failed to read strategy: ${msg}` };
1515
+ }
1516
+ }
1526
1517
  });
1527
- var StatusBar_default = StatusBar;
1528
- function formatPrice(price) {
1529
- if (price >= 1) return price.toFixed(2);
1530
- if (price >= 0.01) return price.toFixed(4);
1531
- return price.toFixed(6);
1532
- }
1533
- function formatNum(n) {
1534
- if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
1535
- if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
1536
- return n.toLocaleString(void 0, { maximumFractionDigits: 0 });
1537
- }
1538
- function moodColor(mood) {
1539
- switch (mood) {
1540
- case "euphoria":
1541
- case "bullish":
1542
- return "#00ff00";
1543
- case "fear":
1544
- case "bearish":
1545
- return "#ff4444";
1546
- case "crab":
1547
- return "#ffff00";
1548
- default:
1549
- return "white";
1518
+ function loadStrategy(agentName) {
1519
+ const filePath = strategyPath(agentName);
1520
+ if (!fs6.existsSync(filePath)) return "";
1521
+ try {
1522
+ return fs6.readFileSync(filePath, "utf-8");
1523
+ } catch {
1524
+ return "";
1550
1525
  }
1551
1526
  }
1552
- async function fetchMarket(agentName) {
1553
- const result = await apiCall("GET", "/api/v1/market/state", void 0, agentName);
1554
- if (!result.ok) return null;
1555
- const d = result.data;
1556
- const m = d.market ?? d;
1557
- return {
1558
- price: m.price ?? 0,
1559
- mood: m.mood ?? "",
1560
- epochId: m.epoch?.global ?? 0
1561
- };
1562
- }
1563
- async function fetchPortfolio(agentName) {
1564
- const result = await apiCall(
1565
- "GET",
1566
- "/api/v1/portfolio",
1567
- void 0,
1568
- agentName
1569
- );
1570
- if (!result.ok) return null;
1571
- const d = result.data;
1572
- const p = d.portfolio ?? d;
1573
- const cash = p.cash ?? 0;
1574
- const tokens = p.tokens ?? 0;
1575
- const currentPrice = p.currentPrice ?? 0;
1576
- return {
1577
- cash,
1578
- tokens,
1579
- portfolioValue: p.portfolioValue ?? cash + tokens * currentPrice,
1580
- pnl: p.pnl ?? 0,
1581
- pnlPct: p.pnlPct ?? 0
1582
- };
1583
- }
1584
1527
 
1585
- // src/ui/ChatView.tsx
1586
- import { Box as Box5, Text as Text5 } from "ink";
1528
+ // src/daemon/autopilot-worker.ts
1529
+ import process3 from "process";
1587
1530
 
1588
- // src/ui/MarkdownText.tsx
1589
- import { Text as Text4, Box as Box4 } from "ink";
1531
+ // src/agent/loop.ts
1532
+ import { streamText, generateText } from "ai";
1533
+ import { zodToJsonSchema } from "zod-to-json-schema";
1590
1534
 
1591
- // src/ui/PortfolioCard.tsx
1592
- import { Box as Box2, Text as Text2 } from "ink";
1593
- import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1594
- function PortfolioCard({ data }) {
1595
- const price = data.currentPrice ?? 0;
1596
- const cash = data.cash ?? 0;
1597
- const tokens = data.tokens ?? 0;
1598
- const value = data.portfolioValue ?? cash + tokens * price;
1599
- const pnl = data.pnl ?? 0;
1600
- const pnlPct = data.pnlPct ?? 0;
1601
- const earned = data.totalEarned ? Number(data.totalEarned) / 1e9 : 0;
1602
- const claimable = data.claimable ? Number(data.claimable) / 1e9 : 0;
1603
- const pnlColor = pnl >= 0 ? "#00ff00" : "#ff4444";
1604
- const pnlSign = pnl >= 0 ? "+" : "";
1605
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 1, marginY: 1, children: [
1606
- /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "#00ffff", children: "Portfolio Overview" }) }),
1607
- /* @__PURE__ */ jsx2(Row, { label: "$SIM Balance", value: formatNum2(cash), color: "#00ffff" }),
1608
- /* @__PURE__ */ jsx2(Row, { label: "$NOVA Holdings", value: tokens > 0 ? formatNum2(tokens) : "\u2014", color: "#ff00ff" }),
1609
- /* @__PURE__ */ jsx2(Row, { label: "$NOVA Price", value: price > 0 ? formatPrice2(price) : "\u2014", color: "#ffff00" }),
1610
- /* @__PURE__ */ jsx2(Row, { label: "Portfolio Value", value: formatNum2(value), color: "white" }),
1611
- /* @__PURE__ */ jsx2(Row, { label: "P&L", value: `${pnlSign}${formatNum2(pnl)} (${pnlSign}${pnlPct.toFixed(1)}%)`, color: pnlColor }),
1612
- (data.hasWallet !== void 0 || data.walletLocal !== void 0) && /* @__PURE__ */ jsx2(
1613
- Row,
1614
- {
1615
- label: "Wallet",
1616
- value: data.hasWallet ? "registered" : data.walletLocal ? "needs registration" : "not set",
1617
- color: data.hasWallet ? "#00ff00" : data.walletLocal ? "#ffff00" : "gray"
1618
- }
1619
- ),
1620
- (earned > 0 || claimable > 0) && /* @__PURE__ */ jsxs2(Fragment2, { children: [
1621
- /* @__PURE__ */ jsx2(Row, { label: "$ASTRA Earned", value: earned > 0 ? formatAstra(earned) : "\u2014", color: "#ffff00" }),
1622
- /* @__PURE__ */ jsx2(Row, { label: "Claimable", value: claimable > 0 ? formatAstra(claimable) : "\u2014", color: claimable > 0 ? "#00ff00" : "gray" })
1623
- ] })
1624
- ] });
1625
- }
1626
- function Row({ label, value, color }) {
1627
- return /* @__PURE__ */ jsxs2(Box2, { children: [
1628
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
1629
- label,
1630
- ": "
1631
- ] }),
1632
- /* @__PURE__ */ jsx2(Text2, { color, bold: true, children: value })
1633
- ] });
1634
- }
1635
- function formatPrice2(price) {
1636
- if (price >= 1) return `$${price.toFixed(2)}`;
1637
- if (price >= 0.01) return `$${price.toFixed(4)}`;
1638
- return `$${price.toFixed(6)}`;
1535
+ // src/agent/provider.ts
1536
+ import { createAnthropic } from "@ai-sdk/anthropic";
1537
+ import { createOpenAI } from "@ai-sdk/openai";
1538
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
1539
+ function isCodexOAuth() {
1540
+ const override = process.env.ASTRA_PROVIDER;
1541
+ if (override) return override === "openai-oauth";
1542
+ const config = loadConfig();
1543
+ return config?.provider === "openai-oauth";
1639
1544
  }
1640
- function formatNum2(n) {
1641
- if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
1642
- if (Math.abs(n) >= 1e4) return `${(n / 1e3).toFixed(1)}K`;
1643
- return n.toLocaleString(void 0, { maximumFractionDigits: 2 });
1545
+ function isOpenAIResponses() {
1546
+ const override = process.env.ASTRA_PROVIDER;
1547
+ if (override) return override === "openai";
1548
+ const config = loadConfig();
1549
+ return config?.provider === "openai";
1644
1550
  }
1645
- function formatAstra(n) {
1646
- if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M ASTRA`;
1647
- if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K ASTRA`;
1648
- return `${n.toFixed(2)} ASTRA`;
1551
+ function getOpenAIApiKey() {
1552
+ const envKey = process.env.ASTRA_API_KEY;
1553
+ if (envKey) return envKey;
1554
+ const config = loadConfig();
1555
+ if (config?.auth?.type === "api-key" && config.auth.apiKey) {
1556
+ return config.auth.apiKey;
1557
+ }
1558
+ throw new Error(
1559
+ "OpenAI API key not found. Set ASTRA_API_KEY or re-run onboarding."
1560
+ );
1649
1561
  }
1650
-
1651
- // src/ui/RewardsCard.tsx
1652
- import { Box as Box3, Text as Text3 } from "ink";
1653
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1654
- function RewardsCard({ data }) {
1655
- const total = data.totalAstra ? Number(data.totalAstra) / 1e9 : 0;
1656
- const epoch = data.epochAstra ? Number(data.epochAstra) / 1e9 : 0;
1657
- const bonus = data.bonusAstra ? Number(data.bonusAstra) / 1e9 : 0;
1658
- const statusColor = data.claimStatus === "claimable" ? "#00ff00" : data.claimStatus === "sent" ? "#00ffff" : "#ffff00";
1659
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingLeft: 1, marginY: 1, children: [
1660
- /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
1661
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "#ffff00", children: "$ASTRA Rewards" }),
1662
- data.seasonId && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1663
- " ",
1664
- data.seasonId
1665
- ] })
1666
- ] }),
1667
- /* @__PURE__ */ jsx3(Row2, { label: "Total Earned", value: formatAstra2(total), color: "#ffff00" }),
1668
- /* @__PURE__ */ jsx3(Row2, { label: "Epoch Rewards", value: formatAstra2(epoch), color: "#00ffff" }),
1669
- /* @__PURE__ */ jsx3(Row2, { label: "Season Bonus", value: formatAstra2(bonus), color: "#ff00ff" }),
1670
- /* @__PURE__ */ jsx3(Row2, { label: "Status", value: data.claimStatus ?? "\u2014", color: statusColor }),
1671
- /* @__PURE__ */ jsx3(Row2, { label: "Epochs Rewarded", value: data.epochsRewarded?.toString() ?? "\u2014", color: "white" }),
1672
- data.bestEpochPnl !== void 0 && data.bestEpochPnl > 0 && /* @__PURE__ */ jsx3(Row2, { label: "Best Epoch P&L", value: `+${data.bestEpochPnl.toFixed(2)}`, color: "#00ff00" }),
1673
- data.txSignature && /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
1674
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tx: " }),
1675
- /* @__PURE__ */ jsx3(Text3, { color: "#00ffff", children: data.txSignature })
1676
- ] })
1677
- ] });
1562
+ async function getCodexAccessToken() {
1563
+ const config = loadConfig();
1564
+ if (!config || config.auth.type !== "oauth" || !config.auth.oauth) {
1565
+ throw new Error("Codex OAuth not configured. Re-run onboarding.");
1566
+ }
1567
+ return ensureFreshToken(config);
1678
1568
  }
1679
- function Row2({ label, value, color }) {
1680
- return /* @__PURE__ */ jsxs3(Box3, { children: [
1681
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1682
- label,
1683
- ": "
1684
- ] }),
1685
- /* @__PURE__ */ jsx3(Text3, { color, bold: true, children: value })
1686
- ] });
1569
+ async function getModel() {
1570
+ const config = loadConfig();
1571
+ if (!config) {
1572
+ throw new Error(
1573
+ "No config found. Run the onboarding wizard first (delete ~/.config/astranova/config.json to re-run)."
1574
+ );
1575
+ }
1576
+ const providerOverride = process.env.ASTRA_PROVIDER;
1577
+ const apiKeyOverride = process.env.ASTRA_API_KEY;
1578
+ const modelOverride = process.env.ASTRA_MODEL;
1579
+ if (providerOverride) {
1580
+ if (!apiKeyOverride) {
1581
+ throw new Error(
1582
+ `ASTRA_PROVIDER=${providerOverride} is set but ASTRA_API_KEY is missing.
1583
+ Export your API key: export ASTRA_API_KEY=sk-...`
1584
+ );
1585
+ }
1586
+ return createModelFromConfig({
1587
+ ...config,
1588
+ provider: providerOverride,
1589
+ model: modelOverride ?? config.model,
1590
+ auth: { type: "api-key", apiKey: apiKeyOverride }
1591
+ });
1592
+ }
1593
+ return createModelFromConfig(config);
1687
1594
  }
1688
- function formatAstra2(n) {
1689
- if (n === 0) return "\u2014";
1690
- if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M ASTRA`;
1691
- if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K ASTRA`;
1692
- return `${n.toFixed(4)} ASTRA`;
1595
+ async function ensureFreshToken(config) {
1596
+ const oauth = config.auth.oauth;
1597
+ if (!oauth) throw new Error("OAuth config missing");
1598
+ if (!isTokenExpired(oauth.expiresAt)) return oauth.accessToken;
1599
+ try {
1600
+ const tokens = await refreshTokens({
1601
+ refreshToken: oauth.refreshToken,
1602
+ clientId: oauth.clientId
1603
+ });
1604
+ const updatedConfig = {
1605
+ ...config,
1606
+ auth: {
1607
+ ...config.auth,
1608
+ oauth: {
1609
+ ...oauth,
1610
+ accessToken: tokens.accessToken,
1611
+ refreshToken: tokens.refreshToken,
1612
+ expiresAt: tokens.expiresAt
1613
+ }
1614
+ }
1615
+ };
1616
+ saveConfig(updatedConfig);
1617
+ return tokens.accessToken;
1618
+ } catch (error) {
1619
+ const message = error instanceof Error ? error.message : "Unknown error";
1620
+ throw new Error(
1621
+ `OAuth token refresh failed: ${message}
1622
+ Please re-run onboarding to log in again (delete ~/.config/astranova/config.json).`
1623
+ );
1624
+ }
1693
1625
  }
1694
-
1695
- // src/ui/MarkdownText.tsx
1696
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1697
- function MarkdownText({ children }) {
1698
- const lines = children.split("\n");
1699
- const elements = [];
1700
- let i = 0;
1701
- while (i < lines.length) {
1702
- const line = lines[i];
1703
- if (line.trimStart().startsWith(":::portfolio") || line.trimStart().startsWith(":::rewards")) {
1704
- const cardType = line.trimStart().startsWith(":::portfolio") ? "portfolio" : "rewards";
1705
- const jsonLines = [];
1706
- i++;
1707
- while (i < lines.length && !lines[i].trimStart().startsWith(":::")) {
1708
- jsonLines.push(lines[i]);
1709
- i++;
1626
+ function createModelFromConfig(config) {
1627
+ const { provider, model, auth } = config;
1628
+ switch (provider) {
1629
+ case "claude": {
1630
+ if (auth.type !== "api-key" || !auth.apiKey) {
1631
+ throw new Error("Claude requires an API key. Re-run onboarding to set one up.");
1710
1632
  }
1711
- i++;
1712
- try {
1713
- const raw = jsonLines.join("\n");
1714
- if (cardType === "portfolio") {
1715
- const data = JSON.parse(raw);
1716
- elements.push(/* @__PURE__ */ jsx4(PortfolioCard, { data }, elements.length));
1717
- } else {
1718
- const data = JSON.parse(raw);
1719
- elements.push(/* @__PURE__ */ jsx4(RewardsCard, { data }, elements.length));
1720
- }
1721
- } catch {
1722
- elements.push(
1723
- /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: jsonLines.join("\n") }) }, elements.length)
1724
- );
1633
+ const anthropic = createAnthropic({ apiKey: auth.apiKey });
1634
+ return anthropic(model);
1635
+ }
1636
+ case "openai": {
1637
+ if (auth.type !== "api-key" || !auth.apiKey) {
1638
+ throw new Error("OpenAI requires an API key. Re-run onboarding to set one up.");
1725
1639
  }
1726
- continue;
1640
+ const openai = createOpenAI({ apiKey: auth.apiKey });
1641
+ return openai(model);
1727
1642
  }
1728
- if (line.trimStart().startsWith("```")) {
1729
- const codeLines = [];
1730
- i++;
1731
- while (i < lines.length && !lines[i].trimStart().startsWith("```")) {
1732
- codeLines.push(lines[i]);
1733
- i++;
1643
+ case "google": {
1644
+ if (auth.type !== "api-key" || !auth.apiKey) {
1645
+ throw new Error("Gemini requires an API key. Re-run onboarding to set one up.");
1734
1646
  }
1735
- i++;
1736
- elements.push(
1737
- /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginY: 0, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", children: codeLines.join("\n") }) }, elements.length)
1738
- );
1739
- continue;
1647
+ const google = createGoogleGenerativeAI({ apiKey: auth.apiKey });
1648
+ return google(model);
1740
1649
  }
1741
- if (/^---+$/.test(line.trim())) {
1742
- elements.push(
1743
- /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2500".repeat(40) }) }, elements.length)
1650
+ case "openai-oauth":
1651
+ throw new Error("Codex OAuth uses custom provider. This is a bug \u2014 please report.");
1652
+ case "ollama":
1653
+ throw new Error(
1654
+ "Ollama support is coming soon. Please use Claude or ChatGPT/Codex.\nTo switch, delete ~/.config/astranova/config.json and re-run astra."
1744
1655
  );
1745
- i++;
1746
- continue;
1747
- }
1748
- const headerMatch = /^(#{1,4})\s+(.+)$/.exec(line);
1749
- if (headerMatch) {
1750
- elements.push(
1751
- /* @__PURE__ */ jsx4(Box4, { marginTop: elements.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: headerMatch[2] }) }, elements.length)
1752
- );
1753
- i++;
1754
- continue;
1755
- }
1756
- const bulletMatch = /^(\s*)[-*]\s+(.+)$/.exec(line);
1757
- if (bulletMatch) {
1758
- const indent = Math.floor((bulletMatch[1]?.length ?? 0) / 2);
1759
- elements.push(
1760
- /* @__PURE__ */ jsxs4(Box4, { marginLeft: indent * 2, children: [
1761
- /* @__PURE__ */ jsx4(Text4, { children: " \u25CF " }),
1762
- renderInline(bulletMatch[2])
1763
- ] }, elements.length)
1764
- );
1765
- i++;
1766
- continue;
1767
- }
1768
- const numMatch = /^(\s*)(\d+)[.)]\s+(.+)$/.exec(line);
1769
- if (numMatch) {
1770
- const indent = Math.floor((numMatch[1]?.length ?? 0) / 2);
1771
- elements.push(
1772
- /* @__PURE__ */ jsxs4(Box4, { marginLeft: indent * 2, children: [
1773
- /* @__PURE__ */ jsxs4(Text4, { children: [
1774
- " ",
1775
- numMatch[2],
1776
- ". "
1777
- ] }),
1778
- renderInline(numMatch[3])
1779
- ] }, elements.length)
1780
- );
1781
- i++;
1782
- continue;
1783
- }
1784
- if (line.trim() === "") {
1785
- elements.push(/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { children: " " }) }, elements.length));
1786
- i++;
1787
- continue;
1788
- }
1789
- elements.push(
1790
- /* @__PURE__ */ jsx4(Box4, { flexWrap: "wrap", children: renderInline(line) }, elements.length)
1791
- );
1792
- i++;
1656
+ default:
1657
+ throw new Error(`Unknown provider: ${provider}`);
1793
1658
  }
1794
- return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: elements });
1795
1659
  }
1796
- function renderInline(text3) {
1797
- const parts = [];
1798
- let remaining = text3;
1799
- let key = 0;
1800
- while (remaining.length > 0) {
1801
- const candidates = [];
1802
- const boldMatch = /\*\*(.+?)\*\*|__(.+?)__/.exec(remaining);
1803
- if (boldMatch) {
1804
- candidates.push({
1805
- index: boldMatch.index,
1806
- length: boldMatch[0].length,
1807
- node: /* @__PURE__ */ jsx4(Text4, { bold: true, children: boldMatch[1] ?? boldMatch[2] }, key++)
1808
- });
1809
- }
1810
- const codeMatch = /`([^`]+?)`/.exec(remaining);
1811
- if (codeMatch) {
1812
- candidates.push({
1813
- index: codeMatch.index,
1814
- length: codeMatch[0].length,
1815
- node: /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: codeMatch[1] }, key++)
1816
- });
1817
- }
1818
- const italicMatch = /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/.exec(remaining);
1819
- if (italicMatch) {
1820
- candidates.push({
1821
- index: italicMatch.index,
1822
- length: italicMatch[0].length,
1823
- node: /* @__PURE__ */ jsx4(Text4, { italic: true, children: italicMatch[1] ?? italicMatch[2] }, key++)
1824
- });
1825
- }
1826
- candidates.sort((a, b) => a.index - b.index);
1827
- const pick = candidates[0];
1828
- if (!pick) {
1829
- parts.push(/* @__PURE__ */ jsx4(Text4, { children: remaining }, key++));
1830
- break;
1831
- }
1832
- if (pick.index > 0) {
1833
- parts.push(/* @__PURE__ */ jsx4(Text4, { children: remaining.slice(0, pick.index) }, key++));
1834
- }
1835
- parts.push(pick.node);
1836
- remaining = remaining.slice(pick.index + pick.length);
1660
+
1661
+ // src/agent/system-prompt.ts
1662
+ function buildSystemPrompt(skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext, profile, memoryContent) {
1663
+ const stage = profile.journeyStage ?? "full";
1664
+ const isPending = stage === "fresh" || stage === "pending";
1665
+ const parts = [
1666
+ ROLE_DESCRIPTION,
1667
+ "",
1668
+ "---",
1669
+ "",
1670
+ TOOL_OVERRIDES,
1671
+ "",
1672
+ "---",
1673
+ ""
1674
+ ];
1675
+ if (skillContext) {
1676
+ parts.push("## AstraNova API Instructions", "");
1677
+ parts.push(skillContext);
1678
+ parts.push("", "---", "");
1837
1679
  }
1838
- return /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: parts });
1839
- }
1680
+ if (onboardingContext && isPending) {
1681
+ parts.push("## Onboarding Guide", "");
1682
+ parts.push(onboardingContext);
1683
+ parts.push("", "---", "");
1684
+ }
1685
+ if (tradingContext && !isPending) {
1686
+ parts.push("## Trading Guide", "");
1687
+ parts.push(tradingContext);
1688
+ parts.push("", "---", "");
1689
+ }
1690
+ if (rewardsContext && (stage === "wallet_ready" || stage === "full")) {
1691
+ parts.push("## Rewards Guide", "");
1692
+ parts.push(rewardsContext);
1693
+ parts.push("", "---", "");
1694
+ }
1695
+ if (apiContext) {
1696
+ parts.push("## API Reference", "");
1697
+ parts.push(apiContext);
1698
+ parts.push("", "---", "");
1699
+ }
1700
+ parts.push(DOCS_AWARENESS);
1701
+ parts.push("", "---", "");
1702
+ parts.push("## Current Agent State", "");
1703
+ parts.push(`Agent: ${profile.agentName}`);
1704
+ parts.push(`Status: ${profile.status ?? "unknown"}`);
1705
+ parts.push(`Journey Stage: ${stage}`);
1706
+ if (profile.simBalance !== void 0) {
1707
+ parts.push(`$SIM Balance: ${profile.simBalance.toLocaleString()}`);
1708
+ }
1709
+ if (profile.novaHoldings !== void 0) {
1710
+ parts.push(`$NOVA Holdings: ${profile.novaHoldings.toLocaleString()}`);
1711
+ }
1712
+ parts.push(`Wallet: ${profile.walletAddress ?? "not set"}`);
1713
+ parts.push(`Wallet Local: ${profile.walletLocal ? "yes" : "no"}`);
1714
+ if (profile.verificationCode) {
1715
+ parts.push(`Verification Code: ${profile.verificationCode}`);
1716
+ }
1717
+ if (profile.season !== void 0) {
1718
+ parts.push(`Season: ${profile.season}`);
1719
+ }
1720
+ const apMode = profile.autopilotMode ?? "off";
1721
+ if (apMode !== "off") {
1722
+ parts.push("", "---", "");
1723
+ parts.push(`## AUTOPILOT: ${apMode.toUpperCase()}`);
1724
+ parts.push("");
1725
+ parts.push(
1726
+ `Autopilot is active. When you receive an AUTOPILOT CHECK trigger, your strategy will be included in that message \u2014 execute per those instructions. Do NOT ask the user for confirmation or trade size during autopilot checks.`
1727
+ );
1728
+ }
1729
+ if (memoryContent && memoryContent.trim()) {
1730
+ parts.push("", "---", "");
1731
+ parts.push("## Agent Memory (persistent across sessions)", "");
1732
+ parts.push(memoryContent.trim());
1733
+ parts.push("");
1734
+ 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.
1840
1735
 
1841
- // src/ui/ChatView.tsx
1842
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1843
- function ChatView({
1844
- messages,
1845
- streamingText
1846
- }) {
1847
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", paddingX: 1, children: [
1848
- messages.map((msg, i) => {
1849
- if (msg.role === "log") {
1850
- return /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: msg.content }) }, i);
1851
- }
1852
- if (msg.role === "autopilot") {
1853
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
1854
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "#ff00ff", children: " Autopilot" }),
1855
- /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "wrap", children: msg.content }) })
1856
- ] }, i);
1857
- }
1858
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
1859
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: msg.role === "user" ? "#00ff00" : "#00ffff", children: msg.role === "user" ? " You" : " Agent" }),
1860
- /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: msg.role === "assistant" ? /* @__PURE__ */ jsx5(MarkdownText, { children: msg.content }) : /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: msg.content }) })
1861
- ] }, i);
1862
- }),
1863
- streamingText !== void 0 && streamingText.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
1864
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "#00ffff", children: " Agent" }),
1865
- /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: /* @__PURE__ */ jsx5(MarkdownText, { children: streamingText }) })
1866
- ] })
1867
- ] });
1736
+ Specific triggers to save memory:
1737
+ - User mentions their trading style ("I like to hold long", "I always sell at 5%") \u2192 save it
1738
+ - User mentions preferred position size or risk tolerance \u2192 save it
1739
+ - User mentions their goals ("I want to be top 10 on the leaderboard") \u2192 save it
1740
+ - User reacts positively or negatively to a suggestion \u2192 note what they prefer`);
1741
+ } else {
1742
+ parts.push("", "---", "");
1743
+ parts.push("## Agent Memory");
1744
+ parts.push("");
1745
+ 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.");
1746
+ }
1747
+ parts.push("", "---", "");
1748
+ parts.push(buildJourneyGuidance(stage, profile));
1749
+ return parts.join("\n");
1868
1750
  }
1751
+ function buildJourneyGuidance(stage, profile) {
1752
+ switch (stage) {
1753
+ case "fresh":
1754
+ case "pending":
1755
+ return buildVerificationGuidance(profile);
1756
+ case "verified": {
1757
+ 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.
1869
1758
 
1870
- // src/ui/Input.tsx
1871
- import { useState as useState2 } from "react";
1872
- import { Box as Box6, Text as Text6 } from "ink";
1873
- import TextInput from "ink-text-input";
1874
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1875
- function Input({
1876
- isActive,
1877
- onSubmit
1878
- }) {
1879
- const [value, setValue] = useState2("");
1880
- const handleSubmit = (submitted) => {
1881
- const trimmed = submitted.trim();
1882
- if (!trimmed) return;
1883
- onSubmit(trimmed);
1884
- setValue("");
1885
- };
1886
- return /* @__PURE__ */ jsxs6(Box6, { width: "100%", paddingX: 2, paddingY: 1, children: [
1887
- /* @__PURE__ */ jsx6(Text6, { color: isActive ? "yellow" : "gray", bold: true, children: "\u276F\u276F " }),
1888
- /* @__PURE__ */ jsx6(
1889
- TextInput,
1890
- {
1891
- value,
1892
- onChange: setValue,
1893
- onSubmit: handleSubmit,
1894
- focus: isActive,
1895
- showCursor: isActive,
1896
- placeholder: ""
1897
- }
1898
- )
1899
- ] });
1900
- }
1901
-
1902
- // src/ui/Spinner.tsx
1903
- import { useState as useState3, useEffect as useEffect2 } from "react";
1904
- import { Text as Text7 } from "ink";
1905
- import { jsxs as jsxs7 } from "react/jsx-runtime";
1906
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1907
- var INTERVAL_MS = 80;
1908
- function Spinner({ label }) {
1909
- const [frame, setFrame] = useState3(0);
1910
- useEffect2(() => {
1911
- const timer = setInterval(() => {
1912
- setFrame((prev) => (prev + 1) % FRAMES.length);
1913
- }, INTERVAL_MS);
1914
- return () => clearInterval(timer);
1915
- }, []);
1916
- return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1917
- FRAMES[frame],
1918
- " ",
1919
- label ?? "Thinking..."
1920
- ] });
1921
- }
1922
-
1923
- // src/agent/loop.ts
1924
- import { streamText, generateText } from "ai";
1925
- import { zodToJsonSchema } from "zod-to-json-schema";
1926
-
1927
- // src/agent/provider.ts
1928
- import { createAnthropic } from "@ai-sdk/anthropic";
1929
- import { createOpenAI } from "@ai-sdk/openai";
1930
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
1931
- function isCodexOAuth() {
1932
- const override = process.env.ASTRA_PROVIDER;
1933
- if (override) return override === "openai-oauth";
1934
- const config = loadConfig();
1935
- return config?.provider === "openai-oauth";
1936
- }
1937
- function isOpenAIResponses() {
1938
- const override = process.env.ASTRA_PROVIDER;
1939
- if (override) return override === "openai";
1940
- const config = loadConfig();
1941
- return config?.provider === "openai";
1942
- }
1943
- function getOpenAIApiKey() {
1944
- const envKey = process.env.ASTRA_API_KEY;
1945
- if (envKey) return envKey;
1946
- const config = loadConfig();
1947
- if (config?.auth?.type === "api-key" && config.auth.apiKey) {
1948
- return config.auth.apiKey;
1949
- }
1950
- throw new Error(
1951
- "OpenAI API key not found. Set ASTRA_API_KEY or re-run onboarding."
1952
- );
1953
- }
1954
- async function getCodexAccessToken() {
1955
- const config = loadConfig();
1956
- if (!config || config.auth.type !== "oauth" || !config.auth.oauth) {
1957
- throw new Error("Codex OAuth not configured. Re-run onboarding.");
1958
- }
1959
- return ensureFreshToken(config);
1960
- }
1961
- async function getModel() {
1962
- const config = loadConfig();
1963
- if (!config) {
1964
- throw new Error(
1965
- "No config found. Run the onboarding wizard first (delete ~/.config/astranova/config.json to re-run)."
1966
- );
1967
- }
1968
- const providerOverride = process.env.ASTRA_PROVIDER;
1969
- const apiKeyOverride = process.env.ASTRA_API_KEY;
1970
- const modelOverride = process.env.ASTRA_MODEL;
1971
- if (providerOverride) {
1972
- if (!apiKeyOverride) {
1973
- throw new Error(
1974
- `ASTRA_PROVIDER=${providerOverride} is set but ASTRA_API_KEY is missing.
1975
- Export your API key: export ASTRA_API_KEY=sk-...`
1976
- );
1977
- }
1978
- return createModelFromConfig({
1979
- ...config,
1980
- provider: providerOverride,
1981
- model: modelOverride ?? config.model,
1982
- auth: { type: "api-key", apiKey: apiKeyOverride }
1983
- });
1984
- }
1985
- return createModelFromConfig(config);
1986
- }
1987
- async function ensureFreshToken(config) {
1988
- const oauth = config.auth.oauth;
1989
- if (!oauth) throw new Error("OAuth config missing");
1990
- if (!isTokenExpired(oauth.expiresAt)) return oauth.accessToken;
1991
- try {
1992
- const tokens = await refreshTokens({
1993
- refreshToken: oauth.refreshToken,
1994
- clientId: oauth.clientId
1995
- });
1996
- const updatedConfig = {
1997
- ...config,
1998
- auth: {
1999
- ...config.auth,
2000
- oauth: {
2001
- ...oauth,
2002
- accessToken: tokens.accessToken,
2003
- refreshToken: tokens.refreshToken,
2004
- expiresAt: tokens.expiresAt
2005
- }
2006
- }
2007
- };
2008
- saveConfig(updatedConfig);
2009
- return tokens.accessToken;
2010
- } catch (error) {
2011
- const message = error instanceof Error ? error.message : "Unknown error";
2012
- throw new Error(
2013
- `OAuth token refresh failed: ${message}
2014
- Please re-run onboarding to log in again (delete ~/.config/astranova/config.json).`
2015
- );
2016
- }
2017
- }
2018
- function createModelFromConfig(config) {
2019
- const { provider, model, auth } = config;
2020
- switch (provider) {
2021
- case "claude": {
2022
- if (auth.type !== "api-key" || !auth.apiKey) {
2023
- throw new Error("Claude requires an API key. Re-run onboarding to set one up.");
2024
- }
2025
- const anthropic = createAnthropic({ apiKey: auth.apiKey });
2026
- return anthropic(model);
2027
- }
2028
- case "openai": {
2029
- if (auth.type !== "api-key" || !auth.apiKey) {
2030
- throw new Error("OpenAI requires an API key. Re-run onboarding to set one up.");
2031
- }
2032
- const openai = createOpenAI({ apiKey: auth.apiKey });
2033
- return openai(model);
2034
- }
2035
- case "google": {
2036
- if (auth.type !== "api-key" || !auth.apiKey) {
2037
- throw new Error("Gemini requires an API key. Re-run onboarding to set one up.");
2038
- }
2039
- const google = createGoogleGenerativeAI({ apiKey: auth.apiKey });
2040
- return google(model);
2041
- }
2042
- case "openai-oauth":
2043
- throw new Error("Codex OAuth uses custom provider. This is a bug \u2014 please report.");
2044
- case "ollama":
2045
- throw new Error(
2046
- "Ollama support is coming soon. Please use Claude or ChatGPT/Codex.\nTo switch, delete ~/.config/astranova/config.json and re-run astra."
2047
- );
2048
- default:
2049
- throw new Error(`Unknown provider: ${provider}`);
2050
- }
2051
- }
2052
-
2053
- // src/agent/system-prompt.ts
2054
- function buildSystemPrompt(skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext, profile, memoryContent) {
2055
- const stage = profile.journeyStage ?? "full";
2056
- const isPending = stage === "fresh" || stage === "pending";
2057
- const parts = [
2058
- ROLE_DESCRIPTION,
2059
- "",
2060
- "---",
2061
- "",
2062
- TOOL_OVERRIDES,
2063
- "",
2064
- "---",
2065
- ""
2066
- ];
2067
- if (skillContext) {
2068
- parts.push("## AstraNova API Instructions", "");
2069
- parts.push(skillContext);
2070
- parts.push("", "---", "");
2071
- }
2072
- if (onboardingContext && isPending) {
2073
- parts.push("## Onboarding Guide", "");
2074
- parts.push(onboardingContext);
2075
- parts.push("", "---", "");
2076
- }
2077
- if (tradingContext && !isPending) {
2078
- parts.push("## Trading Guide", "");
2079
- parts.push(tradingContext);
2080
- parts.push("", "---", "");
2081
- }
2082
- if (rewardsContext && (stage === "wallet_ready" || stage === "full")) {
2083
- parts.push("## Rewards Guide", "");
2084
- parts.push(rewardsContext);
2085
- parts.push("", "---", "");
2086
- }
2087
- if (apiContext) {
2088
- parts.push("## API Reference", "");
2089
- parts.push(apiContext);
2090
- parts.push("", "---", "");
2091
- }
2092
- parts.push(DOCS_AWARENESS);
2093
- parts.push("", "---", "");
2094
- parts.push("## Current Agent State", "");
2095
- parts.push(`Agent: ${profile.agentName}`);
2096
- parts.push(`Status: ${profile.status ?? "unknown"}`);
2097
- parts.push(`Journey Stage: ${stage}`);
2098
- if (profile.simBalance !== void 0) {
2099
- parts.push(`$SIM Balance: ${profile.simBalance.toLocaleString()}`);
2100
- }
2101
- if (profile.novaHoldings !== void 0) {
2102
- parts.push(`$NOVA Holdings: ${profile.novaHoldings.toLocaleString()}`);
2103
- }
2104
- parts.push(`Wallet: ${profile.walletAddress ?? "not set"}`);
2105
- parts.push(`Wallet Local: ${profile.walletLocal ? "yes" : "no"}`);
2106
- if (profile.verificationCode) {
2107
- parts.push(`Verification Code: ${profile.verificationCode}`);
2108
- }
2109
- if (profile.season !== void 0) {
2110
- parts.push(`Season: ${profile.season}`);
2111
- }
2112
- const apMode = profile.autopilotMode ?? "off";
2113
- if (apMode !== "off") {
2114
- parts.push("", "---", "");
2115
- parts.push(`## AUTOPILOT: ${apMode.toUpperCase()}`);
2116
- parts.push("");
2117
- parts.push(`On AUTOPILOT CHECK triggers:`);
2118
- parts.push(`- GET /api/v1/market/state + GET /api/v1/portfolio`);
2119
- parts.push(`- Apply strategy from memory.md (default: momentum + balance)`);
2120
- if (apMode === "semi") {
2121
- parts.push(`- SEMI: propose trade, wait for explicit user approval`);
2122
- } else {
2123
- parts.push(`- FULL: execute if signal clear; skip if uncertain`);
2124
- }
2125
- parts.push(`- Max 2-3 lines. No action = "Market checked \u2014 holding."`);
2126
- }
2127
- if (memoryContent && memoryContent.trim()) {
2128
- parts.push("", "---", "");
2129
- parts.push("## Agent Memory (persistent across sessions)", "");
2130
- parts.push(memoryContent.trim());
2131
- parts.push("");
2132
- 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.
2133
-
2134
- Specific triggers to save memory:
2135
- - User mentions their trading style ("I like to hold long", "I always sell at 5%") \u2192 save it
2136
- - User mentions preferred position size or risk tolerance \u2192 save it
2137
- - User mentions their goals ("I want to be top 10 on the leaderboard") \u2192 save it
2138
- - User reacts positively or negatively to a suggestion \u2192 note what they prefer`);
2139
- } else {
2140
- parts.push("", "---", "");
2141
- parts.push("## Agent Memory");
2142
- parts.push("");
2143
- 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.");
2144
- }
2145
- parts.push("", "---", "");
2146
- parts.push(buildJourneyGuidance(stage, profile));
2147
- return parts.join("\n");
2148
- }
2149
- function buildJourneyGuidance(stage, profile) {
2150
- switch (stage) {
2151
- case "fresh":
2152
- case "pending":
2153
- return buildVerificationGuidance(profile);
2154
- case "verified": {
2155
- 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.
2156
-
2157
- `;
2158
- return `## Your Opening Message
1759
+ `;
1760
+ return `## Your Opening Message
2159
1761
 
2160
1762
  Hey \u2014 welcome! This is exciting, "${profile.agentName}" is verified and ready to go.
2161
1763
 
@@ -2172,7 +1774,9 @@ If they trade, pull their portfolio afterwards using the card format and add a b
2172
1774
  - Don't mention wallets yet \u2014 that comes later after they've traded a bit.
2173
1775
  - If they ask what they can do, give them 2-3 quick options: "Check the market, make a trade, or look at the board."`;
2174
1776
  }
2175
- case "trading":
1777
+ case "trading": {
1778
+ const strategyNudge = !profile.hasStrategy ? `- If it comes up naturally, mention autopilot once: "By the way \u2014 I can trade on autopilot for you if you set up a strategy. Want to try?" Only mention once per session. If they say no or ignore it, drop it.
1779
+ ` : "";
2176
1780
  return `## Your Opening Message
2177
1781
 
2178
1782
  Welcome back! Greet the user casually: "Hey! Want to see what the market's been up to?"
@@ -2186,13 +1790,14 @@ Wait for their response. Don't auto-pull data unless they say yes or ask for som
2186
1790
  - After the user makes their 3rd trade OR after you've shown them the portfolio at least once, casually mention wallet setup once: "By the way \u2014 setting up a wallet takes about a minute and means your $ASTRA rewards are claimable the moment the season ends. Want to do it now?"
2187
1791
  - If portfolio shows claimable rewards (rewards.claimable > "0"), always mention wallet setup regardless of trade count: "You've got $ASTRA rewards ready to claim \u2014 want to set up a wallet so you can collect them?"
2188
1792
  - Only mention wallet setup once per session. If they say no or ignore it, drop it.
2189
-
1793
+ ${strategyNudge}
2190
1794
  **Wallet setup:** 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.
2191
1795
 
2192
1796
  **Conversation style:**
2193
1797
  - Like a friend who's also trading. Casual, helpful, never pushy.
2194
1798
  - Suggest things, wait for their input. Don't dump multiple suggestions at once.
2195
1799
  - If they just want to trade, help them trade. If they want to chat, chat.`;
1800
+ }
2196
1801
  case "wallet_ready":
2197
1802
  return `## Your Opening Message
2198
1803
 
@@ -2536,39 +2141,39 @@ You have access to tools for interacting with the AstraNova Agent API, reading/w
2536
2141
  - 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.`;
2537
2142
 
2538
2143
  // src/tools/api.ts
2539
- import { tool as tool2 } from "ai";
2144
+ import { tool as tool3 } from "ai";
2540
2145
 
2541
2146
  // src/tools/schemas.ts
2542
- import { z as z3 } from "zod";
2543
- var apiCallSchema = z3.object({
2544
- method: z3.enum(["GET", "POST", "PUT", "PATCH"]),
2545
- path: z3.string().describe("API path, e.g. /api/v1/agents/me"),
2546
- body: z3.record(z3.unknown()).optional().describe("JSON body for POST/PUT/PATCH requests")
2147
+ import { z as z4 } from "zod";
2148
+ var apiCallSchema = z4.object({
2149
+ method: z4.enum(["GET", "POST", "PUT", "PATCH"]),
2150
+ path: z4.string().describe("API path, e.g. /api/v1/agents/me"),
2151
+ body: z4.record(z4.unknown()).optional().describe("JSON body for POST/PUT/PATCH requests")
2547
2152
  }).passthrough();
2548
- var readConfigSchema = z3.object({
2549
- key: z3.enum(["profile", "wallet", "all_agents", "settings"]),
2550
- agentName: z3.string().optional().describe("Agent name. Uses the active agent if not specified.")
2153
+ var readConfigSchema = z4.object({
2154
+ key: z4.enum(["profile", "wallet", "all_agents", "settings"]),
2155
+ agentName: z4.string().optional().describe("Agent name. Uses the active agent if not specified.")
2551
2156
  });
2552
- var writeConfigSchema = z3.object({
2553
- agentName: z3.string().describe("Agent name to write config for"),
2554
- data: z3.record(z3.unknown()).describe("Data to write"),
2555
- file: z3.enum(["credentials", "settings", "profile"]).describe("Which config file to write")
2157
+ var writeConfigSchema = z4.object({
2158
+ agentName: z4.string().describe("Agent name to write config for"),
2159
+ data: z4.record(z4.unknown()).describe("Data to write"),
2160
+ file: z4.enum(["credentials", "settings", "profile"]).describe("Which config file to write")
2556
2161
  });
2557
- var createWalletSchema = z3.object({
2558
- agentName: z3.string().describe("Agent name to associate the wallet with")
2162
+ var createWalletSchema = z4.object({
2163
+ agentName: z4.string().describe("Agent name to associate the wallet with")
2559
2164
  });
2560
- var signChallengeSchema = z3.object({
2561
- challenge: z3.string().describe("The challenge string received from the wallet registration API")
2165
+ var signChallengeSchema = z4.object({
2166
+ challenge: z4.string().describe("The challenge string received from the wallet registration API")
2562
2167
  });
2563
- var signAndSendTransactionSchema = z3.object({
2564
- transaction: z3.string().describe("Base64-encoded partially-signed transaction from the API")
2168
+ var signAndSendTransactionSchema = z4.object({
2169
+ transaction: z4.string().describe("Base64-encoded partially-signed transaction from the API")
2565
2170
  });
2566
- var registerAgentSchema = z3.object({
2567
- name: z3.string().describe("Agent name (2-32 chars, lowercase, letters/numbers/hyphens/underscores)"),
2568
- description: z3.string().describe("Short agent description \u2014 personality-driven, a few words")
2171
+ var registerAgentSchema = z4.object({
2172
+ name: z4.string().describe("Agent name (2-32 chars, lowercase, letters/numbers/hyphens/underscores)"),
2173
+ description: z4.string().describe("Short agent description \u2014 personality-driven, a few words")
2569
2174
  });
2570
- var switchAgentSchema = z3.object({
2571
- agentName: z3.string().describe("Name of the agent to switch to")
2175
+ var switchAgentSchema = z4.object({
2176
+ agentName: z4.string().describe("Name of the agent to switch to")
2572
2177
  });
2573
2178
 
2574
2179
  // src/tools/api.ts
@@ -2600,7 +2205,7 @@ function resolveBody(body, rest, method) {
2600
2205
  }
2601
2206
  return void 0;
2602
2207
  }
2603
- var apiCallTool = tool2({
2208
+ var apiCallTool = tool3({
2604
2209
  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.",
2605
2210
  parameters: apiCallSchema,
2606
2211
  execute: async (args) => {
@@ -2700,11 +2305,11 @@ var apiCallTool = tool2({
2700
2305
  });
2701
2306
 
2702
2307
  // src/tools/config.ts
2703
- import { tool as tool3 } from "ai";
2704
- import fs6 from "fs";
2308
+ import { tool as tool4 } from "ai";
2309
+ import fs7 from "fs";
2705
2310
  import path5 from "path";
2706
2311
  import crypto2 from "crypto";
2707
- var readConfigTool = tool3({
2312
+ var readConfigTool = tool4({
2708
2313
  description: "Read local AstraNova configuration or credentials. Returns public information only \u2014 private keys and API keys are never included.",
2709
2314
  parameters: readConfigSchema,
2710
2315
  execute: async ({ key, agentName }) => {
@@ -2759,7 +2364,7 @@ var readConfigTool = tool3({
2759
2364
  }
2760
2365
  }
2761
2366
  });
2762
- var writeConfigTool = tool3({
2367
+ var writeConfigTool = tool4({
2763
2368
  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.",
2764
2369
  parameters: writeConfigSchema,
2765
2370
  execute: async ({ agentName, data, file }) => {
@@ -2781,18 +2386,18 @@ var writeConfigTool = tool3({
2781
2386
  });
2782
2387
  } else {
2783
2388
  const tmpPath = path5.join(dir, `.tmp-${crypto2.randomBytes(6).toString("hex")}`);
2784
- fs6.writeFileSync(tmpPath, JSON.stringify(data, null, 2), {
2389
+ fs7.writeFileSync(tmpPath, JSON.stringify(data, null, 2), {
2785
2390
  encoding: "utf-8",
2786
2391
  mode: 384
2787
2392
  });
2788
- fs6.renameSync(tmpPath, filePath);
2393
+ fs7.renameSync(tmpPath, filePath);
2789
2394
  }
2790
2395
  return { success: true, file: `${file}.json`, agent: agentName };
2791
2396
  }
2792
2397
  });
2793
2398
 
2794
2399
  // src/tools/wallet.ts
2795
- import { tool as tool4 } from "ai";
2400
+ import { tool as tool5 } from "ai";
2796
2401
  import {
2797
2402
  Keypair,
2798
2403
  Connection,
@@ -2801,7 +2406,7 @@ import {
2801
2406
  import nacl from "tweetnacl";
2802
2407
  import bs58 from "bs58";
2803
2408
  var SOLANA_RPC = "https://api.mainnet-beta.solana.com";
2804
- var createWalletTool = tool4({
2409
+ var createWalletTool = tool5({
2805
2410
  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.",
2806
2411
  parameters: createWalletSchema,
2807
2412
  execute: async ({ agentName }) => {
@@ -2824,7 +2429,7 @@ var createWalletTool = tool4({
2824
2429
  };
2825
2430
  }
2826
2431
  });
2827
- var signChallengeTool = tool4({
2432
+ var signChallengeTool = tool5({
2828
2433
  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.",
2829
2434
  parameters: signChallengeSchema,
2830
2435
  execute: async ({ challenge }) => {
@@ -2865,7 +2470,7 @@ var signChallengeTool = tool4({
2865
2470
  }
2866
2471
  }
2867
2472
  });
2868
- var signAndSendTransactionTool = tool4({
2473
+ var signAndSendTransactionTool = tool5({
2869
2474
  description: "Co-sign and submit a partially-signed Solana transaction (base64). Used for claiming $ASTRA rewards. Returns the transaction signature.",
2870
2475
  parameters: signAndSendTransactionSchema,
2871
2476
  execute: async ({ transaction: txBase64 }) => {
@@ -2919,15 +2524,58 @@ var signAndSendTransactionTool = tool4({
2919
2524
  });
2920
2525
 
2921
2526
  // src/tools/agent-management.ts
2922
- import { tool as tool5 } from "ai";
2923
- import { z as z4 } from "zod";
2924
- var registerAgentTool = tool5({
2925
- 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.",
2926
- parameters: registerAgentSchema,
2927
- execute: async ({ name, description }) => {
2928
- if (!/^[a-z0-9_-]{2,32}$/.test(name)) {
2929
- return {
2930
- error: "Invalid agent name. Must be 2-32 chars, lowercase letters, numbers, hyphens, or underscores."
2527
+ import { tool as tool6 } from "ai";
2528
+ import { z as z5 } from "zod";
2529
+
2530
+ // src/daemon/daemon-manager.ts
2531
+ import { spawn } from "child_process";
2532
+ import process2 from "process";
2533
+ function isProcessAlive(pid) {
2534
+ try {
2535
+ process2.kill(pid, 0);
2536
+ return true;
2537
+ } catch {
2538
+ return false;
2539
+ }
2540
+ }
2541
+ function isDaemonRunning(agentName) {
2542
+ const pid = loadDaemonPid(agentName);
2543
+ if (pid === null) return false;
2544
+ if (isProcessAlive(pid)) return true;
2545
+ clearDaemonPid(agentName);
2546
+ return false;
2547
+ }
2548
+ function startDaemon(agentName) {
2549
+ if (isDaemonRunning(agentName)) return;
2550
+ const child = spawn(process2.execPath, [process2.argv[1], "--daemon"], {
2551
+ detached: true,
2552
+ stdio: "ignore",
2553
+ env: { ...process2.env }
2554
+ });
2555
+ child.unref();
2556
+ if (child.pid) {
2557
+ saveDaemonPid(agentName, child.pid);
2558
+ }
2559
+ }
2560
+ function stopDaemon(agentName) {
2561
+ const pid = loadDaemonPid(agentName);
2562
+ if (pid !== null && isProcessAlive(pid)) {
2563
+ try {
2564
+ process2.kill(pid, "SIGTERM");
2565
+ } catch {
2566
+ }
2567
+ }
2568
+ clearDaemonPid(agentName);
2569
+ }
2570
+
2571
+ // src/tools/agent-management.ts
2572
+ var registerAgentTool = tool6({
2573
+ 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.",
2574
+ parameters: registerAgentSchema,
2575
+ execute: async ({ name, description }) => {
2576
+ if (!/^[a-z0-9_-]{2,32}$/.test(name)) {
2577
+ return {
2578
+ error: "Invalid agent name. Must be 2-32 chars, lowercase letters, numbers, hyphens, or underscores."
2931
2579
  };
2932
2580
  }
2933
2581
  const result = await apiCall("POST", "/api/v1/agents/register", { name, description });
@@ -2943,6 +2591,10 @@ var registerAgentTool = tool5({
2943
2591
  if (!data.api_key) {
2944
2592
  return { error: "Registration response missing api_key. Something went wrong." };
2945
2593
  }
2594
+ const currentAgent = getActiveAgent();
2595
+ if (currentAgent) {
2596
+ stopDaemon(currentAgent);
2597
+ }
2946
2598
  saveCredentials(name, {
2947
2599
  agent_name: name,
2948
2600
  api_key: data.api_key,
@@ -2966,7 +2618,7 @@ var registerAgentTool = tool5({
2966
2618
  };
2967
2619
  }
2968
2620
  });
2969
- var switchAgentTool = tool5({
2621
+ var switchAgentTool = tool6({
2970
2622
  description: "Switch to a different registered agent. Updates the active agent. The CLI will need to restart to load the new agent's context.",
2971
2623
  parameters: switchAgentSchema,
2972
2624
  execute: async ({ agentName }) => {
@@ -2986,6 +2638,9 @@ var switchAgentTool = tool5({
2986
2638
  agentName
2987
2639
  };
2988
2640
  }
2641
+ if (currentAgent) {
2642
+ stopDaemon(currentAgent);
2643
+ }
2989
2644
  setActiveAgent(agentName);
2990
2645
  requestRestart();
2991
2646
  return {
@@ -2997,9 +2652,9 @@ var switchAgentTool = tool5({
2997
2652
  };
2998
2653
  }
2999
2654
  });
3000
- var listAgentsTool = tool5({
2655
+ var listAgentsTool = tool6({
3001
2656
  description: "List all AstraNova agents registered on this machine, showing which one is active.",
3002
- parameters: z4.object({}),
2657
+ parameters: z5.object({}),
3003
2658
  execute: async () => {
3004
2659
  const agents = listAgents();
3005
2660
  const active = getActiveAgent();
@@ -3030,7 +2685,9 @@ var astraTools = {
3030
2685
  register_agent: registerAgentTool,
3031
2686
  switch_agent: switchAgentTool,
3032
2687
  list_agents: listAgentsTool,
3033
- update_memory: updateMemoryTool
2688
+ update_memory: updateMemoryTool,
2689
+ read_strategy: readStrategyTool,
2690
+ write_strategy: writeStrategyTool
3034
2691
  };
3035
2692
 
3036
2693
  // src/agent/codex-provider.ts
@@ -3263,7 +2920,7 @@ function convertToolsForCodex(tools) {
3263
2920
  }
3264
2921
 
3265
2922
  // src/utils/audit.ts
3266
- import fs7 from "fs";
2923
+ import fs8 from "fs";
3267
2924
  var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
3268
2925
  "secretKey",
3269
2926
  "secret_key",
@@ -3297,12 +2954,12 @@ function sanitize(obj) {
3297
2954
  function writeAuditEntry(entry) {
3298
2955
  const logPath = auditLogPath();
3299
2956
  try {
3300
- if (fs7.existsSync(logPath)) {
3301
- const stat = fs7.statSync(logPath);
2957
+ if (fs8.existsSync(logPath)) {
2958
+ const stat = fs8.statSync(logPath);
3302
2959
  if (stat.size > MAX_LOG_SIZE_BYTES) {
3303
2960
  const backupPath = logPath.replace(".log", ".old.log");
3304
- if (fs7.existsSync(backupPath)) fs7.unlinkSync(backupPath);
3305
- fs7.renameSync(logPath, backupPath);
2961
+ if (fs8.existsSync(backupPath)) fs8.unlinkSync(backupPath);
2962
+ fs8.renameSync(logPath, backupPath);
3306
2963
  }
3307
2964
  }
3308
2965
  const line = JSON.stringify({
@@ -3310,7 +2967,7 @@ function writeAuditEntry(entry) {
3310
2967
  args: sanitize(entry.args),
3311
2968
  result: truncateResult(sanitize(entry.result))
3312
2969
  });
3313
- fs7.appendFileSync(logPath, line + "\n", { encoding: "utf-8" });
2970
+ fs8.appendFileSync(logPath, line + "\n", { encoding: "utf-8" });
3314
2971
  } catch {
3315
2972
  }
3316
2973
  }
@@ -3659,260 +3316,902 @@ async function runResponsesApiTurn(messages, systemPrompt, callbacks, turnConfig
3659
3316
  });
3660
3317
  }
3661
3318
  }
3662
- responseMessages.push({ role: "assistant", content: assistantContent });
3663
- if (toolResultParts.length > 0) {
3664
- responseMessages.push({ role: "tool", content: toolResultParts });
3665
- }
3666
- accessToken = await getAccessToken();
3667
- result = await callCodexWithRetry({
3668
- accessToken,
3669
- model,
3670
- instructions: systemPrompt,
3671
- input: codexInput,
3672
- tools: codexTools,
3673
- callbacks,
3674
- baseUrl,
3675
- onTokenExpired
3676
- });
3677
- finalText = result.text;
3678
- if (result.incomplete) {
3679
- debugLog3(`Step ${steps} incomplete: ${result.incompleteReason}`);
3680
- if (result.toolCalls.length > 0) {
3681
- debugLog3(`Discarding ${result.toolCalls.length} tool calls from incomplete response`);
3682
- result = { ...result, toolCalls: [] };
3319
+ responseMessages.push({ role: "assistant", content: assistantContent });
3320
+ if (toolResultParts.length > 0) {
3321
+ responseMessages.push({ role: "tool", content: toolResultParts });
3322
+ }
3323
+ accessToken = await getAccessToken();
3324
+ result = await callCodexWithRetry({
3325
+ accessToken,
3326
+ model,
3327
+ instructions: systemPrompt,
3328
+ input: codexInput,
3329
+ tools: codexTools,
3330
+ callbacks,
3331
+ baseUrl,
3332
+ onTokenExpired
3333
+ });
3334
+ finalText = result.text;
3335
+ if (result.incomplete) {
3336
+ debugLog3(`Step ${steps} incomplete: ${result.incompleteReason}`);
3337
+ if (result.toolCalls.length > 0) {
3338
+ debugLog3(`Discarding ${result.toolCalls.length} tool calls from incomplete response`);
3339
+ result = { ...result, toolCalls: [] };
3340
+ }
3341
+ finalText += "\n\n(Response was truncated \u2014 try a shorter message or start a new session.)";
3342
+ }
3343
+ debugLog3(`Step ${steps}: text=${result.text.length}chars, toolCalls=${result.toolCalls.map((tc) => tc.name).join(",") || "none"}`);
3344
+ }
3345
+ debugLog3(`Responses loop done after ${steps} steps, finalText=${finalText.length}chars`);
3346
+ let text3 = finalText;
3347
+ if (!text3 && steps > 0) {
3348
+ debugLog3("Tools ran but no summary text \u2014 nudging model for a summary");
3349
+ codexInput.push({ role: "user", content: "Please summarize what just happened." });
3350
+ accessToken = await getAccessToken();
3351
+ const summaryRetry = await callCodexWithRetry({
3352
+ accessToken,
3353
+ model,
3354
+ instructions: systemPrompt,
3355
+ input: codexInput,
3356
+ tools: codexTools,
3357
+ callbacks,
3358
+ baseUrl,
3359
+ onTokenExpired
3360
+ });
3361
+ text3 = summaryRetry.text;
3362
+ }
3363
+ if (!text3 && steps > 0) {
3364
+ text3 = "I ran the requested action but the model returned no summary. Please try asking again.";
3365
+ } else if (!text3) {
3366
+ text3 = "(No response from model)";
3367
+ }
3368
+ responseMessages.push({ role: "assistant", content: text3 });
3369
+ return { text: text3, responseMessages };
3370
+ }
3371
+ async function runSdkTurn(messages, systemPrompt, callbacks) {
3372
+ debugLog3(`SDK turn starting \u2014 getting model...`);
3373
+ const model = await getModel();
3374
+ debugLog3(`Model ready: ${model.modelId ?? "unknown"} \u2014 calling streamText...`);
3375
+ const abortController = new AbortController();
3376
+ let idleTimer;
3377
+ let timedOutBy;
3378
+ const overallTimer = setTimeout(() => {
3379
+ debugLog3("SDK turn timeout \u2014 aborting after overall timeout");
3380
+ timedOutBy = "overall";
3381
+ abortController.abort();
3382
+ }, TURN_TIMEOUT_MS);
3383
+ const resetIdleTimer = () => {
3384
+ if (idleTimer) clearTimeout(idleTimer);
3385
+ idleTimer = setTimeout(() => {
3386
+ debugLog3("SDK turn idle timeout \u2014 no data for 30s, aborting");
3387
+ timedOutBy = "idle";
3388
+ abortController.abort();
3389
+ }, IDLE_TIMEOUT_MS);
3390
+ };
3391
+ try {
3392
+ const result = streamText({
3393
+ model,
3394
+ system: systemPrompt,
3395
+ messages,
3396
+ tools: astraTools,
3397
+ maxSteps: 10,
3398
+ temperature: 0.7,
3399
+ abortSignal: abortController.signal,
3400
+ onStepFinish: ({ toolCalls, toolResults }) => {
3401
+ resetIdleTimer();
3402
+ if (toolCalls && toolCalls.length > 0) {
3403
+ for (let i = 0; i < toolCalls.length; i++) {
3404
+ const tc = toolCalls[i];
3405
+ const tr = toolResults?.[i];
3406
+ callbacks.onToolCallEnd?.(tc.toolName);
3407
+ writeAuditEntry({
3408
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3409
+ tool: tc.toolName,
3410
+ args: tc.args,
3411
+ result: tr?.result,
3412
+ ok: !tr?.result?.error,
3413
+ durationMs: 0
3414
+ // not available from SDK callback
3415
+ });
3416
+ }
3417
+ }
3418
+ }
3419
+ });
3420
+ const abortPromise = new Promise((_, reject) => {
3421
+ abortController.signal.addEventListener("abort", () => {
3422
+ const label = timedOutBy === "idle" ? `No response for ${IDLE_TIMEOUT_MS / 1e3}s` : `Response timed out after ${TURN_TIMEOUT_MS / 1e3}s`;
3423
+ reject(new Error(`${label}. Please try again.`));
3424
+ });
3425
+ });
3426
+ debugLog3("streamText created \u2014 consuming textStream...");
3427
+ resetIdleTimer();
3428
+ await Promise.race([
3429
+ (async () => {
3430
+ for await (const chunk of result.textStream) {
3431
+ resetIdleTimer();
3432
+ callbacks.onTextChunk(chunk);
3433
+ }
3434
+ })(),
3435
+ abortPromise
3436
+ ]);
3437
+ debugLog3("textStream consumed \u2014 awaiting response...");
3438
+ const response = await Promise.race([result.response, abortPromise]);
3439
+ const text3 = await Promise.race([result.text, abortPromise]);
3440
+ debugLog3(`SDK turn done \u2014 text=${text3.length}chars, messages=${response.messages.length}`);
3441
+ return {
3442
+ text: text3 || "(No response from LLM)",
3443
+ responseMessages: response.messages
3444
+ };
3445
+ } catch (error) {
3446
+ const message = error instanceof Error ? error.message : String(error);
3447
+ debugLog3(`SDK turn error: ${message}`);
3448
+ throw error;
3449
+ } finally {
3450
+ clearTimeout(overallTimer);
3451
+ if (idleTimer) clearTimeout(idleTimer);
3452
+ }
3453
+ }
3454
+ function convertToCodexInput(messages) {
3455
+ const codexInput = [];
3456
+ for (const m of messages) {
3457
+ if (m.role === "user" || m.role === "assistant") {
3458
+ if (m.role === "assistant" && Array.isArray(m.content)) {
3459
+ const textParts = m.content.filter(
3460
+ (p) => p.type === "text"
3461
+ );
3462
+ if (textParts.length > 0) {
3463
+ codexInput.push({
3464
+ role: "assistant",
3465
+ content: textParts.map((p) => p.text).join("")
3466
+ });
3467
+ }
3468
+ for (const part of m.content) {
3469
+ if (part.type === "tool-call") {
3470
+ const tc = part;
3471
+ const fcId = tc.toolCallId.startsWith("fc_") ? tc.toolCallId : `fc_${tc.toolCallId}`;
3472
+ codexInput.push({
3473
+ type: "function_call",
3474
+ id: fcId,
3475
+ call_id: tc.toolCallId,
3476
+ name: tc.toolName,
3477
+ arguments: JSON.stringify(tc.args)
3478
+ });
3479
+ }
3480
+ }
3481
+ } else {
3482
+ codexInput.push({
3483
+ role: m.role,
3484
+ content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
3485
+ });
3486
+ }
3487
+ } else if (m.role === "tool") {
3488
+ if (Array.isArray(m.content)) {
3489
+ for (const part of m.content) {
3490
+ if (part.type === "tool-result") {
3491
+ const tr = part;
3492
+ codexInput.push({
3493
+ type: "function_call_output",
3494
+ call_id: tr.toolCallId,
3495
+ output: JSON.stringify(tr.result)
3496
+ });
3497
+ }
3498
+ }
3499
+ }
3500
+ }
3501
+ }
3502
+ return codexInput;
3503
+ }
3504
+ async function summarizeForCompaction(messages, systemPrompt, provider, modelName) {
3505
+ const summaryInstruction = `${systemPrompt}
3506
+
3507
+ ---
3508
+
3509
+ ${COMPACTION_PROMPT}`;
3510
+ if (provider === "openai-oauth") {
3511
+ const accessToken = await getCodexAccessToken();
3512
+ const codexInput = convertToCodexInput(messages);
3513
+ const result2 = await callCodex({
3514
+ accessToken,
3515
+ model: modelName ?? "gpt-5.3-codex",
3516
+ instructions: summaryInstruction,
3517
+ input: codexInput,
3518
+ timeoutMs: 6e4
3519
+ });
3520
+ return result2.text || "No summary generated.";
3521
+ }
3522
+ if (provider === "openai") {
3523
+ const apiKey = getOpenAIApiKey();
3524
+ const codexInput = convertToCodexInput(messages);
3525
+ const result2 = await callCodex({
3526
+ accessToken: apiKey,
3527
+ model: modelName ?? "gpt-4o-mini",
3528
+ instructions: summaryInstruction,
3529
+ input: codexInput,
3530
+ timeoutMs: 6e4,
3531
+ baseUrl: OPENAI_RESPONSES_URL
3532
+ });
3533
+ return result2.text || "No summary generated.";
3534
+ }
3535
+ const model = await getModel();
3536
+ const result = await generateText({
3537
+ model,
3538
+ system: summaryInstruction,
3539
+ messages,
3540
+ temperature: 0.3
3541
+ });
3542
+ return result.text || "No summary generated.";
3543
+ }
3544
+ var EMPTY_SENTINELS = [
3545
+ "(No response from model)",
3546
+ "(No response from LLM)",
3547
+ "I ran the requested action but the model returned no summary."
3548
+ ];
3549
+ function isEmptyResponse(result) {
3550
+ if (!result.text) return true;
3551
+ const t = result.text.trim();
3552
+ return EMPTY_SENTINELS.some((s) => t === s || t.startsWith(s));
3553
+ }
3554
+ function extractJsonSchema(toolDef) {
3555
+ const t = toolDef;
3556
+ if (!t.parameters) return {};
3557
+ const params = t.parameters;
3558
+ if (params._def) {
3559
+ try {
3560
+ return zodToJsonSchema(params);
3561
+ } catch (e) {
3562
+ process.stderr.write(
3563
+ `Warning: Failed to convert tool schema to JSON Schema: ${e instanceof Error ? e.message : "unknown error"}
3564
+ `
3565
+ );
3566
+ return {};
3567
+ }
3568
+ }
3569
+ if (typeof t.parameters === "object") {
3570
+ return t.parameters;
3571
+ }
3572
+ return {};
3573
+ }
3574
+
3575
+ // src/autopilot/scheduler.ts
3576
+ var EPOCH_BUDGET = 10;
3577
+ var BUDGET_BUFFER = 2;
3578
+ var SEMI_TRIGGER_BASE = "AUTOPILOT CHECK (SEMI): Analyze market and execute a trade based on your strategy. Do NOT ask for confirmation \u2014 execute autonomously and report the result in 2-3 lines. If conditions are not met, say 'Market checked \u2014 holding.' with a brief reason.";
3579
+ var FULL_TRIGGER_BASE = "AUTOPILOT CHECK (FULL): Analyze market and execute a trade based on your strategy. Do NOT ask for confirmation. Do NOT ask about trade size \u2014 use position sizing from your strategy. Execute immediately if conditions are met, skip if uncertain. Keep response to 2-3 lines max.";
3580
+ var STRATEGY_RUN_BASE = "STRATEGY RUN: Check the market against your strategy and execute a trade if conditions are met. Do NOT ask for confirmation. Report what you did (or why you held) in 2-3 lines.";
3581
+ function buildAutopilotTrigger(mode, strategyContent) {
3582
+ const base = mode === "semi" ? SEMI_TRIGGER_BASE : mode === "full" ? FULL_TRIGGER_BASE : null;
3583
+ if (!base) return null;
3584
+ if (!strategyContent?.trim()) return base;
3585
+ return `${base}
3586
+
3587
+ ## Your Strategy
3588
+ ${strategyContent.trim()}`;
3589
+ }
3590
+ function buildStrategyRunTrigger(strategyContent) {
3591
+ if (!strategyContent?.trim()) return STRATEGY_RUN_BASE;
3592
+ return `${STRATEGY_RUN_BASE}
3593
+
3594
+ ## Your Strategy
3595
+ ${strategyContent.trim()}`;
3596
+ }
3597
+ function formatInterval(ms) {
3598
+ const minutes = Math.round(ms / 6e4);
3599
+ return `${minutes}m`;
3600
+ }
3601
+ function parseInterval(input) {
3602
+ const match = input.match(/^(\d+)m$/);
3603
+ if (!match) return null;
3604
+ const minutes = parseInt(match[1], 10);
3605
+ if (minutes < 1 || minutes > 60) return null;
3606
+ return minutes * 6e4;
3607
+ }
3608
+
3609
+ // src/daemon/autopilot-worker.ts
3610
+ var DEBUG4 = !!process3.env.ASTRA_DEBUG;
3611
+ function debugLog4(msg) {
3612
+ if (DEBUG4) process3.stderr.write(`[astra-daemon] ${msg}
3613
+ `);
3614
+ }
3615
+ async function runDaemon() {
3616
+ const agentName = getActiveAgent();
3617
+ if (!agentName) {
3618
+ process3.stderr.write("[astra-daemon] No active agent \u2014 exiting.\n");
3619
+ process3.exit(1);
3620
+ }
3621
+ const autopilotConfig = loadAutopilotConfig();
3622
+ if (autopilotConfig.mode !== "full") {
3623
+ process3.stderr.write("[astra-daemon] Autopilot mode is not full \u2014 exiting.\n");
3624
+ process3.exit(0);
3625
+ }
3626
+ debugLog4(`Starting for agent "${agentName}", interval ${autopilotConfig.intervalMs}ms`);
3627
+ const [skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext] = await Promise.all([
3628
+ getSkillContext(),
3629
+ fetchRemoteContext("TRADING.md").then((c) => c ?? ""),
3630
+ fetchRemoteContext("WALLET.md").then((c) => c ?? ""),
3631
+ fetchRemoteContext("REWARDS.md").then((c) => c ?? ""),
3632
+ fetchRemoteContext("ONBOARDING.md").then((c) => c ?? ""),
3633
+ fetchRemoteContext("API.md").then((c) => c ?? "")
3634
+ ]);
3635
+ let intervalHandle = null;
3636
+ let isRunning = false;
3637
+ process3.on("SIGTERM", () => {
3638
+ debugLog4("Received SIGTERM \u2014 shutting down.");
3639
+ if (intervalHandle) clearInterval(intervalHandle);
3640
+ process3.exit(0);
3641
+ });
3642
+ async function runTick() {
3643
+ if (isRunning) {
3644
+ debugLog4("Tick skipped \u2014 previous turn still running.");
3645
+ return;
3646
+ }
3647
+ const strategy = loadStrategy(agentName);
3648
+ if (!strategy) {
3649
+ debugLog4("No strategy found \u2014 skipping tick.");
3650
+ return;
3651
+ }
3652
+ const budget = loadEpochBudget(agentName);
3653
+ const tradeCount = budget?.callCount ?? 0;
3654
+ if (tradeCount >= EPOCH_BUDGET - BUDGET_BUFFER) {
3655
+ debugLog4(`Epoch budget reached (${tradeCount}/${EPOCH_BUDGET}) \u2014 skipping tick.`);
3656
+ appendAutopilotLog(agentName, {
3657
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3658
+ action: `Budget reached (${tradeCount}/${EPOCH_BUDGET}) \u2014 skipping until next epoch`
3659
+ });
3660
+ return;
3661
+ }
3662
+ const trigger = buildAutopilotTrigger("full", strategy);
3663
+ if (!trigger) return;
3664
+ const state = loadState();
3665
+ const profile = {
3666
+ agentName,
3667
+ autopilotMode: "full",
3668
+ hasStrategy: true,
3669
+ journeyStage: state?.agents[agentName]?.journeyStage ?? "full"
3670
+ };
3671
+ const memoryContent = loadMemory(agentName);
3672
+ const coreMessages = [{ role: "user", content: trigger }];
3673
+ isRunning = true;
3674
+ try {
3675
+ debugLog4("Running autopilot turn...");
3676
+ const result = await runAgentTurn(
3677
+ coreMessages,
3678
+ skillContext,
3679
+ tradingContext,
3680
+ walletContext,
3681
+ rewardsContext,
3682
+ onboardingContext,
3683
+ apiContext,
3684
+ profile,
3685
+ { onTextChunk: () => {
3686
+ }, onToolCallStart: () => {
3687
+ }, onToolCallEnd: () => {
3688
+ } },
3689
+ memoryContent
3690
+ );
3691
+ const responseText = result.text.trim();
3692
+ const summary = responseText.split("\n")[0].slice(0, 120) || "checked \u2192 no response";
3693
+ debugLog4(`Turn complete: ${summary}`);
3694
+ appendAutopilotLog(agentName, {
3695
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3696
+ action: summary
3697
+ });
3698
+ } catch (err) {
3699
+ const msg = err instanceof Error ? err.message : String(err);
3700
+ debugLog4(`Turn error: ${msg}`);
3701
+ appendAutopilotLog(agentName, {
3702
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3703
+ action: `error: ${msg.slice(0, 100)}`
3704
+ });
3705
+ } finally {
3706
+ isRunning = false;
3707
+ }
3708
+ }
3709
+ await runTick();
3710
+ intervalHandle = setInterval(() => {
3711
+ void runTick();
3712
+ }, autopilotConfig.intervalMs);
3713
+ debugLog4("Daemon running. Waiting for ticks...");
3714
+ }
3715
+
3716
+ // src/ui/App.tsx
3717
+ import { useState as useState4, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect3 } from "react";
3718
+ import { Box as Box7, Text as Text8, useApp, useInput } from "ink";
3719
+
3720
+ // src/ui/StatusBar.tsx
3721
+ import React, { useState, useEffect, useRef, useCallback } from "react";
3722
+ import { Box, Text } from "ink";
3723
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3724
+ var POLL_INTERVAL_MS = 6e4;
3725
+ var StatusBar = React.memo(function StatusBar2({
3726
+ agentName,
3727
+ journeyStage,
3728
+ autopilotMode = "off",
3729
+ autopilotIntervalMs = 3e5,
3730
+ onEpochChange
3731
+ }) {
3732
+ const [data, setData] = useState({ market: null, portfolio: null });
3733
+ const mounted = useRef(true);
3734
+ const canFetchData = journeyStage !== "fresh" && journeyStage !== "pending";
3735
+ const poll = useCallback(async () => {
3736
+ const [marketRes, portfolioRes] = await Promise.all([
3737
+ fetchMarket(agentName),
3738
+ fetchPortfolio(agentName)
3739
+ ]);
3740
+ if (!mounted.current) return;
3741
+ setData((prev) => ({
3742
+ market: marketRes ?? prev.market,
3743
+ portfolio: portfolioRes ?? prev.portfolio
3744
+ }));
3745
+ if (marketRes && onEpochChange) {
3746
+ onEpochChange(marketRes.epochId);
3747
+ }
3748
+ }, [agentName, onEpochChange]);
3749
+ useEffect(() => {
3750
+ mounted.current = true;
3751
+ if (!canFetchData) return;
3752
+ void poll();
3753
+ const interval = setInterval(() => void poll(), POLL_INTERVAL_MS);
3754
+ return () => {
3755
+ mounted.current = false;
3756
+ clearInterval(interval);
3757
+ };
3758
+ }, [canFetchData, poll]);
3759
+ const { market, portfolio } = data;
3760
+ const apActive = autopilotMode !== "off";
3761
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: "100%", children: /* @__PURE__ */ jsxs(Box, { paddingX: 1, justifyContent: "space-between", children: [
3762
+ /* @__PURE__ */ jsxs(Box, { children: [
3763
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "#00ff00", children: "AstraNova" }),
3764
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3765
+ /* @__PURE__ */ jsx(Text, { color: "#ff8800", children: agentName }),
3766
+ canFetchData && market && /* @__PURE__ */ jsxs(Fragment, { children: [
3767
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3768
+ /* @__PURE__ */ jsx(Text, { color: "#ffff00", children: "$NOVA " }),
3769
+ /* @__PURE__ */ jsx(Text, { color: "white", children: formatPrice(market.price) }),
3770
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3771
+ /* @__PURE__ */ jsx(Text, { color: moodColor(market.mood), children: market.mood })
3772
+ ] }),
3773
+ canFetchData && portfolio && /* @__PURE__ */ jsxs(Fragment, { children: [
3774
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3775
+ /* @__PURE__ */ jsxs(Text, { color: "#00ffff", children: [
3776
+ formatNum(portfolio.cash),
3777
+ " $SIM"
3778
+ ] }),
3779
+ portfolio.tokens > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
3780
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3781
+ /* @__PURE__ */ jsxs(Text, { color: "#ff00ff", children: [
3782
+ formatNum(portfolio.tokens),
3783
+ " $NOVA"
3784
+ ] })
3785
+ ] }),
3786
+ portfolio.pnl !== 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
3787
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3788
+ /* @__PURE__ */ jsxs(Text, { color: portfolio.pnl >= 0 ? "#00ff00" : "#ff4444", children: [
3789
+ "P&L ",
3790
+ portfolio.pnl >= 0 ? "+" : "",
3791
+ formatNum(portfolio.pnl),
3792
+ " (",
3793
+ portfolio.pnlPct >= 0 ? "+" : "",
3794
+ portfolio.pnlPct.toFixed(1),
3795
+ "%)"
3796
+ ] })
3797
+ ] }),
3798
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3799
+ /* @__PURE__ */ jsxs(Text, { color: "#e2f902", children: [
3800
+ "Net ",
3801
+ formatNum(portfolio.portfolioValue)
3802
+ ] })
3803
+ ] }),
3804
+ !canFetchData && /* @__PURE__ */ jsxs(Fragment, { children: [
3805
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3806
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "pending verification" })
3807
+ ] }),
3808
+ canFetchData && !market && !portfolio && /* @__PURE__ */ jsxs(Fragment, { children: [
3809
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \u2502 " }),
3810
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "loading..." })
3811
+ ] })
3812
+ ] }),
3813
+ apActive && /* @__PURE__ */ jsxs(Text, { color: "#00ff00", children: [
3814
+ "AP: \u25CF ",
3815
+ autopilotMode.toUpperCase(),
3816
+ " ",
3817
+ formatInterval(autopilotIntervalMs)
3818
+ ] })
3819
+ ] }) });
3820
+ });
3821
+ var StatusBar_default = StatusBar;
3822
+ function formatPrice(price) {
3823
+ if (price >= 1) return price.toFixed(2);
3824
+ if (price >= 0.01) return price.toFixed(4);
3825
+ return price.toFixed(6);
3826
+ }
3827
+ function formatNum(n) {
3828
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
3829
+ if (Math.abs(n) >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
3830
+ return n.toLocaleString(void 0, { maximumFractionDigits: 0 });
3831
+ }
3832
+ function moodColor(mood) {
3833
+ switch (mood) {
3834
+ case "euphoria":
3835
+ case "bullish":
3836
+ return "#00ff00";
3837
+ case "fear":
3838
+ case "bearish":
3839
+ return "#ff4444";
3840
+ case "crab":
3841
+ return "#ffff00";
3842
+ default:
3843
+ return "white";
3844
+ }
3845
+ }
3846
+ async function fetchMarket(agentName) {
3847
+ const result = await apiCall("GET", "/api/v1/market/state", void 0, agentName);
3848
+ if (!result.ok) return null;
3849
+ const d = result.data;
3850
+ const m = d.market ?? d;
3851
+ return {
3852
+ price: m.price ?? 0,
3853
+ mood: m.mood ?? "",
3854
+ epochId: m.epoch?.global ?? 0
3855
+ };
3856
+ }
3857
+ async function fetchPortfolio(agentName) {
3858
+ const result = await apiCall(
3859
+ "GET",
3860
+ "/api/v1/portfolio",
3861
+ void 0,
3862
+ agentName
3863
+ );
3864
+ if (!result.ok) return null;
3865
+ const d = result.data;
3866
+ const p = d.portfolio ?? d;
3867
+ const cash = p.cash ?? 0;
3868
+ const tokens = p.tokens ?? 0;
3869
+ const currentPrice = p.currentPrice ?? 0;
3870
+ return {
3871
+ cash,
3872
+ tokens,
3873
+ portfolioValue: p.portfolioValue ?? cash + tokens * currentPrice,
3874
+ pnl: p.pnl ?? 0,
3875
+ pnlPct: p.pnlPct ?? 0
3876
+ };
3877
+ }
3878
+
3879
+ // src/ui/ChatView.tsx
3880
+ import { Box as Box5, Text as Text5 } from "ink";
3881
+
3882
+ // src/ui/MarkdownText.tsx
3883
+ import { Text as Text4, Box as Box4 } from "ink";
3884
+
3885
+ // src/ui/PortfolioCard.tsx
3886
+ import { Box as Box2, Text as Text2 } from "ink";
3887
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
3888
+ function PortfolioCard({ data }) {
3889
+ const price = data.currentPrice ?? 0;
3890
+ const cash = data.cash ?? 0;
3891
+ const tokens = data.tokens ?? 0;
3892
+ const value = data.portfolioValue ?? cash + tokens * price;
3893
+ const pnl = data.pnl ?? 0;
3894
+ const pnlPct = data.pnlPct ?? 0;
3895
+ const earned = data.totalEarned ? Number(data.totalEarned) / 1e9 : 0;
3896
+ const claimable = data.claimable ? Number(data.claimable) / 1e9 : 0;
3897
+ const pnlColor = pnl >= 0 ? "#00ff00" : "#ff4444";
3898
+ const pnlSign = pnl >= 0 ? "+" : "";
3899
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", paddingLeft: 1, marginY: 1, children: [
3900
+ /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "#00ffff", children: "Portfolio Overview" }) }),
3901
+ /* @__PURE__ */ jsx2(Row, { label: "$SIM Balance", value: formatNum2(cash), color: "#00ffff" }),
3902
+ /* @__PURE__ */ jsx2(Row, { label: "$NOVA Holdings", value: tokens > 0 ? formatNum2(tokens) : "\u2014", color: "#ff00ff" }),
3903
+ /* @__PURE__ */ jsx2(Row, { label: "$NOVA Price", value: price > 0 ? formatPrice2(price) : "\u2014", color: "#ffff00" }),
3904
+ /* @__PURE__ */ jsx2(Row, { label: "Portfolio Value", value: formatNum2(value), color: "white" }),
3905
+ /* @__PURE__ */ jsx2(Row, { label: "P&L", value: `${pnlSign}${formatNum2(pnl)} (${pnlSign}${pnlPct.toFixed(1)}%)`, color: pnlColor }),
3906
+ (data.hasWallet !== void 0 || data.walletLocal !== void 0) && /* @__PURE__ */ jsx2(
3907
+ Row,
3908
+ {
3909
+ label: "Wallet",
3910
+ value: data.hasWallet ? "registered" : data.walletLocal ? "needs registration" : "not set",
3911
+ color: data.hasWallet ? "#00ff00" : data.walletLocal ? "#ffff00" : "gray"
3912
+ }
3913
+ ),
3914
+ (earned > 0 || claimable > 0) && /* @__PURE__ */ jsxs2(Fragment2, { children: [
3915
+ /* @__PURE__ */ jsx2(Row, { label: "$ASTRA Earned", value: earned > 0 ? formatAstra(earned) : "\u2014", color: "#ffff00" }),
3916
+ /* @__PURE__ */ jsx2(Row, { label: "Claimable", value: claimable > 0 ? formatAstra(claimable) : "\u2014", color: claimable > 0 ? "#00ff00" : "gray" })
3917
+ ] })
3918
+ ] });
3919
+ }
3920
+ function Row({ label, value, color }) {
3921
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
3922
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
3923
+ label,
3924
+ ": "
3925
+ ] }),
3926
+ /* @__PURE__ */ jsx2(Text2, { color, bold: true, children: value })
3927
+ ] });
3928
+ }
3929
+ function formatPrice2(price) {
3930
+ if (price >= 1) return `$${price.toFixed(2)}`;
3931
+ if (price >= 0.01) return `$${price.toFixed(4)}`;
3932
+ return `$${price.toFixed(6)}`;
3933
+ }
3934
+ function formatNum2(n) {
3935
+ if (Math.abs(n) >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
3936
+ if (Math.abs(n) >= 1e4) return `${(n / 1e3).toFixed(1)}K`;
3937
+ return n.toLocaleString(void 0, { maximumFractionDigits: 2 });
3938
+ }
3939
+ function formatAstra(n) {
3940
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M ASTRA`;
3941
+ if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K ASTRA`;
3942
+ return `${n.toFixed(2)} ASTRA`;
3943
+ }
3944
+
3945
+ // src/ui/RewardsCard.tsx
3946
+ import { Box as Box3, Text as Text3 } from "ink";
3947
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3948
+ function RewardsCard({ data }) {
3949
+ const total = data.totalAstra ? Number(data.totalAstra) / 1e9 : 0;
3950
+ const epoch = data.epochAstra ? Number(data.epochAstra) / 1e9 : 0;
3951
+ const bonus = data.bonusAstra ? Number(data.bonusAstra) / 1e9 : 0;
3952
+ const statusColor = data.claimStatus === "claimable" ? "#00ff00" : data.claimStatus === "sent" ? "#00ffff" : "#ffff00";
3953
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingLeft: 1, marginY: 1, children: [
3954
+ /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
3955
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "#ffff00", children: "$ASTRA Rewards" }),
3956
+ data.seasonId && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3957
+ " ",
3958
+ data.seasonId
3959
+ ] })
3960
+ ] }),
3961
+ /* @__PURE__ */ jsx3(Row2, { label: "Total Earned", value: formatAstra2(total), color: "#ffff00" }),
3962
+ /* @__PURE__ */ jsx3(Row2, { label: "Epoch Rewards", value: formatAstra2(epoch), color: "#00ffff" }),
3963
+ /* @__PURE__ */ jsx3(Row2, { label: "Season Bonus", value: formatAstra2(bonus), color: "#ff00ff" }),
3964
+ /* @__PURE__ */ jsx3(Row2, { label: "Status", value: data.claimStatus ?? "\u2014", color: statusColor }),
3965
+ /* @__PURE__ */ jsx3(Row2, { label: "Epochs Rewarded", value: data.epochsRewarded?.toString() ?? "\u2014", color: "white" }),
3966
+ data.bestEpochPnl !== void 0 && data.bestEpochPnl > 0 && /* @__PURE__ */ jsx3(Row2, { label: "Best Epoch P&L", value: `+${data.bestEpochPnl.toFixed(2)}`, color: "#00ff00" }),
3967
+ data.txSignature && /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, children: [
3968
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tx: " }),
3969
+ /* @__PURE__ */ jsx3(Text3, { color: "#00ffff", children: data.txSignature })
3970
+ ] })
3971
+ ] });
3972
+ }
3973
+ function Row2({ label, value, color }) {
3974
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
3975
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3976
+ label,
3977
+ ": "
3978
+ ] }),
3979
+ /* @__PURE__ */ jsx3(Text3, { color, bold: true, children: value })
3980
+ ] });
3981
+ }
3982
+ function formatAstra2(n) {
3983
+ if (n === 0) return "\u2014";
3984
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M ASTRA`;
3985
+ if (n >= 1e3) return `${(n / 1e3).toFixed(2)}K ASTRA`;
3986
+ return `${n.toFixed(4)} ASTRA`;
3987
+ }
3988
+
3989
+ // src/ui/MarkdownText.tsx
3990
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3991
+ function MarkdownText({ children }) {
3992
+ const lines = children.split("\n");
3993
+ const elements = [];
3994
+ let i = 0;
3995
+ while (i < lines.length) {
3996
+ const line = lines[i];
3997
+ if (line.trimStart().startsWith(":::portfolio") || line.trimStart().startsWith(":::rewards")) {
3998
+ const cardType = line.trimStart().startsWith(":::portfolio") ? "portfolio" : "rewards";
3999
+ const jsonLines = [];
4000
+ i++;
4001
+ while (i < lines.length && !lines[i].trimStart().startsWith(":::")) {
4002
+ jsonLines.push(lines[i]);
4003
+ i++;
4004
+ }
4005
+ i++;
4006
+ try {
4007
+ const raw = jsonLines.join("\n");
4008
+ if (cardType === "portfolio") {
4009
+ const data = JSON.parse(raw);
4010
+ elements.push(/* @__PURE__ */ jsx4(PortfolioCard, { data }, elements.length));
4011
+ } else {
4012
+ const data = JSON.parse(raw);
4013
+ elements.push(/* @__PURE__ */ jsx4(RewardsCard, { data }, elements.length));
4014
+ }
4015
+ } catch {
4016
+ elements.push(
4017
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: jsonLines.join("\n") }) }, elements.length)
4018
+ );
4019
+ }
4020
+ continue;
4021
+ }
4022
+ if (line.trimStart().startsWith("```")) {
4023
+ const codeLines = [];
4024
+ i++;
4025
+ while (i < lines.length && !lines[i].trimStart().startsWith("```")) {
4026
+ codeLines.push(lines[i]);
4027
+ i++;
3683
4028
  }
3684
- finalText += "\n\n(Response was truncated \u2014 try a shorter message or start a new session.)";
4029
+ i++;
4030
+ elements.push(
4031
+ /* @__PURE__ */ jsx4(Box4, { marginLeft: 2, marginY: 0, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", children: codeLines.join("\n") }) }, elements.length)
4032
+ );
4033
+ continue;
3685
4034
  }
3686
- debugLog3(`Step ${steps}: text=${result.text.length}chars, toolCalls=${result.toolCalls.map((tc) => tc.name).join(",") || "none"}`);
3687
- }
3688
- debugLog3(`Responses loop done after ${steps} steps, finalText=${finalText.length}chars`);
3689
- let text3 = finalText;
3690
- if (!text3 && steps > 0) {
3691
- debugLog3("Tools ran but no summary text \u2014 nudging model for a summary");
3692
- codexInput.push({ role: "user", content: "Please summarize what just happened." });
3693
- accessToken = await getAccessToken();
3694
- const summaryRetry = await callCodexWithRetry({
3695
- accessToken,
3696
- model,
3697
- instructions: systemPrompt,
3698
- input: codexInput,
3699
- tools: codexTools,
3700
- callbacks,
3701
- baseUrl,
3702
- onTokenExpired
3703
- });
3704
- text3 = summaryRetry.text;
3705
- }
3706
- if (!text3 && steps > 0) {
3707
- text3 = "I ran the requested action but the model returned no summary. Please try asking again.";
3708
- } else if (!text3) {
3709
- text3 = "(No response from model)";
4035
+ if (/^---+$/.test(line.trim())) {
4036
+ elements.push(
4037
+ /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2500".repeat(40) }) }, elements.length)
4038
+ );
4039
+ i++;
4040
+ continue;
4041
+ }
4042
+ const headerMatch = /^(#{1,4})\s+(.+)$/.exec(line);
4043
+ if (headerMatch) {
4044
+ elements.push(
4045
+ /* @__PURE__ */ jsx4(Box4, { marginTop: elements.length > 0 ? 1 : 0, children: /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: headerMatch[2] }) }, elements.length)
4046
+ );
4047
+ i++;
4048
+ continue;
4049
+ }
4050
+ const bulletMatch = /^(\s*)[-*]\s+(.+)$/.exec(line);
4051
+ if (bulletMatch) {
4052
+ const indent = Math.floor((bulletMatch[1]?.length ?? 0) / 2);
4053
+ elements.push(
4054
+ /* @__PURE__ */ jsxs4(Box4, { marginLeft: indent * 2, children: [
4055
+ /* @__PURE__ */ jsx4(Text4, { children: " \u25CF " }),
4056
+ renderInline(bulletMatch[2])
4057
+ ] }, elements.length)
4058
+ );
4059
+ i++;
4060
+ continue;
4061
+ }
4062
+ const numMatch = /^(\s*)(\d+)[.)]\s+(.+)$/.exec(line);
4063
+ if (numMatch) {
4064
+ const indent = Math.floor((numMatch[1]?.length ?? 0) / 2);
4065
+ elements.push(
4066
+ /* @__PURE__ */ jsxs4(Box4, { marginLeft: indent * 2, children: [
4067
+ /* @__PURE__ */ jsxs4(Text4, { children: [
4068
+ " ",
4069
+ numMatch[2],
4070
+ ". "
4071
+ ] }),
4072
+ renderInline(numMatch[3])
4073
+ ] }, elements.length)
4074
+ );
4075
+ i++;
4076
+ continue;
4077
+ }
4078
+ if (line.trim() === "") {
4079
+ elements.push(/* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsx4(Text4, { children: " " }) }, elements.length));
4080
+ i++;
4081
+ continue;
4082
+ }
4083
+ elements.push(
4084
+ /* @__PURE__ */ jsx4(Box4, { flexWrap: "wrap", children: renderInline(line) }, elements.length)
4085
+ );
4086
+ i++;
3710
4087
  }
3711
- responseMessages.push({ role: "assistant", content: text3 });
3712
- return { text: text3, responseMessages };
4088
+ return /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", children: elements });
3713
4089
  }
3714
- async function runSdkTurn(messages, systemPrompt, callbacks) {
3715
- debugLog3(`SDK turn starting \u2014 getting model...`);
3716
- const model = await getModel();
3717
- debugLog3(`Model ready: ${model.modelId ?? "unknown"} \u2014 calling streamText...`);
3718
- const abortController = new AbortController();
3719
- let idleTimer;
3720
- let timedOutBy;
3721
- const overallTimer = setTimeout(() => {
3722
- debugLog3("SDK turn timeout \u2014 aborting after overall timeout");
3723
- timedOutBy = "overall";
3724
- abortController.abort();
3725
- }, TURN_TIMEOUT_MS);
3726
- const resetIdleTimer = () => {
3727
- if (idleTimer) clearTimeout(idleTimer);
3728
- idleTimer = setTimeout(() => {
3729
- debugLog3("SDK turn idle timeout \u2014 no data for 30s, aborting");
3730
- timedOutBy = "idle";
3731
- abortController.abort();
3732
- }, IDLE_TIMEOUT_MS);
3733
- };
3734
- try {
3735
- const result = streamText({
3736
- model,
3737
- system: systemPrompt,
3738
- messages,
3739
- tools: astraTools,
3740
- maxSteps: 10,
3741
- temperature: 0.7,
3742
- abortSignal: abortController.signal,
3743
- onStepFinish: ({ toolCalls, toolResults }) => {
3744
- resetIdleTimer();
3745
- if (toolCalls && toolCalls.length > 0) {
3746
- for (let i = 0; i < toolCalls.length; i++) {
3747
- const tc = toolCalls[i];
3748
- const tr = toolResults?.[i];
3749
- callbacks.onToolCallEnd?.(tc.toolName);
3750
- writeAuditEntry({
3751
- ts: (/* @__PURE__ */ new Date()).toISOString(),
3752
- tool: tc.toolName,
3753
- args: tc.args,
3754
- result: tr?.result,
3755
- ok: !tr?.result?.error,
3756
- durationMs: 0
3757
- // not available from SDK callback
3758
- });
3759
- }
3760
- }
3761
- }
3762
- });
3763
- const abortPromise = new Promise((_, reject) => {
3764
- abortController.signal.addEventListener("abort", () => {
3765
- const label = timedOutBy === "idle" ? `No response for ${IDLE_TIMEOUT_MS / 1e3}s` : `Response timed out after ${TURN_TIMEOUT_MS / 1e3}s`;
3766
- reject(new Error(`${label}. Please try again.`));
4090
+ function renderInline(text3) {
4091
+ const parts = [];
4092
+ let remaining = text3;
4093
+ let key = 0;
4094
+ while (remaining.length > 0) {
4095
+ const candidates = [];
4096
+ const boldMatch = /\*\*(.+?)\*\*|__(.+?)__/.exec(remaining);
4097
+ if (boldMatch) {
4098
+ candidates.push({
4099
+ index: boldMatch.index,
4100
+ length: boldMatch[0].length,
4101
+ node: /* @__PURE__ */ jsx4(Text4, { bold: true, children: boldMatch[1] ?? boldMatch[2] }, key++)
3767
4102
  });
3768
- });
3769
- debugLog3("streamText created \u2014 consuming textStream...");
3770
- resetIdleTimer();
3771
- await Promise.race([
3772
- (async () => {
3773
- for await (const chunk of result.textStream) {
3774
- resetIdleTimer();
3775
- callbacks.onTextChunk(chunk);
3776
- }
3777
- })(),
3778
- abortPromise
3779
- ]);
3780
- debugLog3("textStream consumed \u2014 awaiting response...");
3781
- const response = await Promise.race([result.response, abortPromise]);
3782
- const text3 = await Promise.race([result.text, abortPromise]);
3783
- debugLog3(`SDK turn done \u2014 text=${text3.length}chars, messages=${response.messages.length}`);
3784
- return {
3785
- text: text3 || "(No response from LLM)",
3786
- responseMessages: response.messages
3787
- };
3788
- } catch (error) {
3789
- const message = error instanceof Error ? error.message : String(error);
3790
- debugLog3(`SDK turn error: ${message}`);
3791
- throw error;
3792
- } finally {
3793
- clearTimeout(overallTimer);
3794
- if (idleTimer) clearTimeout(idleTimer);
4103
+ }
4104
+ const codeMatch = /`([^`]+?)`/.exec(remaining);
4105
+ if (codeMatch) {
4106
+ candidates.push({
4107
+ index: codeMatch.index,
4108
+ length: codeMatch[0].length,
4109
+ node: /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: codeMatch[1] }, key++)
4110
+ });
4111
+ }
4112
+ const italicMatch = /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/.exec(remaining);
4113
+ if (italicMatch) {
4114
+ candidates.push({
4115
+ index: italicMatch.index,
4116
+ length: italicMatch[0].length,
4117
+ node: /* @__PURE__ */ jsx4(Text4, { italic: true, children: italicMatch[1] ?? italicMatch[2] }, key++)
4118
+ });
4119
+ }
4120
+ candidates.sort((a, b) => a.index - b.index);
4121
+ const pick = candidates[0];
4122
+ if (!pick) {
4123
+ parts.push(/* @__PURE__ */ jsx4(Text4, { children: remaining }, key++));
4124
+ break;
4125
+ }
4126
+ if (pick.index > 0) {
4127
+ parts.push(/* @__PURE__ */ jsx4(Text4, { children: remaining.slice(0, pick.index) }, key++));
4128
+ }
4129
+ parts.push(pick.node);
4130
+ remaining = remaining.slice(pick.index + pick.length);
3795
4131
  }
4132
+ return /* @__PURE__ */ jsx4(Text4, { wrap: "wrap", children: parts });
3796
4133
  }
3797
- function convertToCodexInput(messages) {
3798
- const codexInput = [];
3799
- for (const m of messages) {
3800
- if (m.role === "user" || m.role === "assistant") {
3801
- if (m.role === "assistant" && Array.isArray(m.content)) {
3802
- const textParts = m.content.filter(
3803
- (p) => p.type === "text"
3804
- );
3805
- if (textParts.length > 0) {
3806
- codexInput.push({
3807
- role: "assistant",
3808
- content: textParts.map((p) => p.text).join("")
3809
- });
3810
- }
3811
- for (const part of m.content) {
3812
- if (part.type === "tool-call") {
3813
- const tc = part;
3814
- const fcId = tc.toolCallId.startsWith("fc_") ? tc.toolCallId : `fc_${tc.toolCallId}`;
3815
- codexInput.push({
3816
- type: "function_call",
3817
- id: fcId,
3818
- call_id: tc.toolCallId,
3819
- name: tc.toolName,
3820
- arguments: JSON.stringify(tc.args)
3821
- });
3822
- }
3823
- }
3824
- } else {
3825
- codexInput.push({
3826
- role: m.role,
3827
- content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
3828
- });
4134
+
4135
+ // src/ui/ChatView.tsx
4136
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
4137
+ function ChatView({
4138
+ messages,
4139
+ streamingText
4140
+ }) {
4141
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", paddingX: 1, children: [
4142
+ messages.map((msg, i) => {
4143
+ if (msg.role === "log") {
4144
+ return /* @__PURE__ */ jsx5(Box5, { paddingLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: msg.content }) }, i);
3829
4145
  }
3830
- } else if (m.role === "tool") {
3831
- if (Array.isArray(m.content)) {
3832
- for (const part of m.content) {
3833
- if (part.type === "tool-result") {
3834
- const tr = part;
3835
- codexInput.push({
3836
- type: "function_call_output",
3837
- call_id: tr.toolCallId,
3838
- output: JSON.stringify(tr.result)
3839
- });
3840
- }
3841
- }
4146
+ if (msg.role === "autopilot") {
4147
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
4148
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "#ff00ff", children: " Autopilot" }),
4149
+ /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, wrap: "wrap", children: msg.content }) })
4150
+ ] }, i);
3842
4151
  }
3843
- }
3844
- }
3845
- return codexInput;
4152
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
4153
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: msg.role === "user" ? "#00ff00" : "#00ffff", children: msg.role === "user" ? " You" : " Agent" }),
4154
+ /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: msg.role === "assistant" ? /* @__PURE__ */ jsx5(MarkdownText, { children: msg.content }) : /* @__PURE__ */ jsx5(Text5, { wrap: "wrap", children: msg.content }) })
4155
+ ] }, i);
4156
+ }),
4157
+ streamingText !== void 0 && streamingText.length > 0 && /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
4158
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "#00ffff", children: " Agent" }),
4159
+ /* @__PURE__ */ jsx5(Box5, { marginLeft: 1, children: /* @__PURE__ */ jsx5(MarkdownText, { children: streamingText }) })
4160
+ ] })
4161
+ ] });
3846
4162
  }
3847
- async function summarizeForCompaction(messages, systemPrompt, provider, modelName) {
3848
- const summaryInstruction = `${systemPrompt}
3849
4163
 
3850
- ---
3851
-
3852
- ${COMPACTION_PROMPT}`;
3853
- if (provider === "openai-oauth") {
3854
- const accessToken = await getCodexAccessToken();
3855
- const codexInput = convertToCodexInput(messages);
3856
- const result2 = await callCodex({
3857
- accessToken,
3858
- model: modelName ?? "gpt-5.3-codex",
3859
- instructions: summaryInstruction,
3860
- input: codexInput,
3861
- timeoutMs: 6e4
3862
- });
3863
- return result2.text || "No summary generated.";
3864
- }
3865
- if (provider === "openai") {
3866
- const apiKey = getOpenAIApiKey();
3867
- const codexInput = convertToCodexInput(messages);
3868
- const result2 = await callCodex({
3869
- accessToken: apiKey,
3870
- model: modelName ?? "gpt-4o-mini",
3871
- instructions: summaryInstruction,
3872
- input: codexInput,
3873
- timeoutMs: 6e4,
3874
- baseUrl: OPENAI_RESPONSES_URL
3875
- });
3876
- return result2.text || "No summary generated.";
3877
- }
3878
- const model = await getModel();
3879
- const result = await generateText({
3880
- model,
3881
- system: summaryInstruction,
3882
- messages,
3883
- temperature: 0.3
3884
- });
3885
- return result.text || "No summary generated.";
3886
- }
3887
- var EMPTY_SENTINELS = [
3888
- "(No response from model)",
3889
- "(No response from LLM)",
3890
- "I ran the requested action but the model returned no summary."
3891
- ];
3892
- function isEmptyResponse(result) {
3893
- if (!result.text) return true;
3894
- const t = result.text.trim();
3895
- return EMPTY_SENTINELS.some((s) => t === s || t.startsWith(s));
4164
+ // src/ui/Input.tsx
4165
+ import { useState as useState2 } from "react";
4166
+ import { Box as Box6, Text as Text6 } from "ink";
4167
+ import TextInput from "ink-text-input";
4168
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
4169
+ function Input({
4170
+ isActive,
4171
+ onSubmit
4172
+ }) {
4173
+ const [value, setValue] = useState2("");
4174
+ const handleSubmit = (submitted) => {
4175
+ const trimmed = submitted.trim();
4176
+ if (!trimmed) return;
4177
+ onSubmit(trimmed);
4178
+ setValue("");
4179
+ };
4180
+ return /* @__PURE__ */ jsxs6(Box6, { width: "100%", paddingX: 2, paddingY: 1, children: [
4181
+ /* @__PURE__ */ jsx6(Text6, { color: isActive ? "yellow" : "gray", bold: true, children: "\u276F\u276F " }),
4182
+ /* @__PURE__ */ jsx6(
4183
+ TextInput,
4184
+ {
4185
+ value,
4186
+ onChange: setValue,
4187
+ onSubmit: handleSubmit,
4188
+ focus: isActive,
4189
+ showCursor: isActive,
4190
+ placeholder: ""
4191
+ }
4192
+ )
4193
+ ] });
3896
4194
  }
3897
- function extractJsonSchema(toolDef) {
3898
- const t = toolDef;
3899
- if (!t.parameters) return {};
3900
- const params = t.parameters;
3901
- if (params._def) {
3902
- try {
3903
- return zodToJsonSchema(params);
3904
- } catch (e) {
3905
- process.stderr.write(
3906
- `Warning: Failed to convert tool schema to JSON Schema: ${e instanceof Error ? e.message : "unknown error"}
3907
- `
3908
- );
3909
- return {};
3910
- }
3911
- }
3912
- if (typeof t.parameters === "object") {
3913
- return t.parameters;
3914
- }
3915
- return {};
4195
+
4196
+ // src/ui/Spinner.tsx
4197
+ import { useState as useState3, useEffect as useEffect2 } from "react";
4198
+ import { Text as Text7 } from "ink";
4199
+ import { jsxs as jsxs7 } from "react/jsx-runtime";
4200
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4201
+ var INTERVAL_MS = 80;
4202
+ function Spinner({ label }) {
4203
+ const [frame, setFrame] = useState3(0);
4204
+ useEffect2(() => {
4205
+ const timer = setInterval(() => {
4206
+ setFrame((prev) => (prev + 1) % FRAMES.length);
4207
+ }, INTERVAL_MS);
4208
+ return () => clearInterval(timer);
4209
+ }, []);
4210
+ return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
4211
+ FRAMES[frame],
4212
+ " ",
4213
+ label ?? "Thinking..."
4214
+ ] });
3916
4215
  }
3917
4216
 
3918
4217
  // src/ui/App.tsx
@@ -3931,6 +4230,7 @@ function App({
3931
4230
  initialCoreMessages,
3932
4231
  initialChatMessages,
3933
4232
  initialAutopilotConfig,
4233
+ initialPendingTrades = 0,
3934
4234
  debug
3935
4235
  }) {
3936
4236
  const { exit } = useApp();
@@ -3959,6 +4259,18 @@ function App({
3959
4259
  epochCallCountRef.current = saved.callCount;
3960
4260
  }
3961
4261
  }, [agentName]);
4262
+ useEffect3(() => {
4263
+ if (initialPendingTrades > 0) {
4264
+ const count = initialPendingTrades;
4265
+ setChatMessages((prev) => [
4266
+ ...prev,
4267
+ {
4268
+ role: "assistant",
4269
+ content: `Hey! Autopilot made **${count}** trade${count > 1 ? "s" : ""} while you were away. Type \`/auto report\` to see what happened.`
4270
+ }
4271
+ ]);
4272
+ }
4273
+ }, []);
3962
4274
  const coreMessagesRef = useRef2(coreMessages);
3963
4275
  useEffect3(() => {
3964
4276
  coreMessagesRef.current = coreMessages;
@@ -3986,7 +4298,7 @@ function App({
3986
4298
  setChatMessages((prev) => [...prev, { role: "log", content: text3 }]);
3987
4299
  }, []);
3988
4300
  const runAutopilotTurn = useCallback2(
3989
- async (triggerMsg) => {
4301
+ async (triggerMsg, displayMode = "log") => {
3990
4302
  const currentCore = coreMessagesRef.current;
3991
4303
  const newCoreMessages = [
3992
4304
  ...currentCore,
@@ -4004,11 +4316,17 @@ function App({
4004
4316
  apiContext,
4005
4317
  { ...profile, autopilotMode },
4006
4318
  {
4007
- onTextChunk: () => {
4319
+ onTextChunk: displayMode === "chat" ? (chunk) => {
4320
+ setStreamingText((prev) => (prev ?? "") + chunk);
4321
+ } : () => {
4008
4322
  },
4009
- onToolCallStart: () => {
4323
+ onToolCallStart: displayMode === "chat" ? (name) => {
4324
+ setToolName(name);
4325
+ } : () => {
4010
4326
  },
4011
- onToolCallEnd: () => {
4327
+ onToolCallEnd: displayMode === "chat" ? () => {
4328
+ setToolName(void 0);
4329
+ } : () => {
4012
4330
  }
4013
4331
  },
4014
4332
  memoryContent
@@ -4017,12 +4335,16 @@ function App({
4017
4335
  const updatedCore = [...baseCoreMessages, ...result.responseMessages];
4018
4336
  setCoreMessages(updatedCore);
4019
4337
  const responseText = result.text.trim();
4020
- if (responseText) {
4021
- const firstLine = responseText.split("\n")[0].slice(0, 80);
4022
- const detail = responseText.length > 80 ? responseText.slice(0, 120) : void 0;
4023
- addLogEntry(firstLine, detail !== firstLine ? detail : void 0);
4338
+ if (displayMode === "chat") {
4339
+ setStreamingText(void 0);
4340
+ setChatMessages((prev) => [
4341
+ ...prev,
4342
+ { role: "assistant", content: responseText || "Market checked \u2014 holding." }
4343
+ ]);
4024
4344
  } else {
4025
- addLogEntry("checked \u2192 no response");
4345
+ const summary = responseText.split("\n")[0].slice(0, 120) || "checked \u2192 no response";
4346
+ addLogEntry(summary);
4347
+ appendAutopilotLog(agentName, { ts: (/* @__PURE__ */ new Date()).toISOString(), action: summary });
4026
4348
  }
4027
4349
  const updatedChat = chatMessagesRef.current;
4028
4350
  saveSession({
@@ -4034,9 +4356,15 @@ function App({
4034
4356
  });
4035
4357
  } catch (error) {
4036
4358
  const message = error instanceof Error ? error.message : "Unknown error";
4037
- addLogEntry("error", message);
4359
+ if (displayMode === "chat") {
4360
+ setStreamingText(void 0);
4361
+ setChatMessages((prev) => [...prev, { role: "assistant", content: `Autopilot error: ${message}` }]);
4362
+ } else {
4363
+ addLogEntry("error", message);
4364
+ }
4038
4365
  } finally {
4039
4366
  setIsLoading(false);
4367
+ if (displayMode === "chat") setToolName(void 0);
4040
4368
  }
4041
4369
  },
4042
4370
  [skillContext, tradingContext, walletContext, rewardsContext, onboardingContext, apiContext, profile, autopilotMode, agentName, sessionId, memoryContent, addLogEntry]
@@ -4051,18 +4379,20 @@ function App({
4051
4379
  addLogEntry("Budget reached \u2014 skipping until next epoch");
4052
4380
  return;
4053
4381
  }
4054
- const trigger = buildAutopilotTrigger(autopilotMode);
4382
+ const strategy = loadStrategy(agentName);
4383
+ const trigger = buildAutopilotTrigger(autopilotMode, strategy);
4055
4384
  if (!trigger) return;
4056
4385
  if (autopilotMode === "semi") {
4057
- void sendMessageRef.current(trigger, "autopilot");
4386
+ void runAutopilotTurn(trigger, "chat");
4058
4387
  } else {
4059
- void runAutopilotTurn(trigger);
4388
+ void runAutopilotTurn(trigger, "log");
4060
4389
  }
4061
4390
  }, autopilotIntervalMs);
4062
4391
  return () => clearInterval(interval);
4063
4392
  }, [autopilotMode, autopilotIntervalMs, addLogEntry, runAutopilotTurn]);
4064
4393
  const sendMessage = useCallback2(
4065
4394
  async (userText, displayRole = "user") => {
4395
+ let skipChatDisplay = false;
4066
4396
  if (userText.startsWith("/")) {
4067
4397
  const parts = userText.trim().split(/\s+/);
4068
4398
  const cmd = parts[0].toLowerCase();
@@ -4101,28 +4431,61 @@ Epoch Budget: ${budgetLabel} calls used` }
4101
4431
  return;
4102
4432
  }
4103
4433
  if (sub === "on" || sub === "semi") {
4434
+ stopDaemon(agentName);
4104
4435
  setAutopilotMode("semi");
4105
4436
  saveAutopilotConfig({ mode: "semi", intervalMs: autopilotIntervalMs });
4437
+ const hasStrat = !!loadStrategy(agentName);
4106
4438
  setChatMessages((prev) => [
4107
4439
  ...prev,
4108
4440
  { role: "user", content: userText },
4109
- { role: "assistant", content: `Autopilot enabled: **SEMI** mode (every ${formatInterval(autopilotIntervalMs)}). I'll propose trades for your approval.` }
4441
+ { role: "assistant", content: `Autopilot enabled: **SEMI** mode (every ${formatInterval(autopilotIntervalMs)}). I'll execute trades automatically based on your strategy.${!hasStrat ? "\n\n\u26A0\uFE0F No strategy set yet \u2014 I'll use general momentum signals. Use `/strategy setup` to create one." : ""}` }
4110
4442
  ]);
4111
4443
  addLogEntry("Mode set to SEMI");
4112
4444
  return;
4113
4445
  }
4114
4446
  if (sub === "full") {
4447
+ const strategy = loadStrategy(agentName);
4448
+ if (!strategy) {
4449
+ setChatMessages((prev) => [
4450
+ ...prev,
4451
+ { role: "user", content: userText },
4452
+ { role: "assistant", content: "You need a trading strategy before enabling full autopilot. Use `/strategy setup` to create one." }
4453
+ ]);
4454
+ return;
4455
+ }
4115
4456
  setAutopilotMode("full");
4116
4457
  saveAutopilotConfig({ mode: "full", intervalMs: autopilotIntervalMs });
4458
+ startDaemon(agentName);
4117
4459
  setChatMessages((prev) => [
4118
4460
  ...prev,
4119
4461
  { role: "user", content: userText },
4120
- { role: "assistant", content: `Autopilot enabled: **FULL** mode (every ${formatInterval(autopilotIntervalMs)}). I'll execute trades autonomously \u2014 check the log panel.` }
4462
+ { role: "assistant", content: `Autopilot enabled: **FULL** mode (every ${formatInterval(autopilotIntervalMs)}). I'll execute trades autonomously based on your strategy \u2014 even when you close the app.` }
4121
4463
  ]);
4122
- addLogEntry("Mode set to FULL");
4464
+ addLogEntry("Mode set to FULL \u2014 daemon started");
4465
+ return;
4466
+ }
4467
+ if (sub === "report") {
4468
+ const entries = loadAutopilotLogSince(agentName, null);
4469
+ if (entries.length === 0) {
4470
+ setChatMessages((prev) => [
4471
+ ...prev,
4472
+ { role: "user", content: userText },
4473
+ { role: "assistant", content: "No autopilot trades logged yet." }
4474
+ ]);
4475
+ } else {
4476
+ const lines = entries.slice(-20).map((e) => `\`${e.ts.slice(11, 16)}\` ${e.action}`);
4477
+ setChatMessages((prev) => [
4478
+ ...prev,
4479
+ { role: "user", content: userText },
4480
+ { role: "assistant", content: `**Autopilot log** (last ${lines.length} entries):
4481
+
4482
+ ${lines.join("\n")}` }
4483
+ ]);
4484
+ }
4123
4485
  return;
4124
4486
  }
4125
4487
  if (sub === "off") {
4488
+ stopDaemon(agentName);
4126
4489
  setAutopilotMode("off");
4127
4490
  saveAutopilotConfig({ mode: "off", intervalMs: autopilotIntervalMs });
4128
4491
  setChatMessages((prev) => [
@@ -4154,6 +4517,49 @@ Epoch Budget: ${budgetLabel} calls used` }
4154
4517
  ]);
4155
4518
  return;
4156
4519
  }
4520
+ if (cmd === "/strategy") {
4521
+ const sub = parts[1]?.toLowerCase();
4522
+ const originalCmd = userText;
4523
+ if (sub === "status") {
4524
+ const strategy = loadStrategy(agentName);
4525
+ setChatMessages((prev) => [
4526
+ ...prev,
4527
+ { role: "user", content: originalCmd },
4528
+ strategy ? { role: "assistant", content: `**Your current strategy:**
4529
+
4530
+ ${strategy}` } : { role: "assistant", content: "No strategy set up yet. Use `/strategy setup` to create one." }
4531
+ ]);
4532
+ return;
4533
+ }
4534
+ if (sub === "setup") {
4535
+ const existing = loadStrategy(agentName);
4536
+ userText = existing ? `I want to review and update my trading strategy. Here it is:
4537
+
4538
+ ${existing}
4539
+
4540
+ Let's go through it and improve or replace it.` : "I want to create a trading strategy. Please guide me through the options.";
4541
+ setChatMessages((prev) => [...prev, { role: "user", content: originalCmd }]);
4542
+ skipChatDisplay = true;
4543
+ } else if (!sub) {
4544
+ const strategy = loadStrategy(agentName);
4545
+ if (strategy) {
4546
+ const trigger = buildStrategyRunTrigger(strategy);
4547
+ setChatMessages((prev) => [...prev, { role: "user", content: originalCmd }]);
4548
+ void runAutopilotTurn(trigger, "chat");
4549
+ return;
4550
+ }
4551
+ userText = "I want to create a trading strategy. Please guide me through the options.";
4552
+ setChatMessages((prev) => [...prev, { role: "user", content: originalCmd }]);
4553
+ skipChatDisplay = true;
4554
+ } else {
4555
+ setChatMessages((prev) => [
4556
+ ...prev,
4557
+ { role: "user", content: originalCmd },
4558
+ { role: "assistant", content: "Usage: `/strategy` \xB7 `/strategy setup` \xB7 `/strategy status`" }
4559
+ ]);
4560
+ return;
4561
+ }
4562
+ }
4157
4563
  if (cmd === "/help" || cmd === "/?") {
4158
4564
  setChatMessages((prev) => [
4159
4565
  ...prev,
@@ -4172,13 +4578,17 @@ Epoch Budget: ${budgetLabel} calls used` }
4172
4578
  " `/buy <amt>` \u2014 Buy $NOVA (e.g. `/buy 500`)",
4173
4579
  " `/sell <amt>`\u2014 Sell $NOVA (e.g. `/sell 200`)",
4174
4580
  "",
4175
- "**Autopilot**",
4581
+ "**Strategy & Autopilot**",
4176
4582
  "",
4177
- " `/auto on` \u2014 Enable semi-auto mode",
4178
- " `/auto full` \u2014 Enable full-auto mode",
4179
- " `/auto off` \u2014 Disable autopilot",
4180
- " `/auto 5m` \u2014 Set interval (1m-60m)",
4181
- " `/auto status` \u2014 Show autopilot status",
4583
+ " `/strategy` \u2014 Run strategy once (or create if none)",
4584
+ " `/strategy setup` \u2014 Create or edit your strategy",
4585
+ " `/strategy status` \u2014 View current strategy",
4586
+ " `/auto on` \u2014 Enable semi-auto mode",
4587
+ " `/auto full` \u2014 Enable full-auto mode (requires strategy)",
4588
+ " `/auto off` \u2014 Disable autopilot",
4589
+ " `/auto 5m` \u2014 Set interval (1m-60m)",
4590
+ " `/auto status` \u2014 Show autopilot status",
4591
+ " `/auto report` \u2014 Show autopilot trade log",
4182
4592
  "",
4183
4593
  "**System**",
4184
4594
  "",
@@ -4203,7 +4613,7 @@ Epoch Budget: ${budgetLabel} calls used` }
4203
4613
  };
4204
4614
  if (shortcuts[cmd]) {
4205
4615
  userText = shortcuts[cmd];
4206
- } else {
4616
+ } else if (cmd !== "/strategy") {
4207
4617
  setChatMessages((prev) => [
4208
4618
  ...prev,
4209
4619
  { role: "user", content: userText },
@@ -4212,10 +4622,12 @@ Epoch Budget: ${budgetLabel} calls used` }
4212
4622
  return;
4213
4623
  }
4214
4624
  }
4215
- setChatMessages((prev) => [
4216
- ...prev,
4217
- { role: displayRole, content: userText }
4218
- ]);
4625
+ if (!skipChatDisplay) {
4626
+ setChatMessages((prev) => [
4627
+ ...prev,
4628
+ { role: displayRole, content: userText }
4629
+ ]);
4630
+ }
4219
4631
  const newCoreMessages = [
4220
4632
  ...coreMessages,
4221
4633
  { role: "user", content: userText }
@@ -4326,7 +4738,7 @@ ${stack}
4326
4738
  }
4327
4739
  ) }),
4328
4740
  /* @__PURE__ */ jsxs8(Box7, { flexShrink: 0, width: "100%", paddingX: 2, marginTop: 1, justifyContent: "space-between", children: [
4329
- /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: "/help \xB7 /portfolio \xB7 /market \xB7 /exit" }),
4741
+ /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: "/help \xB7 /portfolio \xB7 /market \xB7 /strategy \xB7 /exit" }),
4330
4742
  /* @__PURE__ */ jsx7(Text8, { dimColor: true, children: "/auto on\xB7off\xB7set \xB7 Ctrl+C quit" })
4331
4743
  ] })
4332
4744
  ] });
@@ -4348,14 +4760,19 @@ function detectJourneyStage(params) {
4348
4760
  }
4349
4761
  async function main() {
4350
4762
  ensureBaseStructure();
4763
+ const args = process4.argv.slice(2);
4764
+ const shouldContinue = args.includes("--continue") || args.includes("-c");
4765
+ const debug = args.includes("--debug") || args.includes("-d");
4766
+ const isDaemonMode = args.includes("--daemon");
4767
+ if (isDaemonMode) {
4768
+ await runDaemon();
4769
+ return;
4770
+ }
4351
4771
  if (isRestartRequested()) {
4352
4772
  clearRestartFlag();
4353
4773
  }
4354
- const args = process2.argv.slice(2);
4355
- const shouldContinue = args.includes("--continue") || args.includes("-c");
4356
- const debug = args.includes("--debug") || args.includes("-d");
4357
4774
  if (debug) {
4358
- process2.env.ASTRA_DEBUG = "1";
4775
+ process4.env.ASTRA_DEBUG = "1";
4359
4776
  }
4360
4777
  const isReturning = isConfigured();
4361
4778
  let onboardingResult = null;
@@ -4363,7 +4780,7 @@ async function main() {
4363
4780
  onboardingResult = await runOnboarding();
4364
4781
  if (!onboardingResult) {
4365
4782
  console.error("Onboarding failed. Please try again.");
4366
- process2.exit(1);
4783
+ process4.exit(1);
4367
4784
  }
4368
4785
  }
4369
4786
  const config = loadConfig();
@@ -4371,21 +4788,21 @@ async function main() {
4371
4788
  console.error(
4372
4789
  "No config found. Delete ~/.config/astranova/config.json and re-run to start fresh."
4373
4790
  );
4374
- process2.exit(1);
4791
+ process4.exit(1);
4375
4792
  }
4376
4793
  const agentName = getActiveAgent();
4377
4794
  if (!agentName) {
4378
4795
  console.error(
4379
4796
  "No active agent found. Delete ~/.config/astranova/ and re-run to start fresh."
4380
4797
  );
4381
- process2.exit(1);
4798
+ process4.exit(1);
4382
4799
  }
4383
4800
  const credentials = loadCredentials(agentName);
4384
4801
  if (!credentials) {
4385
4802
  console.error(
4386
4803
  `No credentials found for agent "${agentName}". Delete ~/.config/astranova/ and re-run to start fresh.`
4387
4804
  );
4388
- process2.exit(1);
4805
+ process4.exit(1);
4389
4806
  }
4390
4807
  let apiStatus = null;
4391
4808
  if (isReturning) {
@@ -4421,6 +4838,7 @@ async function main() {
4421
4838
  journeyStage: stage,
4422
4839
  verificationCode: onboardingResult?.verificationCode ?? apiStatus?.verificationCode
4423
4840
  });
4841
+ const hasStrategy = !!loadStrategy(agentName);
4424
4842
  const profile = {
4425
4843
  agentName,
4426
4844
  status: apiStatus?.status ?? (isNewAgent ? "pending_verification" : "active"),
@@ -4430,7 +4848,8 @@ async function main() {
4430
4848
  verificationCode: onboardingResult?.verificationCode ?? apiStatus?.verificationCode,
4431
4849
  isNewAgent,
4432
4850
  boardPosted,
4433
- journeyStage: stage
4851
+ journeyStage: stage,
4852
+ hasStrategy
4434
4853
  };
4435
4854
  pruneOldSessions(agentName);
4436
4855
  const memoryContent = loadMemory(agentName);
@@ -4452,6 +4871,12 @@ async function main() {
4452
4871
  }
4453
4872
  }
4454
4873
  const initialAutopilotConfig = loadAutopilotConfig();
4874
+ const lastSessionAt = shouldContinue ? (() => {
4875
+ const s = loadLatestSession(agentName);
4876
+ return s ? new Date(s.updatedAt) : null;
4877
+ })() : null;
4878
+ const pendingTrades = loadAutopilotLogSince(agentName, lastSessionAt);
4879
+ const initialPendingTrades = pendingTrades.length;
4455
4880
  const { waitUntilExit } = render(
4456
4881
  React5.createElement(App, {
4457
4882
  agentName,
@@ -4467,6 +4892,7 @@ async function main() {
4467
4892
  initialCoreMessages,
4468
4893
  initialChatMessages,
4469
4894
  initialAutopilotConfig,
4895
+ initialPendingTrades,
4470
4896
  debug
4471
4897
  })
4472
4898
  );
@@ -4478,18 +4904,18 @@ async function main() {
4478
4904
  Restarting as ${newAgent}...
4479
4905
  `);
4480
4906
  try {
4481
- execFileSync(process2.execPath, process2.argv.slice(1), {
4907
+ execFileSync(process4.execPath, process4.argv.slice(1), {
4482
4908
  stdio: "inherit",
4483
- env: process2.env
4909
+ env: process4.env
4484
4910
  });
4485
4911
  } catch {
4486
4912
  }
4487
- process2.exit(0);
4913
+ process4.exit(0);
4488
4914
  }
4489
4915
  }
4490
4916
  main().catch((error) => {
4491
4917
  const message = error instanceof Error ? error.message : String(error);
4492
4918
  console.error(`Fatal: ${message}`);
4493
- process2.exit(1);
4919
+ process4.exit(1);
4494
4920
  });
4495
4921
  //# sourceMappingURL=astra.js.map