@eventmodelers/cli 1.0.68 → 1.0.70
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 +22 -2
- package/cli.js +117 -33
- package/lib/adapters/spec-kitty-adapter.js +5 -1
- package/lib/fetch.js +7 -1
- package/package.json +1 -1
- package/shared/build-kit/lib/ralph.js +22 -6
- package/shared/build-kit/ralph-claude.js +3 -0
- package/shared/skills/connect/SKILL.md +44 -4
- package/shared/skills/learn-eventmodelers-api/SKILL.md +11 -2
- package/stacks/modeling-kit/templates/kit/lib/config.js +2 -1
- package/stacks/react/templates/build-kit/lib/ralph.js +12 -3
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
|
|
@@ -210,8 +211,10 @@ starting in one command. Resolution order for a run is `--credentials` and the i
|
|
|
210
211
|
`--token`/`--organization-id`/`--board-id`/`--base-url` flags, then `EVENTMODELERS_*` env vars,
|
|
211
212
|
then `~/.eventmodelers/boards/<board>.json`, then the usual `.eventmodelers/config.json` walk,
|
|
212
213
|
and finally the account's default board. Whatever a run resolves is saved back to the
|
|
213
|
-
per-board file (`0600`, in a `0700` directory)
|
|
214
|
-
|
|
214
|
+
per-board file (`0600`, in a `0700` directory). The agent id is *not* stored there — a standalone
|
|
215
|
+
run mints a fresh one each time, so two ad-hoc agents on one board stay two agents (see
|
|
216
|
+
[Naming an agent](#naming-an-agent)). One machine can therefore drive several boards, across
|
|
217
|
+
several accounts, at once.
|
|
215
218
|
The global kit itself holds no credentials at all — the token reaches `claude` through the
|
|
216
219
|
spawned process's environment.
|
|
217
220
|
|
|
@@ -395,8 +398,24 @@ npx @eventmodelers/cli init-config # interactive, writes to
|
|
|
395
398
|
npx @eventmodelers/cli init-config --board-id <uuid> # non-interactive, just overrides one field
|
|
396
399
|
npx @eventmodelers/cli init-config --credentials "token=...,boardId=...,organizationId=...,baseUrl=..." # configure ONE board (~/.eventmodelers/boards/<board>.json), no prompts
|
|
397
400
|
npx @eventmodelers/cli init-config --credentials - # same, read from stdin (keeps the token out of shell history)
|
|
401
|
+
npx @eventmodelers/cli init-config --name ci-builder # name the agent this config's runs identify as
|
|
398
402
|
```
|
|
399
403
|
|
|
404
|
+
### Naming an agent
|
|
405
|
+
|
|
406
|
+
Every agent identifies itself to the platform with an `agentId`, and the board's live-agent view shows that bare uuid. A **project install** mints one on first use and reuses it on every restart (`agentIds` in the project root's `.eventmodelers/config.json`). A **standalone/global run** mints a fresh one per run instead: nothing stops two ad-hoc agents running for one board, and since the heartbeat is keyed on `(token, agentId, agentType)`, a shared id would make the second agent replace the first — one agent visible however many are running, and their board writes indistinguishable.
|
|
407
|
+
|
|
408
|
+
`--name` gives an agent a readable label instead of the uuid — accepted by `init`, `re-init`, and `init-config`, saved into `config.json` as `agentName`, and sent with every heartbeat from then on:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
npx @eventmodelers/cli init --stack node --name ci-builder # persisted for every later run of this kit
|
|
412
|
+
npx @eventmodelers/cli init-config --name martins-laptop # same, without re-installing
|
|
413
|
+
npx @eventmodelers/cli run --name one-off-check # override for a single run, nothing written
|
|
414
|
+
npx @eventmodelers/cli run --id <id> # pin ONE identity across restarts
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
`run --id` is what you want when an agent has to keep the same identity every time it starts: a supervisor that already knows the id, a second agent of the same type in one *project* (which would otherwise share the project's single minted id), or an agent a board has starred as its **preferred agent** — that star addresses prompts to one id, so an agent whose id changes per run loses it on restart. `run --id`/`run --name` are per-run only: nothing is written to disk.
|
|
418
|
+
|
|
400
419
|
### Env vars and `--config` (scripted/CI installs)
|
|
401
420
|
|
|
402
421
|
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 +429,7 @@ Every config field can be set via an `EVENTMODELERS_*` env var instead of the in
|
|
|
410
429
|
| `EVENTMODELERS_ANTHROPIC_BASE_URL` | `anthropicBaseUrl` |
|
|
411
430
|
| `EVENTMODELERS_MODEL` | `model` |
|
|
412
431
|
| `EVENTMODELERS_SUBAGENT_MODEL` | `subagentModel` |
|
|
432
|
+
| `EVENTMODELERS_AGENT_NAME` | `agentName` |
|
|
413
433
|
|
|
414
434
|
```bash
|
|
415
435
|
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
|
|
1093
|
-
// win over both the config file and env vars before we even check
|
|
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)) {
|
|
@@ -1377,7 +1390,7 @@ function ensureEnvToken(targetDir, token) {
|
|
|
1377
1390
|
const GLOBAL_DIR = join(homedir(), '.eventmodelers');
|
|
1378
1391
|
const GLOBAL_KIT_DIR = join(GLOBAL_DIR, 'kit');
|
|
1379
1392
|
|
|
1380
|
-
// One file per board: `{token, organizationId, boardId, baseUrl
|
|
1393
|
+
// One file per board: `{token, organizationId, boardId, baseUrl}`. Credentials
|
|
1381
1394
|
// ARE per board — a token is scoped to the org that owns it — so one machine can drive
|
|
1382
1395
|
// several boards across several accounts at once, each with its own. Written 0600 in a
|
|
1383
1396
|
// 0700 dir: unlike a project's .eventmodelers/config.json, there is no .gitignore standing
|
|
@@ -1395,16 +1408,21 @@ 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 is stored, the agent id is not: the name is a label for whoever runs here (and
|
|
1412
|
+
// this file is the only config a `run --standalone` from an arbitrary directory reads, so
|
|
1413
|
+
// dropping it would lose `init-config --name` on the very next run), while the id is minted
|
|
1414
|
+
// per run now — see resolveModelingCredentials for why.
|
|
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,
|
|
1419
|
+
? { boardId: config.boardId, useGlobal: true, ...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
|
+
...agentName,
|
|
1408
1426
|
};
|
|
1409
1427
|
writeFileSync(path, JSON.stringify(body, null, 2), { mode: 0o600 });
|
|
1410
1428
|
}
|
|
@@ -1586,12 +1604,18 @@ async function resolveModelingCredentials(cwd, flags, explicitConfigPath, print)
|
|
|
1586
1604
|
process.exit(1);
|
|
1587
1605
|
}
|
|
1588
1606
|
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1607
|
+
// A fresh identity for every standalone run, deliberately not persisted. A standalone agent
|
|
1608
|
+
// is started ad hoc from wherever, and nothing stops two of them running for the same board —
|
|
1609
|
+
// with one id stored per board they upserted the same alive row (the heartbeat is keyed on
|
|
1610
|
+
// token + agent_id + agent_type), so the second agent replaced the first instead of joining
|
|
1611
|
+
// it: the board showed one agent however many were running, and their writes were
|
|
1612
|
+
// indistinguishable. A per-run uuid costs the identity its continuity across restarts (a
|
|
1613
|
+
// restarted agent is a new row, and the old one lingers until its 45s window lapses) — pass
|
|
1614
|
+
// `run --id <uuid>` when an agent needs to keep one identity, which is also what makes
|
|
1615
|
+
// "preferred agent" on the board stick to it.
|
|
1616
|
+
config.agentId = randomUUID();
|
|
1593
1617
|
writeBoardCredentials(stored.useGlobal
|
|
1594
|
-
? { boardId: config.boardId, useGlobal: true,
|
|
1618
|
+
? { boardId: config.boardId, useGlobal: true, agentName: config.agentName }
|
|
1595
1619
|
: config);
|
|
1596
1620
|
|
|
1597
1621
|
return config;
|
|
@@ -1648,7 +1672,7 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1648
1672
|
// question, sketch a screen. Without the flag that channel is still subscribed on
|
|
1649
1673
|
// the same connection and every event on it is dropped, so the two modes differ by
|
|
1650
1674
|
// 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) {
|
|
1675
|
+
async function runModeling(kitDir, projectDir, verbose = false, standalone = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}) {
|
|
1652
1676
|
const configLibPath = join(kitDir, 'lib', 'config.js');
|
|
1653
1677
|
if (!existsSync(configLibPath)) {
|
|
1654
1678
|
console.error(`❌ ${relative(process.cwd(), configLibPath)} not found — --modeling needs a kit installed via \`init --modeling\`.`);
|
|
@@ -1670,12 +1694,18 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1670
1694
|
// ~/.eventmodelers/boards/<board>.json — one dir driving several boards must not have
|
|
1671
1695
|
// them all upsert one shared alive row. A project install keeps its id in the project
|
|
1672
1696
|
// root config, namespaced by agent type, as it always has.
|
|
1673
|
-
|
|
1697
|
+
// `run --id` overrides that for this run only — ensureAgentId is skipped rather than
|
|
1698
|
+
// overwritten, so the project's own stable id stays on disk and the next run without the
|
|
1699
|
+
// flag is the same agent the platform saw before.
|
|
1700
|
+
if (!overrides) local.agentId = identity.agentId || ensureAgentId(kitDir, 'MODELING');
|
|
1674
1701
|
if (!local.token || !local.organizationId) {
|
|
1675
1702
|
console.error('❌ --modeling needs platform credentials in .eventmodelers/config.json (token + organizationId) — run `/connect` once or paste your config first.');
|
|
1676
1703
|
process.exit(1);
|
|
1677
1704
|
}
|
|
1678
|
-
|
|
1705
|
+
// The identity flags go on last: the global install's `overrides` carry the agent id
|
|
1706
|
+
// resolveModelingCredentials just minted for this run, which would otherwise win back over
|
|
1707
|
+
// an explicit --id.
|
|
1708
|
+
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
1709
|
if (!cfg.boardId) {
|
|
1680
1710
|
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
1711
|
process.exit(1);
|
|
@@ -1733,6 +1763,9 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1733
1763
|
...process.env,
|
|
1734
1764
|
...(cfg.anthropicBaseUrl ? { ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl } : {}),
|
|
1735
1765
|
EVENTMODELERS_TOKEN: cfg.token,
|
|
1766
|
+
// What the connect skill puts in `.mcp.json`'s x-agent-id header and every curl-fallback
|
|
1767
|
+
// call, so the board work this agent does on the platform is attributed to this agent.
|
|
1768
|
+
...(cfg.agentId ? { EVENTMODELERS_AGENT_ID: cfg.agentId } : {}),
|
|
1736
1769
|
};
|
|
1737
1770
|
|
|
1738
1771
|
let proc = null;
|
|
@@ -1889,6 +1922,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1889
1922
|
}
|
|
1890
1923
|
|
|
1891
1924
|
spawnProcess();
|
|
1925
|
+
log(`agent: ${cfg.agentName ? `${cfg.agentName} (${cfg.agentId})` : cfg.agentId}`);
|
|
1892
1926
|
log(
|
|
1893
1927
|
standalone
|
|
1894
1928
|
? `standalone: ON — reacting to direct prompts AND to board changes on its own initiative (max ${maxAgents} subagent(s) per self-directed turn)`
|
|
@@ -1898,7 +1932,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1898
1932
|
|
|
1899
1933
|
async function getRealtimeToken() {
|
|
1900
1934
|
const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/prompts/realtime-token`, {
|
|
1901
|
-
headers: { 'x-token': cfg.token },
|
|
1935
|
+
headers: { 'x-token': cfg.token, ...agentHeaders(cfg) },
|
|
1902
1936
|
});
|
|
1903
1937
|
if (!res.ok) throw new Error(`realtime-token: HTTP ${res.status}`);
|
|
1904
1938
|
return (await res.json()).token;
|
|
@@ -1906,7 +1940,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
1906
1940
|
|
|
1907
1941
|
async function fetchNextPrompt(jwtToken) {
|
|
1908
1942
|
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}
|
|
1943
|
+
headers: { 'x-token': cfg.token, Authorization: `Bearer ${jwtToken}`, ...agentHeaders(cfg) },
|
|
1910
1944
|
});
|
|
1911
1945
|
if (res.status === 404) return null;
|
|
1912
1946
|
if (!res.ok) throw new Error(`prompts/next: HTTP ${res.status}`);
|
|
@@ -2279,7 +2313,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2279
2313
|
const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
|
|
2280
2314
|
method: 'POST',
|
|
2281
2315
|
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 }),
|
|
2316
|
+
body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: 'MODELING', agent_id: cfg.agentId, ...(cfg.agentName ? { agent_name: cfg.agentName } : {}) }),
|
|
2283
2317
|
signal: AbortSignal.timeout(10_000),
|
|
2284
2318
|
});
|
|
2285
2319
|
if (!res.ok) log(`ping failed: ${res.status} ${await res.text().catch(() => '')}`);
|
|
@@ -2344,6 +2378,23 @@ function credentialOverridesFromOpts(opts) {
|
|
|
2344
2378
|
return { token: opts.token, boardId: opts.boardId, organizationId: opts.organizationId, baseUrl: opts.baseUrl };
|
|
2345
2379
|
}
|
|
2346
2380
|
|
|
2381
|
+
// `--name` is not a credential — it's the display name of the agent a kit runs, sent with
|
|
2382
|
+
// every heartbeat (POST /api/agent-alive's agent_name) so the board can show which agent is
|
|
2383
|
+
// live instead of a bare uuid. It belongs beside the credentials in config.json rather than
|
|
2384
|
+
// on every command line, so `init`/`re-init`/`init-config` persist it as `agentName` and
|
|
2385
|
+
// every later run of that kit's agent picks it up from the config walk (both runtimes merge
|
|
2386
|
+
// unknown config fields through verbatim). `run --name` is the per-run override on top, and
|
|
2387
|
+
// writes nothing.
|
|
2388
|
+
const AGENT_NAME_OPTION = [
|
|
2389
|
+
'--name <name>',
|
|
2390
|
+
'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`.',
|
|
2391
|
+
];
|
|
2392
|
+
|
|
2393
|
+
function identityOverridesFromOpts(opts) {
|
|
2394
|
+
const agentName = typeof opts.name === 'string' ? opts.name.trim() : undefined;
|
|
2395
|
+
return agentName ? { agentName } : {};
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2347
2398
|
credentialFlags(program
|
|
2348
2399
|
.command('init')
|
|
2349
2400
|
.alias('install')
|
|
@@ -2358,7 +2409,8 @@ credentialFlags(program
|
|
|
2358
2409
|
.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
2410
|
.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
2411
|
.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')
|
|
2412
|
+
.option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json')
|
|
2413
|
+
.option(...AGENT_NAME_OPTION))
|
|
2362
2414
|
.action(async (opts, command) => {
|
|
2363
2415
|
const globalOpts = command.optsWithGlobals();
|
|
2364
2416
|
|
|
@@ -2376,7 +2428,7 @@ credentialFlags(program
|
|
|
2376
2428
|
print: globalOpts.print,
|
|
2377
2429
|
global: opts.global,
|
|
2378
2430
|
force: opts.force,
|
|
2379
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2431
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2380
2432
|
});
|
|
2381
2433
|
return;
|
|
2382
2434
|
}
|
|
@@ -2387,7 +2439,7 @@ credentialFlags(program
|
|
|
2387
2439
|
print: globalOpts.print,
|
|
2388
2440
|
global: opts.global,
|
|
2389
2441
|
force: opts.force,
|
|
2390
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2442
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2391
2443
|
});
|
|
2392
2444
|
return;
|
|
2393
2445
|
}
|
|
@@ -2406,7 +2458,7 @@ credentialFlags(program
|
|
|
2406
2458
|
print: globalOpts.print,
|
|
2407
2459
|
global: opts.global,
|
|
2408
2460
|
force: opts.force,
|
|
2409
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2461
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2410
2462
|
target: opts.target,
|
|
2411
2463
|
});
|
|
2412
2464
|
// Deliberately NOT under .bridge-kit/.eventmodelers/ — that whole name is
|
|
@@ -2444,7 +2496,7 @@ credentialFlags(program
|
|
|
2444
2496
|
print: globalOpts.print,
|
|
2445
2497
|
global: opts.global,
|
|
2446
2498
|
force: opts.force,
|
|
2447
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2499
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2448
2500
|
templatesSource: join(clonedDir, 'templates'),
|
|
2449
2501
|
hooks: opts.hooks,
|
|
2450
2502
|
});
|
|
@@ -2457,7 +2509,7 @@ credentialFlags(program
|
|
|
2457
2509
|
print: globalOpts.print,
|
|
2458
2510
|
global: opts.global,
|
|
2459
2511
|
force: opts.force,
|
|
2460
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2512
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2461
2513
|
hooks: opts.hooks,
|
|
2462
2514
|
});
|
|
2463
2515
|
});
|
|
@@ -2474,7 +2526,8 @@ credentialFlags(program
|
|
|
2474
2526
|
.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
2527
|
.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
2528
|
.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')
|
|
2529
|
+
.option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json')
|
|
2530
|
+
.option(...AGENT_NAME_OPTION))
|
|
2478
2531
|
.action(async (opts, command) => {
|
|
2479
2532
|
const globalOpts = command.optsWithGlobals();
|
|
2480
2533
|
const targetDir = process.cwd();
|
|
@@ -2512,7 +2565,7 @@ credentialFlags(program
|
|
|
2512
2565
|
print: globalOpts.print,
|
|
2513
2566
|
global: opts.global !== undefined ? opts.global : !!manifest.global,
|
|
2514
2567
|
force: opts.force,
|
|
2515
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2568
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2516
2569
|
skipRootScaffold: true,
|
|
2517
2570
|
hooks: opts.hooks,
|
|
2518
2571
|
});
|
|
@@ -2642,10 +2695,11 @@ credentialFlags(program
|
|
|
2642
2695
|
.command('init-config')
|
|
2643
2696
|
.description('Configure credentials only — writes .eventmodelers/config.json in the current directory, or ~/.eventmodelers/config.json with --global')
|
|
2644
2697
|
.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.')
|
|
2698
|
+
.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.')
|
|
2699
|
+
.option(...AGENT_NAME_OPTION))
|
|
2646
2700
|
.action(async (opts, command) => {
|
|
2647
2701
|
const globalOpts = command.optsWithGlobals();
|
|
2648
|
-
const overrides = credentialOverridesFromOpts(opts);
|
|
2702
|
+
const overrides = { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) };
|
|
2649
2703
|
|
|
2650
2704
|
// A blob naming a board configures that board's own file rather than a project or
|
|
2651
2705
|
// account-wide config: the per-board store is keyed by board id, and the blob is
|
|
@@ -2662,9 +2716,6 @@ credentialFlags(program
|
|
|
2662
2716
|
process.exit(1);
|
|
2663
2717
|
}
|
|
2664
2718
|
if (!parsed.baseUrl) parsed.baseUrl = DEFAULT_BASE_URL;
|
|
2665
|
-
// Preserved across re-configuration: the platform keys a board's alive-ping on it, so
|
|
2666
|
-
// regenerating it would present a long-running agent as a brand-new one.
|
|
2667
|
-
parsed.agentId = readJsonSafe(boardCredentialsPath(parsed.boardId)).agentId || randomUUID();
|
|
2668
2719
|
writeBoardCredentials(parsed);
|
|
2669
2720
|
console.log('\n ✓ Saved credentials for board ' + parsed.boardId + ' to ' + boardCredentialsPath(parsed.boardId));
|
|
2670
2721
|
console.log('\n Start the agent from anywhere with:\n');
|
|
@@ -2688,6 +2739,11 @@ credentialFlags(program
|
|
|
2688
2739
|
if (pasted.token) base.token = pasted.token;
|
|
2689
2740
|
if (overrides.organizationId) base.organizationId = overrides.organizationId;
|
|
2690
2741
|
if (overrides.token) base.token = overrides.token;
|
|
2742
|
+
// Carried through the same narrowing: this branch rebuilds the file from scratch, so
|
|
2743
|
+
// an already-configured agentName has to be read back in or a later `init-config
|
|
2744
|
+
// --global` (e.g. rotating the token) would silently drop it.
|
|
2745
|
+
if (existing.agentName) base.agentName = existing.agentName;
|
|
2746
|
+
if (overrides.agentName) base.agentName = overrides.agentName;
|
|
2691
2747
|
|
|
2692
2748
|
const configured = await configureCredentials({
|
|
2693
2749
|
config: base,
|
|
@@ -2708,7 +2764,9 @@ credentialFlags(program
|
|
|
2708
2764
|
// configureCredentials' generic paste/manual flow may have picked up
|
|
2709
2765
|
// boardId/baseUrl too (e.g. from a pasted JSON blob) — strip them back out
|
|
2710
2766
|
// before the final write, since --global only ever persists identity.
|
|
2711
|
-
|
|
2767
|
+
// --name is the one non-credential that belongs here: it names the agent, not the
|
|
2768
|
+
// project, so an account-wide default is as portable as the org/token beside it.
|
|
2769
|
+
writeFileSync(configPath, JSON.stringify({ organizationId: configured.organizationId, token: configured.token, ...(configured.agentName ? { agentName: configured.agentName } : {}) }, null, 2));
|
|
2712
2770
|
console.log(`\n ✓ Saved account-wide defaults to ${configPath}`);
|
|
2713
2771
|
} else {
|
|
2714
2772
|
const targetDir = process.cwd();
|
|
@@ -2724,7 +2782,11 @@ credentialFlags(program
|
|
|
2724
2782
|
boardIdOptional: true,
|
|
2725
2783
|
overrides,
|
|
2726
2784
|
print: globalOpts.print,
|
|
2727
|
-
|
|
2785
|
+
// Same reasoning as the --global branch above: a bare `init-config` means "re-ask
|
|
2786
|
+
// me", but an invocation that already carries its answers on the command line —
|
|
2787
|
+
// credentials, or just a --name to record — must not stop to prompt, or every
|
|
2788
|
+
// non-interactive caller hangs (a closed stdin crashes outright).
|
|
2789
|
+
force: !Object.values(overrides).some(Boolean),
|
|
2728
2790
|
});
|
|
2729
2791
|
|
|
2730
2792
|
// Keep `.mcp.json` in sync — this command can change `baseUrl` (e.g.
|
|
@@ -2747,6 +2809,8 @@ credentialFlags(program
|
|
|
2747
2809
|
.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
2810
|
.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
2811
|
.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.')
|
|
2812
|
+
.option('--id <id>', 'Pin the agent id this run identifies itself with on the platform. A project install otherwise mints one id per project and reuses it on every restart; --global/--standalone mints a fresh one per run, since two ad-hoc agents for one board must not share a row (the heartbeat is keyed on token + agent_id + agent_type, so the second would replace the first). Pass this when an agent has to keep ONE identity across restarts — a supervisor that already knows the id, or a board where it is the starred "preferred agent". Per-run only: nothing is written to disk.')
|
|
2813
|
+
.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
2814
|
.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
2815
|
.action(async (opts, command) => {
|
|
2752
2816
|
const globalOpts = command.optsWithGlobals();
|
|
@@ -2758,6 +2822,23 @@ credentialFlags(program
|
|
|
2758
2822
|
if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
|
|
2759
2823
|
console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
|
|
2760
2824
|
}
|
|
2825
|
+
// --id/--name are what the platform will see for this run, so a blank one is a
|
|
2826
|
+
// mistake worth failing on rather than silently falling back to the stored identity.
|
|
2827
|
+
const identity = {
|
|
2828
|
+
agentId: opts.id === undefined ? null : String(opts.id).trim(),
|
|
2829
|
+
agentName: opts.name === undefined ? null : String(opts.name).trim(),
|
|
2830
|
+
};
|
|
2831
|
+
for (const [flag, value] of [['--id', identity.agentId], ['--name', identity.agentName]]) {
|
|
2832
|
+
if (value === '') {
|
|
2833
|
+
console.error(`❌ ${flag} needs a non-empty value.`);
|
|
2834
|
+
process.exit(1);
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
// ralph.sh has no realtime agent and never pings /api/agent-alive, so there is no
|
|
2838
|
+
// identity for either flag to override there.
|
|
2839
|
+
if ((identity.agentId || identity.agentName) && opts.bash) {
|
|
2840
|
+
console.log('ℹ️ --id/--name only apply to agents that ping the platform; the --bash loop does not, so they are ignored here.');
|
|
2841
|
+
}
|
|
2761
2842
|
// Both kit dirs can be installed side by side (e.g. running a build-kit and a
|
|
2762
2843
|
// modeling-kit agent from the same project). findInstalledKitDir only ever
|
|
2763
2844
|
// returns its first fixed-order match, which would silently prefer one stack
|
|
@@ -2821,7 +2902,7 @@ credentialFlags(program
|
|
|
2821
2902
|
const shown = relative(cwd, kitDir);
|
|
2822
2903
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (warm Claude process) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
2823
2904
|
try {
|
|
2824
|
-
await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents);
|
|
2905
|
+
await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents, identity);
|
|
2825
2906
|
} catch (err) {
|
|
2826
2907
|
console.error('[modeling] Fatal:', err);
|
|
2827
2908
|
process.exit(1);
|
|
@@ -2866,7 +2947,9 @@ credentialFlags(program
|
|
|
2866
2947
|
// their own separate output paths with no stream-json parsing to gate. RALPH_LOCAL is
|
|
2867
2948
|
// read by all three runners (ralph.js's startRalph, and ralph.sh directly) to force the
|
|
2868
2949
|
// local-only branch even when .eventmodelers/config.json has valid credentials.
|
|
2869
|
-
|
|
2950
|
+
// RALPH_AGENT_ID/RALPH_AGENT_NAME (--id/--name) are read in ralph.js's startRalph, so
|
|
2951
|
+
// they reach both node runners but not ralph.sh, which has no heartbeat to identify.
|
|
2952
|
+
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
2953
|
} catch (err) {
|
|
2871
2954
|
process.exit(err.status || 1);
|
|
2872
2955
|
}
|
|
@@ -3180,6 +3263,7 @@ program
|
|
|
3180
3263
|
'x-token': cfg.token,
|
|
3181
3264
|
'x-board-id': cfg.boardId,
|
|
3182
3265
|
'x-user-id': 'cli-set-slice-status',
|
|
3266
|
+
...agentHeaders(cfg),
|
|
3183
3267
|
},
|
|
3184
3268
|
body: JSON.stringify([{
|
|
3185
3269
|
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: {
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.0.70",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|