@eventmodelers/cli 0.0.7 → 0.0.9
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 +614 -149
- 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,11 +270,31 @@ 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
|
+
|
|
159
278
|
// Canonical order the account page pastes values in, regardless of which fields a
|
|
160
279
|
// given stack actually requires — a modeling-kit install (no boardId required) still
|
|
161
280
|
// gets a paste containing all 4 fields, so we must not drop the ones we don't need.
|
|
162
281
|
const PASTE_FIELD_ORDER = ['organizationId', 'boardId', 'token'];
|
|
163
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
|
+
|
|
164
298
|
// Accepts either a JSON object (as copied from the account page) or a comma-separated
|
|
165
299
|
// line of values, in PASTE_FIELD_ORDER, with an optional base URL anywhere in the list.
|
|
166
300
|
function parseCredentialsPaste(text, requiredFields) {
|
|
@@ -170,11 +304,18 @@ function parseCredentialsPaste(text, requiredFields) {
|
|
|
170
304
|
try {
|
|
171
305
|
const obj = JSON.parse(trimmed);
|
|
172
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
|
+
|
|
173
313
|
const result = {};
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
+
}
|
|
178
319
|
if (requiredFields.every((f) => result[f])) return result;
|
|
179
320
|
return null;
|
|
180
321
|
}
|
|
@@ -186,18 +327,26 @@ function parseCredentialsPaste(text, requiredFields) {
|
|
|
186
327
|
const result = {};
|
|
187
328
|
const remaining = [];
|
|
188
329
|
for (const v of values) {
|
|
189
|
-
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;
|
|
190
336
|
else remaining.push(v);
|
|
191
337
|
}
|
|
192
|
-
if (remaining.length < requiredFields.length) return null;
|
|
338
|
+
if (remaining.length < requiredFields.length - Object.keys(result).length) return null;
|
|
193
339
|
// If more values were pasted than this stack strictly requires (e.g. a boardId
|
|
194
340
|
// in a modeling-kit paste), use the full canonical order so the extra field is
|
|
195
341
|
// still captured instead of being mis-zipped against the shorter requiredFields
|
|
196
342
|
// list and silently dropped/misassigned.
|
|
197
343
|
const fieldOrder = remaining.length > requiredFields.length ? PASTE_FIELD_ORDER : requiredFields;
|
|
198
|
-
|
|
344
|
+
let i = 0;
|
|
345
|
+
for (const field of fieldOrder) {
|
|
346
|
+
if (result[field]) continue; // already resolved via an embedded "field=" prefix
|
|
199
347
|
if (remaining[i] !== undefined) result[field] = remaining[i];
|
|
200
|
-
|
|
348
|
+
i++;
|
|
349
|
+
}
|
|
201
350
|
|
|
202
351
|
return requiredFields.every((f) => result[f]) ? result : null;
|
|
203
352
|
}
|
|
@@ -263,9 +412,15 @@ function findConfigInParents(startDir) {
|
|
|
263
412
|
const candidate = join(dir, '.eventmodelers', 'config.json');
|
|
264
413
|
if (existsSync(candidate)) return candidate;
|
|
265
414
|
const parent = dirname(dir);
|
|
266
|
-
if (parent === dir)
|
|
415
|
+
if (parent === dir) break;
|
|
267
416
|
dir = parent;
|
|
268
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;
|
|
269
424
|
}
|
|
270
425
|
|
|
271
426
|
function readJsonSafe(path) {
|
|
@@ -356,6 +511,12 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
356
511
|
const targetDir = process.cwd();
|
|
357
512
|
const templatesSource = join(__dirname, 'stacks', stackKey, 'templates');
|
|
358
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');
|
|
359
520
|
|
|
360
521
|
if (!existsSync(templatesSource)) {
|
|
361
522
|
console.error('❌ Templates directory not found at:', templatesSource);
|
|
@@ -366,16 +527,21 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
366
527
|
// Recorded into the install manifest (step 8) so `uninstall` can remove exactly
|
|
367
528
|
// these files later and nothing the user added independently.
|
|
368
529
|
const claudeSkillsSrc = join(templatesSource, '.claude', 'skills');
|
|
369
|
-
const installedSkills =
|
|
530
|
+
const installedSkills = [
|
|
531
|
+
...(existsSync(sharedSkills) ? readdirSync(sharedSkills) : []),
|
|
532
|
+
...(existsSync(claudeSkillsSrc) ? readdirSync(claudeSkillsSrc) : []),
|
|
533
|
+
];
|
|
370
534
|
let claudeExtras = [];
|
|
371
535
|
|
|
372
536
|
if (options.global) {
|
|
373
537
|
const globalSkillsDir = join(homedir(), '.claude', 'skills');
|
|
374
538
|
console.log('📦 Installing skills globally...');
|
|
539
|
+
copyDirContents(sharedSkills, globalSkillsDir);
|
|
375
540
|
copyDirContents(claudeSkillsSrc, globalSkillsDir);
|
|
376
541
|
} else {
|
|
377
542
|
console.log('📦 Installing skills...');
|
|
378
543
|
copyDirContents(join(templatesSource, '.claude'), join(targetDir, '.claude'));
|
|
544
|
+
copyDirContents(sharedSkills, join(targetDir, '.claude', 'skills'));
|
|
379
545
|
claudeExtras = existsSync(join(templatesSource, '.claude'))
|
|
380
546
|
? readdirSync(join(templatesSource, '.claude')).filter((f) => f !== 'skills')
|
|
381
547
|
: [];
|
|
@@ -426,9 +592,60 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
426
592
|
const configPath = options.configPath
|
|
427
593
|
? resolve(targetDir, options.configPath)
|
|
428
594
|
: join(targetDir, '.eventmodelers', 'config.json');
|
|
429
|
-
const configDir = dirname(configPath);
|
|
430
|
-
mkdirSync(configDir, { recursive: true });
|
|
431
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, force = 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) {
|
|
432
649
|
const gitignorePath = join(targetDir, '.gitignore');
|
|
433
650
|
const relConfigDir = relative(targetDir, configDir);
|
|
434
651
|
if (relConfigDir && !relConfigDir.startsWith('..')) {
|
|
@@ -442,147 +659,245 @@ async function installStack(stackKey, stackCfg, options = {}) {
|
|
|
442
659
|
writeFileSync(gitignorePath, `${gitignoreEntry}\n`);
|
|
443
660
|
}
|
|
444
661
|
}
|
|
662
|
+
}
|
|
445
663
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
console.log('
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
{ label: 'Skip for now', value: 'skip' },
|
|
466
|
-
], 1);
|
|
467
|
-
|
|
468
|
-
if (choice === 'paste') {
|
|
469
|
-
console.log('\n Copy your credentials from https://app.eventmodelers.ai/account,');
|
|
470
|
-
console.log(' then paste them below and press Enter:\n');
|
|
471
|
-
const pasted = await promptPasteBlock();
|
|
472
|
-
const parsed = parseCredentialsPaste(pasted, requiredFields);
|
|
473
|
-
if (parsed) {
|
|
474
|
-
config = { ...config, ...parsed };
|
|
475
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
476
|
-
console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
|
|
477
|
-
} else {
|
|
478
|
-
console.log(`\n ⚠️ Couldn't make sense of that paste — nothing was saved.`);
|
|
479
|
-
console.log(` Paste it into ${relative(targetDir, configPath)} yourself, or use /connect later.`);
|
|
480
|
-
}
|
|
481
|
-
} else if (choice === 'manual') {
|
|
482
|
-
console.log('\n🔑 Enter your Eventmodelers credentials:\n');
|
|
483
|
-
config.organizationId = await prompt(' Organization ID: ');
|
|
484
|
-
// Always ask, even when this stack doesn't strictly require it — it's still
|
|
485
|
-
// used as a fallback default by the agent loop (see BOARD_ID resolution).
|
|
486
|
-
const boardId = await prompt(` Board ID${stackCfg.needsBoardId ? '' : ' (optional)'}: `);
|
|
487
|
-
if (boardId) config.boardId = boardId;
|
|
488
|
-
config.token = await prompt(' Token: ');
|
|
664
|
+
const stillMissing = force || 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;
|
|
489
683
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
490
|
-
console.log(`\n ✓
|
|
491
|
-
} else if (choice === 'instructions') {
|
|
492
|
-
console.log(`\n Paste your credentials into:\n`);
|
|
493
|
-
console.log(` ${configPath}`);
|
|
494
|
-
console.log(`\n (or any ancestor directory's .eventmodelers/config.json, e.g. ~/.eventmodelers/config.json`);
|
|
495
|
-
console.log(` to share the same credentials across multiple projects)\n`);
|
|
496
|
-
console.log(` The file should look like:`);
|
|
497
|
-
const sample = ` {\n "token": "...",\n "boardId": "...",\n "organizationId": "...",\n "baseUrl": "https://api.eventmodelers.ai"\n }\n`;
|
|
498
|
-
console.log(sample);
|
|
499
|
-
console.log(' Then re-run this installer, or just run the agent afterwards.\n');
|
|
684
|
+
console.log(`\n ✓ Saved to ${relative(targetDir, configPath)}`);
|
|
500
685
|
} else {
|
|
501
|
-
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.`);
|
|
502
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');
|
|
503
709
|
} else {
|
|
504
|
-
console.log('\n
|
|
710
|
+
console.log('\n ℹ️ Skipped — use /connect in Claude Code to add credentials later');
|
|
505
711
|
}
|
|
712
|
+
} else {
|
|
713
|
+
console.log('\n ✓ Config already present — skipping credential prompt');
|
|
714
|
+
}
|
|
506
715
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
513
|
-
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
|
+
}
|
|
514
721
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
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
|
+
}
|
|
520
726
|
|
|
521
|
-
|
|
522
|
-
|
|
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);
|
|
523
788
|
try {
|
|
524
|
-
|
|
789
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
790
|
+
console.log(` ✓ ${client.label} connected via: ${cmd}`);
|
|
525
791
|
} catch {
|
|
526
|
-
|
|
792
|
+
console.error(` ⚠️ Command failed — you can run it manually:`);
|
|
793
|
+
console.error(` ${cmd}`);
|
|
527
794
|
}
|
|
528
795
|
}
|
|
529
796
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
538
|
-
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
|
+
}
|
|
539
803
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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}`);
|
|
565
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
|
+
}
|
|
566
869
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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`));
|
|
570
886
|
}
|
|
571
|
-
}
|
|
887
|
+
});
|
|
888
|
+
log('warm session started');
|
|
889
|
+
}
|
|
572
890
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
join(manifestDir, 'install-manifest.json'),
|
|
581
|
-
JSON.stringify({ stack: stackKey, global: !!options.global, skills: installedSkills, claudeExtras, mcpRegistered: true }, null, 2),
|
|
582
|
-
);
|
|
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
|
+
}
|
|
583
898
|
|
|
584
|
-
|
|
585
|
-
|
|
899
|
+
spawnProcess();
|
|
900
|
+
await startRalph({ kitDir, projectDir, onTask: runClaudeWarm });
|
|
586
901
|
}
|
|
587
902
|
|
|
588
903
|
const program = new Command();
|
|
@@ -594,26 +909,131 @@ program
|
|
|
594
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)')
|
|
595
910
|
.option('--print', 'Print follow-up commands (e.g. claude mcp add) instead of prompting to run them');
|
|
596
911
|
|
|
597
|
-
|
|
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
|
|
598
929
|
.command('init')
|
|
599
930
|
.alias('install')
|
|
600
931
|
.description('Scaffold a stack + install the agent modeling kit into the current directory')
|
|
601
932
|
.option('--stack <name>', `Stack to install (${Object.keys(STACKS).join(', ')})`)
|
|
602
|
-
.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'))
|
|
603
934
|
.action(async (opts, command) => {
|
|
604
935
|
const stackKey = await resolveStack(opts.stack);
|
|
605
936
|
const globalOpts = command.optsWithGlobals();
|
|
606
|
-
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
|
+
});
|
|
607
943
|
});
|
|
608
944
|
|
|
609
|
-
program
|
|
945
|
+
credentialFlags(program
|
|
610
946
|
.command('init-modeling')
|
|
611
947
|
.alias('modeling')
|
|
612
948
|
.description('Install skills + the agent loop only — no backend scaffold')
|
|
613
|
-
.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)')
|
|
963
|
+
.action(async (opts, command) => {
|
|
964
|
+
const globalOpts = command.optsWithGlobals();
|
|
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'))
|
|
614
987
|
.action(async (opts, command) => {
|
|
615
988
|
const globalOpts = command.optsWithGlobals();
|
|
616
|
-
|
|
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
|
+
force: true,
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
// configureCredentials' generic paste/manual flow may have picked up
|
|
1016
|
+
// boardId/baseUrl too (e.g. from a pasted JSON blob) — strip them back out
|
|
1017
|
+
// before the final write, since --global only ever persists identity.
|
|
1018
|
+
writeFileSync(configPath, JSON.stringify({ organizationId: configured.organizationId, token: configured.token }, null, 2));
|
|
1019
|
+
console.log(`\n ✓ Saved account-wide defaults to ${configPath}`);
|
|
1020
|
+
} else {
|
|
1021
|
+
const targetDir = process.cwd();
|
|
1022
|
+
const configPath = globalOpts.config
|
|
1023
|
+
? resolve(targetDir, globalOpts.config)
|
|
1024
|
+
: join(targetDir, '.eventmodelers', 'config.json');
|
|
1025
|
+
const effective = loadEffectiveConfig(targetDir, null, globalOpts.config);
|
|
1026
|
+
await configureCredentials({
|
|
1027
|
+
config: effective.config,
|
|
1028
|
+
configPath,
|
|
1029
|
+
targetDir,
|
|
1030
|
+
requiredFields: ['organizationId', 'token'],
|
|
1031
|
+
boardIdOptional: true,
|
|
1032
|
+
overrides,
|
|
1033
|
+
print: globalOpts.print,
|
|
1034
|
+
force: true,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
617
1037
|
});
|
|
618
1038
|
|
|
619
1039
|
program
|
|
@@ -621,7 +1041,8 @@ program
|
|
|
621
1041
|
.description('Start the agent loop from the installed kit dir (default: ralph-claude.js)')
|
|
622
1042
|
.option('--ollama', 'Use ralph-ollama.js instead of the default Claude runner')
|
|
623
1043
|
.option('--bash', 'Use the bash-only ralph.sh loop (no realtime)')
|
|
624
|
-
.
|
|
1044
|
+
.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.')
|
|
1045
|
+
.action(async (opts) => {
|
|
625
1046
|
const cwd = process.cwd();
|
|
626
1047
|
const kitDir = findInstalledKitDir(cwd);
|
|
627
1048
|
if (!kitDir) {
|
|
@@ -629,6 +1050,23 @@ program
|
|
|
629
1050
|
process.exit(1);
|
|
630
1051
|
}
|
|
631
1052
|
|
|
1053
|
+
const pickedCount = [opts.bash, opts.ollama, opts.realTime].filter(Boolean).length;
|
|
1054
|
+
if (pickedCount > 1) {
|
|
1055
|
+
console.error('❌ --bash, --ollama, and --real-time are mutually exclusive — pick one.');
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (opts.realTime) {
|
|
1060
|
+
console.log(`▶ Starting real-time loop (warm Claude process) for ${relative(cwd, kitDir)}...\n`);
|
|
1061
|
+
try {
|
|
1062
|
+
await runRealtime(kitDir, resolve(kitDir, '..'));
|
|
1063
|
+
} catch (err) {
|
|
1064
|
+
console.error('[realtime] Fatal:', err);
|
|
1065
|
+
process.exit(1);
|
|
1066
|
+
}
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
632
1070
|
// The actual agent loop lives in the scaffolded kit dir, not in this package — this
|
|
633
1071
|
// is just a thin dispatcher so users don't have to remember the kit-dir name or which
|
|
634
1072
|
// runner file to invoke. Users (and the agent itself, via AGENT.md) may customize these
|
|
@@ -689,6 +1127,33 @@ function uninstallKitDir(kitDir, cwd) {
|
|
|
689
1127
|
}
|
|
690
1128
|
}
|
|
691
1129
|
|
|
1130
|
+
if (manifest.agentHostFiles?.length) {
|
|
1131
|
+
const touchedDirs = new Set();
|
|
1132
|
+
for (const relPath of manifest.agentHostFiles) {
|
|
1133
|
+
const p = join(cwd, relPath);
|
|
1134
|
+
if (existsSync(p)) {
|
|
1135
|
+
rmSync(p, { force: true });
|
|
1136
|
+
console.log(` ✓ Removed ${relPath}`);
|
|
1137
|
+
touchedDirs.add(dirname(p));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// Prune now-empty host/package directories (e.g. .cursor/commands/,
|
|
1141
|
+
// .agents/skills/eventmodelers.timeline/) so uninstall doesn't leave an
|
|
1142
|
+
// empty dotfile forest behind — but never walk above cwd.
|
|
1143
|
+
for (const dir of touchedDirs) {
|
|
1144
|
+
let d = dir;
|
|
1145
|
+
while (d.startsWith(cwd) && d !== cwd) {
|
|
1146
|
+
try {
|
|
1147
|
+
if (readdirSync(d).length > 0) break;
|
|
1148
|
+
rmSync(d, { recursive: true, force: true });
|
|
1149
|
+
d = dirname(d);
|
|
1150
|
+
} catch {
|
|
1151
|
+
break;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
692
1157
|
if (manifest.mcpRegistered) {
|
|
693
1158
|
const settingsPath = join(cwd, '.claude', 'settings.json');
|
|
694
1159
|
if (existsSync(settingsPath)) {
|
|
@@ -714,7 +1179,7 @@ function uninstallKitDir(kitDir, cwd) {
|
|
|
714
1179
|
|
|
715
1180
|
program
|
|
716
1181
|
.command('uninstall')
|
|
717
|
-
.description('Remove everything init/init-modeling installed: the kit dir, the skills it copied (project-local or ~/.claude/skills with --global),
|
|
1182
|
+
.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.')
|
|
718
1183
|
.option('--build-kit', `Remove ${STACKS.node.kitDirName}/ (the backend-stack kit dir)`)
|
|
719
1184
|
.option('--modeling-kit', `Remove ${MODELING_KIT.kitDirName}/ (the modeling-only kit dir)`)
|
|
720
1185
|
.action((opts) => {
|
|
@@ -767,7 +1232,7 @@ program
|
|
|
767
1232
|
console.log(`Ralph agent: ${ralphPath && existsSync(ralphPath) ? '✅ present' : '❌ missing'}`);
|
|
768
1233
|
|
|
769
1234
|
if (sources.length) {
|
|
770
|
-
console.log(`\nConnected to: ${cfg.baseUrl ||
|
|
1235
|
+
console.log(`\nConnected to: ${cfg.baseUrl || DEFAULT_BASE_URL}`);
|
|
771
1236
|
console.log(`Organization: ${cfg.organizationId}`);
|
|
772
1237
|
if (cfg.boardId) console.log(`Board: ${cfg.boardId}`);
|
|
773
1238
|
console.log(`\nConfig source${sources.length > 1 ? 's (later overrides earlier)' : ''}:`);
|