@eventmodelers/cli 1.0.85 → 1.0.87
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 +2 -2
- package/cli.js +109 -11
- package/package.json +1 -1
- package/shared/build-kit/README.md +43 -1
- package/shared/build-kit/lib/ralph.js +40 -11
- package/shared/build-kit/lib/tracing.js +155 -0
- package/shared/build-kit/ralph-claude.js +23 -3
- package/shared/build-kit/ralph-exec.js +60 -5
- package/shared/skills/learn-eventmodelers-api/SKILL.md +0 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +2 -1
- package/stacks/modeling-kit/templates/.claude/skills/html-screen/SKILL.md +1 -0
- package/stacks/react/templates/build-kit/README.md +0 -134
- package/stacks/react/templates/build-kit/lib/ralph.js +0 -517
- package/stacks/react/templates/build-kit/package.json +0 -9
- package/stacks/react/templates/build-kit/ralph-claude.js +0 -107
- package/stacks/react/templates/build-kit/ralph-local-ai.js +0 -40
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// RALPH_EXEC_CMD="opencode run" node ralph-exec.js
|
|
18
18
|
// Or persist it as localAi.exec in .eventmodelers/config.json.
|
|
19
19
|
|
|
20
|
-
import { startRalph, loadLocalConfig } from './lib/ralph.js';
|
|
20
|
+
import { startRalph, loadLocalConfig, resolveAgentIdentity } from './lib/ralph.js';
|
|
21
|
+
import { createSliceTracer, usageFromHarnessEvent, sumUsage } from './lib/tracing.js';
|
|
21
22
|
import { spawn } from 'child_process';
|
|
22
23
|
import { writeFileSync, mkdtempSync } from 'fs';
|
|
23
24
|
import { tmpdir } from 'os';
|
|
@@ -29,6 +30,8 @@ const projectDir = process.argv[2] ? resolve(process.argv[2]) : resolve(kitDir,
|
|
|
29
30
|
|
|
30
31
|
const cfg = loadLocalConfig(kitDir);
|
|
31
32
|
const localOnly = process.env.RALPH_LOCAL === '1';
|
|
33
|
+
// Same as ralph-claude.js: childEnv is built at load time, before startRalph resolves identity.
|
|
34
|
+
Object.assign(cfg, resolveAgentIdentity(kitDir, 'BUILD', cfg));
|
|
32
35
|
const execCmd = process.env.RALPH_EXEC_CMD || cfg.localAi?.exec;
|
|
33
36
|
|
|
34
37
|
if (!execCmd) {
|
|
@@ -50,6 +53,21 @@ const childEnv = {
|
|
|
50
53
|
...(cfg.agentId && !localOnly ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
|
|
51
54
|
};
|
|
52
55
|
|
|
56
|
+
// What each slice build turn cost — see lib/tracing.js. Same opt-in as ralph-claude.js, but a
|
|
57
|
+
// harness only reports usage in its JSON output mode (`codex exec --json`, `opencode run --format
|
|
58
|
+
// json`, `gemini --output-format stream-json`), so without that flag a turn has nothing to record.
|
|
59
|
+
const tracingOn = !localOnly && cfg.agentTracing === true;
|
|
60
|
+
if (!tracingOn && !localOnly) console.log('[ralph-exec] agent tracing off — no slice cost data is sent (turn on: eventmodelers config --agent-tracing on)');
|
|
61
|
+
const tracer = createSliceTracer({
|
|
62
|
+
baseUrl: cfg.baseUrl,
|
|
63
|
+
token: cfg.token,
|
|
64
|
+
organizationId: cfg.organizationId,
|
|
65
|
+
agentId: cfg.agentId,
|
|
66
|
+
traceFile: join(projectDir, '.eventmodelers', 'trace', 'slices.jsonl'),
|
|
67
|
+
log: (line) => console.log(`[ralph-exec] ${line}`),
|
|
68
|
+
enabled: tracingOn,
|
|
69
|
+
});
|
|
70
|
+
|
|
53
71
|
const promptDir = mkdtempSync(join(tmpdir(), 'ralph-exec-'));
|
|
54
72
|
|
|
55
73
|
// POSIX single-quote escaping: close, insert an escaped quote, reopen. The prompt is
|
|
@@ -60,22 +78,59 @@ function shellQuote(s) {
|
|
|
60
78
|
|
|
61
79
|
console.log(`[ralph-exec] command: ${execCmd}`);
|
|
62
80
|
|
|
63
|
-
|
|
81
|
+
// Parses a harness's stdout for usage while still passing it through. Most harnesses emit JSONL;
|
|
82
|
+
// Gemini's `--output-format json` is one pretty-printed object, hence the whole-output fallback.
|
|
83
|
+
function collectUsage(stream) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
let buffer = '';
|
|
86
|
+
let all = '';
|
|
87
|
+
const take = (text) => {
|
|
88
|
+
try { const u = usageFromHarnessEvent(JSON.parse(text)); if (u) parts.push(u); } catch { /* not JSON */ }
|
|
89
|
+
};
|
|
90
|
+
stream.on('data', (chunk) => {
|
|
91
|
+
process.stdout.write(chunk);
|
|
92
|
+
const text = chunk.toString();
|
|
93
|
+
all += text;
|
|
94
|
+
buffer += text;
|
|
95
|
+
const lines = buffer.split('\n');
|
|
96
|
+
buffer = lines.pop();
|
|
97
|
+
for (const line of lines) if (line.trim()) take(line);
|
|
98
|
+
});
|
|
99
|
+
return () => {
|
|
100
|
+
if (buffer.trim()) take(buffer);
|
|
101
|
+
if (!parts.length && all.trim()) take(all);
|
|
102
|
+
return sumUsage(parts);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// `slice` is set for a planned-slice build (see startRalph) — the only turns that are traced.
|
|
107
|
+
function runExec(prompt, slice = null) {
|
|
64
108
|
return new Promise((resolvePromise, reject) => {
|
|
65
109
|
const full = inlineHeader + prompt;
|
|
66
110
|
const promptFile = join(promptDir, 'prompt.md');
|
|
67
111
|
writeFileSync(promptFile, full);
|
|
112
|
+
const traced = tracingOn && !!slice;
|
|
113
|
+
const startedAt = Date.now();
|
|
68
114
|
|
|
69
115
|
// stdio inherit: the harness owns its own output format, and there is no
|
|
70
116
|
// cross-harness stream schema to parse into the condensed per-step logging
|
|
71
|
-
// that ralph-claude.js does — so it goes straight through.
|
|
117
|
+
// that ralph-claude.js does — so it goes straight through. A traced turn pipes
|
|
118
|
+
// stdout instead, only to read usage from it; the output is still echoed as is.
|
|
72
119
|
const proc = spawn(`${execCmd} ${shellQuote(full)}`, {
|
|
73
120
|
cwd: projectDir,
|
|
74
|
-
stdio: 'inherit',
|
|
121
|
+
stdio: ['inherit', traced ? 'pipe' : 'inherit', 'inherit'],
|
|
75
122
|
shell: true,
|
|
76
123
|
env: { ...childEnv, RALPH_PROMPT_FILE: promptFile },
|
|
77
124
|
});
|
|
78
|
-
|
|
125
|
+
const usage = traced ? collectUsage(proc.stdout) : null;
|
|
126
|
+
proc.on('close', (code) => {
|
|
127
|
+
if (traced) {
|
|
128
|
+
const u = usage();
|
|
129
|
+
if (u) tracer.record({ ...slice, status: code === 0 ? 'ok' : 'error' }, { ...u, durationMs: u.durationMs ?? Date.now() - startedAt });
|
|
130
|
+
else console.log('[ralph-exec] trace: no usage in the command output — run the harness in its JSON output mode to record slice cost');
|
|
131
|
+
}
|
|
132
|
+
code === 0 ? resolvePromise() : reject(new Error(`exec command exited ${code}`));
|
|
133
|
+
});
|
|
79
134
|
proc.on('error', reject);
|
|
80
135
|
});
|
|
81
136
|
}
|
|
@@ -63,7 +63,6 @@ Write tools take their items as an array (`nodeIds[]`, `elements[]`, `connection
|
|
|
63
63
|
| `render_screen` | `boardId`, `nodeId`, `elements[]?` (SCREEN) or `pages[]?`+`backgroundColor?` (HTML_SCREEN), `description?` | Update an existing screen's content — exactly one of `elements`/`pages` | §4 `POST .../images/:id/sketch` + `image-nodes` |
|
|
64
64
|
| `add_field_examples` | `boardId`, `nodeId?`, `name?`, `cellName?`, `timelineId?` | Fill empty field examples using linked-node context | — (MCP-only convenience) |
|
|
65
65
|
| `get_attribute_chain` | `boardId`, `timelineId`, `targetCellName`, `sourceCellName` | Resolve every node between two cells, ordered target→source. Both cell names must come from a node's reported `cellName` (`get_board_outline`, `get_nodes`, `get_node`) — a guessed address that happens to exist silently resolves the wrong chain | — (MCP-only convenience) |
|
|
66
|
-
| `verify_screen` | `boardId`, `nodeId` | Check a screen node exists and has rendered content — works for both SCREEN and HTML_SCREEN, dispatching on the node's actual type | — (MCP-only convenience) |
|
|
67
66
|
| `get_image_snapshot_description` | `boardId`, `nodeId` | Load the `{elements:[...]}` sketch description from storage | — (reads what §4 sketch endpoints write) |
|
|
68
67
|
| `validate_slice_data` | `sliceData` | Offline validation of a `SliceDataOutput` payload — no board access | — (MCP-only, pure function) |
|
|
69
68
|
| `commit_board_to_git` | `boardId` | Force a git-extension commit/push, bypassing the autoCommit gate | — (MCP-only; git extension) |
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md
CHANGED
|
@@ -231,6 +231,7 @@ mcp__eventmodelers__create_screen {
|
|
|
231
231
|
"nodeId": "<node-uuid>",
|
|
232
232
|
"chapterId": "<CHAPTER_ID>",
|
|
233
233
|
"cellId": "<actorRowId>-<columnId>",
|
|
234
|
+
"title": "<Screen Title>",
|
|
234
235
|
"pages": ["<div>...</div>"],
|
|
235
236
|
"description": "<concise description of what this screen shows>",
|
|
236
237
|
"fields": [ /* per "Mandatory Field Definitions" below — set in this same call */ ]
|
|
@@ -240,7 +241,7 @@ mcp__eventmodelers__create_screen {
|
|
|
240
241
|
|
|
241
242
|
**Fallback (no MCP):** see `references/api-fallback.md` — "Mandatory Screen Rendering — Step B: Create the HTML_SCREEN node".
|
|
242
243
|
|
|
243
|
-
The MCP `create_screen` call above already sets `meta.fields` (per "Mandatory Field Definitions" below) in the same call — no separate `node:changed` follow-up needed when using MCP.
|
|
244
|
+
The MCP `create_screen` call above already sets the node's `title` and `meta.fields` (per "Mandatory Field Definitions" below) in the same call — no separate `node:changed` follow-up needed when using MCP.
|
|
244
245
|
|
|
245
246
|
A storyboard screen is placed at a *provisional* position — Steps 4 and 5 wire it to its COMMAND / READMODEL once those exist, and may move it first. Pass `autoConnect: false` on `create_screen` here so the placement doesn't pre-wire the screen to whatever happens to sit in the adjacent column; the real `SCREEN → COMMAND` and `READMODEL → SCREEN` edges are created deliberately in Steps 4 and 5. When creating several screens whose HTML is already authored, put them all in one `create_screen` call's `screens` array with `autoConnect: false`.
|
|
246
247
|
|
|
@@ -140,6 +140,7 @@ mcp__eventmodelers__create_screen {
|
|
|
140
140
|
"nodeId": "<generated-uuid>",
|
|
141
141
|
"chapterId": "<CHAPTER_ID>",
|
|
142
142
|
"cellName": "<CELL_NAME>",
|
|
143
|
+
"title": "<Screen Title>",
|
|
143
144
|
"pages": ["<div>...</div>"],
|
|
144
145
|
"fields": [ /* optional — set meta.fields here too (see Step 5) instead of a separate call */ ]
|
|
145
146
|
}]
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
# .build-kit
|
|
2
|
-
|
|
3
|
-
Ralph's runtime directory. Contains the agent loop, board poller, prompts, and Claude skills.
|
|
4
|
-
|
|
5
|
-
> This is the **react** (API-access) stack — it talks to the board purely through
|
|
6
|
-
> the plain REST `slicedata` endpoint on `api.eventmodelers.ai`, polled on an interval.
|
|
7
|
-
> There is no Supabase/PocketBase realtime subscription and no direct database table
|
|
8
|
-
> access anywhere in this kit. For instant push notifications instead of polling, use
|
|
9
|
-
> the `supabase-react` stack.
|
|
10
|
-
|
|
11
|
-
## Quick start
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# Claude (default)
|
|
15
|
-
node .build-kit/ralph-claude.js
|
|
16
|
-
|
|
17
|
-
# Local or self-hosted model — Ollama (run `ollama serve` first)
|
|
18
|
-
LOCAL_AI_TARGET=ollama node .build-kit/ralph-local-ai.js
|
|
19
|
-
|
|
20
|
-
# …or any OpenAI-compatible server (vLLM, LM Studio, llama.cpp, TGI)
|
|
21
|
-
LOCAL_AI_TARGET=vllm LOCAL_AI_MODEL=Qwen/Qwen3-8B node .build-kit/ralph-local-ai.js
|
|
22
|
-
|
|
23
|
-
# Custom project directory (defaults to the parent of .build-kit)
|
|
24
|
-
node .build-kit/ralph-claude.js /path/to/project
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Files
|
|
28
|
-
|
|
29
|
-
**Entry points** (top level):
|
|
30
|
-
|
|
31
|
-
| File | Purpose |
|
|
32
|
-
|------|---------|
|
|
33
|
-
| `ralph-claude.js` | Runs the full loop using Claude Code as the executor |
|
|
34
|
-
| `ralph-local-ai.js` | Runs the full loop using a local/self-hosted model (Ollama, vLLM, LM Studio, llama.cpp) |
|
|
35
|
-
| `ralph-exec.js` | Runs the full loop handing each prompt to an external agent command (Codex CLI, OpenCode, …) |
|
|
36
|
-
| `ralph.sh` | Shell-based loop — alternative to the JS entry points |
|
|
37
|
-
|
|
38
|
-
**Internals** (`lib/`):
|
|
39
|
-
|
|
40
|
-
| File | Purpose |
|
|
41
|
-
|------|---------|
|
|
42
|
-
| `lib/ralph.js` | Shared library — board poller + loop logic; imported by the entry points |
|
|
43
|
-
| `lib/local-ai-agent.js` | Local-AI executor — called by `ralph-local-ai.js`, can also run manually |
|
|
44
|
-
| `lib/agent.sh` | Thin shell wrapper around `claude` — called by `ralph.sh` |
|
|
45
|
-
| `lib/prompt.md` | Phase 1 prompt: tells Claude how to load a slice from the board |
|
|
46
|
-
| `lib/backend-prompt.md` | Phase 2 prompt: tells Claude how to build a planned slice |
|
|
47
|
-
| `lib/AGENT.md` | Agent instructions included in Claude's context |
|
|
48
|
-
|
|
49
|
-
## How it works
|
|
50
|
-
|
|
51
|
-
**Phase 1** — triggered when `tasks.json` has entries:
|
|
52
|
-
- The poller writes a task to `tasks.json` each time it notices a slice's status changed since the last poll
|
|
53
|
-
- The loop picks it up and runs Claude (or a local model) with `prompt.md`
|
|
54
|
-
- Claude loads the slice data and updates `.slices/`
|
|
55
|
-
|
|
56
|
-
**Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
|
|
57
|
-
- The loop runs Claude with `backend-prompt.md`
|
|
58
|
-
- Claude implements the slice in the project
|
|
59
|
-
- Phase 2 is Claude-only; local-AI mode skips it (local-ai-agent handles its own queue)
|
|
60
|
-
|
|
61
|
-
Both phases run in a continuous loop with a 3-second idle sleep. The board poller runs concurrently in the same process, re-fetching `slicedata/slices` every `RALPH_POLL_INTERVAL_MS` (default 10s — see `lib/ralph.js`).
|
|
62
|
-
|
|
63
|
-
## Local-AI configuration
|
|
64
|
-
|
|
65
|
-
`ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
|
|
66
|
-
Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
|
|
70
|
-
LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
|
|
71
|
-
LOCAL_AI_MODEL=qwen3.5:9b # model name as the server knows it
|
|
72
|
-
LOCAL_AI_API=openai # force the wire dialect: ollama | openai (normally inferred)
|
|
73
|
-
LOCAL_AI_API_KEY=local # sent as `Authorization: Bearer` on the openai dialect
|
|
74
|
-
LOCAL_AI_NUM_CTX=32768 # ollama only — context window (default 32768)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
**Do not lower `LOCAL_AI_NUM_CTX`.** The MCP tool schemas are ~16k tokens on their own.
|
|
78
|
-
Ollama's own default is 4096, which silently truncates them — the model then sees a
|
|
79
|
-
fragment of the tool list and invents tool names instead of failing, which is why the
|
|
80
|
-
default here is raised rather than left to the server. On the `openai` dialect the
|
|
81
|
-
equivalent is set when you launch the server (vLLM `--max-model-len 32768`,
|
|
82
|
-
llama.cpp `-c 32768`); an overflow there surfaces as an HTTP 400.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
## External agent commands (`--exec`)
|
|
86
|
-
|
|
87
|
-
Agentic harnesses that bring their own tool loop — Codex CLI, OpenCode, Gemini CLI —
|
|
88
|
-
are not `--local-ai` targets: `--local-ai` *supplies* the agent loop, while a harness
|
|
89
|
-
already is one and only wants a prompt. They go through `ralph-exec.js` instead:
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
npx @eventmodelers/cli run --exec "codex exec --full-auto"
|
|
93
|
-
npx @eventmodelers/cli run --exec "opencode run"
|
|
94
|
-
|
|
95
|
-
# …or persist it and use the bare flag
|
|
96
|
-
RALPH_EXEC_CMD="codex exec --full-auto" node .build-kit/ralph-exec.js
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
The prompt is appended to the command as one shell-quoted argument, and is also written
|
|
100
|
-
to a temp file named by `RALPH_PROMPT_FILE` for commands that prefer to read it. The
|
|
101
|
-
child runs with the project dir as its cwd and inherits stdio — a harness owns its own
|
|
102
|
-
output format, so there is no condensed per-step logging here the way `ralph-claude.js`
|
|
103
|
-
has it.
|
|
104
|
-
|
|
105
|
-
Persist a default alongside the local-AI settings:
|
|
106
|
-
|
|
107
|
-
```json
|
|
108
|
-
{
|
|
109
|
-
"localAi": {
|
|
110
|
-
"exec": "codex exec --full-auto"
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
One caveat worth knowing before reaching for this: the kits' prompts assume Claude
|
|
116
|
-
Code's `Skill` tool and `CLAUDE.md`. Other harnesses read `AGENTS.md` and have no skill
|
|
117
|
-
primitive, so `init-agents` puts the skill files where they can find them, but
|
|
118
|
-
`lib/prompt.md` / `lib/backend-prompt.md` still need wording that says *read and follow*
|
|
119
|
-
a skill file rather than *invoke* it.
|
|
120
|
-
|
|
121
|
-
## Config
|
|
122
|
-
|
|
123
|
-
Credentials are stored in `.build-kit/.eventmodelers/config.json` (written by `eventmodelers init`):
|
|
124
|
-
|
|
125
|
-
```json
|
|
126
|
-
{
|
|
127
|
-
"organizationId": "...",
|
|
128
|
-
"boardId": "...",
|
|
129
|
-
"token": "...",
|
|
130
|
-
"baseUrl": "https://api.eventmodelers.ai"
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Claude skills live in `.build-kit/.claude/skills/` and are available inside any Claude Code session started from `.build-kit/`.
|