@jellyos/agent 0.1.4 → 0.1.5

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 (94) hide show
  1. package/README.npm.md +212 -0
  2. package/bin/jellyos-mcp +26 -0
  3. package/dist/api/ExtensionAPI.d.ts +6 -0
  4. package/dist/cli.js +114 -48
  5. package/dist/index.d.ts +15 -2
  6. package/dist/index.js +13 -3
  7. package/dist/mcp/entry.d.ts +2 -0
  8. package/dist/mcp/entry.js +71 -0
  9. package/dist/mcp/server.d.ts +31 -0
  10. package/dist/mcp/server.js +128 -0
  11. package/dist/models/ModelRegistry.d.ts +12 -1
  12. package/dist/models/ModelRegistry.js +105 -9
  13. package/dist/runner/AgentRunner.d.ts +19 -2
  14. package/dist/runner/AgentRunner.js +247 -17
  15. package/dist/runner/ModelClient.d.ts +10 -1
  16. package/dist/runner/ModelClient.js +79 -6
  17. package/dist/runner/SwarmRouter.d.ts +6 -6
  18. package/dist/runner/SwarmRouter.js +73 -24
  19. package/dist/runner/ToolDispatcher.d.ts +10 -0
  20. package/dist/runner/ToolDispatcher.js +106 -2
  21. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  22. package/dist/scheduler/AgentScheduler.js +253 -0
  23. package/dist/session/ContextStore.d.ts +96 -0
  24. package/dist/session/ContextStore.js +207 -0
  25. package/dist/session/GoalManager.d.ts +101 -0
  26. package/dist/session/GoalManager.js +167 -0
  27. package/dist/session/MemoryStore.d.ts +48 -0
  28. package/dist/session/MemoryStore.js +166 -0
  29. package/dist/session/SessionManager.d.ts +45 -4
  30. package/dist/session/SessionManager.js +151 -8
  31. package/dist/telemetry/Tracer.d.ts +48 -0
  32. package/dist/telemetry/Tracer.js +102 -0
  33. package/dist/tests/ContextStore.test.d.ts +2 -0
  34. package/dist/tests/ContextStore.test.js +74 -0
  35. package/dist/tests/ModelRegistry.test.d.ts +2 -0
  36. package/dist/tests/ModelRegistry.test.js +69 -0
  37. package/dist/tests/SessionManager.test.d.ts +2 -0
  38. package/dist/tests/SessionManager.test.js +108 -0
  39. package/dist/tests/TechnicalAnalysis.test.d.ts +2 -0
  40. package/dist/tests/TechnicalAnalysis.test.js +109 -0
  41. package/dist/tools/MarketSentiment.d.ts +166 -0
  42. package/dist/tools/MarketSentiment.js +209 -0
  43. package/dist/tools/NewsSentiment.js +40 -13
  44. package/dist/tools/PriceFeed.d.ts +2 -0
  45. package/dist/tools/PriceFeed.js +79 -27
  46. package/dist/tools/TechnicalAnalysis.d.ts +37 -0
  47. package/dist/tools/TechnicalAnalysis.js +85 -0
  48. package/dist/tui/App.d.ts +2 -2
  49. package/dist/tui/App.js +280 -117
  50. package/dist/tui/REPL.d.ts +2 -1
  51. package/dist/tui/REPL.js +11 -6
  52. package/package.json +9 -4
  53. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  54. package/dist/api/ExtensionAPI.js.map +0 -1
  55. package/dist/api/Registry.d.ts.map +0 -1
  56. package/dist/api/Registry.js.map +0 -1
  57. package/dist/cli.d.ts.map +0 -1
  58. package/dist/cli.js.map +0 -1
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js.map +0 -1
  61. package/dist/loader.d.ts.map +0 -1
  62. package/dist/loader.js.map +0 -1
  63. package/dist/models/CostTracker.d.ts.map +0 -1
  64. package/dist/models/CostTracker.js.map +0 -1
  65. package/dist/models/ModelRegistry.d.ts.map +0 -1
  66. package/dist/models/ModelRegistry.js.map +0 -1
  67. package/dist/models/index.d.ts.map +0 -1
  68. package/dist/models/index.js.map +0 -1
  69. package/dist/runner/AgentRunner.d.ts.map +0 -1
  70. package/dist/runner/AgentRunner.js.map +0 -1
  71. package/dist/runner/ModelClient.d.ts.map +0 -1
  72. package/dist/runner/ModelClient.js.map +0 -1
  73. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  74. package/dist/runner/SwarmRouter.js.map +0 -1
  75. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  76. package/dist/runner/ToolDispatcher.js.map +0 -1
  77. package/dist/session/SessionManager.d.ts.map +0 -1
  78. package/dist/session/SessionManager.js.map +0 -1
  79. package/dist/tools/NewsSentiment.d.ts.map +0 -1
  80. package/dist/tools/NewsSentiment.js.map +0 -1
  81. package/dist/tools/PriceFeed.d.ts.map +0 -1
  82. package/dist/tools/PriceFeed.js.map +0 -1
  83. package/dist/tools/TechnicalAnalysis.d.ts.map +0 -1
  84. package/dist/tools/TechnicalAnalysis.js.map +0 -1
  85. package/dist/tools/index.d.ts.map +0 -1
  86. package/dist/tools/index.js.map +0 -1
  87. package/dist/tui/App.d.ts.map +0 -1
  88. package/dist/tui/App.js.map +0 -1
  89. package/dist/tui/REPL.d.ts.map +0 -1
  90. package/dist/tui/REPL.js.map +0 -1
  91. package/dist/tui/StatusBar.d.ts.map +0 -1
  92. package/dist/tui/StatusBar.js.map +0 -1
  93. package/dist/tui/theme.d.ts.map +0 -1
  94. package/dist/tui/theme.js.map +0 -1
package/dist/tui/App.js CHANGED
@@ -1,24 +1,87 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
3
  * App — root Ink component.
4
- * Wires StatusBar + REPL + AgentRunner, registers built-in tools,
5
- * starts background feeds, and renders a multi-pane trading terminal.
4
+ * Multi-pane TUI with context bar, syntax-highlighted tool output,
5
+ * live side panel with ticker/prices, and command palette triggered via /palette.
6
6
  */
7
7
  import { useState, useCallback, useEffect, useRef } from "react";
8
- import { Box, useApp, useInput } from "ink";
8
+ import { Box, Text, useApp, useInput } from "ink";
9
9
  import { StatusBar } from "./StatusBar.js";
10
10
  import { REPL } from "./REPL.js";
11
- import { makeTheme, T } from "./theme.js";
11
+ import { makeTheme, T, JELLY_COLORS } from "./theme.js";
12
12
  import { AgentRunner } from "../runner/AgentRunner.js";
13
13
  import { SessionManager } from "../session/SessionManager.js";
14
14
  import { resolveModelConfig } from "../runner/ModelClient.js";
15
15
  import { priceFeed, getPricesTool, topMoversTool, marketOverviewTool, getPricesParams, topMoversParams, marketOverviewParams } from "../tools/PriceFeed.js";
16
16
  import { getNewsTool, getNewsParams } from "../tools/NewsSentiment.js";
17
- import { analyzeTAParams } from "../tools/TechnicalAnalysis.js";
17
+ import { analyzeTAParams, getCandlesParams, getCandlesTool } from "../tools/TechnicalAnalysis.js";
18
18
  import { fullAnalysis } from "../tools/TechnicalAnalysis.js";
19
19
  import { newsFeed } from "../tools/NewsSentiment.js";
20
+ import { getFearGreedTool, fearGreedParams, getFundingRatesTool, fundingRatesParams, getBtcMempoolTool, btcMempoolParams, getDefiTvlTool, defiTvlParams, getSolanaStatsTool, solanaStatsParams, } from "../tools/MarketSentiment.js";
21
+ import { contextStore } from "../session/ContextStore.js";
22
+ import { goalManager } from "../session/GoalManager.js";
23
+ import { memoryStore } from "../session/MemoryStore.js";
24
+ import { agentScheduler } from "../scheduler/AgentScheduler.js";
25
+ import { Tracer } from "../telemetry/Tracer.js";
26
+ // ── Context window tracking (#33) ───────────────────────────────────────────
27
+ function getContextBar(session) {
28
+ if (!session)
29
+ return { pct: 0, bar: "░".repeat(20), color: JELLY_COLORS.dim, turboReady: true };
30
+ const pressure = session.getContextPressure();
31
+ const filled = Math.round(pressure.pct / 5);
32
+ const color = pressure.level === "critical" ? JELLY_COLORS.error
33
+ : pressure.level === "red" ? JELLY_COLORS.warn
34
+ : pressure.level === "yellow" ? JELLY_COLORS.header
35
+ : JELLY_COLORS.success;
36
+ return {
37
+ pct: pressure.pct,
38
+ bar: "█".repeat(Math.min(filled, 20)) + "░".repeat(Math.max(0, 20 - filled)),
39
+ color,
40
+ turboReady: pressure.turboReady,
41
+ };
42
+ }
43
+ // ── Syntax-highlighted JSON formatter ───────────────────────────────────────
44
+ function highlightJson(obj, indent = 0) {
45
+ const pad = " ".repeat(indent);
46
+ if (obj === null)
47
+ return T.dim("null");
48
+ if (typeof obj === "boolean")
49
+ return T.warn(String(obj));
50
+ if (typeof obj === "number")
51
+ return T.success(String(obj));
52
+ if (typeof obj === "string") {
53
+ if (obj.startsWith("0x") && obj.length > 10)
54
+ return T.header(obj.slice(0, 10) + "…");
55
+ return T.accent(`"${obj}"`);
56
+ }
57
+ if (Array.isArray(obj)) {
58
+ if (obj.length === 0)
59
+ return "[]";
60
+ if (obj.length <= 3)
61
+ return `[${obj.map(v => highlightJson(v, 0)).join(", ")}]`;
62
+ return `[\n${obj.slice(0, 5).map(v => `${pad} ${highlightJson(v, indent + 1)}`).join(",\n")}${obj.length > 5 ? `\n${pad} …${obj.length - 5} more` : ""}\n${pad}]`;
63
+ }
64
+ if (typeof obj === "object") {
65
+ const entries = Object.entries(obj);
66
+ if (entries.length === 0)
67
+ return "{}";
68
+ return `{\n${entries.slice(0, 8).map(([k, v]) => `${pad} ${T.accent(k)}: ${highlightJson(v, indent + 1)}`).join(",\n")}${entries.length > 8 ? `\n${pad} …${entries.length - 8} more` : ""}\n${pad}}`;
69
+ }
70
+ return String(obj);
71
+ }
72
+ function formatToolContent(text) {
73
+ try {
74
+ const obj = JSON.parse(text);
75
+ return highlightJson(obj);
76
+ }
77
+ catch {
78
+ return text.length > 300 ? text.slice(0, 300) + T.dim("\n…[truncated]") : text;
79
+ }
80
+ }
20
81
  let _msgIdCounter = 0;
21
82
  function nextId() { return String(++_msgIdCounter); }
83
+ // Unique session ID for memory persistence
84
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
22
85
  export function App({ registry, systemPrompt, effectLevel: initialEffect = "normal", chain: initialChain = "ethereum", modelReg, costTracker, onNotifyReady, onStatusReady, }) {
23
86
  const { exit } = useApp();
24
87
  const [messages, setMessages] = useState([]);
@@ -41,7 +104,6 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
41
104
  modelName = resolveModelConfig(modelReg).model;
42
105
  }
43
106
  catch { /* shown via banner */ }
44
- // ── Push helpers ─────────────────────────────────────────────────────────
45
107
  const push = useCallback((msg) => {
46
108
  setMessages(prev => [...prev, { ...msg, id: nextId(), ts: Date.now() }]);
47
109
  }, []);
@@ -57,123 +119,98 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
57
119
  if (key === "effect_level")
58
120
  setEffectLevel(value);
59
121
  }, []);
60
- const uiCtx = { notify, setStatus, setTheme(_name) { }, setHeader(_factory) { }, theme };
61
- // ── Register built-in tools ──────────────────────────────────────────────
122
+ const uiCtx = { notify, setStatus, setTheme(_n) { }, setHeader(_f) { }, theme };
123
+ // Register built-in tools
62
124
  function registerBuiltinTools() {
63
125
  if (!modelReg)
64
126
  return;
65
- // Model tools
66
- registry.addTool({
67
- name: "list_models", label: "List Models",
68
- description: "Search and filter available AI models by name, provider, or tier. Use this to find the best model for a task.",
69
- parameters: modelReg.listModelsParams,
70
- execute: (id, p) => modelReg.listModelsTool(id, p),
71
- });
72
- registry.addTool({
73
- name: "pick_model", label: "Pick Model",
74
- description: "Find the cheapest available model meeting requirements (tier, context length, max cost).",
75
- parameters: modelReg.pickModelParams,
76
- execute: (id, p) => modelReg.pickModelTool(id, p),
77
- });
78
- registry.addTool({
79
- name: "model_summary", label: "Model Summary",
80
- description: "Get a summary of available model tiers and counts.",
81
- parameters: modelReg.summaryParams,
82
- execute: (_id) => modelReg.summaryTool(),
83
- });
84
- // Cost tools
127
+ const mk = modelReg;
128
+ registry.addTool({ name: "list_models", label: "List Models", description: "Search and filter available AI models by name, provider, or tier.", parameters: mk.listModelsParams, execute: (id, p) => mk.listModelsTool(id, p) });
129
+ registry.addTool({ name: "pick_model", label: "Pick Model", description: "Find the cheapest available model meeting requirements.", parameters: mk.pickModelParams, execute: (id, p) => mk.pickModelTool(id, p) });
130
+ registry.addTool({ name: "model_summary", label: "Model Summary", description: "Get a summary of available model tiers and counts.", parameters: mk.summaryParams, execute: () => mk.summaryTool() });
85
131
  if (costTracker) {
86
- registry.addTool({
87
- name: "cost_report", label: "Cost Report",
88
- description: "Show current session and lifetime token usage and estimated cost.",
89
- parameters: costTracker.costReportParams,
90
- execute: (_id) => costTracker.costReportTool(),
91
- });
132
+ registry.addTool({ name: "cost_report", label: "Cost Report", description: "Show session and lifetime token usage.", parameters: costTracker.costReportParams, execute: () => costTracker.costReportTool() });
92
133
  }
93
- // Price tools
94
- registry.addTool({
95
- name: "get_prices", label: "Get Prices",
96
- description: "Get current prices and 24h change for symbols (btc, eth, sol, etc).",
97
- parameters: getPricesParams,
98
- execute: (id, p) => getPricesTool(id, p),
99
- });
100
- registry.addTool({
101
- name: "get_top_movers", label: "Top Movers",
102
- description: "Get assets with the largest 24h price movement.",
103
- parameters: topMoversParams,
104
- execute: (id, p) => topMoversTool(id, p),
105
- });
106
- registry.addTool({
107
- name: "get_market_overview", label: "Market Overview",
108
- description: "Get aggregated market data: total cap, average change, gainers/losers.",
109
- parameters: marketOverviewParams,
110
- execute: (_id) => marketOverviewTool(),
111
- });
112
- // News tools
113
- registry.addTool({
114
- name: "get_news", label: "Get News",
115
- description: "Get latest crypto news headlines with sentiment scoring. Use to gauge market sentiment.",
116
- parameters: getNewsParams,
117
- execute: (id, p) => getNewsTool(id, p),
118
- });
119
- // Technical Analysis tools
120
- registry.addTool({
121
- name: "analyze_ta", label: "Technical Analysis",
122
- description: "Run full technical analysis on price data: RSI, MACD, Bollinger Bands, EMA crossover, ATR, volume profile. Returns a summary with buy/sell signals.",
123
- parameters: analyzeTAParams,
124
- execute: async (_id, p) => {
134
+ registry.addTool({ name: "get_prices", label: "Get Prices", description: "Get current prices and 24h change.", parameters: getPricesParams, execute: (id, p) => getPricesTool(id, p) });
135
+ registry.addTool({ name: "get_top_movers", label: "Top Movers", description: "Assets with largest 24h price movement.", parameters: topMoversParams, execute: (id, p) => topMoversTool(id, p) });
136
+ registry.addTool({ name: "get_market_overview", label: "Market Overview", description: "Aggregated market data.", parameters: marketOverviewParams, execute: () => marketOverviewTool() });
137
+ registry.addTool({ name: "get_news", label: "Get News", description: "Crypto news headlines with sentiment scoring.", parameters: getNewsParams, execute: (id, p) => getNewsTool(id, p) });
138
+ // #18: OHLCV candles from Binance + TA analysis
139
+ registry.addTool({ name: "get_candles", label: "Get Candles", description: "Fetch OHLCV candlestick data from Binance and run technical analysis (RSI, MACD, Bollinger, EMA). Use this before analyze_ta.", parameters: getCandlesParams, execute: (id, p) => getCandlesTool(id, p) });
140
+ // #20: Free market sentiment tools
141
+ registry.addTool({ name: "get_fear_greed", label: "Fear & Greed", description: "Crypto Fear & Greed Index with 7-day history.", parameters: fearGreedParams, execute: (id, p) => getFearGreedTool(id, p) });
142
+ registry.addTool({ name: "get_funding_rates", label: "Funding Rates", description: "Binance perpetual funding rates — shows long/short bias.", parameters: fundingRatesParams, execute: (id, p) => getFundingRatesTool(id, p) });
143
+ registry.addTool({ name: "get_btc_mempool", label: "BTC Mempool", description: "Bitcoin mempool stats and recommended fee rates.", parameters: btcMempoolParams, execute: () => getBtcMempoolTool() });
144
+ registry.addTool({ name: "get_defi_tvl", label: "DeFi TVL", description: "DeFiLlama TVL by chain or all chains.", parameters: defiTvlParams, execute: (id, p) => getDefiTvlTool(id, p) });
145
+ registry.addTool({ name: "get_solana_stats", label: "Solana Stats", description: "Solana network TPS and health.", parameters: solanaStatsParams, execute: () => getSolanaStatsTool() });
146
+ // #31: Ephemeral task context
147
+ registry.addTool({ name: "read_task_context", label: "Read Task Context", description: "Read saved task context from a previous multi-step operation.", parameters: require("@sinclair/typebox").Type.Object({ taskId: require("@sinclair/typebox").Type.String({ description: "Task ID from a previous task reference" }) }), execute: (id, p) => contextStore.readContextTool(id, p) });
148
+ registry.addTool({ name: "list_tasks", label: "List Tasks", description: "List active and recent task context folders.", parameters: require("@sinclair/typebox").Type.Object({}), execute: () => contextStore.listTasksTool() });
149
+ // #12: Goal management
150
+ registry.addTool({ name: "set_goal", label: "Set Goal", description: "Set a persistent cross-session goal for the agent to monitor.", parameters: goalManager.setGoalParams, execute: (id, p) => goalManager.setGoalTool(id, p) });
151
+ registry.addTool({ name: "complete_goal", label: "Complete Goal", description: "Mark a goal as completed.", parameters: goalManager.completeGoalParams, execute: (id, p) => goalManager.completeGoalTool(id, p) });
152
+ registry.addTool({ name: "list_goals", label: "List Goals", description: "List active or all persistent goals.", parameters: goalManager.listGoalsParams, execute: (id, p) => goalManager.listGoalsTool(id, p) });
153
+ registry.addTool({ name: "add_goal_note", label: "Add Goal Note", description: "Add a progress note to an existing goal.", parameters: goalManager.addGoalNoteParams, execute: (id, p) => goalManager.addGoalNoteTool(id, p) });
154
+ registry.addTool({ name: "analyze_ta", label: "Technical Analysis", description: "RSI, MACD, Bollinger, EMA crossover, ATR. Tip: use get_candles first to fetch real price data.", parameters: analyzeTAParams, execute: async (_id, p) => {
125
155
  const closes = p.prices;
126
- const highs = p.highs ?? [];
127
- const lows = p.lows ?? [];
128
- const volumes = p.volumes ?? [];
129
- const candles = closes.map((c, i) => ({
130
- timestamp: 0, open: c, high: highs[i] ?? c, low: lows[i] ?? c, close: c, volume: volumes[i] ?? 0,
131
- }));
156
+ const candles = closes.map((c, i) => ({ timestamp: 0, open: c, high: p.highs?.[i] ?? c, low: p.lows?.[i] ?? c, close: c, volume: p.volumes?.[i] ?? 0 }));
132
157
  const results = fullAnalysis(candles);
133
- const text = results.map(r => {
158
+ const summary = results.find((r) => r.indicator === "SUMMARY");
159
+ const text = results.map((r) => {
134
160
  const s = r.signal === "bullish" ? "🟢" : r.signal === "bearish" ? "🔴" : "⚪";
135
- const v = Array.isArray(r.value) ? `[${r.value.length} values]` : typeof r.value === "number" ? r.value : "-";
161
+ const v = Array.isArray(r.value) ? `[${r.value.length}]` : typeof r.value === "number" ? r.value : "-";
136
162
  return `${s} ${r.indicator}: ${v}`;
137
163
  }).join("\n");
138
- const summary = results.find(r => r.indicator === "SUMMARY");
139
- return {
140
- content: [{ type: "text", text: `Technical Analysis Results:\n${text}` }],
141
- details: { results, overall_signal: summary?.signal, overall_score: summary?.value },
142
- };
143
- },
144
- });
164
+ return { content: [{ type: "text", text: `Technical Analysis:\n${text}` }], details: { results, overall_signal: summary?.signal, overall_score: summary?.value } };
165
+ } });
145
166
  }
146
167
  useEffect(() => { onNotifyReady?.(notify); onStatusReady?.(setStatus); }, []);
147
- // ── Boot: start feeds, register tools, fire session_start ────────────────
148
168
  useEffect(() => {
149
169
  registerBuiltinTools();
150
- // Start background feeds
151
170
  priceFeed.track("btc", "eth", "sol", "bnb", "matic", "arb", "op", "avax", "link", "uni", "doge", "xrp", "ada", "dot", "atom", "near", "sui", "apt", "pepe", "aave");
152
171
  priceFeed.start();
153
172
  newsFeed.start();
154
- // Ticker update every 5 seconds from cached prices
173
+ // #11: Register scheduler tools
174
+ registry.addTool({ name: "schedule_task", label: "Schedule Task", description: "Schedule a recurring or one-shot agent task (cron or price trigger).", parameters: agentScheduler.addTaskParams, execute: (id, p) => agentScheduler.addTaskTool(id, p) });
175
+ registry.addTool({ name: "list_schedule", label: "List Schedule", description: "List all scheduled tasks.", parameters: agentScheduler.listTasksParams, execute: (id, p) => agentScheduler.listTasksTool(id, p) });
176
+ registry.addTool({ name: "remove_schedule", label: "Remove Schedule", description: "Remove a scheduled task by ID.", parameters: agentScheduler.removeTaskParams, execute: (id, p) => agentScheduler.removeTaskTool(id, p) });
177
+ // Start scheduler — fires agent turns autonomously
178
+ agentScheduler.start((task) => {
179
+ push({ role: "system", content: T.muted(`⏰ Scheduler: running "${task.name}"`) });
180
+ // Only fire if agent is not currently busy
181
+ if (!disabled && runnerRef.current) {
182
+ setDisabled(true);
183
+ setStreaming("");
184
+ runnerRef.current.run(`[SCHEDULED TASK: ${task.name}] ${task.prompt}`)
185
+ .catch((e) => notify(T.error(`Scheduler error: ${e.message}`)));
186
+ }
187
+ });
155
188
  const tickerInterval = setInterval(() => {
156
- setTicker(priceFeed.tickerLine(5));
189
+ setTicker(priceFeed.tickerLine(6));
157
190
  if (costTracker)
158
191
  setCostBadge(costTracker.statusLine());
159
192
  setNewsBadge(newsFeed.statusBadge());
160
193
  }, 5_000);
161
- // Save cost on exit
162
- const saveInterval = setInterval(() => {
163
- costTracker?.saveLifetime();
164
- }, 60_000);
165
- // Agent session
194
+ const saveInterval = setInterval(() => { costTracker?.saveLifetime(); }, 60_000);
166
195
  const session = new SessionManager();
167
- session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt ||
168
- "You are JellyOS, an autonomous AI trading agent.");
196
+ session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt || "You are JellyOS, an autonomous AI trading agent.");
169
197
  sessionRef.current = session;
170
- const sessionCtx = {
171
- ui: uiCtx, hasUI: true,
172
- config: { OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, ALCHEMY_KEY: process.env.ALCHEMY_KEY, DEFAULT_MODEL: process.env.DEFAULT_MODEL },
173
- };
198
+ const sessionCtx = { ui: uiCtx, hasUI: true, config: { OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY, ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY, ALCHEMY_KEY: process.env.ALCHEMY_KEY, DEFAULT_MODEL: process.env.DEFAULT_MODEL } };
174
199
  sessionCtxRef.current = sessionCtx;
175
200
  registry.fireHook("session_start", sessionCtx).then(() => {
176
- push({ role: "system", content: T.muted("Session started. Type a message or /help.") });
201
+ // #7: Inject memory context into system prompt
202
+ if (memoryStore.isAvailable) {
203
+ const memCtx = memoryStore.buildContextBlock(sessionId);
204
+ if (memCtx) {
205
+ session.setSystemPrompt((session.getSystemPrompt() || "") + memCtx);
206
+ }
207
+ }
208
+ // #12: Inject active goals into system prompt
209
+ const goalCtx = goalManager.buildContextBlock();
210
+ if (goalCtx) {
211
+ session.setSystemPrompt((session.getSystemPrompt() || "") + goalCtx);
212
+ }
213
+ push({ role: "system", content: T.muted("Session started. Type /help for commands.") });
177
214
  });
178
215
  const runner = new AgentRunner(registry, session, (event) => {
179
216
  if (event.type === "text_delta")
@@ -182,13 +219,20 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
182
219
  setToolRunning(event.name);
183
220
  else if (event.type === "tool_done") {
184
221
  setToolRunning(null);
185
- push({ role: "tool", content: event.result, toolName: event.name, isError: event.isError });
222
+ // Format tool output with syntax highlighting
223
+ const formatted = formatToolContent(event.result);
224
+ push({ role: "tool", content: formatted, toolName: event.name, isError: event.isError });
186
225
  }
187
226
  else if (event.type === "turn_done") {
188
227
  setDisabled(false);
189
228
  setToolRunning(null);
190
- setStreaming(prev => { if (prev.trim())
191
- push({ role: "assistant", content: prev }); return ""; });
229
+ setStreaming(prev => {
230
+ if (prev.trim()) {
231
+ push({ role: "assistant", content: prev });
232
+ memoryStore.save(sessionId, "assistant", prev); // #7 persist to memory
233
+ }
234
+ return "";
235
+ });
192
236
  }
193
237
  else if (event.type === "error") {
194
238
  setDisabled(false);
@@ -196,10 +240,27 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
196
240
  setStreaming("");
197
241
  notify(T.error(`Error: ${event.message}`));
198
242
  }
199
- }, sessionCtx, effectLevel, modelReg, costTracker);
243
+ else if (event.type === "approval_request") {
244
+ // #10: Show approval prompt — user must type y or n
245
+ setToolRunning(null);
246
+ const { toolName, args, approve } = event;
247
+ let parsed = "";
248
+ try {
249
+ parsed = JSON.stringify(JSON.parse(args), null, 2).slice(0, 200);
250
+ }
251
+ catch {
252
+ parsed = args.slice(0, 200);
253
+ }
254
+ push({
255
+ role: "notify",
256
+ content: `⚠️ APPROVAL REQUIRED\n\nTool: ${toolName}\nArgs: ${parsed}\n\nType /approve or /deny to continue.`,
257
+ });
258
+ // Store the approve callback so /approve /deny commands can resolve it
259
+ runnerRef.current._pendingApproval = approve;
260
+ }
261
+ }, sessionCtx, effectLevel, modelReg, costTracker, goalManager, contextStore);
200
262
  runnerRef.current = runner;
201
- // Initial ticker
202
- setTicker(priceFeed.tickerLine(5));
263
+ setTicker(priceFeed.tickerLine(6));
203
264
  if (costTracker)
204
265
  setCostBadge(costTracker.statusLine());
205
266
  setNewsBadge(newsFeed.statusBadge());
@@ -208,13 +269,14 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
208
269
  registry.fireHook("session_end", sessionCtxRef.current).catch(() => { });
209
270
  priceFeed.stop();
210
271
  newsFeed.stop();
272
+ agentScheduler.stop();
211
273
  clearInterval(tickerInterval);
212
274
  clearInterval(saveInterval);
213
275
  costTracker?.saveLifetime();
214
276
  };
215
277
  }, []);
216
278
  useEffect(() => { runnerRef.current?.setEffectLevel(effectLevel); }, [effectLevel]);
217
- // ── Ctrl-C ────────────────────────────────────────────────────────────────
279
+ // ── Ctrl-C to exit ──────────────────────────────────────────────────────
218
280
  useInput((_input, key) => {
219
281
  if (key.ctrl && _input === "c") {
220
282
  push({ role: "system", content: T.muted("Goodbye 🪼") });
@@ -227,7 +289,6 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
227
289
  const input = raw.trim();
228
290
  if (!input)
229
291
  return;
230
- // ── Slash commands ────────────────────────────────────────────────────
231
292
  if (input.startsWith("/")) {
232
293
  const [cmd, ...rest] = input.slice(1).split(" ");
233
294
  const args = rest.join(" ");
@@ -239,21 +300,18 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
239
300
  }
240
301
  if (cmd === "help") {
241
302
  const lines = registry.listCommands().map(([n, d]) => T.accent(`/${n}`.padEnd(16)) + " " + d.description);
242
- notify("Available commands:\n\n" + lines.join("\n") + "\n\nBuilt-in tools: list_models, get_prices, get_news, analyze_ta, cost_report, get_market_overview, get_top_movers, model_summary");
303
+ notify("Commands:\n" + lines.join("\n") + "\n\nTools: list_models, pick_model, get_prices, get_news, analyze_ta, cost_report, get_market_overview, get_top_movers, model_summary");
243
304
  return;
244
305
  }
245
- // Built-in /models command
246
306
  if (cmd === "models") {
247
307
  if (!modelReg) {
248
308
  notify(T.error("Model registry not available"));
249
309
  return;
250
310
  }
251
- const query = rest.join(" ") || undefined;
252
- const result = await modelReg.listModelsTool("", { query: query, limit: 20, available_only: true });
311
+ const result = await modelReg.listModelsTool("", { query: rest.join(" ") || undefined, limit: 20, available_only: true });
253
312
  notify(result.content[0].text);
254
313
  return;
255
314
  }
256
- // Built-in /cost command
257
315
  if (cmd === "cost") {
258
316
  if (!costTracker) {
259
317
  notify(T.error("Cost tracker not available"));
@@ -263,28 +321,120 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
263
321
  notify(result.content[0].text);
264
322
  return;
265
323
  }
266
- // Built-in /news command
267
324
  if (cmd === "news") {
268
325
  const result = await getNewsTool("", { limit: 15 });
269
326
  notify(result.content[0].text);
270
327
  return;
271
328
  }
272
- // Built-in /prices command
273
329
  if (cmd === "prices") {
274
330
  const result = await getPricesTool("", { symbols: args ? args.split(/\s+/) : ["btc", "eth", "sol"] });
275
331
  notify(result.content[0].text);
276
332
  return;
277
333
  }
334
+ if (cmd === "palette") {
335
+ // Command palette — list all commands and tools
336
+ const lines = registry.listCommands().map(([n, d]) => T.accent(`/${n}`.padEnd(16)) + " " + d.description);
337
+ const tools = registry.listTools().map(t => T.success(`🔧 ${t.name.padEnd(22)}`) + " " + T.muted(t.description.slice(0, 50)));
338
+ notify("Command Palette\n\n" + lines.join("\n") + "\n\nTools:\n" + tools.join("\n"));
339
+ return;
340
+ }
341
+ if (cmd === "effect" && ["eco", "normal", "turbo", "max"].includes(args.trim().toLowerCase())) {
342
+ setEffectLevel(args.trim().toLowerCase());
343
+ notify(T.accent(`Effect → ${args.trim().toUpperCase()}`));
344
+ return;
345
+ }
346
+ // #12: Goal commands
347
+ if (cmd === "goals" || cmd === "goal") {
348
+ const subCmd = args.trim().split(" ")[0];
349
+ if (subCmd === "add") {
350
+ const text = args.trim().slice(4).trim();
351
+ if (!text) {
352
+ notify(T.error("Usage: /goal add <text>"));
353
+ return;
354
+ }
355
+ const result = await goalManager.setGoalTool("", { text });
356
+ notify(result.content[0].text);
357
+ }
358
+ else if (subCmd === "done" || subCmd === "complete") {
359
+ const id = args.trim().split(" ")[1];
360
+ if (!id) {
361
+ notify(T.error("Usage: /goal done <id>"));
362
+ return;
363
+ }
364
+ const result = await goalManager.completeGoalTool("", { id });
365
+ notify(result.content[0].text);
366
+ }
367
+ else {
368
+ const result = await goalManager.listGoalsTool("", { status: "all" });
369
+ notify(result.content[0].text);
370
+ }
371
+ return;
372
+ }
373
+ // #31: Task context commands
374
+ if (cmd === "tasks") {
375
+ const result = await contextStore.listTasksTool();
376
+ notify(result.content[0].text);
377
+ return;
378
+ }
379
+ if (cmd === "keep" && args.trim()) {
380
+ const ok = contextStore.keepTask(args.trim());
381
+ notify(ok ? T.success(`Task ${args.trim()} marked for permanent retention.`) : T.error(`Task ${args.trim()} not found.`));
382
+ return;
383
+ }
384
+ // #10: Approval gate responses
385
+ if (cmd === "approve" || cmd === "y") {
386
+ const pending = runnerRef.current?._pendingApproval;
387
+ if (pending) {
388
+ pending(true);
389
+ push({ role: "system", content: T.success("✅ Tool approved.") });
390
+ }
391
+ else
392
+ notify(T.error("No pending approval."));
393
+ return;
394
+ }
395
+ if (cmd === "deny" || cmd === "n") {
396
+ const pending = runnerRef.current?._pendingApproval;
397
+ if (pending) {
398
+ pending(false);
399
+ push({ role: "system", content: T.error("❌ Tool denied.") });
400
+ }
401
+ else
402
+ notify(T.error("No pending approval."));
403
+ return;
404
+ }
405
+ // #30: Traces command
406
+ if (cmd === "traces") {
407
+ const recent = Tracer.readRecent(5);
408
+ notify(Tracer.formatSummary(recent));
409
+ return;
410
+ }
411
+ // #11: Scheduler commands
412
+ if (cmd === "schedule" || cmd === "sched") {
413
+ const result = await agentScheduler.listTasksTool("", { enabled_only: false });
414
+ notify(result.content[0].text);
415
+ return;
416
+ }
417
+ // #7: Memory search
418
+ if (cmd === "memory" && args.trim()) {
419
+ const results = memoryStore.search(args.trim(), 8);
420
+ if (results.length === 0) {
421
+ notify(T.muted("No memories found for: " + args.trim()));
422
+ return;
423
+ }
424
+ const lines = results.map(r => `[${new Date(r.ts).toLocaleDateString()} ${r.role}] ${r.content.slice(0, 120)}`);
425
+ notify(`Memory search: "${args.trim()}"\n${lines.join("\n")}`);
426
+ return;
427
+ }
278
428
  const def = registry.getCommand(cmd);
279
429
  if (!def) {
280
- notify(T.error(`Unknown command: /${cmd}\nType /help to list all commands.`));
430
+ notify(T.error(`Unknown: /${cmd} — try /help or /palette`));
281
431
  return;
282
432
  }
283
433
  try {
284
434
  await def.handler(args, { ui: uiCtx });
285
435
  }
286
436
  catch (e) {
287
- notify(T.error(`Command error: ${e.message}`));
437
+ notify(T.error(`Error: ${e.message}`));
288
438
  }
289
439
  return;
290
440
  }
@@ -293,6 +443,7 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
293
443
  return;
294
444
  }
295
445
  push({ role: "user", content: input });
446
+ memoryStore.save(sessionId, "user", input); // #7 persist user message
296
447
  setDisabled(true);
297
448
  setStreaming("");
298
449
  try {
@@ -300,11 +451,23 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
300
451
  }
301
452
  catch (e) {
302
453
  setDisabled(false);
303
- notify(T.error(`Runner error: ${e.message}`));
454
+ notify(T.error(`Error: ${e.message}`));
304
455
  }
305
456
  }, [registry, exit, push, notify, uiCtx, modelReg, costTracker]);
306
- // Build status line: ticker + cost + badges
457
+ const ctx = getContextBar(sessionRef.current);
307
458
  const statusLine = [ticker, costBadge, newsBadge, ...Object.values(statusBadges)].filter(Boolean).join(" ") || null;
308
- return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(StatusBar, { model: modelName, chain: chain, vaultLocked: vaultLocked, effectLevel: effectLevel, toolRunning: toolRunning, connected: true, statusLine: statusLine }), _jsx(REPL, { messages: messages, streamingText: streaming, toolRunning: toolRunning, onSubmit: handleSubmit, disabled: disabled })] }));
459
+ // ── Multi-pane layout ────────────────────────────────────────────────────
460
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(StatusBar, { model: modelName, chain: chain, vaultLocked: vaultLocked, effectLevel: effectLevel, toolRunning: toolRunning, connected: true, statusLine: statusLine }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: 32, borderStyle: "single", borderColor: JELLY_COLORS.dim, paddingX: 1, flexShrink: 0, children: [_jsx(Text, { color: JELLY_COLORS.accent, bold: true, children: "\uD83D\uDCE1 Ticker" }), _jsx(Text, { color: JELLY_COLORS.muted, wrap: "truncate", children: ticker || "Loading…" }), _jsx(Text, { color: JELLY_COLORS.dim, children: "─".repeat(28) }), _jsxs(Text, { color: JELLY_COLORS.accent, children: ["Context ", ctx.turboReady ? "" : "⚠"] }), _jsxs(Text, { color: ctx.color, children: [ctx.bar, " ", ctx.pct, "%", ctx.turboReady ? "" : " no turbo"] }), _jsx(Text, { color: JELLY_COLORS.dim, children: "─".repeat(28) }), _jsx(Text, { color: JELLY_COLORS.accent, children: "Effect" }), _jsx(Text, { color: effectLevel === "eco" ? "#22c55e" : effectLevel === "turbo" ? "#f59e0b" : effectLevel === "max" ? "#ef4444" : JELLY_COLORS.accent, children: effectLevel.toUpperCase() }), _jsx(Text, { color: JELLY_COLORS.dim, children: "─".repeat(28) }), _jsx(Text, { color: JELLY_COLORS.accent, children: "News" }), _jsx(Text, { color: JELLY_COLORS.muted, wrap: "truncate", children: newsBadge || "…" })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(REPL, { messages: messages, streamingText: streaming, toolRunning: toolRunning, onSubmit: handleSubmit, disabled: disabled, onAbort: () => {
461
+ // #25: Escape aborts in-flight stream
462
+ runnerRef.current?.abort();
463
+ setDisabled(false);
464
+ setToolRunning(null);
465
+ setStreaming(prev => {
466
+ if (prev.trim())
467
+ push({ role: "assistant", content: prev + " \u2014 [interrupted]" });
468
+ return "";
469
+ });
470
+ push({ role: "system", content: T.dim("— stream interrupted —") });
471
+ } }) })] })] }));
309
472
  }
310
473
  //# sourceMappingURL=App.js.map
@@ -16,7 +16,8 @@ export interface REPLProps {
16
16
  streamingText: string;
17
17
  toolRunning: string | null;
18
18
  onSubmit(input: string): void;
19
+ onAbort?(): void;
19
20
  disabled: boolean;
20
21
  }
21
- export declare function REPL({ messages, streamingText, toolRunning, onSubmit, disabled }: REPLProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function REPL({ messages, streamingText, toolRunning, onSubmit, onAbort, disabled }: REPLProps): import("react/jsx-runtime").JSX.Element;
22
23
  //# sourceMappingURL=REPL.d.ts.map
package/dist/tui/REPL.js CHANGED
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * Renders assistant text, tool calls, tool results, and user messages.
5
5
  */
6
6
  import { useState, useCallback } from "react";
7
- import { Box, Text, useStdout } from "ink";
7
+ import { Box, Text, useStdout, useInput } from "ink";
8
8
  import TextInput from "ink-text-input";
9
9
  import { JELLY_COLORS } from "./theme.js";
10
10
  const MAX_VISIBLE = 40;
@@ -19,9 +19,8 @@ function MessageLine({ msg }) {
19
19
  if (msg.role === "tool") {
20
20
  const icon = msg.isError ? "✗" : "✓";
21
21
  const col = msg.isError ? JELLY_COLORS.error : JELLY_COLORS.success;
22
- return (_jsxs(Box, { flexDirection: "row", gap: 1, marginY: 0, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: time }), _jsxs(Text, { color: col, children: [icon, " ", msg.toolName ?? "tool"] }), msg.content.length < 120
23
- ? _jsxs(Text, { color: JELLY_COLORS.muted, wrap: "wrap", children: [" ", msg.content] })
24
- : _jsxs(Text, { color: JELLY_COLORS.muted, children: [" (", msg.content.length, " chars)"] })] }));
22
+ const isLong = msg.content.length > 120;
23
+ return (_jsxs(Box, { flexDirection: "column", marginY: 0, children: [_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: time }), _jsxs(Text, { color: col, children: [icon, " ", msg.toolName ?? "tool"] }), isLong && _jsxs(Text, { color: JELLY_COLORS.dim, children: [" (", msg.content.length, " chars)"] })] }), !isLong && (_jsx(Box, { marginLeft: 6, children: _jsx(Text, { color: JELLY_COLORS.muted, wrap: "wrap", children: msg.content }) }))] }));
25
24
  }
26
25
  if (msg.role === "notify") {
27
26
  return (_jsx(Box, { borderStyle: "round", borderColor: JELLY_COLORS.accent, marginY: 1, paddingX: 1, children: _jsx(Text, { wrap: "wrap", children: msg.content }) }));
@@ -29,7 +28,7 @@ function MessageLine({ msg }) {
29
28
  // system messages — dimmed
30
29
  return (_jsx(Box, { flexDirection: "row", gap: 1, children: _jsx(Text, { color: JELLY_COLORS.dim, wrap: "wrap", children: msg.content }) }));
31
30
  }
32
- export function REPL({ messages, streamingText, toolRunning, onSubmit, disabled }) {
31
+ export function REPL({ messages, streamingText, toolRunning, onSubmit, onAbort, disabled }) {
33
32
  const [input, setInput] = useState("");
34
33
  const { stdout } = useStdout();
35
34
  const termWidth = stdout?.columns ?? 80;
@@ -40,7 +39,13 @@ export function REPL({ messages, streamingText, toolRunning, onSubmit, disabled
40
39
  setInput("");
41
40
  onSubmit(trimmed);
42
41
  }, [onSubmit, disabled]);
42
+ // #25: Escape key aborts in-flight stream
43
+ useInput((_input, key) => {
44
+ if (key.escape && disabled && onAbort) {
45
+ onAbort();
46
+ }
47
+ });
43
48
  const visible = messages.slice(-MAX_VISIBLE);
44
- return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [visible.map(m => _jsx(MessageLine, { msg: m }, m.id)), streamingText && (_jsxs(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: " " }), _jsx(Text, { color: JELLY_COLORS.header, bold: true, children: "\uD83E\uDEBC " }), _jsx(Text, { wrap: "wrap", children: streamingText })] })), toolRunning && (_jsx(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: _jsxs(Text, { color: JELLY_COLORS.warn, children: ["\u2699 running ", toolRunning, "\u2026"] }) }))] }), _jsxs(Box, { borderStyle: "round", borderColor: disabled ? JELLY_COLORS.dim : JELLY_COLORS.accent, paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.accent, children: "\u203A " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: disabled ? "thinking…" : "message or /command" })] }), _jsx(Box, { paddingX: 2, children: _jsx(Text, { color: JELLY_COLORS.dim, children: "/help \u00B7 /status \u00B7 /vault \u00B7 /wallets \u00B7 /panic \u00B7 Tab to complete \u00B7 Ctrl-C to exit" }) })] }));
49
+ return (_jsxs(Box, { flexDirection: "column", width: termWidth, children: [_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [visible.map(m => _jsx(MessageLine, { msg: m }, m.id)), streamingText && (_jsxs(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: " " }), _jsx(Text, { color: JELLY_COLORS.header, bold: true, children: "\uD83E\uDEBC " }), _jsx(Text, { wrap: "wrap", children: streamingText })] })), toolRunning && (_jsx(Box, { flexDirection: "row", gap: 1, marginTop: 1, children: _jsxs(Text, { color: JELLY_COLORS.warn, children: ["\u2699 running ", toolRunning, "\u2026"] }) }))] }), _jsxs(Box, { borderStyle: "round", borderColor: disabled ? JELLY_COLORS.dim : JELLY_COLORS.accent, paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: JELLY_COLORS.accent, children: "\u203A " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: disabled ? "thinking…" : "message or /command" })] }), _jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: JELLY_COLORS.dim, children: ["/help \u00B7 /palette \u00B7 /cost \u00B7 /prices \u00B7 /news \u00B7 /goals \u00B7 /tasks \u00B7 ", disabled ? "Esc=abort · " : "", "Ctrl-C to exit"] }) })] }));
45
50
  }
46
51
  //# sourceMappingURL=REPL.js.map