@crmy/cli 0.5.5 → 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +514 -97
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,67 +2,286 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command as Command27 } from "commander";
5
+ import { createRequire as createRequire2 } from "module";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
7
+ import path5 from "path";
5
8
 
6
9
  // src/commands/init.ts
7
10
  import { Command } from "commander";
8
11
  import crypto from "crypto";
12
+ import fs2 from "fs";
13
+ import path2 from "path";
14
+
15
+ // src/spinner.ts
16
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17
+ function createSpinner(initialMessage) {
18
+ if (!process.stdout.isTTY) {
19
+ if (initialMessage) console.log(` ... ${initialMessage}`);
20
+ return {
21
+ update(message) {
22
+ if (message) console.log(` ... ${message}`);
23
+ },
24
+ succeed(message) {
25
+ console.log(` \u2713 ${message}`);
26
+ },
27
+ fail(message) {
28
+ console.log(` \u2717 ${message}`);
29
+ },
30
+ stop() {
31
+ }
32
+ };
33
+ }
34
+ let frame = 0;
35
+ let current = initialMessage;
36
+ let stopped = false;
37
+ const cols = () => process.stdout.columns ?? 80;
38
+ const interval = setInterval(() => {
39
+ if (stopped) return;
40
+ const symbol = FRAMES[frame % FRAMES.length];
41
+ const line = ` ${symbol} ${current}`;
42
+ process.stdout.write(`\r${line.padEnd(cols())}`);
43
+ frame++;
44
+ }, 80);
45
+ const clearLine = () => {
46
+ process.stdout.write(`\r${" ".repeat(cols())}\r`);
47
+ };
48
+ return {
49
+ update(message) {
50
+ current = message;
51
+ },
52
+ succeed(message) {
53
+ stopped = true;
54
+ clearInterval(interval);
55
+ clearLine();
56
+ console.log(` \x1B[32m\u2713\x1B[0m ${message}`);
57
+ },
58
+ fail(message) {
59
+ stopped = true;
60
+ clearInterval(interval);
61
+ clearLine();
62
+ console.log(` \x1B[31m\u2717\x1B[0m ${message}`);
63
+ },
64
+ stop() {
65
+ stopped = true;
66
+ clearInterval(interval);
67
+ clearLine();
68
+ }
69
+ };
70
+ }
71
+
72
+ // src/config.ts
9
73
  import fs from "fs";
10
74
  import path from "path";
75
+ import os from "os";
76
+ var CRMY_DIR = path.join(os.homedir(), ".crmy");
77
+ var GLOBAL_CONFIG = path.join(CRMY_DIR, "config.json");
78
+ var AUTH_FILE = path.join(CRMY_DIR, "auth.json");
79
+ function loadConfigFile(explicitPath) {
80
+ if (explicitPath) {
81
+ try {
82
+ return JSON.parse(fs.readFileSync(explicitPath, "utf-8"));
83
+ } catch {
84
+ return {};
85
+ }
86
+ }
87
+ const localPath = path.join(process.cwd(), ".crmy.json");
88
+ if (fs.existsSync(localPath)) {
89
+ try {
90
+ return JSON.parse(fs.readFileSync(localPath, "utf-8"));
91
+ } catch {
92
+ }
93
+ }
94
+ try {
95
+ return JSON.parse(fs.readFileSync(GLOBAL_CONFIG, "utf-8"));
96
+ } catch {
97
+ return {};
98
+ }
99
+ }
100
+ function saveConfigFile(config) {
101
+ const json = JSON.stringify(config, null, 2) + "\n";
102
+ fs.mkdirSync(CRMY_DIR, { recursive: true });
103
+ fs.writeFileSync(GLOBAL_CONFIG, json, { mode: 384 });
104
+ const localPath = path.join(process.cwd(), ".crmy.json");
105
+ fs.writeFileSync(localPath, json);
106
+ }
107
+ function loadAuthState() {
108
+ try {
109
+ const raw = fs.readFileSync(AUTH_FILE, "utf-8");
110
+ const state = JSON.parse(raw);
111
+ if (state.expiresAt && new Date(state.expiresAt) < /* @__PURE__ */ new Date()) {
112
+ return null;
113
+ }
114
+ return state;
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+ function saveAuthState(state) {
120
+ fs.mkdirSync(CRMY_DIR, { recursive: true });
121
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
122
+ }
123
+ function clearAuthState() {
124
+ try {
125
+ fs.unlinkSync(AUTH_FILE);
126
+ } catch {
127
+ }
128
+ }
129
+ function resolveServerUrl() {
130
+ return process.env.CRMY_SERVER_URL ?? loadAuthState()?.serverUrl ?? loadConfigFile().serverUrl;
131
+ }
132
+
133
+ // src/commands/init.ts
134
+ function validateEmail(input) {
135
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.trim()) ? true : "Please enter a valid email address (e.g. you@example.com)";
136
+ }
137
+ function validatePassword(input) {
138
+ return input.length >= 8 ? true : "Password must be at least 8 characters";
139
+ }
11
140
  function initCommand() {
12
- return new Command("init").description("Initialize crmy.ai: configure database, run migrations, create user").action(async () => {
141
+ return new Command("init").description("Interactive setup wizard: database, migrations, admin account").action(async () => {
13
142
  const { default: inquirer } = await import("inquirer");
14
- console.log("\n crmy.ai \u2014 Agent-first CRM setup\n");
15
- const answers = await inquirer.prompt([
143
+ console.log("\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
144
+ console.log(" \u2502 crmy.ai \u2014 Setup Wizard \u2502");
145
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
146
+ console.log(" This wizard will:\n");
147
+ console.log(" Step 1 \u2014 Connect to your PostgreSQL database");
148
+ console.log(" Step 2 \u2014 Create all CRMy tables (migrations)");
149
+ console.log(" Step 3 \u2014 Create your admin account\n");
150
+ const localConfigPath = path2.join(process.cwd(), ".crmy.json");
151
+ if (fs2.existsSync(localConfigPath)) {
152
+ console.log(" \x1B[33m\u26A0\x1B[0m A .crmy.json already exists in this directory.\n");
153
+ const { overwrite } = await inquirer.prompt([
154
+ {
155
+ type: "confirm",
156
+ name: "overwrite",
157
+ message: " Overwrite it and run setup again?",
158
+ default: false
159
+ }
160
+ ]);
161
+ if (!overwrite) {
162
+ console.log("\n Setup cancelled. Your existing config was not changed.\n");
163
+ process.exit(0);
164
+ }
165
+ console.log("");
166
+ }
167
+ console.log(" \u2500\u2500 Step 1 of 3: Database Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
168
+ console.log(" Enter your PostgreSQL connection string.");
169
+ console.log(" Format: postgresql://user:password@host:5432/dbname\n");
170
+ console.log(" Options:");
171
+ console.log(" \u2022 Local install: postgresql://localhost:5432/crmy");
172
+ console.log(" \u2022 Docker: postgresql://postgres:postgres@localhost:5432/crmy");
173
+ console.log(" \u2022 Supabase: Project Settings \u2192 Database \u2192 Connection String");
174
+ console.log(" \u2022 Neon: Dashboard \u2192 Connection Details\n");
175
+ console.log(" \x1B[2mNote: the wizard will retry the connection up to 5 times.\x1B[0m\n");
176
+ const { databaseUrl } = await inquirer.prompt([
16
177
  {
17
178
  type: "input",
18
179
  name: "databaseUrl",
19
- message: "PostgreSQL URL?",
180
+ message: " PostgreSQL connection string:",
20
181
  default: "postgresql://localhost:5432/crmy"
21
- },
182
+ }
183
+ ]);
184
+ console.log("\n \u2500\u2500 Step 2 of 3: Database Setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
185
+ process.env.CRMY_IMPORTED = "1";
186
+ const { initPool, closePool, runMigrations } = await import("@crmy/server");
187
+ let spinner = createSpinner("Connecting to database\u2026");
188
+ let db;
189
+ try {
190
+ db = await initPool(databaseUrl);
191
+ spinner.succeed("Connected to database");
192
+ } catch (err) {
193
+ spinner.fail("Database connection failed");
194
+ const msg = err.message ?? String(err);
195
+ console.error(
196
+ `
197
+ Error: ${msg}
198
+
199
+ Common causes:
200
+ \u2022 PostgreSQL is not running
201
+ \u2022 Wrong host, port, or database name in the URL
202
+ \u2022 Wrong username or password
203
+ \u2022 Database does not exist \u2014 create it with: createdb crmy
204
+ `
205
+ );
206
+ process.exit(1);
207
+ }
208
+ spinner = createSpinner("Running database migrations\u2026");
209
+ let ran = [];
210
+ try {
211
+ ran = await runMigrations(db);
212
+ if (ran.length > 0) {
213
+ spinner.succeed(`Migrations complete \x1B[2m(${ran.length} applied)\x1B[0m`);
214
+ } else {
215
+ spinner.succeed("Migrations complete \x1B[2m(already up to date)\x1B[0m");
216
+ }
217
+ } catch (err) {
218
+ spinner.fail("Migration failed");
219
+ const msg = err.message ?? String(err);
220
+ console.error(
221
+ `
222
+ SQL error: ${msg}
223
+
224
+ This may mean:
225
+ \u2022 The database user lacks CREATE TABLE permissions
226
+ \u2022 A previous partial migration left the schema in a bad state
227
+ Try: drop and recreate the database, then run init again
228
+ `
229
+ );
230
+ await closePool();
231
+ process.exit(1);
232
+ }
233
+ spinner = createSpinner("Seeding default tenant\u2026");
234
+ let tenantId;
235
+ try {
236
+ const result = await db.query(
237
+ `INSERT INTO tenants (slug, name) VALUES ('default', 'Default Tenant')
238
+ ON CONFLICT (slug) DO UPDATE SET name = 'Default Tenant'
239
+ RETURNING id`
240
+ );
241
+ tenantId = result.rows[0].id;
242
+ spinner.succeed("Default tenant ready");
243
+ } catch (err) {
244
+ spinner.fail("Failed to seed tenant");
245
+ console.error(`
246
+ Error: ${err.message}
247
+ `);
248
+ await closePool();
249
+ process.exit(1);
250
+ }
251
+ console.log("\n \u2500\u2500 Step 3 of 3: Admin Account \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
252
+ console.log(" Create the first admin account for the CRMy web UI and CLI.\n");
253
+ console.log(
254
+ " \x1B[33mNOTE:\x1B[0m These are your \x1B[1mCRMy login credentials\x1B[0m \u2014 NOT your database credentials.\n"
255
+ );
256
+ const { name, email, password } = await inquirer.prompt([
22
257
  {
23
258
  type: "input",
24
259
  name: "name",
25
- message: "Your name?"
260
+ message: " Your full name:"
26
261
  },
27
262
  {
28
263
  type: "input",
29
264
  name: "email",
30
- message: "Your email?"
265
+ message: " Email address (used to log in):",
266
+ validate: validateEmail
31
267
  },
32
268
  {
33
269
  type: "password",
34
270
  name: "password",
35
- message: "Password?",
36
- mask: "*"
271
+ message: " Password (min 8 characters):",
272
+ mask: "*",
273
+ validate: validatePassword
37
274
  }
38
275
  ]);
39
- process.env.CRMY_IMPORTED = "1";
276
+ spinner = createSpinner("Creating admin account\u2026");
40
277
  try {
41
- const { initPool, closePool } = await import("@crmy/server");
42
- const { runMigrations } = await import("@crmy/server");
43
- console.log("\nConnecting to database...");
44
- const db = await initPool(answers.databaseUrl);
45
- console.log("Connected.");
46
- console.log("Running migrations...");
47
- const ran = await runMigrations(db);
48
- if (ran.length > 0) {
49
- console.log(` Ran ${ran.length} migration(s): ${ran.join(", ")}`);
50
- } else {
51
- console.log(" No pending migrations.");
52
- }
53
- const tenantResult = await db.query(
54
- `INSERT INTO tenants (slug, name) VALUES ('default', 'Default Tenant')
55
- ON CONFLICT (slug) DO UPDATE SET name = 'Default Tenant'
56
- RETURNING id`
57
- );
58
- const tenantId = tenantResult.rows[0].id;
59
- const passwordHash = crypto.createHash("sha256").update(answers.password).digest("hex");
278
+ const passwordHash = crypto.createHash("sha256").update(password).digest("hex");
60
279
  const userResult = await db.query(
61
280
  `INSERT INTO users (tenant_id, email, name, role, password_hash)
62
281
  VALUES ($1, $2, $3, 'owner', $4)
63
282
  ON CONFLICT (tenant_id, email) DO UPDATE SET name = $3, password_hash = $4
64
283
  RETURNING id`,
65
- [tenantId, answers.email, answers.name, passwordHash]
284
+ [tenantId, email.trim(), name, passwordHash]
66
285
  );
67
286
  const userId = userResult.rows[0].id;
68
287
  const rawKey = "crmy_" + crypto.randomBytes(32).toString("hex");
@@ -72,102 +291,265 @@ function initCommand() {
72
291
  VALUES ($1, $2, $3, 'default', '{read,write,admin}')`,
73
292
  [tenantId, userId, keyHash]
74
293
  );
294
+ spinner.succeed("Admin account created");
75
295
  const jwtSecret = crypto.randomBytes(32).toString("hex");
76
- const config = {
296
+ const crmmyConfig = {
77
297
  serverUrl: "http://localhost:3000",
78
298
  apiKey: rawKey,
79
299
  tenantId: "default",
80
- database: {
81
- url: answers.databaseUrl
82
- },
300
+ database: { url: databaseUrl },
83
301
  jwtSecret,
84
302
  hitl: {
85
303
  requireApproval: ["bulk_update", "bulk_delete", "send_email"],
86
304
  autoApproveSeconds: 0
87
305
  }
88
306
  };
89
- const configPath = path.join(process.cwd(), ".crmy.json");
90
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
91
- const gitignorePath = path.join(process.cwd(), ".gitignore");
92
- const gitignoreContent = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
307
+ saveConfigFile(crmmyConfig);
308
+ const gitignorePath = path2.join(process.cwd(), ".gitignore");
309
+ const gitignoreContent = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf-8") : "";
93
310
  if (!gitignoreContent.includes(".crmy.json")) {
94
- fs.appendFileSync(gitignorePath, "\n.crmy.json\n");
311
+ fs2.appendFileSync(gitignorePath, "\n.crmy.json\n");
95
312
  }
96
- await closePool();
97
- console.log("\n \u2713 crmy.ai initialized\n");
98
- console.log(" Add to Claude Code:");
99
- console.log(" claude mcp add crmy -- npx @crmy/cli mcp\n");
100
- console.log(" Or start the server:");
101
- console.log(" npx @crmy/cli server\n");
102
313
  } catch (err) {
103
- console.error("\nSetup failed:", err instanceof Error ? err.message : err);
314
+ spinner.fail("Failed to create admin account");
315
+ console.error(`
316
+ Error: ${err.message}
317
+ `);
318
+ await closePool();
104
319
  process.exit(1);
105
320
  }
321
+ await closePool();
322
+ console.log("\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
323
+ console.log(" \u2502 \x1B[32m\u2713\x1B[0m CRMy is ready! \u2502");
324
+ console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n");
325
+ console.log(` Admin account: ${email.trim()}`);
326
+ console.log(" Config saved: .crmy.json \x1B[2m(added to .gitignore)\x1B[0m\n");
327
+ console.log(" Next steps:\n");
328
+ console.log(" Start the server:");
329
+ console.log(" \x1B[1mnpx @crmy/cli server\x1B[0m\n");
330
+ console.log(" Connect to Claude Code:");
331
+ console.log(" \x1B[1mclaude mcp add crmy -- npx @crmy/cli mcp\x1B[0m\n");
106
332
  });
107
333
  }
108
334
 
109
335
  // src/commands/server.ts
110
336
  import { Command as Command2 } from "commander";
337
+ import { createRequire } from "module";
338
+ import { fileURLToPath } from "url";
339
+ import path4 from "path";
111
340
 
112
- // src/config.ts
113
- import fs2 from "fs";
114
- import path2 from "path";
115
- import os from "os";
116
- var AUTH_DIR = path2.join(os.homedir(), ".crmy");
117
- var AUTH_FILE = path2.join(AUTH_DIR, "auth.json");
118
- function loadConfigFile() {
119
- const configPath = path2.join(process.cwd(), ".crmy.json");
120
- try {
121
- const raw = fs2.readFileSync(configPath, "utf-8");
122
- return JSON.parse(raw);
123
- } catch {
124
- return {};
125
- }
341
+ // src/banner.ts
342
+ var ASCII_ART = `
343
+
344
+
345
+ ##*++++*###
346
+ #+-:.......:--=+#
347
+ *+-:::----------:--==*#
348
+ *-:-----------::--::=++=*
349
+ #=:-::-==--------------+++=#
350
+ *=..:--==-=-----==------+++==#
351
+ #-..:--==-=:----====----=+*++=#
352
+ *::.:-===-=-----====-:--=+++++=#
353
+ +-:.:-===-=---=-====-:-==++++++=#
354
+ *:-:--=====--=-======:-==++++++=#*
355
+ #+:::--====-------====:-==++++++=+*
356
+ *=:::--====-:----=====--==+++++++=*
357
+ #=:::-======:-========-:===++++++=*#
358
+ #-::--======:-==---===-:===++++++=+#
359
+ *----:-=====--:.......--===*+++++==#*
360
+ *--:...:==-:...........:===+++++**+**
361
+ *......:................:==-=++***++#
362
+ #=......:..:........:......---++**++++*
363
+ #=......::..................:-==+**+++**
364
+ #=.....:::::................:-==-+*+++*+*
365
+ *-......::::.......:........:-==--++++#==#
366
+ *-........:..........:...:...:-=-:-++=*#*+#
367
+ *:.........................:::-==----=+*
368
+ *:.......:....................::=-------+
369
+ *-............................::-----=#*-+
370
+ *-::......-=:.:++-...:-......::==----*##=+
371
+ ***=.=*--**+++#*+.:**++:::=+=::----=***-+
372
+ *=++-**++++**+*+=.-**+==-+*+*-.----+*++.=*
373
+ #=-++-***==**#*===++=*##**+==*=.--=**+++:=#
374
+ #*+#+-# ## #=-----+###++*-=*=.-+***++#*+
375
+ +-:# #-:----=### ###-=#+.-***+**
376
+ +..=# #=:-----*## #-=#=.:=#
377
+ =..-# +:-----+*% #:-#-::=+
378
+ *+=*+ *:-----=*## #:-#*==+*
379
+ #*:-----=**# #::#
380
+ *-------+*# *::#
381
+ #=------+*## +.:*
382
+ #*-----+*## #-..+
383
+ %#***#% +:..-#
384
+ =:..-+
385
+ +:..=*
386
+ #*=+*#
387
+
388
+
389
+ `;
390
+ function printBanner(version) {
391
+ console.log(ASCII_ART);
392
+ console.log(` CRMy v${version} \x1B[2m\u2014\x1B[0m Agent-first open source CRM
393
+ `);
126
394
  }
127
- function loadAuthState() {
395
+
396
+ // src/logger.ts
397
+ import fs3 from "fs";
398
+ import path3 from "path";
399
+ import os2 from "os";
400
+ var LOG_DIR = path3.join(os2.homedir(), ".crmy");
401
+ var LOG_FILE = path3.join(LOG_DIR, "crmy-server.log");
402
+ function ensureLogDir() {
403
+ fs3.mkdirSync(LOG_DIR, { recursive: true });
404
+ }
405
+ function logToFile(line) {
128
406
  try {
129
- const raw = fs2.readFileSync(AUTH_FILE, "utf-8");
130
- const state = JSON.parse(raw);
131
- if (state.expiresAt && new Date(state.expiresAt) < /* @__PURE__ */ new Date()) {
132
- return null;
133
- }
134
- return state;
407
+ ensureLogDir();
408
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
409
+ fs3.appendFileSync(LOG_FILE, `[${ts}] ${line}
410
+ `);
135
411
  } catch {
136
- return null;
137
412
  }
138
413
  }
139
- function saveAuthState(state) {
140
- fs2.mkdirSync(AUTH_DIR, { recursive: true });
141
- fs2.writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
142
- }
143
- function clearAuthState() {
414
+
415
+ // src/commands/server.ts
416
+ var _require = createRequire(import.meta.url);
417
+ function getCLIVersion() {
144
418
  try {
145
- fs2.unlinkSync(AUTH_FILE);
419
+ const pkg = _require(
420
+ path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "../../package.json")
421
+ );
422
+ return pkg.version;
146
423
  } catch {
424
+ return "0.5.5";
147
425
  }
148
426
  }
149
- function resolveServerUrl() {
150
- return process.env.CRMY_SERVER_URL ?? loadAuthState()?.serverUrl ?? loadConfigFile().serverUrl;
427
+ function printReadyBox(port) {
428
+ const lines = [
429
+ ` Web UI \u2192 http://localhost:${port}/app`,
430
+ ` API \u2192 http://localhost:${port}/api/v1`,
431
+ ` MCP \u2192 http://localhost:${port}/mcp`,
432
+ ` Health \u2192 http://localhost:${port}/health`
433
+ ];
434
+ const innerWidth = Math.max(...lines.map((l) => l.length)) + 2;
435
+ const bar = "\u2500".repeat(innerWidth);
436
+ console.log(`
437
+ \u250C${bar}\u2510`);
438
+ for (const line of lines) {
439
+ console.log(` \u2502${line.padEnd(innerWidth)}\u2502`);
440
+ }
441
+ console.log(` \u2514${bar}\u2518
442
+ `);
443
+ console.log(` Log file: ${LOG_FILE}`);
444
+ console.log(" Press Ctrl+C to stop\n");
151
445
  }
152
-
153
- // src/commands/server.ts
154
446
  function serverCommand() {
155
447
  return new Command2("server").description("Start the crmy HTTP server").option("--port <port>", "HTTP port", "3000").action(async (opts) => {
156
- const config = loadConfigFile();
448
+ const version = getCLIVersion();
449
+ const port = parseInt(opts.port, 10);
450
+ printBanner(version);
451
+ logToFile(`=== CRMy server starting (v${version}) ===`);
452
+ let config;
453
+ try {
454
+ config = loadConfigFile();
455
+ } catch (err) {
456
+ console.error(
457
+ "\n No .crmy.json found in the current directory.\n Run `npx @crmy/cli init` to set up CRMy first.\n"
458
+ );
459
+ logToFile("ERROR: .crmy.json not found");
460
+ process.exit(1);
461
+ }
157
462
  process.env.DATABASE_URL = config.database?.url ?? process.env.DATABASE_URL;
158
463
  process.env.JWT_SECRET = config.jwtSecret ?? process.env.JWT_SECRET ?? "dev-secret";
159
- process.env.PORT = opts.port;
464
+ process.env.PORT = String(port);
160
465
  process.env.CRMY_IMPORTED = "1";
161
466
  if (!process.env.DATABASE_URL) {
162
- console.error("No database URL. Run `crmy init` first or set DATABASE_URL.");
467
+ console.error(
468
+ "\n No database URL configured.\n Run `npx @crmy/cli init` first, or set the DATABASE_URL environment variable.\n"
469
+ );
470
+ logToFile("ERROR: No DATABASE_URL configured");
163
471
  process.exit(1);
164
472
  }
165
- const { createApp, loadConfig } = await import("@crmy/server");
473
+ const { createApp, loadConfig, closePool, shutdownPlugins } = await import("@crmy/server");
166
474
  const serverConfig = loadConfig();
167
- const { app } = await createApp(serverConfig);
168
- app.listen(serverConfig.port, () => {
169
- console.log(`crmy server ready on :${serverConfig.port}`);
170
- });
475
+ let spinner = createSpinner("");
476
+ const stepLabels = {
477
+ db_connect: "Connecting to database",
478
+ migrations: "Running database migrations",
479
+ seed_defaults: "Seeding defaults"
480
+ };
481
+ serverConfig.onProgress = (step, status, detail) => {
482
+ logToFile(`[${status.toUpperCase()}] ${step}${detail ? ": " + detail : ""}`);
483
+ if (status === "start") {
484
+ spinner = createSpinner(stepLabels[step] ?? step);
485
+ } else if (status === "done") {
486
+ const label = stepLabels[step] ?? step;
487
+ const suffix = detail ? ` \x1B[2m(${detail})\x1B[0m` : "";
488
+ spinner.succeed(`${label}${suffix}`);
489
+ } else if (status === "error") {
490
+ spinner.fail(`${stepLabels[step] ?? step} failed`);
491
+ }
492
+ };
493
+ let hitlInterval;
494
+ try {
495
+ const result = await createApp(serverConfig);
496
+ hitlInterval = result.hitlInterval;
497
+ const app = result.app;
498
+ spinner = createSpinner(`Starting HTTP server on port ${port}`);
499
+ logToFile(`[START] bind_port: ${port}`);
500
+ await new Promise((resolve, reject) => {
501
+ const srv = app.listen(port, () => {
502
+ spinner.succeed(`Server listening on port ${port}`);
503
+ logToFile(`[DONE] bind_port: ${port}`);
504
+ resolve();
505
+ });
506
+ srv.on("error", reject);
507
+ });
508
+ printReadyBox(port);
509
+ const shutdown = async () => {
510
+ console.log("\n Shutting down...");
511
+ logToFile("Received shutdown signal");
512
+ if (hitlInterval) clearInterval(hitlInterval);
513
+ await shutdownPlugins?.();
514
+ await closePool();
515
+ process.exit(0);
516
+ };
517
+ process.on("SIGTERM", shutdown);
518
+ process.on("SIGINT", shutdown);
519
+ } catch (err) {
520
+ const error = err;
521
+ spinner.stop();
522
+ if (error.code === "EADDRINUSE") {
523
+ console.error(
524
+ `
525
+ Port ${port} is already in use.
526
+ Try a different port:
527
+ npx @crmy/cli server --port ${port + 1}
528
+ `
529
+ );
530
+ logToFile(`ERROR: EADDRINUSE on port ${port}`);
531
+ } else if (error.message?.includes("ECONNREFUSED") || error.message?.includes("password authentication") || error.message?.includes("SCRAM") || error.message?.includes("connect") || error.message?.includes("database")) {
532
+ console.error(
533
+ `
534
+ Could not connect to PostgreSQL.
535
+ Error: ${error.message}
536
+
537
+ Check that:
538
+ \u2022 PostgreSQL is running
539
+ \u2022 The DATABASE_URL in .crmy.json is correct
540
+ \u2022 The database exists (create it: createdb crmy)
541
+ \u2022 The database user has the right permissions
542
+ `
543
+ );
544
+ logToFile(`ERROR: DB connection: ${error.message}`);
545
+ } else {
546
+ console.error(`
547
+ Server failed to start: ${error.message}
548
+ `);
549
+ logToFile(`ERROR: ${error.message}`);
550
+ }
551
+ process.exit(1);
552
+ }
171
553
  });
172
554
  }
173
555
 
@@ -175,19 +557,35 @@ function serverCommand() {
175
557
  import { Command as Command3 } from "commander";
176
558
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
177
559
  function mcpCommand() {
178
- return new Command3("mcp").description("Start stdio MCP server (for Claude Code)").action(async () => {
179
- const config = loadConfigFile();
560
+ return new Command3("mcp").description("Start stdio MCP server (for Claude Code)").option("--config <path>", "Explicit path to a .crmy.json config file").action(async (opts) => {
561
+ const config = loadConfigFile(opts.config);
180
562
  const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
181
563
  const apiKey = process.env.CRMY_API_KEY ?? config.apiKey;
182
564
  if (!databaseUrl) {
183
- console.error("No database URL. Run `crmy init` first or set DATABASE_URL.");
565
+ process.stderr.write(
566
+ "[crmy mcp] No database URL found.\n Run `npx @crmy/cli init` first, or pass --config <path>.\n Config lookup order:\n 1. process.cwd()/.crmy.json\n 2. ~/.crmy/config.json (written by init)\n"
567
+ );
184
568
  process.exit(1);
185
569
  }
186
570
  process.env.CRMY_IMPORTED = "1";
187
- const { initPool, createMcpServer } = await import("@crmy/server");
188
- const { runMigrations } = await import("@crmy/server");
189
- const db = await initPool(databaseUrl);
190
- await runMigrations(db);
571
+ const { initPool, createMcpServer, runMigrations } = await import("@crmy/server");
572
+ let db;
573
+ try {
574
+ db = await initPool(databaseUrl);
575
+ } catch (err) {
576
+ process.stderr.write(
577
+ `[crmy mcp] Failed to connect to database: ${err.message}
578
+ `
579
+ );
580
+ process.exit(1);
581
+ }
582
+ try {
583
+ await runMigrations(db);
584
+ } catch (err) {
585
+ process.stderr.write(`[crmy mcp] Migration error: ${err.message}
586
+ `);
587
+ process.exit(1);
588
+ }
191
589
  let actor = {
192
590
  tenant_id: "",
193
591
  actor_id: "cli-agent",
@@ -214,11 +612,19 @@ function mcpCommand() {
214
612
  }
215
613
  }
216
614
  if (!actor.tenant_id) {
217
- const tenantResult = await db.query("SELECT id FROM tenants WHERE slug = 'default' LIMIT 1");
615
+ const tenantResult = await db.query(
616
+ "SELECT id FROM tenants WHERE slug = 'default' LIMIT 1"
617
+ );
218
618
  if (tenantResult.rows.length > 0) {
219
619
  actor.tenant_id = tenantResult.rows[0].id;
220
620
  }
221
621
  }
622
+ if (!actor.tenant_id) {
623
+ process.stderr.write(
624
+ "[crmy mcp] No tenant found in database.\n Run `npx @crmy/cli init` to set up the database.\n"
625
+ );
626
+ process.exit(1);
627
+ }
222
628
  const server = createMcpServer(db, () => actor);
223
629
  const transport = new StdioServerTransport();
224
630
  await server.connect(transport);
@@ -354,8 +760,8 @@ function createHttpClient(serverUrl, token) {
354
760
  if (!mapping) {
355
761
  throw new Error(`Unknown tool: ${toolName} (no REST mapping)`);
356
762
  }
357
- const { method, path: path3 } = mapping;
358
- const url = `${serverUrl.replace(/\/$/, "")}${path3(input)}`;
763
+ const { method, path: path6 } = mapping;
764
+ const url = `${serverUrl.replace(/\/$/, "")}${path6(input)}`;
359
765
  const headers = {
360
766
  "Authorization": `Bearer ${token}`,
361
767
  "Content-Type": "application/json"
@@ -2003,8 +2409,19 @@ function helpCommand() {
2003
2409
  }
2004
2410
 
2005
2411
  // src/index.ts
2412
+ var _require2 = createRequire2(import.meta.url);
2413
+ function getCLIVersion2() {
2414
+ try {
2415
+ const pkg = _require2(
2416
+ path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "../package.json")
2417
+ );
2418
+ return pkg.version;
2419
+ } catch {
2420
+ return "0.5.5";
2421
+ }
2422
+ }
2006
2423
  var program = new Command27();
2007
- program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version("0.5.0");
2424
+ program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version(getCLIVersion2());
2008
2425
  program.addCommand(authCommand());
2009
2426
  program.addCommand(initCommand());
2010
2427
  program.addCommand(serverCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crmy/cli",
3
- "version": "0.5.5",
3
+ "version": "0.5.9",
4
4
  "description": "CRMy CLI — Local CLI + stdio MCP server",
5
5
  "type": "module",
6
6
  "files": [