@crmy/cli 0.5.7 → 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.
Files changed (2) hide show
  1. package/dist/index.js +375 -84
  2. 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 Command27 } from "commander";
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";
@@ -9,8 +9,8 @@ import path5 from "path";
9
9
  // src/commands/init.ts
10
10
  import { Command } from "commander";
11
11
  import crypto from "crypto";
12
- import fs from "fs";
13
- import path from "path";
12
+ import fs2 from "fs";
13
+ import path2 from "path";
14
14
 
15
15
  // src/spinner.ts
16
16
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -69,12 +69,73 @@ function createSpinner(initialMessage) {
69
69
  };
70
70
  }
71
71
 
72
+ // src/config.ts
73
+ import fs from "fs";
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
+
72
133
  // src/commands/init.ts
73
134
  function validateEmail(input) {
74
135
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.trim()) ? true : "Please enter a valid email address (e.g. you@example.com)";
75
136
  }
76
137
  function validatePassword(input) {
77
- return input.length >= 8 ? true : "Password must be at least 8 characters";
138
+ return input.length >= 12 ? true : "Password must be at least 12 characters";
78
139
  }
79
140
  function initCommand() {
80
141
  return new Command("init").description("Interactive setup wizard: database, migrations, admin account").action(async () => {
@@ -86,8 +147,8 @@ function initCommand() {
86
147
  console.log(" Step 1 \u2014 Connect to your PostgreSQL database");
87
148
  console.log(" Step 2 \u2014 Create all CRMy tables (migrations)");
88
149
  console.log(" Step 3 \u2014 Create your admin account\n");
89
- const configPath = path.join(process.cwd(), ".crmy.json");
90
- if (fs.existsSync(configPath)) {
150
+ const localConfigPath = path2.join(process.cwd(), ".crmy.json");
151
+ if (fs2.existsSync(localConfigPath)) {
91
152
  console.log(" \x1B[33m\u26A0\x1B[0m A .crmy.json already exists in this directory.\n");
92
153
  const { overwrite } = await inquirer.prompt([
93
154
  {
@@ -188,33 +249,82 @@ function initCommand() {
188
249
  process.exit(1);
189
250
  }
190
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");
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([
196
- {
197
- type: "input",
198
- name: "name",
199
- message: " Your full name:"
200
- },
201
- {
202
- type: "input",
203
- name: "email",
204
- message: " Email address (used to log in):",
205
- validate: validateEmail
206
- },
207
- {
208
- type: "password",
209
- name: "password",
210
- message: " Password (min 8 characters):",
211
- mask: "*",
212
- validate: validatePassword
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);
213
301
  }
214
- ]);
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
+ }
215
323
  spinner = createSpinner("Creating admin account\u2026");
216
324
  try {
217
- const passwordHash = crypto.createHash("sha256").update(password).digest("hex");
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")}`;
218
328
  const userResult = await db.query(
219
329
  `INSERT INTO users (tenant_id, email, name, role, password_hash)
220
330
  VALUES ($1, $2, $3, 'owner', $4)
@@ -243,11 +353,11 @@ function initCommand() {
243
353
  autoApproveSeconds: 0
244
354
  }
245
355
  };
246
- fs.writeFileSync(configPath, JSON.stringify(crmmyConfig, null, 2) + "\n");
247
- const gitignorePath = path.join(process.cwd(), ".gitignore");
248
- const gitignoreContent = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf-8") : "";
356
+ saveConfigFile(crmmyConfig);
357
+ const gitignorePath = path2.join(process.cwd(), ".gitignore");
358
+ const gitignoreContent = fs2.existsSync(gitignorePath) ? fs2.readFileSync(gitignorePath, "utf-8") : "";
249
359
  if (!gitignoreContent.includes(".crmy.json")) {
250
- fs.appendFileSync(gitignorePath, "\n.crmy.json\n");
360
+ fs2.appendFileSync(gitignorePath, "\n.crmy.json\n");
251
361
  }
252
362
  } catch (err) {
253
363
  spinner.fail("Failed to create admin account");
@@ -257,7 +367,31 @@ function initCommand() {
257
367
  await closePool();
258
368
  process.exit(1);
259
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
+ }
260
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
+ }
261
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");
262
396
  console.log(" \u2502 \x1B[32m\u2713\x1B[0m CRMy is ready! \u2502");
263
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");
@@ -277,47 +411,6 @@ import { createRequire } from "module";
277
411
  import { fileURLToPath } from "url";
278
412
  import path4 from "path";
279
413
 
280
- // src/config.ts
281
- import fs2 from "fs";
282
- import path2 from "path";
283
- import os from "os";
284
- var AUTH_DIR = path2.join(os.homedir(), ".crmy");
285
- var AUTH_FILE = path2.join(AUTH_DIR, "auth.json");
286
- function loadConfigFile() {
287
- const configPath = path2.join(process.cwd(), ".crmy.json");
288
- try {
289
- const raw = fs2.readFileSync(configPath, "utf-8");
290
- return JSON.parse(raw);
291
- } catch {
292
- return {};
293
- }
294
- }
295
- function loadAuthState() {
296
- try {
297
- const raw = fs2.readFileSync(AUTH_FILE, "utf-8");
298
- const state = JSON.parse(raw);
299
- if (state.expiresAt && new Date(state.expiresAt) < /* @__PURE__ */ new Date()) {
300
- return null;
301
- }
302
- return state;
303
- } catch {
304
- return null;
305
- }
306
- }
307
- function saveAuthState(state) {
308
- fs2.mkdirSync(AUTH_DIR, { recursive: true });
309
- fs2.writeFileSync(AUTH_FILE, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
310
- }
311
- function clearAuthState() {
312
- try {
313
- fs2.unlinkSync(AUTH_FILE);
314
- } catch {
315
- }
316
- }
317
- function resolveServerUrl() {
318
- return process.env.CRMY_SERVER_URL ?? loadAuthState()?.serverUrl ?? loadConfigFile().serverUrl;
319
- }
320
-
321
414
  // src/banner.ts
322
415
  var ASCII_ART = `
323
416
 
@@ -537,19 +630,35 @@ function serverCommand() {
537
630
  import { Command as Command3 } from "commander";
538
631
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
539
632
  function mcpCommand() {
540
- return new Command3("mcp").description("Start stdio MCP server (for Claude Code)").action(async () => {
541
- const config = loadConfigFile();
633
+ 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) => {
634
+ const config = loadConfigFile(opts.config);
542
635
  const databaseUrl = process.env.DATABASE_URL ?? config.database?.url;
543
636
  const apiKey = process.env.CRMY_API_KEY ?? config.apiKey;
544
637
  if (!databaseUrl) {
545
- console.error("No database URL. Run `crmy init` first or set DATABASE_URL.");
638
+ process.stderr.write(
639
+ "[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"
640
+ );
546
641
  process.exit(1);
547
642
  }
548
643
  process.env.CRMY_IMPORTED = "1";
549
- const { initPool, createMcpServer } = await import("@crmy/server");
550
- const { runMigrations } = await import("@crmy/server");
551
- const db = await initPool(databaseUrl);
552
- await runMigrations(db);
644
+ const { initPool, createMcpServer, runMigrations } = await import("@crmy/server");
645
+ let db;
646
+ try {
647
+ db = await initPool(databaseUrl);
648
+ } catch (err) {
649
+ process.stderr.write(
650
+ `[crmy mcp] Failed to connect to database: ${err.message}
651
+ `
652
+ );
653
+ process.exit(1);
654
+ }
655
+ try {
656
+ await runMigrations(db);
657
+ } catch (err) {
658
+ process.stderr.write(`[crmy mcp] Migration error: ${err.message}
659
+ `);
660
+ process.exit(1);
661
+ }
553
662
  let actor = {
554
663
  tenant_id: "",
555
664
  actor_id: "cli-agent",
@@ -576,11 +685,19 @@ function mcpCommand() {
576
685
  }
577
686
  }
578
687
  if (!actor.tenant_id) {
579
- const tenantResult = await db.query("SELECT id FROM tenants WHERE slug = 'default' LIMIT 1");
688
+ const tenantResult = await db.query(
689
+ "SELECT id FROM tenants WHERE slug = 'default' LIMIT 1"
690
+ );
580
691
  if (tenantResult.rows.length > 0) {
581
692
  actor.tenant_id = tenantResult.rows[0].id;
582
693
  }
583
694
  }
695
+ if (!actor.tenant_id) {
696
+ process.stderr.write(
697
+ "[crmy mcp] No tenant found in database.\n Run `npx @crmy/cli init` to set up the database.\n"
698
+ );
699
+ process.exit(1);
700
+ }
584
701
  const server = createMcpServer(db, () => actor);
585
702
  const transport = new StdioServerTransport();
586
703
  await server.connect(transport);
@@ -2364,6 +2481,179 @@ function helpCommand() {
2364
2481
  });
2365
2482
  }
2366
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
+
2367
2657
  // src/index.ts
2368
2658
  var _require2 = createRequire2(import.meta.url);
2369
2659
  function getCLIVersion2() {
@@ -2376,7 +2666,7 @@ function getCLIVersion2() {
2376
2666
  return "0.5.5";
2377
2667
  }
2378
2668
  }
2379
- var program = new Command27();
2669
+ var program = new Command28();
2380
2670
  program.name("crmy").description("CRMy \u2014 The agent-first open source CRM").version(getCLIVersion2());
2381
2671
  program.addCommand(authCommand());
2382
2672
  program.addCommand(initCommand());
@@ -2403,6 +2693,7 @@ program.addCommand(contextCommand());
2403
2693
  program.addCommand(activityTypesCommand());
2404
2694
  program.addCommand(contextTypesCommand());
2405
2695
  program.addCommand(briefingCommand());
2696
+ program.addCommand(seedDemoCommand());
2406
2697
  program.addCommand(helpCommand());
2407
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) => {
2408
2699
  const args = ["auth", "login"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crmy/cli",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
4
4
  "description": "CRMy CLI — Local CLI + stdio MCP server",
5
5
  "type": "module",
6
6
  "files": [