@crmy/cli 0.5.9 → 0.6.0
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 +274 -27
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command28 } from "commander";
|
|
5
5
|
import { createRequire as createRequire2 } from "module";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
import path5 from "path";
|
|
@@ -135,7 +135,7 @@ function validateEmail(input) {
|
|
|
135
135
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.trim()) ? true : "Please enter a valid email address (e.g. you@example.com)";
|
|
136
136
|
}
|
|
137
137
|
function validatePassword(input) {
|
|
138
|
-
return input.length >=
|
|
138
|
+
return input.length >= 12 ? true : "Password must be at least 12 characters";
|
|
139
139
|
}
|
|
140
140
|
function initCommand() {
|
|
141
141
|
return new Command("init").description("Interactive setup wizard: database, migrations, admin account").action(async () => {
|
|
@@ -249,33 +249,82 @@ function initCommand() {
|
|
|
249
249
|
process.exit(1);
|
|
250
250
|
}
|
|
251
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
252
|
+
const envEmail = process.env.CRMY_ADMIN_EMAIL;
|
|
253
|
+
const envPassword = process.env.CRMY_ADMIN_PASSWORD;
|
|
254
|
+
const envName = process.env.CRMY_ADMIN_NAME;
|
|
255
|
+
const isInteractive = process.stdin.isTTY !== false && !envEmail;
|
|
256
|
+
let name;
|
|
257
|
+
let email;
|
|
258
|
+
let password;
|
|
259
|
+
if (isInteractive) {
|
|
260
|
+
console.log(" Create the first admin account for the CRMy web UI and CLI.\n");
|
|
261
|
+
console.log(
|
|
262
|
+
" \x1B[33mNOTE:\x1B[0m These are your \x1B[1mCRMy login credentials\x1B[0m \u2014 NOT your database credentials.\n"
|
|
263
|
+
);
|
|
264
|
+
const answers = await inquirer.prompt([
|
|
265
|
+
{
|
|
266
|
+
type: "input",
|
|
267
|
+
name: "name",
|
|
268
|
+
message: " Your full name:"
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
type: "input",
|
|
272
|
+
name: "email",
|
|
273
|
+
message: " Admin email:",
|
|
274
|
+
validate: validateEmail
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
type: "password",
|
|
278
|
+
name: "password",
|
|
279
|
+
message: " Admin password (min 12 chars):",
|
|
280
|
+
mask: "*",
|
|
281
|
+
validate: validatePassword
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
type: "password",
|
|
285
|
+
name: "confirmPassword",
|
|
286
|
+
message: " Confirm password:",
|
|
287
|
+
mask: "*",
|
|
288
|
+
validate: (input, answers2) => input === answers2?.password ? true : "Passwords do not match"
|
|
289
|
+
}
|
|
290
|
+
]);
|
|
291
|
+
name = answers.name;
|
|
292
|
+
email = answers.email;
|
|
293
|
+
password = answers.password;
|
|
294
|
+
} else {
|
|
295
|
+
if (!envEmail || !envPassword) {
|
|
296
|
+
console.error(
|
|
297
|
+
"\n Error: Non-interactive environment detected.\n\n Set CRMY_ADMIN_EMAIL and CRMY_ADMIN_PASSWORD environment variables\n to create the admin account without interactive prompts.\n"
|
|
298
|
+
);
|
|
299
|
+
await closePool();
|
|
300
|
+
process.exit(1);
|
|
274
301
|
}
|
|
275
|
-
|
|
302
|
+
const emailValid = validateEmail(envEmail);
|
|
303
|
+
if (emailValid !== true) {
|
|
304
|
+
console.error(`
|
|
305
|
+
Error: CRMY_ADMIN_EMAIL is invalid \u2014 ${emailValid}
|
|
306
|
+
`);
|
|
307
|
+
await closePool();
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
const pwValid = validatePassword(envPassword);
|
|
311
|
+
if (pwValid !== true) {
|
|
312
|
+
console.error(`
|
|
313
|
+
Error: CRMY_ADMIN_PASSWORD is invalid \u2014 ${pwValid}
|
|
314
|
+
`);
|
|
315
|
+
await closePool();
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
name = envName ?? "Admin";
|
|
319
|
+
email = envEmail;
|
|
320
|
+
password = envPassword;
|
|
321
|
+
console.log(" Creating admin account from environment variables\u2026\n");
|
|
322
|
+
}
|
|
276
323
|
spinner = createSpinner("Creating admin account\u2026");
|
|
277
324
|
try {
|
|
278
|
-
const
|
|
325
|
+
const salt = crypto.randomBytes(16);
|
|
326
|
+
const hash = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1 });
|
|
327
|
+
const passwordHash = `scrypt:${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
279
328
|
const userResult = await db.query(
|
|
280
329
|
`INSERT INTO users (tenant_id, email, name, role, password_hash)
|
|
281
330
|
VALUES ($1, $2, $3, 'owner', $4)
|
|
@@ -318,7 +367,31 @@ function initCommand() {
|
|
|
318
367
|
await closePool();
|
|
319
368
|
process.exit(1);
|
|
320
369
|
}
|
|
370
|
+
let seedDemo = false;
|
|
371
|
+
if (isInteractive) {
|
|
372
|
+
const { loadDemo } = await inquirer.prompt([
|
|
373
|
+
{
|
|
374
|
+
type: "confirm",
|
|
375
|
+
name: "loadDemo",
|
|
376
|
+
message: " Load demo data to explore CRMy?",
|
|
377
|
+
default: true
|
|
378
|
+
}
|
|
379
|
+
]);
|
|
380
|
+
seedDemo = loadDemo;
|
|
381
|
+
}
|
|
321
382
|
await closePool();
|
|
383
|
+
if (seedDemo) {
|
|
384
|
+
console.log("");
|
|
385
|
+
const { execSync } = await import("child_process");
|
|
386
|
+
try {
|
|
387
|
+
execSync(`DATABASE_URL="${databaseUrl}" npx tsx ${path2.resolve(path2.dirname(new URL(import.meta.url).pathname), "../../../../scripts/seed-demo.ts")}`, {
|
|
388
|
+
stdio: "inherit",
|
|
389
|
+
env: { ...process.env, DATABASE_URL: databaseUrl }
|
|
390
|
+
});
|
|
391
|
+
} catch {
|
|
392
|
+
console.log(" \x1B[33m\u26A0\x1B[0m Demo data seeding failed \u2014 you can run it later with: crmy seed-demo\n");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
322
395
|
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
396
|
console.log(" \u2502 \x1B[32m\u2713\x1B[0m CRMy is ready! \u2502");
|
|
324
397
|
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");
|
|
@@ -2408,6 +2481,179 @@ function helpCommand() {
|
|
|
2408
2481
|
});
|
|
2409
2482
|
}
|
|
2410
2483
|
|
|
2484
|
+
// src/commands/seed-demo.ts
|
|
2485
|
+
import { Command as Command27 } from "commander";
|
|
2486
|
+
function seedDemoCommand() {
|
|
2487
|
+
return new Command27("seed-demo").description("Seed rich demo data for exploring CRMy (idempotent)").option("--reset", "Drop and re-seed demo data (dev only)").action(async (opts) => {
|
|
2488
|
+
const config = loadConfigFile();
|
|
2489
|
+
const databaseUrl = config.database?.url ?? process.env.DATABASE_URL;
|
|
2490
|
+
if (!databaseUrl) {
|
|
2491
|
+
console.error(
|
|
2492
|
+
"\n Error: No database URL found.\n\n Either run `crmy init` first or set DATABASE_URL in your environment.\n"
|
|
2493
|
+
);
|
|
2494
|
+
process.exit(1);
|
|
2495
|
+
}
|
|
2496
|
+
const spinner = createSpinner("Seeding demo data\u2026");
|
|
2497
|
+
try {
|
|
2498
|
+
let daysAgo2 = function(n) {
|
|
2499
|
+
const d = /* @__PURE__ */ new Date();
|
|
2500
|
+
d.setDate(d.getDate() - n);
|
|
2501
|
+
return d.toISOString();
|
|
2502
|
+
}, daysFromNow2 = function(n) {
|
|
2503
|
+
const d = /* @__PURE__ */ new Date();
|
|
2504
|
+
d.setDate(d.getDate() + n);
|
|
2505
|
+
return d.toISOString().slice(0, 10);
|
|
2506
|
+
};
|
|
2507
|
+
var daysAgo = daysAgo2, daysFromNow = daysFromNow2;
|
|
2508
|
+
const pgMod = await import("pg");
|
|
2509
|
+
const { Pool } = pgMod.default ?? pgMod;
|
|
2510
|
+
const pool = new Pool({ connectionString: databaseUrl });
|
|
2511
|
+
const tenantRes = await pool.query(`SELECT id FROM tenants LIMIT 1`);
|
|
2512
|
+
if (tenantRes.rows.length === 0) {
|
|
2513
|
+
spinner.fail("No tenant found");
|
|
2514
|
+
console.error("\n Run `crmy init` first to create the database schema.\n");
|
|
2515
|
+
await pool.end();
|
|
2516
|
+
process.exit(1);
|
|
2517
|
+
}
|
|
2518
|
+
const tenantId = tenantRes.rows[0].id;
|
|
2519
|
+
const IDS = {
|
|
2520
|
+
ACTOR_CODY: "d0000000-0000-4000-a000-000000000001",
|
|
2521
|
+
ACTOR_SARAH_R: "d0000000-0000-4000-a000-000000000002",
|
|
2522
|
+
ACTOR_OUTREACH: "d0000000-0000-4000-a000-000000000003",
|
|
2523
|
+
ACTOR_RESEARCH: "d0000000-0000-4000-a000-000000000004",
|
|
2524
|
+
ACCT_ACME: "d0000000-0000-4000-b000-000000000001",
|
|
2525
|
+
ACCT_BRIGHTSIDE: "d0000000-0000-4000-b000-000000000002",
|
|
2526
|
+
ACCT_VERTEX: "d0000000-0000-4000-b000-000000000003",
|
|
2527
|
+
CT_SARAH_CHEN: "d0000000-0000-4000-c000-000000000001",
|
|
2528
|
+
CT_MARCUS_WEBB: "d0000000-0000-4000-c000-000000000002",
|
|
2529
|
+
CT_PRIYA_NAIR: "d0000000-0000-4000-c000-000000000003",
|
|
2530
|
+
CT_JORDAN_LIU: "d0000000-0000-4000-c000-000000000004",
|
|
2531
|
+
CT_TOMAS_RIVERA: "d0000000-0000-4000-c000-000000000005",
|
|
2532
|
+
CT_KEIKO_YAMAMOTO: "d0000000-0000-4000-c000-000000000006",
|
|
2533
|
+
OPP_ACME: "d0000000-0000-4000-d000-000000000001",
|
|
2534
|
+
OPP_BRIGHTSIDE: "d0000000-0000-4000-d000-000000000002",
|
|
2535
|
+
OPP_VERTEX: "d0000000-0000-4000-d000-000000000003",
|
|
2536
|
+
ACT_1: "d0000000-0000-4000-e000-000000000001",
|
|
2537
|
+
ACT_2: "d0000000-0000-4000-e000-000000000002",
|
|
2538
|
+
ACT_3: "d0000000-0000-4000-e000-000000000003",
|
|
2539
|
+
ACT_4: "d0000000-0000-4000-e000-000000000004",
|
|
2540
|
+
ACT_5: "d0000000-0000-4000-e000-000000000005",
|
|
2541
|
+
ACT_6: "d0000000-0000-4000-e000-000000000006",
|
|
2542
|
+
ACT_7: "d0000000-0000-4000-e000-000000000007",
|
|
2543
|
+
ACT_8: "d0000000-0000-4000-e000-000000000008",
|
|
2544
|
+
ACT_9: "d0000000-0000-4000-e000-000000000009",
|
|
2545
|
+
ACT_10: "d0000000-0000-4000-e000-000000000010",
|
|
2546
|
+
CTX_1: "d0000000-0000-4000-f000-000000000001",
|
|
2547
|
+
CTX_2: "d0000000-0000-4000-f000-000000000002",
|
|
2548
|
+
CTX_3: "d0000000-0000-4000-f000-000000000003",
|
|
2549
|
+
CTX_4: "d0000000-0000-4000-f000-000000000004",
|
|
2550
|
+
CTX_5: "d0000000-0000-4000-f000-000000000005",
|
|
2551
|
+
CTX_6: "d0000000-0000-4000-f000-000000000006",
|
|
2552
|
+
CTX_7: "d0000000-0000-4000-f000-000000000007",
|
|
2553
|
+
CTX_8: "d0000000-0000-4000-f000-000000000008",
|
|
2554
|
+
CTX_9: "d0000000-0000-4000-f000-000000000009",
|
|
2555
|
+
CTX_10: "d0000000-0000-4000-f000-000000000010",
|
|
2556
|
+
CTX_11: "d0000000-0000-4000-f000-000000000011",
|
|
2557
|
+
CTX_12: "d0000000-0000-4000-f000-000000000012",
|
|
2558
|
+
ASSIGN_1: "d0000000-0000-4000-f100-000000000001",
|
|
2559
|
+
ASSIGN_2: "d0000000-0000-4000-f100-000000000002",
|
|
2560
|
+
ASSIGN_3: "d0000000-0000-4000-f100-000000000003"
|
|
2561
|
+
};
|
|
2562
|
+
if (opts.reset) {
|
|
2563
|
+
const demoIds = Object.values(IDS);
|
|
2564
|
+
await pool.query(`DELETE FROM assignments WHERE id = ANY($1::uuid[])`, [demoIds]);
|
|
2565
|
+
await pool.query(`DELETE FROM context_entries WHERE id = ANY($1::uuid[])`, [demoIds]);
|
|
2566
|
+
await pool.query(`DELETE FROM activities WHERE id = ANY($1::uuid[])`, [demoIds]);
|
|
2567
|
+
await pool.query(`DELETE FROM opportunities WHERE id = ANY($1::uuid[])`, [demoIds]);
|
|
2568
|
+
await pool.query(`DELETE FROM contacts WHERE id = ANY($1::uuid[])`, [demoIds]);
|
|
2569
|
+
await pool.query(`DELETE FROM accounts WHERE id = ANY($1::uuid[])`, [demoIds]);
|
|
2570
|
+
await pool.query(`DELETE FROM actors WHERE id = ANY($1::uuid[])`, [demoIds]);
|
|
2571
|
+
}
|
|
2572
|
+
await pool.query(
|
|
2573
|
+
`INSERT INTO actors (id, tenant_id, actor_type, display_name, email) VALUES ($1, $2, 'human', 'Cody Harris', 'cody@crmy.ai') ON CONFLICT (id) DO NOTHING`,
|
|
2574
|
+
[IDS.ACTOR_CODY, tenantId]
|
|
2575
|
+
);
|
|
2576
|
+
await pool.query(
|
|
2577
|
+
`INSERT INTO actors (id, tenant_id, actor_type, display_name, email) VALUES ($1, $2, 'human', 'Sarah Reeves', 'sarah@crmy.ai') ON CONFLICT (id) DO NOTHING`,
|
|
2578
|
+
[IDS.ACTOR_SARAH_R, tenantId]
|
|
2579
|
+
);
|
|
2580
|
+
await pool.query(
|
|
2581
|
+
`INSERT INTO actors (id, tenant_id, actor_type, display_name, email, agent_identifier, agent_model) VALUES ($1, $2, 'agent', 'Outreach Agent', NULL, 'outreach-v1', 'claude-sonnet-4-20250514') ON CONFLICT (id) DO NOTHING`,
|
|
2582
|
+
[IDS.ACTOR_OUTREACH, tenantId]
|
|
2583
|
+
);
|
|
2584
|
+
await pool.query(
|
|
2585
|
+
`INSERT INTO actors (id, tenant_id, actor_type, display_name, email, agent_identifier, agent_model) VALUES ($1, $2, 'agent', 'Research Agent', NULL, 'research-v1', 'claude-sonnet-4-20250514') ON CONFLICT (id) DO NOTHING`,
|
|
2586
|
+
[IDS.ACTOR_RESEARCH, tenantId]
|
|
2587
|
+
);
|
|
2588
|
+
await pool.query(
|
|
2589
|
+
`INSERT INTO accounts (id, tenant_id, name, industry, health_score, annual_revenue, domain, website) VALUES ($1, $2, 'Acme Corp', 'SaaS', 72, 180000, 'acme.com', 'https://acme.com') ON CONFLICT (id) DO NOTHING`,
|
|
2590
|
+
[IDS.ACCT_ACME, tenantId]
|
|
2591
|
+
);
|
|
2592
|
+
await pool.query(
|
|
2593
|
+
`INSERT INTO accounts (id, tenant_id, name, industry, health_score, annual_revenue, domain, website) VALUES ($1, $2, 'Brightside Health', 'Healthcare', 45, 96000, 'brightsidehealth.com', 'https://brightsidehealth.com') ON CONFLICT (id) DO NOTHING`,
|
|
2594
|
+
[IDS.ACCT_BRIGHTSIDE, tenantId]
|
|
2595
|
+
);
|
|
2596
|
+
await pool.query(
|
|
2597
|
+
`INSERT INTO accounts (id, tenant_id, name, industry, health_score, annual_revenue, domain, website) VALUES ($1, $2, 'Vertex Logistics', 'Logistics', 88, 240000, 'vertex.io', 'https://vertex.io') ON CONFLICT (id) DO NOTHING`,
|
|
2598
|
+
[IDS.ACCT_VERTEX, tenantId]
|
|
2599
|
+
);
|
|
2600
|
+
await pool.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Sarah', 'Chen', 'sarah.chen@acme.com', 'VP Engineering', $3, 'prospect') ON CONFLICT (id) DO NOTHING`, [IDS.CT_SARAH_CHEN, tenantId, IDS.ACCT_ACME]);
|
|
2601
|
+
await pool.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Marcus', 'Webb', 'marcus.webb@acme.com', 'CFO', $3, 'prospect') ON CONFLICT (id) DO NOTHING`, [IDS.CT_MARCUS_WEBB, tenantId, IDS.ACCT_ACME]);
|
|
2602
|
+
await pool.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Priya', 'Nair', 'p.nair@brightsidehealth.com', 'CTO', $3, 'active') ON CONFLICT (id) DO NOTHING`, [IDS.CT_PRIYA_NAIR, tenantId, IDS.ACCT_BRIGHTSIDE]);
|
|
2603
|
+
await pool.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Jordan', 'Liu', 'j.liu@brightsidehealth.com', 'RevOps Lead', $3, 'active') ON CONFLICT (id) DO NOTHING`, [IDS.CT_JORDAN_LIU, tenantId, IDS.ACCT_BRIGHTSIDE]);
|
|
2604
|
+
await pool.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Tom\xE1s', 'Rivera', 't.rivera@vertex.io', 'Head of Sales Ops', $3, 'champion') ON CONFLICT (id) DO NOTHING`, [IDS.CT_TOMAS_RIVERA, tenantId, IDS.ACCT_VERTEX]);
|
|
2605
|
+
await pool.query(`INSERT INTO contacts (id, tenant_id, first_name, last_name, email, title, account_id, lifecycle_stage) VALUES ($1, $2, 'Keiko', 'Yamamoto', 'k.yamamoto@vertex.io', 'CEO', $3, 'champion') ON CONFLICT (id) DO NOTHING`, [IDS.CT_KEIKO_YAMAMOTO, tenantId, IDS.ACCT_VERTEX]);
|
|
2606
|
+
await pool.query(`INSERT INTO opportunities (id, tenant_id, name, account_id, stage, amount, close_date) VALUES ($1, $2, 'Acme Corp Enterprise Deal', $3, 'Discovery', 180000, '2026-06-30') ON CONFLICT (id) DO NOTHING`, [IDS.OPP_ACME, tenantId, IDS.ACCT_ACME]);
|
|
2607
|
+
await pool.query(`INSERT INTO opportunities (id, tenant_id, name, account_id, stage, amount, close_date) VALUES ($1, $2, 'Brightside Health Platform Deal', $3, 'PoC', 96000, '2026-05-15') ON CONFLICT (id) DO NOTHING`, [IDS.OPP_BRIGHTSIDE, tenantId, IDS.ACCT_BRIGHTSIDE]);
|
|
2608
|
+
await pool.query(`INSERT INTO opportunities (id, tenant_id, name, account_id, stage, amount, close_date) VALUES ($1, $2, 'Vertex Logistics Expansion', $3, 'Negotiation', 240000, '2026-04-30') ON CONFLICT (id) DO NOTHING`, [IDS.OPP_VERTEX, tenantId, IDS.ACCT_VERTEX]);
|
|
2609
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'outreach_email', 'Initial outreach to Sarah Chen', 'Personalized email introducing CRMy platform capabilities and requesting a discovery call.', $3, 'contact', $4, $5, 'replied', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_1, tenantId, IDS.ACTOR_OUTREACH, IDS.CT_SARAH_CHEN, daysAgo2(14), JSON.stringify({ to: "sarah.chen@acme.com", subject: "Scaling your sales ops after Series B", channel: "email" })]);
|
|
2610
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, related_type, related_id, occurred_at, outcome, detail) VALUES ($1, $2, 'meeting_held', 'Discovery call \u2014 Acme Corp', 'Discussed current pain points with Salesforce. Sarah is the champion. Marcus asked pointed questions about 6-month ROI.', $3, 'opportunity', $4, 'account', $5, $6, 'completed', $7) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_2, tenantId, IDS.ACTOR_CODY, IDS.OPP_ACME, IDS.ACCT_ACME, daysAgo2(10), JSON.stringify({ duration_minutes: 45, attendees: ["sarah.chen@acme.com", "marcus.webb@acme.com", "cody@crmy.ai"] })]);
|
|
2611
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'proposal_drafted', 'Draft proposal for Acme Corp Enterprise Deal', 'Generated initial proposal with three tiers. Included ROI projections and the Vertex case study.', $3, 'opportunity', $4, $5, 'completed', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_3, tenantId, IDS.ACTOR_OUTREACH, IDS.OPP_ACME, daysAgo2(7), JSON.stringify({ document_type: "proposal", version: "v1-draft" })]);
|
|
2612
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'research_completed', 'Deep research on Brightside Health', 'Compiled competitive landscape, org chart, recent funding, and technology stack analysis.', $3, 'account', $4, $5, 'completed', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_4, tenantId, IDS.ACTOR_RESEARCH, IDS.ACCT_BRIGHTSIDE, daysAgo2(5), JSON.stringify({ sources: ["crunchbase", "linkedin", "company_blog"], findings_count: 8 })]);
|
|
2613
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'outreach_call', 'Follow-up call to Dr. Priya Nair', 'Attempted direct call. Went to voicemail. Left message highlighting open-source model and self-hosting.', $3, 'contact', $4, $5, 'voicemail', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_5, tenantId, IDS.ACTOR_CODY, IDS.CT_PRIYA_NAIR, daysAgo2(3), JSON.stringify({ duration_minutes: 2, phone: "+1-555-0147", voicemail_left: true })]);
|
|
2614
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'meeting_scheduled', 'Executive alignment call \u2014 Vertex Logistics', 'Scheduled 30-minute call with Keiko Yamamoto and Tom\xE1s Rivera for next week.', $3, 'opportunity', $4, $5, 'scheduled', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_6, tenantId, IDS.ACTOR_OUTREACH, IDS.OPP_VERTEX, daysAgo2(2), JSON.stringify({ meeting_date: daysFromNow2(5), duration_minutes: 30 })]);
|
|
2615
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'stage_change', 'Vertex Logistics \u2192 Negotiation', 'Advanced deal from Qualification to Negotiation after PoC exceeded throughput targets by 22%.', $3, 'opportunity', $4, $5, 'completed', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_7, tenantId, IDS.ACTOR_CODY, IDS.OPP_VERTEX, daysAgo2(1), JSON.stringify({ from_stage: "Qualification", to_stage: "Negotiation" })]);
|
|
2616
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'outreach_email', 'Technical deep-dive request to Jordan Liu', 'Follow-up with API docs and sandbox link. Positioned around their pipeline update pain point.', $3, 'contact', $4, $5, 'opened', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_8, tenantId, IDS.ACTOR_OUTREACH, IDS.CT_JORDAN_LIU, daysAgo2(4), JSON.stringify({ to: "j.liu@brightsidehealth.com", subject: "CRMy sandbox access + MCP integration docs", channel: "email" })]);
|
|
2617
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'review', 'Internal review of Acme Corp proposal', 'Reviewed draft with Sarah Reeves. Identified three areas for revision.', $3, 'opportunity', $4, $5, 'completed', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_9, tenantId, IDS.ACTOR_SARAH_R, IDS.OPP_ACME, daysAgo2(6), JSON.stringify({ reviewers: ["sarah@crmy.ai", "cody@crmy.ai"], revision_items: 3 })]);
|
|
2618
|
+
await pool.query(`INSERT INTO activities (id, tenant_id, type, subject, body, performed_by, subject_type, subject_id, occurred_at, outcome, detail) VALUES ($1, $2, 'research_completed', 'Competitive analysis \u2014 Vertex Logistics', 'Vertex using custom Airtable CRM with Zapier. 20 hrs/week manual updates. Attio lacks MCP.', $3, 'account', $4, $5, 'completed', $6) ON CONFLICT (id) DO NOTHING`, [IDS.ACT_10, tenantId, IDS.ACTOR_RESEARCH, IDS.ACCT_VERTEX, daysAgo2(8), JSON.stringify({ sources: ["linkedin", "company_website", "glassdoor"], findings_count: 5 })]);
|
|
2619
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, valid_until, tags) VALUES ($1, $2, 'account', $3, 'objection', $4, 'CFO skeptical of ROI timeline', 'CFO Marcus Webb is skeptical about 6-month ROI claims \u2014 referenced a failed Salesforce implementation in 2023 that took 14 months to break even instead of the promised 6. He explicitly said "I will not sign off on anything that leads with theoretical projections." Approach with concrete case studies from similar-sized SaaS companies, not projections. The Vertex case study (40% ramp reduction in 90 days) is the strongest proof point we have.', 0.95, 'discovery_call', $5, $6) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_1, tenantId, IDS.ACCT_ACME, IDS.ACTOR_CODY, daysFromNow2(60), JSON.stringify(["roi", "cfo", "salesforce-migration"])]);
|
|
2620
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, valid_until, tags) VALUES ($1, $2, 'account', $3, 'objection', $4, 'Concern about vendor lock-in with proprietary MCP', 'Dr. Nair raised concerns about MCP being a proprietary protocol controlled by Anthropic. She asked specifically whether CRMy would work with non-Anthropic models and whether the MCP specification is truly open. This objection can likely be addressed by showing the open-source MCP spec and demonstrating multi-model support, but we have not had the opportunity to do so yet.', 0.7, 'research', $5, $6) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_2, tenantId, IDS.ACCT_BRIGHTSIDE, IDS.ACTOR_RESEARCH, daysFromNow2(30), JSON.stringify(["lock-in", "open-source", "mcp"])]);
|
|
2621
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, valid_until, tags) VALUES ($1, $2, 'account', $3, 'competitive_intel', $4, 'Brightside evaluating HubSpot and Attio', 'Brightside Health is actively evaluating HubSpot (Enterprise tier, $3,600/mo) and Attio (Growth plan, $1,200/mo) alongside CRMy. Neither has MCP support. Our differentiator is the open-source, self-hosted model with native MCP.', 0.85, 'linkedin_research', $5, $6) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_3, tenantId, IDS.ACCT_BRIGHTSIDE, IDS.ACTOR_RESEARCH, daysFromNow2(45), JSON.stringify(["hubspot", "attio", "competitive"])]);
|
|
2622
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, valid_until, tags) VALUES ($1, $2, 'account', $3, 'competitive_intel', $4, 'Vertex considering Attio as alternative', 'Vertex Logistics was evaluating Attio as a potential CRM replacement. This intelligence is from initial conversations and may be outdated \u2014 Tom\xE1s has since expressed strong preference for CRMy after the PoC results.', 0.6, 'initial_call', $5, $6) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_4, tenantId, IDS.ACCT_VERTEX, IDS.ACTOR_CODY, daysAgo2(10).slice(0, 10), JSON.stringify(["attio", "competitive", "airtable"])]);
|
|
2623
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, tags) VALUES ($1, $2, 'contact', $3, 'preference', $4, 'Sarah Chen communication preferences', 'Sarah Chen prefers async communication (Slack or email) over calls. She responds fastest to technical content \u2014 architecture diagrams, API documentation, and code examples. Avoid scheduling calls before 10am PT. She is the internal champion at Acme.', 0.9, 'observation', $5) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_5, tenantId, IDS.CT_SARAH_CHEN, IDS.ACTOR_OUTREACH, JSON.stringify(["communication", "async", "technical"])]);
|
|
2624
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, tags) VALUES ($1, $2, 'contact', $3, 'preference', $4, 'Marcus Webb decision-making style', 'Marcus Webb makes decisions based on quantitative data and peer references, not product demos. He explicitly asked for "three CFOs I can call who switched from Salesforce to CRMy." Lead every communication with a specific number or case study result.', 0.85, 'discovery_call', $5) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_6, tenantId, IDS.CT_MARCUS_WEBB, IDS.ACTOR_CODY, JSON.stringify(["decision-maker", "data-driven", "brevity"])]);
|
|
2625
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, tags) VALUES ($1, $2, 'account', $3, 'relationship_map', $4, 'Acme Corp internal dynamics', 'Sarah Chen (VP Engineering) is the champion. Marcus Webb (CFO) is the economic buyer and final decision maker. There is a third stakeholder \u2014 their VP Sales (name unknown) who would be the primary end user.', 0.8, 'discovery_call', $5) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_7, tenantId, IDS.ACCT_ACME, IDS.ACTOR_CODY, JSON.stringify(["champion", "buyer", "stakeholder-map"])]);
|
|
2626
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, tags) VALUES ($1, $2, 'account', $3, 'relationship_map', $4, 'Vertex Logistics buying committee', 'Tom\xE1s Rivera is the champion and day-to-day contact. Keiko Yamamoto (CEO) has final sign-off authority on all annual contracts over $100K. Tom\xE1s warned: "Don''t oversell to Keiko \u2014 she values directness."', 0.9, 'partner_feedback', $5) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_8, tenantId, IDS.ACCT_VERTEX, IDS.ACTOR_OUTREACH, JSON.stringify(["champion", "ceo", "buying-committee"])]);
|
|
2627
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, valid_until, tags) VALUES ($1, $2, 'account', $3, 'research', $4, 'Brightside Health org chart and tech stack', 'Brightside Health uses microservices on AWS (EKS). CTO Dr. Priya Nair reports to CEO. Engineering team of ~40, with 6 dedicated to internal tools. New CTO hire rumored for Q1 2026.', 0.65, 'web_research', $5, $6) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_9, tenantId, IDS.ACCT_BRIGHTSIDE, IDS.ACTOR_RESEARCH, daysAgo2(45).slice(0, 10), JSON.stringify(["org-chart", "tech-stack", "aws"])]);
|
|
2628
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, tags) VALUES ($1, $2, 'account', $3, 'summary', $4, 'Vertex Logistics account summary', 'Vertex Logistics is our highest-probability deal ($240K ARR, Negotiation stage). The PoC exceeded throughput targets by 22%. Remaining gate: CEO Keiko Yamamoto sign-off. Next action: executive alignment call scheduled.', 0.9, 'agent_synthesis', $5) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_10, tenantId, IDS.ACCT_VERTEX, IDS.ACTOR_RESEARCH, JSON.stringify(["deal-summary", "high-priority"])]);
|
|
2629
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, tags) VALUES ($1, $2, 'opportunity', $3, 'agent_reasoning', $4, 'Outreach strategy decision for Acme proposal revision', 'Decision: Lead revised Acme proposal with Vertex case study (40% ramp reduction in 90 days) rather than original ROI projections. Marcus Webb explicitly rejected theoretical projections. Sarah Chen confirmed concrete examples carry more weight.', 0.85, 'agent_analysis', $5) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_11, tenantId, IDS.OPP_ACME, IDS.ACTOR_OUTREACH, JSON.stringify(["strategy", "proposal", "reasoning"])]);
|
|
2630
|
+
await pool.query(`INSERT INTO context_entries (id, tenant_id, subject_type, subject_id, context_type, authored_by, title, body, confidence, source, source_activity_id, tags) VALUES ($1, $2, 'opportunity', $3, 'meeting_notes', $4, 'Acme Corp discovery call notes \u2014 2026-03-16', E'Attendees: Cody Harris, Sarah Chen, Marcus Webb. Duration: 45 min.
|
|
2631
|
+
|
|
2632
|
+
Key points: (1) Acme spending 15 hrs/week on manual CRM data entry. (2) Failed Salesforce impl in 2023. (3) Sarah wants API-first CRM for AI agents. (4) Marcus wants reference customers, not projections. (5) Sarah offered 2-week pilot.', 1.0, 'meeting_transcript', $5, $6) ON CONFLICT (id) DO NOTHING`, [IDS.CTX_12, tenantId, IDS.OPP_ACME, IDS.ACTOR_CODY, IDS.ACT_2, JSON.stringify(["discovery", "meeting", "acme"])]);
|
|
2633
|
+
await pool.query(`INSERT INTO assignments (id, tenant_id, title, description, assignment_type, assigned_by, assigned_to, subject_type, subject_id, status, priority, context) VALUES ($1, $2, 'Send revised proposal to Acme Corp \u2014 address Marcus Webb''s ROI concern', 'Review and send the revised proposal that leads with the Vertex case study.', 'send', $3, $4, 'opportunity', $5, 'pending', 'high', 'Marcus pushed back on the 6-month ROI claim in the last call. I''ve drafted a revised proposal in section 3 that leads with the Vertex case study (40% ramp reduction in 90 days). Sarah Chen is the champion \u2014 copy her on send. Do not cc Marcus directly.') ON CONFLICT (id) DO NOTHING`, [IDS.ASSIGN_1, tenantId, IDS.ACTOR_OUTREACH, IDS.ACTOR_CODY, IDS.OPP_ACME]);
|
|
2634
|
+
await pool.query(`INSERT INTO assignments (id, tenant_id, title, description, assignment_type, assigned_by, assigned_to, subject_type, subject_id, status, priority, context) VALUES ($1, $2, 'Review stale research on Brightside Health before next call', 'The research entry from January is past its valid_until date and needs verification.', 'review', $3, $4, 'account', $5, 'pending', 'normal', 'The research entry from January is past its valid_until date. Dr. Nair mentioned a new CTO hire at their last board meeting \u2014 the org chart context entry may be wrong.') ON CONFLICT (id) DO NOTHING`, [IDS.ASSIGN_2, tenantId, IDS.ACTOR_RESEARCH, IDS.ACTOR_SARAH_R, IDS.ACCT_BRIGHTSIDE]);
|
|
2635
|
+
await pool.query(`INSERT INTO assignments (id, tenant_id, title, description, assignment_type, assigned_by, assigned_to, subject_type, subject_id, status, priority, context) VALUES ($1, $2, 'Schedule executive alignment call with Keiko Yamamoto at Vertex', 'Coordinate schedules for a 15-minute executive alignment call.', 'call', $3, $4, 'opportunity', $5, 'accepted', 'urgent', 'Tomas has given us the green light but Keiko needs to sign off. Use the Calendly link in my preferences. Propose Tues/Thurs mornings PT.') ON CONFLICT (id) DO NOTHING`, [IDS.ASSIGN_3, tenantId, IDS.ACTOR_CODY, IDS.ACTOR_OUTREACH, IDS.OPP_VERTEX]);
|
|
2636
|
+
spinner.succeed("Demo data seeded successfully");
|
|
2637
|
+
await pool.end();
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
spinner.fail("Failed to seed demo data");
|
|
2640
|
+
console.error(`
|
|
2641
|
+
Error: ${err.message}
|
|
2642
|
+
`);
|
|
2643
|
+
process.exit(1);
|
|
2644
|
+
}
|
|
2645
|
+
console.log(" 4 actors (2 humans, 2 agents)");
|
|
2646
|
+
console.log(" 3 accounts \xB7 6 contacts \xB7 3 opportunities");
|
|
2647
|
+
console.log(" 10 activities \xB7 12 context entries \xB7 3 assignments");
|
|
2648
|
+
console.log("");
|
|
2649
|
+
console.log("Try it:");
|
|
2650
|
+
console.log(" crmy briefing contact:d0000000-0000-4000-c000-000000000001");
|
|
2651
|
+
console.log(" crmy briefing account:d0000000-0000-4000-b000-000000000001");
|
|
2652
|
+
console.log(" crmy briefing opportunity:d0000000-0000-4000-d000-000000000001");
|
|
2653
|
+
console.log("");
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2411
2657
|
// src/index.ts
|
|
2412
2658
|
var _require2 = createRequire2(import.meta.url);
|
|
2413
2659
|
function getCLIVersion2() {
|
|
@@ -2420,7 +2666,7 @@ function getCLIVersion2() {
|
|
|
2420
2666
|
return "0.5.5";
|
|
2421
2667
|
}
|
|
2422
2668
|
}
|
|
2423
|
-
var program = new
|
|
2669
|
+
var program = new Command28();
|
|
2424
2670
|
program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version(getCLIVersion2());
|
|
2425
2671
|
program.addCommand(authCommand());
|
|
2426
2672
|
program.addCommand(initCommand());
|
|
@@ -2447,6 +2693,7 @@ program.addCommand(contextCommand());
|
|
|
2447
2693
|
program.addCommand(activityTypesCommand());
|
|
2448
2694
|
program.addCommand(contextTypesCommand());
|
|
2449
2695
|
program.addCommand(briefingCommand());
|
|
2696
|
+
program.addCommand(seedDemoCommand());
|
|
2450
2697
|
program.addCommand(helpCommand());
|
|
2451
2698
|
program.command("login").description("Sign in to a CRMy server (shortcut for `crmy auth login`)").option("-e, --email <email>", "Email address").option("-p, --password <password>", "Password").action(async (opts) => {
|
|
2452
2699
|
const args = ["auth", "login"];
|