@eventmodelers/cli 1.0.7 → 1.0.9
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
|
@@ -1932,9 +1932,10 @@ program
|
|
|
1932
1932
|
.command('fetch')
|
|
1933
1933
|
.description('Pull full slice detail for one context on the board via the slicedata API and write it into .slices/ — the pull-based counterpart to `listen`, without screen images')
|
|
1934
1934
|
.requiredOption('--context <name>', 'Name of the MODEL_CONTEXT to fetch')
|
|
1935
|
-
.option('--
|
|
1936
|
-
.option('--slice-
|
|
1937
|
-
.option('--
|
|
1935
|
+
.option('--format <format>', 'Output format: json (default, builds the full .slices/ folder structure), yaml, textual, toon, emlang, or esdm (each of these five is dumped to a single .slices/<context>/slicedata.<ext> file instead)', 'json')
|
|
1936
|
+
.option('--slice-id <id>', 'After fetching, print just the slice with this id (requires --format json)')
|
|
1937
|
+
.option('--slice-title <title>', 'After fetching, print just the slice with this title, case-insensitive (requires --format json)')
|
|
1938
|
+
.option('--spec-kitty', "After fetching, also restate this context as a Spec Kitty mission brief (.kittify/mission-brief.md via `spec-kitty intake`) — deterministic, no LLM call, no mission/spec.md/tasks created. Run `/spec-kitty.specify` afterward to turn the brief into a mission. Requires `spec-kitty init` to already be set up in this project (see lib/adapters/spec-kitty-adapter.js) and --format json. One-shot: does not start a loop.")
|
|
1938
1939
|
.action(async (opts, command) => {
|
|
1939
1940
|
const cwd = process.cwd();
|
|
1940
1941
|
const kitDir = findInstalledKitDir(cwd);
|
package/lib/fetch.js
CHANGED
|
@@ -17,6 +17,14 @@ export class FetchAuthError extends Error {
|
|
|
17
17
|
|
|
18
18
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
19
|
|
|
20
|
+
// Mirrors the server's exporter registry (backend/src/slices/change/slicedata/exporters/
|
|
21
|
+
// ExporterRegistry.ts) — json is the canonical shape (drives the full .slices/ folder
|
|
22
|
+
// structure below); every other format is a transform of it and gets dumped as a single
|
|
23
|
+
// file instead, so its extension reflects the transform's actual output, not its name
|
|
24
|
+
// (textual is JSON-wrapped; emlang/esdm are YAML documents; toon is its own thing).
|
|
25
|
+
const SUPPORTED_FORMATS = ['json', 'yaml', 'textual', 'toon', 'emlang', 'esdm'];
|
|
26
|
+
const FORMAT_EXTENSIONS = { yaml: 'yaml', textual: 'json', toon: 'toon', emlang: 'yaml', esdm: 'yaml' };
|
|
27
|
+
|
|
20
28
|
// `--context` accepts any of: a MODEL_CONTEXT name or id, or a timeline (CHAPTER) name or id.
|
|
21
29
|
// /slicedata's contextId/contextName params now both resolve against MODEL_CONTEXT nodes first,
|
|
22
30
|
// then timelines (id matched exactly, name case-insensitively) — including a timeline with no
|
|
@@ -72,7 +80,7 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
72
80
|
// never "board not found". Only 401/403 are credential problems worth the
|
|
73
81
|
// reconfigure-and-retry dance in cli.js; 404 gets reported and the process exits,
|
|
74
82
|
// same as any other non-auth error.
|
|
75
|
-
async function
|
|
83
|
+
async function fetchResponse(url, what, { allow404 = false } = {}) {
|
|
76
84
|
let res;
|
|
77
85
|
try {
|
|
78
86
|
res = await fetch(url, { headers });
|
|
@@ -92,7 +100,19 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
92
100
|
console.error(`❌ ${what}: HTTP ${res.status}`);
|
|
93
101
|
process.exit(1);
|
|
94
102
|
}
|
|
95
|
-
return res
|
|
103
|
+
return res;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function fetchJson(url, what, options) {
|
|
107
|
+
const res = await fetchResponse(url, what, options);
|
|
108
|
+
return res ? res.json() : res;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Non-json formats aren't a {slices: [...]} payload — just the exporter's raw output
|
|
112
|
+
// (a YAML doc, a TOON encoding, ...) — so it's written straight to disk, not parsed.
|
|
113
|
+
async function fetchText(url, what, options) {
|
|
114
|
+
const res = await fetchResponse(url, what, options);
|
|
115
|
+
return res ? res.text() : res;
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
// Falls back to cwd when no kit is installed — fetch doesn't need kit-specific
|
|
@@ -100,6 +120,17 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
100
120
|
const SLICES_DIR = join(kitDir || cwd, '.slices');
|
|
101
121
|
|
|
102
122
|
const contextInput = opts.context;
|
|
123
|
+
const format = (opts.format || 'json').toLowerCase();
|
|
124
|
+
if (!SUPPORTED_FORMATS.includes(format)) {
|
|
125
|
+
console.error(`❌ Unsupported format "${format}". Supported: ${SUPPORTED_FORMATS.join(', ')}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
// --slice-id/--slice-title/--spec-kitty all depend on the parsed {slices: [...]} list
|
|
129
|
+
// that only the json format produces — fail fast instead of silently ignoring them.
|
|
130
|
+
if (format !== 'json' && (opts.sliceId || opts.sliceTitle || opts.specKitty)) {
|
|
131
|
+
console.error('❌ --slice-id, --slice-title, and --spec-kitty require --format json (the default).');
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
103
134
|
|
|
104
135
|
console.log(`▶ Fetching context "${contextInput}" from ${baseUrl} (board ${cfg.boardId})...`);
|
|
105
136
|
|
|
@@ -112,10 +143,25 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
|
|
|
112
143
|
const contextQuery = UUID_RE.test(contextInput)
|
|
113
144
|
? `contextId=${encodeURIComponent(contextInput)}`
|
|
114
145
|
: `contextName=${encodeURIComponent(contextInput)}`;
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
146
|
+
const url = `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${contextQuery}&format=${format}`;
|
|
147
|
+
|
|
148
|
+
// Only json builds the full .slices/<context>/<slice>/slice.json folder structure —
|
|
149
|
+
// every other format has no guaranteed {slices: [...]} shape to walk (it's the
|
|
150
|
+
// exporter's raw output: a YAML doc, a TOON encoding, ...), so it's just dumped
|
|
151
|
+
// as a single file. The resolved context name isn't known without parsing JSON,
|
|
152
|
+
// so the folder is slugified from the raw --context input instead.
|
|
153
|
+
if (format !== 'json') {
|
|
154
|
+
const body = await fetchText(url, `slicedata?${contextQuery}&format=${format}`);
|
|
155
|
+
const contextSlug = slugify(contextInput) || 'default';
|
|
156
|
+
const baseFolder = join(SLICES_DIR, contextSlug);
|
|
157
|
+
mkdirSync(baseFolder, { recursive: true });
|
|
158
|
+
const outFile = join(baseFolder, `slicedata.${FORMAT_EXTENSIONS[format]}`);
|
|
159
|
+
writeFileSync(outFile, body);
|
|
160
|
+
console.log(`✅ Fetched context "${contextInput}" as ${format} → ${relative(cwd, outFile)}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const payload = await fetchJson(url, `slicedata?${contextQuery}&format=${format}`);
|
|
119
165
|
const { slices: allSlices } = payload;
|
|
120
166
|
const displayContext = allSlices[0]?.context || contextInput;
|
|
121
167
|
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
|
|
|
26
26
|
| `get_board_events` | `boardId` | All board events, in sequence | §1 `GET .../events` |
|
|
27
27
|
| `search_board_events` | `boardId`, `name` | Search events by node name | §1 `GET .../events/search` |
|
|
28
28
|
| `submit_node_events` | `boardId`, `events[]` | Create/update nodes (raw `NodeChangeEvent`/edge events) | §3 `POST .../nodes/events` |
|
|
29
|
-
| `delete_node` | `boardId`, `nodeId` | Delete a node | (via `node:deleted` event, §3) |
|
|
29
|
+
| `delete_node` | `boardId`, `nodeId` | Delete a node. Deleting a chapter (timeline) cascades — every node placed in one of its cells, plus any node parented to it (e.g. SLICE_BORDER), is deleted too, along with all their edges | (via `node:deleted` event, §3) |
|
|
30
30
|
| `create_drawing` | `boardId`, `kind`, `x`, `y`, `width`, `height`, ... | Freehand canvas annotation (path/rect/text) — never placed in a cell | — (no REST equivalent; MCP-only) |
|
|
31
31
|
| `find_nodes_in_drawing` | `boardId`, `drawingId` | Nodes fully contained inside a drawing's bounding box | — (no REST equivalent; MCP-only) |
|
|
32
32
|
| `create_chapter` | `boardId`, `x?`, `y?` | Create a timeline | §2 `POST .../chapters` |
|
|
@@ -34,12 +34,12 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
|
|
|
34
34
|
| `delete_column` | `boardId`, `timelineId`, `columnId` | Delete a column | §2 `DELETE .../columns/:columnId` |
|
|
35
35
|
| `add_lane` | `boardId`, `timelineId`, `type`, `label?`, `index?` | Add a lane/row | §2 `POST .../timelines/:id/lanes` |
|
|
36
36
|
| `remove_lane` | `boardId`, `timelineId`, `rowId` | Remove a lane | — (extends §2; no direct REST route) |
|
|
37
|
-
| `move_node_in_timeline` | `boardId`, `timelineId`, `movedNodeId`, `toCellId` | Move a placed node to another cell | — (MCP-only convenience) |
|
|
37
|
+
| `move_node_in_timeline` | `boardId`, `timelineId`, `movedNodeId`, `toCellId` | Move a placed node to another cell — its previous cell is automatically cleared | — (MCP-only convenience) |
|
|
38
38
|
| `move_timeline_structure` | `boardId`, `timelineId`, `kind` (`'column'\|'lane'`), `id`, `toIndex` | Reorder a column or lane (row) — `kind` picks which `id` refers to | — (MCP-only convenience) |
|
|
39
39
|
| `move_timeline_position` | `boardId`, `timelineId`, `x`, `y` | Move a chapter node on canvas | — (MCP-only convenience) |
|
|
40
|
-
| `drop_node_to_cell` | `boardId`, `timelineId`, `cellId`, `nodeId`, `nodeType` | Place an existing node into a cell | §2 `POST .../cells/:cellId/drop` |
|
|
41
|
-
| `clear_cell` | `boardId`, `timelineId`, `cellId` |
|
|
42
|
-
| `create_slice` | `boardId`, `timelineId`, `type`, `index
|
|
40
|
+
| `drop_node_to_cell` | `boardId`, `timelineId`, `cellId`, `nodeId`, `nodeType` | Place an existing node into a cell — if it was already placed elsewhere on this timeline, that cell is automatically cleared | §2 `POST .../cells/:cellId/drop` |
|
|
41
|
+
| `clear_cell` | `boardId`, `timelineId`, `cellId` | Unassign the node from a cell without deleting it — the cell becomes empty and the node survives (unplaced); no-op if already empty. Use `delete_node` to remove the node entirely | — (MCP-only convenience) |
|
|
42
|
+
| `create_slice` | `boardId`, `timelineId`, `type`, `index?`, `nodes?: {actor?, interaction?, swimlane?}` (each `{rowId?, title?}`) | Create a full slice (column + nodes + SLICE_BORDER). `rowId` targets a specific lane when the chapter has more than one lane of that type (e.g. several actor lanes); omit to use the first matching lane | §5 `POST .../slices` |
|
|
43
43
|
| `create_slice_definition` | `boardId`, `timelineId`, `columnId`, `title`, `data?`, `meta?` | Create a SLICE_BORDER over an existing column | §5 `POST .../slice-definitions` |
|
|
44
44
|
| `place_element` | `boardId`, `timelineId`, `elementType`, `title`, `columnIndex?` | Find/create an empty cell in the right lane and place a COMMAND/READMODEL/EVENT | — (MCP-only convenience; composes §2+§3) |
|
|
45
45
|
| `list_slices` | `boardId` | List slices (id, title, status) | §8 `GET .../slicedata/slices` |
|
|
@@ -254,7 +254,7 @@ Add a lane (row) to a timeline.
|
|
|
254
254
|
---
|
|
255
255
|
|
|
256
256
|
### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/cells/:cellId/drop`
|
|
257
|
-
Drop a node into a timeline cell. Validates placement rules.
|
|
257
|
+
Drop a node into a timeline cell. Validates placement rules. If the node was already placed in another cell on this timeline, that cell is automatically cleared as part of the same operation — a node can only ever occupy one cell.
|
|
258
258
|
|
|
259
259
|
**Request body**: `{ nodeId: string, nodeType: ElementType }`
|
|
260
260
|
|
|
@@ -283,6 +283,8 @@ Submit node change events.
|
|
|
283
283
|
|
|
284
284
|
Any `node:created` event carrying a `chapterId` plus `cellId`/`cellName` (i.e. placing a node on a timeline) also triggers a best-effort, fire-and-forget auto-connect to type-compatible neighbors — same rules as the auto-connect endpoint below. Failures there never fail this call.
|
|
285
285
|
|
|
286
|
+
A `node:deleted` event cascades: if the deleted node is a chapter (timeline), every node placed in one of its cells and any node parented to it (e.g. a SLICE_BORDER spanning one of its columns) is deleted too, along with all their edges.
|
|
287
|
+
|
|
286
288
|
**Request body**: `NodeChangeEvent[]`
|
|
287
289
|
|
|
288
290
|
```typescript
|
|
@@ -427,9 +429,9 @@ Create a complete slice (1 column + 3 nodes automatically placed).
|
|
|
427
429
|
type: 'state-change' | 'state-view' | 'automation'
|
|
428
430
|
index?: number
|
|
429
431
|
nodes?: {
|
|
430
|
-
actor?: Partial<NodeData>
|
|
431
|
-
interaction?: Partial<NodeData>
|
|
432
|
-
swimlane?: Partial<NodeData>
|
|
432
|
+
actor?: Partial<NodeData> & { rowId?: string }
|
|
433
|
+
interaction?: Partial<NodeData> & { rowId?: string }
|
|
434
|
+
swimlane?: Partial<NodeData> & { rowId?: string }
|
|
433
435
|
}
|
|
434
436
|
}
|
|
435
437
|
```
|
|
@@ -439,6 +441,8 @@ Create a complete slice (1 column + 3 nodes automatically placed).
|
|
|
439
441
|
- `state-view` → HTML_SCREEN (actor) + READMODEL (interaction) + EVENT (swimlane)
|
|
440
442
|
- `automation` → AUTOMATION (actor) + COMMAND (interaction) + EVENT (swimlane)
|
|
441
443
|
|
|
444
|
+
Each chapter has exactly one actor/interaction/swimlane lane by default, but a chapter can have several lanes of the same type (e.g. multiple actor lanes). Without a `rowId`, the node is always placed in the **first** lane of the matching type — pass `nodes.<actor|interaction|swimlane>.rowId` (a row id from the chapter's `timelineData.rows`) to target a specific lane instead. An invalid `rowId` (not found, or found but the wrong lane type) is a `400 ROW_NOT_FOUND`/`ROW_TYPE_MISMATCH` error.
|
|
445
|
+
|
|
442
446
|
The actor HTML_SCREEN is created as a **stub** — a single visibly-placeholder page ("Untitled screen — design pending") unless `nodes.actor.pages` is passed explicitly. Whoever calls this (the `add-next-slice` skill — the one that creates a brand-new slice from scratch, as opposed to `eventmodeling-slicing-event-models`, which only makes existing elements explicit) is responsible for immediately replacing that stub via the `html-screen` skill — including gathering the board's existing screens first so the new one matches their established style, since `html-screen` itself has no visibility into other screens.
|
|
443
447
|
|
|
444
448
|
**Response**: `200` — slice data
|
|
@@ -63,6 +63,8 @@ mcp__eventmodelers__create_slice { "boardId": "<BOARD_ID>", "timelineId": "<TL>"
|
|
|
63
63
|
|
|
64
64
|
Pick `type` based on what you decided in Step 1 — `state-change` for a new command, `state-view` for a new read model, `automation` for a new automation. Always pass `nodes.interaction.title` as the command/read model/automation name you decided on in Step 1 — per the Core Concept above, the slice is *named after that element*, and the backend only derives the slice title from this field; omitting it produces a useless generic "State Change"/"State View"/"Automation" label instead. This also creates the slice's `SLICE_BORDER` automatically — no separate `create_slice_definition` call needed.
|
|
65
65
|
|
|
66
|
+
If the chapter has more than one lane of the same type (e.g. several actor lanes for different user roles), `create_slice` places each node in the **first** matching lane by default — pass `nodes.<actor|interaction|swimlane>.rowId` (the target row's id, from the chapter's `timelineData.rows`) to target a specific lane instead of the first one.
|
|
67
|
+
|
|
66
68
|
**Fallback (no MCP):**
|
|
67
69
|
```bash
|
|
68
70
|
curl -X POST $BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/timelines/$TL/slices \
|