openbroker 1.0.80 → 1.0.85

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/SKILL.md CHANGED
@@ -4,7 +4,7 @@ description: Hyperliquid trading plugin with background position monitoring and
4
4
  license: MIT
5
5
  compatibility: Requires Node.js 22+, network access to api.hyperliquid.xyz
6
6
  homepage: https://www.npmjs.com/package/openbroker
7
- metadata: {"author": "monemetrics", "version": "1.0.80", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
7
+ metadata: {"author": "monemetrics", "version": "1.0.85", "openclaw": {"requires": {"bins": ["openbroker"], "env": ["HYPERLIQUID_PRIVATE_KEY"]}, "primaryEnv": "HYPERLIQUID_PRIVATE_KEY", "install": [{"id": "node", "kind": "node", "package": "openbroker", "bins": ["openbroker"], "label": "Install openbroker (npm)"}]}}
8
8
  allowed-tools: ob_account ob_positions ob_funding ob_markets ob_search ob_spot ob_fills ob_orders ob_order_status ob_fees ob_candles ob_funding_history ob_trades ob_rate_limit ob_funding_scan ob_buy ob_sell ob_limit ob_trigger ob_tpsl ob_cancel ob_spot_buy ob_spot_sell ob_twap ob_twap_cancel ob_twap_status ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
9
9
  ---
10
10
 
@@ -46,28 +46,52 @@ Or with the `ob_search` plugin tool: `{ "query": "gold" }` or `{ "query": "oil",
46
46
 
47
47
  **HIP-3 assets use `dex:COIN` format** — e.g., `xyz:CL` not just `CL`. If you get an error like "No market data found", search for the asset to find the correct prefixed ticker. Common HIP-3 dexes: `xyz`, `flx`, `km`, `hyna`, `vntl`, `cash`.
48
48
 
49
+ ### Asset IDs (disambiguation)
50
+
51
+ Every info JSON output includes an `assetId` field — the canonical Hyperliquid asset index. Prefer it over the coin name when persisting references, because the same ticker can exist on multiple providers (e.g. `HYPE` perp, `hyna:HYPE` HIP-3, and `HYPE/USDC` spot all coexist).
52
+
53
+ | Scope | Formula | Example |
54
+ |-------|---------|---------|
55
+ | Main perps | universe index | `HYPE` → `159` |
56
+ | HIP-3 perps | `100000 + dexIdx * 10000 + assetIdx` | `hyna:HYPE` → `140002` |
57
+ | Spot | `10000 + pair.index` | `HYPE/USDC` → `10107` |
58
+
59
+ ```bash
60
+ openbroker search HYPE --json | jq '.[] | {coin, assetId, type, provider}'
61
+ ```
62
+
63
+ Trading commands still take `--coin <name>` (including HIP-3 `dex:COIN`) — `assetId` is for queries, comparisons, and agent state, not order placement.
64
+
49
65
  ## Troubleshooting: CLI Fallback
50
66
 
51
67
  If an `ob_*` plugin tool returns unexpected errors, empty results, or crashes, **fall back to the equivalent CLI command** via Bash. The CLI and plugin tools share the same core code, but the CLI has more mature error handling and output.
52
68
 
69
+ **Every info command supports `--json`** for structured output. The table below covers the commands with dedicated plugin tools; any other info command (e.g. `spot`, `trades`, `fees`, `order-status`, `rate-limit`, `funding-history`, `all-markets`) can be run as `openbroker <command> --json` for the same effect.
70
+
53
71
  | Plugin Tool | CLI Equivalent |
54
72
  |-------------|---------------|
55
73
  | `ob_account` | `openbroker account --json` |
56
74
  | `ob_positions` | `openbroker positions --json` |
57
75
  | `ob_funding` | `openbroker funding --json --include-hip3` |
58
76
  | `ob_markets` | `openbroker markets --json --include-hip3` |
59
- | `ob_search` | `openbroker search --query <QUERY>` |
77
+ | `ob_search` | `openbroker search --query <QUERY> --json` |
78
+ | `ob_spot` | `openbroker spot --json` (or `--balances --json`) |
79
+ | `ob_fills` | `openbroker fills --json` |
80
+ | `ob_orders` | `openbroker orders --json` |
81
+ | `ob_order_status` | `openbroker order-status --oid <OID> --json` |
82
+ | `ob_fees` | `openbroker fees --json` |
83
+ | `ob_candles` | `openbroker candles --coin <COIN> --json` |
84
+ | `ob_funding_history` | `openbroker funding-history --coin <COIN> --json` |
85
+ | `ob_trades` | `openbroker trades --coin <COIN> --json` |
86
+ | `ob_rate_limit` | `openbroker rate-limit --json` |
87
+ | `ob_funding_scan` | `openbroker funding-scan --json` |
60
88
  | `ob_buy` | `openbroker buy --coin <COIN> --size <SIZE>` |
61
89
  | `ob_sell` | `openbroker sell --coin <COIN> --size <SIZE>` |
62
90
  | `ob_limit` | `openbroker limit --coin <COIN> --side <SIDE> --size <SIZE> --price <PRICE>` |
63
91
  | `ob_tpsl` | `openbroker tpsl --coin <COIN> --tp <PRICE> --sl <PRICE>` |
64
92
  | `ob_cancel` | `openbroker cancel --all` or `--coin <COIN>` |
65
- | `ob_fills` | `openbroker fills --json` |
66
- | `ob_orders` | `openbroker orders --json` |
67
- | `ob_funding_scan` | `openbroker funding-scan --json` |
68
- | `ob_candles` | `openbroker candles --coin <COIN> --json` |
69
93
  | `ob_auto_run` | `openbroker auto run <script> [--dry]` |
70
- | `ob_auto_stop` | (stop via SIGINT when using CLI) |
94
+ | `ob_auto_stop` | `openbroker auto stop <id>` (or SIGINT if run in foreground) |
71
95
  | `ob_auto_list` | `openbroker auto list` |
72
96
 
73
97
  **When to use CLI fallback:**
@@ -137,12 +161,14 @@ openbroker positions --address 0xabc... # Another account's positions
137
161
  ```bash
138
162
  openbroker funding --top 20 # Top 20 by funding rate
139
163
  openbroker funding --coin ETH # Specific coin
164
+ openbroker funding --top 20 --json # JSON (includes assetId)
140
165
  ```
141
166
 
142
167
  ### Markets
143
168
  ```bash
144
169
  openbroker markets --top 30 # Top 30 main perps
145
170
  openbroker markets --coin BTC # Specific coin
171
+ openbroker markets --coin BTC --json # JSON (includes assetId)
146
172
  ```
147
173
 
148
174
  ### All Markets (Perps + Spot + HIP-3)
@@ -152,6 +178,7 @@ openbroker all-markets --type perp # Main perps only
152
178
  openbroker all-markets --type hip3 # HIP-3 perps only
153
179
  openbroker all-markets --type spot # Spot markets only
154
180
  openbroker all-markets --top 20 # Top 20 by volume
181
+ openbroker all-markets --json # JSON (includes assetId)
155
182
  ```
156
183
 
157
184
  ### Search Markets (Find assets across providers)
@@ -159,6 +186,7 @@ openbroker all-markets --top 20 # Top 20 by volume
159
186
  openbroker search --query GOLD # Find all GOLD markets
160
187
  openbroker search --query BTC # Find BTC across all providers
161
188
  openbroker search --query ETH --type perp # ETH perps only
189
+ openbroker search HYPE --json # JSON with assetId per result
162
190
  ```
163
191
 
164
192
  ### Spot Markets
@@ -168,6 +196,7 @@ openbroker spot --coin PURR # Show PURR market info
168
196
  openbroker spot --balances # Show your spot balances
169
197
  openbroker spot --balances --address 0xabc... # Another account's spot balances
170
198
  openbroker spot --top 20 # Top 20 by volume
199
+ openbroker spot --json # JSON (includes assetId, base, quote)
171
200
  ```
172
201
 
173
202
  ### Trade Fills
@@ -193,12 +222,14 @@ openbroker orders --address 0xabc... --open # Another account's open orders
193
222
  openbroker order-status --oid 123456789 # Check specific order
194
223
  openbroker order-status --oid 0x1234... # By client order ID
195
224
  openbroker order-status --oid 123456789 --address 0xabc... # On another account
225
+ openbroker order-status --oid 123456789 --json
196
226
  ```
197
227
 
198
228
  ### Fee Schedule
199
229
  ```bash
200
230
  openbroker fees # Fee tier, rates, and volume
201
231
  openbroker fees --address 0xabc... # Another account's fees
232
+ openbroker fees --json
202
233
  ```
203
234
 
204
235
  ### Candle Data (OHLCV)
@@ -206,23 +237,27 @@ openbroker fees --address 0xabc... # Another account's fees
206
237
  openbroker candles --coin ETH # 24 hourly candles
207
238
  openbroker candles --coin BTC --interval 4h --bars 48 # 48 four-hour bars
208
239
  openbroker candles --coin SOL --interval 1d --bars 30 # 30 daily bars
240
+ openbroker candles --coin ETH --json # JSON (coin, assetId, interval, candles)
209
241
  ```
210
242
 
211
243
  ### Funding History
212
244
  ```bash
213
245
  openbroker funding-history --coin ETH # Last 24h
214
246
  openbroker funding-history --coin BTC --hours 168 # Last 7 days
247
+ openbroker funding-history --coin ETH --json # JSON (coin, assetId, history)
215
248
  ```
216
249
 
217
250
  ### Recent Trades (Tape)
218
251
  ```bash
219
252
  openbroker trades --coin ETH # Last 30 trades
220
253
  openbroker trades --coin BTC --top 50 # Last 50 trades
254
+ openbroker trades --coin ETH --json # JSON (coin, assetId, trades)
221
255
  ```
222
256
 
223
257
  ### Rate Limit
224
258
  ```bash
225
259
  openbroker rate-limit # API usage and capacity
260
+ openbroker rate-limit --json
226
261
  ```
227
262
 
228
263
  ### Funding Rate Scanner (Cross-Dex)
@@ -231,6 +266,7 @@ openbroker funding-scan # Scan all dexes, >25% threshol
231
266
  openbroker funding-scan --threshold 50 --pairs # Show opposing funding pairs
232
267
  openbroker funding-scan --hip3-only --top 20 # HIP-3 only
233
268
  openbroker funding-scan --watch --interval 120 # Re-scan every 2 minutes
269
+ openbroker funding-scan --json # JSON (includes assetId per result)
234
270
  ```
235
271
 
236
272
  ## Trading Commands
@@ -293,6 +329,27 @@ openbroker cancel --coin ETH # Cancel ETH orders only
293
329
  openbroker cancel --oid 123456 # Cancel specific order
294
330
  ```
295
331
 
332
+ ### Spot Orders
333
+ Spot trading uses a separate order path with its own asset indices (see Asset IDs section). Pass the base token symbol as `--coin` — quote is always USDC.
334
+
335
+ ```bash
336
+ # Market orders (shortcuts)
337
+ openbroker spot-buy --coin PURR --size 1000
338
+ openbroker spot-sell --coin HYPE --size 5
339
+
340
+ # Full form (specify --side)
341
+ openbroker spot-order --coin PURR --side buy --size 1000
342
+
343
+ # Limit orders (add --price)
344
+ openbroker spot-order --coin HYPE --side sell --size 50 --price 25.50
345
+ openbroker spot-order --coin PURR --side buy --size 500 --price 0.20 --tif Alo
346
+
347
+ # Preview without executing
348
+ openbroker spot-buy --coin PURR --size 500 --dry
349
+ ```
350
+
351
+ **Spot flags:** `--coin`, `--side`, `--size`, `--price` (omit → market order), `--tif` (`Gtc`/`Ioc`/`Alo`, default `Gtc`), `--slippage` (bps, market orders only), `--dry`, `--verbose`.
352
+
296
353
  ## Advanced Execution
297
354
 
298
355
  ### TWAP (Native Exchange Order)
@@ -303,6 +360,9 @@ openbroker twap --coin ETH --side buy --size 1 --duration 30
303
360
  # Sell 0.5 BTC over 2 hours without randomized timing
304
361
  openbroker twap --coin BTC --side sell --size 0.5 --duration 120 --randomize false
305
362
 
363
+ # Reduce-only (close position with TWAP). Note: TWAP uses `--reduce-only`, not `--reduce`
364
+ openbroker twap --coin ETH --side sell --size 1 --duration 30 --reduce-only
365
+
306
366
  # Cancel a running TWAP
307
367
  openbroker twap-cancel --coin ETH --twap-id 77738308
308
368
 
@@ -407,6 +467,7 @@ Run `openbroker setup` to create the global config interactively.
407
467
  | `HYPERLIQUID_PRIVATE_KEY` | Yes | Wallet private key (0x...) |
408
468
  | `HYPERLIQUID_NETWORK` | No | `mainnet` (default) or `testnet` |
409
469
  | `HYPERLIQUID_ACCOUNT_ADDRESS` | No | Master account address (required for API wallets) |
470
+ | `OB_DASHBOARD_URL` | No | Dashboard API URL for forwarding audit notes, metrics, and agent actions (e.g. `http://localhost:3001`) |
410
471
 
411
472
  The builder fee (1 bps / 0.01%) is hardcoded and not configurable.
412
473
 
@@ -591,6 +652,8 @@ export default function(api) {
591
652
 
592
653
  Automations now write a local audit trail automatically to `~/.openbroker/automation-audit.sqlite`. The runtime records run config, logs, state changes, write actions, order updates, fills, user events, and per-poll account snapshots so you can generate performance reports later.
593
654
 
655
+ **Dashboard Forwarding:** When `OB_DASHBOARD_URL` is set (e.g. `http://localhost:3001`), audit notes, metrics, and trade actions are automatically forwarded to the OpenBroker Vaults dashboard API in real time. The vault address is read from `HYPERSTABLE_VAULT_ADDRESS` or `VAULT`. Forwarding is fire-and-forget — it never blocks the automation or causes errors if the dashboard is unreachable.
656
+
594
657
  ### Events
595
658
 
596
659
  | Event | Payload | When |
@@ -1053,10 +1116,14 @@ export default function(api) {
1053
1116
  openbroker auto run my-strategy --dry # Test without trading
1054
1117
  openbroker auto run ./funding-scalp.ts # Run from path
1055
1118
  openbroker auto run my-strategy --poll 5000 # Poll every 5s
1119
+ openbroker auto run my-strategy --no-ws # Disable WebSocket, pure REST polling
1056
1120
  openbroker auto run --example dca --set coin=HYPE --set amount=50 --dry # Run bundled example
1057
1121
  openbroker auto examples # List bundled examples with config
1058
1122
  openbroker auto list # Show available scripts
1059
1123
  openbroker auto status # Show running automations
1124
+ openbroker auto stop <id> # Unregister an automation (won't auto-restart)
1125
+ openbroker auto report <id> # Read the local audit report (logs, trades, metrics) for an automation
1126
+ openbroker auto clean # Remove stale entries from the registry
1060
1127
  ```
1061
1128
 
1062
1129
  **Plugin tools (for OpenClaw agents):**
@@ -1072,9 +1139,14 @@ openbroker auto status # Show running automations
1072
1139
  | `--verbose` | Show debug output | false |
1073
1140
  | `--id <name>` | Custom automation ID | filename |
1074
1141
  | `--poll <ms>` | Poll interval in milliseconds | 10000 |
1142
+ | `--no-ws` | Disable WebSocket; fall back to REST-only polling | WebSocket on |
1075
1143
  | `--example <name>` | Run a bundled example automation | - |
1076
1144
  | `--set key=value` | Set config values (repeatable) | - |
1077
1145
 
1146
+ **Inspecting automations after they run:**
1147
+ - `openbroker auto report <id>` — reads the local SQLite audit trail at `~/.openbroker/automation-audit.sqlite` and prints a summary of logs, write actions, fills, PnL, and custom metrics recorded via `api.audit.record()` / `api.audit.metric()`. Use this to review what a strategy actually did.
1148
+ - `openbroker auto clean` — prunes registry entries for automations that are no longer running or whose script file is gone. Safe to run anytime.
1149
+
1078
1150
  **Guidelines for agents writing automations:**
1079
1151
 
1080
1152
  **Risk & Safety (mandatory):**
package/bin/cli.ts CHANGED
@@ -112,6 +112,8 @@ Automations:
112
112
  auto status Show running automations
113
113
 
114
114
  Options:
115
+ -c, --config <path> Use a specific .env config file
116
+ --testnet Use testnet
115
117
  --help, -h Show help for a command
116
118
  --dry Preview without executing
117
119
  --verbose Show debug output
@@ -134,11 +136,30 @@ Documentation: https://github.com/aurracloud/open-broker
134
136
  function runScript(scriptPath: string, args: string[]) {
135
137
  const fullPath = path.join(scriptsDir, scriptPath);
136
138
 
139
+ // Handle global flags: set env vars and strip from args
140
+ const env = { ...process.env };
141
+ const testnetIdx = args.indexOf('--testnet');
142
+ if (testnetIdx !== -1) {
143
+ env.HYPERLIQUID_NETWORK = 'testnet';
144
+ args = [...args.slice(0, testnetIdx), ...args.slice(testnetIdx + 1)];
145
+ }
146
+
147
+ // Handle -c / --config flag: load the specified .env file
148
+ // Resolve relative to the user's original working directory (not the package root)
149
+ let configIdx = args.indexOf('-c');
150
+ if (configIdx === -1) configIdx = args.indexOf('--config');
151
+ if (configIdx !== -1 && args[configIdx + 1]) {
152
+ const originalCwd = process.env.OPENBROKER_CWD || process.cwd();
153
+ const configPath = path.resolve(originalCwd, args[configIdx + 1]);
154
+ env.OPENBROKER_CONFIG = configPath;
155
+ args = [...args.slice(0, configIdx), ...args.slice(configIdx + 2)];
156
+ }
157
+
137
158
  // Use tsx to run TypeScript directly
138
159
  const child = spawn('npx', ['tsx', fullPath, ...args], {
139
160
  stdio: 'inherit',
140
161
  cwd: path.resolve(__dirname, '..'),
141
- env: { ...process.env },
162
+ env,
142
163
  });
143
164
 
144
165
  child.on('error', (err) => {
package/bin/openbroker.js CHANGED
@@ -56,6 +56,10 @@ child.on('error', (err) => {
56
56
  }
57
57
  });
58
58
 
59
+ // Let the child handle SIGINT (Ctrl+C) — don't exit the wrapper early
60
+ process.on('SIGINT', () => {});
61
+ process.on('SIGTERM', () => { child.kill('SIGTERM'); });
62
+
59
63
  child.on('exit', (code) => {
60
64
  process.exit(code ?? 0);
61
65
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.80",
4
+ "version": "1.0.85",
5
5
  "description": "Trade on Hyperliquid DEX with background position monitoring",
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.80",
3
+ "version": "1.0.85",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,7 @@ Options (for run):
32
32
  --verbose Show debug output
33
33
  --id <name> Custom automation ID (default: filename)
34
34
  --poll <ms> Poll interval in milliseconds (default: 10000)
35
+ --no-ws Disable WebSocket; fall back to REST-only polling
35
36
 
36
37
  Scripts are loaded from:
37
38
  1. Absolute or relative path
@@ -71,11 +72,13 @@ function parseSetFlags(rawArgs: string[]): Record<string, unknown> {
71
72
  }
72
73
  const key = pair.slice(0, eqIdx);
73
74
  const raw = pair.slice(eqIdx + 1);
75
+ const isHexLike = /^0x[0-9a-fA-F]+$/.test(raw);
76
+ const isDecimalLike = /^-?(?:\d+|\d+\.\d+|\.\d+)$/.test(raw);
74
77
 
75
78
  // Auto-parse numbers and booleans
76
79
  if (raw === 'true') config[key] = true;
77
80
  else if (raw === 'false') config[key] = false;
78
- else if (raw !== '' && !isNaN(Number(raw))) config[key] = Number(raw);
81
+ else if (!isHexLike && isDecimalLike) config[key] = Number(raw);
79
82
  else config[key] = raw;
80
83
 
81
84
  i++; // skip the value
@@ -116,6 +119,12 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
116
119
  const pollIntervalMs = args.poll ? parseInt(String(args.poll), 10) : undefined;
117
120
  const id = args.id ? String(args.id) : undefined;
118
121
 
122
+ if (args.testnet === true) {
123
+ process.env.HYPERLIQUID_NETWORK = 'testnet';
124
+ } else if (args.mainnet === true) {
125
+ process.env.HYPERLIQUID_NETWORK = 'mainnet';
126
+ }
127
+
119
128
  if (pollIntervalMs !== undefined && (isNaN(pollIntervalMs) || pollIntervalMs < 1000)) {
120
129
  console.error('Error: --poll must be at least 1000ms');
121
130
  process.exit(1);
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Dashboard audit forwarder.
3
+ *
4
+ * When OB_DASHBOARD_URL is set, wraps the AutomationAudit API to also POST
5
+ * audit notes, metrics, and agent action logs to the ob-app backend.
6
+ *
7
+ * Fires HTTP requests in the background — never blocks the automation loop.
8
+ */
9
+
10
+ import type { AutomationAudit } from './types.js';
11
+
12
+ const DASHBOARD_URL = process.env.OB_DASHBOARD_URL; // e.g. "http://localhost:3001"
13
+ const DASHBOARD_API_KEY = process.env.OB_DASHBOARD_API_KEY || '';
14
+ const VAULT_ADDRESS = process.env.HYPERSTABLE_VAULT_ADDRESS || process.env.VAULT || '';
15
+
16
+ function postJSON(path: string, body: unknown): void {
17
+ if (!DASHBOARD_URL || !VAULT_ADDRESS) return;
18
+
19
+ const url = `${DASHBOARD_URL}/api/vaults/${VAULT_ADDRESS.toLowerCase()}${path}`;
20
+
21
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
22
+ if (DASHBOARD_API_KEY) {
23
+ headers['Authorization'] = `Bearer ${DASHBOARD_API_KEY}`;
24
+ }
25
+
26
+ fetch(url, {
27
+ method: 'POST',
28
+ headers,
29
+ body: JSON.stringify(body),
30
+ signal: AbortSignal.timeout(5_000),
31
+ }).catch(() => {
32
+ // Silently ignore — dashboard may be down, automation must not be affected.
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Wrap an existing AutomationAudit to forward calls to the dashboard API.
38
+ * If OB_DASHBOARD_URL is not set, returns the original audit object unchanged.
39
+ */
40
+ export function withDashboardForwarder(audit: AutomationAudit): AutomationAudit {
41
+ if (!DASHBOARD_URL || !VAULT_ADDRESS) return audit;
42
+
43
+ return {
44
+ record(kind: string, payload?: unknown): void {
45
+ audit.record(kind, payload);
46
+ postJSON('/audit/notes', {
47
+ category: kind,
48
+ label: typeof payload === 'object' && payload !== null && 'reason' in payload
49
+ ? String((payload as Record<string, unknown>).reason)
50
+ : kind,
51
+ data: payload ?? {},
52
+ });
53
+ },
54
+
55
+ metric(name: string, value: number, tags?: Record<string, unknown>): void {
56
+ audit.metric(name, value, tags);
57
+ postJSON('/audit/metrics', {
58
+ name,
59
+ value,
60
+ tags: tags ?? {},
61
+ });
62
+ },
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Forward an agent action log to the dashboard.
68
+ * Call this from audited client wrappers or directly from automation code.
69
+ */
70
+ export function forwardAgentAction(
71
+ action: string,
72
+ status: 'success' | 'error' | 'pending',
73
+ details: Record<string, unknown>,
74
+ txHash?: string,
75
+ ): void {
76
+ postJSON('/agent/logs', { action, status, details, txHash });
77
+ }
@@ -22,8 +22,10 @@ export function resolveScriptPath(nameOrPath: string): string {
22
22
  return nameOrPath;
23
23
  }
24
24
 
25
- // Relative to cwd
26
- const cwdPath = path.resolve(process.cwd(), nameOrPath);
25
+ // Relative to the user's original cwd (not the openbroker package root that
26
+ // `bin/openbroker.js` chdirs to before spawning tsx).
27
+ const userCwd = process.env.OPENBROKER_CWD || process.cwd();
28
+ const cwdPath = path.resolve(userCwd, nameOrPath);
27
29
  if (existsSync(cwdPath)) return cwdPath;
28
30
 
29
31
  // Relative to ~/.openbroker/automations/
@@ -70,8 +72,9 @@ export async function loadExampleConfigs(): Promise<Record<string, AutomationCon
70
72
  for (const example of examples) {
71
73
  try {
72
74
  const mod = await import(example.path);
73
- if (mod.config && typeof mod.config === 'object' && mod.config.description) {
74
- configs[example.name] = mod.config as AutomationConfig;
75
+ const config = resolveAutomationConfig(mod);
76
+ if (config && typeof config === 'object' && config.description) {
77
+ configs[example.name] = config;
75
78
  }
76
79
  } catch {
77
80
  // Skip examples that fail to load
@@ -81,6 +84,39 @@ export async function loadExampleConfigs(): Promise<Record<string, AutomationCon
81
84
  return configs;
82
85
  }
83
86
 
87
+ function resolveAutomationFactory(mod: Record<string, unknown>): AutomationFactory | null {
88
+ const candidates = [
89
+ mod.default,
90
+ (mod.default as Record<string, unknown> | undefined)?.default,
91
+ mod["module.exports"],
92
+ ((mod["module.exports"] as Record<string, unknown> | undefined)?.default)
93
+ ];
94
+
95
+ for (const candidate of candidates) {
96
+ if (typeof candidate === "function") {
97
+ return candidate as AutomationFactory;
98
+ }
99
+ }
100
+
101
+ return null;
102
+ }
103
+
104
+ function resolveAutomationConfig(mod: Record<string, unknown>): AutomationConfig | null {
105
+ const candidates = [
106
+ mod.config,
107
+ (mod.default as Record<string, unknown> | undefined)?.config,
108
+ (mod["module.exports"] as Record<string, unknown> | undefined)?.config
109
+ ];
110
+
111
+ for (const candidate of candidates) {
112
+ if (candidate && typeof candidate === "object" && "description" in candidate) {
113
+ return candidate as AutomationConfig;
114
+ }
115
+ }
116
+
117
+ return null;
118
+ }
119
+
84
120
  /** Load an automation module and validate the default export */
85
121
  export async function loadAutomation(scriptPath: string): Promise<AutomationFactory> {
86
122
  const absolutePath = path.resolve(scriptPath);
@@ -88,7 +124,7 @@ export async function loadAutomation(scriptPath: string): Promise<AutomationFact
88
124
  // Dynamic import — tsx handles TypeScript transpilation
89
125
  const mod = await import(absolutePath);
90
126
 
91
- const factory = mod.default;
127
+ const factory = resolveAutomationFactory(mod as Record<string, unknown>);
92
128
  if (typeof factory !== 'function') {
93
129
  throw new Error(
94
130
  `Automation script must export a default function.\n` +
@@ -15,6 +15,7 @@ import { AutomationEventBus } from './events.js';
15
15
  import { loadAutomation } from './loader.js';
16
16
  import { registerAutomation, unregisterAutomation, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
17
17
  import { createAutomationAudit, toSerializable, type AutomationAuditSink } from './audit.js';
18
+ import { withDashboardForwarder, forwardAgentAction } from './dashboard-forwarder.js';
18
19
  import type {
19
20
  AutomationAPI,
20
21
  AutomationEventPayloads,
@@ -178,6 +179,11 @@ function createAuditedClient(
178
179
  result,
179
180
  dryRun,
180
181
  });
182
+ forwardAgentAction(
183
+ prop,
184
+ 'success',
185
+ { args: toSerializable(args), result: toSerializable(result), dryRun },
186
+ );
181
187
  return result;
182
188
  } catch (error) {
183
189
  audit.recordAction({
@@ -187,6 +193,11 @@ function createAuditedClient(
187
193
  error,
188
194
  dryRun,
189
195
  });
196
+ forwardAgentAction(
197
+ prop,
198
+ 'error',
199
+ { args: toSerializable(args), error: String(error), dryRun },
200
+ );
190
201
  throw error;
191
202
  }
192
203
  };
@@ -388,12 +399,10 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
388
399
 
389
400
  const stateController = createState(id);
390
401
 
391
- // Pre-seed state from --set flags (doesn't overwrite already-persisted keys)
402
+ // Apply --set flags before the factory function runs so CLI overrides win over persisted state.
392
403
  if (initialState) {
393
404
  for (const [key, value] of Object.entries(initialState)) {
394
- if (stateController.state.get(key) === undefined) {
395
- stateController.state.set(key, value);
396
- }
405
+ stateController.state.set(key, value);
397
406
  }
398
407
  }
399
408
 
@@ -426,10 +435,10 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
426
435
 
427
436
  // Build the API object
428
437
  const publish = createAuditedPublish(createPublish(id, log, gatewayPort, hooksToken), audit);
429
- const auditApi: AutomationAudit = {
438
+ const auditApi: AutomationAudit = withDashboardForwarder({
430
439
  record: (kind: string, payload?: unknown) => audit.recordNote(kind, payload),
431
440
  metric: (name: string, value: number, tags?: Record<string, unknown>) => audit.recordMetric(name, value, tags),
432
- };
441
+ });
433
442
  const api: AutomationAPI = {
434
443
  client,
435
444
  utils: { roundPrice, roundSize, sleep, normalizeCoin, formatUsd, formatPercent, annualizeFundingRate },
@@ -626,6 +635,9 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
626
635
  } catch (err) {
627
636
  const error = err instanceof Error ? err : new Error(String(err));
628
637
  audit.recordError('websocket_setup', error);
638
+ if (verbose && error.stack) {
639
+ log.debug(`WebSocket setup stack: ${error.stack}`);
640
+ }
629
641
  log.warn(`WebSocket setup failed: ${error.message} — using REST polling only`);
630
642
  ws = null;
631
643
  wsConnected = false;