@spfunctions/cli 1.7.14 → 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.
Files changed (2) hide show
  1. package/dist/commands/agent.js +332 -115
  2. package/package.json +1 -1
@@ -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
- this.thesisId = (ctx.thesisId || '').slice(0, 8);
224
- this.confidence = typeof ctx.confidence === 'number'
225
- ? Math.round(ctx.confidence * 100)
226
- : (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
227
- this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
228
- ? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
229
- : 0;
230
- this.edgeCount = (ctx.edges || []).length;
231
- const edges = ctx.edges || [];
232
- if (edges.length > 0) {
233
- const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
234
- const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
235
- const edge = top.edge || top.edgeSize || 0;
236
- this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
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
- const line1Parts = [id, conf, pnl, edges, top].filter(Boolean);
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 = theses.map((t) => {
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
- return { id: t.id, conf, title };
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
- 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`;
416
- const conf = `\x1b[38;2;55;55;60m${item.conf}%\x1b[39m`;
417
- const title = sel ? `\x1b[38;2;160;160;165m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
418
- write(`${cursor}${id} ${conf} ${title}\n`);
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
- const data = await sfClient.listTheses();
513
- const theses = (data.theses || data);
514
- const active = theses.filter((t) => t.status === 'active');
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
- if (!process.stdin.isTTY) {
517
- console.error('No active thesis. Create one first: sf create "..."');
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 arrow key selector (TUI only)
539
- resolvedThesisId = await selectThesis(active);
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 = await sfClient.getContext(resolvedThesisId);
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
  },
@@ -1811,7 +1873,58 @@ ${edgesSummary}
1811
1873
 
1812
1874
  ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
1813
1875
  }
1814
- const systemPrompt = buildSystemPrompt(latestContext);
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);
1815
1928
  // ── Create Agent ───────────────────────────────────────────────────────────
1816
1929
  const agent = new Agent({
1817
1930
  initialState: {
@@ -1830,7 +1943,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
1830
1943
  // ── Session restore ────────────────────────────────────────────────────────
1831
1944
  let sessionRestored = false;
1832
1945
  if (!opts?.newSession) {
1833
- const saved = loadSession(resolvedThesisId);
1946
+ const saved = loadSession(resolvedThesisId || '_explorer');
1834
1947
  if (saved?.messages?.length > 0) {
1835
1948
  try {
1836
1949
  // Clean corrupted messages: empty content, missing role, broken alternation
@@ -1871,7 +1984,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
1871
1984
  try {
1872
1985
  const msgs = agent.state.messages;
1873
1986
  if (msgs.length > 0) {
1874
- saveSession(resolvedThesisId, currentModelName, msgs);
1987
+ saveSession(resolvedThesisId || '_explorer', currentModelName, msgs);
1875
1988
  }
1876
1989
  }
1877
1990
  catch { /* best-effort save */ }
@@ -2015,29 +2128,58 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
2015
2128
  }
2016
2129
  case '/tree': {
2017
2130
  addSpacer();
2018
- // Refresh context first
2019
- try {
2020
- latestContext = await sfClient.getContext(resolvedThesisId);
2021
- 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.'));
2022
2133
  }
2023
- catch (err) {
2024
- addSystemText(C.red(`Error: ${err.message}`));
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
+ }
2025
2142
  }
2026
2143
  addSpacer();
2027
2144
  return true;
2028
2145
  }
2029
2146
  case '/edges': {
2030
2147
  addSpacer();
2031
- try {
2032
- latestContext = await sfClient.getContext(resolvedThesisId);
2033
- // Attach cached positions for display
2034
- if (cachedPositions) {
2035
- latestContext._positions = cachedPositions;
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}`));
2036
2170
  }
2037
- addSystemText(C.zinc200(bold('Edges')) + '\n' + renderEdges(latestContext, piTui));
2038
2171
  }
2039
- catch (err) {
2040
- addSystemText(C.red(`Error: ${err.message}`));
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
+ }
2041
2183
  }
2042
2184
  addSpacer();
2043
2185
  return true;
@@ -2068,6 +2210,11 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
2068
2210
  }
2069
2211
  case '/eval': {
2070
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
+ }
2071
2218
  addSystemText(C.zinc600('Triggering evaluation...'));
2072
2219
  tui.requestRender();
2073
2220
  try {
@@ -2533,6 +2680,34 @@ Output a structured summary. Be concise but preserve every important detail —
2533
2680
  // ── Welcome dashboard builder ────────────────────────────────────────────
2534
2681
  function buildWelcomeDashboard(ctx, positions) {
2535
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) ────────────────────────────────────
2536
2711
  const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
2537
2712
  const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
2538
2713
  const conf = typeof ctx.confidence === 'number'
@@ -2630,25 +2805,26 @@ Output a structured summary. Be concise but preserve every important detail —
2630
2805
  }
2631
2806
  // absDelta === 0: truly nothing changed, stay silent
2632
2807
  }
2633
- // ── Start heartbeat polling ───────────────────────────────────────────────
2634
- heartbeatPollTimer = setInterval(async () => {
2635
- try {
2636
- const delta = await sfClient.getChanges(resolvedThesisId, lastPollTimestamp);
2637
- lastPollTimestamp = new Date().toISOString();
2638
- if (!delta.changed)
2639
- return;
2640
- if (isProcessing || pendingPrompt) {
2641
- // Agent is busy — queue for delivery after agent_end
2642
- pendingHeartbeatDelta = delta;
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
+ }
2643
2823
  }
2644
- else {
2645
- handleHeartbeatDelta(delta);
2824
+ catch {
2825
+ // Silent — don't spam errors from background polling
2646
2826
  }
2647
- }
2648
- catch {
2649
- // Silent — don't spam errors from background polling
2650
- }
2651
- }, 60_000); // every 60 seconds
2827
+ }, 60_000); // every 60 seconds
2652
2828
  // ── Start TUI ──────────────────────────────────────────────────────────────
2653
2829
  tui.start();
2654
2830
  }
@@ -3487,17 +3663,49 @@ async function runPlainTextAgent(params) {
3487
3663
  }
3488
3664
  // ── System prompt ─────────────────────────────────────────────────────────
3489
3665
  const ctx = latestContext;
3490
- const edgesSummary = ctx.edges
3491
- ?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
3492
- .slice(0, 5)
3493
- .map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
3494
- .join('\n') || ' (no edges)';
3495
- const nodesSummary = ctx.causalTree?.nodes
3496
- ?.filter((n) => n.depth === 0)
3497
- .map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
3498
- .join('\n') || ' (no causal tree)';
3499
- const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
3500
- const 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.
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.
3501
3709
 
3502
3710
  ## Framework
3503
3711
  Edge = thesis price - market price. Positive = market underprices. executableEdge = edge minus spread.
@@ -3540,6 +3748,7 @@ Top edges:
3540
3748
  ${edgesSummary}
3541
3749
 
3542
3750
  ${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
3751
+ }
3543
3752
  // ── Create agent ──────────────────────────────────────────────────────────
3544
3753
  const agent = new Agent({
3545
3754
  initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
@@ -3582,11 +3791,19 @@ ${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice
3582
3791
  }
3583
3792
  });
3584
3793
  // ── Welcome ───────────────────────────────────────────────────────────────
3585
- const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
3586
- console.log(`SF Agent — ${resolvedThesisId.slice(0, 8)} | ${conf}% | ${currentModelName}`);
3587
- console.log(`Thesis: ${thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText}`);
3588
- console.log(`Edges: ${(ctx.edges || []).length} | Status: ${ctx.status}`);
3589
- console.log('Type /help for commands, /exit to quit.\n');
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
+ }
3590
3807
  // ── REPL loop ─────────────────────────────────────────────────────────────
3591
3808
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' });
3592
3809
  rl.prompt();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.7.14",
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"