@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 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 32768 by default (`LOCAL_AI_NUM_CTX`) and warns when the schemas
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 32768`, llama.cpp: `-c 32768`) —
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 32768 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.
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 && !print && process.stdin.isTTY) {
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 (!print && !explicit.token && process.stdin.isTTY) {
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;
@@ -47,7 +47,7 @@ const PRESETS = {
47
47
  };
48
48
 
49
49
  const DEFAULT_MODEL = 'qwen3.5:9b';
50
- const DEFAULT_NUM_CTX = 32768;
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
- const rawCtx = process.env.LOCAL_AI_NUM_CTX || localAi.numCtx;
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' ? (rawCtx ? Number(rawCtx) : DEFAULT_NUM_CTX) : null,
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 32768, llama.cpp: -c 32768).\n${text.slice(0, 300)}`,
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.79",
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": {