@eventmodelers/cli 0.0.39 → 1.0.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/README.md +116 -69
- package/cli.js +98 -32
- package/package.json +4 -2
- package/shared/build-kit/lib/adapters/pocketbase-realtime-adapter.js +29 -0
- package/shared/build-kit/lib/adapters/realtime-adapter.js +27 -0
- package/shared/build-kit/lib/adapters/supabase-realtime-adapter.js +24 -0
- package/shared/build-kit/lib/ralph.js +30 -31
- package/shared/build-kit/package.json +3 -1
- package/shared/build-kit/ralph-claude.js +63 -5
- package/shared/skills/connect/SKILL.md +52 -5
- package/shared/skills/learn-eventmodelers-api/SKILL.md +86 -3
- package/shared/skills/load-slice/SKILL.md +16 -0
- package/shared/skills/update-slice-status/SKILL.md +15 -5
- package/stacks/modeling-kit/templates/.claude/skills/add-next-slice/SKILL.md +86 -0
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +28 -0
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +46 -4
- package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +77 -45
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +99 -13
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +21 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +29 -3
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +71 -11
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +65 -8
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +28 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +8 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +36 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +113 -26
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +14 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +12 -1
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +44 -6
- package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +27 -0
- package/stacks/modeling-kit/templates/.claude/skills/html-screen/SKILL.md +50 -5
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +171 -5
- package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +110 -10
- package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +36 -3
- package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +110 -11
- package/stacks/modeling-kit/templates/.claude/skills/update-prompt-status/SKILL.md +9 -0
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +23 -4
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +7 -3
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +13 -7
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +13 -7
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
One CLI, real-time Claude agent, and skill kit for the [Eventmodelers](https://eventmodelers.ai) platform — pick a stack, scaffold it, connect it to a board.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This CLI covers two audiences. If you just want a project connected to a board and an agent working slices, **Getting started** below is all you need — `init`, `run`, `fetch`, and a couple of read-only helpers. Everything under **Power users** is for customizing the install itself (bridging to another spec framework, CI-driven installs, multi-project config, hooks, adding a new stack) — skip it until you actually need it.
|
|
6
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
### Quick start
|
|
6
10
|
|
|
7
11
|
```bash
|
|
8
12
|
npx @eventmodelers/cli init
|
|
@@ -15,21 +19,44 @@ npx @eventmodelers/cli init --stack node # Node.js / TypeScript
|
|
|
15
19
|
npx @eventmodelers/cli init --stack supabase # Supabase
|
|
16
20
|
npx @eventmodelers/cli init --stack axon # Axon Framework (Java/Kotlin)
|
|
17
21
|
npx @eventmodelers/cli init --stack cratis-csharp # Cratis (.NET/C#)
|
|
18
|
-
npx @eventmodelers/cli init-modeling # skills + agent loop only, no backend scaffold
|
|
19
|
-
npx @eventmodelers/cli init --build-kit # blank build-kit scaffold for a stack not built into this CLI yet
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
The installer prompts for your API token, Organization ID, and Board ID from [app.eventmodelers.ai/account](https://app.eventmodelers.ai/account), scaffolds the stack into your project, and writes `.eventmodelers/config.json` with your credentials.
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
### Common scenarios
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
**Starting from scratch on a new project:**
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
|
-
npx @eventmodelers/cli init
|
|
31
|
+
npx @eventmodelers/cli init --stack node
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Answer the credential prompts once — the stack's scaffold, skills, and agent loop are all installed in this one step.
|
|
35
|
+
|
|
36
|
+
**Already initialized — you just want the agent to start working the board:**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @eventmodelers/cli run
|
|
30
40
|
```
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
`run` doesn't re-configure anything. It finds whatever kit dir `init` already created in this project and starts its agent loop (`ralph-claude.js`) against your existing `.eventmodelers/config.json`.
|
|
43
|
+
|
|
44
|
+
**Pulling the latest board state without starting an agent** — e.g. to inspect a context's slices, or in a script:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @eventmodelers/cli fetch --context <name>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This writes the current slice/event/command detail for that context to disk and prints a summary — no agent loop, no listener, just a one-shot pull.
|
|
51
|
+
|
|
52
|
+
**Checking what's installed or which credentials are active:**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx @eventmodelers/cli status # what's installed in this project
|
|
56
|
+
npx @eventmodelers/cli config # the fully resolved config (file + env), token masked
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### What gets installed
|
|
33
60
|
|
|
34
61
|
```
|
|
35
62
|
your-project/
|
|
@@ -46,9 +73,73 @@ your-project/
|
|
|
46
73
|
└── CLAUDE.md ← agent instructions
|
|
47
74
|
```
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
The four backend stacks (`node`, `supabase`, `axon`, `cratis-csharp`) also scaffold a real project skeleton into your project root (`templates/root/`) — source layout, build files, migrations, etc.
|
|
77
|
+
|
|
78
|
+
## Skills
|
|
79
|
+
|
|
80
|
+
Use skills in Claude Code with `/skill-name`:
|
|
81
|
+
|
|
82
|
+
| Skill | Description |
|
|
83
|
+
|-------|-------------|
|
|
84
|
+
| `/connect` | Set up board connection |
|
|
85
|
+
| `/timeline` | Live event storming facilitator |
|
|
86
|
+
| `/wdyt` | Business analyst review of your event model |
|
|
87
|
+
| `/storyboard` | Build a full visual storyboard |
|
|
88
|
+
| `/html-screen` | Design individual real HTML/CSS screens (default) |
|
|
89
|
+
| `/storyboard-screen` | Design individual wireframe/sketch screens (explicit request only) |
|
|
90
|
+
| `/place-element` | Place commands/events/read models on the board |
|
|
91
|
+
| `/learn-eventmodelers-api` | Full API reference for agent use |
|
|
92
|
+
| `/attributes` | Add/rename attributes across a chain of elements |
|
|
93
|
+
| `/examples` | Add example data to element fields |
|
|
94
|
+
| `/update-slice-status` | Update slice status on the board |
|
|
95
|
+
| `/load-slice` | Persist board slices to disk (backend stacks) |
|
|
96
|
+
| `/build-state-change`, `/build-state-view`, `/build-automation`, `/build-webhook` | Implement a slice's command/view/automation/webhook (backend stacks) |
|
|
97
|
+
|
|
98
|
+
Which skills install depends on the chosen stack — see `stacks/<name>/templates/.claude/skills/`. `/connect`, `/learn-eventmodelers-api`, and `/update-slice-status` have no stack-specific content and install into every stack from `shared/skills/` instead.
|
|
99
|
+
|
|
100
|
+
## Everyday commands
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx @eventmodelers/cli init --stack <name> # scaffold a stack + install + configure (alias: install)
|
|
104
|
+
npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
|
|
105
|
+
npx @eventmodelers/cli run --ollama # same, via local Ollama (ralph-ollama.js)
|
|
106
|
+
npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
|
|
107
|
+
npx @eventmodelers/cli fetch --context <name> # pull full slice detail for one context on the board into <kit-dir>/.slices/
|
|
108
|
+
npx @eventmodelers/cli fetch --context <name> --slice-id <id> # same, then print just that slice
|
|
109
|
+
npx @eventmodelers/cli fetch --context <name> --slice-title <title> # same, then print just the slice matching this title
|
|
110
|
+
npx @eventmodelers/cli stacks # list available stacks
|
|
111
|
+
npx @eventmodelers/cli status # check what's installed
|
|
112
|
+
npx @eventmodelers/cli config # print the fully resolved config (file + env), token masked
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`run` is a thin dispatcher — it just finds the installed kit dir (whatever it's named for the stack) and execs the runner file already sitting in it. The agent loop's actual logic stays in the scaffolded `<kit-dir>/`, not in this package, since you (and the agent itself, via `AGENT.md`) may customize those files per project.
|
|
116
|
+
|
|
117
|
+
`fetch` calls `slicedata?contextName=<name>` for the required `--context` (full slice detail — commands/events/readmodels/screens/processors/specifications/comments), and writes `.slices/<context>/<slice>/slice.json`, `index.json`, and `context.json`. It does not fetch screen images (those only arrive via `listen`'s push, see Power users). Unlike every other command, `fetch` also works with no kit installed at all — it only needs credentials, not kit-specific files. If credentials are missing, it prompts the same way `init-config` does. `--slice-id`/`--slice-title` still fetch and persist the whole context, then just print the one slice you asked about.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Power users
|
|
122
|
+
|
|
123
|
+
Everything below customizes *how the install itself works* — skills-only installs, bridging to another spec framework, CI-driven/non-interactive installs, multi-project config sharing, hooks that replace the AI executor, MCP registration for other harnesses, and adding a new stack. Casual usage never needs this section.
|
|
50
124
|
|
|
51
|
-
|
|
125
|
+
### Skills-only / no backend scaffold
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx @eventmodelers/cli init-modeling # skills + agent loop only, no backend scaffold
|
|
129
|
+
npx @eventmodelers/cli init --build-kit # blank build-kit scaffold for a stack not built into this CLI yet
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`init-modeling` isn't a stack — it's the option for when you don't want a backend scaffolded at all, just the skills and the agent loop. `init --build-kit` isn't one of the four either — it installs the same `.build-kit/` + skills shape as a real stack, but with TODO-marked placeholders instead of real content, for integrating a stack this CLI doesn't support yet (see "Adding a stack" below).
|
|
133
|
+
|
|
134
|
+
**Building a new kit for an unsupported stack:**
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx @eventmodelers/cli init --build-kit
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This scaffolds `.build-kit/CLAUDE.md`, `lib/prompt.md`, `lib/backend-prompt.md`, and the `build-*` skills with TODO placeholders instead of real content. Fill in the TODOs against the actual stack you're integrating (build/test commands, file layout, framework idioms) while building something real with it, then follow "Adding a stack" below to promote it to a first-class stack once it works.
|
|
141
|
+
|
|
142
|
+
Installing both a build stack and `init-modeling` into the same project reuses this one `.eventmodelers/config.json` — run whichever `init` command second and it finds the existing config already satisfies the required fields and skips straight past the credential prompt.
|
|
52
143
|
|
|
53
144
|
### Installing skills globally
|
|
54
145
|
|
|
@@ -60,7 +151,7 @@ npx @eventmodelers/cli init-modeling --global
|
|
|
60
151
|
|
|
61
152
|
Everything else (the kit dir, project scaffold, credentials, MCP registration) still targets the current directory as usual — `--global` only changes where skills land.
|
|
62
153
|
|
|
63
|
-
|
|
154
|
+
### Bridging to another spec framework
|
|
64
155
|
|
|
65
156
|
If you drive development with a different spec/task framework (Spec Kitty today; more later) instead of build-kit's own code generation, a **bridge** kit keeps that framework's artifacts in sync with the board instead of writing application code:
|
|
66
157
|
|
|
@@ -94,7 +185,7 @@ npx @eventmodelers/cli bridge
|
|
|
94
185
|
|
|
95
186
|
The hook command runs with `BRIDGE_TASK_COUNT`, `BRIDGE_SLICE_ID`/`_TITLE`/`_STATUS` (the most recent change in the batch), and `BRIDGE_BATCH_FILE` (path to the full batch as JSON) in its environment. It's invoked once per batch, not once per slice — any change that arrives while the hook is still running is left queued for the next batch rather than dropped.
|
|
96
187
|
|
|
97
|
-
|
|
188
|
+
### Claude execution & config resolution
|
|
98
189
|
|
|
99
190
|
During install you can optionally point the agent at a local LLM server (vLLM, Ollama) instead of the default Claude Code endpoint, and/or pin a specific model:
|
|
100
191
|
|
|
@@ -180,9 +271,16 @@ npx @eventmodelers/cli --config ../shared/config.json status
|
|
|
180
271
|
|
|
181
272
|
Run `npx @eventmodelers/cli config` at any time to see the fully resolved config (file + env overrides merged, token masked).
|
|
182
273
|
|
|
274
|
+
`--config <path>` and `--print` are global flags accepted by every command. `--print` skips the "connect MCP globally?" prompt during `init-mcp` and just prints the `claude mcp add` command instead of running it — combined with the env vars or direct flags above, `--print` makes both `init` and `init-mcp` fully non-interactive:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
EVENTMODELERS_ORGANIZATION_ID=... EVENTMODELERS_BOARD_ID=... EVENTMODELERS_TOKEN=... \
|
|
278
|
+
npx @eventmodelers/cli --print init --stack node
|
|
279
|
+
```
|
|
280
|
+
|
|
183
281
|
### MCP for other harnesses
|
|
184
282
|
|
|
185
|
-
|
|
283
|
+
`init`/`init-modeling` scaffold and configure credentials, but don't register the MCP server — run that as a separate step once you're ready to connect a harness:
|
|
186
284
|
|
|
187
285
|
```bash
|
|
188
286
|
npx @eventmodelers/cli init-mcp
|
|
@@ -199,58 +297,14 @@ It writes the MCP server into `.claude/settings.json` for Claude Code. For other
|
|
|
199
297
|
|
|
200
298
|
Cursor and Windsurf don't have a safe scriptable install, so the installer prints their manual setup steps instead of writing anything. Pass `--print` to always print every harness's command/steps instead of prompting.
|
|
201
299
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
Use skills in Claude Code with `/skill-name`:
|
|
205
|
-
|
|
206
|
-
| Skill | Description |
|
|
207
|
-
|-------|-------------|
|
|
208
|
-
| `/connect` | Set up board connection |
|
|
209
|
-
| `/timeline` | Live event storming facilitator |
|
|
210
|
-
| `/wdyt` | Business analyst review of your event model |
|
|
211
|
-
| `/storyboard` | Build a full visual storyboard |
|
|
212
|
-
| `/storyboard-screen` | Design individual wireframe screens |
|
|
213
|
-
| `/html-screen` | Design individual real HTML/CSS screens (explicit request only) |
|
|
214
|
-
| `/place-element` | Place commands/events/read models on the board |
|
|
215
|
-
| `/learn-eventmodelers-api` | Full API reference for agent use |
|
|
216
|
-
| `/attributes` | Add/rename attributes across a chain of elements |
|
|
217
|
-
| `/examples` | Add example data to element fields |
|
|
218
|
-
| `/update-slice-status` | Update slice status on the board |
|
|
219
|
-
| `/load-slice` | Persist board slices to disk (backend stacks) |
|
|
220
|
-
| `/build-state-change`, `/build-state-view`, `/build-automation`, `/build-webhook` | Implement a slice's command/view/automation/webhook (backend stacks) |
|
|
221
|
-
|
|
222
|
-
Which skills install depends on the chosen stack — see `stacks/<name>/templates/.claude/skills/`. `/connect`, `/learn-eventmodelers-api`, and `/update-slice-status` have no stack-specific content and install into every stack from `shared/skills/` instead.
|
|
223
|
-
|
|
224
|
-
## Commands
|
|
300
|
+
### `listen` — push-based slice export
|
|
225
301
|
|
|
226
302
|
```bash
|
|
227
|
-
npx @eventmodelers/cli init --stack <name> # scaffold a stack + install + configure (alias: install)
|
|
228
|
-
npx @eventmodelers/cli init --stack <name> --global # same, but skills go to ~/.claude/skills/ (every project)
|
|
229
|
-
npx @eventmodelers/cli init-modeling # skills + agent loop only, no backend scaffold (alias: modeling)
|
|
230
|
-
npx @eventmodelers/cli init-modeling --global # same, but skills go to ~/.claude/skills/ (every project)
|
|
231
|
-
npx @eventmodelers/cli init --build-kit # blank build-kit scaffold (TODO placeholders) for a stack not built into this CLI yet
|
|
232
|
-
npx @eventmodelers/cli init-mcp # register the MCP server in .claude/settings.json (+ optionally another harness)
|
|
233
|
-
npx @eventmodelers/cli init-config # credentials only, no scaffold — writes ./.eventmodelers/config.json
|
|
234
|
-
npx @eventmodelers/cli init-config --global # same, but writes organizationId + token to ~/.eventmodelers/config.json
|
|
235
|
-
npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
|
|
236
|
-
npx @eventmodelers/cli run --ollama # same, via local Ollama (ralph-ollama.js)
|
|
237
|
-
npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
|
|
238
303
|
npx @eventmodelers/cli listen # start the code-export listener (code-export.mjs) from the installed kit dir
|
|
239
|
-
npx @eventmodelers/cli listen --port 4000
|
|
240
|
-
npx @eventmodelers/cli fetch --context <name> # pull full slice detail for one context on the board into <kit-dir>/.slices/ (project root if no kit, or a modeling-kit, is installed)
|
|
241
|
-
npx @eventmodelers/cli fetch --context <name> --slice-id <id> # same, then print just that slice
|
|
242
|
-
npx @eventmodelers/cli fetch --context <name> --slice-title <title> # same, then print just the slice matching this title
|
|
243
|
-
npx @eventmodelers/cli stacks # list available stacks
|
|
244
|
-
npx @eventmodelers/cli status # check what's installed
|
|
245
|
-
npx @eventmodelers/cli config # print the fully resolved config (file + env), token masked
|
|
246
|
-
npx @eventmodelers/cli uninstall # remove everything init/init-modeling installed
|
|
304
|
+
npx @eventmodelers/cli listen --port 4000 # same, on a different port
|
|
247
305
|
```
|
|
248
306
|
|
|
249
|
-
`
|
|
250
|
-
|
|
251
|
-
`listen` is the same kind of dispatcher, but for `<kit-dir>/code-export.mjs` — a local HTTP server (port 3001 by default) that the eventmodelers board UI posts slice/screen data to, which then gets written under `<kit-dir>/.slices/`.
|
|
252
|
-
|
|
253
|
-
`fetch` is the pull-based counterpart to `listen`: instead of waiting for the board UI to push data to a running listener, it calls `slicedata?contextName=<name>` for the required `--context` (full slice detail — commands/events/readmodels/screens/processors/specifications/comments), and writes the same `.slices/<context>/<slice>/slice.json`, `index.json`, and `context.json` layout — useful in CI or any context where nothing is listening on a port. It does not fetch screen images (those only arrive via `listen`'s push). Unlike every other command, `fetch` also works with no kit installed at all — it only needs credentials, not kit-specific files — and, unique to modeling-kit, writes `.slices/` to the project root instead of nesting it under `.agent-modeling-kit/`, since nothing reads it from there (modeling-kit has no `code-export.mjs`/`listen`). If credentials are missing, it prompts the same way `init-config` does. `--slice-id`/`--slice-title` still fetch and persist the whole context, then just print the one slice you asked about.
|
|
307
|
+
`listen` is a dispatcher for `<kit-dir>/code-export.mjs` — a local HTTP server (port 3001 by default) that the eventmodelers board UI posts slice/screen data to, which then gets written under `<kit-dir>/.slices/`. Unlike `fetch`, it does receive screen images, since the board UI pushes them directly.
|
|
254
308
|
|
|
255
309
|
### Uninstall
|
|
256
310
|
|
|
@@ -272,18 +326,11 @@ npx @eventmodelers/cli uninstall --build-kit # remove .build-kit/ specif
|
|
|
272
326
|
npx @eventmodelers/cli uninstall --modeling-kit # remove .agent-modeling-kit/ specifically
|
|
273
327
|
```
|
|
274
328
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
```bash
|
|
278
|
-
EVENTMODELERS_ORGANIZATION_ID=... EVENTMODELERS_BOARD_ID=... EVENTMODELERS_TOKEN=... \
|
|
279
|
-
npx @eventmodelers/cli --print init --stack node
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## Adding a stack
|
|
329
|
+
### Adding a stack
|
|
283
330
|
|
|
284
331
|
Each stack lives under `stacks/<name>/templates/` with `.claude/` (skills), `root/` (spread into the project root), and either `build-kit/` (backend stacks) or `kit/` (modeling-only) for the agent runner. Files identical across all backend stacks live once in `shared/build-kit/` and get layered in automatically — only put stack-specific overrides under `stacks/<name>/templates/build-kit/`. Skills with no stack-specific content (`connect`, `learn-eventmodelers-api`, `update-slice-status`) work the same way via `shared/skills/` — a new stack gets them for free without copying anything; add a skill there only once it needs a stack-specific fork.
|
|
285
332
|
|
|
286
|
-
`init --build-kit` (see above)
|
|
333
|
+
Once your `init --build-kit` scaffold (see above) works against a real backend, promote it to a first-class stack:
|
|
287
334
|
|
|
288
335
|
1. Copy `.build-kit/` → `stacks/<name>/templates/build-kit/`, `.claude/skills/build-*` → `stacks/<name>/templates/.claude/skills/`, and whatever `root/` scaffold you built → `stacks/<name>/templates/root/`.
|
|
289
336
|
2. Add an entry for `<name>` to the `STACKS` object in `cli.js` (`label`, `kitSubdir: 'build-kit'`, `kitDirName: '.build-kit'`, `useShared: true`, `needsBoardId: true`).
|
package/cli.js
CHANGED
|
@@ -20,6 +20,11 @@ import { homedir } from 'os';
|
|
|
20
20
|
import { randomUUID } from 'crypto';
|
|
21
21
|
import { runFetch, FetchAuthError } from './lib/fetch.js';
|
|
22
22
|
import { run as runSpecKittyAdapter } from './lib/adapters/spec-kitty-adapter.js';
|
|
23
|
+
// Not a root-level adapter like spec-kitty-adapter.js above: this is the one canonical
|
|
24
|
+
// copy that every useShared:true stack also gets copied into its installed kit (see
|
|
25
|
+
// copyDirContents in installStack) for ralph.js to import standalone — see
|
|
26
|
+
// shared/build-kit/lib/adapters/realtime-adapter.js for why it lives there instead.
|
|
27
|
+
import { createRealtimeAdapter } from './shared/build-kit/lib/adapters/realtime-adapter.js';
|
|
23
28
|
|
|
24
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
30
|
const __dirname = dirname(__filename);
|
|
@@ -713,6 +718,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
713
718
|
// skills. Only relevant to a bridge install — never copied into the four
|
|
714
719
|
// backend stacks or modeling-kit.
|
|
715
720
|
const isBridge = stackKey === BRIDGE_KIT.key;
|
|
721
|
+
const isModelingKit = stackKey === MODELING_KIT.key;
|
|
716
722
|
const sharedBridgeSkills = isBridge ? join(__dirname, 'shared', 'bridge', options.target) : null;
|
|
717
723
|
|
|
718
724
|
if (!existsSync(templatesSource)) {
|
|
@@ -919,6 +925,14 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
919
925
|
force: options.force,
|
|
920
926
|
});
|
|
921
927
|
|
|
928
|
+
// Register the MCP server up front so it's available from the very first
|
|
929
|
+
// `claude` invocation (whether that's an interactive session opened right
|
|
930
|
+
// after install, or the agent loop's first spawn) instead of only appearing
|
|
931
|
+
// once `run`/`run --modeling` or `init-mcp` happens to run. Safe to write
|
|
932
|
+
// even without a token yet — the file only ever holds the env-var
|
|
933
|
+
// placeholder, never the literal secret (see connect/SKILL.md's Security notes).
|
|
934
|
+
ensureMcpRegistered(targetDir, config.baseUrl || DEFAULT_BASE_URL);
|
|
935
|
+
|
|
922
936
|
// --- 6. Install manifest (drives precise `uninstall` later) ---
|
|
923
937
|
// Only the footprint listed here is ever removed by `uninstall` — the root
|
|
924
938
|
// scaffold (step 2) is real project source the user builds on, so it's
|
|
@@ -933,6 +947,8 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
933
947
|
console.log('\n✅ Done! Start your agent:\n');
|
|
934
948
|
if (isBridge) {
|
|
935
949
|
console.log(' npx @eventmodelers/cli bridge\n');
|
|
950
|
+
} else if (isModelingKit) {
|
|
951
|
+
console.log(' npx @eventmodelers/cli run --modeling\n');
|
|
936
952
|
} else {
|
|
937
953
|
console.log(' npx @eventmodelers/cli run (--ollama or --bash for other runners)\n');
|
|
938
954
|
}
|
|
@@ -1118,6 +1134,27 @@ async function configureMcp(options = {}) {
|
|
|
1118
1134
|
}
|
|
1119
1135
|
}
|
|
1120
1136
|
|
|
1137
|
+
// Registers the eventmodelers MCP server in `.mcp.json` at the project root, the
|
|
1138
|
+
// same file/shape the `connect` skill's Step 3.5 produces — kept here as a
|
|
1139
|
+
// belt-and-suspenders guarantee, since an agent executing that skill can skip a
|
|
1140
|
+
// step, but a `claude` process only ever discovers MCP servers at its own
|
|
1141
|
+
// startup. Anything spawning a `claude` process for this project (cold-spawn
|
|
1142
|
+
// per task, or a long-lived warm process) must call this first — a `.mcp.json`
|
|
1143
|
+
// written mid-session by the process itself is too late for that same process.
|
|
1144
|
+
// The token itself is never written to disk here — `${EVENTMODELERS_TOKEN}` is
|
|
1145
|
+
// resolved by `claude` from its own process env, which the caller must set.
|
|
1146
|
+
function ensureMcpRegistered(projectDir, baseUrl) {
|
|
1147
|
+
const mcpConfigPath = join(projectDir, '.mcp.json');
|
|
1148
|
+
const mcpConfig = readJsonSafe(mcpConfigPath);
|
|
1149
|
+
mcpConfig.mcpServers = mcpConfig.mcpServers || {};
|
|
1150
|
+
mcpConfig.mcpServers.eventmodelers = {
|
|
1151
|
+
type: 'http',
|
|
1152
|
+
url: `${baseUrl}/mcp`,
|
|
1153
|
+
headers: { 'x-token': '${EVENTMODELERS_TOKEN}' },
|
|
1154
|
+
};
|
|
1155
|
+
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1121
1158
|
// `run --modeling`: modeling-kit's one and only runtime mode — there is no
|
|
1122
1159
|
// cold-spawn/tasks.json loop for this kit (that's a build-kit concept; see the
|
|
1123
1160
|
// `run` command's build-kit-vs-modeling-kit gate above). It keeps ONE Claude
|
|
@@ -1129,14 +1166,13 @@ async function configureMcp(options = {}) {
|
|
|
1129
1166
|
// from the kit's lib/config.js, to avoid duplicating the config-file-walk logic.
|
|
1130
1167
|
// See `.agent-modeling-kit/CLAUDE.md` for the per-turn instructions this mode's
|
|
1131
1168
|
// modeling session follows.
|
|
1132
|
-
async function runModeling(kitDir, projectDir) {
|
|
1169
|
+
async function runModeling(kitDir, projectDir, verbose = false) {
|
|
1133
1170
|
const configLibPath = join(kitDir, 'lib', 'config.js');
|
|
1134
1171
|
if (!existsSync(configLibPath)) {
|
|
1135
1172
|
console.error(`❌ ${relative(process.cwd(), configLibPath)} not found — --modeling needs a kit installed via \`init --modeling\`.`);
|
|
1136
1173
|
process.exit(1);
|
|
1137
1174
|
}
|
|
1138
1175
|
const { loadLocalConfig, fetchPlatformConfig } = await import(pathToFileURL(configLibPath).href);
|
|
1139
|
-
const { createClient } = await import('@supabase/supabase-js');
|
|
1140
1176
|
|
|
1141
1177
|
const local = loadLocalConfig(kitDir);
|
|
1142
1178
|
local.agentId = ensureAgentId(kitDir, 'MODELING');
|
|
@@ -1144,7 +1180,7 @@ async function runModeling(kitDir, projectDir) {
|
|
|
1144
1180
|
console.error('❌ --modeling needs platform credentials in .eventmodelers/config.json (token + organizationId) — run `/connect` once or paste your config first.');
|
|
1145
1181
|
process.exit(1);
|
|
1146
1182
|
}
|
|
1147
|
-
const cfg = await fetchPlatformConfig(local); // adds supabaseUrl/supabaseAnonKey
|
|
1183
|
+
const cfg = await fetchPlatformConfig(local); // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
|
|
1148
1184
|
if (!cfg.boardId) {
|
|
1149
1185
|
console.error('❌ --modeling needs a boardId — a modeling agent always runs for exactly one board. Run `/connect board=<uuid>` once, or add boardId to .eventmodelers/config.json.');
|
|
1150
1186
|
process.exit(1);
|
|
@@ -1181,33 +1217,50 @@ async function runModeling(kitDir, projectDir) {
|
|
|
1181
1217
|
|
|
1182
1218
|
const claudeArgs = ['--dangerously-skip-permissions', '-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose'];
|
|
1183
1219
|
if (cfg.model) claudeArgs.push('--model', cfg.model);
|
|
1184
|
-
const claudeEnv =
|
|
1220
|
+
const claudeEnv = {
|
|
1221
|
+
...process.env,
|
|
1222
|
+
...(cfg.anthropicBaseUrl ? { ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl } : {}),
|
|
1223
|
+
EVENTMODELERS_TOKEN: cfg.token,
|
|
1224
|
+
};
|
|
1185
1225
|
|
|
1186
1226
|
let proc = null;
|
|
1187
1227
|
let stdoutBuffer = '';
|
|
1188
1228
|
let pending = null; // one in-flight turn at a time
|
|
1189
1229
|
|
|
1230
|
+
// Collapses whitespace/newlines to a single line and truncates past `max` chars —
|
|
1231
|
+
// a long multi-line curl command or grep pattern wrapped across many terminal lines
|
|
1232
|
+
// is just as unreadable as no detail at all. Keeps one tool call to one log line.
|
|
1233
|
+
function oneLine(s, max) {
|
|
1234
|
+
const collapsed = String(s ?? '').replace(/\s+/g, ' ').trim();
|
|
1235
|
+
return collapsed.length > max ? `${collapsed.slice(0, max)}…` : collapsed;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1190
1238
|
// Bare tool names (`→ Bash`, `→ Skill`) tell you nothing happened worth
|
|
1191
1239
|
// reading — this pulls out the one input field that actually says what the
|
|
1192
|
-
// tool did, so the trace is skimmable without the interactive TUI.
|
|
1240
|
+
// tool did, so the trace is skimmable without the interactive TUI. Only used
|
|
1241
|
+
// in --verbose mode; the condensed default logs the bare name (or, for Skill,
|
|
1242
|
+
// just the skill name) instead — see handleLine.
|
|
1193
1243
|
function describeToolUse(block) {
|
|
1194
1244
|
const input = block.input ?? {};
|
|
1195
1245
|
switch (block.name) {
|
|
1196
|
-
case 'Bash': return `Bash: ${input.command}`;
|
|
1197
|
-
case 'Skill': return `Skill: ${input.skill}${input.args ? ` ${input.args}` : ''}`;
|
|
1246
|
+
case 'Bash': return `Bash: ${oneLine(input.command, 100)}`;
|
|
1247
|
+
case 'Skill': return `Skill: ${input.skill}${input.args ? ` ${oneLine(input.args, 60)}` : ''}`;
|
|
1198
1248
|
case 'Read': return `Read: ${input.file_path}`;
|
|
1199
1249
|
case 'Edit': return `Edit: ${input.file_path}`;
|
|
1200
1250
|
case 'Write': return `Write: ${input.file_path}`;
|
|
1201
|
-
case 'Grep': return `Grep: ${input.pattern}`;
|
|
1251
|
+
case 'Grep': return `Grep: ${oneLine(input.pattern, 60)}`;
|
|
1202
1252
|
case 'Glob': return `Glob: ${input.pattern}`;
|
|
1203
1253
|
case 'WebFetch': return `WebFetch: ${input.url}`;
|
|
1204
|
-
case 'Agent': return `Agent: ${input.description ?? input.subagent_type ?? ''}`;
|
|
1254
|
+
case 'Agent': return `Agent: ${oneLine(input.description ?? input.subagent_type ?? '', 60)}`;
|
|
1205
1255
|
default: return block.name;
|
|
1206
1256
|
}
|
|
1207
1257
|
}
|
|
1208
1258
|
|
|
1209
1259
|
// stream-json output loses the normal interactive TUI (tool cards, live diffs) —
|
|
1210
1260
|
// this is a plain-text approximation, good enough for a headless/voice runner.
|
|
1261
|
+
// --verbose logs full tool input and assistant reasoning text; the default
|
|
1262
|
+
// (condensed) mode logs only the high-level step — a skill name, or a bare tool
|
|
1263
|
+
// name — so a long session reads as a step list instead of a full trace.
|
|
1211
1264
|
function handleLine(line) {
|
|
1212
1265
|
if (!line.trim()) return;
|
|
1213
1266
|
let msg;
|
|
@@ -1215,8 +1268,12 @@ async function runModeling(kitDir, projectDir) {
|
|
|
1215
1268
|
|
|
1216
1269
|
if (msg.type === 'assistant') {
|
|
1217
1270
|
for (const block of msg.message?.content ?? []) {
|
|
1218
|
-
if (block.type === 'text' && block.text) log(block.text);
|
|
1219
|
-
if (block.type === 'tool_use')
|
|
1271
|
+
if (block.type === 'text' && block.text && verbose) log(block.text);
|
|
1272
|
+
if (block.type === 'tool_use') {
|
|
1273
|
+
if (verbose) log(`→ ${describeToolUse(block)}`);
|
|
1274
|
+
else if (block.name === 'Skill') log(`→ Skill: ${block.input?.skill ?? ''}`);
|
|
1275
|
+
else log(`→ ${block.name}`);
|
|
1276
|
+
}
|
|
1220
1277
|
}
|
|
1221
1278
|
return;
|
|
1222
1279
|
}
|
|
@@ -1278,10 +1335,6 @@ async function runModeling(kitDir, projectDir) {
|
|
|
1278
1335
|
}
|
|
1279
1336
|
|
|
1280
1337
|
let realtimeToken = await getRealtimeToken();
|
|
1281
|
-
const supabase = createClient(cfg.supabaseUrl, cfg.supabaseAnonKey, {
|
|
1282
|
-
realtime: { params: { apikey: cfg.supabaseAnonKey } },
|
|
1283
|
-
});
|
|
1284
|
-
await supabase.realtime.setAuth(realtimeToken);
|
|
1285
1338
|
|
|
1286
1339
|
let draining = false;
|
|
1287
1340
|
async function drain() {
|
|
@@ -1303,26 +1356,30 @@ async function runModeling(kitDir, projectDir) {
|
|
|
1303
1356
|
}
|
|
1304
1357
|
|
|
1305
1358
|
const channelName = `org:${cfg.organizationId}`;
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1359
|
+
const realtime = await createRealtimeAdapter(cfg, realtimeToken);
|
|
1360
|
+
realtime.subscribe(
|
|
1361
|
+
channelName,
|
|
1362
|
+
{
|
|
1363
|
+
message: (payload) => {
|
|
1364
|
+
if (payload === 'Exit') {
|
|
1365
|
+
log('received "Exit" — shutting down');
|
|
1366
|
+
process.exit(0);
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
1369
|
+
'prompt:created': () => {
|
|
1370
|
+
drain().catch((err) => log(`drain error: ${err.message}`));
|
|
1371
|
+
},
|
|
1372
|
+
},
|
|
1373
|
+
(status) => {
|
|
1318
1374
|
log(`channel "${channelName}": ${status}`);
|
|
1319
1375
|
if (status === 'SUBSCRIBED') drain().catch((err) => log(`initial drain error: ${err.message}`));
|
|
1320
|
-
}
|
|
1376
|
+
},
|
|
1377
|
+
);
|
|
1321
1378
|
|
|
1322
1379
|
setInterval(async () => {
|
|
1323
1380
|
try {
|
|
1324
1381
|
realtimeToken = await getRealtimeToken();
|
|
1325
|
-
await
|
|
1382
|
+
await realtime.setAuth(realtimeToken);
|
|
1326
1383
|
log('token refreshed');
|
|
1327
1384
|
} catch (err) {
|
|
1328
1385
|
log(`token refresh failed: ${err.message}`);
|
|
@@ -1577,7 +1634,7 @@ credentialFlags(program
|
|
|
1577
1634
|
? resolve(targetDir, globalOpts.config)
|
|
1578
1635
|
: join(targetDir, '.eventmodelers', 'config.json');
|
|
1579
1636
|
const effective = loadEffectiveConfig(targetDir, null, globalOpts.config);
|
|
1580
|
-
await configureCredentials({
|
|
1637
|
+
const cfg = await configureCredentials({
|
|
1581
1638
|
config: effective.config,
|
|
1582
1639
|
configPath,
|
|
1583
1640
|
targetDir,
|
|
@@ -1587,6 +1644,12 @@ credentialFlags(program
|
|
|
1587
1644
|
print: globalOpts.print,
|
|
1588
1645
|
force: true,
|
|
1589
1646
|
});
|
|
1647
|
+
|
|
1648
|
+
// Keep `.mcp.json` in sync — this command can change `baseUrl` (e.g.
|
|
1649
|
+
// switching a project from prod to beta) independently of `init`, and a
|
|
1650
|
+
// stale MCP registration pointing at the wrong host is worse than none
|
|
1651
|
+
// (see the beta-api protected-resource-metadata incident this fixed).
|
|
1652
|
+
ensureMcpRegistered(targetDir, cfg.baseUrl || DEFAULT_BASE_URL);
|
|
1590
1653
|
}
|
|
1591
1654
|
});
|
|
1592
1655
|
|
|
@@ -1596,6 +1659,7 @@ program
|
|
|
1596
1659
|
.option('--ollama', 'Use ralph-ollama.js instead of the default Claude runner (build-kit stacks only)')
|
|
1597
1660
|
.option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
|
|
1598
1661
|
.option('--modeling', 'Keep one Claude process warm across prompts instead of spawning a fresh one per task, for low-latency voice/live use. Modeling-kit installs only — there is no cold-spawn/tasks.json loop for modeling-kit. Built into the CLI, not a per-project file.')
|
|
1662
|
+
.option('--verbose', 'Log every tool call\'s full input (commands, skill args, file paths) and assistant reasoning text. Default is condensed, high-level per-step logging only.')
|
|
1599
1663
|
.action(async (opts) => {
|
|
1600
1664
|
const cwd = process.cwd();
|
|
1601
1665
|
// Both kit dirs can be installed side by side (e.g. running a build-kit and a
|
|
@@ -1633,7 +1697,7 @@ program
|
|
|
1633
1697
|
// runModeling's own [modeling] log lines instead of before them.
|
|
1634
1698
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (warm Claude process) for ${relative(cwd, modelingKitDir)}...\n\n`, res));
|
|
1635
1699
|
try {
|
|
1636
|
-
await runModeling(modelingKitDir, resolve(modelingKitDir, '..'));
|
|
1700
|
+
await runModeling(modelingKitDir, resolve(modelingKitDir, '..'), !!opts.verbose);
|
|
1637
1701
|
} catch (err) {
|
|
1638
1702
|
console.error('[modeling] Fatal:', err);
|
|
1639
1703
|
process.exit(1);
|
|
@@ -1673,7 +1737,9 @@ program
|
|
|
1673
1737
|
console.log(`▶ Starting ${relative(cwd, runnerPath)}...\n`);
|
|
1674
1738
|
const cmd = runner.endsWith('.sh') ? `"${runnerPath}"` : `node "${runnerPath}"`;
|
|
1675
1739
|
try {
|
|
1676
|
-
|
|
1740
|
+
// Only ralph-claude.js reads this — the bash loop and the ollama executor have
|
|
1741
|
+
// their own separate output paths with no stream-json parsing to gate.
|
|
1742
|
+
execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '' } });
|
|
1677
1743
|
} catch (err) {
|
|
1678
1744
|
process.exit(err.status || 1);
|
|
1679
1745
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, or modeling-only)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@supabase/supabase-js": "^2.0.0",
|
|
33
|
-
"commander": "^12.0.0"
|
|
33
|
+
"commander": "^12.0.0",
|
|
34
|
+
"eventsource": "^3.0.7",
|
|
35
|
+
"pocketbase": "^0.27.0"
|
|
34
36
|
},
|
|
35
37
|
"engines": {
|
|
36
38
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// PocketBase realtime adapter — the self-hosted/on-prem transport. PocketBase has no
|
|
2
|
+
// generic pub/sub broadcast API; realtime only fires on record create/update/delete
|
|
3
|
+
// within a subscribed collection, so the backend piggybacks broadcasts onto a
|
|
4
|
+
// `realtime_events` collection (topic/event/payload fields) and this adapter filters
|
|
5
|
+
// that collection's create-stream down to the requested topic. See realtime-adapter.js
|
|
6
|
+
// for the interface both this and the Supabase adapter implement.
|
|
7
|
+
|
|
8
|
+
const REALTIME_EVENTS_COLLECTION = 'realtime_events';
|
|
9
|
+
|
|
10
|
+
export async function createPocketBaseRealtimeAdapter(cfg, initialToken) {
|
|
11
|
+
const { EventSource } = await import('eventsource');
|
|
12
|
+
if (!globalThis.EventSource) globalThis.EventSource = EventSource; // PocketBase's SDK assumes a browser-style global
|
|
13
|
+
const { default: PocketBase } = await import('pocketbase');
|
|
14
|
+
const pb = new PocketBase(cfg.pocketbaseUrl);
|
|
15
|
+
pb.authStore.save(initialToken, null);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
async subscribe(topic, handlers, onStatus) {
|
|
19
|
+
await pb.collection(REALTIME_EVENTS_COLLECTION).subscribe('*', (e) => {
|
|
20
|
+
if (e.action !== 'create' || e.record.topic !== topic) return;
|
|
21
|
+
handlers[e.record.event]?.(e.record.payload);
|
|
22
|
+
});
|
|
23
|
+
onStatus?.('SUBSCRIBED');
|
|
24
|
+
},
|
|
25
|
+
setAuth(token) {
|
|
26
|
+
pb.authStore.save(token, null);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Realtime adapter — picks the platform's realtime transport per cfg.realtimeProvider
|
|
2
|
+
// (set by /api/config: Supabase broadcast channels for the hosted SaaS, PocketBase
|
|
3
|
+
// record-change events for self-hosted/on-prem) and hands back one common interface:
|
|
4
|
+
//
|
|
5
|
+
// subscribe(topic, handlers, onStatus?) — handlers is a { [eventName]: (payload) => void }
|
|
6
|
+
// map; onStatus?.(status) fires once the subscription is live ('SUBSCRIBED').
|
|
7
|
+
// setAuth(token) — refresh the short-lived realtime token on the live connection.
|
|
8
|
+
//
|
|
9
|
+
// cfg.realtimeProvider absent (older servers, predating PocketBase support) means
|
|
10
|
+
// 'supabase' — the only transport that ever existed before it.
|
|
11
|
+
//
|
|
12
|
+
// Single source of truth for both runtimes that need it: cli.js's modeling loop
|
|
13
|
+
// imports this file directly out of the npm package (shared/build-kit/lib/adapters/...),
|
|
14
|
+
// and every useShared:true stack gets this whole adapters/ folder copied alongside
|
|
15
|
+
// ralph.js into the installed kit (see copyDirContents in cli.js), so ralph.js's copy
|
|
16
|
+
// imports it via the relative path './adapters/realtime-adapter.js' from wherever it
|
|
17
|
+
// ends up on disk.
|
|
18
|
+
|
|
19
|
+
import { createSupabaseRealtimeAdapter } from './supabase-realtime-adapter.js';
|
|
20
|
+
import { createPocketBaseRealtimeAdapter } from './pocketbase-realtime-adapter.js';
|
|
21
|
+
|
|
22
|
+
export async function createRealtimeAdapter(cfg, initialToken) {
|
|
23
|
+
const provider = cfg.realtimeProvider ?? 'supabase';
|
|
24
|
+
return provider === 'pocketbase'
|
|
25
|
+
? createPocketBaseRealtimeAdapter(cfg, initialToken)
|
|
26
|
+
: createSupabaseRealtimeAdapter(cfg, initialToken);
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Supabase realtime adapter — the hosted-SaaS transport. Subscribes to a private
|
|
2
|
+
// broadcast channel and dispatches named broadcast events to caller-supplied handlers.
|
|
3
|
+
// See realtime-adapter.js for the interface both this and the PocketBase adapter implement.
|
|
4
|
+
|
|
5
|
+
export async function createSupabaseRealtimeAdapter(cfg, initialToken) {
|
|
6
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
7
|
+
const supabase = createClient(cfg.supabaseUrl, cfg.supabaseAnonKey, {
|
|
8
|
+
realtime: { params: { apikey: cfg.supabaseAnonKey } },
|
|
9
|
+
});
|
|
10
|
+
await supabase.realtime.setAuth(initialToken);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
subscribe(topic, handlers, onStatus) {
|
|
14
|
+
let channel = supabase.channel(topic, { config: { private: true } });
|
|
15
|
+
for (const [event, handler] of Object.entries(handlers)) {
|
|
16
|
+
channel = channel.on('broadcast', { event }, (msg) => handler(msg.payload));
|
|
17
|
+
}
|
|
18
|
+
channel.subscribe((status) => onStatus?.(status));
|
|
19
|
+
},
|
|
20
|
+
setAuth(token) {
|
|
21
|
+
return supabase.realtime.setAuth(token);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|