@spfunctions/cli 1.7.16 → 1.7.17

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.
@@ -10,5 +10,6 @@
10
10
  */
11
11
  export declare function queryCommand(q: string, opts?: {
12
12
  json?: boolean;
13
+ share?: boolean;
13
14
  limit?: string;
14
15
  }): Promise<void>;
@@ -11,7 +11,8 @@
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.queryCommand = queryCommand;
14
- const BASE_URL = 'https://simplefunctions.dev';
14
+ const share_js_1 = require("../share.js");
15
+ const BASE_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
15
16
  async function queryCommand(q, opts) {
16
17
  const limit = opts?.limit || '10';
17
18
  const url = `${BASE_URL}/api/public/query?q=${encodeURIComponent(q)}&limit=${limit}`;
@@ -27,6 +28,10 @@ async function queryCommand(q, opts) {
27
28
  return;
28
29
  }
29
30
  const data = await res.json();
31
+ if (opts?.share) {
32
+ await (0, share_js_1.shareOutput)('query', q, data);
33
+ return;
34
+ }
30
35
  if (opts?.json) {
31
36
  console.log(JSON.stringify(data, null, 2));
32
37
  return;
@@ -3,6 +3,7 @@ interface ScanOpts {
3
3
  market?: string;
4
4
  venue?: string;
5
5
  json?: boolean;
6
+ share?: boolean;
6
7
  apiKey?: string;
7
8
  apiUrl?: string;
8
9
  }
@@ -4,6 +4,7 @@ exports.scanCommand = scanCommand;
4
4
  const client_js_1 = require("../client.js");
5
5
  const polymarket_js_1 = require("../polymarket.js");
6
6
  const utils_js_1 = require("../utils.js");
7
+ const share_js_1 = require("../share.js");
7
8
  // SF API base — scan goes through the server which holds the proxy key
8
9
  const SF_API_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
9
10
  async function scanCommand(query, opts) {
@@ -18,7 +19,7 @@ async function scanCommand(query, opts) {
18
19
  return;
19
20
  }
20
21
  // Mode 3: keyword scan across all series + Polymarket
21
- await keywordScan(query, opts.json, opts.venue || 'all');
22
+ await keywordScan(query, opts.json, opts.venue || 'all', opts.share);
22
23
  }
23
24
  async function showMarket(ticker, json) {
24
25
  console.log(`${utils_js_1.c.dim}Fetching market ${ticker}...${utils_js_1.c.reset}`);
@@ -80,7 +81,7 @@ async function showSeries(seriesTicker, json) {
80
81
  printMarketsTable(markets);
81
82
  }
82
83
  }
83
- async function keywordScan(query, json, venue = 'all') {
84
+ async function keywordScan(query, json, venue = 'all', share) {
84
85
  // ── Polymarket search (runs in parallel with Kalshi) ───────────────────────
85
86
  const polyPromise = (venue === 'kalshi')
86
87
  ? Promise.resolve([])
@@ -154,6 +155,10 @@ async function keywordScan(query, json, venue = 'all') {
154
155
  }
155
156
  const combined = [...allMarkets, ...polyMarkets];
156
157
  combined.sort((a, b) => b.liquidity - a.liquidity);
158
+ if (share) {
159
+ await (0, share_js_1.shareOutput)('scan', query, combined);
160
+ return;
161
+ }
157
162
  if (json) {
158
163
  console.log(JSON.stringify(combined, null, 2));
159
164
  return;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * sf watch
3
+ *
4
+ * Push-mode monitoring. Polls GET /api/changes?since= for server-side
5
+ * detected market changes. No client-side diffing — the server handles
6
+ * change detection every 15 minutes via the scan-prices cron.
7
+ *
8
+ * Usage:
9
+ * sf watch "gold" — Watch gold-related changes
10
+ * sf watch "iran oil" — Watch iran/oil changes
11
+ * sf watch — Watch all market changes
12
+ * sf watch --json — JSON change events for piping
13
+ */
14
+ interface WatchOpts {
15
+ interval?: string;
16
+ json?: boolean;
17
+ }
18
+ export declare function watchCommand(query: string | undefined, opts: WatchOpts): Promise<void>;
19
+ export {};
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /**
3
+ * sf watch
4
+ *
5
+ * Push-mode monitoring. Polls GET /api/changes?since= for server-side
6
+ * detected market changes. No client-side diffing — the server handles
7
+ * change detection every 15 minutes via the scan-prices cron.
8
+ *
9
+ * Usage:
10
+ * sf watch "gold" — Watch gold-related changes
11
+ * sf watch "iran oil" — Watch iran/oil changes
12
+ * sf watch — Watch all market changes
13
+ * sf watch --json — JSON change events for piping
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.watchCommand = watchCommand;
17
+ const utils_js_1 = require("../utils.js");
18
+ const SF_API_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
19
+ async function watchCommand(query, opts) {
20
+ const intervalSec = parseInt(opts.interval || '60');
21
+ const intervalMs = intervalSec * 1000;
22
+ const isJson = opts.json || false;
23
+ if (!isJson) {
24
+ console.log();
25
+ console.log(` ${utils_js_1.c.bold}sf watch${utils_js_1.c.reset}${query ? ` "${query}"` : ''}`);
26
+ console.log(` ${utils_js_1.c.dim}Polling /api/changes every ${intervalSec}s · Ctrl+C to stop${utils_js_1.c.reset}`);
27
+ console.log();
28
+ }
29
+ let lastSeen = new Date().toISOString();
30
+ // Handle Ctrl+C gracefully
31
+ process.on('SIGINT', () => {
32
+ if (!isJson) {
33
+ console.log(`\n ${utils_js_1.c.dim}Watch stopped.${utils_js_1.c.reset}\n`);
34
+ }
35
+ process.exit(0);
36
+ });
37
+ // First poll: show recent changes from last hour
38
+ try {
39
+ const initialSince = new Date(Date.now() - 60 * 60 * 1000).toISOString();
40
+ const changes = await fetchChanges(initialSince, query);
41
+ if (changes.length > 0) {
42
+ if (!isJson) {
43
+ console.log(` ${utils_js_1.c.dim}${ts()} Recent changes (last hour):${utils_js_1.c.reset}`);
44
+ }
45
+ const deduped = dedupeByTitle(changes);
46
+ for (const change of deduped) {
47
+ if (isJson) {
48
+ console.log(JSON.stringify(change));
49
+ }
50
+ else {
51
+ printChange(change);
52
+ }
53
+ }
54
+ lastSeen = changes[0].detectedAt;
55
+ }
56
+ else if (!isJson) {
57
+ console.log(` ${utils_js_1.c.dim}${ts()} No recent changes. Watching...${utils_js_1.c.reset}`);
58
+ }
59
+ }
60
+ catch (err) {
61
+ if (!isJson) {
62
+ console.error(` ${utils_js_1.c.dim}${ts()} Error: ${err.message}${utils_js_1.c.reset}`);
63
+ }
64
+ }
65
+ // Polling loop
66
+ while (true) {
67
+ await sleep(intervalMs);
68
+ try {
69
+ const changes = await fetchChanges(lastSeen, query);
70
+ const deduped = isJson ? changes : dedupeByTitle(changes);
71
+ for (const change of deduped) {
72
+ if (isJson) {
73
+ console.log(JSON.stringify(change));
74
+ }
75
+ else {
76
+ printChange(change);
77
+ }
78
+ }
79
+ if (changes.length > 0) {
80
+ lastSeen = changes[0].detectedAt;
81
+ }
82
+ else if (!isJson) {
83
+ process.stdout.write(` ${utils_js_1.c.dim}${ts()} · no new changes${utils_js_1.c.reset}\r`);
84
+ }
85
+ }
86
+ catch (err) {
87
+ if (!isJson) {
88
+ console.error(` ${utils_js_1.c.dim}${ts()} Error: ${err.message}${utils_js_1.c.reset}`);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ // Group same-title same-type changes into one line (e.g. 12 FaZe CEO contracts → 1 line)
94
+ function dedupeByTitle(changes) {
95
+ const groups = new Map();
96
+ for (const c of changes) {
97
+ const key = `${c.changeType}:${c.title}`;
98
+ const existing = groups.get(key);
99
+ if (existing) {
100
+ existing.count++;
101
+ }
102
+ else {
103
+ groups.set(key, { count: 1, representative: c });
104
+ }
105
+ }
106
+ return Array.from(groups.values()).map(({ count, representative }) => {
107
+ if (count > 1) {
108
+ return { ...representative, title: `${representative.title} (${count} contracts)` };
109
+ }
110
+ return representative;
111
+ });
112
+ }
113
+ async function fetchChanges(since, query) {
114
+ let url = `${SF_API_URL}/api/changes?since=${encodeURIComponent(since)}&limit=50`;
115
+ if (query)
116
+ url += `&q=${encodeURIComponent(query)}`;
117
+ const res = await fetch(url);
118
+ if (!res.ok)
119
+ return [];
120
+ const data = await res.json();
121
+ return data.changes || [];
122
+ }
123
+ function printChange(change) {
124
+ const time = ts();
125
+ const venue = change.venue === 'kalshi' ? `${utils_js_1.c.cyan}K${utils_js_1.c.reset}` :
126
+ change.venue === 'polymarket' ? `\x1b[35mP\x1b[39m` :
127
+ change.venue === 'traditional' ? `${utils_js_1.c.dim}$${utils_js_1.c.reset}` : '·';
128
+ switch (change.changeType) {
129
+ case 'new_contract':
130
+ console.log(` ${time} ${venue} ${utils_js_1.c.green}+ NEW${utils_js_1.c.reset} ${change.afterPrice}¢ ${change.title}`);
131
+ break;
132
+ case 'removed_contract':
133
+ console.log(` ${time} ${venue} ${utils_js_1.c.red}− GONE${utils_js_1.c.reset} ${change.title}`);
134
+ break;
135
+ case 'price_move': {
136
+ if (change.venue === 'traditional') {
137
+ const sign = (change.delta || 0) > 0 ? '+' : '';
138
+ const color = (change.delta || 0) > 0 ? utils_js_1.c.green : utils_js_1.c.red;
139
+ const beforeDollars = ((change.beforePrice || 0) / 100).toFixed(0);
140
+ const afterDollars = ((change.afterPrice || 0) / 100).toFixed(0);
141
+ console.log(` ${time} ${venue} ${color}${sign}${change.delta}¢${utils_js_1.c.reset} $${beforeDollars}→$${afterDollars} ${change.title}`);
142
+ }
143
+ else {
144
+ const sign = (change.delta || 0) > 0 ? '+' : '';
145
+ const color = (change.delta || 0) > 0 ? utils_js_1.c.green : utils_js_1.c.red;
146
+ console.log(` ${time} ${venue} ${color}${sign}${change.delta}¢${utils_js_1.c.reset} ${change.beforePrice}→${change.afterPrice}¢ ${change.title}`);
147
+ }
148
+ break;
149
+ }
150
+ }
151
+ }
152
+ function ts() {
153
+ return new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
154
+ }
155
+ function sleep(ms) {
156
+ return new Promise(resolve => setTimeout(resolve, ms));
157
+ }
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ const create_js_1 = require("./commands/create.js");
29
29
  const signal_js_1 = require("./commands/signal.js");
30
30
  const evaluate_js_1 = require("./commands/evaluate.js");
31
31
  const scan_js_1 = require("./commands/scan.js");
32
+ const watch_js_1 = require("./commands/watch.js");
32
33
  const positions_js_1 = require("./commands/positions.js");
33
34
  const edges_js_1 = require("./commands/edges.js");
34
35
  const agent_js_1 = require("./commands/agent.js");
@@ -99,12 +100,18 @@ const GROUPED_HELP = `
99
100
  \x1b[36mscan\x1b[39m "keywords" Search Kalshi + Polymarket
100
101
  \x1b[36mscan\x1b[39m --series TICKER Browse a Kalshi series
101
102
  \x1b[36medges\x1b[39m [--json] Top edges across all theses
103
+ \x1b[36mwatch\x1b[39m [query] Watch for market changes (e.g. sf watch "gold")
102
104
  \x1b[36mwhatif\x1b[39m <id> What-if scenario analysis
103
105
  \x1b[36mliquidity\x1b[39m [topic] Orderbook liquidity scanner
104
106
  \x1b[36mbook\x1b[39m <ticker> [ticker2...] Orderbook depth for specific markets
105
107
  \x1b[36mexplore\x1b[39m [slug] Browse public theses
106
108
  \x1b[36mforecast\x1b[39m <event> Market distribution (P50/P75/P90)
107
109
 
110
+ \x1b[1mShare\x1b[22m \x1b[2m(any command with --share generates a short URL)\x1b[22m
111
+ \x1b[36mscan\x1b[39m "gold" --share Share scan results
112
+ \x1b[36mquery\x1b[39m "fed rate" --share Share query results
113
+ \x1b[36mcontext\x1b[39m --share Share global context snapshot
114
+
108
115
  \x1b[1mPortfolio\x1b[22m
109
116
  \x1b[36mpositions\x1b[39m Kalshi + Polymarket positions
110
117
  \x1b[36mbalance\x1b[39m Account balance
@@ -312,9 +319,10 @@ program
312
319
  .command('context [id]')
313
320
  .description('Context snapshot. With ID: thesis-specific. Without: global market snapshot (no auth)')
314
321
  .option('--json', 'Output raw JSON')
322
+ .option('--share', 'Share output via short URL')
315
323
  .action(async (id, opts, cmd) => {
316
324
  const g = cmd.optsWithGlobals();
317
- await run(() => (0, context_js_1.contextCommand)(id, { json: opts.json, apiKey: g.apiKey, apiUrl: g.apiUrl }));
325
+ await run(() => (0, context_js_1.contextCommand)(id, { json: opts.json, share: opts.share, apiKey: g.apiKey, apiUrl: g.apiUrl }));
318
326
  });
319
327
  // ── sf create <thesis> ────────────────────────────────────────────────────────
320
328
  program
@@ -376,6 +384,7 @@ program
376
384
  .option('--market <ticker>', 'Get single market detail (e.g. KXWTIMAX-26DEC31-T140)')
377
385
  .option('--venue <venue>', 'Filter by venue: kalshi, polymarket, or all (default: all)')
378
386
  .option('--json', 'Output raw JSON')
387
+ .option('--share', 'Share output via short URL')
379
388
  .action(async (query, opts, cmd) => {
380
389
  const g = cmd.optsWithGlobals();
381
390
  const q = query || '';
@@ -388,15 +397,26 @@ program
388
397
  market: opts.market,
389
398
  venue: opts.venue,
390
399
  json: opts.json,
400
+ share: opts.share,
391
401
  apiKey: g.apiKey,
392
402
  apiUrl: g.apiUrl,
393
403
  }));
394
404
  });
405
+ // ── sf watch [query] ─────────────────────────────────────────────────────────
406
+ program
407
+ .command('watch [query]')
408
+ .description('Watch for market changes (e.g. sf watch "gold")')
409
+ .option('--interval <seconds>', 'Poll interval in seconds (default: 60)', '60')
410
+ .option('--json', 'JSON change events (for piping)')
411
+ .action(async (query, opts) => {
412
+ await run(() => (0, watch_js_1.watchCommand)(query, { interval: opts.interval, json: opts.json }));
413
+ });
395
414
  // ── sf edges ──────────────────────────────────────────────────────────────────
396
415
  program
397
416
  .command('edges')
398
417
  .description('Top edges across all theses — what to trade now')
399
418
  .option('--json', 'JSON output for agents')
419
+ .option('--share', 'Share output via short URL')
400
420
  .option('--limit <n>', 'Max edges to show', '20')
401
421
  .option('--thesis <id>', 'Filter to a single thesis')
402
422
  .option('--min-edge <cents>', 'Minimum absolute edge size in cents')
@@ -406,6 +426,7 @@ program
406
426
  const g = cmd.optsWithGlobals();
407
427
  await run(() => (0, edges_js_1.edgesCommand)({
408
428
  json: opts.json,
429
+ share: opts.share,
409
430
  limit: opts.limit,
410
431
  thesis: opts.thesis,
411
432
  minEdge: opts.minEdge,
@@ -465,8 +486,9 @@ program
465
486
  .command('explore [slug]')
466
487
  .description('Browse public theses (no auth required)')
467
488
  .option('--json', 'JSON output')
489
+ .option('--share', 'Share output via short URL')
468
490
  .action(async (slug, opts) => {
469
- await run(() => (0, explore_js_1.exploreCommand)(slug, { json: opts.json }));
491
+ await run(() => (0, explore_js_1.exploreCommand)(slug, { json: opts.json, share: opts.share }));
470
492
  });
471
493
  // ── sf dashboard ──────────────────────────────────────────────────────────────
472
494
  program
@@ -702,17 +724,19 @@ program
702
724
  .command('markets')
703
725
  .description('Traditional market snapshot — SPY, VIX, Treasury, Gold, Oil (no auth)')
704
726
  .option('--json', 'JSON output')
727
+ .option('--share', 'Share output via short URL')
705
728
  .action(async (opts) => {
706
- await run(() => (0, markets_js_1.marketsCommand)({ json: opts.json }));
729
+ await run(() => (0, markets_js_1.marketsCommand)({ json: opts.json, share: opts.share }));
707
730
  });
708
731
  // ── sf query "question" ──────────────────────────────────────────────────────
709
732
  program
710
733
  .command('query <question>')
711
734
  .description('LLM-enhanced prediction market knowledge search (no auth required)')
712
735
  .option('--json', 'JSON output')
736
+ .option('--share', 'Share output via short URL')
713
737
  .option('--limit <n>', 'Max results per category (default 10)', '10')
714
738
  .action(async (question, opts) => {
715
- await run(() => (0, query_js_1.queryCommand)(question, { json: opts.json, limit: opts.limit }));
739
+ await run(() => (0, query_js_1.queryCommand)(question, { json: opts.json, share: opts.share, limit: opts.limit }));
716
740
  });
717
741
  // ── sf telegram ──────────────────────────────────────────────────────────────
718
742
  program
@@ -0,0 +1,4 @@
1
+ /**
2
+ * --share helper: POST CLI output to /api/share and print the URL.
3
+ */
4
+ export declare function shareOutput(command: string, args: string, data: unknown): Promise<void>;
package/dist/share.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * --share helper: POST CLI output to /api/share and print the URL.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.shareOutput = shareOutput;
7
+ const SF_API_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
8
+ async function shareOutput(command, args, data) {
9
+ try {
10
+ const res = await fetch(`${SF_API_URL}/api/share`, {
11
+ method: 'POST',
12
+ headers: { 'Content-Type': 'application/json' },
13
+ body: JSON.stringify({ command, args, data }),
14
+ });
15
+ if (!res.ok) {
16
+ console.error(`Share failed: ${res.status}`);
17
+ return;
18
+ }
19
+ const { url } = await res.json();
20
+ console.log();
21
+ console.log(` \x1b[36m${url}\x1b[39m`);
22
+ console.log();
23
+ }
24
+ catch (err) {
25
+ console.error(`Share failed: ${err.message}`);
26
+ }
27
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Skill loader — reads .md files from the skills directory,
3
+ * parses frontmatter, and makes them available as slash commands.
4
+ */
5
+ export interface Skill {
6
+ name: string;
7
+ trigger: string;
8
+ description: string;
9
+ author: string;
10
+ version: string;
11
+ category: string;
12
+ tags: string[];
13
+ toolsUsed: string[];
14
+ estimatedTime: string;
15
+ auto?: string;
16
+ prompt: string;
17
+ raw: string;
18
+ }
19
+ export declare function loadSkills(): Skill[];
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Skill loader — reads .md files from the skills directory,
4
+ * parses frontmatter, and makes them available as slash commands.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.loadSkills = loadSkills;
8
+ const fs_1 = require("fs");
9
+ const path_1 = require("path");
10
+ function loadSkills() {
11
+ const skills = [];
12
+ // Built-in skills from cli/src/skills/
13
+ // __dirname works in CJS; resolve to src/skills regardless of whether running from dist/ or src/
14
+ const thisDir = __dirname;
15
+ const skillsDir = thisDir.includes('dist')
16
+ ? (0, path_1.join)(thisDir, '../../src/skills') // running from dist/skills/
17
+ : thisDir; // running from src/skills/
18
+ let files = [];
19
+ try {
20
+ files = (0, fs_1.readdirSync)(skillsDir).filter(f => f.endsWith('.md'));
21
+ }
22
+ catch {
23
+ return skills;
24
+ }
25
+ for (const file of files) {
26
+ try {
27
+ const raw = (0, fs_1.readFileSync)((0, path_1.join)(skillsDir, file), 'utf-8');
28
+ const skill = parseSkillFile(raw);
29
+ if (skill)
30
+ skills.push(skill);
31
+ }
32
+ catch { }
33
+ }
34
+ // User skills from ~/.sf/skills/ (future extensibility)
35
+ try {
36
+ const home = process.env.HOME || process.env.USERPROFILE || '';
37
+ const userDir = (0, path_1.join)(home, '.sf', 'skills');
38
+ const userFiles = (0, fs_1.readdirSync)(userDir).filter(f => f.endsWith('.md'));
39
+ for (const file of userFiles) {
40
+ try {
41
+ const raw = (0, fs_1.readFileSync)((0, path_1.join)(userDir, file), 'utf-8');
42
+ const skill = parseSkillFile(raw);
43
+ if (skill)
44
+ skills.push(skill);
45
+ }
46
+ catch { }
47
+ }
48
+ }
49
+ catch { }
50
+ return skills;
51
+ }
52
+ function parseSkillFile(raw) {
53
+ // Parse YAML frontmatter between --- delimiters
54
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
55
+ if (!match)
56
+ return null;
57
+ const frontmatter = match[1];
58
+ const body = match[2].trim();
59
+ const meta = {};
60
+ for (const line of frontmatter.split('\n')) {
61
+ const m = line.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
62
+ if (m)
63
+ meta[m[1]] = m[2].trim();
64
+ }
65
+ if (!meta.name || !meta.trigger)
66
+ return null;
67
+ // Extract the "Instructions" section as the prompt (everything after ## Instructions)
68
+ const instructionsMatch = body.match(/## Instructions\n([\s\S]*)$/i);
69
+ const prompt = instructionsMatch ? instructionsMatch[1].trim() : body;
70
+ // Parse array fields: [a, b, c]
71
+ const parseArray = (s) => (s || '').replace(/[\[\]]/g, '').split(',').map(x => x.trim()).filter(Boolean);
72
+ return {
73
+ name: meta.name,
74
+ trigger: meta.trigger,
75
+ description: meta.description || '',
76
+ author: meta.author || 'unknown',
77
+ version: meta.version || '1.0.0',
78
+ category: meta.category || 'general',
79
+ tags: parseArray(meta.tags),
80
+ toolsUsed: parseArray(meta.tools_used),
81
+ estimatedTime: meta.estimated_time || '',
82
+ auto: meta.auto,
83
+ prompt,
84
+ raw,
85
+ };
86
+ }