@jellyos/agent 0.1.4 → 0.1.6

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.npm.md +212 -0
  2. package/bin/jellyos-mcp +26 -0
  3. package/dist/api/ExtensionAPI.d.ts +11 -0
  4. package/dist/cli.js +127 -49
  5. package/dist/index.d.ts +15 -2
  6. package/dist/index.js +13 -3
  7. package/dist/loader.d.ts +2 -9
  8. package/dist/loader.js +2 -1
  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/ModelRegistry.d.ts +12 -1
  14. package/dist/models/ModelRegistry.js +105 -9
  15. package/dist/runner/AgentRunner.d.ts +19 -2
  16. package/dist/runner/AgentRunner.js +247 -17
  17. package/dist/runner/ModelClient.d.ts +10 -1
  18. package/dist/runner/ModelClient.js +79 -6
  19. package/dist/runner/SwarmRouter.d.ts +6 -6
  20. package/dist/runner/SwarmRouter.js +73 -24
  21. package/dist/runner/ToolDispatcher.d.ts +10 -0
  22. package/dist/runner/ToolDispatcher.js +106 -2
  23. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  24. package/dist/scheduler/AgentScheduler.js +253 -0
  25. package/dist/session/ContextStore.d.ts +96 -0
  26. package/dist/session/ContextStore.js +207 -0
  27. package/dist/session/GoalManager.d.ts +101 -0
  28. package/dist/session/GoalManager.js +167 -0
  29. package/dist/session/MemoryStore.d.ts +48 -0
  30. package/dist/session/MemoryStore.js +166 -0
  31. package/dist/session/SessionManager.d.ts +45 -4
  32. package/dist/session/SessionManager.js +151 -8
  33. package/dist/telemetry/Tracer.d.ts +48 -0
  34. package/dist/telemetry/Tracer.js +102 -0
  35. package/dist/tools/MarketSentiment.d.ts +166 -0
  36. package/dist/tools/MarketSentiment.js +209 -0
  37. package/dist/tools/NewsSentiment.js +40 -13
  38. package/dist/tools/PriceFeed.d.ts +2 -0
  39. package/dist/tools/PriceFeed.js +79 -27
  40. package/dist/tools/TechnicalAnalysis.d.ts +37 -0
  41. package/dist/tools/TechnicalAnalysis.js +85 -0
  42. package/dist/tui/App.d.ts +4 -3
  43. package/dist/tui/App.js +346 -119
  44. package/dist/tui/ModelSelector.d.ts +22 -0
  45. package/dist/tui/ModelSelector.js +86 -0
  46. package/dist/tui/REPL.d.ts +2 -1
  47. package/dist/tui/REPL.js +11 -6
  48. package/package.json +10 -6
  49. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  50. package/dist/api/ExtensionAPI.js.map +0 -1
  51. package/dist/api/Registry.d.ts.map +0 -1
  52. package/dist/api/Registry.js.map +0 -1
  53. package/dist/cli.d.ts.map +0 -1
  54. package/dist/cli.js.map +0 -1
  55. package/dist/index.d.ts.map +0 -1
  56. package/dist/index.js.map +0 -1
  57. package/dist/loader.d.ts.map +0 -1
  58. package/dist/loader.js.map +0 -1
  59. package/dist/models/CostTracker.d.ts.map +0 -1
  60. package/dist/models/CostTracker.js.map +0 -1
  61. package/dist/models/ModelRegistry.d.ts.map +0 -1
  62. package/dist/models/ModelRegistry.js.map +0 -1
  63. package/dist/models/index.d.ts.map +0 -1
  64. package/dist/models/index.js.map +0 -1
  65. package/dist/runner/AgentRunner.d.ts.map +0 -1
  66. package/dist/runner/AgentRunner.js.map +0 -1
  67. package/dist/runner/ModelClient.d.ts.map +0 -1
  68. package/dist/runner/ModelClient.js.map +0 -1
  69. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  70. package/dist/runner/SwarmRouter.js.map +0 -1
  71. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  72. package/dist/runner/ToolDispatcher.js.map +0 -1
  73. package/dist/session/SessionManager.d.ts.map +0 -1
  74. package/dist/session/SessionManager.js.map +0 -1
  75. package/dist/tools/NewsSentiment.d.ts.map +0 -1
  76. package/dist/tools/NewsSentiment.js.map +0 -1
  77. package/dist/tools/PriceFeed.d.ts.map +0 -1
  78. package/dist/tools/PriceFeed.js.map +0 -1
  79. package/dist/tools/TechnicalAnalysis.d.ts.map +0 -1
  80. package/dist/tools/TechnicalAnalysis.js.map +0 -1
  81. package/dist/tools/index.d.ts.map +0 -1
  82. package/dist/tools/index.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,25 +1,89 @@
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
- import { useState, useCallback, useEffect, useRef } from "react";
8
- import { Box, useApp, useInput } from "ink";
7
+ import { useState, useCallback, useEffect, useRef, useMemo } from "react";
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 { ModelSelector } from "./ModelSelector.js";
12
+ import { makeTheme, T, JELLY_COLORS } from "./theme.js";
12
13
  import { AgentRunner } from "../runner/AgentRunner.js";
13
14
  import { SessionManager } from "../session/SessionManager.js";
14
15
  import { resolveModelConfig } from "../runner/ModelClient.js";
15
16
  import { priceFeed, getPricesTool, topMoversTool, marketOverviewTool, getPricesParams, topMoversParams, marketOverviewParams } from "../tools/PriceFeed.js";
16
17
  import { getNewsTool, getNewsParams } from "../tools/NewsSentiment.js";
17
- import { analyzeTAParams } from "../tools/TechnicalAnalysis.js";
18
+ import { analyzeTAParams, getCandlesParams, getCandlesTool } from "../tools/TechnicalAnalysis.js";
18
19
  import { fullAnalysis } from "../tools/TechnicalAnalysis.js";
19
20
  import { newsFeed } from "../tools/NewsSentiment.js";
21
+ import { getFearGreedTool, fearGreedParams, getFundingRatesTool, fundingRatesParams, getBtcMempoolTool, btcMempoolParams, getDefiTvlTool, defiTvlParams, getSolanaStatsTool, solanaStatsParams, } from "../tools/MarketSentiment.js";
22
+ import { contextStore } from "../session/ContextStore.js";
23
+ import { goalManager } from "../session/GoalManager.js";
24
+ import { memoryStore } from "../session/MemoryStore.js";
25
+ import { agentScheduler } from "../scheduler/AgentScheduler.js";
26
+ import { Tracer } from "../telemetry/Tracer.js";
27
+ // ── Context window tracking (#33) ───────────────────────────────────────────
28
+ function getContextBar(session) {
29
+ if (!session)
30
+ return { pct: 0, bar: "░".repeat(20), color: JELLY_COLORS.dim, turboReady: true };
31
+ const pressure = session.getContextPressure();
32
+ const filled = Math.round(pressure.pct / 5);
33
+ const color = pressure.level === "critical" ? JELLY_COLORS.error
34
+ : pressure.level === "red" ? JELLY_COLORS.warn
35
+ : pressure.level === "yellow" ? JELLY_COLORS.header
36
+ : JELLY_COLORS.success;
37
+ return {
38
+ pct: pressure.pct,
39
+ bar: "█".repeat(Math.min(filled, 20)) + "░".repeat(Math.max(0, 20 - filled)),
40
+ color,
41
+ turboReady: pressure.turboReady,
42
+ };
43
+ }
44
+ // ── Syntax-highlighted JSON formatter ───────────────────────────────────────
45
+ function highlightJson(obj, indent = 0) {
46
+ const pad = " ".repeat(indent);
47
+ if (obj === null)
48
+ return T.dim("null");
49
+ if (typeof obj === "boolean")
50
+ return T.warn(String(obj));
51
+ if (typeof obj === "number")
52
+ return T.success(String(obj));
53
+ if (typeof obj === "string") {
54
+ if (obj.startsWith("0x") && obj.length > 10)
55
+ return T.header(obj.slice(0, 10) + "…");
56
+ return T.accent(`"${obj}"`);
57
+ }
58
+ if (Array.isArray(obj)) {
59
+ if (obj.length === 0)
60
+ return "[]";
61
+ if (obj.length <= 3)
62
+ return `[${obj.map(v => highlightJson(v, 0)).join(", ")}]`;
63
+ 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}]`;
64
+ }
65
+ if (typeof obj === "object") {
66
+ const entries = Object.entries(obj);
67
+ if (entries.length === 0)
68
+ return "{}";
69
+ 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}}`;
70
+ }
71
+ return String(obj);
72
+ }
73
+ function formatToolContent(text) {
74
+ try {
75
+ const obj = JSON.parse(text);
76
+ return highlightJson(obj);
77
+ }
78
+ catch {
79
+ return text.length > 300 ? text.slice(0, 300) + T.dim("\n…[truncated]") : text;
80
+ }
81
+ }
20
82
  let _msgIdCounter = 0;
21
83
  function nextId() { return String(++_msgIdCounter); }
22
- export function App({ registry, systemPrompt, effectLevel: initialEffect = "normal", chain: initialChain = "ethereum", modelReg, costTracker, onNotifyReady, onStatusReady, }) {
84
+ // Unique session ID for memory persistence
85
+ const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
86
+ export function App({ registry, systemPrompt, effectLevel: initialEffect = "normal", chain: initialChain = "ethereum", modelReg, costTracker, onNotifyReady, onStatusReady, onModelSelectorReady, }) {
23
87
  const { exit } = useApp();
24
88
  const [messages, setMessages] = useState([]);
25
89
  const [streaming, setStreaming] = useState("");
@@ -32,6 +96,9 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
32
96
  const [ticker, setTicker] = useState("");
33
97
  const [costBadge, setCostBadge] = useState("");
34
98
  const [newsBadge, setNewsBadge] = useState("");
99
+ // ── Model selector overlay state ──────────────────────────────────────
100
+ const [showModelSelector, setShowModelSelector] = useState(false);
101
+ const [modelSelectorInitialQuery, setModelSelectorInitialQuery] = useState("");
35
102
  const runnerRef = useRef(null);
36
103
  const sessionRef = useRef(null);
37
104
  const sessionCtxRef = useRef(null);
@@ -41,7 +108,6 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
41
108
  modelName = resolveModelConfig(modelReg).model;
42
109
  }
43
110
  catch { /* shown via banner */ }
44
- // ── Push helpers ─────────────────────────────────────────────────────────
45
111
  const push = useCallback((msg) => {
46
112
  setMessages(prev => [...prev, { ...msg, id: nextId(), ts: Date.now() }]);
47
113
  }, []);
@@ -57,123 +123,114 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
57
123
  if (key === "effect_level")
58
124
  setEffectLevel(value);
59
125
  }, []);
60
- const uiCtx = { notify, setStatus, setTheme(_name) { }, setHeader(_factory) { }, theme };
61
- // ── Register built-in tools ──────────────────────────────────────────────
126
+ const uiCtx = {
127
+ notify, setStatus,
128
+ setTheme(_n) { },
129
+ setHeader(_f) { },
130
+ showModelSelector: (query) => {
131
+ setModelSelectorInitialQuery(query ?? "");
132
+ setShowModelSelector(true);
133
+ },
134
+ theme,
135
+ };
136
+ // Register built-in tools
62
137
  function registerBuiltinTools() {
63
138
  if (!modelReg)
64
139
  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
140
+ const mk = modelReg;
141
+ 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) });
142
+ 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) });
143
+ 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
144
  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
- });
145
+ registry.addTool({ name: "cost_report", label: "Cost Report", description: "Show session and lifetime token usage.", parameters: costTracker.costReportParams, execute: () => costTracker.costReportTool() });
92
146
  }
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) => {
147
+ registry.addTool({ name: "get_prices", label: "Get Prices", description: "Get current prices and 24h change.", parameters: getPricesParams, execute: (id, p) => getPricesTool(id, p) });
148
+ registry.addTool({ name: "get_top_movers", label: "Top Movers", description: "Assets with largest 24h price movement.", parameters: topMoversParams, execute: (id, p) => topMoversTool(id, p) });
149
+ registry.addTool({ name: "get_market_overview", label: "Market Overview", description: "Aggregated market data.", parameters: marketOverviewParams, execute: () => marketOverviewTool() });
150
+ registry.addTool({ name: "get_news", label: "Get News", description: "Crypto news headlines with sentiment scoring.", parameters: getNewsParams, execute: (id, p) => getNewsTool(id, p) });
151
+ // #18: OHLCV candles from Binance + TA analysis
152
+ 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) });
153
+ // #20: Free market sentiment tools
154
+ 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) });
155
+ 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) });
156
+ registry.addTool({ name: "get_btc_mempool", label: "BTC Mempool", description: "Bitcoin mempool stats and recommended fee rates.", parameters: btcMempoolParams, execute: () => getBtcMempoolTool() });
157
+ 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) });
158
+ registry.addTool({ name: "get_solana_stats", label: "Solana Stats", description: "Solana network TPS and health.", parameters: solanaStatsParams, execute: () => getSolanaStatsTool() });
159
+ // #31: Ephemeral task context
160
+ 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) });
161
+ 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() });
162
+ // #12: Goal management
163
+ 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) });
164
+ registry.addTool({ name: "complete_goal", label: "Complete Goal", description: "Mark a goal as completed.", parameters: goalManager.completeGoalParams, execute: (id, p) => goalManager.completeGoalTool(id, p) });
165
+ 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) });
166
+ 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) });
167
+ 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
168
  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
- }));
169
+ 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
170
  const results = fullAnalysis(candles);
133
- const text = results.map(r => {
171
+ const summary = results.find((r) => r.indicator === "SUMMARY");
172
+ const text = results.map((r) => {
134
173
  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 : "-";
174
+ const v = Array.isArray(r.value) ? `[${r.value.length}]` : typeof r.value === "number" ? r.value : "-";
136
175
  return `${s} ${r.indicator}: ${v}`;
137
176
  }).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
- });
177
+ return { content: [{ type: "text", text: `Technical Analysis:\n${text}` }], details: { results, overall_signal: summary?.signal, overall_score: summary?.value } };
178
+ } });
145
179
  }
146
180
  useEffect(() => { onNotifyReady?.(notify); onStatusReady?.(setStatus); }, []);
147
- // ── Boot: start feeds, register tools, fire session_start ────────────────
181
+ // ── Expose model selector trigger to extensions ──────────────────────────
182
+ useEffect(() => {
183
+ onModelSelectorReady?.((initialQuery) => {
184
+ setModelSelectorInitialQuery(initialQuery ?? "");
185
+ setShowModelSelector(true);
186
+ });
187
+ }, [onModelSelectorReady]);
148
188
  useEffect(() => {
149
189
  registerBuiltinTools();
150
- // Start background feeds
151
190
  priceFeed.track("btc", "eth", "sol", "bnb", "matic", "arb", "op", "avax", "link", "uni", "doge", "xrp", "ada", "dot", "atom", "near", "sui", "apt", "pepe", "aave");
152
191
  priceFeed.start();
153
192
  newsFeed.start();
154
- // Ticker update every 5 seconds from cached prices
193
+ // #11: Register scheduler tools
194
+ 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) });
195
+ registry.addTool({ name: "list_schedule", label: "List Schedule", description: "List all scheduled tasks.", parameters: agentScheduler.listTasksParams, execute: (id, p) => agentScheduler.listTasksTool(id, p) });
196
+ 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) });
197
+ // Start scheduler — fires agent turns autonomously
198
+ agentScheduler.start((task) => {
199
+ push({ role: "system", content: T.muted(`⏰ Scheduler: running "${task.name}"`) });
200
+ // Only fire if agent is not currently busy
201
+ if (!disabled && runnerRef.current) {
202
+ setDisabled(true);
203
+ setStreaming("");
204
+ runnerRef.current.run(`[SCHEDULED TASK: ${task.name}] ${task.prompt}`)
205
+ .catch((e) => notify(T.error(`Scheduler error: ${e.message}`)));
206
+ }
207
+ });
155
208
  const tickerInterval = setInterval(() => {
156
- setTicker(priceFeed.tickerLine(5));
209
+ setTicker(priceFeed.tickerLine(6));
157
210
  if (costTracker)
158
211
  setCostBadge(costTracker.statusLine());
159
212
  setNewsBadge(newsFeed.statusBadge());
160
213
  }, 5_000);
161
- // Save cost on exit
162
- const saveInterval = setInterval(() => {
163
- costTracker?.saveLifetime();
164
- }, 60_000);
165
- // Agent session
214
+ const saveInterval = setInterval(() => { costTracker?.saveLifetime(); }, 60_000);
166
215
  const session = new SessionManager();
167
- session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt ||
168
- "You are JellyOS, an autonomous AI trading agent.");
216
+ session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt || "You are JellyOS, an autonomous AI trading agent.");
169
217
  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
- };
218
+ 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
219
  sessionCtxRef.current = sessionCtx;
175
220
  registry.fireHook("session_start", sessionCtx).then(() => {
176
- push({ role: "system", content: T.muted("Session started. Type a message or /help.") });
221
+ // #7: Inject memory context into system prompt
222
+ if (memoryStore.isAvailable) {
223
+ const memCtx = memoryStore.buildContextBlock(sessionId);
224
+ if (memCtx) {
225
+ session.setSystemPrompt((session.getSystemPrompt() || "") + memCtx);
226
+ }
227
+ }
228
+ // #12: Inject active goals into system prompt
229
+ const goalCtx = goalManager.buildContextBlock();
230
+ if (goalCtx) {
231
+ session.setSystemPrompt((session.getSystemPrompt() || "") + goalCtx);
232
+ }
233
+ push({ role: "system", content: T.muted("Session started. Type /help for commands.") });
177
234
  });
178
235
  const runner = new AgentRunner(registry, session, (event) => {
179
236
  if (event.type === "text_delta")
@@ -182,13 +239,20 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
182
239
  setToolRunning(event.name);
183
240
  else if (event.type === "tool_done") {
184
241
  setToolRunning(null);
185
- push({ role: "tool", content: event.result, toolName: event.name, isError: event.isError });
242
+ // Format tool output with syntax highlighting
243
+ const formatted = formatToolContent(event.result);
244
+ push({ role: "tool", content: formatted, toolName: event.name, isError: event.isError });
186
245
  }
187
246
  else if (event.type === "turn_done") {
188
247
  setDisabled(false);
189
248
  setToolRunning(null);
190
- setStreaming(prev => { if (prev.trim())
191
- push({ role: "assistant", content: prev }); return ""; });
249
+ setStreaming(prev => {
250
+ if (prev.trim()) {
251
+ push({ role: "assistant", content: prev });
252
+ memoryStore.save(sessionId, "assistant", prev); // #7 persist to memory
253
+ }
254
+ return "";
255
+ });
192
256
  }
193
257
  else if (event.type === "error") {
194
258
  setDisabled(false);
@@ -196,10 +260,27 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
196
260
  setStreaming("");
197
261
  notify(T.error(`Error: ${event.message}`));
198
262
  }
199
- }, sessionCtx, effectLevel, modelReg, costTracker);
263
+ else if (event.type === "approval_request") {
264
+ // #10: Show approval prompt — user must type y or n
265
+ setToolRunning(null);
266
+ const { toolName, args, approve } = event;
267
+ let parsed = "";
268
+ try {
269
+ parsed = JSON.stringify(JSON.parse(args), null, 2).slice(0, 200);
270
+ }
271
+ catch {
272
+ parsed = args.slice(0, 200);
273
+ }
274
+ push({
275
+ role: "notify",
276
+ content: `⚠️ APPROVAL REQUIRED\n\nTool: ${toolName}\nArgs: ${parsed}\n\nType /approve or /deny to continue.`,
277
+ });
278
+ // Store the approve callback so /approve /deny commands can resolve it
279
+ runnerRef.current._pendingApproval = approve;
280
+ }
281
+ }, sessionCtx, effectLevel, modelReg, costTracker, goalManager, contextStore);
200
282
  runnerRef.current = runner;
201
- // Initial ticker
202
- setTicker(priceFeed.tickerLine(5));
283
+ setTicker(priceFeed.tickerLine(6));
203
284
  if (costTracker)
204
285
  setCostBadge(costTracker.statusLine());
205
286
  setNewsBadge(newsFeed.statusBadge());
@@ -208,13 +289,14 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
208
289
  registry.fireHook("session_end", sessionCtxRef.current).catch(() => { });
209
290
  priceFeed.stop();
210
291
  newsFeed.stop();
292
+ agentScheduler.stop();
211
293
  clearInterval(tickerInterval);
212
294
  clearInterval(saveInterval);
213
295
  costTracker?.saveLifetime();
214
296
  };
215
297
  }, []);
216
298
  useEffect(() => { runnerRef.current?.setEffectLevel(effectLevel); }, [effectLevel]);
217
- // ── Ctrl-C ────────────────────────────────────────────────────────────────
299
+ // ── Ctrl-C to exit ──────────────────────────────────────────────────────
218
300
  useInput((_input, key) => {
219
301
  if (key.ctrl && _input === "c") {
220
302
  push({ role: "system", content: T.muted("Goodbye 🪼") });
@@ -227,7 +309,6 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
227
309
  const input = raw.trim();
228
310
  if (!input)
229
311
  return;
230
- // ── Slash commands ────────────────────────────────────────────────────
231
312
  if (input.startsWith("/")) {
232
313
  const [cmd, ...rest] = input.slice(1).split(" ");
233
314
  const args = rest.join(" ");
@@ -239,21 +320,18 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
239
320
  }
240
321
  if (cmd === "help") {
241
322
  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");
323
+ 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
324
  return;
244
325
  }
245
- // Built-in /models command
246
326
  if (cmd === "models") {
247
327
  if (!modelReg) {
248
328
  notify(T.error("Model registry not available"));
249
329
  return;
250
330
  }
251
- const query = rest.join(" ") || undefined;
252
- const result = await modelReg.listModelsTool("", { query: query, limit: 20, available_only: true });
331
+ const result = await modelReg.listModelsTool("", { query: rest.join(" ") || undefined, limit: 20, available_only: true });
253
332
  notify(result.content[0].text);
254
333
  return;
255
334
  }
256
- // Built-in /cost command
257
335
  if (cmd === "cost") {
258
336
  if (!costTracker) {
259
337
  notify(T.error("Cost tracker not available"));
@@ -263,28 +341,120 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
263
341
  notify(result.content[0].text);
264
342
  return;
265
343
  }
266
- // Built-in /news command
267
344
  if (cmd === "news") {
268
345
  const result = await getNewsTool("", { limit: 15 });
269
346
  notify(result.content[0].text);
270
347
  return;
271
348
  }
272
- // Built-in /prices command
273
349
  if (cmd === "prices") {
274
350
  const result = await getPricesTool("", { symbols: args ? args.split(/\s+/) : ["btc", "eth", "sol"] });
275
351
  notify(result.content[0].text);
276
352
  return;
277
353
  }
354
+ if (cmd === "palette") {
355
+ // Command palette — list all commands and tools
356
+ const lines = registry.listCommands().map(([n, d]) => T.accent(`/${n}`.padEnd(16)) + " " + d.description);
357
+ const tools = registry.listTools().map(t => T.success(`🔧 ${t.name.padEnd(22)}`) + " " + T.muted(t.description.slice(0, 50)));
358
+ notify("Command Palette\n\n" + lines.join("\n") + "\n\nTools:\n" + tools.join("\n"));
359
+ return;
360
+ }
361
+ if (cmd === "effect" && ["eco", "normal", "turbo", "max"].includes(args.trim().toLowerCase())) {
362
+ setEffectLevel(args.trim().toLowerCase());
363
+ notify(T.accent(`Effect → ${args.trim().toUpperCase()}`));
364
+ return;
365
+ }
366
+ // #12: Goal commands
367
+ if (cmd === "goals" || cmd === "goal") {
368
+ const subCmd = args.trim().split(" ")[0];
369
+ if (subCmd === "add") {
370
+ const text = args.trim().slice(4).trim();
371
+ if (!text) {
372
+ notify(T.error("Usage: /goal add <text>"));
373
+ return;
374
+ }
375
+ const result = await goalManager.setGoalTool("", { text });
376
+ notify(result.content[0].text);
377
+ }
378
+ else if (subCmd === "done" || subCmd === "complete") {
379
+ const id = args.trim().split(" ")[1];
380
+ if (!id) {
381
+ notify(T.error("Usage: /goal done <id>"));
382
+ return;
383
+ }
384
+ const result = await goalManager.completeGoalTool("", { id });
385
+ notify(result.content[0].text);
386
+ }
387
+ else {
388
+ const result = await goalManager.listGoalsTool("", { status: "all" });
389
+ notify(result.content[0].text);
390
+ }
391
+ return;
392
+ }
393
+ // #31: Task context commands
394
+ if (cmd === "tasks") {
395
+ const result = await contextStore.listTasksTool();
396
+ notify(result.content[0].text);
397
+ return;
398
+ }
399
+ if (cmd === "keep" && args.trim()) {
400
+ const ok = contextStore.keepTask(args.trim());
401
+ notify(ok ? T.success(`Task ${args.trim()} marked for permanent retention.`) : T.error(`Task ${args.trim()} not found.`));
402
+ return;
403
+ }
404
+ // #10: Approval gate responses
405
+ if (cmd === "approve" || cmd === "y") {
406
+ const pending = runnerRef.current?._pendingApproval;
407
+ if (pending) {
408
+ pending(true);
409
+ push({ role: "system", content: T.success("✅ Tool approved.") });
410
+ }
411
+ else
412
+ notify(T.error("No pending approval."));
413
+ return;
414
+ }
415
+ if (cmd === "deny" || cmd === "n") {
416
+ const pending = runnerRef.current?._pendingApproval;
417
+ if (pending) {
418
+ pending(false);
419
+ push({ role: "system", content: T.error("❌ Tool denied.") });
420
+ }
421
+ else
422
+ notify(T.error("No pending approval."));
423
+ return;
424
+ }
425
+ // #30: Traces command
426
+ if (cmd === "traces") {
427
+ const recent = Tracer.readRecent(5);
428
+ notify(Tracer.formatSummary(recent));
429
+ return;
430
+ }
431
+ // #11: Scheduler commands
432
+ if (cmd === "schedule" || cmd === "sched") {
433
+ const result = await agentScheduler.listTasksTool("", { enabled_only: false });
434
+ notify(result.content[0].text);
435
+ return;
436
+ }
437
+ // #7: Memory search
438
+ if (cmd === "memory" && args.trim()) {
439
+ const results = memoryStore.search(args.trim(), 8);
440
+ if (results.length === 0) {
441
+ notify(T.muted("No memories found for: " + args.trim()));
442
+ return;
443
+ }
444
+ const lines = results.map(r => `[${new Date(r.ts).toLocaleDateString()} ${r.role}] ${r.content.slice(0, 120)}`);
445
+ notify(`Memory search: "${args.trim()}"\n${lines.join("\n")}`);
446
+ return;
447
+ }
278
448
  const def = registry.getCommand(cmd);
279
449
  if (!def) {
280
- notify(T.error(`Unknown command: /${cmd}\nType /help to list all commands.`));
450
+ notify(T.error(`Unknown: /${cmd} — try /help or /palette`));
281
451
  return;
282
452
  }
283
453
  try {
284
454
  await def.handler(args, { ui: uiCtx });
285
455
  }
286
456
  catch (e) {
287
- notify(T.error(`Command error: ${e.message}`));
457
+ notify(T.error(`Error: ${e.message}`));
288
458
  }
289
459
  return;
290
460
  }
@@ -293,6 +463,7 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
293
463
  return;
294
464
  }
295
465
  push({ role: "user", content: input });
466
+ memoryStore.save(sessionId, "user", input); // #7 persist user message
296
467
  setDisabled(true);
297
468
  setStreaming("");
298
469
  try {
@@ -300,11 +471,67 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
300
471
  }
301
472
  catch (e) {
302
473
  setDisabled(false);
303
- notify(T.error(`Runner error: ${e.message}`));
474
+ notify(T.error(`Error: ${e.message}`));
304
475
  }
305
476
  }, [registry, exit, push, notify, uiCtx, modelReg, costTracker]);
306
- // Build status line: ticker + cost + badges
477
+ // ── Handle model selection ───────────────────────────────────────────
478
+ const handleModelSelect = useCallback((modelId) => {
479
+ setShowModelSelector(false);
480
+ try {
481
+ const { writeFileSync, readFileSync, existsSync, mkdirSync } = require("node:fs");
482
+ const { join } = require("node:path");
483
+ const { homedir } = require("node:os");
484
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
485
+ const envFile = join(JELLY_HOME, ".env");
486
+ mkdirSync(JELLY_HOME, { recursive: true });
487
+ const content = existsSync(envFile) ? readFileSync(envFile, "utf-8") : "";
488
+ const re = /^DEFAULT_MODEL=.*$/m;
489
+ const line = `DEFAULT_MODEL=${modelId}`;
490
+ writeFileSync(envFile, re.test(content) ? content.replace(re, line) : content + "\n" + line + "\n", "utf-8");
491
+ process.env.DEFAULT_MODEL = modelId;
492
+ const ctxPath = join(JELLY_HOME, "context.json");
493
+ const store = existsSync(ctxPath) ? JSON.parse(readFileSync(ctxPath, "utf-8")) : {};
494
+ store.model = modelId;
495
+ writeFileSync(ctxPath, JSON.stringify(store, null, 2), "utf-8");
496
+ }
497
+ catch { /* non-fatal */ }
498
+ notify(T.accent(`Model set to: ${modelId}\nRestart jellyos to apply.`));
499
+ }, [notify]);
500
+ const handleModelCancel = useCallback(() => {
501
+ setShowModelSelector(false);
502
+ }, []);
503
+ const modelList = useMemo(() => {
504
+ if (!modelReg)
505
+ return [];
506
+ const tiers = ["orchestrator", "analyst", "worker", "free"];
507
+ const items = [];
508
+ for (const tier of tiers) {
509
+ const pool = modelReg.getPool(tier);
510
+ for (const tm of pool) {
511
+ if (tm.available && tm.failures < 3) {
512
+ items.push({ id: tm.model.id, tier });
513
+ }
514
+ }
515
+ }
516
+ return items;
517
+ }, [modelReg]);
518
+ const ctx = getContextBar(sessionRef.current);
307
519
  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 })] }));
520
+ // ── Overlay: model selector ────────────────────────────────────────────
521
+ if (showModelSelector) {
522
+ 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(ModelSelector, { models: modelList, currentModelId: process.env.DEFAULT_MODEL ?? "", onSelect: handleModelSelect, onCancel: handleModelCancel, initialQuery: modelSelectorInitialQuery })] }));
523
+ }
524
+ // ── Multi-pane layout ────────────────────────────────────────────────────
525
+ 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: () => {
526
+ runnerRef.current?.abort();
527
+ setDisabled(false);
528
+ setToolRunning(null);
529
+ setStreaming(prev => {
530
+ if (prev.trim())
531
+ push({ role: "assistant", content: prev + " \u2014 [interrupted]" });
532
+ return "";
533
+ });
534
+ push({ role: "system", content: T.dim("— stream interrupted —") });
535
+ } }) })] })] }));
309
536
  }
310
537
  //# sourceMappingURL=App.js.map