@jellyos/agent 0.1.3 → 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 (90) hide show
  1. package/README.md +9 -9
  2. package/README.npm.md +212 -0
  3. package/bin/jellyos-mcp +26 -0
  4. package/dist/api/ExtensionAPI.d.ts +6 -0
  5. package/dist/api/Registry.js +3 -1
  6. package/dist/cli.js +117 -42
  7. package/dist/index.d.ts +24 -1
  8. package/dist/index.js +19 -2
  9. package/dist/mcp/entry.d.ts +2 -0
  10. package/dist/mcp/entry.js +71 -0
  11. package/dist/mcp/server.d.ts +31 -0
  12. package/dist/mcp/server.js +128 -0
  13. package/dist/models/CostTracker.d.ts +66 -0
  14. package/dist/models/CostTracker.js +148 -0
  15. package/dist/models/ModelRegistry.d.ts +157 -0
  16. package/dist/models/ModelRegistry.js +496 -0
  17. package/dist/models/index.d.ts +5 -0
  18. package/dist/models/index.js +3 -0
  19. package/dist/runner/AgentRunner.d.ts +23 -2
  20. package/dist/runner/AgentRunner.js +264 -24
  21. package/dist/runner/ModelClient.d.ts +26 -6
  22. package/dist/runner/ModelClient.js +147 -28
  23. package/dist/runner/SwarmRouter.d.ts +10 -7
  24. package/dist/runner/SwarmRouter.js +85 -28
  25. package/dist/runner/ToolDispatcher.d.ts +10 -0
  26. package/dist/runner/ToolDispatcher.js +106 -2
  27. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  28. package/dist/scheduler/AgentScheduler.js +253 -0
  29. package/dist/session/ContextStore.d.ts +96 -0
  30. package/dist/session/ContextStore.js +207 -0
  31. package/dist/session/GoalManager.d.ts +101 -0
  32. package/dist/session/GoalManager.js +167 -0
  33. package/dist/session/MemoryStore.d.ts +48 -0
  34. package/dist/session/MemoryStore.js +166 -0
  35. package/dist/session/SessionManager.d.ts +45 -4
  36. package/dist/session/SessionManager.js +151 -8
  37. package/dist/telemetry/Tracer.d.ts +48 -0
  38. package/dist/telemetry/Tracer.js +102 -0
  39. package/dist/tests/ContextStore.test.d.ts +2 -0
  40. package/dist/tests/ContextStore.test.js +74 -0
  41. package/dist/tests/ModelRegistry.test.d.ts +2 -0
  42. package/dist/tests/ModelRegistry.test.js +69 -0
  43. package/dist/tests/SessionManager.test.d.ts +2 -0
  44. package/dist/tests/SessionManager.test.js +108 -0
  45. package/dist/tests/TechnicalAnalysis.test.d.ts +2 -0
  46. package/dist/tests/TechnicalAnalysis.test.js +109 -0
  47. package/dist/tools/MarketSentiment.d.ts +166 -0
  48. package/dist/tools/MarketSentiment.js +209 -0
  49. package/dist/tools/NewsSentiment.d.ts +67 -0
  50. package/dist/tools/NewsSentiment.js +226 -0
  51. package/dist/tools/PriceFeed.d.ts +105 -0
  52. package/dist/tools/PriceFeed.js +282 -0
  53. package/dist/tools/TechnicalAnalysis.d.ts +110 -0
  54. package/dist/tools/TechnicalAnalysis.js +357 -0
  55. package/dist/tools/index.d.ts +7 -0
  56. package/dist/tools/index.js +4 -0
  57. package/dist/tui/App.d.ts +7 -5
  58. package/dist/tui/App.js +350 -65
  59. package/dist/tui/REPL.d.ts +2 -1
  60. package/dist/tui/REPL.js +11 -6
  61. package/dist/tui/StatusBar.js +1 -1
  62. package/package.json +9 -4
  63. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  64. package/dist/api/ExtensionAPI.js.map +0 -1
  65. package/dist/api/Registry.d.ts.map +0 -1
  66. package/dist/api/Registry.js.map +0 -1
  67. package/dist/cli.d.ts.map +0 -1
  68. package/dist/cli.js.map +0 -1
  69. package/dist/index.d.ts.map +0 -1
  70. package/dist/index.js.map +0 -1
  71. package/dist/loader.d.ts.map +0 -1
  72. package/dist/loader.js.map +0 -1
  73. package/dist/runner/AgentRunner.d.ts.map +0 -1
  74. package/dist/runner/AgentRunner.js.map +0 -1
  75. package/dist/runner/ModelClient.d.ts.map +0 -1
  76. package/dist/runner/ModelClient.js.map +0 -1
  77. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  78. package/dist/runner/SwarmRouter.js.map +0 -1
  79. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  80. package/dist/runner/ToolDispatcher.js.map +0 -1
  81. package/dist/session/SessionManager.d.ts.map +0 -1
  82. package/dist/session/SessionManager.js.map +0 -1
  83. package/dist/tui/App.d.ts.map +0 -1
  84. package/dist/tui/App.js.map +0 -1
  85. package/dist/tui/REPL.d.ts.map +0 -1
  86. package/dist/tui/REPL.js.map +0 -1
  87. package/dist/tui/StatusBar.d.ts.map +0 -1
  88. package/dist/tui/StatusBar.js.map +0 -1
  89. package/dist/tui/theme.d.ts.map +0 -1
  90. package/dist/tui/theme.js.map +0 -1
package/dist/tui/App.js CHANGED
@@ -1,20 +1,88 @@
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. Passes live setStatus/notify callbacks
5
- * to the extension API so Pi compat calls (ui.setStatus, ui.notify) update the TUI.
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
+ import { priceFeed, getPricesTool, topMoversTool, marketOverviewTool, getPricesParams, topMoversParams, marketOverviewParams } from "../tools/PriceFeed.js";
16
+ import { getNewsTool, getNewsParams } from "../tools/NewsSentiment.js";
17
+ import { analyzeTAParams, getCandlesParams, getCandlesTool } from "../tools/TechnicalAnalysis.js";
18
+ import { fullAnalysis } from "../tools/TechnicalAnalysis.js";
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
+ }
15
81
  let _msgIdCounter = 0;
16
82
  function nextId() { return String(++_msgIdCounter); }
17
- export function App({ registry, systemPrompt, effectLevel: initialEffect = "normal", chain: initialChain = "ethereum", onNotifyReady, onStatusReady, }) {
83
+ // Unique session ID for memory persistence
84
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
85
+ export function App({ registry, systemPrompt, effectLevel: initialEffect = "normal", chain: initialChain = "ethereum", modelReg, costTracker, onNotifyReady, onStatusReady, }) {
18
86
  const { exit } = useApp();
19
87
  const [messages, setMessages] = useState([]);
20
88
  const [streaming, setStreaming] = useState("");
@@ -24,16 +92,18 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
24
92
  const [effectLevel, setEffectLevel] = useState(initialEffect);
25
93
  const [chain, setChain] = useState(initialChain);
26
94
  const [statusBadges, setStatusBadges] = useState({});
95
+ const [ticker, setTicker] = useState("");
96
+ const [costBadge, setCostBadge] = useState("");
97
+ const [newsBadge, setNewsBadge] = useState("");
27
98
  const runnerRef = useRef(null);
28
99
  const sessionRef = useRef(null);
29
100
  const sessionCtxRef = useRef(null);
30
101
  const theme = makeTheme();
31
102
  let modelName = "no-model";
32
103
  try {
33
- modelName = resolveModelConfig().model;
104
+ modelName = resolveModelConfig(modelReg).model;
34
105
  }
35
106
  catch { /* shown via banner */ }
36
- // ── Push helpers ─────────────────────────────────────────────────────────
37
107
  const push = useCallback((msg) => {
38
108
  setMessages(prev => [...prev, { ...msg, id: nextId(), ts: Date.now() }]);
39
109
  }, []);
@@ -42,70 +112,125 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
42
112
  }, [push]);
43
113
  const setStatus = useCallback((key, value) => {
44
114
  setStatusBadges(prev => ({ ...prev, [key]: value }));
45
- // Mirror vault lock state when extension calls setStatus("vault", ...)
46
- if (key === "vault") {
115
+ if (key === "vault")
47
116
  setVaultLocked(!value.includes("🔓") && !value.includes("unlocked"));
48
- }
49
117
  if (key === "chain" || key === "active_chain")
50
118
  setChain(value);
51
119
  if (key === "effect_level")
52
120
  setEffectLevel(value);
53
121
  }, []);
54
- // ── Build UIContext that extension commands can call ──────────────────────
55
- const uiCtx = {
56
- notify,
57
- setStatus,
58
- setTheme(_name) { },
59
- setHeader(_factory) { },
60
- theme,
61
- };
62
- // ── Wire live callbacks BEFORE session_start fires ────────────────────────
63
- useEffect(() => {
64
- onNotifyReady?.(notify);
65
- onStatusReady?.(setStatus);
66
- }, [notify, setStatus, onNotifyReady, onStatusReady]);
67
- // ── Boot: fire session_start ──────────────────────────────────────────────
122
+ const uiCtx = { notify, setStatus, setTheme(_n) { }, setHeader(_f) { }, theme };
123
+ // Register built-in tools
124
+ function registerBuiltinTools() {
125
+ if (!modelReg)
126
+ return;
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() });
131
+ if (costTracker) {
132
+ registry.addTool({ name: "cost_report", label: "Cost Report", description: "Show session and lifetime token usage.", parameters: costTracker.costReportParams, execute: () => costTracker.costReportTool() });
133
+ }
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) => {
155
+ const closes = p.prices;
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 }));
157
+ const results = fullAnalysis(candles);
158
+ const summary = results.find((r) => r.indicator === "SUMMARY");
159
+ const text = results.map((r) => {
160
+ const s = r.signal === "bullish" ? "🟢" : r.signal === "bearish" ? "🔴" : "⚪";
161
+ const v = Array.isArray(r.value) ? `[${r.value.length}]` : typeof r.value === "number" ? r.value : "-";
162
+ return `${s} ${r.indicator}: ${v}`;
163
+ }).join("\n");
164
+ return { content: [{ type: "text", text: `Technical Analysis:\n${text}` }], details: { results, overall_signal: summary?.signal, overall_score: summary?.value } };
165
+ } });
166
+ }
167
+ useEffect(() => { onNotifyReady?.(notify); onStatusReady?.(setStatus); }, []);
68
168
  useEffect(() => {
169
+ registerBuiltinTools();
170
+ priceFeed.track("btc", "eth", "sol", "bnb", "matic", "arb", "op", "avax", "link", "uni", "doge", "xrp", "ada", "dot", "atom", "near", "sui", "apt", "pepe", "aave");
171
+ priceFeed.start();
172
+ newsFeed.start();
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
+ });
188
+ const tickerInterval = setInterval(() => {
189
+ setTicker(priceFeed.tickerLine(6));
190
+ if (costTracker)
191
+ setCostBadge(costTracker.statusLine());
192
+ setNewsBadge(newsFeed.statusBadge());
193
+ }, 5_000);
194
+ const saveInterval = setInterval(() => { costTracker?.saveLifetime(); }, 60_000);
69
195
  const session = new SessionManager();
70
- session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt ||
71
- "You are JellyOS, an autonomous AI trading agent.");
196
+ session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt || "You are JellyOS, an autonomous AI trading agent.");
72
197
  sessionRef.current = session;
73
- const sessionCtx = {
74
- ui: uiCtx,
75
- hasUI: true,
76
- config: {
77
- OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
78
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
79
- ALCHEMY_KEY: process.env.ALCHEMY_KEY,
80
- DEFAULT_MODEL: process.env.DEFAULT_MODEL,
81
- },
82
- };
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 } };
83
199
  sessionCtxRef.current = sessionCtx;
84
200
  registry.fireHook("session_start", sessionCtx).then(() => {
85
- 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.") });
86
214
  });
87
215
  const runner = new AgentRunner(registry, session, (event) => {
88
- if (event.type === "text_delta") {
216
+ if (event.type === "text_delta")
89
217
  setStreaming(prev => prev + event.text);
90
- }
91
- else if (event.type === "tool_start") {
218
+ else if (event.type === "tool_start")
92
219
  setToolRunning(event.name);
93
- }
94
220
  else if (event.type === "tool_done") {
95
221
  setToolRunning(null);
96
- push({
97
- role: "tool",
98
- content: event.result,
99
- toolName: event.name,
100
- isError: event.isError,
101
- });
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 });
102
225
  }
103
226
  else if (event.type === "turn_done") {
104
227
  setDisabled(false);
105
228
  setToolRunning(null);
106
229
  setStreaming(prev => {
107
- if (prev.trim())
230
+ if (prev.trim()) {
108
231
  push({ role: "assistant", content: prev });
232
+ memoryStore.save(sessionId, "assistant", prev); // #7 persist to memory
233
+ }
109
234
  return "";
110
235
  });
111
236
  }
@@ -115,23 +240,47 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
115
240
  setStreaming("");
116
241
  notify(T.error(`Error: ${event.message}`));
117
242
  }
118
- }, sessionCtx);
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);
119
262
  runnerRef.current = runner;
263
+ setTicker(priceFeed.tickerLine(6));
264
+ if (costTracker)
265
+ setCostBadge(costTracker.statusLine());
266
+ setNewsBadge(newsFeed.statusBadge());
120
267
  return () => {
121
- if (sessionCtxRef.current) {
268
+ if (sessionCtxRef.current)
122
269
  registry.fireHook("session_end", sessionCtxRef.current).catch(() => { });
123
- }
270
+ priceFeed.stop();
271
+ newsFeed.stop();
272
+ agentScheduler.stop();
273
+ clearInterval(tickerInterval);
274
+ clearInterval(saveInterval);
275
+ costTracker?.saveLifetime();
124
276
  };
125
- // eslint-disable-next-line react-hooks/exhaustive-deps
126
277
  }, []);
127
- // ── Live /effect runner reconfiguration ────────────────────────────────
128
- useEffect(() => {
129
- runnerRef.current?.setEffectLevel(effectLevel);
130
- }, [effectLevel]);
131
- // ── Ctrl-C ────────────────────────────────────────────────────────────────
278
+ useEffect(() => { runnerRef.current?.setEffectLevel(effectLevel); }, [effectLevel]);
279
+ // ── Ctrl-C to exit ──────────────────────────────────────────────────────
132
280
  useInput((_input, key) => {
133
281
  if (key.ctrl && _input === "c") {
134
282
  push({ role: "system", content: T.muted("Goodbye 🪼") });
283
+ costTracker?.saveLifetime();
135
284
  setTimeout(exit, 200);
136
285
  }
137
286
  });
@@ -145,24 +294,147 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
145
294
  const args = rest.join(" ");
146
295
  if (cmd === "exit" || cmd === "quit") {
147
296
  push({ role: "system", content: T.muted("Goodbye 🪼") });
297
+ costTracker?.saveLifetime();
148
298
  setTimeout(exit, 200);
149
299
  return;
150
300
  }
151
301
  if (cmd === "help") {
152
302
  const lines = registry.listCommands().map(([n, d]) => T.accent(`/${n}`.padEnd(16)) + " " + d.description);
153
- notify("Available commands:\n\n" + lines.join("\n"));
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");
304
+ return;
305
+ }
306
+ if (cmd === "models") {
307
+ if (!modelReg) {
308
+ notify(T.error("Model registry not available"));
309
+ return;
310
+ }
311
+ const result = await modelReg.listModelsTool("", { query: rest.join(" ") || undefined, limit: 20, available_only: true });
312
+ notify(result.content[0].text);
313
+ return;
314
+ }
315
+ if (cmd === "cost") {
316
+ if (!costTracker) {
317
+ notify(T.error("Cost tracker not available"));
318
+ return;
319
+ }
320
+ const result = await costTracker.costReportTool();
321
+ notify(result.content[0].text);
322
+ return;
323
+ }
324
+ if (cmd === "news") {
325
+ const result = await getNewsTool("", { limit: 15 });
326
+ notify(result.content[0].text);
327
+ return;
328
+ }
329
+ if (cmd === "prices") {
330
+ const result = await getPricesTool("", { symbols: args ? args.split(/\s+/) : ["btc", "eth", "sol"] });
331
+ notify(result.content[0].text);
332
+ return;
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")}`);
154
426
  return;
155
427
  }
156
428
  const def = registry.getCommand(cmd);
157
429
  if (!def) {
158
- notify(T.error(`Unknown command: /${cmd}\nType /help to list all commands.`));
430
+ notify(T.error(`Unknown: /${cmd} — try /help or /palette`));
159
431
  return;
160
432
  }
161
433
  try {
162
434
  await def.handler(args, { ui: uiCtx });
163
435
  }
164
436
  catch (e) {
165
- notify(T.error(`Command error: ${e.message}`));
437
+ notify(T.error(`Error: ${e.message}`));
166
438
  }
167
439
  return;
168
440
  }
@@ -171,6 +443,7 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
171
443
  return;
172
444
  }
173
445
  push({ role: "user", content: input });
446
+ memoryStore.save(sessionId, "user", input); // #7 persist user message
174
447
  setDisabled(true);
175
448
  setStreaming("");
176
449
  try {
@@ -178,11 +451,23 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
178
451
  }
179
452
  catch (e) {
180
453
  setDisabled(false);
181
- notify(T.error(`Runner error: ${e.message}`));
454
+ notify(T.error(`Error: ${e.message}`));
182
455
  }
183
- }, [registry, exit, push, notify, uiCtx]);
184
- // Build status line from all active badges
185
- const statusLine = Object.values(statusBadges).join(" ") || null;
186
- 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 })] }));
456
+ }, [registry, exit, push, notify, uiCtx, modelReg, costTracker]);
457
+ const ctx = getContextBar(sessionRef.current);
458
+ const statusLine = [ticker, costBadge, newsBadge, ...Object.values(statusBadges)].filter(Boolean).join(" ") || null;
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
+ } }) })] })] }));
187
472
  }
188
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
@@ -9,7 +9,7 @@ export function StatusBar({ model, chain, vaultLocked, effectLevel, toolRunning,
9
9
  return (_jsxs(Box, { borderStyle: "single", borderColor: JELLY_COLORS.dim, paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: JELLY_COLORS.accent, bold: true, children: "\uD83E\uDEBC JellyOS" }), _jsx(Text, { color: JELLY_COLORS.muted, children: modelShort })] }), _jsx(Box, { children: toolRunning
10
10
  ? _jsxs(Text, { color: JELLY_COLORS.warn, children: ["\u2699 ", toolRunning] })
11
11
  : statusLine
12
- ? _jsx(Text, { color: JELLY_COLORS.success, children: statusLine })
12
+ ? _jsx(Text, { color: JELLY_COLORS.muted, children: statusLine.slice(0, 80) })
13
13
  : _jsx(Text, { color: JELLY_COLORS.muted, children: connected ? "ready" : "connecting…" }) }), _jsxs(Box, { gap: 2, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: chainShort }), _jsx(Text, { children: vaultIcon }), _jsxs(Text, { color: JELLY_COLORS.header, children: [effectIcon, " ", effectLevel] })] })] }));
14
14
  }
15
15
  //# sourceMappingURL=StatusBar.js.map