@jellyos/agent 0.1.5 → 0.1.8

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.
@@ -60,6 +60,11 @@ export interface UIContext {
60
60
  * Pi compat — accepted but engine renders its own Ink header.
61
61
  */
62
62
  setHeader(factory: HeaderFactory): void;
63
+ /**
64
+ * Open the interactive model selector overlay.
65
+ * @param query Optional initial search query to pre-filter the list.
66
+ */
67
+ showModelSelector(query?: string): void;
63
68
  theme: ThemeContext;
64
69
  }
65
70
  export interface CommandContext {
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * cli.ts — JellyOS entry point.
3
3
  * Completely standalone — all outbound, no inbound ports exposed.
4
4
  */
5
- import { readFileSync, existsSync } from "node:fs";
5
+ import { readFileSync, existsSync, writeFileSync } from "node:fs";
6
6
  import { resolve, join } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import { render } from "ink";
@@ -80,6 +80,16 @@ function loadContext() {
80
80
  return { effectLevel: "normal", chain: "ethereum" };
81
81
  try {
82
82
  const ctx = JSON.parse(readFileSync(ctxPath, "utf-8"));
83
+ if (ctx.model && typeof ctx.model === "string") {
84
+ process.env.DEFAULT_MODEL = ctx.model;
85
+ try {
86
+ const ef = join(JELLY_HOME, ".env");
87
+ const c = existsSync(ef) ? readFileSync(ef, "utf-8") : "";
88
+ const re = /^DEFAULT_MODEL=.*$/m;
89
+ writeFileSync(ef, re.test(c) ? c.replace(re, `DEFAULT_MODEL=${ctx.model}`) : c + `\nDEFAULT_MODEL=${ctx.model}\n`, "utf-8");
90
+ }
91
+ catch { /* non-fatal */ }
92
+ }
83
93
  return { effectLevel: ctx.effect_level ?? "normal", chain: ctx.active_chain ?? "ethereum" };
84
94
  }
85
95
  catch {
@@ -114,6 +124,7 @@ if (headlessMsg) {
114
124
  setStatus: () => { },
115
125
  setTheme: () => { },
116
126
  setHeader: () => { },
127
+ showModelSelector: () => { },
117
128
  theme,
118
129
  };
119
130
  const sessionCtx = {
@@ -162,16 +173,16 @@ else {
162
173
  console.log(T.muted(" Standalone AI trading agent — all local, zero exposure\n"));
163
174
  const registry = new Registry();
164
175
  const { effectLevel, chain } = loadContext();
165
- // These callbacks are forwarded into the extension API so ui.setStatus
166
- // and ui.notify work even during the session_start hook (before Ink mounts).
167
176
  let _notifyFn = null;
168
177
  let _setStatusFn = null;
178
+ let _showModelSelectorFn = null;
169
179
  if (extensionPath) {
170
180
  try {
171
181
  console.log(T.muted(` Loading: ${extensionPath}`));
172
182
  await loadExtension(extensionPath, registry, {
173
183
  onNotify: (msg) => { _notifyFn?.(msg); },
174
184
  onStatusUpdate: (k, v) => { _setStatusFn?.(k, v); },
185
+ onShowModelSelector: (q) => { _showModelSelectorFn?.(q); },
175
186
  });
176
187
  console.log(T.success(` ✓ ${registry.listTools().length} tools · ${registry.listCommands().length} commands`));
177
188
  }
@@ -203,6 +214,7 @@ else {
203
214
  costTracker,
204
215
  onNotifyReady: (fn) => { _notifyFn = fn; },
205
216
  onStatusReady: (fn) => { _setStatusFn = fn; },
217
+ onModelSelectorReady: (fn) => { _showModelSelectorFn = fn; },
206
218
  }), { exitOnCtrlC: false });
207
219
  })();
208
220
  } // end headless else
package/dist/loader.d.ts CHANGED
@@ -7,17 +7,10 @@
7
7
  */
8
8
  import { Registry } from "./api/Registry.js";
9
9
  export interface LoaderOptions {
10
- /**
11
- * Called when the extension uses ui.setStatus(key, value).
12
- * The App component passes a state-setter here so status badges update live.
13
- */
14
10
  onStatusUpdate?: (key: string, value: string) => void;
15
- /**
16
- * Called when the extension calls ui.notify(message).
17
- * Before the TUI is mounted, the App wires this to React state.
18
- * We store the latest notifier here so the extension can call it any time.
19
- */
20
11
  onNotify?: (message: string) => void;
12
+ /** Called when the extension calls ui.showModelSelector(query). */
13
+ onShowModelSelector?: (query?: string) => void;
21
14
  }
22
15
  export declare function loadExtension(extensionPath: string, registry: Registry, opts?: LoaderOptions): Promise<void>;
23
16
  //# sourceMappingURL=loader.d.ts.map
package/dist/loader.js CHANGED
@@ -15,14 +15,15 @@ export async function loadExtension(extensionPath, registry, opts = {}) {
15
15
  throw new Error(`Extension file not found: ${abs}`);
16
16
  }
17
17
  const theme = makeTheme();
18
- // Live-updateable notifier — App.tsx replaces this once mounted
19
18
  let _notify = opts.onNotify ?? ((_msg) => { });
20
19
  let _setStatus = opts.onStatusUpdate ?? ((_k, _v) => { });
20
+ let _showModelSelector = opts.onShowModelSelector ?? ((_q) => { });
21
21
  const ui = {
22
22
  notify(message) { _notify(message); },
23
23
  setStatus(key, value) { _setStatus(key, value); },
24
24
  setTheme(_name) { },
25
25
  setHeader(_factory) { },
26
+ showModelSelector(query) { _showModelSelector(query); },
26
27
  theme,
27
28
  };
28
29
  const api = {
package/dist/tui/App.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface AppProps {
15
15
  costTracker?: CostTracker;
16
16
  onNotifyReady?: (fn: (msg: string) => void) => void;
17
17
  onStatusReady?: (fn: (key: string, val: string) => void) => void;
18
+ onModelSelectorReady?: (fn: (query?: string) => void) => void;
18
19
  }
19
- export declare function App({ registry, systemPrompt, effectLevel: initialEffect, chain: initialChain, modelReg, costTracker, onNotifyReady, onStatusReady, }: AppProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function App({ registry, systemPrompt, effectLevel: initialEffect, chain: initialChain, modelReg, costTracker, onNotifyReady, onStatusReady, onModelSelectorReady, }: AppProps): import("react/jsx-runtime").JSX.Element;
20
21
  //# sourceMappingURL=App.d.ts.map
package/dist/tui/App.js CHANGED
@@ -4,10 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * Multi-pane TUI with context bar, syntax-highlighted tool output,
5
5
  * live side panel with ticker/prices, and command palette triggered via /palette.
6
6
  */
7
- import { useState, useCallback, useEffect, useRef } from "react";
7
+ import { useState, useCallback, useEffect, useRef, useMemo } from "react";
8
8
  import { Box, Text, useApp, useInput } from "ink";
9
9
  import { StatusBar } from "./StatusBar.js";
10
10
  import { REPL } from "./REPL.js";
11
+ import { ModelSelector } from "./ModelSelector.js";
11
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";
@@ -82,7 +83,7 @@ let _msgIdCounter = 0;
82
83
  function nextId() { return String(++_msgIdCounter); }
83
84
  // Unique session ID for memory persistence
84
85
  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, }) {
86
+ export function App({ registry, systemPrompt, effectLevel: initialEffect = "normal", chain: initialChain = "ethereum", modelReg, costTracker, onNotifyReady, onStatusReady, onModelSelectorReady, }) {
86
87
  const { exit } = useApp();
87
88
  const [messages, setMessages] = useState([]);
88
89
  const [streaming, setStreaming] = useState("");
@@ -95,6 +96,13 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
95
96
  const [ticker, setTicker] = useState("");
96
97
  const [costBadge, setCostBadge] = useState("");
97
98
  const [newsBadge, setNewsBadge] = useState("");
99
+ // ── Sidebar live data state ─────────────────────────────────────────────
100
+ const [sidebarTickers, setSidebarTickers] = useState([]);
101
+ const [sidebarNews, setSidebarNews] = useState([]);
102
+ const [rotationSlots, setRotationSlots] = useState([null, null, null, null, null]);
103
+ // ── Model selector overlay state ──────────────────────────────────────
104
+ const [showModelSelector, setShowModelSelector] = useState(false);
105
+ const [modelSelectorInitialQuery, setModelSelectorInitialQuery] = useState("");
98
106
  const runnerRef = useRef(null);
99
107
  const sessionRef = useRef(null);
100
108
  const sessionCtxRef = useRef(null);
@@ -119,7 +127,16 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
119
127
  if (key === "effect_level")
120
128
  setEffectLevel(value);
121
129
  }, []);
122
- const uiCtx = { notify, setStatus, setTheme(_n) { }, setHeader(_f) { }, theme };
130
+ const uiCtx = {
131
+ notify, setStatus,
132
+ setTheme(_n) { },
133
+ setHeader(_f) { },
134
+ showModelSelector: (query) => {
135
+ setModelSelectorInitialQuery(query ?? "");
136
+ setShowModelSelector(true);
137
+ },
138
+ theme,
139
+ };
123
140
  // Register built-in tools
124
141
  function registerBuiltinTools() {
125
142
  if (!modelReg)
@@ -165,6 +182,13 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
165
182
  } });
166
183
  }
167
184
  useEffect(() => { onNotifyReady?.(notify); onStatusReady?.(setStatus); }, []);
185
+ // ── Expose model selector trigger to extensions ──────────────────────────
186
+ useEffect(() => {
187
+ onModelSelectorReady?.((initialQuery) => {
188
+ setModelSelectorInitialQuery(initialQuery ?? "");
189
+ setShowModelSelector(true);
190
+ });
191
+ }, [onModelSelectorReady]);
168
192
  useEffect(() => {
169
193
  registerBuiltinTools();
170
194
  priceFeed.track("btc", "eth", "sol", "bnb", "matic", "arb", "op", "avax", "link", "uni", "doge", "xrp", "ada", "dot", "atom", "near", "sui", "apt", "pepe", "aave");
@@ -190,7 +214,28 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
190
214
  if (costTracker)
191
215
  setCostBadge(costTracker.statusLine());
192
216
  setNewsBadge(newsFeed.statusBadge());
193
- }, 5_000);
217
+ // Update sidebar live data
218
+ const allTicks = priceFeed.getAll();
219
+ setSidebarTickers(allTicks.slice(0, 8).map(t => ({ symbol: t.symbol, price: t.price, changePct: t.change24h })));
220
+ const latestNews = newsFeed.getLatest();
221
+ if (latestNews?.items) {
222
+ setSidebarNews(latestNews.items.slice(0, 6).map(item => ({ title: item.title ?? item.source ?? "News", sentiment: item.sentiment ?? 0, source: item.source ?? "" })));
223
+ }
224
+ // Read rotation slots from context.json
225
+ try {
226
+ const { existsSync: exists, readFileSync: read } = require("node:fs");
227
+ const { join } = require("node:path");
228
+ const { homedir } = require("node:os");
229
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
230
+ const ctxPath = join(JELLY_HOME, "context.json");
231
+ if (exists(ctxPath)) {
232
+ const store = JSON.parse(read(ctxPath, "utf-8"));
233
+ if (store.rotationSlots)
234
+ setRotationSlots(store.rotationSlots);
235
+ }
236
+ }
237
+ catch { /* non-fatal */ }
238
+ }, 3_000);
194
239
  const saveInterval = setInterval(() => { costTracker?.saveLifetime(); }, 60_000);
195
240
  const session = new SessionManager();
196
241
  session.setSystemPrompt(registry.getSystemPrompt() || systemPrompt || "You are JellyOS, an autonomous AI trading agent.");
@@ -264,6 +309,13 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
264
309
  if (costTracker)
265
310
  setCostBadge(costTracker.statusLine());
266
311
  setNewsBadge(newsFeed.statusBadge());
312
+ // Initial sidebar data fetch (before interval fires)
313
+ const initialTicks = priceFeed.getAll();
314
+ setSidebarTickers(initialTicks.slice(0, 8).map(t => ({ symbol: t.symbol, price: t.price, changePct: t.change24h })));
315
+ const initialNews = newsFeed.getLatest();
316
+ if (initialNews?.items) {
317
+ setSidebarNews(initialNews.items.slice(0, 6).map(item => ({ title: item.title ?? item.source ?? "News", sentiment: item.sentiment ?? 0, source: item.source ?? "" })));
318
+ }
267
319
  return () => {
268
320
  if (sessionCtxRef.current)
269
321
  registry.fireHook("session_end", sessionCtxRef.current).catch(() => { });
@@ -454,11 +506,74 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
454
506
  notify(T.error(`Error: ${e.message}`));
455
507
  }
456
508
  }, [registry, exit, push, notify, uiCtx, modelReg, costTracker]);
509
+ // ── Handle model selection ───────────────────────────────────────────
510
+ const handleModelSelect = useCallback((modelId) => {
511
+ setShowModelSelector(false);
512
+ try {
513
+ const { writeFileSync, readFileSync, existsSync, mkdirSync } = require("node:fs");
514
+ const { join } = require("node:path");
515
+ const { homedir } = require("node:os");
516
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
517
+ const envFile = join(JELLY_HOME, ".env");
518
+ mkdirSync(JELLY_HOME, { recursive: true });
519
+ const content = existsSync(envFile) ? readFileSync(envFile, "utf-8") : "";
520
+ const re = /^DEFAULT_MODEL=.*$/m;
521
+ const line = `DEFAULT_MODEL=${modelId}`;
522
+ writeFileSync(envFile, re.test(content) ? content.replace(re, line) : content + "\n" + line + "\n", "utf-8");
523
+ process.env.DEFAULT_MODEL = modelId;
524
+ const ctxPath = join(JELLY_HOME, "context.json");
525
+ const store = existsSync(ctxPath) ? JSON.parse(readFileSync(ctxPath, "utf-8")) : {};
526
+ store.model = modelId;
527
+ // Also save as rotation slot 1 (primary)
528
+ const tier = modelReg?.getTier?.(modelId) ?? "worker";
529
+ const slots = store.rotationSlots ?? [null, null, null, null, null];
530
+ slots[0] = { id: modelId, tier };
531
+ store.rotationSlots = slots;
532
+ writeFileSync(ctxPath, JSON.stringify(store, null, 2), "utf-8");
533
+ }
534
+ catch { /* non-fatal */ }
535
+ notify(T.accent(`Model set to: ${modelId}\nRestart jellyos to apply.`));
536
+ }, [notify]);
537
+ const handleModelCancel = useCallback(() => {
538
+ setShowModelSelector(false);
539
+ }, []);
540
+ const modelList = useMemo(() => {
541
+ if (!modelReg)
542
+ return [];
543
+ const tiers = ["orchestrator", "analyst", "worker", "free"];
544
+ const items = [];
545
+ for (const tier of tiers) {
546
+ const pool = modelReg.getPool(tier);
547
+ for (const tm of pool) {
548
+ if (tm.available && tm.failures < 3) {
549
+ items.push({ id: tm.model.id, tier });
550
+ }
551
+ }
552
+ }
553
+ return items;
554
+ }, [modelReg]);
457
555
  const ctx = getContextBar(sessionRef.current);
458
556
  const statusLine = [ticker, costBadge, newsBadge, ...Object.values(statusBadges)].filter(Boolean).join(" ") || null;
557
+ // ── Sidebar helper functions ───────────────────────────────────────────────
558
+ const changeColor = (pct) => pct > 0 ? JELLY_COLORS.success : pct < 0 ? JELLY_COLORS.error : JELLY_COLORS.muted;
559
+ const changeArrow = (pct) => pct > 0 ? "▲" : pct < 0 ? "▼" : "─";
560
+ const SIDEBAR_W = 42;
561
+ // ── Overlay: model selector ────────────────────────────────────────────
562
+ if (showModelSelector) {
563
+ 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 })] }));
564
+ }
459
565
  // ── 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
566
+ 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: SIDEBAR_W, flexShrink: 0, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, children: [_jsxs(Text, { bold: true, children: ["\uD83D\uDCE1 Ticker", rotationSlots.filter(s => s && s.id).length > 0 && (_jsxs(Text, { color: "#6b7280", children: [" \u21BB", rotationSlots.filter(s => s && s.id).length] }))] }), sidebarTickers.length > 0 ? sidebarTickers.map((t, i) => {
567
+ const arrow = changeArrow(t.changePct);
568
+ const col = changeColor(t.changePct);
569
+ const pctStr = (t.changePct > 0 ? "+" : "") + t.changePct.toFixed(1) + "%";
570
+ return _jsxs(Text, { color: col, children: [t.symbol.padEnd(8), "$", String(t.price).padStart(9), " ", arrow, pctStr] }, i);
571
+ }) : _jsx(Text, { color: JELLY_COLORS.muted, children: "Loading prices\u2026" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Context" }), _jsxs(Text, { color: ctx.color, children: [ctx.bar, " ", ctx.pct, "%"] }), ctx.turboReady ? null : _jsx(Text, { color: JELLY_COLORS.warn, children: "\u26A0 no turbo" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Effect" }), _jsx(Text, { color: effectLevel === "eco" ? "#22c55e" : effectLevel === "turbo" ? "#f59e0b" : effectLevel === "max" ? "#ef4444" : JELLY_COLORS.accent, children: effectLevel.toUpperCase() })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "News" }), sidebarNews.length > 0 ? sidebarNews.map((n, i) => {
572
+ const sentColor = n.sentiment > 0.3 ? JELLY_COLORS.success : n.sentiment < -0.3 ? JELLY_COLORS.error : JELLY_COLORS.muted;
573
+ const sentPct = Math.round((n.sentiment + 1) * 50);
574
+ const title = n.title.length > 28 ? n.title.slice(0, 26) + "…" : n.title;
575
+ return (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { color: JELLY_COLORS.muted, children: title }), _jsxs(Text, { color: sentColor, children: [sentPct, "%"] })] }, i));
576
+ }) : _jsx(Text, { color: JELLY_COLORS.muted, children: "Loading news\u2026" })] }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: JELLY_COLORS.dim, paddingX: 1, marginTop: 1, children: [_jsx(Text, { bold: true, children: "Feeds" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[coingecko] SOLANA\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[coingecko] ETHEREUM\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[etherscan] BTC Price\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[etherscan] ETF Gas\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[binance] XRPUSDT 24h\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[binance] AVAXUSDT 24h\u2026" }), _jsx(Text, { color: JELLY_COLORS.muted, children: "[binance] SOLUSDT 24h\u2026" })] })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: _jsx(REPL, { messages: messages, streamingText: streaming, toolRunning: toolRunning, onSubmit: handleSubmit, disabled: disabled, onAbort: () => {
462
577
  runnerRef.current?.abort();
463
578
  setDisabled(false);
464
579
  setToolRunning(null);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ModelSelector — interactive model picker overlay.
3
+ * Renders a searchable, scrollable list of models with keyboard navigation.
4
+ *
5
+ * Up/Down: move selection
6
+ * Enter: select model
7
+ * Escape: cancel
8
+ * Type: filter models by name/provider
9
+ */
10
+ export interface ModelItem {
11
+ id: string;
12
+ tier: string;
13
+ }
14
+ export interface ModelSelectorProps {
15
+ models: ModelItem[];
16
+ currentModelId: string;
17
+ onSelect(modelId: string): void;
18
+ onCancel(): void;
19
+ initialQuery?: string;
20
+ }
21
+ export declare function ModelSelector({ models, currentModelId, onSelect, onCancel, initialQuery, }: ModelSelectorProps): import("react/jsx-runtime").JSX.Element;
22
+ //# sourceMappingURL=ModelSelector.d.ts.map
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ModelSelector — interactive model picker overlay.
4
+ * Renders a searchable, scrollable list of models with keyboard navigation.
5
+ *
6
+ * Up/Down: move selection
7
+ * Enter: select model
8
+ * Escape: cancel
9
+ * Type: filter models by name/provider
10
+ */
11
+ import { useState, useCallback, useEffect, useMemo } from "react";
12
+ import { Box, Text, useInput } from "ink";
13
+ import TextInput from "ink-text-input";
14
+ import { JELLY_COLORS, T } from "./theme.js";
15
+ const VISIBLE_COUNT = 10;
16
+ function fuzzyFilter(items, query) {
17
+ const q = query.toLowerCase().trim();
18
+ if (!q)
19
+ return items;
20
+ return items.filter((item) => {
21
+ const hay = `${item.id} ${item.tier}`.toLowerCase();
22
+ if (hay.includes(q))
23
+ return true;
24
+ let hi = 0;
25
+ for (let qi = 0; qi < q.length && hi < hay.length; hi++) {
26
+ if (hay[hi] === q[qi])
27
+ qi++;
28
+ }
29
+ return hi <= hay.length;
30
+ });
31
+ }
32
+ export function ModelSelector({ models, currentModelId, onSelect, onCancel, initialQuery = "", }) {
33
+ const [query, setQuery] = useState(initialQuery);
34
+ const [selectedIndex, setSelectedIndex] = useState(0);
35
+ const filtered = useMemo(() => fuzzyFilter(models, query), [models, query]);
36
+ useEffect(() => {
37
+ setSelectedIndex((prev) => Math.min(prev, Math.max(0, filtered.length - 1)));
38
+ }, [filtered.length]);
39
+ useEffect(() => {
40
+ const idx = models.findIndex((m) => m.id === currentModelId);
41
+ if (idx >= 0)
42
+ setSelectedIndex(idx);
43
+ }, [models, currentModelId]);
44
+ const handleSubmit = useCallback(() => {
45
+ const selected = filtered[selectedIndex];
46
+ if (selected)
47
+ onSelect(selected.id);
48
+ }, [filtered, selectedIndex, onSelect]);
49
+ useInput((input, key) => {
50
+ if (key.escape) {
51
+ onCancel();
52
+ return;
53
+ }
54
+ if (key.return) {
55
+ handleSubmit();
56
+ return;
57
+ }
58
+ if (key.upArrow) {
59
+ setSelectedIndex((prev) => (prev <= 0 ? filtered.length - 1 : prev - 1));
60
+ return;
61
+ }
62
+ if (key.downArrow) {
63
+ setSelectedIndex((prev) => (prev >= filtered.length - 1 ? 0 : prev + 1));
64
+ return;
65
+ }
66
+ if (key.backspace || key.delete) {
67
+ setQuery((prev) => prev.slice(0, -1));
68
+ return;
69
+ }
70
+ if (!key.ctrl && !key.meta && input) {
71
+ setQuery((prev) => prev + input);
72
+ return;
73
+ }
74
+ });
75
+ const startIdx = Math.max(0, Math.min(selectedIndex - Math.floor(VISIBLE_COUNT / 2), filtered.length - VISIBLE_COUNT));
76
+ const endIdx = Math.min(startIdx + VISIBLE_COUNT, filtered.length);
77
+ const visible = filtered.slice(startIdx, endIdx);
78
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", children: [_jsx(Box, { borderStyle: "round", borderColor: JELLY_COLORS.accent, marginY: 1, paddingX: 1, children: _jsx(Text, { color: JELLY_COLORS.accent, bold: true, children: "\uD83E\uDEBC Select Model" }) }), _jsxs(Box, { paddingX: 2, children: [_jsx(Text, { color: JELLY_COLORS.muted, children: " Search: " }), _jsx(TextInput, { value: query, onChange: setQuery, onSubmit: handleSubmit, placeholder: "type to filter..." })] }), _jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: JELLY_COLORS.dim, children: " \u2191\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel" }), filtered.length !== models.length && (_jsxs(Text, { color: JELLY_COLORS.muted, children: [" \u00B7 ", filtered.length, "/", models.length, " shown"] }))] }), _jsx(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, children: filtered.length === 0 ? (_jsxs(Text, { color: JELLY_COLORS.warn, children: [" No models match \"", query, "\""] })) : (visible.map((item, visIdx) => {
79
+ const realIdx = startIdx + visIdx;
80
+ const isSelected = realIdx === selectedIndex;
81
+ const isCurrent = item.id === currentModelId;
82
+ const checkMark = isCurrent ? T.success(" ✓") : "";
83
+ return (_jsxs(Text, { color: isSelected ? JELLY_COLORS.accent : undefined, children: [_jsx(Text, { color: JELLY_COLORS.accent, children: isSelected ? "→" : " " }), " ", _jsx(Text, { color: isSelected ? JELLY_COLORS.accent : JELLY_COLORS.header, bold: isSelected, children: item.id }), _jsxs(Text, { color: JELLY_COLORS.muted, children: [" [", item.tier, "]"] }), checkMark] }, item.id));
84
+ })) }), endIdx < filtered.length && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { color: JELLY_COLORS.dim, children: ["\u00B7\u00B7\u00B7 ", filtered.length - endIdx, " more (scroll down or refine search)"] }) })), _jsx(Box, { paddingX: 2, marginTop: 1, children: _jsxs(Text, { color: JELLY_COLORS.muted, children: [" (", selectedIndex + 1, "/", filtered.length, ")"] }) })] }));
85
+ }
86
+ //# sourceMappingURL=ModelSelector.js.map
package/dist/tui/REPL.js CHANGED
@@ -46,6 +46,6 @@ export function REPL({ messages, streamingText, toolRunning, onSubmit, onAbort,
46
46
  }
47
47
  });
48
48
  const visible = messages.slice(-MAX_VISIBLE);
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"] }) })] }));
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" })] })] }));
50
50
  }
51
51
  //# sourceMappingURL=REPL.js.map
@@ -1,12 +1,28 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  import { JELLY_COLORS } from "./theme.js";
4
+ import { existsSync, readFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { homedir } from "node:os";
4
7
  export function StatusBar({ model, chain, vaultLocked, effectLevel, toolRunning, connected, statusLine, }) {
5
8
  const vaultIcon = vaultLocked ? "🔒" : "🔓";
6
9
  const chainShort = chain.slice(0, 8);
7
10
  const modelShort = model.split("/").pop()?.slice(0, 18) ?? model.slice(0, 18);
8
11
  const effectIcon = { eco: "🌿", normal: "⚡", turbo: "🚀", max: "🌊" }[effectLevel] ?? "⚡";
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
12
+ // Read rotation slot count for display
13
+ let rotationCount = 0;
14
+ try {
15
+ const JELLY_HOME = process.env.JELLYOS_HOME ?? join(homedir(), ".jelly");
16
+ const ctxPath = join(JELLY_HOME, "context.json");
17
+ if (existsSync(ctxPath)) {
18
+ const store = JSON.parse(readFileSync(ctxPath, "utf-8"));
19
+ const slots = store.rotationSlots ?? [];
20
+ rotationCount = slots.filter((s) => s && s.id).length;
21
+ }
22
+ }
23
+ catch { /* non-fatal */ }
24
+ const rotationBadge = rotationCount > 0 ? `↻${rotationCount}` : null;
25
+ 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 }), rotationBadge ? _jsx(Text, { color: "#6b7280", children: rotationBadge }) : null] }), _jsx(Box, { children: toolRunning
10
26
  ? _jsxs(Text, { color: JELLY_COLORS.warn, children: ["\u2699 ", toolRunning] })
11
27
  : statusLine
12
28
  ? _jsx(Text, { color: JELLY_COLORS.muted, children: statusLine.slice(0, 80) })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jellyos/agent",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "JellyOS — standalone AI trading agent. Runs locally, no server required.",
5
5
  "author": "JellyChain",
6
6
  "license": "MIT",
@@ -34,8 +34,7 @@
34
34
  "dotenv": "^16.4.5",
35
35
  "ink": "^5.0.1",
36
36
  "ink-text-input": "^6.0.0",
37
- "react": "^18.3.1",
38
- "tsx": "^4.15.0"
37
+ "react": "^18.3.1"
39
38
  },
40
39
  "devDependencies": {
41
40
  "@types/node": "^20.19.41",
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ContextStore.test.d.ts.map
@@ -1,74 +0,0 @@
1
- /**
2
- * ContextStore tests — task lifecycle, findings, auto-delete.
3
- */
4
- import { describe, it, expect, afterEach } from "vitest";
5
- import { ContextStore } from "../session/ContextStore.js";
6
- import { existsSync } from "node:fs";
7
- // Use a temp dir for tests
8
- process.env.JELLYOS_HOME = `/tmp/jellyos-test-${Date.now()}`;
9
- describe("ContextStore", () => {
10
- let store;
11
- afterEach(() => {
12
- store = new ContextStore(); // fresh store
13
- });
14
- it("openTask creates a context.md file", () => {
15
- store = new ContextStore();
16
- const ctx = store.openTask("Test task");
17
- expect(existsSync(ctx.contextMd)).toBe(true);
18
- expect(ctx.taskId.length).toBeGreaterThan(0);
19
- expect(ctx.findings).toBe(0);
20
- });
21
- it("appendFinding increments findings count", () => {
22
- store = new ContextStore();
23
- const ctx = store.openTask("Test");
24
- store.appendFinding(ctx.taskId, "Price Data", "BTC: $70,000");
25
- store.appendFinding(ctx.taskId, "RSI", "RSI: 67 bullish");
26
- expect(store.getTask(ctx.taskId)?.findings).toBe(2);
27
- });
28
- it("getReference returns path + count info", () => {
29
- store = new ContextStore();
30
- const ctx = store.openTask("Analysis");
31
- store.appendFinding(ctx.taskId, "Data", "some findings");
32
- const ref = store.getReference(ctx.taskId);
33
- expect(ref).toContain("context.md");
34
- expect(ref).toContain("1 findings");
35
- expect(ref).toContain(ctx.taskId);
36
- });
37
- it("getActiveTasks returns open tasks", () => {
38
- store = new ContextStore();
39
- const t1 = store.openTask("Task 1");
40
- const t2 = store.openTask("Task 2");
41
- const active = store.getActiveTasks();
42
- expect(active.map(t => t.taskId)).toContain(t1.taskId);
43
- expect(active.map(t => t.taskId)).toContain(t2.taskId);
44
- });
45
- it("closeTask removes from active tasks", () => {
46
- store = new ContextStore();
47
- const ctx = store.openTask("Temp task");
48
- store.closeTask(ctx.taskId);
49
- expect(store.getTask(ctx.taskId)).toBeUndefined();
50
- });
51
- it("keepTask prevents deletion", () => {
52
- store = new ContextStore();
53
- const ctx = store.openTask("Important");
54
- store.keepTask(ctx.taskId);
55
- expect(store.getTask(ctx.taskId)?.keep).toBe(true);
56
- });
57
- it("readContextTool returns file contents", async () => {
58
- store = new ContextStore();
59
- const ctx = store.openTask("Read test");
60
- store.appendFinding(ctx.taskId, "Finding", "ETH price is $3000");
61
- const result = await store.readContextTool("", { taskId: ctx.taskId });
62
- expect(result.content[0]?.text).toContain("ETH price is $3000");
63
- });
64
- it("caps individual findings at 3000 chars", () => {
65
- store = new ContextStore();
66
- const ctx = store.openTask("Big task");
67
- const bigContent = "x".repeat(5000);
68
- store.appendFinding(ctx.taskId, "Big finding", bigContent);
69
- // Should not throw and should be capped
70
- const result = store.getReference(ctx.taskId);
71
- expect(result).toBeTruthy();
72
- });
73
- });
74
- //# sourceMappingURL=ContextStore.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=ModelRegistry.test.d.ts.map
@@ -1,69 +0,0 @@
1
- /**
2
- * ModelRegistry tests — tier classification + temperature profiles.
3
- */
4
- import { describe, it, expect } from "vitest";
5
- import { classifyModel } from "../models/ModelRegistry.js";
6
- function makeModel(id, promptPrice = "0.000005") {
7
- return {
8
- id,
9
- name: id,
10
- created: Date.now(),
11
- description: "",
12
- context_length: 128_000,
13
- architecture: { modality: "text", tokenizer: "cl100k", instruct_type: null },
14
- pricing: { prompt: promptPrice, completion: "0.000015" },
15
- top_provider: { context_length: 128_000, max_completion_tokens: 4096, is_moderated: false },
16
- };
17
- }
18
- describe("classifyModel() — 2025 models", () => {
19
- // Orchestrator tier
20
- it("classifies claude-opus-4.7 as orchestrator", () => {
21
- expect(classifyModel(makeModel("anthropic/claude-opus-4.7"))).toBe("orchestrator");
22
- });
23
- it("classifies claude-opus-4.6 as orchestrator", () => {
24
- expect(classifyModel(makeModel("anthropic/claude-opus-4.6"))).toBe("orchestrator");
25
- });
26
- it("classifies gpt-5.5 as orchestrator", () => {
27
- expect(classifyModel(makeModel("openai/gpt-5.5"))).toBe("orchestrator");
28
- });
29
- it("classifies gpt-5.5-pro as orchestrator", () => {
30
- expect(classifyModel(makeModel("openai/gpt-5.5-pro"))).toBe("orchestrator");
31
- });
32
- it("classifies gemini-3.1-pro as orchestrator", () => {
33
- expect(classifyModel(makeModel("google/gemini-3.1-pro-preview"))).toBe("orchestrator");
34
- });
35
- it("classifies deepseek-v4-pro as orchestrator", () => {
36
- expect(classifyModel(makeModel("deepseek/deepseek-v4-pro"))).toBe("orchestrator");
37
- });
38
- it("classifies grok-4.0 as orchestrator", () => {
39
- expect(classifyModel(makeModel("x-ai/grok-4.0"))).toBe("orchestrator");
40
- });
41
- // Analyst tier
42
- it("classifies claude-sonnet-4.6 as analyst", () => {
43
- expect(classifyModel(makeModel("anthropic/claude-sonnet-4.6"))).toBe("analyst");
44
- });
45
- it("classifies claude-sonnet-4.5 as analyst", () => {
46
- expect(classifyModel(makeModel("anthropic/claude-sonnet-4.5"))).toBe("analyst");
47
- });
48
- it("classifies gemini-3.5-flash as analyst", () => {
49
- expect(classifyModel(makeModel("google/gemini-3.5-flash"))).toBe("analyst");
50
- });
51
- it("classifies deepseek-v4 (non-pro) as analyst", () => {
52
- expect(classifyModel(makeModel("deepseek/deepseek-v4-flash"))).toBe("analyst");
53
- });
54
- // Free tier
55
- it("classifies :free models as free", () => {
56
- expect(classifyModel(makeModel("deepseek/deepseek-v4-flash:free", "0"))).toBe("free");
57
- });
58
- it("classifies free-priced models as free", () => {
59
- expect(classifyModel(makeModel("meta-llama/llama-3.1-8b-instruct:free", "0"))).toBe("free");
60
- });
61
- // Worker fallback
62
- it("classifies unknown model as worker", () => {
63
- expect(classifyModel(makeModel("some-unknown/model-v1"))).toBe("worker");
64
- });
65
- it("classifies small llama as worker", () => {
66
- expect(classifyModel(makeModel("meta-llama/llama-3.1-8b-instruct", "0.0002"))).toBe("worker");
67
- });
68
- });
69
- //# sourceMappingURL=ModelRegistry.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=SessionManager.test.d.ts.map
@@ -1,108 +0,0 @@
1
- /**
2
- * SessionManager tests — atomic compaction, context pressure, semantic anchors.
3
- */
4
- import { describe, it, expect, beforeEach } from "vitest";
5
- import { SessionManager } from "../session/SessionManager.js";
6
- function makeUserTurn(userContent, assistantContent) {
7
- return [
8
- { role: "user", content: userContent },
9
- { role: "assistant", content: assistantContent },
10
- ];
11
- }
12
- function makeToolTurn(userContent, toolContent) {
13
- const tc_id = `call_${Math.random().toString(36).slice(2)}`;
14
- return [
15
- { role: "user", content: userContent },
16
- { role: "assistant", content: null, tool_calls: [{ id: tc_id, type: "function", function: { name: "get_prices", arguments: "{}" } }] },
17
- { role: "tool", content: toolContent, name: "get_prices", tool_call_id: tc_id },
18
- ];
19
- }
20
- describe("SessionManager", () => {
21
- let session;
22
- beforeEach(() => {
23
- session = new SessionManager();
24
- session.setSystemPrompt("You are JellyOS.");
25
- });
26
- it("getMessages() prepends system prompt", () => {
27
- session.addMessage({ role: "user", content: "hello" });
28
- const msgs = session.getMessages();
29
- expect(msgs[0]?.role).toBe("system");
30
- expect(msgs[0]?.content).toContain("JellyOS");
31
- expect(msgs.length).toBe(2);
32
- });
33
- it("charCount() counts history chars", () => {
34
- session.addMessage({ role: "user", content: "hello" });
35
- expect(session.charCount()).toBe(5);
36
- });
37
- it("getContextPressure() returns green for small history", () => {
38
- session.addMessage({ role: "user", content: "hi" });
39
- const p = session.getContextPressure();
40
- expect(p.level).toBe("green");
41
- expect(p.turboReady).toBe(true);
42
- expect(p.pct).toBeLessThan(50);
43
- });
44
- it("getContextPressure() reports yellow at 60%", () => {
45
- // Add ~48KB of content (60% of 80KB)
46
- const bigMsg = "x".repeat(48_000);
47
- session.addMessage({ role: "user", content: bigMsg });
48
- const p = session.getContextPressure();
49
- expect(p.pct).toBeGreaterThanOrEqual(50);
50
- });
51
- it("turboReady is false when context >70%", () => {
52
- const bigMsg = "x".repeat(60_000);
53
- session.addMessage({ role: "user", content: bigMsg });
54
- const p = session.getContextPressure();
55
- expect(p.turboReady).toBe(false);
56
- });
57
- it("compaction never orphans tool_call_id pairs", () => {
58
- // Add enough turns to trigger compaction
59
- for (let i = 0; i < 20; i++) {
60
- for (const msg of makeToolTurn(`question ${i}`, `tool result ${i} — ${"data".repeat(200)}`)) {
61
- session.addMessage(msg);
62
- }
63
- }
64
- const history = session.getHistory();
65
- // Find all assistant messages with tool_calls
66
- const toolCallMsgs = history.filter(m => m.tool_calls && m.tool_calls.length > 0);
67
- for (const tcMsg of toolCallMsgs) {
68
- for (const tc of tcMsg.tool_calls) {
69
- // Every tool_call_id must have a corresponding tool result
70
- const hasResult = history.some(m => m.role === "tool" && m.tool_call_id === tc.id);
71
- expect(hasResult).toBe(true);
72
- }
73
- }
74
- });
75
- it("forceCompact() reduces history", () => {
76
- for (let i = 0; i < 30; i++) {
77
- for (const msg of makeUserTurn(`q${i}`, `a${i}`)) {
78
- session.addMessage(msg);
79
- }
80
- }
81
- const before = session.getHistory().length;
82
- session.forceCompact();
83
- const after = session.getHistory().length;
84
- expect(after).toBeLessThanOrEqual(before);
85
- });
86
- it("clear() empties history", () => {
87
- session.addMessage({ role: "user", content: "hello" });
88
- session.clear();
89
- expect(session.getHistory().length).toBe(0);
90
- });
91
- });
92
- describe("SessionManager — SwarmRouter integration", () => {
93
- it("scoreComplexity returns low score for simple price check", async () => {
94
- const { scoreComplexity } = await import("../runner/SwarmRouter.js");
95
- expect(scoreComplexity("what is the price of ETH")).toBeLessThan(40);
96
- });
97
- it("scoreComplexity returns high score for complex multi-chain analysis", async () => {
98
- const { scoreComplexity } = await import("../runner/SwarmRouter.js");
99
- const score = scoreComplexity("analyze ETH and BTC then compare their RSI and predict which will pump first");
100
- expect(score).toBeGreaterThanOrEqual(40);
101
- });
102
- it("decomposeHeuristic splits on conjunctions", async () => {
103
- const { decomposeHeuristic } = await import("../runner/SwarmRouter.js");
104
- const tasks = decomposeHeuristic("analyze ETH and check BTC mempool and get SOL TPS", 5);
105
- expect(tasks.length).toBeGreaterThanOrEqual(2);
106
- });
107
- });
108
- //# sourceMappingURL=SessionManager.test.js.map
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=TechnicalAnalysis.test.d.ts.map
@@ -1,109 +0,0 @@
1
- /**
2
- * TechnicalAnalysis tests — pure math functions, no network calls.
3
- */
4
- import { describe, it, expect } from "vitest";
5
- import { rsi, macd, ema, sma, bollingerBands, rsiSignal, macdSignal } from "../tools/TechnicalAnalysis.js";
6
- // Generate price series for testing
7
- const risingPrices = Array.from({ length: 60 }, (_, i) => 100 + i * 2); // 100→218
8
- const fallingPrices = Array.from({ length: 60 }, (_, i) => 220 - i * 2); // 220→102
9
- const flatPrices = Array.from({ length: 60 }, () => 150);
10
- describe("sma()", () => {
11
- it("returns empty array when insufficient data", () => {
12
- expect(sma([100, 99], 14)).toEqual([]);
13
- });
14
- it("calculates correct 3-period SMA", () => {
15
- const result = sma([10, 20, 30, 40], 3);
16
- expect(result[0]).toBeCloseTo(20, 2); // (10+20+30)/3
17
- expect(result[1]).toBeCloseTo(30, 2); // (20+30+40)/3
18
- });
19
- it("result length = prices.length - period + 1", () => {
20
- const result = sma(risingPrices, 20);
21
- expect(result.length).toBe(risingPrices.length - 20 + 1);
22
- });
23
- });
24
- describe("ema()", () => {
25
- it("returns first price as first EMA value", () => {
26
- const result = ema([100, 110, 120], 3);
27
- expect(result[0]).toBe(100);
28
- });
29
- it("EMA tracks price direction", () => {
30
- const result = ema(risingPrices, 12);
31
- expect(result[result.length - 1]).toBeGreaterThan(result[0]);
32
- });
33
- });
34
- describe("rsi()", () => {
35
- it("returns empty array when insufficient data", () => {
36
- expect(rsi([100, 99], 14)).toEqual([]);
37
- });
38
- it("returns oversold (<30) for falling prices", () => {
39
- const result = rsi(fallingPrices, 14);
40
- expect(result[result.length - 1]).toBeLessThan(30);
41
- });
42
- it("returns overbought (>70) for rising prices", () => {
43
- const result = rsi(risingPrices, 14);
44
- expect(result[result.length - 1]).toBeGreaterThan(70);
45
- });
46
- it("returns neutral (~50) for flat prices", () => {
47
- // RSI is undefined for perfectly flat — but should handle gracefully
48
- const result = rsi(flatPrices, 14);
49
- // avgLoss = 0, so RSI = 100 (no change)
50
- expect(result.length).toBeGreaterThan(0);
51
- });
52
- it("all values are in [0, 100]", () => {
53
- const result = rsi(risingPrices, 14);
54
- for (const v of result) {
55
- expect(v).toBeGreaterThanOrEqual(0);
56
- expect(v).toBeLessThanOrEqual(100);
57
- }
58
- });
59
- });
60
- describe("rsiSignal()", () => {
61
- it("returns bearish for overbought RSI", () => {
62
- const sig = rsiSignal([75]);
63
- expect(sig.signal).toBe("bearish");
64
- expect(sig.metadata?.condition).toBe("overbought");
65
- });
66
- it("returns bullish for oversold RSI", () => {
67
- const sig = rsiSignal([25]);
68
- expect(sig.signal).toBe("bullish");
69
- expect(sig.metadata?.condition).toBe("oversold");
70
- });
71
- it("returns neutral for mid-range RSI", () => {
72
- const sig = rsiSignal([50]);
73
- expect(sig.signal).toBe("neutral");
74
- });
75
- });
76
- describe("macd()", () => {
77
- it("returns empty arrays for insufficient data", () => {
78
- const result = macd([100, 99], 12, 26, 9);
79
- expect(result.signal).toEqual([]);
80
- });
81
- it("produces histogram for sufficient data", () => {
82
- const result = macd(risingPrices, 12, 26, 9);
83
- expect(result.macd.length).toBeGreaterThan(0);
84
- expect(result.histogram.length).toBeGreaterThan(0);
85
- });
86
- });
87
- describe("macdSignal()", () => {
88
- it("returns neutral for empty histogram", () => {
89
- expect(macdSignal({ macd: [], signal: [], histogram: [] }).signal).toBe("neutral");
90
- });
91
- it("returns bullish when histogram crosses above zero", () => {
92
- const sig = macdSignal({ macd: [0.1], signal: [0], histogram: [-0.1, 0.1] });
93
- expect(sig.signal).toBe("bullish");
94
- });
95
- });
96
- describe("bollingerBands()", () => {
97
- it("upper band > middle > lower band", () => {
98
- const bands = bollingerBands(risingPrices, 20, 2);
99
- const last = bands.upper.length - 1;
100
- expect(bands.upper[last]).toBeGreaterThan(bands.middle[last]);
101
- expect(bands.middle[last]).toBeGreaterThan(bands.lower[last]);
102
- });
103
- it("flat prices produce narrow bands", () => {
104
- const bands = bollingerBands(flatPrices, 20, 2);
105
- const last = bands.width.length - 1;
106
- expect(bands.width[last]).toBeLessThan(1); // near-zero width for flat prices
107
- });
108
- });
109
- //# sourceMappingURL=TechnicalAnalysis.test.js.map