@eventmodelers/cli 1.0.68 → 1.0.69

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
@@ -119,6 +119,7 @@ npx @eventmodelers/cli run --local # skip platform config/crede
119
119
  npx @eventmodelers/cli run --modeling # modeling-kit: warm Claude process driven by the board's prompt queue
120
120
  npx @eventmodelers/cli run --standalone # same, plus acting on board changes unprompted — and needs no install at all
121
121
  npx @eventmodelers/cli run --standalone --board-id <uuid> # …from any directory, against any board (see Power users)
122
+ npx @eventmodelers/cli run --id <id> --name <label> # override this run's agent identity (id + display name); `init --name` persists a name instead
122
123
  npx @eventmodelers/cli fetch --context <name> # pull full slice detail for one context on the board into <kit-dir>/.slices/
123
124
  npx @eventmodelers/cli fetch --context <name> --slice-id <id> # same, then print just that slice
124
125
  npx @eventmodelers/cli fetch --context <name> --slice-title <title> # same, then print just the slice matching this title
@@ -395,8 +396,22 @@ npx @eventmodelers/cli init-config # interactive, writes to
395
396
  npx @eventmodelers/cli init-config --board-id <uuid> # non-interactive, just overrides one field
396
397
  npx @eventmodelers/cli init-config --credentials "token=...,boardId=...,organizationId=...,baseUrl=..." # configure ONE board (~/.eventmodelers/boards/<board>.json), no prompts
397
398
  npx @eventmodelers/cli init-config --credentials - # same, read from stdin (keeps the token out of shell history)
399
+ npx @eventmodelers/cli init-config --name ci-builder # name the agent this config's runs identify as
398
400
  ```
399
401
 
402
+ ### Naming an agent
403
+
404
+ Every agent identifies itself to the platform with a stable `agentId` it mints on first use (per project, or per board for `run --standalone`), and the board's live-agent view shows that bare uuid. `--name` gives it a readable one instead — accepted by `init`, `re-init`, and `init-config`, saved into `config.json` as `agentName`, and sent with every heartbeat from then on:
405
+
406
+ ```bash
407
+ npx @eventmodelers/cli init --stack node --name ci-builder # persisted for every later run of this kit
408
+ npx @eventmodelers/cli init-config --name martins-laptop # same, without re-installing
409
+ npx @eventmodelers/cli run --name one-off-check # override for a single run, nothing written
410
+ npx @eventmodelers/cli run --id <id> # …and a second agent of the same type, side by side
411
+ ```
412
+
413
+ The heartbeat is keyed on `(token, agentId, agentType)`, so two agents of the same type in one project need distinct ids to both show up — that's what `run --id` is for. `run --id`/`run --name` are per-run only: the persisted `agentName` (and the minted id) stay untouched, so the next plain `run` is the same agent the platform already knows.
414
+
400
415
  ### Env vars and `--config` (scripted/CI installs)
401
416
 
402
417
  Every config field can be set via an `EVENTMODELERS_*` env var instead of the interactive prompts — these always win over whatever's in `config.json`, so a fully env-driven install never prompts for credentials or Claude execution settings:
@@ -410,6 +425,7 @@ Every config field can be set via an `EVENTMODELERS_*` env var instead of the in
410
425
  | `EVENTMODELERS_ANTHROPIC_BASE_URL` | `anthropicBaseUrl` |
411
426
  | `EVENTMODELERS_MODEL` | `model` |
412
427
  | `EVENTMODELERS_SUBAGENT_MODEL` | `subagentModel` |
428
+ | `EVENTMODELERS_AGENT_NAME` | `agentName` |
413
429
 
414
430
  ```bash
415
431
  EVENTMODELERS_ORGANIZATION_ID=... EVENTMODELERS_BOARD_ID=... EVENTMODELERS_TOKEN=... \
package/cli.js CHANGED
@@ -312,6 +312,7 @@ const ENV_CONFIG_MAP = {
312
312
  EVENTMODELERS_ANTHROPIC_BASE_URL: 'anthropicBaseUrl',
313
313
  EVENTMODELERS_MODEL: 'model',
314
314
  EVENTMODELERS_SUBAGENT_MODEL: 'subagentModel',
315
+ EVENTMODELERS_AGENT_NAME: 'agentName',
315
316
  };
316
317
 
317
318
  function applyEnvOverrides(config) {
@@ -586,6 +587,17 @@ function readJsonSafe(path) {
586
587
  }
587
588
  }
588
589
 
590
+ // The `x-agent-id` header every platform call carries when this process knows its own id.
591
+ // The heartbeat says an agent is alive; this says which of the calls arriving are its — the
592
+ // platform stamps it on the board_events a write produces (so the board can show "alice moved
593
+ // this" rather than one anonymous robot), and matches it when claiming a prompt the user
594
+ // addressed to one preferred agent. Optional everywhere: an id-less caller behaves exactly as
595
+ // callers did before it existed.
596
+ function agentHeaders(cfg) {
597
+ const agentId = cfg?.agentId || process.env.EVENTMODELERS_AGENT_ID || '';
598
+ return agentId ? { 'x-agent-id': agentId } : {};
599
+ }
600
+
589
601
  // Distinguishes this agent process from any other agent pinging the same
590
602
  // token/board — e.g. a build-kit and a modeling-kit install in the same project
591
603
  // share one root config.json, and without a per-agent id both would upsert the
@@ -1089,8 +1101,9 @@ async function installStack(stackKey, stackCfg, options = {}) {
1089
1101
  // Extracted from installStack so `init-config` can reuse the exact same
1090
1102
  // paste/manual/instructions/skip flow without also scaffolding a stack.
1091
1103
  // `overrides` are values passed directly on the command line (--token, --board-id,
1092
- // --organization-id, --base-url) — the most explicit source available, so they
1093
- // win over both the config file and env vars before we even check what's missing.
1104
+ // --organization-id, --base-url, plus --name as `agentName`) — the most explicit source
1105
+ // available, so they win over both the config file and env vars before we even check
1106
+ // what's missing. Any field is written through verbatim; only requiredFields gate the prompt.
1094
1107
  async function configureCredentials({ config, configPath, targetDir, requiredFields, boardIdOptional, overrides = {}, print, skipGitignore = false, force = false }) {
1095
1108
  config = { ...config };
1096
1109
  for (const [field, value] of Object.entries(overrides)) {
@@ -1395,16 +1408,22 @@ function boardCredentialsPath(boardId) {
1395
1408
  function writeBoardCredentials(config) {
1396
1409
  const path = boardCredentialsPath(config.boardId);
1397
1410
  mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
1411
+ // agentName rides along with agentId in both credential-bearing shapes: it is per-board
1412
+ // identity, same as the id, and this file is the only config a `run --standalone` from an
1413
+ // arbitrary directory ever reads — drop it here and `init-config --name` would be lost on
1414
+ // the very next run.
1415
+ const agentName = config.agentName ? { agentName: config.agentName } : {};
1398
1416
  const body = config.useBoard
1399
1417
  ? { boardId: config.boardId, useBoard: config.useBoard }
1400
1418
  : config.useGlobal
1401
- ? { boardId: config.boardId, useGlobal: true, agentId: config.agentId }
1419
+ ? { boardId: config.boardId, useGlobal: true, agentId: config.agentId, ...agentName }
1402
1420
  : {
1403
1421
  token: config.token,
1404
1422
  organizationId: config.organizationId,
1405
1423
  boardId: config.boardId,
1406
1424
  baseUrl: config.baseUrl,
1407
1425
  agentId: config.agentId,
1426
+ ...agentName,
1408
1427
  };
1409
1428
  writeFileSync(path, JSON.stringify(body, null, 2), { mode: 0o600 });
1410
1429
  }
@@ -1591,7 +1610,7 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print)
1591
1610
  // that is the identity the alive-ping is scoped to.
1592
1611
  config.agentId = stored.agentId || readJsonSafe(boardCredentialsPath(config.boardId)).agentId || randomUUID();
1593
1612
  writeBoardCredentials(stored.useGlobal
1594
- ? { boardId: config.boardId, useGlobal: true, agentId: config.agentId }
1613
+ ? { boardId: config.boardId, useGlobal: true, agentId: config.agentId, agentName: config.agentName }
1595
1614
  : config);
1596
1615
 
1597
1616
  return config;
@@ -1648,7 +1667,7 @@ async function ensureGlobalKit(baseUrl) {
1648
1667
  // question, sketch a screen. Without the flag that channel is still subscribed on
1649
1668
  // the same connection and every event on it is dropped, so the two modes differ by
1650
1669
  // one filter rather than by a whole second realtime stack.
1651
- async function runModeling(kitDir, projectDir, verbose = false, standalone = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS) {
1670
+ async function runModeling(kitDir, projectDir, verbose = false, standalone = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}) {
1652
1671
  const configLibPath = join(kitDir, 'lib', 'config.js');
1653
1672
  if (!existsSync(configLibPath)) {
1654
1673
  console.error(`❌ ${relative(process.cwd(), configLibPath)} not found — --modeling needs a kit installed via \`init --modeling\`.`);
@@ -1670,12 +1689,18 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1670
1689
  // ~/.eventmodelers/boards/<board>.json — one dir driving several boards must not have
1671
1690
  // them all upsert one shared alive row. A project install keeps its id in the project
1672
1691
  // root config, namespaced by agent type, as it always has.
1673
- if (!overrides) local.agentId = ensureAgentId(kitDir, 'MODELING');
1692
+ // `run --id` overrides that for this run only — ensureAgentId is skipped rather than
1693
+ // overwritten, so the project's own stable id stays on disk and the next run without the
1694
+ // flag is the same agent the platform saw before.
1695
+ if (!overrides) local.agentId = identity.agentId || ensureAgentId(kitDir, 'MODELING');
1674
1696
  if (!local.token || !local.organizationId) {
1675
1697
  console.error('❌ --modeling needs platform credentials in .eventmodelers/config.json (token + organizationId) — run `/connect` once or paste your config first.');
1676
1698
  process.exit(1);
1677
1699
  }
1678
- const cfg = { ...(await fetchPlatformConfig(local)), ...(overrides ?? {}) }; // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
1700
+ // The identity flags go on last: the global install's `overrides` carry the board's own
1701
+ // stored agentId (already persisted by resolveModelingCredentials), which would otherwise
1702
+ // win back over an explicit --id.
1703
+ const cfg = { ...(await fetchPlatformConfig(local)), ...(overrides ?? {}), ...(identity.agentId ? { agentId: identity.agentId } : {}), ...(identity.agentName ? { agentName: identity.agentName } : {}) }; // adds realtimeProvider + its provider-specific fields (supabaseUrl/supabaseAnonKey or pocketbaseUrl), + boardId if the config has a default one
1679
1704
  if (!cfg.boardId) {
1680
1705
  console.error('❌ --modeling needs a boardId — a modeling agent always runs for exactly one board. Run `/connect board=<uuid>` once, or add boardId to .eventmodelers/config.json.');
1681
1706
  process.exit(1);
@@ -1733,6 +1758,9 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1733
1758
  ...process.env,
1734
1759
  ...(cfg.anthropicBaseUrl ? { ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl } : {}),
1735
1760
  EVENTMODELERS_TOKEN: cfg.token,
1761
+ // What the connect skill puts in `.mcp.json`'s x-agent-id header and every curl-fallback
1762
+ // call, so the board work this agent does on the platform is attributed to this agent.
1763
+ ...(cfg.agentId ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
1736
1764
  };
1737
1765
 
1738
1766
  let proc = null;
@@ -1898,7 +1926,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1898
1926
 
1899
1927
  async function getRealtimeToken() {
1900
1928
  const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/realtime-token`, {
1901
- headers: { 'x-token': cfg.token },
1929
+ headers: { 'x-token': cfg.token, ...agentHeaders(cfg) },
1902
1930
  });
1903
1931
  if (!res.ok) throw new Error(`realtime-token: HTTP ${res.status}`);
1904
1932
  return (await res.json()).token;
@@ -1906,7 +1934,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
1906
1934
 
1907
1935
  async function fetchNextPrompt(jwtToken) {
1908
1936
  const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/next?board_id=${encodeURIComponent(cfg.boardId)}`, {
1909
- headers: { 'x-token': cfg.token, Authorization: `Bearer ${jwtToken}` },
1937
+ headers: { 'x-token': cfg.token, Authorization: `Bearer ${jwtToken}`, ...agentHeaders(cfg) },
1910
1938
  });
1911
1939
  if (res.status === 404) return null;
1912
1940
  if (!res.ok) throw new Error(`prompts/next: HTTP ${res.status}`);
@@ -2279,7 +2307,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
2279
2307
  const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
2280
2308
  method: 'POST',
2281
2309
  headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
2282
- body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: 'MODELING', agent_id: cfg.agentId }),
2310
+ body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: 'MODELING', agent_id: cfg.agentId, ...(cfg.agentName ? { agent_name: cfg.agentName } : {}) }),
2283
2311
  signal: AbortSignal.timeout(10_000),
2284
2312
  });
2285
2313
  if (!res.ok) log(`ping failed: ${res.status} ${await res.text().catch(() => '')}`);
@@ -2344,6 +2372,23 @@ function credentialOverridesFromOpts(opts) {
2344
2372
  return { token: opts.token, boardId: opts.boardId, organizationId: opts.organizationId, baseUrl: opts.baseUrl };
2345
2373
  }
2346
2374
 
2375
+ // `--name` is not a credential — it's the display name of the agent a kit runs, sent with
2376
+ // every heartbeat (POST /api/agent-alive's agent_name) so the board can show which agent is
2377
+ // live instead of a bare uuid. It belongs beside the credentials in config.json rather than
2378
+ // on every command line, so `init`/`re-init`/`init-config` persist it as `agentName` and
2379
+ // every later run of that kit's agent picks it up from the config walk (both runtimes merge
2380
+ // unknown config fields through verbatim). `run --name` is the per-run override on top, and
2381
+ // writes nothing.
2382
+ const AGENT_NAME_OPTION = [
2383
+ '--name <name>',
2384
+ 'Human-readable name for the agent this kit runs (e.g. "ci-builder", "martins-laptop"), saved to config.json as `agentName` and sent with every heartbeat so the board shows a name instead of a bare uuid. Also settable via EVENTMODELERS_AGENT_NAME, and overridable for a single run with `run --name`.',
2385
+ ];
2386
+
2387
+ function identityOverridesFromOpts(opts) {
2388
+ const agentName = typeof opts.name === 'string' ? opts.name.trim() : undefined;
2389
+ return agentName ? { agentName } : {};
2390
+ }
2391
+
2347
2392
  credentialFlags(program
2348
2393
  .command('init')
2349
2394
  .alias('install')
@@ -2358,7 +2403,8 @@ credentialFlags(program
2358
2403
  .option('--build-kit', 'Install a blank build-kit scaffold (.build-kit/ + .claude/skills/build-*/SKILL.md placeholders, all TODO-marked) for a stack not built into this CLI yet — no fixed backend. Mutually exclusive with --stack/--modeling/--bridge.')
2359
2404
  .option('--hooks', 'Install the slice commit-scope guard (.githooks/pre-commit, running .build-kit/lib/check-commit-scope.cjs) and wire it up via `git config core.hooksPath .githooks` — only meaningful with --stack (build-kit stacks). Off by default.')
2360
2405
  .option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
2361
- .option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json'))
2406
+ .option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json')
2407
+ .option(...AGENT_NAME_OPTION))
2362
2408
  .action(async (opts, command) => {
2363
2409
  const globalOpts = command.optsWithGlobals();
2364
2410
 
@@ -2376,7 +2422,7 @@ credentialFlags(program
2376
2422
  print: globalOpts.print,
2377
2423
  global: opts.global,
2378
2424
  force: opts.force,
2379
- credentialOverrides: credentialOverridesFromOpts(opts),
2425
+ credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
2380
2426
  });
2381
2427
  return;
2382
2428
  }
@@ -2387,7 +2433,7 @@ credentialFlags(program
2387
2433
  print: globalOpts.print,
2388
2434
  global: opts.global,
2389
2435
  force: opts.force,
2390
- credentialOverrides: credentialOverridesFromOpts(opts),
2436
+ credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
2391
2437
  });
2392
2438
  return;
2393
2439
  }
@@ -2406,7 +2452,7 @@ credentialFlags(program
2406
2452
  print: globalOpts.print,
2407
2453
  global: opts.global,
2408
2454
  force: opts.force,
2409
- credentialOverrides: credentialOverridesFromOpts(opts),
2455
+ credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
2410
2456
  target: opts.target,
2411
2457
  });
2412
2458
  // Deliberately NOT under .bridge-kit/.eventmodelers/ — that whole name is
@@ -2444,7 +2490,7 @@ credentialFlags(program
2444
2490
  print: globalOpts.print,
2445
2491
  global: opts.global,
2446
2492
  force: opts.force,
2447
- credentialOverrides: credentialOverridesFromOpts(opts),
2493
+ credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
2448
2494
  templatesSource: join(clonedDir, 'templates'),
2449
2495
  hooks: opts.hooks,
2450
2496
  });
@@ -2457,7 +2503,7 @@ credentialFlags(program
2457
2503
  print: globalOpts.print,
2458
2504
  global: opts.global,
2459
2505
  force: opts.force,
2460
- credentialOverrides: credentialOverridesFromOpts(opts),
2506
+ credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
2461
2507
  hooks: opts.hooks,
2462
2508
  });
2463
2509
  });
@@ -2474,7 +2520,8 @@ credentialFlags(program
2474
2520
  .option('--stack <name>', `Override which stack to refresh from (${Object.keys(REINITIABLE_STACKS).join(', ')}) instead of the one recorded in install-manifest.json — use this when the manifest is missing/stale, or to switch a .build-kit install to a different stack`)
2475
2521
  .option('--hooks', 'Install the slice commit-scope guard (.githooks/pre-commit) and wire it up via `git config core.hooksPath .githooks` — same as `init --hooks`, for turning it on after the fact without a full re-scaffold. Off by default.')
2476
2522
  .option('--global', 'Re-install skills into ~/.claude/skills/ instead of the project — defaults to however they were originally installed')
2477
- .option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json'))
2523
+ .option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json')
2524
+ .option(...AGENT_NAME_OPTION))
2478
2525
  .action(async (opts, command) => {
2479
2526
  const globalOpts = command.optsWithGlobals();
2480
2527
  const targetDir = process.cwd();
@@ -2512,7 +2559,7 @@ credentialFlags(program
2512
2559
  print: globalOpts.print,
2513
2560
  global: opts.global !== undefined ? opts.global : !!manifest.global,
2514
2561
  force: opts.force,
2515
- credentialOverrides: credentialOverridesFromOpts(opts),
2562
+ credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
2516
2563
  skipRootScaffold: true,
2517
2564
  hooks: opts.hooks,
2518
2565
  });
@@ -2642,10 +2689,11 @@ credentialFlags(program
2642
2689
  .command('init-config')
2643
2690
  .description('Configure credentials only — writes .eventmodelers/config.json in the current directory, or ~/.eventmodelers/config.json with --global')
2644
2691
  .option('--global', 'Write account-wide defaults (organizationId + token only) to ~/.eventmodelers/config.json instead of the project')
2645
- .option('--credentials <values>', 'Credentials as the comma-separated blob from app.eventmodelers.ai/account (token=...,boardId=...,organizationId=...,baseUrl=...), the equivalent JSON, or - to read either from stdin. When the blob names a board it configures THAT board (~/.eventmodelers/boards/<board>.json), which is all a later run --standalone --board-id <uuid> then needs.'))
2692
+ .option('--credentials <values>', 'Credentials as the comma-separated blob from app.eventmodelers.ai/account (token=...,boardId=...,organizationId=...,baseUrl=...), the equivalent JSON, or - to read either from stdin. When the blob names a board it configures THAT board (~/.eventmodelers/boards/<board>.json), which is all a later run --standalone --board-id <uuid> then needs.')
2693
+ .option(...AGENT_NAME_OPTION))
2646
2694
  .action(async (opts, command) => {
2647
2695
  const globalOpts = command.optsWithGlobals();
2648
- const overrides = credentialOverridesFromOpts(opts);
2696
+ const overrides = { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) };
2649
2697
 
2650
2698
  // A blob naming a board configures that board's own file rather than a project or
2651
2699
  // account-wide config: the per-board store is keyed by board id, and the blob is
@@ -2688,6 +2736,11 @@ credentialFlags(program
2688
2736
  if (pasted.token) base.token = pasted.token;
2689
2737
  if (overrides.organizationId) base.organizationId = overrides.organizationId;
2690
2738
  if (overrides.token) base.token = overrides.token;
2739
+ // Carried through the same narrowing: this branch rebuilds the file from scratch, so
2740
+ // an already-configured agentName has to be read back in or a later `init-config
2741
+ // --global` (e.g. rotating the token) would silently drop it.
2742
+ if (existing.agentName) base.agentName = existing.agentName;
2743
+ if (overrides.agentName) base.agentName = overrides.agentName;
2691
2744
 
2692
2745
  const configured = await configureCredentials({
2693
2746
  config: base,
@@ -2708,7 +2761,9 @@ credentialFlags(program
2708
2761
  // configureCredentials' generic paste/manual flow may have picked up
2709
2762
  // boardId/baseUrl too (e.g. from a pasted JSON blob) — strip them back out
2710
2763
  // before the final write, since --global only ever persists identity.
2711
- writeFileSync(configPath, JSON.stringify({ organizationId: configured.organizationId, token: configured.token }, null, 2));
2764
+ // --name is the one non-credential that belongs here: it names the agent, not the
2765
+ // project, so an account-wide default is as portable as the org/token beside it.
2766
+ writeFileSync(configPath, JSON.stringify({ organizationId: configured.organizationId, token: configured.token, ...(configured.agentName ? { agentName: configured.agentName } : {}) }, null, 2));
2712
2767
  console.log(`\n ✓ Saved account-wide defaults to ${configPath}`);
2713
2768
  } else {
2714
2769
  const targetDir = process.cwd();
@@ -2724,7 +2779,11 @@ credentialFlags(program
2724
2779
  boardIdOptional: true,
2725
2780
  overrides,
2726
2781
  print: globalOpts.print,
2727
- force: true,
2782
+ // Same reasoning as the --global branch above: a bare `init-config` means "re-ask
2783
+ // me", but an invocation that already carries its answers on the command line —
2784
+ // credentials, or just a --name to record — must not stop to prompt, or every
2785
+ // non-interactive caller hangs (a closed stdin crashes outright).
2786
+ force: !Object.values(overrides).some(Boolean),
2728
2787
  });
2729
2788
 
2730
2789
  // Keep `.mcp.json` in sync — this command can change `baseUrl` (e.g.
@@ -2747,6 +2806,8 @@ credentialFlags(program
2747
2806
  .option('--global', 'Run the modeling agent from the global install (~/.eventmodelers/kit), initializing it on first use, and ignore any kit in this directory. This is also what --modeling/--standalone fall back to on their own when nothing is installed here — pass it explicitly to prefer the global install over a local one. Credentials come from the flags below, EVENTMODELERS_* env vars, or ~/.eventmodelers/boards/<board>.json, so nothing is written into the current directory.')
2748
2807
  .option('--local', 'Skip platform config/credential lookup entirely and run the local-only loop (no board sync, no realtime agent) — even if .eventmodelers/config.json has credentials (build-kit stacks only)')
2749
2808
  .option('--verbose', 'Log every tool call\'s full input (commands, skill args, file paths) and assistant reasoning text. Default is condensed, high-level per-step logging only.')
2809
+ .option('--id <id>', 'Override the agent id this run identifies itself with on the platform, instead of the stable one minted once per project (per board for --global/--standalone) and reused on every restart. The heartbeat is keyed on (token, agent_id, agent_type), so this is what lets a second agent of the same type run side by side without the two overwriting each other\'s row — or pins one to an id a supervisor already knows. Per-run only: nothing is written to disk, so the next run without the flag is the original agent again.')
2810
+ .option('--name <name>', 'A human-readable name for this agent, sent with every heartbeat so the board shows which agent is live rather than a bare uuid (e.g. "ci-builder", "martins-laptop"). Per-run only, like --id: the persistent name is `agentName` in config.json (set via `init --name` / `init-config --name`), and this overrides it for one run without writing anything.')
2750
2811
  .option('--credentials <values>', 'Credentials as the comma-separated blob from app.eventmodelers.ai/account (token=...,boardId=...,organizationId=...,baseUrl=...), the equivalent JSON, or - to read either from stdin. Saved to ~/.eventmodelers/boards/<board>.json, so it is only needed once per board, and passing it skips the first-run question. The individual flags below override single fields of it.'))
2751
2812
  .action(async (opts, command) => {
2752
2813
  const globalOpts = command.optsWithGlobals();
@@ -2758,6 +2819,23 @@ credentialFlags(program
2758
2819
  if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
2759
2820
  console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
2760
2821
  }
2822
+ // --id/--name are what the platform will see for this run, so a blank one is a
2823
+ // mistake worth failing on rather than silently falling back to the stored identity.
2824
+ const identity = {
2825
+ agentId: opts.id === undefined ? null : String(opts.id).trim(),
2826
+ agentName: opts.name === undefined ? null : String(opts.name).trim(),
2827
+ };
2828
+ for (const [flag, value] of [['--id', identity.agentId], ['--name', identity.agentName]]) {
2829
+ if (value === '') {
2830
+ console.error(`❌ ${flag} needs a non-empty value.`);
2831
+ process.exit(1);
2832
+ }
2833
+ }
2834
+ // ralph.sh has no realtime agent and never pings /api/agent-alive, so there is no
2835
+ // identity for either flag to override there.
2836
+ if ((identity.agentId || identity.agentName) && opts.bash) {
2837
+ console.log('ℹ️ --id/--name only apply to agents that ping the platform; the --bash loop does not, so they are ignored here.');
2838
+ }
2761
2839
  // Both kit dirs can be installed side by side (e.g. running a build-kit and a
2762
2840
  // modeling-kit agent from the same project). findInstalledKitDir only ever
2763
2841
  // returns its first fixed-order match, which would silently prefer one stack
@@ -2821,7 +2899,7 @@ credentialFlags(program
2821
2899
  const shown = relative(cwd, kitDir);
2822
2900
  await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (warm Claude process) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
2823
2901
  try {
2824
- await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents);
2902
+ await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents, identity);
2825
2903
  } catch (err) {
2826
2904
  console.error('[modeling] Fatal:', err);
2827
2905
  process.exit(1);
@@ -2866,7 +2944,9 @@ credentialFlags(program
2866
2944
  // their own separate output paths with no stream-json parsing to gate. RALPH_LOCAL is
2867
2945
  // read by all three runners (ralph.js's startRalph, and ralph.sh directly) to force the
2868
2946
  // local-only branch even when .eventmodelers/config.json has valid credentials.
2869
- execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '' } });
2947
+ // RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
2948
+ // they reach both node runners but not ralph.sh, which has no heartbeat to identify.
2949
+ execSync(cmd, { cwd: kitDir, stdio: 'inherit', env: { ...process.env, RALPH_VERBOSE: opts.verbose ? '1' : '', RALPH_LOCAL: opts.local ? '1' : '', RALPH_AGENT_ID: identity.agentId ?? '', RALPH_AGENT_NAME: identity.agentName ?? '' } });
2870
2950
  } catch (err) {
2871
2951
  process.exit(err.status || 1);
2872
2952
  }
@@ -3180,6 +3260,7 @@ program
3180
3260
  'x-token': cfg.token,
3181
3261
  'x-board-id': cfg.boardId,
3182
3262
  'x-user-id': 'cli-set-slice-status',
3263
+ ...agentHeaders(cfg),
3183
3264
  },
3184
3265
  body: JSON.stringify([{
3185
3266
  id: randomUUID(),
@@ -48,8 +48,12 @@ function slugify(text) {
48
48
  async function fetchFullSliceData(cfg, contextName) {
49
49
  const baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
50
50
  const url = `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?contextName=${encodeURIComponent(contextName)}`;
51
+ const agentId = cfg.agentId || process.env.EVENTMODELERS_AGENT_ID || '';
51
52
  const res = await fetch(url, {
52
- headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'spec-kitty-adapter' },
53
+ headers: {
54
+ 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'spec-kitty-adapter',
55
+ ...(agentId ? { 'x-agent-id': agentId } : {}),
56
+ },
53
57
  });
54
58
  if (!res.ok) throw new Error(`slicedata fetch failed for context "${contextName}": HTTP ${res.status}`);
55
59
  return res.json();
package/lib/fetch.js CHANGED
@@ -72,7 +72,13 @@ function sliceFolderName(title) {
72
72
  // { cwd, kitDir, cfg: { token, organizationId, boardId, baseUrl }, opts: { context, sliceId?, sliceTitle? } }
73
73
  export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
74
74
  const baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
75
- const headers = { 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'cli-fetch' };
75
+ // x-agent-id when we know it (see cli.js's agentHeaders) — a read doesn't write anything, but
76
+ // sending it everywhere keeps one rule instead of a per-call judgment about which calls count.
77
+ const agentId = cfg.agentId || process.env.EVENTMODELERS_AGENT_ID || '';
78
+ const headers = {
79
+ 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'cli-fetch',
80
+ ...(agentId ? { 'x-agent-id': agentId } : {}),
81
+ };
76
82
 
77
83
  // assertBoardAccess (the guard every one of these routes runs behind) only ever
78
84
  // answers 401/403 for credential/board-access problems — a 404 here always means
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.68",
3
+ "version": "1.0.69",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -148,9 +148,18 @@ function ensureAgentId(kitDir, agentType) {
148
148
  return agentId;
149
149
  }
150
150
 
151
+ // `x-agent-id` on every platform call this loop makes, when it knows its own agent id (see
152
+ // ensureAgentId above / RALPH_AGENT_ID). The heartbeat says this agent is alive; the header says
153
+ // which calls are its, so its board writes are attributed to it and a prompt the user addressed
154
+ // to one preferred agent is only ever claimed by that agent.
155
+ function agentHeaders(cfg) {
156
+ const agentId = cfg?.agentId || process.env.RALPH_AGENT_ID || process.env.EVENTMODELERS_AGENT_ID || '';
157
+ return agentId ? { 'x-agent-id': agentId } : {};
158
+ }
159
+
151
160
  async function fetchPlatformConfig(local) {
152
161
  const remote = await fetchJSON(`${local.baseUrl}/api/config`, {
153
- headers: { 'x-token': local.token },
162
+ headers: { 'x-token': local.token, ...agentHeaders(local) },
154
163
  });
155
164
  return { ...local, ...remote };
156
165
  }
@@ -160,7 +169,7 @@ async function fetchPlatformConfig(local) {
160
169
  async function getRealtimeToken(cfg) {
161
170
  const { token } = await fetchJSON(
162
171
  `${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/realtime-token`,
163
- { headers: { 'x-token': cfg.token } },
172
+ { headers: { 'x-token': cfg.token, ...agentHeaders(cfg) } },
164
173
  );
165
174
  return token;
166
175
  }
@@ -172,7 +181,7 @@ function slugify(str) {
172
181
  async function fetchAndPersistSlices(cfg, kitDir) {
173
182
  const url = `${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata/slices`;
174
183
  const { slices } = await fetchJSON(url, {
175
- headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId },
184
+ headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId, ...agentHeaders(cfg) },
176
185
  });
177
186
  const slicesDir = join(kitDir, '.slices');
178
187
  mkdirSync(slicesDir, { recursive: true });
@@ -367,7 +376,7 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', queueAllSt
367
376
  const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
368
377
  method: 'POST',
369
378
  headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
370
- body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: agentType, agent_id: cfg.agentId }),
379
+ body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: agentType, agent_id: cfg.agentId, ...(cfg.agentName ? { agent_name: cfg.agentName } : {}) }),
371
380
  signal: AbortSignal.timeout(10_000),
372
381
  });
373
382
  if (!res.ok) {
@@ -489,7 +498,7 @@ async function blockStuckSlice(kitDir, cfg, credentialed, planned, attempts) {
489
498
  try {
490
499
  await fetchJSON(`${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes/events`, {
491
500
  method: 'POST',
492
- headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'ralph-loop' },
501
+ headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'ralph-loop', ...agentHeaders(cfg) },
493
502
  body: JSON.stringify([{
494
503
  id: randomUUID(),
495
504
  eventType: 'node:changed',
@@ -583,10 +592,17 @@ export { loadLocalConfig, fetchPlatformConfig, retryOn401, startRealtimeAgent };
583
592
 
584
593
  export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice, agentType = 'BUILD', queueAllStatuses = false, localOnly = false }) {
585
594
  const local = loadLocalConfig(kitDir);
586
- local.agentId = ensureAgentId(kitDir, agentType);
595
+ // RALPH_AGENT_ID/RALPH_AGENT_NAME are `eventmodelers run --id/--name`, passed down as env
596
+ // (see cli.js's run dispatcher): a per-run identity override so a second agent of the same
597
+ // type can run side by side without the two overwriting each other's heartbeat row, and so
598
+ // the board can show a name instead of a bare uuid. An override skips ensureAgentId rather
599
+ // than overwriting it — the project's stable id stays on disk for the next plain run.
600
+ local.agentId = process.env.RALPH_AGENT_ID || ensureAgentId(kitDir, agentType);
601
+ if (process.env.RALPH_AGENT_NAME) local.agentName = process.env.RALPH_AGENT_NAME;
587
602
 
588
603
  console.log(`Ralph — kit: ${kitDir}`);
589
604
  console.log(` project: ${projectDir}`);
605
+ console.log(` agent: ${local.agentName ? `${local.agentName} (${local.agentId})` : local.agentId}`);
590
606
 
591
607
  // localOnly (set via `eventmodelers run --local`) forces this branch even when
592
608
  // credentials are present — it skips fetchPlatformConfig's network call to
@@ -31,6 +31,9 @@ const claudeEnv = {
31
31
  ...process.env,
32
32
  ...(cfg.anthropicBaseUrl ? { ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl } : {}),
33
33
  ...(cfg.token ? { EVENTMODELERS_TOKEN: cfg.token } : {}),
34
+ // Lets the skills this agent runs send x-agent-id on their own calls (connect puts it in
35
+ // `.mcp.json` and in every curl fallback), so their board writes are attributed to this agent.
36
+ ...(cfg.agentId ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
34
37
  };
35
38
 
36
39
  // Collapses whitespace/newlines to a single line and truncates past `max` chars — a long
@@ -9,7 +9,7 @@ description: Resolve eventmodelers connection config (token, boardId, baseUrl) f
9
9
 
10
10
  **This should happen once per session, not once per skill.** If `TOKEN`/`BOARD_ID`/`ORG_ID`/`BASE_URL` are already resolved and verified from earlier in the current session — including earlier in the *same turn*, e.g. one skill internally invoking a second skill (`add-next-slice` → `html-screen`) — every subsequent "invoke `connect`" instruction is satisfied immediately by reusing those values. Do not re-run Steps 0–4 below. Only re-run this skill from scratch when a value actually needs to change: a fresh `401`/`403`/access-denied response from some other call, a different `board_id` on this turn, or a new inline param that overrides what's already resolved.
11
11
 
12
- **Subagents are a fresh session — hand them the resolved values.** When you spawn a subagent to do board work, put the already-resolved credentials inline in its prompt (`token=… board=… org=… baseUrl=…`). Its `connect` then satisfies everything at Step 0 and skips Steps 1–4 entirely: no config-file walk, no MCP re-registration, no verify call. Spawning three subagents without passing them down means paying the whole resolve-and-verify round three more times for values you already have.
12
+ **Subagents are a fresh session — hand them the resolved values.** When you spawn a subagent to do board work, put the already-resolved credentials inline in its prompt (`token=… board=… org=… baseUrl=…`, plus `agent=…` when you have an `AGENT_ID` — a subagent's writes are still this agent's writes, and it cannot read your environment). Its `connect` then satisfies everything at Step 0 and skips Steps 1–4 entirely: no config-file walk, no MCP re-registration, no verify call. Spawning three subagents without passing them down means paying the whole resolve-and-verify round three more times for values you already have.
13
13
 
14
14
  This skill also registers the **eventmodelers MCP server** for the project (Step 3.5) so other skills can call MCP tools (`mcp__eventmodelers__*`) instead of raw curl. MCP is the preferred transport; curl remains a fallback for hosts without MCP support, or for the one or two endpoints (documented in `learn-eventmodelers-api`) the MCP server doesn't expose.
15
15
 
@@ -25,17 +25,24 @@ After running, the following variables are available for the rest of the session
25
25
  | `BOARD_ID` | `x-board-id` | Target board UUID |
26
26
  | `ORG_ID` | — | Organization UUID (used in all board-scoped URLs) |
27
27
  | `BASE_URL` | — | Base URL, e.g. `http://localhost:3000` |
28
+ | `AGENT_ID` | `x-agent-id` | This agent process's own id, when running as one (Step 0.5). Optional — skip the header when there is no value. |
28
29
 
29
30
  Every curl-fallback call in every skill must include these headers:
30
31
  ```
31
32
  x-token: <TOKEN>
32
33
  x-board-id: <BOARD_ID>
33
34
  x-user-id: <skill-name> ← set by each skill individually
35
+ x-agent-id: <AGENT_ID> ← only when AGENT_ID resolved; omit the line entirely otherwise
34
36
  ```
35
37
 
38
+ `x-agent-id` is what makes the board say *which* agent did something: the platform stamps it on
39
+ every change the call writes, so the canvas shows this agent by name (rather than one anonymous
40
+ robot standing in for every agent at once), and a prompt a user addressed to one preferred agent
41
+ is only handed to the agent that claims it with that id.
42
+
36
43
  All board-scoped URLs follow the pattern: `<BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/...`
37
44
 
38
- When calling MCP tools instead, no `x-*` headers are needed — the MCP server resolves `ORG_ID` from `TOKEN` itself and every tool takes `boardId` as an explicit argument. See `learn-eventmodelers-api` for the full tool catalog.
45
+ When calling MCP tools instead, no `x-*` headers are needed per call — the MCP server resolves `ORG_ID` from `TOKEN` itself and every tool takes `boardId` as an explicit argument. (`x-agent-id` rides along with the registered server config from Step 3.5, so MCP writes are attributed too.) See `learn-eventmodelers-api` for the full tool catalog.
39
46
 
40
47
  ---
41
48
 
@@ -49,13 +56,38 @@ Before reading the config file, scan the prompt/arguments that invoked this skil
49
56
  | `token=<uuid>` | `token=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
50
57
  | `org=<uuid>` | `org=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
51
58
  | `baseUrl=<url>` | `baseUrl=http://localhost:3000` |
59
+ | `agent=<uuid>` | `agent=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` (the agent id a parent agent hands a subagent, so the subagent's board writes are attributed to the same agent) |
60
+
61
+ If an inline `board=<uuid>` is found, use it as `BOARD_ID` — **it takes priority over the config file**. Same for `token`, `org`, `baseUrl`, and `agent` (as `AGENT_ID`, which then makes Step 0.5 a no-op). Record which values came from inline params so they are not overwritten in Step 3.
52
62
 
53
- If an inline `board=<uuid>` is found, use it as `BOARD_ID`**it takes priority over the config file**. Same for `token`, `org`, and `baseUrl`. Record which values came from inline params so they are not overwritten in Step 3.
63
+ (`agent=<uuid>` is not one of the four required values — it is optional, and its absence never makes this skill ask anything.)
54
64
 
55
65
  **All four inline means this skill is already finished — stop here.** `token=`, `board=`, `org=` and `baseUrl=` arriving together is the shape a parent agent hands a subagent, and it resolves every required value in this one step. Do not walk the config file (Step 1), do not ask anything (Step 2), do not persist (Step 3), and do not make the verify call (Step 4): the parent resolved these values against this board and verified them there, so a subagent verifying them again learns nothing it wasn't just told and pays a round trip for it. Step 3.5 is a no-op too whenever `.mcp.json` already carries an `eventmodelers` entry — read the file, don't rewrite it, and don't re-register a server the session is already connected to. Print `Connected — board <BOARD_ID>` and return to the skill that invoked you.
56
66
 
57
67
  ---
58
68
 
69
+ ## Step 0.5 — Resolve `AGENT_ID` (agents only)
70
+
71
+ Optional, and only ever relevant when this session *is* an agent's session (`eventmodelers run`
72
+ spawned it). Resolve in this order and stop at the first hit:
73
+
74
+ 1. `$EVENTMODELERS_AGENT_ID` — the CLI exports it for the agent it runs. This is the normal case.
75
+ 2. `agentIds.MODELING` (or `agentIds.BUILD` for a build kit) in the project root's
76
+ `.eventmodelers/config.json` — the id the CLI minted once for this project and reuses on every
77
+ restart.
78
+
79
+ If neither exists, there is no `AGENT_ID`: this is a human's session, not an agent's. Leave it
80
+ unset and omit the `x-agent-id` header everywhere. Never invent one, and never reuse another
81
+ agent's id — the id is what the board attributes work to.
82
+
83
+ ```bash
84
+ agent_id="${EVENTMODELERS_AGENT_ID:-}"
85
+ [ -z "$agent_id" ] && [ -n "$config_file" ] && agent_id="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("agentIds",{}).get("MODELING",""))' "$config_file" 2>/dev/null)"
86
+ echo "${agent_id:-<none>}"
87
+ ```
88
+
89
+ ---
90
+
59
91
  ## Step 1 — Read config file
60
92
 
61
93
  Search for `.eventmodelers/config.json` starting from the current working directory and walking up through all parent directories. This is the same file every kit installed in this project reads and writes, so credentials only need to be entered once per project:
@@ -164,17 +196,25 @@ The eventmodelers backend exposes the same board capabilities as an MCP server a
164
196
  "eventmodelers": {
165
197
  "type": "http",
166
198
  "url": "<BASE_URL>/mcp",
167
- "headers": { "x-token": "${EVENTMODELERS_TOKEN}" }
199
+ "headers": { "x-token": "${EVENTMODELERS_TOKEN}", "x-agent-id": "${EVENTMODELERS_AGENT_ID}" }
168
200
  }
169
201
  }
170
202
  }
171
203
  ```
204
+ Keep the `x-agent-id` line in even when this session has no `AGENT_ID`: an unset variable
205
+ reaches the server as the literal `${EVENTMODELERS_AGENT_ID}` text, which it ignores (it only
206
+ accepts a uuid), so the entry is the same for a human session and an agent's. Writing one
207
+ `.mcp.json` for both is what keeps a second agent from inheriting the first one's id.
172
208
  3. Read the existing `.claude/settings.local.json` if present and merge in — never clobber `permissions`/`enabledMcpjsonServers` or anything else already there. Ensure it has `EVENTMODELERS_TOKEN` set under `env` (create the file with just this key if it doesn't exist yet):
173
209
  ```json
174
210
  {
175
211
  "env": { "EVENTMODELERS_TOKEN": "<TOKEN>" }
176
212
  }
177
213
  ```
214
+ Do **not** write `EVENTMODELERS_AGENT_ID` into this file. It is per running agent process, not
215
+ per project: the CLI exports it for the agent it spawns, and a value frozen into a shared
216
+ settings file would hand every session — including a second agent and the user's own — the
217
+ same agent identity.
178
218
  A plain `.env` file does **not** work here — Claude Code never sources one, so a `${EVENTMODELERS_TOKEN}` placeholder in `.mcp.json` would be left unexpanded (sent as the literal `${EVENTMODELERS_TOKEN}` text), which fails auth and pushes the client into an OAuth flow the eventmodelers server can't satisfy for this client. `.claude/settings.local.json`'s `env` block — alongside the inherited shell environment — is the only thing Claude Code actually resolves `.mcp.json` placeholders against.
179
219
  4. Ensure `.claude/settings.local.json` is listed in `.gitignore` (same check-then-append pattern as Step 3 uses for `.eventmodelers/config.json`) — it now holds the same secret and must never be committed. (Gitignored by Claude Code's own convention already, but don't rely on that silently.)
180
220
 
@@ -90,6 +90,7 @@ Server name: `eventmodelers`. Every tool takes `boardId` explicitly; none need `
90
90
  |---|---|---|
91
91
  | `Authorization` | Some routes | Supabase JWT bearer token |
92
92
  | `x-user-id` | Node operations | User identifier |
93
+ | `x-agent-id` | Optional | The agent process making the call — the uuid it also sends as `agent_id` on its heartbeat (`$EVENTMODELERS_AGENT_ID`, resolved by the `connect` skill). Stored on the `board_events` rows a write produces and broadcast with them, so the board shows *which* agent touched an element instead of one anonymous agent for all of them, and it decides who may claim a prompt addressed to a preferred agent. Send it on every call; a value that isn't uuid-shaped is ignored. |
93
94
  | `x-causation-id` | Optional | Event causation tracing |
94
95
  | `x-correlation-id` | Optional | Correlation tracing |
95
96
 
@@ -324,6 +325,8 @@ The node's content lives in `meta.description` (a plain string of markdown sourc
324
325
 
325
326
  All node endpoints require header: `x-user-id`
326
327
 
328
+ Also send `x-agent-id` (`$EVENTMODELERS_AGENT_ID`) on every write — that is what attributes the change to this agent by name on the board. See **Authentication & Headers** above.
329
+
327
330
  ### POST `/api/org/:orgId/boards/:boardId/nodes/events`
328
331
  Submit node change events.
329
332
 
@@ -855,6 +858,8 @@ element was selected) plus the `focusArea`, and nothing else.
855
858
  ### GET `/api/org/:orgId/prompts/next`
856
859
  Claim the next pending (`ADDED`) prompt for a board — atomically flips it to `CLAIMED` and returns it. This is what a running modeling agent's warm loop polls. Auth: `x-token` **and** a Supabase JWT (`Authorization: Bearer`) together.
857
860
 
861
+ Send `x-agent-id` here too: a prompt the user addressed to one preferred agent (`prompts.agent_id`) is only ever handed to the agent claiming with that id, and a caller without the header claims untargeted prompts only. Addressing one is `POST /api/org/:orgId/prompts` with `agent_id: "<uuid>"` — the board's prompts panel does it when someone stars an agent.
862
+
858
863
  **Query params**: `board_id` (required)
859
864
  **Response**: `200` — the claimed row (now `status: "CLAIMED"`), including its parsed `context` and the `hidden` flag · `404` — no `ADDED` prompts available
860
865
 
@@ -902,16 +907,20 @@ Exchange an `x-token` for a short-lived Supabase-compatible JWT, used to subscri
902
907
  ### POST `/api/agent-alive`
903
908
  Record a heartbeat ping for a running modeling/build agent. Auth: Supabase JWT (`Authorization: Bearer`) — exchange the `x-token` for one first via `GET /api/org/:orgId/prompts/realtime-token` above; a raw `x-token` alone is not accepted here.
904
909
 
905
- **Body**: `{ token: string, board_id?: string, agent_type: 'MODELING' | 'BUILD', agent_id: string }`
910
+ **Body**: `{ token: string, board_id?: string, agent_type: 'MODELING' | 'BUILD', agent_id: string, agent_name?: string }`
906
911
  **Response**: `200` — `{ ok: true }`
907
912
  **Errors**: `400` `agent_id`/`agent_type` missing · `404` token not found
908
913
 
914
+ `agent_id` is the client's own stable id (the heartbeat is keyed on `token` + `agent_id` + `agent_type`); `agent_name` is an optional display name for it, so the board can show which agent is live instead of a bare uuid. Both are overridable per run via `eventmodelers run --id/--name`.
915
+
916
+ The same `agent_id` belongs in the `x-agent-id` header of every board call this agent makes: the heartbeat says the agent is alive, the header says which of the incoming calls are its. Without it a write is attributed to "some agent", every live agent shares one presence identity on the board, and prompts addressed to a preferred agent are never claimed.
917
+
909
918
  ---
910
919
 
911
920
  ### GET `/api/org/:orgId/boards/:boardId/agent-alive`
912
921
  Check whether an agent has pinged for a board within the last 45s. Auth: `x-token` (bot) or a Supabase JWT (`Authorization: Bearer`) — either works.
913
922
 
914
- **Response**: `200` — `{ alive: boolean, agentTypes: string[] }`
923
+ **Response**: `200` — `{ alive: boolean, agentTypes: string[], agents: { agentId: string, agentType: string, agentName: string | null }[] }` — one `agents` entry per live agent process, `agentTypes` the de-duplicated set of their types
915
924
 
916
925
  ---
917
926
 
@@ -89,7 +89,8 @@ function hasCredentials(cfg) {
89
89
 
90
90
  async function fetchPlatformConfig(local) {
91
91
  const remote = await fetchJSON(`${local.baseUrl}/api/config`, {
92
- headers: { 'x-token': local.token },
92
+ // x-agent-id when this install knows its agent id — see the CLI's agentHeaders.
93
+ headers: { 'x-token': local.token, ...(local.agentId || process.env.EVENTMODELERS_AGENT_ID ? { 'x-agent-id': local.agentId || process.env.EVENTMODELERS_AGENT_ID } : {}) },
93
94
  });
94
95
  return { ...local, ...remote };
95
96
  }
@@ -153,9 +153,18 @@ function ensureAgentId(kitDir, agentType) {
153
153
  return agentId;
154
154
  }
155
155
 
156
+ // `x-agent-id` on every platform call this loop makes, when it knows its own agent id (see
157
+ // ensureAgentId above / RALPH_AGENT_ID). The heartbeat says this agent is alive; the header says
158
+ // which calls are its, so its board writes are attributed to it and a prompt the user addressed
159
+ // to one preferred agent is only ever claimed by that agent.
160
+ function agentHeaders(cfg) {
161
+ const agentId = cfg?.agentId || process.env.RALPH_AGENT_ID || process.env.EVENTMODELERS_AGENT_ID || '';
162
+ return agentId ? { 'x-agent-id': agentId } : {};
163
+ }
164
+
156
165
  async function fetchPlatformConfig(local) {
157
166
  const remote = await fetchJSON(`${local.baseUrl}/api/config`, {
158
- headers: { 'x-token': local.token },
167
+ headers: { 'x-token': local.token, ...agentHeaders(local) },
159
168
  });
160
169
  return { ...local, ...remote };
161
170
  }
@@ -169,7 +178,7 @@ function slugify(str) {
169
178
  async function fetchAndPersistSlices(cfg, kitDir) {
170
179
  const url = `${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata/slices`;
171
180
  const { slices } = await fetchJSON(url, {
172
- headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId },
181
+ headers: { 'x-token': cfg.token, 'x-board-id': cfg.boardId, ...agentHeaders(cfg) },
173
182
  });
174
183
  const slicesDir = join(kitDir, '.slices');
175
184
  mkdirSync(slicesDir, { recursive: true });
@@ -391,7 +400,7 @@ async function blockStuckSlice(kitDir, cfg, credentialed, planned, attempts) {
391
400
  try {
392
401
  await fetchJSON(`${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes/events`, {
393
402
  method: 'POST',
394
- headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'ralph-loop' },
403
+ headers: { 'Content-Type': 'application/json', 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'ralph-loop', ...agentHeaders(cfg) },
395
404
  body: JSON.stringify([{
396
405
  id: randomUUID(),
397
406
  eventType: 'node:changed',