@eventmodelers/cli 1.0.67 → 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 +16 -0
- package/cli.js +128 -30
- 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 +30 -10
- 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/CLAUDE-STANDALONE.md +6 -5
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +18 -3
- 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
|
|
@@ -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
|
|
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)) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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}`);
|
|
@@ -2212,15 +2240,32 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2212
2240
|
}
|
|
2213
2241
|
}
|
|
2214
2242
|
|
|
2243
|
+
// A kill names exactly one agent: {type: 'kill', id: '<agentId>', instruction: 'exit'}. Anything
|
|
2244
|
+
// that doesn't name this agent is ignored — a broadcast reaches every agent on the board, and
|
|
2245
|
+
// the older signal (the bare string "Exit") took all of them down at once. That string form is
|
|
2246
|
+
// gone for good, not just unhandled: Supabase's broadcast API rejects a non-object payload with
|
|
2247
|
+
// 422, so it never actually arrived here.
|
|
2248
|
+
const exitIfAddressed = (payload) => {
|
|
2249
|
+
if (payload?.type !== 'kill' || payload?.id !== cfg.agentId) return;
|
|
2250
|
+
log(`received kill (instruction: ${payload.instruction ?? 'exit'}) — shutting down`);
|
|
2251
|
+
process.exit(0);
|
|
2252
|
+
};
|
|
2253
|
+
|
|
2254
|
+
// The kill signal is broadcast on the BOARD channel (see the platform's agent-kill /
|
|
2255
|
+
// backoffice/killagent slices), not on the org channel this agent uses for prompts — without
|
|
2256
|
+
// this second subscription a modeling agent can only be stopped with a kill(1).
|
|
2257
|
+
realtime.subscribe(
|
|
2258
|
+
`board:${cfg.boardId}-slicechanged`,
|
|
2259
|
+
{message: exitIfAddressed},
|
|
2260
|
+
(status) => log(`channel "board:${cfg.boardId}-slicechanged": ${status}`),
|
|
2261
|
+
).catch((err) => {
|
|
2262
|
+
log(`board channel subscribe failed, remote kill won't reach this agent: ${err.message}`);
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2215
2265
|
realtime.subscribe(
|
|
2216
2266
|
channelName,
|
|
2217
2267
|
{
|
|
2218
|
-
message:
|
|
2219
|
-
if (payload === 'Exit') {
|
|
2220
|
-
log('received "Exit" — shutting down');
|
|
2221
|
-
process.exit(0);
|
|
2222
|
-
}
|
|
2223
|
-
},
|
|
2268
|
+
message: exitIfAddressed,
|
|
2224
2269
|
'prompt:created': () => {
|
|
2225
2270
|
drain().catch((err) => log(`drain error: ${err.message}`));
|
|
2226
2271
|
},
|
|
@@ -2262,7 +2307,7 @@ async function runModeling(kitDir, projectDir, verbose = false, standalone = fal
|
|
|
2262
2307
|
const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
|
|
2263
2308
|
method: 'POST',
|
|
2264
2309
|
headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
|
|
2265
|
-
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 } : {}) }),
|
|
2266
2311
|
signal: AbortSignal.timeout(10_000),
|
|
2267
2312
|
});
|
|
2268
2313
|
if (!res.ok) log(`ping failed: ${res.status} ${await res.text().catch(() => '')}`);
|
|
@@ -2327,6 +2372,23 @@ function credentialOverridesFromOpts(opts) {
|
|
|
2327
2372
|
return { token: opts.token, boardId: opts.boardId, organizationId: opts.organizationId, baseUrl: opts.baseUrl };
|
|
2328
2373
|
}
|
|
2329
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
|
+
|
|
2330
2392
|
credentialFlags(program
|
|
2331
2393
|
.command('init')
|
|
2332
2394
|
.alias('install')
|
|
@@ -2341,7 +2403,8 @@ credentialFlags(program
|
|
|
2341
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.')
|
|
2342
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.')
|
|
2343
2405
|
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
|
|
2344
|
-
.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))
|
|
2345
2408
|
.action(async (opts, command) => {
|
|
2346
2409
|
const globalOpts = command.optsWithGlobals();
|
|
2347
2410
|
|
|
@@ -2359,7 +2422,7 @@ credentialFlags(program
|
|
|
2359
2422
|
print: globalOpts.print,
|
|
2360
2423
|
global: opts.global,
|
|
2361
2424
|
force: opts.force,
|
|
2362
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2425
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2363
2426
|
});
|
|
2364
2427
|
return;
|
|
2365
2428
|
}
|
|
@@ -2370,7 +2433,7 @@ credentialFlags(program
|
|
|
2370
2433
|
print: globalOpts.print,
|
|
2371
2434
|
global: opts.global,
|
|
2372
2435
|
force: opts.force,
|
|
2373
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2436
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2374
2437
|
});
|
|
2375
2438
|
return;
|
|
2376
2439
|
}
|
|
@@ -2389,7 +2452,7 @@ credentialFlags(program
|
|
|
2389
2452
|
print: globalOpts.print,
|
|
2390
2453
|
global: opts.global,
|
|
2391
2454
|
force: opts.force,
|
|
2392
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2455
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2393
2456
|
target: opts.target,
|
|
2394
2457
|
});
|
|
2395
2458
|
// Deliberately NOT under .bridge-kit/.eventmodelers/ — that whole name is
|
|
@@ -2427,7 +2490,7 @@ credentialFlags(program
|
|
|
2427
2490
|
print: globalOpts.print,
|
|
2428
2491
|
global: opts.global,
|
|
2429
2492
|
force: opts.force,
|
|
2430
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2493
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2431
2494
|
templatesSource: join(clonedDir, 'templates'),
|
|
2432
2495
|
hooks: opts.hooks,
|
|
2433
2496
|
});
|
|
@@ -2440,7 +2503,7 @@ credentialFlags(program
|
|
|
2440
2503
|
print: globalOpts.print,
|
|
2441
2504
|
global: opts.global,
|
|
2442
2505
|
force: opts.force,
|
|
2443
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2506
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2444
2507
|
hooks: opts.hooks,
|
|
2445
2508
|
});
|
|
2446
2509
|
});
|
|
@@ -2457,7 +2520,8 @@ credentialFlags(program
|
|
|
2457
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`)
|
|
2458
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.')
|
|
2459
2522
|
.option('--global', 'Re-install skills into ~/.claude/skills/ instead of the project — defaults to however they were originally installed')
|
|
2460
|
-
.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))
|
|
2461
2525
|
.action(async (opts, command) => {
|
|
2462
2526
|
const globalOpts = command.optsWithGlobals();
|
|
2463
2527
|
const targetDir = process.cwd();
|
|
@@ -2495,7 +2559,7 @@ credentialFlags(program
|
|
|
2495
2559
|
print: globalOpts.print,
|
|
2496
2560
|
global: opts.global !== undefined ? opts.global : !!manifest.global,
|
|
2497
2561
|
force: opts.force,
|
|
2498
|
-
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
2562
|
+
credentialOverrides: { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) },
|
|
2499
2563
|
skipRootScaffold: true,
|
|
2500
2564
|
hooks: opts.hooks,
|
|
2501
2565
|
});
|
|
@@ -2625,10 +2689,11 @@ credentialFlags(program
|
|
|
2625
2689
|
.command('init-config')
|
|
2626
2690
|
.description('Configure credentials only — writes .eventmodelers/config.json in the current directory, or ~/.eventmodelers/config.json with --global')
|
|
2627
2691
|
.option('--global', 'Write account-wide defaults (organizationId + token only) to ~/.eventmodelers/config.json instead of the project')
|
|
2628
|
-
.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))
|
|
2629
2694
|
.action(async (opts, command) => {
|
|
2630
2695
|
const globalOpts = command.optsWithGlobals();
|
|
2631
|
-
const overrides = credentialOverridesFromOpts(opts);
|
|
2696
|
+
const overrides = { ...credentialOverridesFromOpts(opts), ...identityOverridesFromOpts(opts) };
|
|
2632
2697
|
|
|
2633
2698
|
// A blob naming a board configures that board's own file rather than a project or
|
|
2634
2699
|
// account-wide config: the per-board store is keyed by board id, and the blob is
|
|
@@ -2671,6 +2736,11 @@ credentialFlags(program
|
|
|
2671
2736
|
if (pasted.token) base.token = pasted.token;
|
|
2672
2737
|
if (overrides.organizationId) base.organizationId = overrides.organizationId;
|
|
2673
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;
|
|
2674
2744
|
|
|
2675
2745
|
const configured = await configureCredentials({
|
|
2676
2746
|
config: base,
|
|
@@ -2691,7 +2761,9 @@ credentialFlags(program
|
|
|
2691
2761
|
// configureCredentials' generic paste/manual flow may have picked up
|
|
2692
2762
|
// boardId/baseUrl too (e.g. from a pasted JSON blob) — strip them back out
|
|
2693
2763
|
// before the final write, since --global only ever persists identity.
|
|
2694
|
-
|
|
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));
|
|
2695
2767
|
console.log(`\n ✓ Saved account-wide defaults to ${configPath}`);
|
|
2696
2768
|
} else {
|
|
2697
2769
|
const targetDir = process.cwd();
|
|
@@ -2707,7 +2779,11 @@ credentialFlags(program
|
|
|
2707
2779
|
boardIdOptional: true,
|
|
2708
2780
|
overrides,
|
|
2709
2781
|
print: globalOpts.print,
|
|
2710
|
-
|
|
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),
|
|
2711
2787
|
});
|
|
2712
2788
|
|
|
2713
2789
|
// Keep `.mcp.json` in sync — this command can change `baseUrl` (e.g.
|
|
@@ -2730,6 +2806,8 @@ credentialFlags(program
|
|
|
2730
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.')
|
|
2731
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)')
|
|
2732
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.')
|
|
2733
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.'))
|
|
2734
2812
|
.action(async (opts, command) => {
|
|
2735
2813
|
const globalOpts = command.optsWithGlobals();
|
|
@@ -2741,6 +2819,23 @@ credentialFlags(program
|
|
|
2741
2819
|
if (command.getOptionValueSource('maxAgents') === 'cli' && !opts.standalone) {
|
|
2742
2820
|
console.log('ℹ️ --max-agents only applies to --standalone turns; ignoring it here.');
|
|
2743
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
|
+
}
|
|
2744
2839
|
// Both kit dirs can be installed side by side (e.g. running a build-kit and a
|
|
2745
2840
|
// modeling-kit agent from the same project). findInstalledKitDir only ever
|
|
2746
2841
|
// returns its first fixed-order match, which would silently prefer one stack
|
|
@@ -2804,7 +2899,7 @@ credentialFlags(program
|
|
|
2804
2899
|
const shown = relative(cwd, kitDir);
|
|
2805
2900
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (warm Claude process) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
2806
2901
|
try {
|
|
2807
|
-
await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents);
|
|
2902
|
+
await runModeling(kitDir, projectDir, !!opts.verbose, !!opts.standalone, overrides, maxAgents, identity);
|
|
2808
2903
|
} catch (err) {
|
|
2809
2904
|
console.error('[modeling] Fatal:', err);
|
|
2810
2905
|
process.exit(1);
|
|
@@ -2849,7 +2944,9 @@ credentialFlags(program
|
|
|
2849
2944
|
// their own separate output paths with no stream-json parsing to gate. RALPH_LOCAL is
|
|
2850
2945
|
// read by all three runners (ralph.js's startRalph, and ralph.sh directly) to force the
|
|
2851
2946
|
// local-only branch even when .eventmodelers/config.json has valid credentials.
|
|
2852
|
-
|
|
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 ?? '' } });
|
|
2853
2950
|
} catch (err) {
|
|
2854
2951
|
process.exit(err.status || 1);
|
|
2855
2952
|
}
|
|
@@ -3163,6 +3260,7 @@ program
|
|
|
3163
3260
|
'x-token': cfg.token,
|
|
3164
3261
|
'x-board-id': cfg.boardId,
|
|
3165
3262
|
'x-user-id': 'cli-set-slice-status',
|
|
3263
|
+
...agentHeaders(cfg),
|
|
3166
3264
|
},
|
|
3167
3265
|
body: JSON.stringify([{
|
|
3168
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: {
|
|
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.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 });
|
|
@@ -316,11 +325,15 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', queueAllSt
|
|
|
316
325
|
realtime.subscribe(
|
|
317
326
|
channelName,
|
|
318
327
|
{
|
|
328
|
+
// A kill names exactly one agent: {type: 'kill', id: '<agentId>', instruction: 'exit'}.
|
|
329
|
+
// Anything that doesn't name this agent is ignored — a broadcast reaches every agent on
|
|
330
|
+
// the board, and the older signal (the bare string "Exit") took all of them down at once.
|
|
331
|
+
// That string form is gone for good, not just unhandled: Supabase's broadcast API rejects
|
|
332
|
+
// a non-object payload with 422, so it never actually arrived here.
|
|
319
333
|
message: (payload) => {
|
|
320
|
-
if (payload
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
334
|
+
if (payload?.type !== 'kill' || payload?.id !== cfg.agentId) return;
|
|
335
|
+
console.log(`[agent] ${ts()} Received kill (instruction: ${payload.instruction ?? 'exit'}) — shutting down`);
|
|
336
|
+
process.exit(0);
|
|
324
337
|
},
|
|
325
338
|
'slice:changed': (payload) => handleSliceChanged(payload, cfg, kitDir, queueAllStatuses),
|
|
326
339
|
},
|
|
@@ -363,7 +376,7 @@ async function startRealtimeAgent(cfg, kitDir, { agentType = 'BUILD', queueAllSt
|
|
|
363
376
|
const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
|
|
364
377
|
method: 'POST',
|
|
365
378
|
headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
|
|
366
|
-
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 } : {}) }),
|
|
367
380
|
signal: AbortSignal.timeout(10_000),
|
|
368
381
|
});
|
|
369
382
|
if (!res.ok) {
|
|
@@ -485,7 +498,7 @@ async function blockStuckSlice(kitDir, cfg, credentialed, planned, attempts) {
|
|
|
485
498
|
try {
|
|
486
499
|
await fetchJSON(`${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes/events`, {
|
|
487
500
|
method: 'POST',
|
|
488
|
-
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) },
|
|
489
502
|
body: JSON.stringify([{
|
|
490
503
|
id: randomUUID(),
|
|
491
504
|
eventType: 'node:changed',
|
|
@@ -579,10 +592,17 @@ export { loadLocalConfig, fetchPlatformConfig, retryOn401, startRealtimeAgent };
|
|
|
579
592
|
|
|
580
593
|
export async function startRalph({ kitDir, projectDir, onTask, onPlannedSlice, agentType = 'BUILD', queueAllStatuses = false, localOnly = false }) {
|
|
581
594
|
const local = loadLocalConfig(kitDir);
|
|
582
|
-
|
|
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;
|
|
583
602
|
|
|
584
603
|
console.log(`Ralph — kit: ${kitDir}`);
|
|
585
604
|
console.log(` project: ${projectDir}`);
|
|
605
|
+
console.log(` agent: ${local.agentName ? `${local.agentName} (${local.agentId})` : local.agentId}`);
|
|
586
606
|
|
|
587
607
|
// localOnly (set via `eventmodelers run --local`) forces this branch even when
|
|
588
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
|
|
|
@@ -181,11 +181,12 @@ Steps:
|
|
|
181
181
|
6. **If you already said it, don't say it again.** Before posting a comment — or having a
|
|
182
182
|
subagent post one — read the node's existing comments. An unresolved question already
|
|
183
183
|
there means that contribution is on the board.
|
|
184
|
-
7. **Write
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
subagent reported back
|
|
184
|
+
7. **Write nothing to disk.** A self-directed turn is modeling, not tracked progress — nothing
|
|
185
|
+
goes into `progress.txt`, and nothing into `.agent-modeling-kit/AGENTS.md` either. You only
|
|
186
|
+
ever get here in a `standalone=on` session, which is ad-hoc: its kit dir is shared across
|
|
187
|
+
every board and nobody reads it afterwards. The board is the only place anything is kept, so
|
|
188
|
+
anything reusable — including what a subagent reported back — goes there, as a comment on the
|
|
189
|
+
node it concerns.
|
|
189
190
|
8. Reply `<promise>DONE</promise>`, naming what you dispatched and what each agent did, or —
|
|
190
191
|
when step 2 turned up nothing worth doing — change nothing at all and reply
|
|
191
192
|
`<promise>NOOP</promise>`. A NOOP is a perfectly good outcome, and the CLI widens the gap
|
|
@@ -12,6 +12,20 @@ for their independent, self-contained slice-implementation tasks). Each user mes
|
|
|
12
12
|
receive already IS the one prompt to handle; there's nothing to read, pre-filter, or pick
|
|
13
13
|
from.
|
|
14
14
|
|
|
15
|
+
### `standalone=on` is ad-hoc — write nothing to disk
|
|
16
|
+
|
|
17
|
+
The session header carries `standalone=on` or `standalone=off`. `standalone=on` is an **ad-hoc**
|
|
18
|
+
session: it belongs to no project, its kit dir is a `~/.eventmodelers/kit` shared by every board,
|
|
19
|
+
and nobody goes looking in there afterwards. So in a `standalone=on` session the **board is the
|
|
20
|
+
only place anything is kept** — comments, elements, slice statuses, scenarios. Write no file at
|
|
21
|
+
all: no `progress.txt` (step 8), no `.agent-modeling-kit/AGENTS.md` (step 9), and nothing a skill's
|
|
22
|
+
own instructions suggest writing down either. Anything worth keeping goes on the board, as a
|
|
23
|
+
comment on the node it concerns. This overrides every "write it down" instruction elsewhere in this
|
|
24
|
+
file and in any skill.
|
|
25
|
+
|
|
26
|
+
With `standalone=off` the session belongs to one project and the kit dir is that project's, so
|
|
27
|
+
steps 8 and 9 apply as written.
|
|
28
|
+
|
|
15
29
|
You are a long-lived process handling many turns in a row. **Read this file once**, on
|
|
16
30
|
the first turn (the one whose message begins with `MODE=modeling`) — don't re-read it on
|
|
17
31
|
every later turn just because a new prompt came in. The same applies to other one-time
|
|
@@ -122,8 +136,8 @@ mention it in the `DONE` comment, and leave it for a self-directed turn (or for
|
|
|
122
136
|
- If it doesn't — the prompt is ambiguous enough that any guess risks doing the wrong thing — stop instead of guessing. Skip straight to step 6 and mark the prompt `DONE` with a comment explaining what's unclear and pointing to the comment you just posted. Never leave a prompt neither progressed nor closed.
|
|
123
137
|
6. **Mark the prompt as finished** — invoke `/update-prompt-status` with this turn's `prompt_id`, `newStatus=DONE`, and a `comment` that summarizes what you actually did (e.g. "Added the OrderPlaced event and wired it to the read model"). Do this once, right after the work is done — not per skill call within the turn.
|
|
124
138
|
7. If this turn has a `comment_id` field, invoke `/handle-comment` with `action=resolve`, `nodeId` from the resolved `NODE_ID` (step 3), `commentId` from `comment_id`.
|
|
125
|
-
8.
|
|
126
|
-
9.
|
|
139
|
+
8. **`standalone=off` only** — append a progress entry to `progress.txt`; see the Progress Entry Format below. Fill in the `Learnings` line with anything reusable noticed this turn (pattern, gotcha, useful context), or "none". In a `standalone=on` session, skip this: that session writes no files (see Mode), so note anything worth keeping as a board comment instead.
|
|
140
|
+
9. **`standalone=off` only** — if this turn's `Learnings` line was not "none", promote it to `.agent-modeling-kit/AGENTS.md` (create it if it doesn't exist) — only add it if it's not already there.
|
|
127
141
|
10. Reply `<promise>DONE</promise>` and wait for the next turn.
|
|
128
142
|
|
|
129
143
|
|
|
@@ -197,7 +211,8 @@ Read `.claude/skills/<skill-name>/SKILL.md` before executing — each skill has
|
|
|
197
211
|
|
|
198
212
|
## Progress Entry Format
|
|
199
213
|
|
|
200
|
-
|
|
214
|
+
`standalone=off` prompt turns only. A `standalone=on` session writes no progress file at all (see
|
|
215
|
+
Mode), and a self-directed board-change turn never writes one in any session.
|
|
201
216
|
|
|
202
217
|
APPEND to `progress.txt` (never replace):
|
|
203
218
|
```
|
|
@@ -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',
|