@spfunctions/cli 1.4.4 → 1.5.0

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 (86) hide show
  1. package/README.md +205 -48
  2. package/dist/cache.d.ts +6 -0
  3. package/dist/cache.js +31 -0
  4. package/dist/cache.test.d.ts +1 -0
  5. package/dist/cache.test.js +73 -0
  6. package/dist/client.test.d.ts +1 -0
  7. package/dist/client.test.js +89 -0
  8. package/dist/commands/agent.js +594 -106
  9. package/dist/commands/book.d.ts +17 -0
  10. package/dist/commands/book.js +220 -0
  11. package/dist/commands/dashboard.d.ts +6 -3
  12. package/dist/commands/dashboard.js +53 -22
  13. package/dist/commands/liquidity.d.ts +2 -0
  14. package/dist/commands/liquidity.js +128 -43
  15. package/dist/commands/performance.js +9 -2
  16. package/dist/commands/positions.js +50 -0
  17. package/dist/commands/scan.d.ts +1 -0
  18. package/dist/commands/scan.js +66 -15
  19. package/dist/commands/setup.d.ts +1 -0
  20. package/dist/commands/setup.js +71 -6
  21. package/dist/commands/telegram.d.ts +15 -0
  22. package/dist/commands/telegram.js +125 -0
  23. package/dist/config.d.ts +3 -0
  24. package/dist/config.js +9 -0
  25. package/dist/config.test.d.ts +1 -0
  26. package/dist/config.test.js +138 -0
  27. package/dist/index.js +107 -9
  28. package/dist/polymarket.d.ts +237 -0
  29. package/dist/polymarket.js +353 -0
  30. package/dist/polymarket.test.d.ts +1 -0
  31. package/dist/polymarket.test.js +424 -0
  32. package/dist/telegram/agent-bridge.d.ts +15 -0
  33. package/dist/telegram/agent-bridge.js +368 -0
  34. package/dist/telegram/bot.d.ts +10 -0
  35. package/dist/telegram/bot.js +297 -0
  36. package/dist/telegram/commands.d.ts +11 -0
  37. package/dist/telegram/commands.js +120 -0
  38. package/dist/telegram/format.d.ts +11 -0
  39. package/dist/telegram/format.js +51 -0
  40. package/dist/telegram/format.test.d.ts +1 -0
  41. package/dist/telegram/format.test.js +73 -0
  42. package/dist/telegram/poller.d.ts +6 -0
  43. package/dist/telegram/poller.js +32 -0
  44. package/dist/topics.d.ts +3 -0
  45. package/dist/topics.js +65 -7
  46. package/dist/topics.test.d.ts +1 -0
  47. package/dist/topics.test.js +131 -0
  48. package/dist/tui/border.d.ts +33 -0
  49. package/dist/tui/border.js +87 -0
  50. package/dist/tui/chart.d.ts +19 -0
  51. package/dist/tui/chart.js +117 -0
  52. package/dist/tui/dashboard.d.ts +9 -0
  53. package/dist/tui/dashboard.js +814 -0
  54. package/dist/tui/layout.d.ts +16 -0
  55. package/dist/tui/layout.js +41 -0
  56. package/dist/tui/screen.d.ts +33 -0
  57. package/dist/tui/screen.js +102 -0
  58. package/dist/tui/state.d.ts +40 -0
  59. package/dist/tui/state.js +36 -0
  60. package/dist/tui/widgets/commandbar.d.ts +8 -0
  61. package/dist/tui/widgets/commandbar.js +82 -0
  62. package/dist/tui/widgets/detail.d.ts +9 -0
  63. package/dist/tui/widgets/detail.js +151 -0
  64. package/dist/tui/widgets/edges.d.ts +4 -0
  65. package/dist/tui/widgets/edges.js +34 -0
  66. package/dist/tui/widgets/liquidity.d.ts +9 -0
  67. package/dist/tui/widgets/liquidity.js +142 -0
  68. package/dist/tui/widgets/orders.d.ts +4 -0
  69. package/dist/tui/widgets/orders.js +37 -0
  70. package/dist/tui/widgets/portfolio.d.ts +4 -0
  71. package/dist/tui/widgets/portfolio.js +59 -0
  72. package/dist/tui/widgets/signals.d.ts +4 -0
  73. package/dist/tui/widgets/signals.js +31 -0
  74. package/dist/tui/widgets/statusbar.d.ts +8 -0
  75. package/dist/tui/widgets/statusbar.js +72 -0
  76. package/dist/tui/widgets/thesis.d.ts +4 -0
  77. package/dist/tui/widgets/thesis.js +66 -0
  78. package/dist/tui/widgets/trade.d.ts +9 -0
  79. package/dist/tui/widgets/trade.js +117 -0
  80. package/dist/tui/widgets/upcoming.d.ts +4 -0
  81. package/dist/tui/widgets/upcoming.js +41 -0
  82. package/dist/tui/widgets/whatif.d.ts +7 -0
  83. package/dist/tui/widgets/whatif.js +113 -0
  84. package/dist/utils.test.d.ts +1 -0
  85. package/dist/utils.test.js +111 -0
  86. package/package.json +6 -2
package/README.md CHANGED
@@ -1,92 +1,249 @@
1
1
  # SimpleFunctions CLI (`sf`)
2
2
 
3
- Prediction market thesis agent CLI. Pure HTTP clientno project dependencies.
3
+ Prediction market intelligence CLI. Build causal thesis models, scan Kalshi/Polymarket for mispricings, detect edges, and trade all from the terminal.
4
4
 
5
- ## Install
5
+ ## Quick Start
6
6
 
7
7
  ```bash
8
8
  npm install -g @spfunctions/cli
9
+ sf setup # interactive config wizard
10
+ sf list # see your theses
11
+ sf context <id> --json # get thesis state as JSON
9
12
  ```
10
13
 
11
- ## Configuration
14
+ ## Setup
15
+
16
+ ### Interactive (recommended)
12
17
 
13
18
  ```bash
14
- export SF_API_KEY=sf_live_xxx # required
15
- export SF_API_URL=https://simplefunctions.dev # optional, defaults to production
19
+ sf setup
16
20
  ```
17
21
 
18
- Or pass inline:
22
+ This walks you through:
23
+ 1. **SF API key** (required) — get one at [simplefunctions.dev](https://simplefunctions.dev)
24
+ 2. **Kalshi credentials** (optional) — for positions, trading, and orderbook data
25
+ 3. **Trading mode** (optional) — enable `sf buy`/`sf sell` commands
26
+
27
+ Config is saved to `~/.sf/config.json`. Environment variables override config values.
28
+
29
+ ### Manual
30
+
19
31
  ```bash
20
- sf --api-key sf_live_xxx list
32
+ export SF_API_KEY=sf_live_xxx # required
33
+ export KALSHI_API_KEY_ID=xxx # optional, for positions/trading
34
+ export KALSHI_PRIVATE_KEY_PATH=~/.ssh/kalshi.pem # optional, for positions/trading
21
35
  ```
22
36
 
23
- ## Commands
37
+ ### Verify
24
38
 
25
- ### `sf list`
26
- List all theses.
27
- ```
28
- ID Status Conf Updated Title
29
- f582bf76 active 82% Mar 12 11:13 Trump cannot exit the Iran war...
39
+ ```bash
40
+ sf setup --check # show current config status
41
+ sf list # should show your theses
30
42
  ```
31
43
 
32
- ### `sf get <id>`
33
- Full thesis details: causal tree, edge analysis, positions, last evaluation.
44
+ ## Commands
45
+
46
+ ### Thesis Management
47
+
48
+ | Command | Description |
49
+ |---------|-------------|
50
+ | `sf list` | List all theses with status, confidence, and update time |
51
+ | `sf get <id>` | Full thesis details: causal tree, edges, positions, last evaluation |
52
+ | `sf context <id>` | Compact context snapshot (primary command for agents) |
53
+ | `sf create "thesis"` | Create a new thesis (waits for formation by default) |
54
+ | `sf signal <id> "text"` | Inject a signal (news, observation) for next evaluation |
55
+ | `sf evaluate <id>` | Trigger deep evaluation with heavy model |
56
+ | `sf publish <id>` | Make thesis publicly viewable |
57
+ | `sf unpublish <id>` | Remove from public view |
58
+
59
+ ### Market Exploration (no auth required)
60
+
61
+ | Command | Description |
62
+ |---------|-------------|
63
+ | `sf scan "keywords"` | Search Kalshi markets by keyword |
64
+ | `sf scan --series KXWTIMAX` | List all markets in a series |
65
+ | `sf scan --market TICKER` | Get single market detail |
66
+ | `sf explore` | Browse public theses |
67
+
68
+ ### Portfolio & Trading (requires Kalshi credentials)
69
+
70
+ | Command | Description |
71
+ |---------|-------------|
72
+ | `sf edges` | Top edges across all theses — what to trade now |
73
+ | `sf positions` | Current positions with P&L and edge overlay |
74
+ | `sf balance` | Account balance |
75
+ | `sf orders` | Resting (open) orders |
76
+ | `sf fills` | Recent trade fills |
77
+ | `sf performance` | P&L over time with thesis event annotations |
78
+ | `sf settlements` | Settled contracts with final P&L |
79
+ | `sf liquidity` | Market liquidity scanner by topic |
80
+
81
+ ### Trading (requires `sf setup --enable-trading`)
82
+
83
+ | Command | Description |
84
+ |---------|-------------|
85
+ | `sf buy <ticker> <qty>` | Buy contracts |
86
+ | `sf sell <ticker> <qty>` | Sell contracts |
87
+ | `sf cancel [orderId]` | Cancel order(s) |
88
+ | `sf rfq <ticker> <qty>` | Request for quote on large orders |
89
+
90
+ ### Analysis
91
+
92
+ | Command | Description |
93
+ |---------|-------------|
94
+ | `sf whatif <id>` | What-if scenario: "if node X drops to 10%..." |
95
+ | `sf feed` | Evaluation history stream |
96
+ | `sf forecast <event>` | Market distribution forecast (P50/P75/P90) |
97
+ | `sf dashboard` | Interactive TUI portfolio overview |
98
+
99
+ ### Interactive Modes
100
+
101
+ | Command | Description |
102
+ |---------|-------------|
103
+ | `sf agent [id]` | Interactive agent with natural language + tool calling |
104
+ | `sf telegram` | Telegram bot for monitoring and trading |
105
+
106
+ ## JSON Output
107
+
108
+ Most commands support `--json` for machine-readable output:
109
+
34
110
  ```bash
35
- sf get f582bf76
111
+ sf list --json
112
+ sf context f582bf76 --json
113
+ sf edges --json
114
+ sf scan "oil" --json
36
115
  sf get f582bf76 --json
37
116
  ```
38
117
 
39
- ### `sf context <id>`
40
- **Primary command for agents.** Returns a compact context snapshot: thesis, confidence, causal tree nodes, top edges, positions, last evaluation summary.
118
+ ## For AI Agents
119
+
120
+ **If you are an AI coding agent (Claude, GPT, OpenClaw, Cursor, etc.), follow these steps:**
121
+
122
+ ### Step 1: Install and configure
123
+
41
124
  ```bash
42
- sf context f582bf76
43
- sf context f582bf76 --json # machine-readable for agent parsing
125
+ npm install -g @spfunctions/cli
126
+ sf setup --key sf_live_YOUR_KEY_HERE
44
127
  ```
45
128
 
46
- ### `sf create "thesis text"`
47
- Create a new thesis. Sync by default (waits for formation agent to complete).
129
+ If `sf setup` is not available or you cannot run interactive commands:
48
130
  ```bash
49
- sf create "Trump cannot exit the Iran war gracefully before 2027"
50
- sf create "..." --async # return immediately
131
+ export SF_API_KEY=sf_live_YOUR_KEY_HERE
51
132
  ```
52
133
 
53
- ### `sf signal <id> "content"`
54
- Inject a signal into the thesis queue. Queued for next monitor cycle.
134
+ ### Step 2: Verify it works
135
+
55
136
  ```bash
56
- sf signal f582bf76 "Oil closes at $95 today"
57
- sf signal f582bf76 "Iran closes Strait of Hormuz" --type news
58
- sf signal f582bf76 "My read: escalation likely" --type user_note
137
+ sf list
138
+ ```
139
+
140
+ Expected output (table format):
141
+ ```
142
+ ID Status Conf Updated Title
143
+ f582bf76 active 82% Mar 12 11:13 Oil prices will exceed $100...
144
+ a1b2c3d4 active 65% Mar 11 09:30 Fed will cut rates by June...
59
145
  ```
60
- Signal types: `news` | `user_note` | `external` (default: `user_note`)
61
146
 
62
- ### `sf evaluate <id>`
63
- Trigger a deep evaluation using the heavy model (Claude Opus).
147
+ If you see theses, the CLI is working. If you see an error, check that `SF_API_KEY` is set correctly.
148
+
149
+ ### Step 3: Get thesis context (most important command)
150
+
64
151
  ```bash
65
- sf evaluate f582bf76
152
+ sf context <thesisId> --json
66
153
  ```
67
154
 
68
- ### `sf scan "keywords"`
69
- Explore Kalshi markets directly (no auth required).
155
+ This returns a JSON object with the complete thesis state:
156
+
157
+ ```json
158
+ {
159
+ "thesisId": "f582bf76-3113-4208-b0c1-...",
160
+ "thesis": "Oil prices will exceed $100 by end of 2026",
161
+ "title": "Oil Bull Thesis",
162
+ "status": "active",
163
+ "confidence": 0.82,
164
+ "causalTree": {
165
+ "rootClaim": "Oil prices will exceed $100",
166
+ "nodes": [
167
+ {
168
+ "id": "n1",
169
+ "label": "Supply disruption",
170
+ "probability": 0.75,
171
+ "importance": 0.6,
172
+ "depth": 0
173
+ }
174
+ ]
175
+ },
176
+ "edges": [
177
+ {
178
+ "marketId": "KXWTIMAX-26DEC31-T100",
179
+ "market": "Will oil exceed $100 by Dec 2026?",
180
+ "venue": "kalshi",
181
+ "direction": "yes",
182
+ "marketPrice": 35,
183
+ "thesisPrice": 55,
184
+ "edge": 20,
185
+ "confidence": 0.8
186
+ }
187
+ ],
188
+ "lastEvaluation": {
189
+ "summary": "Supply concerns rising due to...",
190
+ "newConfidence": 0.82,
191
+ "confidenceDelta": 0.03
192
+ }
193
+ }
194
+ ```
195
+
196
+ **Key fields:**
197
+ - `confidence` — overall thesis probability (0 to 1)
198
+ - `edges[].edge` — mispricing size in cents (positive = market underpriced vs thesis)
199
+ - `edges[].marketPrice` — current market price in cents (0-100)
200
+ - `edges[].thesisPrice` — what the thesis model thinks the price should be
201
+ - `lastEvaluation.summary` — human-readable summary of latest analysis
202
+
203
+ ### Step 4: Other useful commands
204
+
70
205
  ```bash
71
- sf scan "oil recession iran"
72
- sf scan --series KXWTIMAX
73
- sf scan --market KXWTIMAX-26DEC31-T140
74
- sf scan "oil" --json
206
+ # Inject a signal for the thesis to consider in its next evaluation
207
+ sf signal <thesisId> "Breaking: OPEC announces production cut" --type news
208
+
209
+ # View top edges (mispricings) across all theses
210
+ sf edges --json
211
+
212
+ # Search Kalshi markets by keyword
213
+ sf scan "recession" --json
214
+
215
+ # Trigger a deep re-evaluation
216
+ sf evaluate <thesisId>
217
+
218
+ # What-if analysis: what happens if a node probability changes?
219
+ sf whatif <thesisId>
75
220
  ```
76
221
 
77
- ## For AI Agents (OpenClaw etc.)
222
+ ### Common patterns for agents
78
223
 
79
- After `npm install -g simplefunctions` and setting `SF_API_KEY`:
224
+ **Monitor a thesis:**
225
+ ```bash
226
+ sf context <id> --json # poll periodically, check confidence changes
227
+ ```
80
228
 
229
+ **React to news:**
230
+ ```bash
231
+ sf signal <id> "Reuters: Iran nuclear deal collapses" --type news
232
+ sf evaluate <id> # trigger re-evaluation after injecting signal
233
+ sf context <id> --json # read updated state
81
234
  ```
82
- You can use the sf CLI to interact with SimpleFunctions:
83
- - sf context <id> --json Get current thesis state (JSON)
84
- - sf signal <id> "content" Inject an observation note
85
- - sf list List all theses
86
- - sf scan "keywords" Explore Kalshi markets
235
+
236
+ **Find trading opportunities:**
237
+ ```bash
238
+ sf edges --json # get top mispricings sorted by edge size
87
239
  ```
88
240
 
89
- Agents should call `sf context <id> --json` periodically to get the latest state, then decide whether to inject signals or alert the user.
241
+ ### Error handling
242
+
243
+ - **"API key required"** — set `SF_API_KEY` env var or run `sf setup --key <key>`
244
+ - **"Thesis not found"** — use `sf list` to get valid thesis IDs. IDs can be short prefixes (first 8 chars)
245
+ - **"Kalshi not configured"** — positions/trading commands need Kalshi credentials via `sf setup`
246
+ - **Exit code 0** — success. **Exit code 1** — error (message printed to stderr)
90
247
 
91
248
  ## Local Development
92
249
 
@@ -95,6 +252,6 @@ cd cli
95
252
  npm install
96
253
  npm run dev -- list # run without building
97
254
  npm run build # compile to dist/
255
+ npm run test # run unit tests
98
256
  npm link # install as global 'sf' command
99
- sf list
100
257
  ```
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Simple in-memory TTL cache for dashboard data
3
+ */
4
+ export declare function cached<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T>;
5
+ export declare function invalidate(key: string): void;
6
+ export declare function invalidateAll(): void;
package/dist/cache.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * Simple in-memory TTL cache for dashboard data
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.cached = cached;
7
+ exports.invalidate = invalidate;
8
+ exports.invalidateAll = invalidateAll;
9
+ const store = new Map();
10
+ async function cached(key, ttlMs, fn) {
11
+ const hit = store.get(key);
12
+ if (hit && Date.now() < hit.expiry)
13
+ return hit.data;
14
+ try {
15
+ const data = await fn();
16
+ store.set(key, { data, expiry: Date.now() + ttlMs });
17
+ return data;
18
+ }
19
+ catch (err) {
20
+ // On error, return stale data if available
21
+ if (hit)
22
+ return hit.data;
23
+ throw err;
24
+ }
25
+ }
26
+ function invalidate(key) {
27
+ store.delete(key);
28
+ }
29
+ function invalidateAll() {
30
+ store.clear();
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const cache_js_1 = require("./cache.js");
5
+ (0, vitest_1.beforeEach)(() => {
6
+ (0, cache_js_1.invalidateAll)();
7
+ vitest_1.vi.useRealTimers();
8
+ });
9
+ (0, vitest_1.describe)('cached', () => {
10
+ (0, vitest_1.it)('returns fresh data on cache miss', async () => {
11
+ const fn = vitest_1.vi.fn().mockResolvedValue('data');
12
+ const result = await (0, cache_js_1.cached)('key1', 1000, fn);
13
+ (0, vitest_1.expect)(result).toBe('data');
14
+ (0, vitest_1.expect)(fn).toHaveBeenCalledOnce();
15
+ });
16
+ (0, vitest_1.it)('returns cached data on hit without calling fn', async () => {
17
+ const fn = vitest_1.vi.fn().mockResolvedValue('data');
18
+ await (0, cache_js_1.cached)('key2', 5000, fn);
19
+ const result = await (0, cache_js_1.cached)('key2', 5000, fn);
20
+ (0, vitest_1.expect)(result).toBe('data');
21
+ (0, vitest_1.expect)(fn).toHaveBeenCalledOnce();
22
+ });
23
+ (0, vitest_1.it)('expires after TTL', async () => {
24
+ vitest_1.vi.useFakeTimers();
25
+ const fn = vitest_1.vi.fn()
26
+ .mockResolvedValueOnce('first')
27
+ .mockResolvedValueOnce('second');
28
+ await (0, cache_js_1.cached)('key3', 1000, fn);
29
+ vitest_1.vi.advanceTimersByTime(1500);
30
+ const result = await (0, cache_js_1.cached)('key3', 1000, fn);
31
+ (0, vitest_1.expect)(result).toBe('second');
32
+ (0, vitest_1.expect)(fn).toHaveBeenCalledTimes(2);
33
+ });
34
+ (0, vitest_1.it)('returns stale data on error when cache exists', async () => {
35
+ vitest_1.vi.useFakeTimers();
36
+ const fn = vitest_1.vi.fn()
37
+ .mockResolvedValueOnce('stale')
38
+ .mockRejectedValueOnce(new Error('fail'));
39
+ await (0, cache_js_1.cached)('key4', 1000, fn);
40
+ vitest_1.vi.advanceTimersByTime(1500);
41
+ const result = await (0, cache_js_1.cached)('key4', 1000, fn);
42
+ (0, vitest_1.expect)(result).toBe('stale');
43
+ });
44
+ (0, vitest_1.it)('throws on error when no stale data', async () => {
45
+ const fn = vitest_1.vi.fn().mockRejectedValue(new Error('fail'));
46
+ await (0, vitest_1.expect)((0, cache_js_1.cached)('key5', 1000, fn)).rejects.toThrow('fail');
47
+ });
48
+ });
49
+ (0, vitest_1.describe)('invalidate', () => {
50
+ (0, vitest_1.it)('clears a specific key', async () => {
51
+ const fn = vitest_1.vi.fn()
52
+ .mockResolvedValueOnce('first')
53
+ .mockResolvedValueOnce('second');
54
+ await (0, cache_js_1.cached)('key6', 60000, fn);
55
+ (0, cache_js_1.invalidate)('key6');
56
+ const result = await (0, cache_js_1.cached)('key6', 60000, fn);
57
+ (0, vitest_1.expect)(result).toBe('second');
58
+ (0, vitest_1.expect)(fn).toHaveBeenCalledTimes(2);
59
+ });
60
+ });
61
+ (0, vitest_1.describe)('invalidateAll', () => {
62
+ (0, vitest_1.it)('clears all keys', async () => {
63
+ const fn1 = vitest_1.vi.fn().mockResolvedValue('a');
64
+ const fn2 = vitest_1.vi.fn().mockResolvedValue('b');
65
+ await (0, cache_js_1.cached)('x', 60000, fn1);
66
+ await (0, cache_js_1.cached)('y', 60000, fn2);
67
+ (0, cache_js_1.invalidateAll)();
68
+ await (0, cache_js_1.cached)('x', 60000, fn1);
69
+ await (0, cache_js_1.cached)('y', 60000, fn2);
70
+ (0, vitest_1.expect)(fn1).toHaveBeenCalledTimes(2);
71
+ (0, vitest_1.expect)(fn2).toHaveBeenCalledTimes(2);
72
+ });
73
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const client_js_1 = require("./client.js");
5
+ const mockFetch = vitest_1.vi.fn();
6
+ vitest_1.vi.stubGlobal('fetch', mockFetch);
7
+ (0, vitest_1.beforeEach)(() => {
8
+ mockFetch.mockReset();
9
+ delete process.env.SF_API_KEY;
10
+ delete process.env.SF_API_URL;
11
+ });
12
+ (0, vitest_1.describe)('SFClient constructor', () => {
13
+ (0, vitest_1.it)('throws when no API key provided', () => {
14
+ (0, vitest_1.expect)(() => new client_js_1.SFClient()).toThrow('API key required');
15
+ });
16
+ (0, vitest_1.it)('accepts explicit API key', () => {
17
+ (0, vitest_1.expect)(() => new client_js_1.SFClient('sf_live_test')).not.toThrow();
18
+ });
19
+ (0, vitest_1.it)('reads from env', () => {
20
+ process.env.SF_API_KEY = 'sf_live_env';
21
+ (0, vitest_1.expect)(() => new client_js_1.SFClient()).not.toThrow();
22
+ });
23
+ (0, vitest_1.it)('strips trailing slash from base URL', () => {
24
+ const client = new client_js_1.SFClient('key', 'https://example.com/');
25
+ mockFetch.mockResolvedValue({ ok: true, json: () => Promise.resolve({ theses: [] }) });
26
+ client.listTheses();
27
+ (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://example.com/api/thesis', vitest_1.expect.any(Object));
28
+ });
29
+ });
30
+ (0, vitest_1.describe)('SFClient requests', () => {
31
+ let client;
32
+ (0, vitest_1.beforeEach)(() => {
33
+ client = new client_js_1.SFClient('sf_live_testkey', 'https://api.test.com');
34
+ });
35
+ (0, vitest_1.it)('sends GET with correct headers', async () => {
36
+ mockFetch.mockResolvedValue({
37
+ ok: true,
38
+ json: () => Promise.resolve({ theses: [] }),
39
+ });
40
+ await client.listTheses();
41
+ (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/api/thesis', vitest_1.expect.objectContaining({
42
+ method: 'GET',
43
+ headers: vitest_1.expect.objectContaining({
44
+ Authorization: 'Bearer sf_live_testkey',
45
+ 'Content-Type': 'application/json',
46
+ }),
47
+ }));
48
+ });
49
+ (0, vitest_1.it)('sends POST with JSON body', async () => {
50
+ mockFetch.mockResolvedValue({
51
+ ok: true,
52
+ json: () => Promise.resolve({ id: '123' }),
53
+ });
54
+ await client.injectSignal('thesis1', 'news', 'breaking news');
55
+ const call = mockFetch.mock.calls[0];
56
+ (0, vitest_1.expect)(call[1].method).toBe('POST');
57
+ (0, vitest_1.expect)(JSON.parse(call[1].body)).toEqual({
58
+ type: 'news',
59
+ content: 'breaking news',
60
+ source: 'cli',
61
+ });
62
+ });
63
+ (0, vitest_1.it)('throws on non-ok response', async () => {
64
+ mockFetch.mockResolvedValue({
65
+ ok: false,
66
+ status: 404,
67
+ text: () => Promise.resolve('Not found'),
68
+ });
69
+ await (0, vitest_1.expect)(client.getThesis('bad-id')).rejects.toThrow('API error 404: Not found');
70
+ });
71
+ (0, vitest_1.it)('getContext calls correct path', async () => {
72
+ mockFetch.mockResolvedValue({
73
+ ok: true,
74
+ json: () => Promise.resolve({}),
75
+ });
76
+ await client.getContext('abc123');
77
+ (0, vitest_1.expect)(mockFetch).toHaveBeenCalledWith('https://api.test.com/api/thesis/abc123/context', vitest_1.expect.any(Object));
78
+ });
79
+ (0, vitest_1.it)('evaluate sends POST', async () => {
80
+ mockFetch.mockResolvedValue({
81
+ ok: true,
82
+ json: () => Promise.resolve({ evaluation: {} }),
83
+ });
84
+ await client.evaluate('thesis1');
85
+ const call = mockFetch.mock.calls[0];
86
+ (0, vitest_1.expect)(call[1].method).toBe('POST');
87
+ (0, vitest_1.expect)(call[0]).toBe('https://api.test.com/api/thesis/thesis1/evaluate');
88
+ });
89
+ });