@spfunctions/cli 1.7.14 → 1.7.17
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/dist/commands/agent.js +633 -183
- package/dist/commands/context.d.ts +1 -0
- package/dist/commands/context.js +5 -0
- package/dist/commands/edges.d.ts +1 -0
- package/dist/commands/edges.js +13 -7
- package/dist/commands/explore.d.ts +1 -0
- package/dist/commands/explore.js +5 -0
- package/dist/commands/markets.d.ts +1 -0
- package/dist/commands/markets.js +5 -0
- package/dist/commands/query.d.ts +1 -0
- package/dist/commands/query.js +6 -1
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +7 -2
- package/dist/commands/watch.d.ts +19 -0
- package/dist/commands/watch.js +157 -0
- package/dist/index.js +28 -4
- package/dist/share.d.ts +4 -0
- package/dist/share.js +27 -0
- package/dist/skills/loader.d.ts +19 -0
- package/dist/skills/loader.js +86 -0
- package/dist/types/output.d.ts +412 -0
- package/dist/types/output.js +9 -0
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -26,6 +26,7 @@ const kalshi_js_1 = require("../kalshi.js");
|
|
|
26
26
|
const polymarket_js_1 = require("../polymarket.js");
|
|
27
27
|
const topics_js_1 = require("../topics.js");
|
|
28
28
|
const config_js_1 = require("../config.js");
|
|
29
|
+
const loader_js_1 = require("../skills/loader.js");
|
|
29
30
|
// ─── Session persistence ─────────────────────────────────────────────────────
|
|
30
31
|
function getSessionDir() {
|
|
31
32
|
return path_1.default.join(os_1.default.homedir(), '.sf', 'sessions');
|
|
@@ -219,21 +220,37 @@ function createFooterBar(piTui) {
|
|
|
219
220
|
exchangeOpen = null;
|
|
220
221
|
cachedWidth;
|
|
221
222
|
cachedLines;
|
|
223
|
+
isExplorer = false;
|
|
222
224
|
setFromContext(ctx, positions) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
this.
|
|
225
|
+
if (ctx._explorerMode) {
|
|
226
|
+
this.isExplorer = true;
|
|
227
|
+
this.thesisId = 'Explorer';
|
|
228
|
+
this.confidence = 0;
|
|
229
|
+
this.confidenceDelta = 0;
|
|
230
|
+
this.edgeCount = (ctx.edges || []).length;
|
|
231
|
+
const edges = ctx.edges || [];
|
|
232
|
+
if (edges.length > 0) {
|
|
233
|
+
const top = [...edges].sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))[0];
|
|
234
|
+
this.topEdge = `${(top.title || '').slice(0, 20)} +${Math.round(top.edge)}¢`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
this.isExplorer = false;
|
|
239
|
+
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
240
|
+
this.confidence = typeof ctx.confidence === 'number'
|
|
241
|
+
? Math.round(ctx.confidence * 100)
|
|
242
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
243
|
+
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
244
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
245
|
+
: 0;
|
|
246
|
+
this.edgeCount = (ctx.edges || []).length;
|
|
247
|
+
const edges = ctx.edges || [];
|
|
248
|
+
if (edges.length > 0) {
|
|
249
|
+
const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
250
|
+
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
251
|
+
const edge = top.edge || top.edgeSize || 0;
|
|
252
|
+
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
253
|
+
}
|
|
237
254
|
}
|
|
238
255
|
if (positions && positions.length > 0) {
|
|
239
256
|
this.positionCount = positions.length;
|
|
@@ -260,23 +277,39 @@ function createFooterBar(piTui) {
|
|
|
260
277
|
if (this.cachedLines && this.cachedWidth === width)
|
|
261
278
|
return this.cachedLines;
|
|
262
279
|
this.cachedWidth = width;
|
|
263
|
-
// Line 1: thesis info
|
|
264
|
-
const id = C.emerald(this.thesisId);
|
|
265
|
-
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
266
|
-
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
267
|
-
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
268
|
-
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
269
|
-
let pnl = '';
|
|
270
|
-
if (this.positionCount > 0) {
|
|
271
|
-
const pnlStr = this.pnlDollars >= 0
|
|
272
|
-
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
273
|
-
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
274
|
-
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
275
|
-
}
|
|
276
|
-
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
277
|
-
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
280
|
+
// Line 1: thesis info (or explorer mode)
|
|
278
281
|
const sep = C.zinc600(' \u2502 ');
|
|
279
|
-
|
|
282
|
+
let line1Parts;
|
|
283
|
+
if (this.isExplorer) {
|
|
284
|
+
const id = C.emerald(bold('Explorer'));
|
|
285
|
+
const edges = C.zinc600(`${this.edgeCount} public edges`);
|
|
286
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
287
|
+
let pnl = '';
|
|
288
|
+
if (this.positionCount > 0) {
|
|
289
|
+
const pnlStr = this.pnlDollars >= 0
|
|
290
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
291
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
292
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
293
|
+
}
|
|
294
|
+
line1Parts = [id, pnl, edges, top].filter(Boolean);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
const id = C.emerald(this.thesisId);
|
|
298
|
+
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
299
|
+
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
300
|
+
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
301
|
+
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
302
|
+
let pnl = '';
|
|
303
|
+
if (this.positionCount > 0) {
|
|
304
|
+
const pnlStr = this.pnlDollars >= 0
|
|
305
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
306
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
307
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
308
|
+
}
|
|
309
|
+
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
310
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
311
|
+
line1Parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
312
|
+
}
|
|
280
313
|
let line1 = C.bgZinc800(' ' + truncateToWidth(line1Parts.join(sep), width - 2, '') + ' ');
|
|
281
314
|
const l1vw = visibleWidth(line1);
|
|
282
315
|
if (l1vw < width)
|
|
@@ -393,14 +426,18 @@ function renderPositions(positions) {
|
|
|
393
426
|
return lines.join('\n');
|
|
394
427
|
}
|
|
395
428
|
// ─── Thesis selector (arrow keys + enter, like Claude Code) ─────────────────
|
|
396
|
-
async function selectThesis(theses) {
|
|
429
|
+
async function selectThesis(theses, includeExplorer = false) {
|
|
397
430
|
return new Promise((resolve) => {
|
|
398
431
|
let selected = 0;
|
|
399
|
-
const items =
|
|
432
|
+
const items = [];
|
|
433
|
+
if (includeExplorer) {
|
|
434
|
+
items.push({ id: '_explorer', conf: -1, title: 'Explorer mode — no thesis, full market access' });
|
|
435
|
+
}
|
|
436
|
+
for (const t of theses) {
|
|
400
437
|
const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
|
|
401
438
|
const title = (t.rawThesis || t.thesis || t.title || '').slice(0, 55);
|
|
402
|
-
|
|
403
|
-
}
|
|
439
|
+
items.push({ id: t.id, conf, title });
|
|
440
|
+
}
|
|
404
441
|
const write = process.stdout.write.bind(process.stdout);
|
|
405
442
|
// Use alternate screen buffer for clean rendering (like Claude Code)
|
|
406
443
|
write('\x1b[?1049h'); // enter alternate screen
|
|
@@ -412,10 +449,16 @@ async function selectThesis(theses) {
|
|
|
412
449
|
const item = items[i];
|
|
413
450
|
const sel = i === selected;
|
|
414
451
|
const cursor = sel ? '\x1b[38;2;16;185;129m › \x1b[39m' : ' ';
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
452
|
+
if (item.id === '_explorer') {
|
|
453
|
+
const title = sel ? `\x1b[38;2;16;185;129m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
454
|
+
write(`${cursor}${title}\n`);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const id = sel ? `\x1b[38;2;16;185;129m${item.id.slice(0, 8)}\x1b[39m` : `\x1b[38;2;55;55;60m${item.id.slice(0, 8)}\x1b[39m`;
|
|
458
|
+
const conf = `\x1b[38;2;55;55;60m${item.conf}%\x1b[39m`;
|
|
459
|
+
const title = sel ? `\x1b[38;2;160;160;165m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
460
|
+
write(`${cursor}${id} ${conf} ${title}\n`);
|
|
461
|
+
}
|
|
419
462
|
}
|
|
420
463
|
write(`\n \x1b[38;2;55;55;60m↑↓ navigate · enter select\x1b[39m`);
|
|
421
464
|
}
|
|
@@ -507,36 +550,35 @@ async function agentCommand(thesisId, opts) {
|
|
|
507
550
|
}
|
|
508
551
|
const sfClient = new client_js_1.SFClient();
|
|
509
552
|
// ── Resolve thesis ID (interactive selection if needed) ─────────────────────
|
|
510
|
-
let resolvedThesisId = thesisId;
|
|
553
|
+
let resolvedThesisId = thesisId || null;
|
|
554
|
+
let explorerMode = false;
|
|
511
555
|
if (!resolvedThesisId) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
556
|
+
let active = [];
|
|
557
|
+
try {
|
|
558
|
+
const data = await sfClient.listTheses();
|
|
559
|
+
const theses = (data.theses || data);
|
|
560
|
+
active = theses.filter((t) => t.status === 'active');
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// No API key or network error — explorer mode
|
|
564
|
+
active = [];
|
|
565
|
+
}
|
|
515
566
|
if (active.length === 0) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
process.exit(1);
|
|
519
|
-
}
|
|
520
|
-
// No theses — offer to create one
|
|
521
|
-
console.log('\n No active theses found.\n');
|
|
522
|
-
const readline = await import('readline');
|
|
523
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
524
|
-
const answer = await new Promise(resolve => rl.question(' Enter a thesis to create (or press Enter to exit):\n > ', resolve));
|
|
525
|
-
rl.close();
|
|
526
|
-
if (!answer.trim()) {
|
|
527
|
-
process.exit(0);
|
|
528
|
-
}
|
|
529
|
-
console.log('\n Creating thesis...\n');
|
|
530
|
-
const result = await sfClient.createThesis(answer.trim(), true);
|
|
531
|
-
resolvedThesisId = result.id;
|
|
532
|
-
console.log(` ✓ Created: ${result.id?.slice(0, 8)}\n`);
|
|
567
|
+
// No theses — go straight to explorer mode
|
|
568
|
+
explorerMode = true;
|
|
533
569
|
}
|
|
534
570
|
else if (active.length === 1) {
|
|
535
571
|
resolvedThesisId = active[0].id;
|
|
536
572
|
}
|
|
537
573
|
else if (process.stdin.isTTY && !opts?.noTui) {
|
|
538
|
-
// Multiple theses — interactive
|
|
539
|
-
|
|
574
|
+
// Multiple theses — interactive selector with explorer option at top
|
|
575
|
+
const selected = await selectThesis(active, true);
|
|
576
|
+
if (selected === '_explorer') {
|
|
577
|
+
explorerMode = true;
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
resolvedThesisId = selected;
|
|
581
|
+
}
|
|
540
582
|
}
|
|
541
583
|
else {
|
|
542
584
|
// Non-interactive (--plain, telegram, piped) — use first active
|
|
@@ -544,10 +586,18 @@ async function agentCommand(thesisId, opts) {
|
|
|
544
586
|
}
|
|
545
587
|
}
|
|
546
588
|
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
547
|
-
let latestContext
|
|
589
|
+
let latestContext;
|
|
590
|
+
if (explorerMode) {
|
|
591
|
+
const { fetchGlobalContext } = await import('../client.js');
|
|
592
|
+
latestContext = await fetchGlobalContext();
|
|
593
|
+
latestContext._explorerMode = true;
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
597
|
+
}
|
|
548
598
|
// ── Branch: plain-text mode ────────────────────────────────────────────────
|
|
549
599
|
if (opts?.noTui) {
|
|
550
|
-
return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId, latestContext, useProxy, llmBaseUrl, sfApiKey, sfApiUrl, opts });
|
|
600
|
+
return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId || '_explorer', latestContext, useProxy, llmBaseUrl, sfApiKey, sfApiUrl, opts });
|
|
551
601
|
}
|
|
552
602
|
// ── Dynamic imports (all ESM-only packages) ────────────────────────────────
|
|
553
603
|
const piTui = await import('@mariozechner/pi-tui');
|
|
@@ -690,6 +740,12 @@ async function agentCommand(thesisId, opts) {
|
|
|
690
740
|
slashCommands.splice(-2, 0, // insert before /clear and /exit
|
|
691
741
|
{ name: 'buy', description: 'TICKER QTY PRICE — quick buy' }, { name: 'sell', description: 'TICKER QTY PRICE — quick sell' }, { name: 'cancel', description: 'ORDER_ID — cancel order' });
|
|
692
742
|
}
|
|
743
|
+
// Load skills and register as slash commands
|
|
744
|
+
const skills = (0, loader_js_1.loadSkills)();
|
|
745
|
+
for (const skill of skills) {
|
|
746
|
+
const trigger = skill.trigger.replace(/^\//, ''); // remove leading /
|
|
747
|
+
slashCommands.splice(-2, 0, { name: trigger, description: `[skill] ${skill.description.slice(0, 50)}` });
|
|
748
|
+
}
|
|
693
749
|
const autocompleteProvider = new CombinedAutocompleteProvider(slashCommands, process.cwd());
|
|
694
750
|
editor.setAutocompleteProvider(autocompleteProvider);
|
|
695
751
|
// Assemble TUI tree
|
|
@@ -1218,16 +1274,34 @@ async function agentCommand(thesisId, opts) {
|
|
|
1218
1274
|
{
|
|
1219
1275
|
name: 'get_orders',
|
|
1220
1276
|
label: 'Orders',
|
|
1221
|
-
description: 'Get current resting orders on Kalshi.',
|
|
1277
|
+
description: 'Get current resting orders on Kalshi. Stale orders (>7 days old AND >10¢ from market) are flagged.',
|
|
1222
1278
|
parameters: Type.Object({
|
|
1223
1279
|
status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })),
|
|
1224
1280
|
}),
|
|
1225
1281
|
execute: async (_toolCallId, params) => {
|
|
1226
|
-
const { getOrders } = await import('../kalshi.js');
|
|
1282
|
+
const { getOrders, getMarketPrice } = await import('../kalshi.js');
|
|
1227
1283
|
const result = await getOrders({ status: params.status || 'resting', limit: 100 });
|
|
1228
1284
|
if (!result)
|
|
1229
1285
|
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1230
|
-
|
|
1286
|
+
// Enrich orders with staleness detection
|
|
1287
|
+
const enriched = await Promise.all((result.orders || []).map(async (order) => {
|
|
1288
|
+
const daysSinceCreated = order.created_time
|
|
1289
|
+
? Math.round((Date.now() - new Date(order.created_time).getTime()) / 86400000)
|
|
1290
|
+
: null;
|
|
1291
|
+
let distanceFromMarket = null;
|
|
1292
|
+
let stale = false;
|
|
1293
|
+
try {
|
|
1294
|
+
const price = await getMarketPrice(order.ticker);
|
|
1295
|
+
if (price != null && order.yes_price_dollars) {
|
|
1296
|
+
distanceFromMarket = Math.round(Math.abs(price - parseFloat(order.yes_price_dollars)) * 100);
|
|
1297
|
+
if (daysSinceCreated != null && daysSinceCreated > 7 && distanceFromMarket > 10)
|
|
1298
|
+
stale = true;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
catch { }
|
|
1302
|
+
return { ...order, daysSinceCreated, distanceFromMarket, stale };
|
|
1303
|
+
}));
|
|
1304
|
+
return { content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }], details: {} };
|
|
1231
1305
|
},
|
|
1232
1306
|
},
|
|
1233
1307
|
{
|
|
@@ -1307,42 +1381,71 @@ async function agentCommand(thesisId, opts) {
|
|
|
1307
1381
|
{
|
|
1308
1382
|
name: 'inspect_book',
|
|
1309
1383
|
label: 'Orderbook',
|
|
1310
|
-
description: 'Get orderbook depth, spread, and liquidity
|
|
1384
|
+
description: 'Get orderbook depth, spread, and liquidity. Returns a status field per market: "ok", "empty_orderbook", "market_closed", or "api_error". Supports multiple tickers in one call — use tickers array for batch position checks.',
|
|
1311
1385
|
parameters: Type.Object({
|
|
1312
|
-
ticker: Type.Optional(Type.String({ description: 'Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
1386
|
+
ticker: Type.Optional(Type.String({ description: 'Single Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
1387
|
+
tickers: Type.Optional(Type.Array(Type.String(), { description: 'Multiple Kalshi tickers for batch check (e.g. ["T$135", "T$140", "T$150"])' })),
|
|
1313
1388
|
polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
|
|
1314
1389
|
}),
|
|
1315
1390
|
execute: async (_toolCallId, params) => {
|
|
1316
1391
|
const results = [];
|
|
1317
|
-
|
|
1392
|
+
// Batch: expand tickers array into individual lookups
|
|
1393
|
+
const tickerList = [];
|
|
1394
|
+
if (params.tickers?.length)
|
|
1395
|
+
tickerList.push(...params.tickers);
|
|
1396
|
+
else if (params.ticker)
|
|
1397
|
+
tickerList.push(params.ticker);
|
|
1398
|
+
for (const tkr of tickerList) {
|
|
1318
1399
|
try {
|
|
1319
|
-
const market = await (0, client_js_1.kalshiFetchMarket)(
|
|
1320
|
-
const
|
|
1321
|
-
|
|
1322
|
-
.
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1400
|
+
const market = await (0, client_js_1.kalshiFetchMarket)(tkr);
|
|
1401
|
+
const mStatus = market.status || 'unknown';
|
|
1402
|
+
if (mStatus !== 'open' && mStatus !== 'active') {
|
|
1403
|
+
results.push({
|
|
1404
|
+
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
1405
|
+
status: 'market_closed', reason: `Market status: ${mStatus}. Orderbook unavailable for closed/settled markets.`,
|
|
1406
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
const ob = await (0, kalshi_js_1.getPublicOrderbook)(tkr);
|
|
1411
|
+
const yesBids = (ob?.yes_dollars || [])
|
|
1412
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
1413
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1414
|
+
const noAsks = (ob?.no_dollars || [])
|
|
1415
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
1416
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1417
|
+
if (yesBids.length === 0 && noAsks.length === 0) {
|
|
1418
|
+
results.push({
|
|
1419
|
+
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
1420
|
+
status: 'empty_orderbook', reason: 'Market open but no resting orders. Normal for illiquid/OTM contracts. Use lastPrice as reference.',
|
|
1421
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
1422
|
+
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
1423
|
+
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
1424
|
+
expiry: market.close_time || null,
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
const bestBid = yesBids[0]?.price || 0;
|
|
1429
|
+
const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : (yesBids[0] ? yesBids[0].price + 1 : 100);
|
|
1430
|
+
const spread = bestAsk - bestBid;
|
|
1431
|
+
const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
|
|
1432
|
+
const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
|
|
1433
|
+
results.push({
|
|
1434
|
+
venue: 'kalshi', ticker: tkr, title: market.title, status: 'ok',
|
|
1435
|
+
bestBid, bestAsk, spread, liquidityScore: liq,
|
|
1436
|
+
bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
|
|
1437
|
+
totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
|
|
1438
|
+
totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
|
|
1439
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
1440
|
+
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
1441
|
+
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
1442
|
+
expiry: market.close_time || null,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1343
1446
|
}
|
|
1344
1447
|
catch (err) {
|
|
1345
|
-
|
|
1448
|
+
results.push({ venue: 'kalshi', ticker: tkr, status: 'api_error', reason: `Kalshi API error: ${err.message}` });
|
|
1346
1449
|
}
|
|
1347
1450
|
}
|
|
1348
1451
|
if (params.polyQuery) {
|
|
@@ -1399,7 +1502,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
1399
1502
|
{
|
|
1400
1503
|
name: 'create_thesis',
|
|
1401
1504
|
label: 'Create Thesis',
|
|
1402
|
-
description: 'Create a new thesis from a raw thesis statement. Returns the thesis ID, confidence, node count, and edge count.',
|
|
1505
|
+
description: 'Create a new thesis from a raw thesis statement. Returns the thesis ID, confidence, node count, and edge count. In explorer mode, this automatically transitions to thesis mode.',
|
|
1403
1506
|
parameters: Type.Object({
|
|
1404
1507
|
rawThesis: Type.String({ description: 'The raw thesis statement to create' }),
|
|
1405
1508
|
webhookUrl: Type.Optional(Type.String({ description: 'Optional webhook URL for notifications' })),
|
|
@@ -1410,8 +1513,21 @@ async function agentCommand(thesisId, opts) {
|
|
|
1410
1513
|
const nodeCount = thesis.causalTree?.nodes?.length || 0;
|
|
1411
1514
|
const edgeCount = (thesis.edges || []).length;
|
|
1412
1515
|
const confidence = typeof thesis.confidence === 'number' ? Math.round(thesis.confidence * 100) : 0;
|
|
1516
|
+
// ── Auto-transition from explorer to thesis mode ──────────────────
|
|
1517
|
+
if (explorerMode && thesis.id) {
|
|
1518
|
+
explorerMode = false;
|
|
1519
|
+
resolvedThesisId = thesis.id;
|
|
1520
|
+
try {
|
|
1521
|
+
latestContext = await sfClient.getContext(thesis.id);
|
|
1522
|
+
const newPrompt = buildSystemPrompt(latestContext);
|
|
1523
|
+
agent.setSystemPrompt(newPrompt);
|
|
1524
|
+
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
1525
|
+
tui.requestRender();
|
|
1526
|
+
}
|
|
1527
|
+
catch { /* context fetch failed, still switch */ }
|
|
1528
|
+
}
|
|
1413
1529
|
return {
|
|
1414
|
-
content: [{ type: 'text', text: `Thesis created.\nID: ${thesis.id}\nConfidence: ${confidence}%\nNodes: ${nodeCount}\nEdges: ${edgeCount}
|
|
1530
|
+
content: [{ type: 'text', text: `Thesis created.\nID: ${thesis.id}\nConfidence: ${confidence}%\nNodes: ${nodeCount}\nEdges: ${edgeCount}\n\nHeartbeat engine is now monitoring this thesis 24/7. Use /switch ${thesis.id?.slice(0, 8)} to focus on it.` }],
|
|
1415
1531
|
details: {},
|
|
1416
1532
|
};
|
|
1417
1533
|
},
|
|
@@ -1450,14 +1566,64 @@ async function agentCommand(thesisId, opts) {
|
|
|
1450
1566
|
{
|
|
1451
1567
|
name: 'get_feed',
|
|
1452
1568
|
label: 'Get Feed',
|
|
1453
|
-
description: 'Get evaluation history
|
|
1569
|
+
description: 'Get evaluation history with topSignal highlighting. The most important signal (largest confidence change or most actionable) is surfaced first so you don\'t have to scan all entries.',
|
|
1454
1570
|
parameters: Type.Object({
|
|
1455
1571
|
hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
|
|
1456
1572
|
}),
|
|
1457
1573
|
execute: async (_toolCallId, params) => {
|
|
1458
1574
|
const result = await sfClient.getFeed(params.hours || 24);
|
|
1575
|
+
const items = Array.isArray(result) ? result : (result?.evaluations || result?.items || []);
|
|
1576
|
+
// Find the most important signal: largest |confidenceDelta|, or newest with actual content
|
|
1577
|
+
let topSignal = null;
|
|
1578
|
+
let topScore = 0;
|
|
1579
|
+
for (const item of items) {
|
|
1580
|
+
let score = 0;
|
|
1581
|
+
const delta = Math.abs(item.confidenceDelta || item.confidence_delta || 0);
|
|
1582
|
+
if (delta > 0)
|
|
1583
|
+
score = delta * 100; // confidence changes are most important
|
|
1584
|
+
else if (item.summary?.length > 50)
|
|
1585
|
+
score = 0.1; // has substance but no delta
|
|
1586
|
+
if (score > topScore) {
|
|
1587
|
+
topScore = score;
|
|
1588
|
+
topSignal = item;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const output = { total: items.length };
|
|
1592
|
+
if (topSignal) {
|
|
1593
|
+
output.topSignal = {
|
|
1594
|
+
summary: topSignal.summary || topSignal.content || '',
|
|
1595
|
+
confidenceDelta: topSignal.confidenceDelta || topSignal.confidence_delta || 0,
|
|
1596
|
+
evaluatedAt: topSignal.evaluatedAt || topSignal.evaluated_at || topSignal.createdAt || '',
|
|
1597
|
+
why: topScore > 1 ? 'Largest confidence movement in this period'
|
|
1598
|
+
: topScore > 0 ? 'Most substantive evaluation (no confidence change)'
|
|
1599
|
+
: 'Most recent evaluation',
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
output.items = items;
|
|
1459
1603
|
return {
|
|
1460
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1604
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1605
|
+
details: {},
|
|
1606
|
+
};
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
name: 'get_changes',
|
|
1611
|
+
label: 'Get Changes',
|
|
1612
|
+
description: 'Get recent market changes detected server-side. Returns real price moves (>5¢), new contracts, and removed/settled contracts across Kalshi, Polymarket, and traditional markets. Checked every 15 minutes. Use for situational awareness, discovering new opportunities, or checking if anything material happened recently.',
|
|
1613
|
+
parameters: Type.Object({
|
|
1614
|
+
hours: Type.Optional(Type.Number({ description: 'Hours of history (default 1)' })),
|
|
1615
|
+
}),
|
|
1616
|
+
execute: async (_toolCallId, params) => {
|
|
1617
|
+
const hours = params.hours || 1;
|
|
1618
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
1619
|
+
const apiUrl = process.env.SF_API_URL || 'https://simplefunctions.dev';
|
|
1620
|
+
const res = await fetch(`${apiUrl}/api/changes?since=${encodeURIComponent(since)}&limit=100`);
|
|
1621
|
+
if (!res.ok) {
|
|
1622
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `API error ${res.status}` }) }], details: {} };
|
|
1623
|
+
}
|
|
1624
|
+
const data = await res.json();
|
|
1625
|
+
return {
|
|
1626
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
1461
1627
|
details: {},
|
|
1462
1628
|
};
|
|
1463
1629
|
},
|
|
@@ -1475,7 +1641,13 @@ async function agentCommand(thesisId, opts) {
|
|
|
1475
1641
|
}), { description: 'Node probability overrides' }),
|
|
1476
1642
|
}),
|
|
1477
1643
|
execute: async (_toolCallId, params) => {
|
|
1478
|
-
//
|
|
1644
|
+
// Refresh context before simulation to avoid stale confidence values
|
|
1645
|
+
if (resolvedThesisId) {
|
|
1646
|
+
try {
|
|
1647
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
1648
|
+
}
|
|
1649
|
+
catch { }
|
|
1650
|
+
}
|
|
1479
1651
|
const ctx = latestContext;
|
|
1480
1652
|
const allNodes = [];
|
|
1481
1653
|
function flatten(nodes) {
|
|
@@ -1489,10 +1661,28 @@ async function agentCommand(thesisId, opts) {
|
|
|
1489
1661
|
flatten(rawNodes);
|
|
1490
1662
|
const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
|
|
1491
1663
|
const overrideMap = new Map(params.overrides.map((o) => [o.nodeId, o.newProbability]));
|
|
1664
|
+
// Propagate child node overrides to parent nodes.
|
|
1665
|
+
// If n2.2 is overridden, recalculate n2's effective probability
|
|
1666
|
+
// as the average of its children's (possibly overridden) probabilities.
|
|
1667
|
+
function effectiveProb(node) {
|
|
1668
|
+
// Direct override on this node
|
|
1669
|
+
if (overrideMap.has(node.id))
|
|
1670
|
+
return overrideMap.get(node.id);
|
|
1671
|
+
// If node has children, aggregate from children
|
|
1672
|
+
if (node.children?.length > 0) {
|
|
1673
|
+
const childProbs = node.children.map((c) => effectiveProb(c));
|
|
1674
|
+
const childImps = node.children.map((c) => c.importance || 1);
|
|
1675
|
+
const totalImp = childImps.reduce((s, w) => s + w, 0);
|
|
1676
|
+
if (totalImp > 0) {
|
|
1677
|
+
return childProbs.reduce((s, p, i) => s + p * childImps[i], 0) / totalImp;
|
|
1678
|
+
}
|
|
1679
|
+
return childProbs.reduce((s, p) => s + p, 0) / childProbs.length;
|
|
1680
|
+
}
|
|
1681
|
+
return node.probability ?? 0;
|
|
1682
|
+
}
|
|
1492
1683
|
const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
|
|
1493
1684
|
const newConf = treeNodes.reduce((s, n) => {
|
|
1494
|
-
|
|
1495
|
-
return s + p * (n.importance || 0);
|
|
1685
|
+
return s + effectiveProb(n) * (n.importance || 0);
|
|
1496
1686
|
}, 0);
|
|
1497
1687
|
const nodeScales = new Map();
|
|
1498
1688
|
for (const [nid, np] of overrideMap.entries()) {
|
|
@@ -1527,12 +1717,20 @@ async function agentCommand(thesisId, opts) {
|
|
|
1527
1717
|
signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
|
|
1528
1718
|
};
|
|
1529
1719
|
}).filter((e) => e.signal !== 'unchanged');
|
|
1720
|
+
// Server confidence = LLM's holistic assessment (includes factors beyond the tree)
|
|
1721
|
+
// Tree confidence = weighted sum of node probabilities (pure math from causal tree)
|
|
1722
|
+
// These measure different things and will often differ.
|
|
1723
|
+
const serverConf = ctx.confidence != null ? Math.round(Number(ctx.confidence) * 100) : null;
|
|
1530
1724
|
const result = {
|
|
1531
1725
|
overrides: params.overrides.map((o) => {
|
|
1532
1726
|
const node = allNodes.find((n) => n.id === o.nodeId);
|
|
1533
1727
|
return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
|
|
1534
1728
|
}),
|
|
1535
|
-
|
|
1729
|
+
serverConfidence: serverConf,
|
|
1730
|
+
treeConfidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
|
|
1731
|
+
note: serverConf != null && Math.abs(serverConf - Math.round(oldConf * 100)) > 5
|
|
1732
|
+
? `serverConfidence (${serverConf}%) differs from treeConfidence (${Math.round(oldConf * 100)}%) because the LLM evaluation considers factors beyond the causal tree.`
|
|
1733
|
+
: undefined,
|
|
1536
1734
|
affectedEdges: edges,
|
|
1537
1735
|
};
|
|
1538
1736
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
@@ -1811,7 +2009,58 @@ ${edgesSummary}
|
|
|
1811
2009
|
|
|
1812
2010
|
${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
|
|
1813
2011
|
}
|
|
1814
|
-
|
|
2012
|
+
function buildExplorerPrompt(ctx) {
|
|
2013
|
+
const config = (0, config_js_1.loadConfig)();
|
|
2014
|
+
const theseCount = ctx.theses?.length || 0;
|
|
2015
|
+
const edgeCount = ctx.edges?.length || 0;
|
|
2016
|
+
const topEdges = (ctx.edges || [])
|
|
2017
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
2018
|
+
.slice(0, 5)
|
|
2019
|
+
.map((e) => ` ${(e.title || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.price}¢ | edge +${e.edge}`)
|
|
2020
|
+
.join('\n') || ' (no edges)';
|
|
2021
|
+
return `You are a prediction market research assistant with access to live data across Kalshi, Polymarket, X/Twitter, and traditional markets.
|
|
2022
|
+
|
|
2023
|
+
You are in EXPLORER MODE — not bound to any specific thesis. Help the user research, compare, and understand prediction market data across all sources.
|
|
2024
|
+
|
|
2025
|
+
## What you can do
|
|
2026
|
+
- Search and compare markets across Kalshi and Polymarket (scan_markets)
|
|
2027
|
+
- Answer questions with live market data + LLM synthesis (query)
|
|
2028
|
+
- Check traditional market prices — SPY, VIX, gold, oil, bonds (get_markets)
|
|
2029
|
+
- Browse public theses and their edges (explore_public)
|
|
2030
|
+
- Search X/Twitter for sentiment and breaking news (search_x, x_volume, x_news)
|
|
2031
|
+
- Check orderbook depth and liquidity (inspect_book, get_liquidity)
|
|
2032
|
+
- View user positions across venues (get_positions)
|
|
2033
|
+
- Create a new thesis when the user forms a view (create_thesis)
|
|
2034
|
+
|
|
2035
|
+
## CRITICAL: Thesis creation transition
|
|
2036
|
+
When the user expresses a market view worth tracking — explicitly ("create a thesis") or implicitly ("I think oil stays above $100", "the war won't end soon") — use create_thesis to create it. After creation, tell the user: "Thesis created. The heartbeat engine is now monitoring this 24/7. Use /switch <id> to focus on it."
|
|
2037
|
+
|
|
2038
|
+
## Your analytical framework
|
|
2039
|
+
Edge = thesis price - market price. Positive = market underprices.
|
|
2040
|
+
Edge types: "consensus_gap" (real disagreement), "attention_gap" (no real pricing), "timing_gap" (market lags), "risk_premium" (settlement risk).
|
|
2041
|
+
Price reliability: depth >= 500 = consensus. depth < 100 = unreliable. spread > 5¢ = noisy.
|
|
2042
|
+
Always state contract expiry and next catalyst. No catalyst = capital lock risk.
|
|
2043
|
+
|
|
2044
|
+
## Your behavioral rules
|
|
2045
|
+
- Be concise. Use tools for fresh data. Don't guess prices.
|
|
2046
|
+
- You do NOT know the user's positions at start. Call get_positions before discussing trades.
|
|
2047
|
+
- If user mentions news, offer to create a thesis or inject a signal if one exists.
|
|
2048
|
+
- Don't end with "anything else?"
|
|
2049
|
+
- Use Chinese if user writes Chinese, English if English.
|
|
2050
|
+
- Prices in cents (¢). P&L in dollars ($).
|
|
2051
|
+
|
|
2052
|
+
## Trading status
|
|
2053
|
+
${config.tradingEnabled ? 'Trading is ENABLED.' : 'Trading is DISABLED. Tell user: sf setup --enable-trading'}
|
|
2054
|
+
|
|
2055
|
+
## Current market snapshot
|
|
2056
|
+
Public theses tracked: ${theseCount}
|
|
2057
|
+
Top edges across all public theses:
|
|
2058
|
+
${topEdges}
|
|
2059
|
+
`;
|
|
2060
|
+
}
|
|
2061
|
+
const systemPrompt = explorerMode
|
|
2062
|
+
? buildExplorerPrompt(latestContext)
|
|
2063
|
+
: buildSystemPrompt(latestContext);
|
|
1815
2064
|
// ── Create Agent ───────────────────────────────────────────────────────────
|
|
1816
2065
|
const agent = new Agent({
|
|
1817
2066
|
initialState: {
|
|
@@ -1830,7 +2079,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1830
2079
|
// ── Session restore ────────────────────────────────────────────────────────
|
|
1831
2080
|
let sessionRestored = false;
|
|
1832
2081
|
if (!opts?.newSession) {
|
|
1833
|
-
const saved = loadSession(resolvedThesisId);
|
|
2082
|
+
const saved = loadSession(resolvedThesisId || '_explorer');
|
|
1834
2083
|
if (saved?.messages?.length > 0) {
|
|
1835
2084
|
try {
|
|
1836
2085
|
// Clean corrupted messages: empty content, missing role, broken alternation
|
|
@@ -1871,7 +2120,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1871
2120
|
try {
|
|
1872
2121
|
const msgs = agent.state.messages;
|
|
1873
2122
|
if (msgs.length > 0) {
|
|
1874
|
-
saveSession(resolvedThesisId, currentModelName, msgs);
|
|
2123
|
+
saveSession(resolvedThesisId || '_explorer', currentModelName, msgs);
|
|
1875
2124
|
}
|
|
1876
2125
|
}
|
|
1877
2126
|
catch { /* best-effort save */ }
|
|
@@ -2008,6 +2257,10 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
2008
2257
|
C.emerald('/sell ') + C.zinc400('TICKER QTY PRICE \u2014 quick sell') + '\n' +
|
|
2009
2258
|
C.emerald('/cancel ') + C.zinc400('ORDER_ID \u2014 cancel order') + '\n' +
|
|
2010
2259
|
C.zinc600('\u2500'.repeat(30)) + '\n') : '') +
|
|
2260
|
+
(skills.length > 0 ? (C.zinc600('\u2500'.repeat(30)) + '\n' +
|
|
2261
|
+
C.zinc200(bold('Skills')) + '\n' +
|
|
2262
|
+
skills.map(s => C.emerald(`/${s.name.padEnd(10)}`) + C.zinc400(s.description.slice(0, 45))).join('\n') + '\n' +
|
|
2263
|
+
C.zinc600('\u2500'.repeat(30)) + '\n') : '') +
|
|
2011
2264
|
C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
|
|
2012
2265
|
C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
|
|
2013
2266
|
addSpacer();
|
|
@@ -2015,29 +2268,58 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
2015
2268
|
}
|
|
2016
2269
|
case '/tree': {
|
|
2017
2270
|
addSpacer();
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2021
|
-
addSystemText(C.zinc200(bold('Causal Tree')) + '\n' + renderCausalTree(latestContext, piTui));
|
|
2271
|
+
if (explorerMode) {
|
|
2272
|
+
addSystemText(C.zinc400('No thesis selected. Use /switch <id> to pick one, or ask me to create one.'));
|
|
2022
2273
|
}
|
|
2023
|
-
|
|
2024
|
-
|
|
2274
|
+
else {
|
|
2275
|
+
try {
|
|
2276
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2277
|
+
addSystemText(C.zinc200(bold('Causal Tree')) + '\n' + renderCausalTree(latestContext, piTui));
|
|
2278
|
+
}
|
|
2279
|
+
catch (err) {
|
|
2280
|
+
addSystemText(C.red(`Error: ${err.message}`));
|
|
2281
|
+
}
|
|
2025
2282
|
}
|
|
2026
2283
|
addSpacer();
|
|
2027
2284
|
return true;
|
|
2028
2285
|
}
|
|
2029
2286
|
case '/edges': {
|
|
2030
2287
|
addSpacer();
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2288
|
+
if (explorerMode) {
|
|
2289
|
+
// Show global public edges
|
|
2290
|
+
try {
|
|
2291
|
+
const { fetchGlobalContext } = await import('../client.js');
|
|
2292
|
+
const global = await fetchGlobalContext();
|
|
2293
|
+
const edges = (global.edges || []).sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 10);
|
|
2294
|
+
if (edges.length === 0) {
|
|
2295
|
+
addSystemText(C.zinc400('No public edges available.'));
|
|
2296
|
+
}
|
|
2297
|
+
else {
|
|
2298
|
+
const lines = edges.map((e) => {
|
|
2299
|
+
const name = (e.title || '').slice(0, 35).padEnd(35);
|
|
2300
|
+
const venue = (e.venue || 'kalshi').padEnd(5);
|
|
2301
|
+
const mkt = String(Math.round(e.price || 0)).padStart(3) + '¢';
|
|
2302
|
+
const edge = '+' + Math.round(e.edge || 0);
|
|
2303
|
+
return ` ${C.zinc400(name)} ${C.zinc600(venue)} ${C.zinc400(mkt)} ${C.emerald(edge.padStart(4))}`;
|
|
2304
|
+
}).join('\n');
|
|
2305
|
+
addSystemText(C.zinc200(bold('Public Edges')) + '\n' + lines);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
catch (err) {
|
|
2309
|
+
addSystemText(C.red(`Error: ${err.message}`));
|
|
2036
2310
|
}
|
|
2037
|
-
addSystemText(C.zinc200(bold('Edges')) + '\n' + renderEdges(latestContext, piTui));
|
|
2038
2311
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2312
|
+
else {
|
|
2313
|
+
try {
|
|
2314
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2315
|
+
if (cachedPositions) {
|
|
2316
|
+
latestContext._positions = cachedPositions;
|
|
2317
|
+
}
|
|
2318
|
+
addSystemText(C.zinc200(bold('Edges')) + '\n' + renderEdges(latestContext, piTui));
|
|
2319
|
+
}
|
|
2320
|
+
catch (err) {
|
|
2321
|
+
addSystemText(C.red(`Error: ${err.message}`));
|
|
2322
|
+
}
|
|
2041
2323
|
}
|
|
2042
2324
|
addSpacer();
|
|
2043
2325
|
return true;
|
|
@@ -2068,6 +2350,11 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
2068
2350
|
}
|
|
2069
2351
|
case '/eval': {
|
|
2070
2352
|
addSpacer();
|
|
2353
|
+
if (explorerMode) {
|
|
2354
|
+
addSystemText(C.zinc400('No thesis selected. Use /switch <id> to pick one, or ask me to create one.'));
|
|
2355
|
+
addSpacer();
|
|
2356
|
+
return true;
|
|
2357
|
+
}
|
|
2071
2358
|
addSystemText(C.zinc600('Triggering evaluation...'));
|
|
2072
2359
|
tui.requestRender();
|
|
2073
2360
|
try {
|
|
@@ -2456,8 +2743,29 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
2456
2743
|
cleanup();
|
|
2457
2744
|
return true;
|
|
2458
2745
|
}
|
|
2459
|
-
default:
|
|
2746
|
+
default: {
|
|
2747
|
+
// Check if it's a skill trigger
|
|
2748
|
+
const skill = skills.find(s => s.trigger === command);
|
|
2749
|
+
if (skill) {
|
|
2750
|
+
addSpacer();
|
|
2751
|
+
addSystemText(C.zinc200(`Running skill: ${bold(skill.name)}`) + C.zinc600(` \u2014 ${skill.description.slice(0, 60)}`));
|
|
2752
|
+
addSpacer();
|
|
2753
|
+
tui.requestRender();
|
|
2754
|
+
// Inject the skill prompt → agent executes using existing tools
|
|
2755
|
+
isProcessing = true;
|
|
2756
|
+
try {
|
|
2757
|
+
await agent.prompt(skill.prompt);
|
|
2758
|
+
}
|
|
2759
|
+
catch (err) {
|
|
2760
|
+
addSystemText(C.red(`Skill error: ${err.message}`));
|
|
2761
|
+
}
|
|
2762
|
+
finally {
|
|
2763
|
+
isProcessing = false;
|
|
2764
|
+
}
|
|
2765
|
+
return true;
|
|
2766
|
+
}
|
|
2460
2767
|
return false;
|
|
2768
|
+
}
|
|
2461
2769
|
}
|
|
2462
2770
|
}
|
|
2463
2771
|
// ── Editor submit handler ──────────────────────────────────────────────────
|
|
@@ -2533,6 +2841,34 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
2533
2841
|
// ── Welcome dashboard builder ────────────────────────────────────────────
|
|
2534
2842
|
function buildWelcomeDashboard(ctx, positions) {
|
|
2535
2843
|
const lines = [];
|
|
2844
|
+
// ── Explorer mode welcome ──────────────────────────────────────────────
|
|
2845
|
+
if (ctx._explorerMode) {
|
|
2846
|
+
const edgeCount = ctx.edges?.length || 0;
|
|
2847
|
+
const theseCount = ctx.theses?.length || 0;
|
|
2848
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2849
|
+
lines.push(' ' + C.emerald(bold('Explorer mode')) + C.zinc600(' — full market access, no thesis'));
|
|
2850
|
+
lines.push(' ' + C.zinc600(`${theseCount} public theses \u2502 ${edgeCount} edges tracked`));
|
|
2851
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2852
|
+
// Show top public edges
|
|
2853
|
+
const edges = ctx.edges || [];
|
|
2854
|
+
if (edges.length > 0) {
|
|
2855
|
+
const sorted = [...edges].sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 5);
|
|
2856
|
+
lines.push(' ' + C.zinc400(bold('TOP PUBLIC EDGES')) + C.zinc600(' mkt edge'));
|
|
2857
|
+
for (const e of sorted) {
|
|
2858
|
+
const name = (e.title || '').slice(0, 30).padEnd(30);
|
|
2859
|
+
const mkt = String(Math.round(e.price || 0)).padStart(3) + '\u00A2';
|
|
2860
|
+
const edge = e.edge || 0;
|
|
2861
|
+
const edgeStr = '+' + Math.round(edge);
|
|
2862
|
+
const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
|
|
2863
|
+
lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))}`);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2867
|
+
lines.push(' ' + C.zinc600('Ask anything, or describe a view to create a thesis.'));
|
|
2868
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2869
|
+
return lines.join('\n');
|
|
2870
|
+
}
|
|
2871
|
+
// ── Thesis mode welcome (existing) ────────────────────────────────────
|
|
2536
2872
|
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
2537
2873
|
const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
|
|
2538
2874
|
const conf = typeof ctx.confidence === 'number'
|
|
@@ -2630,25 +2966,26 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
2630
2966
|
}
|
|
2631
2967
|
// absDelta === 0: truly nothing changed, stay silent
|
|
2632
2968
|
}
|
|
2633
|
-
// ── Start heartbeat polling
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2969
|
+
// ── Start heartbeat polling (thesis mode only) ──────────────────────────
|
|
2970
|
+
if (!explorerMode)
|
|
2971
|
+
heartbeatPollTimer = setInterval(async () => {
|
|
2972
|
+
try {
|
|
2973
|
+
const delta = await sfClient.getChanges(resolvedThesisId, lastPollTimestamp);
|
|
2974
|
+
lastPollTimestamp = new Date().toISOString();
|
|
2975
|
+
if (!delta.changed)
|
|
2976
|
+
return;
|
|
2977
|
+
if (isProcessing || pendingPrompt) {
|
|
2978
|
+
// Agent is busy — queue for delivery after agent_end
|
|
2979
|
+
pendingHeartbeatDelta = delta;
|
|
2980
|
+
}
|
|
2981
|
+
else {
|
|
2982
|
+
handleHeartbeatDelta(delta);
|
|
2983
|
+
}
|
|
2643
2984
|
}
|
|
2644
|
-
|
|
2645
|
-
|
|
2985
|
+
catch {
|
|
2986
|
+
// Silent — don't spam errors from background polling
|
|
2646
2987
|
}
|
|
2647
|
-
}
|
|
2648
|
-
catch {
|
|
2649
|
-
// Silent — don't spam errors from background polling
|
|
2650
|
-
}
|
|
2651
|
-
}, 60_000); // every 60 seconds
|
|
2988
|
+
}, 60_000); // every 60 seconds
|
|
2652
2989
|
// ── Start TUI ──────────────────────────────────────────────────────────────
|
|
2653
2990
|
tui.start();
|
|
2654
2991
|
}
|
|
@@ -3008,42 +3345,71 @@ async function runPlainTextAgent(params) {
|
|
|
3008
3345
|
{
|
|
3009
3346
|
name: 'inspect_book',
|
|
3010
3347
|
label: 'Orderbook',
|
|
3011
|
-
description: 'Get orderbook depth, spread, and liquidity
|
|
3348
|
+
description: 'Get orderbook depth, spread, and liquidity. Returns a status field per market: "ok", "empty_orderbook", "market_closed", or "api_error". Supports multiple tickers in one call — use tickers array for batch position checks.',
|
|
3012
3349
|
parameters: Type.Object({
|
|
3013
|
-
ticker: Type.Optional(Type.String({ description: 'Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
3350
|
+
ticker: Type.Optional(Type.String({ description: 'Single Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
3351
|
+
tickers: Type.Optional(Type.Array(Type.String(), { description: 'Multiple Kalshi tickers for batch check (e.g. ["T$135", "T$140", "T$150"])' })),
|
|
3014
3352
|
polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
|
|
3015
3353
|
}),
|
|
3016
3354
|
execute: async (_toolCallId, params) => {
|
|
3017
3355
|
const results = [];
|
|
3018
|
-
|
|
3356
|
+
// Batch: expand tickers array into individual lookups
|
|
3357
|
+
const tickerList = [];
|
|
3358
|
+
if (params.tickers?.length)
|
|
3359
|
+
tickerList.push(...params.tickers);
|
|
3360
|
+
else if (params.ticker)
|
|
3361
|
+
tickerList.push(params.ticker);
|
|
3362
|
+
for (const tkr of tickerList) {
|
|
3019
3363
|
try {
|
|
3020
|
-
const market = await (0, client_js_1.kalshiFetchMarket)(
|
|
3021
|
-
const
|
|
3022
|
-
|
|
3023
|
-
.
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3364
|
+
const market = await (0, client_js_1.kalshiFetchMarket)(tkr);
|
|
3365
|
+
const mStatus = market.status || 'unknown';
|
|
3366
|
+
if (mStatus !== 'open' && mStatus !== 'active') {
|
|
3367
|
+
results.push({
|
|
3368
|
+
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
3369
|
+
status: 'market_closed', reason: `Market status: ${mStatus}. Orderbook unavailable for closed/settled markets.`,
|
|
3370
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
3371
|
+
});
|
|
3372
|
+
}
|
|
3373
|
+
else {
|
|
3374
|
+
const ob = await (0, kalshi_js_1.getPublicOrderbook)(tkr);
|
|
3375
|
+
const yesBids = (ob?.yes_dollars || [])
|
|
3376
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
3377
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
3378
|
+
const noAsks = (ob?.no_dollars || [])
|
|
3379
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
3380
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
3381
|
+
if (yesBids.length === 0 && noAsks.length === 0) {
|
|
3382
|
+
results.push({
|
|
3383
|
+
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
3384
|
+
status: 'empty_orderbook', reason: 'Market open but no resting orders. Normal for illiquid/OTM contracts. Use lastPrice as reference.',
|
|
3385
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
3386
|
+
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
3387
|
+
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
3388
|
+
expiry: market.close_time || null,
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
else {
|
|
3392
|
+
const bestBid = yesBids[0]?.price || 0;
|
|
3393
|
+
const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : (yesBids[0] ? yesBids[0].price + 1 : 100);
|
|
3394
|
+
const spread = bestAsk - bestBid;
|
|
3395
|
+
const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
|
|
3396
|
+
const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
|
|
3397
|
+
results.push({
|
|
3398
|
+
venue: 'kalshi', ticker: tkr, title: market.title, status: 'ok',
|
|
3399
|
+
bestBid, bestAsk, spread, liquidityScore: liq,
|
|
3400
|
+
bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
|
|
3401
|
+
totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
|
|
3402
|
+
totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
|
|
3403
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
3404
|
+
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
3405
|
+
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
3406
|
+
expiry: market.close_time || null,
|
|
3407
|
+
});
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3044
3410
|
}
|
|
3045
3411
|
catch (err) {
|
|
3046
|
-
|
|
3412
|
+
results.push({ venue: 'kalshi', ticker: tkr, status: 'api_error', reason: `Kalshi API error: ${err.message}` });
|
|
3047
3413
|
}
|
|
3048
3414
|
}
|
|
3049
3415
|
if (params.polyQuery) {
|
|
@@ -3151,18 +3517,61 @@ async function runPlainTextAgent(params) {
|
|
|
3151
3517
|
{
|
|
3152
3518
|
name: 'get_feed',
|
|
3153
3519
|
label: 'Get Feed',
|
|
3154
|
-
description: 'Get evaluation history
|
|
3520
|
+
description: 'Get evaluation history with topSignal highlighting. The most important signal is surfaced first.',
|
|
3155
3521
|
parameters: Type.Object({
|
|
3156
3522
|
hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
|
|
3157
3523
|
}),
|
|
3158
3524
|
execute: async (_id, p) => {
|
|
3159
3525
|
const result = await sfClient.getFeed(p.hours || 24);
|
|
3526
|
+
const items = Array.isArray(result) ? result : (result?.evaluations || result?.items || []);
|
|
3527
|
+
let topSignal = null;
|
|
3528
|
+
let topScore = 0;
|
|
3529
|
+
for (const item of items) {
|
|
3530
|
+
let score = 0;
|
|
3531
|
+
const delta = Math.abs(item.confidenceDelta || item.confidence_delta || 0);
|
|
3532
|
+
if (delta > 0)
|
|
3533
|
+
score = delta * 100;
|
|
3534
|
+
else if (item.summary?.length > 50)
|
|
3535
|
+
score = 0.1;
|
|
3536
|
+
if (score > topScore) {
|
|
3537
|
+
topScore = score;
|
|
3538
|
+
topSignal = item;
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
const output = { total: items.length };
|
|
3542
|
+
if (topSignal) {
|
|
3543
|
+
output.topSignal = {
|
|
3544
|
+
summary: topSignal.summary || topSignal.content || '',
|
|
3545
|
+
confidenceDelta: topSignal.confidenceDelta || topSignal.confidence_delta || 0,
|
|
3546
|
+
evaluatedAt: topSignal.evaluatedAt || topSignal.evaluated_at || '',
|
|
3547
|
+
why: topScore > 1 ? 'Largest confidence movement' : topScore > 0 ? 'Most substantive (no confidence change)' : 'Most recent',
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
output.items = items;
|
|
3160
3551
|
return {
|
|
3161
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
3552
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
3162
3553
|
details: {},
|
|
3163
3554
|
};
|
|
3164
3555
|
},
|
|
3165
3556
|
},
|
|
3557
|
+
{
|
|
3558
|
+
name: 'get_changes',
|
|
3559
|
+
label: 'Get Changes',
|
|
3560
|
+
description: 'Get recent market changes detected server-side. Returns real price moves (>5¢), new contracts, and removed/settled contracts across Kalshi, Polymarket, and traditional markets. Use for situational awareness and discovering new opportunities.',
|
|
3561
|
+
parameters: Type.Object({
|
|
3562
|
+
hours: Type.Optional(Type.Number({ description: 'Hours of history (default 1)' })),
|
|
3563
|
+
}),
|
|
3564
|
+
execute: async (_id, p) => {
|
|
3565
|
+
const hours = p.hours || 1;
|
|
3566
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
3567
|
+
const apiUrl = process.env.SF_API_URL || 'https://simplefunctions.dev';
|
|
3568
|
+
const res = await fetch(`${apiUrl}/api/changes?since=${encodeURIComponent(since)}&limit=100`);
|
|
3569
|
+
if (!res.ok)
|
|
3570
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `API error ${res.status}` }) }], details: {} };
|
|
3571
|
+
const data = await res.json();
|
|
3572
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3573
|
+
},
|
|
3574
|
+
},
|
|
3166
3575
|
{
|
|
3167
3576
|
name: 'explore_public',
|
|
3168
3577
|
label: 'Explore Public Theses',
|
|
@@ -3487,17 +3896,49 @@ async function runPlainTextAgent(params) {
|
|
|
3487
3896
|
}
|
|
3488
3897
|
// ── System prompt ─────────────────────────────────────────────────────────
|
|
3489
3898
|
const ctx = latestContext;
|
|
3490
|
-
const
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3899
|
+
const isExplorerPlain = ctx._explorerMode || resolvedThesisId === '_explorer';
|
|
3900
|
+
let systemPrompt;
|
|
3901
|
+
if (isExplorerPlain) {
|
|
3902
|
+
const topEdges = (ctx.edges || [])
|
|
3903
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
3904
|
+
.slice(0, 5)
|
|
3905
|
+
.map((e) => ` ${(e.title || '').slice(0, 40)} | ${e.venue || 'kalshi'} | +${e.edge}`)
|
|
3906
|
+
.join('\n') || ' (no edges)';
|
|
3907
|
+
systemPrompt = `You are a prediction market research assistant in EXPLORER MODE — not bound to any thesis.
|
|
3908
|
+
|
|
3909
|
+
## What you can do
|
|
3910
|
+
- query: LLM-enhanced market search
|
|
3911
|
+
- scan_markets: search Kalshi + Polymarket
|
|
3912
|
+
- get_markets: traditional markets (SPY, VIX, gold, oil)
|
|
3913
|
+
- explore_public: browse public theses
|
|
3914
|
+
- search_x, x_volume, x_news: X/Twitter signals
|
|
3915
|
+
- get_positions: portfolio positions
|
|
3916
|
+
- create_thesis: create a thesis when user forms a view
|
|
3917
|
+
|
|
3918
|
+
## CRITICAL: When the user expresses a view worth tracking, use create_thesis. After creation, confirm and continue with the new thesis context.
|
|
3919
|
+
|
|
3920
|
+
## Rules
|
|
3921
|
+
- Be concise. Use tools for fresh data.
|
|
3922
|
+
- Use Chinese if user writes Chinese, English if English.
|
|
3923
|
+
- Prices in cents (¢). P&L in dollars ($).
|
|
3924
|
+
${config.tradingEnabled ? '- Trading ENABLED.' : '- Trading DISABLED.'}
|
|
3925
|
+
|
|
3926
|
+
## Market snapshot
|
|
3927
|
+
Public edges:
|
|
3928
|
+
${topEdges}`;
|
|
3929
|
+
}
|
|
3930
|
+
else {
|
|
3931
|
+
const edgesSummary = ctx.edges
|
|
3932
|
+
?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
3933
|
+
.slice(0, 5)
|
|
3934
|
+
.map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
|
|
3935
|
+
.join('\n') || ' (no edges)';
|
|
3936
|
+
const nodesSummary = ctx.causalTree?.nodes
|
|
3937
|
+
?.filter((n) => n.depth === 0)
|
|
3938
|
+
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
|
|
3939
|
+
.join('\n') || ' (no causal tree)';
|
|
3940
|
+
const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
3941
|
+
systemPrompt = `You are a prediction market trading assistant. Your job is not to please the user — it is to help them see reality clearly and make correct trading decisions.
|
|
3501
3942
|
|
|
3502
3943
|
## Framework
|
|
3503
3944
|
Edge = thesis price - market price. Positive = market underprices. executableEdge = edge minus spread.
|
|
@@ -3540,6 +3981,7 @@ Top edges:
|
|
|
3540
3981
|
${edgesSummary}
|
|
3541
3982
|
|
|
3542
3983
|
${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
|
|
3984
|
+
}
|
|
3543
3985
|
// ── Create agent ──────────────────────────────────────────────────────────
|
|
3544
3986
|
const agent = new Agent({
|
|
3545
3987
|
initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
|
|
@@ -3582,11 +4024,19 @@ ${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice
|
|
|
3582
4024
|
}
|
|
3583
4025
|
});
|
|
3584
4026
|
// ── Welcome ───────────────────────────────────────────────────────────────
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
4027
|
+
if (isExplorerPlain) {
|
|
4028
|
+
console.log(`SF Agent — Explorer mode | ${currentModelName}`);
|
|
4029
|
+
console.log(`Public edges: ${(ctx.edges || []).length}`);
|
|
4030
|
+
console.log('Ask anything about prediction markets. Type /help for commands, /exit to quit.\n');
|
|
4031
|
+
}
|
|
4032
|
+
else {
|
|
4033
|
+
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
4034
|
+
const plainConf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
4035
|
+
console.log(`SF Agent — ${resolvedThesisId.slice(0, 8)} | ${plainConf}% | ${currentModelName}`);
|
|
4036
|
+
console.log(`Thesis: ${thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText}`);
|
|
4037
|
+
console.log(`Edges: ${(ctx.edges || []).length} | Status: ${ctx.status}`);
|
|
4038
|
+
console.log('Type /help for commands, /exit to quit.\n');
|
|
4039
|
+
}
|
|
3590
4040
|
// ── REPL loop ─────────────────────────────────────────────────────────────
|
|
3591
4041
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' });
|
|
3592
4042
|
rl.prompt();
|