kalshi-trading-bot-cli 2.1.0

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +360 -0
  3. package/assets/kalshi-flow-light.png +0 -0
  4. package/assets/screenshot.png +0 -0
  5. package/env.example +43 -0
  6. package/kalshi-flow-light.png +0 -0
  7. package/package.json +66 -0
  8. package/src/agent/agent.ts +249 -0
  9. package/src/agent/channels.ts +53 -0
  10. package/src/agent/index.ts +29 -0
  11. package/src/agent/prompts.ts +171 -0
  12. package/src/agent/run-context.ts +23 -0
  13. package/src/agent/scratchpad.ts +465 -0
  14. package/src/agent/token-counter.ts +33 -0
  15. package/src/agent/tool-executor.ts +166 -0
  16. package/src/agent/types.ts +221 -0
  17. package/src/audit/index.ts +25 -0
  18. package/src/audit/reader.ts +43 -0
  19. package/src/audit/trail.ts +29 -0
  20. package/src/audit/types.ts +133 -0
  21. package/src/backtest/discovery.ts +170 -0
  22. package/src/backtest/fetcher.ts +247 -0
  23. package/src/backtest/metrics.ts +165 -0
  24. package/src/backtest/renderer.ts +196 -0
  25. package/src/backtest/types.ts +45 -0
  26. package/src/cli.ts +943 -0
  27. package/src/commands/alerts.ts +48 -0
  28. package/src/commands/analyze.ts +662 -0
  29. package/src/commands/backtest.ts +276 -0
  30. package/src/commands/clear-cache.ts +24 -0
  31. package/src/commands/config.ts +107 -0
  32. package/src/commands/dispatch.ts +473 -0
  33. package/src/commands/edge.ts +62 -0
  34. package/src/commands/formatters.ts +339 -0
  35. package/src/commands/help.ts +263 -0
  36. package/src/commands/helpers.ts +48 -0
  37. package/src/commands/index.ts +287 -0
  38. package/src/commands/json.ts +43 -0
  39. package/src/commands/parse-args.ts +229 -0
  40. package/src/commands/portfolio.ts +236 -0
  41. package/src/commands/review.ts +176 -0
  42. package/src/commands/scan-formatters.ts +98 -0
  43. package/src/commands/scan.ts +38 -0
  44. package/src/commands/search-edge.ts +139 -0
  45. package/src/commands/status.ts +70 -0
  46. package/src/commands/themes.ts +117 -0
  47. package/src/commands/watch.ts +295 -0
  48. package/src/components/answer-box.ts +57 -0
  49. package/src/components/approval-prompt.ts +34 -0
  50. package/src/components/browse-list.ts +134 -0
  51. package/src/components/chat-log.ts +291 -0
  52. package/src/components/custom-editor.ts +18 -0
  53. package/src/components/debug-panel.ts +52 -0
  54. package/src/components/index.ts +17 -0
  55. package/src/components/intro.ts +92 -0
  56. package/src/components/select-list.ts +155 -0
  57. package/src/components/tool-event.ts +127 -0
  58. package/src/components/user-query.ts +18 -0
  59. package/src/components/working-indicator.ts +87 -0
  60. package/src/controllers/agent-runner.ts +283 -0
  61. package/src/controllers/browse.ts +1013 -0
  62. package/src/controllers/index.ts +7 -0
  63. package/src/controllers/input-history.ts +76 -0
  64. package/src/controllers/model-selection.ts +244 -0
  65. package/src/db/alerts.ts +77 -0
  66. package/src/db/edge.ts +105 -0
  67. package/src/db/event-index.ts +323 -0
  68. package/src/db/events.ts +41 -0
  69. package/src/db/index.ts +60 -0
  70. package/src/db/octagon-cache.ts +118 -0
  71. package/src/db/positions.ts +71 -0
  72. package/src/db/risk.ts +51 -0
  73. package/src/db/schema.ts +227 -0
  74. package/src/db/themes.ts +34 -0
  75. package/src/db/trades.ts +50 -0
  76. package/src/eval/brier.ts +90 -0
  77. package/src/eval/index.ts +4 -0
  78. package/src/eval/performance.ts +87 -0
  79. package/src/gateway/access-control.ts +253 -0
  80. package/src/gateway/agent-runner.ts +75 -0
  81. package/src/gateway/alerts/formatter.ts +90 -0
  82. package/src/gateway/alerts/index.ts +4 -0
  83. package/src/gateway/alerts/router.ts +32 -0
  84. package/src/gateway/alerts/terminal.ts +16 -0
  85. package/src/gateway/alerts/types.ts +13 -0
  86. package/src/gateway/channels/index.ts +9 -0
  87. package/src/gateway/channels/manager.ts +153 -0
  88. package/src/gateway/channels/types.ts +48 -0
  89. package/src/gateway/channels/whatsapp/README.md +234 -0
  90. package/src/gateway/channels/whatsapp/auth-store.ts +140 -0
  91. package/src/gateway/channels/whatsapp/dedupe.ts +60 -0
  92. package/src/gateway/channels/whatsapp/error.ts +122 -0
  93. package/src/gateway/channels/whatsapp/inbound.ts +326 -0
  94. package/src/gateway/channels/whatsapp/index.ts +5 -0
  95. package/src/gateway/channels/whatsapp/lid.ts +56 -0
  96. package/src/gateway/channels/whatsapp/logger.ts +25 -0
  97. package/src/gateway/channels/whatsapp/login.ts +94 -0
  98. package/src/gateway/channels/whatsapp/outbound.ts +119 -0
  99. package/src/gateway/channels/whatsapp/plugin.ts +54 -0
  100. package/src/gateway/channels/whatsapp/reconnect.ts +40 -0
  101. package/src/gateway/channels/whatsapp/runtime.ts +122 -0
  102. package/src/gateway/channels/whatsapp/session.ts +89 -0
  103. package/src/gateway/channels/whatsapp/types.ts +32 -0
  104. package/src/gateway/commands/handler.ts +64 -0
  105. package/src/gateway/commands/index.ts +7 -0
  106. package/src/gateway/commands/parser.ts +29 -0
  107. package/src/gateway/commands/wa-formatters.ts +92 -0
  108. package/src/gateway/config.ts +244 -0
  109. package/src/gateway/extension-points.ts +17 -0
  110. package/src/gateway/gateway.ts +301 -0
  111. package/src/gateway/group/history-buffer.ts +75 -0
  112. package/src/gateway/group/index.ts +8 -0
  113. package/src/gateway/group/member-tracker.ts +60 -0
  114. package/src/gateway/group/mention-detection.ts +42 -0
  115. package/src/gateway/heartbeat/index.ts +8 -0
  116. package/src/gateway/heartbeat/prompt.ts +73 -0
  117. package/src/gateway/heartbeat/runner.ts +200 -0
  118. package/src/gateway/heartbeat/suppression.ts +74 -0
  119. package/src/gateway/index.ts +138 -0
  120. package/src/gateway/routing/resolve-route.ts +119 -0
  121. package/src/gateway/sessions/store.ts +65 -0
  122. package/src/gateway/types.ts +11 -0
  123. package/src/gateway/utils.ts +82 -0
  124. package/src/index.tsx +30 -0
  125. package/src/model/llm.ts +247 -0
  126. package/src/providers.ts +94 -0
  127. package/src/risk/circuit-breaker.ts +113 -0
  128. package/src/risk/correlation.ts +40 -0
  129. package/src/risk/gate.ts +125 -0
  130. package/src/risk/index.ts +10 -0
  131. package/src/risk/kelly.ts +230 -0
  132. package/src/scan/alerter.ts +64 -0
  133. package/src/scan/edge-computer.ts +164 -0
  134. package/src/scan/invoker.ts +199 -0
  135. package/src/scan/loop.ts +184 -0
  136. package/src/scan/octagon-client.ts +627 -0
  137. package/src/scan/octagon-events-api.ts +105 -0
  138. package/src/scan/octagon-prefetch.ts +172 -0
  139. package/src/scan/theme-resolver.ts +179 -0
  140. package/src/scan/types.ts +62 -0
  141. package/src/scan/watchdog.ts +126 -0
  142. package/src/setup/wizard.ts +659 -0
  143. package/src/theme.ts +67 -0
  144. package/src/tools/fetch/cache.ts +95 -0
  145. package/src/tools/fetch/external-content.ts +200 -0
  146. package/src/tools/fetch/index.ts +1 -0
  147. package/src/tools/fetch/web-fetch-utils.ts +122 -0
  148. package/src/tools/fetch/web-fetch.ts +419 -0
  149. package/src/tools/index.ts +10 -0
  150. package/src/tools/kalshi/api.ts +251 -0
  151. package/src/tools/kalshi/dlq.ts +35 -0
  152. package/src/tools/kalshi/events.ts +84 -0
  153. package/src/tools/kalshi/exchange.ts +24 -0
  154. package/src/tools/kalshi/historical.ts +89 -0
  155. package/src/tools/kalshi/index.ts +11 -0
  156. package/src/tools/kalshi/kalshi-search.ts +437 -0
  157. package/src/tools/kalshi/kalshi-trade.ts +102 -0
  158. package/src/tools/kalshi/markets.ts +76 -0
  159. package/src/tools/kalshi/portfolio.ts +100 -0
  160. package/src/tools/kalshi/search-index.ts +198 -0
  161. package/src/tools/kalshi/series.ts +16 -0
  162. package/src/tools/kalshi/trading.ts +115 -0
  163. package/src/tools/kalshi/types.ts +199 -0
  164. package/src/tools/registry.ts +160 -0
  165. package/src/tools/search/index.ts +25 -0
  166. package/src/tools/search/tavily.ts +35 -0
  167. package/src/tools/types.ts +53 -0
  168. package/src/tools/v2/edge-query.ts +135 -0
  169. package/src/tools/v2/octagon-report.ts +112 -0
  170. package/src/tools/v2/portfolio-query.ts +79 -0
  171. package/src/tools/v2/portfolio-review.ts +59 -0
  172. package/src/tools/v2/risk-status.ts +94 -0
  173. package/src/tools/v2/scan.ts +78 -0
  174. package/src/types/qrcode-terminal.d.ts +7 -0
  175. package/src/types/whiskeysockets-baileys.d.ts +41 -0
  176. package/src/types.ts +22 -0
  177. package/src/utils/ai-message.ts +26 -0
  178. package/src/utils/bot-config.ts +219 -0
  179. package/src/utils/cache.ts +195 -0
  180. package/src/utils/config.ts +113 -0
  181. package/src/utils/env.ts +111 -0
  182. package/src/utils/errors.ts +313 -0
  183. package/src/utils/history-context.ts +32 -0
  184. package/src/utils/in-memory-chat-history.ts +268 -0
  185. package/src/utils/index.ts +28 -0
  186. package/src/utils/input-key-handlers.ts +64 -0
  187. package/src/utils/logger.ts +67 -0
  188. package/src/utils/long-term-chat-history.ts +138 -0
  189. package/src/utils/markdown-table.ts +227 -0
  190. package/src/utils/model.ts +70 -0
  191. package/src/utils/ollama.ts +37 -0
  192. package/src/utils/paths.ts +12 -0
  193. package/src/utils/progress-channel.ts +84 -0
  194. package/src/utils/telemetry.ts +103 -0
  195. package/src/utils/text-navigation.ts +81 -0
  196. package/src/utils/thinking-verbs.ts +18 -0
  197. package/src/utils/tokens.ts +36 -0
  198. package/src/utils/tool-description.ts +61 -0
@@ -0,0 +1,134 @@
1
+ import { Container, Text, type SelectItem } from '@mariozechner/pi-tui';
2
+ import { VimSelectList } from './select-list.js';
3
+ import { selectListTheme, theme } from '../theme.js';
4
+ import type { BrowseEventRow, BrowseMarketRow } from '../controllers/browse.js';
5
+
6
+ function pad(s: string, len: number): string {
7
+ return s.length > len ? s.slice(0, len - 1) + '…' : s.padEnd(len);
8
+ }
9
+
10
+ function fmtPct(val: number | null): string {
11
+ if (val === null) return '--';
12
+ return `${(val * 100).toFixed(1)}%`;
13
+ }
14
+
15
+ function buildMarketItems(events: BrowseEventRow[]): SelectItem[] {
16
+ const items: SelectItem[] = [];
17
+ for (const ev of events) {
18
+ for (const m of ev.markets) {
19
+ const ticker = pad(m.ticker, 20);
20
+ const title = pad(m.title, 48);
21
+ const mktPct = pad(fmtPct(m.marketProb), 7);
22
+ const isPending = ev.pending === true;
23
+ const modelPct = pad(isPending && m.modelProb === null ? '...' : fmtPct(m.modelProb), 7);
24
+ const edgeStr = pad(isPending && m.edge === null ? '...' : (m.edge !== null ? `${m.edge > 0 ? '+' : ''}${(m.edge * 100).toFixed(1)}%` : '--'), 7);
25
+ const conf = pad(isPending && m.confidence === null ? 'pending' : (m.confidence ?? '--'), 8);
26
+
27
+ items.push({
28
+ value: JSON.stringify({ eventTicker: ev.eventTicker, marketTicker: m.ticker }),
29
+ label: `${ticker} ${title} ${mktPct} ${modelPct} ${edgeStr} ${conf}`,
30
+ });
31
+ }
32
+ }
33
+ return items;
34
+ }
35
+
36
+ /** Update an existing browse selector's item labels in-place (preserves scroll/selection). */
37
+ export function updateBrowseMarketSelector(
38
+ container: Container,
39
+ events: BrowseEventRow[],
40
+ ): void {
41
+ const list = (container as any)._browseList as VimSelectList | undefined;
42
+ if (!list) return;
43
+ const newItems = buildMarketItems(events);
44
+ // Access private arrays via any cast to update labels without recreating the list
45
+ const items = (list as any).items as SelectItem[];
46
+ const filtered = (list as any).filteredItems as SelectItem[];
47
+ for (let i = 0; i < items.length && i < newItems.length; i++) {
48
+ items[i].label = newItems[i].label;
49
+ }
50
+ for (let i = 0; i < filtered.length && i < newItems.length; i++) {
51
+ filtered[i].label = newItems[i].label;
52
+ }
53
+ }
54
+
55
+ export function createBrowseMarketSelector(
56
+ events: BrowseEventRow[],
57
+ onSelect: (eventTicker: string, marketTicker: string) => void,
58
+ onCancel: () => void,
59
+ errorMessage?: string | null,
60
+ progressMessage?: string | null,
61
+ ): Container {
62
+ const items = buildMarketItems(events);
63
+
64
+ const container = new Container();
65
+
66
+ // Progress message (shown above header)
67
+ if (progressMessage) {
68
+ container.addChild(new Text(theme.muted(progressMessage), 0, 0));
69
+ }
70
+
71
+ // Error message (shown above header so it's always visible)
72
+ if (errorMessage) {
73
+ container.addChild(new Text(theme.bold(theme.warning(errorMessage)), 0, 0));
74
+ }
75
+
76
+ // Header row
77
+ const header = `${pad('Ticker', 20)} ${pad('Title', 48)} ${pad('Mkt %', 7)} ${pad('Model%', 7)} ${pad('Edge', 7)} ${pad('Conf', 8)}`;
78
+ container.addChild(new Text(theme.muted(header), 0, 0));
79
+
80
+ if (items.length === 0) {
81
+ container.addChild(new Text(theme.muted('No markets found.'), 0, 0));
82
+ container.addChild(new Text(theme.muted('esc to go back'), 0, 0));
83
+ return container;
84
+ }
85
+
86
+ const list = new VimSelectList(items, Math.min(items.length, 20), selectListTheme);
87
+ list.onSelect = (item) => {
88
+ let parsed: { eventTicker: string; marketTicker: string };
89
+ try {
90
+ parsed = JSON.parse(item.value);
91
+ } catch {
92
+ return;
93
+ }
94
+ onSelect(parsed.eventTicker, parsed.marketTicker);
95
+ };
96
+ list.onCancel = () => onCancel();
97
+ container.addChild(list);
98
+
99
+ // Store list reference for focus
100
+ (container as any)._browseList = list;
101
+
102
+ return container;
103
+ }
104
+
105
+ export function createBrowseActionSelector(
106
+ onSelect: (action: string) => void,
107
+ onCancel: () => void,
108
+ hasReport = true,
109
+ directMode = false,
110
+ ): Container {
111
+ const items: SelectItem[] = [];
112
+ let n = 1;
113
+ if (hasReport) {
114
+ items.push({ value: 'view_report', label: `${n++}. View research report` });
115
+ } else {
116
+ items.push({ value: 'no_report', label: theme.muted(`${n++}. No cached report available`) });
117
+ }
118
+ items.push({ value: 'refresh', label: `${n++}. Refresh this research report (costs credits)` });
119
+ if (!directMode) {
120
+ items.push({ value: 'refresh_all', label: `${n++}. Refresh all research reports for this theme (costs credits)` });
121
+ }
122
+ items.push({ value: 'trade', label: `${n++}. Make a trade` });
123
+ items.push({ value: 'back', label: `${n++}. Back` });
124
+
125
+ const list = new VimSelectList(items, items.length, selectListTheme);
126
+ list.onSelect = (item) => onSelect(item.value);
127
+ list.onCancel = () => onCancel();
128
+
129
+ const container = new Container();
130
+ container.addChild(list);
131
+ (container as any)._browseList = list;
132
+
133
+ return container;
134
+ }
@@ -0,0 +1,291 @@
1
+ import { Container, Spacer, Text, type TUI } from '@mariozechner/pi-tui';
2
+ import type { TokenUsage } from '../agent/types.js';
3
+ import { theme } from '../theme.js';
4
+ import { AnswerBoxComponent } from './answer-box.js';
5
+ import { ToolEventComponent } from './tool-event.js';
6
+ import { UserQueryComponent } from './user-query.js';
7
+
8
+ function formatDuration(ms: number): string {
9
+ if (ms < 1000) {
10
+ return `${Math.round(ms)}ms`;
11
+ }
12
+ const totalSeconds = Math.round(ms / 1000);
13
+ const minutes = Math.floor(totalSeconds / 60);
14
+ const seconds = totalSeconds % 60;
15
+ if (minutes === 0) {
16
+ return `${seconds}s`;
17
+ }
18
+ return `${minutes}m ${seconds}s`;
19
+ }
20
+
21
+ function truncateUrl(url: string, maxLen = 45): string {
22
+ try {
23
+ const parsed = new URL(url);
24
+ const display = parsed.hostname + parsed.pathname;
25
+ return display.length <= maxLen ? display : `${display.slice(0, maxLen)}...`;
26
+ } catch {
27
+ return url.length > maxLen ? `${url.slice(0, maxLen)}...` : url;
28
+ }
29
+ }
30
+
31
+ function formatBrowserStep(args: Record<string, unknown>): string | null {
32
+ const action = args.action as string | undefined;
33
+ const url = args.url as string | undefined;
34
+ switch (action) {
35
+ case 'open':
36
+ return `Opening ${truncateUrl(url || '')}`;
37
+ case 'navigate':
38
+ return `Navigating to ${truncateUrl(url || '')}`;
39
+ case 'snapshot':
40
+ return 'Reading page structure';
41
+ case 'read':
42
+ return 'Extracting page text';
43
+ case 'close':
44
+ return 'Closing browser';
45
+ case 'act':
46
+ return null;
47
+ default:
48
+ return null;
49
+ }
50
+ }
51
+
52
+ interface ToolDisplayComponent {
53
+ setActive(progressMessage?: string): void;
54
+ setComplete(summary: string, duration: number): void;
55
+ setError(error: string): void;
56
+ setLimitWarning(warning?: string): void;
57
+ setApproval(decision: 'allow-once' | 'allow-session' | 'deny'): void;
58
+ setDenied(path: string, tool: string): void;
59
+ }
60
+
61
+ class BrowserSessionComponent extends Container implements ToolDisplayComponent {
62
+ private readonly header: Text;
63
+ private detail: Text | null = null;
64
+ private currentStep: string | null = null;
65
+
66
+ constructor(_tui: TUI) {
67
+ super();
68
+ this.addChild(new Spacer(1));
69
+ this.header = new Text('⏺ Browser', 0, 0);
70
+ this.addChild(this.header);
71
+ }
72
+
73
+ setStep(args: Record<string, unknown>) {
74
+ const step = formatBrowserStep(args);
75
+ if (step) {
76
+ this.currentStep = step;
77
+ }
78
+ }
79
+
80
+ setActive(progressMessage?: string): void {
81
+ this.clearDetail();
82
+ const message = progressMessage || this.currentStep || 'Searching...';
83
+ this.detail = new Text(`${theme.muted('⎿ ')}${message}`, 0, 0);
84
+ this.addChild(this.detail);
85
+ }
86
+
87
+ setComplete(summary: string, duration: number): void {
88
+ this.clearDetail();
89
+ const text = this.currentStep || `${summary}${theme.muted(` in ${formatDuration(duration)}`)}`;
90
+ this.detail = new Text(`${theme.muted('⎿ ')}${text}`, 0, 0);
91
+ this.addChild(this.detail);
92
+ }
93
+
94
+ setError(error: string): void {
95
+ this.clearDetail();
96
+ this.detail = new Text(`${theme.muted('⎿ ')}${theme.error(`Error: ${error}`)}`, 0, 0);
97
+ this.addChild(this.detail);
98
+ }
99
+
100
+ setLimitWarning(warning?: string): void {
101
+ this.clearDetail();
102
+ this.detail = new Text(`${theme.muted('⎿ ')}${theme.warning(warning || 'Approaching suggested limit')}`, 0, 0);
103
+ this.addChild(this.detail);
104
+ }
105
+
106
+ setApproval(decision: 'allow-once' | 'allow-session' | 'deny'): void {
107
+ this.clearDetail();
108
+ const label =
109
+ decision === 'allow-once'
110
+ ? 'Approved'
111
+ : decision === 'allow-session'
112
+ ? 'Approved (session)'
113
+ : 'Denied';
114
+ const color = decision === 'deny' ? theme.warning : theme.primary;
115
+ this.detail = new Text(`${theme.muted('⎿ ')}${color(label)}`, 0, 0);
116
+ this.addChild(this.detail);
117
+ }
118
+
119
+ setDenied(path: string, tool: string): void {
120
+ this.clearDetail();
121
+ const action = tool === 'write_file' ? 'write to' : tool === 'edit_file' ? 'edit of' : tool;
122
+ this.detail = new Text(`${theme.muted('⎿ ')}${theme.warning(`User denied ${action} ${path}`)}`, 0, 0);
123
+ this.addChild(this.detail);
124
+ }
125
+
126
+ private clearDetail() {
127
+ if (this.detail) {
128
+ this.removeChild(this.detail);
129
+ this.detail = null;
130
+ }
131
+ }
132
+ }
133
+
134
+ export class ChatLogComponent extends Container {
135
+ private readonly tui: TUI;
136
+ private readonly toolById = new Map<string, ToolDisplayComponent>();
137
+ private currentBrowserSession: BrowserSessionComponent | null = null;
138
+ private activeAnswer: AnswerBoxComponent | null = null;
139
+ private lastToolName: string | null = null;
140
+ private lastToolComponent: ToolDisplayComponent | null = null;
141
+
142
+ constructor(tui: TUI) {
143
+ super();
144
+ this.tui = tui;
145
+ }
146
+
147
+ clearAll() {
148
+ this.clear();
149
+ this.toolById.clear();
150
+ this.currentBrowserSession = null;
151
+ this.activeAnswer = null;
152
+ this.lastToolName = null;
153
+ this.lastToolComponent = null;
154
+ }
155
+
156
+ addQuery(query: string) {
157
+ this.addChild(new UserQueryComponent(query));
158
+ }
159
+
160
+ resetToolGrouping() {
161
+ this.lastToolName = null;
162
+ this.lastToolComponent = null;
163
+ }
164
+
165
+ addInterrupted() {
166
+ this.addChild(new Text(`${theme.muted('⎿ Interrupted · What should the agent do instead?')}`, 0, 0));
167
+ }
168
+
169
+ startTool(toolCallId: string, toolName: string, args: Record<string, unknown>) {
170
+ if (toolName !== 'browser') {
171
+ this.currentBrowserSession = null;
172
+ }
173
+
174
+ const existing = this.toolById.get(toolCallId);
175
+ if (existing) {
176
+ existing.setActive();
177
+ return existing;
178
+ }
179
+
180
+ if (toolName === 'browser') {
181
+ if (!this.currentBrowserSession) {
182
+ this.currentBrowserSession = new BrowserSessionComponent(this.tui);
183
+ this.addChild(this.currentBrowserSession);
184
+ }
185
+ this.currentBrowserSession.setStep(args);
186
+ this.currentBrowserSession.setActive();
187
+ this.toolById.set(toolCallId, this.currentBrowserSession);
188
+ this.lastToolName = null;
189
+ this.lastToolComponent = null;
190
+ return this.currentBrowserSession;
191
+ }
192
+
193
+ if (this.lastToolName === toolName && this.lastToolComponent) {
194
+ this.lastToolComponent.setActive();
195
+ this.toolById.set(toolCallId, this.lastToolComponent);
196
+ return this.lastToolComponent;
197
+ }
198
+
199
+ const component = new ToolEventComponent(this.tui, toolName, args);
200
+ component.setActive();
201
+ this.toolById.set(toolCallId, component);
202
+ this.addChild(component);
203
+ this.lastToolName = toolName;
204
+ this.lastToolComponent = component;
205
+ return component;
206
+ }
207
+
208
+ updateToolProgress(toolCallId: string, message: string) {
209
+ const existing = this.toolById.get(toolCallId);
210
+ if (!existing) {
211
+ return;
212
+ }
213
+ existing.setActive(message);
214
+ }
215
+
216
+ completeTool(toolCallId: string, summary: string, duration: number) {
217
+ const existing = this.toolById.get(toolCallId);
218
+ if (!existing) {
219
+ return;
220
+ }
221
+ existing.setComplete(summary, duration);
222
+ }
223
+
224
+ errorTool(toolCallId: string, error: string) {
225
+ const existing = this.toolById.get(toolCallId);
226
+ if (!existing) {
227
+ return;
228
+ }
229
+ existing.setError(error);
230
+ }
231
+
232
+ limitTool(toolCallId: string, warning?: string) {
233
+ const existing = this.toolById.get(toolCallId);
234
+ if (!existing) {
235
+ return;
236
+ }
237
+ existing.setLimitWarning(warning);
238
+ }
239
+
240
+ approveTool(toolCallId: string, decision: 'allow-once' | 'allow-session' | 'deny') {
241
+ const existing = this.toolById.get(toolCallId);
242
+ if (!existing) {
243
+ return;
244
+ }
245
+ existing.setApproval(decision);
246
+ }
247
+
248
+ denyTool(toolCallId: string, path: string, tool: string) {
249
+ const existing = this.toolById.get(toolCallId);
250
+ if (!existing) {
251
+ return;
252
+ }
253
+ existing.setDenied(path, tool);
254
+ }
255
+
256
+ finalizeAnswer(text: string): AnswerBoxComponent {
257
+ if (!this.activeAnswer) {
258
+ const box = new AnswerBoxComponent(text);
259
+ this.addChild(box);
260
+ return box;
261
+ }
262
+ this.activeAnswer.setText(text);
263
+ const box = this.activeAnswer;
264
+ this.activeAnswer = null;
265
+ return box;
266
+ }
267
+
268
+ addContextCleared(clearedCount: number, keptCount: number) {
269
+ this.addChild(
270
+ new Text(
271
+ `${theme.muted(
272
+ `⏺ Context threshold reached - cleared ${clearedCount} old tool result${clearedCount !== 1 ? 's' : ''}, kept ${keptCount} most recent`,
273
+ )}`,
274
+ 0,
275
+ 0,
276
+ ),
277
+ );
278
+ }
279
+
280
+ addPerformanceStats(duration: number, tokenUsage?: TokenUsage, tokensPerSecond?: number) {
281
+ const parts = [formatDuration(duration)];
282
+ if (tokenUsage && tokenUsage.totalTokens > 20_000) {
283
+ parts.push(`${tokenUsage.totalTokens.toLocaleString()} tokens`);
284
+ if (tokensPerSecond !== undefined) {
285
+ parts.push(`(${tokensPerSecond.toFixed(1)} tok/s)`);
286
+ }
287
+ }
288
+ this.addChild(new Spacer(1));
289
+ this.addChild(new Text(`${theme.muted('✻ ')}${theme.muted(parts.join(' · '))}`, 0, 0));
290
+ }
291
+ }
@@ -0,0 +1,18 @@
1
+ import { Editor, Key, matchesKey } from '@mariozechner/pi-tui';
2
+
3
+ export class CustomEditor extends Editor {
4
+ onEscape?: () => void;
5
+ onCtrlC?: () => void;
6
+
7
+ handleInput(data: string): void {
8
+ if (matchesKey(data, Key.escape) && this.onEscape) {
9
+ this.onEscape();
10
+ return;
11
+ }
12
+ if (matchesKey(data, Key.ctrl('c')) && this.onCtrlC) {
13
+ this.onCtrlC();
14
+ return;
15
+ }
16
+ super.handleInput(data);
17
+ }
18
+ }
@@ -0,0 +1,52 @@
1
+ import { Box, Container, Text } from '@mariozechner/pi-tui';
2
+ import { logger, type LogEntry, type LogLevel } from '../utils/logger.js';
3
+ import { theme } from '../theme.js';
4
+
5
+ const LEVEL_COLORS: Record<LogLevel, (text: string) => string> = {
6
+ debug: theme.mutedDark,
7
+ info: theme.info,
8
+ warn: theme.warning,
9
+ error: theme.error,
10
+ };
11
+
12
+ export class DebugPanelComponent extends Container {
13
+ private readonly box: Box;
14
+ private readonly maxLines: number;
15
+ private readonly show: boolean;
16
+ private logs: LogEntry[] = [];
17
+ private unsubscribe: (() => void) | null = null;
18
+
19
+ constructor(maxLines = 8, show = true) {
20
+ super();
21
+ this.maxLines = maxLines;
22
+ this.show = show;
23
+ this.box = new Box(1, 0, () => '');
24
+ this.addChild(this.box);
25
+ this.unsubscribe = logger.subscribe((entries) => {
26
+ this.logs = entries;
27
+ this.refresh();
28
+ });
29
+ this.refresh();
30
+ }
31
+
32
+ dispose() {
33
+ this.unsubscribe?.();
34
+ this.unsubscribe = null;
35
+ }
36
+
37
+ private refresh() {
38
+ this.box.clear();
39
+ if (!this.show || this.logs.length === 0) {
40
+ return;
41
+ }
42
+
43
+ this.box.addChild(new Text(theme.dim('─ Debug ─'), 0, 0));
44
+ const displayLogs = this.logs.slice(-this.maxLines);
45
+ for (const entry of displayLogs) {
46
+ const level = `[${entry.level.toUpperCase().padEnd(5)}]`;
47
+ const prefix = LEVEL_COLORS[entry.level](level);
48
+ const data = entry.data !== undefined ? ` ${theme.mutedDark(JSON.stringify(entry.data))}` : '';
49
+ this.box.addChild(new Text(`${prefix} ${entry.message}${data}`, 0, 0));
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,17 @@
1
+ export { AnswerBoxComponent } from './answer-box.js';
2
+ export { ApprovalPromptComponent } from './approval-prompt.js';
3
+ export { ChatLogComponent } from './chat-log.js';
4
+ export { DebugPanelComponent } from './debug-panel.js';
5
+ export { CustomEditor } from './custom-editor.js';
6
+ export { IntroComponent } from './intro.js';
7
+ export {
8
+ ApiKeyInputComponent,
9
+ createApiKeyConfirmSelector,
10
+ createApprovalSelector,
11
+ createModelSelector,
12
+ createProviderSelector,
13
+ } from './select-list.js';
14
+ export { ToolEventComponent } from './tool-event.js';
15
+ export { UserQueryComponent } from './user-query.js';
16
+ export { WorkingIndicatorComponent } from './working-indicator.js';
17
+ export { createBrowseMarketSelector, updateBrowseMarketSelector, createBrowseActionSelector } from './browse-list.js';
@@ -0,0 +1,92 @@
1
+ import { Container, Spacer, Text } from '@mariozechner/pi-tui';
2
+ import packageJson from '../../package.json';
3
+ import { theme } from '../theme.js';
4
+ import { getModelDisplayName } from '../utils/model.js';
5
+
6
+ const INTRO_WIDTH = 60;
7
+
8
+ export class IntroComponent extends Container {
9
+ private readonly modelText: Text;
10
+
11
+ constructor(model: string) {
12
+ super();
13
+
14
+ const isDemo = process.env.KALSHI_USE_DEMO === 'true';
15
+ const welcomeText = isDemo ? 'Kalshi Trading Bot CLI [DEMO MODE]' : 'Kalshi Trading Bot CLI';
16
+ const versionText = ` v${packageJson.version}`;
17
+ const fullText = welcomeText + versionText;
18
+ const padding = Math.max(0, Math.floor((INTRO_WIDTH - fullText.length - 2) / 2));
19
+ const trailing = Math.max(0, INTRO_WIDTH - fullText.length - padding - 2);
20
+
21
+ this.addChild(new Spacer(1));
22
+ this.addChild(new Text(theme.primary('═'.repeat(INTRO_WIDTH)), 0, 0));
23
+ this.addChild(
24
+ new Text(
25
+ theme.primary(
26
+ `║${' '.repeat(padding)}${theme.bold(welcomeText)}${theme.muted(versionText)}${' '.repeat(
27
+ trailing,
28
+ )}║`,
29
+ ),
30
+ 0,
31
+ 0,
32
+ ),
33
+ );
34
+ this.addChild(new Text(theme.primary('═'.repeat(INTRO_WIDTH)), 0, 0));
35
+ this.addChild(new Spacer(1));
36
+
37
+ this.addChild(
38
+ new Text(
39
+ theme.bold(
40
+ theme.primary(
41
+ `
42
+ ██████╗ ██████╗████████╗ █████╗ ██████╗ ██████╗ ███╗ ██╗
43
+ ██╔═══██╗██╔════╝╚══██╔══╝██╔══██╗██╔════╝ ██╔═══██╗████╗ ██║
44
+ ██║ ██║██║ ██║ ███████║██║ ███╗██║ ██║██╔██╗ ██║
45
+ ██║ ██║██║ ██║ ██╔══██║██║ ██║██║ ██║██║╚██╗██║
46
+ ╚██████╔╝╚██████╗ ██║ ██║ ██║╚██████╔╝╚██████╔╝██║ ╚████║
47
+ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝`,
48
+ ),
49
+ ),
50
+ 0,
51
+ 0,
52
+ ),
53
+ );
54
+
55
+ if (isDemo) {
56
+ this.addChild(new Spacer(1));
57
+ this.addChild(
58
+ new Text(
59
+ theme.warning(' ⚠ DEMO MODE — orders are simulated, no real money at risk ⚠'),
60
+ 0,
61
+ 0,
62
+ ),
63
+ );
64
+ }
65
+
66
+ this.addChild(new Spacer(1));
67
+ this.addChild(new Text('AI-powered prediction market terminal.', 0, 0));
68
+ this.addChild(new Spacer(1));
69
+ const cmd = (label: string) => theme.muted(label.padEnd(11));
70
+ this.addChild(new Text(cmd('/search') + 'Search events by theme, ticker, or free-text; /search edge for edge scan', 0, 0));
71
+ this.addChild(new Text(cmd('/portfolio') + 'Overview, positions, orders, balance, status', 0, 0));
72
+ this.addChild(new Text(cmd('/analyze') + '<ticker> Full analysis: edge, research, Kelly sizing', 0, 0));
73
+ this.addChild(new Text(cmd('/watch') + '<ticker> Live price/orderbook feed', 0, 0));
74
+ this.addChild(new Text(cmd('/backtest') + 'Model accuracy scorecard + live edge scanner', 0, 0));
75
+ this.addChild(new Text(cmd('/buy /sell') + '<ticker> <n> [price] /cancel <order_id>', 0, 0));
76
+ this.addChild(new Text(cmd('/help') + '[command] Show help (/help <command> for details)', 0, 0));
77
+ this.addChild(new Text(cmd('/quit') + 'Quit CLI session', 0, 0));
78
+ this.addChild(new Spacer(1));
79
+ this.addChild(new Text(theme.muted('Ask anything: ') + '"analyze KXBTC" "search crypto" "show my portfolio"', 0, 0));
80
+ this.modelText = new Text('', 0, 0);
81
+ this.addChild(this.modelText);
82
+ this.setModel(model);
83
+ }
84
+
85
+ setModel(model: string) {
86
+ this.modelText.setText(
87
+ `${theme.muted('Model: ')}${theme.primary(getModelDisplayName(model))}${theme.muted(
88
+ '. Type /model to change.',
89
+ )}`,
90
+ );
91
+ }
92
+ }