@spfunctions/cli 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * sf agent — Interactive TUI agent powered by pi-tui + pi-agent-core.
3
+ *
4
+ * Layout:
5
+ * [Header overlay] — thesis id, confidence, model
6
+ * [Spacer] — room for header
7
+ * [Chat container] — messages (user, assistant, tool, system)
8
+ * [Editor] — multi-line input with slash command autocomplete
9
+ * [Spacer] — room for footer
10
+ * [Footer overlay] — tokens, cost, tool count, /help hint
11
+ *
12
+ * Slash commands (bypass LLM):
13
+ * /help /tree /edges /pos /eval /model /clear /exit
14
+ */
15
+ export declare function agentCommand(thesisId?: string, opts?: {
16
+ model?: string;
17
+ modelKey?: string;
18
+ }): Promise<void>;
@@ -0,0 +1,829 @@
1
+ "use strict";
2
+ /**
3
+ * sf agent — Interactive TUI agent powered by pi-tui + pi-agent-core.
4
+ *
5
+ * Layout:
6
+ * [Header overlay] — thesis id, confidence, model
7
+ * [Spacer] — room for header
8
+ * [Chat container] — messages (user, assistant, tool, system)
9
+ * [Editor] — multi-line input with slash command autocomplete
10
+ * [Spacer] — room for footer
11
+ * [Footer overlay] — tokens, cost, tool count, /help hint
12
+ *
13
+ * Slash commands (bypass LLM):
14
+ * /help /tree /edges /pos /eval /model /clear /exit
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.agentCommand = agentCommand;
18
+ const client_js_1 = require("../client.js");
19
+ const kalshi_js_1 = require("../kalshi.js");
20
+ // ─── ANSI 24-bit color helpers (no chalk dependency) ─────────────────────────
21
+ const rgb = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
22
+ const bgRgb = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
23
+ const bold = (s) => `\x1b[1m${s}\x1b[22m`;
24
+ const dim = (s) => `\x1b[2m${s}\x1b[22m`;
25
+ const italic = (s) => `\x1b[3m${s}\x1b[23m`;
26
+ const underline = (s) => `\x1b[4m${s}\x1b[24m`;
27
+ const strikethrough = (s) => `\x1b[9m${s}\x1b[29m`;
28
+ const C = {
29
+ emerald: rgb(16, 185, 129), // #10b981
30
+ zinc200: rgb(228, 228, 231), // #e4e4e7
31
+ zinc400: rgb(161, 161, 170), // #a1a1aa
32
+ zinc600: rgb(82, 82, 91), // #52525b
33
+ zinc800: rgb(39, 39, 42), // #27272a
34
+ red: rgb(239, 68, 68), // #ef4444
35
+ amber: rgb(245, 158, 11), // #f59e0b
36
+ white: rgb(255, 255, 255),
37
+ bgZinc900: bgRgb(24, 24, 27), // #18181b
38
+ bgZinc800: bgRgb(39, 39, 42), // #27272a
39
+ };
40
+ // ─── Custom components ───────────────────────────────────────────────────────
41
+ /** Mutable single-line component (TruncatedText is immutable) */
42
+ function createMutableLine(piTui) {
43
+ const { truncateToWidth, visibleWidth } = piTui;
44
+ return class MutableLine {
45
+ text;
46
+ cachedWidth;
47
+ cachedLines;
48
+ constructor(text) {
49
+ this.text = text;
50
+ }
51
+ setText(text) {
52
+ this.text = text;
53
+ this.cachedWidth = undefined;
54
+ this.cachedLines = undefined;
55
+ }
56
+ invalidate() {
57
+ this.cachedWidth = undefined;
58
+ this.cachedLines = undefined;
59
+ }
60
+ render(width) {
61
+ if (this.cachedLines && this.cachedWidth === width)
62
+ return this.cachedLines;
63
+ this.cachedWidth = width;
64
+ this.cachedLines = [truncateToWidth(this.text, width)];
65
+ return this.cachedLines;
66
+ }
67
+ };
68
+ }
69
+ /** Header bar: [SF Agent — {id}] [{confidence}%] [{model}] */
70
+ function createHeaderBar(piTui) {
71
+ const { truncateToWidth, visibleWidth } = piTui;
72
+ return class HeaderBar {
73
+ left;
74
+ center;
75
+ right;
76
+ cachedWidth;
77
+ cachedLines;
78
+ constructor(left, center, right) {
79
+ this.left = left;
80
+ this.center = center;
81
+ this.right = right;
82
+ }
83
+ update(left, center, right) {
84
+ if (left !== undefined)
85
+ this.left = left;
86
+ if (center !== undefined)
87
+ this.center = center;
88
+ if (right !== undefined)
89
+ this.right = right;
90
+ this.cachedWidth = undefined;
91
+ this.cachedLines = undefined;
92
+ }
93
+ invalidate() {
94
+ this.cachedWidth = undefined;
95
+ this.cachedLines = undefined;
96
+ }
97
+ render(width) {
98
+ if (this.cachedLines && this.cachedWidth === width)
99
+ return this.cachedLines;
100
+ this.cachedWidth = width;
101
+ const l = this.left;
102
+ const c = this.center;
103
+ const r = this.right;
104
+ const lw = visibleWidth(l);
105
+ const cw = visibleWidth(c);
106
+ const rw = visibleWidth(r);
107
+ const totalContent = lw + cw + rw;
108
+ const totalPad = Math.max(0, width - totalContent);
109
+ const leftPad = Math.max(1, Math.floor(totalPad / 2));
110
+ const rightPad = Math.max(1, totalPad - leftPad);
111
+ let line = l + ' '.repeat(leftPad) + c + ' '.repeat(rightPad) + r;
112
+ line = C.bgZinc900(truncateToWidth(line, width, ''));
113
+ // Pad to full width for background
114
+ const lineVw = visibleWidth(line);
115
+ if (lineVw < width) {
116
+ line = line + C.bgZinc900(' '.repeat(width - lineVw));
117
+ }
118
+ this.cachedLines = [line];
119
+ return this.cachedLines;
120
+ }
121
+ };
122
+ }
123
+ /** Footer bar: [tokens: N | cost: $N | tools: N] [/help] */
124
+ function createFooterBar(piTui) {
125
+ const { truncateToWidth, visibleWidth } = piTui;
126
+ return class FooterBar {
127
+ tokens = 0;
128
+ cost = 0;
129
+ toolCount = 0;
130
+ cachedWidth;
131
+ cachedLines;
132
+ invalidate() {
133
+ this.cachedWidth = undefined;
134
+ this.cachedLines = undefined;
135
+ }
136
+ update() {
137
+ this.cachedWidth = undefined;
138
+ this.cachedLines = undefined;
139
+ }
140
+ render(width) {
141
+ if (this.cachedLines && this.cachedWidth === width)
142
+ return this.cachedLines;
143
+ this.cachedWidth = width;
144
+ const tokStr = this.tokens >= 1000
145
+ ? `${(this.tokens / 1000).toFixed(1)}k`
146
+ : `${this.tokens}`;
147
+ const leftText = C.zinc600(`tokens: ${tokStr} cost: $${this.cost.toFixed(3)} tools: ${this.toolCount}`);
148
+ const rightText = C.zinc600('/help');
149
+ const lw = visibleWidth(leftText);
150
+ const rw = visibleWidth(rightText);
151
+ const gap = Math.max(1, width - lw - rw);
152
+ let line = leftText + ' '.repeat(gap) + rightText;
153
+ line = C.bgZinc900(truncateToWidth(line, width, ''));
154
+ const lineVw = visibleWidth(line);
155
+ if (lineVw < width) {
156
+ line = line + C.bgZinc900(' '.repeat(width - lineVw));
157
+ }
158
+ this.cachedLines = [line];
159
+ return this.cachedLines;
160
+ }
161
+ };
162
+ }
163
+ // ─── Formatted renderers ─────────────────────────────────────────────────────
164
+ function renderCausalTree(context, piTui) {
165
+ const tree = context.causalTree;
166
+ if (!tree?.nodes?.length)
167
+ return C.zinc600(' No causal tree data');
168
+ const lines = [];
169
+ for (const node of tree.nodes) {
170
+ const id = node.id || '';
171
+ const label = node.label || node.description || '';
172
+ const prob = typeof node.probability === 'number'
173
+ ? Math.round(node.probability * 100)
174
+ : (typeof node.impliedProbability === 'number' ? Math.round(node.impliedProbability * 100) : null);
175
+ const depth = (id.match(/\./g) || []).length;
176
+ const indent = ' '.repeat(depth + 1);
177
+ if (prob !== null) {
178
+ // Progress bar: 10 chars
179
+ const filled = Math.round(prob / 10);
180
+ const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled);
181
+ const probColor = prob >= 70 ? C.emerald : prob >= 40 ? C.amber : C.red;
182
+ // Dots to pad between label and percentage
183
+ const labelPart = `${indent}${C.zinc600(id)} ${C.zinc400(label)} `;
184
+ const probPart = ` ${probColor(`${prob}%`)} ${probColor(bar)}`;
185
+ lines.push(labelPart + probPart);
186
+ }
187
+ else {
188
+ lines.push(`${indent}${C.zinc600(id)} ${C.zinc400(label)}`);
189
+ }
190
+ }
191
+ return lines.join('\n');
192
+ }
193
+ function renderEdges(context, piTui) {
194
+ const edges = context.edges;
195
+ if (!edges?.length)
196
+ return C.zinc600(' No edge data');
197
+ const positions = context._positions || [];
198
+ const lines = [];
199
+ for (const e of edges) {
200
+ const name = (e.marketTitle || e.ticker || '').slice(0, 18).padEnd(18);
201
+ const market = typeof e.marketPrice === 'number' ? `${e.marketPrice}\u00A2` : '?';
202
+ const thesis = typeof e.thesisImpliedPrice === 'number' ? `${e.thesisImpliedPrice}\u00A2` : '?';
203
+ const edge = typeof e.edgeSize === 'number' ? (e.edgeSize > 0 ? `+${e.edgeSize}` : `${e.edgeSize}`) : '?';
204
+ const spread = typeof e.spread === 'number' ? `${e.spread}\u00A2` : '?';
205
+ const liq = e.liquidityScore || 'low';
206
+ const liqBars = liq === 'high' ? '\u25A0\u25A0\u25A0' : liq === 'medium' ? '\u25A0\u25A0 ' : '\u25A0 ';
207
+ const liqColor = liq === 'high' ? C.emerald : liq === 'medium' ? C.amber : C.red;
208
+ // Check if we have a position on this edge
209
+ const pos = positions.find((p) => p.ticker === e.ticker);
210
+ let posStr = C.zinc600('\u2014');
211
+ if (pos) {
212
+ const side = pos.side?.toUpperCase() || 'YES';
213
+ const pnl = typeof pos.unrealized_pnl === 'number'
214
+ ? (pos.unrealized_pnl >= 0 ? C.emerald(`+$${(pos.unrealized_pnl / 100).toFixed(0)}`) : C.red(`-$${(Math.abs(pos.unrealized_pnl) / 100).toFixed(0)}`))
215
+ : '';
216
+ posStr = C.emerald(`${side} (${pos.quantity}@${pos.average_price_paid}\u00A2 ${pnl})`);
217
+ }
218
+ lines.push(` ${C.zinc200(name)} ${C.zinc400(market)} \u2192 ${C.zinc400(thesis)} edge ${edge.includes('+') ? C.emerald(edge) : C.red(edge)} spread ${C.zinc600(spread)} ${liqColor(liqBars)} ${liqColor(liq.padEnd(4))} ${posStr}`);
219
+ }
220
+ return lines.join('\n');
221
+ }
222
+ function renderPositions(positions) {
223
+ if (!positions?.length)
224
+ return C.zinc600(' No positions');
225
+ const lines = [];
226
+ let totalPnl = 0;
227
+ for (const p of positions) {
228
+ const ticker = (p.ticker || '').slice(0, 18).padEnd(18);
229
+ const side = (p.side || 'yes').toUpperCase().padEnd(3);
230
+ const qty = String(p.quantity || 0);
231
+ const avg = `${p.average_price_paid || 0}\u00A2`;
232
+ const now = typeof p.current_value === 'number' && p.current_value > 0
233
+ ? `${p.current_value}\u00A2`
234
+ : '?\u00A2';
235
+ const pnlCents = p.unrealized_pnl || 0;
236
+ totalPnl += pnlCents;
237
+ const pnlDollars = (pnlCents / 100).toFixed(2);
238
+ const pnlStr = pnlCents >= 0
239
+ ? C.emerald(`+$${pnlDollars}`)
240
+ : C.red(`-$${Math.abs(parseFloat(pnlDollars)).toFixed(2)}`);
241
+ const arrow = pnlCents >= 0 ? C.emerald('\u25B2') : C.red('\u25BC');
242
+ lines.push(` ${C.zinc200(ticker)} ${C.zinc400(side)} ${C.zinc400(qty)} @ ${C.zinc400(avg)} now ${C.zinc200(now)} ${pnlStr} ${arrow}`);
243
+ }
244
+ const totalDollars = (totalPnl / 100).toFixed(2);
245
+ lines.push(C.zinc600(' ' + '\u2500'.repeat(40)));
246
+ lines.push(totalPnl >= 0
247
+ ? ` Total P&L: ${C.emerald(bold(`+$${totalDollars}`))}`
248
+ : ` Total P&L: ${C.red(bold(`-$${Math.abs(parseFloat(totalDollars)).toFixed(2)}`))}`);
249
+ return lines.join('\n');
250
+ }
251
+ // ─── Main command ────────────────────────────────────────────────────────────
252
+ async function agentCommand(thesisId, opts) {
253
+ // ── Dynamic imports (all ESM-only packages) ────────────────────────────────
254
+ const piTui = await import('@mariozechner/pi-tui');
255
+ const piAi = await import('@mariozechner/pi-ai');
256
+ const piAgent = await import('@mariozechner/pi-agent-core');
257
+ const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
258
+ const { getModel, streamSimple, Type } = piAi;
259
+ const { Agent } = piAgent;
260
+ // ── Component class factories (need piTui ref) ─────────────────────────────
261
+ const MutableLine = createMutableLine(piTui);
262
+ const HeaderBar = createHeaderBar(piTui);
263
+ const FooterBar = createFooterBar(piTui);
264
+ // ── Validate API keys ──────────────────────────────────────────────────────
265
+ const openrouterKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
266
+ if (!openrouterKey) {
267
+ console.error('Need OpenRouter API key. Set OPENROUTER_API_KEY or use --model-key.');
268
+ process.exit(1);
269
+ }
270
+ const sfClient = new client_js_1.SFClient();
271
+ // ── Resolve thesis ID ──────────────────────────────────────────────────────
272
+ let resolvedThesisId = thesisId;
273
+ if (!resolvedThesisId) {
274
+ const data = await sfClient.listTheses();
275
+ const theses = data.theses || data;
276
+ const active = theses.find((t) => t.status === 'active');
277
+ if (!active) {
278
+ console.error('No active thesis. Create one first: sf create "..."');
279
+ process.exit(1);
280
+ }
281
+ resolvedThesisId = active.id;
282
+ }
283
+ // ── Fetch initial context ──────────────────────────────────────────────────
284
+ let latestContext = await sfClient.getContext(resolvedThesisId);
285
+ // ── Model setup ────────────────────────────────────────────────────────────
286
+ const rawModelName = opts?.model || 'anthropic/claude-sonnet-4-20250514';
287
+ let currentModelName = rawModelName.replace(/^openrouter\//, '');
288
+ function resolveModel(name) {
289
+ try {
290
+ return getModel('openrouter', name);
291
+ }
292
+ catch {
293
+ return {
294
+ modelId: name,
295
+ provider: 'openrouter',
296
+ api: 'openai-completions',
297
+ baseUrl: 'https://openrouter.ai/api/v1',
298
+ id: name,
299
+ name: name,
300
+ inputPrice: 0,
301
+ outputPrice: 0,
302
+ contextWindow: 200000,
303
+ supportsImages: true,
304
+ supportsTools: true,
305
+ };
306
+ }
307
+ }
308
+ let model = resolveModel(currentModelName);
309
+ // ── Tracking state ─────────────────────────────────────────────────────────
310
+ let totalTokens = 0;
311
+ let totalCost = 0;
312
+ let totalToolCalls = 0;
313
+ let isProcessing = false;
314
+ // Cache for positions (fetched by /pos or get_positions tool)
315
+ let cachedPositions = null;
316
+ // ── Setup TUI ──────────────────────────────────────────────────────────────
317
+ const terminal = new ProcessTerminal();
318
+ const tui = new TUI(terminal);
319
+ // Markdown theme for assistant messages
320
+ const mdTheme = {
321
+ heading: (s) => C.zinc200(bold(s)),
322
+ link: (s) => C.emerald(s),
323
+ linkUrl: (s) => C.zinc600(s),
324
+ code: (s) => C.zinc200(s),
325
+ codeBlock: (s) => C.zinc400(s),
326
+ codeBlockBorder: (s) => C.zinc600(s),
327
+ quote: (s) => C.zinc400(s),
328
+ quoteBorder: (s) => C.zinc600(s),
329
+ hr: (s) => C.zinc600(s),
330
+ listBullet: (s) => C.emerald(s),
331
+ bold: (s) => bold(s),
332
+ italic: (s) => italic(s),
333
+ strikethrough: (s) => strikethrough(s),
334
+ underline: (s) => underline(s),
335
+ };
336
+ const mdDefaultStyle = {
337
+ color: (s) => C.zinc400(s),
338
+ };
339
+ // Editor theme
340
+ const editorTheme = {
341
+ borderColor: (s) => C.zinc800(s),
342
+ selectList: {
343
+ selectedPrefix: (s) => C.emerald(s),
344
+ selectedText: (s) => C.zinc200(s),
345
+ description: (s) => C.zinc600(s),
346
+ scrollInfo: (s) => C.zinc600(s),
347
+ noMatch: (s) => C.zinc600(s),
348
+ },
349
+ };
350
+ // ── Build components ───────────────────────────────────────────────────────
351
+ const shortId = (resolvedThesisId || '').slice(0, 8);
352
+ const confidencePct = typeof latestContext.confidence === 'number'
353
+ ? Math.round(latestContext.confidence * 100)
354
+ : (typeof latestContext.confidence === 'string' ? parseInt(latestContext.confidence) : 0);
355
+ const headerBar = new HeaderBar(C.emerald(bold('SF Agent')) + C.zinc600(` \u2014 ${shortId}`), confidencePct > 0 ? C.zinc200(`${confidencePct}%`) : '', C.zinc600(currentModelName.split('/').pop() || currentModelName));
356
+ const footerBar = new FooterBar();
357
+ const topSpacer = new Spacer(1);
358
+ const bottomSpacer = new Spacer(1);
359
+ const chatContainer = new Container();
360
+ const editor = new Editor(tui, editorTheme, { paddingX: 1 });
361
+ // Slash command autocomplete
362
+ const autocompleteProvider = new CombinedAutocompleteProvider([
363
+ { name: 'help', description: 'Show available commands' },
364
+ { name: 'tree', description: 'Display causal tree' },
365
+ { name: 'edges', description: 'Display edge/spread table' },
366
+ { name: 'pos', description: 'Display Kalshi positions' },
367
+ { name: 'eval', description: 'Trigger deep evaluation' },
368
+ { name: 'model', description: 'Switch model (e.g. /model anthropic/claude-sonnet-4)' },
369
+ { name: 'clear', description: 'Clear chat' },
370
+ { name: 'exit', description: 'Exit agent' },
371
+ ], process.cwd());
372
+ editor.setAutocompleteProvider(autocompleteProvider);
373
+ // Assemble TUI tree
374
+ tui.addChild(topSpacer);
375
+ tui.addChild(chatContainer);
376
+ tui.addChild(editor);
377
+ tui.addChild(bottomSpacer);
378
+ // Focus on editor
379
+ tui.setFocus(editor);
380
+ // ── Overlays (pinned header + footer) ──────────────────────────────────────
381
+ const headerOverlay = tui.showOverlay(headerBar, {
382
+ row: 0,
383
+ col: 0,
384
+ width: '100%',
385
+ nonCapturing: true,
386
+ });
387
+ const footerOverlay = tui.showOverlay(footerBar, {
388
+ anchor: 'bottom-left',
389
+ width: '100%',
390
+ nonCapturing: true,
391
+ });
392
+ // ── Helper: add system text to chat ────────────────────────────────────────
393
+ function addSystemText(content) {
394
+ const text = new Text(content, 1, 0);
395
+ chatContainer.addChild(text);
396
+ tui.requestRender();
397
+ }
398
+ function addSpacer() {
399
+ chatContainer.addChild(new Spacer(1));
400
+ }
401
+ // ── Define agent tools (same as before) ────────────────────────────────────
402
+ const thesisIdParam = Type.Object({
403
+ thesisId: Type.String({ description: 'Thesis ID (short or full UUID)' }),
404
+ });
405
+ const signalParams = Type.Object({
406
+ thesisId: Type.String({ description: 'Thesis ID' }),
407
+ content: Type.String({ description: 'Signal content' }),
408
+ type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external. Default: user_note' })),
409
+ });
410
+ const scanParams = Type.Object({
411
+ query: Type.Optional(Type.String({ description: 'Keyword search for Kalshi markets' })),
412
+ series: Type.Optional(Type.String({ description: 'Kalshi series ticker (e.g. KXWTIMAX)' })),
413
+ market: Type.Optional(Type.String({ description: 'Specific market ticker' })),
414
+ });
415
+ const emptyParams = Type.Object({});
416
+ const tools = [
417
+ {
418
+ name: 'get_context',
419
+ label: 'Get Context',
420
+ description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
421
+ parameters: thesisIdParam,
422
+ execute: async (_toolCallId, params) => {
423
+ const ctx = await sfClient.getContext(params.thesisId);
424
+ latestContext = ctx;
425
+ // Update header with new confidence
426
+ const conf = typeof ctx.confidence === 'number'
427
+ ? Math.round(ctx.confidence * 100)
428
+ : 0;
429
+ headerBar.update(undefined, conf > 0 ? C.zinc200(`${conf}%`) : '', undefined);
430
+ tui.requestRender();
431
+ return {
432
+ content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
433
+ details: {},
434
+ };
435
+ },
436
+ },
437
+ {
438
+ name: 'inject_signal',
439
+ label: 'Inject Signal',
440
+ description: 'Inject a signal into the thesis (news, note, external event)',
441
+ parameters: signalParams,
442
+ execute: async (_toolCallId, params) => {
443
+ const result = await sfClient.injectSignal(params.thesisId, params.type || 'user_note', params.content);
444
+ return {
445
+ content: [{ type: 'text', text: JSON.stringify(result) }],
446
+ details: {},
447
+ };
448
+ },
449
+ },
450
+ {
451
+ name: 'trigger_evaluation',
452
+ label: 'Evaluate',
453
+ description: 'Trigger a deep evaluation cycle (heavy model, takes longer)',
454
+ parameters: thesisIdParam,
455
+ execute: async (_toolCallId, params) => {
456
+ const result = await sfClient.evaluate(params.thesisId);
457
+ return {
458
+ content: [{ type: 'text', text: JSON.stringify(result) }],
459
+ details: {},
460
+ };
461
+ },
462
+ },
463
+ {
464
+ name: 'scan_markets',
465
+ label: 'Scan Markets',
466
+ description: 'Search Kalshi prediction markets: by keywords, series ticker, or specific market ticker',
467
+ parameters: scanParams,
468
+ execute: async (_toolCallId, params) => {
469
+ let result;
470
+ if (params.market) {
471
+ result = await (0, client_js_1.kalshiFetchMarket)(params.market);
472
+ }
473
+ else if (params.series) {
474
+ result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
475
+ }
476
+ else if (params.query) {
477
+ const series = await (0, client_js_1.kalshiFetchAllSeries)();
478
+ const keywords = params.query.toLowerCase().split(/\s+/);
479
+ const matched = series
480
+ .filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
481
+ (s.ticker || '').toLowerCase().includes(kw)))
482
+ .slice(0, 15);
483
+ result = matched;
484
+ }
485
+ else {
486
+ result = { error: 'Provide query, series, or market parameter' };
487
+ }
488
+ return {
489
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
490
+ details: {},
491
+ };
492
+ },
493
+ },
494
+ {
495
+ name: 'list_theses',
496
+ label: 'List Theses',
497
+ description: 'List all theses for the current user',
498
+ parameters: emptyParams,
499
+ execute: async () => {
500
+ const theses = await sfClient.listTheses();
501
+ return {
502
+ content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }],
503
+ details: {},
504
+ };
505
+ },
506
+ },
507
+ {
508
+ name: 'get_positions',
509
+ label: 'Get Positions',
510
+ description: 'Get Kalshi exchange positions with live prices and PnL',
511
+ parameters: emptyParams,
512
+ execute: async () => {
513
+ const positions = await (0, kalshi_js_1.getPositions)();
514
+ if (!positions) {
515
+ return {
516
+ content: [{ type: 'text', text: 'Kalshi not configured. Set KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY_PATH.' }],
517
+ details: {},
518
+ };
519
+ }
520
+ for (const pos of positions) {
521
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
522
+ if (livePrice !== null) {
523
+ pos.current_value = livePrice;
524
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
525
+ }
526
+ }
527
+ cachedPositions = positions;
528
+ return {
529
+ content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }],
530
+ details: {},
531
+ };
532
+ },
533
+ },
534
+ ];
535
+ // ── System prompt ──────────────────────────────────────────────────────────
536
+ const systemPrompt = `You are a SimpleFunctions prediction market trading assistant.
537
+
538
+ Current thesis: ${latestContext.thesis || latestContext.rawThesis || 'N/A'}
539
+ Confidence: ${confidencePct}%
540
+ Status: ${latestContext.status}
541
+ Thesis ID: ${latestContext.thesisId || resolvedThesisId}
542
+
543
+ You have six tools available. Use them when you need real-time data. Answer directly when you don't.
544
+ Be concise. Use Chinese if the user writes in Chinese, English if they write in English.
545
+ Do NOT make up data. Always call tools to get current state.`;
546
+ // ── Create Agent ───────────────────────────────────────────────────────────
547
+ const agent = new Agent({
548
+ initialState: {
549
+ systemPrompt,
550
+ model,
551
+ tools,
552
+ thinkingLevel: 'off',
553
+ },
554
+ streamFn: streamSimple,
555
+ getApiKey: (provider) => {
556
+ if (provider === 'openrouter')
557
+ return openrouterKey;
558
+ return undefined;
559
+ },
560
+ });
561
+ // ── Subscribe to agent events → update TUI ────────────────────────────────
562
+ let currentAssistantMd = null;
563
+ let currentAssistantText = '';
564
+ let currentLoader = null;
565
+ const toolStartTimes = new Map();
566
+ const toolLines = new Map();
567
+ agent.subscribe((event) => {
568
+ if (event.type === 'message_start') {
569
+ // Show loader while waiting for first text
570
+ currentAssistantText = '';
571
+ currentAssistantMd = null;
572
+ currentLoader = new Loader(tui, (s) => C.emerald(s), (s) => C.zinc600(s), 'thinking...');
573
+ currentLoader.start();
574
+ chatContainer.addChild(currentLoader);
575
+ tui.requestRender();
576
+ }
577
+ if (event.type === 'message_update') {
578
+ const e = event.assistantMessageEvent;
579
+ if (e.type === 'text_delta') {
580
+ // Remove loader on first text delta
581
+ if (currentLoader) {
582
+ currentLoader.stop();
583
+ chatContainer.removeChild(currentLoader);
584
+ currentLoader = null;
585
+ // Create markdown component for assistant response
586
+ currentAssistantMd = new Markdown('', 1, 0, mdTheme, mdDefaultStyle);
587
+ chatContainer.addChild(currentAssistantMd);
588
+ }
589
+ currentAssistantText += e.delta;
590
+ if (currentAssistantMd) {
591
+ currentAssistantMd.setText(currentAssistantText);
592
+ }
593
+ tui.requestRender();
594
+ }
595
+ }
596
+ if (event.type === 'message_complete') {
597
+ // Clean up loader if still present (no text was generated)
598
+ if (currentLoader) {
599
+ currentLoader.stop();
600
+ chatContainer.removeChild(currentLoader);
601
+ currentLoader = null;
602
+ }
603
+ // Add spacer after message
604
+ addSpacer();
605
+ currentAssistantMd = null;
606
+ currentAssistantText = '';
607
+ isProcessing = false;
608
+ tui.requestRender();
609
+ // Update token/cost tracking from event if available
610
+ if (event.usage) {
611
+ totalTokens += (event.usage.inputTokens || 0) + (event.usage.outputTokens || 0);
612
+ totalCost += event.usage.totalCost || 0;
613
+ footerBar.tokens = totalTokens;
614
+ footerBar.cost = totalCost;
615
+ footerBar.update();
616
+ }
617
+ }
618
+ if (event.type === 'tool_execution_start') {
619
+ const toolLine = new MutableLine(C.zinc600(` \u26A1 ${event.toolName}...`));
620
+ toolStartTimes.set(event.toolCallId || event.toolName, Date.now());
621
+ toolLines.set(event.toolCallId || event.toolName, toolLine);
622
+ chatContainer.addChild(toolLine);
623
+ totalToolCalls++;
624
+ footerBar.toolCount = totalToolCalls;
625
+ footerBar.update();
626
+ tui.requestRender();
627
+ }
628
+ if (event.type === 'tool_execution_end') {
629
+ const key = event.toolCallId || event.toolName;
630
+ const startTime = toolStartTimes.get(key);
631
+ const elapsed = startTime ? ((Date.now() - startTime) / 1000).toFixed(1) : '?';
632
+ const line = toolLines.get(key);
633
+ if (line) {
634
+ if (event.isError) {
635
+ line.setText(C.red(` \u2717 ${event.toolName} (${elapsed}s) error`));
636
+ }
637
+ else {
638
+ line.setText(C.zinc600(` \u26A1 ${event.toolName}`) + C.emerald(` \u2713`) + C.zinc600(` (${elapsed}s)`));
639
+ }
640
+ }
641
+ toolStartTimes.delete(key);
642
+ toolLines.delete(key);
643
+ tui.requestRender();
644
+ }
645
+ });
646
+ // ── Slash command handlers ─────────────────────────────────────────────────
647
+ async function handleSlashCommand(cmd) {
648
+ const parts = cmd.trim().split(/\s+/);
649
+ const command = parts[0].toLowerCase();
650
+ switch (command) {
651
+ case '/help': {
652
+ addSpacer();
653
+ addSystemText(C.zinc200(bold('Commands')) + '\n' +
654
+ C.emerald('/help ') + C.zinc400(' Show this help') + '\n' +
655
+ C.emerald('/tree ') + C.zinc400(' Display causal tree') + '\n' +
656
+ C.emerald('/edges ') + C.zinc400(' Display edge/spread table') + '\n' +
657
+ C.emerald('/pos ') + C.zinc400(' Display Kalshi positions') + '\n' +
658
+ C.emerald('/eval ') + C.zinc400(' Trigger deep evaluation') + '\n' +
659
+ C.emerald('/model ') + C.zinc400(' Switch model (e.g. /model anthropic/claude-sonnet-4)') + '\n' +
660
+ C.emerald('/clear ') + C.zinc400(' Clear chat') + '\n' +
661
+ C.emerald('/exit ') + C.zinc400(' Exit agent'));
662
+ addSpacer();
663
+ return true;
664
+ }
665
+ case '/tree': {
666
+ addSpacer();
667
+ // Refresh context first
668
+ try {
669
+ latestContext = await sfClient.getContext(resolvedThesisId);
670
+ addSystemText(C.zinc200(bold('Causal Tree')) + '\n' + renderCausalTree(latestContext, piTui));
671
+ }
672
+ catch (err) {
673
+ addSystemText(C.red(`Error: ${err.message}`));
674
+ }
675
+ addSpacer();
676
+ return true;
677
+ }
678
+ case '/edges': {
679
+ addSpacer();
680
+ try {
681
+ latestContext = await sfClient.getContext(resolvedThesisId);
682
+ // Attach cached positions for display
683
+ if (cachedPositions) {
684
+ latestContext._positions = cachedPositions;
685
+ }
686
+ addSystemText(C.zinc200(bold('Edges')) + '\n' + renderEdges(latestContext, piTui));
687
+ }
688
+ catch (err) {
689
+ addSystemText(C.red(`Error: ${err.message}`));
690
+ }
691
+ addSpacer();
692
+ return true;
693
+ }
694
+ case '/pos': {
695
+ addSpacer();
696
+ try {
697
+ const positions = await (0, kalshi_js_1.getPositions)();
698
+ if (!positions) {
699
+ addSystemText(C.zinc600('Kalshi not configured'));
700
+ return true;
701
+ }
702
+ for (const pos of positions) {
703
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
704
+ if (livePrice !== null) {
705
+ pos.current_value = livePrice;
706
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
707
+ }
708
+ }
709
+ cachedPositions = positions;
710
+ addSystemText(C.zinc200(bold('Positions')) + '\n' + renderPositions(positions));
711
+ }
712
+ catch (err) {
713
+ addSystemText(C.red(`Error: ${err.message}`));
714
+ }
715
+ addSpacer();
716
+ return true;
717
+ }
718
+ case '/eval': {
719
+ addSpacer();
720
+ addSystemText(C.zinc600('Triggering evaluation...'));
721
+ tui.requestRender();
722
+ try {
723
+ const result = await sfClient.evaluate(resolvedThesisId);
724
+ addSystemText(C.emerald('Evaluation complete') + '\n' + C.zinc400(JSON.stringify(result, null, 2)));
725
+ }
726
+ catch (err) {
727
+ addSystemText(C.red(`Error: ${err.message}`));
728
+ }
729
+ addSpacer();
730
+ return true;
731
+ }
732
+ case '/model': {
733
+ const newModel = parts.slice(1).join(' ').trim();
734
+ if (!newModel) {
735
+ addSystemText(C.zinc400(`Current model: ${currentModelName}`));
736
+ return true;
737
+ }
738
+ addSpacer();
739
+ currentModelName = newModel.replace(/^openrouter\//, '');
740
+ model = resolveModel(currentModelName);
741
+ // Update agent model
742
+ agent.setModel(model);
743
+ headerBar.update(undefined, undefined, C.zinc600(currentModelName.split('/').pop() || currentModelName));
744
+ addSystemText(C.emerald(`Model switched to ${currentModelName}`));
745
+ addSpacer();
746
+ tui.requestRender();
747
+ return true;
748
+ }
749
+ case '/clear': {
750
+ chatContainer.clear();
751
+ tui.requestRender();
752
+ return true;
753
+ }
754
+ case '/exit':
755
+ case '/quit': {
756
+ cleanup();
757
+ return true;
758
+ }
759
+ default:
760
+ return false;
761
+ }
762
+ }
763
+ // ── Editor submit handler ──────────────────────────────────────────────────
764
+ editor.onSubmit = async (input) => {
765
+ const trimmed = input.trim();
766
+ if (!trimmed)
767
+ return;
768
+ if (isProcessing)
769
+ return;
770
+ // Add to editor history
771
+ editor.addToHistory(trimmed);
772
+ // Check for slash commands
773
+ if (trimmed.startsWith('/')) {
774
+ const handled = await handleSlashCommand(trimmed);
775
+ if (handled)
776
+ return;
777
+ }
778
+ // Regular message → send to agent
779
+ isProcessing = true;
780
+ // Add user message to chat
781
+ const userMsg = new Text(C.emerald(bold('>')) + ' ' + C.white(trimmed), 1, 0);
782
+ chatContainer.addChild(userMsg);
783
+ addSpacer();
784
+ tui.requestRender();
785
+ try {
786
+ await agent.prompt(trimmed);
787
+ }
788
+ catch (err) {
789
+ // Remove loader if present
790
+ if (currentLoader) {
791
+ currentLoader.stop();
792
+ chatContainer.removeChild(currentLoader);
793
+ currentLoader = null;
794
+ }
795
+ addSystemText(C.red(`Error: ${err.message}`));
796
+ addSpacer();
797
+ isProcessing = false;
798
+ }
799
+ };
800
+ // ── Ctrl+C handler ─────────────────────────────────────────────────────────
801
+ function cleanup() {
802
+ if (currentLoader)
803
+ currentLoader.stop();
804
+ tui.stop();
805
+ process.exit(0);
806
+ }
807
+ // Listen for Ctrl+C at the TUI level
808
+ tui.addInputListener((data) => {
809
+ // Ctrl+C = \x03
810
+ if (data === '\x03') {
811
+ cleanup();
812
+ return { consume: true };
813
+ }
814
+ return undefined;
815
+ });
816
+ // Also handle SIGINT
817
+ process.on('SIGINT', cleanup);
818
+ process.on('SIGTERM', cleanup);
819
+ // ── Show initial welcome ───────────────────────────────────────────────────
820
+ const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
821
+ const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
822
+ addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
823
+ C.zinc200(bold(truncatedThesis)) + '\n' +
824
+ C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + '\n' +
825
+ C.zinc600('\u2500'.repeat(50)));
826
+ addSpacer();
827
+ // ── Start TUI ──────────────────────────────────────────────────────────────
828
+ tui.start();
829
+ }
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ const signal_js_1 = require("./commands/signal.js");
25
25
  const evaluate_js_1 = require("./commands/evaluate.js");
26
26
  const scan_js_1 = require("./commands/scan.js");
27
27
  const positions_js_1 = require("./commands/positions.js");
28
+ const agent_js_1 = require("./commands/agent.js");
28
29
  const utils_js_1 = require("./utils.js");
29
30
  const program = new commander_1.Command();
30
31
  program
@@ -130,6 +131,16 @@ program
130
131
  apiUrl: g.apiUrl,
131
132
  }));
132
133
  });
134
+ // ── sf agent [thesisId] ───────────────────────────────────────────────────────
135
+ program
136
+ .command('agent [thesisId]')
137
+ .description('Interactive agent mode — natural language interface to SimpleFunctions')
138
+ .option('--model <model>', 'Model via OpenRouter (default: anthropic/claude-sonnet-4-20250514)')
139
+ .option('--model-key <key>', 'OpenRouter API key (or set OPENROUTER_API_KEY)')
140
+ .action(async (thesisId, opts, cmd) => {
141
+ const g = cmd.optsWithGlobals();
142
+ await run(() => (0, agent_js_1.agentCommand)(thesisId, { model: opts.model, modelKey: opts.modelKey }));
143
+ });
133
144
  // ── Error wrapper ─────────────────────────────────────────────────────────────
134
145
  async function run(fn) {
135
146
  try {
package/dist/kalshi.js CHANGED
@@ -104,9 +104,6 @@ async function getPositions() {
104
104
  return null;
105
105
  try {
106
106
  const data = await kalshiAuthGet('/portfolio/positions');
107
- // DEBUG: dump raw Kalshi response to see actual field names
108
- console.log('[Kalshi DEBUG] Raw /portfolio/positions response:');
109
- console.log(JSON.stringify(data, null, 2));
110
107
  // Kalshi returns { market_positions: [...] } or { positions: [...] }
111
108
  const raw = data.market_positions || data.positions || [];
112
109
  return raw.map((p) => {
package/package.json CHANGED
@@ -1,16 +1,19 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI for SimpleFunctions prediction market thesis agent",
5
5
  "bin": {
6
6
  "sf": "./dist/index.js"
7
7
  },
8
8
  "scripts": {
9
- "build": "tsc",
9
+ "build": "node ./node_modules/typescript/bin/tsc",
10
10
  "dev": "tsx src/index.ts",
11
11
  "prepublishOnly": "npm run build"
12
12
  },
13
13
  "dependencies": {
14
+ "@mariozechner/pi-agent-core": "^0.57.1",
15
+ "@mariozechner/pi-ai": "^0.57.1",
16
+ "@mariozechner/pi-tui": "^0.57.1",
14
17
  "commander": "^12.0.0",
15
18
  "kalshi-typescript": "^3.9.0"
16
19
  },