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,276 @@
1
+ import type { ParsedArgs } from './parse-args.js';
2
+ import type { CLIResponse } from './json.js';
3
+ import { wrapSuccess } from './json.js';
4
+ import { getDb } from '../db/index.js';
5
+ import { discoverSettledMarkets, discoverOpenMarkets, parallelMap } from '../backtest/discovery.js';
6
+ import { fetchAndCacheHistory, selectSnapshotByDate, SubscriptionRequiredError, type OutcomeProbability } from '../backtest/fetcher.js';
7
+ import { computeMetrics } from '../backtest/metrics.js';
8
+ import type { BacktestResult, ScoredSignal } from '../backtest/types.js';
9
+ import { formatBacktestHuman, exportCSV, type FormatOpts } from '../backtest/renderer.js';
10
+
11
+ /** Look up the per-contract outcome entry from outcome_probabilities array. */
12
+ function findOutcomeProb(
13
+ outcomes: OutcomeProbability[] | null | undefined,
14
+ marketTicker: string,
15
+ ): OutcomeProbability | null {
16
+ if (!outcomes || !Array.isArray(outcomes)) return null;
17
+ const match = outcomes.find(
18
+ o => o.market_ticker.toUpperCase() === marketTicker.toUpperCase(),
19
+ );
20
+ return match ?? null;
21
+ }
22
+
23
+ /** Absolute-edge bucket label matching the Supabase-methodology buckets. */
24
+ function edgeBucketLabel(edgePp: number): string {
25
+ const abs = Math.abs(edgePp);
26
+ if (abs < 5) return '0-5%';
27
+ if (abs < 10) return '5-10%';
28
+ if (abs < 20) return '10-20%';
29
+ if (abs < 30) return '20-30%';
30
+ if (abs < 40) return '30-40%';
31
+ if (abs < 50) return '40-50%';
32
+ if (abs < 60) return '50-60%';
33
+ if (abs < 70) return '60-70%';
34
+ if (abs < 80) return '70-80%';
35
+ if (abs < 90) return '80-90%';
36
+ return '90%+';
37
+ }
38
+
39
+ /**
40
+ * Return the tradeable volume for a contract.
41
+ * Prefers per-contract volume fields from the Octagon snapshot (as the
42
+ * Supabase methodology does); falls back to Kalshi lifetime volume for
43
+ * older cached snapshots that pre-date the API's per-contract volume.
44
+ */
45
+ function contractVolume(
46
+ perContract: OutcomeProbability | null,
47
+ fallbackLifetimeVolume: number,
48
+ ): number {
49
+ if (perContract) {
50
+ const v = typeof perContract.volume === 'number' ? perContract.volume : null;
51
+ const v24 = typeof perContract.volume_24h === 'number' ? perContract.volume_24h : null;
52
+ if (v !== null || v24 !== null) return Math.max(v ?? 0, v24 ?? 0);
53
+ }
54
+ return fallbackLifetimeVolume;
55
+ }
56
+
57
+ export { formatBacktestHuman };
58
+ export type { FormatOpts };
59
+
60
+ export async function handleBacktest(args: ParsedArgs): Promise<CLIResponse<BacktestResult>> {
61
+ const db = getDb();
62
+ const days = args.days ?? 15;
63
+ const maxAgeDays = args.maxAge ?? days;
64
+ // Default 0.5pp matches the Supabase reference methodology — enough to
65
+ // skip near-zero-edge noise without excluding the 0-5% bucket.
66
+ const minEdge = args.minEdge ?? 0.005;
67
+ const minEdgePp = minEdge * 100;
68
+ const minVolume = args.minVolume ?? 1;
69
+ const minPrice = args.minPrice ?? 5; // 0-100 scale
70
+ const maxPrice = args.maxPrice ?? 95; // 0-100 scale
71
+ const now = new Date();
72
+ const lookbackDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
73
+ const minPredictionDate = new Date(lookbackDate.getTime() - maxAgeDays * 24 * 60 * 60 * 1000);
74
+
75
+ const signals: ScoredSignal[] = [];
76
+ let subscriptionNotice: string | undefined;
77
+
78
+ // ─── RESOLVED: settled markets with historical Octagon snapshots ────────
79
+ if (!args.unresolved) {
80
+ try {
81
+ const settled = await discoverSettledMarkets(db, { category: args.category });
82
+
83
+ if (settled.length > 0) {
84
+ // Group by event_ticker to batch history fetches
85
+ const byEvent = new Map<string, typeof settled>();
86
+ for (const m of settled) {
87
+ const arr = byEvent.get(m.event_ticker) ?? [];
88
+ arr.push(m);
89
+ byEvent.set(m.event_ticker, arr);
90
+ }
91
+
92
+ // Fetch & score events concurrently — Octagon history is I/O-bound and
93
+ // processing them serially made cold-cache backtests take 15+ min.
94
+ const perEvent = await parallelMap([...byEvent.entries()], async ([eventTicker, markets]) => {
95
+ let snapshots;
96
+ try {
97
+ snapshots = await fetchAndCacheHistory(db, eventTicker, { maxAgeDays });
98
+ } catch (err) {
99
+ if (err instanceof SubscriptionRequiredError) throw err;
100
+ return [] as ScoredSignal[];
101
+ }
102
+
103
+ // Find the snapshot closest to N days ago, rejecting snapshots
104
+ // older than the prediction-age window so we don't score stale
105
+ // model outputs as if they were recent.
106
+ const snap = selectSnapshotByDate(snapshots, lookbackDate, minPredictionDate);
107
+ if (!snap) return [];
108
+
109
+ const out: ScoredSignal[] = [];
110
+ for (const m of markets) {
111
+ // Strict per-contract extraction — no event-level fallback.
112
+ const perMarket = findOutcomeProb(snap.outcome_probabilities, m.ticker);
113
+ if (!perMarket) continue;
114
+ const modelProb = perMarket.model_probability;
115
+ const marketThen = perMarket.market_probability;
116
+ if (!Number.isFinite(modelProb) || !Number.isFinite(marketThen)) continue;
117
+ const marketNow = m.result === 'yes' ? 100 : 0;
118
+ const edgePp = Math.round((modelProb - marketThen) * 10) / 10;
119
+
120
+ // Tradeable filter — per-contract volume from the Octagon snapshot
121
+ // (matches Supabase methodology); falls back to Kalshi lifetime
122
+ // volume for pre-API-change cached snapshots.
123
+ const vol = contractVolume(perMarket, m.volume);
124
+ if (vol < minVolume) continue;
125
+ // Price is marketThen (the price you'd transact at for a resolved bet).
126
+ if (marketThen < minPrice || marketThen > maxPrice) continue;
127
+
128
+ // P&L and capital per $1 face value.
129
+ let pnl = 0;
130
+ let capital = 0;
131
+ if (edgePp > 0) {
132
+ // Buy YES at marketThen, settles at marketNow
133
+ pnl = (marketNow - marketThen) / 100;
134
+ capital = marketThen / 100;
135
+ } else if (edgePp < 0) {
136
+ // Buy NO at (100 - marketThen), settles at (100 - marketNow)
137
+ pnl = (marketThen - marketNow) / 100;
138
+ capital = (100 - marketThen) / 100;
139
+ } else {
140
+ // Zero edge: capital still reflects the tradeable side implied by sign
141
+ // (use YES side so divide-by-zero checks don't fire on 0-edge signals).
142
+ capital = marketThen / 100;
143
+ }
144
+ if (capital <= 0) continue;
145
+
146
+ out.push({
147
+ event_ticker: m.event_ticker,
148
+ market_ticker: m.ticker,
149
+ series_category: m.series_category,
150
+ model_prob: modelProb,
151
+ market_then: marketThen,
152
+ market_now: marketNow,
153
+ resolved: true,
154
+ edge_pp: edgePp,
155
+ pnl: Math.round(pnl * 10000) / 10000,
156
+ capital: Math.round(capital * 10000) / 10000,
157
+ edge_bucket: edgeBucketLabel(edgePp),
158
+ confidence_score: snap.confidence_score ?? 0,
159
+ close_time: m.close_time,
160
+ });
161
+ }
162
+ return out;
163
+ }, 10);
164
+ for (const arr of perEvent) signals.push(...arr);
165
+ }
166
+ } catch (err) {
167
+ if (err instanceof SubscriptionRequiredError) {
168
+ subscriptionNotice = err.message;
169
+ } else {
170
+ throw err;
171
+ }
172
+ }
173
+ }
174
+
175
+ // ─── UNRESOLVED: open markets with current Kalshi prices ───────────────
176
+ if (!args.resolved) {
177
+ try {
178
+ const openMarkets = await discoverOpenMarkets(db, { category: args.category });
179
+
180
+ // Group by event_ticker to batch history fetches (same as resolved path).
181
+ const openByEvent = new Map<string, typeof openMarkets>();
182
+ for (const m of openMarkets) {
183
+ const arr = openByEvent.get(m.event_ticker) ?? [];
184
+ arr.push(m);
185
+ openByEvent.set(m.event_ticker, arr);
186
+ }
187
+
188
+ const perEvent = await parallelMap([...openByEvent.entries()], async ([eventTicker, markets]) => {
189
+ let snapshots;
190
+ try {
191
+ snapshots = await fetchAndCacheHistory(db, eventTicker, { maxAgeDays });
192
+ } catch (err) {
193
+ if (err instanceof SubscriptionRequiredError) throw err;
194
+ return [] as ScoredSignal[];
195
+ }
196
+ const snap = selectSnapshotByDate(snapshots, lookbackDate, minPredictionDate);
197
+ if (!snap) return [];
198
+
199
+ const out: ScoredSignal[] = [];
200
+ for (const m of markets) {
201
+ // Strict per-contract extraction — no event-level fallback.
202
+ const perMarket = findOutcomeProb(snap.outcome_probabilities, m.ticker);
203
+ if (!perMarket) continue;
204
+ const modelProb = perMarket.model_probability;
205
+ const marketThen = perMarket.market_probability;
206
+ if (!Number.isFinite(modelProb) || !Number.isFinite(marketThen)) continue;
207
+ const confidenceScore = snap.confidence_score ?? 0;
208
+
209
+ const marketNow = m.market_prob * 100; // current Kalshi price (0-100)
210
+ const edgePp = Math.round((modelProb - marketThen) * 10) / 10;
211
+
212
+ // Tradeable filter — per-contract volume from the Octagon snapshot.
213
+ const vol = contractVolume(perMarket, m.volume);
214
+ if (vol < minVolume) continue;
215
+ // Price is marketNow (the current transactable price for an open position).
216
+ if (marketNow < minPrice || marketNow > maxPrice) continue;
217
+
218
+ // M2M P&L and capital per $1 face value.
219
+ let pnl = 0;
220
+ let capital = 0;
221
+ if (edgePp > 0) {
222
+ pnl = (marketNow - marketThen) / 100;
223
+ capital = marketThen / 100;
224
+ } else if (edgePp < 0) {
225
+ pnl = (marketThen - marketNow) / 100;
226
+ capital = (100 - marketThen) / 100;
227
+ } else {
228
+ capital = marketThen / 100;
229
+ }
230
+ if (capital <= 0) continue;
231
+
232
+ out.push({
233
+ event_ticker: m.event_ticker,
234
+ market_ticker: m.ticker,
235
+ series_category: m.series_category,
236
+ model_prob: modelProb,
237
+ market_then: marketThen,
238
+ market_now: marketNow,
239
+ resolved: false,
240
+ edge_pp: edgePp,
241
+ pnl: Math.round(pnl * 10000) / 10000,
242
+ capital: Math.round(capital * 10000) / 10000,
243
+ edge_bucket: edgeBucketLabel(edgePp),
244
+ confidence_score: confidenceScore,
245
+ close_time: m.close_time,
246
+ });
247
+ }
248
+ return out;
249
+ }, 10);
250
+ for (const arr of perEvent) signals.push(...arr);
251
+ } catch (err) {
252
+ // Mirror the resolved block: a subscription wall hit while scoring open
253
+ // markets becomes a notice rather than crashing out before CSV export.
254
+ if (err instanceof SubscriptionRequiredError) {
255
+ subscriptionNotice = subscriptionNotice ?? err.message;
256
+ } else {
257
+ throw err;
258
+ }
259
+ }
260
+ }
261
+
262
+ // ─── COMPUTE METRICS ───────────────────────────────────────────────────
263
+ const metrics = computeMetrics(signals, minEdgePp);
264
+
265
+ const result: BacktestResult = {
266
+ ...metrics,
267
+ days,
268
+ subscription_notice: subscriptionNotice,
269
+ };
270
+
271
+ if (args.exportPath) {
272
+ exportCSV(result, args.exportPath);
273
+ }
274
+
275
+ return wrapSuccess('backtest', result);
276
+ }
@@ -0,0 +1,24 @@
1
+ import { existsSync, unlinkSync } from 'fs';
2
+ import { closeDb } from '../db/index.js';
3
+ import { appPath } from '../utils/paths.js';
4
+
5
+ const DB_PATH = appPath('kalshi-bot.db');
6
+
7
+ export function handleClearCache(): { deleted: boolean; path: string; message: string } {
8
+ if (!existsSync(DB_PATH)) {
9
+ return { deleted: false, path: DB_PATH, message: `No cache file found at ${DB_PATH}` };
10
+ }
11
+
12
+ // Close the singleton if open so file descriptors are released
13
+ closeDb();
14
+
15
+ // Remove WAL/SHM files if present
16
+ for (const suffix of ['', '-wal', '-shm']) {
17
+ const file = DB_PATH + suffix;
18
+ if (existsSync(file)) {
19
+ unlinkSync(file);
20
+ }
21
+ }
22
+
23
+ return { deleted: true, path: DB_PATH, message: `Cache cleared: ${DB_PATH}\nA fresh database will be created on next command.` };
24
+ }
@@ -0,0 +1,107 @@
1
+ import type { ParsedArgs } from './parse-args.js';
2
+ import type { CLIResponse } from './json.js';
3
+ import { wrapSuccess, wrapError } from './json.js';
4
+ import { formatTable } from './scan-formatters.js';
5
+ import { auditTrail } from '../audit/index.js';
6
+ import {
7
+ getAllSettings,
8
+ getBotSetting,
9
+ setBotSetting,
10
+ type FlatSetting,
11
+ } from '../utils/bot-config.js';
12
+
13
+ export interface ConfigEntry {
14
+ key: string;
15
+ value: unknown;
16
+ default: unknown;
17
+ isDefault: boolean;
18
+ }
19
+
20
+ export interface ConfigListData {
21
+ mode: 'list';
22
+ entries: ConfigEntry[];
23
+ }
24
+
25
+ export interface ConfigGetData {
26
+ mode: 'get';
27
+ entry: ConfigEntry;
28
+ }
29
+
30
+ export interface ConfigSetData {
31
+ mode: 'set';
32
+ key: string;
33
+ oldValue: unknown;
34
+ newValue: unknown;
35
+ }
36
+
37
+ export type ConfigData = ConfigListData | ConfigGetData | ConfigSetData;
38
+
39
+ function toEntry(s: FlatSetting): ConfigEntry {
40
+ return { key: s.key, value: s.value, default: s.default, isDefault: s.isDefault };
41
+ }
42
+
43
+ export async function handleConfig(args: ParsedArgs): Promise<CLIResponse<ConfigData>> {
44
+ const positional = args.positionalArgs;
45
+
46
+ // 0 args → list all
47
+ if (positional.length === 0) {
48
+ const entries = getAllSettings().map(toEntry);
49
+ return wrapSuccess('config', { mode: 'list', entries } as ConfigData);
50
+ }
51
+
52
+ const key = positional[0];
53
+
54
+ // 1 arg → get
55
+ if (positional.length === 1) {
56
+ const value = getBotSetting(key);
57
+ if (value === undefined) {
58
+ return wrapError('config', 'UNKNOWN_KEY', `Unknown config key: ${key}`) as CLIResponse<ConfigData>;
59
+ }
60
+ const all = getAllSettings();
61
+ const match = all.find((s) => s.key === key);
62
+ const entry: ConfigEntry = match
63
+ ? toEntry(match)
64
+ : { key, value, default: value, isDefault: true };
65
+ return wrapSuccess('config', { mode: 'get', entry } as ConfigData);
66
+ }
67
+
68
+ // 2 args → set
69
+ const rawValue = positional[1];
70
+ try {
71
+ const { oldValue, newValue } = setBotSetting(key, rawValue);
72
+
73
+ auditTrail.log({
74
+ type: 'CONFIG_SET',
75
+ key,
76
+ old_value: JSON.stringify(oldValue),
77
+ new_value: JSON.stringify(newValue),
78
+ });
79
+
80
+ return wrapSuccess('config', { mode: 'set', key, oldValue, newValue } as ConfigData);
81
+ } catch (err) {
82
+ const msg = err instanceof Error ? err.message : String(err);
83
+ return wrapError('config', 'INVALID_CONFIG', msg) as CLIResponse<ConfigData>;
84
+ }
85
+ }
86
+
87
+ function formatValue(v: unknown): string {
88
+ if (typeof v === 'string') return v;
89
+ return JSON.stringify(v);
90
+ }
91
+
92
+ export function formatConfigHuman(data: ConfigData): string {
93
+ switch (data.mode) {
94
+ case 'list': {
95
+ const rows = data.entries.map((e) => [
96
+ e.key,
97
+ formatValue(e.value),
98
+ formatValue(e.default),
99
+ ]);
100
+ return formatTable(['Key', 'Value', 'Default'], rows);
101
+ }
102
+ case 'get':
103
+ return `${data.entry.key} = ${formatValue(data.entry.value)}`;
104
+ case 'set':
105
+ return `${data.key}: ${formatValue(data.oldValue)} → ${formatValue(data.newValue)}`;
106
+ }
107
+ }