@senpi/runtime 3.0.0-dev.runtime-phase-2.20260623210537 → 3.0.0-dev.runtime-phase-2.20260625112547
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/README.md +10 -9
- package/dist/actions/action-runtime-module.d.ts.map +1 -1
- package/dist/actions/action-runtime-module.js +1 -1
- package/dist/actions/action-runtime-module.js.map +1 -1
- package/dist/actions/decision-engine.d.ts.map +1 -1
- package/dist/actions/decision-engine.js +3 -0
- package/dist/actions/decision-engine.js.map +1 -1
- package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
- package/dist/actions/open-position/open-position.action.js +54 -14
- package/dist/actions/open-position/open-position.action.js.map +1 -1
- package/dist/actions/types.d.ts +10 -2
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +8 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/cli/guide-commands.d.ts.map +1 -1
- package/dist/cli/guide-commands.js +80 -74
- package/dist/cli/guide-commands.js.map +1 -1
- package/dist/cli/senpi-commands.d.ts.map +1 -1
- package/dist/cli/senpi-commands.js +0 -108
- package/dist/cli/senpi-commands.js.map +1 -1
- package/dist/constants/dsl/index.d.ts +2 -0
- package/dist/constants/dsl/index.d.ts.map +1 -1
- package/dist/constants/dsl/index.js +2 -0
- package/dist/constants/dsl/index.js.map +1 -1
- package/dist/dsl/config/parse-tiers.d.ts.map +1 -1
- package/dist/dsl/config/parse-tiers.js +6 -2
- package/dist/dsl/config/parse-tiers.js.map +1 -1
- package/dist/dsl/engine-client/config-mapping.d.ts +7 -0
- package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
- package/dist/dsl/engine-client/config-mapping.js +16 -2
- package/dist/dsl/engine-client/config-mapping.js.map +1 -1
- package/dist/dsl/events/handlers.d.ts +18 -2
- package/dist/dsl/events/handlers.d.ts.map +1 -1
- package/dist/dsl/events/handlers.js +302 -213
- package/dist/dsl/events/handlers.js.map +1 -1
- package/dist/dsl/monitor/create-monitor.d.ts +15 -0
- package/dist/dsl/monitor/create-monitor.d.ts.map +1 -1
- package/dist/dsl/monitor/create-monitor.js +46 -9
- package/dist/dsl/monitor/create-monitor.js.map +1 -1
- package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
- package/dist/dsl/monitor/process-one-position.js +138 -77
- package/dist/dsl/monitor/process-one-position.js.map +1 -1
- package/dist/dsl/plugin/index.d.ts +16 -1
- package/dist/dsl/plugin/index.d.ts.map +1 -1
- package/dist/dsl/plugin/index.js +35 -4
- package/dist/dsl/plugin/index.js.map +1 -1
- package/dist/dsl/plugin/options.d.ts +18 -0
- package/dist/dsl/plugin/options.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime/create-scanner-module.d.ts +4 -3
- package/dist/runtime/create-scanner-module.d.ts.map +1 -1
- package/dist/runtime/create-scanner-module.js +14 -5
- package/dist/runtime/create-scanner-module.js.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.d.ts.map +1 -1
- package/dist/runtime/create-scanner-providers-for-run.js +12 -1
- package/dist/runtime/create-scanner-providers-for-run.js.map +1 -1
- package/dist/runtime/load-runtime-config.js +1 -1
- package/dist/runtime/load-runtime-config.js.map +1 -1
- package/dist/runtime/map-strategies-registration.js +7 -8
- package/dist/runtime/map-strategies-registration.js.map +1 -1
- package/dist/runtime/map-strategies.d.ts.map +1 -1
- package/dist/runtime/map-strategies.js +12 -3
- package/dist/runtime/map-strategies.js.map +1 -1
- package/dist/runtime/reconcile-decide.d.ts +73 -0
- package/dist/runtime/reconcile-decide.d.ts.map +1 -0
- package/dist/runtime/reconcile-decide.js +186 -0
- package/dist/runtime/reconcile-decide.js.map +1 -0
- package/dist/runtime/reconcile-run.d.ts +62 -0
- package/dist/runtime/reconcile-run.d.ts.map +1 -0
- package/dist/runtime/reconcile-run.js +173 -0
- package/dist/runtime/reconcile-run.js.map +1 -0
- package/dist/runtime/reconcile-state.d.ts +44 -0
- package/dist/runtime/reconcile-state.d.ts.map +1 -1
- package/dist/runtime/reconcile-state.js +80 -44
- package/dist/runtime/reconcile-state.js.map +1 -1
- package/dist/runtime/risk-guard.d.ts.map +1 -1
- package/dist/runtime/risk-guard.js +17 -12
- package/dist/runtime/risk-guard.js.map +1 -1
- package/dist/runtime/runtime-schema.d.ts +2912 -1052
- package/dist/runtime/runtime-schema.d.ts.map +1 -1
- package/dist/runtime/runtime-schema.js +136 -42
- package/dist/runtime/runtime-schema.js.map +1 -1
- package/dist/runtime/runtime.d.ts +26 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +254 -8
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scanner-registry.d.ts +1 -1
- package/dist/runtime/scanner-registry.d.ts.map +1 -1
- package/dist/runtime/scanner-registry.js +3 -12
- package/dist/runtime/scanner-registry.js.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.d.ts.map +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
- package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js +1 -1
- package/dist/runtime/supervisor/orphan-sweep.js.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts +1 -1
- package/dist/runtime/supervisor/supervisor-utils.d.ts.map +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js +1 -1
- package/dist/runtime/supervisor/supervisor-utils.js.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.d.ts +10 -0
- package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
- package/dist/runtime-api/intake/acceptance-record.js +28 -0
- package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
- package/dist/runtime-api/intake/intake-mount.d.ts.map +1 -1
- package/dist/runtime-api/intake/intake-mount.js +28 -36
- package/dist/runtime-api/intake/intake-mount.js.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts +20 -1
- package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
- package/dist/runtime-api/intake/process-signal-post.js +57 -6
- package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.d.ts +18 -4
- package/dist/runtime-api/intake/scanner-launcher.d.ts.map +1 -1
- package/dist/runtime-api/intake/scanner-launcher.js +24 -3
- package/dist/runtime-api/intake/scanner-launcher.js.map +1 -1
- package/dist/runtime-api/intake/types.d.ts +16 -0
- package/dist/runtime-api/intake/types.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.d.ts +4 -0
- package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
- package/dist/runtime-api/routes/signals.schema.js +12 -0
- package/dist/runtime-api/routes/signals.schema.js.map +1 -1
- package/dist/scanners/external/scaffold/__main__.py +65 -24
- package/dist/scanners/external/scaffold/ctx.py +78 -0
- package/dist/scanners/external/scaffold/delivery.py +56 -22
- package/dist/scanners/external/scaffold/entrypoint_loader.py +42 -48
- package/dist/scanners/external/scaffold/journal.py +13 -2
- package/dist/scanners/external/scaffold/requirements.txt +11 -0
- package/dist/scanners/external/scaffold/scaffold.py +159 -128
- package/dist/scanners/external/scaffold/senpi_mcp.py +250 -0
- package/dist/scanners/external/scaffold/signal_envelope.py +177 -34
- package/dist/scanners/external/scaffold/state_manager.py +175 -0
- package/dist/scanners/external-scanner-receiver.d.ts +6 -0
- package/dist/scanners/external-scanner-receiver.d.ts.map +1 -1
- package/dist/scanners/external-scanner-receiver.js +14 -1
- package/dist/scanners/external-scanner-receiver.js.map +1 -1
- package/dist/scripts/run-dsl-standalone-live.d.ts +1 -1
- package/dist/scripts/run-dsl-standalone-live.js +5 -3
- package/dist/scripts/run-dsl-standalone-live.js.map +1 -1
- package/dist/senpi/client.d.ts +37 -4
- package/dist/senpi/client.d.ts.map +1 -1
- package/dist/senpi/client.js +101 -24
- package/dist/senpi/client.js.map +1 -1
- package/dist/state/state-manager.d.ts +17 -0
- package/dist/state/state-manager.d.ts.map +1 -1
- package/dist/state/state-manager.js +50 -4
- package/dist/state/state-manager.js.map +1 -1
- package/dist/strategy/strategy-state.d.ts +9 -1
- package/dist/strategy/strategy-state.d.ts.map +1 -1
- package/dist/strategy/strategy-state.js +62 -9
- package/dist/strategy/strategy-state.js.map +1 -1
- package/dist/tracking/constants.d.ts.map +1 -1
- package/dist/tracking/constants.js +2 -0
- package/dist/tracking/constants.js.map +1 -1
- package/dist/types/dsl/index.d.ts +1 -1
- package/dist/types/dsl/index.d.ts.map +1 -1
- package/dist/types/scanner.d.ts +7 -0
- package/dist/types/scanner.d.ts.map +1 -1
- package/dist/types/strategy.d.ts +3 -1
- package/dist/types/strategy.d.ts.map +1 -1
- package/dist/utils/keyed-mutex.d.ts +37 -0
- package/dist/utils/keyed-mutex.d.ts.map +1 -0
- package/dist/utils/keyed-mutex.js +77 -0
- package/dist/utils/keyed-mutex.js.map +1 -0
- package/examples/strategies/README.md +17 -12
- package/examples/strategies/iguana/README.md +135 -0
- package/examples/strategies/iguana/recipe.yaml +137 -0
- package/examples/strategies/iguana/scanners/scan.py +243 -0
- package/examples/strategies/iguana/scanners/scoring.py +152 -0
- package/examples/strategies/raptor/README.md +156 -0
- package/examples/strategies/raptor/recipe.yaml +154 -0
- package/examples/strategies/raptor/scanners/scan.py +349 -0
- package/examples/strategies/raptor/scanners/scoring.py +452 -0
- package/examples/strategies/spider/README.md +89 -0
- package/examples/strategies/spider/scalp/recipe.yaml +129 -0
- package/examples/strategies/spider/scalp/scanners/scan.py +219 -0
- package/examples/strategies/spider/scalp/scanners/scoring.py +179 -0
- package/examples/strategies/spider/swing/recipe.yaml +137 -0
- package/examples/strategies/spider/swing/scanners/scan.py +334 -0
- package/examples/strategies/spider/swing/scanners/scoring.py +184 -0
- package/examples/strategies/turbine/README.md +166 -0
- package/examples/strategies/turbine/recipe-runners.yaml +123 -0
- package/examples/strategies/turbine/recipe-volume.yaml +129 -0
- package/examples/strategies/turbine/scanners/scan.py +283 -0
- package/examples/strategies/turbine/scanners/scoring.py +190 -0
- package/package.json +1 -1
- package/dist/scanners/external/liquidation-watchdog/README.md +0 -107
- package/dist/scanners/external/liquidation-watchdog/liquidation-watchdog.yaml +0 -48
- package/dist/scanners/external/liquidation-watchdog/producer.mjs +0 -336
- package/dist/scanners/external/momentum/README.md +0 -148
- package/dist/scanners/external/momentum/momentum.yaml +0 -116
- package/dist/scanners/external/momentum/producer.mjs +0 -251
- package/dist/scanners/external/shared/ingest.mjs +0 -112
- package/examples/README.md +0 -89
- package/examples/scanners-consumer-quickstart.mjs +0 -134
- package/examples/scanners-emerging-movers-v4.mjs +0 -287
- package/examples/scanners-liquidation-watchdog.mjs +0 -341
- package/examples/scanners-live-mcp.mjs +0 -208
- package/examples/scanners-multi-strategy.mjs +0 -165
- package/examples/scanners-position-tracker-e2e.mjs +0 -225
- package/examples/scanners-scheduled-events.mjs +0 -96
- package/examples/scanners-senpi-provider-client.mjs +0 -182
- package/examples/scanners-single-strategy.mjs +0 -198
- package/examples/strategies/dsl-showcase.yaml +0 -68
- package/examples/strategies/dsl.yaml +0 -51
- package/examples/strategies/fox-minimal-loosened.yaml +0 -240
- package/examples/strategies/fox-minimal.yaml +0 -242
- package/examples/strategies/minimal.yaml +0 -46
- package/examples/strategies/momentum/how-it-works.md +0 -48
- package/examples/strategies/momentum/momentum-guarded.yaml +0 -161
- package/examples/strategies/risk-showcase.yaml +0 -93
- package/examples/strategies/viper.yaml +0 -67
- package/examples/strategies/wolf.yaml +0 -72
- package/examples/yamls/sample-with-dsl-and-emerging-movers.yaml +0 -123
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
# External momentum producer
|
|
2
|
-
|
|
3
|
-
A standalone momentum scanner that runs as an external producer:
|
|
4
|
-
|
|
5
|
-
1. Pulls instruments + latest candles from the live Senpi MCP server.
|
|
6
|
-
2. Computes last-vs-prev close % move per candidate using a self-contained
|
|
7
|
-
port of the momentum scanner logic (no dependency on the runtime's internal
|
|
8
|
-
`momentumScanner`).
|
|
9
|
-
3. Pushes resulting `MOMENTUM_BREAKOUT` signals into a running runtime via the
|
|
10
|
-
`senpi.ingestExternalScannerData` gateway RPC.
|
|
11
|
-
|
|
12
|
-
The companion recipe wires those signals into an LLM `OPEN_POSITION` action,
|
|
13
|
-
demonstrating the full e2e path from external signal → LLM gauntlet →
|
|
14
|
-
Senpi MCP position open → DSL trailing-exit management.
|
|
15
|
-
|
|
16
|
-
## Files
|
|
17
|
-
|
|
18
|
-
- `momentum.yaml` — runtime recipe declaring the matching `external_scanner`
|
|
19
|
-
(`external_momentum`), the `momentum_entry` LLM OPEN_POSITION action, and
|
|
20
|
-
a DSL exit preset tuned for short-horizon momentum trades.
|
|
21
|
-
- `producer.mjs` — the external producer entrypoint.
|
|
22
|
-
|
|
23
|
-
## Prereqs
|
|
24
|
-
|
|
25
|
-
1. Build the project so the MCP provider bridge is available:
|
|
26
|
-
```bash
|
|
27
|
-
npm run build
|
|
28
|
-
```
|
|
29
|
-
2. Install the runtime from `momentum.yaml` for your strategy wallet and start it:
|
|
30
|
-
```bash
|
|
31
|
-
openclaw senpi runtime create -p src/scanners/external/momentum/momentum.yaml
|
|
32
|
-
```
|
|
33
|
-
3. Ensure `openclaw` is on `PATH` and authenticated against the host running
|
|
34
|
-
that runtime.
|
|
35
|
-
4. Export Senpi MCP credentials and the target wallet:
|
|
36
|
-
```bash
|
|
37
|
-
export SENPI_API_KEY="..."
|
|
38
|
-
export SENPI_MCP_URL="https://mcp.prod.senpi.ai/mcp"
|
|
39
|
-
export STRATEGY_ADDRESS="0x..."
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Run
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
node src/scanners/external/momentum/producer.mjs
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The producer fetches one snapshot (instruments + candles), derives signals,
|
|
49
|
-
and pushes them. It exits non-zero on any rejection or signal-count mismatch.
|
|
50
|
-
Re-run (or wrap in `watch`/cron) for continuous monitoring.
|
|
51
|
-
|
|
52
|
-
## Schedule with OpenClaw cron
|
|
53
|
-
|
|
54
|
-
The producer runs **once per invocation** — it fetches a snapshot, pushes
|
|
55
|
-
signals, and exits. For continuous monitoring you need a scheduler. The
|
|
56
|
-
recommended approach is to register an OpenClaw cron immediately after
|
|
57
|
-
creating the runtime strategy, and let your agent wire it up.
|
|
58
|
-
|
|
59
|
-
Ask your agent something like:
|
|
60
|
-
|
|
61
|
-
> Set up an OpenClaw cron that runs
|
|
62
|
-
> `src/scanners/external/momentum/producer.mjs` every 5 minutes, with
|
|
63
|
-
> wake now, in an isolated session, and capture output to a dedicated log
|
|
64
|
-
> file named `external_momentum.log` for debugging.
|
|
65
|
-
|
|
66
|
-
That request maps to these properties on the cron:
|
|
67
|
-
|
|
68
|
-
- **Command:** `node /absolute/path/to/src/scanners/external/momentum/producer.mjs`
|
|
69
|
-
(export the same `SENPI_*` and `STRATEGY_ADDRESS` env vars the manual run uses).
|
|
70
|
-
- **Interval:** every X minutes — pick a cadence that matches your
|
|
71
|
-
`TIMEFRAME` (e.g. `*/5 * * * *` for `1h` candles is plenty; tighten only
|
|
72
|
-
if you actually need faster reactions).
|
|
73
|
-
- **Wake now:** trigger an immediate first run on creation so you don't
|
|
74
|
-
wait a full interval to confirm wiring.
|
|
75
|
-
- **Isolated session:** each tick runs in its own session so a hung or
|
|
76
|
-
long-running invocation can't block the next one.
|
|
77
|
-
- **Log capture:** redirect stdout/stderr to `<scanner-name>.log` (here:
|
|
78
|
-
`external_momentum.log`) so each run's accept/reject output and any
|
|
79
|
-
errors are inspectable after the fact.
|
|
80
|
-
|
|
81
|
-
Verify by tailing the log after the first wake-now tick — you should see
|
|
82
|
-
the same `accepted: true` / `signalCount` payload shown above. If signals
|
|
83
|
-
aren't flowing, check the log first; producer rejections and missing env
|
|
84
|
-
vars surface there.
|
|
85
|
-
|
|
86
|
-
## Env
|
|
87
|
-
|
|
88
|
-
| Var | Default | Purpose |
|
|
89
|
-
|---|---|---|
|
|
90
|
-
| `SENPI_API_KEY` | _required_ | Senpi MCP auth |
|
|
91
|
-
| `SENPI_MCP_URL` | _required_ | Senpi MCP endpoint |
|
|
92
|
-
| `STRATEGY_ADDRESS` | _required_ | Target wallet — must match the installed runtime |
|
|
93
|
-
| `EXTERNAL_SCANNER_NAME` | `external_momentum` | Must match `scanners[].name` in the recipe |
|
|
94
|
-
| `OPENCLAW_BIN` | `openclaw` | Path/name of the OpenClaw CLI binary |
|
|
95
|
-
| `TIMEFRAME` | `1h` | Candle interval to evaluate |
|
|
96
|
-
| `MIN_MOVE_PCT` | `1.5` | Minimum absolute % move to emit a signal |
|
|
97
|
-
| `MIN_DAY_VOLUME` | `1000000` | Minimum 24h notional volume to consider an instrument |
|
|
98
|
-
| `MIN_LEVERAGE` | `5` | Minimum max leverage for an instrument |
|
|
99
|
-
| `MAX_CANDIDATES` | `20` | Top-N by volume to fetch candles for |
|
|
100
|
-
| `MIN_SIGNAL_SCORE` | `0.2` | Drop signals below this normalized score |
|
|
101
|
-
|
|
102
|
-
## Expected response
|
|
103
|
-
|
|
104
|
-
```json
|
|
105
|
-
{
|
|
106
|
-
"accepted": true,
|
|
107
|
-
"address": "0x...",
|
|
108
|
-
"scanner": "external_momentum",
|
|
109
|
-
"timestamp": 1775680200000,
|
|
110
|
-
"signalCount": 3,
|
|
111
|
-
"contextUpdated": false
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Live test: force an OPEN_POSITION
|
|
116
|
-
|
|
117
|
-
To verify the end-to-end entry path, loosen thresholds so almost any moving
|
|
118
|
-
asset trips a signal, then let the LLM gauntlet in `momentum.yaml` decide:
|
|
119
|
-
|
|
120
|
-
```bash
|
|
121
|
-
MIN_MOVE_PCT=0.05 \
|
|
122
|
-
MIN_SIGNAL_SCORE=0 \
|
|
123
|
-
MIN_DAY_VOLUME=0 \
|
|
124
|
-
node src/scanners/external/momentum/producer.mjs
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Expect `accepted: true` with `signalCount >= 1`. In the runtime logs you
|
|
128
|
-
should see `momentum_entry` firing, the LLM returning `execute: true` for
|
|
129
|
-
the strongest-scoring eligible signal, and Senpi MCP opening the position.
|
|
130
|
-
The DSL preset in `momentum.yaml` then takes over for trailing-exit
|
|
131
|
-
management. Revert the env values before running unattended.
|
|
132
|
-
|
|
133
|
-
## Manual e2e checklist
|
|
134
|
-
|
|
135
|
-
- Happy path: live wallet, no strong movers → producer prints `0` signals
|
|
136
|
-
and skips the ingest.
|
|
137
|
-
- Triggered path: a high-volume asset with a notable 1h move → producer
|
|
138
|
-
emits `MOMENTUM_BREAKOUT`; gateway returns `accepted: true` with matching
|
|
139
|
-
`signalCount`; runtime invokes the LLM action; position opens if the
|
|
140
|
-
gauntlet passes.
|
|
141
|
-
- DSL handoff: after a fill, confirm `openclaw senpi position list` shows
|
|
142
|
-
the open position and that the DSL monitor is tracking it.
|
|
143
|
-
- Unknown wallet: change `STRATEGY_ADDRESS` to a wallet without an installed
|
|
144
|
-
runtime → expect `NOT_FOUND`.
|
|
145
|
-
- Disabled scanner: stop the runtime or rename the scanner in the recipe →
|
|
146
|
-
expect `INVALID_REQUEST`.
|
|
147
|
-
- Schema rejection: edit `toExternalBatchItem` in `producer.mjs` to drop
|
|
148
|
-
`sourceMeta` → expect a validation `INVALID_REQUEST`.
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
name: external-momentum
|
|
2
|
-
version: 1.0.0
|
|
3
|
-
description: >
|
|
4
|
-
External producer variant of the momentum scanner. Latest-candle breakout
|
|
5
|
-
detection runs out-of-process (see producer.mjs) and pushes signals in via
|
|
6
|
-
senpi.ingestExternalScannerData. An LLM gauntlet filters breakouts into
|
|
7
|
-
OPEN_POSITION decisions; the DSL exit engine manages trailing exits.
|
|
8
|
-
|
|
9
|
-
strategy:
|
|
10
|
-
wallet: "${WALLET_ADDRESS}"
|
|
11
|
-
budget: 500
|
|
12
|
-
slots: 2
|
|
13
|
-
margin_per_slot: 200
|
|
14
|
-
trading_risk: moderate
|
|
15
|
-
enabled: true
|
|
16
|
-
|
|
17
|
-
scanners:
|
|
18
|
-
# Onchain position tracker — emits position lifecycle events into the DSL
|
|
19
|
-
# exit engine for trailing stop-loss management.
|
|
20
|
-
- name: position_tracker
|
|
21
|
-
type: position_tracker
|
|
22
|
-
interval: 10s
|
|
23
|
-
|
|
24
|
-
# External scanner: signals are pushed by producer.mjs on its own cadence.
|
|
25
|
-
# `fields` documents the payload shape the gateway will accept.
|
|
26
|
-
- name: external_momentum
|
|
27
|
-
type: external_scanner
|
|
28
|
-
outputs:
|
|
29
|
-
signals: true
|
|
30
|
-
context: false
|
|
31
|
-
config:
|
|
32
|
-
fields:
|
|
33
|
-
sourceScannerId: { type: string, required: true }
|
|
34
|
-
sourceSignalType: { type: string, required: true }
|
|
35
|
-
sourceTimestamp: { type: number, required: true }
|
|
36
|
-
sourceFactors: { type: object, required: true }
|
|
37
|
-
sourceMeta: { type: object, required: true }
|
|
38
|
-
|
|
39
|
-
actions:
|
|
40
|
-
# Position lifecycle tracking — feeds DSL exit engine.
|
|
41
|
-
- name: position_tracker_action
|
|
42
|
-
action_type: POSITION_TRACKER
|
|
43
|
-
decision_mode: rule
|
|
44
|
-
scanners: [position_tracker]
|
|
45
|
-
|
|
46
|
-
# Entry: LLM gauntlet on momentum breakout signals.
|
|
47
|
-
- name: momentum_entry
|
|
48
|
-
action_type: OPEN_POSITION
|
|
49
|
-
decision_mode: llm
|
|
50
|
-
decision_model: gpt-5.1-codex
|
|
51
|
-
scanners: [external_momentum]
|
|
52
|
-
min_confidence: 6
|
|
53
|
-
context:
|
|
54
|
-
- type: signal
|
|
55
|
-
scanner: external_momentum
|
|
56
|
-
decision_prompt: |
|
|
57
|
-
You are a trade-decisioning evaluator. Given a MOMENTUM_BREAKOUT
|
|
58
|
-
signal, decide whether to open a position on it or skip.
|
|
59
|
-
|
|
60
|
-
A MOMENTUM_BREAKOUT fires when an asset's latest closed candle moves
|
|
61
|
-
by at least a configured threshold vs. the previous close on the
|
|
62
|
-
signal's timeframe — i.e. a short-term directional breakout in price.
|
|
63
|
-
`meta.movePct` carries the signed % change (positive = LONG bias,
|
|
64
|
-
negative = SHORT bias); `score` (0..1) scales with the move's
|
|
65
|
-
magnitude; `meta.dayNtlVlm` and `meta.maxLeverage` describe the
|
|
66
|
-
instrument's liquidity and leverage headroom.
|
|
67
|
-
|
|
68
|
-
SIGNAL DATA:
|
|
69
|
-
{{signal_external_momentum}}
|
|
70
|
-
|
|
71
|
-
═══ GAUNTLET ═══
|
|
72
|
-
1. movePct magnitude — require |movePct| >= 1.5; reject sub-threshold noise.
|
|
73
|
-
2. Direction sanity — LONG only if movePct > 0, SHORT only if movePct < 0.
|
|
74
|
-
3. Liquidity — meta.dayNtlVlm must be >= 1_000_000.
|
|
75
|
-
4. Leverage — meta.maxLeverage must be >= 5; skip otherwise.
|
|
76
|
-
5. Score — use the producer score (0..1) as a proxy for strength; require >= 0.4.
|
|
77
|
-
6. One signal at a time — pick the highest-score eligible signal; skip the rest.
|
|
78
|
-
|
|
79
|
-
Respond with a single JSON object:
|
|
80
|
-
{
|
|
81
|
-
"execute": true|false,
|
|
82
|
-
"actionType": "OPEN_POSITION",
|
|
83
|
-
"confidence": 1-10,
|
|
84
|
-
"reasoning": "one sentence",
|
|
85
|
-
"payload": {
|
|
86
|
-
"signals": [
|
|
87
|
-
{ "asset": "<ticker>", "direction": "LONG"|"SHORT", "reason": "one sentence" }
|
|
88
|
-
]
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
# ── EXIT (DSL) ──
|
|
93
|
-
# Trailing stop-loss tuned for short-horizon momentum entries: a slightly
|
|
94
|
-
# tighter Phase-1 fixed floor than Fox (momentum reversals are fast), the same
|
|
95
|
-
# 7% retrace trigger so winners get room to breathe, and a four-tier Phase-2
|
|
96
|
-
# ladder that locks in progressively more of the high-water as gains extend.
|
|
97
|
-
exit:
|
|
98
|
-
engine: dsl
|
|
99
|
-
interval_seconds: 30
|
|
100
|
-
dsl_preset:
|
|
101
|
-
phase1:
|
|
102
|
-
enabled: true
|
|
103
|
-
max_loss_pct: 2.5
|
|
104
|
-
retrace_threshold: 7
|
|
105
|
-
consecutive_breaches_required: 3
|
|
106
|
-
phase2:
|
|
107
|
-
enabled: true
|
|
108
|
-
tiers:
|
|
109
|
-
- { trigger_pct: 6, lock_hw_pct: 35 }
|
|
110
|
-
- { trigger_pct: 10, lock_hw_pct: 55 }
|
|
111
|
-
- { trigger_pct: 15, lock_hw_pct: 70 }
|
|
112
|
-
- { trigger_pct: 20, lock_hw_pct: 85 }
|
|
113
|
-
|
|
114
|
-
# ── NOTIFICATIONS ──
|
|
115
|
-
notifications:
|
|
116
|
-
telegram_chat_id: "${TELEGRAM_CHAT_ID}"
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External momentum producer.
|
|
3
|
-
*
|
|
4
|
-
* A standalone momentum scanner that:
|
|
5
|
-
* 1. Pulls instruments + latest candles from the live Senpi MCP server.
|
|
6
|
-
* 2. Computes last-vs-prev close % move against configurable thresholds.
|
|
7
|
-
* 3. Pushes MOMENTUM_BREAKOUT signals into a running runtime via the
|
|
8
|
-
* `openclaw senpi external-scanner ingest` CLI command.
|
|
9
|
-
*
|
|
10
|
-
* Prereqs:
|
|
11
|
-
* - `npm run build` so dist/ is current (used only for the MCP provider bridge).
|
|
12
|
-
* - A runtime installed from ./momentum.yaml is running for STRATEGY_ADDRESS.
|
|
13
|
-
* - `openclaw` CLI on PATH and authenticated against that runtime.
|
|
14
|
-
* - SENPI_API_KEY and SENPI_MCP_URL env vars set.
|
|
15
|
-
*
|
|
16
|
-
* Run: node src/scanners/external/momentum/producer.mjs
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
20
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
21
|
-
import { createSenpiScannerProviders } from "../../../../dist/scanners/index.js";
|
|
22
|
-
import { pushScannerSignals } from "../shared/ingest.mjs";
|
|
23
|
-
|
|
24
|
-
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
const SENPI_API_KEY = process.env.SENPI_API_KEY;
|
|
27
|
-
const SENPI_MCP_URL = process.env.SENPI_MCP_URL;
|
|
28
|
-
const STRATEGY_ADDRESS = process.env.STRATEGY_ADDRESS;
|
|
29
|
-
const SCANNER_NAME =
|
|
30
|
-
process.env.EXTERNAL_SCANNER_NAME ?? "external_momentum";
|
|
31
|
-
const OPENCLAW_BIN = process.env.OPENCLAW_BIN ?? "openclaw";
|
|
32
|
-
const SCANNER_ID = "external-momentum-producer";
|
|
33
|
-
|
|
34
|
-
const config = {
|
|
35
|
-
timeframe: process.env.TIMEFRAME ?? "1h",
|
|
36
|
-
minMovePct: numEnv("MIN_MOVE_PCT", 1.5),
|
|
37
|
-
minDayVolume: numEnv("MIN_DAY_VOLUME", 1_000_000),
|
|
38
|
-
minLeverage: numEnv("MIN_LEVERAGE", 5),
|
|
39
|
-
maxCandidates: numEnv("MAX_CANDIDATES", 20),
|
|
40
|
-
minSignalScore: numEnv("MIN_SIGNAL_SCORE", 0.2),
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
if (!SENPI_API_KEY || !SENPI_MCP_URL || !STRATEGY_ADDRESS) {
|
|
44
|
-
console.error(
|
|
45
|
-
"Missing required env vars. Set SENPI_API_KEY, SENPI_MCP_URL, and STRATEGY_ADDRESS.\n" +
|
|
46
|
-
" export SENPI_API_KEY='...'\n" +
|
|
47
|
-
" export SENPI_MCP_URL='https://mcp.prod.senpi.ai/mcp'\n" +
|
|
48
|
-
" export STRATEGY_ADDRESS='0x...'",
|
|
49
|
-
);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function numEnv(name, fallback) {
|
|
54
|
-
const raw = process.env[name];
|
|
55
|
-
if (raw === undefined || raw === "") return fallback;
|
|
56
|
-
const parsed = Number(raw);
|
|
57
|
-
if (!Number.isFinite(parsed)) {
|
|
58
|
-
throw new Error(`Env ${name} must be a finite number, got "${raw}"`);
|
|
59
|
-
}
|
|
60
|
-
return parsed;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ─── MCP → SenpiToolClient bridge ───────────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
function createSenpiToolClient(mcpClient) {
|
|
66
|
-
return {
|
|
67
|
-
async callTool(name, args) {
|
|
68
|
-
const result = await mcpClient.callTool({ name, arguments: args });
|
|
69
|
-
|
|
70
|
-
if (result.isError) {
|
|
71
|
-
const errorText =
|
|
72
|
-
Array.isArray(result.content) &&
|
|
73
|
-
result.content.length > 0 &&
|
|
74
|
-
result.content[0].type === "text"
|
|
75
|
-
? result.content[0].text
|
|
76
|
-
: JSON.stringify(result.content);
|
|
77
|
-
throw new Error(`MCP tool "${name}" returned error: ${errorText}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (result.structuredContent !== undefined) {
|
|
81
|
-
return result.structuredContent;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (Array.isArray(result.content) && result.content.length > 0) {
|
|
85
|
-
const first = result.content[0];
|
|
86
|
-
if (first.type === "text") {
|
|
87
|
-
try {
|
|
88
|
-
return JSON.parse(first.text);
|
|
89
|
-
} catch {
|
|
90
|
-
return first.text;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return result.content;
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ─── Momentum logic ─────────────────────────────────────────────────────────
|
|
101
|
-
// Self-contained port of src/scanners/implementations/momentum.ts. Kept free
|
|
102
|
-
// of any runtime-internal imports.
|
|
103
|
-
|
|
104
|
-
function clamp01(value) {
|
|
105
|
-
if (!Number.isFinite(value)) return 0;
|
|
106
|
-
if (value < 0) return 0;
|
|
107
|
-
if (value > 1) return 1;
|
|
108
|
-
return value;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function selectCandidates(instruments, cfg) {
|
|
112
|
-
const eligible = instruments.filter((inst) => {
|
|
113
|
-
if (inst.isDelisted) return false;
|
|
114
|
-
if ((inst.maxLeverage ?? 0) < cfg.minLeverage) return false;
|
|
115
|
-
const vol = inst.context?.dayNtlVlm ?? 0;
|
|
116
|
-
return vol >= cfg.minDayVolume;
|
|
117
|
-
});
|
|
118
|
-
eligible.sort(
|
|
119
|
-
(a, b) => (b.context?.dayNtlVlm ?? 0) - (a.context?.dayNtlVlm ?? 0),
|
|
120
|
-
);
|
|
121
|
-
return eligible.slice(0, cfg.maxCandidates);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function computeMovePct(series) {
|
|
125
|
-
if (!series || series.length < 2) return null;
|
|
126
|
-
const prev = series[series.length - 2]?.close;
|
|
127
|
-
const last = series[series.length - 1]?.close;
|
|
128
|
-
if (!Number.isFinite(prev) || !Number.isFinite(last) || prev <= 0) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
return { prev, last, movePct: ((last - prev) / prev) * 100 };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function buildSignal({ instrument, move, timeframe, now }) {
|
|
135
|
-
const movePct = move.movePct;
|
|
136
|
-
const absMove = Math.abs(movePct);
|
|
137
|
-
const direction = movePct > 0 ? "LONG" : "SHORT";
|
|
138
|
-
// Score scales from 0 at minMovePct to 1 at 5% move, clamped.
|
|
139
|
-
const score = clamp01((absMove - config.minMovePct) / (5 - config.minMovePct));
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
scannerId: SCANNER_ID,
|
|
143
|
-
signalType: "MOMENTUM_BREAKOUT",
|
|
144
|
-
asset: instrument.name,
|
|
145
|
-
direction,
|
|
146
|
-
score,
|
|
147
|
-
timestamp: now,
|
|
148
|
-
factors: {
|
|
149
|
-
breakout: true,
|
|
150
|
-
large_move: absMove >= 3,
|
|
151
|
-
},
|
|
152
|
-
meta: {
|
|
153
|
-
timeframe,
|
|
154
|
-
movePct,
|
|
155
|
-
prev: move.prev,
|
|
156
|
-
last: move.last,
|
|
157
|
-
dayNtlVlm: instrument.context?.dayNtlVlm ?? 0,
|
|
158
|
-
maxLeverage: instrument.maxLeverage,
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
164
|
-
|
|
165
|
-
async function main() {
|
|
166
|
-
console.log(`External momentum producer`);
|
|
167
|
-
console.log(` MCP: ${SENPI_MCP_URL}`);
|
|
168
|
-
console.log(` address: ${STRATEGY_ADDRESS}`);
|
|
169
|
-
console.log(` scanner: ${SCANNER_NAME}`);
|
|
170
|
-
console.log(` config: ${JSON.stringify(config)}`);
|
|
171
|
-
|
|
172
|
-
const transport = new StreamableHTTPClientTransport(new URL(SENPI_MCP_URL), {
|
|
173
|
-
requestInit: { headers: { Authorization: `Bearer ${SENPI_API_KEY}` } },
|
|
174
|
-
});
|
|
175
|
-
const mcpClient = new Client({
|
|
176
|
-
name: "external-momentum",
|
|
177
|
-
version: "0.0.1",
|
|
178
|
-
});
|
|
179
|
-
await mcpClient.connect(transport);
|
|
180
|
-
|
|
181
|
-
const providers = createSenpiScannerProviders({
|
|
182
|
-
client: createSenpiToolClient(mcpClient),
|
|
183
|
-
logLevel: "warn",
|
|
184
|
-
cacheTtlMs: { instruments: 0, smData: 0, candles: 0 },
|
|
185
|
-
candleIntervals: [config.timeframe],
|
|
186
|
-
maxCandleAssets: config.maxCandidates,
|
|
187
|
-
retries: 2,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
try {
|
|
191
|
-
const instruments = await providers.getInstruments(STRATEGY_ADDRESS);
|
|
192
|
-
const candidates = selectCandidates(instruments, config);
|
|
193
|
-
console.log(
|
|
194
|
-
`\nInstruments: ${instruments.length}, candidates: ${candidates.length}`,
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
if (candidates.length === 0) {
|
|
198
|
-
console.log("No candidates after filtering — skipping ingest.");
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const candles = await providers.getCandles(STRATEGY_ADDRESS, candidates, {
|
|
203
|
-
scannerId: SCANNER_ID,
|
|
204
|
-
preferredAssets: candidates.map((c) => c.name),
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
const now = Date.now();
|
|
208
|
-
const signals = [];
|
|
209
|
-
for (const inst of candidates) {
|
|
210
|
-
const series = candles[inst.name]?.candles?.[config.timeframe];
|
|
211
|
-
const move = computeMovePct(series);
|
|
212
|
-
if (!move) continue;
|
|
213
|
-
if (Math.abs(move.movePct) < config.minMovePct) continue;
|
|
214
|
-
const signal = buildSignal({
|
|
215
|
-
instrument: inst,
|
|
216
|
-
move,
|
|
217
|
-
timeframe: config.timeframe,
|
|
218
|
-
now,
|
|
219
|
-
});
|
|
220
|
-
if (signal.score < config.minSignalScore) continue;
|
|
221
|
-
signals.push(signal);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
console.log(`\nSignals: ${signals.length}`);
|
|
225
|
-
for (const s of signals) {
|
|
226
|
-
console.log(
|
|
227
|
-
` ${s.asset} ${s.direction} move=${s.meta.movePct.toFixed(2)}% score=${s.score.toFixed(2)}`,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (signals.length === 0) {
|
|
232
|
-
console.log("\nNothing to ingest — skipping gateway call.");
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const response = await pushScannerSignals({
|
|
237
|
-
address: STRATEGY_ADDRESS,
|
|
238
|
-
scanner: SCANNER_NAME,
|
|
239
|
-
signals,
|
|
240
|
-
openclawBin: OPENCLAW_BIN,
|
|
241
|
-
});
|
|
242
|
-
console.log("\nIngest response:", JSON.stringify(response));
|
|
243
|
-
} finally {
|
|
244
|
-
await transport.close();
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
main().catch((error) => {
|
|
249
|
-
console.error("\nProducer failed:", error.message ?? error);
|
|
250
|
-
process.exit(1);
|
|
251
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared ingest helpers for external-scanner producers under
|
|
3
|
-
* `src/scanners/external/`.
|
|
4
|
-
*
|
|
5
|
-
* Producers all need the same two things:
|
|
6
|
-
* 1. Normalize an in-producer signal into the external batch item shape
|
|
7
|
-
* expected by `senpi.ingestExternalScannerData`.
|
|
8
|
-
* 2. Shell out to the openclaw CLI to actually push the batch and parse the
|
|
9
|
-
* response.
|
|
10
|
-
*
|
|
11
|
-
* Keeping this in one place means producers never diverge on the wire format
|
|
12
|
-
* or response-validation rules, and a future CLI contract change only needs to
|
|
13
|
-
* be made here.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { execFile } from "node:child_process";
|
|
17
|
-
import { promisify } from "node:util";
|
|
18
|
-
|
|
19
|
-
const execFileAsync = promisify(execFile);
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Convert a producer-native signal object into the batch item shape consumed
|
|
23
|
-
* by the external-scanner ingest CLI/RPC.
|
|
24
|
-
*
|
|
25
|
-
* Producers emit richer signal objects (scannerId/factors/meta/timestamp) but
|
|
26
|
-
* the runtime only needs the top-level routing fields plus an opaque `data`
|
|
27
|
-
* blob — everything else is preserved under `data.source*` so downstream
|
|
28
|
-
* consumers can reconstruct the original context.
|
|
29
|
-
*/
|
|
30
|
-
export function toExternalBatchItem(signal) {
|
|
31
|
-
return {
|
|
32
|
-
asset: signal.asset,
|
|
33
|
-
direction: signal.direction,
|
|
34
|
-
score: signal.score,
|
|
35
|
-
signal_type: signal.signalType,
|
|
36
|
-
data: {
|
|
37
|
-
sourceScannerId: signal.scannerId,
|
|
38
|
-
sourceSignalType: signal.signalType,
|
|
39
|
-
sourceTimestamp: signal.timestamp,
|
|
40
|
-
sourceFactors: signal.factors,
|
|
41
|
-
sourceMeta: signal.meta,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Push a batch of scanner signals from an external producer into a running
|
|
48
|
-
* runtime via the `openclaw senpi external-scanner ingest` CLI command.
|
|
49
|
-
*
|
|
50
|
-
* Named from the producer's point of view: the scanner executing this code
|
|
51
|
-
* is pushing its signals out to the senpi-trading-runtime service.
|
|
52
|
-
*
|
|
53
|
-
* The CLI emits pure JSON on stdout:
|
|
54
|
-
* - success: { ok: true, result: { accepted, signalCount, ... } }
|
|
55
|
-
* - failure: { ok: false, error: { code, message } }
|
|
56
|
-
*
|
|
57
|
-
* @param {object} opts
|
|
58
|
-
* @param {string} opts.address Strategy wallet address.
|
|
59
|
-
* @param {string} opts.scanner Configured external_scanner name.
|
|
60
|
-
* @param {Array<object>} opts.signals Producer-native signals (will be
|
|
61
|
-
* normalized via `toExternalBatchItem`).
|
|
62
|
-
* @param {string} [opts.openclawBin] Path or name of the openclaw CLI
|
|
63
|
-
* (defaults to `"openclaw"`).
|
|
64
|
-
* @returns {Promise<object>} The inner `result` payload from the
|
|
65
|
-
* gateway on success.
|
|
66
|
-
*/
|
|
67
|
-
export async function pushScannerSignals({
|
|
68
|
-
address,
|
|
69
|
-
scanner,
|
|
70
|
-
signals,
|
|
71
|
-
openclawBin = "openclaw",
|
|
72
|
-
}) {
|
|
73
|
-
const payload = {
|
|
74
|
-
signals: signals.map(toExternalBatchItem),
|
|
75
|
-
};
|
|
76
|
-
const { stdout } = await execFileAsync(openclawBin, [
|
|
77
|
-
"senpi",
|
|
78
|
-
"external-scanner",
|
|
79
|
-
"ingest",
|
|
80
|
-
"--address",
|
|
81
|
-
address,
|
|
82
|
-
"--scanner",
|
|
83
|
-
scanner,
|
|
84
|
-
"--payload",
|
|
85
|
-
JSON.stringify(payload),
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
let response;
|
|
89
|
-
try {
|
|
90
|
-
response = JSON.parse(stdout);
|
|
91
|
-
} catch {
|
|
92
|
-
throw new Error(`CLI returned non-JSON response:\n${stdout}`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (response?.ok !== true) {
|
|
96
|
-
const err = response?.error ?? {};
|
|
97
|
-
const code = err.code ?? "UNKNOWN";
|
|
98
|
-
const message = err.message ?? JSON.stringify(response);
|
|
99
|
-
throw new Error(`CLI rejected ingest (${code}): ${message}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const result = response.result ?? {};
|
|
103
|
-
if (!result.accepted) {
|
|
104
|
-
throw new Error(`CLI did not accept ingest: ${JSON.stringify(result)}`);
|
|
105
|
-
}
|
|
106
|
-
if (result.signalCount !== signals.length) {
|
|
107
|
-
throw new Error(
|
|
108
|
-
`Expected signalCount=${signals.length}, got ${result.signalCount}`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|