openbroker 1.3.2 → 1.5.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 (169) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/auto/audit.d.ts +57 -0
  3. package/dist/auto/audit.d.ts.map +1 -0
  4. package/dist/auto/audit.js +407 -0
  5. package/dist/auto/cli.d.ts +2 -0
  6. package/dist/auto/cli.d.ts.map +1 -0
  7. package/dist/auto/cli.js +423 -0
  8. package/dist/auto/events.d.ts +11 -0
  9. package/dist/auto/events.d.ts.map +1 -0
  10. package/dist/auto/events.js +36 -0
  11. package/dist/auto/examples/dca.d.ts +4 -0
  12. package/dist/auto/examples/dca.d.ts.map +1 -0
  13. package/dist/auto/examples/dca.js +60 -0
  14. package/dist/auto/examples/funding-arb.d.ts +4 -0
  15. package/dist/auto/examples/funding-arb.d.ts.map +1 -0
  16. package/dist/auto/examples/funding-arb.js +81 -0
  17. package/dist/auto/examples/grid.d.ts +4 -0
  18. package/dist/auto/examples/grid.d.ts.map +1 -0
  19. package/dist/auto/examples/grid.js +114 -0
  20. package/dist/auto/examples/mm-maker.d.ts +4 -0
  21. package/dist/auto/examples/mm-maker.d.ts.map +1 -0
  22. package/dist/auto/examples/mm-maker.js +131 -0
  23. package/dist/auto/examples/mm-spread.d.ts +4 -0
  24. package/dist/auto/examples/mm-spread.d.ts.map +1 -0
  25. package/dist/auto/examples/mm-spread.js +119 -0
  26. package/dist/auto/examples/price-alert.d.ts +4 -0
  27. package/dist/auto/examples/price-alert.d.ts.map +1 -0
  28. package/dist/auto/examples/price-alert.js +85 -0
  29. package/dist/auto/keep-awake.d.ts +11 -0
  30. package/dist/auto/keep-awake.d.ts.map +1 -0
  31. package/dist/auto/keep-awake.js +70 -0
  32. package/dist/auto/loader.d.ts +22 -0
  33. package/dist/auto/loader.d.ts.map +1 -0
  34. package/dist/auto/loader.js +127 -0
  35. package/dist/auto/prune.d.ts +40 -0
  36. package/dist/auto/prune.d.ts.map +1 -0
  37. package/dist/auto/prune.js +204 -0
  38. package/dist/auto/registry.d.ts +24 -0
  39. package/dist/auto/registry.d.ts.map +1 -0
  40. package/dist/auto/registry.js +93 -0
  41. package/dist/auto/report.d.ts +3 -0
  42. package/dist/auto/report.d.ts.map +1 -0
  43. package/dist/auto/report.js +385 -0
  44. package/dist/auto/runtime.d.ts +33 -0
  45. package/dist/auto/runtime.d.ts.map +1 -0
  46. package/dist/auto/runtime.js +844 -0
  47. package/dist/auto/types.d.ts +236 -0
  48. package/dist/auto/types.d.ts.map +1 -0
  49. package/dist/auto/types.js +3 -0
  50. package/dist/core/client.d.ts +691 -0
  51. package/dist/core/client.d.ts.map +1 -0
  52. package/dist/core/client.js +2061 -0
  53. package/dist/core/config.d.ts +22 -0
  54. package/dist/core/config.d.ts.map +1 -0
  55. package/dist/core/config.js +143 -0
  56. package/dist/core/types.d.ts +228 -0
  57. package/dist/core/types.d.ts.map +1 -0
  58. package/dist/core/types.js +2 -0
  59. package/dist/core/utils.d.ts +61 -0
  60. package/dist/core/utils.d.ts.map +1 -0
  61. package/dist/core/utils.js +142 -0
  62. package/dist/core/ws.d.ts +121 -0
  63. package/dist/core/ws.d.ts.map +1 -0
  64. package/dist/core/ws.js +222 -0
  65. package/dist/info/account.d.ts +3 -0
  66. package/dist/info/account.d.ts.map +1 -0
  67. package/dist/info/account.js +198 -0
  68. package/dist/info/all-markets.d.ts +3 -0
  69. package/dist/info/all-markets.d.ts.map +1 -0
  70. package/dist/info/all-markets.js +272 -0
  71. package/dist/info/candles.d.ts +3 -0
  72. package/dist/info/candles.d.ts.map +1 -0
  73. package/dist/info/candles.js +120 -0
  74. package/dist/info/fees.d.ts +3 -0
  75. package/dist/info/fees.d.ts.map +1 -0
  76. package/dist/info/fees.js +87 -0
  77. package/dist/info/fills.d.ts +3 -0
  78. package/dist/info/fills.d.ts.map +1 -0
  79. package/dist/info/fills.js +105 -0
  80. package/dist/info/funding-history.d.ts +3 -0
  81. package/dist/info/funding-history.d.ts.map +1 -0
  82. package/dist/info/funding-history.js +98 -0
  83. package/dist/info/funding-scan.d.ts +3 -0
  84. package/dist/info/funding-scan.d.ts.map +1 -0
  85. package/dist/info/funding-scan.js +178 -0
  86. package/dist/info/funding.d.ts +3 -0
  87. package/dist/info/funding.d.ts.map +1 -0
  88. package/dist/info/funding.js +158 -0
  89. package/dist/info/markets.d.ts +3 -0
  90. package/dist/info/markets.d.ts.map +1 -0
  91. package/dist/info/markets.js +178 -0
  92. package/dist/info/order-status.d.ts +3 -0
  93. package/dist/info/order-status.d.ts.map +1 -0
  94. package/dist/info/order-status.js +85 -0
  95. package/dist/info/orders.d.ts +3 -0
  96. package/dist/info/orders.d.ts.map +1 -0
  97. package/dist/info/orders.js +162 -0
  98. package/dist/info/outcomes.d.ts +3 -0
  99. package/dist/info/outcomes.d.ts.map +1 -0
  100. package/dist/info/outcomes.js +175 -0
  101. package/dist/info/positions.d.ts +3 -0
  102. package/dist/info/positions.d.ts.map +1 -0
  103. package/dist/info/positions.js +127 -0
  104. package/dist/info/rate-limit.d.ts +3 -0
  105. package/dist/info/rate-limit.d.ts.map +1 -0
  106. package/dist/info/rate-limit.js +58 -0
  107. package/dist/info/search-markets.d.ts +3 -0
  108. package/dist/info/search-markets.d.ts.map +1 -0
  109. package/dist/info/search-markets.js +296 -0
  110. package/dist/info/spot.d.ts +3 -0
  111. package/dist/info/spot.d.ts.map +1 -0
  112. package/dist/info/spot.js +192 -0
  113. package/dist/info/trades.d.ts +3 -0
  114. package/dist/info/trades.d.ts.map +1 -0
  115. package/dist/info/trades.js +97 -0
  116. package/dist/lib.d.ts +14 -0
  117. package/dist/lib.d.ts.map +1 -0
  118. package/dist/lib.js +17 -0
  119. package/dist/operations/bracket.d.ts +28 -0
  120. package/dist/operations/bracket.d.ts.map +1 -0
  121. package/dist/operations/bracket.js +266 -0
  122. package/dist/operations/cancel.d.ts +3 -0
  123. package/dist/operations/cancel.d.ts.map +1 -0
  124. package/dist/operations/cancel.js +107 -0
  125. package/dist/operations/chase.d.ts +25 -0
  126. package/dist/operations/chase.d.ts.map +1 -0
  127. package/dist/operations/chase.js +215 -0
  128. package/dist/operations/limit-order.d.ts +3 -0
  129. package/dist/operations/limit-order.d.ts.map +1 -0
  130. package/dist/operations/limit-order.js +144 -0
  131. package/dist/operations/market-order.d.ts +3 -0
  132. package/dist/operations/market-order.d.ts.map +1 -0
  133. package/dist/operations/market-order.js +153 -0
  134. package/dist/operations/outcome-order.d.ts +3 -0
  135. package/dist/operations/outcome-order.d.ts.map +1 -0
  136. package/dist/operations/outcome-order.js +171 -0
  137. package/dist/operations/scale.d.ts +3 -0
  138. package/dist/operations/scale.d.ts.map +1 -0
  139. package/dist/operations/scale.js +212 -0
  140. package/dist/operations/set-tpsl.d.ts +3 -0
  141. package/dist/operations/set-tpsl.d.ts.map +1 -0
  142. package/dist/operations/set-tpsl.js +277 -0
  143. package/dist/operations/spot-order.d.ts +3 -0
  144. package/dist/operations/spot-order.d.ts.map +1 -0
  145. package/dist/operations/spot-order.js +173 -0
  146. package/dist/operations/trigger-order.d.ts +3 -0
  147. package/dist/operations/trigger-order.d.ts.map +1 -0
  148. package/dist/operations/trigger-order.js +177 -0
  149. package/dist/operations/twap-cancel.d.ts +3 -0
  150. package/dist/operations/twap-cancel.d.ts.map +1 -0
  151. package/dist/operations/twap-cancel.js +57 -0
  152. package/dist/operations/twap-status.d.ts +3 -0
  153. package/dist/operations/twap-status.d.ts.map +1 -0
  154. package/dist/operations/twap-status.js +81 -0
  155. package/dist/operations/twap.d.ts +3 -0
  156. package/dist/operations/twap.d.ts.map +1 -0
  157. package/dist/operations/twap.js +124 -0
  158. package/dist/setup/approve-builder.d.ts +3 -0
  159. package/dist/setup/approve-builder.d.ts.map +1 -0
  160. package/dist/setup/approve-builder.js +155 -0
  161. package/dist/setup/env.d.ts +4 -0
  162. package/dist/setup/env.d.ts.map +1 -0
  163. package/dist/setup/env.js +8 -0
  164. package/dist/setup/onboard.d.ts +10 -0
  165. package/dist/setup/onboard.d.ts.map +1 -0
  166. package/dist/setup/onboard.js +462 -0
  167. package/package.json +10 -4
  168. package/scripts/core/client.ts +19 -1
  169. package/scripts/core/types.ts +7 -0
@@ -0,0 +1,127 @@
1
+ // Automation script loader — discovers and loads .ts automation files
2
+ import { existsSync, readdirSync, mkdirSync } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { fileURLToPath } from 'url';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const AUTOMATIONS_DIR = path.join(os.homedir(), '.openbroker', 'automations');
9
+ const EXAMPLES_DIR = path.join(__dirname, 'examples');
10
+ /** Resolve a script path from a name or path */
11
+ export function resolveScriptPath(nameOrPath) {
12
+ // Absolute path
13
+ if (path.isAbsolute(nameOrPath)) {
14
+ if (!existsSync(nameOrPath)) {
15
+ throw new Error(`Automation script not found: ${nameOrPath}`);
16
+ }
17
+ return nameOrPath;
18
+ }
19
+ // Relative to the user's original cwd (not the openbroker package root that
20
+ // `bin/openbroker.js` chdirs to before spawning tsx).
21
+ const userCwd = process.env.OPENBROKER_CWD || process.cwd();
22
+ const cwdPath = path.resolve(userCwd, nameOrPath);
23
+ if (existsSync(cwdPath))
24
+ return cwdPath;
25
+ // Relative to ~/.openbroker/automations/
26
+ const globalPath = path.join(AUTOMATIONS_DIR, nameOrPath);
27
+ if (existsSync(globalPath))
28
+ return globalPath;
29
+ // Try appending .ts
30
+ const withExt = path.join(AUTOMATIONS_DIR, `${nameOrPath}.ts`);
31
+ if (existsSync(withExt))
32
+ return withExt;
33
+ throw new Error(`Automation script not found: ${nameOrPath}\n` +
34
+ `Searched:\n ${cwdPath}\n ${globalPath}\n ${withExt}`);
35
+ }
36
+ /** Resolve a bundled example by name */
37
+ export function resolveExamplePath(name) {
38
+ const examplePath = path.join(EXAMPLES_DIR, `${name}.ts`);
39
+ if (!existsSync(examplePath)) {
40
+ const available = listExamples().map(e => e.name).join(', ');
41
+ throw new Error(`Unknown example: ${name}\nAvailable: ${available}`);
42
+ }
43
+ return examplePath;
44
+ }
45
+ /** List bundled example automations */
46
+ export function listExamples() {
47
+ if (!existsSync(EXAMPLES_DIR))
48
+ return [];
49
+ return readdirSync(EXAMPLES_DIR)
50
+ .filter(f => f.endsWith('.ts') && !f.startsWith('.'))
51
+ .map(f => ({
52
+ name: f.replace(/\.ts$/, ''),
53
+ path: path.join(EXAMPLES_DIR, f),
54
+ }));
55
+ }
56
+ /** Load config metadata from all bundled examples */
57
+ export async function loadExampleConfigs() {
58
+ const examples = listExamples();
59
+ const configs = {};
60
+ for (const example of examples) {
61
+ try {
62
+ const mod = await import(example.path);
63
+ const config = resolveAutomationConfig(mod);
64
+ if (config && typeof config === 'object' && config.description) {
65
+ configs[example.name] = config;
66
+ }
67
+ }
68
+ catch {
69
+ // Skip examples that fail to load
70
+ }
71
+ }
72
+ return configs;
73
+ }
74
+ function resolveAutomationFactory(mod) {
75
+ const candidates = [
76
+ mod.default,
77
+ mod.default?.default,
78
+ mod["module.exports"],
79
+ (mod["module.exports"]?.default)
80
+ ];
81
+ for (const candidate of candidates) {
82
+ if (typeof candidate === "function") {
83
+ return candidate;
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+ function resolveAutomationConfig(mod) {
89
+ const candidates = [
90
+ mod.config,
91
+ mod.default?.config,
92
+ mod["module.exports"]?.config
93
+ ];
94
+ for (const candidate of candidates) {
95
+ if (candidate && typeof candidate === "object" && "description" in candidate) {
96
+ return candidate;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ /** Load an automation module and validate the default export */
102
+ export async function loadAutomation(scriptPath) {
103
+ const absolutePath = path.resolve(scriptPath);
104
+ // Dynamic import — tsx handles TypeScript transpilation
105
+ const mod = await import(absolutePath);
106
+ const factory = resolveAutomationFactory(mod);
107
+ if (typeof factory !== 'function') {
108
+ throw new Error(`Automation script must export a default function.\n` +
109
+ `Got: ${typeof factory} from ${scriptPath}`);
110
+ }
111
+ return factory;
112
+ }
113
+ /** List available automation scripts in ~/.openbroker/automations/ */
114
+ export function listAutomations() {
115
+ if (!existsSync(AUTOMATIONS_DIR))
116
+ return [];
117
+ return readdirSync(AUTOMATIONS_DIR)
118
+ .filter(f => f.endsWith('.ts') && !f.startsWith('.'))
119
+ .map(f => ({
120
+ name: f.replace(/\.ts$/, ''),
121
+ path: path.join(AUTOMATIONS_DIR, f),
122
+ }));
123
+ }
124
+ /** Ensure the automations directory exists */
125
+ export function ensureAutomationsDir() {
126
+ mkdirSync(AUTOMATIONS_DIR, { recursive: true });
127
+ }
@@ -0,0 +1,40 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ export declare const AUDIT_DB_PATH: string;
3
+ export interface PruneFilters {
4
+ /** Delete runs whose started_at < (now - olderThanMs). Falsy = no age filter. */
5
+ olderThanMs?: number;
6
+ /** Delete runs whose status is in this set. Default: stopped, error, stale. */
7
+ statuses?: Set<string>;
8
+ /** For each automation_id, keep the N most recent runs regardless of other filters. */
9
+ keepLastPerAutomation?: number;
10
+ /** Delete every run that is not currently alive (overrides status/age). */
11
+ all?: boolean;
12
+ }
13
+ export interface PruneOptions extends PruneFilters {
14
+ dbPath?: string;
15
+ dryRun?: boolean;
16
+ vacuum?: boolean;
17
+ /**
18
+ * When true, skip the deletion phase and only update status of orphaned
19
+ * 'running' rows whose pid is dead — used by `auto clean` to reconcile state
20
+ * without losing history.
21
+ */
22
+ reconcileOnly?: boolean;
23
+ }
24
+ export interface PruneResult {
25
+ reconciled: number;
26
+ candidateRunIds: string[];
27
+ deletedRows: Record<string, number>;
28
+ freedBytes: number;
29
+ dryRun: boolean;
30
+ }
31
+ /** Parse human-friendly durations like `7d`, `24h`, `30m`, `45s`. */
32
+ export declare function parseDuration(input: string): number;
33
+ /** Reconcile orphan-running rows in the DB (process dead → mark stopped). */
34
+ export declare function reconcileStaleRuns(db: DatabaseSync, opts?: {
35
+ dryRun?: boolean;
36
+ now?: number;
37
+ }): number;
38
+ export declare function prune(opts?: PruneOptions): PruneResult;
39
+ export declare function fmtBytes(n: number): string;
40
+ //# sourceMappingURL=prune.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prune.d.ts","sourceRoot":"","sources":["../../scripts/auto/prune.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG3C,eAAO,MAAM,aAAa,QACkC,CAAC;AAE7D,MAAM,WAAW,YAAY;IAC3B,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,uFAAuF;IACvF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,2EAA2E;IAC3E,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB;AA6BD,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASnD;AAED,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CAuB1G;AAED,wBAAgB,KAAK,CAAC,IAAI,GAAE,YAAiB,GAAG,WAAW,CAgI1D;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAO1C"}
@@ -0,0 +1,204 @@
1
+ // Audit DB pruning — delete stale automation runs and their child rows.
2
+ //
3
+ // The audit DB is the same SQLite file the daemon writes to and the dashboard
4
+ // reads from. WAL mode lets us open it from another process for delete writes
5
+ // without blocking the daemon. We always protect runs whose status is 'running'
6
+ // AND whose pid is alive.
7
+ //
8
+ // Used by: `openbroker auto prune` and as a sub-step of `openbroker auto clean`.
9
+ import path from 'path';
10
+ import { existsSync } from 'fs';
11
+ import { DatabaseSync } from 'node:sqlite';
12
+ import { ensureConfigDir } from '../core/config.js';
13
+ export const AUDIT_DB_PATH = process.env.OPENBROKER_AUDIT_DB_PATH
14
+ || path.join(ensureConfigDir(), 'automation-audit.sqlite');
15
+ const CHILD_TABLES = [
16
+ 'automation_logs',
17
+ 'automation_events',
18
+ 'automation_actions',
19
+ 'automation_snapshots',
20
+ 'automation_order_updates',
21
+ 'automation_fills',
22
+ 'automation_user_events',
23
+ 'automation_state_changes',
24
+ 'automation_publishes',
25
+ 'automation_errors',
26
+ 'automation_notes',
27
+ 'automation_metrics',
28
+ ];
29
+ const DEFAULT_STATUSES = new Set(['stopped', 'error', 'stale']);
30
+ function isProcessAlive(pid) {
31
+ if (typeof pid !== 'number' || !Number.isFinite(pid) || pid <= 0)
32
+ return false;
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /** Parse human-friendly durations like `7d`, `24h`, `30m`, `45s`. */
42
+ export function parseDuration(input) {
43
+ const m = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)?\s*$/.exec(input);
44
+ if (!m)
45
+ throw new Error(`invalid duration: ${input}`);
46
+ const n = Number(m[1]);
47
+ const unit = m[2] ?? 'ms';
48
+ const mult = {
49
+ ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000,
50
+ };
51
+ return n * mult[unit];
52
+ }
53
+ /** Reconcile orphan-running rows in the DB (process dead → mark stopped). */
54
+ export function reconcileStaleRuns(db, opts = {}) {
55
+ const now = opts.now ?? Date.now();
56
+ const runningRows = db.prepare(`
57
+ SELECT run_id, pid FROM automation_runs WHERE status = 'running'
58
+ `).all();
59
+ const orphans = runningRows.filter((r) => !isProcessAlive(r.pid));
60
+ if (orphans.length === 0)
61
+ return 0;
62
+ if (opts.dryRun)
63
+ return orphans.length;
64
+ const update = db.prepare(`
65
+ UPDATE automation_runs
66
+ SET status = 'stopped',
67
+ stop_reason = COALESCE(stop_reason, 'reconciled (process exited)'),
68
+ stopped_at = COALESCE(stopped_at, ?)
69
+ WHERE run_id = ?
70
+ `);
71
+ let n = 0;
72
+ for (const o of orphans) {
73
+ update.run(now, o.run_id);
74
+ n++;
75
+ }
76
+ return n;
77
+ }
78
+ export function prune(opts = {}) {
79
+ const dbPath = opts.dbPath ?? AUDIT_DB_PATH;
80
+ if (!existsSync(dbPath)) {
81
+ return {
82
+ reconciled: 0,
83
+ candidateRunIds: [],
84
+ deletedRows: {},
85
+ freedBytes: 0,
86
+ dryRun: !!opts.dryRun,
87
+ };
88
+ }
89
+ const db = new DatabaseSync(dbPath);
90
+ db.exec('PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000;');
91
+ try {
92
+ const reconciled = reconcileStaleRuns(db, { dryRun: opts.dryRun });
93
+ if (opts.reconcileOnly) {
94
+ return {
95
+ reconciled,
96
+ candidateRunIds: [],
97
+ deletedRows: {},
98
+ freedBytes: 0,
99
+ dryRun: !!opts.dryRun,
100
+ };
101
+ }
102
+ const allRuns = db.prepare(`
103
+ SELECT run_id, automation_id, status, pid, started_at
104
+ FROM automation_runs
105
+ ORDER BY started_at DESC
106
+ `).all();
107
+ const statuses = opts.statuses ?? DEFAULT_STATUSES;
108
+ const cutoff = opts.olderThanMs && opts.olderThanMs > 0 ? Date.now() - opts.olderThanMs : null;
109
+ // group runs per automation_id (already sorted DESC by started_at)
110
+ const byAuto = new Map();
111
+ for (const r of allRuns) {
112
+ const arr = byAuto.get(r.automation_id) ?? [];
113
+ arr.push(r);
114
+ byAuto.set(r.automation_id, arr);
115
+ }
116
+ const candidates = [];
117
+ for (const [, runs] of byAuto) {
118
+ const protectedIdx = new Set();
119
+ if (opts.keepLastPerAutomation && opts.keepLastPerAutomation > 0) {
120
+ for (let i = 0; i < Math.min(opts.keepLastPerAutomation, runs.length); i++) {
121
+ protectedIdx.add(i);
122
+ }
123
+ }
124
+ runs.forEach((r, i) => {
125
+ if (protectedIdx.has(i))
126
+ return;
127
+ // never delete a truly-running automation
128
+ if (r.status === 'running' && isProcessAlive(r.pid))
129
+ return;
130
+ if (opts.all) {
131
+ candidates.push(r);
132
+ return;
133
+ }
134
+ if (!statuses.has(r.status)) {
135
+ // 'running' rows that aren't actually alive were just reconciled to
136
+ // 'stopped' above, so the status check catches them.
137
+ return;
138
+ }
139
+ if (cutoff !== null && r.started_at >= cutoff)
140
+ return;
141
+ candidates.push(r);
142
+ });
143
+ }
144
+ const candidateRunIds = candidates.map((r) => r.run_id);
145
+ const deletedRows = {};
146
+ let freedBytes = 0;
147
+ if (!opts.dryRun && candidateRunIds.length > 0) {
148
+ const sizeBefore = db.prepare('PRAGMA page_count').get();
149
+ const pageSize = db.prepare('PRAGMA page_size').get();
150
+ db.exec('BEGIN');
151
+ try {
152
+ for (const table of CHILD_TABLES) {
153
+ let n = 0;
154
+ const stmt = db.prepare(`DELETE FROM ${table} WHERE run_id = ?`);
155
+ for (const id of candidateRunIds) {
156
+ const info = stmt.run(id);
157
+ n += Number(info.changes ?? 0);
158
+ }
159
+ deletedRows[table] = n;
160
+ }
161
+ const runStmt = db.prepare('DELETE FROM automation_runs WHERE run_id = ?');
162
+ let runChanges = 0;
163
+ for (const id of candidateRunIds) {
164
+ const info = runStmt.run(id);
165
+ runChanges += Number(info.changes ?? 0);
166
+ }
167
+ deletedRows.automation_runs = runChanges;
168
+ db.exec('COMMIT');
169
+ }
170
+ catch (err) {
171
+ db.exec('ROLLBACK');
172
+ throw err;
173
+ }
174
+ if (opts.vacuum) {
175
+ // VACUUM cannot run inside a transaction
176
+ db.exec('VACUUM');
177
+ }
178
+ const sizeAfter = db.prepare('PRAGMA page_count').get();
179
+ freedBytes = (Number(sizeBefore.page_count) - Number(sizeAfter.page_count)) * Number(pageSize.page_size);
180
+ }
181
+ return {
182
+ reconciled,
183
+ candidateRunIds,
184
+ deletedRows,
185
+ freedBytes: Math.max(0, freedBytes),
186
+ dryRun: !!opts.dryRun,
187
+ };
188
+ }
189
+ finally {
190
+ db.close();
191
+ }
192
+ }
193
+ export function fmtBytes(n) {
194
+ if (!Number.isFinite(n) || n <= 0)
195
+ return '0 B';
196
+ const u = ['B', 'KB', 'MB', 'GB'];
197
+ let i = 0;
198
+ let v = n;
199
+ while (v >= 1024 && i < u.length - 1) {
200
+ v /= 1024;
201
+ i++;
202
+ }
203
+ return `${v.toFixed(v >= 100 || i === 0 ? 0 : 1)} ${u[i]}`;
204
+ }
@@ -0,0 +1,24 @@
1
+ export interface RegistryEntry {
2
+ id: string;
3
+ scriptPath: string;
4
+ dryRun: boolean;
5
+ verbose: boolean;
6
+ pollIntervalMs: number;
7
+ startedAt: string;
8
+ pid: number;
9
+ status: 'running' | 'stopped' | 'error';
10
+ error?: string;
11
+ }
12
+ /** Register an automation as running */
13
+ export declare function registerAutomation(entry: Omit<RegistryEntry, 'status' | 'pid' | 'startedAt'>): void;
14
+ /** Unregister an automation (remove from desired state) */
15
+ export declare function unregisterAutomation(id: string): void;
16
+ /** Mark an automation as errored (keep in registry for visibility) */
17
+ export declare function markAutomationError(id: string, error: string): void;
18
+ /** Get all registered automations, with stale process detection */
19
+ export declare function getRegisteredAutomations(): RegistryEntry[];
20
+ /** Get automations that should be restarted (were running when process died) */
21
+ export declare function getAutomationsToRestart(): RegistryEntry[];
22
+ /** Clean up the registry — remove stopped/errored entries */
23
+ export declare function cleanRegistry(): void;
24
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../scripts/auto/registry.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgCD,wCAAwC;AACxC,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,KAAK,GAAG,WAAW,CAAC,GAAG,IAAI,CAcnG;AAED,2DAA2D;AAC3D,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAGrD;AAED,sEAAsE;AACtE,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQnE;AAED,mEAAmE;AACnE,wBAAgB,wBAAwB,IAAI,aAAa,EAAE,CAc1D;AAED,gFAAgF;AAChF,wBAAgB,uBAAuB,IAAI,aAAa,EAAE,CAMzD;AAED,6DAA6D;AAC7D,wBAAgB,aAAa,IAAI,IAAI,CAGpC"}
@@ -0,0 +1,93 @@
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
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ const STATE_DIR = path.join(os.homedir(), '.openbroker', 'state');
8
+ const REGISTRY_FILE = path.join(STATE_DIR, '_registry.json');
9
+ function ensureDir() {
10
+ mkdirSync(STATE_DIR, { recursive: true });
11
+ }
12
+ function readRegistry() {
13
+ if (!existsSync(REGISTRY_FILE))
14
+ return [];
15
+ try {
16
+ const raw = readFileSync(REGISTRY_FILE, 'utf-8');
17
+ const entries = JSON.parse(raw);
18
+ return Array.isArray(entries) ? entries : [];
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ }
24
+ function writeRegistry(entries) {
25
+ ensureDir();
26
+ writeFileSync(REGISTRY_FILE, JSON.stringify(entries, null, 2));
27
+ }
28
+ /** Check if a process is still alive */
29
+ function isProcessAlive(pid) {
30
+ try {
31
+ process.kill(pid, 0); // Signal 0 = just check, don't kill
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /** Register an automation as running */
39
+ export function registerAutomation(entry) {
40
+ const entries = readRegistry();
41
+ // Remove any existing entry with the same id
42
+ const filtered = entries.filter(e => e.id !== entry.id);
43
+ filtered.push({
44
+ ...entry,
45
+ status: 'running',
46
+ pid: process.pid,
47
+ startedAt: new Date().toISOString(),
48
+ });
49
+ writeRegistry(filtered);
50
+ }
51
+ /** Unregister an automation (remove from desired state) */
52
+ export function unregisterAutomation(id) {
53
+ const entries = readRegistry();
54
+ writeRegistry(entries.filter(e => e.id !== id));
55
+ }
56
+ /** Mark an automation as errored (keep in registry for visibility) */
57
+ export function markAutomationError(id, error) {
58
+ const entries = readRegistry();
59
+ const entry = entries.find(e => e.id === id);
60
+ if (entry) {
61
+ entry.status = 'error';
62
+ entry.error = error;
63
+ writeRegistry(entries);
64
+ }
65
+ }
66
+ /** Get all registered automations, with stale process detection */
67
+ export function getRegisteredAutomations() {
68
+ const entries = readRegistry();
69
+ let dirty = false;
70
+ for (const entry of entries) {
71
+ if (entry.status === 'running' && !isProcessAlive(entry.pid)) {
72
+ // Process died without cleanup — mark as stopped
73
+ entry.status = 'stopped';
74
+ dirty = true;
75
+ }
76
+ }
77
+ if (dirty)
78
+ writeRegistry(entries);
79
+ return entries;
80
+ }
81
+ /** Get automations that should be restarted (were running when process died) */
82
+ export function getAutomationsToRestart() {
83
+ const entries = getRegisteredAutomations();
84
+ // Return entries that were running but whose process is no longer alive
85
+ // (getRegisteredAutomations already marked them as 'stopped')
86
+ // We want entries that are 'stopped' — they need to be restarted
87
+ return entries.filter(e => e.status === 'stopped');
88
+ }
89
+ /** Clean up the registry — remove stopped/errored entries */
90
+ export function cleanRegistry() {
91
+ const entries = readRegistry();
92
+ writeRegistry(entries.filter(e => e.status === 'running' && isProcessAlive(e.pid)));
93
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../scripts/auto/report.ts"],"names":[],"mappings":""}