openbroker 1.10.0 → 1.12.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 (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +52 -0
  3. package/SKILL.md +45 -2
  4. package/bin/cli.ts +11 -0
  5. package/dist/auto/examples/grid.js +1 -1
  6. package/dist/auto/examples/mm-maker.js +4 -4
  7. package/dist/auto/examples/mm-spread.js +4 -4
  8. package/dist/core/client.d.ts +23 -26
  9. package/dist/core/client.d.ts.map +1 -1
  10. package/dist/core/client.js +24 -3
  11. package/dist/core/types.d.ts +22 -0
  12. package/dist/core/types.d.ts.map +1 -1
  13. package/dist/core/ws.d.ts +5 -0
  14. package/dist/core/ws.d.ts.map +1 -1
  15. package/dist/core/ws.js +9 -7
  16. package/dist/guardian/cli.d.ts +2 -0
  17. package/dist/guardian/cli.d.ts.map +1 -0
  18. package/dist/guardian/cli.js +288 -0
  19. package/dist/guardian/engine.d.ts +107 -0
  20. package/dist/guardian/engine.d.ts.map +1 -0
  21. package/dist/guardian/engine.js +526 -0
  22. package/dist/guardian/rules.d.ts +35 -0
  23. package/dist/guardian/rules.d.ts.map +1 -0
  24. package/dist/guardian/rules.js +237 -0
  25. package/dist/guardian/telegram.d.ts +24 -0
  26. package/dist/guardian/telegram.d.ts.map +1 -0
  27. package/dist/guardian/telegram.js +125 -0
  28. package/dist/guardian/types.d.ts +90 -0
  29. package/dist/guardian/types.d.ts.map +1 -0
  30. package/dist/guardian/types.js +45 -0
  31. package/dist/lib.d.ts +8 -0
  32. package/dist/lib.d.ts.map +1 -1
  33. package/dist/lib.js +5 -0
  34. package/dist/operations/cancel.js +4 -1
  35. package/dist/operations/chase.d.ts +3 -1
  36. package/dist/operations/chase.d.ts.map +1 -1
  37. package/dist/operations/chase.js +4 -2
  38. package/package.json +2 -2
  39. package/scripts/auto/examples/grid.ts +1 -1
  40. package/scripts/auto/examples/mm-maker.ts +4 -4
  41. package/scripts/auto/examples/mm-spread.ts +4 -4
  42. package/scripts/core/client.ts +29 -3
  43. package/scripts/core/types.ts +23 -0
  44. package/scripts/core/ws.ts +9 -6
  45. package/scripts/guardian/cli.ts +322 -0
  46. package/scripts/guardian/engine.ts +614 -0
  47. package/scripts/guardian/rules.ts +265 -0
  48. package/scripts/guardian/telegram.ts +147 -0
  49. package/scripts/guardian/types.ts +160 -0
  50. package/scripts/lib.ts +33 -0
  51. package/scripts/operations/cancel.ts +4 -1
  52. package/scripts/operations/chase.ts +5 -3
@@ -144,7 +144,7 @@ export default function grid(api: AutomationAPI) {
144
144
  api.log.info('Cancelling grid orders...');
145
145
  for (const level of levels) {
146
146
  if (level.oid) {
147
- try { await api.client.cancel(COIN, level.oid); } catch { /* may be filled */ }
147
+ try { await api.client.cancel(COIN, level.oid, { fast: true }); } catch { /* may be filled */ }
148
148
  }
149
149
  }
150
150
  api.log.info(`Grid stopped. Realized PnL: ${api.utils.formatUsd(realizedPnl)}`);
@@ -98,14 +98,14 @@ export default function mmMaker(api: AutomationAPI) {
98
98
  if (bidOid) {
99
99
  const drift = Math.abs(bidPrice - safeBid) / book.midPrice;
100
100
  if (drift > 0.0005 || !shouldBid) {
101
- try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
101
+ try { await api.client.cancel(COIN, bidOid, { fast: true }); } catch { /* */ }
102
102
  bidOid = undefined;
103
103
  }
104
104
  }
105
105
  if (askOid) {
106
106
  const drift = Math.abs(askPrice - safeAsk) / book.midPrice;
107
107
  if (drift > 0.0005 || !shouldAsk) {
108
- try { await api.client.cancel(COIN, askOid); } catch { /* */ }
108
+ try { await api.client.cancel(COIN, askOid, { fast: true }); } catch { /* */ }
109
109
  askOid = undefined;
110
110
  }
111
111
  }
@@ -132,8 +132,8 @@ export default function mmMaker(api: AutomationAPI) {
132
132
  });
133
133
 
134
134
  api.onStop(async () => {
135
- if (bidOid) try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
136
- if (askOid) try { await api.client.cancel(COIN, askOid); } catch { /* */ }
135
+ if (bidOid) try { await api.client.cancel(COIN, bidOid, { fast: true }); } catch { /* */ }
136
+ if (askOid) try { await api.client.cancel(COIN, askOid, { fast: true }); } catch { /* */ }
137
137
  const pnl = totalSellRevenue - totalBuyCost;
138
138
  const volume = totalBuyCost + totalSellRevenue;
139
139
  const rebates = volume * 0.00003;
@@ -94,14 +94,14 @@ export default function mmSpread(api: AutomationAPI) {
94
94
  if (bidOid) {
95
95
  const drift = Math.abs(bidPrice - targetBid) / mid;
96
96
  if (drift > 0.001 || !shouldBid) {
97
- try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
97
+ try { await api.client.cancel(COIN, bidOid, { fast: true }); } catch { /* */ }
98
98
  bidOid = undefined;
99
99
  }
100
100
  }
101
101
  if (askOid) {
102
102
  const drift = Math.abs(askPrice - targetAsk) / mid;
103
103
  if (drift > 0.001 || !shouldAsk) {
104
- try { await api.client.cancel(COIN, askOid); } catch { /* */ }
104
+ try { await api.client.cancel(COIN, askOid, { fast: true }); } catch { /* */ }
105
105
  askOid = undefined;
106
106
  }
107
107
  }
@@ -124,8 +124,8 @@ export default function mmSpread(api: AutomationAPI) {
124
124
  });
125
125
 
126
126
  api.onStop(async () => {
127
- if (bidOid) try { await api.client.cancel(COIN, bidOid); } catch { /* */ }
128
- if (askOid) try { await api.client.cancel(COIN, askOid); } catch { /* */ }
127
+ if (bidOid) try { await api.client.cancel(COIN, bidOid, { fast: true }); } catch { /* */ }
128
+ if (askOid) try { await api.client.cancel(COIN, askOid, { fast: true }); } catch { /* */ }
129
129
  const pnl = totalSellRevenue - totalBuyCost;
130
130
  api.log.info(`MM stopped. Bought: ${totalBought.toFixed(6)} | Sold: ${totalSold.toFixed(6)} | PnL: ${api.utils.formatUsd(pnl)}`);
131
131
  });
@@ -14,6 +14,7 @@ import type {
14
14
  ClearinghouseState,
15
15
  MarginSummary,
16
16
  OpenOrder,
17
+ FrontendOpenOrder,
17
18
  OutcomeMetaResponse,
18
19
  OutcomeMarket,
19
20
  OutcomeQuestion,
@@ -2350,6 +2351,22 @@ export class HyperliquidClient {
2350
2351
  return orders;
2351
2352
  }
2352
2353
 
2354
+ /**
2355
+ * Open orders with frontend display fields (isTrigger, reduceOnly,
2356
+ * isPositionTpsl, …) for one dex — empty/omitted dex = main. Unlike
2357
+ * getOpenOrders this does NOT aggregate HIP-3 dexes; callers that need a
2358
+ * HIP-3 book pass its dex name explicitly.
2359
+ */
2360
+ async getFrontendOpenOrders(user?: string, dex?: string): Promise<FrontendOpenOrder[]> {
2361
+ const target = (user ?? this.address) as `0x${string}`;
2362
+ this.log('Fetching frontendOpenOrders for:', target, dex ? `(dex: ${dex})` : '');
2363
+ const response = await this.withRetry(
2364
+ () => this.info.frontendOpenOrders(dex ? { user: target, dex } : { user: target }),
2365
+ 'frontendOpenOrders',
2366
+ );
2367
+ return response as unknown as FrontendOpenOrder[];
2368
+ }
2369
+
2353
2370
  // ============ Trading ============
2354
2371
 
2355
2372
  /**
@@ -2990,17 +3007,24 @@ export class HyperliquidClient {
2990
3007
  }
2991
3008
  }
2992
3009
 
2993
- async cancel(coin: string, oid: number): Promise<CancelResponse> {
3010
+ /**
3011
+ * `fast: true` sets the action's `f` flag so the mempool prioritizes the cancel
3012
+ * (post-2026-07 upgrade, only fast cancels get prioritization). The API REJECTS
3013
+ * fast cancels for trigger orders (TP/SL), so only pass it when the oid is known
3014
+ * to be a plain resting limit — e.g. a quoting loop cancelling its own quotes.
3015
+ */
3016
+ async cancel(coin: string, oid: number, opts?: { fast?: boolean }): Promise<CancelResponse> {
2994
3017
  await this.requireTrading();
2995
3018
  await this.getMetaAndAssetCtxs();
2996
3019
 
2997
3020
  const assetIndex = this.getAssetIndex(coin);
2998
3021
 
2999
- this.log(`Cancelling order: ${coin} (asset ${assetIndex}) oid ${oid}`);
3022
+ this.log(`Cancelling order: ${coin} (asset ${assetIndex}) oid ${oid}${opts?.fast ? ' (fast)' : ''}`);
3000
3023
 
3001
3024
  try {
3002
3025
  const response = await this.exchange.cancel({
3003
3026
  cancels: [{ a: assetIndex, o: oid }],
3027
+ ...(opts?.fast ? { f: true as const } : {}),
3004
3028
  }, this.vaultParam);
3005
3029
  this.log('Cancel response:', JSON.stringify(response, null, 2));
3006
3030
  return response as unknown as CancelResponse;
@@ -3016,14 +3040,16 @@ export class HyperliquidClient {
3016
3040
  /**
3017
3041
  * Cancel MANY resting orders in a SINGLE exchange request (one action-rate request for ≤40 cancels),
3018
3042
  * the counterpart to `bulkOrder`. `response.data.statuses[i]` aligns with `cancels[i]`.
3043
+ * `fast` applies to the whole action and is rejected for trigger orders — see cancel().
3019
3044
  */
3020
- async bulkCancel(cancels: Array<{ coin: string; oid: number }>): Promise<CancelResponse> {
3045
+ async bulkCancel(cancels: Array<{ coin: string; oid: number }>, opts?: { fast?: boolean }): Promise<CancelResponse> {
3021
3046
  await this.requireTrading();
3022
3047
  await this.getMetaAndAssetCtxs();
3023
3048
  if (cancels.length === 0) return { status: 'ok', response: { type: 'cancel', data: { statuses: [] } } } as unknown as CancelResponse;
3024
3049
  try {
3025
3050
  const response = await this.exchange.cancel({
3026
3051
  cancels: cancels.map((c) => ({ a: this.getAssetIndex(c.coin), o: c.oid })),
3052
+ ...(opts?.fast ? { f: true as const } : {}),
3027
3053
  }, this.vaultParam);
3028
3054
  this.log('Bulk cancel response:', JSON.stringify(response, null, 2));
3029
3055
  return response as unknown as CancelResponse;
@@ -241,6 +241,29 @@ export interface OpenOrder {
241
241
  timestamp: number;
242
242
  }
243
243
 
244
+ /**
245
+ * Open order from the `frontendOpenOrders` info endpoint — same as OpenOrder
246
+ * plus the trigger/reduce-only display fields (needed to distinguish resting
247
+ * TP/SL protection from plain limit orders).
248
+ */
249
+ export interface FrontendOpenOrder {
250
+ coin: string;
251
+ side: 'B' | 'A';
252
+ limitPx: string;
253
+ sz: string;
254
+ oid: number;
255
+ timestamp: number;
256
+ origSz: string;
257
+ triggerCondition: string;
258
+ isTrigger: boolean;
259
+ triggerPx: string;
260
+ isPositionTpsl: boolean;
261
+ reduceOnly: boolean;
262
+ orderType: string;
263
+ tif: string | null;
264
+ cloid: string | null;
265
+ }
266
+
244
267
  // ============ API Request/Response ============
245
268
 
246
269
  export interface InfoRequest {
@@ -251,12 +251,10 @@ export class WebSocketManager {
251
251
  }
252
252
 
253
253
  private trackSub(sub: ISubscription): ISubscription {
254
+ // SDK ≥0.33 dropped ISubscription.failureSignal, so a sub that dies after a
255
+ // failed resubscribe stays in this list; close() swallows unsubscribe errors,
256
+ // so a dead handle there is harmless.
254
257
  this.subscriptions.push(sub);
255
- sub.failureSignal.addEventListener('abort', () => {
256
- this.log('Subscription failed, removing from tracked list');
257
- const idx = this.subscriptions.indexOf(sub);
258
- if (idx >= 0) this.subscriptions.splice(idx, 1);
259
- });
260
258
  return sub;
261
259
  }
262
260
 
@@ -275,10 +273,15 @@ export class WebSocketManager {
275
273
 
276
274
  /**
277
275
  * Subscribe to L2 order book snapshots for a specific coin.
276
+ *
277
+ * `fast: true` = 5 levels every 0.5s; without it the API degrades to 20 levels
278
+ * every 5s (per the 2026-07 network-upgrade announcement). Every consumer here
279
+ * reads top-of-book only (chase / mm quoting / mid fallback), so fast wins;
280
+ * anything needing depth should use the REST l2Book, which stays full-depth.
278
281
  */
279
282
  async subscribeL2Book(coin: string): Promise<ISubscription> {
280
283
  const client = this.ensureClient();
281
- const sub = await client.l2Book({ coin }, (data: L2BookWsEvent) => {
284
+ const sub = await client.l2Book({ coin, fast: true }, (data: L2BookWsEvent) => {
282
285
  this.emit('l2Book', {
283
286
  coin: data.coin,
284
287
  time: data.time,
@@ -0,0 +1,322 @@
1
+ // CLI entry point for `openbroker guardian` — read-only position risk
2
+ // monitoring with Telegram / agent-hook delivery. The CLI counterpart of the
3
+ // hosted guardian at /markets/guardian.
4
+
5
+ import { parseArgs } from '../core/utils.js';
6
+ import { getClient } from '../core/client.js';
7
+ import { getConfigPath } from '../core/config.js';
8
+ import { startGuardian, type GuardianOptions } from './engine.js';
9
+ import {
10
+ generateLinkCode,
11
+ getTelegramBotInfo,
12
+ loadTelegramSettings,
13
+ saveEnvVar,
14
+ sendTelegramMessage,
15
+ waitForTelegramLink,
16
+ } from './telegram.js';
17
+ import {
18
+ GUARDIAN_RULE_IDS,
19
+ GUARDIAN_RULE_LABELS,
20
+ type GuardianPrefs,
21
+ type GuardianRuleId,
22
+ type GuardianSeverity,
23
+ type GuardianThresholds,
24
+ } from './types.js';
25
+
26
+ function printUsage() {
27
+ console.log(`
28
+ OpenBroker Guardian — read-only position risk monitoring
29
+
30
+ Watches Hyperliquid addresses for liquidation risk, missing TP/SL, funding
31
+ bleed and more. Never touches your keys or places orders. Alerts go to the
32
+ console and (once connected) Telegram; if an OpenClaw agent gateway is
33
+ configured, alerts also wake the agent.
34
+
35
+ Usage:
36
+ openbroker guardian run [options] Start watching (long-running)
37
+ openbroker guardian connect Link a Telegram chat for alerts
38
+ openbroker guardian test Send a test Telegram message
39
+ openbroker guardian status Show guardian configuration
40
+ openbroker guardian rules List alert rules
41
+
42
+ Options (for run):
43
+ --address <0x..[,0x..]> Address(es) to watch (default: configured account)
44
+ --min-severity <level> info | warning | critical (default: info)
45
+ --disable <rules> CSV of rules to turn off (see: guardian rules)
46
+ --only <rules> CSV of rules to run exclusively
47
+ --poll <ms> Position poll interval (default: 30000; 15000 near liquidation)
48
+ --orders-poll <ms> Open-orders poll interval (default: 120000)
49
+ --liq-warn <pct> Liq-distance warning threshold (default: 10)
50
+ --liq-critical <pct> Liq-distance critical threshold (default: 5)
51
+ --margin-pct <pct> Margin-usage warning threshold (default: 80)
52
+ --funding-apr <pct> Funding-bleed APR threshold (default: 15)
53
+ --tpsl-minutes <min> Minutes unprotected before no-TP/SL alert (default: 15)
54
+ --stale-hours <h> Hours resting before stale-order alert (default: 12)
55
+ --no-telegram Don't deliver to Telegram even if linked
56
+ --no-ws Disable the WebSocket liquidation fast lane
57
+ --json Print alerts as JSON lines (agent-friendly)
58
+ --verbose Show debug output
59
+
60
+ Telegram setup (one-time):
61
+ 1. Message @BotFather on Telegram, send /newbot, and copy the bot token
62
+ 2. Put it in your config: TELEGRAM_BOT_TOKEN=123456:ABC-...
63
+ 3. Run: openbroker guardian connect (then tap the printed link)
64
+
65
+ Examples:
66
+ openbroker guardian run
67
+ openbroker guardian run --address 0xabc...,0xdef... --min-severity warning
68
+ openbroker guardian run --disable position_lifecycle,stale_order
69
+ openbroker guardian run --json --no-telegram
70
+ `);
71
+ }
72
+
73
+ const SEVERITIES: GuardianSeverity[] = ['info', 'warning', 'critical'];
74
+
75
+ function parseRuleCsv(raw: string, flag: string): GuardianRuleId[] {
76
+ const rules = raw.split(',').map((r) => r.trim()).filter(Boolean);
77
+ for (const rule of rules) {
78
+ if (!GUARDIAN_RULE_IDS.includes(rule as GuardianRuleId)) {
79
+ console.error(`Error: unknown rule '${rule}' in ${flag}. Valid rules: ${GUARDIAN_RULE_IDS.join(', ')}`);
80
+ process.exit(1);
81
+ }
82
+ }
83
+ return rules as GuardianRuleId[];
84
+ }
85
+
86
+ function parseNum(args: Record<string, string | boolean>, key: string): number | undefined {
87
+ const raw = args[key];
88
+ if (raw === undefined || raw === true) return undefined;
89
+ const n = Number(raw);
90
+ if (!Number.isFinite(n) || n <= 0) {
91
+ console.error(`Error: --${key} must be a positive number`);
92
+ process.exit(1);
93
+ }
94
+ return n;
95
+ }
96
+
97
+ async function runCommand(args: Record<string, string | boolean>) {
98
+ const addresses = typeof args.address === 'string'
99
+ ? args.address.split(',').map((a) => a.trim()).filter(Boolean)
100
+ : undefined;
101
+
102
+ const prefs: GuardianPrefs = {};
103
+ if (typeof args['min-severity'] === 'string') {
104
+ const sev = args['min-severity'] as GuardianSeverity;
105
+ if (!SEVERITIES.includes(sev)) {
106
+ console.error(`Error: --min-severity must be one of: ${SEVERITIES.join(', ')}`);
107
+ process.exit(1);
108
+ }
109
+ prefs.minSeverity = sev;
110
+ }
111
+ if (typeof args.disable === 'string') {
112
+ prefs.rules = Object.fromEntries(parseRuleCsv(args.disable, '--disable').map((r) => [r, false]));
113
+ }
114
+ if (typeof args.only === 'string') {
115
+ const only = new Set(parseRuleCsv(args.only, '--only'));
116
+ prefs.rules = Object.fromEntries(GUARDIAN_RULE_IDS.map((r) => [r, only.has(r)]));
117
+ }
118
+
119
+ const thresholds: Partial<GuardianThresholds> = {};
120
+ const liqWarn = parseNum(args, 'liq-warn');
121
+ const liqCritical = parseNum(args, 'liq-critical');
122
+ if (liqWarn !== undefined || liqCritical !== undefined) {
123
+ const warn = (liqWarn ?? 10) / 100;
124
+ const crit = (liqCritical ?? 5) / 100;
125
+ thresholds.liqThresholds = [
126
+ { pct: warn, severity: 'warning' },
127
+ { pct: crit, severity: 'critical' },
128
+ { pct: crit / 2.5, severity: 'critical' },
129
+ ];
130
+ }
131
+ const marginPct = parseNum(args, 'margin-pct');
132
+ if (marginPct !== undefined) {
133
+ thresholds.marginThresholdPct = marginPct;
134
+ thresholds.marginRearmPct = marginPct * 0.9;
135
+ }
136
+ const fundingApr = parseNum(args, 'funding-apr');
137
+ if (fundingApr !== undefined) thresholds.fundingBleedAprPct = fundingApr;
138
+ const tpslMinutes = parseNum(args, 'tpsl-minutes');
139
+ if (tpslMinutes !== undefined) thresholds.noTpslAfterMs = tpslMinutes * 60_000;
140
+ const staleHours = parseNum(args, 'stale-hours');
141
+ if (staleHours !== undefined) thresholds.staleOrderAgeMs = staleHours * 3600_000;
142
+
143
+ const options: GuardianOptions = {
144
+ addresses,
145
+ prefs,
146
+ thresholds,
147
+ pollIntervalMs: parseNum(args, 'poll'),
148
+ ordersIntervalMs: parseNum(args, 'orders-poll'),
149
+ useWebSocket: args['no-ws'] !== true,
150
+ telegram: args['no-telegram'] === true ? false : undefined,
151
+ json: args.json === true,
152
+ verbose: args.verbose === true,
153
+ };
154
+
155
+ const guardian = await startGuardian(options);
156
+
157
+ const stats = guardian.getStats();
158
+ const disabled = GUARDIAN_RULE_IDS.filter((r) => prefs.rules?.[r] === false);
159
+ if (!options.json) {
160
+ console.log('OpenBroker Guardian — watching (read-only, Ctrl+C to stop)');
161
+ console.log(` Addresses: ${stats.addresses.join(', ')}`);
162
+ console.log(` Min severity: ${prefs.minSeverity ?? 'info'}`);
163
+ console.log(` Rules off: ${disabled.length > 0 ? disabled.join(', ') : 'none'}`);
164
+ console.log(` Channels: ${stats.channels.join(', ')}`);
165
+ if (!stats.channels.includes('telegram')) {
166
+ console.log(' Tip: run `openbroker guardian connect` to get alerts in Telegram.');
167
+ }
168
+ console.log('');
169
+ for (const target of guardian.getTargets()) {
170
+ const positions = [...target.positions.values()];
171
+ if (positions.length === 0) {
172
+ console.log(` ${target.address}: no open positions`);
173
+ continue;
174
+ }
175
+ console.log(` ${target.address}: equity $${target.equity.toFixed(2)}, margin ${target.marginUsedPct.toFixed(1)}%`);
176
+ for (const pos of positions) {
177
+ const liq = pos.liquidationPx !== null ? `liq $${pos.liquidationPx}` : 'no liq px';
178
+ console.log(` ${pos.side} ${Math.abs(pos.szi)} ${pos.coin} @ $${pos.entryPx ?? '?'} (${pos.leverage}x, ${liq}, uPnL $${pos.unrealizedPnl.toFixed(2)})`);
179
+ }
180
+ }
181
+ console.log('');
182
+ }
183
+
184
+ const shutdown = async () => {
185
+ if (!options.json) console.log('\nStopping guardian...');
186
+ await guardian.stop();
187
+ process.exit(0);
188
+ };
189
+ process.on('SIGINT', shutdown);
190
+ process.on('SIGTERM', shutdown);
191
+
192
+ await new Promise(() => {});
193
+ }
194
+
195
+ async function connectCommand(args: Record<string, string | boolean>) {
196
+ const settings = loadTelegramSettings();
197
+ const token = typeof args.token === 'string' ? args.token : settings.token;
198
+
199
+ if (!token) {
200
+ console.log('No Telegram bot token configured.\n');
201
+ console.log('One-time setup:');
202
+ console.log(' 1. Open Telegram and message @BotFather');
203
+ console.log(' 2. Send /newbot and follow the prompts (any name, e.g. "my-guardian-bot")');
204
+ console.log(' 3. Copy the bot token BotFather gives you');
205
+ console.log(` 4. Add it to your config (${getConfigPath()}):`);
206
+ console.log(' TELEGRAM_BOT_TOKEN=123456789:AAF...');
207
+ console.log(' 5. Re-run: openbroker guardian connect');
208
+ process.exit(1);
209
+ }
210
+
211
+ let bot: { username: string };
212
+ try {
213
+ bot = await getTelegramBotInfo(token);
214
+ } catch (err) {
215
+ console.error(`Could not reach the Telegram bot: ${err instanceof Error ? err.message : String(err)}`);
216
+ console.error('Check TELEGRAM_BOT_TOKEN.');
217
+ process.exit(1);
218
+ }
219
+
220
+ const code = generateLinkCode();
221
+ console.log(`Bot: @${bot!.username}`);
222
+ console.log('\nOpen this link in Telegram and tap START (or send the code to the bot):\n');
223
+ console.log(` https://t.me/${bot!.username}?start=${code}`);
224
+ console.log(`\n code: ${code}`);
225
+ console.log('\nWaiting for the link (10 min timeout, Ctrl+C to abort)...');
226
+
227
+ const chatId = await waitForTelegramLink(token, code);
228
+ if (chatId === null) {
229
+ console.error('\nTimed out waiting for the Telegram link. Re-run: openbroker guardian connect');
230
+ process.exit(1);
231
+ }
232
+
233
+ const savedTo = saveEnvVar('TELEGRAM_CHAT_ID', String(chatId));
234
+ if (typeof args.token === 'string') saveEnvVar('TELEGRAM_BOT_TOKEN', args.token);
235
+ console.log(`\nLinked chat ${chatId} — saved TELEGRAM_CHAT_ID to ${savedTo}`);
236
+ console.log('Guardian alerts will now be delivered to Telegram. Try: openbroker guardian test');
237
+ }
238
+
239
+ async function testCommand() {
240
+ const { token, chatId } = loadTelegramSettings();
241
+ if (!token || !chatId) {
242
+ console.error('Telegram not connected. Run: openbroker guardian connect');
243
+ process.exit(1);
244
+ }
245
+ await sendTelegramMessage(token, chatId, '<b>[TEST]</b> OpenBroker guardian is connected. Risk alerts will arrive here.');
246
+ console.log('Test message sent.');
247
+ }
248
+
249
+ async function statusCommand() {
250
+ const { token, chatId } = loadTelegramSettings();
251
+ let address = 'not configured';
252
+ try {
253
+ address = getClient().address;
254
+ } catch { /* read-only mode without an address */ }
255
+
256
+ console.log('OpenBroker Guardian — status\n');
257
+ console.log(` Default address: ${address}`);
258
+ console.log(` Config file: ${getConfigPath()}`);
259
+ console.log(` Telegram token: ${token ? 'set' : 'NOT SET (see: openbroker guardian connect)'}`);
260
+ console.log(` Telegram chat: ${chatId ? `linked (${chatId})` : 'not linked'}`);
261
+ console.log(` Agent hook: ${process.env.OPENCLAW_HOOKS_TOKEN ? 'configured' : 'not configured'}`);
262
+ console.log('\nRules (all on by default):');
263
+ for (const rule of GUARDIAN_RULE_IDS) {
264
+ console.log(` ${rule.padEnd(20)} ${GUARDIAN_RULE_LABELS[rule]}`);
265
+ }
266
+ }
267
+
268
+ function rulesCommand() {
269
+ console.log('Guardian alert rules:\n');
270
+ const details: Record<GuardianRuleId, string> = {
271
+ liq_proximity: 'warning at 10% from liquidation price, critical at 5% and 2% (--liq-warn/--liq-critical)',
272
+ no_tpsl: 'info when a position is open 15+ min with no reduce-only trigger orders (--tpsl-minutes)',
273
+ stale_order: 'info for limit orders resting 12h+ and 3%+ from mid (--stale-hours)',
274
+ margin_usage: 'warning when margin used exceeds 80% of equity (--margin-pct)',
275
+ funding_bleed: 'warning when paying 15%+ APR funding for 60+ min (--funding-apr)',
276
+ position_lifecycle: 'info on position open / close / resize',
277
+ };
278
+ for (const rule of GUARDIAN_RULE_IDS) {
279
+ console.log(` ${rule.padEnd(20)} ${GUARDIAN_RULE_LABELS[rule]}`);
280
+ console.log(` ${''.padEnd(20)} ${details[rule]}\n`);
281
+ }
282
+ console.log('Disable rules with --disable <csv>, or run a subset with --only <csv>.');
283
+ }
284
+
285
+ async function main() {
286
+ const rawArgs = process.argv.slice(2);
287
+
288
+ if (rawArgs.length === 0 || rawArgs[0] === '--help' || rawArgs[0] === '-h') {
289
+ printUsage();
290
+ process.exit(0);
291
+ }
292
+
293
+ const subcommand = rawArgs[0];
294
+ const args = parseArgs(rawArgs.slice(1));
295
+
296
+ switch (subcommand) {
297
+ case 'run':
298
+ await runCommand(args);
299
+ break;
300
+ case 'connect':
301
+ await connectCommand(args);
302
+ break;
303
+ case 'test':
304
+ await testCommand();
305
+ break;
306
+ case 'status':
307
+ await statusCommand();
308
+ break;
309
+ case 'rules':
310
+ rulesCommand();
311
+ break;
312
+ default:
313
+ console.error(`Unknown subcommand: ${subcommand}`);
314
+ console.log('Run "openbroker guardian --help" for usage');
315
+ process.exit(1);
316
+ }
317
+ }
318
+
319
+ main().catch((err) => {
320
+ console.error(err instanceof Error ? err.message : err);
321
+ process.exit(1);
322
+ });