@eventmodelers/cli 0.0.6 → 0.0.8
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 +42 -9
- package/cli.js +633 -175
- package/package.json +1 -1
- package/{stacks/node/templates → shared}/build-kit/README.md +3 -3
- package/{stacks/axon/templates → shared}/build-kit/lib/ralph.js +10 -3
- package/shared/build-kit/ralph-ollama.js +2 -2
- package/{stacks/node/templates → shared}/build-kit/ralph.sh +1 -1
- package/shared/build-kit/realtime-agent.js +1 -1
- package/{stacks/supabase/templates/.claude → shared}/skills/connect/SKILL.md +10 -5
- package/stacks/cratis-csharp/templates/build-kit/lib/AGENT.md +1 -0
- package/stacks/modeling-kit/templates/kit/README.md +79 -0
- package/stacks/modeling-kit/templates/kit/lib/ralph.js +9 -2
- package/stacks/modeling-kit/templates/kit/ralph.sh +1 -1
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/axon/templates/.claude/skills/connect/SKILL.md +0 -178
- package/stacks/axon/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +0 -611
- package/stacks/axon/templates/.claude/skills/update-slice-status/SKILL.md +0 -105
- package/stacks/axon/templates/build-kit/ralph.sh +0 -98
- package/stacks/cratis-csharp/templates/.claude/skills/connect/SKILL.md +0 -178
- package/stacks/cratis-csharp/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +0 -609
- package/stacks/cratis-csharp/templates/.claude/skills/update-slice-status/SKILL.md +0 -105
- package/stacks/cratis-csharp/templates/build-kit/lib/agent.sh +0 -20
- package/stacks/cratis-csharp/templates/build-kit/lib/ralph.js +0 -302
- package/stacks/cratis-csharp/templates/build-kit/ralph-claude.js +0 -37
- package/stacks/cratis-csharp/templates/build-kit/ralph.sh +0 -98
- package/stacks/modeling-kit/templates/.claude/skills/connect/SKILL.md +0 -178
- package/stacks/modeling-kit/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +0 -441
- package/stacks/modeling-kit/templates/.claude/skills/update-slice-status/SKILL.md +0 -110
- package/stacks/modeling-kit/templates/kit/lib/agent.sh +0 -20
- package/stacks/modeling-kit/templates/kit/lib/ollama-agent.js +0 -147
- package/stacks/modeling-kit/templates/kit/ralph-ollama.js +0 -38
- package/stacks/modeling-kit/templates/kit/realtime-agent.js +0 -18
- package/stacks/node/templates/.claude/skills/connect/SKILL.md +0 -178
- package/stacks/node/templates/build-kit/lib/agent.sh +0 -20
- package/stacks/node/templates/build-kit/lib/ralph.js +0 -369
- package/stacks/node/templates/build-kit/ralph-claude.js +0 -44
- package/stacks/supabase/templates/.claude/skills/learn-eventmodelers-api/SKILL.md +0 -628
- package/stacks/supabase/templates/.claude/skills/update-slice-status/SKILL.md +0 -110
- package/stacks/supabase/templates/build-kit/README.md +0 -86
- package/stacks/supabase/templates/build-kit/lib/agent.sh +0 -20
- package/stacks/supabase/templates/build-kit/lib/ralph.js +0 -369
- package/stacks/supabase/templates/build-kit/ralph-claude.js +0 -44
- package/stacks/supabase/templates/build-kit/ralph.sh +0 -98
- /package/{stacks/axon/templates → shared}/build-kit/lib/agent.sh +0 -0
- /package/{stacks/axon/templates → shared}/build-kit/ralph-claude.js +0 -0
- /package/{stacks/node/templates/.claude → shared}/skills/learn-eventmodelers-api/SKILL.md +0 -0
- /package/{stacks/node/templates/.claude → shared}/skills/update-slice-status/SKILL.md +0 -0
package/cli.js
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
5
|
import { dirname, join, relative, resolve, sep } from 'path';
|
|
6
6
|
import {
|
|
7
7
|
existsSync,
|
|
8
8
|
mkdirSync,
|
|
9
9
|
cpSync,
|
|
10
|
+
copyFileSync,
|
|
10
11
|
rmSync,
|
|
11
12
|
readdirSync,
|
|
12
13
|
writeFileSync,
|
|
13
14
|
readFileSync,
|
|
14
15
|
appendFileSync,
|
|
15
16
|
} from 'fs';
|
|
16
|
-
import { execSync } from 'child_process';
|
|
17
|
+
import { execSync, spawn } from 'child_process';
|
|
17
18
|
import { createInterface, emitKeypressEvents, moveCursor, clearScreenDown } from 'readline';
|
|
18
19
|
import { homedir } from 'os';
|
|
19
20
|
|
|
20
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
21
22
|
const __dirname = dirname(__filename);
|
|
22
23
|
|
|
23
|
-
// Each
|
|
24
|
-
//
|
|
25
|
-
// (ralph
|
|
26
|
-
//
|
|
24
|
+
// Each stack is a template set under stacks/<key>/templates/{.claude,root,<kitSubdir>}.
|
|
25
|
+
// Stacks with useShared:true also get shared/build-kit/* copied into their kit dir
|
|
26
|
+
// first (ralph.js, ralph-claude.js, ralph-ollama.js, ralph.sh, realtime-agent.js,
|
|
27
|
+
// code-export.mjs, lib/agent.sh, lib/ollama-agent.js, package.json, README.md) —
|
|
28
|
+
// those files have no per-stack content, so they live once instead of being
|
|
29
|
+
// copy-pasted into every stack (that copy-pasting is exactly how they drifted out
|
|
30
|
+
// of sync before: a bugfix or default landing in one stack's copy but not another's).
|
|
31
|
+
// Each stack's own templates/<kitSubdir>/* is then overlaid on top for genuine
|
|
32
|
+
// per-stack differences (ralph-claude.js's build tooling, lib/prompt.md, etc.) —
|
|
33
|
+
// see stacks/modeling-kit for an example of a kit with fully different entry-point
|
|
34
|
+
// logic that still reuses the shared runtime pieces.
|
|
27
35
|
const STACKS = {
|
|
28
36
|
node: {
|
|
29
37
|
label: 'Node.js / TypeScript',
|
|
@@ -62,7 +70,7 @@ const MODELING_KIT = {
|
|
|
62
70
|
label: 'Modeling only — skills + agent loop, no backend scaffold',
|
|
63
71
|
kitSubdir: 'kit',
|
|
64
72
|
kitDirName: '.agent-modeling-kit',
|
|
65
|
-
useShared:
|
|
73
|
+
useShared: true,
|
|
66
74
|
needsBoardId: false,
|
|
67
75
|
};
|
|
68
76
|
|
|
@@ -88,6 +96,112 @@ const MCP_MANUAL_CLIENTS = [
|
|
|
88
96
|
{ label: 'Windsurf', hint: (url) => `Add an HTTP MCP server pointing at ${url} in Windsurf's MCP settings` },
|
|
89
97
|
];
|
|
90
98
|
|
|
99
|
+
// Other AI agent hosts can read our Event Modeling skills without us duplicating
|
|
100
|
+
// skill content per host: a thin stub file in the host's own command/workflow
|
|
101
|
+
// directory tells it to go read the canonical .claude/skills/<name>/SKILL.md and
|
|
102
|
+
// follow it — the same pattern spec-kitty uses (verified directly against its repo,
|
|
103
|
+
// not just its docs: every "stub" host below reads plain Markdown, no per-host
|
|
104
|
+
// format transform needed). Codex CLI, Mistral Vibe, Pi, and Letta Code share one
|
|
105
|
+
// convention that already matches our native SKILL.md format, so those get the
|
|
106
|
+
// real file copied as-is instead of a stub.
|
|
107
|
+
const AGENT_HOSTS = {
|
|
108
|
+
cursor: { label: 'Cursor', dir: '.cursor/commands', kind: 'stub' },
|
|
109
|
+
windsurf: { label: 'Windsurf', dir: '.windsurf/workflows', kind: 'stub' },
|
|
110
|
+
gemini: { label: 'Google Gemini CLI', dir: '.gemini/commands', kind: 'stub' },
|
|
111
|
+
qwen: { label: 'Qwen Code', dir: '.qwen/commands', kind: 'stub' },
|
|
112
|
+
opencode: { label: 'OpenCode', dir: '.opencode/command', kind: 'stub' },
|
|
113
|
+
copilot: { label: 'GitHub Copilot', dir: '.github/prompts', kind: 'stub' },
|
|
114
|
+
amazonq: { label: 'Amazon Q (legacy)', dir: '.amazonq/prompts', kind: 'stub' },
|
|
115
|
+
kiro: { label: 'Kiro', dir: '.kiro/prompts', kind: 'stub' },
|
|
116
|
+
kilocode: { label: 'Kilocode', dir: '.kilocode/workflows', kind: 'stub' },
|
|
117
|
+
augment: { label: 'Augment Code', dir: '.augment/commands', kind: 'stub' },
|
|
118
|
+
antigravity: { label: 'Google Antigravity', dir: '.agent/workflows', kind: 'stub' },
|
|
119
|
+
codex: {
|
|
120
|
+
label: 'Codex CLI / Mistral Vibe / Pi / Letta Code (shared .agents/skills/ convention)',
|
|
121
|
+
dir: '.agents/skills',
|
|
122
|
+
kind: 'skill-package',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function agentHostStub(skillName) {
|
|
127
|
+
return `# ${skillName} (eventmodelers)\n\nThis host should read the canonical skill at:\n\n**\`.claude/skills/${skillName}/SKILL.md\`**\n\nFollow those instructions when this command is invoked.\n`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Writes stub/skill-package files for the requested hosts and records exactly what
|
|
131
|
+
// it wrote into every installed kit dir's manifest — same convention as
|
|
132
|
+
// `mcpRegistered` — so `uninstall` can remove precisely these files later.
|
|
133
|
+
async function configureAgentHosts({ hosts, global: useGlobal } = {}) {
|
|
134
|
+
const targetDir = process.cwd();
|
|
135
|
+
const skillsDir = useGlobal ? join(homedir(), '.claude', 'skills') : join(targetDir, '.claude', 'skills');
|
|
136
|
+
|
|
137
|
+
if (!existsSync(skillsDir)) {
|
|
138
|
+
console.error(`❌ No skills found at ${relative(targetDir, skillsDir) || skillsDir} — run \`eventmodelers init\` or \`init-modeling\` first.`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const skills = readdirSync(skillsDir).filter((f) => existsSync(join(skillsDir, f, 'SKILL.md')));
|
|
142
|
+
if (!skills.length) {
|
|
143
|
+
console.error('❌ No installed skills found — nothing to expose.');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let hostKeys = hosts;
|
|
148
|
+
if (!hostKeys || !hostKeys.length) {
|
|
149
|
+
console.log('\nAvailable agent hosts:');
|
|
150
|
+
Object.entries(AGENT_HOSTS).forEach(([key, h]) => console.log(` ${key.padEnd(12)} ${h.label}`));
|
|
151
|
+
const answer = await prompt('\nWhich hosts? (comma-separated keys, or "all"): ');
|
|
152
|
+
hostKeys = answer.trim() === 'all'
|
|
153
|
+
? Object.keys(AGENT_HOSTS)
|
|
154
|
+
: answer.split(',').map((s) => s.trim()).filter(Boolean);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const unknown = hostKeys.filter((k) => !AGENT_HOSTS[k]);
|
|
158
|
+
if (unknown.length) {
|
|
159
|
+
console.error(`❌ Unknown host(s): ${unknown.join(', ')}. Available: ${Object.keys(AGENT_HOSTS).join(', ')}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
if (!hostKeys.length) {
|
|
163
|
+
console.log('ℹ️ No hosts selected — nothing to do.');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log(`\n📦 Exposing ${skills.length} skill(s) to ${hostKeys.length} host(s)...`);
|
|
168
|
+
const generatedFiles = [];
|
|
169
|
+
|
|
170
|
+
for (const key of hostKeys) {
|
|
171
|
+
const host = AGENT_HOSTS[key];
|
|
172
|
+
if (host.kind === 'stub') {
|
|
173
|
+
const hostDir = join(targetDir, host.dir);
|
|
174
|
+
mkdirSync(hostDir, { recursive: true });
|
|
175
|
+
for (const skill of skills) {
|
|
176
|
+
const filePath = join(hostDir, `${skill}.md`);
|
|
177
|
+
writeFileSync(filePath, agentHostStub(skill));
|
|
178
|
+
generatedFiles.push(relative(targetDir, filePath));
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// skill-package: copy the real SKILL.md verbatim — already the native format.
|
|
182
|
+
for (const skill of skills) {
|
|
183
|
+
const pkgDir = join(targetDir, host.dir, `eventmodelers.${skill}`);
|
|
184
|
+
mkdirSync(pkgDir, { recursive: true });
|
|
185
|
+
const dest = join(pkgDir, 'SKILL.md');
|
|
186
|
+
copyFileSync(join(skillsDir, skill, 'SKILL.md'), dest);
|
|
187
|
+
generatedFiles.push(relative(targetDir, dest));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
console.log(` ✓ ${host.label} (${host.dir}/)`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const dirName of KIT_DIR_NAMES) {
|
|
194
|
+
const manifestPath = join(targetDir, dirName, '.eventmodelers', 'install-manifest.json');
|
|
195
|
+
if (existsSync(manifestPath)) {
|
|
196
|
+
const manifest = readJsonSafe(manifestPath);
|
|
197
|
+
manifest.agentHostFiles = [...new Set([...(manifest.agentHostFiles || []), ...generatedFiles])];
|
|
198
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(`\n✅ Done — ${generatedFiles.length} file(s) written.`);
|
|
203
|
+
}
|
|
204
|
+
|
|
91
205
|
// Every config field can also be set via an EVENTMODELERS_* env var — these always
|
|
92
206
|
// win over whatever's in config.json, so scripted/CI installs can skip prompts entirely.
|
|
93
207
|
const ENV_CONFIG_MAP = {
|
|
@@ -156,9 +270,33 @@ async function promptPasteBlock() {
|
|
|
156
270
|
return lines.join('\n').trim();
|
|
157
271
|
}
|
|
158
272
|
|
|
273
|
+
// Platform base URL when a config doesn't specify one — every install method
|
|
274
|
+
// (paste, manual entry, or a hand-edited config.json) should fall back to this
|
|
275
|
+
// rather than silently disabling platform sync.
|
|
276
|
+
const DEFAULT_BASE_URL = 'https://api.eventmodelers.ai';
|
|
277
|
+
|
|
278
|
+
// Canonical order the account page pastes values in, regardless of which fields a
|
|
279
|
+
// given stack actually requires — a modeling-kit install (no boardId required) still
|
|
280
|
+
// gets a paste containing all 4 fields, so we must not drop the ones we don't need.
|
|
281
|
+
const PASTE_FIELD_ORDER = ['organizationId', 'boardId', 'token'];
|
|
282
|
+
|
|
283
|
+
// Values are sometimes copied with a "field=" prefix still attached (e.g. lifted
|
|
284
|
+
// straight out of a query string) — and occasionally under the wrong JSON key
|
|
285
|
+
// entirely, e.g. { "organizationId": "token=abc..." }. When a value carries its own
|
|
286
|
+
// "field=" prefix, that's a more reliable source of truth than whatever key/position
|
|
287
|
+
// it was pasted under, so it wins.
|
|
288
|
+
const PASTE_FIELD_ALIASES = { organizationId: 'organizationId', orgId: 'organizationId', boardId: 'boardId', token: 'token', baseUrl: 'baseUrl' };
|
|
289
|
+
|
|
290
|
+
function splitEmbeddedField(value) {
|
|
291
|
+
if (typeof value !== 'string') return null;
|
|
292
|
+
const match = value.match(/^([a-zA-Z]+)=(.+)$/);
|
|
293
|
+
if (!match) return null;
|
|
294
|
+
const field = PASTE_FIELD_ALIASES[match[1]];
|
|
295
|
+
return field ? { field, value: match[2] } : null;
|
|
296
|
+
}
|
|
297
|
+
|
|
159
298
|
// Accepts either a JSON object (as copied from the account page) or a comma-separated
|
|
160
|
-
// line of values, in
|
|
161
|
-
// with an optional base URL anywhere in the list.
|
|
299
|
+
// line of values, in PASTE_FIELD_ORDER, with an optional base URL anywhere in the list.
|
|
162
300
|
function parseCredentialsPaste(text, requiredFields) {
|
|
163
301
|
const trimmed = text.trim();
|
|
164
302
|
if (!trimmed) return null;
|
|
@@ -166,11 +304,18 @@ function parseCredentialsPaste(text, requiredFields) {
|
|
|
166
304
|
try {
|
|
167
305
|
const obj = JSON.parse(trimmed);
|
|
168
306
|
if (obj && typeof obj === 'object') {
|
|
307
|
+
const raw = {};
|
|
308
|
+
if (obj.organizationId || obj.orgId) raw.organizationId = obj.organizationId || obj.orgId;
|
|
309
|
+
if (obj.boardId) raw.boardId = obj.boardId;
|
|
310
|
+
if (obj.token) raw.token = obj.token;
|
|
311
|
+
if (obj.baseUrl) raw.baseUrl = obj.baseUrl;
|
|
312
|
+
|
|
169
313
|
const result = {};
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
314
|
+
for (const [outerKey, value] of Object.entries(raw)) {
|
|
315
|
+
const embedded = splitEmbeddedField(value);
|
|
316
|
+
if (embedded) result[embedded.field] = embedded.value;
|
|
317
|
+
else result[outerKey] = value;
|
|
318
|
+
}
|
|
174
319
|
if (requiredFields.every((f) => result[f])) return result;
|
|
175
320
|
return null;
|
|
176
321
|
}
|
|
@@ -182,13 +327,26 @@ function parseCredentialsPaste(text, requiredFields) {
|
|
|
182
327
|
const result = {};
|
|
183
328
|
const remaining = [];
|
|
184
329
|
for (const v of values) {
|
|
185
|
-
if (/^https?:\/\//i.test(v))
|
|
330
|
+
if (/^https?:\/\//i.test(v)) {
|
|
331
|
+
result.baseUrl = v;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const embedded = splitEmbeddedField(v);
|
|
335
|
+
if (embedded) result[embedded.field] = embedded.value;
|
|
186
336
|
else remaining.push(v);
|
|
187
337
|
}
|
|
188
|
-
if (remaining.length < requiredFields.length) return null;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
338
|
+
if (remaining.length < requiredFields.length - Object.keys(result).length) return null;
|
|
339
|
+
// If more values were pasted than this stack strictly requires (e.g. a boardId
|
|
340
|
+
// in a modeling-kit paste), use the full canonical order so the extra field is
|
|
341
|
+
// still captured instead of being mis-zipped against the shorter requiredFields
|
|
342
|
+
// list and silently dropped/misassigned.
|
|
343
|
+
const fieldOrder = remaining.length > requiredFields.length ? PASTE_FIELD_ORDER : requiredFields;
|
|
344
|
+
let i = 0;
|
|
345
|
+
for (const field of fieldOrder) {
|
|
346
|
+
if (result[field]) continue; // already resolved via an embedded "field=" prefix
|
|
347
|
+
if (remaining[i] !== undefined) result[field] = remaining[i];
|
|
348
|
+
i++;
|
|
349
|
+
}
|
|
192
350
|
|
|
193
351
|
return requiredFields.every((f) => result[f]) ? result : null;
|
|
194
352
|
}
|
|
@@ -254,9 +412,15 @@ function findConfigInParents(startDir) {
|
|
|
254
412
|
const candidate = join(dir, '.eventmodelers', 'config.json');
|
|
255
413
|
if (existsSync(candidate)) return candidate;
|
|
256
414
|
const parent = dirname(dir);
|
|
257
|
-
if (parent === dir)
|
|
415
|
+
if (parent === dir) break;
|
|
258
416
|
dir = parent;
|
|
259
417
|
}
|
|
418
|
+
// Last resort: the walk above only passes through $HOME if the project happens
|
|
419
|
+
// to live under it. A project outside $HOME (e.g. /tmp/foo) never sees it, so
|
|
420
|
+
// check it explicitly — this is where `init-config --global` writes account-wide
|
|
421
|
+
// defaults (organizationId/token) shared across every project.
|
|
422
|
+
const globalCandidate = join(homedir(), '.eventmodelers', 'config.json');
|
|
423
|
+
return existsSync(globalCandidate) ? globalCandidate : null;
|
|
260
424
|
}
|
|
261
425
|
|
|
262
426
|
function readJsonSafe(path) {
|
|
@@ -311,6 +475,7 @@ function findAllInstalledKitDirs(cwd) {
|
|
|
311
475
|
function copyDirContents(srcDir, destDir, { skip = [] } = {}) {
|
|
312
476
|
if (!existsSync(srcDir)) return;
|
|
313
477
|
mkdirSync(destDir, { recursive: true });
|
|
478
|
+
let count = 0;
|
|
314
479
|
for (const item of readdirSync(srcDir)) {
|
|
315
480
|
if (skip.includes(item)) continue;
|
|
316
481
|
const src = join(srcDir, item);
|
|
@@ -319,8 +484,9 @@ function copyDirContents(srcDir, destDir, { skip = [] } = {}) {
|
|
|
319
484
|
recursive: true,
|
|
320
485
|
filter: (s) => !relative(src, s).split(sep).includes('node_modules'),
|
|
321
486
|
});
|
|
322
|
-
|
|
487
|
+
count++;
|
|
323
488
|
}
|
|
489
|
+
if (count) console.log(` ✓ Installed ${count} item${count === 1 ? '' : 's'} into ${relative(process.cwd(), destDir) || '.'}`);
|
|
324
490
|
}
|
|
325
491
|
|
|
326
492
|
async function resolveStack(cliStack) {
|
|
@@ -345,6 +511,12 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
345
511
|
const targetDir = process.cwd();
|
|
346
512
|
const templatesSource = join(__dirname, 'stacks', stackKey, 'templates');
|
|
347
513
|
const sharedBuildKit = join(__dirname, 'shared', 'build-kit');
|
|
514
|
+
// Skills with no stack-specific content (connect, learn-eventmodelers-api,
|
|
515
|
+
// update-slice-status, ...) live once here instead of being copy-pasted into
|
|
516
|
+
// every stack's templates — that copy-pasting is exactly how they drifted out
|
|
517
|
+
// of sync with each other before (e.g. one stack's connect skill silently
|
|
518
|
+
// missing a bugfix another stack's copy had).
|
|
519
|
+
const sharedSkills = join(__dirname, 'shared', 'skills');
|
|
348
520
|
|
|
349
521
|
if (!existsSync(templatesSource)) {
|
|
350
522
|
console.error('❌ Templates directory not found at:', templatesSource);
|
|
@@ -355,18 +527,21 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
355
527
|
// Recorded into the install manifest (step 8) so `uninstall` can remove exactly
|
|
356
528
|
// these files later and nothing the user added independently.
|
|
357
529
|
const claudeSkillsSrc = join(templatesSource, '.claude', 'skills');
|
|
358
|
-
const installedSkills =
|
|
530
|
+
const installedSkills = [
|
|
531
|
+
...(existsSync(sharedSkills) ? readdirSync(sharedSkills) : []),
|
|
532
|
+
...(existsSync(claudeSkillsSrc) ? readdirSync(claudeSkillsSrc) : []),
|
|
533
|
+
];
|
|
359
534
|
let claudeExtras = [];
|
|
360
535
|
|
|
361
536
|
if (options.global) {
|
|
362
537
|
const globalSkillsDir = join(homedir(), '.claude', 'skills');
|
|
363
|
-
console.log('📦 Installing
|
|
364
|
-
|
|
538
|
+
console.log('📦 Installing skills globally...');
|
|
539
|
+
copyDirContents(sharedSkills, globalSkillsDir);
|
|
365
540
|
copyDirContents(claudeSkillsSrc, globalSkillsDir);
|
|
366
541
|
} else {
|
|
367
|
-
console.log('📦 Installing
|
|
368
|
-
console.log(' Copies skills and settings into .claude/ so Claude Code picks them up automatically.\n');
|
|
542
|
+
console.log('📦 Installing skills...');
|
|
369
543
|
copyDirContents(join(templatesSource, '.claude'), join(targetDir, '.claude'));
|
|
544
|
+
copyDirContents(sharedSkills, join(targetDir, '.claude', 'skills'));
|
|
370
545
|
claudeExtras = existsSync(join(templatesSource, '.claude'))
|
|
371
546
|
? readdirSync(join(templatesSource, '.claude')).filter((f) => f !== 'skills')
|
|
372
547
|
: [];
|
|
@@ -375,15 +550,14 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
375
550
|
// --- 2. Spread stack scaffold files into the project root ---
|
|
376
551
|
const rootSrc = join(templatesSource, 'root');
|
|
377
552
|
if (existsSync(rootSrc)) {
|
|
378
|
-
console.log('
|
|
553
|
+
console.log('📦 Installing project files...');
|
|
379
554
|
copyDirContents(rootSrc, targetDir);
|
|
380
555
|
}
|
|
381
556
|
|
|
382
557
|
// --- 3. Create the kit dir and install the agent runner ---
|
|
383
558
|
const kitDir = join(targetDir, stackCfg.kitDirName);
|
|
384
559
|
mkdirSync(kitDir, { recursive: true });
|
|
385
|
-
console.log(
|
|
386
|
-
console.log(' Sets up the Ralph agent loop, scripts, and configuration that drive realtime modeling.\n');
|
|
560
|
+
console.log(`📦 Installing agent kit into ${stackCfg.kitDirName}/...`);
|
|
387
561
|
|
|
388
562
|
if (stackCfg.useShared) {
|
|
389
563
|
copyDirContents(sharedBuildKit, kitDir);
|
|
@@ -400,8 +574,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
400
574
|
|
|
401
575
|
// --- 4. Install kit dependencies ---
|
|
402
576
|
if (existsSync(join(kitDir, 'package.json'))) {
|
|
403
|
-
console.log('
|
|
404
|
-
console.log(' Installs npm packages required by the agent scripts (e.g. websocket client, utilities).');
|
|
577
|
+
console.log('📦 Installing kit dependencies...');
|
|
405
578
|
try {
|
|
406
579
|
execSync('npm install', { cwd: kitDir, stdio: ['ignore', 'inherit', 'inherit'] });
|
|
407
580
|
console.log(' ✓ kit dependencies installed');
|
|
@@ -411,9 +584,7 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
411
584
|
}
|
|
412
585
|
|
|
413
586
|
// --- 5. Credentials ---
|
|
414
|
-
console.log('
|
|
415
|
-
console.log(' Stores your Organization ID (and Board ID, if this stack needs one) and token');
|
|
416
|
-
console.log(' so the agent can connect to app.eventmodelers.ai.\n');
|
|
587
|
+
console.log('🔐 Configuring credentials...');
|
|
417
588
|
|
|
418
589
|
// Written at the project root (not inside the kit dir) so a modeling-kit install
|
|
419
590
|
// and a build-kit install in the same project share one config.json instead of
|
|
@@ -421,9 +592,60 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
421
592
|
const configPath = options.configPath
|
|
422
593
|
? resolve(targetDir, options.configPath)
|
|
423
594
|
: join(targetDir, '.eventmodelers', 'config.json');
|
|
424
|
-
const configDir = dirname(configPath);
|
|
425
|
-
mkdirSync(configDir, { recursive: true });
|
|
426
595
|
|
|
596
|
+
const requiredFields = stackCfg.needsBoardId
|
|
597
|
+
? ['organizationId', 'boardId', 'token']
|
|
598
|
+
: ['organizationId', 'token'];
|
|
599
|
+
|
|
600
|
+
const effective = loadEffectiveConfig(targetDir, kitDir, options.configPath);
|
|
601
|
+
if (effective.sources.length > 1) {
|
|
602
|
+
console.log(`\n ✓ Found shared defaults in ${effective.sources[0]}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const config = await configureCredentials({
|
|
606
|
+
config: effective.config,
|
|
607
|
+
configPath,
|
|
608
|
+
targetDir,
|
|
609
|
+
requiredFields,
|
|
610
|
+
boardIdOptional: !stackCfg.needsBoardId,
|
|
611
|
+
overrides: options.credentialOverrides,
|
|
612
|
+
print: options.print,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// --- 6. Install manifest (drives precise `uninstall` later) ---
|
|
616
|
+
// Only the footprint listed here is ever removed by `uninstall` — the root
|
|
617
|
+
// scaffold (step 2) is real project source the user builds on, so it's
|
|
618
|
+
// deliberately left out and never touched by uninstall.
|
|
619
|
+
const manifestDir = join(kitDir, '.eventmodelers');
|
|
620
|
+
mkdirSync(manifestDir, { recursive: true });
|
|
621
|
+
writeFileSync(
|
|
622
|
+
join(manifestDir, 'install-manifest.json'),
|
|
623
|
+
JSON.stringify({ stack: stackKey, global: !!options.global, skills: installedSkills, claudeExtras, mcpRegistered: false }, null, 2),
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
console.log('\n✅ Done! Start your agent:\n');
|
|
627
|
+
console.log(' npx @eventmodelers/cli run (--ollama or --bash for other runners)\n');
|
|
628
|
+
console.log('Connect this project to an MCP client (Claude Code, VS Code, ...):\n');
|
|
629
|
+
console.log(` npx @eventmodelers/cli init-mcp\n`);
|
|
630
|
+
console.log('Expose these skills to other AI agent hosts (Cursor, Windsurf, Gemini CLI, Copilot, Codex CLI, Kiro, ...):\n');
|
|
631
|
+
console.log(` npx @eventmodelers/cli init-agents\n`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Extracted from installStack so `init-config` can reuse the exact same
|
|
635
|
+
// paste/manual/instructions/skip flow without also scaffolding a stack.
|
|
636
|
+
// `overrides` are values passed directly on the command line (--token, --board-id,
|
|
637
|
+
// --organization-id, --base-url) — the most explicit source available, so they
|
|
638
|
+
// win over both the config file and env vars before we even check what's missing.
|
|
639
|
+
async function configureCredentials({ config, configPath, targetDir, requiredFields, boardIdOptional, overrides = {}, print, skipGitignore = false }) {
|
|
640
|
+
config = { ...config };
|
|
641
|
+
for (const [field, value] of Object.entries(overrides)) {
|
|
642
|
+
if (value) config[field] = value;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const configDir = dirname(configPath);
|
|
646
|
+
mkdirSync(configDir, { recursive: true });
|
|
647
|
+
|
|
648
|
+
if (!skipGitignore) {
|
|
427
649
|
const gitignorePath = join(targetDir, '.gitignore');
|
|
428
650
|
const relConfigDir = relative(targetDir, configDir);
|
|
429
651
|
if (relConfigDir && !relConfigDir.startsWith('..')) {
|
|
@@ -437,157 +659,245 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
437
659
|
writeFileSync(gitignorePath, `${gitignoreEntry}\n`);
|
|
438
660
|
}
|
|
439
661
|
}
|
|
662
|
+
}
|
|
440
663
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
console.log('
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
{ label: 'Skip for now', value: 'skip' },
|
|
461
|
-
], 1);
|
|
462
|
-
|
|
463
|
-
if (choice === 'paste') {
|
|
464
|
-
console.log('\n Copy your credentials from https://app.eventmodelers.ai/account,');
|
|
465
|
-
console.log(' then paste them below and press Enter:\n');
|
|
466
|
-
const pasted = await promptPasteBlock();
|
|
467
|
-
const parsed = parseCredentialsPaste(pasted, requiredFields);
|
|
468
|
-
if (parsed) {
|
|
469
|
-
config = { ...config, ...parsed };
|
|
470
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
471
|
-
console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
|
|
472
|
-
} else {
|
|
473
|
-
console.log(`\n ⚠️ Couldn't make sense of that paste — nothing was saved.`);
|
|
474
|
-
console.log(` Paste it into ${relative(targetDir, configPath)} yourself, or use /connect later.`);
|
|
475
|
-
}
|
|
476
|
-
} else if (choice === 'manual') {
|
|
477
|
-
console.log('\n🔑 Enter your Eventmodelers credentials:\n');
|
|
478
|
-
config.organizationId = await prompt(' Organization ID: ');
|
|
479
|
-
if (stackCfg.needsBoardId) {
|
|
480
|
-
config.boardId = await prompt(' Board ID: ');
|
|
481
|
-
}
|
|
482
|
-
config.token = await prompt(' Token: ');
|
|
664
|
+
const stillMissing = requiredFields.some((f) => !config[f]);
|
|
665
|
+
if (stillMissing && print) {
|
|
666
|
+
console.log('\n ℹ️ --print — skipping credential prompt, missing fields must be set via flags, EVENTMODELERS_* env vars, or config.json');
|
|
667
|
+
} else if (stillMissing) {
|
|
668
|
+
const choice = await selectPrompt('How do you want to configure credentials?', [
|
|
669
|
+
{ label: 'Paste values copied from app.eventmodelers.ai/account', value: 'paste' },
|
|
670
|
+
{ label: 'Enter values one by one', value: 'manual' },
|
|
671
|
+
{ label: 'Get instructions for configuring later', value: 'instructions' },
|
|
672
|
+
{ label: 'Skip for now', value: 'skip' },
|
|
673
|
+
], 1);
|
|
674
|
+
|
|
675
|
+
if (choice === 'paste') {
|
|
676
|
+
console.log('\n Copy your credentials from https://app.eventmodelers.ai/account,');
|
|
677
|
+
console.log(' then paste them below and press Enter:\n');
|
|
678
|
+
const pasted = await promptPasteBlock();
|
|
679
|
+
const parsed = parseCredentialsPaste(pasted, requiredFields);
|
|
680
|
+
if (parsed) {
|
|
681
|
+
config = { ...config, ...parsed };
|
|
682
|
+
if (!config.baseUrl) config.baseUrl = DEFAULT_BASE_URL;
|
|
483
683
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
484
|
-
console.log(`\n ✓
|
|
485
|
-
} else if (choice === 'instructions') {
|
|
486
|
-
console.log(`\n Paste your credentials into:\n`);
|
|
487
|
-
console.log(` ${configPath}`);
|
|
488
|
-
console.log(`\n (or any ancestor directory's .eventmodelers/config.json, e.g. ~/.eventmodelers/config.json`);
|
|
489
|
-
console.log(` to share the same credentials across multiple projects)\n`);
|
|
490
|
-
console.log(` The file should look like:`);
|
|
491
|
-
const sample = stackCfg.needsBoardId
|
|
492
|
-
? ` {\n "token": "...",\n "boardId": "...",\n "organizationId": "...",\n "baseUrl": "https://api.eventmodelers.ai"\n }\n`
|
|
493
|
-
: ` {\n "token": "...",\n "organizationId": "...",\n "baseUrl": "https://api.eventmodelers.ai"\n }\n`;
|
|
494
|
-
console.log(sample);
|
|
495
|
-
console.log(' Then re-run this installer, or just run the agent afterwards.\n');
|
|
684
|
+
console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
|
|
496
685
|
} else {
|
|
497
|
-
console.log(
|
|
686
|
+
console.log(`\n ⚠️ Couldn't make sense of that paste — nothing was saved.`);
|
|
687
|
+
console.log(` Paste it into ${relative(targetDir, configPath)} yourself, or use /connect later.`);
|
|
498
688
|
}
|
|
689
|
+
} else if (choice === 'manual') {
|
|
690
|
+
console.log('\n🔑 Enter your Eventmodelers credentials:\n');
|
|
691
|
+
config.organizationId = await prompt(' Organization ID: ');
|
|
692
|
+
// Always ask, even when this install doesn't strictly require it — it's still
|
|
693
|
+
// used as a fallback default by the agent loop (see BOARD_ID resolution).
|
|
694
|
+
const boardId = await prompt(` Board ID${boardIdOptional ? ' (optional)' : ''}: `);
|
|
695
|
+
if (boardId) config.boardId = boardId;
|
|
696
|
+
config.token = await prompt(' Token: ');
|
|
697
|
+
if (!config.baseUrl) config.baseUrl = DEFAULT_BASE_URL;
|
|
698
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
699
|
+
console.log(`\n ✓ Credentials saved to ${relative(targetDir, configPath)}`);
|
|
700
|
+
} else if (choice === 'instructions') {
|
|
701
|
+
console.log(`\n Paste your credentials into:\n`);
|
|
702
|
+
console.log(` ${configPath}`);
|
|
703
|
+
console.log(`\n (or any ancestor directory's .eventmodelers/config.json, e.g. ~/.eventmodelers/config.json`);
|
|
704
|
+
console.log(` to share the same credentials across multiple projects)\n`);
|
|
705
|
+
console.log(` The file should look like:`);
|
|
706
|
+
const sample = ` {\n "token": "...",\n "boardId": "...",\n "organizationId": "...",\n "baseUrl": "https://api.eventmodelers.ai"\n }\n`;
|
|
707
|
+
console.log(sample);
|
|
708
|
+
console.log(' Then re-run this installer, or just run the agent afterwards.\n');
|
|
499
709
|
} else {
|
|
500
|
-
console.log('\n
|
|
710
|
+
console.log('\n ℹ️ Skipped — use /connect in Claude Code to add credentials later');
|
|
501
711
|
}
|
|
712
|
+
} else {
|
|
713
|
+
console.log('\n ✓ Config already present — skipping credential prompt');
|
|
714
|
+
}
|
|
502
715
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
509
|
-
console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
|
|
716
|
+
// Backfill baseUrl for configs that already had real credentials but predate
|
|
717
|
+
// this default (e.g. a config.json written by hand or by an older CLI version).
|
|
718
|
+
if (config.token && config.organizationId && !config.baseUrl) {
|
|
719
|
+
config.baseUrl = DEFAULT_BASE_URL;
|
|
720
|
+
}
|
|
510
721
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const settingsPath = join(claudeSettingsDir, 'settings.json');
|
|
516
|
-
mkdirSync(claudeSettingsDir, { recursive: true });
|
|
722
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
723
|
+
console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
|
|
724
|
+
return config;
|
|
725
|
+
}
|
|
517
726
|
|
|
518
|
-
|
|
519
|
-
|
|
727
|
+
// Configure the MCP server registration for a project — split out of `init` into
|
|
728
|
+
// its own command (`init-mcp`) since not every harness/workflow wants an
|
|
729
|
+
// automatic .claude/settings.json edit or an interactive "connect elsewhere?"
|
|
730
|
+
// prompt bundled into scaffolding.
|
|
731
|
+
async function configureMcp(options = {}) {
|
|
732
|
+
const targetDir = process.cwd();
|
|
733
|
+
const effective = loadEffectiveConfig(targetDir, null, options.configPath);
|
|
734
|
+
const baseUrl = effective.config.baseUrl || DEFAULT_BASE_URL;
|
|
735
|
+
|
|
736
|
+
console.log('🔌 Configuring MCP server...');
|
|
737
|
+
const claudeSettingsDir = join(targetDir, '.claude');
|
|
738
|
+
const settingsPath = join(claudeSettingsDir, 'settings.json');
|
|
739
|
+
mkdirSync(claudeSettingsDir, { recursive: true });
|
|
740
|
+
|
|
741
|
+
let settings = {};
|
|
742
|
+
if (existsSync(settingsPath)) {
|
|
743
|
+
try {
|
|
744
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
745
|
+
} catch {
|
|
746
|
+
settings = {};
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
settings.mcpServers = settings.mcpServers || {};
|
|
751
|
+
settings.mcpServers.eventmodelers = {
|
|
752
|
+
type: 'http',
|
|
753
|
+
url: `${baseUrl}/mcp`,
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
757
|
+
console.log(' ✓ MCP server configured in .claude/settings.json');
|
|
758
|
+
|
|
759
|
+
// Record that MCP was registered so `uninstall` knows to clean up the
|
|
760
|
+
// settings.json entry — checked against every kit dir's manifest.
|
|
761
|
+
for (const dirName of KIT_DIR_NAMES) {
|
|
762
|
+
const manifestPath = join(targetDir, dirName, '.eventmodelers', 'install-manifest.json');
|
|
763
|
+
if (existsSync(manifestPath)) {
|
|
764
|
+
const manifest = readJsonSafe(manifestPath);
|
|
765
|
+
manifest.mcpRegistered = true;
|
|
766
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const mcpUrl = `${baseUrl}/mcp`;
|
|
771
|
+
if (options.print) {
|
|
772
|
+
console.log('\nConnect the same MCP server in another harness:');
|
|
773
|
+
for (const client of Object.values(MCP_CLIENTS)) {
|
|
774
|
+
console.log(` ${client.label.padEnd(12)} ${client.command(mcpUrl)}`);
|
|
775
|
+
}
|
|
776
|
+
for (const client of MCP_MANUAL_CLIENTS) {
|
|
777
|
+
console.log(` ${client.label.padEnd(12)} ${client.hint(mcpUrl)}`);
|
|
778
|
+
}
|
|
779
|
+
} else {
|
|
780
|
+
const clientChoice = await selectPrompt('\nConnect the MCP globally to another harness?', [
|
|
781
|
+
{ label: 'Skip', value: 'skip' },
|
|
782
|
+
...Object.entries(MCP_CLIENTS).map(([key, c]) => ({ label: c.label, value: key })),
|
|
783
|
+
], 0);
|
|
784
|
+
|
|
785
|
+
if (clientChoice !== 'skip') {
|
|
786
|
+
const client = MCP_CLIENTS[clientChoice];
|
|
787
|
+
const cmd = client.command(mcpUrl);
|
|
520
788
|
try {
|
|
521
|
-
|
|
789
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
790
|
+
console.log(` ✓ ${client.label} connected via: ${cmd}`);
|
|
522
791
|
} catch {
|
|
523
|
-
|
|
792
|
+
console.error(` ⚠️ Command failed — you can run it manually:`);
|
|
793
|
+
console.error(` ${cmd}`);
|
|
524
794
|
}
|
|
525
795
|
}
|
|
526
796
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
535
|
-
console.log(' ✓ MCP server configured in .claude/settings.json');
|
|
797
|
+
if (MCP_MANUAL_CLIENTS.length) {
|
|
798
|
+
console.log('\nOther harnesses without a scriptable installer:');
|
|
799
|
+
MCP_MANUAL_CLIENTS.forEach((c) => console.log(` ${c.label.padEnd(12)} ${c.hint(mcpUrl)}`));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
536
803
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
804
|
+
// `run --real-time`: same job as ralph-claude.js (drive the installed kit's ralph
|
|
805
|
+
// loop with Claude as the executor), but instead of spawning a fresh `claude -p`
|
|
806
|
+
// process per task, it keeps ONE process warm across tasks via
|
|
807
|
+
// `--input-format stream-json` and writes each task's prompt to its stdin —
|
|
808
|
+
// verified (see dev notes) to stay alive and accept further turns after a result.
|
|
809
|
+
// This closes the remaining latency gap for voice/live use: the realtime channel
|
|
810
|
+
// + trigger wake in the kit's own lib/ralph.js already deliver new tasks with ~no
|
|
811
|
+
// delay, so the dominant cost left was the cold start (process boot, CLAUDE.md
|
|
812
|
+
// re-read, /connect, skill discovery) a fresh spawn pays every single task.
|
|
813
|
+
//
|
|
814
|
+
// Deliberately NOT a templated file in the kit dir (unlike ralph-claude.js) —
|
|
815
|
+
// this is exactly the kind of stack-agnostic runtime plumbing that drifted out of
|
|
816
|
+
// sync when duplicated per stack before (see the "Unify ralph/agent runtime
|
|
817
|
+
// files" commit). It ships once, here, and reuses whatever lib/ralph.js the kit
|
|
818
|
+
// already has installed — that file legitimately differs per kit (modeling-kit's
|
|
819
|
+
// org-wide prompt queue vs. build-kit's per-board slice tracking), so it stays
|
|
820
|
+
// where `init`/`init-modeling` put it.
|
|
821
|
+
async function runRealtime(kitDir, projectDir) {
|
|
822
|
+
const ralphLibPath = join(kitDir, 'lib', 'ralph.js');
|
|
823
|
+
if (!existsSync(ralphLibPath)) {
|
|
824
|
+
console.error(`❌ ${relative(process.cwd(), ralphLibPath)} not found — --real-time needs a kit installed via \`init\`/\`init-modeling\`.`);
|
|
825
|
+
process.exit(1);
|
|
826
|
+
}
|
|
827
|
+
const { startRalph, loadLocalConfig } = await import(pathToFileURL(ralphLibPath).href);
|
|
828
|
+
|
|
829
|
+
const cfg = loadLocalConfig(kitDir);
|
|
830
|
+
const QUESTIONING_RULE =
|
|
831
|
+
'IMPORTANT: You are running autonomously — no human is available to answer questions. ' +
|
|
832
|
+
'If you need clarification to proceed, do NOT pause or ask interactively. Instead, post your question ' +
|
|
833
|
+
'as a QUESTION-type comment (via /handle-comment with action=place and type=QUESTION) on the most ' +
|
|
834
|
+
'relevant slice or column node on the board, then continue with your best interpretation of the prompt.\n\n';
|
|
835
|
+
const inlineHeader = cfg.boardId
|
|
836
|
+
? `board=${cfg.boardId} token=${cfg.token} org=${cfg.organizationId} baseUrl=${cfg.baseUrl}\n\n${QUESTIONING_RULE}`
|
|
837
|
+
: QUESTIONING_RULE;
|
|
838
|
+
|
|
839
|
+
const claudeArgs = ['--dangerously-skip-permissions', '-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose'];
|
|
840
|
+
if (cfg.model) claudeArgs.push('--model', cfg.model);
|
|
841
|
+
const claudeEnv = cfg.anthropicBaseUrl ? { ...process.env, ANTHROPIC_BASE_URL: cfg.anthropicBaseUrl } : process.env;
|
|
842
|
+
|
|
843
|
+
let proc = null;
|
|
844
|
+
let stdoutBuffer = '';
|
|
845
|
+
let pending = null; // one in-flight task at a time, matching the ralph loop's own serial processing
|
|
846
|
+
const log = (line) => console.log(`[realtime] ${line}`);
|
|
847
|
+
|
|
848
|
+
// stream-json output loses the normal interactive TUI (tool cards, live diffs) —
|
|
849
|
+
// this is a plain-text approximation, good enough for a headless/voice runner.
|
|
850
|
+
function handleLine(line) {
|
|
851
|
+
if (!line.trim()) return;
|
|
852
|
+
let msg;
|
|
853
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
854
|
+
|
|
855
|
+
if (msg.type === 'assistant') {
|
|
856
|
+
for (const block of msg.message?.content ?? []) {
|
|
857
|
+
if (block.type === 'text' && block.text) log(block.text);
|
|
858
|
+
if (block.type === 'tool_use') log(`→ ${block.name}`);
|
|
562
859
|
}
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (msg.type === 'result') {
|
|
863
|
+
log(`done (${msg.duration_ms}ms${msg.total_cost_usd ? `, $${msg.total_cost_usd.toFixed(4)}` : ''})`);
|
|
864
|
+
const turn = pending;
|
|
865
|
+
pending = null;
|
|
866
|
+
if (turn) (msg.is_error ? turn.reject(new Error(msg.result || 'Claude turn errored')) : turn.resolve());
|
|
867
|
+
}
|
|
868
|
+
}
|
|
563
869
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
870
|
+
function spawnProcess() {
|
|
871
|
+
proc = spawn('claude', claudeArgs, { cwd: projectDir, env: claudeEnv, stdio: ['pipe', 'pipe', 'inherit'] });
|
|
872
|
+
stdoutBuffer = '';
|
|
873
|
+
proc.stdout.on('data', (chunk) => {
|
|
874
|
+
stdoutBuffer += chunk.toString();
|
|
875
|
+
const lines = stdoutBuffer.split('\n');
|
|
876
|
+
stdoutBuffer = lines.pop();
|
|
877
|
+
for (const l of lines) handleLine(l);
|
|
878
|
+
});
|
|
879
|
+
proc.on('exit', (code) => {
|
|
880
|
+
log(`process exited (${code}) — will respawn on next task`);
|
|
881
|
+
proc = null;
|
|
882
|
+
if (pending) {
|
|
883
|
+
const turn = pending;
|
|
884
|
+
pending = null;
|
|
885
|
+
turn.reject(new Error(`claude process exited (${code}) mid-turn`));
|
|
567
886
|
}
|
|
568
|
-
}
|
|
887
|
+
});
|
|
888
|
+
log('warm session started');
|
|
889
|
+
}
|
|
569
890
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
join(manifestDir, 'install-manifest.json'),
|
|
578
|
-
JSON.stringify({ stack: stackKey, global: !!options.global, skills: installedSkills, claudeExtras, mcpRegistered: true }, null, 2),
|
|
579
|
-
);
|
|
891
|
+
function runClaudeWarm(prompt) {
|
|
892
|
+
if (!proc) spawnProcess();
|
|
893
|
+
return new Promise((resolveTurn, rejectTurn) => {
|
|
894
|
+
pending = { resolve: resolveTurn, reject: rejectTurn };
|
|
895
|
+
proc.stdin.write(JSON.stringify({ type: 'user', message: { role: 'user', content: inlineHeader + prompt } }) + '\n');
|
|
896
|
+
});
|
|
897
|
+
}
|
|
580
898
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
console.log(' npx @eventmodelers/cli run\n');
|
|
584
|
-
console.log('Or using Ollama (run `ollama serve` first):');
|
|
585
|
-
console.log(' npx @eventmodelers/cli run --ollama\n');
|
|
586
|
-
console.log('Or using the bash loop only (no realtime):');
|
|
587
|
-
console.log(' npx @eventmodelers/cli run --bash\n');
|
|
588
|
-
console.log(`Skills are ready in ${options.global ? join(homedir(), '.claude', 'skills') : '.claude/skills/'} — use /connect to set a board ID.\n`);
|
|
589
|
-
console.log('💡 Recommended: add Chrome DevTools MCP for browser inspection:');
|
|
590
|
-
console.log(' claude mcp add chrome-devtools --scope user -- npx chrome-devtools-mcp@latest\n');
|
|
899
|
+
spawnProcess();
|
|
900
|
+
await startRalph({ kitDir, projectDir, onTask: runClaudeWarm });
|
|
591
901
|
}
|
|
592
902
|
|
|
593
903
|
const program = new Command();
|
|
@@ -599,26 +909,129 @@ program
|
|
|
599
909
|
.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)')
|
|
600
910
|
.option('--print', 'Print follow-up commands (e.g. claude mcp add) instead of prompting to run them');
|
|
601
911
|
|
|
602
|
-
|
|
912
|
+
// Shared by init/init-modeling/init-config: direct command-line credentials,
|
|
913
|
+
// Protractor-style (--base-url=..., not a generic --param key=value passthrough) —
|
|
914
|
+
// self-documenting in --help and typo-safe. These win over both the config file
|
|
915
|
+
// and EVENTMODELERS_* env vars, same as any explicitly-passed flag should.
|
|
916
|
+
function credentialFlags(cmd) {
|
|
917
|
+
return cmd
|
|
918
|
+
.option('--token <uuid>', 'API token (overrides config file / env var)')
|
|
919
|
+
.option('--board-id <uuid>', 'Board ID (overrides config file / env var)')
|
|
920
|
+
.option('--organization-id <uuid>', 'Organization ID (overrides config file / env var)')
|
|
921
|
+
.option('--base-url <url>', 'Platform base URL (overrides config file / env var)');
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function credentialOverridesFromOpts(opts) {
|
|
925
|
+
return { token: opts.token, boardId: opts.boardId, organizationId: opts.organizationId, baseUrl: opts.baseUrl };
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
credentialFlags(program
|
|
603
929
|
.command('init')
|
|
604
930
|
.alias('install')
|
|
605
931
|
.description('Scaffold a stack + install the agent modeling kit into the current directory')
|
|
606
932
|
.option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')})`)
|
|
607
|
-
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
|
|
933
|
+
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project'))
|
|
608
934
|
.action(async (opts, command) => {
|
|
609
935
|
const stackKey = await resolveStack(opts.stack);
|
|
610
936
|
const globalOpts = command.optsWithGlobals();
|
|
611
|
-
await installStack(stackKey, STACKS[stackKey], {
|
|
937
|
+
await installStack(stackKey, STACKS[stackKey], {
|
|
938
|
+
configPath: globalOpts.config,
|
|
939
|
+
print: globalOpts.print,
|
|
940
|
+
global: opts.global,
|
|
941
|
+
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
942
|
+
});
|
|
612
943
|
});
|
|
613
944
|
|
|
614
|
-
program
|
|
945
|
+
credentialFlags(program
|
|
615
946
|
.command('init-modeling')
|
|
616
947
|
.alias('modeling')
|
|
617
948
|
.description('Install skills + the agent loop only — no backend scaffold')
|
|
618
|
-
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project')
|
|
949
|
+
.option('--global', 'Install skills into ~/.claude/skills/ instead of the project — available in every project'))
|
|
950
|
+
.action(async (opts, command) => {
|
|
951
|
+
const globalOpts = command.optsWithGlobals();
|
|
952
|
+
await installStack(MODELING_KIT.key, MODELING_KIT, {
|
|
953
|
+
configPath: globalOpts.config,
|
|
954
|
+
print: globalOpts.print,
|
|
955
|
+
global: opts.global,
|
|
956
|
+
credentialOverrides: credentialOverridesFromOpts(opts),
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
program
|
|
961
|
+
.command('init-mcp')
|
|
962
|
+
.description('Register the eventmodelers MCP server in .claude/settings.json (and optionally another harness)')
|
|
619
963
|
.action(async (opts, command) => {
|
|
620
964
|
const globalOpts = command.optsWithGlobals();
|
|
621
|
-
await
|
|
965
|
+
await configureMcp({ configPath: globalOpts.config, print: globalOpts.print });
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
program
|
|
969
|
+
.command('init-agents')
|
|
970
|
+
.description(`Expose installed skills to other AI agent hosts (${Object.keys(AGENT_HOSTS).join(', ')}) as thin stub commands pointing at the canonical .claude/skills/ files — no skill content duplicated per host`)
|
|
971
|
+
.option('--hosts <list>', `Comma-separated host keys (${Object.keys(AGENT_HOSTS).join(', ')})`)
|
|
972
|
+
.option('--all', 'Expose to every known host')
|
|
973
|
+
.option('--global', 'Read skills from ~/.claude/skills/ instead of the project')
|
|
974
|
+
.action(async (opts) => {
|
|
975
|
+
const hosts = opts.all
|
|
976
|
+
? Object.keys(AGENT_HOSTS)
|
|
977
|
+
: opts.hosts
|
|
978
|
+
? opts.hosts.split(',').map((s) => s.trim()).filter(Boolean)
|
|
979
|
+
: null;
|
|
980
|
+
await configureAgentHosts({ hosts, global: opts.global });
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
credentialFlags(program
|
|
984
|
+
.command('init-config')
|
|
985
|
+
.description('Configure credentials only — writes .eventmodelers/config.json in the current directory, or ~/.eventmodelers/config.json with --global')
|
|
986
|
+
.option('--global', 'Write account-wide defaults (organizationId + token only) to ~/.eventmodelers/config.json instead of the project'))
|
|
987
|
+
.action(async (opts, command) => {
|
|
988
|
+
const globalOpts = command.optsWithGlobals();
|
|
989
|
+
const overrides = credentialOverridesFromOpts(opts);
|
|
990
|
+
|
|
991
|
+
if (opts.global) {
|
|
992
|
+
// Deliberately narrower than a project config: a board is specific to one
|
|
993
|
+
// project, and baseUrl already has its own runtime default, so the only
|
|
994
|
+
// things worth defaulting across every project are your identity (org)
|
|
995
|
+
// and how you authenticate (token).
|
|
996
|
+
const configPath = join(homedir(), '.eventmodelers', 'config.json');
|
|
997
|
+
const requiredFields = ['organizationId', 'token'];
|
|
998
|
+
const existing = readJsonSafe(configPath);
|
|
999
|
+
const base = { organizationId: existing.organizationId, token: existing.token };
|
|
1000
|
+
if (overrides.organizationId) base.organizationId = overrides.organizationId;
|
|
1001
|
+
if (overrides.token) base.token = overrides.token;
|
|
1002
|
+
|
|
1003
|
+
const configured = await configureCredentials({
|
|
1004
|
+
config: base,
|
|
1005
|
+
configPath,
|
|
1006
|
+
targetDir: homedir(),
|
|
1007
|
+
requiredFields,
|
|
1008
|
+
boardIdOptional: true,
|
|
1009
|
+
overrides: {},
|
|
1010
|
+
print: globalOpts.print,
|
|
1011
|
+
skipGitignore: true,
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// configureCredentials' generic paste/manual flow may have picked up
|
|
1015
|
+
// boardId/baseUrl too (e.g. from a pasted JSON blob) — strip them back out
|
|
1016
|
+
// before the final write, since --global only ever persists identity.
|
|
1017
|
+
writeFileSync(configPath, JSON.stringify({ organizationId: configured.organizationId, token: configured.token }, null, 2));
|
|
1018
|
+
console.log(`\n ✓ Saved account-wide defaults to ${configPath}`);
|
|
1019
|
+
} else {
|
|
1020
|
+
const targetDir = process.cwd();
|
|
1021
|
+
const configPath = globalOpts.config
|
|
1022
|
+
? resolve(targetDir, globalOpts.config)
|
|
1023
|
+
: join(targetDir, '.eventmodelers', 'config.json');
|
|
1024
|
+
const effective = loadEffectiveConfig(targetDir, null, globalOpts.config);
|
|
1025
|
+
await configureCredentials({
|
|
1026
|
+
config: effective.config,
|
|
1027
|
+
configPath,
|
|
1028
|
+
targetDir,
|
|
1029
|
+
requiredFields: ['organizationId', 'token'],
|
|
1030
|
+
boardIdOptional: true,
|
|
1031
|
+
overrides,
|
|
1032
|
+
print: globalOpts.print,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
622
1035
|
});
|
|
623
1036
|
|
|
624
1037
|
program
|
|
@@ -626,7 +1039,8 @@ program
|
|
|
626
1039
|
.description('Start the agent loop from the installed kit dir (default: ralph-claude.js)')
|
|
627
1040
|
.option('--ollama', 'Use ralph-ollama.js instead of the default Claude runner')
|
|
628
1041
|
.option('--bash', 'Use the bash-only ralph.sh loop (no realtime)')
|
|
629
|
-
.
|
|
1042
|
+
.option('--real-time', 'Keep one Claude process warm across tasks instead of spawning a fresh one per task, for low-latency voice/live use. Built into the CLI, not a per-project file.')
|
|
1043
|
+
.action(async (opts) => {
|
|
630
1044
|
const cwd = process.cwd();
|
|
631
1045
|
const kitDir = findInstalledKitDir(cwd);
|
|
632
1046
|
if (!kitDir) {
|
|
@@ -634,6 +1048,23 @@ program
|
|
|
634
1048
|
process.exit(1);
|
|
635
1049
|
}
|
|
636
1050
|
|
|
1051
|
+
const pickedCount = [opts.bash, opts.ollama, opts.realTime].filter(Boolean).length;
|
|
1052
|
+
if (pickedCount > 1) {
|
|
1053
|
+
console.error('❌ --bash, --ollama, and --real-time are mutually exclusive — pick one.');
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
if (opts.realTime) {
|
|
1058
|
+
console.log(`▶ Starting real-time loop (warm Claude process) for ${relative(cwd, kitDir)}...\n`);
|
|
1059
|
+
try {
|
|
1060
|
+
await runRealtime(kitDir, resolve(kitDir, '..'));
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
console.error('[realtime] Fatal:', err);
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
637
1068
|
// The actual agent loop lives in the scaffolded kit dir, not in this package — this
|
|
638
1069
|
// is just a thin dispatcher so users don't have to remember the kit-dir name or which
|
|
639
1070
|
// runner file to invoke. Users (and the agent itself, via AGENT.md) may customize these
|
|
@@ -694,6 +1125,33 @@ function uninstallKitDir(kitDir, cwd) {
|
|
|
694
1125
|
}
|
|
695
1126
|
}
|
|
696
1127
|
|
|
1128
|
+
if (manifest.agentHostFiles?.length) {
|
|
1129
|
+
const touchedDirs = new Set();
|
|
1130
|
+
for (const relPath of manifest.agentHostFiles) {
|
|
1131
|
+
const p = join(cwd, relPath);
|
|
1132
|
+
if (existsSync(p)) {
|
|
1133
|
+
rmSync(p, { force: true });
|
|
1134
|
+
console.log(` ✓ Removed ${relPath}`);
|
|
1135
|
+
touchedDirs.add(dirname(p));
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Prune now-empty host/package directories (e.g. .cursor/commands/,
|
|
1139
|
+
// .agents/skills/eventmodelers.timeline/) so uninstall doesn't leave an
|
|
1140
|
+
// empty dotfile forest behind — but never walk above cwd.
|
|
1141
|
+
for (const dir of touchedDirs) {
|
|
1142
|
+
let d = dir;
|
|
1143
|
+
while (d.startsWith(cwd) && d !== cwd) {
|
|
1144
|
+
try {
|
|
1145
|
+
if (readdirSync(d).length > 0) break;
|
|
1146
|
+
rmSync(d, { recursive: true, force: true });
|
|
1147
|
+
d = dirname(d);
|
|
1148
|
+
} catch {
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
697
1155
|
if (manifest.mcpRegistered) {
|
|
698
1156
|
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
699
1157
|
if (existsSync(settingsPath)) {
|
|
@@ -719,7 +1177,7 @@ function uninstallKitDir(kitDir, cwd) {
|
|
|
719
1177
|
|
|
720
1178
|
program
|
|
721
1179
|
.command('uninstall')
|
|
722
|
-
.description('Remove everything init/init-modeling installed: the kit dir, the skills it copied (project-local or ~/.claude/skills with --global),
|
|
1180
|
+
.description('Remove everything init/init-modeling installed: the kit dir, the skills it copied (project-local or ~/.claude/skills with --global), its MCP entry in .claude/settings.json, and any files written by init-agents. Leaves the root project scaffold untouched.')
|
|
723
1181
|
.option('--build-kit', `Remove ${STACKS.node.kitDirName}/ (the backend-stack kit dir)`)
|
|
724
1182
|
.option('--modeling-kit', `Remove ${MODELING_KIT.kitDirName}/ (the modeling-only kit dir)`)
|
|
725
1183
|
.action((opts) => {
|
|
@@ -772,7 +1230,7 @@ program
|
|
|
772
1230
|
console.log(`Ralph agent: ${ralphPath && existsSync(ralphPath) ? '✅ present' : '❌ missing'}`);
|
|
773
1231
|
|
|
774
1232
|
if (sources.length) {
|
|
775
|
-
console.log(`\nConnected to: ${cfg.baseUrl ||
|
|
1233
|
+
console.log(`\nConnected to: ${cfg.baseUrl || DEFAULT_BASE_URL}`);
|
|
776
1234
|
console.log(`Organization: ${cfg.organizationId}`);
|
|
777
1235
|
if (cfg.boardId) console.log(`Board: ${cfg.boardId}`);
|
|
778
1236
|
console.log(`\nConfig source${sources.length > 1 ? 's (later overrides earlier)' : ''}:`);
|