@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.
- package/dist/index.js +514 -97
- 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("
|
|
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
|
|
15
|
-
|
|
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
|
|
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: "
|
|
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
|
-
|
|
276
|
+
spinner = createSpinner("Creating admin account\u2026");
|
|
40
277
|
try {
|
|
41
|
-
const
|
|
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,
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
function clearAuthState() {
|
|
414
|
+
|
|
415
|
+
// src/commands/server.ts
|
|
416
|
+
var _require = createRequire(import.meta.url);
|
|
417
|
+
function getCLIVersion() {
|
|
144
418
|
try {
|
|
145
|
-
|
|
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
|
|
150
|
-
|
|
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
|
|
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 =
|
|
464
|
+
process.env.PORT = String(port);
|
|
160
465
|
process.env.CRMY_IMPORTED = "1";
|
|
161
466
|
if (!process.env.DATABASE_URL) {
|
|
162
|
-
console.error(
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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(
|
|
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:
|
|
358
|
-
const url = `${serverUrl.replace(/\/$/, "")}${
|
|
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(
|
|
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());
|