@eventmodelers/cli 1.0.90 â 1.0.92
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 +2 -0
- package/RELEASE_NOTES.md +3 -0
- package/cli.js +308 -23
- package/lib/update-check.js +67 -0
- package/package.json +4 -1
- package/shared/build-kit/lib/adapters/pocketbase-realtime-adapter.js +11 -3
- package/shared/build-kit/lib/adapters/realtime-adapter.js +3 -1
- package/shared/build-kit/lib/adapters/supabase-realtime-adapter.js +14 -3
- package/shared/build-kit/lib/ralph.js +136 -59
- package/shared/build-kit/lib/redact.js +36 -0
- package/shared/build-kit/lib/turn.js +56 -20
- package/shared/build-kit/ralph-claude.js +7 -19
- package/shared/build-kit/ralph-exec.js +5 -9
- package/shared/build-kit/realtime-agent.js +4 -2
- package/shared/skills/connect/SKILL.md +14 -9
- package/shared/skills/learn-eventmodelers-api/SKILL.md +61 -24
- package/shared/skills/load-slice/SKILL.md +3 -1
- package/stacks/modeling-kit/templates/.claude/skills/add-next-slice/SKILL.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +18 -9
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/references/api-fallback.md +31 -11
- package/stacks/modeling-kit/templates/.claude/skills/attributes/SKILL.md +16 -13
- package/stacks/modeling-kit/templates/.claude/skills/attributes/references/api-fallback.md +3 -1
- package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/SKILL.md +6 -6
- package/stacks/modeling-kit/templates/.claude/skills/discover-storyboard/references/api-fallback.md +3 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/references/api-fallback.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/SKILL.md +8 -12
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-checking-completeness/references/api-fallback.md +16 -6
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +3 -3
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/references/api-fallback.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-inputs/references/api-fallback.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/references/api-fallback.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +3 -3
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/references/api-fallback.md +3 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/references/api-fallback.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/SKILL.md +3 -3
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-slicing-event-models/references/api-fallback.md +12 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +7 -6
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/references/api-fallback.md +4 -3
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +22 -6
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/references/api-fallback.md +12 -3
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +18 -6
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/references/api-fallback.md +12 -3
- package/stacks/modeling-kit/templates/.claude/skills/examples/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/examples/references/api-fallback.md +20 -14
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +12 -12
- package/stacks/modeling-kit/templates/.claude/skills/place-element/references/api-fallback.md +6 -4
- package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/storyboard/references/api-fallback.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/timeline/SKILL.md +10 -10
- package/stacks/modeling-kit/templates/.claude/skills/timeline/references/api-fallback.md +4 -4
- package/stacks/modeling-kit/templates/.claude/skills/wdyt/SKILL.md +11 -24
- package/stacks/modeling-kit/templates/kit/AGENTS.md +3 -3
- package/stacks/modeling-kit/templates/kit/CLAUDE-STANDALONE.md +56 -10
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +110 -44
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +106 -102
- package/stacks/react/templates/build-kit/lib/prompt.md +102 -106
package/README.md
CHANGED
|
@@ -635,6 +635,8 @@ npx @eventmodelers/cli re-init --stack supabase # override which stack to r
|
|
|
635
635
|
|
|
636
636
|
`re-init` re-runs `init` against whichever stack `install-manifest.json` says was installed (no need to pass `--stack` again), but skips step 2 of `init` entirely â the root project scaffold (`package.json`, `src/`, `server.ts`, `docker-compose.yml`, etc.) and the root `CLAUDE.md` router are never touched. Use it after upgrading the CLI to pick up fixes to `ralph.js`/`ralph.sh`/skills without re-scaffolding a project you've since built on top of.
|
|
637
637
|
|
|
638
|
+
You don't have to remember to: on startup every command prints a đĄ hint (on stderr) when a newer CLI is on npm, or when a kit in the current directory was installed by an older CLI than the one running â naming the exact `re-init` command to run. The npm lookup happens at most once a day (cached in `~/.eventmodelers/update-check.json`, 1.5 s timeout, silent when offline). Set `EVENTMODELERS_NO_UPDATE_CHECK=1` to turn it off; it is also off when `CI` is set.
|
|
639
|
+
|
|
638
640
|
Pass `--stack <name>` to override the stack instead of relying on the manifest â useful if the manifest is missing/stale, or you want to point a `.build-kit` install at a different built-in stack's templates. It's mutually exclusive with `--modeling`.
|
|
639
641
|
|
|
640
642
|
Credentials are left alone unless you pass `--force` â same rule `init` already follows when everything required is already configured. `--global` defaults to however skills were originally installed; pass it explicitly to move them.
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
- The `anthropicBaseUrl` option no longer implies Ollama works behind it. That route keeps Claude Code and swaps only the endpoint, so the server has to speak Anthropic's own `/v1/messages`; Ollama does not, and pointing it at `localhost:11434` returns a 404. For Ollama, `--local-ai` is the supported route, and the README now says which of the two to reach for.
|
|
10
10
|
- `LOCAL_AI_*` is documented for the first time â the `--local-ai` help text had been pointing at "the docs" for vars that appeared nowhere in the README.
|
|
11
11
|
|
|
12
|
+
### Fixes
|
|
13
|
+
- Chat messages are limited to 1000 characters, in both directions â the platform now rejects longer ones (`CHAT_TEXT_TOO_LONG`). The kit instructions say so, and the CLI's fallback reply (a chat turn's final text, posted when the turn didn't reply itself) is cut at the limit instead of failing with a 400 and leaving the message unanswered.
|
|
14
|
+
|
|
12
15
|
## v1.0.72
|
|
13
16
|
|
|
14
17
|
### Features
|
package/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { randomUUID } from 'crypto';
|
|
|
21
21
|
import { runFetch, FetchAuthError } from './lib/fetch.js';
|
|
22
22
|
import { createModelingLocalAiRunner } from './lib/modeling-local-ai.js';
|
|
23
23
|
import { run as runSpecKittyAdapter } from './lib/adapters/spec-kitty-adapter.js';
|
|
24
|
+
import { latestPublishedVersion, updateHints } from './lib/update-check.js';
|
|
24
25
|
// Not a root-level adapter like spec-kitty-adapter.js above: this is the one canonical
|
|
25
26
|
// copy that every useShared:true stack also gets copied into its installed kit (see
|
|
26
27
|
// copyDirContents in installStack) for ralph.js to import standalone â see
|
|
@@ -1282,6 +1283,11 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
|
|
|
1282
1283
|
async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
|
|
1283
1284
|
console.log(`đĻ Refreshing the shared runtime in ${relative(targetDir, kitDir)}/ (stack files untouched)...`);
|
|
1284
1285
|
copyDirContents(join(__dirname, 'shared', 'build-kit'), kitDir);
|
|
1286
|
+
// The kit is now this CLI's runtime â record that, so the startup "kit is outdated" hint stops.
|
|
1287
|
+
const manifestPath = join(kitDir, '.eventmodelers', 'install-manifest.json');
|
|
1288
|
+
if (existsSync(manifestPath)) {
|
|
1289
|
+
writeFileSync(manifestPath, JSON.stringify({ ...readJsonSafe(manifestPath), version: CLI_VERSION }, null, 2));
|
|
1290
|
+
}
|
|
1285
1291
|
for (const script of ['ralph.sh', 'lib/agent.sh', 'ralph-claude.js', 'ralph-local-ai.js', 'ralph-exec.js']) {
|
|
1286
1292
|
const p = join(kitDir, script);
|
|
1287
1293
|
if (existsSync(p)) {
|
|
@@ -1861,7 +1867,9 @@ async function ensureGlobalKit(baseUrl) {
|
|
|
1861
1867
|
// untargeted is handed straight back to the queue for another agent to take. It says
|
|
1862
1868
|
// nothing about the standalone lane â a self-directed turn is nobody's task, so an
|
|
1863
1869
|
// exclusive standalone agent still works the board on its own initiative.
|
|
1864
|
-
async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null } = {}) {
|
|
1870
|
+
async function runModeling(kitDir, projectDir, { verbose = false, standalone = false, exclusive = false, worker = false, overrides = null, maxAgents = DEFAULT_MAX_AGENTS, identity = {}, localAi = null } = {}) {
|
|
1871
|
+
// --worker: only prompts â no chat, and no board-change turns (it has nowhere to propose).
|
|
1872
|
+
const chat = !worker;
|
|
1865
1873
|
const configLibPath = join(kitDir, 'lib', 'config.js');
|
|
1866
1874
|
if (!existsSync(configLibPath)) {
|
|
1867
1875
|
console.error(`â ${relative(process.cwd(), configLibPath)} not found â --modeling needs a kit installed via \`init --modeling\`.`);
|
|
@@ -1931,7 +1939,10 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
1931
1939
|
'IMPORTANT: You are running autonomously â no human is available to answer questions. ' +
|
|
1932
1940
|
'If you need clarification to proceed, do NOT pause or ask interactively. Instead, post your question ' +
|
|
1933
1941
|
'as a QUESTION-type comment (via /handle-comment with action=place and type=QUESTION) on the most ' +
|
|
1934
|
-
'relevant slice or column node on the board, then continue with your best interpretation of the prompt
|
|
1942
|
+
'relevant slice or column node on the board, then continue with your best interpretation of the prompt. ' +
|
|
1943
|
+
'The one exception is a CHAT turn: there a person is waiting in the chat, so you ask back with post_chat_message ' +
|
|
1944
|
+
'(see "Chat turns" in .agent-modeling-kit/CLAUDE.md), and never by pausing the session. ' +
|
|
1945
|
+
'A chat message is at most 1000 characters.\n\n';
|
|
1935
1946
|
|
|
1936
1947
|
// Sent once, on the first turn only â it's what tells the agent to follow
|
|
1937
1948
|
// .agent-modeling-kit/CLAUDE.md's per-turn steps for this warm session (instead
|
|
@@ -1943,13 +1954,16 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
1943
1954
|
// board-change turn can just as well be the first turn a (re)spawned process
|
|
1944
1955
|
// ever sees, so both turn builders go through this rather than buildTurn owning it.
|
|
1945
1956
|
function withSessionHeader(body) {
|
|
1957
|
+
// token= names the env var, never its value: claudeEnv already carries EVENTMODELERS_TOKEN,
|
|
1958
|
+
// and a literal token here would sit in every session transcript (the connect skill treats
|
|
1959
|
+
// the reference as an inline token â see its Step 0).
|
|
1946
1960
|
// Nothing to prepend for a local model: it has no CLAUDE.md to read and no /connect to run
|
|
1947
1961
|
// (its tools are already authenticated), and handing it the raw token would put credentials
|
|
1948
1962
|
// in a context that has no way to use them.
|
|
1949
1963
|
if (localRunner) return body;
|
|
1950
1964
|
if (!firstTurn) return body;
|
|
1951
1965
|
firstTurn = false;
|
|
1952
|
-
return `MODE=modeling token=$
|
|
1966
|
+
return `MODE=modeling token=$EVENTMODELERS_TOKEN org=${cfg.organizationId} baseUrl=${cfg.baseUrl} standalone=${standalone ? 'on' : 'off'}${standalone ? ` max_agents=${maxAgents}` : ''} subagent_model=${subagentModel}\n\n${QUESTIONING_RULE}Read .agent-modeling-kit/CLAUDE.md now and follow it for every prompt in this session â it's a one-time read; don't re-read it on later turns.\n\n${body}`;
|
|
1953
1967
|
}
|
|
1954
1968
|
|
|
1955
1969
|
function buildTurn(p) {
|
|
@@ -1960,13 +1974,15 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
1960
1974
|
p.timeline_id ? `timeline_id=${p.timeline_id}` : null,
|
|
1961
1975
|
p.comment_id ? `comment_id=${p.comment_id}` : null,
|
|
1962
1976
|
p.node_id ? `node_id=${p.node_id}` : null,
|
|
1977
|
+
// Work the agent created out of a chat message: where to report back when it's done.
|
|
1978
|
+
p.origin_session_id ? `origin_session_id=${p.origin_session_id}` : null,
|
|
1979
|
+
p.origin_message_id ? `origin_message_id=${p.origin_message_id}` : null,
|
|
1963
1980
|
].filter(Boolean).join(' ');
|
|
1964
1981
|
// What the user had selected and on screen when they submitted (selectedCell,
|
|
1965
1982
|
// selectedNodes, timelineId, focusArea). CLAUDE.md's step 3 resolves CELL_ID/NODE_ID/
|
|
1966
|
-
// TIMELINE_ID from it in preference to the flat fields above, and
|
|
1967
|
-
//
|
|
1968
|
-
// the
|
|
1969
|
-
// it is structured, unlike the flat k=v fields.
|
|
1983
|
+
// TIMELINE_ID from it in preference to the flat fields above, and it is what "this" or
|
|
1984
|
+
// "here" in the prompt refers to. Sent as JSON on its own line because it is structured,
|
|
1985
|
+
// unlike the flat k=v fields.
|
|
1970
1986
|
const context = p.context && typeof p.context === 'object' && Object.keys(p.context).length
|
|
1971
1987
|
? `\ncontext=${JSON.stringify(p.context)}`
|
|
1972
1988
|
: '';
|
|
@@ -1987,6 +2003,21 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
1987
2003
|
let proc = null;
|
|
1988
2004
|
let stdoutBuffer = '';
|
|
1989
2005
|
let pending = null; // one in-flight turn at a time
|
|
2006
|
+
// The last message of each session this process has handed to Claude. A session the process has
|
|
2007
|
+
// never seen is read whole; after that only what came since â each message enters the warm
|
|
2008
|
+
// session's context once. Cleared whenever the process is (re)spawned: a fresh process has read
|
|
2009
|
+
// nothing.
|
|
2010
|
+
const chatCursor = new Map();
|
|
2011
|
+
// --modeling: the people this agent has taken a chat message from â the only ones whose board
|
|
2012
|
+
// edits it proposes on. Everyone else's edits cost no turn: an agent one person started must not
|
|
2013
|
+
// spend their money reacting to the whole board, nor write to people who never talked to it. Kept
|
|
2014
|
+
// across respawns (it is about people, not the Claude process), lost when the CLI restarts.
|
|
2015
|
+
const chatPartners = new Set();
|
|
2016
|
+
// When each partner was last sent a proposal. The backend lets an unanswered proposal stand for
|
|
2017
|
+
// UNPROMPTED_COOLDOWN_MS before another; waiting here keeps a turn from being spent on one it
|
|
2018
|
+
// would refuse. Cleared when the person writes in the chat.
|
|
2019
|
+
const lastProposalAt = new Map();
|
|
2020
|
+
|
|
1990
2021
|
let lastTurnEndedAt = 0; // when the last turn finished â the standalone lane's echo window (see below)
|
|
1991
2022
|
let warmUp = null; // this process's session warm-up turn (see warmUpSession) â null until one is started
|
|
1992
2023
|
let warmingUp = false; // the in-flight turn is the warm-up: it only reads, so its writes can't echo
|
|
@@ -2056,6 +2087,7 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2056
2087
|
proc = spawn('claude', claudeArgs, { cwd: projectDir, env: claudeEnv, stdio: ['pipe', 'pipe', 'inherit'] });
|
|
2057
2088
|
stdoutBuffer = '';
|
|
2058
2089
|
warmUp = null; // a fresh process has connected to nothing and read nothing
|
|
2090
|
+
chatCursor.clear(); // âĻand has read no chat session either
|
|
2059
2091
|
proc.stdout.on('data', (chunk) => {
|
|
2060
2092
|
stdoutBuffer += chunk.toString();
|
|
2061
2093
|
const lines = stdoutBuffer.split('\n');
|
|
@@ -2154,8 +2186,11 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2154
2186
|
log(
|
|
2155
2187
|
standalone
|
|
2156
2188
|
? `standalone: ON â reacting to direct prompts AND to board changes on its own initiative (${localRunner ? 'work is done inline â a local model has no subagents' : `max ${maxAgents} subagent(s) per self-directed turn`})`
|
|
2157
|
-
:
|
|
2189
|
+
: worker
|
|
2190
|
+
? 'worker: ON â working prompts only (no chat, board changes are dropped)'
|
|
2191
|
+
: 'standalone: off â board changes get a proposal in the chat, nothing is changed without a yes',
|
|
2158
2192
|
);
|
|
2193
|
+
log(chat ? 'chat: ON â answering chat messages addressed to this agent' : 'chat: off (--worker)');
|
|
2159
2194
|
if (exclusive) {
|
|
2160
2195
|
log(`exclusive: ON â only prompts addressed to ${cfg.agentId} are worked; every untargeted prompt is handed back to the queue`);
|
|
2161
2196
|
// A global/standalone run mints its id per run (see resolveModelingCredentials), so an id
|
|
@@ -2198,6 +2233,92 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2198
2233
|
if (!res.ok) throw new Error(`prompts/${promptId}/status: HTTP ${res.status}`);
|
|
2199
2234
|
}
|
|
2200
2235
|
|
|
2236
|
+
// ââ Chat âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
2237
|
+
//
|
|
2238
|
+
// Chat messages are conversation, not work: a person talks to this agent in the board's chat
|
|
2239
|
+
// panel, and each message addressed to it is claimed here (`chat/next` flips it to picked_up)
|
|
2240
|
+
// and handed to Claude as a CHAT turn. Claude decides â answer, ask back, or create prompts for
|
|
2241
|
+
// the work â and replies with post_chat_message; any prompts it creates are addressed to this
|
|
2242
|
+
// agent and worked by the same drain pass right after. Off with --worker.
|
|
2243
|
+
async function fetchNextChatMessage(jwtToken) {
|
|
2244
|
+
try {
|
|
2245
|
+
const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/chat/next?board_id=${encodeURIComponent(cfg.boardId)}`, {
|
|
2246
|
+
headers: { 'x-token': cfg.token, Authorization: `Bearer ${jwtToken}`, ...agentHeaders(cfg) },
|
|
2247
|
+
});
|
|
2248
|
+
// 404 is "nothing waiting" â and also what a backend without chat answers, which is fine.
|
|
2249
|
+
if (res.status === 404) return null;
|
|
2250
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2251
|
+
return res.json();
|
|
2252
|
+
} catch (err) {
|
|
2253
|
+
// Never let chat take the prompt queue down with it: log, skip chat for this pass.
|
|
2254
|
+
log(`chat/next failed, skipping chat this pass: ${err.message}`);
|
|
2255
|
+
return null;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
function buildChatTurn(m) {
|
|
2260
|
+
const after = chatCursor.get(m.sessionId);
|
|
2261
|
+
const fields = [
|
|
2262
|
+
`CHAT message_id=${m.id}`,
|
|
2263
|
+
`session_id=${m.sessionId}`,
|
|
2264
|
+
`board_id=${cfg.boardId}`,
|
|
2265
|
+
`organization_id=${cfg.organizationId}`,
|
|
2266
|
+
after ? `after_message_id=${after}` : 'first_read=true',
|
|
2267
|
+
m.nodeId ? `node_id=${m.nodeId}` : null,
|
|
2268
|
+
m.commentId ? `comment_id=${m.commentId}` : null,
|
|
2269
|
+
].filter(Boolean).join(' ');
|
|
2270
|
+
const context = m.context && typeof m.context === 'object' && Object.keys(m.context).length
|
|
2271
|
+
? `\ncontext=${JSON.stringify(m.context)}`
|
|
2272
|
+
: '';
|
|
2273
|
+
return withSessionHeader(`${fields}${context}\n\n${m.text}`);
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// The backend rejects a chat message over this many characters (CHAT_TEXT_TOO_LONG).
|
|
2277
|
+
const CHAT_TEXT_MAX_LENGTH = 1000;
|
|
2278
|
+
|
|
2279
|
+
// Posted only if the turn didn't answer â the backend checks (only_if_unanswered), so a reply
|
|
2280
|
+
// the agent did post is never doubled. What makes "every message gets an answer" hold even for a
|
|
2281
|
+
// turn that errored or forgot its post_chat_message.
|
|
2282
|
+
async function replyIfUnanswered(m, text) {
|
|
2283
|
+
try {
|
|
2284
|
+
const res = await fetch(`${cfg.baseUrl}/api/org/${cfg.organizationId}/boards/${encodeURIComponent(cfg.boardId)}/chat/agent-messages`, {
|
|
2285
|
+
method: 'POST',
|
|
2286
|
+
headers: { 'x-token': cfg.token, 'Content-Type': 'application/json', ...agentHeaders(cfg) },
|
|
2287
|
+
// A turn's final text can run long; cut it rather than lose the reply to a 400.
|
|
2288
|
+
body: JSON.stringify({
|
|
2289
|
+
text: text.length > CHAT_TEXT_MAX_LENGTH ? `${text.slice(0, CHAT_TEXT_MAX_LENGTH - 1)}âĻ` : text,
|
|
2290
|
+
reply_to: m.id,
|
|
2291
|
+
only_if_unanswered: true,
|
|
2292
|
+
}),
|
|
2293
|
+
});
|
|
2294
|
+
if (!res.ok) log(`chat fallback reply failed: HTTP ${res.status}`);
|
|
2295
|
+
else if (res.status === 201) log(`chat turn left message ${m.id} unanswered â posted its final text as the reply`);
|
|
2296
|
+
} catch (err) {
|
|
2297
|
+
log(`chat fallback reply failed: ${err.message}`);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
async function runChatTurn(m) {
|
|
2302
|
+
log(`chat message received: "${oneLine(m.text, 80)}" (session=${m.sessionId})`);
|
|
2303
|
+
if (m.userId) {
|
|
2304
|
+
chatPartners.add(m.userId);
|
|
2305
|
+
lastProposalAt.delete(m.userId);
|
|
2306
|
+
}
|
|
2307
|
+
let result = '';
|
|
2308
|
+
let failed = false;
|
|
2309
|
+
try {
|
|
2310
|
+
result = await runTurn(buildChatTurn(m));
|
|
2311
|
+
} catch (err) {
|
|
2312
|
+
failed = true;
|
|
2313
|
+
log(`chat turn failed: ${err.message}`);
|
|
2314
|
+
}
|
|
2315
|
+
chatCursor.set(m.sessionId, m.id);
|
|
2316
|
+
const text = String(result ?? '').replace(/<promise>[^<]*<\/promise>/g, '').trim();
|
|
2317
|
+
await replyIfUnanswered(m, text && !failed ? text : 'Sorry â I could not finish that just now. Please try again.');
|
|
2318
|
+
// Edits they made while their last proposal waited for this answer get their turn now.
|
|
2319
|
+
if (m.userId && proposalBuffer.has(m.userId)) armProposalTurn(STANDALONE_DEBOUNCE_MS);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2201
2322
|
let realtimeToken = await getRealtimeToken();
|
|
2202
2323
|
|
|
2203
2324
|
let draining = false;
|
|
@@ -2208,8 +2329,16 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2208
2329
|
// handed back once the pass is over (see below).
|
|
2209
2330
|
const handBack = [];
|
|
2210
2331
|
try {
|
|
2211
|
-
|
|
2212
|
-
|
|
2332
|
+
for (;;) {
|
|
2333
|
+
// A person waiting in the chat goes first; a prompt the chat turn created is picked up
|
|
2334
|
+
// by the fetch below on this same pass.
|
|
2335
|
+
const m = chat ? await fetchNextChatMessage(realtimeToken) : null;
|
|
2336
|
+
if (m) {
|
|
2337
|
+
await runChatTurn(m);
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
const p = await fetchNextPrompt(realtimeToken);
|
|
2341
|
+
if (p === null) break;
|
|
2213
2342
|
// The queue can't filter by addressee for us: `prompts/next` hands an agent both the
|
|
2214
2343
|
// prompts addressed to it and every untargeted one (`agent_id IS NULL`) â claiming is
|
|
2215
2344
|
// what reveals which kind arrived â so an exclusive run claims as usual and gives back
|
|
@@ -2323,8 +2452,18 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2323
2452
|
// session rather than by another agent. It is what lets a human's edit skip the
|
|
2324
2453
|
// MIN_INTERVAL floor: a person cannot be this agent's feedback loop, whereas two agents on
|
|
2325
2454
|
// one board can ping-pong, so another agent's write keeps waiting its turn.
|
|
2455
|
+
// Whether board changes get a turn at all, and what that turn may do. --standalone acts on its
|
|
2456
|
+
// own initiative, on everyone's edits. A plain --modeling agent only proposes, and only to the
|
|
2457
|
+
// people talking to it in the chat â about their own edits (the propose lane below). A --worker
|
|
2458
|
+
// takes neither: it only works prompts.
|
|
2459
|
+
const boardLane = standalone || chat;
|
|
2460
|
+
const proposeOnly = !standalone;
|
|
2461
|
+
|
|
2326
2462
|
const observed = new Map();
|
|
2327
2463
|
let observedCount = 0;
|
|
2464
|
+
// --standalone: the person behind the latest human edit in the buffer â who a blocking question
|
|
2465
|
+
// may be put to in the chat.
|
|
2466
|
+
let lastChangedBy = null;
|
|
2328
2467
|
let seqLo = null;
|
|
2329
2468
|
let seqHi = null;
|
|
2330
2469
|
let standaloneTimer = null;
|
|
@@ -2347,11 +2486,12 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2347
2486
|
seqLo = null;
|
|
2348
2487
|
seqHi = null;
|
|
2349
2488
|
firstObservedAt = 0;
|
|
2489
|
+
lastChangedBy = null;
|
|
2350
2490
|
}
|
|
2351
2491
|
|
|
2352
2492
|
function onBoardEvent(type, payload) {
|
|
2353
|
-
if (!
|
|
2354
|
-
if (verbose) log(`board event ${type} dropped â
|
|
2493
|
+
if (!boardLane) {
|
|
2494
|
+
if (verbose) log(`board event ${type} dropped â a --worker only works prompts`);
|
|
2355
2495
|
return;
|
|
2356
2496
|
}
|
|
2357
2497
|
const sinceTurn = Date.now() - lastTurnEndedAt;
|
|
@@ -2366,6 +2506,10 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2366
2506
|
// editing while this agent worked had their change written off as an echo of it.
|
|
2367
2507
|
const writerAgent = payload?.agent_id || null;
|
|
2368
2508
|
const writerUser = payload?.user_id || null;
|
|
2509
|
+
if (proposeOnly) {
|
|
2510
|
+
onProposableEvent(type, payload, writerUser, writerAgent);
|
|
2511
|
+
return;
|
|
2512
|
+
}
|
|
2369
2513
|
const origin =
|
|
2370
2514
|
writerAgent && cfg.agentId && writerAgent === cfg.agentId
|
|
2371
2515
|
? 'own'
|
|
@@ -2380,7 +2524,10 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2380
2524
|
entry.count += 1;
|
|
2381
2525
|
entry[origin] += 1;
|
|
2382
2526
|
// A browser session id and no agent id: a human at the canvas.
|
|
2383
|
-
if (writerUser && !writerAgent)
|
|
2527
|
+
if (writerUser && !writerAgent) {
|
|
2528
|
+
entry.person += 1;
|
|
2529
|
+
lastChangedBy = writerUser;
|
|
2530
|
+
}
|
|
2384
2531
|
observed.set(nodeId, entry);
|
|
2385
2532
|
observedCount += 1;
|
|
2386
2533
|
if (!firstObservedAt) firstObservedAt = Date.now();
|
|
@@ -2474,6 +2621,111 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2474
2621
|
`actually needs doing, and then ${FAN_OUT} If the model genuinely needs nothing right now, spawn ` +
|
|
2475
2622
|
'nothing, change nothing and reply <promise>NOOP</promise>.';
|
|
2476
2623
|
|
|
2624
|
+
// --modeling: the same judgement as a standalone turn, but it changes nothing â it puts what it
|
|
2625
|
+
// would do to the person in the chat and waits. A "yes" comes back as a CHAT turn, which is where
|
|
2626
|
+
// the work is actually created (as prompts).
|
|
2627
|
+
const PROPOSE_TASK =
|
|
2628
|
+
'Nobody asked you for this â the board just changed, and you are a modeling agent that proposes rather ' +
|
|
2629
|
+
'than acts: you may NOT change the board in this turn, and you dispatch no Agents. The change list above ' +
|
|
2630
|
+
'is a notification, not the task. Judge the changed areas in their context (slice, chain, timeline) the ' +
|
|
2631
|
+
'way .agent-modeling-kit/CLAUDE-STANDALONE.md describes â read it now if you have not this session, and ' +
|
|
2632
|
+
'follow its "Propose mode" section. Read what this needs and no more: every nodeId above in one get_nodes, ' +
|
|
2633
|
+
'plus one get_board_outline per chapter you have not read this session. Every change above is by one person, ' +
|
|
2634
|
+
'changed_by, who is talking to you in the chat. ALWAYS post ONE short chat message to them with ' +
|
|
2635
|
+
'post_chat_message (no sessionId, no replyTo, preferUserId = changed_by; at most 1000 characters) â never stay silent after their ' +
|
|
2636
|
+
'edit. A fresh, half-finished element (placeholder name, no ' +
|
|
2637
|
+
'fields, names still changing) is the reason to speak, not to wait. Something worth doing â say concretely ' +
|
|
2638
|
+
'what you would do (element names) and ask whether to go ahead. Nothing concrete â say what you saw and ' +
|
|
2639
|
+
'that you are not sure what to do with it, and ask ("You renamed Orders to take â not sure what to do with ' +
|
|
2640
|
+
'it yet. Want fields on it?"). Fold anything your earlier unanswered message proposed into this one instead ' +
|
|
2641
|
+
'of repeating it. One to three sentences. Only CHAT_NO_ACTIVE_SESSION (they cleared the conversation or ' +
|
|
2642
|
+
'switched to another agent) or CHAT_AWAITING_REPLY (you wrote to them less than 3 minutes ago) keep you ' +
|
|
2643
|
+
'silent: then change nothing and reply <promise>NOOP</promise>.';
|
|
2644
|
+
|
|
2645
|
+
// ââ Propose lane (--modeling) âââââââââââââââââââââââââââââââââââââââââââââ
|
|
2646
|
+
//
|
|
2647
|
+
// Per person: what each chat partner changed since their last proposal. Each gets their own turn
|
|
2648
|
+
// and their own proposal, about their own edits only â nobody hears about someone else's work.
|
|
2649
|
+
const PROPOSAL_COOLDOWN_MS = 3 * 60_000; // the backend's UNPROMPTED_COOLDOWN_MS
|
|
2650
|
+
const proposalBuffer = new Map(); // userId -> Map(nodeId -> { types: Set<string>, count })
|
|
2651
|
+
let proposalFirstAt = 0; // start of the current burst â MAX_WAIT is measured from here
|
|
2652
|
+
let proposalTimer = null;
|
|
2653
|
+
|
|
2654
|
+
function onProposableEvent(type, payload, writerUser, writerAgent) {
|
|
2655
|
+
const nodeId = payload?.node_id ?? '(board)';
|
|
2656
|
+
// Only a person at the canvas who is talking to this agent. Agents' writes (this one's
|
|
2657
|
+
// included) and unattributed ones name nobody to propose to.
|
|
2658
|
+
if (!writerUser || writerAgent || !chatPartners.has(writerUser)) {
|
|
2659
|
+
if (verbose) log(`board change: ${type} node=${nodeId} â ignored, not by a person chatting with this agent`);
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
const nodes = proposalBuffer.get(writerUser) ?? new Map();
|
|
2663
|
+
const entry = nodes.get(nodeId) ?? { types: new Set(), count: 0 };
|
|
2664
|
+
entry.types.add(type);
|
|
2665
|
+
entry.count += 1;
|
|
2666
|
+
nodes.set(nodeId, entry);
|
|
2667
|
+
proposalBuffer.set(writerUser, nodes);
|
|
2668
|
+
if (!proposalFirstAt) proposalFirstAt = Date.now();
|
|
2669
|
+
log(`board change: ${type} node=${nodeId} â by a chat partner`);
|
|
2670
|
+
armProposalTurn(Math.max(0, Math.min(STANDALONE_DEBOUNCE_MS, STANDALONE_MAX_WAIT_MS - (Date.now() - proposalFirstAt))));
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
function armProposalTurn(delayMs) {
|
|
2674
|
+
if (proposalTimer) clearTimeout(proposalTimer);
|
|
2675
|
+
proposalTimer = setTimeout(() => {
|
|
2676
|
+
proposalTimer = null;
|
|
2677
|
+
dispatchProposals().catch((err) => log(`proposal dispatch error: ${err.message}`));
|
|
2678
|
+
}, delayMs);
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
function buildProposalTurn(userId, nodes) {
|
|
2682
|
+
const lines = [...nodes.entries()].map(([nodeId, entry]) => `- ${nodeId}: ${[...entry.types].join(', ')} (${entry.count}Ã)`);
|
|
2683
|
+
const events = [...nodes.values()].reduce((sum, entry) => sum + entry.count, 0);
|
|
2684
|
+
const header = [
|
|
2685
|
+
'BOARD_CHANGE',
|
|
2686
|
+
'mode=propose',
|
|
2687
|
+
`changed_by=${userId}`,
|
|
2688
|
+
`board_id=${cfg.boardId}`,
|
|
2689
|
+
`organization_id=${cfg.organizationId}`,
|
|
2690
|
+
`events=${events}`,
|
|
2691
|
+
`nodes=${nodes.size}`,
|
|
2692
|
+
].join(' ');
|
|
2693
|
+
return withSessionHeader(`${header}\nchanged:\n${lines.join('\n')}\n\n${PROPOSE_TASK}`);
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
async function dispatchProposals() {
|
|
2697
|
+
// A direct message always outranks the agent's own initiative.
|
|
2698
|
+
if (pending || draining) {
|
|
2699
|
+
armProposalTurn(STANDALONE_DEBOUNCE_MS);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
const now = Date.now();
|
|
2703
|
+
const due = [];
|
|
2704
|
+
let waitMs = Infinity;
|
|
2705
|
+
for (const [userId, nodes] of proposalBuffer) {
|
|
2706
|
+
// Their last proposal is still unanswered: keep collecting, fold it all into the next one.
|
|
2707
|
+
const left = (lastProposalAt.get(userId) ?? 0) + PROPOSAL_COOLDOWN_MS - now;
|
|
2708
|
+
if (left > 0) {
|
|
2709
|
+
waitMs = Math.min(waitMs, left);
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
due.push([userId, nodes]);
|
|
2713
|
+
proposalBuffer.delete(userId);
|
|
2714
|
+
}
|
|
2715
|
+
if (!proposalBuffer.size) proposalFirstAt = 0;
|
|
2716
|
+
for (const [userId, nodes] of due) {
|
|
2717
|
+
log(`propose turn: ${nodes.size} node(s) changed by ${userId.slice(0, 8)}`);
|
|
2718
|
+
lastProposalAt.set(userId, Date.now());
|
|
2719
|
+
try {
|
|
2720
|
+
await runTurn(buildProposalTurn(userId, nodes));
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
log(`propose turn failed: ${err.message}`);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
// Still waiting out a cooldown, or edits that arrived while these turns ran.
|
|
2726
|
+
if (proposalBuffer.size) armProposalTurn(Number.isFinite(waitMs) && !due.length ? waitMs : STANDALONE_DEBOUNCE_MS);
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2477
2729
|
function buildStandaloneTurn() {
|
|
2478
2730
|
const lines = [...observed.entries()].map(([nodeId, entry]) => {
|
|
2479
2731
|
const origin =
|
|
@@ -2488,6 +2740,8 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2488
2740
|
});
|
|
2489
2741
|
const header = [
|
|
2490
2742
|
'BOARD_CHANGE',
|
|
2743
|
+
'mode=act',
|
|
2744
|
+
lastChangedBy ? `changed_by=${lastChangedBy}` : null,
|
|
2491
2745
|
`board_id=${cfg.boardId}`,
|
|
2492
2746
|
`organization_id=${cfg.organizationId}`,
|
|
2493
2747
|
seqLo !== null ? `seq=${seqLo}${seqHi !== seqLo ? `..${seqHi}` : ''}` : null,
|
|
@@ -2650,6 +2904,12 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2650
2904
|
'prompt:created': () => {
|
|
2651
2905
|
drain().catch((err) => log(`drain error: ${err.message}`));
|
|
2652
2906
|
},
|
|
2907
|
+
// Sent for every chat message on the org; only one addressed to this agent on this board
|
|
2908
|
+
// is worth a drain pass.
|
|
2909
|
+
'chat:created': (payload) => {
|
|
2910
|
+
if (!chat || payload?.agent_id !== cfg.agentId || payload?.board_id !== cfg.boardId) return;
|
|
2911
|
+
drain().catch((err) => log(`drain error: ${err.message}`));
|
|
2912
|
+
},
|
|
2653
2913
|
},
|
|
2654
2914
|
(status) => {
|
|
2655
2915
|
log(`channel "${channelName}": ${status}`);
|
|
@@ -2670,7 +2930,7 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2670
2930
|
boardChannelName,
|
|
2671
2931
|
Object.fromEntries(BOARD_CHANGE_EVENTS.map((event) => [event, (payload) => onBoardEvent(event, payload)])),
|
|
2672
2932
|
(status) => {
|
|
2673
|
-
log(`channel "${boardChannelName}": ${status}${
|
|
2933
|
+
log(`channel "${boardChannelName}": ${status}${boardLane ? '' : ' (events dropped â --worker)'}`);
|
|
2674
2934
|
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
|
|
2675
2935
|
refreshRealtimeToken(status).catch(() => {});
|
|
2676
2936
|
}
|
|
@@ -2688,7 +2948,10 @@ async function runModeling(kitDir, projectDir, { verbose = false, standalone = f
|
|
|
2688
2948
|
const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
|
|
2689
2949
|
method: 'POST',
|
|
2690
2950
|
headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
|
|
2691
|
-
|
|
2951
|
+
// chat_enabled tells the board this agent answers chat messages (the panel only offers chat
|
|
2952
|
+
// with one that does); a --worker reports false, and older CLIs send nothing, which the
|
|
2953
|
+
// backend reads as false too.
|
|
2954
|
+
body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: 'MODELING', agent_id: cfg.agentId, chat_enabled: chat, ...(cfg.agentName ? { agent_name: cfg.agentName } : {}) }),
|
|
2692
2955
|
signal: AbortSignal.timeout(10_000),
|
|
2693
2956
|
});
|
|
2694
2957
|
if (!res.ok) log(`ping failed: ${res.status} ${await res.text().catch(() => '')}`);
|
|
@@ -2705,7 +2968,7 @@ const program = new Command();
|
|
|
2705
2968
|
program
|
|
2706
2969
|
.name('eventmodelers')
|
|
2707
2970
|
.description('Eventmodelers CLI â real-time Claude agent + skills for Claude Code, for any stack')
|
|
2708
|
-
.version(
|
|
2971
|
+
.version(CLI_VERSION)
|
|
2709
2972
|
.option('--config <path>', 'Path to an explicit config.json, overriding directory-based resolution (individual fields can also be set via EVENTMODELERS_* env vars, which always win)')
|
|
2710
2973
|
.option('--print', 'Print follow-up commands (e.g. claude mcp add) instead of prompting to run them');
|
|
2711
2974
|
|
|
@@ -2725,6 +2988,21 @@ program
|
|
|
2725
2988
|
// than this generic gate can.
|
|
2726
2989
|
const NO_INIT_REQUIRED = new Set(['init', 'init-config', 'stacks', 'status', 'config', 'uninstall', 'fetch', 'activate-context', 'set-slice-status', 'release-notes', 'run']);
|
|
2727
2990
|
|
|
2991
|
+
// Commands that install or refresh the kit themselves â a "your kit is outdated" hint in
|
|
2992
|
+
// front of them would only be noise.
|
|
2993
|
+
const NO_KIT_UPDATE_HINT = new Set(['init', 're-init', 'uninstall']);
|
|
2994
|
+
|
|
2995
|
+
// Startup hint when this CLI is behind npm, or an installed kit here is behind this CLI.
|
|
2996
|
+
// Best effort and on stderr, so it never breaks piped output; EVENTMODELERS_NO_UPDATE_CHECK
|
|
2997
|
+
// (or CI) turns it off.
|
|
2998
|
+
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
2999
|
+
if (process.env.EVENTMODELERS_NO_UPDATE_CHECK || process.env.CI) return;
|
|
3000
|
+
const latestVersion = await latestPublishedVersion({ cachePath: join(GLOBAL_DIR, 'update-check.json') });
|
|
3001
|
+
const kitDirs = NO_KIT_UPDATE_HINT.has(actionCommand.name()) ? [] : findAllInstalledKitDirs(process.cwd());
|
|
3002
|
+
const hints = updateHints({ currentVersion: CLI_VERSION, latestVersion, kitDirs });
|
|
3003
|
+
if (hints.length) console.error(`${hints.join('\n')}\n`);
|
|
3004
|
+
});
|
|
3005
|
+
|
|
2728
3006
|
program.hook('preAction', (_thisCommand, actionCommand) => {
|
|
2729
3007
|
if (NO_INIT_REQUIRED.has(actionCommand.name())) return;
|
|
2730
3008
|
if (findInstalledKitDir(process.cwd())) return;
|
|
@@ -3197,9 +3475,10 @@ credentialFlags(program
|
|
|
3197
3475
|
.option('--exec [command]', 'Hand each prompt to an external agent command instead of the default Claude runner, via ralph-exec.js (build-kit stacks only) â for agentic harnesses that bring their own tool loop, e.g. "codex exec --full-auto" or "opencode run". The prompt is appended as a quoted argument and also written to the file named by RALPH_PROMPT_FILE. Bare --exec uses localAi.exec from .eventmodelers/config.json. Claude remains the default when this flag is absent. With agent tracing on, include the harness\'s JSON output flag (codex exec --json, opencode run --format json, gemini --output-format stream-json) â without it no slice cost is recorded.')
|
|
3198
3476
|
.option('--bash', 'Use the bash-only ralph.sh loop (build-kit stacks only, no realtime)')
|
|
3199
3477
|
.option('--modeling', 'Keep one Claude process warm across prompts instead of spawning a fresh one per task, for low-latency voice/live use. Runs from a modeling-kit install in this directory, or from the global install (~/.eventmodelers/kit) when there is none. Built into the CLI, not a per-project file.')
|
|
3200
|
-
.option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal â right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--global).')
|
|
3201
|
-
.option('--standalone', 'Let the modeling agent
|
|
3478
|
+
.option('--non-interactive', 'Never ask anything: take the board and credentials that resolve from flags, EVENTMODELERS_* env vars and the config files, and fail with the reason if they are incomplete instead of interviewing for them. A TTY was previously the only signal â right for CI or a supervisor, wrong for a loop started from a terminal, where stdin is a TTY nobody is watching and the run stops on a question. Only affects the modeling loop (--modeling/--standalone/--worker/--global).')
|
|
3479
|
+
.option('--standalone', 'Let the modeling agent act on board changes on its own initiative, without asking: it subscribes to the board\'s change channel and, whenever the board goes quiet after an edit â or has simply been idle for a while â it takes a turn nobody asked for, judges the model as a whole and fans the work out over parallel subagents, one per changed area (examples on a new node, specs for a new command or read model, a missing attribute along a chain, a screen, a question). Filling that detail in while the human keeps modeling is the point. A plain --modeling agent only proposes, and only to the people chatting with it: after their edits it says in the chat what it would do and waits for a yes; everyone else\'s edits are ignored. Implies --modeling.')
|
|
3202
3480
|
.option('--max-agents <n>', 'Cap how many subagents a self-directed --standalone turn may dispatch at once, to bound what an unattended agent can spend per turn. The agent merges work that shares a slice or chain first, then takes the most valuable pieces up to this many and leaves the rest for a later turn. 1 makes it do the single most valuable piece itself, without spawning anything. Default 5. Ignored without --standalone â prompt turns are one piece of work by definition.', '5')
|
|
3481
|
+
.option('--worker', 'A modeling agent that only works prompts: it neither takes part in the board\'s chat (the heartbeat reports chat_enabled=false, so the chat panel does not offer it) nor reacts to board changes â it waits for prompts and does them. For a dedicated agent a supervisor or another agent feeds, often paired with --exclusive and --id. Implies --modeling; cannot be combined with --standalone.')
|
|
3203
3482
|
.option('--exclusive', 'Work only the prompts addressed to this agent\'s id â the board\'s "preferred agent" (the star in the prompts panel) â and hand every untargeted prompt straight back to the queue for another agent to take. Without it an agent also works everything nobody addressed to anyone, which is what you want for a single agent and exactly what you do not want for a dedicated one (a board with a general agent plus a specialist, or an agent a supervisor drives by id). Pair it with --id so the same agent is addressable across restarts â --global/--standalone otherwise mint a fresh id per run, and prompts addressed to the previous run\'s id are never claimed. Leaves --standalone alone: a self-directed turn is nobody\'s prompt, so an exclusive standalone agent still works the board on its own initiative.')
|
|
3204
3483
|
.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.')
|
|
3205
3484
|
.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)')
|
|
@@ -3219,8 +3498,14 @@ credentialFlags(program
|
|
|
3219
3498
|
}
|
|
3220
3499
|
// The build-kit runners claim their work from the same queue but have no addressee
|
|
3221
3500
|
// filter, so the flag would silently do nothing there rather than half of what it says.
|
|
3222
|
-
if (opts.exclusive && !(opts.modeling || opts.standalone || opts.global)) {
|
|
3223
|
-
console.log('âšī¸ --exclusive only applies to the modeling loop (--modeling/--standalone/--global); ignoring it here.');
|
|
3501
|
+
if (opts.exclusive && !(opts.modeling || opts.standalone || opts.worker || opts.global)) {
|
|
3502
|
+
console.log('âšī¸ --exclusive only applies to the modeling loop (--modeling/--standalone/--worker/--global); ignoring it here.');
|
|
3503
|
+
}
|
|
3504
|
+
// A worker is defined by what it doesn't do on its own â --standalone is exactly that, so the
|
|
3505
|
+
// two contradict each other rather than one quietly winning.
|
|
3506
|
+
if (opts.worker && opts.standalone) {
|
|
3507
|
+
console.error('â --worker and --standalone are mutually exclusive â a worker never acts on board changes.');
|
|
3508
|
+
process.exit(1);
|
|
3224
3509
|
}
|
|
3225
3510
|
// --id/--name are what the platform will see for this run, so a blank one is a
|
|
3226
3511
|
// mistake worth failing on rather than silently falling back to the stored identity.
|
|
@@ -3262,8 +3547,8 @@ credentialFlags(program
|
|
|
3262
3547
|
// was never a second thing it could have selected, and requiring both flags only made
|
|
3263
3548
|
// the shorter, more obvious command fail. --global picks the modeling loop too â it has
|
|
3264
3549
|
// no meaning for a build kit, which is scaffolded per project by definition.
|
|
3265
|
-
if (opts.modeling || opts.standalone || opts.global) {
|
|
3266
|
-
const picked = opts.modeling ? '--modeling' : opts.standalone ? '--standalone' : '--global';
|
|
3550
|
+
if (opts.modeling || opts.standalone || opts.worker || opts.global) {
|
|
3551
|
+
const picked = opts.modeling ? '--modeling' : opts.standalone ? '--standalone' : opts.worker ? '--worker' : '--global';
|
|
3267
3552
|
// --bash/--exec stay build-kit only: they drive the cold-spawn tasks.json loop, which the
|
|
3268
3553
|
// modeling loop has no equivalent of. --local-ai is different â it names a *model*, not a
|
|
3269
3554
|
// queue, and the modeling loop has its own runner for one (lib/modeling-local-ai.js).
|
|
@@ -3313,7 +3598,7 @@ credentialFlags(program
|
|
|
3313
3598
|
const runnerLabel = modelingLocalAi ? 'local model' : 'warm Claude process';
|
|
3314
3599
|
await new Promise((res) => process.stdout.write(`âļ Starting modeling loop (${runnerLabel}) for ${shown && !shown.startsWith('..') ? shown : kitDir}...\n\n`, res));
|
|
3315
3600
|
try {
|
|
3316
|
-
await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, overrides, maxAgents, identity, localAi: modelingLocalAi });
|
|
3601
|
+
await runModeling(kitDir, projectDir, { verbose: !!opts.verbose, standalone: !!opts.standalone, exclusive: !!opts.exclusive, worker: !!opts.worker, overrides, maxAgents, identity, localAi: modelingLocalAi });
|
|
3317
3602
|
} catch (err) {
|
|
3318
3603
|
console.error('[modeling] Fatal:', err);
|
|
3319
3604
|
process.exit(1);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { basename, dirname, join, relative } from 'path';
|
|
3
|
+
|
|
4
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@eventmodelers/cli/latest';
|
|
5
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
7
|
+
|
|
8
|
+
// Kit dirs `re-init` can refresh, and how to ask it to.
|
|
9
|
+
const REINIT_COMMANDS = {
|
|
10
|
+
'.build-kit': 'eventmodelers re-init',
|
|
11
|
+
'.agent-modeling-kit': 'eventmodelers re-init --modeling',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Numeric compare of x.y.z versions â negative when a < b. Pre-release suffixes are ignored.
|
|
15
|
+
export function compareVersions(a, b) {
|
|
16
|
+
const pa = String(a).split('-')[0].split('.').map(Number);
|
|
17
|
+
const pb = String(b).split('-')[0].split('.').map(Number);
|
|
18
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
19
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
20
|
+
if (d) return d;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJson(path) {
|
|
26
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return {}; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The latest published version, asked of the npm registry at most once a day â in between,
|
|
30
|
+
// the answer cached at cachePath is reused. Best effort: offline, slow or broken registry
|
|
31
|
+
// responses just yield whatever was cached last (or null).
|
|
32
|
+
export async function latestPublishedVersion({ cachePath, now = Date.now(), fetchImpl = fetch } = {}) {
|
|
33
|
+
const cache = readJson(cachePath);
|
|
34
|
+
if (cache.latest && now - (cache.checkedAt || 0) < CHECK_INTERVAL_MS) return cache.latest;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
38
|
+
if (!res.ok) return cache.latest || null;
|
|
39
|
+
const { version } = await res.json();
|
|
40
|
+
if (!version) return cache.latest || null;
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
43
|
+
writeFileSync(cachePath, JSON.stringify({ latest: version, checkedAt: now }));
|
|
44
|
+
} catch {}
|
|
45
|
+
return version;
|
|
46
|
+
} catch {
|
|
47
|
+
return cache.latest || null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Hint lines for everything that's behind: this CLI against npm, and each installed kit
|
|
52
|
+
// against the CLI running now (its install manifest records the version that wrote it).
|
|
53
|
+
export function updateHints({ currentVersion, latestVersion, kitDirs = [], cwd = process.cwd() }) {
|
|
54
|
+
const hints = [];
|
|
55
|
+
if (latestVersion && compareVersions(currentVersion, latestVersion) < 0) {
|
|
56
|
+
hints.push(`đĄ A new eventmodelers CLI is available: ${currentVersion} â ${latestVersion}. Update with: npm i -g @eventmodelers/cli@latest (or use npx @eventmodelers/cli@latest)`);
|
|
57
|
+
}
|
|
58
|
+
for (const kitDir of kitDirs) {
|
|
59
|
+
const reinit = REINIT_COMMANDS[basename(kitDir)];
|
|
60
|
+
const manifestPath = join(kitDir, '.eventmodelers', 'install-manifest.json');
|
|
61
|
+
if (!reinit || !existsSync(manifestPath)) continue;
|
|
62
|
+
const kitVersion = readJson(manifestPath).version;
|
|
63
|
+
if (kitVersion && compareVersions(kitVersion, currentVersion) >= 0) continue;
|
|
64
|
+
hints.push(`đĄ ${relative(cwd, kitDir) || kitDir}/ was installed by CLI ${kitVersion || '(unknown version)'} â refresh it to ${currentVersion} with: ${reinit}`);
|
|
65
|
+
}
|
|
66
|
+
return hints;
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventmodelers/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.92",
|
|
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": {
|
|
7
7
|
"eventmodelers": "cli.js"
|
|
8
8
|
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js"
|
|
11
|
+
},
|
|
9
12
|
"files": [
|
|
10
13
|
"cli.js",
|
|
11
14
|
"lib",
|