@spfunctions/cli 1.7.13 → 1.7.16
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 +397 -115
- package/dist/commands/heartbeat.d.ts +20 -0
- package/dist/commands/heartbeat.js +73 -0
- package/dist/index.js +26 -0
- package/dist/telegram/agent-bridge.js +31 -0
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -219,21 +219,37 @@ function createFooterBar(piTui) {
|
|
|
219
219
|
exchangeOpen = null;
|
|
220
220
|
cachedWidth;
|
|
221
221
|
cachedLines;
|
|
222
|
+
isExplorer = false;
|
|
222
223
|
setFromContext(ctx, positions) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
this.
|
|
224
|
+
if (ctx._explorerMode) {
|
|
225
|
+
this.isExplorer = true;
|
|
226
|
+
this.thesisId = 'Explorer';
|
|
227
|
+
this.confidence = 0;
|
|
228
|
+
this.confidenceDelta = 0;
|
|
229
|
+
this.edgeCount = (ctx.edges || []).length;
|
|
230
|
+
const edges = ctx.edges || [];
|
|
231
|
+
if (edges.length > 0) {
|
|
232
|
+
const top = [...edges].sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))[0];
|
|
233
|
+
this.topEdge = `${(top.title || '').slice(0, 20)} +${Math.round(top.edge)}¢`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
this.isExplorer = false;
|
|
238
|
+
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
239
|
+
this.confidence = typeof ctx.confidence === 'number'
|
|
240
|
+
? Math.round(ctx.confidence * 100)
|
|
241
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
242
|
+
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
243
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
244
|
+
: 0;
|
|
245
|
+
this.edgeCount = (ctx.edges || []).length;
|
|
246
|
+
const edges = ctx.edges || [];
|
|
247
|
+
if (edges.length > 0) {
|
|
248
|
+
const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
249
|
+
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
250
|
+
const edge = top.edge || top.edgeSize || 0;
|
|
251
|
+
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
252
|
+
}
|
|
237
253
|
}
|
|
238
254
|
if (positions && positions.length > 0) {
|
|
239
255
|
this.positionCount = positions.length;
|
|
@@ -260,23 +276,39 @@ function createFooterBar(piTui) {
|
|
|
260
276
|
if (this.cachedLines && this.cachedWidth === width)
|
|
261
277
|
return this.cachedLines;
|
|
262
278
|
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) : '';
|
|
279
|
+
// Line 1: thesis info (or explorer mode)
|
|
278
280
|
const sep = C.zinc600(' \u2502 ');
|
|
279
|
-
|
|
281
|
+
let line1Parts;
|
|
282
|
+
if (this.isExplorer) {
|
|
283
|
+
const id = C.emerald(bold('Explorer'));
|
|
284
|
+
const edges = C.zinc600(`${this.edgeCount} public edges`);
|
|
285
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
286
|
+
let pnl = '';
|
|
287
|
+
if (this.positionCount > 0) {
|
|
288
|
+
const pnlStr = this.pnlDollars >= 0
|
|
289
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
290
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
291
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
292
|
+
}
|
|
293
|
+
line1Parts = [id, pnl, edges, top].filter(Boolean);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const id = C.emerald(this.thesisId);
|
|
297
|
+
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
298
|
+
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
299
|
+
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
300
|
+
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
301
|
+
let pnl = '';
|
|
302
|
+
if (this.positionCount > 0) {
|
|
303
|
+
const pnlStr = this.pnlDollars >= 0
|
|
304
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
305
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
306
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
307
|
+
}
|
|
308
|
+
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
309
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
310
|
+
line1Parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
311
|
+
}
|
|
280
312
|
let line1 = C.bgZinc800(' ' + truncateToWidth(line1Parts.join(sep), width - 2, '') + ' ');
|
|
281
313
|
const l1vw = visibleWidth(line1);
|
|
282
314
|
if (l1vw < width)
|
|
@@ -393,14 +425,18 @@ function renderPositions(positions) {
|
|
|
393
425
|
return lines.join('\n');
|
|
394
426
|
}
|
|
395
427
|
// ─── Thesis selector (arrow keys + enter, like Claude Code) ─────────────────
|
|
396
|
-
async function selectThesis(theses) {
|
|
428
|
+
async function selectThesis(theses, includeExplorer = false) {
|
|
397
429
|
return new Promise((resolve) => {
|
|
398
430
|
let selected = 0;
|
|
399
|
-
const items =
|
|
431
|
+
const items = [];
|
|
432
|
+
if (includeExplorer) {
|
|
433
|
+
items.push({ id: '_explorer', conf: -1, title: 'Explorer mode — no thesis, full market access' });
|
|
434
|
+
}
|
|
435
|
+
for (const t of theses) {
|
|
400
436
|
const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
|
|
401
437
|
const title = (t.rawThesis || t.thesis || t.title || '').slice(0, 55);
|
|
402
|
-
|
|
403
|
-
}
|
|
438
|
+
items.push({ id: t.id, conf, title });
|
|
439
|
+
}
|
|
404
440
|
const write = process.stdout.write.bind(process.stdout);
|
|
405
441
|
// Use alternate screen buffer for clean rendering (like Claude Code)
|
|
406
442
|
write('\x1b[?1049h'); // enter alternate screen
|
|
@@ -412,10 +448,16 @@ async function selectThesis(theses) {
|
|
|
412
448
|
const item = items[i];
|
|
413
449
|
const sel = i === selected;
|
|
414
450
|
const cursor = sel ? '\x1b[38;2;16;185;129m › \x1b[39m' : ' ';
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
451
|
+
if (item.id === '_explorer') {
|
|
452
|
+
const title = sel ? `\x1b[38;2;16;185;129m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
453
|
+
write(`${cursor}${title}\n`);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
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`;
|
|
457
|
+
const conf = `\x1b[38;2;55;55;60m${item.conf}%\x1b[39m`;
|
|
458
|
+
const title = sel ? `\x1b[38;2;160;160;165m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
459
|
+
write(`${cursor}${id} ${conf} ${title}\n`);
|
|
460
|
+
}
|
|
419
461
|
}
|
|
420
462
|
write(`\n \x1b[38;2;55;55;60m↑↓ navigate · enter select\x1b[39m`);
|
|
421
463
|
}
|
|
@@ -507,36 +549,35 @@ async function agentCommand(thesisId, opts) {
|
|
|
507
549
|
}
|
|
508
550
|
const sfClient = new client_js_1.SFClient();
|
|
509
551
|
// ── Resolve thesis ID (interactive selection if needed) ─────────────────────
|
|
510
|
-
let resolvedThesisId = thesisId;
|
|
552
|
+
let resolvedThesisId = thesisId || null;
|
|
553
|
+
let explorerMode = false;
|
|
511
554
|
if (!resolvedThesisId) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
555
|
+
let active = [];
|
|
556
|
+
try {
|
|
557
|
+
const data = await sfClient.listTheses();
|
|
558
|
+
const theses = (data.theses || data);
|
|
559
|
+
active = theses.filter((t) => t.status === 'active');
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
// No API key or network error — explorer mode
|
|
563
|
+
active = [];
|
|
564
|
+
}
|
|
515
565
|
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`);
|
|
566
|
+
// No theses — go straight to explorer mode
|
|
567
|
+
explorerMode = true;
|
|
533
568
|
}
|
|
534
569
|
else if (active.length === 1) {
|
|
535
570
|
resolvedThesisId = active[0].id;
|
|
536
571
|
}
|
|
537
572
|
else if (process.stdin.isTTY && !opts?.noTui) {
|
|
538
|
-
// Multiple theses — interactive
|
|
539
|
-
|
|
573
|
+
// Multiple theses — interactive selector with explorer option at top
|
|
574
|
+
const selected = await selectThesis(active, true);
|
|
575
|
+
if (selected === '_explorer') {
|
|
576
|
+
explorerMode = true;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
resolvedThesisId = selected;
|
|
580
|
+
}
|
|
540
581
|
}
|
|
541
582
|
else {
|
|
542
583
|
// Non-interactive (--plain, telegram, piped) — use first active
|
|
@@ -544,10 +585,18 @@ async function agentCommand(thesisId, opts) {
|
|
|
544
585
|
}
|
|
545
586
|
}
|
|
546
587
|
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
547
|
-
let latestContext
|
|
588
|
+
let latestContext;
|
|
589
|
+
if (explorerMode) {
|
|
590
|
+
const { fetchGlobalContext } = await import('../client.js');
|
|
591
|
+
latestContext = await fetchGlobalContext();
|
|
592
|
+
latestContext._explorerMode = true;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
596
|
+
}
|
|
548
597
|
// ── Branch: plain-text mode ────────────────────────────────────────────────
|
|
549
598
|
if (opts?.noTui) {
|
|
550
|
-
return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId, latestContext, useProxy, llmBaseUrl, sfApiKey, sfApiUrl, opts });
|
|
599
|
+
return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId || '_explorer', latestContext, useProxy, llmBaseUrl, sfApiKey, sfApiUrl, opts });
|
|
551
600
|
}
|
|
552
601
|
// ── Dynamic imports (all ESM-only packages) ────────────────────────────────
|
|
553
602
|
const piTui = await import('@mariozechner/pi-tui');
|
|
@@ -1399,7 +1448,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
1399
1448
|
{
|
|
1400
1449
|
name: 'create_thesis',
|
|
1401
1450
|
label: 'Create Thesis',
|
|
1402
|
-
description: 'Create a new thesis from a raw thesis statement. Returns the thesis ID, confidence, node count, and edge count.',
|
|
1451
|
+
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
1452
|
parameters: Type.Object({
|
|
1404
1453
|
rawThesis: Type.String({ description: 'The raw thesis statement to create' }),
|
|
1405
1454
|
webhookUrl: Type.Optional(Type.String({ description: 'Optional webhook URL for notifications' })),
|
|
@@ -1410,8 +1459,21 @@ async function agentCommand(thesisId, opts) {
|
|
|
1410
1459
|
const nodeCount = thesis.causalTree?.nodes?.length || 0;
|
|
1411
1460
|
const edgeCount = (thesis.edges || []).length;
|
|
1412
1461
|
const confidence = typeof thesis.confidence === 'number' ? Math.round(thesis.confidence * 100) : 0;
|
|
1462
|
+
// ── Auto-transition from explorer to thesis mode ──────────────────
|
|
1463
|
+
if (explorerMode && thesis.id) {
|
|
1464
|
+
explorerMode = false;
|
|
1465
|
+
resolvedThesisId = thesis.id;
|
|
1466
|
+
try {
|
|
1467
|
+
latestContext = await sfClient.getContext(thesis.id);
|
|
1468
|
+
const newPrompt = buildSystemPrompt(latestContext);
|
|
1469
|
+
agent.setSystemPrompt(newPrompt);
|
|
1470
|
+
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
1471
|
+
tui.requestRender();
|
|
1472
|
+
}
|
|
1473
|
+
catch { /* context fetch failed, still switch */ }
|
|
1474
|
+
}
|
|
1413
1475
|
return {
|
|
1414
|
-
content: [{ type: 'text', text: `Thesis created.\nID: ${thesis.id}\nConfidence: ${confidence}%\nNodes: ${nodeCount}\nEdges: ${edgeCount}
|
|
1476
|
+
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
1477
|
details: {},
|
|
1416
1478
|
};
|
|
1417
1479
|
},
|
|
@@ -1588,6 +1650,37 @@ async function agentCommand(thesisId, opts) {
|
|
|
1588
1650
|
const data = await sfClient.getXAccount(params.username, { hours: params.hours });
|
|
1589
1651
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1590
1652
|
},
|
|
1653
|
+
}, {
|
|
1654
|
+
name: 'heartbeat_config',
|
|
1655
|
+
label: 'Heartbeat Config',
|
|
1656
|
+
description: 'View or update heartbeat settings for a thesis: scan intervals, model tier, budget cap, pause/resume. Also shows this month\'s cost breakdown.',
|
|
1657
|
+
parameters: Type.Object({
|
|
1658
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
1659
|
+
newsIntervalMin: Type.Optional(Type.Number({ description: 'News scan interval in minutes (15-1440)' })),
|
|
1660
|
+
xIntervalMin: Type.Optional(Type.Number({ description: 'X scan interval in minutes (60-1440)' })),
|
|
1661
|
+
evalModelTier: Type.Optional(Type.String({ description: 'Eval model: cheap, medium, or heavy' })),
|
|
1662
|
+
monthlyBudgetUsd: Type.Optional(Type.Number({ description: 'Monthly budget cap in USD (0 = unlimited)' })),
|
|
1663
|
+
paused: Type.Optional(Type.Boolean({ description: 'Pause (true) or resume (false) heartbeat' })),
|
|
1664
|
+
}),
|
|
1665
|
+
execute: async (_toolCallId, params) => {
|
|
1666
|
+
const hasUpdates = params.newsIntervalMin || params.xIntervalMin || params.evalModelTier || params.monthlyBudgetUsd !== undefined || params.paused !== undefined;
|
|
1667
|
+
if (hasUpdates) {
|
|
1668
|
+
const updates = {};
|
|
1669
|
+
if (params.newsIntervalMin)
|
|
1670
|
+
updates.newsIntervalMin = params.newsIntervalMin;
|
|
1671
|
+
if (params.xIntervalMin)
|
|
1672
|
+
updates.xIntervalMin = params.xIntervalMin;
|
|
1673
|
+
if (params.evalModelTier)
|
|
1674
|
+
updates.evalModelTier = params.evalModelTier;
|
|
1675
|
+
if (params.monthlyBudgetUsd !== undefined)
|
|
1676
|
+
updates.monthlyBudgetUsd = params.monthlyBudgetUsd;
|
|
1677
|
+
if (params.paused !== undefined)
|
|
1678
|
+
updates.paused = params.paused;
|
|
1679
|
+
await sfClient.updateHeartbeatConfig(params.thesisId, updates);
|
|
1680
|
+
}
|
|
1681
|
+
const data = await sfClient.getHeartbeatConfig(params.thesisId);
|
|
1682
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1683
|
+
},
|
|
1591
1684
|
});
|
|
1592
1685
|
// ── Trading tools (conditional on tradingEnabled) ──────────────────────────
|
|
1593
1686
|
const config = (0, config_js_1.loadConfig)();
|
|
@@ -1727,6 +1820,9 @@ Don't answer a complex question with a single tool call.
|
|
|
1727
1820
|
### Social signal research
|
|
1728
1821
|
Use search_x to check X/Twitter sentiment on any topic — especially useful for geopolitical events, macro shifts, and breaking news that moves prediction markets. Use x_volume to detect discussion spikes (velocity > 1 = increasing attention). Use x_account to track specific analysts or officials.
|
|
1729
1822
|
|
|
1823
|
+
### Heartbeat config
|
|
1824
|
+
Use heartbeat_config to view or adjust per-thesis heartbeat settings: news/X scan intervals, evaluation model tier (cheap/medium/heavy), monthly budget cap, pause/resume. Also shows this month's cost breakdown (LLM calls, search calls, tokens, spend). Useful when the user asks about costs, wants to slow down/speed up monitoring, or if you detect budget overrun.
|
|
1825
|
+
|
|
1730
1826
|
### Conditional rules
|
|
1731
1827
|
- Portfolio/positions questions: flag correlated exposure — positions sharing upstream causal nodes are not independent bets.
|
|
1732
1828
|
- No catalyst visible within 30 days + edge not improving: flag "stale capital risk."
|
|
@@ -1777,7 +1873,58 @@ ${edgesSummary}
|
|
|
1777
1873
|
|
|
1778
1874
|
${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
|
|
1779
1875
|
}
|
|
1780
|
-
|
|
1876
|
+
function buildExplorerPrompt(ctx) {
|
|
1877
|
+
const config = (0, config_js_1.loadConfig)();
|
|
1878
|
+
const theseCount = ctx.theses?.length || 0;
|
|
1879
|
+
const edgeCount = ctx.edges?.length || 0;
|
|
1880
|
+
const topEdges = (ctx.edges || [])
|
|
1881
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
1882
|
+
.slice(0, 5)
|
|
1883
|
+
.map((e) => ` ${(e.title || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.price}¢ | edge +${e.edge}`)
|
|
1884
|
+
.join('\n') || ' (no edges)';
|
|
1885
|
+
return `You are a prediction market research assistant with access to live data across Kalshi, Polymarket, X/Twitter, and traditional markets.
|
|
1886
|
+
|
|
1887
|
+
You are in EXPLORER MODE — not bound to any specific thesis. Help the user research, compare, and understand prediction market data across all sources.
|
|
1888
|
+
|
|
1889
|
+
## What you can do
|
|
1890
|
+
- Search and compare markets across Kalshi and Polymarket (scan_markets)
|
|
1891
|
+
- Answer questions with live market data + LLM synthesis (query)
|
|
1892
|
+
- Check traditional market prices — SPY, VIX, gold, oil, bonds (get_markets)
|
|
1893
|
+
- Browse public theses and their edges (explore_public)
|
|
1894
|
+
- Search X/Twitter for sentiment and breaking news (search_x, x_volume, x_news)
|
|
1895
|
+
- Check orderbook depth and liquidity (inspect_book, get_liquidity)
|
|
1896
|
+
- View user positions across venues (get_positions)
|
|
1897
|
+
- Create a new thesis when the user forms a view (create_thesis)
|
|
1898
|
+
|
|
1899
|
+
## CRITICAL: Thesis creation transition
|
|
1900
|
+
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."
|
|
1901
|
+
|
|
1902
|
+
## Your analytical framework
|
|
1903
|
+
Edge = thesis price - market price. Positive = market underprices.
|
|
1904
|
+
Edge types: "consensus_gap" (real disagreement), "attention_gap" (no real pricing), "timing_gap" (market lags), "risk_premium" (settlement risk).
|
|
1905
|
+
Price reliability: depth >= 500 = consensus. depth < 100 = unreliable. spread > 5¢ = noisy.
|
|
1906
|
+
Always state contract expiry and next catalyst. No catalyst = capital lock risk.
|
|
1907
|
+
|
|
1908
|
+
## Your behavioral rules
|
|
1909
|
+
- Be concise. Use tools for fresh data. Don't guess prices.
|
|
1910
|
+
- You do NOT know the user's positions at start. Call get_positions before discussing trades.
|
|
1911
|
+
- If user mentions news, offer to create a thesis or inject a signal if one exists.
|
|
1912
|
+
- Don't end with "anything else?"
|
|
1913
|
+
- Use Chinese if user writes Chinese, English if English.
|
|
1914
|
+
- Prices in cents (¢). P&L in dollars ($).
|
|
1915
|
+
|
|
1916
|
+
## Trading status
|
|
1917
|
+
${config.tradingEnabled ? 'Trading is ENABLED.' : 'Trading is DISABLED. Tell user: sf setup --enable-trading'}
|
|
1918
|
+
|
|
1919
|
+
## Current market snapshot
|
|
1920
|
+
Public theses tracked: ${theseCount}
|
|
1921
|
+
Top edges across all public theses:
|
|
1922
|
+
${topEdges}
|
|
1923
|
+
`;
|
|
1924
|
+
}
|
|
1925
|
+
const systemPrompt = explorerMode
|
|
1926
|
+
? buildExplorerPrompt(latestContext)
|
|
1927
|
+
: buildSystemPrompt(latestContext);
|
|
1781
1928
|
// ── Create Agent ───────────────────────────────────────────────────────────
|
|
1782
1929
|
const agent = new Agent({
|
|
1783
1930
|
initialState: {
|
|
@@ -1796,7 +1943,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1796
1943
|
// ── Session restore ────────────────────────────────────────────────────────
|
|
1797
1944
|
let sessionRestored = false;
|
|
1798
1945
|
if (!opts?.newSession) {
|
|
1799
|
-
const saved = loadSession(resolvedThesisId);
|
|
1946
|
+
const saved = loadSession(resolvedThesisId || '_explorer');
|
|
1800
1947
|
if (saved?.messages?.length > 0) {
|
|
1801
1948
|
try {
|
|
1802
1949
|
// Clean corrupted messages: empty content, missing role, broken alternation
|
|
@@ -1837,7 +1984,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1837
1984
|
try {
|
|
1838
1985
|
const msgs = agent.state.messages;
|
|
1839
1986
|
if (msgs.length > 0) {
|
|
1840
|
-
saveSession(resolvedThesisId, currentModelName, msgs);
|
|
1987
|
+
saveSession(resolvedThesisId || '_explorer', currentModelName, msgs);
|
|
1841
1988
|
}
|
|
1842
1989
|
}
|
|
1843
1990
|
catch { /* best-effort save */ }
|
|
@@ -1981,29 +2128,58 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1981
2128
|
}
|
|
1982
2129
|
case '/tree': {
|
|
1983
2130
|
addSpacer();
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
1987
|
-
addSystemText(C.zinc200(bold('Causal Tree')) + '\n' + renderCausalTree(latestContext, piTui));
|
|
2131
|
+
if (explorerMode) {
|
|
2132
|
+
addSystemText(C.zinc400('No thesis selected. Use /switch <id> to pick one, or ask me to create one.'));
|
|
1988
2133
|
}
|
|
1989
|
-
|
|
1990
|
-
|
|
2134
|
+
else {
|
|
2135
|
+
try {
|
|
2136
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2137
|
+
addSystemText(C.zinc200(bold('Causal Tree')) + '\n' + renderCausalTree(latestContext, piTui));
|
|
2138
|
+
}
|
|
2139
|
+
catch (err) {
|
|
2140
|
+
addSystemText(C.red(`Error: ${err.message}`));
|
|
2141
|
+
}
|
|
1991
2142
|
}
|
|
1992
2143
|
addSpacer();
|
|
1993
2144
|
return true;
|
|
1994
2145
|
}
|
|
1995
2146
|
case '/edges': {
|
|
1996
2147
|
addSpacer();
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2148
|
+
if (explorerMode) {
|
|
2149
|
+
// Show global public edges
|
|
2150
|
+
try {
|
|
2151
|
+
const { fetchGlobalContext } = await import('../client.js');
|
|
2152
|
+
const global = await fetchGlobalContext();
|
|
2153
|
+
const edges = (global.edges || []).sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 10);
|
|
2154
|
+
if (edges.length === 0) {
|
|
2155
|
+
addSystemText(C.zinc400('No public edges available.'));
|
|
2156
|
+
}
|
|
2157
|
+
else {
|
|
2158
|
+
const lines = edges.map((e) => {
|
|
2159
|
+
const name = (e.title || '').slice(0, 35).padEnd(35);
|
|
2160
|
+
const venue = (e.venue || 'kalshi').padEnd(5);
|
|
2161
|
+
const mkt = String(Math.round(e.price || 0)).padStart(3) + '¢';
|
|
2162
|
+
const edge = '+' + Math.round(e.edge || 0);
|
|
2163
|
+
return ` ${C.zinc400(name)} ${C.zinc600(venue)} ${C.zinc400(mkt)} ${C.emerald(edge.padStart(4))}`;
|
|
2164
|
+
}).join('\n');
|
|
2165
|
+
addSystemText(C.zinc200(bold('Public Edges')) + '\n' + lines);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
catch (err) {
|
|
2169
|
+
addSystemText(C.red(`Error: ${err.message}`));
|
|
2002
2170
|
}
|
|
2003
|
-
addSystemText(C.zinc200(bold('Edges')) + '\n' + renderEdges(latestContext, piTui));
|
|
2004
2171
|
}
|
|
2005
|
-
|
|
2006
|
-
|
|
2172
|
+
else {
|
|
2173
|
+
try {
|
|
2174
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2175
|
+
if (cachedPositions) {
|
|
2176
|
+
latestContext._positions = cachedPositions;
|
|
2177
|
+
}
|
|
2178
|
+
addSystemText(C.zinc200(bold('Edges')) + '\n' + renderEdges(latestContext, piTui));
|
|
2179
|
+
}
|
|
2180
|
+
catch (err) {
|
|
2181
|
+
addSystemText(C.red(`Error: ${err.message}`));
|
|
2182
|
+
}
|
|
2007
2183
|
}
|
|
2008
2184
|
addSpacer();
|
|
2009
2185
|
return true;
|
|
@@ -2034,6 +2210,11 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
2034
2210
|
}
|
|
2035
2211
|
case '/eval': {
|
|
2036
2212
|
addSpacer();
|
|
2213
|
+
if (explorerMode) {
|
|
2214
|
+
addSystemText(C.zinc400('No thesis selected. Use /switch <id> to pick one, or ask me to create one.'));
|
|
2215
|
+
addSpacer();
|
|
2216
|
+
return true;
|
|
2217
|
+
}
|
|
2037
2218
|
addSystemText(C.zinc600('Triggering evaluation...'));
|
|
2038
2219
|
tui.requestRender();
|
|
2039
2220
|
try {
|
|
@@ -2499,6 +2680,34 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
2499
2680
|
// ── Welcome dashboard builder ────────────────────────────────────────────
|
|
2500
2681
|
function buildWelcomeDashboard(ctx, positions) {
|
|
2501
2682
|
const lines = [];
|
|
2683
|
+
// ── Explorer mode welcome ──────────────────────────────────────────────
|
|
2684
|
+
if (ctx._explorerMode) {
|
|
2685
|
+
const edgeCount = ctx.edges?.length || 0;
|
|
2686
|
+
const theseCount = ctx.theses?.length || 0;
|
|
2687
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2688
|
+
lines.push(' ' + C.emerald(bold('Explorer mode')) + C.zinc600(' — full market access, no thesis'));
|
|
2689
|
+
lines.push(' ' + C.zinc600(`${theseCount} public theses \u2502 ${edgeCount} edges tracked`));
|
|
2690
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2691
|
+
// Show top public edges
|
|
2692
|
+
const edges = ctx.edges || [];
|
|
2693
|
+
if (edges.length > 0) {
|
|
2694
|
+
const sorted = [...edges].sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 5);
|
|
2695
|
+
lines.push(' ' + C.zinc400(bold('TOP PUBLIC EDGES')) + C.zinc600(' mkt edge'));
|
|
2696
|
+
for (const e of sorted) {
|
|
2697
|
+
const name = (e.title || '').slice(0, 30).padEnd(30);
|
|
2698
|
+
const mkt = String(Math.round(e.price || 0)).padStart(3) + '\u00A2';
|
|
2699
|
+
const edge = e.edge || 0;
|
|
2700
|
+
const edgeStr = '+' + Math.round(edge);
|
|
2701
|
+
const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
|
|
2702
|
+
lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))}`);
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2706
|
+
lines.push(' ' + C.zinc600('Ask anything, or describe a view to create a thesis.'));
|
|
2707
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2708
|
+
return lines.join('\n');
|
|
2709
|
+
}
|
|
2710
|
+
// ── Thesis mode welcome (existing) ────────────────────────────────────
|
|
2502
2711
|
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
2503
2712
|
const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
|
|
2504
2713
|
const conf = typeof ctx.confidence === 'number'
|
|
@@ -2596,25 +2805,26 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
2596
2805
|
}
|
|
2597
2806
|
// absDelta === 0: truly nothing changed, stay silent
|
|
2598
2807
|
}
|
|
2599
|
-
// ── Start heartbeat polling
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2808
|
+
// ── Start heartbeat polling (thesis mode only) ──────────────────────────
|
|
2809
|
+
if (!explorerMode)
|
|
2810
|
+
heartbeatPollTimer = setInterval(async () => {
|
|
2811
|
+
try {
|
|
2812
|
+
const delta = await sfClient.getChanges(resolvedThesisId, lastPollTimestamp);
|
|
2813
|
+
lastPollTimestamp = new Date().toISOString();
|
|
2814
|
+
if (!delta.changed)
|
|
2815
|
+
return;
|
|
2816
|
+
if (isProcessing || pendingPrompt) {
|
|
2817
|
+
// Agent is busy — queue for delivery after agent_end
|
|
2818
|
+
pendingHeartbeatDelta = delta;
|
|
2819
|
+
}
|
|
2820
|
+
else {
|
|
2821
|
+
handleHeartbeatDelta(delta);
|
|
2822
|
+
}
|
|
2609
2823
|
}
|
|
2610
|
-
|
|
2611
|
-
|
|
2824
|
+
catch {
|
|
2825
|
+
// Silent — don't spam errors from background polling
|
|
2612
2826
|
}
|
|
2613
|
-
}
|
|
2614
|
-
catch {
|
|
2615
|
-
// Silent — don't spam errors from background polling
|
|
2616
|
-
}
|
|
2617
|
-
}, 60_000); // every 60 seconds
|
|
2827
|
+
}, 60_000); // every 60 seconds
|
|
2618
2828
|
// ── Start TUI ──────────────────────────────────────────────────────────────
|
|
2619
2829
|
tui.start();
|
|
2620
2830
|
}
|
|
@@ -3345,6 +3555,36 @@ async function runPlainTextAgent(params) {
|
|
|
3345
3555
|
const data = await sfClient.getXAccount(p.username, { hours: p.hours });
|
|
3346
3556
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3347
3557
|
},
|
|
3558
|
+
}, {
|
|
3559
|
+
name: 'heartbeat_config', label: 'Heartbeat Config',
|
|
3560
|
+
description: 'View or update heartbeat settings: scan intervals, model tier, budget cap, pause/resume. Shows cost breakdown.',
|
|
3561
|
+
parameters: Type.Object({
|
|
3562
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
3563
|
+
newsIntervalMin: Type.Optional(Type.Number({ description: 'News scan interval (15-1440 min)' })),
|
|
3564
|
+
xIntervalMin: Type.Optional(Type.Number({ description: 'X scan interval (60-1440 min)' })),
|
|
3565
|
+
evalModelTier: Type.Optional(Type.String({ description: 'cheap, medium, or heavy' })),
|
|
3566
|
+
monthlyBudgetUsd: Type.Optional(Type.Number({ description: 'Monthly budget (0 = unlimited)' })),
|
|
3567
|
+
paused: Type.Optional(Type.Boolean({ description: 'Pause or resume heartbeat' })),
|
|
3568
|
+
}),
|
|
3569
|
+
execute: async (_id, p) => {
|
|
3570
|
+
const hasUp = p.newsIntervalMin || p.xIntervalMin || p.evalModelTier || p.monthlyBudgetUsd !== undefined || p.paused !== undefined;
|
|
3571
|
+
if (hasUp) {
|
|
3572
|
+
const u = {};
|
|
3573
|
+
if (p.newsIntervalMin)
|
|
3574
|
+
u.newsIntervalMin = p.newsIntervalMin;
|
|
3575
|
+
if (p.xIntervalMin)
|
|
3576
|
+
u.xIntervalMin = p.xIntervalMin;
|
|
3577
|
+
if (p.evalModelTier)
|
|
3578
|
+
u.evalModelTier = p.evalModelTier;
|
|
3579
|
+
if (p.monthlyBudgetUsd !== undefined)
|
|
3580
|
+
u.monthlyBudgetUsd = p.monthlyBudgetUsd;
|
|
3581
|
+
if (p.paused !== undefined)
|
|
3582
|
+
u.paused = p.paused;
|
|
3583
|
+
await sfClient.updateHeartbeatConfig(p.thesisId, u);
|
|
3584
|
+
}
|
|
3585
|
+
const data = await sfClient.getHeartbeatConfig(p.thesisId);
|
|
3586
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3587
|
+
},
|
|
3348
3588
|
});
|
|
3349
3589
|
// ── Trading tools (conditional on tradingEnabled) for plain mode ──────────
|
|
3350
3590
|
const config = (0, config_js_1.loadConfig)();
|
|
@@ -3423,17 +3663,49 @@ async function runPlainTextAgent(params) {
|
|
|
3423
3663
|
}
|
|
3424
3664
|
// ── System prompt ─────────────────────────────────────────────────────────
|
|
3425
3665
|
const ctx = latestContext;
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3666
|
+
const isExplorerPlain = ctx._explorerMode || resolvedThesisId === '_explorer';
|
|
3667
|
+
let systemPrompt;
|
|
3668
|
+
if (isExplorerPlain) {
|
|
3669
|
+
const topEdges = (ctx.edges || [])
|
|
3670
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
3671
|
+
.slice(0, 5)
|
|
3672
|
+
.map((e) => ` ${(e.title || '').slice(0, 40)} | ${e.venue || 'kalshi'} | +${e.edge}`)
|
|
3673
|
+
.join('\n') || ' (no edges)';
|
|
3674
|
+
systemPrompt = `You are a prediction market research assistant in EXPLORER MODE — not bound to any thesis.
|
|
3675
|
+
|
|
3676
|
+
## What you can do
|
|
3677
|
+
- query: LLM-enhanced market search
|
|
3678
|
+
- scan_markets: search Kalshi + Polymarket
|
|
3679
|
+
- get_markets: traditional markets (SPY, VIX, gold, oil)
|
|
3680
|
+
- explore_public: browse public theses
|
|
3681
|
+
- search_x, x_volume, x_news: X/Twitter signals
|
|
3682
|
+
- get_positions: portfolio positions
|
|
3683
|
+
- create_thesis: create a thesis when user forms a view
|
|
3684
|
+
|
|
3685
|
+
## CRITICAL: When the user expresses a view worth tracking, use create_thesis. After creation, confirm and continue with the new thesis context.
|
|
3686
|
+
|
|
3687
|
+
## Rules
|
|
3688
|
+
- Be concise. Use tools for fresh data.
|
|
3689
|
+
- Use Chinese if user writes Chinese, English if English.
|
|
3690
|
+
- Prices in cents (¢). P&L in dollars ($).
|
|
3691
|
+
${config.tradingEnabled ? '- Trading ENABLED.' : '- Trading DISABLED.'}
|
|
3692
|
+
|
|
3693
|
+
## Market snapshot
|
|
3694
|
+
Public edges:
|
|
3695
|
+
${topEdges}`;
|
|
3696
|
+
}
|
|
3697
|
+
else {
|
|
3698
|
+
const edgesSummary = ctx.edges
|
|
3699
|
+
?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
3700
|
+
.slice(0, 5)
|
|
3701
|
+
.map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
|
|
3702
|
+
.join('\n') || ' (no edges)';
|
|
3703
|
+
const nodesSummary = ctx.causalTree?.nodes
|
|
3704
|
+
?.filter((n) => n.depth === 0)
|
|
3705
|
+
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
|
|
3706
|
+
.join('\n') || ' (no causal tree)';
|
|
3707
|
+
const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
3708
|
+
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.
|
|
3437
3709
|
|
|
3438
3710
|
## Framework
|
|
3439
3711
|
Edge = thesis price - market price. Positive = market underprices. executableEdge = edge minus spread.
|
|
@@ -3449,6 +3721,7 @@ Always state contract expiry and next catalyst. No catalyst = flag capital lock
|
|
|
3449
3721
|
|
|
3450
3722
|
For complex questions, chain: get_context -> inspect_book -> get_liquidity -> web_search -> search_x -> synthesize.
|
|
3451
3723
|
Use search_x for social sentiment on any topic. Use x_volume to detect discussion spikes. Use x_account to track key people.
|
|
3724
|
+
Use heartbeat_config to view/adjust scan intervals, model tier, budget cap, or check cost breakdown.
|
|
3452
3725
|
|
|
3453
3726
|
Flag correlated exposure across positions sharing upstream nodes. If nothing to do, say so.
|
|
3454
3727
|
|
|
@@ -3475,6 +3748,7 @@ Top edges:
|
|
|
3475
3748
|
${edgesSummary}
|
|
3476
3749
|
|
|
3477
3750
|
${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
|
|
3751
|
+
}
|
|
3478
3752
|
// ── Create agent ──────────────────────────────────────────────────────────
|
|
3479
3753
|
const agent = new Agent({
|
|
3480
3754
|
initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
|
|
@@ -3517,11 +3791,19 @@ ${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice
|
|
|
3517
3791
|
}
|
|
3518
3792
|
});
|
|
3519
3793
|
// ── Welcome ───────────────────────────────────────────────────────────────
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3794
|
+
if (isExplorerPlain) {
|
|
3795
|
+
console.log(`SF Agent — Explorer mode | ${currentModelName}`);
|
|
3796
|
+
console.log(`Public edges: ${(ctx.edges || []).length}`);
|
|
3797
|
+
console.log('Ask anything about prediction markets. Type /help for commands, /exit to quit.\n');
|
|
3798
|
+
}
|
|
3799
|
+
else {
|
|
3800
|
+
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
3801
|
+
const plainConf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
3802
|
+
console.log(`SF Agent — ${resolvedThesisId.slice(0, 8)} | ${plainConf}% | ${currentModelName}`);
|
|
3803
|
+
console.log(`Thesis: ${thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText}`);
|
|
3804
|
+
console.log(`Edges: ${(ctx.edges || []).length} | Status: ${ctx.status}`);
|
|
3805
|
+
console.log('Type /help for commands, /exit to quit.\n');
|
|
3806
|
+
}
|
|
3525
3807
|
// ── REPL loop ─────────────────────────────────────────────────────────────
|
|
3526
3808
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' });
|
|
3527
3809
|
rl.prompt();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sf heartbeat <thesisId> — View/update per-thesis heartbeat config & costs.
|
|
3
|
+
*
|
|
4
|
+
* Examples:
|
|
5
|
+
* sf heartbeat abc123 — show config + costs
|
|
6
|
+
* sf heartbeat abc123 --news-interval 30 — set news scan to 30 min
|
|
7
|
+
* sf heartbeat abc123 --model heavy — use heavy model for eval
|
|
8
|
+
* sf heartbeat abc123 --budget 5 — $5/month cap
|
|
9
|
+
* sf heartbeat abc123 --pause — pause heartbeat
|
|
10
|
+
* sf heartbeat abc123 --resume — resume heartbeat
|
|
11
|
+
*/
|
|
12
|
+
import type { SFClient } from '../client.js';
|
|
13
|
+
export declare function heartbeatCommand(sfClient: SFClient, thesisId: string, opts: {
|
|
14
|
+
newsInterval?: string;
|
|
15
|
+
xInterval?: string;
|
|
16
|
+
model?: string;
|
|
17
|
+
budget?: string;
|
|
18
|
+
pause?: boolean;
|
|
19
|
+
resume?: boolean;
|
|
20
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sf heartbeat <thesisId> — View/update per-thesis heartbeat config & costs.
|
|
4
|
+
*
|
|
5
|
+
* Examples:
|
|
6
|
+
* sf heartbeat abc123 — show config + costs
|
|
7
|
+
* sf heartbeat abc123 --news-interval 30 — set news scan to 30 min
|
|
8
|
+
* sf heartbeat abc123 --model heavy — use heavy model for eval
|
|
9
|
+
* sf heartbeat abc123 --budget 5 — $5/month cap
|
|
10
|
+
* sf heartbeat abc123 --pause — pause heartbeat
|
|
11
|
+
* sf heartbeat abc123 --resume — resume heartbeat
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.heartbeatCommand = heartbeatCommand;
|
|
15
|
+
const utils_js_1 = require("../utils.js");
|
|
16
|
+
async function heartbeatCommand(sfClient, thesisId, opts) {
|
|
17
|
+
// Check if any update flags were passed
|
|
18
|
+
const hasUpdates = opts.newsInterval || opts.xInterval || opts.model || opts.budget !== undefined || opts.pause || opts.resume;
|
|
19
|
+
if (hasUpdates) {
|
|
20
|
+
const updates = {};
|
|
21
|
+
if (opts.newsInterval)
|
|
22
|
+
updates.newsIntervalMin = parseInt(opts.newsInterval, 10);
|
|
23
|
+
if (opts.xInterval)
|
|
24
|
+
updates.xIntervalMin = parseInt(opts.xInterval, 10);
|
|
25
|
+
if (opts.model)
|
|
26
|
+
updates.evalModelTier = opts.model;
|
|
27
|
+
if (opts.budget !== undefined)
|
|
28
|
+
updates.monthlyBudgetUsd = parseFloat(opts.budget);
|
|
29
|
+
if (opts.pause)
|
|
30
|
+
updates.paused = true;
|
|
31
|
+
if (opts.resume)
|
|
32
|
+
updates.paused = false;
|
|
33
|
+
try {
|
|
34
|
+
const result = await sfClient.updateHeartbeatConfig(thesisId, updates);
|
|
35
|
+
console.log(`\n ${utils_js_1.c.green}✓${utils_js_1.c.reset} Updated heartbeat config`);
|
|
36
|
+
console.log(` ${utils_js_1.c.dim}Updated fields: ${result.updated.join(', ')}${utils_js_1.c.reset}\n`);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(` ${utils_js_1.c.red}✗ ${err.message}${utils_js_1.c.reset}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Always show current config
|
|
44
|
+
try {
|
|
45
|
+
const data = await sfClient.getHeartbeatConfig(thesisId);
|
|
46
|
+
const cfg = data.config;
|
|
47
|
+
const costs = data.costs;
|
|
48
|
+
console.log(`\n ${utils_js_1.c.bold}Heartbeat Config${utils_js_1.c.reset} ${utils_js_1.c.dim}(${thesisId.slice(0, 8)})${utils_js_1.c.reset}`);
|
|
49
|
+
console.log();
|
|
50
|
+
// Config
|
|
51
|
+
const statusIcon = cfg.paused ? `${utils_js_1.c.red}⏸ paused${utils_js_1.c.reset}` : `${utils_js_1.c.green}▶ active${utils_js_1.c.reset}`;
|
|
52
|
+
console.log(` Status ${statusIcon}`);
|
|
53
|
+
console.log(` News interval ${utils_js_1.c.cyan}${cfg.newsIntervalMin}${utils_js_1.c.reset} min ${utils_js_1.c.dim}(${cfg.newsIntervalMin === data.defaults.newsIntervalMin ? 'default' : 'custom'})${utils_js_1.c.reset}`);
|
|
54
|
+
console.log(` X interval ${utils_js_1.c.cyan}${cfg.xIntervalMin}${utils_js_1.c.reset} min ${utils_js_1.c.dim}(${cfg.xIntervalMin === data.defaults.xIntervalMin ? 'default' : 'custom'})${utils_js_1.c.reset}`);
|
|
55
|
+
console.log(` Eval model ${utils_js_1.c.cyan}${cfg.evalModelTier}${utils_js_1.c.reset} ${utils_js_1.c.dim}(${cfg.evalModelTier === data.defaults.evalModelTier ? 'default' : 'custom'})${utils_js_1.c.reset}`);
|
|
56
|
+
console.log(` Monthly budget ${cfg.monthlyBudgetUsd > 0 ? `${utils_js_1.c.cyan}$${cfg.monthlyBudgetUsd}${utils_js_1.c.reset}` : `${utils_js_1.c.dim}unlimited${utils_js_1.c.reset}`}`);
|
|
57
|
+
console.log();
|
|
58
|
+
// Costs
|
|
59
|
+
console.log(` ${utils_js_1.c.bold}This Month${utils_js_1.c.reset}`);
|
|
60
|
+
console.log(` Total cost ${utils_js_1.c.cyan}$${costs.monthlyTotal.toFixed(4)}${utils_js_1.c.reset}`);
|
|
61
|
+
console.log(` LLM calls ${costs.llmCalls}`);
|
|
62
|
+
console.log(` Search calls ${costs.searchCalls}`);
|
|
63
|
+
console.log(` Tokens ${utils_js_1.c.dim}${costs.inputTokens.toLocaleString()} in / ${costs.outputTokens.toLocaleString()} out${utils_js_1.c.reset}`);
|
|
64
|
+
if (costs.budgetRemaining !== null) {
|
|
65
|
+
const pct = cfg.monthlyBudgetUsd > 0 ? Math.round((costs.monthlyTotal / cfg.monthlyBudgetUsd) * 100) : 0;
|
|
66
|
+
console.log(` Budget used ${pct}% ${utils_js_1.c.dim}($${costs.budgetRemaining.toFixed(2)} remaining)${utils_js_1.c.reset}`);
|
|
67
|
+
}
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error(` ${utils_js_1.c.red}✗ ${err.message}${utils_js_1.c.reset}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -59,6 +59,7 @@ const augment_js_1 = require("./commands/augment.js");
|
|
|
59
59
|
const telegram_js_1 = require("./commands/telegram.js");
|
|
60
60
|
const delta_js_1 = require("./commands/delta.js");
|
|
61
61
|
const login_js_1 = require("./commands/login.js");
|
|
62
|
+
const heartbeat_js_1 = require("./commands/heartbeat.js");
|
|
62
63
|
const query_js_1 = require("./commands/query.js");
|
|
63
64
|
const markets_js_1 = require("./commands/markets.js");
|
|
64
65
|
const x_js_1 = require("./commands/x.js");
|
|
@@ -88,6 +89,7 @@ const GROUPED_HELP = `
|
|
|
88
89
|
\x1b[36mevaluate\x1b[39m <id> Trigger deep evaluation
|
|
89
90
|
\x1b[36maugment\x1b[39m <id> Evolve causal tree with new nodes
|
|
90
91
|
\x1b[36mpublish\x1b[39m / \x1b[36munpublish\x1b[39m <id> Manage public visibility
|
|
92
|
+
\x1b[36mheartbeat\x1b[39m <id> View/configure heartbeat settings & costs
|
|
91
93
|
|
|
92
94
|
\x1b[1mSearch\x1b[22m
|
|
93
95
|
\x1b[36mquery\x1b[39m "question" LLM-enhanced market knowledge search \x1b[2m(no auth)\x1b[22m
|
|
@@ -342,6 +344,30 @@ program
|
|
|
342
344
|
const g = cmd.optsWithGlobals();
|
|
343
345
|
await run(() => (0, evaluate_js_1.evaluateCommand)(id, { apiKey: g.apiKey, apiUrl: g.apiUrl }));
|
|
344
346
|
});
|
|
347
|
+
// ── sf heartbeat <id> ─────────────────────────────────────────────────────────
|
|
348
|
+
program
|
|
349
|
+
.command('heartbeat <id>')
|
|
350
|
+
.description('View/configure per-thesis heartbeat settings and costs')
|
|
351
|
+
.option('--news-interval <min>', 'News scan interval in minutes (15-1440)')
|
|
352
|
+
.option('--x-interval <min>', 'X/social scan interval in minutes (60-1440)')
|
|
353
|
+
.option('--model <tier>', 'Eval model tier: cheap, medium, heavy')
|
|
354
|
+
.option('--budget <usd>', 'Monthly budget cap in USD (0 = unlimited)')
|
|
355
|
+
.option('--pause', 'Pause heartbeat')
|
|
356
|
+
.option('--resume', 'Resume heartbeat')
|
|
357
|
+
.action(async (id, opts, cmd) => {
|
|
358
|
+
const g = cmd.optsWithGlobals();
|
|
359
|
+
await run(() => {
|
|
360
|
+
const client = new client_js_1.SFClient(g.apiKey, g.apiUrl);
|
|
361
|
+
return (0, heartbeat_js_1.heartbeatCommand)(client, id, {
|
|
362
|
+
newsInterval: opts.newsInterval,
|
|
363
|
+
xInterval: opts.xInterval,
|
|
364
|
+
model: opts.model,
|
|
365
|
+
budget: opts.budget,
|
|
366
|
+
pause: opts.pause,
|
|
367
|
+
resume: opts.resume,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
});
|
|
345
371
|
// ── sf scan [query] ───────────────────────────────────────────────────────────
|
|
346
372
|
program
|
|
347
373
|
.command('scan [query]')
|
|
@@ -362,6 +362,36 @@ async function buildTools(sfClient, thesisId, latestContext) {
|
|
|
362
362
|
const data = await sfClient.getXAccount(p.username, { hours: p.hours });
|
|
363
363
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
364
364
|
},
|
|
365
|
+
}, {
|
|
366
|
+
name: 'heartbeat_config', label: 'Heartbeat Config',
|
|
367
|
+
description: 'View or update heartbeat settings: scan intervals, model tier, budget cap, pause/resume. Shows cost breakdown.',
|
|
368
|
+
parameters: Type.Object({
|
|
369
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
370
|
+
newsIntervalMin: Type.Optional(Type.Number({ description: 'News scan interval (15-1440 min)' })),
|
|
371
|
+
xIntervalMin: Type.Optional(Type.Number({ description: 'X scan interval (60-1440 min)' })),
|
|
372
|
+
evalModelTier: Type.Optional(Type.String({ description: 'cheap, medium, or heavy' })),
|
|
373
|
+
monthlyBudgetUsd: Type.Optional(Type.Number({ description: 'Monthly budget (0 = unlimited)' })),
|
|
374
|
+
paused: Type.Optional(Type.Boolean({ description: 'Pause or resume heartbeat' })),
|
|
375
|
+
}),
|
|
376
|
+
execute: async (_id, p) => {
|
|
377
|
+
const hasUp = p.newsIntervalMin || p.xIntervalMin || p.evalModelTier || p.monthlyBudgetUsd !== undefined || p.paused !== undefined;
|
|
378
|
+
if (hasUp) {
|
|
379
|
+
const u = {};
|
|
380
|
+
if (p.newsIntervalMin)
|
|
381
|
+
u.newsIntervalMin = p.newsIntervalMin;
|
|
382
|
+
if (p.xIntervalMin)
|
|
383
|
+
u.xIntervalMin = p.xIntervalMin;
|
|
384
|
+
if (p.evalModelTier)
|
|
385
|
+
u.evalModelTier = p.evalModelTier;
|
|
386
|
+
if (p.monthlyBudgetUsd !== undefined)
|
|
387
|
+
u.monthlyBudgetUsd = p.monthlyBudgetUsd;
|
|
388
|
+
if (p.paused !== undefined)
|
|
389
|
+
u.paused = p.paused;
|
|
390
|
+
await sfClient.updateHeartbeatConfig(p.thesisId, u);
|
|
391
|
+
}
|
|
392
|
+
const data = await sfClient.getHeartbeatConfig(p.thesisId);
|
|
393
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
394
|
+
},
|
|
365
395
|
});
|
|
366
396
|
// Trading tools (only if enabled)
|
|
367
397
|
if (config.tradingEnabled) {
|
|
@@ -470,6 +500,7 @@ Price: depth >= 500 = consensus, < 100 = unreliable, spread > 5 = noisy.
|
|
|
470
500
|
- Prices in cents (¢). P&L in dollars ($). Don't re-convert tool output units.
|
|
471
501
|
- Call tools for fresh data. Never guess prices or P&L from this prompt.
|
|
472
502
|
- Use search_x for X/Twitter sentiment. Use x_volume for discussion spikes. Use x_account to track key people.
|
|
503
|
+
- Use heartbeat_config to view/adjust scan intervals, model tier, budget cap, or check cost breakdown.
|
|
473
504
|
- You don't know user's positions. Call get_positions before discussing trades.
|
|
474
505
|
- If user mentions news, inject_signal immediately. Don't ask "should I?"
|
|
475
506
|
- If user says "evaluate" or "run it", trigger immediately.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfunctions/cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.16",
|
|
4
4
|
"description": "Prediction market intelligence CLI. Causal thesis model, 24/7 Kalshi/Polymarket scan, live orderbook, edge detection. Interactive agent mode with tool calling.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sf": "./dist/index.js"
|