@eventmodelers/cli 0.0.39 → 1.0.1

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 (44) hide show
  1. package/README.md +116 -69
  2. package/cli.js +98 -32
  3. package/package.json +4 -2
  4. package/shared/build-kit/lib/adapters/pocketbase-realtime-adapter.js +29 -0
  5. package/shared/build-kit/lib/adapters/realtime-adapter.js +27 -0
  6. package/shared/build-kit/lib/adapters/supabase-realtime-adapter.js +24 -0
  7. package/shared/build-kit/lib/ralph.js +30 -31
  8. package/shared/build-kit/package.json +3 -1
  9. package/shared/build-kit/ralph-claude.js +63 -5
  10. package/shared/skills/connect/SKILL.md +54 -5
  11. package/shared/skills/learn-eventmodelers-api/SKILL.md +88 -3
  12. package/shared/skills/load-slice/SKILL.md +16 -0
  13. package/shared/skills/update-slice-status/SKILL.md +15 -5
  14. package/stacks/modeling-kit/templates/.claude/skills/add-next-slice/SKILL.md +86 -0
  15. package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +29 -1
  16. package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +47 -5
  17. package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +73 -59
  18. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-applying-conways-law/SKILL.md +1 -1
  19. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +100 -14
  20. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +22 -2
  21. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-designing-event-models/SKILL.md +1 -1
  22. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +30 -4
  23. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +72 -12
  24. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +66 -9
  25. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-integrating-legacy-systems/SKILL.md +1 -1
  26. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +29 -5
  27. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +9 -1
  28. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +37 -3
  29. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +114 -27
  30. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-translating-external-events/SKILL.md +1 -1
  31. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +15 -3
  32. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +13 -2
  33. package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +45 -7
  34. package/stacks/modeling-kit/templates/.claude/skills/handle-comment/SKILL.md +28 -1
  35. package/stacks/modeling-kit/templates/.claude/skills/html-screen/SKILL.md +45 -19
  36. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +172 -6
  37. package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +104 -25
  38. package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +31 -17
  39. package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +111 -12
  40. package/stacks/modeling-kit/templates/.claude/skills/update-prompt-status/SKILL.md +10 -1
  41. package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +24 -5
  42. package/stacks/modeling-kit/templates/kit/CLAUDE.md +12 -4
  43. package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +13 -7
  44. 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
- ## Quick start
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
- `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).
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
- The installer prompts for your API token, Organization ID (and Board ID, for the four backend stacks) from [app.eventmodelers.ai/account](https://app.eventmodelers.ai/account).
26
+ ### Common scenarios
25
27
 
26
- `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:
28
+ **Starting from scratch on a new project:**
27
29
 
28
30
  ```bash
29
- npx @eventmodelers/cli init-mcp
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
- ## What gets installed
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
- 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.
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
- 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. `modeling-kit` only installs skills + the agent loop, with no backend opinion.
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
- ## Bridging to another spec framework
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
- ## Claude execution & config resolution
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
- MCP registration is a separate step from `init`/`init-modeling` — run `init-mcp` whenever you're ready to connect a harness:
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
- ## Skills
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 # same, on a different port
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
- `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.
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
- `--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:
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) installs exactly that layout into a real project — `.build-kit/CLAUDE.md`, `lib/prompt.md`, `lib/backend-prompt.md`, and `.claude/skills/build-{state-change,state-view,automation}/SKILL.md` — but with TODO-marked placeholders instead of real content, since there's no fixed backend to generate them from. Fill in the TODOs against the actual stack you're integrating (build/test commands, file layout, framework idioms) while building something real with it. Once it works, promote it to a first-class stack:
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 (+ boardId if the config has a default one)
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 = cfg.anthropicBaseUrl ? { ...process.env, ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl } : process.env;
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') log(`→ ${describeToolUse(block)}`);
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
- supabase
1307
- .channel(channelName, { config: { private: true } })
1308
- .on('broadcast', { event: 'message' }, (msg) => {
1309
- if (msg.payload === 'Exit') {
1310
- log('received "Exit" — shutting down');
1311
- process.exit(0);
1312
- }
1313
- })
1314
- .on('broadcast', { event: 'prompt:created' }, () => {
1315
- drain().catch((err) => log(`drain error: ${err.message}`));
1316
- })
1317
- .subscribe((status) => {
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 supabase.realtime.setAuth(realtimeToken);
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
- execSync(cmd, { cwd: kitDir, stdio: 'inherit' });
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.39",
3
+ "version": "1.0.1",
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
+ }