@eventmodelers/cli 2.0.33 → 2.0.35

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
@@ -3534,6 +3534,30 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
3534
3534
  onSubscribed,
3535
3535
  });
3536
3536
 
3537
+ // The first heartbeat goes out BEFORE any channel is joined: joining runs a catch-up drain, and the platform only hands
3538
+ // prompts to an agent with a live heartbeat (403 AGENT_NOT_CONNECTED otherwise). Pinging after subscribing made that first
3539
+ // drain fail and left the backlog waiting for the next prompt:created.
3540
+ // `status`: 'shutting_down' is the last ping before an exit the board asked for; every other ping says what the agent is
3541
+ // doing right now — modeling while a turn runs (a prompt, a chat reply, a board change), idle otherwise.
3542
+ const ping = async (status) => {
3543
+ const agentStatus = status === 'shutting_down' ? status : turnsInFlight > 0 ? 'modeling' : 'idle';
3544
+ try {
3545
+ const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
3546
+ method: 'POST',
3547
+ headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
3548
+ // chat_enabled tells the board this agent answers chat messages (the panel only offers chat
3549
+ // with one that does); a --worker reports false, and older CLIs send nothing, which the
3550
+ // backend reads as false too.
3551
+ body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: 'MODELING', agent_id: cfg.agentId, chat_enabled: chat, status: agentStatus, ...(cfg.agentName ? { agent_name: cfg.agentName } : {}) }),
3552
+ signal: AbortSignal.timeout(10_000),
3553
+ });
3554
+ if (!res.ok) log(`ping failed: ${res.status} ${await res.text().catch(() => '')}`);
3555
+ } catch (err) {
3556
+ log(`ping error: ${err.message}`);
3557
+ }
3558
+ };
3559
+ await ping();
3560
+
3537
3561
  // The kill signal is broadcast on the BOARD channel (see the platform's agent-kill /
3538
3562
  // backoffice/killagent slices), not on the org channel this agent uses for prompts — without
3539
3563
  // this second subscription a modeling agent can only be stopped with a kill(1).
@@ -3567,26 +3591,6 @@ async function runModeling(kitDir, projectDir, { codeDir = null, verbose = false
3567
3591
  refreshRealtimeToken('scheduled').catch(() => {});
3568
3592
  }, 10 * 60 * 1000);
3569
3593
 
3570
- // `status`: 'shutting_down' is the last ping before an exit the board asked for; every other ping says what the agent is
3571
- // doing right now — modeling while a turn runs (a prompt, a chat reply, a board change), idle otherwise.
3572
- const ping = async (status) => {
3573
- const agentStatus = status === 'shutting_down' ? status : turnsInFlight > 0 ? 'modeling' : 'idle';
3574
- try {
3575
- const res = await fetch(`${cfg.baseUrl}/api/agent-alive`, {
3576
- method: 'POST',
3577
- headers: { Authorization: `Bearer ${realtimeToken}`, 'Content-Type': 'application/json' },
3578
- // chat_enabled tells the board this agent answers chat messages (the panel only offers chat
3579
- // with one that does); a --worker reports false, and older CLIs send nothing, which the
3580
- // backend reads as false too.
3581
- body: JSON.stringify({ token: cfg.token, board_id: cfg.boardId, agent_type: 'MODELING', agent_id: cfg.agentId, chat_enabled: chat, status: agentStatus, ...(cfg.agentName ? { agent_name: cfg.agentName } : {}) }),
3582
- signal: AbortSignal.timeout(10_000),
3583
- });
3584
- if (!res.ok) log(`ping failed: ${res.status} ${await res.text().catch(() => '')}`);
3585
- } catch (err) {
3586
- log(`ping error: ${err.message}`);
3587
- }
3588
- };
3589
- await ping();
3590
3594
  setInterval(ping, 15_000);
3591
3595
  reportStatus = () => { ping(); };
3592
3596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "2.0.33",
3
+ "version": "2.0.35",
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": {
@@ -34,23 +34,45 @@
34
34
  .em-badge button:hover { color: #bfdbfe; }
35
35
  .em-outline-toggle { display: inline-flex; gap: 5px; align-items: center; font: 12px system-ui; color: #9ca3af; cursor: pointer; }
36
36
  .em-part-label { font: 600 11px system-ui; color: #6b7280; margin-bottom: 6px; text-transform: uppercase; letter-spacing: .06em; }
37
- .em-panel { background: #111827; color: #e5e7eb; padding: 12px; overflow-y: auto; max-height: 100vh; position: sticky; top: 0; font: 14px/1.4 system-ui, sans-serif; }
38
- .em-panel h2, .em-auto h2 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #9ca3af; margin: 16px 0 6px; }
37
+ .em-panel { --p-bg: #0e1522; --p-surface: #161f30; --p-raised: #1d283c; --p-line: #232f45; --p-text: #e7eaf0; --p-muted: #8a95a9; --p-accent: #f97316;
38
+ background: var(--p-bg); color: var(--p-text); padding: 0 16px 24px; overflow-y: auto; max-height: 100vh; position: sticky; top: 0; font: 13px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
39
+ .em-panel::-webkit-scrollbar { width: 8px; } .em-panel::-webkit-scrollbar-thumb { background: var(--p-line); border-radius: 4px; }
40
+ .em-panel h2, .em-auto h2 { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: #8a95a9; margin: 0; }
41
+ .em-sect { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 18px 0 12px; min-height: 28px; }
42
+ .em-sect .em-sub { color: var(--p-muted); font-size: 12px; margin-top: 2px; text-transform: none; letter-spacing: 0; font-weight: 400; }
43
+ .em-empty { color: var(--p-muted); background: var(--p-surface); border: 1px dashed var(--p-line); border-radius: 10px; padding: 18px; text-align: center; }
39
44
  /* The panel's tabs are real tabs: a strip with a rule under it, the open tab joined to the content below (same colour, no rule under it) with an orange top edge. */
40
45
  .em-panel.em-collapsed { padding: 8px 4px; overflow: hidden; }
41
- .em-toggle { background: #1f2937; color: #e5e7eb; border: 0; border-radius: 6px; width: 28px; height: 28px; cursor: pointer; font: 14px system-ui; flex: none; }
42
- .em-toggle:hover { background: #374151; }
46
+ .em-toggle { background: #161f30; color: #8a95a9; border: 1px solid #232f45; border-radius: 7px; width: 28px; height: 28px; cursor: pointer; font: 14px system-ui; flex: none; }
47
+ .em-toggle:hover { background: #1d283c; color: #e7eaf0; }
43
48
  .em-tabs .em-toggle { width: 28px; height: 28px; padding: 0; margin-bottom: 0; border-radius: 6px; color: #e5e7eb; }
44
49
  .em-collapsed .em-toggle { display: block; margin: 0 auto 8px; }
45
50
  .em-collapsed .em-vlabel { writing-mode: vertical-rl; color: #9ca3af; font: 12px system-ui; letter-spacing: .06em; text-transform: uppercase; text-align: center; cursor: pointer; width: 100%; }
46
- .em-tabs { display: flex; gap: 2px; border-bottom: 1px solid #374151; margin: 0 -12px 4px; padding: 0 12px; }
47
- .em-tabs button { background: #1f2937; color: #9ca3af; border: 1px solid transparent; border-bottom: 0; border-radius: 6px 6px 0 0; padding: 7px 12px; margin-bottom: -1px; cursor: pointer; font: 13px system-ui; }
48
- .em-tabs button:hover { color: #e5e7eb; }
49
- .em-tabs button.em-active { background: #111827; color: #fff; font-weight: 600; border-color: #374151; border-top: 2px solid #f97316; padding-top: 6px; }
50
- .em-btn { background: #374151; color: #e5e7eb; border: 0; border-radius: 6px; padding: 5px 9px; cursor: pointer; font: 12px system-ui; }
51
- .em-btn.em-on { background: #f97316; color: #111827; }
52
- .em-ev { background: #1f2937; border-left: 4px solid #f97316; border-radius: 6px; padding: 6px 8px; margin: 6px 0; font-size: 12px; color: #e5e7eb; }
53
- .em-ev b { color: #fdba74; }
51
+ .em-tabs { display: flex; align-items: center; gap: 4px; border-bottom: 1px solid var(--p-line); margin: 0 -16px; padding: 10px 16px 0; position: sticky; top: 0; background: var(--p-bg); z-index: 5; }
52
+ .em-tabs button:not(.em-toggle) { background: none; color: var(--p-muted); border: 0; border-bottom: 2px solid transparent; border-radius: 0; padding: 8px 12px 10px; margin-bottom: -1px; cursor: pointer; font: 500 13px -apple-system, BlinkMacSystemFont, system-ui, sans-serif; text-transform: capitalize; transition: color .15s, border-color .15s; }
53
+ .em-tabs button:not(.em-toggle):hover { color: var(--p-text); }
54
+ .em-tabs button.em-active { color: #fff; border-bottom-color: var(--p-accent); }
55
+ .em-btn { background: var(--p-raised, #1d283c); color: #e7eaf0; border: 1px solid #2c3a55; border-radius: 7px; padding: 5px 11px; cursor: pointer; font: 500 12px -apple-system, BlinkMacSystemFont, system-ui, sans-serif; transition: background .15s, border-color .15s; }
56
+ .em-btn:hover { background: #26344e; border-color: #3a4b6b; }
57
+ .em-btn.em-on { background: #f97316; border-color: #f97316; color: #1a0f05; }
58
+ .em-btn.em-ghost { background: transparent; border-color: transparent; color: #8a95a9; padding: 5px 8px; } .em-btn.em-ghost:hover { background: #1d283c; color: #e7eaf0; }
59
+ .em-btn.em-icon { width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; }
60
+ .em-ev { background: var(--p-surface, #161f30); border: 1px solid var(--p-line, #232f45); border-left: 3px solid #f97316; border-radius: 10px; padding: 10px 12px; margin: 8px 0; font-size: 12px; color: #e7eaf0; }
61
+ .em-ev b { color: #fdba74; font-weight: 600; }
62
+ .em-ev-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; cursor: pointer; }
63
+ .em-ev-pos { font: 600 11px ui-monospace, SFMono-Regular, Menlo, monospace; color: #8a95a9; background: #0e1522; border-radius: 5px; padding: 1px 6px; }
64
+ .em-ev-type { font-weight: 600; font-size: 13px; color: #fdba74; }
65
+ .em-ev-from { color: #8a95a9; font-size: 12px; }
66
+ .em-ev-time { margin-left: auto; color: #6b7690; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; }
67
+ .em-kv { display: grid; grid-template-columns: minmax(70px, max-content) 1fr; gap: 3px 14px; margin: 8px 0 2px; padding-top: 8px; border-top: 1px solid var(--p-line, #232f45); }
68
+ .em-kv dt { color: #8a95a9; margin: 0; } .em-kv dd { margin: 0; word-break: break-word; color: #e7eaf0; } .em-kv dd.em-none { color: #4f5b73; }
69
+ .em-kv dd a { color: #e7eaf0; text-decoration: none; border-bottom: 1px dotted #4f5b73; } .em-kv dd a:hover { color: #fdba74; border-color: #fdba74; }
70
+ .em-ev-foot { margin-top: 10px; }
71
+ .em-pill { display: inline-flex; align-items: center; gap: 4px; font: 600 11px -apple-system, system-ui, sans-serif; padding: 1px 8px; border-radius: 999px; background: #1d283c; color: #8a95a9; }
72
+ .em-pill.em-pass { background: rgba(34,197,94,.14); color: #4ade80; } .em-pill.em-fail { background: rgba(239,68,68,.15); color: #f87171; }
73
+ .em-spec-title { font-weight: 600; font-size: 13px; color: #e7eaf0; line-height: 1.35; }
74
+ .em-spec-meta { color: #8a95a9; font-size: 12px; margin-top: 2px; }
75
+ .em-chev { display: inline-block; width: 14px; color: #6b7690; font-size: 10px; }
54
76
  .em-ev pre { margin: 4px 0 0; padding: 0; background: transparent; white-space: pre-wrap; word-break: break-word; color: #d1d5db; font-size: 11px; } /* background set explicitly: a screen framework (Bulma) styles bare <pre> light, which left this text unreadable */
55
77
  .em-slider { display: flex; align-items: center; gap: 8px; margin: 8px 0; font-size: 12px; color: #9ca3af; } .em-slider .em-slider-text { min-width: 120px; text-align: right; }
56
78
  /* `.em-panel input` (below) makes every panel input a full-width text box: the slider and the checkbox undo that, with a selector that beats it. */
@@ -59,8 +81,10 @@
59
81
  .em-hl { outline: 3px solid #f97316 !important; outline-offset: 2px; box-shadow: 0 0 0 6px rgba(249, 115, 22, 0.25) !important; }
60
82
  .em-ev.em-cmd { border-left-color: #3b82f6; } .em-ev.em-cmd b { color: #93c5fd; }
61
83
  .em-ev.em-fail { border-left-color: #ef4444; } .em-ev.em-pass { border-left-color: #22c55e; }
84
+ .em-ev.em-pass:hover, .em-ev.em-fail:hover { border-color: #2c3a55; }
62
85
  .em-ev.em-out { border-left-color: #14b8a6; }
63
- .em-panel input, .em-panel textarea { width: 100%; box-sizing: border-box; margin: 2px 0 6px; padding: 4px 6px; border-radius: 4px; border: 1px solid #4b5563; background: #111827; color: #fff; }
86
+ .em-panel input, .em-panel textarea { width: 100%; box-sizing: border-box; margin: 2px 0 6px; padding: 6px 9px; border-radius: 7px; border: 1px solid #2c3a55; background: #0e1522; color: #fff; font: inherit; }
87
+ .em-panel input:focus, .em-panel textarea:focus { outline: none; border-color: #f97316; }
64
88
  .em-auto { max-width: 760px; }
65
89
  .em-auto .em-ev { font-size: 13px; }
66
90
  .em-auto h2 { color: #6b7280; }
@@ -309,7 +333,8 @@ function EventBrowser({ events }) {
309
333
  <button className="em-btn" onClick={() => setFollow(null)}>×</button>
310
334
  </div>
311
335
  )}
312
- <h2>{shown.length === events.length ? `${events.length} events` : `${shown.length} of ${events.length} events`}{withIx ? ` · ${recorded.length} interactions` : ''}</h2>
336
+ <div className="em-sect" style={{ margin: '4px 0 0' }}><span className="em-pill">{shown.length === events.length ? `${events.length} event${events.length === 1 ? '' : 's'}` : `${shown.length} of ${events.length} events`}{withIx ? ` · ${recorded.length} interactions` : ''}</span></div>
337
+ {rows.length === 0 && <div className="em-empty">No events yet. Use the prototype and they appear here.</div>}
313
338
  {[...rows].reverse().map((row) => {
314
339
  if (row.kind === 'interaction') {
315
340
  return (
@@ -326,35 +351,41 @@ function EventBrowser({ events }) {
326
351
  const future = cursor != null && e.position > cursor;
327
352
  return (
328
353
  <div key={e.id} className="em-ev" style={future ? { opacity: 0.4 } : null}>
329
- <div style={{ cursor: 'pointer' }} onClick={() => setOpen(isOpen ? null : e.id)}>
330
- <b>#{e.position} {e.type}</b>
331
- {e.metadata.command && <span> ← {e.metadata.command}</span>}
332
- {e.metadata.automation && <span> (automation)</span>}
333
- {e.metadata.simulated && <span> (simulated)</span>}
334
- <span style={{ float: 'right', color: '#9ca3af' }}>{new Date(e.metadata.at).toLocaleTimeString()}</span>
335
- </div>
336
- <div style={{ marginTop: 4 }}>
337
- {Object.entries(e.data || {}).map(([k, v]) => (
338
- <div key={k}>
339
- <span style={{ color: '#9ca3af' }}>{k}: </span>
340
- {v != null && typeof v === 'object' ? (
341
- <code>{JSON.stringify(v)}</code>
342
- ) : (
343
- <a href="#" title="Show every event carrying this value" style={{ color: '#e5e7eb' }}
344
- onClick={(ev) => { ev.preventDefault(); setFollow({ field: k, value: v }); }}>
345
- {String(v)}
346
- </a>
347
- )}
348
- </div>
349
- ))}
354
+ <div className="em-ev-head" onClick={() => setOpen(isOpen ? null : e.id)}>
355
+ <span className="em-ev-pos">#{e.position}</span>
356
+ <span className="em-ev-type">{e.type}</span>
357
+ {e.metadata.command && <span className="em-ev-from">← {e.metadata.command}</span>}
358
+ {e.metadata.automation && <span className="em-pill">automation</span>}
359
+ {e.metadata.simulated && <span className="em-pill">simulated</span>}
360
+ <span className="em-ev-time">{new Date(e.metadata.at).toLocaleTimeString()}</span>
350
361
  </div>
362
+ {Object.keys(e.data || {}).length > 0 && (
363
+ <dl className="em-kv">
364
+ {Object.entries(e.data || {}).flatMap(([k, v]) => {
365
+ const none = v === '' || v == null || v === undefined || v === 'undefined';
366
+ return [
367
+ <dt key={k + 't'}>{k}</dt>,
368
+ <dd key={k + 'd'} className={none ? 'em-none' : ''}>
369
+ {none ? '—' : v != null && typeof v === 'object' ? (
370
+ <code>{JSON.stringify(v)}</code>
371
+ ) : (
372
+ <a href="#" title="Show every event carrying this value"
373
+ onClick={(ev) => { ev.preventDefault(); setFollow({ field: k, value: v }); }}>
374
+ {String(v)}
375
+ </a>
376
+ )}
377
+ </dd>,
378
+ ];
379
+ })}
380
+ </dl>
381
+ )}
351
382
  {isOpen && (
352
383
  <pre>{JSON.stringify(
353
384
  Object.assign({ id: e.id, at: e.metadata.at }, e.metadata.command && { command: e.metadata.command },
354
385
  e.metadata.actor && { actor: e.metadata.actor }, e.metadata.automation && { automation: e.metadata.automation },
355
386
  e.metadata.simulated && { simulated: true }), null, 1)}</pre>
356
387
  )}
357
- <button className="em-btn" style={{ marginTop: 4 }} onClick={() => rt.viewAsOf(e.position)}>View as of here</button>
388
+ <div className="em-ev-foot"><button className="em-btn" onClick={() => rt.viewAsOf(e.position)}>View as of here</button></div>
358
389
  </div>
359
390
  );
360
391
  })}
@@ -467,8 +498,7 @@ function Panel({ slices, open: panelOpen, onToggle }) {
467
498
 
468
499
  {tab === 'events' && (
469
500
  <>
470
- <h2>Event log</h2>
471
- <button className="em-btn" onClick={() => rt.reset()}>Reset</button>
501
+ <div className="em-sect"><h2>Event log</h2><button className="em-btn" onClick={() => rt.reset()}>Reset</button></div>
472
502
  <EventBrowser events={events} />
473
503
  </>
474
504
  )}
@@ -483,27 +513,31 @@ function Panel({ slices, open: panelOpen, onToggle }) {
483
513
 
484
514
  {tab === 'specs' && (
485
515
  <>
486
- <h2>Scenarios from the board</h2>
487
- {slices && (
488
- <p style={{ color: '#9ca3af' }}>
489
- {allSpecs ? 'All screens' : 'This screen'}: {shownSpecs.length} of {specResults.length}{' '}
490
- <button className="em-btn" onClick={() => setAllSpecs(!allSpecs)}>{allSpecs ? 'This screen only' : 'Show all'}</button>
491
- </p>
492
- )}
516
+ <div className="em-sect">
517
+ <div>
518
+ <h2>Scenarios</h2>
519
+ {slices && <div className="em-sub">{allSpecs ? 'All screens' : 'This screen'} · {shownSpecs.length} of {specResults.length}</div>}
520
+ </div>
521
+ {slices && <button className="em-btn" onClick={() => setAllSpecs(!allSpecs)}>{allSpecs ? 'This screen only' : 'Show all'}</button>}
522
+ </div>
523
+ {shownSpecs.length === 0 && <div className="em-empty">No scenarios for this screen.</div>}
493
524
  {shownSpecs.map(([r, i]) => (
494
- <div key={i} className={'em-ev ' + (r.pass ? 'em-pass' : 'em-fail')} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
525
+ <div key={i} className={'em-ev ' + (r.pass ? 'em-pass' : 'em-fail')} style={{ display: 'flex', alignItems: 'flex-start', gap: 6 }}>
495
526
  <div style={{ flex: 1, minWidth: 0 }}>
496
- <div style={{ cursor: 'pointer' }} onClick={() => setOpen({ ...open, [i]: !open[i] })} title="Show or hide the scenario as text">
497
- <span style={{ display: 'inline-block', width: 12 }}>{open[i] ? '▾' : '▸'}</span>
498
- <b>{r.pass ? '✓' : '✗'} {r.spec.title}</b> <span>{r.spec.slice}</span>
527
+ <div style={{ cursor: 'pointer', display: 'flex', gap: 6 }} onClick={() => setOpen({ ...open, [i]: !open[i] })} title="Show or hide the scenario as text">
528
+ <span className="em-chev" style={{ marginTop: 3 }}>{open[i] ? '▼' : '▶'}</span>
529
+ <div style={{ minWidth: 0 }}>
530
+ <div className="em-spec-title">{r.spec.title}</div>
531
+ <div className="em-spec-meta"><span className={'em-pill ' + (r.pass ? 'em-pass' : 'em-fail')}>{r.pass ? '✓ Passing' : '✗ Failing'}</span> <span style={{ marginLeft: 4 }}>{r.spec.slice}</span></div>
532
+ </div>
499
533
  </div>
500
534
  {open[i] && <SpecView spec={r.spec} />}
501
535
  {!r.pass && <pre>{r.detail}</pre>}
502
536
  </div>
503
- <button className={'em-btn' + (armed === i ? ' em-on' : '')} style={{ flex: 'none' }} onClick={() => playGiven(r.spec, i)}
537
+ <button className={'em-btn em-icon' + (armed === i ? ' em-on' : '')} style={{ flex: 'none' }} onClick={() => playGiven(r.spec, i)}
504
538
  title="Play the given events: the screens show this scenario's starting state">▶</button>
505
539
  {r.spec.sliceId && (
506
- <button className="em-btn" style={{ flex: 'none' }} onClick={() => EMRuntime.zoomToSlice(r.spec.sliceId)}
540
+ <button className="em-btn em-icon" style={{ flex: 'none' }} onClick={() => EMRuntime.zoomToSlice(r.spec.sliceId)}
507
541
  title="Show this scenario's slice on the board"><span aria-hidden>◎</span></button>
508
542
  )}
509
543
  </div>