@jellyos/agent 0.1.5 → 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.
@@ -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,9 @@ 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
+ // ── Model selector overlay state ──────────────────────────────────────
100
+ const [showModelSelector, setShowModelSelector] = useState(false);
101
+ const [modelSelectorInitialQuery, setModelSelectorInitialQuery] = useState("");
98
102
  const runnerRef = useRef(null);
99
103
  const sessionRef = useRef(null);
100
104
  const sessionCtxRef = useRef(null);
@@ -119,7 +123,16 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
119
123
  if (key === "effect_level")
120
124
  setEffectLevel(value);
121
125
  }, []);
122
- const uiCtx = { notify, setStatus, setTheme(_n) { }, setHeader(_f) { }, theme };
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
+ };
123
136
  // Register built-in tools
124
137
  function registerBuiltinTools() {
125
138
  if (!modelReg)
@@ -165,6 +178,13 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
165
178
  } });
166
179
  }
167
180
  useEffect(() => { onNotifyReady?.(notify); onStatusReady?.(setStatus); }, []);
181
+ // ── Expose model selector trigger to extensions ──────────────────────────
182
+ useEffect(() => {
183
+ onModelSelectorReady?.((initialQuery) => {
184
+ setModelSelectorInitialQuery(initialQuery ?? "");
185
+ setShowModelSelector(true);
186
+ });
187
+ }, [onModelSelectorReady]);
168
188
  useEffect(() => {
169
189
  registerBuiltinTools();
170
190
  priceFeed.track("btc", "eth", "sol", "bnb", "matic", "arb", "op", "avax", "link", "uni", "doge", "xrp", "ada", "dot", "atom", "near", "sui", "apt", "pepe", "aave");
@@ -454,11 +474,55 @@ export function App({ registry, systemPrompt, effectLevel: initialEffect = "norm
454
474
  notify(T.error(`Error: ${e.message}`));
455
475
  }
456
476
  }, [registry, exit, push, notify, uiCtx, modelReg, costTracker]);
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]);
457
518
  const ctx = getContextBar(sessionRef.current);
458
519
  const statusLine = [ticker, costBadge, newsBadge, ...Object.values(statusBadges)].filter(Boolean).join(" ") || null;
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
+ }
459
524
  // ── Multi-pane layout ────────────────────────────────────────────────────
460
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: () => {
461
- // #25: Escape aborts in-flight stream
462
526
  runnerRef.current?.abort();
463
527
  setDisabled(false);
464
528
  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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jellyos/agent",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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