@spfunctions/cli 1.1.0 → 1.1.3
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 +286 -41
- package/package.json +9 -5
package/dist/commands/agent.js
CHANGED
|
@@ -405,6 +405,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
405
405
|
{ name: 'compact', description: 'Compress conversation history' },
|
|
406
406
|
{ name: 'new', description: 'Start fresh session' },
|
|
407
407
|
{ name: 'model', description: 'Switch model (e.g. /model anthropic/claude-sonnet-4)' },
|
|
408
|
+
{ name: 'env', description: 'Show environment variable status' },
|
|
408
409
|
{ name: 'clear', description: 'Clear screen (keeps history)' },
|
|
409
410
|
{ name: 'exit', description: 'Exit agent (auto-saves)' },
|
|
410
411
|
], process.cwd());
|
|
@@ -451,6 +452,9 @@ async function agentCommand(thesisId, opts) {
|
|
|
451
452
|
series: Type.Optional(Type.String({ description: 'Kalshi series ticker (e.g. KXWTIMAX)' })),
|
|
452
453
|
market: Type.Optional(Type.String({ description: 'Specific market ticker' })),
|
|
453
454
|
});
|
|
455
|
+
const webSearchParams = Type.Object({
|
|
456
|
+
query: Type.String({ description: 'Search keywords' }),
|
|
457
|
+
});
|
|
454
458
|
const emptyParams = Type.Object({});
|
|
455
459
|
const tools = [
|
|
456
460
|
{
|
|
@@ -502,7 +506,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
502
506
|
{
|
|
503
507
|
name: 'scan_markets',
|
|
504
508
|
label: 'Scan Markets',
|
|
505
|
-
description: 'Search Kalshi prediction markets:
|
|
509
|
+
description: 'Search Kalshi prediction markets. Provide exactly one of: query (keyword search), series (series ticker), or market (specific ticker). If multiple are provided, priority is: market > series > query.',
|
|
506
510
|
parameters: scanParams,
|
|
507
511
|
execute: async (_toolCallId, params) => {
|
|
508
512
|
let result;
|
|
@@ -570,18 +574,102 @@ async function agentCommand(thesisId, opts) {
|
|
|
570
574
|
};
|
|
571
575
|
},
|
|
572
576
|
},
|
|
577
|
+
{
|
|
578
|
+
name: 'web_search',
|
|
579
|
+
label: 'Web Search',
|
|
580
|
+
description: 'Search latest news and information. Use for real-time info not yet covered by the causal tree or heartbeat engine.',
|
|
581
|
+
parameters: webSearchParams,
|
|
582
|
+
execute: async (_toolCallId, params) => {
|
|
583
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
584
|
+
if (!apiKey) {
|
|
585
|
+
return {
|
|
586
|
+
content: [{ type: 'text', text: 'Tavily not configured. Set TAVILY_API_KEY to enable web search. You can also manually inject a signal and let the heartbeat engine search.' }],
|
|
587
|
+
details: {},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
const res = await fetch('https://api.tavily.com/search', {
|
|
591
|
+
method: 'POST',
|
|
592
|
+
headers: { 'Content-Type': 'application/json' },
|
|
593
|
+
body: JSON.stringify({
|
|
594
|
+
api_key: apiKey,
|
|
595
|
+
query: params.query,
|
|
596
|
+
max_results: 5,
|
|
597
|
+
search_depth: 'basic',
|
|
598
|
+
include_answer: true,
|
|
599
|
+
}),
|
|
600
|
+
});
|
|
601
|
+
if (!res.ok) {
|
|
602
|
+
return {
|
|
603
|
+
content: [{ type: 'text', text: `Search failed: ${res.status}` }],
|
|
604
|
+
details: {},
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
const data = await res.json();
|
|
608
|
+
const results = (data.results || []).map((r) => `[${r.title}](${r.url})\n${r.content?.slice(0, 200)}`).join('\n\n');
|
|
609
|
+
const answer = data.answer ? `Summary: ${data.answer}\n\n---\n\n` : '';
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: 'text', text: `${answer}${results}` }],
|
|
612
|
+
details: {},
|
|
613
|
+
};
|
|
614
|
+
},
|
|
615
|
+
},
|
|
573
616
|
];
|
|
574
|
-
// ── System prompt
|
|
575
|
-
|
|
617
|
+
// ── System prompt builder ──────────────────────────────────────────────────
|
|
618
|
+
function buildSystemPrompt(ctx) {
|
|
619
|
+
const edgesSummary = ctx.edges
|
|
620
|
+
?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
621
|
+
.slice(0, 5)
|
|
622
|
+
.map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 \u2192 thesis ${e.thesisPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge} | ${e.orderbook?.liquidityScore || '?'}`)
|
|
623
|
+
.join('\n') || ' (no edge data)';
|
|
624
|
+
const nodesSummary = ctx.causalTree?.nodes
|
|
625
|
+
?.filter((n) => n.depth === 0)
|
|
626
|
+
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
|
|
627
|
+
.join('\n') || ' (no causal tree)';
|
|
628
|
+
const conf = typeof ctx.confidence === 'number'
|
|
629
|
+
? Math.round(ctx.confidence * 100)
|
|
630
|
+
: (typeof ctx.confidence === 'string' ? parseInt(ctx.confidence) : 0);
|
|
631
|
+
return `You are a prediction market trading assistant. Your job is not to please the user \u2014 it is to help them see reality clearly and make correct trading decisions.
|
|
632
|
+
|
|
633
|
+
## Your analytical framework
|
|
634
|
+
|
|
635
|
+
Each thesis has a causal tree. Every node is a causal hypothesis with a probability. Nodes have causal relationships \u2014 when upstream nodes change, downstream nodes follow.
|
|
636
|
+
|
|
637
|
+
Edge = thesis-implied price - actual market price. Positive edge means the market underprices this event. Negative edge means overpriced. Contracts with large edges AND good liquidity are the most tradeable.
|
|
638
|
+
|
|
639
|
+
executableEdge is the real edge after subtracting the bid-ask spread. A contract with a big theoretical edge but wide spread may not be worth entering.
|
|
640
|
+
|
|
641
|
+
Short-term markets (weekly/monthly contracts) settle into hard data that calibrates the long-term thesis. Don't use them to bet (outcomes are nearly known) \u2014 use them to verify whether causal tree node probabilities are accurate.
|
|
642
|
+
|
|
643
|
+
## Your behavioral rules
|
|
644
|
+
|
|
645
|
+
- Think before calling tools. If the data is already in context, don't re-fetch.
|
|
646
|
+
- If the user asks about positions, check if Kalshi is configured first. If not, say so directly.
|
|
647
|
+
- If the user says "note this" or mentions a news event, inject a signal. Don't ask "should I note this?"
|
|
648
|
+
- If the user says "evaluate" or "run it", trigger immediately. Don't confirm.
|
|
649
|
+
- Don't end every response with "anything else?" \u2014 the user will ask when they want to.
|
|
650
|
+
- If the user asks about latest news or real-time events, use web_search first, then answer based on results. If you find important information, suggest injecting it as a signal.
|
|
651
|
+
- If you notice an edge narrowing or disappearing, say so proactively. Don't only report good news.
|
|
652
|
+
- If a causal tree node probability seriously contradicts the market price, point it out.
|
|
653
|
+
- Use Chinese if the user writes in Chinese, English if they write in English.
|
|
654
|
+
- For any question about prices, positions, or P&L, ALWAYS call a tool to get fresh data first. Never answer price-related questions using the cached data in this system prompt.
|
|
655
|
+
- Align tables. Be precise with numbers to the cent.
|
|
576
656
|
|
|
577
|
-
Current thesis
|
|
578
|
-
Confidence: ${confidencePct}%
|
|
579
|
-
Status: ${latestContext.status}
|
|
580
|
-
Thesis ID: ${latestContext.thesisId || resolvedThesisId}
|
|
657
|
+
## Current thesis state
|
|
581
658
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
659
|
+
Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
|
|
660
|
+
ID: ${ctx.thesisId || resolvedThesisId}
|
|
661
|
+
Confidence: ${conf}%
|
|
662
|
+
Status: ${ctx.status}
|
|
663
|
+
|
|
664
|
+
Top-level causal tree nodes:
|
|
665
|
+
${nodesSummary}
|
|
666
|
+
|
|
667
|
+
Top 5 edges by magnitude:
|
|
668
|
+
${edgesSummary}
|
|
669
|
+
|
|
670
|
+
${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
|
|
671
|
+
}
|
|
672
|
+
const systemPrompt = buildSystemPrompt(latestContext);
|
|
585
673
|
// ── Create Agent ───────────────────────────────────────────────────────────
|
|
586
674
|
const agent = new Agent({
|
|
587
675
|
initialState: {
|
|
@@ -741,6 +829,7 @@ Do NOT make up data. Always call tools to get current state.`;
|
|
|
741
829
|
C.emerald('/compact ') + C.zinc400('Compress conversation history') + '\n' +
|
|
742
830
|
C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
|
|
743
831
|
C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
|
|
832
|
+
C.emerald('/env ') + C.zinc400('Show environment variable status') + '\n' +
|
|
744
833
|
C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
|
|
745
834
|
C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
|
|
746
835
|
addSpacer();
|
|
@@ -844,10 +933,14 @@ Do NOT make up data. Always call tools to get current state.`;
|
|
|
844
933
|
const newContext = await sfClient.getContext(newId);
|
|
845
934
|
resolvedThesisId = newContext.thesisId || newId;
|
|
846
935
|
latestContext = newContext;
|
|
847
|
-
// Build new system prompt
|
|
936
|
+
// Build new system prompt using the rich builder
|
|
937
|
+
const newSysPrompt = buildSystemPrompt(newContext);
|
|
848
938
|
const newConf = typeof newContext.confidence === 'number'
|
|
849
939
|
? Math.round(newContext.confidence * 100) : 0;
|
|
850
|
-
|
|
940
|
+
// CRITICAL: Always clearMessages() first to reset agent internal state.
|
|
941
|
+
// replaceMessages() on a mid-conversation agent corrupts pi-agent-core's
|
|
942
|
+
// state machine, causing the TUI to freeze.
|
|
943
|
+
agent.clearMessages();
|
|
851
944
|
// Load saved session or start fresh
|
|
852
945
|
const saved = loadSession(resolvedThesisId);
|
|
853
946
|
if (saved?.messages?.length > 0) {
|
|
@@ -856,7 +949,6 @@ Do NOT make up data. Always call tools to get current state.`;
|
|
|
856
949
|
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(` (resumed ${saved.messages.length} messages)`));
|
|
857
950
|
}
|
|
858
951
|
else {
|
|
859
|
-
agent.clearMessages();
|
|
860
952
|
agent.setSystemPrompt(newSysPrompt);
|
|
861
953
|
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
|
|
862
954
|
}
|
|
@@ -873,42 +965,168 @@ Do NOT make up data. Always call tools to get current state.`;
|
|
|
873
965
|
addSystemText(C.red(`Switch failed: ${err.message}`));
|
|
874
966
|
}
|
|
875
967
|
addSpacer();
|
|
968
|
+
// Force re-focus editor so input stays responsive
|
|
969
|
+
tui.setFocus(editor);
|
|
876
970
|
tui.requestRender();
|
|
877
971
|
return true;
|
|
878
972
|
}
|
|
879
973
|
case '/compact': {
|
|
880
974
|
addSpacer();
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
975
|
+
try {
|
|
976
|
+
const msgs = agent.state.messages;
|
|
977
|
+
if (msgs.length <= 10) {
|
|
978
|
+
addSystemText(C.zinc400('Conversation too short to compact'));
|
|
979
|
+
addSpacer();
|
|
980
|
+
tui.setFocus(editor);
|
|
981
|
+
return true;
|
|
982
|
+
}
|
|
983
|
+
// ── Find clean cut point ──────────────────────────────────────
|
|
984
|
+
// Walk backwards counting user messages as turn starts.
|
|
985
|
+
// Keep 3 complete turns. Never split a tool_call/tool_result pair.
|
|
986
|
+
const turnsToKeep = 3;
|
|
987
|
+
let turnsSeen = 0;
|
|
988
|
+
let cutIndex = msgs.length;
|
|
989
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
990
|
+
if (msgs[i].role === 'user') {
|
|
991
|
+
turnsSeen++;
|
|
992
|
+
if (turnsSeen >= turnsToKeep) {
|
|
993
|
+
cutIndex = i;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
897
997
|
}
|
|
898
|
-
|
|
899
|
-
|
|
998
|
+
if (cutIndex <= 2) {
|
|
999
|
+
addSystemText(C.zinc400('Not enough complete turns to compact'));
|
|
1000
|
+
addSpacer();
|
|
1001
|
+
tui.setFocus(editor);
|
|
1002
|
+
return true;
|
|
1003
|
+
}
|
|
1004
|
+
const toCompress = msgs.slice(0, cutIndex);
|
|
1005
|
+
const toKeep = msgs.slice(cutIndex);
|
|
1006
|
+
// ── Show loader ───────────────────────────────────────────────
|
|
1007
|
+
const compactLoader = new Loader(tui, (s) => C.emerald(s), (s) => C.zinc600(s), 'compacting with LLM...');
|
|
1008
|
+
compactLoader.start();
|
|
1009
|
+
chatContainer.addChild(compactLoader);
|
|
1010
|
+
tui.requestRender();
|
|
1011
|
+
// ── Serialize messages for the summarizer ─────────────────────
|
|
1012
|
+
// Strip tool results to raw text, cap total length to ~12k chars
|
|
1013
|
+
const serialized = [];
|
|
1014
|
+
let totalLen = 0;
|
|
1015
|
+
const MAX_CHARS = 12000;
|
|
1016
|
+
for (const m of toCompress) {
|
|
1017
|
+
if (totalLen >= MAX_CHARS)
|
|
1018
|
+
break;
|
|
1019
|
+
let text = '';
|
|
1020
|
+
if (typeof m.content === 'string') {
|
|
1021
|
+
text = m.content;
|
|
1022
|
+
}
|
|
1023
|
+
else if (Array.isArray(m.content)) {
|
|
1024
|
+
// OpenAI format: content blocks
|
|
1025
|
+
text = m.content
|
|
1026
|
+
.filter((b) => b.type === 'text')
|
|
1027
|
+
.map((b) => b.text)
|
|
1028
|
+
.join('\n');
|
|
1029
|
+
}
|
|
1030
|
+
if (!text)
|
|
1031
|
+
continue;
|
|
1032
|
+
const role = (m.role || 'unknown').toUpperCase();
|
|
1033
|
+
const truncated = text.slice(0, 800);
|
|
1034
|
+
const line = `[${role}]: ${truncated}`;
|
|
1035
|
+
serialized.push(line);
|
|
1036
|
+
totalLen += line.length;
|
|
900
1037
|
}
|
|
1038
|
+
const conversationDump = serialized.join('\n\n');
|
|
1039
|
+
// ── Call OpenRouter for LLM summary ───────────────────────────
|
|
1040
|
+
// Use a cheap/fast model — gemini flash
|
|
1041
|
+
const summaryModel = 'google/gemini-2.0-flash-001';
|
|
1042
|
+
const summarySystemPrompt = `You are a conversation compressor. Given a conversation between a user and a prediction-market trading assistant, produce a dense summary that preserves:
|
|
1043
|
+
1. All factual conclusions, numbers, prices, and probabilities mentioned
|
|
1044
|
+
2. Key trading decisions, positions taken or discussed
|
|
1045
|
+
3. Signals injected, evaluations triggered, and their outcomes
|
|
1046
|
+
4. Any action items or pending questions
|
|
1047
|
+
|
|
1048
|
+
Output a structured summary. Be concise but preserve every important detail — this summary replaces the original messages for continued conversation. Do NOT add commentary or meta-text. Just the summary.`;
|
|
1049
|
+
let summaryText;
|
|
1050
|
+
try {
|
|
1051
|
+
const orRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
1052
|
+
method: 'POST',
|
|
1053
|
+
headers: {
|
|
1054
|
+
'Content-Type': 'application/json',
|
|
1055
|
+
'Authorization': `Bearer ${openrouterKey}`,
|
|
1056
|
+
'HTTP-Referer': 'https://simplefunctions.com',
|
|
1057
|
+
'X-Title': 'SF Agent Compact',
|
|
1058
|
+
},
|
|
1059
|
+
body: JSON.stringify({
|
|
1060
|
+
model: summaryModel,
|
|
1061
|
+
messages: [
|
|
1062
|
+
{ role: 'system', content: summarySystemPrompt },
|
|
1063
|
+
{ role: 'user', content: `Summarize this conversation (${toCompress.length} messages):\n\n${conversationDump}` },
|
|
1064
|
+
],
|
|
1065
|
+
max_tokens: 2000,
|
|
1066
|
+
temperature: 0.2,
|
|
1067
|
+
}),
|
|
1068
|
+
});
|
|
1069
|
+
if (!orRes.ok) {
|
|
1070
|
+
const errText = await orRes.text().catch(() => '');
|
|
1071
|
+
throw new Error(`OpenRouter ${orRes.status}: ${errText.slice(0, 200)}`);
|
|
1072
|
+
}
|
|
1073
|
+
const orData = await orRes.json();
|
|
1074
|
+
summaryText = orData.choices?.[0]?.message?.content || '';
|
|
1075
|
+
if (!summaryText) {
|
|
1076
|
+
throw new Error('Empty summary from LLM');
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
catch (llmErr) {
|
|
1080
|
+
// LLM failed — fall back to bullet-point extraction
|
|
1081
|
+
const bulletPoints = [];
|
|
1082
|
+
for (const m of toCompress) {
|
|
1083
|
+
const content = typeof m.content === 'string' ? m.content : '';
|
|
1084
|
+
if (m.role === 'user' && content) {
|
|
1085
|
+
bulletPoints.push(`- User: ${content.slice(0, 100)}`);
|
|
1086
|
+
}
|
|
1087
|
+
else if (m.role === 'assistant' && content) {
|
|
1088
|
+
bulletPoints.push(`- Assistant: ${content.slice(0, 150)}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
summaryText = `[LLM summary failed: ${llmErr.message}. Fallback bullet points:]\n\n${bulletPoints.slice(-20).join('\n')}`;
|
|
1092
|
+
}
|
|
1093
|
+
// ── Remove loader ─────────────────────────────────────────────
|
|
1094
|
+
compactLoader.stop();
|
|
1095
|
+
chatContainer.removeChild(compactLoader);
|
|
1096
|
+
// ── Build compacted message array ──────────────────────────────
|
|
1097
|
+
// user(summary) → assistant(ack) → ...toKeep
|
|
1098
|
+
// This maintains valid user→assistant alternation.
|
|
1099
|
+
// toKeep starts with a user message (guaranteed by our cut logic).
|
|
1100
|
+
const compactedMessages = [
|
|
1101
|
+
{
|
|
1102
|
+
role: 'user',
|
|
1103
|
+
content: `[Conversation summary — ${toCompress.length} messages compressed]\n\n${summaryText}`,
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
role: 'assistant',
|
|
1107
|
+
content: 'Understood. I have the full conversation context from the summary above. Continuing from where we left off.',
|
|
1108
|
+
},
|
|
1109
|
+
...toKeep,
|
|
1110
|
+
];
|
|
1111
|
+
// ── Replace agent state ───────────────────────────────────────
|
|
1112
|
+
// Clear first to reset internal state, then load compacted messages
|
|
1113
|
+
agent.clearMessages();
|
|
1114
|
+
agent.replaceMessages(compactedMessages);
|
|
1115
|
+
agent.setSystemPrompt(systemPrompt);
|
|
1116
|
+
persistSession();
|
|
1117
|
+
addSystemText(C.emerald(`Compacted: ${toCompress.length} messages \u2192 summary + ${toKeep.length} recent`) +
|
|
1118
|
+
C.zinc600(` (via ${summaryModel.split('/').pop()})`));
|
|
1119
|
+
addSpacer();
|
|
1120
|
+
// Force re-focus and render so editor stays responsive
|
|
1121
|
+
tui.setFocus(editor);
|
|
1122
|
+
tui.requestRender();
|
|
1123
|
+
}
|
|
1124
|
+
catch (err) {
|
|
1125
|
+
addSystemText(C.red(`Compact failed: ${err.message || err}`));
|
|
1126
|
+
addSpacer();
|
|
1127
|
+
tui.setFocus(editor);
|
|
1128
|
+
tui.requestRender();
|
|
901
1129
|
}
|
|
902
|
-
const summary = bulletPoints.slice(-20).join('\n');
|
|
903
|
-
// Replace messages: summary + recent
|
|
904
|
-
const compactedMessages = [
|
|
905
|
-
{ role: 'assistant', content: `[Conversation summary - ${toCompress.length} messages compressed]\n${summary}` },
|
|
906
|
-
...toKeep,
|
|
907
|
-
];
|
|
908
|
-
agent.replaceMessages(compactedMessages);
|
|
909
|
-
persistSession();
|
|
910
|
-
addSystemText(C.emerald(`Compacted: ${toCompress.length} messages \u2192 summary + ${toKeep.length} recent`));
|
|
911
|
-
addSpacer();
|
|
912
1130
|
return true;
|
|
913
1131
|
}
|
|
914
1132
|
case '/new': {
|
|
@@ -922,6 +1140,33 @@ Do NOT make up data. Always call tools to get current state.`;
|
|
|
922
1140
|
tui.requestRender();
|
|
923
1141
|
return true;
|
|
924
1142
|
}
|
|
1143
|
+
case '/env': {
|
|
1144
|
+
addSpacer();
|
|
1145
|
+
const envVars = [
|
|
1146
|
+
{ name: 'SF_API_KEY', key: 'SF_API_KEY', required: true, mask: true },
|
|
1147
|
+
{ name: 'SF_API_URL', key: 'SF_API_URL', required: false, mask: false },
|
|
1148
|
+
{ name: 'OPENROUTER_KEY', key: 'OPENROUTER_API_KEY', required: true, mask: true },
|
|
1149
|
+
{ name: 'KALSHI_KEY_ID', key: 'KALSHI_API_KEY_ID', required: false, mask: true },
|
|
1150
|
+
{ name: 'KALSHI_PEM_PATH', key: 'KALSHI_PRIVATE_KEY_PATH', required: false, mask: false },
|
|
1151
|
+
{ name: 'TAVILY_API_KEY', key: 'TAVILY_API_KEY', required: false, mask: true },
|
|
1152
|
+
];
|
|
1153
|
+
const lines = envVars.map(v => {
|
|
1154
|
+
const val = process.env[v.key];
|
|
1155
|
+
if (val) {
|
|
1156
|
+
const display = v.mask
|
|
1157
|
+
? val.slice(0, Math.min(8, val.length)) + '...' + val.slice(-4)
|
|
1158
|
+
: val;
|
|
1159
|
+
return ` ${v.name.padEnd(18)} ${C.emerald('\u2713')} ${C.zinc400(display)}`;
|
|
1160
|
+
}
|
|
1161
|
+
else {
|
|
1162
|
+
const note = v.required ? '\u5FC5\u987B' : '\u53EF\u9009';
|
|
1163
|
+
return ` ${v.name.padEnd(18)} ${C.red('\u2717')} ${C.zinc600(`\u672A\u914D\u7F6E\uFF08${note}\uFF09`)}`;
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
addSystemText(C.zinc200(bold('Environment')) + '\n' + lines.join('\n'));
|
|
1167
|
+
addSpacer();
|
|
1168
|
+
return true;
|
|
1169
|
+
}
|
|
925
1170
|
case '/clear': {
|
|
926
1171
|
chatContainer.clear();
|
|
927
1172
|
tui.requestRender();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfunctions/cli",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.3",
|
|
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"
|
|
7
7
|
},
|
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
"dist"
|
|
27
27
|
],
|
|
28
28
|
"keywords": [
|
|
29
|
-
"prediction-
|
|
30
|
-
"thesis-agent",
|
|
29
|
+
"prediction-market",
|
|
31
30
|
"kalshi",
|
|
32
31
|
"polymarket",
|
|
33
|
-
"
|
|
32
|
+
"trading",
|
|
33
|
+
"cli",
|
|
34
|
+
"agent",
|
|
35
|
+
"orderbook",
|
|
36
|
+
"market-intelligence",
|
|
37
|
+
"edge-detection"
|
|
34
38
|
],
|
|
35
39
|
"license": "MIT",
|
|
36
40
|
"repository": {
|