@spfunctions/cli 1.4.3 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -48
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +31 -0
- package/dist/cache.test.d.ts +1 -0
- package/dist/cache.test.js +73 -0
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +89 -0
- package/dist/commands/agent.js +257 -70
- package/dist/commands/dashboard.d.ts +6 -3
- package/dist/commands/dashboard.js +28 -26
- package/dist/commands/performance.js +9 -2
- package/dist/commands/telegram.d.ts +15 -0
- package/dist/commands/telegram.js +125 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +138 -0
- package/dist/index.js +16 -2
- package/dist/telegram/agent-bridge.d.ts +15 -0
- package/dist/telegram/agent-bridge.js +368 -0
- package/dist/telegram/bot.d.ts +10 -0
- package/dist/telegram/bot.js +297 -0
- package/dist/telegram/commands.d.ts +11 -0
- package/dist/telegram/commands.js +120 -0
- package/dist/telegram/format.d.ts +11 -0
- package/dist/telegram/format.js +51 -0
- package/dist/telegram/format.test.d.ts +1 -0
- package/dist/telegram/format.test.js +73 -0
- package/dist/telegram/poller.d.ts +6 -0
- package/dist/telegram/poller.js +32 -0
- package/dist/topics.test.d.ts +1 -0
- package/dist/topics.test.js +54 -0
- package/dist/tui/border.d.ts +33 -0
- package/dist/tui/border.js +87 -0
- package/dist/tui/chart.d.ts +19 -0
- package/dist/tui/chart.js +117 -0
- package/dist/tui/dashboard.d.ts +9 -0
- package/dist/tui/dashboard.js +779 -0
- package/dist/tui/layout.d.ts +16 -0
- package/dist/tui/layout.js +41 -0
- package/dist/tui/screen.d.ts +33 -0
- package/dist/tui/screen.js +102 -0
- package/dist/tui/state.d.ts +40 -0
- package/dist/tui/state.js +36 -0
- package/dist/tui/widgets/commandbar.d.ts +8 -0
- package/dist/tui/widgets/commandbar.js +82 -0
- package/dist/tui/widgets/detail.d.ts +9 -0
- package/dist/tui/widgets/detail.js +151 -0
- package/dist/tui/widgets/edges.d.ts +4 -0
- package/dist/tui/widgets/edges.js +33 -0
- package/dist/tui/widgets/liquidity.d.ts +9 -0
- package/dist/tui/widgets/liquidity.js +142 -0
- package/dist/tui/widgets/orders.d.ts +4 -0
- package/dist/tui/widgets/orders.js +37 -0
- package/dist/tui/widgets/portfolio.d.ts +4 -0
- package/dist/tui/widgets/portfolio.js +58 -0
- package/dist/tui/widgets/signals.d.ts +4 -0
- package/dist/tui/widgets/signals.js +31 -0
- package/dist/tui/widgets/statusbar.d.ts +8 -0
- package/dist/tui/widgets/statusbar.js +72 -0
- package/dist/tui/widgets/thesis.d.ts +4 -0
- package/dist/tui/widgets/thesis.js +66 -0
- package/dist/tui/widgets/trade.d.ts +9 -0
- package/dist/tui/widgets/trade.js +117 -0
- package/dist/tui/widgets/upcoming.d.ts +4 -0
- package/dist/tui/widgets/upcoming.js +41 -0
- package/dist/tui/widgets/whatif.d.ts +7 -0
- package/dist/tui/widgets/whatif.js +113 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +111 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -1,92 +1,249 @@
|
|
|
1
1
|
# SimpleFunctions CLI (`sf`)
|
|
2
2
|
|
|
3
|
-
Prediction market
|
|
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
|
-
##
|
|
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
|
-
##
|
|
14
|
+
## Setup
|
|
15
|
+
|
|
16
|
+
### Interactive (recommended)
|
|
12
17
|
|
|
13
18
|
```bash
|
|
14
|
-
|
|
15
|
-
export SF_API_URL=https://simplefunctions.dev # optional, defaults to production
|
|
19
|
+
sf setup
|
|
16
20
|
```
|
|
17
21
|
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
+
### Verify
|
|
24
38
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
sf
|
|
125
|
+
npm install -g @spfunctions/cli
|
|
126
|
+
sf setup --key sf_live_YOUR_KEY_HERE
|
|
44
127
|
```
|
|
45
128
|
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
sf create "..." --async # return immediately
|
|
131
|
+
export SF_API_KEY=sf_live_YOUR_KEY_HERE
|
|
51
132
|
```
|
|
52
133
|
|
|
53
|
-
###
|
|
54
|
-
|
|
134
|
+
### Step 2: Verify it works
|
|
135
|
+
|
|
55
136
|
```bash
|
|
56
|
-
sf
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
|
152
|
+
sf context <thesisId> --json
|
|
66
153
|
```
|
|
67
154
|
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
sf
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
222
|
+
### Common patterns for agents
|
|
78
223
|
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
```
|
package/dist/cache.d.ts
ADDED
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
|
+
});
|