openbroker 1.0.61 → 1.0.62

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.61", "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.62", "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_twap ob_bracket ob_chase ob_watcher_status ob_auto_run ob_auto_stop ob_auto_list Bash(openbroker:*)
9
9
  ---
10
10
 
@@ -546,6 +546,7 @@ export default function(api) {
546
546
  | `api.state.set(key, value)` | Set a persisted value |
547
547
  | `api.state.delete(key)` | Delete a persisted value |
548
548
  | `api.state.clear()` | Clear all state |
549
+ | `api.publish(message, options?)` | Send a message to the OpenClaw agent via webhook. Triggers an agent turn — the agent receives the message and can notify the user, take action, etc. Returns `true` if delivered. Options: `{ name?, wakeMode?, deliver?, channel? }` |
549
550
  | `api.log.info/warn/error/debug(msg)` | Structured logger |
550
551
  | `api.utils` | `roundPrice`, `roundSize`, `sleep`, `normalizeCoin`, `formatUsd`, `annualizeFundingRate` |
551
552
  | `api.id` | Automation ID (filename or `--id` flag) |
@@ -652,6 +653,47 @@ export default function(api) {
652
653
  }
653
654
  ```
654
655
 
656
+ ### Publishing to the Agent (Webhooks)
657
+
658
+ Use `api.publish()` to send messages back to the OpenClaw agent. This triggers an agent turn — the agent receives the message and can notify the user via their preferred channel, take trading actions, or log the event.
659
+
660
+ ```typescript
661
+ // Simple notification
662
+ await api.publish(`ETH broke above $4000 — current price: $${price}`);
663
+
664
+ // With options
665
+ await api.publish(`Margin at ${pct}% — positions at risk`, {
666
+ name: 'margin-alert', // appears in logs
667
+ wakeMode: 'now', // 'now' (default) or 'next-heartbeat'
668
+ channel: 'slack', // target channel (optional)
669
+ });
670
+ ```
671
+
672
+ `api.publish()` returns `true` if delivered, `false` if webhooks are not configured (no hooks token). It requires `OPENCLAW_HOOKS_TOKEN` to be set (automatically configured when running as an OpenClaw plugin).
673
+
674
+ **Example: Price alert automation with publish**
675
+ ```typescript
676
+ // ~/.openbroker/automations/price-alert.ts
677
+ export default function(api) {
678
+ const COIN = 'ETH';
679
+ const THRESHOLD = 4000;
680
+
681
+ api.on('price_change', async ({ coin, newPrice, changePct }) => {
682
+ if (coin !== COIN) return;
683
+
684
+ const crossed = api.state.get<boolean>('crossed', false);
685
+ if (!crossed && newPrice >= THRESHOLD) {
686
+ api.state.set('crossed', true);
687
+ await api.publish(
688
+ `${COIN} crossed above $${THRESHOLD}! Price: $${newPrice.toFixed(2)} (+${changePct.toFixed(2)}%)`,
689
+ );
690
+ } else if (crossed && newPrice < THRESHOLD) {
691
+ api.state.set('crossed', false);
692
+ }
693
+ });
694
+ }
695
+ ```
696
+
655
697
  ### Running Automations
656
698
 
657
699
  **CLI:**
@@ -680,9 +722,11 @@ openbroker auto status # Show running automations
680
722
  - Always test with `--dry` first before live trading
681
723
  - Use `api.state` to track position state across restarts
682
724
  - Use `api.onStop()` to clean up — close positions, cancel orders
725
+ - Use `api.publish()` to send alerts/events back to the OpenClaw agent — do NOT manually construct webhook requests
683
726
  - The runtime catches errors per handler — one failing handler won't crash others
684
727
  - Scripts are loaded from `~/.openbroker/automations/` by name, or from any absolute path
685
728
  - All trading commands support HIP-3 assets (`api.client.marketOrder('xyz:CL', true, 1)`)
729
+ - Automations persist across gateway restarts — they are automatically restarted when the gateway comes back up
686
730
 
687
731
  ## Risk Warning
688
732
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "openbroker",
3
3
  "name": "OpenBroker — Hyperliquid Trading",
4
- "version": "1.0.61",
4
+ "version": "1.0.62",
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.61",
3
+ "version": "1.0.62",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { parseArgs } from '../core/utils.js';
4
4
  import { resolveScriptPath, listAutomations, ensureAutomationsDir } from './loader.js';
5
- import { startAutomation, getRunningAutomations } from './runtime.js';
5
+ import { startAutomation, getRunningAutomations, getRegisteredAutomations } from './runtime.js';
6
+ import { unregisterAutomation, cleanRegistry } from './registry.js';
6
7
 
7
8
  function printUsage() {
8
9
  console.log(`
@@ -10,8 +11,10 @@ OpenBroker Automations — event-driven trading scripts
10
11
 
11
12
  Usage:
12
13
  openbroker auto run <script> [options] Run an automation script
14
+ openbroker auto stop <id> Unregister an automation (won't restart)
13
15
  openbroker auto list List available automations
14
16
  openbroker auto status Show running automations
17
+ openbroker auto clean Remove stale entries from registry
15
18
 
16
19
  Options (for run):
17
20
  --dry Intercept write methods (no real trades)
@@ -100,24 +103,82 @@ function listCommand() {
100
103
  }
101
104
 
102
105
  function statusCommand() {
103
- const running = getRunningAutomations();
106
+ // Show in-process automations (if any running in this process)
107
+ const inProcess = getRunningAutomations();
104
108
 
105
- if (running.length === 0) {
109
+ // Show all registered automations from file-based registry (cross-process)
110
+ const registered = getRegisteredAutomations();
111
+
112
+ if (inProcess.length === 0 && registered.length === 0) {
106
113
  console.log('No automations running');
107
114
  return;
108
115
  }
109
116
 
110
- console.log('Running automations:\n');
111
- for (const a of running) {
112
- const uptime = Math.round((Date.now() - a.startedAt.getTime()) / 1000);
113
- console.log(` ${a.id}`);
114
- console.log(` Script: ${a.scriptPath}`);
115
- console.log(` Uptime: ${uptime}s`);
116
- console.log(` Polls: ${a.pollCount}`);
117
- console.log(` Events: ${a.eventsEmitted}`);
118
- console.log(` Dry run: ${a.dryRun}`);
119
- console.log('');
117
+ // Show in-process automations with live stats
118
+ if (inProcess.length > 0) {
119
+ console.log('Running in this process:\n');
120
+ for (const a of inProcess) {
121
+ const uptime = Math.round((Date.now() - a.startedAt.getTime()) / 1000);
122
+ console.log(` ${a.id}`);
123
+ console.log(` Script: ${a.scriptPath}`);
124
+ console.log(` Uptime: ${uptime}s`);
125
+ console.log(` Polls: ${a.pollCount}`);
126
+ console.log(` Events: ${a.eventsEmitted}`);
127
+ console.log(` Dry run: ${a.dryRun}`);
128
+ console.log('');
129
+ }
130
+ }
131
+
132
+ // Show all registered automations (may include ones from other processes)
133
+ const external = registered.filter(
134
+ r => !inProcess.some(ip => ip.id === r.id),
135
+ );
136
+
137
+ if (external.length > 0) {
138
+ if (inProcess.length > 0) console.log('Other processes:\n');
139
+ else console.log('Registered automations:\n');
140
+
141
+ for (const a of external) {
142
+ const uptime = a.status === 'running'
143
+ ? `${Math.round((Date.now() - new Date(a.startedAt).getTime()) / 1000)}s`
144
+ : '-';
145
+ console.log(` ${a.id}`);
146
+ console.log(` Script: ${a.scriptPath}`);
147
+ console.log(` Status: ${a.status}${a.error ? ` (${a.error})` : ''}`);
148
+ console.log(` PID: ${a.pid}`);
149
+ console.log(` Uptime: ${uptime}`);
150
+ console.log(` Dry run: ${a.dryRun}`);
151
+ console.log('');
152
+ }
153
+ }
154
+ }
155
+
156
+ function stopCommand(positional: string[]) {
157
+ const id = positional[0];
158
+ if (!id) {
159
+ console.error('Error: automation ID required');
160
+ console.log('Usage: openbroker auto stop <id>');
161
+ process.exit(1);
162
+ }
163
+
164
+ // Check if running in this process
165
+ const inProcess = getRunningAutomations();
166
+ const running = inProcess.find(a => a.id === id);
167
+ if (running) {
168
+ running.stop().then(() => {
169
+ console.log(`Stopped and unregistered: ${id}`);
170
+ });
171
+ return;
120
172
  }
173
+
174
+ // Otherwise just remove from file registry (prevents restart)
175
+ unregisterAutomation(id);
176
+ console.log(`Unregistered: ${id} (will not restart on next gateway start)`);
177
+ }
178
+
179
+ function cleanCommand() {
180
+ cleanRegistry();
181
+ console.log('Cleaned stale entries from registry');
121
182
  }
122
183
 
123
184
  async function main() {
@@ -153,12 +214,18 @@ async function main() {
153
214
  case 'run':
154
215
  await runCommand(args, positional);
155
216
  break;
217
+ case 'stop':
218
+ stopCommand(positional);
219
+ break;
156
220
  case 'list':
157
221
  listCommand();
158
222
  break;
159
223
  case 'status':
160
224
  statusCommand();
161
225
  break;
226
+ case 'clean':
227
+ cleanCommand();
228
+ break;
162
229
  default:
163
230
  console.error(`Unknown subcommand: ${subcommand}`);
164
231
  console.log('Run "openbroker auto --help" for usage');
@@ -0,0 +1,118 @@
1
+ // File-based automation registry — tracks desired state across processes
2
+ // Persisted at ~/.openbroker/state/_registry.json so both CLI and plugin
3
+ // can see which automations should be running.
4
+
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
6
+ import path from 'path';
7
+ import os from 'os';
8
+
9
+ const STATE_DIR = path.join(os.homedir(), '.openbroker', 'state');
10
+ const REGISTRY_FILE = path.join(STATE_DIR, '_registry.json');
11
+
12
+ export interface RegistryEntry {
13
+ id: string;
14
+ scriptPath: string;
15
+ dryRun: boolean;
16
+ verbose: boolean;
17
+ pollIntervalMs: number;
18
+ startedAt: string; // ISO timestamp
19
+ pid: number; // Process that started it
20
+ status: 'running' | 'stopped' | 'error';
21
+ error?: string; // Last error message if status is 'error'
22
+ }
23
+
24
+ function ensureDir(): void {
25
+ mkdirSync(STATE_DIR, { recursive: true });
26
+ }
27
+
28
+ function readRegistry(): RegistryEntry[] {
29
+ if (!existsSync(REGISTRY_FILE)) return [];
30
+ try {
31
+ const raw = readFileSync(REGISTRY_FILE, 'utf-8');
32
+ const entries = JSON.parse(raw);
33
+ return Array.isArray(entries) ? entries : [];
34
+ } catch {
35
+ return [];
36
+ }
37
+ }
38
+
39
+ function writeRegistry(entries: RegistryEntry[]): void {
40
+ ensureDir();
41
+ writeFileSync(REGISTRY_FILE, JSON.stringify(entries, null, 2));
42
+ }
43
+
44
+ /** Check if a process is still alive */
45
+ function isProcessAlive(pid: number): boolean {
46
+ try {
47
+ process.kill(pid, 0); // Signal 0 = just check, don't kill
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /** Register an automation as running */
55
+ export function registerAutomation(entry: Omit<RegistryEntry, 'status' | 'pid' | 'startedAt'>): void {
56
+ const entries = readRegistry();
57
+
58
+ // Remove any existing entry with the same id
59
+ const filtered = entries.filter(e => e.id !== entry.id);
60
+
61
+ filtered.push({
62
+ ...entry,
63
+ status: 'running',
64
+ pid: process.pid,
65
+ startedAt: new Date().toISOString(),
66
+ });
67
+
68
+ writeRegistry(filtered);
69
+ }
70
+
71
+ /** Unregister an automation (remove from desired state) */
72
+ export function unregisterAutomation(id: string): void {
73
+ const entries = readRegistry();
74
+ writeRegistry(entries.filter(e => e.id !== id));
75
+ }
76
+
77
+ /** Mark an automation as errored (keep in registry for visibility) */
78
+ export function markAutomationError(id: string, error: string): void {
79
+ const entries = readRegistry();
80
+ const entry = entries.find(e => e.id === id);
81
+ if (entry) {
82
+ entry.status = 'error';
83
+ entry.error = error;
84
+ writeRegistry(entries);
85
+ }
86
+ }
87
+
88
+ /** Get all registered automations, with stale process detection */
89
+ export function getRegisteredAutomations(): RegistryEntry[] {
90
+ const entries = readRegistry();
91
+ let dirty = false;
92
+
93
+ for (const entry of entries) {
94
+ if (entry.status === 'running' && !isProcessAlive(entry.pid)) {
95
+ // Process died without cleanup — mark as stopped
96
+ entry.status = 'stopped';
97
+ dirty = true;
98
+ }
99
+ }
100
+
101
+ if (dirty) writeRegistry(entries);
102
+ return entries;
103
+ }
104
+
105
+ /** Get automations that should be restarted (were running when process died) */
106
+ export function getAutomationsToRestart(): RegistryEntry[] {
107
+ const entries = getRegisteredAutomations();
108
+ // Return entries that were running but whose process is no longer alive
109
+ // (getRegisteredAutomations already marked them as 'stopped')
110
+ // We want entries that are 'stopped' — they need to be restarted
111
+ return entries.filter(e => e.status === 'stopped');
112
+ }
113
+
114
+ /** Clean up the registry — remove stopped/errored entries */
115
+ export function cleanRegistry(): void {
116
+ const entries = readRegistry();
117
+ writeRegistry(entries.filter(e => e.status === 'running' && isProcessAlive(e.pid)));
118
+ }
@@ -11,12 +11,14 @@ import {
11
11
  } from '../core/utils.js';
12
12
  import { AutomationEventBus } from './events.js';
13
13
  import { loadAutomation } from './loader.js';
14
+ import { registerAutomation, unregisterAutomation, markAutomationError, getRegisteredAutomations as getRegisteredFromFile } from './registry.js';
14
15
  import type {
15
16
  AutomationAPI,
16
17
  AutomationLogger,
17
18
  AutomationState,
18
19
  AutomationSnapshot,
19
20
  PositionSnapshot,
21
+ PublishOptions,
20
22
  ScheduledTask,
21
23
  RunningAutomation,
22
24
  } from './types.js';
@@ -167,6 +169,56 @@ async function buildSnapshot(
167
169
  };
168
170
  }
169
171
 
172
+ // ── Publish (webhook) ───────────────────────────────────────────────
173
+
174
+ function createPublish(
175
+ automationId: string,
176
+ log: AutomationLogger,
177
+ gatewayPort?: number,
178
+ hooksToken?: string,
179
+ ): (message: string, options?: PublishOptions) => Promise<boolean> {
180
+ return async (message: string, options?: PublishOptions): Promise<boolean> => {
181
+ const token = hooksToken || process.env.OPENCLAW_HOOKS_TOKEN;
182
+ const port = gatewayPort || parseInt(process.env.OPENCLAW_GATEWAY_PORT || '18789', 10);
183
+
184
+ if (!token) {
185
+ log.debug('publish() skipped — no hooks token configured (set OPENCLAW_HOOKS_TOKEN or pass hooksToken in plugin config)');
186
+ return false;
187
+ }
188
+
189
+ const body: Record<string, unknown> = {
190
+ message,
191
+ name: options?.name || `ob-auto-${automationId}`,
192
+ wakeMode: options?.wakeMode || 'now',
193
+ };
194
+
195
+ if (options?.deliver !== undefined) body.deliver = options.deliver;
196
+ if (options?.channel) body.channel = options.channel;
197
+
198
+ try {
199
+ const res = await fetch(`http://127.0.0.1:${port}/hooks/agent`, {
200
+ method: 'POST',
201
+ headers: {
202
+ 'Content-Type': 'application/json',
203
+ 'Authorization': `Bearer ${token}`,
204
+ },
205
+ body: JSON.stringify(body),
206
+ });
207
+
208
+ if (!res.ok) {
209
+ log.warn(`publish() failed: HTTP ${res.status} ${res.statusText}`);
210
+ return false;
211
+ }
212
+
213
+ log.debug(`publish() delivered to /hooks/agent (${message.length} chars)`);
214
+ return true;
215
+ } catch (err) {
216
+ log.warn(`publish() error: ${err instanceof Error ? err.message : String(err)}`);
217
+ return false;
218
+ }
219
+ };
220
+ }
221
+
170
222
  // ── Runtime ─────────────────────────────────────────────────────────
171
223
 
172
224
  export interface RuntimeOptions {
@@ -175,6 +227,10 @@ export interface RuntimeOptions {
175
227
  dryRun?: boolean;
176
228
  verbose?: boolean;
177
229
  pollIntervalMs?: number;
230
+ /** Gateway port for webhook delivery. Falls back to OPENCLAW_GATEWAY_PORT or 18789 */
231
+ gatewayPort?: number;
232
+ /** Hooks token for webhook auth. Falls back to OPENCLAW_HOOKS_TOKEN */
233
+ hooksToken?: string;
178
234
  }
179
235
 
180
236
  /** Registry of all running automations */
@@ -188,12 +244,17 @@ export function getAutomation(id: string): RunningAutomation | undefined {
188
244
  return registry.get(id);
189
245
  }
190
246
 
247
+ /** Get all automations from file-based registry (cross-process visibility) */
248
+ export { getRegisteredFromFile as getRegisteredAutomations };
249
+
191
250
  export async function startAutomation(options: RuntimeOptions): Promise<RunningAutomation> {
192
251
  const {
193
252
  scriptPath,
194
253
  dryRun = false,
195
254
  verbose = false,
196
255
  pollIntervalMs = 10_000,
256
+ gatewayPort,
257
+ hooksToken,
197
258
  } = options;
198
259
 
199
260
  const id = options.id || path.basename(scriptPath, '.ts');
@@ -215,6 +276,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
215
276
  const scheduledTasks: ScheduledTask[] = [];
216
277
 
217
278
  // Build the API object
279
+ const publish = createPublish(id, log, gatewayPort, hooksToken);
218
280
  const api: AutomationAPI = {
219
281
  client,
220
282
  utils: { roundPrice, roundSize, sleep, normalizeCoin, formatUsd, formatPercent, annualizeFundingRate },
@@ -223,6 +285,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
223
285
  onStart: (handler) => startHooks.push(handler),
224
286
  onStop: (handler) => stopHooks.push(handler),
225
287
  onError: (handler) => errorHooks.push(handler),
288
+ publish,
226
289
  state,
227
290
  log,
228
291
  id,
@@ -416,7 +479,7 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
416
479
  await poll();
417
480
 
418
481
  // Stop function
419
- async function stop() {
482
+ async function stop(opts?: { persist?: boolean }) {
420
483
  if (stopped) return;
421
484
  stopped = true;
422
485
  clearInterval(timer);
@@ -429,6 +492,12 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
429
492
 
430
493
  eventBus.removeAll();
431
494
  registry.delete(id);
495
+
496
+ // persist defaults to true — fully remove from file registry.
497
+ // When false (gateway shutdown), keep the entry so it restarts next time.
498
+ if (opts?.persist !== false) {
499
+ unregisterAutomation(id);
500
+ }
432
501
  log.info(`Stopped (${pollCount} polls, ${eventsEmitted} events)`);
433
502
  }
434
503
 
@@ -443,5 +512,15 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
443
512
  };
444
513
 
445
514
  registry.set(id, entry);
515
+
516
+ // Persist to file-based registry so other processes (CLI, plugin) can see it
517
+ registerAutomation({
518
+ id,
519
+ scriptPath,
520
+ dryRun,
521
+ verbose,
522
+ pollIntervalMs,
523
+ });
524
+
446
525
  return entry;
447
526
  }
@@ -52,6 +52,19 @@ export interface AutomationLogger {
52
52
  debug(message: string): void;
53
53
  }
54
54
 
55
+ // ── Publish (webhook) ───────────────────────────────────────────────
56
+
57
+ export interface PublishOptions {
58
+ /** Human-readable name for the hook (appears in logs). Default: "ob-auto-<id>" */
59
+ name?: string;
60
+ /** Wake mode: "now" triggers immediate agent turn, "next-heartbeat" queues. Default: "now" */
61
+ wakeMode?: 'now' | 'next-heartbeat';
62
+ /** Whether to deliver the agent response to messaging channels. Default: true */
63
+ deliver?: boolean;
64
+ /** Target channel (e.g. "slack", "telegram", "last"). Default: agent decides */
65
+ channel?: string;
66
+ }
67
+
55
68
  // ── Core API ────────────────────────────────────────────────────────
56
69
 
57
70
  export interface AutomationAPI {
@@ -84,6 +97,17 @@ export interface AutomationAPI {
84
97
  /** Called when a handler throws. The error is already logged — use this for recovery logic. */
85
98
  onError(handler: (error: Error) => void | Promise<void>): void;
86
99
 
100
+ /**
101
+ * Publish a message to the OpenClaw agent via webhook.
102
+ * Sends to POST /hooks/agent on the local gateway, triggering an agent turn.
103
+ * The agent receives the message and can act on it (notify user, trade, etc.).
104
+ *
105
+ * @param message — The message string the agent will receive
106
+ * @param options — Optional: name, wakeMode, deliver, channel
107
+ * @returns true if delivered, false if webhook is not configured
108
+ */
109
+ publish(message: string, options?: PublishOptions): Promise<boolean>;
110
+
87
111
  /** Persisted key-value state (~/.openbroker/state/<id>.json) */
88
112
  state: AutomationState;
89
113
 
@@ -134,5 +158,10 @@ export interface RunningAutomation {
134
158
  pollCount: number;
135
159
  eventsEmitted: number;
136
160
  dryRun: boolean;
137
- stop: () => Promise<void>;
161
+ /**
162
+ * Stop the automation.
163
+ * @param opts.persist If false, keep the entry in the file registry so it
164
+ * restarts when the gateway comes back up. Default: true (fully remove).
165
+ */
166
+ stop: (opts?: { persist?: boolean }) => Promise<void>;
138
167
  }
@@ -1,11 +1,78 @@
1
1
  // OpenClaw Plugin Entry Point for OpenBroker
2
2
 
3
- import type { OpenClawPluginApi, OpenBrokerPluginConfig } from './types.js';
3
+ import type { OpenClawPluginApi, OpenBrokerPluginConfig, PluginLogger } from './types.js';
4
4
  import { applyConfigBridge } from './config-bridge.js';
5
5
  import { PositionWatcher } from './watcher.js';
6
6
  import { createTools } from './tools.js';
7
7
  import { registerCliCommands } from './cli.js';
8
8
 
9
+ /**
10
+ * AutomationService — restarts automations from the file-based registry
11
+ * when the OpenClaw gateway starts. When the gateway process dies,
12
+ * automations die with it. On next start, this service reads the registry
13
+ * and restarts any automations that were previously running.
14
+ */
15
+ function createAutomationService(logger: PluginLogger, gatewayPort?: number, hooksToken?: string) {
16
+ return {
17
+ id: 'openbroker-automations',
18
+
19
+ async start() {
20
+ const { getAutomationsToRestart } = await import('../auto/registry.js');
21
+ const entries = getAutomationsToRestart();
22
+
23
+ if (entries.length === 0) {
24
+ logger.debug('No automations to restart');
25
+ return;
26
+ }
27
+
28
+ logger.info(`Restarting ${entries.length} automation(s) from previous session`);
29
+
30
+ const { startAutomation } = await import('../auto/runtime.js');
31
+ const { resolveScriptPath } = await import('../auto/loader.js');
32
+
33
+ for (const entry of entries) {
34
+ try {
35
+ // Verify script still exists before restarting
36
+ const scriptPath = resolveScriptPath(entry.scriptPath);
37
+ await startAutomation({
38
+ scriptPath,
39
+ id: entry.id,
40
+ dryRun: entry.dryRun,
41
+ verbose: entry.verbose,
42
+ pollIntervalMs: entry.pollIntervalMs,
43
+ gatewayPort,
44
+ hooksToken,
45
+ });
46
+ logger.info(`Restarted automation: ${entry.id}`);
47
+ } catch (err) {
48
+ const msg = err instanceof Error ? err.message : String(err);
49
+ logger.error(`Failed to restart automation "${entry.id}": ${msg}`);
50
+
51
+ // Mark as errored in registry so it doesn't retry forever
52
+ const { markAutomationError } = await import('../auto/registry.js');
53
+ markAutomationError(entry.id, msg);
54
+ }
55
+ }
56
+ },
57
+
58
+ async stop() {
59
+ // Stop all in-process automations but keep them in the file registry
60
+ // so they restart when the gateway comes back up
61
+ const { getRunningAutomations } = await import('../auto/runtime.js');
62
+ const running = getRunningAutomations();
63
+
64
+ for (const auto of running) {
65
+ try {
66
+ await auto.stop({ persist: false }); // Keep in registry for restart
67
+ logger.info(`Stopped automation for gateway shutdown: ${auto.id}`);
68
+ } catch (err) {
69
+ logger.error(`Error stopping automation "${auto.id}": ${err instanceof Error ? err.message : String(err)}`);
70
+ }
71
+ }
72
+ },
73
+ };
74
+ }
75
+
9
76
  export default {
10
77
  id: 'openbroker',
11
78
  name: 'OpenBroker — Hyperliquid Trading',
@@ -43,14 +110,23 @@ export default {
43
110
  logger.debug('OpenBroker position watcher disabled by config');
44
111
  }
45
112
 
46
- // 3. Register agent tools
47
- const tools = createTools(watcher);
113
+ // 3. Register automation restart service
114
+ const resolvedHooksToken = pluginConfig.hooksToken || process.env.OPENCLAW_HOOKS_TOKEN;
115
+ api.registerService(createAutomationService(logger, gatewayPort, resolvedHooksToken));
116
+ logger.debug('OpenBroker automation service registered');
117
+
118
+ // 4. Register agent tools
119
+ const tools = createTools({
120
+ watcher,
121
+ gatewayPort,
122
+ hooksToken: pluginConfig.hooksToken || process.env.OPENCLAW_HOOKS_TOKEN,
123
+ });
48
124
  for (const tool of tools) {
49
125
  api.registerTool(tool);
50
126
  }
51
127
  logger.debug(`Registered ${tools.length} OpenBroker agent tools`);
52
128
 
53
- // 4. Register CLI commands
129
+ // 5. Register CLI commands
54
130
  registerCliCommands(api, watcher, logger);
55
131
  logger.debug('OpenBroker CLI commands registered');
56
132
  },
@@ -18,7 +18,18 @@ function error(message: string) {
18
18
  return json({ error: message });
19
19
  }
20
20
 
21
- export function createTools(watcher: PositionWatcher | null): PluginTool[] {
21
+ export interface ToolsContext {
22
+ watcher: PositionWatcher | null;
23
+ gatewayPort?: number;
24
+ hooksToken?: string;
25
+ }
26
+
27
+ export function createTools(watcherOrCtx: PositionWatcher | null | ToolsContext): PluginTool[] {
28
+ // Support both old signature (watcher only) and new (full context)
29
+ const ctx: ToolsContext = watcherOrCtx !== null && typeof watcherOrCtx === 'object' && 'watcher' in watcherOrCtx
30
+ ? watcherOrCtx
31
+ : { watcher: watcherOrCtx };
32
+ const { watcher, gatewayPort, hooksToken } = ctx;
22
33
  return [
23
34
  // ── Info Tools ──────────────────────────────────────────────
24
35
 
@@ -1326,6 +1337,8 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
1326
1337
  id: params.id ? String(params.id) : undefined,
1327
1338
  dryRun: params.dry === true,
1328
1339
  pollIntervalMs: params.poll ? Number(params.poll) : 10_000,
1340
+ gatewayPort,
1341
+ hooksToken,
1329
1342
  });
1330
1343
 
1331
1344
  return json({
@@ -1367,23 +1380,41 @@ export function createTools(watcher: PositionWatcher | null): PluginTool[] {
1367
1380
 
1368
1381
  {
1369
1382
  name: 'ob_auto_list',
1370
- description: 'List available automation scripts and running automations',
1383
+ description: 'List available automation scripts and running automations (including those started from other processes)',
1371
1384
  parameters: { type: 'object', properties: {} },
1372
1385
  async execute() {
1373
1386
  const { listAutomations } = await import('../auto/loader.js');
1374
- const { getRunningAutomations } = await import('../auto/runtime.js');
1387
+ const { getRunningAutomations, getRegisteredAutomations } = await import('../auto/runtime.js');
1375
1388
 
1376
1389
  const available = listAutomations();
1377
- const running = getRunningAutomations().map(a => ({
1390
+
1391
+ // In-process automations with live stats
1392
+ const inProcess = getRunningAutomations().map(a => ({
1378
1393
  id: a.id,
1379
1394
  scriptPath: a.scriptPath,
1380
1395
  uptime: Math.round((Date.now() - a.startedAt.getTime()) / 1000),
1381
1396
  pollCount: a.pollCount,
1382
1397
  eventsEmitted: a.eventsEmitted,
1383
1398
  dryRun: a.dryRun,
1399
+ source: 'this_process',
1384
1400
  }));
1385
1401
 
1386
- return json({ available, running });
1402
+ // File-registry entries from other processes
1403
+ const registered = getRegisteredAutomations();
1404
+ const external = registered
1405
+ .filter(r => !inProcess.some(ip => ip.id === r.id))
1406
+ .map(r => ({
1407
+ id: r.id,
1408
+ scriptPath: r.scriptPath,
1409
+ status: r.status,
1410
+ pid: r.pid,
1411
+ startedAt: r.startedAt,
1412
+ dryRun: r.dryRun,
1413
+ error: r.error,
1414
+ source: 'other_process',
1415
+ }));
1416
+
1417
+ return json({ available, running: [...inProcess, ...external] });
1387
1418
  },
1388
1419
  },
1389
1420
  ];