@eventmodelers/cli 1.0.86 → 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 +3 -3
- package/package.json +1 -1
- package/shared/build-kit/README.md +43 -1
- package/shared/build-kit/lib/tracing.js +63 -0
- package/shared/build-kit/ralph-exec.js +57 -4
package/README.md
CHANGED
|
@@ -125,8 +125,8 @@ npx @eventmodelers/cli init --stack <name> # scaffold a stack + install
|
|
|
125
125
|
npx @eventmodelers/cli init --stack <name> --demo # same, plus a ready-made demo model in the kit's .slices/ to build against
|
|
126
126
|
npx @eventmodelers/cli re-init # refresh an already-installed kit's scripts/skills only — never touches the root scaffold
|
|
127
127
|
npx @eventmodelers/cli run # start the agent loop (ralph-claude.js) from the installed kit dir
|
|
128
|
-
npx @eventmodelers/cli run --local-ai [target] #
|
|
129
|
-
npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js)
|
|
128
|
+
npx @eventmodelers/cli run --local-ai [target] # board work only via a local/self-hosted model (ralph-local-ai.js) — does NOT build slices; build locally with --exec
|
|
129
|
+
npx @eventmodelers/cli run --exec "<command>" # same, via an external agent harness (ralph-exec.js) — agent tracing needs its JSON flag, e.g. "codex exec --json --full-auto"
|
|
130
130
|
npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
|
|
131
131
|
npx @eventmodelers/cli run --local # skip platform config/credential lookup entirely — local-only, no board sync
|
|
132
132
|
npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
|
package/cli.js
CHANGED
|
@@ -3193,8 +3193,8 @@ credentialFlags(program
|
|
|
3193
3193
|
credentialFlags(program
|
|
3194
3194
|
.command('run')
|
|
3195
3195
|
.description('Start the agent loop from the installed kit dir — build-kit stacks: ralph-claude.js (default); modeling-kit: --modeling, or --standalone, which needs no install at all')
|
|
3196
|
-
.option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner: a build kit runs it via ralph-local-ai.js, and --modeling/--standalone via lib/modeling-local-ai.js (board tools over MCP, but no skills and no subagent fan-out — those are Claude Code features). Optional target preset picks the URL and wire dialect: ${LOCAL_AI_TARGETS.join(', ')} — bare --local-ai means ollama. Anything OpenAI-compatible (vLLM, LM Studio, llama.cpp, TGI) works by pointing LOCAL_AI_URL at it; see LOCAL_AI_* in the docs. Claude remains the default when this flag is absent.`)
|
|
3197
|
-
.option('--exec [command]', 'Hand each prompt to an external agent command instead of the default Claude runner, via ralph-exec.js (build-kit stacks only) — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto" or "opencode run". The prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent.')
|
|
3196
|
+
.option('--local-ai [target]', `Drive the loop with a local (or self-hosted) model instead of the default Claude runner: a build kit runs it via ralph-local-ai.js, and --modeling/--standalone via lib/modeling-local-ai.js (board tools over MCP, but no skills and no subagent fan-out — those are Claude Code features). Optional target preset picks the URL and wire dialect: ${LOCAL_AI_TARGETS.join(', ')} — bare --local-ai means ollama. Anything OpenAI-compatible (vLLM, LM Studio, llama.cpp, TGI) works by pointing LOCAL_AI_URL at it; see LOCAL_AI_* in the docs. Board work only: --local-ai never builds a Planned slice (no file, shell or git tools). To build code with a local model, use --exec with a coding harness pointed at it, e.g. "codex exec --oss --full-auto". Claude remains the default when this flag is absent.`)
|
|
3197
|
+
.option('--exec [command]', 'Hand each prompt to an external agent command instead of the default Claude runner, via ralph-exec.js (build-kit stacks only) — for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto" or "opencode run". The prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent. With agent tracing on, include the harness\'s JSON output flag (codex exec --json, opencode run --format json, gemini --output-format stream-json) — without it no slice cost is recorded.')
|
|
3198
3198
|
.option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
|
|
3199
3199
|
.option('--modeling', 'Keep one Claude process warm across prompts instead of spawning a fresh one per task, for low-latency voice/live use. Runs from a modeling-kit install in this directory, or from the global install (~/.eventmodelers/kit) when there is none. Built into the CLI, not a per-project file.')
|
|
3200
3200
|
.option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal — right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--global).')
|
|
@@ -3907,7 +3907,7 @@ program
|
|
|
3907
3907
|
program
|
|
3908
3908
|
.command('config')
|
|
3909
3909
|
.description('Print the fully resolved config (merged across the directory hierarchy + EVENTMODELERS_* env vars), with the token masked')
|
|
3910
|
-
.option('--agent-tracing <on|off>', "Turn agent tracing on or off for this project (saved as `agentTracing` in the project's .eventmodelers/config.json). Off: the build agent sends no slice cost data to the platform at all.")
|
|
3910
|
+
.option('--agent-tracing <on|off>', "Turn agent tracing on or off for this project (saved as `agentTracing` in the project's .eventmodelers/config.json). Off: the build agent sends no slice cost data to the platform at all. A non-Claude agent via run --exec is only traced when its command includes the harness's JSON output flag, e.g. \"codex exec --json --full-auto\".")
|
|
3911
3911
|
.action((opts, command) => {
|
|
3912
3912
|
const cwd = process.cwd();
|
|
3913
3913
|
const kitDir = findInstalledKitDir(cwd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.87",
|
|
4
4
|
"description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,7 @@ node .build-kit/ralph-claude.js /path/to/project
|
|
|
51
51
|
**Phase 2** — triggered when any file in `.slices/` contains `"status": "Planned"`:
|
|
52
52
|
- The loop runs Claude with `backend-prompt.md`
|
|
53
53
|
- Claude implements the slice in the project
|
|
54
|
-
- Phase 2
|
|
54
|
+
- Phase 2 (building slices) never runs with `--local-ai` — see *`--local-ai` does not build code*. Build with `--exec` instead
|
|
55
55
|
|
|
56
56
|
Both phases run in a continuous loop with a 3-second idle sleep. The realtime agent runs concurrently in the same process.
|
|
57
57
|
|
|
@@ -72,6 +72,34 @@ node .build-kit/realtime-agent.js
|
|
|
72
72
|
`ralph-local-ai.js` drives any local or self-hosted model that can do tool calling.
|
|
73
73
|
Claude (`ralph-claude.js`) stays the default runner — this is opt-in.
|
|
74
74
|
|
|
75
|
+
### `--local-ai` does not build code
|
|
76
|
+
|
|
77
|
+
**`--local-ai` works on the board only. It never builds a slice.** Planned slices stay
|
|
78
|
+
Planned. That is by design, not a bug:
|
|
79
|
+
|
|
80
|
+
- **Planned slices are never handed to it.** `ralph-local-ai.js` gives `startRalph` no
|
|
81
|
+
`onPlannedSlice`, so the loop only feeds it `tasks.json` prompts.
|
|
82
|
+
- **It has no coding tools.** `lib/local-ai-agent.js` exposes only the board MCP tools:
|
|
83
|
+
no file read/write, no shell, no git, no test run.
|
|
84
|
+
- **Its system prompt forbids file and shell access.** Such a request is answered with
|
|
85
|
+
`Blocked: …`.
|
|
86
|
+
- **A prompt stops after 12 tool-call rounds.** That is enough for board edits, but far
|
|
87
|
+
short of a slice build.
|
|
88
|
+
|
|
89
|
+
Here the kit is the agent loop and the model only picks tools. Building would mean
|
|
90
|
+
rewriting a full coding agent inside the kit, so it is left out on purpose.
|
|
91
|
+
|
|
92
|
+
**To build with a local model, use `--exec`** with a coding harness pointed at that model
|
|
93
|
+
(see *External agent commands* below):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx @eventmodelers/cli run --exec "codex exec --oss --json --full-auto" # Codex on local Ollama
|
|
97
|
+
npx @eventmodelers/cli run --exec "opencode run --auto --format json" # OpenCode, with an Ollama/vLLM/LM Studio provider in its config
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or keep Claude Code as the builder and point `anthropicBaseUrl` at a local endpoint that
|
|
101
|
+
speaks the Anthropic API.
|
|
102
|
+
|
|
75
103
|
```bash
|
|
76
104
|
LOCAL_AI_TARGET=ollama # preset: ollama | vllm | lmstudio | llamacpp
|
|
77
105
|
LOCAL_AI_URL=http://host:8000/v1 # any OpenAI-compatible server (overrides the preset URL)
|
|
@@ -109,6 +137,20 @@ child runs with the project dir as its cwd and inherits stdio — a harness owns
|
|
|
109
137
|
output format, so there is no condensed per-step logging here the way `ralph-claude.js`
|
|
110
138
|
has it.
|
|
111
139
|
|
|
140
|
+
**Agent tracing needs the harness's JSON output flag.** With agent tracing on
|
|
141
|
+
(`eventmodelers config --agent-tracing on`), slice builds are costed just like with Claude —
|
|
142
|
+
but only if the harness runs in its JSON output mode. Its normal output has no token counts,
|
|
143
|
+
so without the flag a turn is not traced (the runner logs a hint). Add the flag yourself:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npx @eventmodelers/cli run --exec "codex exec --json --full-auto"
|
|
147
|
+
npx @eventmodelers/cli run --exec "opencode run --auto --format json"
|
|
148
|
+
npx @eventmodelers/cli run --exec "gemini --yolo --output-format stream-json -p"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The terminal then shows the raw JSON events. Only OpenCode reports a dollar cost; Codex and
|
|
152
|
+
Gemini turns record tokens and duration with no cost. See `agent-tracking.md`.
|
|
153
|
+
|
|
112
154
|
Persist a default alongside the local-AI settings:
|
|
113
155
|
|
|
114
156
|
```json
|
|
@@ -45,6 +45,69 @@ export function usageFromLocalAi(raw, durationMs) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** One event from an external harness's JSON output (ralph-exec.js), or null if it carries no usage.
|
|
49
|
+
* Matched on shape, not on the command name, so a wrapper script around a harness still counts:
|
|
50
|
+
* - Codex `codex exec --json`: `turn.completed` with `usage` (input_tokens includes the cached ones)
|
|
51
|
+
* - OpenCode `opencode run --format json`: `step_finish` per model step, with tokens and cost
|
|
52
|
+
* - Gemini CLI `--output-format stream-json`: the final `result` with `stats`;
|
|
53
|
+
* `--output-format json`: one object whose `stats.models` holds tokens per model
|
|
54
|
+
* Only OpenCode reports a cost — the others leave costUsd null rather than guessing a price. */
|
|
55
|
+
export function usageFromHarnessEvent(msg) {
|
|
56
|
+
if (msg?.type === 'turn.completed' && msg.usage) {
|
|
57
|
+
const u = msg.usage;
|
|
58
|
+
const cached = n(u.cached_input_tokens);
|
|
59
|
+
return { inputTokens: Math.max(0, n(u.input_tokens) - cached), outputTokens: n(u.output_tokens), cacheReadTokens: cached };
|
|
60
|
+
}
|
|
61
|
+
if (msg?.type === 'step_finish' && msg.part?.tokens) {
|
|
62
|
+
const t = msg.part.tokens;
|
|
63
|
+
return {
|
|
64
|
+
inputTokens: n(t.input),
|
|
65
|
+
outputTokens: n(t.output) + n(t.reasoning),
|
|
66
|
+
cacheReadTokens: n(t.cache?.read),
|
|
67
|
+
cacheWriteTokens: n(t.cache?.write),
|
|
68
|
+
costUsd: Number.isFinite(msg.part.cost) ? msg.part.cost : null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (msg?.type === 'result' && msg.stats && !('total_cost_usd' in msg)) {
|
|
72
|
+
const s = msg.stats;
|
|
73
|
+
return {
|
|
74
|
+
inputTokens: n(s.input_tokens),
|
|
75
|
+
outputTokens: n(s.output_tokens),
|
|
76
|
+
durationMs: Number.isFinite(s.duration_ms) ? s.duration_ms : null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (msg?.stats?.models && typeof msg.stats.models === 'object') {
|
|
80
|
+
const models = Object.entries(msg.stats.models);
|
|
81
|
+
const tok = (key) => models.reduce((sum, [, m]) => sum + n(m?.tokens?.[key]), 0);
|
|
82
|
+
const cached = tok('cached');
|
|
83
|
+
const [dominant] = models.sort((a, b) => n(b[1]?.tokens?.candidates) - n(a[1]?.tokens?.candidates))[0] ?? [];
|
|
84
|
+
return {
|
|
85
|
+
inputTokens: Math.max(0, tok('prompt') - cached),
|
|
86
|
+
outputTokens: tok('candidates') + tok('thoughts'),
|
|
87
|
+
cacheReadTokens: cached,
|
|
88
|
+
model: dominant ?? null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Sums the usage of every event in one turn. A field no event reported stays null (cost, model)
|
|
95
|
+
* or 0 (tokens), so a harness without a cost does not show up as a free turn. */
|
|
96
|
+
export function sumUsage(parts) {
|
|
97
|
+
if (!parts.length) return null;
|
|
98
|
+
const add = (key) => parts.reduce((sum, p) => sum + n(p[key]), 0);
|
|
99
|
+
const costs = parts.map((p) => p.costUsd).filter(Number.isFinite);
|
|
100
|
+
return {
|
|
101
|
+
inputTokens: add('inputTokens'),
|
|
102
|
+
outputTokens: add('outputTokens'),
|
|
103
|
+
cacheReadTokens: add('cacheReadTokens'),
|
|
104
|
+
cacheWriteTokens: add('cacheWriteTokens'),
|
|
105
|
+
costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
|
|
106
|
+
durationMs: parts.findLast((p) => Number.isFinite(p.durationMs))?.durationMs ?? null,
|
|
107
|
+
model: parts.findLast((p) => p.model)?.model ?? null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
48
111
|
/** Records slice build turns. The agent id is the session: the platform requires one, and a
|
|
49
112
|
* restart of the same build agent is still the same agent building the same slices. */
|
|
50
113
|
export function createSliceTracer({ baseUrl, token, organizationId, agentId, traceFile, log = () => {}, enabled = true } = {}) {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
// Or persist it as localAi.exec in .eventmodelers/config.json.
|
|
19
19
|
|
|
20
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';
|
|
@@ -52,6 +53,21 @@ const childEnv = {
|
|
|
52
53
|
...(cfg.agentId && !localOnly ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
|
|
53
54
|
};
|
|
54
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
|
+
|
|
55
71
|
const promptDir = mkdtempSync(join(tmpdir(), 'ralph-exec-'));
|
|
56
72
|
|
|
57
73
|
// POSIX single-quote escaping: close, insert an escaped quote, reopen. The prompt is
|
|
@@ -62,22 +78,59 @@ function shellQuote(s) {
|
|
|
62
78
|
|
|
63
79
|
console.log(`[ralph-exec] command: ${execCmd}`);
|
|
64
80
|
|
|
65
|
-
|
|
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) {
|
|
66
108
|
return new Promise((resolvePromise, reject) => {
|
|
67
109
|
const full = inlineHeader + prompt;
|
|
68
110
|
const promptFile = join(promptDir, 'prompt.md');
|
|
69
111
|
writeFileSync(promptFile, full);
|
|
112
|
+
const traced = tracingOn && !!slice;
|
|
113
|
+
const startedAt = Date.now();
|
|
70
114
|
|
|
71
115
|
// stdio inherit: the harness owns its own output format, and there is no
|
|
72
116
|
// cross-harness stream schema to parse into the condensed per-step logging
|
|
73
|
-
// 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.
|
|
74
119
|
const proc = spawn(`${execCmd} ${shellQuote(full)}`, {
|
|
75
120
|
cwd: projectDir,
|
|
76
|
-
stdio: 'inherit',
|
|
121
|
+
stdio: ['inherit', traced ? 'pipe' : 'inherit', 'inherit'],
|
|
77
122
|
shell: true,
|
|
78
123
|
env: { ...childEnv, RALPH_PROMPT_FILE: promptFile },
|
|
79
124
|
});
|
|
80
|
-
|
|
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
|
+
});
|
|
81
134
|
proc.on('error', reject);
|
|
82
135
|
});
|
|
83
136
|
}
|