@spfunctions/cli 0.1.4 → 0.1.5
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.d.ts +18 -0
- package/dist/commands/agent.js +290 -0
- package/dist/index.js +11 -0
- package/dist/kalshi.js +0 -3
- package/package.json +3 -1
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sf agent — Interactive natural language terminal agent.
|
|
3
|
+
*
|
|
4
|
+
* Uses pi-agent-core's Agent class for the tool-calling loop.
|
|
5
|
+
* Uses pi-ai for unified LLM access via OpenRouter.
|
|
6
|
+
*
|
|
7
|
+
* The agent has 6 tools:
|
|
8
|
+
* get_context — thesis snapshot (causal tree, edges, confidence)
|
|
9
|
+
* inject_signal — inject news/note/external signal
|
|
10
|
+
* trigger_evaluation — trigger deep eval (heavy model)
|
|
11
|
+
* scan_markets — search Kalshi markets
|
|
12
|
+
* list_theses — list all theses
|
|
13
|
+
* get_positions — Kalshi portfolio positions
|
|
14
|
+
*/
|
|
15
|
+
export declare function agentCommand(thesisId?: string, opts?: {
|
|
16
|
+
model?: string;
|
|
17
|
+
modelKey?: string;
|
|
18
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* sf agent — Interactive natural language terminal agent.
|
|
4
|
+
*
|
|
5
|
+
* Uses pi-agent-core's Agent class for the tool-calling loop.
|
|
6
|
+
* Uses pi-ai for unified LLM access via OpenRouter.
|
|
7
|
+
*
|
|
8
|
+
* The agent has 6 tools:
|
|
9
|
+
* get_context — thesis snapshot (causal tree, edges, confidence)
|
|
10
|
+
* inject_signal — inject news/note/external signal
|
|
11
|
+
* trigger_evaluation — trigger deep eval (heavy model)
|
|
12
|
+
* scan_markets — search Kalshi markets
|
|
13
|
+
* list_theses — list all theses
|
|
14
|
+
* get_positions — Kalshi portfolio positions
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.agentCommand = agentCommand;
|
|
21
|
+
const readline_1 = __importDefault(require("readline"));
|
|
22
|
+
const client_js_1 = require("../client.js");
|
|
23
|
+
const kalshi_js_1 = require("../kalshi.js");
|
|
24
|
+
async function agentCommand(thesisId, opts) {
|
|
25
|
+
// ── Dynamic imports for ESM-only packages ──────────────────────────────────
|
|
26
|
+
const piAi = await import('@mariozechner/pi-ai');
|
|
27
|
+
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
28
|
+
const { getModel, streamSimple, Type } = piAi;
|
|
29
|
+
const { Agent } = piAgent;
|
|
30
|
+
// ── Validate API keys ──────────────────────────────────────────────────────
|
|
31
|
+
const openrouterKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
|
|
32
|
+
if (!openrouterKey) {
|
|
33
|
+
console.error('Need OpenRouter API key. Set OPENROUTER_API_KEY or use --model-key.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const sfClient = new client_js_1.SFClient();
|
|
37
|
+
// ── Resolve thesis ID ──────────────────────────────────────────────────────
|
|
38
|
+
let resolvedThesisId = thesisId;
|
|
39
|
+
if (!resolvedThesisId) {
|
|
40
|
+
const data = await sfClient.listTheses();
|
|
41
|
+
const theses = data.theses || data;
|
|
42
|
+
const active = theses.find((t) => t.status === 'active');
|
|
43
|
+
if (!active) {
|
|
44
|
+
console.error('No active thesis. Create one first: sf create "..."');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
resolvedThesisId = active.id;
|
|
48
|
+
}
|
|
49
|
+
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
50
|
+
const context = await sfClient.getContext(resolvedThesisId);
|
|
51
|
+
// ── Define tools ───────────────────────────────────────────────────────────
|
|
52
|
+
const thesisIdParam = Type.Object({
|
|
53
|
+
thesisId: Type.String({ description: 'Thesis ID (short or full UUID)' }),
|
|
54
|
+
});
|
|
55
|
+
const signalParams = Type.Object({
|
|
56
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
57
|
+
content: Type.String({ description: 'Signal content' }),
|
|
58
|
+
type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external. Default: user_note' })),
|
|
59
|
+
});
|
|
60
|
+
const scanParams = Type.Object({
|
|
61
|
+
query: Type.Optional(Type.String({ description: 'Keyword search for Kalshi markets' })),
|
|
62
|
+
series: Type.Optional(Type.String({ description: 'Kalshi series ticker (e.g. KXWTIMAX)' })),
|
|
63
|
+
market: Type.Optional(Type.String({ description: 'Specific market ticker' })),
|
|
64
|
+
});
|
|
65
|
+
const emptyParams = Type.Object({});
|
|
66
|
+
const tools = [
|
|
67
|
+
{
|
|
68
|
+
name: 'get_context',
|
|
69
|
+
label: 'Get Context',
|
|
70
|
+
description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
|
|
71
|
+
parameters: thesisIdParam,
|
|
72
|
+
execute: async (_toolCallId, params) => {
|
|
73
|
+
const ctx = await sfClient.getContext(params.thesisId);
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
|
|
76
|
+
details: {},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'inject_signal',
|
|
82
|
+
label: 'Inject Signal',
|
|
83
|
+
description: 'Inject a signal into the thesis (news, note, external event)',
|
|
84
|
+
parameters: signalParams,
|
|
85
|
+
execute: async (_toolCallId, params) => {
|
|
86
|
+
const result = await sfClient.injectSignal(params.thesisId, params.type || 'user_note', params.content);
|
|
87
|
+
return {
|
|
88
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
89
|
+
details: {},
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'trigger_evaluation',
|
|
95
|
+
label: 'Evaluate',
|
|
96
|
+
description: 'Trigger a deep evaluation cycle (heavy model, takes longer)',
|
|
97
|
+
parameters: thesisIdParam,
|
|
98
|
+
execute: async (_toolCallId, params) => {
|
|
99
|
+
const result = await sfClient.evaluate(params.thesisId);
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
102
|
+
details: {},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'scan_markets',
|
|
108
|
+
label: 'Scan Markets',
|
|
109
|
+
description: 'Search Kalshi prediction markets: by keywords, series ticker, or specific market ticker',
|
|
110
|
+
parameters: scanParams,
|
|
111
|
+
execute: async (_toolCallId, params) => {
|
|
112
|
+
let result;
|
|
113
|
+
if (params.market) {
|
|
114
|
+
result = await (0, client_js_1.kalshiFetchMarket)(params.market);
|
|
115
|
+
}
|
|
116
|
+
else if (params.series) {
|
|
117
|
+
result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
|
|
118
|
+
}
|
|
119
|
+
else if (params.query) {
|
|
120
|
+
const series = await (0, client_js_1.kalshiFetchAllSeries)();
|
|
121
|
+
const keywords = params.query.toLowerCase().split(/\s+/);
|
|
122
|
+
const matched = series
|
|
123
|
+
.filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
|
|
124
|
+
(s.ticker || '').toLowerCase().includes(kw)))
|
|
125
|
+
.slice(0, 15);
|
|
126
|
+
result = matched;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
result = { error: 'Provide query, series, or market parameter' };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
133
|
+
details: {},
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'list_theses',
|
|
139
|
+
label: 'List Theses',
|
|
140
|
+
description: 'List all theses for the current user',
|
|
141
|
+
parameters: emptyParams,
|
|
142
|
+
execute: async () => {
|
|
143
|
+
const theses = await sfClient.listTheses();
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }],
|
|
146
|
+
details: {},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'get_positions',
|
|
152
|
+
label: 'Get Positions',
|
|
153
|
+
description: 'Get Kalshi exchange positions with live prices and PnL (requires local Kalshi key config)',
|
|
154
|
+
parameters: emptyParams,
|
|
155
|
+
execute: async () => {
|
|
156
|
+
const positions = await (0, kalshi_js_1.getPositions)();
|
|
157
|
+
if (!positions) {
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: 'text',
|
|
162
|
+
text: 'Kalshi not configured. Set KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY_PATH environment variables.',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
details: {},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Enrich with live prices (same logic as sf positions command)
|
|
169
|
+
for (const pos of positions) {
|
|
170
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
171
|
+
if (livePrice !== null) {
|
|
172
|
+
pos.current_value = livePrice;
|
|
173
|
+
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }],
|
|
178
|
+
details: {},
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
];
|
|
183
|
+
// ── System prompt ──────────────────────────────────────────────────────────
|
|
184
|
+
const confidencePct = typeof context.confidence === 'number'
|
|
185
|
+
? Math.round(context.confidence * 100)
|
|
186
|
+
: context.confidence;
|
|
187
|
+
const systemPrompt = `You are a SimpleFunctions prediction market trading assistant.
|
|
188
|
+
|
|
189
|
+
Current thesis: ${context.thesis || context.rawThesis || 'N/A'}
|
|
190
|
+
Confidence: ${confidencePct}%
|
|
191
|
+
Status: ${context.status}
|
|
192
|
+
Thesis ID: ${context.thesisId || resolvedThesisId}
|
|
193
|
+
|
|
194
|
+
You have six tools available. Use them when you need real-time data. Answer directly when you don't.
|
|
195
|
+
Be concise. Use Chinese if the user writes in Chinese, English if they write in English.
|
|
196
|
+
Do NOT make up data. Always call tools to get current state.`;
|
|
197
|
+
// ── Configure model via OpenRouter ─────────────────────────────────────────
|
|
198
|
+
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4-20250514';
|
|
199
|
+
// Strip 'openrouter/' prefix if user passed it (provider is already openrouter)
|
|
200
|
+
const modelName = rawModelName.replace(/^openrouter\//, '');
|
|
201
|
+
// Try the registry first. If the model isn't registered, construct manually.
|
|
202
|
+
// OpenRouter accepts any model ID — the registry just may not have it yet.
|
|
203
|
+
let model;
|
|
204
|
+
try {
|
|
205
|
+
model = getModel('openrouter', modelName);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Manual fallback: construct a Model object compatible with pi-ai's openai-completions API
|
|
209
|
+
// (OpenRouter uses OpenAI-compatible API)
|
|
210
|
+
model = {
|
|
211
|
+
modelId: modelName,
|
|
212
|
+
provider: 'openrouter',
|
|
213
|
+
api: 'openai-completions',
|
|
214
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
215
|
+
id: modelName,
|
|
216
|
+
name: modelName,
|
|
217
|
+
inputPrice: 0,
|
|
218
|
+
outputPrice: 0,
|
|
219
|
+
contextWindow: 200000,
|
|
220
|
+
supportsImages: true,
|
|
221
|
+
supportsTools: true,
|
|
222
|
+
};
|
|
223
|
+
console.log(`\x1b[33mModel '${modelName}' not in registry, using direct OpenRouter API\x1b[0m`);
|
|
224
|
+
}
|
|
225
|
+
// ── Create Agent ───────────────────────────────────────────────────────────
|
|
226
|
+
const agent = new Agent({
|
|
227
|
+
initialState: {
|
|
228
|
+
systemPrompt,
|
|
229
|
+
model,
|
|
230
|
+
tools,
|
|
231
|
+
thinkingLevel: 'off',
|
|
232
|
+
},
|
|
233
|
+
streamFn: streamSimple,
|
|
234
|
+
getApiKey: (provider) => {
|
|
235
|
+
if (provider === 'openrouter')
|
|
236
|
+
return openrouterKey;
|
|
237
|
+
return undefined;
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
// ── Subscribe to streaming events ──────────────────────────────────────────
|
|
241
|
+
agent.subscribe((event) => {
|
|
242
|
+
if (event.type === 'message_update') {
|
|
243
|
+
const e = event.assistantMessageEvent;
|
|
244
|
+
if (e.type === 'text_delta') {
|
|
245
|
+
process.stdout.write(e.delta);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (event.type === 'tool_execution_start') {
|
|
249
|
+
process.stdout.write(`\n\x1b[33m⚡ ${event.toolName}\x1b[0m `);
|
|
250
|
+
}
|
|
251
|
+
if (event.type === 'tool_execution_end') {
|
|
252
|
+
if (event.isError) {
|
|
253
|
+
process.stdout.write('\x1b[31m✗\x1b[0m\n');
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
process.stdout.write('\x1b[32m✓\x1b[0m\n');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// ── Terminal REPL ──────────────────────────────────────────────────────────
|
|
261
|
+
const shortId = (resolvedThesisId || '').slice(0, 8);
|
|
262
|
+
console.log(`\n\x1b[32mSimpleFunctions Agent\x1b[0m — ${shortId}`);
|
|
263
|
+
console.log(`Model: ${modelName} | Type "exit" to quit\n`);
|
|
264
|
+
const rl = readline_1.default.createInterface({
|
|
265
|
+
input: process.stdin,
|
|
266
|
+
output: process.stdout,
|
|
267
|
+
});
|
|
268
|
+
const prompt = () => {
|
|
269
|
+
rl.question('\x1b[36m> \x1b[0m', async (input) => {
|
|
270
|
+
const trimmed = input.trim();
|
|
271
|
+
if (!trimmed) {
|
|
272
|
+
prompt();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (trimmed === 'exit' || trimmed === 'quit') {
|
|
276
|
+
rl.close();
|
|
277
|
+
process.exit(0);
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
await agent.prompt(trimmed);
|
|
281
|
+
console.log('\n');
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
console.error(`\n\x1b[31mError: ${err.message}\x1b[0m\n`);
|
|
285
|
+
}
|
|
286
|
+
prompt();
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
prompt();
|
|
290
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const signal_js_1 = require("./commands/signal.js");
|
|
|
25
25
|
const evaluate_js_1 = require("./commands/evaluate.js");
|
|
26
26
|
const scan_js_1 = require("./commands/scan.js");
|
|
27
27
|
const positions_js_1 = require("./commands/positions.js");
|
|
28
|
+
const agent_js_1 = require("./commands/agent.js");
|
|
28
29
|
const utils_js_1 = require("./utils.js");
|
|
29
30
|
const program = new commander_1.Command();
|
|
30
31
|
program
|
|
@@ -130,6 +131,16 @@ program
|
|
|
130
131
|
apiUrl: g.apiUrl,
|
|
131
132
|
}));
|
|
132
133
|
});
|
|
134
|
+
// ── sf agent [thesisId] ───────────────────────────────────────────────────────
|
|
135
|
+
program
|
|
136
|
+
.command('agent [thesisId]')
|
|
137
|
+
.description('Interactive agent mode — natural language interface to SimpleFunctions')
|
|
138
|
+
.option('--model <model>', 'Model via OpenRouter (default: anthropic/claude-sonnet-4-20250514)')
|
|
139
|
+
.option('--model-key <key>', 'OpenRouter API key (or set OPENROUTER_API_KEY)')
|
|
140
|
+
.action(async (thesisId, opts, cmd) => {
|
|
141
|
+
const g = cmd.optsWithGlobals();
|
|
142
|
+
await run(() => (0, agent_js_1.agentCommand)(thesisId, { model: opts.model, modelKey: opts.modelKey }));
|
|
143
|
+
});
|
|
133
144
|
// ── Error wrapper ─────────────────────────────────────────────────────────────
|
|
134
145
|
async function run(fn) {
|
|
135
146
|
try {
|
package/dist/kalshi.js
CHANGED
|
@@ -104,9 +104,6 @@ async function getPositions() {
|
|
|
104
104
|
return null;
|
|
105
105
|
try {
|
|
106
106
|
const data = await kalshiAuthGet('/portfolio/positions');
|
|
107
|
-
// DEBUG: dump raw Kalshi response to see actual field names
|
|
108
|
-
console.log('[Kalshi DEBUG] Raw /portfolio/positions response:');
|
|
109
|
-
console.log(JSON.stringify(data, null, 2));
|
|
110
107
|
// Kalshi returns { market_positions: [...] } or { positions: [...] }
|
|
111
108
|
const raw = data.market_positions || data.positions || [];
|
|
112
109
|
return raw.map((p) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfunctions/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "CLI for SimpleFunctions prediction market thesis agent",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sf": "./dist/index.js"
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"prepublishOnly": "npm run build"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@mariozechner/pi-agent-core": "^0.57.1",
|
|
15
|
+
"@mariozechner/pi-ai": "^0.57.1",
|
|
14
16
|
"commander": "^12.0.0",
|
|
15
17
|
"kalshi-typescript": "^3.9.0"
|
|
16
18
|
},
|