@eventmodelers/cli 1.0.136 → 1.0.137
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/cli.js +8 -2
- package/lib/agent-permissions.js +9 -5
- package/lib/bash-guard.js +54 -7
- package/package.json +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +1 -0
- package/stacks/modeling-kit/templates/.claude/skills/place-element/references/api-fallback.md +6 -6
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +9 -7
package/cli.js
CHANGED
|
@@ -1952,7 +1952,7 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1952
1952
|
// untargeted is handed straight back to the queue for another agent to take. It says
|
|
1953
1953
|
// nothing about the standalone lane — a self-directed turn is nobody's task, so an
|
|
1954
1954
|
// exclusive standalone agent still works the board on its own initiative.
|
|
1955
|
-
async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null, exec = null, model = null, skipPermissions = false } = {}) {
|
|
1955
|
+
async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null, exec = null, model = null, skipPermissions = false, browser = false } = {}) {
|
|
1956
1956
|
// --worker: only prompts — no chat, and no board-change turns (it has nowhere to propose).
|
|
1957
1957
|
const chat = !worker;
|
|
1958
1958
|
const configLibPath = join(kitDir, 'lib', 'config.js');
|
|
@@ -2117,6 +2117,7 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
|
|
|
2117
2117
|
codeDir,
|
|
2118
2118
|
tmpDir: tmpdir(),
|
|
2119
2119
|
hookCommand: `"${process.execPath}" "${fileURLToPath(new URL('./lib/bash-guard-hook.js', import.meta.url))}"`,
|
|
2120
|
+
browser,
|
|
2120
2121
|
});
|
|
2121
2122
|
if (skipPermissions) {
|
|
2122
2123
|
console.warn('⚠️ --dangerously-skip-permissions: every tool call this agent makes is approved — any shell command, curl to anywhere, any file you can read or write. Anyone who can chat with it on this board can make it do that.\n');
|
|
@@ -3681,6 +3682,7 @@ credentialFlags(program
|
|
|
3681
3682
|
.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.')
|
|
3682
3683
|
.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)')
|
|
3683
3684
|
.option('--dangerously-skip-permissions', 'Run the modeling agent\'s Claude process with --dangerously-skip-permissions, as before the lockdown: every tool call is approved — any shell command, curl to anywhere, any file the user can read or write, every MCP server the machine configures. Off by default because the agent takes chat messages and snippet clicks from any collaborator on the board; pass it only for a board whose collaborators you trust with your shell. Default Claude runner of --modeling/--standalone/--worker/--global only — not with --agent, --exec or --local-ai, which bring their own permission flags.')
|
|
3685
|
+
.option('--browser', 'Give the locked-down modeling agent a browser: the Playwright MCP server (npx @playwright/mcp) next to the eventmodelers one, for discover-storyboard. Off by default because a browser can reach any site, which the lockdown otherwise rules out. Default Claude runner only; --dangerously-skip-permissions already loads every MCP server the machine configures.')
|
|
3684
3686
|
.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.')
|
|
3685
3687
|
.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.')
|
|
3686
3688
|
.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.')
|
|
@@ -3793,6 +3795,10 @@ credentialFlags(program
|
|
|
3793
3795
|
console.error(`❌ --force asks for credentials again, which --non-interactive/--print rule out — pick one.`);
|
|
3794
3796
|
process.exit(1);
|
|
3795
3797
|
}
|
|
3798
|
+
if (opts.browser && (opts.exec || modelingLocalAi !== null || opts.dangerouslySkipPermissions)) {
|
|
3799
|
+
console.error('❌ --browser only applies to the locked-down default Claude runner — --agent/--exec and --dangerously-skip-permissions use the MCP servers the machine configures, and --local-ai has no tools.');
|
|
3800
|
+
process.exit(1);
|
|
3801
|
+
}
|
|
3796
3802
|
if (opts.dangerouslySkipPermissions && (opts.exec || modelingLocalAi !== null)) {
|
|
3797
3803
|
console.error('❌ --dangerously-skip-permissions only applies to the default Claude runner — --agent/--exec bring their own permission flags (put them in the command), and --local-ai has no shell or file tools to approve.');
|
|
3798
3804
|
process.exit(1);
|
|
@@ -3861,7 +3867,7 @@ credentialFlags(program
|
|
|
3861
3867
|
const runnerLabel = modelingLocalAi ? 'local model' : opts.exec ? 'one agent process per turn' : 'warm Claude process';
|
|
3862
3868
|
await new Promise((res) => process.stdout.write(`▶ Starting modeling loop (${runnerLabel}) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
3863
3869
|
try {
|
|
3864
|
-
await runModeling(kitDir, projectDir, { codeDir, verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, worker: !!opts.worker, overrides, maxAgents, identity, localAi: modelingLocalAi, exec: opts.exec ?? null, model: !opts.exec && modelingLocalAi === null ? model : null, skipPermissions: !!opts.dangerouslySkipPermissions });
|
|
3870
|
+
await runModeling(kitDir, projectDir, { codeDir, verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, worker: !!opts.worker, overrides, maxAgents, identity, localAi: modelingLocalAi, exec: opts.exec ?? null, model: !opts.exec && modelingLocalAi === null ? model : null, skipPermissions: !!opts.dangerouslySkipPermissions, browser: !!opts.browser });
|
|
3865
3871
|
} catch (err) {
|
|
3866
3872
|
console.error('[modeling] Fatal:', err);
|
|
3867
3873
|
process.exit(1);
|
package/lib/agent-permissions.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
// - --permission-mode dontAsk: a tool call that is not explicitly allowed is refused on the spot — nobody is asked
|
|
6
6
|
// (with stream-json a permission prompt would go to this CLI, which never answers, and the turn would hang).
|
|
7
7
|
// - --strict-mcp-config + --mcp-config: the eventmodelers server is the only MCP server, whatever else the machine's
|
|
8
|
-
// user or project settings configure; mcp__eventmodelers allows all of its tools.
|
|
8
|
+
// user or project settings configure; mcp__eventmodelers allows all of its tools. With `browser`, the Playwright
|
|
9
|
+
// server (@playwright/mcp) joins it for discover-storyboard — opt-in, since a browser reaches any site.
|
|
9
10
|
// - Bash goes through lib/bash-guard-hook.js (a PreToolUse hook): it allows only the commands on lib/bash-guard.js's
|
|
10
11
|
// list — curl only to the platform — and denies the rest. There is no Bash allow rule, so a command the hook does
|
|
11
12
|
// not explicitly allow is refused by dontAsk.
|
|
12
13
|
// - Files: reading and editing in the kit directory (the working directory) and the temp directories; reading the
|
|
13
|
-
// code directory (--add-dir). Never the files holding the token
|
|
14
|
+
// code directory (--add-dir). Never the files holding the token (.eventmodelers/*.json — the rest of .eventmodelers/,
|
|
15
|
+
// e.g. the interview trail in .eventmodelers/interviews/, stays usable), never .claude/ or .mcp.json (an agent rewriting
|
|
14
16
|
// its own permissions or hooks would outlive the turn), never .env files. No WebFetch / WebSearch.
|
|
15
17
|
|
|
16
18
|
import { realpathSync } from 'fs';
|
|
@@ -23,23 +25,24 @@ const abs = (path) => `/${path}`;
|
|
|
23
25
|
* The claude arguments and environment for a locked-down modeling agent.
|
|
24
26
|
* { baseUrl, projectDir, codeDir?, tmpDir, hookCommand } → { args: string[], env: Record<string, string> }
|
|
25
27
|
*/
|
|
26
|
-
export function modelingPermissions({ baseUrl, projectDir, codeDir, tmpDir, hookCommand }) {
|
|
28
|
+
export function modelingPermissions({ baseUrl, projectDir, codeDir, tmpDir, hookCommand, browser = false }) {
|
|
27
29
|
const tempDirs = [...new Set(['/tmp', '/private/tmp', tmpDir, realOrSame(tmpDir)].filter(Boolean))];
|
|
28
30
|
const roots = [...new Set([projectDir, realOrSame(projectDir), codeDir, codeDir && realOrSame(codeDir), ...tempDirs].filter(Boolean))];
|
|
29
31
|
|
|
30
32
|
const allowed = [
|
|
31
33
|
'mcp__eventmodelers',
|
|
34
|
+
...(browser ? ['mcp__playwright'] : []),
|
|
32
35
|
'Skill', 'Agent', 'Task', 'TodoWrite', 'ToolSearch',
|
|
33
36
|
// Edit rules cover every file-editing tool (Write included) — Claude ignores Write(path) rules.
|
|
34
37
|
'Edit(./**)',
|
|
35
38
|
...tempDirs.flatMap((dir) => [`Read(${abs(dir)}/**)`, `Edit(${abs(dir)}/**)`]),
|
|
36
39
|
];
|
|
37
|
-
const secretReads = ['./.claude/settings.local.json', './.claude/settings.json', './.eventmodelers
|
|
40
|
+
const secretReads = ['./.claude/settings.local.json', './.claude/settings.json', './.eventmodelers/*.json', './**/.eventmodelers/*.json', './**/.env', './**/.env.*'];
|
|
38
41
|
const disallowed = [
|
|
39
42
|
'WebFetch', 'WebSearch',
|
|
40
43
|
...secretReads.map((p) => `Read(${p})`),
|
|
41
44
|
...(codeDir ? [`Read(${abs(codeDir)}/**/.env)`, `Read(${abs(codeDir)}/**/.env.*)`] : []),
|
|
42
|
-
'Edit(./.claude/**)', 'Edit(./.mcp.json)', 'Edit(./.eventmodelers
|
|
45
|
+
'Edit(./.claude/**)', 'Edit(./.mcp.json)', 'Edit(./.eventmodelers/*.json)', 'Edit(./**/.eventmodelers/*.json)',
|
|
43
46
|
];
|
|
44
47
|
|
|
45
48
|
const mcpConfig = {
|
|
@@ -50,6 +53,7 @@ export function modelingPermissions({ baseUrl, projectDir, codeDir, tmpDir, hook
|
|
|
50
53
|
// Resolved by claude from its own environment — the token never sits in an argument.
|
|
51
54
|
headers: { 'x-token': '${EVENTMODELERS_TOKEN}', 'x-agent-id': '${EVENTMODELERS_AGENT_ID}' },
|
|
52
55
|
},
|
|
56
|
+
...(browser ? { playwright: { type: 'stdio', command: 'npx', args: ['-y', '@playwright/mcp@latest'] } } : {}),
|
|
53
57
|
},
|
|
54
58
|
};
|
|
55
59
|
const settings = {
|
package/lib/bash-guard.js
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
// - Programs: curl (only to the platform's baseUrl), python3 (two exact forms), uuidgen, jq, git (read-only
|
|
10
10
|
// subcommands), and read-only text tools (cat, head, tail, wc, sort, uniq, grep, cut, tr, ls), echo, printf,
|
|
11
11
|
// mkdir, sleep, date, true. Nothing else — no sh/bash/eval/env/xargs/find/sed/awk/node/npm/rm/mv/cp.
|
|
12
|
-
// - No command substitution ($(…), `…`, $((…)), <(…))
|
|
13
|
-
//
|
|
12
|
+
// - No command substitution ($(…), `…`, $((…)), <(…)) except exactly $(uuidgen) and $(seq N) / $(seq A B) — they mint
|
|
13
|
+
// ids and run nothing else. No subshells, brace groups, brace expansion, background jobs or ~ paths. The only control
|
|
14
|
+
// flow is `for NAME in …; do …; done` (its body checked like any other command; NAME counts as set).
|
|
14
15
|
// - Variables: only BASE_URL, ORG_ID, BOARD_ID, AGENT_ID, CHAT_SESSION_ID, EVENTMODELERS_AGENT_ID, CLAUDE_CODE_SESSION_ID and names assigned in
|
|
15
16
|
// the same command. The token ($EVENTMODELERS_TOKEN / $TOKEN) only inside a curl -H value — never echoed, written or
|
|
16
17
|
// passed elsewhere. Assigning BASE_URL is fine only to the real baseUrl; PATH, proxies, LD_*/DYLD_*/GIT_* … never.
|
|
@@ -21,7 +22,7 @@ import { isAbsolute, resolve, sep } from 'path';
|
|
|
21
22
|
|
|
22
23
|
const SAFE_VARS = new Set(['BASE_URL', 'ORG_ID', 'BOARD_ID', 'AGENT_ID', 'CHAT_SESSION_ID', 'EVENTMODELERS_AGENT_ID', 'CLAUDE_CODE_SESSION_ID']);
|
|
23
24
|
const TOKEN_VARS = new Set(['TOKEN', 'EVENTMODELERS_TOKEN']);
|
|
24
|
-
const PROTECTED_VAR = /^(PATH|HOME|IFS|ENV|BASH_ENV|SHELLOPTS|BASHOPTS|PS4|PROMPT_COMMAND|CDPATH|GLOBIGNORE|TMPDIR|SHELL|USER|LD_.*|DYLD_.*|GIT_.*|CURL.*|SSL_.*|
|
|
25
|
+
const PROTECTED_VAR = /^(PATH|HOME|IFS|ENV|BASH_ENV|SHELLOPTS|BASHOPTS|PS4|PROMPT_COMMAND|CDPATH|GLOBIGNORE|TMPDIR|SHELL|USER|LD_.*|DYLD_.*|GIT_.*|CURL.*|SSL_.*|NODE_OPTIONS|NODE_PATH|NODE_TLS_.*|NODE_EXTRA_.*|PYTHON.*|.*PROXY.*|EVENTMODELERS_.*|ANTHROPIC_.*|CLAUDE.*)$/i;
|
|
25
26
|
|
|
26
27
|
class Refused extends Error {}
|
|
27
28
|
const refuse = (reason) => { throw new Refused(reason); };
|
|
@@ -39,7 +40,11 @@ function lex(command) {
|
|
|
39
40
|
const readVar = (word) => {
|
|
40
41
|
// at command[i] === '$'
|
|
41
42
|
const next = command[i + 1];
|
|
42
|
-
if (next === '(')
|
|
43
|
+
if (next === '(') {
|
|
44
|
+
const sub = /^\$\((?:uuidgen|seq \d{1,4}(?: \d{1,4})?)\)/.exec(command.slice(i));
|
|
45
|
+
if (!sub) refuse('command substitution $(…) is not allowed — only $(uuidgen) and $(seq N)');
|
|
46
|
+
word.text += sub[0]; i += sub[0].length; return;
|
|
47
|
+
}
|
|
43
48
|
if (next === '{') {
|
|
44
49
|
const end = command.indexOf('}', i + 2);
|
|
45
50
|
const name = end < 0 ? '' : command.slice(i + 2, end);
|
|
@@ -155,6 +160,8 @@ function lex(command) {
|
|
|
155
160
|
|
|
156
161
|
// ─── Commands ───────────────────────────────────────────────────────────────────────────────────
|
|
157
162
|
|
|
163
|
+
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
164
|
+
|
|
158
165
|
/** Splits the tokens into simple commands: leading assignments, words, redirections, heredocs. */
|
|
159
166
|
function simpleCommands(tokens) {
|
|
160
167
|
const commands = [];
|
|
@@ -164,7 +171,7 @@ function simpleCommands(tokens) {
|
|
|
164
171
|
if (!current) { current = { assignments: [], words: [], redirects: [], heredocs: [] }; commands.push(current); }
|
|
165
172
|
if (t.type === 'redirect') current.redirects.push(t);
|
|
166
173
|
else if (t.type === 'heredoc') current.heredocs.push(t);
|
|
167
|
-
else if (!current.words.length &&
|
|
174
|
+
else if (!current.words.length && ASSIGNMENT.test(t.text)) {
|
|
168
175
|
const eq = t.text.indexOf('=');
|
|
169
176
|
current.assignments.push({ name: t.text.slice(0, eq), value: t.text.slice(eq + 1), word: t });
|
|
170
177
|
} else current.words.push(t);
|
|
@@ -172,6 +179,46 @@ function simpleCommands(tokens) {
|
|
|
172
179
|
return commands;
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Takes the `for NAME in …; do …; done` keywords out of the simple commands, leaving the loop items on the `for`
|
|
184
|
+
* command (as `loopItems`, checked like words) and each body command as a plain command. Returns the loop variables.
|
|
185
|
+
*/
|
|
186
|
+
function unwrapLoops(commands) {
|
|
187
|
+
const loopVars = [];
|
|
188
|
+
let open = 0;
|
|
189
|
+
let expectDo = false;
|
|
190
|
+
for (const cmd of commands) {
|
|
191
|
+
const first = cmd.words[0];
|
|
192
|
+
const keyword = first && !first.quoted && !first.refs.length ? first.text : null;
|
|
193
|
+
if (expectDo && keyword !== 'do') refuse('a for loop needs "do" after its list');
|
|
194
|
+
if (keyword === 'for') {
|
|
195
|
+
const [, name, inWord, ...items] = cmd.words;
|
|
196
|
+
if (cmd.assignments.length || cmd.redirects.length || cmd.heredocs.length || !name || name.quoted
|
|
197
|
+
|| !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name.text) || inWord?.text !== 'in' || inWord.quoted) {
|
|
198
|
+
refuse('only "for NAME in …; do …; done" loops are allowed');
|
|
199
|
+
}
|
|
200
|
+
if (PROTECTED_VAR.test(name.text) || TOKEN_VARS.has(name.text)) refuse(`setting ${name.text} is not allowed`);
|
|
201
|
+
loopVars.push(name.text);
|
|
202
|
+
cmd.loopItems = items; cmd.words = [];
|
|
203
|
+
open += 1; expectDo = true;
|
|
204
|
+
} else if (keyword === 'do') {
|
|
205
|
+
if (!expectDo) refuse('"do" outside a for loop');
|
|
206
|
+
expectDo = false;
|
|
207
|
+
cmd.words.shift();
|
|
208
|
+
while (cmd.words.length && ASSIGNMENT.test(cmd.words[0].text)) {
|
|
209
|
+
const w = cmd.words.shift();
|
|
210
|
+
const eq = w.text.indexOf('=');
|
|
211
|
+
cmd.assignments.push({ name: w.text.slice(0, eq), value: w.text.slice(eq + 1), word: w });
|
|
212
|
+
}
|
|
213
|
+
} else if (keyword === 'done') {
|
|
214
|
+
if (!open || cmd.words.length > 1 || cmd.assignments.length) refuse('"done" outside a for loop');
|
|
215
|
+
open -= 1; cmd.words = [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (open || expectDo) refuse('a for loop is never closed with "done"');
|
|
219
|
+
return loopVars;
|
|
220
|
+
}
|
|
221
|
+
|
|
175
222
|
function withinRoots(path, policy) {
|
|
176
223
|
if (path === '/dev/null' || path === '-') return true;
|
|
177
224
|
if (path.includes('$')) refuse(`path "${path}" may not contain a variable`);
|
|
@@ -373,7 +420,7 @@ export function checkBashCommand(command, policy) {
|
|
|
373
420
|
try {
|
|
374
421
|
if (typeof command !== 'string' || !command.trim()) refuse('empty command');
|
|
375
422
|
const commands = simpleCommands(lex(command));
|
|
376
|
-
const assigned = new Set();
|
|
423
|
+
const assigned = new Set(unwrapLoops(commands));
|
|
377
424
|
for (const cmd of commands) {
|
|
378
425
|
for (const a of cmd.assignments) {
|
|
379
426
|
if (PROTECTED_VAR.test(a.name) || TOKEN_VARS.has(a.name)) refuse(`setting ${a.name} is not allowed`);
|
|
@@ -386,7 +433,7 @@ export function checkBashCommand(command, policy) {
|
|
|
386
433
|
for (const r of cmd.redirects) {
|
|
387
434
|
if (r.op !== '<<<') checkPath(r.target.text, r.op === '<' ? 'input file' : 'output file', policy);
|
|
388
435
|
}
|
|
389
|
-
const parts = [...cmd.words, ...cmd.assignments.map((a) => a.word), ...cmd.redirects.map((r) => r.target), ...cmd.heredocs];
|
|
436
|
+
const parts = [...cmd.words, ...(cmd.loopItems ?? []), ...cmd.assignments.map((a) => a.word), ...cmd.redirects.map((r) => r.target), ...cmd.heredocs];
|
|
390
437
|
for (const w of parts) {
|
|
391
438
|
if (w.braceOrTilde && w.type === 'word') refuse(`"${w.text}": brace expansion and ~ are not allowed — quote it or spell the path out`);
|
|
392
439
|
for (const name of w.refs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.137",
|
|
4
4
|
"description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -43,6 +43,7 @@ Install a browser automation MCP and restart Claude Code. Options:
|
|
|
43
43
|
• Playwright MCP: @playwright/mcp
|
|
44
44
|
|
|
45
45
|
Once installed, run /discover-storyboard again.
|
|
46
|
+
A board modeling agent (`run --modeling`) only gets one when started with --browser.
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
Do NOT invoke `connect`. Do NOT proceed to Step 1.
|
package/stacks/modeling-kit/templates/.claude/skills/place-element/references/api-fallback.md
CHANGED
|
@@ -172,17 +172,17 @@ Placing an EVENT called "Order Placed" at the end of a timeline, over curl:
|
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
174
|
# 1. Add a column (append at end)
|
|
175
|
-
curl -s -X POST "
|
|
176
|
-
-H "Content-Type: application/json" \
|
|
175
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/timelines/<TIMELINE_ID>/columns" \
|
|
176
|
+
-H "x-token: $TOKEN" -H "Content-Type: application/json" \
|
|
177
177
|
-d '{}'
|
|
178
178
|
|
|
179
179
|
# 2. Fetch chapter to find the target lane cell for the new column
|
|
180
|
-
curl -s -H "x-user-id: place-element-skill" \
|
|
181
|
-
"
|
|
180
|
+
curl -s -H "x-token: $TOKEN" -H "x-user-id: place-element-skill" \
|
|
181
|
+
"$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/<TIMELINE_ID>?projection=cells"
|
|
182
182
|
|
|
183
183
|
# 3. Create the EVENT node — do not skip the x-user-id header
|
|
184
|
-
curl -s -X POST "
|
|
185
|
-
-H "Content-Type: application/json" \
|
|
184
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
|
|
185
|
+
-H "x-token: $TOKEN" -H "Content-Type: application/json" \
|
|
186
186
|
-H "x-user-id: place-element-skill" \
|
|
187
187
|
-d '[{
|
|
188
188
|
"id": "<event-uuid>",
|
|
@@ -144,7 +144,7 @@ mention it in the `DONE` comment, and leave it for a self-directed turn (or for
|
|
|
144
144
|
And it applies **downwards**, to any subagent you dispatch. A subagent is a fresh session that inherits none of this one's state, so hand it `token=`, `org=`, `baseUrl=` and `board=` inline as already-resolved values (the token exactly as the header gave it, `token=$EVENTMODELERS_TOKEN` — the reference, never its value; the subagent inherits the same environment) and tell it explicitly not to invoke `/connect`: all four inline satisfy that skill outright at its Step 0. When this turn has a `CHAT_SESSION_ID` (step 3), hand that down too, as `session=` — the subagent's writes are part of the same chat's work. Three agents that each resolve and verify the same credentials pay for the connect you already did, three more times over.
|
|
145
145
|
|
|
146
146
|
The same "don't reload what's already loaded" logic applies to `/learn-eventmodelers-api`: it's a lookup reference, not a mandatory preamble. Every skill already documents the exact API calls it needs inline — only invoke `/learn-eventmodelers-api` on demand, for a specific endpoint/field/type a skill's own instructions don't cover, and only once per session even then.
|
|
147
|
-
3. **Resolve `BOARD_ID`** from this turn's `board_id` field; if absent, fall back to `
|
|
147
|
+
3. **Resolve `BOARD_ID`** from this turn's `board_id` field; if absent, fall back to `$BOARD_ID` (set in your environment from the connected board's config).
|
|
148
148
|
**Resolve `TIMELINE_ID`** from this turn's `context.timelineId`, if present and non-null; otherwise use this turn's `timeline_id` field. `context.timelineId` reflects the chapter the user was actually pointing at on the canvas (a selected cell or node) when they issued the prompt, which can differ from `timeline_id` — the chapter the voice/prompt session happened to be scoped to — so it wins whenever both are present.
|
|
149
149
|
**Resolve `NODE_ID`** from the first entry of this turn's `context.selectedNodes`, if that array is present and non-empty; otherwise use this turn's `node_id` field. `context.selectedNodes` reflects what was actually selected on the canvas when the prompt was issued, which can differ from `node_id` — set only when the prompt originated from a specific node/comment — so it wins whenever both are present.
|
|
150
150
|
**Resolve `CELL_ID`** from this turn's `context.selectedCell.id`, if present and non-null. When present, it overrules any cell reference (e.g. `"A2"`) parsed from the prompt text itself — it reflects the actual cell the user had selected on the canvas when they issued the prompt, and is more reliable than free-text parsing.
|
|
@@ -173,15 +173,17 @@ mention it in the `DONE` comment, and leave it for a self-directed turn (or for
|
|
|
173
173
|
You run locked down, because chat messages and snippet clicks reach you from any collaborator on the board. A tool
|
|
174
174
|
call that isn't allowed is refused at once — nobody is asked — and the refusal says why; don't retry it in another
|
|
175
175
|
form, do the work another way or tell the person what you can't do.
|
|
176
|
-
- **MCP:** only the eventmodelers server (`mcp__eventmodelers__*`) — prefer it for every board read and write
|
|
176
|
+
- **MCP:** only the eventmodelers server (`mcp__eventmodelers__*`) — prefer it for every board read and write — plus
|
|
177
|
+
a Playwright browser (`mcp__playwright__*`) when the agent was started with `--browser`.
|
|
177
178
|
- **Bash:** only `curl` to the platform (`$BASE_URL` — set for you, like `$ORG_ID`, `$BOARD_ID` and `$TOKEN`), with
|
|
178
|
-
`$TOKEN` / `$EVENTMODELERS_TOKEN` only in a `-H` header; uuids with `uuidgen
|
|
179
|
-
`
|
|
180
|
-
`cat`/`head`/`tail`/`wc`/`sort`/`uniq`/`grep`/`cut`/`tr`/`ls`, `echo`,
|
|
181
|
-
|
|
179
|
+
`$TOKEN` / `$EVENTMODELERS_TOKEN` only in a `-H` header; uuids with `uuidgen`, `$(uuidgen)`,
|
|
180
|
+
`for i in $(seq <n>); do uuidgen; done` or `python3 -c` using only `uuid.uuid4()`; `jq`; `python3 -m json.tool`;
|
|
181
|
+
read-only `git` (`git -C <code dir> …`); `cat`/`head`/`tail`/`wc`/`sort`/`uniq`/`grep`/`cut`/`tr`/`ls`, `echo`,
|
|
182
|
+
`printf`, `mkdir`; `for NAME in …; do …; done` loops over these. No other `$(…)`, no `while`/`if`, other `python3`
|
|
183
|
+
scripts, `node`, `rm`, `cd`. Build a JSON payload with the Write tool in `/tmp`, then
|
|
182
184
|
`curl … --data-binary @/tmp/<file>.json`.
|
|
183
185
|
- **Files:** read and write in this directory and `/tmp`, read the code directory. Never `.claude/`, `.mcp.json`,
|
|
184
|
-
`.eventmodelers
|
|
186
|
+
`.eventmodelers/*.json` or `.env` files (`.eventmodelers/interviews/` is fine). No web access.
|
|
185
187
|
|
|
186
188
|
## Chat turns — `CHAT`
|
|
187
189
|
|