@eventmodelers/cli 1.0.79 → 1.0.80
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 +54 -3
- package/RELEASE_NOTES.md +3 -1
- package/cli.js +20 -6
- package/lib/modeling-local-ai.js +19 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -335,7 +335,20 @@ SGLang all serve. Everything above the transport is identical, which is why anyt
|
|
|
335
335
|
OpenAI-compatible works by pointing `LOCAL_AI_URL` at it. The `LOCAL_AI_*` vars are the same
|
|
336
336
|
ones a build kit's local runner reads (`LOCAL_AI_TARGET`, `LOCAL_AI_URL`, `LOCAL_AI_API`,
|
|
337
337
|
`LOCAL_AI_MODEL`, `LOCAL_AI_API_KEY`, `LOCAL_AI_NUM_CTX`), or set them once as `localAi` in
|
|
338
|
-
`.eventmodelers/config.json
|
|
338
|
+
`.eventmodelers/config.json` — env wins over the file, the file over the preset:
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"boardId": "...",
|
|
343
|
+
"token": "...",
|
|
344
|
+
"localAi": {
|
|
345
|
+
"target": "ollama",
|
|
346
|
+
"url": "http://localhost:11434",
|
|
347
|
+
"model": "qwen3.5:9b",
|
|
348
|
+
"numCtx": 49152
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
339
352
|
|
|
340
353
|
**What you get and what you don't.** The loop around the turn is unchanged — the prompt queue,
|
|
341
354
|
the standalone board-change lane with all its damping, the idle review, the alive-ping, and the
|
|
@@ -349,9 +362,9 @@ than read from `.agent-modeling-kit/CLAUDE.md`. `--max-agents` has nothing to ca
|
|
|
349
362
|
|
|
350
363
|
Give it room: the platform's MCP tool schemas alone run to ~16k tokens, so Ollama's default
|
|
351
364
|
`num_ctx` of 4096 would silently truncate the tool block and leave the model inventing tool
|
|
352
|
-
names. The runner raises it to
|
|
365
|
+
names. The runner raises it to 49152 by default (`LOCAL_AI_NUM_CTX`) and warns when the schemas
|
|
353
366
|
still fill more than 60% of it. On an OpenAI-compatible server the context is fixed at launch
|
|
354
|
-
instead, so start it accordingly (vLLM: `--max-model-len
|
|
367
|
+
instead, so start it accordingly (vLLM: `--max-model-len 49152`, llama.cpp: `-c 49152`) —
|
|
355
368
|
overflow there surfaces as an HTTP 400, which the runner reports with that advice attached.
|
|
356
369
|
A turn is capped at 24 tool iterations, which ends a model that has lost the plot without
|
|
357
370
|
ending the session.
|
|
@@ -361,6 +374,44 @@ different thing, and `anthropicBaseUrl` below is that path — but note that it
|
|
|
361
374
|
speaking Anthropic's own `/v1/messages`, which Ollama does not serve. Pointing it straight at
|
|
362
375
|
`localhost:11434` gets you a 404; a translating proxy has to sit in between.
|
|
363
376
|
|
|
377
|
+
### Running headless, in a container
|
|
378
|
+
|
|
379
|
+
`run` normally confirms which board it is about to drive and where that board's credentials
|
|
380
|
+
come from. Those questions are worth one keystroke at a terminal and fatal anywhere else, so
|
|
381
|
+
`--non-interactive` turns them off: the board and credentials that resolve from flags,
|
|
382
|
+
`EVENTMODELERS_*` env vars and the config files are taken as final, and an incomplete set fails
|
|
383
|
+
the run with the reason instead of being interviewed for.
|
|
384
|
+
|
|
385
|
+
A missing TTY already implied this, which covers CI and most process supervisors — but not a
|
|
386
|
+
container started with `-it`, or a loop started from a terminal: stdin is a TTY nobody is
|
|
387
|
+
watching, and the run stops on a question forever. Pass the flag rather than relying on how
|
|
388
|
+
stdin happens to be wired.
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
docker run --rm \
|
|
392
|
+
-e EVENTMODELERS_TOKEN=<token> \
|
|
393
|
+
-e EVENTMODELERS_ORGANIZATION_ID=<uuid> \
|
|
394
|
+
-e EVENTMODELERS_BOARD_ID=<uuid> \
|
|
395
|
+
-e LOCAL_AI_URL=http://host.docker.internal:11434 \
|
|
396
|
+
node:20 npx -y @eventmodelers/cli run --standalone --local-ai --non-interactive
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Two things a container needs that a laptop already had:
|
|
400
|
+
|
|
401
|
+
- **A writable `HOME`.** With no kit in the working directory the agent installs one under
|
|
402
|
+
`~/.eventmodelers/kit` and keeps per-board credentials in `~/.eventmodelers/boards/` (`0600`,
|
|
403
|
+
in a `0700` directory). Run as a user whose home exists and is writable, or mount a volume
|
|
404
|
+
there to keep the kit across restarts instead of reinstalling it on every boot.
|
|
405
|
+
- **A reachable model server.** `localhost` inside the container is the container, not the host
|
|
406
|
+
— point `LOCAL_AI_URL` at `host.docker.internal` (Docker Desktop), the host's LAN address, or
|
|
407
|
+
the service name if Ollama/vLLM is a sibling container.
|
|
408
|
+
|
|
409
|
+
`EVENTMODELERS_TOKEN` now also answers the credentials question on its own, the way
|
|
410
|
+
`EVENTMODELERS_BOARD_ID` always answered the board question — an env var that wins over every
|
|
411
|
+
config file anyway makes asking where the credentials come from moot. So a fully env-driven run
|
|
412
|
+
is silent even without the flag; pass it anyway, so a half-set environment fails loudly instead
|
|
413
|
+
of waiting for an answer.
|
|
414
|
+
|
|
364
415
|
### Installing skills globally
|
|
365
416
|
|
|
366
417
|
By default, skills are copied into the project's own `.claude/skills/`. Pass `--global` to `init` or `init-modeling` to install them into `~/.claude/skills/` instead — available in every project without re-running the installer each time:
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
3
|
### Features
|
|
4
|
-
- `run --modeling`/`run --standalone` accept `--local-ai`, so the modeling agent can be driven by a local or self-hosted model instead of Claude. It was previously rejected as a build-kit-only flag, which conflated two different things: `--bash`/`--exec` select a *queue* the modeling loop has no equivalent of, while `--local-ai` selects a *model*. Both wire dialects are supported — `--local-ai` (bare) or `--local-ai ollama` for Ollama's native `POST /api/chat`, and `--local-ai vllm`/`lmstudio`/`llamacpp` for the OpenAI-compatible `POST /v1/chat/completions` that vLLM, LM Studio, llama.cpp-server, TGI and SGLang serve; anything else OpenAI-compatible works via `LOCAL_AI_URL`. The loop itself is untouched: the prompt queue, the standalone board-change lane and its damping, the idle review and the platform's full MCP tool set all behave as before, because everything Claude-specific already sat behind a single `runTurn(text)` seam. What a local model cannot bring along is the part that is not a wire format — the skills (`/place-element`, `/timeline`, `eventmodeling-*`) and the subagent fan-out are Claude Code features — so a self-directed turn does the most valuable piece of work itself, inline, and the board rules it needs come from the new runner's system prompt (`lib/modeling-local-ai.js`) instead of `.agent-modeling-kit/CLAUDE.md`. `--max-agents` is ignored in this mode. Ollama's `num_ctx` is raised to
|
|
4
|
+
- `run --modeling`/`run --standalone` accept `--local-ai`, so the modeling agent can be driven by a local or self-hosted model instead of Claude. It was previously rejected as a build-kit-only flag, which conflated two different things: `--bash`/`--exec` select a *queue* the modeling loop has no equivalent of, while `--local-ai` selects a *model*. Both wire dialects are supported — `--local-ai` (bare) or `--local-ai ollama` for Ollama's native `POST /api/chat`, and `--local-ai vllm`/`lmstudio`/`llamacpp` for the OpenAI-compatible `POST /v1/chat/completions` that vLLM, LM Studio, llama.cpp-server, TGI and SGLang serve; anything else OpenAI-compatible works via `LOCAL_AI_URL`. The loop itself is untouched: the prompt queue, the standalone board-change lane and its damping, the idle review and the platform's full MCP tool set all behave as before, because everything Claude-specific already sat behind a single `runTurn(text)` seam. What a local model cannot bring along is the part that is not a wire format — the skills (`/place-element`, `/timeline`, `eventmodeling-*`) and the subagent fan-out are Claude Code features — so a self-directed turn does the most valuable piece of work itself, inline, and the board rules it needs come from the new runner's system prompt (`lib/modeling-local-ai.js`) instead of `.agent-modeling-kit/CLAUDE.md`. `--max-agents` is ignored in this mode. Ollama's `num_ctx` is raised to 49152 by default (the MCP tool schemas alone are ~16k tokens, well past Ollama's 4096 default, which would silently truncate the tool block), with a warning when the schemas still fill >60% of it and a turn cap of 24 tool iterations.
|
|
5
|
+
|
|
6
|
+
- `run --non-interactive` never asks anything: the board and credentials that resolve from flags, `EVENTMODELERS_*` env vars and the config files are final, and an incomplete set fails the run with the reason instead of being interviewed for. A missing TTY already implied this, which covered CI and most supervisors, but not a container started with `-it` or a loop started from a terminal — stdin is a TTY nobody is watching, and the run stopped on the board confirmation or the credentials question forever. `EVENTMODELERS_TOKEN` also now answers the credentials question on its own, the way `EVENTMODELERS_BOARD_ID` always answered the board question: an env var that outranks every config file makes asking where the credentials come from moot. The README gains a container recipe, including the two things a container needs that a laptop already had — a writable HOME for the global kit and per-board credentials, and a `LOCAL_AI_URL` that isn't `localhost`.
|
|
5
7
|
|
|
6
8
|
### Docs
|
|
7
9
|
- The `anthropicBaseUrl` option no longer implies Ollama works behind it. That route keeps Claude Code and swaps only the endpoint, so the server has to speak Anthropic's own `/v1/messages`; Ollama does not, and pointing it at `localhost:11434` returns a 404. For Ollama, `--local-ai` is the supported route, and the README now says which of the two to reach for.
|
package/cli.js
CHANGED
|
@@ -1568,10 +1568,22 @@ async function fetchDefaultBoardId(baseUrl, token) {
|
|
|
1568
1568
|
// .eventmodelers/config.json up the tree beat ~/.eventmodelers/config.json. So
|
|
1569
1569
|
// `run --standalone --board-id <uuid>` is enough for a board used before, and any run can
|
|
1570
1570
|
// be pointed somewhere else entirely with --token/--organization-id.
|
|
1571
|
-
async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print) {
|
|
1571
|
+
async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print, nonInteractive = false) {
|
|
1572
1572
|
const walked = loadEffectiveConfig(cwd, null, explicitConfigPath).config;
|
|
1573
1573
|
const explicit = Object.fromEntries(Object.entries(flags ?? {}).filter(([, v]) => v));
|
|
1574
1574
|
|
|
1575
|
+
// Whether there is anyone to ask, decided once. A TTY was the only signal before, which is
|
|
1576
|
+
// right for a supervisor or CI (no stdin, so nothing to ask) but wrong for a loop started
|
|
1577
|
+
// from a terminal: stdin is a TTY nobody is watching, and the run stops on a question.
|
|
1578
|
+
// --non-interactive says so explicitly; --print never asks either.
|
|
1579
|
+
const interactive = !print && !nonInteractive && process.stdin.isTTY;
|
|
1580
|
+
|
|
1581
|
+
// An EVENTMODELERS_TOKEN set for this run answers the credentials question below as
|
|
1582
|
+
// squarely as --token does — applyEnvOverrides makes it win over every file anyway, so
|
|
1583
|
+
// asking where this board's credentials come from could not change the outcome. The board
|
|
1584
|
+
// question has always treated its env var this way; this is the same rule for the token.
|
|
1585
|
+
const credentialsNamed = !!(explicit.token || process.env.EVENTMODELERS_TOKEN);
|
|
1586
|
+
|
|
1575
1587
|
// Which board comes first — everything else is stored per board, so there is nothing to
|
|
1576
1588
|
// look up until we know which board this run is for.
|
|
1577
1589
|
let boardId = explicit.boardId || process.env.EVENTMODELERS_BOARD_ID || walked.boardId || null;
|
|
@@ -1583,7 +1595,7 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print)
|
|
|
1583
1595
|
// non-interactive stdin such as CI or a process supervisor), where the resolved value
|
|
1584
1596
|
// stands on its own exactly as before.
|
|
1585
1597
|
let boardChosen = !!(explicit.boardId || process.env.EVENTMODELERS_BOARD_ID);
|
|
1586
|
-
if (!boardChosen &&
|
|
1598
|
+
if (!boardChosen && interactive) {
|
|
1587
1599
|
const answer = await prompt(boardId ? `\n Board ID [${boardId}]: ` : '\n Board ID: ');
|
|
1588
1600
|
if (answer) {
|
|
1589
1601
|
boardId = answer;
|
|
@@ -1613,7 +1625,7 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print)
|
|
|
1613
1625
|
// implied — explicit credentials on the command line — or when there is no one to ask:
|
|
1614
1626
|
// --print, or a non-interactive stdin such as CI or a supervisor that would otherwise
|
|
1615
1627
|
// hang here forever (those keep using whatever is on file, silently).
|
|
1616
|
-
if (
|
|
1628
|
+
if (interactive && !credentialsNamed) {
|
|
1617
1629
|
const hasAccountWide = !!(walked.token && walked.organizationId);
|
|
1618
1630
|
|
|
1619
1631
|
// What "keep" would keep. A pointer entry is deliberately not offered: it says the
|
|
@@ -1678,9 +1690,10 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print)
|
|
|
1678
1690
|
: { ...applyEnvOverrides({ ...walked, ...stored }), ...explicit };
|
|
1679
1691
|
if (boardId) config.boardId = boardId;
|
|
1680
1692
|
|
|
1681
|
-
if (!config.token || !config.organizationId) {
|
|
1693
|
+
if ((!config.token || !config.organizationId) && interactive) {
|
|
1682
1694
|
// Nothing anywhere — ask once, and save it account-wide rather than into this
|
|
1683
|
-
// directory, so every later run from anywhere is silent.
|
|
1695
|
+
// directory, so every later run from anywhere is silent. Headless, there is nobody to
|
|
1696
|
+
// interview, so this is skipped and the check below fails the run with the reason.
|
|
1684
1697
|
console.log('🔐 No Eventmodelers credentials found — configuring them once, account-wide.\n');
|
|
1685
1698
|
config = await configureCredentials({
|
|
1686
1699
|
config,
|
|
@@ -3107,6 +3120,7 @@ credentialFlags(program
|
|
|
3107
3120
|
.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.')
|
|
3108
3121
|
.option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
|
|
3109
3122
|
.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.')
|
|
3123
|
+
.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).')
|
|
3110
3124
|
.option('--standalone', 'Let the modeling agent work the board in the background, on its own initiative: on top of direct prompts it subscribes to the board\'s change channel (like the build agents do) and, whenever the board goes quiet after an edit — or has simply been idle for a while — it takes a turn nobody asked for. Changed nodes are a notification, not the task: it judges the model as a whole and fans the work out over parallel subagents, one per changed area (examples on a new node, specs for a new command or read model, a missing attribute along a chain, a screen, a question comment). Filling that detail in while the human keeps modeling is the point — it does not wait for the board to be finished. Implies --modeling.')
|
|
3111
3125
|
.option('--max-agents <n>', 'Cap how many subagents a self-directed --standalone turn may dispatch at once, to bound what an unattended agent can spend per turn. The agent merges work that shares a slice or chain first, then takes the most valuable pieces up to this many and leaves the rest for a later turn. 1 makes it do the single most valuable piece itself, without spawning anything. Default 5. Ignored without --standalone — prompt turns are one piece of work by definition.', '5')
|
|
3112
3126
|
.option('--exclusive', 'Work only the prompts addressed to this agent\'s id — the board\'s "preferred agent" (the star in the prompts panel) — and hand every untargeted prompt straight back to the queue for another agent to take. Without it an agent also works everything nobody addressed to anyone, which is what you want for a single agent and exactly what you do not want for a dedicated one (a board with a general agent plus a specialist, or an agent a supervisor drives by id). Pair it with --id so the same agent is addressable across restarts — --global/--standalone otherwise mint a fresh id per run, and prompts addressed to the previous run\'s id are never claimed. Leaves --standalone alone: a self-directed turn is nobody\'s prompt, so an exclusive standalone agent still works the board on its own initiative.')
|
|
@@ -3201,7 +3215,7 @@ credentialFlags(program
|
|
|
3201
3215
|
...(opts.credentials ? parseCredentialsArg(opts.credentials) : {}),
|
|
3202
3216
|
...Object.fromEntries(Object.entries(credentialOverridesFromOpts(opts)).filter(([, v]) => v)),
|
|
3203
3217
|
};
|
|
3204
|
-
const config = await resolveModelingCredentials(cwd, flags, globalOpts.config, globalOpts.print);
|
|
3218
|
+
const config = await resolveModelingCredentials(cwd, flags, globalOpts.config, globalOpts.print, !!opts.nonInteractive);
|
|
3205
3219
|
projectDir = await ensureGlobalKit(config.baseUrl);
|
|
3206
3220
|
kitDir = join(projectDir, MODELING_KIT.kitDirName);
|
|
3207
3221
|
overrides = config;
|
package/lib/modeling-local-ai.js
CHANGED
|
@@ -47,7 +47,7 @@ const PRESETS = {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
const DEFAULT_MODEL = 'qwen3.5:9b';
|
|
50
|
-
const DEFAULT_NUM_CTX =
|
|
50
|
+
const DEFAULT_NUM_CTX = 49152;
|
|
51
51
|
// A modeling turn is read-then-write (get_nodes, then a placement or a field change),
|
|
52
52
|
// so it needs more round trips than the build kit's 12 — but a local model that has
|
|
53
53
|
// lost the plot loops on one tool forever, and this is what ends that turn instead of
|
|
@@ -77,7 +77,22 @@ export function resolveLocalAiTarget({ target, localAi = {} } = {}) {
|
|
|
77
77
|
throw new Error(`Unknown local-AI dialect "${dialect}" — one of: ${Object.keys(DIALECTS).join(', ')}`);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
// Configured the same way every other knob here is: env wins over config.json's `localAi`,
|
|
81
|
+
// and the default stands when neither says otherwise. Validated rather than coerced, because
|
|
82
|
+
// Number('32k') is NaN, which JSON.stringify turns into `num_ctx: null` — a request Ollama
|
|
83
|
+
// accepts and silently answers with its own 4096 default, which is the exact failure the
|
|
84
|
+
// default below exists to prevent.
|
|
85
|
+
const rawCtx = process.env.LOCAL_AI_NUM_CTX ?? localAi.numCtx;
|
|
86
|
+
let numCtx = DEFAULT_NUM_CTX;
|
|
87
|
+
if (rawCtx !== undefined && rawCtx !== null && String(rawCtx).trim() !== '') {
|
|
88
|
+
numCtx = Number(rawCtx);
|
|
89
|
+
if (!Number.isInteger(numCtx) || numCtx <= 0) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Invalid context size "${rawCtx}" (LOCAL_AI_NUM_CTX or localAi.numCtx) — a positive whole number of tokens, e.g. ${DEFAULT_NUM_CTX}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
return {
|
|
82
97
|
url,
|
|
83
98
|
dialect,
|
|
@@ -87,7 +102,7 @@ export function resolveLocalAiTarget({ target, localAi = {} } = {}) {
|
|
|
87
102
|
// num_ctx is per-request in Ollama and its default (4096) is far below what the
|
|
88
103
|
// MCP tool schemas alone need; on an OpenAI-compatible server the context is fixed
|
|
89
104
|
// at launch, so there is nothing to send and overflow surfaces as an HTTP 400.
|
|
90
|
-
numCtx: dialect === 'ollama' ?
|
|
105
|
+
numCtx: dialect === 'ollama' ? numCtx : null,
|
|
91
106
|
};
|
|
92
107
|
}
|
|
93
108
|
|
|
@@ -200,7 +215,7 @@ export function createModelingLocalAiRunner({ cfg, target, log, verbose = false,
|
|
|
200
215
|
throw new Error(
|
|
201
216
|
`${t.dialect} HTTP 400 — the request exceeds the server's context window. The MCP tool schemas alone ` +
|
|
202
217
|
`are ~${approxTokens(tools)} tokens; restart the server with a larger context ` +
|
|
203
|
-
`(vLLM: --max-model-len
|
|
218
|
+
`(vLLM: --max-model-len 49152, llama.cpp: -c 49152).\n${text.slice(0, 300)}`,
|
|
204
219
|
);
|
|
205
220
|
}
|
|
206
221
|
throw new Error(`${t.dialect} HTTP ${res.status}: ${text.slice(0, 300)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
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": {
|