@spfunctions/cli 1.7.16 → 1.7.19

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.
@@ -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');
@@ -318,7 +319,7 @@ function createFooterBar(piTui) {
318
319
  const tokStr = this.tokens >= 1000 ? `${(this.tokens / 1000).toFixed(1)}k` : `${this.tokens}`;
319
320
  const tokens = C.zinc600(`${tokStr} tok`);
320
321
  const exchange = this.exchangeOpen === true ? C.emerald('OPEN') : this.exchangeOpen === false ? C.red('CLOSED') : C.zinc600('...');
321
- const trading = this.tradingEnabled ? C.amber('\u26A1 trading') : C.zinc600('\u26A1 read-only');
322
+ const trading = this.tradingEnabled ? C.amber('trading') : C.zinc600('read-only');
322
323
  const help = C.zinc600('/help');
323
324
  const leftText = [model, tokens, exchange, trading].join(sep);
324
325
  const lw = visibleWidth(leftText);
@@ -739,6 +740,12 @@ async function agentCommand(thesisId, opts) {
739
740
  slashCommands.splice(-2, 0, // insert before /clear and /exit
740
741
  { name: 'buy', description: 'TICKER QTY PRICE — quick buy' }, { name: 'sell', description: 'TICKER QTY PRICE — quick sell' }, { name: 'cancel', description: 'ORDER_ID — cancel order' });
741
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
+ }
742
749
  const autocompleteProvider = new CombinedAutocompleteProvider(slashCommands, process.cwd());
743
750
  editor.setAutocompleteProvider(autocompleteProvider);
744
751
  // Assemble TUI tree
@@ -1267,16 +1274,34 @@ async function agentCommand(thesisId, opts) {
1267
1274
  {
1268
1275
  name: 'get_orders',
1269
1276
  label: 'Orders',
1270
- 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.',
1271
1278
  parameters: Type.Object({
1272
1279
  status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })),
1273
1280
  }),
1274
1281
  execute: async (_toolCallId, params) => {
1275
- const { getOrders } = await import('../kalshi.js');
1282
+ const { getOrders, getMarketPrice } = await import('../kalshi.js');
1276
1283
  const result = await getOrders({ status: params.status || 'resting', limit: 100 });
1277
1284
  if (!result)
1278
1285
  return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
1279
- return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
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: {} };
1280
1305
  },
1281
1306
  },
1282
1307
  {
@@ -1356,42 +1381,71 @@ async function agentCommand(thesisId, opts) {
1356
1381
  {
1357
1382
  name: 'inspect_book',
1358
1383
  label: 'Orderbook',
1359
- description: 'Get orderbook depth, spread, and liquidity for a specific market. Works with Kalshi tickers or Polymarket search queries. Returns bid/ask levels, depth, spread, liquidity score.',
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.',
1360
1385
  parameters: Type.Object({
1361
- 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"])' })),
1362
1388
  polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
1363
1389
  }),
1364
1390
  execute: async (_toolCallId, params) => {
1365
1391
  const results = [];
1366
- if (params.ticker) {
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) {
1367
1399
  try {
1368
- const market = await (0, client_js_1.kalshiFetchMarket)(params.ticker);
1369
- const ob = await (0, kalshi_js_1.getPublicOrderbook)(params.ticker);
1370
- const yesBids = (ob?.yes_dollars || [])
1371
- .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
1372
- .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
1373
- const noAsks = (ob?.no_dollars || [])
1374
- .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
1375
- .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
1376
- const bestBid = yesBids[0]?.price || 0;
1377
- const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : 100;
1378
- const spread = bestAsk - bestBid;
1379
- const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
1380
- const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
1381
- results.push({
1382
- venue: 'kalshi', ticker: params.ticker, title: market.title,
1383
- bestBid, bestAsk, spread, liquidityScore: liq,
1384
- bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
1385
- totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
1386
- totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
1387
- lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
1388
- volume24h: parseFloat(market.volume_24h_fp || '0'),
1389
- openInterest: parseFloat(market.open_interest_fp || '0'),
1390
- expiry: market.close_time || null,
1391
- });
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
+ }
1392
1446
  }
1393
1447
  catch (err) {
1394
- return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
1448
+ results.push({ venue: 'kalshi', ticker: tkr, status: 'api_error', reason: `Kalshi API error: ${err.message}` });
1395
1449
  }
1396
1450
  }
1397
1451
  if (params.polyQuery) {
@@ -1512,14 +1566,64 @@ async function agentCommand(thesisId, opts) {
1512
1566
  {
1513
1567
  name: 'get_feed',
1514
1568
  label: 'Get Feed',
1515
- description: 'Get evaluation history / activity feed. Shows recent evaluations, signals, and changes across theses.',
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.',
1516
1570
  parameters: Type.Object({
1517
1571
  hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
1518
1572
  }),
1519
1573
  execute: async (_toolCallId, params) => {
1520
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;
1521
1603
  return {
1522
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
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) }],
1523
1627
  details: {},
1524
1628
  };
1525
1629
  },
@@ -1537,7 +1641,13 @@ async function agentCommand(thesisId, opts) {
1537
1641
  }), { description: 'Node probability overrides' }),
1538
1642
  }),
1539
1643
  execute: async (_toolCallId, params) => {
1540
- // Inline what-if simulation
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
+ }
1541
1651
  const ctx = latestContext;
1542
1652
  const allNodes = [];
1543
1653
  function flatten(nodes) {
@@ -1551,10 +1661,28 @@ async function agentCommand(thesisId, opts) {
1551
1661
  flatten(rawNodes);
1552
1662
  const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
1553
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
+ }
1554
1683
  const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
1555
1684
  const newConf = treeNodes.reduce((s, n) => {
1556
- const p = overrideMap.get(n.id) ?? n.probability ?? 0;
1557
- return s + p * (n.importance || 0);
1685
+ return s + effectiveProb(n) * (n.importance || 0);
1558
1686
  }, 0);
1559
1687
  const nodeScales = new Map();
1560
1688
  for (const [nid, np] of overrideMap.entries()) {
@@ -1589,12 +1717,20 @@ async function agentCommand(thesisId, opts) {
1589
1717
  signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
1590
1718
  };
1591
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;
1592
1724
  const result = {
1593
1725
  overrides: params.overrides.map((o) => {
1594
1726
  const node = allNodes.find((n) => n.id === o.nodeId);
1595
1727
  return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
1596
1728
  }),
1597
- confidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
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,
1598
1734
  affectedEdges: edges,
1599
1735
  };
1600
1736
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
@@ -2071,7 +2207,7 @@ ${topEdges}
2071
2207
  }
2072
2208
  }
2073
2209
  if (event.type === 'tool_execution_start') {
2074
- const toolLine = new MutableLine(C.zinc600(` \u26A1 ${event.toolName}...`));
2210
+ const toolLine = new MutableLine(C.zinc600(` \u25B8 ${event.toolName}...`));
2075
2211
  toolStartTimes.set(event.toolCallId || event.toolName, Date.now());
2076
2212
  toolLines.set(event.toolCallId || event.toolName, toolLine);
2077
2213
  chatContainer.addChild(toolLine);
@@ -2090,7 +2226,7 @@ ${topEdges}
2090
2226
  line.setText(C.red(` \u2717 ${event.toolName} (${elapsed}s) error`));
2091
2227
  }
2092
2228
  else {
2093
- line.setText(C.zinc600(` \u26A1 ${event.toolName}`) + C.emerald(` \u2713`) + C.zinc600(` (${elapsed}s)`));
2229
+ line.setText(C.zinc600(` \u25B8 ${event.toolName} `) + C.emerald(`\u2713`) + C.zinc600(` ${elapsed}s`));
2094
2230
  }
2095
2231
  }
2096
2232
  toolStartTimes.delete(key);
@@ -2121,6 +2257,10 @@ ${topEdges}
2121
2257
  C.emerald('/sell ') + C.zinc400('TICKER QTY PRICE \u2014 quick sell') + '\n' +
2122
2258
  C.emerald('/cancel ') + C.zinc400('ORDER_ID \u2014 cancel order') + '\n' +
2123
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') : '') +
2124
2264
  C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
2125
2265
  C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
2126
2266
  addSpacer();
@@ -2603,8 +2743,29 @@ Output a structured summary. Be concise but preserve every important detail —
2603
2743
  cleanup();
2604
2744
  return true;
2605
2745
  }
2606
- 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
+ }
2607
2767
  return false;
2768
+ }
2608
2769
  }
2609
2770
  }
2610
2771
  // ── Editor submit handler ──────────────────────────────────────────────────
@@ -3184,42 +3345,71 @@ async function runPlainTextAgent(params) {
3184
3345
  {
3185
3346
  name: 'inspect_book',
3186
3347
  label: 'Orderbook',
3187
- description: 'Get orderbook depth, spread, and liquidity for a specific market. Works with Kalshi tickers or Polymarket search queries. Returns bid/ask levels, depth, spread, liquidity score.',
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.',
3188
3349
  parameters: Type.Object({
3189
- 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"])' })),
3190
3352
  polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
3191
3353
  }),
3192
3354
  execute: async (_toolCallId, params) => {
3193
3355
  const results = [];
3194
- if (params.ticker) {
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) {
3195
3363
  try {
3196
- const market = await (0, client_js_1.kalshiFetchMarket)(params.ticker);
3197
- const ob = await (0, kalshi_js_1.getPublicOrderbook)(params.ticker);
3198
- const yesBids = (ob?.yes_dollars || [])
3199
- .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
3200
- .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
3201
- const noAsks = (ob?.no_dollars || [])
3202
- .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
3203
- .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
3204
- const bestBid = yesBids[0]?.price || 0;
3205
- const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : 100;
3206
- const spread = bestAsk - bestBid;
3207
- const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
3208
- const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
3209
- results.push({
3210
- venue: 'kalshi', ticker: params.ticker, title: market.title,
3211
- bestBid, bestAsk, spread, liquidityScore: liq,
3212
- bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
3213
- totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
3214
- totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
3215
- lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
3216
- volume24h: parseFloat(market.volume_24h_fp || '0'),
3217
- openInterest: parseFloat(market.open_interest_fp || '0'),
3218
- expiry: market.close_time || null,
3219
- });
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
+ }
3220
3410
  }
3221
3411
  catch (err) {
3222
- return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
3412
+ results.push({ venue: 'kalshi', ticker: tkr, status: 'api_error', reason: `Kalshi API error: ${err.message}` });
3223
3413
  }
3224
3414
  }
3225
3415
  if (params.polyQuery) {
@@ -3327,18 +3517,61 @@ async function runPlainTextAgent(params) {
3327
3517
  {
3328
3518
  name: 'get_feed',
3329
3519
  label: 'Get Feed',
3330
- description: 'Get evaluation history / activity feed. Shows recent evaluations, signals, and changes across theses.',
3520
+ description: 'Get evaluation history with topSignal highlighting. The most important signal is surfaced first.',
3331
3521
  parameters: Type.Object({
3332
3522
  hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
3333
3523
  }),
3334
3524
  execute: async (_id, p) => {
3335
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;
3336
3551
  return {
3337
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
3552
+ content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
3338
3553
  details: {},
3339
3554
  };
3340
3555
  },
3341
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
+ },
3342
3575
  {
3343
3576
  name: 'explore_public',
3344
3577
  label: 'Explore Public Theses',
@@ -3783,7 +4016,7 @@ ${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice
3783
4016
  }
3784
4017
  }
3785
4018
  if (event.type === 'tool_execution_start') {
3786
- process.stderr.write(` \u26A1 ${event.toolName}...\n`);
4019
+ process.stderr.write(` \u25B8 ${event.toolName}...\n`);
3787
4020
  }
3788
4021
  if (event.type === 'tool_execution_end') {
3789
4022
  const status = event.isError ? '\u2717' : '\u2713';
@@ -6,7 +6,7 @@ const utils_js_1 = require("../utils.js");
6
6
  async function balanceCommand(opts) {
7
7
  const result = await (0, kalshi_js_1.getBalance)();
8
8
  if (!result)
9
- throw new Error('Kalshi not configured. Set KALSHI_API_KEY_ID + KALSHI_PRIVATE_KEY_PATH.');
9
+ throw new Error('Kalshi not configured. Run: sf setup --kalshi');
10
10
  if (opts.json) {
11
11
  console.log(JSON.stringify(result, null, 2));
12
12
  return;
@@ -152,7 +152,7 @@ async function bookCommand(tickers, opts) {
152
152
  }
153
153
  }
154
154
  if (results.length === 0) {
155
- console.log(`${utils_js_1.c.dim}No markets found.${utils_js_1.c.reset}`);
155
+ console.log(`\n ${utils_js_1.c.dim}No markets found. Check the ticker and try again.${utils_js_1.c.reset}\n`);
156
156
  return;
157
157
  }
158
158
  // ── JSON output ──
@@ -1,5 +1,6 @@
1
1
  export declare function contextCommand(id: string | undefined, opts: {
2
2
  json?: boolean;
3
+ share?: boolean;
3
4
  apiKey?: string;
4
5
  apiUrl?: string;
5
6
  }): Promise<void>;
@@ -4,6 +4,7 @@ exports.contextCommand = contextCommand;
4
4
  const client_js_1 = require("../client.js");
5
5
  const kalshi_js_1 = require("../kalshi.js");
6
6
  const utils_js_1 = require("../utils.js");
7
+ const share_js_1 = require("../share.js");
7
8
  const SF_API_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
8
9
  async function contextCommand(id, opts) {
9
10
  // ── Mode 1: No thesis ID → global market intelligence ─────────────────────
@@ -14,6 +15,10 @@ async function contextCommand(id, opts) {
14
15
  return;
15
16
  }
16
17
  const data = await res.json();
18
+ if (opts.share) {
19
+ await (0, share_js_1.shareOutput)('context', '', data);
20
+ return;
21
+ }
17
22
  if (opts.json) {
18
23
  console.log(JSON.stringify(data, null, 2));
19
24
  return;
@@ -13,6 +13,7 @@
13
13
  */
14
14
  interface EdgesOpts {
15
15
  json?: boolean;
16
+ share?: boolean;
16
17
  limit?: string;
17
18
  thesis?: string;
18
19
  minEdge?: string;