memoire-ai 0.3.2 → 0.3.4

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.
@@ -1,5 +1,5 @@
1
- import { spawn, execFileSync } from 'node:child_process';
2
- import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, writeFileSync } from 'node:fs';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { homedir, platform } from 'node:os';
@@ -10,16 +10,11 @@ import { init, install } from './configure.js';
10
10
  // ---------------------------------------------------------------------------
11
11
  // Constants
12
12
  // ---------------------------------------------------------------------------
13
- const CLOUD_API_URL = 'https://api.memoire.dev';
13
+ const CLOUD_API_URL = 'http://esprit-prod-alb-1025392673.us-east-1.elb.amazonaws.com';
14
14
  const DEFAULT_API_PORT = 3100;
15
15
  const DEFAULT_DASHBOARD_PORT = 4310;
16
- /** Agent IDs that have working install() / init() support in configure.ts. */
17
- const INSTALL_READY_AGENTS = ['cursor', 'claude', 'codex', 'amp'];
18
- /** All agents we show in the wizard (detected even if no install logic yet). */
19
- const ALL_WIZARD_AGENTS = [
20
- 'cursor', 'claude', 'codex', 'amp', 'windsurf',
21
- 'opencode', 'zed', 'aider', 'continue', 'cline', 'copilot', 'trae', 'void',
22
- ];
16
+ /** Agent IDs that have working install() / init() support. */
17
+ const SUPPORTED_AGENTS = ['cursor', 'claude', 'codex', 'amp'];
23
18
  // ---------------------------------------------------------------------------
24
19
  // Helpers
25
20
  // ---------------------------------------------------------------------------
@@ -83,149 +78,6 @@ function cancelAndExit(value, message = 'Setup cancelled.') {
83
78
  return false;
84
79
  }
85
80
  // ---------------------------------------------------------------------------
86
- // Local infrastructure helpers
87
- // ---------------------------------------------------------------------------
88
- const MEMOIRE_REPO = 'https://github.com/esprit-cli/Memoire.git';
89
- const MEMOIRE_DOCKER_IMAGE = 'ghcr.io/esprit-cli/memoire';
90
- function runCmd(cmd, args, opts) {
91
- return execFileSync(cmd, args, {
92
- cwd: opts?.cwd,
93
- env: { ...process.env, ...(opts?.env ?? {}) },
94
- encoding: 'utf-8',
95
- stdio: ['pipe', 'pipe', 'pipe'],
96
- timeout: 300_000,
97
- }).trim();
98
- }
99
- /** Clone the Memoire repo, install deps, build, and return the root path. */
100
- async function cloneAndBuild(s, isJson) {
101
- const installDir = join(homedir(), '.memoire', 'server');
102
- if (existsSync(join(installDir, 'apps', 'api', 'dist', 'index.js'))) {
103
- if (!isJson)
104
- p.log.info('Memoire server already installed, pulling latest...');
105
- try {
106
- runCmd('git', ['pull', '--ff-only'], { cwd: installDir });
107
- }
108
- catch {
109
- // Pull failed — that's OK, use what we have
110
- }
111
- }
112
- else {
113
- if (!isJson)
114
- s.start('Cloning Memoire repository...');
115
- mkdirSync(join(homedir(), '.memoire'), { recursive: true });
116
- runCmd('git', ['clone', '--depth', '1', MEMOIRE_REPO, installDir]);
117
- if (!isJson)
118
- s.stop('Repository cloned');
119
- }
120
- // Install deps
121
- if (!isJson)
122
- s.start('Installing dependencies (this may take a minute)...');
123
- runCmd('pnpm', ['install', '--frozen-lockfile'], { cwd: installDir });
124
- if (!isJson)
125
- s.stop('Dependencies installed');
126
- // Build
127
- if (!isJson)
128
- s.start('Building Memoire...');
129
- runCmd('pnpm', ['--filter', '@memoire-ai/shared', 'build'], { cwd: installDir });
130
- runCmd('pnpm', ['--filter', '@memoire-ai/sdk', 'build'], { cwd: installDir });
131
- runCmd('pnpm', ['--filter', '@memoire-ai/api', 'build'], { cwd: installDir });
132
- runCmd('pnpm', ['--filter', '@memoire-ai/dashboard', 'build'], { cwd: installDir });
133
- if (!isJson)
134
- s.stop('Build complete');
135
- return installDir;
136
- }
137
- /** Start Memoire via Docker Compose. */
138
- async function startWithDocker(s, isJson, dbUrl, dbUrlDirect, apiPort, dashboardPort, openaiKey, anthropicKey) {
139
- const installDir = join(homedir(), '.memoire', 'server');
140
- // We need the docker-compose.yml from the repo
141
- if (!existsSync(join(installDir, 'docker-compose.yml'))) {
142
- if (!isJson)
143
- s.start('Downloading Memoire Docker config...');
144
- mkdirSync(join(homedir(), '.memoire'), { recursive: true });
145
- runCmd('git', ['clone', '--depth', '1', MEMOIRE_REPO, installDir]);
146
- if (!isJson)
147
- s.stop('Docker config ready');
148
- }
149
- // Write .env for Docker Compose
150
- const envLines = [
151
- `DATABASE_URL=${dbUrl}`,
152
- `DATABASE_URL_DIRECT=${dbUrlDirect}`,
153
- `API_PORT=${apiPort}`,
154
- `DASHBOARD_PORT=${dashboardPort}`,
155
- ];
156
- if (openaiKey)
157
- envLines.push(`OPENAI_API_KEY=${openaiKey}`);
158
- if (anthropicKey)
159
- envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
160
- writeFileSync(join(installDir, '.env'), envLines.join('\n') + '\n');
161
- if (!isJson)
162
- s.start('Starting Memoire with Docker Compose...');
163
- runCmd('docker', ['compose', 'up', '-d'], { cwd: installDir });
164
- if (!isJson)
165
- s.stop('Docker containers started');
166
- return { memoireRoot: installDir, started: true };
167
- }
168
- /** Start API + push schema from a local monorepo clone. */
169
- async function startFromSource(s, isJson, memoireRoot, dbUrl, dbUrlDirect, apiPort, openaiKey, anthropicKey) {
170
- const apiDir = join(memoireRoot, 'apps', 'api');
171
- // Write .env
172
- const envLines = [
173
- `DATABASE_URL="${dbUrl}"`,
174
- `DATABASE_URL_DIRECT="${dbUrlDirect}"`,
175
- `PORT=${apiPort}`,
176
- ];
177
- if (openaiKey)
178
- envLines.push(`OPENAI_API_KEY="${openaiKey}"`);
179
- if (anthropicKey)
180
- envLines.push(`ANTHROPIC_API_KEY="${anthropicKey}"`);
181
- writeFileSync(join(apiDir, '.env'), envLines.join('\n') + '\n');
182
- // Push schema
183
- if (!isJson)
184
- s.start('Pushing database schema...');
185
- try {
186
- runCmd('npx', ['drizzle-kit', 'push', '--force'], {
187
- cwd: apiDir,
188
- env: { DATABASE_URL_DIRECT: dbUrlDirect },
189
- });
190
- if (!isJson)
191
- s.stop('Schema pushed');
192
- }
193
- catch (err) {
194
- if (!isJson)
195
- s.stop('Schema push failed — you may need to run it manually');
196
- }
197
- // Start API
198
- if (!isJson)
199
- s.start('Starting API server...');
200
- const env = {
201
- ...process.env,
202
- DATABASE_URL: dbUrl,
203
- PORT: String(apiPort),
204
- };
205
- if (openaiKey)
206
- env.OPENAI_API_KEY = openaiKey;
207
- if (anthropicKey)
208
- env.ANTHROPIC_API_KEY = anthropicKey;
209
- const child = spawn('node', ['dist/index.js'], {
210
- cwd: apiDir,
211
- detached: true,
212
- stdio: 'ignore',
213
- env,
214
- });
215
- child.unref();
216
- // Wait for health
217
- for (let i = 0; i < 15; i++) {
218
- await new Promise((resolve) => setTimeout(resolve, 1000));
219
- if (await checkApiHealth(`http://localhost:${apiPort}`)) {
220
- if (!isJson)
221
- s.stop(`API running at http://localhost:${apiPort}`);
222
- return;
223
- }
224
- }
225
- if (!isJson)
226
- s.stop('API started (health check timed out — may still be loading)');
227
- }
228
- // ---------------------------------------------------------------------------
229
81
  // Main wizard
230
82
  // ---------------------------------------------------------------------------
231
83
  export async function runSetupWizard(flags = {}) {
@@ -244,7 +96,7 @@ export async function runSetupWizard(flags = {}) {
244
96
  const allAgents = await detectAllAgents();
245
97
  if (!isJson)
246
98
  s.stop('System scan complete');
247
- const detectedAgents = allAgents.filter((a) => a.detected);
99
+ const detectedAgents = allAgents.filter((a) => a.detected && SUPPORTED_AGENTS.includes(a.id));
248
100
  if (!isJson) {
249
101
  const lines = [
250
102
  `Platform: ${systemInfo.platform} / ${systemInfo.arch}`,
@@ -257,101 +109,53 @@ export async function runSetupWizard(flags = {}) {
257
109
  p.note(lines.join('\n'), 'System Information');
258
110
  }
259
111
  // -----------------------------------------------------------------------
260
- // Phase 2: API connection
112
+ // Phase 2: Hosting mode
261
113
  // -----------------------------------------------------------------------
262
- let apiUrl;
263
- let isCloud = false;
264
- let needsLocalStart = false;
265
- if (flags.apiUrl) {
266
- // Explicit URL provided — use it directly
267
- apiUrl = flags.apiUrl;
268
- isCloud = apiUrl === CLOUD_API_URL;
269
- }
270
- else if (flags.cloud) {
271
- apiUrl = CLOUD_API_URL;
272
- isCloud = true;
114
+ let isCloud;
115
+ if (flags.cloud !== undefined) {
116
+ isCloud = flags.cloud;
117
+ }
118
+ else if (flags.apiUrl) {
119
+ isCloud = flags.apiUrl === CLOUD_API_URL;
273
120
  }
274
121
  else if (isNonInteractive) {
275
- // Default to localhost in non-interactive
276
- apiUrl = `http://localhost:${flags.apiPort ?? DEFAULT_API_PORT}`;
122
+ isCloud = true;
277
123
  }
278
124
  else {
279
- const apiChoice = await p.select({
280
- message: 'Where is your Memoire API?',
125
+ const mode = await p.select({
126
+ message: 'How would you like to host Memoire?',
281
127
  options: [
282
- {
283
- value: 'localhost',
284
- label: 'Run locally',
285
- hint: `starts API on localhost:${flags.apiPort ?? DEFAULT_API_PORT} — uses your Supabase DB`,
286
- },
287
- {
288
- value: 'custom',
289
- label: 'Custom URL',
290
- hint: 'connect to an API already running somewhere',
291
- },
292
- {
293
- value: 'cloud',
294
- label: 'Memoire Cloud',
295
- hint: 'api.memoire.dev — coming soon',
296
- },
128
+ { value: 'cloud', label: 'Memoire Cloud', hint: 'Fastest — no infrastructure to manage' },
129
+ { value: 'self-host', label: 'Self-hosted', hint: 'Your own database and servers' },
297
130
  ],
298
131
  });
299
- cancelAndExit(apiChoice);
300
- if (apiChoice === 'cloud') {
301
- apiUrl = CLOUD_API_URL;
302
- isCloud = true;
303
- }
304
- else if (apiChoice === 'custom') {
305
- const urlInput = await p.text({
306
- message: 'API URL:',
307
- placeholder: 'https://your-api.example.com',
308
- validate: (v) => {
309
- if (!v?.startsWith('http://') && !v?.startsWith('https://'))
310
- return 'Must be an HTTP(S) URL';
311
- },
312
- });
313
- cancelAndExit(urlInput);
314
- apiUrl = urlInput.replace(/\/$/, '');
315
- }
316
- else {
317
- // localhost
318
- apiUrl = `http://localhost:${flags.apiPort ?? DEFAULT_API_PORT}`;
319
- needsLocalStart = true;
320
- }
132
+ cancelAndExit(mode);
133
+ isCloud = mode === 'cloud';
321
134
  }
322
- // -----------------------------------------------------------------------
323
- // Phase 3: Database URL (needed for localhost + custom without running API)
324
- // -----------------------------------------------------------------------
325
- let dbUrl = flags.dbUrl;
326
- let dbUrlDirect = flags.dbUrlDirect;
327
- // Check if the API is already reachable — if so, skip DB prompt
328
- const apiAlreadyRunning = await checkApiHealth(apiUrl);
135
+ let apiUrl;
329
136
  if (isCloud) {
330
- if (!isJson) {
331
- if (apiAlreadyRunning) {
332
- p.log.success(`Memoire Cloud is reachable at ${apiUrl}`);
333
- }
334
- else {
335
- p.log.warn('Memoire Cloud (api.memoire.dev) is coming soon. Your agent configs will be ready when it launches.');
336
- }
337
- }
137
+ apiUrl = flags.apiUrl ?? CLOUD_API_URL;
138
+ if (!isJson)
139
+ p.log.info(`Using Memoire Cloud at ${apiUrl}`);
338
140
  }
339
- else if (!apiAlreadyRunning && !flags.skipStart) {
340
- // API not running — we need DB credentials to start it
341
- needsLocalStart = true;
141
+ else {
142
+ // ------------------------------------------------------------------
143
+ // Phase 3: Database URL (self-host only)
144
+ // ------------------------------------------------------------------
145
+ apiUrl = flags.apiUrl ?? `http://localhost:${flags.apiPort ?? DEFAULT_API_PORT}`;
146
+ let dbUrl = flags.dbUrl;
147
+ let dbUrlDirect = flags.dbUrlDirect;
342
148
  if (!dbUrl) {
343
149
  if (isNonInteractive) {
344
- p.log.error('API is not reachable and --db-url was not provided.');
150
+ p.log.error('Self-hosted mode requires --db-url in non-interactive mode.');
345
151
  process.exit(1);
346
152
  }
347
- p.note('Memoire needs a PostgreSQL database (Supabase recommended).\n' +
348
- 'Get a free one at supabase.com/dashboard → Settings → Database → Connection string.', 'Database Setup');
349
153
  const dbInput = await p.text({
350
- message: 'Database URL (transaction pooler, port 6543):',
351
- placeholder: 'postgresql://postgres.xxx:PASSWORD@aws-0-us-west-2.pooler.supabase.com:6543/postgres',
154
+ message: 'PostgreSQL connection string (pooler / transaction mode):',
155
+ placeholder: 'postgresql://user:pass@host:6543/memoire',
352
156
  validate: (v) => {
353
157
  if (!v?.startsWith('postgresql://') && !v?.startsWith('postgres://')) {
354
- return 'Must be a PostgreSQL connection string';
158
+ return 'Must be a valid PostgreSQL connection string';
355
159
  }
356
160
  },
357
161
  });
@@ -361,9 +165,9 @@ export async function runSetupWizard(flags = {}) {
361
165
  if (!dbUrlDirect) {
362
166
  if (!isNonInteractive) {
363
167
  const directInput = await p.text({
364
- message: 'Direct URL for migrations (port 5432):',
365
- placeholder: dbUrl.replace(':6543/', ':5432/'),
366
- defaultValue: dbUrl.replace(':6543/', ':5432/'),
168
+ message: 'Direct database URL (for migrations, optional):',
169
+ placeholder: dbUrl,
170
+ defaultValue: dbUrl,
367
171
  });
368
172
  cancelAndExit(directInput);
369
173
  dbUrlDirect = directInput || dbUrl;
@@ -372,19 +176,16 @@ export async function runSetupWizard(flags = {}) {
372
176
  dbUrlDirect = dbUrl;
373
177
  }
374
178
  }
179
+ // Store for later use in .env writing
180
+ flags._resolvedDbUrl = dbUrl;
181
+ flags._resolvedDbUrlDirect = dbUrlDirect;
375
182
  }
376
- else if (apiAlreadyRunning && !isJson) {
377
- p.log.success(`API already running at ${apiUrl}`);
378
- }
379
- // Store resolved DB URLs for later phases
380
- flags._resolvedDbUrl = dbUrl;
381
- flags._resolvedDbUrlDirect = dbUrlDirect;
382
183
  // -----------------------------------------------------------------------
383
184
  // Phase 4: Optional API keys
384
185
  // -----------------------------------------------------------------------
385
186
  let openaiKey = flags.openaiKey;
386
187
  let anthropicKey = flags.anthropicKey;
387
- if (needsLocalStart && !isNonInteractive) {
188
+ if (!isCloud && !isNonInteractive) {
388
189
  if (!openaiKey) {
389
190
  const wantOpenai = await p.confirm({
390
191
  message: 'Add an OpenAI API key? (used for embeddings)',
@@ -432,38 +233,30 @@ export async function runSetupWizard(flags = {}) {
432
233
  selectedAgentIds = flags.agents
433
234
  .split(',')
434
235
  .map((a) => a.trim())
435
- .filter((id) => ALL_WIZARD_AGENTS.includes(id));
236
+ .filter((id) => SUPPORTED_AGENTS.includes(id));
436
237
  }
437
238
  else if (isNonInteractive) {
438
- // In non-interactive mode, auto-select all detected agents that we can install
439
- selectedAgentIds = detectedAgents
440
- .filter((a) => INSTALL_READY_AGENTS.includes(a.id))
441
- .map((a) => a.id);
239
+ selectedAgentIds = detectedAgents.map((a) => a.id);
442
240
  if (selectedAgentIds.length === 0) {
443
- p.log.error('No configurable agents detected. Use --agents to specify agents in non-interactive mode.');
241
+ p.log.error('No supported agents detected. Use --agents to specify agents in non-interactive mode.');
444
242
  process.exit(1);
445
243
  }
446
244
  }
447
245
  else {
448
- // Show all wizard agents — mark which are detected and which have full install support
449
- const wizardAgents = allAgents.filter((a) => ALL_WIZARD_AGENTS.includes(a.id));
450
- if (wizardAgents.length === 0) {
451
- p.log.warn('No agents found. Please install an AI coding agent first.');
246
+ const supportedDetected = allAgents.filter((a) => SUPPORTED_AGENTS.includes(a.id));
247
+ if (supportedDetected.length === 0) {
248
+ p.log.warn('No supported agents detected. Please install Cursor, Claude Code, Codex, or Amp first.');
452
249
  process.exit(1);
453
250
  }
454
- const agentChoices = wizardAgents.map((a) => {
455
- const hasInstall = INSTALL_READY_AGENTS.includes(a.id);
456
- let hint = a.detected ? 'detected' : 'not found';
457
- if (a.detected && !hasInstall)
458
- hint = 'detected — MCP setup coming soon';
459
- return { value: a.id, label: a.name, hint };
460
- });
251
+ const agentChoices = supportedDetected.map((a) => ({
252
+ value: a.id,
253
+ label: a.name,
254
+ hint: a.detected ? 'detected' : 'not found',
255
+ }));
461
256
  const chosen = await p.multiselect({
462
257
  message: 'Which agents should Memoire connect to?',
463
258
  options: agentChoices,
464
- initialValues: detectedAgents
465
- .filter((a) => INSTALL_READY_AGENTS.includes(a.id))
466
- .map((a) => a.id),
259
+ initialValues: detectedAgents.map((a) => a.id),
467
260
  required: true,
468
261
  });
469
262
  cancelAndExit(chosen);
@@ -477,45 +270,124 @@ export async function runSetupWizard(flags = {}) {
477
270
  p.log.info(`Selected agents: ${selectedAgentIds.join(', ')}`);
478
271
  }
479
272
  // -----------------------------------------------------------------------
480
- // Phase 6: Workspace info
273
+ // Phase 6: Workspace info (or invite code for cloud join)
481
274
  // -----------------------------------------------------------------------
482
275
  let orgName = flags.orgName;
483
276
  let projectName = flags.projectName;
484
- if (!isNonInteractive) {
485
- if (!orgName) {
486
- const org = await p.text({
487
- message: 'Organization name:',
488
- placeholder: 'My Team',
277
+ let joinInviteCode = flags.inviteCode;
278
+ if (isCloud && !isNonInteractive && !joinInviteCode && !flags.inviteToken) {
279
+ const joinOrCreate = await p.select({
280
+ message: 'How would you like to get started?',
281
+ options: [
282
+ { value: 'join', label: 'Join an existing workspace (I have an invite code)' },
283
+ { value: 'create', label: 'Create a new workspace' },
284
+ ],
285
+ });
286
+ cancelAndExit(joinOrCreate);
287
+ if (joinOrCreate === 'join') {
288
+ const code = await p.text({
289
+ message: 'Enter your invite code:',
290
+ placeholder: 'ABCD-1234',
489
291
  validate: (v) => {
490
292
  if (!v?.trim())
491
- return 'Organization name is required';
293
+ return 'Invite code is required';
492
294
  },
493
295
  });
494
- cancelAndExit(org);
495
- orgName = org;
296
+ cancelAndExit(code);
297
+ joinInviteCode = code;
496
298
  }
497
- if (!projectName) {
498
- const proj = await p.text({
499
- message: 'Project name:',
500
- placeholder: 'my-project',
501
- validate: (v) => {
502
- if (!v?.trim())
503
- return 'Project name is required';
504
- },
505
- });
506
- cancelAndExit(proj);
507
- projectName = proj;
299
+ }
300
+ if (!joinInviteCode && !flags.inviteToken) {
301
+ if (!isNonInteractive) {
302
+ if (!orgName) {
303
+ const org = await p.text({
304
+ message: 'Organization name:',
305
+ placeholder: 'My Team',
306
+ validate: (v) => {
307
+ if (!v?.trim())
308
+ return 'Organization name is required';
309
+ },
310
+ });
311
+ cancelAndExit(org);
312
+ orgName = org;
313
+ }
314
+ if (!projectName) {
315
+ const proj = await p.text({
316
+ message: 'Project name:',
317
+ placeholder: 'my-project',
318
+ validate: (v) => {
319
+ if (!v?.trim())
320
+ return 'Project name is required';
321
+ },
322
+ });
323
+ cancelAndExit(proj);
324
+ projectName = proj;
325
+ }
508
326
  }
509
327
  }
510
328
  // -----------------------------------------------------------------------
511
- // Phase 7+8: Infrastructure setup (self-host only)
512
- // Set up database, start API server — via Docker or clone+build
329
+ // Phase 7: Write .env and push DB schema (self-host only)
513
330
  // -----------------------------------------------------------------------
514
- let apiReachable = apiAlreadyRunning;
515
- if (apiAlreadyRunning || isCloud || flags.skipStart) {
516
- // API is already up, or we're in cloud/skip mode — nothing to start
331
+ if (!isCloud) {
332
+ const memoireRoot = findMemoireRoot();
333
+ if (memoireRoot) {
334
+ const apiDir = join(memoireRoot, 'apps', 'api');
335
+ const envPath = join(apiDir, '.env');
336
+ const dbUrl = flags._resolvedDbUrl;
337
+ const dbUrlDirect = flags._resolvedDbUrlDirect;
338
+ const envLines = [
339
+ `DATABASE_URL="${dbUrl}"`,
340
+ `DATABASE_URL_DIRECT="${dbUrlDirect}"`,
341
+ `PORT=${flags.apiPort ?? DEFAULT_API_PORT}`,
342
+ ];
343
+ if (openaiKey)
344
+ envLines.push(`OPENAI_API_KEY="${openaiKey}"`);
345
+ if (anthropicKey)
346
+ envLines.push(`ANTHROPIC_API_KEY="${anthropicKey}"`);
347
+ writeFileSync(envPath, envLines.join('\n') + '\n');
348
+ if (!isJson)
349
+ p.log.success(`Environment file written to ${envPath}`);
350
+ // Push database schema
351
+ if (!isJson)
352
+ s.start('Pushing database schema...');
353
+ try {
354
+ const { execFileSync } = await import('node:child_process');
355
+ execFileSync('npx', ['drizzle-kit', 'push', '--force'], {
356
+ cwd: apiDir,
357
+ stdio: 'pipe',
358
+ env: { ...process.env, DATABASE_URL_DIRECT: dbUrlDirect },
359
+ });
360
+ if (!isJson)
361
+ s.stop('Database schema pushed');
362
+ }
363
+ catch (err) {
364
+ if (!isJson)
365
+ s.stop('Failed to push database schema');
366
+ p.log.warn(`Could not push schema: ${err instanceof Error ? err.message : 'unknown error'}. You may need to run "npx drizzle-kit push --force" manually in apps/api.`);
367
+ }
368
+ }
369
+ else {
370
+ p.log.warn('Could not locate Memoire monorepo root. Skipping .env and schema push.');
371
+ }
517
372
  }
518
- else if (needsLocalStart && dbUrl) {
373
+ // -----------------------------------------------------------------------
374
+ // Phase 8: Start API server (self-host only)
375
+ // -----------------------------------------------------------------------
376
+ let apiReachable = false;
377
+ if (isCloud) {
378
+ if (!isJson)
379
+ s.start('Checking Memoire Cloud...');
380
+ apiReachable = await checkApiHealth(apiUrl);
381
+ if (!isJson) {
382
+ if (apiReachable) {
383
+ s.stop('Memoire Cloud is reachable');
384
+ }
385
+ else {
386
+ s.stop('Memoire Cloud is not reachable — proceeding anyway');
387
+ }
388
+ }
389
+ }
390
+ else if (!flags.skipStart) {
519
391
  const apiPort = flags.apiPort ?? DEFAULT_API_PORT;
520
392
  const portFree = await isPortAvailable(apiPort);
521
393
  if (!portFree) {
@@ -524,80 +396,37 @@ export async function runSetupWizard(flags = {}) {
524
396
  apiReachable = await checkApiHealth(apiUrl);
525
397
  }
526
398
  else {
527
- // API is not running — we need to start it
528
- const dbUrl = flags._resolvedDbUrl;
529
- const dbUrlDirect = flags._resolvedDbUrlDirect;
530
- // Check if we're already in the monorepo
531
- let memoireRoot = findMemoireRoot();
399
+ const memoireRoot = findMemoireRoot();
532
400
  if (memoireRoot) {
533
- // We're in the monorepo — use it directly
534
401
  if (!isJson)
535
- p.log.info('Found Memoire source — starting from local build');
536
- await startFromSource(s, isJson, memoireRoot, dbUrl, dbUrlDirect, apiPort, openaiKey, anthropicKey);
537
- apiReachable = await checkApiHealth(apiUrl);
538
- }
539
- else if (!isNonInteractive) {
540
- // Not in monorepo — ask how to start
541
- const startMethod = await p.select({
542
- message: 'How should Memoire run locally?',
543
- options: [
544
- ...(systemInfo.hasDocker
545
- ? [{ value: 'docker', label: 'Docker Compose', hint: 'recommended — fastest' }]
546
- : []),
547
- ...(systemInfo.hasPnpm
548
- ? [{ value: 'clone', label: 'Clone & build from source', hint: `installs to ~/.memoire/server` }]
549
- : []),
550
- { value: 'skip', label: 'Skip — I\'ll start the API myself', hint: 'manual setup' },
551
- ],
402
+ s.start('Starting API server...');
403
+ const apiDir = join(memoireRoot, 'apps', 'api');
404
+ const child = spawn('node', ['dist/index.js'], {
405
+ cwd: apiDir,
406
+ detached: true,
407
+ stdio: 'ignore',
408
+ env: { ...process.env, PORT: String(apiPort) },
552
409
  });
553
- cancelAndExit(startMethod);
554
- if (startMethod === 'docker') {
555
- const result = await startWithDocker(s, isJson, dbUrl, dbUrlDirect, apiPort, flags.dashboardPort ?? DEFAULT_DASHBOARD_PORT, openaiKey, anthropicKey);
556
- memoireRoot = result.memoireRoot;
557
- // Wait for API health via Docker
558
- if (!isJson)
559
- s.start('Waiting for API to be ready...');
560
- for (let i = 0; i < 30; i++) {
561
- await new Promise((r) => setTimeout(r, 2000));
562
- apiReachable = await checkApiHealth(apiUrl);
563
- if (apiReachable)
564
- break;
565
- }
566
- if (!isJson) {
567
- if (apiReachable)
568
- s.stop('API is healthy');
569
- else
570
- s.stop('API may still be starting — Docker containers are running');
571
- }
572
- }
573
- else if (startMethod === 'clone') {
574
- memoireRoot = await cloneAndBuild(s, isJson);
575
- await startFromSource(s, isJson, memoireRoot, dbUrl, dbUrlDirect, apiPort, openaiKey, anthropicKey);
410
+ child.unref();
411
+ // Wait briefly for the server to start
412
+ for (let i = 0; i < 10; i++) {
413
+ await new Promise((resolve) => setTimeout(resolve, 1000));
576
414
  apiReachable = await checkApiHealth(apiUrl);
415
+ if (apiReachable)
416
+ break;
577
417
  }
578
- else {
579
- if (!isJson)
580
- p.log.info(`Start the API manually, then run: npx memoire-ai setup --api-url http://localhost:${apiPort}`);
418
+ if (!isJson) {
419
+ if (apiReachable) {
420
+ s.stop(`API server running on port ${apiPort}`);
421
+ }
422
+ else {
423
+ s.stop('API server may still be starting');
424
+ p.log.warn('Could not verify API health. Continuing anyway.');
425
+ }
581
426
  }
582
427
  }
583
428
  else {
584
- // Non-interactive + no monorepo try Docker, then clone
585
- if (systemInfo.hasDocker) {
586
- const dbUrl = flags._resolvedDbUrl;
587
- const dbUrlDirect = flags._resolvedDbUrlDirect;
588
- await startWithDocker(s, isJson, dbUrl, dbUrlDirect, apiPort, flags.dashboardPort ?? DEFAULT_DASHBOARD_PORT, openaiKey, anthropicKey);
589
- for (let i = 0; i < 30; i++) {
590
- await new Promise((r) => setTimeout(r, 2000));
591
- apiReachable = await checkApiHealth(apiUrl);
592
- if (apiReachable)
593
- break;
594
- }
595
- }
596
- else if (systemInfo.hasPnpm) {
597
- memoireRoot = await cloneAndBuild(s, isJson);
598
- await startFromSource(s, isJson, memoireRoot, dbUrl, dbUrlDirect, apiPort, openaiKey, anthropicKey);
599
- apiReachable = await checkApiHealth(apiUrl);
600
- }
429
+ p.log.warn('Could not locate Memoire monorepo root. Skipping API start.');
601
430
  }
602
431
  }
603
432
  }
@@ -662,6 +491,7 @@ export async function runSetupWizard(flags = {}) {
662
491
  apiUrl,
663
492
  setupToken: flags.setupToken,
664
493
  inviteToken: flags.inviteToken,
494
+ inviteCode: joinInviteCode ?? flags.inviteCode,
665
495
  orgName,
666
496
  orgSlug: flags.orgSlug,
667
497
  projectName,
@@ -681,43 +511,10 @@ export async function runSetupWizard(flags = {}) {
681
511
  s.stop('Workspace bootstrapped');
682
512
  }
683
513
  catch (err) {
684
- const msg = err instanceof Error ? err.message : 'unknown error';
685
- // If bootstrap is disabled (workspace already exists), fall back to existing config
686
- if (msg.includes('Bootstrap is disabled') || msg.includes('403')) {
687
- if (!isJson)
688
- s.stop('Workspace already exists');
689
- if (existingConfig?.api_key && existingConfig?.org_id) {
690
- if (!isJson)
691
- p.log.info('Using existing workspace credentials.');
692
- bootstrapResult = {
693
- org_id: existingConfig.org_id,
694
- project_id: existingConfig.project_id ?? '',
695
- token: existingConfig.api_key,
696
- user_id: existingConfig.user_id ?? '',
697
- };
698
- // Install for the first agent
699
- await install({
700
- client: firstAgent,
701
- apiKey: bootstrapResult.token,
702
- apiUrl,
703
- orgId: bootstrapResult.org_id,
704
- projectId: bootstrapResult.project_id || undefined,
705
- userName: flags.userName,
706
- userEmail: flags.userEmail,
707
- });
708
- }
709
- else {
710
- if (!isJson)
711
- p.log.error('Workspace exists but no local credentials found. Run `npx memoire-ai login` to authenticate.');
712
- process.exit(1);
713
- }
714
- }
715
- else {
716
- if (!isJson)
717
- s.stop('Bootstrap failed');
718
- p.log.error(`Failed to bootstrap workspace: ${msg}`);
719
- process.exit(1);
720
- }
514
+ if (!isJson)
515
+ s.stop('Bootstrap failed');
516
+ p.log.error(`Failed to bootstrap workspace: ${err instanceof Error ? err.message : 'unknown error'}`);
517
+ process.exit(1);
721
518
  }
722
519
  }
723
520
  // -----------------------------------------------------------------------
@@ -750,10 +547,9 @@ export async function runSetupWizard(flags = {}) {
750
547
  }
751
548
  // -----------------------------------------------------------------------
752
549
  // Phase 11: Start dashboard (self-host only)
753
- // Docker users already have dashboard running from docker compose up.
754
550
  // -----------------------------------------------------------------------
755
551
  let dashboardUrl = null;
756
- if (!isCloud && !flags.skipStart && apiReachable) {
552
+ if (!isCloud && !flags.skipStart) {
757
553
  const dashPort = flags.dashboardPort ?? DEFAULT_DASHBOARD_PORT;
758
554
  const dashPortFree = await isPortAvailable(dashPort);
759
555
  if (!dashPortFree) {
@@ -762,34 +558,23 @@ export async function runSetupWizard(flags = {}) {
762
558
  p.log.info(`Dashboard already running at ${dashboardUrl}`);
763
559
  }
764
560
  else {
765
- // Try to start dashboard from local source
766
- const memoireRoot = findMemoireRoot() ?? join(homedir(), '.memoire', 'server');
767
- const dashboardBuild = join(memoireRoot, 'apps', 'dashboard', 'dist', 'server.js');
768
- if (existsSync(dashboardBuild)) {
561
+ const memoireRoot = findMemoireRoot();
562
+ if (memoireRoot) {
769
563
  if (!isJson)
770
564
  s.start('Starting dashboard...');
771
- const child = spawn('node', [dashboardBuild], {
772
- cwd: join(memoireRoot, 'apps', 'dashboard'),
565
+ const configPath = join(homedir(), '.memoire', 'config.json');
566
+ const child = spawn('npx', ['memoire', 'dashboard', '--port', String(dashPort), '--config-path', configPath], {
567
+ cwd: memoireRoot,
773
568
  detached: true,
774
569
  stdio: 'ignore',
775
- env: {
776
- ...process.env,
777
- MEMOIRE_API_URL: apiUrl,
778
- MEMOIRE_DASHBOARD_DEFAULT_API_URL: apiUrl,
779
- PORT: String(dashPort),
780
- NODE_ENV: 'production',
781
- },
782
570
  });
783
571
  child.unref();
784
572
  dashboardUrl = `http://localhost:${dashPort}`;
785
- await new Promise((resolve) => setTimeout(resolve, 3000));
573
+ // Wait briefly for dashboard to start
574
+ await new Promise((resolve) => setTimeout(resolve, 2000));
786
575
  if (!isJson)
787
576
  s.stop(`Dashboard starting at ${dashboardUrl}`);
788
577
  }
789
- else {
790
- if (!isJson)
791
- p.log.info('Dashboard not built — run `pnpm --filter @memoire-ai/dashboard build` to enable it');
792
- }
793
578
  }
794
579
  }
795
580
  else if (isCloud) {