kalshi-trading-bot-cli 2.1.3 → 2.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/README.md +214 -5
- package/data/themes_seo.json +159 -0
- package/env.example +5 -2
- package/package.json +1 -1
- package/src/backtest/renderer.ts +0 -2
- package/src/cli.ts +89 -0
- package/src/commands/analyze-batch.ts +101 -0
- package/src/commands/analyze.ts +99 -17
- package/src/commands/basket.ts +653 -0
- package/src/commands/catalysts.ts +121 -0
- package/src/commands/clusters.ts +153 -0
- package/src/commands/correlate.ts +112 -0
- package/src/commands/dispatch.ts +306 -7
- package/src/commands/editorial-themes.ts +494 -0
- package/src/commands/events.ts +140 -0
- package/src/commands/help.ts +343 -19
- package/src/commands/index.ts +137 -6
- package/src/commands/parse-args.ts +213 -1
- package/src/commands/peers.ts +87 -0
- package/src/commands/search-remote.ts +90 -0
- package/src/commands/series.ts +386 -0
- package/src/commands/similar.ts +97 -0
- package/src/components/intro.ts +9 -0
- package/src/db/editorial-themes.ts +111 -0
- package/src/db/event-index.ts +109 -31
- package/src/db/octagon-cache.ts +2 -1
- package/src/db/schema.ts +23 -0
- package/src/gateway/commands/handler.ts +6 -2
- package/src/scan/octagon-events-api.ts +55 -0
- package/src/scan/octagon-kalshi-api.ts +564 -0
- package/src/scan/octagon-prefetch.ts +48 -27
package/src/commands/dispatch.ts
CHANGED
|
@@ -26,6 +26,17 @@ import { scanEdges, formatEdgeScanHuman } from './search-edge.js';
|
|
|
26
26
|
import type { KalshiBalanceResponse } from './formatters.js';
|
|
27
27
|
import { ExitCode, exitCodeFromError } from '../utils/errors.js';
|
|
28
28
|
import { trackEvent } from '../utils/telemetry.js';
|
|
29
|
+
import { handleSimilar, formatSimilarHuman } from './similar.js';
|
|
30
|
+
import { handleClusters, formatClustersHuman } from './clusters.js';
|
|
31
|
+
import { handlePeers, formatPeersHuman } from './peers.js';
|
|
32
|
+
import { handleCorrelate, formatCorrelationHuman } from './correlate.js';
|
|
33
|
+
import { handleBasket, formatBasketHuman } from './basket.js';
|
|
34
|
+
import { searchKalshiMarkets, getMarketsWithEdge } from '../scan/octagon-kalshi-api.js';
|
|
35
|
+
import { formatMarketSearchHuman, formatMarketsWithEdgeHuman } from './search-remote.js';
|
|
36
|
+
import { handleEvents, formatEventsHuman } from './events.js';
|
|
37
|
+
import { handleSeries, formatSeriesHuman } from './series.js';
|
|
38
|
+
import { handleEditorialThemes, formatEditorialThemesHuman } from './editorial-themes.js';
|
|
39
|
+
import { handleCatalysts, formatCatalystsHuman } from './catalysts.js';
|
|
29
40
|
|
|
30
41
|
// ─── Alias resolution ────────────────────────────────────────────────────────
|
|
31
42
|
// Maps legacy CLI subcommands to canonical commands with mode/subview context
|
|
@@ -45,19 +56,96 @@ function resolveAlias(subcommand: Subcommand, positionalArgs: string[]): Resolve
|
|
|
45
56
|
case 'status':
|
|
46
57
|
return { canonical: 'portfolio', subview: 'status' };
|
|
47
58
|
|
|
48
|
-
// themes
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
// `themes` is now the editorial-themes registry (curated narrative buckets).
|
|
60
|
+
// Legacy "kalshi search themes" (Kalshi category labels) is still reachable
|
|
61
|
+
// via `search themes`.
|
|
62
|
+
|
|
63
|
+
// basket sub-routing (build/backtest/size/candles) — exposed for telemetry granularity
|
|
64
|
+
case 'basket': {
|
|
65
|
+
const sub = positionalArgs[0]?.toLowerCase();
|
|
66
|
+
if (sub === 'build' || sub === 'backtest' || sub === 'size' || sub === 'candles') {
|
|
67
|
+
return { canonical: 'basket', subview: sub };
|
|
68
|
+
}
|
|
69
|
+
return { canonical: 'basket' };
|
|
70
|
+
}
|
|
51
71
|
|
|
52
72
|
default:
|
|
53
73
|
return { canonical: subcommand };
|
|
54
74
|
}
|
|
55
75
|
}
|
|
56
76
|
|
|
77
|
+
function modeFlagsFor(canonical: Subcommand, args: ParsedArgs): Record<string, string | boolean> {
|
|
78
|
+
switch (canonical) {
|
|
79
|
+
case 'clusters':
|
|
80
|
+
return { behavioral: args.behavioral, ranked: args.ranked };
|
|
81
|
+
case 'peers':
|
|
82
|
+
return { behavioral: args.behavioral, show_cluster: args.showCluster };
|
|
83
|
+
case 'similar':
|
|
84
|
+
return { anchor: args.ticker ? 'ticker' : args.query ? 'query' : 'positional' };
|
|
85
|
+
case 'basket':
|
|
86
|
+
return { kelly_sizing: args.bankroll !== undefined };
|
|
87
|
+
case 'search':
|
|
88
|
+
return { remote: !!process.env.OCTAGON_API_KEY };
|
|
89
|
+
default:
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* One-time stderr hint when a user pipes `--json` through `bunx` without
|
|
96
|
+
* `--silent`. Bunx prints install chatter to stdout *before* our process even
|
|
97
|
+
* starts, which corrupts JSON pipelines — `--silent` fixes it entirely, but
|
|
98
|
+
* users rarely discover that flag on their own. We can't strip the chatter
|
|
99
|
+
* (it's not in our stdout), but we can nudge them once.
|
|
100
|
+
*
|
|
101
|
+
* Heuristic: --json + non-TTY stdout + BUN_INSTALL_CACHE_DIR set (bunx sets
|
|
102
|
+
* this; `bun add -g` installs don't). Silenced after first emit by touching
|
|
103
|
+
* a sentinel file under ~/.kalshi-bot/.
|
|
104
|
+
*/
|
|
105
|
+
async function maybeEmitBunxHint(args: ParsedArgs): Promise<void> {
|
|
106
|
+
if (!args.json) return;
|
|
107
|
+
if (process.stdout.isTTY) return;
|
|
108
|
+
if (!process.env.BUN_INSTALL_CACHE_DIR) return;
|
|
109
|
+
try {
|
|
110
|
+
// Dynamic ESM imports to avoid pulling these into the module graph at init.
|
|
111
|
+
const { appPath } = await import('../utils/paths.js');
|
|
112
|
+
const { existsSync, writeFileSync, mkdirSync } = await import('fs');
|
|
113
|
+
const sentinel = appPath('.bunx-hint-shown');
|
|
114
|
+
if (existsSync(sentinel)) return;
|
|
115
|
+
process.stderr.write(
|
|
116
|
+
'[kalshi] Tip: for clean JSON output and parallel-safe scripting, install once with\n' +
|
|
117
|
+
'[kalshi] bun add -g kalshi-trading-bot-cli\n' +
|
|
118
|
+
'[kalshi] then call `kalshi …` directly. Or use `bunx --silent` to suppress install\n' +
|
|
119
|
+
'[kalshi] chatter from this invocation. See README → Scripting & Parallel Use.\n',
|
|
120
|
+
);
|
|
121
|
+
const dir = appPath('.');
|
|
122
|
+
mkdirSync(dir, { recursive: true });
|
|
123
|
+
writeFileSync(sentinel, String(Date.now()));
|
|
124
|
+
} catch {
|
|
125
|
+
// Best-effort hint — never fail the actual command because of it.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
130
|
+
|
|
57
131
|
export async function dispatch(args: ParsedArgs): Promise<void> {
|
|
132
|
+
// --days-to-close N is ergonomic sugar over --close-before <iso>. Resolve
|
|
133
|
+
// it once here so every downstream command (search, events, series,
|
|
134
|
+
// catalysts, basket --theme, similar) gets the same filter without each
|
|
135
|
+
// handler reimplementing the arithmetic.
|
|
136
|
+
if (args.daysToClose !== undefined && !args.closeBefore) {
|
|
137
|
+
const target = new Date(Date.now() + args.daysToClose * MILLISECONDS_PER_DAY);
|
|
138
|
+
args.closeBefore = target.toISOString();
|
|
139
|
+
}
|
|
140
|
+
|
|
58
141
|
const { subcommand, json } = args;
|
|
59
142
|
const resolved = resolveAlias(subcommand, args.positionalArgs);
|
|
60
|
-
|
|
143
|
+
await maybeEmitBunxHint(args);
|
|
144
|
+
trackEvent('cli_command', {
|
|
145
|
+
command: resolved.canonical,
|
|
146
|
+
subview: resolved.subview ?? '',
|
|
147
|
+
...modeFlagsFor(resolved.canonical, args),
|
|
148
|
+
});
|
|
61
149
|
|
|
62
150
|
try {
|
|
63
151
|
// ─── reject invalid flags early (for all commands) ───────────────
|
|
@@ -87,8 +175,26 @@ export async function dispatch(args: ParsedArgs): Promise<void> {
|
|
|
87
175
|
return;
|
|
88
176
|
}
|
|
89
177
|
if (sub === 'edge') {
|
|
90
|
-
const db = (await import('../db/index.js')).getDb();
|
|
91
178
|
const minEdgePp = (args.minEdge ?? 0.05) * 100;
|
|
179
|
+
if (process.env.OCTAGON_API_KEY) {
|
|
180
|
+
// edge_pp_min is asymmetric (only filters lower bound). Skip when
|
|
181
|
+
// user passes --min-edge 0 so they see the full distribution.
|
|
182
|
+
const data = await getMarketsWithEdge({
|
|
183
|
+
category: args.category,
|
|
184
|
+
...(minEdgePp > 0 ? { edge_pp_min: minEdgePp } : {}),
|
|
185
|
+
sort_by: (args.sortBy as 'edge_pp' | 'expected_return' | 'total_volume' | 'model_probability' | undefined) ?? 'edge_pp',
|
|
186
|
+
limit: args.limit ?? 20,
|
|
187
|
+
});
|
|
188
|
+
if (json) {
|
|
189
|
+
console.log(JSON.stringify(wrapSuccess('search', data)));
|
|
190
|
+
} else {
|
|
191
|
+
console.log(formatMarketsWithEdgeHuman(data, minEdgePp));
|
|
192
|
+
}
|
|
193
|
+
process.exit(ExitCode.SUCCESS);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
// Local fallback: scan cached Octagon reports in SQLite
|
|
197
|
+
const db = (await import('../db/index.js')).getDb();
|
|
92
198
|
const result = scanEdges(db, { minEdgePp, limit: args.limit, category: args.category });
|
|
93
199
|
if (json) {
|
|
94
200
|
console.log(JSON.stringify(wrapSuccess('search', result)));
|
|
@@ -109,14 +215,60 @@ export async function dispatch(args: ParsedArgs): Promise<void> {
|
|
|
109
215
|
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
110
216
|
return;
|
|
111
217
|
}
|
|
112
|
-
|
|
218
|
+
const query = args.positionalArgs.join(' ');
|
|
219
|
+
|
|
220
|
+
// Octagon-powered server-side search: broader universe, full-text + structured filters.
|
|
221
|
+
if (process.env.OCTAGON_API_KEY) {
|
|
222
|
+
// --aggregate-by series → route to series rollup
|
|
223
|
+
if (args.aggregateBy === 'series') {
|
|
224
|
+
const { handleSeries, formatSeriesHuman } = await import('./series.js');
|
|
225
|
+
const seriesArgs = { ...args, positionalArgs: query ? ['search', query] : ['list'] };
|
|
226
|
+
const resp = await handleSeries(seriesArgs);
|
|
227
|
+
if (json) {
|
|
228
|
+
console.log(JSON.stringify(resp));
|
|
229
|
+
} else if (resp.ok) {
|
|
230
|
+
console.log(formatSeriesHuman(resp.data));
|
|
231
|
+
} else {
|
|
232
|
+
console.error(resp.error?.message ?? 'series rollup failed');
|
|
233
|
+
}
|
|
234
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// sort_by is now server-side (true top-N across the whole universe);
|
|
238
|
+
// series_prefix lets us tree-browse (KXBTC matches all Bitcoin series).
|
|
239
|
+
const serverSortBy = (args.sortBy === 'volume_24h' || args.sortBy === 'close_time' || args.sortBy === 'last_price')
|
|
240
|
+
? args.sortBy
|
|
241
|
+
: undefined;
|
|
242
|
+
const page = await searchKalshiMarkets({
|
|
243
|
+
q: query,
|
|
244
|
+
category: args.category,
|
|
245
|
+
series_ticker: args.seriesTicker,
|
|
246
|
+
series_prefix: args.seriesPrefix,
|
|
247
|
+
min_volume_24h: args.minVolume,
|
|
248
|
+
close_before: args.closeBefore,
|
|
249
|
+
sort_by: serverSortBy,
|
|
250
|
+
limit: args.limit ?? 30,
|
|
251
|
+
});
|
|
252
|
+
// --active-only is defensive — the live universe is active by default.
|
|
253
|
+
const rows = args.activeOnly
|
|
254
|
+
? page.data.filter((m) => m.status === 'active' || m.status === 'open')
|
|
255
|
+
: page.data;
|
|
256
|
+
const filteredPage = { ...page, data: rows };
|
|
257
|
+
if (json) {
|
|
258
|
+
console.log(JSON.stringify(wrapSuccess('search', filteredPage)));
|
|
259
|
+
} else {
|
|
260
|
+
console.log(formatMarketSearchHuman(query, filteredPage));
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Local fallback: query the pre-built event index.
|
|
113
266
|
if (args.refresh) {
|
|
114
267
|
await forceRefreshIndex();
|
|
115
268
|
} else {
|
|
116
269
|
await ensureIndex();
|
|
117
270
|
}
|
|
118
271
|
const db = (await import('../db/index.js')).getDb();
|
|
119
|
-
const query = args.positionalArgs.join(' ');
|
|
120
272
|
const results = searchEventIndex(db, query, 30);
|
|
121
273
|
if (json) {
|
|
122
274
|
console.log(JSON.stringify(wrapSuccess('search', { events: results })));
|
|
@@ -202,6 +354,27 @@ export async function dispatch(args: ParsedArgs): Promise<void> {
|
|
|
202
354
|
|
|
203
355
|
// ─── analyze ───────────────────────────────────────────────────────
|
|
204
356
|
if (resolved.canonical === 'analyze') {
|
|
357
|
+
// Batch mode: 2+ positional tickers OR --tickers csv. Routes through
|
|
358
|
+
// POST /kalshi/markets/edge in a single call (vs. N serial Octagon
|
|
359
|
+
// round-trips). Use --refresh on a single ticker for the full deep
|
|
360
|
+
// analysis pipeline.
|
|
361
|
+
const csvTickers = args.tickers
|
|
362
|
+
? args.tickers.split(',').map((s) => s.trim()).filter(Boolean)
|
|
363
|
+
: [];
|
|
364
|
+
const tickerList = [...args.positionalArgs, ...csvTickers];
|
|
365
|
+
if (tickerList.length > 1) {
|
|
366
|
+
const { handleAnalyzeBatch, formatAnalyzeBatchHuman } = await import('./analyze-batch.js');
|
|
367
|
+
const resp = await handleAnalyzeBatch(tickerList);
|
|
368
|
+
if (json) {
|
|
369
|
+
console.log(JSON.stringify(resp));
|
|
370
|
+
} else if (resp.ok) {
|
|
371
|
+
console.log(formatAnalyzeBatchHuman(resp.data));
|
|
372
|
+
} else {
|
|
373
|
+
console.error(resp.error?.message ?? 'analyze (batch) failed');
|
|
374
|
+
}
|
|
375
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
205
378
|
const ticker = args.positionalArgs[0];
|
|
206
379
|
if (!ticker) {
|
|
207
380
|
const errResp = wrapError('analyze', 'MISSING_TICKER', 'Usage: analyze <ticker> [--refresh] [--report]');
|
|
@@ -228,6 +401,132 @@ export async function dispatch(args: ParsedArgs): Promise<void> {
|
|
|
228
401
|
return;
|
|
229
402
|
}
|
|
230
403
|
|
|
404
|
+
// ─── similar (Octagon semantic search) ─────────────────────────────
|
|
405
|
+
if (resolved.canonical === 'similar') {
|
|
406
|
+
const resp = await handleSimilar(args);
|
|
407
|
+
if (json) {
|
|
408
|
+
console.log(JSON.stringify(resp));
|
|
409
|
+
} else if (resp.ok) {
|
|
410
|
+
console.log(formatSimilarHuman(resp.data));
|
|
411
|
+
} else {
|
|
412
|
+
console.error(resp.error?.message ?? 'similar failed');
|
|
413
|
+
}
|
|
414
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── clusters (Octagon thematic & behavioral) ──────────────────────
|
|
419
|
+
if (resolved.canonical === 'clusters') {
|
|
420
|
+
const resp = await handleClusters(args);
|
|
421
|
+
if (json) {
|
|
422
|
+
console.log(JSON.stringify(resp));
|
|
423
|
+
} else if (resp.ok) {
|
|
424
|
+
console.log(formatClustersHuman(resp.data));
|
|
425
|
+
} else {
|
|
426
|
+
console.error(resp.error?.message ?? 'clusters failed');
|
|
427
|
+
}
|
|
428
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ─── peers (Octagon cluster peers) ─────────────────────────────────
|
|
433
|
+
if (resolved.canonical === 'peers') {
|
|
434
|
+
const resp = await handlePeers(args);
|
|
435
|
+
if (json) {
|
|
436
|
+
console.log(JSON.stringify(resp));
|
|
437
|
+
} else if (resp.ok) {
|
|
438
|
+
console.log(formatPeersHuman(resp.data));
|
|
439
|
+
} else {
|
|
440
|
+
console.error(resp.error?.message ?? 'peers failed');
|
|
441
|
+
}
|
|
442
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ─── correlate (Octagon correlation matrix) ────────────────────────
|
|
447
|
+
if (resolved.canonical === 'correlate') {
|
|
448
|
+
const resp = await handleCorrelate(args);
|
|
449
|
+
if (json) {
|
|
450
|
+
console.log(JSON.stringify(resp));
|
|
451
|
+
} else if (resp.ok) {
|
|
452
|
+
console.log(formatCorrelationHuman(resp.data));
|
|
453
|
+
} else {
|
|
454
|
+
console.error(resp.error?.message ?? 'correlate failed');
|
|
455
|
+
}
|
|
456
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ─── catalysts (upcoming market closes grouped by week) ────────────
|
|
461
|
+
if (resolved.canonical === 'catalysts') {
|
|
462
|
+
const resp = await handleCatalysts(args);
|
|
463
|
+
if (json) {
|
|
464
|
+
console.log(JSON.stringify(resp));
|
|
465
|
+
} else if (resp.ok) {
|
|
466
|
+
console.log(formatCatalystsHuman(resp.data));
|
|
467
|
+
} else {
|
|
468
|
+
console.error(resp.error?.message ?? 'catalysts failed');
|
|
469
|
+
}
|
|
470
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ─── themes (editorial narrative registry) ─────────────────────────
|
|
475
|
+
if (resolved.canonical === 'themes') {
|
|
476
|
+
const resp = await handleEditorialThemes(args);
|
|
477
|
+
if (json) {
|
|
478
|
+
console.log(JSON.stringify(resp));
|
|
479
|
+
} else if (resp.ok) {
|
|
480
|
+
console.log(formatEditorialThemesHuman(resp.data));
|
|
481
|
+
} else {
|
|
482
|
+
console.error(resp.error?.message ?? 'themes failed');
|
|
483
|
+
}
|
|
484
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ─── series (Kalshi series rollup) ─────────────────────────────────
|
|
489
|
+
if (resolved.canonical === 'series') {
|
|
490
|
+
const resp = await handleSeries(args);
|
|
491
|
+
if (json) {
|
|
492
|
+
console.log(JSON.stringify(resp));
|
|
493
|
+
} else if (resp.ok) {
|
|
494
|
+
console.log(formatSeriesHuman(resp.data));
|
|
495
|
+
} else {
|
|
496
|
+
console.error(resp.error?.message ?? 'series failed');
|
|
497
|
+
}
|
|
498
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ─── events (Octagon events list / detail) ─────────────────────────
|
|
503
|
+
if (resolved.canonical === 'events') {
|
|
504
|
+
const resp = await handleEvents(args);
|
|
505
|
+
if (json) {
|
|
506
|
+
console.log(JSON.stringify(resp));
|
|
507
|
+
} else if (resp.ok) {
|
|
508
|
+
console.log(formatEventsHuman(resp.data));
|
|
509
|
+
} else {
|
|
510
|
+
console.error(resp.error?.message ?? 'events failed');
|
|
511
|
+
}
|
|
512
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ─── basket (build, backtest, size, candles) ───────────────────────
|
|
517
|
+
if (resolved.canonical === 'basket') {
|
|
518
|
+
const resp = await handleBasket(args);
|
|
519
|
+
if (json) {
|
|
520
|
+
console.log(JSON.stringify(resp));
|
|
521
|
+
} else if (resp.ok) {
|
|
522
|
+
console.log(formatBasketHuman(resp.data));
|
|
523
|
+
} else {
|
|
524
|
+
console.error(resp.error?.message ?? 'basket failed');
|
|
525
|
+
}
|
|
526
|
+
process.exit(resp.ok ? ExitCode.SUCCESS : ExitCode.USER_ERROR);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
231
530
|
// ─── watch ─────────────────────────────────────────────────────────
|
|
232
531
|
if (resolved.canonical === 'watch') {
|
|
233
532
|
// Force index rebuild before watching if --refresh is set
|