openbroker 1.0.88 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "name": "openbroker",
3
- "version": "1.0.88",
3
+ "version": "1.1.0",
4
4
  "description": "Hyperliquid trading CLI - execute orders, manage positions, and run trading strategies",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "openbroker": "./bin/openbroker.js"
8
8
  },
9
- "openclaw": {
10
- "extensions": ["./scripts/plugin/index.ts"]
9
+ "main": "./scripts/lib.ts",
10
+ "exports": {
11
+ ".": "./scripts/lib.ts",
12
+ "./package.json": "./package.json"
11
13
  },
12
14
  "files": [
13
15
  "bin/",
14
16
  "scripts/",
15
17
  "config/example.env",
16
- "openclaw.plugin.json",
17
18
  "README.md",
18
- "CHANGELOG.md",
19
- "SKILL.md"
19
+ "CHANGELOG.md"
20
20
  ],
21
21
  "scripts": {
22
22
  "onboard": "tsx scripts/setup/onboard.ts",
@@ -130,6 +130,10 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
130
130
  process.exit(1);
131
131
  }
132
132
 
133
+ const envHooksToken = process.env.OPENCLAW_HOOKS_TOKEN;
134
+ const envGatewayPortStr = process.env.OPENCLAW_GATEWAY_PORT;
135
+ const envGatewayPort = envGatewayPortStr ? parseInt(envGatewayPortStr, 10) : undefined;
136
+
133
137
  const automation = await startAutomation({
134
138
  scriptPath,
135
139
  id,
@@ -138,6 +142,8 @@ async function runCommand(args: Record<string, string | boolean>, positional: st
138
142
  pollIntervalMs,
139
143
  useWebSocket,
140
144
  initialState: Object.keys(initialState).length > 0 ? initialState : undefined,
145
+ hooksToken: envHooksToken,
146
+ gatewayPort: envGatewayPort && !isNaN(envGatewayPort) ? envGatewayPort : undefined,
141
147
  });
142
148
 
143
149
  // Graceful shutdown on SIGINT/SIGTERM
@@ -15,9 +15,9 @@ 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';
19
18
  import type {
20
19
  AutomationAPI,
20
+ AutomationAuditObserver,
21
21
  AutomationEventPayloads,
22
22
  AutomationEventType,
23
23
  AutomationLogger,
@@ -30,6 +30,70 @@ import type {
30
30
  RunningAutomation,
31
31
  } from './types.js';
32
32
 
33
+ // ── Observer fan-out ────────────────────────────────────────────────
34
+ //
35
+ // External monitoring packages (e.g. `openbroker-monitoring`) plug into
36
+ // the audit pipeline as observers. We auto-load them via convention
37
+ // dynamic-import: any package whose default export is a factory returning
38
+ // an `AutomationAuditObserver` (or null) and that resolves through Node's
39
+ // module resolver gets wired in at startup.
40
+
41
+ const CONVENTION_OBSERVER_PACKAGES = ['openbroker-monitoring'];
42
+
43
+ type ObserverFactory =
44
+ | AutomationAuditObserver
45
+ | ((opts?: unknown) => AutomationAuditObserver | null | undefined);
46
+
47
+ async function loadConventionObservers(log: AutomationLogger): Promise<AutomationAuditObserver[]> {
48
+ const observers: AutomationAuditObserver[] = [];
49
+ for (const name of CONVENTION_OBSERVER_PACKAGES) {
50
+ try {
51
+ const mod = await import(name);
52
+ const exported: ObserverFactory | undefined = mod.default ?? mod;
53
+ const observer = typeof exported === 'function' ? exported() : exported;
54
+ if (observer && typeof observer === 'object') {
55
+ observers.push(observer as AutomationAuditObserver);
56
+ log.debug(`Loaded audit observer: ${name}`);
57
+ }
58
+ } catch (err) {
59
+ const code = (err as NodeJS.ErrnoException | undefined)?.code;
60
+ if (code !== 'ERR_MODULE_NOT_FOUND' && code !== 'MODULE_NOT_FOUND') {
61
+ log.warn(`Failed to load audit observer "${name}": ${err instanceof Error ? err.message : String(err)}`);
62
+ }
63
+ }
64
+ }
65
+ return observers;
66
+ }
67
+
68
+ function fanOutNote(observers: AutomationAuditObserver[], kind: string, payload?: unknown): void {
69
+ for (const o of observers) {
70
+ try { o.onNote?.(kind, payload); } catch { /* observer must not break runtime */ }
71
+ }
72
+ }
73
+
74
+ function fanOutMetric(
75
+ observers: AutomationAuditObserver[],
76
+ name: string,
77
+ value: number,
78
+ tags?: Record<string, unknown>,
79
+ ): void {
80
+ for (const o of observers) {
81
+ try { o.onMetric?.(name, value, tags); } catch { /* observer must not break runtime */ }
82
+ }
83
+ }
84
+
85
+ function fanOutAgentAction(
86
+ observers: AutomationAuditObserver[],
87
+ action: string,
88
+ status: 'success' | 'error',
89
+ details: Record<string, unknown>,
90
+ txHash?: string,
91
+ ): void {
92
+ for (const o of observers) {
93
+ try { o.onAgentAction?.(action, status, details, txHash); } catch { /* observer must not break runtime */ }
94
+ }
95
+ }
96
+
33
97
  const STATE_DIR = path.join(os.homedir(), '.openbroker', 'state');
34
98
  const AUDITED_WRITE_METHODS = new Set([
35
99
  'order', 'marketOrder', 'limitOrder', 'triggerOrder',
@@ -155,6 +219,7 @@ function createAuditedClient(
155
219
  client: HyperliquidClient,
156
220
  audit: AutomationAuditSink,
157
221
  dryRun: boolean,
222
+ observers: AutomationAuditObserver[],
158
223
  ): HyperliquidClient {
159
224
  return new Proxy(client, {
160
225
  get(target, prop, receiver) {
@@ -179,7 +244,8 @@ function createAuditedClient(
179
244
  result,
180
245
  dryRun,
181
246
  });
182
- forwardAgentAction(
247
+ fanOutAgentAction(
248
+ observers,
183
249
  prop,
184
250
  'success',
185
251
  { args: toSerializable(args), result: toSerializable(result), dryRun },
@@ -193,7 +259,8 @@ function createAuditedClient(
193
259
  error,
194
260
  dryRun,
195
261
  });
196
- forwardAgentAction(
262
+ fanOutAgentAction(
263
+ observers,
197
264
  prop,
198
265
  'error',
199
266
  { args: toSerializable(args), error: String(error), dryRun },
@@ -282,11 +349,11 @@ function createPublish(
282
349
  hooksToken?: string,
283
350
  ): (message: string, options?: PublishOptions) => Promise<boolean> {
284
351
  return async (message: string, options?: PublishOptions): Promise<boolean> => {
285
- const token = hooksToken || process.env.OPENCLAW_HOOKS_TOKEN;
286
- const port = gatewayPort || parseInt(process.env.OPENCLAW_GATEWAY_PORT || '18789', 10);
352
+ const token = hooksToken;
353
+ const port = gatewayPort || 18789;
287
354
 
288
355
  if (!token) {
289
- log.debug('publish() skipped — no hooks token configured (set OPENCLAW_HOOKS_TOKEN or pass hooksToken in plugin config)');
356
+ log.debug('publish() skipped — no hooks token configured (pass --hooks-token, set OPENCLAW_HOOKS_TOKEN before invoking the CLI, or configure plugin.hooksToken)');
290
357
  return false;
291
358
  }
292
359
 
@@ -425,8 +492,9 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
425
492
  stateController.attachAudit(audit);
426
493
 
427
494
  const log = createLogger(id, verbose, audit);
495
+ const observers = await loadConventionObservers(log);
428
496
  const baseClient = dryRun ? createDryClient(rawClient, log) : rawClient;
429
- const client = createAuditedClient(baseClient, audit, dryRun);
497
+ const client = createAuditedClient(baseClient, audit, dryRun, observers);
430
498
 
431
499
  const startHooks: Array<() => void | Promise<void>> = [];
432
500
  const stopHooks: Array<() => void | Promise<void>> = [];
@@ -435,10 +503,16 @@ export async function startAutomation(options: RuntimeOptions): Promise<RunningA
435
503
 
436
504
  // Build the API object
437
505
  const publish = createAuditedPublish(createPublish(id, log, gatewayPort, hooksToken), audit);
438
- const auditApi: AutomationAudit = withDashboardForwarder({
439
- record: (kind: string, payload?: unknown) => audit.recordNote(kind, payload),
440
- metric: (name: string, value: number, tags?: Record<string, unknown>) => audit.recordMetric(name, value, tags),
441
- });
506
+ const auditApi: AutomationAudit = {
507
+ record: (kind: string, payload?: unknown) => {
508
+ audit.recordNote(kind, payload);
509
+ fanOutNote(observers, kind, payload);
510
+ },
511
+ metric: (name: string, value: number, tags?: Record<string, unknown>) => {
512
+ audit.recordMetric(name, value, tags);
513
+ fanOutMetric(observers, name, value, tags);
514
+ },
515
+ };
442
516
  const api: AutomationAPI = {
443
517
  client,
444
518
  utils: { roundPrice, roundSize, sleep, normalizeCoin, formatUsd, formatPercent, annualizeFundingRate },
@@ -116,6 +116,26 @@ export interface AutomationAudit {
116
116
  metric(name: string, value: number, tags?: Record<string, unknown>): void;
117
117
  }
118
118
 
119
+ /**
120
+ * Hook for external monitoring packages (e.g. `openbroker-monitoring`) to
121
+ * receive audit events. The runtime fans out every note, metric, and
122
+ * audited write-method call to every observer it has loaded.
123
+ *
124
+ * Observers are auto-loaded via convention dynamic-import — the runtime
125
+ * tries `await import('openbroker-monitoring')` at startup and uses the
126
+ * default export (a factory returning an observer or null).
127
+ */
128
+ export interface AutomationAuditObserver {
129
+ onNote?(kind: string, payload?: unknown): void;
130
+ onMetric?(name: string, value: number, tags?: Record<string, unknown>): void;
131
+ onAgentAction?(
132
+ action: string,
133
+ status: 'success' | 'error',
134
+ details: Record<string, unknown>,
135
+ txHash?: string,
136
+ ): void;
137
+ }
138
+
119
139
  // ── Publish (webhook) ───────────────────────────────────────────────
120
140
 
121
141
  export interface PublishOptions {
@@ -56,7 +56,7 @@ export class HyperliquidClient {
56
56
  constructor(config?: OpenBrokerConfig) {
57
57
  this.config = config ?? loadConfig();
58
58
  this.account = privateKeyToAccount(this.config.privateKey);
59
- this.verbose = process.env.VERBOSE === '1' || process.env.VERBOSE === 'true';
59
+ this.verbose = this.config.verbose;
60
60
 
61
61
  // Initialize SDK clients
62
62
  this.transport = new HttpTransport({ isTestnet: !isMainnet() });
@@ -142,6 +142,8 @@ export function loadConfig(): OpenBrokerConfig {
142
142
  // Standard API wallets (approved via approveAgent) do NOT need this.
143
143
  const vaultAddress = process.env.HYPERLIQUID_VAULT_ADDRESS?.toLowerCase();
144
144
 
145
+ const verbose = process.env.VERBOSE === '1' || process.env.VERBOSE === 'true';
146
+
145
147
  return {
146
148
  baseUrl,
147
149
  privateKey: privateKey as `0x${string}`,
@@ -153,6 +155,7 @@ export function loadConfig(): OpenBrokerConfig {
153
155
  builderFee,
154
156
  slippageBps,
155
157
  vaultAddress,
158
+ verbose,
156
159
  };
157
160
  }
158
161
 
@@ -13,6 +13,7 @@ export interface OpenBrokerConfig {
13
13
  builderFee: number; // tenths of bps (10 = 1 bps)
14
14
  slippageBps: number;
15
15
  vaultAddress?: string; // Explicit vault address for vault trading (ERC4626 / native vaults via CoreWriter)
16
+ verbose: boolean; // Debug logging — set from VERBOSE env var by loadConfig()
16
17
  }
17
18
 
18
19
  // ============ Builder ============
package/scripts/lib.ts ADDED
@@ -0,0 +1,80 @@
1
+ // Public library API for `openbroker`.
2
+ //
3
+ // External packages — notably `openbroker-plugin` — import from here to
4
+ // drive the CLI's functionality in-process (no `child_process` dispatch).
5
+ //
6
+ // Stability: every symbol re-exported here is a public API. Renames or
7
+ // removals are breaking changes and require a major version bump.
8
+
9
+ export {
10
+ HyperliquidClient,
11
+ getClient,
12
+ } from './core/client.js';
13
+
14
+ export {
15
+ loadConfig,
16
+ isConfigured,
17
+ getNetwork,
18
+ isMainnet,
19
+ ensureConfigDir,
20
+ getConfigPath,
21
+ GLOBAL_CONFIG_DIR,
22
+ GLOBAL_ENV_PATH,
23
+ OPEN_BROKER_BUILDER_ADDRESS,
24
+ } from './core/config.js';
25
+
26
+ export {
27
+ roundPrice,
28
+ roundSize,
29
+ sleep,
30
+ normalizeCoin,
31
+ formatUsd,
32
+ formatPercent,
33
+ annualizeFundingRate,
34
+ parseArgs,
35
+ getSlippagePrice,
36
+ getTimestampMs,
37
+ generateCloid,
38
+ orderToWire,
39
+ checkBuilderFeeApproval,
40
+ } from './core/utils.js';
41
+
42
+ export type * from './core/types.js';
43
+
44
+ // ── Operations (in-process callable) ────────────────────────────────
45
+
46
+ export { runBracket } from './operations/bracket.js';
47
+ export type { BracketOptions, BracketResult } from './operations/bracket.js';
48
+
49
+ export { runChase } from './operations/chase.js';
50
+ export type { ChaseOptions, ChaseResult } from './operations/chase.js';
51
+
52
+ // ── Automation runtime ──────────────────────────────────────────────
53
+
54
+ export {
55
+ startAutomation,
56
+ getRunningAutomations,
57
+ getAutomation,
58
+ getRegisteredAutomations,
59
+ } from './auto/runtime.js';
60
+ export type { RuntimeOptions } from './auto/runtime.js';
61
+
62
+ export {
63
+ resolveScriptPath,
64
+ resolveExamplePath,
65
+ listAutomations,
66
+ listExamples,
67
+ loadExampleConfigs,
68
+ ensureAutomationsDir,
69
+ loadAutomation,
70
+ } from './auto/loader.js';
71
+
72
+ export {
73
+ registerAutomation,
74
+ unregisterAutomation,
75
+ cleanRegistry,
76
+ getAutomationsToRestart,
77
+ markAutomationError,
78
+ } from './auto/registry.js';
79
+
80
+ export type * from './auto/types.js';