@eventmodelers/cli 1.0.20 → 1.0.21

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/cli.js CHANGED
@@ -1413,6 +1413,22 @@ async function runModeling(kitDir, projectDir, verbose = false) {
1413
1413
 
1414
1414
  const channelName = `org:${cfg.organizationId}`;
1415
1415
  const realtime = await createRealtimeAdapter(cfg, realtimeToken);
1416
+
1417
+ let lastTokenRefreshAt = 0;
1418
+ async function refreshRealtimeToken(reason) {
1419
+ // Guard against hammering the token endpoint: a rejected channel retries every
1420
+ // ~14s on its own, so without this a bad token would trigger a refresh call per retry.
1421
+ if (Date.now() - lastTokenRefreshAt < 5000) return;
1422
+ lastTokenRefreshAt = Date.now();
1423
+ try {
1424
+ realtimeToken = await getRealtimeToken();
1425
+ await realtime.setAuth(realtimeToken);
1426
+ log(`token refreshed (${reason})`);
1427
+ } catch (err) {
1428
+ log(`token refresh failed (${reason}): ${err.message}`);
1429
+ }
1430
+ }
1431
+
1416
1432
  realtime.subscribe(
1417
1433
  channelName,
1418
1434
  {
@@ -1429,19 +1445,19 @@ async function runModeling(kitDir, projectDir, verbose = false) {
1429
1445
  (status) => {
1430
1446
  log(`channel "${channelName}": ${status}`);
1431
1447
  if (status === 'SUBSCRIBED') drain().catch((err) => log(`initial drain error: ${err.message}`));
1448
+ // A bad/stale token otherwise sits in realtime-js's own rejoin-retry loop until the
1449
+ // next scheduled refresh below — up to 10 minutes of failed joins. Refresh immediately
1450
+ // instead of waiting on the clock.
1451
+ if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
1452
+ refreshRealtimeToken(status).catch(() => {});
1453
+ }
1432
1454
  },
1433
1455
  ).catch((err) => {
1434
1456
  log(`realtime subscribe failed, prompts won't be pushed live: ${err.message}`);
1435
1457
  });
1436
1458
 
1437
- setInterval(async () => {
1438
- try {
1439
- realtimeToken = await getRealtimeToken();
1440
- await realtime.setAuth(realtimeToken);
1441
- log('token refreshed');
1442
- } catch (err) {
1443
- log(`token refresh failed: ${err.message}`);
1444
- }
1459
+ setInterval(() => {
1460
+ refreshRealtimeToken('scheduled').catch(() => {});
1445
1461
  }, 10 * 60 * 1000);
1446
1462
 
1447
1463
  const ping = async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
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": {
@@ -384,6 +384,16 @@ After the step is done, **every SCREEN and every AUTOMATION on the board must be
384
384
 
385
385
  > **Placement rule**: A read model must be placed in a column that already contains a SCREEN or AUTOMATION it serves. Do not place read models in columns with no screen or automation — doing so creates orphaned read models that will never have a consumer.
386
386
 
387
+ ### Pull field mappings from Step 3 — they are the spec, not a guess
388
+
389
+ **Do not re-derive read model needs from a screen's title or description alone, and do not rely on the orchestrator's phase-summary handoff for this** — if you arrived here via `eventmodeling-orchestrating-event-modeling`, the handoff after Step 3 is a short hand-written prose summary (`.trogonai/interviews/.../EVENTMODELING.md`), not the actual field data. It will not reliably carry the per-field mappings forward. Go back to the board itself:
390
+
391
+ For every SCREEN node, fetch it directly (`get_node`/`get_nodes`, never from memory) and read its `meta.fields`. Step 3 already required every field to carry a `mapping`, and for view fields that mapping is already in the exact form `"<ReadModelTitle>.<fieldName>"` — recorded specifically so this step doesn't have to re-guess it.
392
+
393
+ - **Group the screen's fields by the `<ReadModelTitle>` already named in their `mapping`.** That grouping — not a fresh read of the screen's visuals — is the read model's title and field list. Build the READMODEL node from it directly.
394
+ - If a field's `mapping` names a read model that isn't `"<CommandTitle>.<fieldName>"` or `"session:..."` or `"derived:..."`, it is a read-model reference — treat it as a requirement, not a suggestion.
395
+ - A screen with no fields, or with fields that carry no read-model-shaped mapping, is **not** evidence that it needs no read model. Re-check it against the three rules above (view screen / automation / command screen showing prior state) before concluding it's the rare blank-form exception — and say explicitly why it qualifies.
396
+
387
397
  ### Field data lineage — the `mapping` attribute on READMODEL fields
388
398
 
389
399
  Every field on a READMODEL must carry a `mapping` that says exactly which event (or command) field it is projected from. Use one of these forms:
@@ -582,6 +592,16 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
582
592
 
583
593
  Skip a connection silently if the target cell is empty. Log each created arrow: `→ connected EVENT→READMODEL "OrderPlaced"→"OrderStatusView"`, `→ connected READMODEL→SCREEN "OrderStatusView"→"Order Status Screen"`, or `→ connected READMODEL→AUTOMATION "OrderStatusView"→"Fulfillment Processor"`.
584
594
 
595
+ ### Mandatory per-node verification (run before declaring this step done)
596
+
597
+ Do not declare Step 5 complete on the strength of the read models you happened to design. Instead, **re-fetch every SCREEN and AUTOMATION node on the board** (`get_nodes` per type — don't rely on the list built earlier in this step, the board may have moved on) and check each one individually:
598
+
599
+ 1. Does it now have an incoming `READMODEL → SCREEN` or `READMODEL → AUTOMATION` connection?
600
+ 2. If not — is it a provably blank creation form with no prior state? State the reason in one line (e.g. `"Register Account" screen: blank form, no prior state — exempt`).
601
+ 3. If it's neither connected nor exempt, it is an **unresolved gap**. Fix it now: design the missing read model (pulling from its `meta.fields`/`mapping` as above) and wire the connection. Do not move to Step 6 with an unresolved gap silently carried forward — either fix it or explicitly flag it to the user as accepted debt.
602
+
603
+ List the result of this pass (connected / exempt / fixed) for every screen and automation checked — this list is the evidence the orchestrator's Step 5 gate ("every screen data need is satisfied by a read model") actually holds, not just an assumption.
604
+
585
605
  After all read models, screens, automations, and connections are in place, present the Read Model Catalog summary as text to the user.
586
606
 
587
607
  ---
@@ -694,8 +714,8 @@ Identify UI needs without event sources:
694
714
 
695
715
  ### Read Model Design
696
716
  - [ ] **Typical pattern applied**: most screens follow `READ MODEL → SCREEN → COMMAND → EVENT`
697
- - [ ] **Every SCREEN from storyboarding is connected to at least one read model** (via `READMODEL → SCREEN`); only blank creation forms may be exempt
698
- - [ ] **Every AUTOMATION from storyboarding is connected to at least one read model** (via `READMODEL → AUTOMATION`)
717
+ - [ ] **Every SCREEN from storyboarding is connected to at least one read model** (via `READMODEL → SCREEN`); only blank creation forms may be exempt — verified via the mandatory per-node pass above, not assumed
718
+ - [ ] **Every AUTOMATION from storyboarding is connected to at least one read model** (via `READMODEL → AUTOMATION`) — same per-node verification
699
719
  - [ ] **No read model is placed without a connected SCREEN or AUTOMATION consumer**
700
720
  - [ ] Every read model has clear purpose
701
721
  - [ ] Every data field has event source
@@ -79,6 +79,8 @@ This skill only has a `pages`/`backgroundColor` field to send (no separate marks
79
79
 
80
80
  Apply these only to the specific element(s) the request describes — don't guess at additional areas to call out.
81
81
 
82
+ **Marked screens and field scoping**: when the same underlying screen is rendered multiple times as separate nodes — once per slice, each with a different mark/highlight calling out a different part of the UI — scope each node's `meta.fields` (Step 5 below) to only the data inside that node's highlighted area, not the full screen. Three slice-specific screen nodes sharing one visual base should end up with three different, narrower field lists, each matching what that node's mark calls out.
83
+
82
84
  ## Step 4 — Render the pages
83
85
 
84
86
  **Updating an existing node** (`nodeId` was given) — always sends the **complete** pages array, not just the changed/new entry:
@@ -133,7 +135,54 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/html-screen-nodes/$N
133
135
 
134
136
  Expect `204 No Content` on success from either curl call.
135
137
 
136
- ## Step 5 — Report back
138
+ ## Step 5 — Define field data lineage (mandatory)
139
+
140
+ Every screen — new or updated — needs `meta.fields`: one entry per piece of data the screen displays or captures, each with a `mapping` naming where that data comes from. A screen with only a title and no fields is an empty placeholder from a data-lineage standpoint, even if the mockup itself looks complete.
141
+
142
+ | Field type | `mapping` | Example |
143
+ |---|---|---|
144
+ | User types a value, sent as a command | `"<CommandTitle>.<fieldName>"` | `"ReserveBike.bikeId"` |
145
+ | Read from session | `"session:<fieldName>"` | `"session:customerId"` |
146
+ | Displayed data, sourced from a read model | `"<ReadModelTitle>.<fieldName>"` | `"ActiveReservationView.status"` |
147
+ | Calculated/formatted only for display | `"derived:<expression>"` | `"derived:formatDuration(durationMinutes)"` |
148
+
149
+ Name the read model even if it doesn't exist as a board node yet — this skill only renders the screen, it does not create READMODEL nodes or connections (that's `eventmodeling-identifying-outputs` or `place-element`, if the model is taken that far). But naming the source is **not optional**: a screen displaying data should almost never have a field with no mapping. If you can't say which read model a displayed field comes from, that's a sign the model is missing something — not a reason to skip the field.
150
+
151
+ If this node is one of several sharing the same visual base with different marks/highlights (see "Marked screens and field scoping" above), only list the fields that fall inside *this* node's highlighted area — not every field the shared screen shows.
152
+
153
+ Set `cardinality` too (`"Single"` unless the field is a repeated/list value), then push the fields onto the node:
154
+
155
+ **Prefer MCP:**
156
+ ```
157
+ mcp__eventmodelers__submit_node_events {
158
+ "boardId": "<BOARD_ID>",
159
+ "events": [{
160
+ "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
161
+ "boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
162
+ "changedAttributes": ["meta.fields"],
163
+ "meta": { "type": "HTML_SCREEN", "fields": [
164
+ {"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
165
+ ] }
166
+ }]
167
+ }
168
+ ```
169
+
170
+ **Fallback (no MCP):**
171
+ ```bash
172
+ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
173
+ -H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: agent" \
174
+ -H "Content-Type: application/json" \
175
+ -d '[{
176
+ "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
177
+ "boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
178
+ "changedAttributes": ["meta.fields"],
179
+ "meta": { "type": "HTML_SCREEN", "fields": [
180
+ {"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
181
+ ] }
182
+ }]'
183
+ ```
184
+
185
+ ## Step 6 — Report back
137
186
 
138
187
  Tell the user:
139
188
  - The node ID that was created or updated
@@ -35,6 +35,9 @@ Before making any API calls, plan all N screens. For each screen, decide:
35
35
  - `screenTitle` — human-readable name (e.g. "Enter Credentials")
36
36
  - `pages` (default) — one or more complete HTML/CSS fragments for this screen (see "HTML page design" below), or `elements` — a minimal list of grid elements (see "Sketch path" below, aim for 5–8 elements) **only** when the sketch path applies for this storyboard
37
37
  - `visualDescription` — a prose description of the screen's visual layout and content (2–4 sentences) that lets someone who cannot see the image understand what is shown: what UI sections appear, what text/labels are visible, where buttons and inputs are placed, and the overall purpose of the screen
38
+ - `fields` — one entry per piece of data this screen displays or captures, each with a `mapping` naming its source (see "Field data lineage" in Step 5b below). Plan this alongside the visuals, not as an afterthought — every displayed value needs a named source.
39
+
40
+ If several screens in this storyboard share the same visual base but each highlights a different part of it (e.g. one shared mockup, marked up differently per slice), scope each screen's `fields` to only the data inside *that* screen's highlighted area — not the full shared mockup. Different highlight, different (narrower) field list.
38
41
 
39
42
  Then **create one task per screen** using TaskCreate, naming each task after the screen title. This gives you a visible queue of work. Create the screens directly after each task has been planned.
40
43
 
@@ -257,6 +260,44 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/image-nodes/$SCREEN_
257
260
 
258
261
  Pass the already-computed `actorCellId` directly as `cellId` in either path. Expect success (MCP: `created: true`; curl: `204`). On failure, read the validation error, fix the payload, and retry once before reporting failure.
259
262
 
263
+ ### Step 5b(ii) — Set field data lineage (mandatory)
264
+
265
+ Push the `fields` planned in Step 2 onto the node with a `node:changed` call. Every field needs a `mapping`:
266
+
267
+ | Field type | `mapping` | Example |
268
+ |---|---|---|
269
+ | User types a value, sent as a command | `"<CommandTitle>.<fieldName>"` | `"ReserveBike.bikeId"` |
270
+ | Displayed data, sourced from a read model | `"<ReadModelTitle>.<fieldName>"` | `"ActiveReservationView.status"` |
271
+ | Calculated/formatted only for display | `"derived:<expression>"` | `"derived:formatDuration(durationMinutes)"` |
272
+
273
+ Name the read model even if it doesn't exist as a board node yet — this skill only creates SCREEN/HTML_SCREEN nodes, never READMODEL nodes or connections. But naming the source is **not optional**: a screen displaying data should almost never have a field with no mapping. Set `cardinality` too (`"Single"` unless it's a repeated/list value).
274
+
275
+ **Prefer MCP:**
276
+ ```
277
+ mcp__eventmodelers__submit_node_events {
278
+ "boardId": "<BOARD_ID>",
279
+ "events": [{
280
+ "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<SCREEN_NODE_ID>",
281
+ "boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
282
+ "changedAttributes": ["meta.fields"],
283
+ "meta": { "type": "HTML_SCREEN", "fields": [ /* planned fields */ ] }
284
+ }]
285
+ }
286
+ ```
287
+
288
+ **Fallback (no MCP):**
289
+ ```bash
290
+ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
291
+ -H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: agent" \
292
+ -H "Content-Type: application/json" \
293
+ -d '[{
294
+ "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<SCREEN_NODE_ID>",
295
+ "boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
296
+ "changedAttributes": ["meta.fields"],
297
+ "meta": { "type": "HTML_SCREEN", "fields": [ /* planned fields */ ] }
298
+ }]'
299
+ ```
300
+
260
301
  ### Step 5c — Mark the task complete
261
302
 
262
303
  After the node and sketch succeed, mark the task for this screen as completed using TaskUpdate.
@@ -129,7 +129,52 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/images/$NODE_ID/sket
129
129
 
130
130
  Expect `204 No Content` on success.
131
131
 
132
- ## Step 5 — Report back
132
+ ## Step 5 — Define field data lineage (mandatory)
133
+
134
+ Every screen — new or updated — needs `meta.fields`: one entry per piece of data the screen displays or captures, each with a `mapping` naming where that data comes from. A screen with only a title and no fields is an empty placeholder from a data-lineage standpoint, even once the wireframe is rendered.
135
+
136
+ | Field type | `mapping` | Example |
137
+ |---|---|---|
138
+ | User types a value, sent as a command | `"<CommandTitle>.<fieldName>"` | `"ReserveBike.bikeId"` |
139
+ | Read from session | `"session:<fieldName>"` | `"session:customerId"` |
140
+ | Displayed data, sourced from a read model | `"<ReadModelTitle>.<fieldName>"` | `"ActiveReservationView.status"` |
141
+ | Calculated/formatted only for display | `"derived:<expression>"` | `"derived:formatDuration(durationMinutes)"` |
142
+
143
+ Name the read model even if it doesn't exist as a board node yet — this skill only renders the screen, it does not create READMODEL nodes or connections. But naming the source is **not optional**: a screen displaying data should almost never have a field with no mapping. If you can't say which read model a displayed field comes from, that's a sign the model is missing something — not a reason to skip the field.
144
+
145
+ Set `cardinality` too (`"Single"` unless the field is a repeated/list value), then push the fields onto the node:
146
+
147
+ **Prefer MCP:**
148
+ ```
149
+ mcp__eventmodelers__submit_node_events {
150
+ "boardId": "<BOARD_ID>",
151
+ "events": [{
152
+ "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
153
+ "boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
154
+ "changedAttributes": ["meta.fields"],
155
+ "meta": { "type": "SCREEN", "fields": [
156
+ {"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
157
+ ] }
158
+ }]
159
+ }
160
+ ```
161
+
162
+ **Fallback (no MCP):**
163
+ ```bash
164
+ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
165
+ -H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: agent" \
166
+ -H "Content-Type: application/json" \
167
+ -d '[{
168
+ "id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
169
+ "boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
170
+ "changedAttributes": ["meta.fields"],
171
+ "meta": { "type": "SCREEN", "fields": [
172
+ {"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
173
+ ] }
174
+ }]'
175
+ ```
176
+
177
+ ## Step 6 — Report back
133
178
 
134
179
  Tell the user:
135
180
  - The node ID that was updated