@crmy/cli 0.5.5 → 0.5.7

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 +427 -54
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,67 +2,225 @@
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";
9
12
  import fs from "fs";
10
13
  import path 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/commands/init.ts
73
+ function validateEmail(input) {
74
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.trim()) ? true : "Please enter a valid email address (e.g. you@example.com)";
75
+ }
76
+ function validatePassword(input) {
77
+ return input.length >= 8 ? true : "Password must be at least 8 characters";
78
+ }
11
79
  function initCommand() {
12
- return new Command("init").description("Initialize crmy.ai: configure database, run migrations, create user").action(async () => {
80
+ return new Command("init").description("Interactive setup wizard: database, migrations, admin account").action(async () => {
13
81
  const { default: inquirer } = await import("inquirer");
14
- console.log("\n crmy.ai \u2014 Agent-first CRM setup\n");
15
- const answers = await inquirer.prompt([
82
+ 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");
83
+ console.log(" \u2502 crmy.ai \u2014 Setup Wizard \u2502");
84
+ 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");
85
+ console.log(" This wizard will:\n");
86
+ console.log(" Step 1 \u2014 Connect to your PostgreSQL database");
87
+ console.log(" Step 2 \u2014 Create all CRMy tables (migrations)");
88
+ console.log(" Step 3 \u2014 Create your admin account\n");
89
+ const configPath = path.join(process.cwd(), ".crmy.json");
90
+ if (fs.existsSync(configPath)) {
91
+ console.log(" \x1B[33m\u26A0\x1B[0m A .crmy.json already exists in this directory.\n");
92
+ const { overwrite } = await inquirer.prompt([
93
+ {
94
+ type: "confirm",
95
+ name: "overwrite",
96
+ message: " Overwrite it and run setup again?",
97
+ default: false
98
+ }
99
+ ]);
100
+ if (!overwrite) {
101
+ console.log("\n Setup cancelled. Your existing config was not changed.\n");
102
+ process.exit(0);
103
+ }
104
+ console.log("");
105
+ }
106
+ 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");
107
+ console.log(" Enter your PostgreSQL connection string.");
108
+ console.log(" Format: postgresql://user:password@host:5432/dbname\n");
109
+ console.log(" Options:");
110
+ console.log(" \u2022 Local install: postgresql://localhost:5432/crmy");
111
+ console.log(" \u2022 Docker: postgresql://postgres:postgres@localhost:5432/crmy");
112
+ console.log(" \u2022 Supabase: Project Settings \u2192 Database \u2192 Connection String");
113
+ console.log(" \u2022 Neon: Dashboard \u2192 Connection Details\n");
114
+ console.log(" \x1B[2mNote: the wizard will retry the connection up to 5 times.\x1B[0m\n");
115
+ const { databaseUrl } = await inquirer.prompt([
16
116
  {
17
117
  type: "input",
18
118
  name: "databaseUrl",
19
- message: "PostgreSQL URL?",
119
+ message: " PostgreSQL connection string:",
20
120
  default: "postgresql://localhost:5432/crmy"
21
- },
121
+ }
122
+ ]);
123
+ 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");
124
+ process.env.CRMY_IMPORTED = "1";
125
+ const { initPool, closePool, runMigrations } = await import("@crmy/server");
126
+ let spinner = createSpinner("Connecting to database\u2026");
127
+ let db;
128
+ try {
129
+ db = await initPool(databaseUrl);
130
+ spinner.succeed("Connected to database");
131
+ } catch (err) {
132
+ spinner.fail("Database connection failed");
133
+ const msg = err.message ?? String(err);
134
+ console.error(
135
+ `
136
+ Error: ${msg}
137
+
138
+ Common causes:
139
+ \u2022 PostgreSQL is not running
140
+ \u2022 Wrong host, port, or database name in the URL
141
+ \u2022 Wrong username or password
142
+ \u2022 Database does not exist \u2014 create it with: createdb crmy
143
+ `
144
+ );
145
+ process.exit(1);
146
+ }
147
+ spinner = createSpinner("Running database migrations\u2026");
148
+ let ran = [];
149
+ try {
150
+ ran = await runMigrations(db);
151
+ if (ran.length > 0) {
152
+ spinner.succeed(`Migrations complete \x1B[2m(${ran.length} applied)\x1B[0m`);
153
+ } else {
154
+ spinner.succeed("Migrations complete \x1B[2m(already up to date)\x1B[0m");
155
+ }
156
+ } catch (err) {
157
+ spinner.fail("Migration failed");
158
+ const msg = err.message ?? String(err);
159
+ console.error(
160
+ `
161
+ SQL error: ${msg}
162
+
163
+ This may mean:
164
+ \u2022 The database user lacks CREATE TABLE permissions
165
+ \u2022 A previous partial migration left the schema in a bad state
166
+ Try: drop and recreate the database, then run init again
167
+ `
168
+ );
169
+ await closePool();
170
+ process.exit(1);
171
+ }
172
+ spinner = createSpinner("Seeding default tenant\u2026");
173
+ let tenantId;
174
+ try {
175
+ const result = await db.query(
176
+ `INSERT INTO tenants (slug, name) VALUES ('default', 'Default Tenant')
177
+ ON CONFLICT (slug) DO UPDATE SET name = 'Default Tenant'
178
+ RETURNING id`
179
+ );
180
+ tenantId = result.rows[0].id;
181
+ spinner.succeed("Default tenant ready");
182
+ } catch (err) {
183
+ spinner.fail("Failed to seed tenant");
184
+ console.error(`
185
+ Error: ${err.message}
186
+ `);
187
+ await closePool();
188
+ process.exit(1);
189
+ }
190
+ 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");
191
+ console.log(" Create the first admin account for the CRMy web UI and CLI.\n");
192
+ console.log(
193
+ " \x1B[33mNOTE:\x1B[0m These are your \x1B[1mCRMy login credentials\x1B[0m \u2014 NOT your database credentials.\n"
194
+ );
195
+ const { name, email, password } = await inquirer.prompt([
22
196
  {
23
197
  type: "input",
24
198
  name: "name",
25
- message: "Your name?"
199
+ message: " Your full name:"
26
200
  },
27
201
  {
28
202
  type: "input",
29
203
  name: "email",
30
- message: "Your email?"
204
+ message: " Email address (used to log in):",
205
+ validate: validateEmail
31
206
  },
32
207
  {
33
208
  type: "password",
34
209
  name: "password",
35
- message: "Password?",
36
- mask: "*"
210
+ message: " Password (min 8 characters):",
211
+ mask: "*",
212
+ validate: validatePassword
37
213
  }
38
214
  ]);
39
- process.env.CRMY_IMPORTED = "1";
215
+ spinner = createSpinner("Creating admin account\u2026");
40
216
  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");
217
+ const passwordHash = crypto.createHash("sha256").update(password).digest("hex");
60
218
  const userResult = await db.query(
61
219
  `INSERT INTO users (tenant_id, email, name, role, password_hash)
62
220
  VALUES ($1, $2, $3, 'owner', $4)
63
221
  ON CONFLICT (tenant_id, email) DO UPDATE SET name = $3, password_hash = $4
64
222
  RETURNING id`,
65
- [tenantId, answers.email, answers.name, passwordHash]
223
+ [tenantId, email.trim(), name, passwordHash]
66
224
  );
67
225
  const userId = userResult.rows[0].id;
68
226
  const rawKey = "crmy_" + crypto.randomBytes(32).toString("hex");
@@ -72,42 +230,52 @@ function initCommand() {
72
230
  VALUES ($1, $2, $3, 'default', '{read,write,admin}')`,
73
231
  [tenantId, userId, keyHash]
74
232
  );
233
+ spinner.succeed("Admin account created");
75
234
  const jwtSecret = crypto.randomBytes(32).toString("hex");
76
- const config = {
235
+ const crmmyConfig = {
77
236
  serverUrl: "http://localhost:3000",
78
237
  apiKey: rawKey,
79
238
  tenantId: "default",
80
- database: {
81
- url: answers.databaseUrl
82
- },
239
+ database: { url: databaseUrl },
83
240
  jwtSecret,
84
241
  hitl: {
85
242
  requireApproval: ["bulk_update", "bulk_delete", "send_email"],
86
243
  autoApproveSeconds: 0
87
244
  }
88
245
  };
89
- const configPath = path.join(process.cwd(), ".crmy.json");
90
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
246
+ fs.writeFileSync(configPath, JSON.stringify(crmmyConfig, null, 2) + "\n");
91
247
  const gitignorePath = path.join(process.cwd(), ".gitignore");
92
248
  const gitignoreContent = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
93
249
  if (!gitignoreContent.includes(".crmy.json")) {
94
250
  fs.appendFileSync(gitignorePath, "\n.crmy.json\n");
95
251
  }
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
252
  } catch (err) {
103
- console.error("\nSetup failed:", err instanceof Error ? err.message : err);
253
+ spinner.fail("Failed to create admin account");
254
+ console.error(`
255
+ Error: ${err.message}
256
+ `);
257
+ await closePool();
104
258
  process.exit(1);
105
259
  }
260
+ await closePool();
261
+ 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");
262
+ console.log(" \u2502 \x1B[32m\u2713\x1B[0m CRMy is ready! \u2502");
263
+ 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");
264
+ console.log(` Admin account: ${email.trim()}`);
265
+ console.log(" Config saved: .crmy.json \x1B[2m(added to .gitignore)\x1B[0m\n");
266
+ console.log(" Next steps:\n");
267
+ console.log(" Start the server:");
268
+ console.log(" \x1B[1mnpx @crmy/cli server\x1B[0m\n");
269
+ console.log(" Connect to Claude Code:");
270
+ console.log(" \x1B[1mclaude mcp add crmy -- npx @crmy/cli mcp\x1B[0m\n");
106
271
  });
107
272
  }
108
273
 
109
274
  // src/commands/server.ts
110
275
  import { Command as Command2 } from "commander";
276
+ import { createRequire } from "module";
277
+ import { fileURLToPath } from "url";
278
+ import path4 from "path";
111
279
 
112
280
  // src/config.ts
113
281
  import fs2 from "fs";
@@ -150,24 +318,218 @@ function resolveServerUrl() {
150
318
  return process.env.CRMY_SERVER_URL ?? loadAuthState()?.serverUrl ?? loadConfigFile().serverUrl;
151
319
  }
152
320
 
321
+ // src/banner.ts
322
+ var ASCII_ART = `
323
+
324
+
325
+ ##*++++*###
326
+ #+-:.......:--=+#
327
+ *+-:::----------:--==*#
328
+ *-:-----------::--::=++=*
329
+ #=:-::-==--------------+++=#
330
+ *=..:--==-=-----==------+++==#
331
+ #-..:--==-=:----====----=+*++=#
332
+ *::.:-===-=-----====-:--=+++++=#
333
+ +-:.:-===-=---=-====-:-==++++++=#
334
+ *:-:--=====--=-======:-==++++++=#*
335
+ #+:::--====-------====:-==++++++=+*
336
+ *=:::--====-:----=====--==+++++++=*
337
+ #=:::-======:-========-:===++++++=*#
338
+ #-::--======:-==---===-:===++++++=+#
339
+ *----:-=====--:.......--===*+++++==#*
340
+ *--:...:==-:...........:===+++++**+**
341
+ *......:................:==-=++***++#
342
+ #=......:..:........:......---++**++++*
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
+ function printBanner(version) {
371
+ console.log(ASCII_ART);
372
+ console.log(` CRMy v${version} \x1B[2m\u2014\x1B[0m Agent-first open source CRM
373
+ `);
374
+ }
375
+
376
+ // src/logger.ts
377
+ import fs3 from "fs";
378
+ import path3 from "path";
379
+ import os2 from "os";
380
+ var LOG_DIR = path3.join(os2.homedir(), ".crmy");
381
+ var LOG_FILE = path3.join(LOG_DIR, "crmy-server.log");
382
+ function ensureLogDir() {
383
+ fs3.mkdirSync(LOG_DIR, { recursive: true });
384
+ }
385
+ function logToFile(line) {
386
+ try {
387
+ ensureLogDir();
388
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
389
+ fs3.appendFileSync(LOG_FILE, `[${ts}] ${line}
390
+ `);
391
+ } catch {
392
+ }
393
+ }
394
+
153
395
  // src/commands/server.ts
396
+ var _require = createRequire(import.meta.url);
397
+ function getCLIVersion() {
398
+ try {
399
+ const pkg = _require(
400
+ path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "../../package.json")
401
+ );
402
+ return pkg.version;
403
+ } catch {
404
+ return "0.5.5";
405
+ }
406
+ }
407
+ function printReadyBox(port) {
408
+ const lines = [
409
+ ` Web UI \u2192 http://localhost:${port}/app`,
410
+ ` API \u2192 http://localhost:${port}/api/v1`,
411
+ ` MCP \u2192 http://localhost:${port}/mcp`,
412
+ ` Health \u2192 http://localhost:${port}/health`
413
+ ];
414
+ const innerWidth = Math.max(...lines.map((l) => l.length)) + 2;
415
+ const bar = "\u2500".repeat(innerWidth);
416
+ console.log(`
417
+ \u250C${bar}\u2510`);
418
+ for (const line of lines) {
419
+ console.log(` \u2502${line.padEnd(innerWidth)}\u2502`);
420
+ }
421
+ console.log(` \u2514${bar}\u2518
422
+ `);
423
+ console.log(` Log file: ${LOG_FILE}`);
424
+ console.log(" Press Ctrl+C to stop\n");
425
+ }
154
426
  function serverCommand() {
155
427
  return new Command2("server").description("Start the crmy HTTP server").option("--port <port>", "HTTP port", "3000").action(async (opts) => {
156
- const config = loadConfigFile();
428
+ const version = getCLIVersion();
429
+ const port = parseInt(opts.port, 10);
430
+ printBanner(version);
431
+ logToFile(`=== CRMy server starting (v${version}) ===`);
432
+ let config;
433
+ try {
434
+ config = loadConfigFile();
435
+ } catch (err) {
436
+ console.error(
437
+ "\n No .crmy.json found in the current directory.\n Run `npx @crmy/cli init` to set up CRMy first.\n"
438
+ );
439
+ logToFile("ERROR: .crmy.json not found");
440
+ process.exit(1);
441
+ }
157
442
  process.env.DATABASE_URL = config.database?.url ?? process.env.DATABASE_URL;
158
443
  process.env.JWT_SECRET = config.jwtSecret ?? process.env.JWT_SECRET ?? "dev-secret";
159
- process.env.PORT = opts.port;
444
+ process.env.PORT = String(port);
160
445
  process.env.CRMY_IMPORTED = "1";
161
446
  if (!process.env.DATABASE_URL) {
162
- console.error("No database URL. Run `crmy init` first or set DATABASE_URL.");
447
+ console.error(
448
+ "\n No database URL configured.\n Run `npx @crmy/cli init` first, or set the DATABASE_URL environment variable.\n"
449
+ );
450
+ logToFile("ERROR: No DATABASE_URL configured");
163
451
  process.exit(1);
164
452
  }
165
- const { createApp, loadConfig } = await import("@crmy/server");
453
+ const { createApp, loadConfig, closePool, shutdownPlugins } = await import("@crmy/server");
166
454
  const serverConfig = loadConfig();
167
- const { app } = await createApp(serverConfig);
168
- app.listen(serverConfig.port, () => {
169
- console.log(`crmy server ready on :${serverConfig.port}`);
170
- });
455
+ let spinner = createSpinner("");
456
+ const stepLabels = {
457
+ db_connect: "Connecting to database",
458
+ migrations: "Running database migrations",
459
+ seed_defaults: "Seeding defaults"
460
+ };
461
+ serverConfig.onProgress = (step, status, detail) => {
462
+ logToFile(`[${status.toUpperCase()}] ${step}${detail ? ": " + detail : ""}`);
463
+ if (status === "start") {
464
+ spinner = createSpinner(stepLabels[step] ?? step);
465
+ } else if (status === "done") {
466
+ const label = stepLabels[step] ?? step;
467
+ const suffix = detail ? ` \x1B[2m(${detail})\x1B[0m` : "";
468
+ spinner.succeed(`${label}${suffix}`);
469
+ } else if (status === "error") {
470
+ spinner.fail(`${stepLabels[step] ?? step} failed`);
471
+ }
472
+ };
473
+ let hitlInterval;
474
+ try {
475
+ const result = await createApp(serverConfig);
476
+ hitlInterval = result.hitlInterval;
477
+ const app = result.app;
478
+ spinner = createSpinner(`Starting HTTP server on port ${port}`);
479
+ logToFile(`[START] bind_port: ${port}`);
480
+ await new Promise((resolve, reject) => {
481
+ const srv = app.listen(port, () => {
482
+ spinner.succeed(`Server listening on port ${port}`);
483
+ logToFile(`[DONE] bind_port: ${port}`);
484
+ resolve();
485
+ });
486
+ srv.on("error", reject);
487
+ });
488
+ printReadyBox(port);
489
+ const shutdown = async () => {
490
+ console.log("\n Shutting down...");
491
+ logToFile("Received shutdown signal");
492
+ if (hitlInterval) clearInterval(hitlInterval);
493
+ await shutdownPlugins?.();
494
+ await closePool();
495
+ process.exit(0);
496
+ };
497
+ process.on("SIGTERM", shutdown);
498
+ process.on("SIGINT", shutdown);
499
+ } catch (err) {
500
+ const error = err;
501
+ spinner.stop();
502
+ if (error.code === "EADDRINUSE") {
503
+ console.error(
504
+ `
505
+ Port ${port} is already in use.
506
+ Try a different port:
507
+ npx @crmy/cli server --port ${port + 1}
508
+ `
509
+ );
510
+ logToFile(`ERROR: EADDRINUSE on port ${port}`);
511
+ } else if (error.message?.includes("ECONNREFUSED") || error.message?.includes("password authentication") || error.message?.includes("SCRAM") || error.message?.includes("connect") || error.message?.includes("database")) {
512
+ console.error(
513
+ `
514
+ Could not connect to PostgreSQL.
515
+ Error: ${error.message}
516
+
517
+ Check that:
518
+ \u2022 PostgreSQL is running
519
+ \u2022 The DATABASE_URL in .crmy.json is correct
520
+ \u2022 The database exists (create it: createdb crmy)
521
+ \u2022 The database user has the right permissions
522
+ `
523
+ );
524
+ logToFile(`ERROR: DB connection: ${error.message}`);
525
+ } else {
526
+ console.error(`
527
+ Server failed to start: ${error.message}
528
+ `);
529
+ logToFile(`ERROR: ${error.message}`);
530
+ }
531
+ process.exit(1);
532
+ }
171
533
  });
172
534
  }
173
535
 
@@ -354,8 +716,8 @@ function createHttpClient(serverUrl, token) {
354
716
  if (!mapping) {
355
717
  throw new Error(`Unknown tool: ${toolName} (no REST mapping)`);
356
718
  }
357
- const { method, path: path3 } = mapping;
358
- const url = `${serverUrl.replace(/\/$/, "")}${path3(input)}`;
719
+ const { method, path: path6 } = mapping;
720
+ const url = `${serverUrl.replace(/\/$/, "")}${path6(input)}`;
359
721
  const headers = {
360
722
  "Authorization": `Bearer ${token}`,
361
723
  "Content-Type": "application/json"
@@ -2003,8 +2365,19 @@ function helpCommand() {
2003
2365
  }
2004
2366
 
2005
2367
  // src/index.ts
2368
+ var _require2 = createRequire2(import.meta.url);
2369
+ function getCLIVersion2() {
2370
+ try {
2371
+ const pkg = _require2(
2372
+ path5.resolve(path5.dirname(fileURLToPath2(import.meta.url)), "../package.json")
2373
+ );
2374
+ return pkg.version;
2375
+ } catch {
2376
+ return "0.5.5";
2377
+ }
2378
+ }
2006
2379
  var program = new Command27();
2007
- program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version("0.5.0");
2380
+ program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version(getCLIVersion2());
2008
2381
  program.addCommand(authCommand());
2009
2382
  program.addCommand(initCommand());
2010
2383
  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.7",
4
4
  "description": "CRMy CLI — Local CLI + stdio MCP server",
5
5
  "type": "module",
6
6
  "files": [