@eventmodelers/cli 2.0.51 → 2.0.54

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 CHANGED
@@ -42,9 +42,9 @@ import { recordAgentStatus } from './shared/build-kit/lib/slice-run.js';
42
42
  import { findSliceDir, fingerprint, readBaseline, writeBaseline, compare } from './lib/ui-baseline.js';
43
43
  import { defaultAgentName, withInstanceSuffix } from './shared/build-kit/lib/agent-name.js';
44
44
  import { acquireAgentId } from './shared/build-kit/lib/agent-lock.js';
45
- import { readEvents } from './shared/build-kit/lib/slice-events.js';
45
+ import { readEvents, appendEvent, locationOf } from './shared/build-kit/lib/slice-events.js';
46
46
  import { findPromptEvent, replayRequest } from './shared/build-kit/lib/prompt-replay.js';
47
- import { prunableEvents, pruneLog, rememberPruneAnswer, shouldOfferPrune } from './shared/build-kit/lib/event-prune.js';
47
+ import { clearLog, prunableEvents, pruneLog, rememberPruneAnswer, shouldOfferPrune } from './shared/build-kit/lib/event-prune.js';
48
48
  import { fold } from './shared/build-kit/lib/slice-state.js';
49
49
  import { filterEvents, baselineIds, formatEvent, formatPayload, sliceTitles, summarize } from './shared/build-kit/lib/events-view.js';
50
50
  import { stableGlobalAgentId } from './lib/global-agent-id.js';
@@ -1060,15 +1060,28 @@ async function resolveStack(cliStack) {
1060
1060
  );
1061
1061
  }
1062
1062
 
1063
+ // Names what is installed now (from the kit's install manifest) and what this run installs.
1064
+ function printInstallVersions(kitDir, stackKey) {
1065
+ const prev = readJsonSafe(join(kitDir, '.eventmodelers', 'install-manifest.json'));
1066
+ if (prev.version) {
1067
+ const same = prev.version === CLI_VERSION;
1068
+ console.log(`Installed: v${prev.version}${prev.stack ? ` (${prev.stack})` : ''}`);
1069
+ console.log(`Installing: v${CLI_VERSION}${stackKey ? ` (${stackKey})` : ''}${same ? ' — same version, files are refreshed' : ''}\n`);
1070
+ } else {
1071
+ console.log(`Installed: nothing yet\nInstalling: v${CLI_VERSION}${stackKey ? ` (${stackKey})` : ''}\n`);
1072
+ }
1073
+ }
1074
+
1063
1075
  async function installStack(stackKey, stackCfg, options = {}) {
1064
- console.log('🚀 Eventmodelers CLI\n');
1065
- console.log(`Using: ${stackKey} (${stackCfg.label})\n`);
1076
+ console.log(`🚀 Eventmodelers CLI v${CLI_VERSION}\n`);
1077
+ console.log(`Using: ${stackKey} (${stackCfg.label})`);
1066
1078
 
1067
1079
  // Almost always the cwd. The exception is the global modeling install, which is
1068
1080
  // scaffolded into ~/.eventmodelers/kit from wherever `run --standalone` was invoked
1069
1081
  // (see ensureGlobalKit) — the mirror image of options.templatesSource below: where we
1070
1082
  // install TO, versus where we install FROM.
1071
1083
  const targetDir = options.targetDir ? resolve(options.targetDir) : process.cwd();
1084
+ printInstallVersions(join(targetDir, stackCfg.kitDirName), stackKey);
1072
1085
  // `init --git <url>` passes a resolved clone dir's templates/ here instead — every
1073
1086
  // other input (STACKS, MODELING_KIT) keeps using the built-in path.
1074
1087
  const templatesSource = options.templatesSource || join(__dirname, 'stacks', stackKey, 'templates');
@@ -1487,6 +1500,8 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
1487
1500
  // `re-init` for a build kit: re-copy shared/build-kit (ralph.js, the runners, tracing, adapters)
1488
1501
  // over the installed kit, then re-run the config steps. Stack templates and skills are untouched.
1489
1502
  async function refreshSharedRuntime({ targetDir, kitDir, globalOpts, opts }) {
1503
+ console.log(`🚀 Eventmodelers CLI v${CLI_VERSION}\n`);
1504
+ printInstallVersions(kitDir, readJsonSafe(join(kitDir, '.eventmodelers', 'install-manifest.json')).stack);
1490
1505
  console.log(`📦 Refreshing the shared runtime in ${relative(targetDir, kitDir)}/ (stack files untouched)...`);
1491
1506
  copyDirContents(join(__dirname, 'shared', 'build-kit'), kitDir);
1492
1507
  // The kit is now this CLI's runtime — record that, so the startup "kit is outdated" hint stops.
@@ -5350,6 +5365,10 @@ program
5350
5365
  if (!wanted) {
5351
5366
  const claimed = Object.entries(fold(readEvents(kitDir)).slices).filter(([, s]) => s.claim);
5352
5367
  if (claimed.length !== 1) {
5368
+ if (!claimed.length) {
5369
+ // Where it looked: an agent running from another kit's directory finds no claim in that kit's log.
5370
+ try { appendEvent(kitDir, { type: 'claim-not-found', command: 'build-baseline', claimed: 0, ...locationOf(kitDir, cwd) }, agentEventMeta(cwd, kitDir, command)); } catch { /* best effort */ }
5371
+ }
5353
5372
  console.error(`❌ ${claimed.length ? `${claimed.length} slices are claimed` : 'No slice is claimed by the run loop'} — name the slice with --slice.`);
5354
5373
  process.exit(1);
5355
5374
  }
@@ -5489,6 +5508,9 @@ program
5489
5508
  .option('--verbose', 'Print each event\'s payload under its line — for a tool call, the input it was sent (recorded by runs started with --verbose). Also shows the baseline events: the kit\'s first look at each slice (a fresh kit records every slice already on the board), which are hidden by default. `--type board-observed` and `--json` show them too.')
5490
5509
  .option('--state', 'Print the state the log folds to (slices, queue, open prompts, learnings) instead of the events')
5491
5510
  .option('--prune', 'Delete the events nothing reads any more — the journal of finished prompts — and stop. Slice state, learnings and open prompts are kept; the same prune a run offers at every 1000 events.')
5511
+ .option('--clear', "Delete the whole log, state included, so the kit starts over (it looks at the board again; attempt counts, blocks and failure history are gone). Lists what it would delete and stops unless --yes is given. Refuses while a run holds a claim, unless --force.")
5512
+ .option('--yes', 'With --clear: really delete')
5513
+ .option('--force', 'With --clear: delete even while a run holds a claim')
5492
5514
  .option('--follow', 'Keep running and print events as they are appended')
5493
5515
  .option('--watch', 'Same as --follow')
5494
5516
  .action(async (opts) => {
@@ -5502,6 +5524,19 @@ program
5502
5524
  const prototypeLog = join(cwd, '.prototype');
5503
5525
  const inPrototype = !opts.global && !opts.kit && !findInstalledKitDir(cwd) && existsSync(join(prototypeLog, 'events.log'));
5504
5526
  const kitDir = opts.global ? globalLog : opts.kit ? resolve(cwd, opts.kit) : inPrototype ? prototypeLog : requireBuildKitDir(cwd);
5527
+ if (opts.clear) {
5528
+ try {
5529
+ const where = relative(cwd, kitDir) || kitDir;
5530
+ const { removed } = clearLog(kitDir, { force: opts.force, dryRun: !opts.yes });
5531
+ console.log(opts.yes
5532
+ ? `Cleared ${removed} events from ${where}.`
5533
+ : `Would delete all ${removed} events from ${where} — state included. Nothing was deleted; run again with --yes to do it.`);
5534
+ } catch (err) {
5535
+ console.error(`❌ ${err.message}`);
5536
+ process.exit(1);
5537
+ }
5538
+ return;
5539
+ }
5505
5540
  if (opts.prune) {
5506
5541
  try {
5507
5542
  const { removed, kept } = pruneLog(kitDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "2.0.51",
3
+ "version": "2.0.54",
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,8 +7,9 @@
7
7
  import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'fs';
8
8
  import { isDeepStrictEqual } from 'util';
9
9
  import { join } from 'path';
10
- import { EVENTS_DIR, INDEX_FILE, readEvents } from './slice-events.js';
10
+ import { EVENTS_DIR, INDEX_FILE, SEQ_FILE, readEvents } from './slice-events.js';
11
11
  import { fold } from './slice-state.js';
12
+ import { isProcessAlive } from './slice-run.js';
12
13
 
13
14
  export const PRUNE_EVERY = 1000;
14
15
  const ASKED_FILE = '.prune-asked'; // inside events/, which readEvents never lists and the kit's git rules already cover
@@ -40,6 +41,24 @@ export function pruneLog(kitDir) {
40
41
  return { removed: doomed.size, kept: rest.length };
41
42
  }
42
43
 
44
+ // Deletes the whole log — every event, state included — so the kit starts over as if freshly installed: it looks at
45
+ // the board again and every slice's attempt count, block and failure history is gone. Refuses while a live process
46
+ // holds a claim (its build would end without a trace) unless `force`. Returns { removed, liveClaims }; with
47
+ // `dryRun` nothing is deleted.
48
+ export function clearLog(kitDir, { force = false, dryRun = false } = {}) {
49
+ const events = readEvents(kitDir);
50
+ const liveClaims = Object.entries(fold(events).slices)
51
+ .filter(([, s]) => s.claim && isProcessAlive(s.claim.pid))
52
+ .map(([id, s]) => s.claim.title ?? id);
53
+ if (liveClaims.length && !force) throw new Error(`a run is building ${liveClaims.join(', ')} — stop it first, or pass --force`);
54
+ if (dryRun) return { removed: events.length, liveClaims };
55
+ const dir = join(kitDir, EVENTS_DIR);
56
+ rmSync(dir, { recursive: true, force: true });
57
+ rmSync(join(kitDir, INDEX_FILE), { force: true });
58
+ rmSync(join(kitDir, SEQ_FILE), { force: true });
59
+ return { removed: events.length, liveClaims };
60
+ }
61
+
43
62
  // Asked when the log has reached a multiple of PRUNE_EVERY it has not been asked at yet — 1000, then 2000, … — so a
44
63
  // "no" is not repeated on every start. `remember` records the size the answer was given at.
45
64
  const askedPath = (kitDir) => join(kitDir, EVENTS_DIR, ASKED_FILE);
@@ -26,8 +26,10 @@ function describe(e) {
26
26
  return [e.slice, e.status ?? ''];
27
27
  case 'claimed': {
28
28
  const tree = e.cleanTree === true ? 'clean tree' : e.cleanTree === false ? 'dirty tree' : 'not a git tree';
29
- return [e.slice, `pid ${e.pid}, ${tree}${e.base ? `, ${e.base.branch}@${String(e.base.head).slice(0, 8)}` : ''}${e.ticketNumber ? `, ticket ${e.ticketNumber}` : ''}${e.assignee ? `, assignee ${e.assignee}` : ''}`];
29
+ return [e.slice, `pid ${e.pid}, ${tree}${e.kitDir ? `, kit ${e.kitDir}` : ''}${e.base ? `, ${e.base.branch}@${String(e.base.head).slice(0, 8)}` : ''}${e.ticketNumber ? `, ticket ${e.ticketNumber}` : ''}${e.assignee ? `, assignee ${e.assignee}` : ''}`];
30
30
  }
31
+ case 'claim-not-found':
32
+ return [e.slice ?? e.wanted ?? '', `${e.command ?? 'command'} looked in ${e.kitDir ?? '?'} (cwd ${e.cwd ?? '?'}), ${e.claimed ?? 0} claimed there`];
31
33
  case 'claim-confirmed':
32
34
  return [e.slice, ''];
33
35
  case 'released':
@@ -8,6 +8,7 @@
8
8
  import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, writeFileSync } from 'fs';
9
9
  import { randomUUID } from 'crypto';
10
10
  import { join } from 'path';
11
+ import { homedir } from 'os';
11
12
 
12
13
  export const EVENTS_DIR = 'events';
13
14
  export const INDEX_FILE = 'events.log';
@@ -85,6 +86,14 @@ function nextSeq(kitDir) {
85
86
  // concurrent processes never interleave however large the event. After a torn index line the new id
86
87
  // starts on a fresh line instead of being glued onto the fragment. Every event gets the next sequence number
87
88
  // (`seq`), which — not the timestamp — defines the order. Returns the event id.
89
+ // Where a process ran, for events: the kit it works against and its working directory. The log is shared with
90
+ // everyone who can open the board, so the home directory is shown as `~` rather than the user's name.
91
+ export function locationOf(kitDir, cwd = process.cwd()) {
92
+ const home = homedir();
93
+ const tidy = (p) => (home && p && (p === home || p.startsWith(home + '/')) ? `~${p.slice(home.length)}` : p);
94
+ return { kitDir: tidy(kitDir), cwd: tidy(cwd) };
95
+ }
96
+
88
97
  export function appendEvent(kitDir, event, meta) {
89
98
  const dir = join(kitDir, EVENTS_DIR);
90
99
  mkdirSync(dir, { recursive: true });
@@ -5,7 +5,7 @@
5
5
  // `board` is injected: { setStatus(sliceId, status), claim(sliceId, fromStatus), readStatus(sliceId) → status | null }. null means
6
6
  // no board (--local): nothing is pushed or read, the log alone decides.
7
7
 
8
- import { appendEvent, readEvents } from './slice-events.js';
8
+ import { appendEvent, readEvents, locationOf } from './slice-events.js';
9
9
  import { fold, decideSettle, decideRecovery, statusKey } from './slice-state.js';
10
10
  import { adoptAgentEdits, projectStatus } from './slice-projection.js';
11
11
  import { discardUnfinishedWork, gitBase } from './slice-git.js';
@@ -35,17 +35,25 @@ export function isProcessAlive(pid) {
35
35
  // take the stale file for an edit.
36
36
  export function recordAgentStatus({ kitDir, status, sliceId, meta }) {
37
37
  const claimed = Object.entries(fold(readEvents(kitDir)).slices).filter(([, s]) => s.claim);
38
+ // A refusal is recorded with where it looked: the usual cause is an agent running from another kit's
39
+ // directory (an app with a root kit and a web/ kit), whose log has no claim for the slice.
40
+ const refuse = (message) => {
41
+ try {
42
+ appendEvent(kitDir, { type: 'claim-not-found', ...(sliceId ? { wanted: sliceId } : {}), claimed: claimed.length, command: 'slice-status', ...locationOf(kitDir) }, meta);
43
+ } catch { /* best effort: the refusal itself is what matters */ }
44
+ throw new Error(message);
45
+ };
38
46
  let match;
39
47
  if (sliceId) {
40
48
  const wanted = sliceId.trim().toLowerCase();
41
49
  match = claimed.find(([id, s]) => id === sliceId || s.claim.title?.toLowerCase() === wanted);
42
- if (!match) throw new Error(`slice "${sliceId}" is not claimed by the run loop — only a slice the loop is building has its status recorded here`);
50
+ if (!match) refuse(`slice "${sliceId}" is not claimed by the run loop — only a slice the loop is building has its status recorded here`);
43
51
  } else if (claimed.length === 1) {
44
52
  match = claimed[0];
45
53
  } else if (claimed.length > 1) {
46
54
  throw new Error(`${claimed.length} slices are claimed (${claimed.map(([, s]) => s.claim.title).join(', ')}) — name one with --slice`);
47
55
  } else {
48
- throw new Error('no slice is claimed by the run loop. For a task from tasks.json, set the status with the update-slice-status skill instead');
56
+ refuse('no slice is claimed by the run loop. For a task from tasks.json, set the status with the update-slice-status skill instead');
49
57
  }
50
58
  const [id, { claim }] = match;
51
59
  recordEvent(kitDir, { type: 'agent-recorded', slice: id, status }, meta);
@@ -78,7 +86,7 @@ export async function claimSlice({ kitDir, planned, board, meta, cleanTree, pid
78
86
  }
79
87
  }
80
88
  const state = recordEvent(kitDir, {
81
- type: 'claimed', slice: planned.id, ctx: planned.ctx, title: planned.title, pid, base: gitBase(kitDir), cleanTree,
89
+ type: 'claimed', slice: planned.id, ctx: planned.ctx, title: planned.title, pid, base: gitBase(kitDir), cleanTree, ...locationOf(kitDir),
82
90
  // What the board says about the slice at build time, when it says anything: both can change between builds.
83
91
  ...(planned.ticketNumber ? { ticketNumber: planned.ticketNumber } : {}),
84
92
  ...(planned.assignee ? { assignee: planned.assignee } : {}),
@@ -1,7 +1,7 @@
1
1
  // A slice build's tool calls as events in the slice-state log, so `run --sync` can show them on the board. Any
2
2
  // runner that sees its agent's individual tool calls records them through here, so every runner logs the same shape.
3
3
 
4
- import { appendEvent } from './slice-events.js';
4
+ import { appendEvent, locationOf } from './slice-events.js';
5
5
 
6
6
  // Only the tool's name is recorded — never its input. A Bash command, a skill's arguments or a path can hold
7
7
  // credentials or local details, and this log is shared with everyone who can open the board. Best effort, like the
@@ -9,7 +9,7 @@ import { appendEvent } from './slice-events.js';
9
9
  export function recordToolCall({ kitDir, slice, tool, meta }) {
10
10
  if (!slice?.sliceId) return;
11
11
  try {
12
- appendEvent(kitDir, { type: 'tool-call', slice: slice.sliceId, tool }, meta);
12
+ appendEvent(kitDir, { type: 'tool-call', slice: slice.sliceId, tool, ...locationOf(kitDir) }, meta);
13
13
  } catch (err) {
14
14
  console.error(`[ralph] could not record a tool call (${err.message})`);
15
15
  }