@eventmodelers/cli 0.0.26 → 0.0.28

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.
Files changed (4) hide show
  1. package/README.md +4 -4
  2. package/cli.js +24 -11
  3. package/lib/fetch.js +30 -42
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -225,9 +225,9 @@ npx @eventmodelers/cli run --ollama # same, via local Ollama (ra
225
225
  npx @eventmodelers/cli run --bash # bash-only loop, no realtime (ralph.sh)
226
226
  npx @eventmodelers/cli listen # start the code-export listener (code-export.mjs) from the installed kit dir
227
227
  npx @eventmodelers/cli listen --port 4000 # same, on a different port
228
- npx @eventmodelers/cli fetch # pull full slice detail from every context on the board into <kit-dir>/.slices/
229
- npx @eventmodelers/cli fetch --slice-id <id> # same, then print just that slice
230
- npx @eventmodelers/cli fetch --slice-title <title> # same, then print just the slice matching this title
228
+ npx @eventmodelers/cli fetch --context <name> # pull full slice detail for one context on the board into <kit-dir>/.slices/ (project root if no kit, or a modeling-kit, is installed)
229
+ npx @eventmodelers/cli fetch --context <name> --slice-id <id> # same, then print just that slice
230
+ npx @eventmodelers/cli fetch --context <name> --slice-title <title> # same, then print just the slice matching this title
231
231
  npx @eventmodelers/cli stacks # list available stacks
232
232
  npx @eventmodelers/cli status # check what's installed
233
233
  npx @eventmodelers/cli config # print the fully resolved config (file + env), token masked
@@ -238,7 +238,7 @@ npx @eventmodelers/cli uninstall # remove everything init/ini
238
238
 
239
239
  `listen` is the same kind of dispatcher, but for `<kit-dir>/code-export.mjs` — a local HTTP server (port 3001 by default) that the eventmodelers board UI posts slice/screen data to, which then gets written under `<kit-dir>/.slices/`.
240
240
 
241
- `fetch` is the pull-based counterpart to `listen`: instead of waiting for the board UI to push data to a running listener, it lists every `MODEL_CONTEXT` node on the board, calls `slicedata?contextId=<id>` for each (full slice detail — commands/events/readmodels/screens/processors/specifications/comments), and writes the same `.slices/<context>/<slice>/slice.json`, `index.json`, and `context.json` layout — useful in CI or any context where nothing is listening on a port. It does not fetch screen images (those only arrive via `listen`'s push). If credentials are missing, it prompts the same way `init-config` does. `--slice-id`/`--slice-title` still fetch and persist everything, then just print the one you asked about.
241
+ `fetch` is the pull-based counterpart to `listen`: instead of waiting for the board UI to push data to a running listener, it calls `slicedata?contextName=<name>` for the required `--context` (full slice detail — commands/events/readmodels/screens/processors/specifications/comments), and writes the same `.slices/<context>/<slice>/slice.json`, `index.json`, and `context.json` layout — useful in CI or any context where nothing is listening on a port. It does not fetch screen images (those only arrive via `listen`'s push). Unlike every other command, `fetch` also works with no kit installed at all — it only needs credentials, not kit-specific files — and, unique to modeling-kit, writes `.slices/` to the project root instead of nesting it under `.agent-modeling-kit/`, since nothing reads it from there (modeling-kit has no `code-export.mjs`/`listen`). If credentials are missing, it prompts the same way `init-config` does. `--slice-id`/`--slice-title` still fetch and persist the whole context, then just print the one slice you asked about.
242
242
 
243
243
  ### Uninstall
244
244
 
package/cli.js CHANGED
@@ -734,8 +734,15 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
734
734
  }
735
735
 
736
736
  const stillMissing = force || requiredFields.some((f) => !config[f]);
737
+ // Sole gate on the persist step below — 'instructions'/'skip' and a paste that
738
+ // couldn't be parsed all explicitly tell the user nothing was saved, so the
739
+ // final write must not run for them (it used to run unconditionally, silently
740
+ // writing out whatever `config` happened to be — `{}` on a first run — which
741
+ // contradicted those messages and left behind a bogus config.json).
742
+ let skipWrite = false;
737
743
  if (stillMissing && print) {
738
744
  console.log('\n ℹ️ --print — skipping credential prompt, missing fields must be set via flags, EVENTMODELERS_* env vars, or config.json');
745
+ skipWrite = true;
739
746
  } else if (stillMissing) {
740
747
  const choice = await selectPrompt('How do you want to configure credentials?', [
741
748
  { label: 'Paste values copied from app.eventmodelers.ai/account', value: 'paste' },
@@ -751,12 +758,10 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
751
758
  const parsed = parseCredentialsPaste(pasted, requiredFields);
752
759
  if (parsed) {
753
760
  config = { ...config, ...parsed };
754
- if (!config.baseUrl) config.baseUrl = DEFAULT_BASE_URL;
755
- writeFileSync(configPath, JSON.stringify(config, null, 2));
756
- console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
757
761
  } else {
758
762
  console.log(`\n ⚠️ Couldn't make sense of that paste — nothing was saved.`);
759
763
  console.log(` Paste it into ${relative(targetDir, configPath)} yourself, or use /connect later.`);
764
+ skipWrite = true;
760
765
  }
761
766
  } else if (choice === 'manual') {
762
767
  console.log('\n🔑 Enter your Eventmodelers credentials:\n');
@@ -766,9 +771,6 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
766
771
  const boardId = await prompt(` Board ID${boardIdOptional ? ' (optional)' : ''}: `);
767
772
  if (boardId) config.boardId = boardId;
768
773
  config.token = await prompt(' Token: ');
769
- if (!config.baseUrl) config.baseUrl = DEFAULT_BASE_URL;
770
- writeFileSync(configPath, JSON.stringify(config, null, 2));
771
- console.log(`\n ✓ Credentials saved to ${relative(targetDir, configPath)}`);
772
774
  } else if (choice === 'instructions') {
773
775
  console.log(`\n Paste your credentials into:\n`);
774
776
  console.log(` ${configPath}`);
@@ -778,8 +780,10 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
778
780
  const sample = ` {\n "token": "...",\n "boardId": "...",\n "organizationId": "...",\n "baseUrl": "https://api.eventmodelers.ai"\n }\n`;
779
781
  console.log(sample);
780
782
  console.log(' Then re-run this installer, or just run the agent afterwards.\n');
783
+ skipWrite = true;
781
784
  } else {
782
785
  console.log('\n ℹ️ Skipped — use /connect in Claude Code to add credentials later');
786
+ skipWrite = true;
783
787
  }
784
788
  } else {
785
789
  console.log('\n ✓ Config already present — skipping credential prompt');
@@ -791,8 +795,10 @@ async function configureCredentials({ config, configPath, targetDir, requiredFie
791
795
  config.baseUrl = DEFAULT_BASE_URL;
792
796
  }
793
797
 
794
- writeFileSync(configPath, JSON.stringify(config, null, 2));
795
- console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
798
+ if (!skipWrite) {
799
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
800
+ console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
801
+ }
796
802
  return config;
797
803
  }
798
804
 
@@ -1457,12 +1463,19 @@ program
1457
1463
 
1458
1464
  program
1459
1465
  .command('fetch')
1460
- .description('Pull full slice detail from every context on the board via the slicedata API and write it into .slices/ — the pull-based counterpart to `listen`, without screen images')
1466
+ .description('Pull full slice detail for one context on the board via the slicedata API and write it into .slices/ — the pull-based counterpart to `listen`, without screen images')
1467
+ .requiredOption('--context <name>', 'Name of the MODEL_CONTEXT to fetch')
1461
1468
  .option('--slice-id <id>', 'After fetching, print just the slice with this id')
1462
1469
  .option('--slice-title <title>', 'After fetching, print just the slice with this title (case-insensitive)')
1463
1470
  .action(async (opts, command) => {
1464
1471
  const cwd = process.cwd();
1465
1472
  const kitDir = findInstalledKitDir(cwd);
1473
+ // modeling-kit has no code-export.mjs (useShared:false, no `listen`) and no
1474
+ // skill reads a nested .agent-modeling-kit/.slices/ — so nothing expects fetch's
1475
+ // output there either. Every other kit dir does have a code-export.mjs that
1476
+ // hardcodes its .slices/ next to itself, so those still nest to stay
1477
+ // interchangeable with what `listen` produces.
1478
+ const slicesKitDir = kitDir?.endsWith(MODELING_KIT.kitDirName) ? null : kitDir;
1466
1479
  const globalOpts = command.optsWithGlobals();
1467
1480
  const explicitConfig = globalOpts.config;
1468
1481
  const effective = loadEffectiveConfig(cwd, kitDir, explicitConfig);
@@ -1496,7 +1509,7 @@ program
1496
1509
  if (requiredFields.some((f) => !cfg[f])) await promptForCredentials();
1497
1510
 
1498
1511
  try {
1499
- await runFetch({ cwd, kitDir, cfg, opts });
1512
+ await runFetch({ cwd, kitDir: slicesKitDir, cfg, opts });
1500
1513
  } catch (err) {
1501
1514
  if (!(err instanceof FetchAuthError)) throw err;
1502
1515
  // Present but wrong, not missing — the connect skill's Step 4 (Verify) treats
@@ -1506,7 +1519,7 @@ program
1506
1519
  if (err.status === 404) delete cfg.boardId;
1507
1520
  else delete cfg.token;
1508
1521
  await promptForCredentials();
1509
- await runFetch({ cwd, kitDir, cfg, opts });
1522
+ await runFetch({ cwd, kitDir: slicesKitDir, cfg, opts });
1510
1523
  }
1511
1524
  });
1512
1525
 
package/lib/fetch.js CHANGED
@@ -45,11 +45,13 @@ function sliceFolderName(title) {
45
45
  return (title ?? '').replaceAll(' ', '').replaceAll('slice:', '').toLowerCase();
46
46
  }
47
47
 
48
- // Pulls full slice detail from every context on a board and writes it into
48
+ // Pulls full slice detail from a single context on a board and writes it into
49
49
  // .slices/, mirroring the layout code-export.mjs's /api/generate handler produces
50
50
  // (minus screen images, which only ever arrive via that push-based listener).
51
51
  //
52
- // { cwd, kitDir, cfg: { token, organizationId, boardId, baseUrl }, opts: { sliceId?, sliceTitle? } }
52
+ // kitDir here is null for modeling-kit (see cli.js's fetch action) nothing
53
+ // nests .slices/ under .agent-modeling-kit/, so it lands at cwd instead.
54
+ // { cwd, kitDir, cfg: { token, organizationId, boardId, baseUrl }, opts: { context, sliceId?, sliceTitle? } }
53
55
  export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
54
56
  const baseUrl = cfg.baseUrl || DEFAULT_BASE_URL;
55
57
  const headers = { 'x-token': cfg.token, 'x-board-id': cfg.boardId, 'x-user-id': 'cli-fetch' };
@@ -72,51 +74,42 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
72
74
  return res.json();
73
75
  }
74
76
 
75
- console.log(`▶ Fetching slices from ${baseUrl} (board ${cfg.boardId})...`);
77
+ console.log(`▶ Fetching context "${opts.context}" from ${baseUrl} (board ${cfg.boardId})...`);
76
78
 
77
- // No dedicated "list contexts" endpointMODEL_CONTEXT nodes are the contexts,
78
- // same as how the connect skill lists CHAPTER nodes for its health check.
79
- const contextNodes = await fetchJson(
80
- `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/nodes?type=MODEL_CONTEXT`,
81
- 'nodes?type=MODEL_CONTEXT',
82
- );
83
- if (!contextNodes?.length) {
84
- console.log('ℹ️ No contexts found on this board.');
85
- return;
86
- }
79
+ // Falls back to cwd when no kit is installed fetch doesn't need kit-specific
80
+ // files, just somewhere to write .slices/.
81
+ const SLICES_DIR = join(kitDir || cwd, '.slices');
87
82
 
88
83
  // /slicedata (buildSliceData) is per-context and returns full slice detail —
89
- // commands/events/readmodels/screens/processors/specifications/comments — unlike
90
- // the lightweight /slicedata/slices summary. There's no "all contexts in one
91
- // call" variant, so fetch each context's full data and merge client-side.
92
- const allSlices = [];
93
- for (const node of contextNodes) {
94
- const contextName = node.meta?.title ?? node.node?.data?.title ?? '';
95
- // contextId is the normal path (we already have the node id); contextName is
96
- // the fallback the endpoint itself supports when an id can't be resolved.
97
- const contextQuery = node.id ? `contextId=${encodeURIComponent(node.id)}` : `contextName=${encodeURIComponent(contextName)}`;
98
- const { slices } = await fetchJson(
99
- `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${contextQuery}`,
100
- `slicedata?${contextQuery}`,
101
- );
102
- allSlices.push(...slices);
84
+ // commands/events/readmodels/screens/processors/specifications/comments. It
85
+ // resolves contextName by exact match, so no separate "list contexts" lookup
86
+ // is needed first.
87
+ const contextQuery = `contextName=${encodeURIComponent(opts.context)}`;
88
+ const payload = await fetchJson(
89
+ `${baseUrl}/api/org/${cfg.organizationId}/boards/${cfg.boardId}/slicedata?${contextQuery}`,
90
+ `slicedata?${contextQuery}`,
91
+ );
92
+ const { slices: allSlices } = payload;
93
+
94
+ if (allSlices.length) {
95
+ // Mirrors code-export.mjs's /api/generate: a raw per-context payload dump at
96
+ // .slices/<context>/config.json, alongside the per-slice output below — kept
97
+ // for parity with `listen` even though nothing in this repo reads it back.
98
+ const contextSlug = slugify(allSlices[0]?.context || opts.context || 'default') || 'default';
99
+ const baseFolder = join(SLICES_DIR, contextSlug);
100
+ mkdirSync(baseFolder, { recursive: true });
101
+ writeFileSync(join(baseFolder, 'config.json'), JSON.stringify(payload, null, 2));
103
102
  }
104
103
 
105
104
  if (!allSlices.length) {
106
- console.log('ℹ️ No slices found on this board.');
105
+ console.log(`ℹ️ No slices found in context "${opts.context}".`);
107
106
  return;
108
107
  }
109
108
 
110
- // Falls back to cwd when no kit is installed — fetch doesn't need kit-specific
111
- // files, just somewhere to write .slices/.
112
- const SLICES_DIR = join(kitDir || cwd, '.slices');
113
- const contextNames = new Set();
114
-
115
109
  for (const slice of allSlices) {
116
110
  // buildSliceData names this field `context`, not `contextName` (that's the
117
111
  // /slicedata/slices summary endpoint's field) — read the one this endpoint sends.
118
- const contextName = slice.context || 'default';
119
- contextNames.add(contextName);
112
+ const contextName = slice.context || opts.context || 'default';
120
113
  const contextSlug = slugify(contextName) || 'default';
121
114
  const baseFolder = join(SLICES_DIR, contextSlug);
122
115
  const sliceFolder = sliceFolderName(slice.title);
@@ -151,14 +144,9 @@ export async function runFetch({ cwd, kitDir, cfg, opts = {} }) {
151
144
  writeFileSync(indexFile, JSON.stringify(sliceIndices, null, 2));
152
145
  }
153
146
 
154
- // A single shared current_context.json only makes sense when everything fetched
155
- // belongs to one context — with several, any one choice would be arbitrary, so
156
- // leave whatever `listen`/a prior fetch already wrote there untouched.
157
- if (contextNames.size === 1) {
158
- writeFileSync(join(SLICES_DIR, 'current_context.json'), JSON.stringify({ name: [...contextNames][0] }, null, 2));
159
- }
147
+ writeFileSync(join(SLICES_DIR, 'current_context.json'), JSON.stringify({ name: opts.context }, null, 2));
160
148
 
161
- console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} across ${contextNames.size} context${contextNames.size === 1 ? '' : 's'} → ${relative(cwd, SLICES_DIR)}/`);
149
+ console.log(`✅ Fetched ${allSlices.length} slice${allSlices.length === 1 ? '' : 's'} from context "${opts.context}" → ${relative(cwd, SLICES_DIR)}/`);
162
150
 
163
151
  // --slice-id/--slice-title mirror load-slice's Step 4/5 — fetch+persist everything
164
152
  // regardless, then just report the one the caller asked about.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {