@spfunctions/cli 1.7.12 → 1.7.14

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/client.d.ts CHANGED
@@ -17,6 +17,8 @@ export declare class SFClient {
17
17
  createThesis(rawThesis: string, sync?: boolean): Promise<any>;
18
18
  injectSignal(id: string, type: string, content: string, source?: string): Promise<any>;
19
19
  evaluate(id: string): Promise<any>;
20
+ getHeartbeatConfig(id: string): Promise<any>;
21
+ updateHeartbeatConfig(id: string, config: Record<string, unknown>): Promise<any>;
20
22
  getFeed(hours?: number, limit?: number): Promise<any>;
21
23
  getChanges(id: string, since: string): Promise<any>;
22
24
  updateThesis(id: string, data: Record<string, unknown>): Promise<any>;
package/dist/client.js CHANGED
@@ -74,6 +74,12 @@ class SFClient {
74
74
  async evaluate(id) {
75
75
  return this.request('POST', `/api/thesis/${id}/evaluate`);
76
76
  }
77
+ async getHeartbeatConfig(id) {
78
+ return this.request('GET', `/api/thesis/${id}/heartbeat`);
79
+ }
80
+ async updateHeartbeatConfig(id, config) {
81
+ return this.request('PATCH', `/api/thesis/${id}/heartbeat`, config);
82
+ }
77
83
  async getFeed(hours = 24, limit = 200) {
78
84
  return this.request('GET', `/api/feed?hours=${hours}&limit=${limit}`);
79
85
  }
@@ -1588,6 +1588,37 @@ async function agentCommand(thesisId, opts) {
1588
1588
  const data = await sfClient.getXAccount(params.username, { hours: params.hours });
1589
1589
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
1590
1590
  },
1591
+ }, {
1592
+ name: 'heartbeat_config',
1593
+ label: 'Heartbeat Config',
1594
+ description: 'View or update heartbeat settings for a thesis: scan intervals, model tier, budget cap, pause/resume. Also shows this month\'s cost breakdown.',
1595
+ parameters: Type.Object({
1596
+ thesisId: Type.String({ description: 'Thesis ID' }),
1597
+ newsIntervalMin: Type.Optional(Type.Number({ description: 'News scan interval in minutes (15-1440)' })),
1598
+ xIntervalMin: Type.Optional(Type.Number({ description: 'X scan interval in minutes (60-1440)' })),
1599
+ evalModelTier: Type.Optional(Type.String({ description: 'Eval model: cheap, medium, or heavy' })),
1600
+ monthlyBudgetUsd: Type.Optional(Type.Number({ description: 'Monthly budget cap in USD (0 = unlimited)' })),
1601
+ paused: Type.Optional(Type.Boolean({ description: 'Pause (true) or resume (false) heartbeat' })),
1602
+ }),
1603
+ execute: async (_toolCallId, params) => {
1604
+ const hasUpdates = params.newsIntervalMin || params.xIntervalMin || params.evalModelTier || params.monthlyBudgetUsd !== undefined || params.paused !== undefined;
1605
+ if (hasUpdates) {
1606
+ const updates = {};
1607
+ if (params.newsIntervalMin)
1608
+ updates.newsIntervalMin = params.newsIntervalMin;
1609
+ if (params.xIntervalMin)
1610
+ updates.xIntervalMin = params.xIntervalMin;
1611
+ if (params.evalModelTier)
1612
+ updates.evalModelTier = params.evalModelTier;
1613
+ if (params.monthlyBudgetUsd !== undefined)
1614
+ updates.monthlyBudgetUsd = params.monthlyBudgetUsd;
1615
+ if (params.paused !== undefined)
1616
+ updates.paused = params.paused;
1617
+ await sfClient.updateHeartbeatConfig(params.thesisId, updates);
1618
+ }
1619
+ const data = await sfClient.getHeartbeatConfig(params.thesisId);
1620
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
1621
+ },
1591
1622
  });
1592
1623
  // ── Trading tools (conditional on tradingEnabled) ──────────────────────────
1593
1624
  const config = (0, config_js_1.loadConfig)();
@@ -1727,6 +1758,9 @@ Don't answer a complex question with a single tool call.
1727
1758
  ### Social signal research
1728
1759
  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
1760
 
1761
+ ### Heartbeat config
1762
+ 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.
1763
+
1730
1764
  ### Conditional rules
1731
1765
  - Portfolio/positions questions: flag correlated exposure — positions sharing upstream causal nodes are not independent bets.
1732
1766
  - No catalyst visible within 30 days + edge not improving: flag "stale capital risk."
@@ -3345,6 +3379,36 @@ async function runPlainTextAgent(params) {
3345
3379
  const data = await sfClient.getXAccount(p.username, { hours: p.hours });
3346
3380
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
3347
3381
  },
3382
+ }, {
3383
+ name: 'heartbeat_config', label: 'Heartbeat Config',
3384
+ description: 'View or update heartbeat settings: scan intervals, model tier, budget cap, pause/resume. Shows cost breakdown.',
3385
+ parameters: Type.Object({
3386
+ thesisId: Type.String({ description: 'Thesis ID' }),
3387
+ newsIntervalMin: Type.Optional(Type.Number({ description: 'News scan interval (15-1440 min)' })),
3388
+ xIntervalMin: Type.Optional(Type.Number({ description: 'X scan interval (60-1440 min)' })),
3389
+ evalModelTier: Type.Optional(Type.String({ description: 'cheap, medium, or heavy' })),
3390
+ monthlyBudgetUsd: Type.Optional(Type.Number({ description: 'Monthly budget (0 = unlimited)' })),
3391
+ paused: Type.Optional(Type.Boolean({ description: 'Pause or resume heartbeat' })),
3392
+ }),
3393
+ execute: async (_id, p) => {
3394
+ const hasUp = p.newsIntervalMin || p.xIntervalMin || p.evalModelTier || p.monthlyBudgetUsd !== undefined || p.paused !== undefined;
3395
+ if (hasUp) {
3396
+ const u = {};
3397
+ if (p.newsIntervalMin)
3398
+ u.newsIntervalMin = p.newsIntervalMin;
3399
+ if (p.xIntervalMin)
3400
+ u.xIntervalMin = p.xIntervalMin;
3401
+ if (p.evalModelTier)
3402
+ u.evalModelTier = p.evalModelTier;
3403
+ if (p.monthlyBudgetUsd !== undefined)
3404
+ u.monthlyBudgetUsd = p.monthlyBudgetUsd;
3405
+ if (p.paused !== undefined)
3406
+ u.paused = p.paused;
3407
+ await sfClient.updateHeartbeatConfig(p.thesisId, u);
3408
+ }
3409
+ const data = await sfClient.getHeartbeatConfig(p.thesisId);
3410
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
3411
+ },
3348
3412
  });
3349
3413
  // ── Trading tools (conditional on tradingEnabled) for plain mode ──────────
3350
3414
  const config = (0, config_js_1.loadConfig)();
@@ -3449,6 +3513,7 @@ Always state contract expiry and next catalyst. No catalyst = flag capital lock
3449
3513
 
3450
3514
  For complex questions, chain: get_context -> inspect_book -> get_liquidity -> web_search -> search_x -> synthesize.
3451
3515
  Use search_x for social sentiment on any topic. Use x_volume to detect discussion spikes. Use x_account to track key people.
3516
+ Use heartbeat_config to view/adjust scan intervals, model tier, budget cap, or check cost breakdown.
3452
3517
 
3453
3518
  Flag correlated exposure across positions sharing upstream nodes. If nothing to do, say so.
3454
3519
 
@@ -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.12",
3
+ "version": "1.7.14",
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"