@hasna/contacts 0.3.2 → 0.4.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/cli/index.js CHANGED
@@ -2448,6 +2448,39 @@ var init_database = __esm(() => {
2448
2448
  );
2449
2449
 
2450
2450
  ALTER TABLE contact_notes ADD COLUMN company_id TEXT REFERENCES companies(id) ON DELETE SET NULL;
2451
+ `,
2452
+ `
2453
+ ALTER TABLE contacts ADD COLUMN do_not_contact INTEGER NOT NULL DEFAULT 0;
2454
+ ALTER TABLE contacts ADD COLUMN priority INTEGER NOT NULL DEFAULT 3 CHECK(priority BETWEEN 1 AND 5);
2455
+ ALTER TABLE contacts ADD COLUMN timezone TEXT;
2456
+
2457
+ CREATE TABLE IF NOT EXISTS deals (
2458
+ id TEXT PRIMARY KEY,
2459
+ title TEXT NOT NULL,
2460
+ contact_id TEXT REFERENCES contacts(id) ON DELETE SET NULL,
2461
+ company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
2462
+ stage TEXT NOT NULL DEFAULT 'lead' CHECK(stage IN ('lead','qualified','proposal','negotiation','won','lost','cancelled')),
2463
+ value_usd REAL,
2464
+ currency TEXT NOT NULL DEFAULT 'USD',
2465
+ close_date TEXT,
2466
+ notes TEXT,
2467
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2468
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2469
+ );
2470
+
2471
+ CREATE TABLE IF NOT EXISTS events (
2472
+ id TEXT PRIMARY KEY,
2473
+ title TEXT NOT NULL,
2474
+ type TEXT NOT NULL DEFAULT 'meeting' CHECK(type IN ('meeting','call','lunch','email','demo','conference','intro','other')),
2475
+ event_date TEXT NOT NULL,
2476
+ duration_min INTEGER,
2477
+ contact_ids TEXT NOT NULL DEFAULT '[]',
2478
+ company_id TEXT REFERENCES companies(id) ON DELETE SET NULL,
2479
+ notes TEXT,
2480
+ outcome TEXT,
2481
+ deal_id TEXT REFERENCES deals(id) ON DELETE SET NULL,
2482
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2483
+ );
2451
2484
  `
2452
2485
  ];
2453
2486
  });
@@ -2463,6 +2496,24 @@ var init_activity = __esm(() => {
2463
2496
  });
2464
2497
 
2465
2498
  // src/db/contacts.ts
2499
+ var exports_contacts = {};
2500
+ __export(exports_contacts, {
2501
+ updateContact: () => updateContact,
2502
+ unarchiveContact: () => unarchiveContact,
2503
+ searchContacts: () => searchContacts,
2504
+ mergeContacts: () => mergeContacts,
2505
+ listRecentContacts: () => listRecentContacts,
2506
+ listContacts: () => listContacts,
2507
+ listColdContacts: () => listColdContacts,
2508
+ getContactByEmail: () => getContactByEmail,
2509
+ getContact: () => getContact,
2510
+ deleteContact: () => deleteContact,
2511
+ createContact: () => createContact,
2512
+ autoLinkContactToCompany: () => autoLinkContactToCompany,
2513
+ archiveContact: () => archiveContact,
2514
+ addPhoneToContact: () => addPhoneToContact,
2515
+ addEmailToContact: () => addEmailToContact
2516
+ });
2466
2517
  function rowToContact(row) {
2467
2518
  return {
2468
2519
  ...row,
@@ -2472,7 +2523,10 @@ function rowToContact(row) {
2472
2523
  status: row.status ?? "active",
2473
2524
  follow_up_at: row.follow_up_at ?? null,
2474
2525
  archived: !!row.archived,
2475
- project_id: row.project_id ?? null
2526
+ project_id: row.project_id ?? null,
2527
+ do_not_contact: !!row.do_not_contact,
2528
+ priority: row.priority ?? 3,
2529
+ timezone: row.timezone ?? null
2476
2530
  };
2477
2531
  }
2478
2532
  function rowToEmail(row) {
@@ -2557,8 +2611,8 @@ function createContact(input, db) {
2557
2611
  const firstName = input.first_name ?? "";
2558
2612
  const lastName = input.last_name ?? "";
2559
2613
  const displayName = input.display_name ?? (firstName || lastName ? `${firstName} ${lastName}`.trim() : "Unnamed Contact");
2560
- d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, status, follow_up_at, project_id, created_at, updated_at)
2561
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2614
+ d.run(`INSERT INTO contacts (id, first_name, last_name, display_name, nickname, avatar_url, notes, birthday, company_id, job_title, source, custom_fields, last_contacted_at, website, preferred_contact_method, status, follow_up_at, project_id, do_not_contact, priority, timezone, created_at, updated_at)
2615
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2562
2616
  id,
2563
2617
  firstName,
2564
2618
  lastName,
@@ -2577,6 +2631,9 @@ function createContact(input, db) {
2577
2631
  input.status ?? "active",
2578
2632
  input.follow_up_at ?? null,
2579
2633
  input.project_id ?? null,
2634
+ input.do_not_contact ? 1 : 0,
2635
+ input.priority ?? 3,
2636
+ input.timezone ?? null,
2580
2637
  timestamp,
2581
2638
  timestamp
2582
2639
  ]);
@@ -2620,12 +2677,18 @@ function listContacts(opts = {}, db) {
2620
2677
  last_contacted_after,
2621
2678
  last_contacted_before,
2622
2679
  order_by = "display_name",
2623
- order_dir = "asc"
2680
+ order_dir = "asc",
2681
+ include_dnc = false,
2682
+ priority_min,
2683
+ updated_since
2624
2684
  } = opts;
2625
2685
  const conditions = [];
2626
2686
  const params = [];
2627
2687
  conditions.push("c.archived = ?");
2628
2688
  params.push(archived ? 1 : 0);
2689
+ if (!include_dnc) {
2690
+ conditions.push("c.do_not_contact = 0");
2691
+ }
2629
2692
  if (company_id) {
2630
2693
  conditions.push("c.company_id = ?");
2631
2694
  params.push(company_id);
@@ -2663,6 +2726,14 @@ function listContacts(opts = {}, db) {
2663
2726
  conditions.push("c.last_contacted_at <= ?");
2664
2727
  params.push(last_contacted_before);
2665
2728
  }
2729
+ if (priority_min !== undefined) {
2730
+ conditions.push("c.priority >= ?");
2731
+ params.push(priority_min);
2732
+ }
2733
+ if (updated_since) {
2734
+ conditions.push("c.updated_at >= ?");
2735
+ params.push(updated_since);
2736
+ }
2666
2737
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
2667
2738
  const validOrderBy = ["display_name", "created_at", "updated_at", "last_contacted_at", "follow_up_at"].includes(order_by) ? order_by : "display_name";
2668
2739
  const validOrderDir = order_dir === "desc" ? "DESC" : "ASC";
@@ -2746,6 +2817,18 @@ function updateContact(id, input, db) {
2746
2817
  setClauses.push("project_id = ?");
2747
2818
  params.push(input.project_id);
2748
2819
  }
2820
+ if (input.do_not_contact !== undefined) {
2821
+ setClauses.push("do_not_contact = ?");
2822
+ params.push(input.do_not_contact ? 1 : 0);
2823
+ }
2824
+ if (input.priority !== undefined) {
2825
+ setClauses.push("priority = ?");
2826
+ params.push(input.priority ?? 3);
2827
+ }
2828
+ if (input.timezone !== undefined) {
2829
+ setClauses.push("timezone = ?");
2830
+ params.push(input.timezone);
2831
+ }
2749
2832
  params.push(id);
2750
2833
  d.run(`UPDATE contacts SET ${setClauses.join(", ")} WHERE id = ?`, params);
2751
2834
  if (input.emails_add?.length) {
@@ -2818,6 +2901,160 @@ function listRecentContacts(limit, db) {
2818
2901
  const rows = d.query(`SELECT * FROM contacts ORDER BY updated_at DESC LIMIT ?`).all(limit);
2819
2902
  return rows.map((row) => loadContactDetails(d, rowToContact(row)));
2820
2903
  }
2904
+ function mergeContacts(keepId, mergeId, db) {
2905
+ const d = db || getDatabase();
2906
+ const keepRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
2907
+ if (!keepRow)
2908
+ throw new ContactNotFoundError(keepId);
2909
+ const mergeRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(mergeId);
2910
+ if (!mergeRow)
2911
+ throw new ContactNotFoundError(mergeId);
2912
+ const mergeEmailRows = d.query(`SELECT * FROM emails WHERE contact_id = ?`).all(mergeId);
2913
+ for (const e of mergeEmailRows) {
2914
+ const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(keepId, e.address);
2915
+ if (exists) {
2916
+ d.run(`DELETE FROM emails WHERE id = ?`, [e.id]);
2917
+ } else {
2918
+ d.run(`UPDATE emails SET contact_id = ? WHERE id = ?`, [keepId, e.id]);
2919
+ }
2920
+ }
2921
+ const mergePhoneRows = d.query(`SELECT * FROM phones WHERE contact_id = ?`).all(mergeId);
2922
+ for (const p of mergePhoneRows) {
2923
+ const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(keepId, p.number);
2924
+ if (exists) {
2925
+ d.run(`DELETE FROM phones WHERE id = ?`, [p.id]);
2926
+ } else {
2927
+ d.run(`UPDATE phones SET contact_id = ? WHERE id = ?`, [keepId, p.id]);
2928
+ }
2929
+ }
2930
+ d.run(`UPDATE addresses SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
2931
+ d.run(`UPDATE social_profiles SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
2932
+ const mergeTags = d.query(`SELECT tag_id FROM contact_tags WHERE contact_id = ?`).all(mergeId);
2933
+ for (const { tag_id } of mergeTags) {
2934
+ d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [keepId, tag_id]);
2935
+ }
2936
+ d.run(`UPDATE contact_relationships SET contact_a_id = ? WHERE contact_a_id = ?`, [keepId, mergeId]);
2937
+ d.run(`UPDATE contact_relationships SET contact_b_id = ? WHERE contact_b_id = ?`, [keepId, mergeId]);
2938
+ d.run(`UPDATE activity_log SET contact_id = ? WHERE contact_id = ?`, [keepId, mergeId]);
2939
+ d.run(`DELETE FROM contacts WHERE id = ?`, [mergeId]);
2940
+ const updates = ["updated_at = ?"];
2941
+ const params = [now()];
2942
+ if (!keepRow.notes && mergeRow.notes) {
2943
+ updates.push("notes = ?");
2944
+ params.push(mergeRow.notes);
2945
+ }
2946
+ if (!keepRow.nickname && mergeRow.nickname) {
2947
+ updates.push("nickname = ?");
2948
+ params.push(mergeRow.nickname);
2949
+ }
2950
+ if (!keepRow.avatar_url && mergeRow.avatar_url) {
2951
+ updates.push("avatar_url = ?");
2952
+ params.push(mergeRow.avatar_url);
2953
+ }
2954
+ if (!keepRow.birthday && mergeRow.birthday) {
2955
+ updates.push("birthday = ?");
2956
+ params.push(mergeRow.birthday);
2957
+ }
2958
+ if (!keepRow.company_id && mergeRow.company_id) {
2959
+ updates.push("company_id = ?");
2960
+ params.push(mergeRow.company_id);
2961
+ }
2962
+ if (!keepRow.job_title && mergeRow.job_title) {
2963
+ updates.push("job_title = ?");
2964
+ params.push(mergeRow.job_title);
2965
+ }
2966
+ params.push(keepId);
2967
+ d.run(`UPDATE contacts SET ${updates.join(", ")} WHERE id = ?`, params);
2968
+ logActivity(d, {
2969
+ contact_id: keepId,
2970
+ action: "contact.merged",
2971
+ details: `Merged contact ${mergeRow.display_name} (${mergeId}) into ${keepRow.display_name} (${keepId})`
2972
+ });
2973
+ const finalRow = d.query(`SELECT * FROM contacts WHERE id = ?`).get(keepId);
2974
+ return loadContactDetails(d, rowToContact(finalRow));
2975
+ }
2976
+ function getContactByEmail(email, db) {
2977
+ const d = db || getDatabase();
2978
+ const emailRow = d.query(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(email);
2979
+ if (!emailRow)
2980
+ return null;
2981
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(emailRow.contact_id);
2982
+ if (!row)
2983
+ return null;
2984
+ return loadContactDetails(d, rowToContact(row));
2985
+ }
2986
+ function addEmailToContact(contactId, email, db) {
2987
+ const d = db || getDatabase();
2988
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
2989
+ if (!row)
2990
+ throw new ContactNotFoundError(contactId);
2991
+ const exists = d.query(`SELECT id FROM emails WHERE contact_id = ? AND LOWER(address) = LOWER(?)`).get(contactId, email.address);
2992
+ if (!exists) {
2993
+ d.run(`INSERT INTO emails (id, contact_id, company_id, address, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?)`, [uuid(), contactId, email.address, email.type ?? "work", email.is_primary ? 1 : 0]);
2994
+ }
2995
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
2996
+ return loadContactDetails(d, rowToContact(updated));
2997
+ }
2998
+ function addPhoneToContact(contactId, phone, db) {
2999
+ const d = db || getDatabase();
3000
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
3001
+ if (!row)
3002
+ throw new ContactNotFoundError(contactId);
3003
+ const exists = d.query(`SELECT id FROM phones WHERE contact_id = ? AND number = ?`).get(contactId, phone.number);
3004
+ if (!exists) {
3005
+ d.run(`INSERT INTO phones (id, contact_id, company_id, number, country_code, type, is_primary) VALUES (?, ?, NULL, ?, ?, ?, ?)`, [uuid(), contactId, phone.number, phone.country_code ?? null, phone.type ?? "mobile", phone.is_primary ? 1 : 0]);
3006
+ }
3007
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
3008
+ return loadContactDetails(d, rowToContact(updated));
3009
+ }
3010
+ function archiveContact(id, db) {
3011
+ const d = db || getDatabase();
3012
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
3013
+ if (!row)
3014
+ throw new ContactNotFoundError(id);
3015
+ d.run(`UPDATE contacts SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
3016
+ logActivity(d, { contact_id: id, action: "contact.archived", details: `Archived contact: ${row.display_name}` });
3017
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
3018
+ return loadContactDetails(d, rowToContact(updated));
3019
+ }
3020
+ function unarchiveContact(id, db) {
3021
+ const d = db || getDatabase();
3022
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
3023
+ if (!row)
3024
+ throw new ContactNotFoundError(id);
3025
+ d.run(`UPDATE contacts SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
3026
+ logActivity(d, { contact_id: id, action: "contact.unarchived", details: `Unarchived contact: ${row.display_name}` });
3027
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(id);
3028
+ return loadContactDetails(d, rowToContact(updated));
3029
+ }
3030
+ function listColdContacts(days, db) {
3031
+ const d = db || getDatabase();
3032
+ const rows = d.query(`SELECT c.* FROM contacts c
3033
+ WHERE c.archived = 0 AND c.do_not_contact = 0
3034
+ AND (c.last_contacted_at IS NULL OR c.last_contacted_at < datetime('now', ? || ' days'))
3035
+ ORDER BY c.last_contacted_at ASC NULLS FIRST
3036
+ LIMIT 100`).all(`-${days}`);
3037
+ return rows.map((row) => loadContactDetails(d, rowToContact(row)));
3038
+ }
3039
+ function autoLinkContactToCompany(contactId, db) {
3040
+ const d = db || getDatabase();
3041
+ const row = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
3042
+ if (!row || row.company_id)
3043
+ return null;
3044
+ const emailRow = d.query(`SELECT address FROM emails WHERE contact_id = ? AND contact_id IS NOT NULL LIMIT 1`).get(contactId);
3045
+ if (!emailRow)
3046
+ return null;
3047
+ const domain = emailRow.address.split("@")[1];
3048
+ if (!domain)
3049
+ return null;
3050
+ const companyRow = d.query(`SELECT id FROM companies WHERE domain = ? LIMIT 1`).get(domain);
3051
+ if (!companyRow)
3052
+ return null;
3053
+ d.run(`UPDATE contacts SET company_id = ?, updated_at = ? WHERE id = ?`, [companyRow.id, now(), contactId]);
3054
+ logActivity(d, { contact_id: contactId, action: "contact.auto_linked", details: `Auto-linked to company via email domain: ${domain}` });
3055
+ const updated = d.query(`SELECT * FROM contacts WHERE id = ?`).get(contactId);
3056
+ return loadContactDetails(d, rowToContact(updated));
3057
+ }
2821
3058
  var init_contacts = __esm(() => {
2822
3059
  init_types();
2823
3060
  init_database();
@@ -3053,7 +3290,201 @@ var init_companies = __esm(() => {
3053
3290
  init_activity();
3054
3291
  });
3055
3292
 
3293
+ // src/db/relationships.ts
3294
+ function rowToCompanyRelationship(row) {
3295
+ return {
3296
+ ...row,
3297
+ relationship_type: row.relationship_type,
3298
+ start_date: row.start_date ?? null,
3299
+ end_date: row.end_date ?? null,
3300
+ is_primary: !!row.is_primary,
3301
+ status: row.status ?? "active"
3302
+ };
3303
+ }
3304
+ function createCompanyRelationship(input, db) {
3305
+ const d = db || getDatabase();
3306
+ const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
3307
+ if (!contact)
3308
+ throw new ContactNotFoundError(input.contact_id);
3309
+ const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(input.company_id);
3310
+ if (!company)
3311
+ throw new Error(`Company ${input.company_id} not found`);
3312
+ const id = uuid();
3313
+ d.run(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes, start_date, end_date, is_primary, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3314
+ id,
3315
+ input.contact_id,
3316
+ input.company_id,
3317
+ input.relationship_type,
3318
+ input.notes ?? null,
3319
+ input.start_date ?? null,
3320
+ input.end_date ?? null,
3321
+ input.is_primary ? 1 : 0,
3322
+ input.status ?? "active"
3323
+ ]);
3324
+ return rowToCompanyRelationship(d.query(`SELECT * FROM company_relationships WHERE id = ?`).get(id));
3325
+ }
3326
+ function listCompanyRelationships(opts = {}, db) {
3327
+ const d = db || getDatabase();
3328
+ const conditions = [];
3329
+ const params = [];
3330
+ if (opts.contact_id) {
3331
+ conditions.push("contact_id = ?");
3332
+ params.push(opts.contact_id);
3333
+ }
3334
+ if (opts.company_id) {
3335
+ conditions.push("company_id = ?");
3336
+ params.push(opts.company_id);
3337
+ }
3338
+ if (opts.relationship_type) {
3339
+ conditions.push("relationship_type = ?");
3340
+ params.push(opts.relationship_type);
3341
+ }
3342
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3343
+ const rows = d.query(`SELECT * FROM company_relationships ${where} ORDER BY created_at DESC`).all(...params);
3344
+ return rows.map(rowToCompanyRelationship);
3345
+ }
3346
+ var init_relationships = __esm(() => {
3347
+ init_types();
3348
+ init_database();
3349
+ });
3350
+
3351
+ // src/db/contact-tasks.ts
3352
+ function rowToContactTask(row) {
3353
+ return {
3354
+ id: row.id,
3355
+ title: row.title,
3356
+ description: row.description,
3357
+ contact_id: row.contact_id,
3358
+ assigned_by: row.assigned_by,
3359
+ deadline: row.deadline,
3360
+ status: row.status,
3361
+ priority: row.priority,
3362
+ entity_id: row.entity_id,
3363
+ linked_todos_task_id: row.linked_todos_task_id,
3364
+ escalation_rules: JSON.parse(row.escalation_rules || "[]"),
3365
+ created_at: row.created_at,
3366
+ updated_at: row.updated_at
3367
+ };
3368
+ }
3369
+ function createContactTask(input, db) {
3370
+ const d = db || getDatabase();
3371
+ const id = uuid();
3372
+ const timestamp = now();
3373
+ d.run(`INSERT INTO contact_tasks
3374
+ (id, title, description, contact_id, assigned_by, deadline, status, priority,
3375
+ entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
3376
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3377
+ id,
3378
+ input.title,
3379
+ input.description ?? null,
3380
+ input.contact_id,
3381
+ input.assigned_by ?? null,
3382
+ input.deadline ?? null,
3383
+ input.status ?? "pending",
3384
+ input.priority ?? "medium",
3385
+ input.entity_id ?? null,
3386
+ input.linked_todos_task_id ?? null,
3387
+ JSON.stringify(input.escalation_rules ?? []),
3388
+ timestamp,
3389
+ timestamp
3390
+ ]);
3391
+ return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
3392
+ }
3393
+ function listContactTasks(opts = {}, db) {
3394
+ const d = db || getDatabase();
3395
+ const conditions = [];
3396
+ const params = [];
3397
+ if (opts.contact_id) {
3398
+ conditions.push("contact_id = ?");
3399
+ params.push(opts.contact_id);
3400
+ }
3401
+ if (opts.entity_id) {
3402
+ conditions.push("entity_id = ?");
3403
+ params.push(opts.entity_id);
3404
+ }
3405
+ if (opts.status) {
3406
+ conditions.push("status = ?");
3407
+ params.push(opts.status);
3408
+ }
3409
+ if (opts.priority) {
3410
+ conditions.push("priority = ?");
3411
+ params.push(opts.priority);
3412
+ }
3413
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3414
+ const rows = d.query(`SELECT * FROM contact_tasks ${where} ORDER BY deadline ASC, priority DESC, created_at ASC`).all(...params);
3415
+ return rows.map(rowToContactTask);
3416
+ }
3417
+ function updateContactTask(id, input, db) {
3418
+ const d = db || getDatabase();
3419
+ const setClauses = ["updated_at = ?"];
3420
+ const params = [now()];
3421
+ if (input.title !== undefined) {
3422
+ setClauses.push("title = ?");
3423
+ params.push(input.title);
3424
+ }
3425
+ if ("description" in input) {
3426
+ setClauses.push("description = ?");
3427
+ params.push(input.description ?? null);
3428
+ }
3429
+ if ("assigned_by" in input) {
3430
+ setClauses.push("assigned_by = ?");
3431
+ params.push(input.assigned_by ?? null);
3432
+ }
3433
+ if ("deadline" in input) {
3434
+ setClauses.push("deadline = ?");
3435
+ params.push(input.deadline ?? null);
3436
+ }
3437
+ if (input.status !== undefined) {
3438
+ setClauses.push("status = ?");
3439
+ params.push(input.status);
3440
+ }
3441
+ if (input.priority !== undefined) {
3442
+ setClauses.push("priority = ?");
3443
+ params.push(input.priority);
3444
+ }
3445
+ if ("entity_id" in input) {
3446
+ setClauses.push("entity_id = ?");
3447
+ params.push(input.entity_id ?? null);
3448
+ }
3449
+ if ("linked_todos_task_id" in input) {
3450
+ setClauses.push("linked_todos_task_id = ?");
3451
+ params.push(input.linked_todos_task_id ?? null);
3452
+ }
3453
+ if (input.escalation_rules !== undefined) {
3454
+ setClauses.push("escalation_rules = ?");
3455
+ params.push(JSON.stringify(input.escalation_rules));
3456
+ }
3457
+ params.push(id);
3458
+ d.run(`UPDATE contact_tasks SET ${setClauses.join(", ")} WHERE id = ?`, params);
3459
+ return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
3460
+ }
3461
+ function listOverdueTasks(db) {
3462
+ const d = db || getDatabase();
3463
+ const now_iso = new Date().toISOString();
3464
+ const rows = d.query(`SELECT * FROM contact_tasks
3465
+ WHERE deadline < ? AND status NOT IN ('completed','cancelled')
3466
+ ORDER BY deadline ASC`).all(now_iso);
3467
+ return rows.map(rowToContactTask);
3468
+ }
3469
+ var init_contact_tasks = __esm(() => {
3470
+ init_database();
3471
+ });
3472
+
3056
3473
  // src/db/tags.ts
3474
+ var exports_tags = {};
3475
+ __export(exports_tags, {
3476
+ updateTag: () => updateTag,
3477
+ removeTagFromContact: () => removeTagFromContact,
3478
+ removeTagFromCompany: () => removeTagFromCompany,
3479
+ listTags: () => listTags,
3480
+ listContactsByTag: () => listContactsByTag,
3481
+ getTagByName: () => getTagByName,
3482
+ getTag: () => getTag,
3483
+ deleteTag: () => deleteTag,
3484
+ createTag: () => createTag,
3485
+ addTagToContact: () => addTagToContact,
3486
+ addTagToCompany: () => addTagToCompany
3487
+ });
3057
3488
  function rowToTag2(row) {
3058
3489
  return { ...row };
3059
3490
  }
@@ -3066,10 +3497,52 @@ function createTag(input, db) {
3066
3497
  d.run(`INSERT INTO tags (id, name, color, description) VALUES (?, ?, ?, ?)`, [id, input.name, input.color ?? "#6366f1", input.description ?? null]);
3067
3498
  return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
3068
3499
  }
3069
- function listTags(db) {
3500
+ function getTag(id, db) {
3070
3501
  const d = db || getDatabase();
3071
- return d.query(`SELECT * FROM tags ORDER BY name ASC`).all().map(rowToTag2);
3072
- }
3502
+ const row = d.query(`SELECT * FROM tags WHERE id = ?`).get(id);
3503
+ if (!row)
3504
+ throw new TagNotFoundError(id);
3505
+ return rowToTag2(row);
3506
+ }
3507
+ function getTagByName(name, db) {
3508
+ const d = db || getDatabase();
3509
+ const row = d.query(`SELECT * FROM tags WHERE name = ?`).get(name);
3510
+ return row ? rowToTag2(row) : null;
3511
+ }
3512
+ function listTags(db) {
3513
+ const d = db || getDatabase();
3514
+ return d.query(`SELECT * FROM tags ORDER BY name ASC`).all().map(rowToTag2);
3515
+ }
3516
+ function updateTag(id, input, db) {
3517
+ const d = db || getDatabase();
3518
+ const existing = d.query(`SELECT * FROM tags WHERE id = ?`).get(id);
3519
+ if (!existing)
3520
+ throw new TagNotFoundError(id);
3521
+ if (input.name && input.name !== existing.name) {
3522
+ const dupe = d.query(`SELECT id FROM tags WHERE name = ? AND id != ?`).get(input.name, id);
3523
+ if (dupe)
3524
+ throw new DuplicateTagNameError(input.name);
3525
+ }
3526
+ const setClauses = [];
3527
+ const params = [];
3528
+ if (input.name !== undefined) {
3529
+ setClauses.push("name = ?");
3530
+ params.push(input.name);
3531
+ }
3532
+ if (input.color !== undefined) {
3533
+ setClauses.push("color = ?");
3534
+ params.push(input.color);
3535
+ }
3536
+ if (input.description !== undefined) {
3537
+ setClauses.push("description = ?");
3538
+ params.push(input.description);
3539
+ }
3540
+ if (setClauses.length > 0) {
3541
+ params.push(id);
3542
+ d.run(`UPDATE tags SET ${setClauses.join(", ")} WHERE id = ?`, params);
3543
+ }
3544
+ return rowToTag2(d.query(`SELECT * FROM tags WHERE id = ?`).get(id));
3545
+ }
3073
3546
  function deleteTag(id, db) {
3074
3547
  const d = db || getDatabase();
3075
3548
  const row = d.query(`SELECT id FROM tags WHERE id = ?`).get(id);
@@ -3077,6 +3550,59 @@ function deleteTag(id, db) {
3077
3550
  throw new TagNotFoundError(id);
3078
3551
  d.run(`DELETE FROM tags WHERE id = ?`, [id]);
3079
3552
  }
3553
+ function addTagToContact(contactId, tagId, db) {
3554
+ const d = db || getDatabase();
3555
+ const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
3556
+ if (!contact)
3557
+ throw new ContactNotFoundError(contactId);
3558
+ const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
3559
+ if (!tag)
3560
+ throw new TagNotFoundError(tagId);
3561
+ d.run(`INSERT OR IGNORE INTO contact_tags (contact_id, tag_id) VALUES (?, ?)`, [contactId, tagId]);
3562
+ }
3563
+ function removeTagFromContact(contactId, tagId, db) {
3564
+ const d = db || getDatabase();
3565
+ d.run(`DELETE FROM contact_tags WHERE contact_id = ? AND tag_id = ?`, [contactId, tagId]);
3566
+ }
3567
+ function listContactsByTag(tagId, db) {
3568
+ const d = db || getDatabase();
3569
+ const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
3570
+ if (!tag)
3571
+ throw new TagNotFoundError(tagId);
3572
+ const rows = d.query(`
3573
+ SELECT c.* FROM contacts c
3574
+ JOIN contact_tags ct ON ct.contact_id = c.id
3575
+ WHERE ct.tag_id = ?
3576
+ ORDER BY c.display_name ASC
3577
+ `).all(tagId);
3578
+ return rows.map((r) => ({
3579
+ ...r,
3580
+ source: r.source,
3581
+ custom_fields: JSON.parse(r.custom_fields || "{}"),
3582
+ preferred_contact_method: r.preferred_contact_method ?? null,
3583
+ status: r.status ?? "active",
3584
+ follow_up_at: r.follow_up_at ?? null,
3585
+ archived: !!r.archived,
3586
+ project_id: r.project_id ?? null,
3587
+ do_not_contact: !!r.do_not_contact,
3588
+ priority: r.priority ?? 3,
3589
+ timezone: r.timezone ?? null
3590
+ }));
3591
+ }
3592
+ function addTagToCompany(companyId, tagId, db) {
3593
+ const d = db || getDatabase();
3594
+ const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(companyId);
3595
+ if (!company)
3596
+ throw new CompanyNotFoundError(companyId);
3597
+ const tag = d.query(`SELECT id FROM tags WHERE id = ?`).get(tagId);
3598
+ if (!tag)
3599
+ throw new TagNotFoundError(tagId);
3600
+ d.run(`INSERT OR IGNORE INTO company_tags (company_id, tag_id) VALUES (?, ?)`, [companyId, tagId]);
3601
+ }
3602
+ function removeTagFromCompany(companyId, tagId, db) {
3603
+ const d = db || getDatabase();
3604
+ d.run(`DELETE FROM company_tags WHERE company_id = ? AND tag_id = ?`, [companyId, tagId]);
3605
+ }
3080
3606
  var init_tags = __esm(() => {
3081
3607
  init_types();
3082
3608
  init_database();
@@ -3359,9 +3885,65 @@ function importFromJson(data) {
3359
3885
  };
3360
3886
  });
3361
3887
  }
3888
+ function parseLinkedInCsvLine(line) {
3889
+ return parseCsvLine(line);
3890
+ }
3891
+ function parseLinkedIn(csv) {
3892
+ const lines = csv.split(`
3893
+ `).filter((l) => l.trim());
3894
+ if (!lines.length)
3895
+ return [];
3896
+ const headers = parseLinkedInCsvLine(lines[0]).map((h) => h.replace(/"/g, "").trim());
3897
+ const firstNameIdx = headers.findIndex((h) => h === "First Name");
3898
+ const lastNameIdx = headers.findIndex((h) => h === "Last Name");
3899
+ const emailIdx = headers.findIndex((h) => h === "Email Address");
3900
+ const companyIdx = headers.findIndex((h) => h === "Company");
3901
+ const positionIdx = headers.findIndex((h) => h === "Position");
3902
+ const urlIdx = headers.findIndex((h) => h === "URL");
3903
+ const connectedIdx = headers.findIndex((h) => h === "Connected On");
3904
+ const results = [];
3905
+ for (let i = 1;i < lines.length; i++) {
3906
+ const cols = parseLinkedInCsvLine(lines[i]);
3907
+ const firstName = firstNameIdx >= 0 ? (cols[firstNameIdx] ?? "").trim() : "";
3908
+ const lastName = lastNameIdx >= 0 ? (cols[lastNameIdx] ?? "").trim() : "";
3909
+ if (!firstName && !lastName)
3910
+ continue;
3911
+ const contact = {
3912
+ first_name: firstName,
3913
+ last_name: lastName,
3914
+ display_name: `${firstName} ${lastName}`.trim(),
3915
+ source: "import"
3916
+ };
3917
+ if (emailIdx >= 0 && cols[emailIdx]?.trim()) {
3918
+ contact.emails = [{ address: cols[emailIdx].trim(), type: "work", is_primary: true }];
3919
+ }
3920
+ if (positionIdx >= 0 && cols[positionIdx]?.trim()) {
3921
+ contact.job_title = cols[positionIdx].trim();
3922
+ }
3923
+ if (urlIdx >= 0 && cols[urlIdx]?.trim()) {
3924
+ contact.social_profiles = [{ platform: "linkedin", url: cols[urlIdx].trim(), is_primary: true }];
3925
+ }
3926
+ if (companyIdx >= 0 && cols[companyIdx]?.trim()) {
3927
+ const connectedNote = connectedIdx >= 0 && cols[connectedIdx]?.trim() ? ` Connected on LinkedIn: ${cols[connectedIdx].trim()}` : "";
3928
+ contact.notes = `Company: ${cols[companyIdx].trim()}${connectedNote}`;
3929
+ } else if (connectedIdx >= 0 && cols[connectedIdx]?.trim()) {
3930
+ contact.notes = `Connected on LinkedIn: ${cols[connectedIdx].trim()}`;
3931
+ }
3932
+ results.push(contact);
3933
+ }
3934
+ return results;
3935
+ }
3936
+ function isLinkedInFormat(data) {
3937
+ const firstLine = data.split(`
3938
+ `)[0] ?? "";
3939
+ const lower = firstLine.toLowerCase();
3940
+ return lower.includes("first name") && lower.includes("url") && lower.includes("connected on");
3941
+ }
3362
3942
  async function importContacts(format, data) {
3363
3943
  switch (format) {
3364
3944
  case "csv":
3945
+ if (isLinkedInFormat(data))
3946
+ return parseLinkedIn(data);
3365
3947
  return importFromCsv(data);
3366
3948
  case "vcf":
3367
3949
  return parseVcf(data);
@@ -3808,66 +4390,365 @@ var init_serve = __esm(() => {
3808
4390
  DASHBOARD_DIST = join3(import.meta.dir, "../../dashboard/dist");
3809
4391
  });
3810
4392
 
3811
- // node_modules/commander/esm.mjs
3812
- var import__ = __toESM(require_commander(), 1);
3813
- var {
3814
- program,
3815
- createCommand,
3816
- createArgument,
3817
- createOption,
3818
- CommanderError,
3819
- InvalidArgumentError,
3820
- InvalidOptionArgumentError,
3821
- Command,
3822
- Argument,
3823
- Option,
3824
- Help
3825
- } = import__.default;
4393
+ // src/db/notes.ts
4394
+ var exports_notes = {};
4395
+ __export(exports_notes, {
4396
+ listNotesForContactAtCompany: () => listNotesForContactAtCompany,
4397
+ listNotes: () => listNotes,
4398
+ getNote: () => getNote,
4399
+ deleteNote: () => deleteNote,
4400
+ addNote: () => addNote
4401
+ });
4402
+ function addNote(contactId, body, createdBy, db, companyId) {
4403
+ const d = db || getDatabase();
4404
+ const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(contactId);
4405
+ if (!contact)
4406
+ throw new ContactNotFoundError(contactId);
4407
+ const id = uuid();
4408
+ d.run(`INSERT INTO contact_notes (id, contact_id, body, created_by, company_id) VALUES (?, ?, ?, ?, ?)`, [id, contactId, body, createdBy ?? null, companyId ?? null]);
4409
+ return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(id);
4410
+ }
4411
+ function listNotes(contactId, db) {
4412
+ const d = db || getDatabase();
4413
+ return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? ORDER BY created_at ASC`).all(contactId);
4414
+ }
4415
+ function listNotesForContactAtCompany(contactId, companyId, db) {
4416
+ const d = db || getDatabase();
4417
+ return d.query(`SELECT * FROM contact_notes WHERE contact_id = ? AND company_id = ? ORDER BY created_at ASC`).all(contactId, companyId);
4418
+ }
4419
+ function deleteNote(noteId, db) {
4420
+ const d = db || getDatabase();
4421
+ d.run(`DELETE FROM contact_notes WHERE id = ?`, [noteId]);
4422
+ }
4423
+ function getNote(noteId, db) {
4424
+ const d = db || getDatabase();
4425
+ return d.query(`SELECT * FROM contact_notes WHERE id = ?`).get(noteId);
4426
+ }
4427
+ var init_notes = __esm(() => {
4428
+ init_types();
4429
+ init_database();
4430
+ });
3826
4431
 
3827
- // src/cli/index.tsx
3828
- init_contacts();
3829
- init_companies();
3830
- import chalk from "chalk";
4432
+ // src/lib/timeline.ts
4433
+ var exports_timeline = {};
4434
+ __export(exports_timeline, {
4435
+ getContactTimeline: () => getContactTimeline
4436
+ });
4437
+ function getContactTimeline(contactId, limit = 50, db) {
4438
+ const _db2 = db || getDatabase();
4439
+ const items = [];
4440
+ const notes = _db2.query(`SELECT * FROM contact_notes WHERE contact_id = ? ORDER BY created_at DESC LIMIT 50`).all(contactId);
4441
+ for (const n of notes) {
4442
+ items.push({ date: n.created_at, type: "note", title: "Note", body: n.body });
4443
+ }
4444
+ const events = _db2.query(`SELECT * FROM events WHERE contact_ids LIKE ? ORDER BY event_date DESC LIMIT 50`).all(`%${contactId}%`);
4445
+ for (const e of events) {
4446
+ items.push({ date: e.event_date, type: "event", title: `${e.type}: ${e.title}`, body: e.notes ?? undefined, metadata: { outcome: e.outcome, duration_min: e.duration_min } });
4447
+ }
4448
+ const tasks = _db2.query(`SELECT * FROM contact_tasks WHERE contact_id = ? ORDER BY created_at DESC LIMIT 30`).all(contactId);
4449
+ for (const t of tasks) {
4450
+ items.push({ date: t.created_at, type: "task_created", title: `Task created: ${t.title}`, metadata: { deadline: t.deadline, priority: t.priority } });
4451
+ if (t.status === "completed") {
4452
+ items.push({ date: t.updated_at, type: "task_completed", title: `Task completed: ${t.title}` });
4453
+ }
4454
+ }
4455
+ const comms = _db2.query(`SELECT vc.*, co.name as company_name FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.contact_id = ? ORDER BY vc.comm_date DESC LIMIT 20`).all(contactId);
4456
+ for (const c of comms) {
4457
+ items.push({ date: c.comm_date, type: "vendor_comm", title: `${c.type} \u2014 ${c.company_name}`, body: c.subject ?? undefined });
4458
+ }
4459
+ const activity = _db2.query(`SELECT * FROM activity_log WHERE contact_id = ? ORDER BY created_at DESC LIMIT 30`).all(contactId);
4460
+ for (const a of activity) {
4461
+ items.push({ date: a.created_at, type: "interaction", title: a.action, body: a.details ?? undefined });
4462
+ }
4463
+ return items.sort((a, b) => b.date.localeCompare(a.date)).slice(0, limit);
4464
+ }
4465
+ var init_timeline = __esm(() => {
4466
+ init_database();
4467
+ });
3831
4468
 
3832
- // src/db/relationships.ts
3833
- init_types();
3834
- init_database();
3835
- function rowToCompanyRelationship(row) {
4469
+ // src/lib/brief.ts
4470
+ var exports_brief = {};
4471
+ __export(exports_brief, {
4472
+ generateBrief: () => generateBrief
4473
+ });
4474
+ function generateBrief(contactId, db) {
4475
+ const _db2 = db || getDatabase();
4476
+ const contact = getContact(contactId, _db2);
4477
+ const notes = listNotes(contactId, _db2);
4478
+ const allTasks = listContactTasks({ contact_id: contactId }, _db2);
4479
+ const tasks = allTasks.filter((t) => !["completed", "cancelled"].includes(t.status));
4480
+ const overdueTasks = allTasks.filter((t) => t.deadline && t.deadline < new Date().toISOString() && !["completed", "cancelled"].includes(t.status));
4481
+ const companyRels = listCompanyRelationships({ contact_id: contactId }, _db2);
4482
+ const recentTimeline = getContactTimeline(contactId, 5, _db2);
4483
+ const daysSince = contact.last_contacted_at ? Math.floor((Date.now() - new Date(contact.last_contacted_at).getTime()) / 86400000) : null;
4484
+ const lines = [];
4485
+ lines.push(`# ${contact.display_name}`);
4486
+ if (contact.job_title)
4487
+ lines.push(`**Role:** ${contact.job_title}${contact.company_id ? ` (linked to company)` : ""}`);
4488
+ if (contact.emails?.length) {
4489
+ const primary = contact.emails.find((e) => e.is_primary) || contact.emails[0];
4490
+ if (primary)
4491
+ lines.push(`**Email:** ${primary.address}`);
4492
+ }
4493
+ if (contact.phones?.length) {
4494
+ const primary = contact.phones.find((p) => p.is_primary) || contact.phones[0];
4495
+ if (primary)
4496
+ lines.push(`**Phone:** ${primary.number}`);
4497
+ }
4498
+ if (contact.preferred_contact_method)
4499
+ lines.push(`**Preferred contact:** ${contact.preferred_contact_method}`);
4500
+ lines.push("");
4501
+ lines.push(`## Status`);
4502
+ lines.push(`- Last contacted: ${daysSince !== null ? `${daysSince} days ago` : "never"}`);
4503
+ lines.push(`- Status: ${contact.status || "active"}`);
4504
+ if (contact.follow_up_at)
4505
+ lines.push(`- Follow-up scheduled: ${contact.follow_up_at}`);
4506
+ if (overdueTasks.length)
4507
+ lines.push(`- OVERDUE TASKS: ${overdueTasks.length}`);
4508
+ if (companyRels.length) {
4509
+ lines.push("");
4510
+ lines.push(`## Entity Relationships`);
4511
+ for (const r of companyRels)
4512
+ lines.push(`- ${r.relationship_type} \u2014 ${r.notes || ""}`);
4513
+ }
4514
+ if (tasks.length) {
4515
+ lines.push("");
4516
+ lines.push(`## Open Tasks`);
4517
+ for (const t of tasks)
4518
+ lines.push(`- [${t.priority}] ${t.title}${t.deadline ? ` (due ${t.deadline})` : ""}`);
4519
+ }
4520
+ if (notes.length) {
4521
+ lines.push("");
4522
+ lines.push(`## Recent Notes`);
4523
+ for (const n of notes.slice(0, 3))
4524
+ lines.push(`**${n.created_at.slice(0, 10)}:** ${n.body}`);
4525
+ }
4526
+ if (recentTimeline.length) {
4527
+ lines.push("");
4528
+ lines.push(`## Recent Activity`);
4529
+ for (const item of recentTimeline)
4530
+ lines.push(`- ${item.date.slice(0, 10)} ${item.title}`);
4531
+ }
4532
+ if (contact.notes) {
4533
+ lines.push("");
4534
+ lines.push(`## Background Notes`);
4535
+ lines.push(contact.notes);
4536
+ }
4537
+ return lines.join(`
4538
+ `);
4539
+ }
4540
+ var init_brief = __esm(() => {
4541
+ init_database();
4542
+ init_contacts();
4543
+ init_notes();
4544
+ init_contact_tasks();
4545
+ init_relationships();
4546
+ init_timeline();
4547
+ });
4548
+
4549
+ // src/lib/upcoming.ts
4550
+ var exports_upcoming = {};
4551
+ __export(exports_upcoming, {
4552
+ getUpcomingItems: () => getUpcomingItems
4553
+ });
4554
+ function getUpcomingItems(days = 7, db) {
4555
+ const _db2 = db || getDatabase();
4556
+ const items = [];
4557
+ const now3 = new Date;
4558
+ const future = new Date(now3.getTime() + days * 86400000);
4559
+ const todayStr = now3.toISOString().slice(0, 10);
4560
+ const futureStr = future.toISOString().slice(0, 10);
4561
+ const followUps = _db2.query(`SELECT c.id, c.display_name, c.follow_up_at FROM contacts c WHERE c.follow_up_at IS NOT NULL AND c.follow_up_at <= ? AND c.do_not_contact = 0`).all(futureStr);
4562
+ for (const r of followUps) {
4563
+ items.push({ date: r.follow_up_at, type: "follow_up", contact_id: r.id, contact_name: r.display_name, title: `Follow up with ${r.display_name}`, urgency: r.follow_up_at < todayStr ? "overdue" : r.follow_up_at === todayStr ? "today" : "upcoming" });
4564
+ }
4565
+ const tasks = _db2.query(`SELECT ct.*, c.display_name FROM contact_tasks ct JOIN contacts c ON ct.contact_id = c.id WHERE ct.deadline IS NOT NULL AND ct.deadline <= ? AND ct.status NOT IN ('completed','cancelled')`).all(futureStr);
4566
+ for (const t of tasks) {
4567
+ items.push({ date: t.deadline, type: "task_deadline", contact_id: t.contact_id, contact_name: t.display_name, title: t.title, urgency: t.deadline < todayStr ? "overdue" : t.deadline === todayStr ? "today" : "upcoming" });
4568
+ }
4569
+ const apps = _db2.query(`SELECT a.*, c.display_name as contact_name FROM applications a LEFT JOIN contacts c ON a.primary_contact_id = c.id WHERE a.follow_up_date IS NOT NULL AND a.follow_up_date <= ?`).all(futureStr);
4570
+ for (const a of apps) {
4571
+ items.push({ date: a.follow_up_date, type: "application_followup", contact_name: a.contact_name ?? undefined, title: `Follow up: ${a.program_name}`, urgency: a.follow_up_date < todayStr ? "overdue" : a.follow_up_date === todayStr ? "today" : "upcoming" });
4572
+ }
4573
+ const vendorFU = _db2.query(`SELECT vc.*, co.name as company_name FROM vendor_communications vc JOIN companies co ON vc.company_id = co.id WHERE vc.follow_up_date IS NOT NULL AND vc.follow_up_date <= ? AND vc.follow_up_done = 0`).all(futureStr);
4574
+ for (const v of vendorFU) {
4575
+ items.push({ date: v.follow_up_date, type: "vendor_followup", company_id: v.company_id, company_name: v.company_name, title: `Follow up with ${v.company_name}: ${v.subject || v.type}`, urgency: v.follow_up_date < todayStr ? "overdue" : v.follow_up_date === todayStr ? "today" : "upcoming" });
4576
+ }
4577
+ const contacts = _db2.query(`SELECT id, display_name, birthday FROM contacts WHERE birthday IS NOT NULL AND do_not_contact = 0`).all();
4578
+ for (const c of contacts) {
4579
+ const bday = new Date(c.birthday);
4580
+ const thisYear = new Date(now3.getFullYear(), bday.getMonth(), bday.getDate());
4581
+ const nextBday = thisYear >= now3 ? thisYear : new Date(now3.getFullYear() + 1, bday.getMonth(), bday.getDate());
4582
+ const nextStr = nextBday.toISOString().slice(0, 10);
4583
+ if (nextStr <= futureStr) {
4584
+ items.push({ date: nextStr, type: "birthday", contact_id: c.id, contact_name: c.display_name, title: `Birthday: ${c.display_name}`, urgency: nextStr === todayStr ? "today" : "upcoming" });
4585
+ }
4586
+ }
4587
+ return items.sort((a, b) => a.date.localeCompare(b.date));
4588
+ }
4589
+ var init_upcoming = __esm(() => {
4590
+ init_database();
4591
+ });
4592
+
4593
+ // src/lib/stats.ts
4594
+ var exports_stats = {};
4595
+ __export(exports_stats, {
4596
+ getNetworkStats: () => getNetworkStats
4597
+ });
4598
+ function getNetworkStats(db) {
4599
+ const _db2 = db || getDatabase();
4600
+ const q = (sql) => _db2.query(sql).get();
4601
+ const today = new Date().toISOString().slice(0, 10);
4602
+ const d30 = new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
4603
+ const d60 = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10);
3836
4604
  return {
3837
- ...row,
3838
- relationship_type: row.relationship_type,
3839
- start_date: row.start_date ?? null,
3840
- end_date: row.end_date ?? null,
3841
- is_primary: !!row.is_primary,
3842
- status: row.status ?? "active"
4605
+ total_contacts: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0`).c,
4606
+ total_companies: q(`SELECT COUNT(*) c FROM companies WHERE archived=0`).c,
4607
+ owned_entities: q(`SELECT COUNT(*) c FROM companies WHERE is_owned_entity=1`).c,
4608
+ total_tags: q(`SELECT COUNT(*) c FROM tags`).c,
4609
+ total_groups: q(`SELECT COUNT(*) c FROM groups`).c,
4610
+ total_deals: q(`SELECT COUNT(*) c FROM deals WHERE stage NOT IN ('won','lost','cancelled')`).c,
4611
+ total_events: q(`SELECT COUNT(*) c FROM events`).c,
4612
+ cold_30d: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND (last_contacted_at IS NULL OR last_contacted_at < '${d30}')`).c,
4613
+ cold_60d: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND (last_contacted_at IS NULL OR last_contacted_at < '${d60}')`).c,
4614
+ cold_never: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND do_not_contact=0 AND last_contacted_at IS NULL`).c,
4615
+ contacts_with_email: q(`SELECT COUNT(DISTINCT contact_id) c FROM emails WHERE contact_id IS NOT NULL`).c,
4616
+ contacts_with_phone: q(`SELECT COUNT(DISTINCT contact_id) c FROM phones WHERE contact_id IS NOT NULL`).c,
4617
+ contacts_no_company: q(`SELECT COUNT(*) c FROM contacts WHERE archived=0 AND company_id IS NULL`).c,
4618
+ overdue_tasks: q(`SELECT COUNT(*) c FROM contact_tasks WHERE deadline < '${today}' AND status NOT IN ('completed','cancelled')`).c,
4619
+ pending_applications: q(`SELECT COUNT(*) c FROM applications WHERE status IN ('submitted','pending','follow_up_needed')`).c,
4620
+ missing_invoices: q(`SELECT COUNT(*) c FROM vendor_communications WHERE type='invoice_request' AND status IN ('awaiting_response','no_response')`).c,
4621
+ upcoming_7d: q(`SELECT COUNT(*) c FROM contacts WHERE follow_up_at BETWEEN '${today}' AND date('${today}','+7 days')`).c,
4622
+ notes_count: q(`SELECT COUNT(*) c FROM contact_notes`).c,
4623
+ active_deals_value: q(`SELECT COALESCE(SUM(value_usd),0) c FROM deals WHERE stage NOT IN ('won','lost','cancelled') AND currency='USD'`).c
3843
4624
  };
3844
4625
  }
3845
- function createCompanyRelationship(input, db) {
4626
+ var init_stats = __esm(() => {
4627
+ init_database();
4628
+ });
4629
+
4630
+ // src/lib/audit.ts
4631
+ var exports_audit = {};
4632
+ __export(exports_audit, {
4633
+ listContactAudit: () => listContactAudit,
4634
+ auditContact: () => auditContact
4635
+ });
4636
+ function auditContact(contact) {
4637
+ const missing = [];
4638
+ const suggestions = [];
4639
+ let score = 0;
4640
+ if (contact.emails?.length)
4641
+ score += 20;
4642
+ else {
4643
+ missing.push("email");
4644
+ suggestions.push("Add an email address");
4645
+ }
4646
+ if (contact.phones?.length)
4647
+ score += 15;
4648
+ else {
4649
+ missing.push("phone");
4650
+ suggestions.push("Add a phone number");
4651
+ }
4652
+ if (contact.company_id)
4653
+ score += 15;
4654
+ else {
4655
+ missing.push("company");
4656
+ suggestions.push("Link to a company");
4657
+ }
4658
+ if (contact.last_contacted_at)
4659
+ score += 20;
4660
+ else {
4661
+ missing.push("last_contacted_at");
4662
+ suggestions.push("Log a contact interaction");
4663
+ }
4664
+ if (contact.tags?.length)
4665
+ score += 10;
4666
+ else {
4667
+ missing.push("tags");
4668
+ suggestions.push("Add at least one tag");
4669
+ }
4670
+ if (contact.notes)
4671
+ score += 10;
4672
+ else {
4673
+ missing.push("notes");
4674
+ suggestions.push("Add notes");
4675
+ }
4676
+ if (contact.job_title)
4677
+ score += 10;
4678
+ else {
4679
+ missing.push("job_title");
4680
+ suggestions.push("Add a job title");
4681
+ }
4682
+ return { contact_id: contact.id, display_name: contact.display_name, score, missing, suggestions };
4683
+ }
4684
+ async function listContactAudit(db) {
4685
+ const _db2 = db || getDatabase();
4686
+ const { listContacts: listContacts2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
4687
+ const { contacts } = listContacts2({ limit: 500, include_dnc: true }, _db2);
4688
+ return contacts.map(auditContact).sort((a, b) => a.score - b.score);
4689
+ }
4690
+ var init_audit = __esm(() => {
4691
+ init_database();
4692
+ });
4693
+
4694
+ // src/db/deals.ts
4695
+ var exports_deals = {};
4696
+ __export(exports_deals, {
4697
+ updateDeal: () => updateDeal,
4698
+ listDeals: () => listDeals,
4699
+ getDealsByStage: () => getDealsByStage,
4700
+ getDeal: () => getDeal,
4701
+ deleteDeal: () => deleteDeal,
4702
+ createDeal: () => createDeal
4703
+ });
4704
+ function rowToDeal(row) {
4705
+ return {
4706
+ id: row.id,
4707
+ title: row.title,
4708
+ contact_id: row.contact_id,
4709
+ company_id: row.company_id,
4710
+ stage: row.stage,
4711
+ value_usd: row.value_usd,
4712
+ currency: row.currency,
4713
+ close_date: row.close_date,
4714
+ notes: row.notes,
4715
+ created_at: row.created_at,
4716
+ updated_at: row.updated_at
4717
+ };
4718
+ }
4719
+ function createDeal(input, db) {
3846
4720
  const d = db || getDatabase();
3847
- const contact = d.query(`SELECT id FROM contacts WHERE id = ?`).get(input.contact_id);
3848
- if (!contact)
3849
- throw new ContactNotFoundError(input.contact_id);
3850
- const company = d.query(`SELECT id FROM companies WHERE id = ?`).get(input.company_id);
3851
- if (!company)
3852
- throw new Error(`Company ${input.company_id} not found`);
3853
4721
  const id = uuid();
3854
- d.run(`INSERT INTO company_relationships (id, contact_id, company_id, relationship_type, notes, start_date, end_date, is_primary, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4722
+ const timestamp = now();
4723
+ d.run(`INSERT INTO deals (id, title, contact_id, company_id, stage, value_usd, currency, close_date, notes, created_at, updated_at)
4724
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3855
4725
  id,
3856
- input.contact_id,
3857
- input.company_id,
3858
- input.relationship_type,
4726
+ input.title,
4727
+ input.contact_id ?? null,
4728
+ input.company_id ?? null,
4729
+ input.stage ?? "lead",
4730
+ input.value_usd ?? null,
4731
+ input.currency ?? "USD",
4732
+ input.close_date ?? null,
3859
4733
  input.notes ?? null,
3860
- input.start_date ?? null,
3861
- input.end_date ?? null,
3862
- input.is_primary ? 1 : 0,
3863
- input.status ?? "active"
4734
+ timestamp,
4735
+ timestamp
3864
4736
  ]);
3865
- return rowToCompanyRelationship(d.query(`SELECT * FROM company_relationships WHERE id = ?`).get(id));
4737
+ return rowToDeal(d.query(`SELECT * FROM deals WHERE id = ?`).get(id));
3866
4738
  }
3867
- function listCompanyRelationships(opts = {}, db) {
4739
+ function getDeal(id, db) {
4740
+ const d = db || getDatabase();
4741
+ const row = d.query(`SELECT * FROM deals WHERE id = ?`).get(id);
4742
+ return row ? rowToDeal(row) : null;
4743
+ }
4744
+ function listDeals(opts = {}, db) {
3868
4745
  const d = db || getDatabase();
3869
4746
  const conditions = [];
3870
4747
  const params = [];
4748
+ if (opts.stage) {
4749
+ conditions.push("stage = ?");
4750
+ params.push(opts.stage);
4751
+ }
3871
4752
  if (opts.contact_id) {
3872
4753
  conditions.push("contact_id = ?");
3873
4754
  params.push(opts.contact_id);
@@ -3876,14 +4757,183 @@ function listCompanyRelationships(opts = {}, db) {
3876
4757
  conditions.push("company_id = ?");
3877
4758
  params.push(opts.company_id);
3878
4759
  }
3879
- if (opts.relationship_type) {
3880
- conditions.push("relationship_type = ?");
3881
- params.push(opts.relationship_type);
4760
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4761
+ const rows = d.query(`SELECT * FROM deals ${where} ORDER BY created_at DESC`).all(...params);
4762
+ return rows.map(rowToDeal);
4763
+ }
4764
+ function updateDeal(id, input, db) {
4765
+ const d = db || getDatabase();
4766
+ const existing = d.query(`SELECT * FROM deals WHERE id = ?`).get(id);
4767
+ if (!existing)
4768
+ return null;
4769
+ const setClauses = ["updated_at = ?"];
4770
+ const params = [now()];
4771
+ if (input.title !== undefined) {
4772
+ setClauses.push("title = ?");
4773
+ params.push(input.title);
4774
+ }
4775
+ if (input.contact_id !== undefined) {
4776
+ setClauses.push("contact_id = ?");
4777
+ params.push(input.contact_id ?? null);
4778
+ }
4779
+ if (input.company_id !== undefined) {
4780
+ setClauses.push("company_id = ?");
4781
+ params.push(input.company_id ?? null);
4782
+ }
4783
+ if (input.stage !== undefined) {
4784
+ setClauses.push("stage = ?");
4785
+ params.push(input.stage);
4786
+ }
4787
+ if (input.value_usd !== undefined) {
4788
+ setClauses.push("value_usd = ?");
4789
+ params.push(input.value_usd ?? null);
4790
+ }
4791
+ if (input.currency !== undefined) {
4792
+ setClauses.push("currency = ?");
4793
+ params.push(input.currency);
4794
+ }
4795
+ if (input.close_date !== undefined) {
4796
+ setClauses.push("close_date = ?");
4797
+ params.push(input.close_date ?? null);
4798
+ }
4799
+ if (input.notes !== undefined) {
4800
+ setClauses.push("notes = ?");
4801
+ params.push(input.notes ?? null);
4802
+ }
4803
+ params.push(id);
4804
+ d.run(`UPDATE deals SET ${setClauses.join(", ")} WHERE id = ?`, params);
4805
+ return rowToDeal(d.query(`SELECT * FROM deals WHERE id = ?`).get(id));
4806
+ }
4807
+ function deleteDeal(id, db) {
4808
+ const d = db || getDatabase();
4809
+ d.run(`DELETE FROM deals WHERE id = ?`, [id]);
4810
+ }
4811
+ function getDealsByStage(db) {
4812
+ const d = db || getDatabase();
4813
+ const rows = d.query(`SELECT * FROM deals ORDER BY stage, created_at DESC`).all();
4814
+ const result = {};
4815
+ for (const row of rows) {
4816
+ if (!result[row.stage])
4817
+ result[row.stage] = [];
4818
+ result[row.stage].push(rowToDeal(row));
4819
+ }
4820
+ return result;
4821
+ }
4822
+ var init_deals = __esm(() => {
4823
+ init_database();
4824
+ });
4825
+
4826
+ // src/db/events.ts
4827
+ var exports_events = {};
4828
+ __export(exports_events, {
4829
+ logEvent: () => logEvent,
4830
+ listEvents: () => listEvents,
4831
+ getEvent: () => getEvent,
4832
+ deleteEvent: () => deleteEvent
4833
+ });
4834
+ function rowToEvent(row) {
4835
+ let contact_ids = [];
4836
+ try {
4837
+ contact_ids = JSON.parse(row.contact_ids);
4838
+ } catch {
4839
+ contact_ids = [];
4840
+ }
4841
+ return {
4842
+ id: row.id,
4843
+ title: row.title,
4844
+ type: row.type,
4845
+ event_date: row.event_date,
4846
+ duration_min: row.duration_min,
4847
+ contact_ids,
4848
+ company_id: row.company_id,
4849
+ notes: row.notes,
4850
+ outcome: row.outcome,
4851
+ deal_id: row.deal_id,
4852
+ created_at: row.created_at
4853
+ };
4854
+ }
4855
+ function logEvent(input, db) {
4856
+ const d = db || getDatabase();
4857
+ const id = uuid();
4858
+ const timestamp = now();
4859
+ d.run(`INSERT INTO events (id, title, type, event_date, duration_min, contact_ids, company_id, notes, outcome, deal_id, created_at)
4860
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4861
+ id,
4862
+ input.title,
4863
+ input.type ?? "meeting",
4864
+ input.event_date,
4865
+ input.duration_min ?? null,
4866
+ JSON.stringify(input.contact_ids ?? []),
4867
+ input.company_id ?? null,
4868
+ input.notes ?? null,
4869
+ input.outcome ?? null,
4870
+ input.deal_id ?? null,
4871
+ timestamp
4872
+ ]);
4873
+ return rowToEvent(d.query(`SELECT * FROM events WHERE id = ?`).get(id));
4874
+ }
4875
+ function getEvent(id, db) {
4876
+ const d = db || getDatabase();
4877
+ const row = d.query(`SELECT * FROM events WHERE id = ?`).get(id);
4878
+ return row ? rowToEvent(row) : null;
4879
+ }
4880
+ function listEvents(opts = {}, db) {
4881
+ const d = db || getDatabase();
4882
+ const conditions = [];
4883
+ const params = [];
4884
+ if (opts.contact_id) {
4885
+ conditions.push("contact_ids LIKE ?");
4886
+ params.push(`%${opts.contact_id}%`);
4887
+ }
4888
+ if (opts.company_id) {
4889
+ conditions.push("company_id = ?");
4890
+ params.push(opts.company_id);
4891
+ }
4892
+ if (opts.type) {
4893
+ conditions.push("type = ?");
4894
+ params.push(opts.type);
4895
+ }
4896
+ if (opts.date_from) {
4897
+ conditions.push("event_date >= ?");
4898
+ params.push(opts.date_from);
4899
+ }
4900
+ if (opts.date_to) {
4901
+ conditions.push("event_date <= ?");
4902
+ params.push(opts.date_to);
3882
4903
  }
3883
4904
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3884
- const rows = d.query(`SELECT * FROM company_relationships ${where} ORDER BY created_at DESC`).all(...params);
3885
- return rows.map(rowToCompanyRelationship);
4905
+ const rows = d.query(`SELECT * FROM events ${where} ORDER BY event_date DESC`).all(...params);
4906
+ return rows.map(rowToEvent);
3886
4907
  }
4908
+ function deleteEvent(id, db) {
4909
+ const d = db || getDatabase();
4910
+ d.run(`DELETE FROM events WHERE id = ?`, [id]);
4911
+ }
4912
+ var init_events = __esm(() => {
4913
+ init_database();
4914
+ });
4915
+
4916
+ // node_modules/commander/esm.mjs
4917
+ var import__ = __toESM(require_commander(), 1);
4918
+ var {
4919
+ program,
4920
+ createCommand,
4921
+ createArgument,
4922
+ createOption,
4923
+ CommanderError,
4924
+ InvalidArgumentError,
4925
+ InvalidOptionArgumentError,
4926
+ Command,
4927
+ Argument,
4928
+ Option,
4929
+ Help
4930
+ } = import__.default;
4931
+
4932
+ // src/cli/index.tsx
4933
+ init_contacts();
4934
+ init_companies();
4935
+ init_relationships();
4936
+ import chalk from "chalk";
3887
4937
 
3888
4938
  // src/db/vendor-comms.ts
3889
4939
  init_database();
@@ -3926,166 +4976,49 @@ function logVendorCommunication(input, db) {
3926
4976
  input.invoice_currency ?? null,
3927
4977
  input.invoice_ref ?? null,
3928
4978
  input.follow_up_date ?? null,
3929
- input.follow_up_done ? 1 : 0
3930
- ]);
3931
- return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
3932
- }
3933
- function listVendorCommunications(companyId, opts = {}, db) {
3934
- const d = db || getDatabase();
3935
- const conditions = ["company_id = ?"];
3936
- const params = [companyId];
3937
- if (opts.type) {
3938
- conditions.push("type = ?");
3939
- params.push(opts.type);
3940
- }
3941
- if (opts.status) {
3942
- conditions.push("status = ?");
3943
- params.push(opts.status);
3944
- }
3945
- if (opts.direction) {
3946
- conditions.push("direction = ?");
3947
- params.push(opts.direction);
3948
- }
3949
- const where = conditions.join(" AND ");
3950
- const rows = d.query(`SELECT * FROM vendor_communications WHERE ${where} ORDER BY comm_date DESC`).all(...params);
3951
- return rows.map(rowToVendorComm);
3952
- }
3953
- function listPendingFollowUps(db) {
3954
- const d = db || getDatabase();
3955
- const today = new Date().toISOString().slice(0, 10);
3956
- const rows = d.query(`SELECT * FROM vendor_communications
3957
- WHERE follow_up_date <= ? AND follow_up_done = 0
3958
- ORDER BY follow_up_date ASC`).all(today);
3959
- return rows.map(rowToVendorComm);
3960
- }
3961
- function listMissingInvoices(db) {
3962
- const d = db || getDatabase();
3963
- const rows = d.query(`SELECT * FROM vendor_communications
3964
- WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')
3965
- ORDER BY comm_date ASC`).all();
3966
- return rows.map(rowToVendorComm);
3967
- }
3968
-
3969
- // src/db/contact-tasks.ts
3970
- init_database();
3971
- function rowToContactTask(row) {
3972
- return {
3973
- id: row.id,
3974
- title: row.title,
3975
- description: row.description,
3976
- contact_id: row.contact_id,
3977
- assigned_by: row.assigned_by,
3978
- deadline: row.deadline,
3979
- status: row.status,
3980
- priority: row.priority,
3981
- entity_id: row.entity_id,
3982
- linked_todos_task_id: row.linked_todos_task_id,
3983
- escalation_rules: JSON.parse(row.escalation_rules || "[]"),
3984
- created_at: row.created_at,
3985
- updated_at: row.updated_at
3986
- };
3987
- }
3988
- function createContactTask(input, db) {
3989
- const d = db || getDatabase();
3990
- const id = uuid();
3991
- const timestamp = now();
3992
- d.run(`INSERT INTO contact_tasks
3993
- (id, title, description, contact_id, assigned_by, deadline, status, priority,
3994
- entity_id, linked_todos_task_id, escalation_rules, created_at, updated_at)
3995
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3996
- id,
3997
- input.title,
3998
- input.description ?? null,
3999
- input.contact_id,
4000
- input.assigned_by ?? null,
4001
- input.deadline ?? null,
4002
- input.status ?? "pending",
4003
- input.priority ?? "medium",
4004
- input.entity_id ?? null,
4005
- input.linked_todos_task_id ?? null,
4006
- JSON.stringify(input.escalation_rules ?? []),
4007
- timestamp,
4008
- timestamp
4009
- ]);
4010
- return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
4011
- }
4012
- function listContactTasks(opts = {}, db) {
4013
- const d = db || getDatabase();
4014
- const conditions = [];
4015
- const params = [];
4016
- if (opts.contact_id) {
4017
- conditions.push("contact_id = ?");
4018
- params.push(opts.contact_id);
4019
- }
4020
- if (opts.entity_id) {
4021
- conditions.push("entity_id = ?");
4022
- params.push(opts.entity_id);
4979
+ input.follow_up_done ? 1 : 0
4980
+ ]);
4981
+ return rowToVendorComm(d.query(`SELECT * FROM vendor_communications WHERE id = ?`).get(id));
4982
+ }
4983
+ function listVendorCommunications(companyId, opts = {}, db) {
4984
+ const d = db || getDatabase();
4985
+ const conditions = ["company_id = ?"];
4986
+ const params = [companyId];
4987
+ if (opts.type) {
4988
+ conditions.push("type = ?");
4989
+ params.push(opts.type);
4023
4990
  }
4024
4991
  if (opts.status) {
4025
4992
  conditions.push("status = ?");
4026
4993
  params.push(opts.status);
4027
4994
  }
4028
- if (opts.priority) {
4029
- conditions.push("priority = ?");
4030
- params.push(opts.priority);
4995
+ if (opts.direction) {
4996
+ conditions.push("direction = ?");
4997
+ params.push(opts.direction);
4031
4998
  }
4032
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4033
- const rows = d.query(`SELECT * FROM contact_tasks ${where} ORDER BY deadline ASC, priority DESC, created_at ASC`).all(...params);
4034
- return rows.map(rowToContactTask);
4999
+ const where = conditions.join(" AND ");
5000
+ const rows = d.query(`SELECT * FROM vendor_communications WHERE ${where} ORDER BY comm_date DESC`).all(...params);
5001
+ return rows.map(rowToVendorComm);
4035
5002
  }
4036
- function updateContactTask(id, input, db) {
5003
+ function listPendingFollowUps(db) {
4037
5004
  const d = db || getDatabase();
4038
- const setClauses = ["updated_at = ?"];
4039
- const params = [now()];
4040
- if (input.title !== undefined) {
4041
- setClauses.push("title = ?");
4042
- params.push(input.title);
4043
- }
4044
- if ("description" in input) {
4045
- setClauses.push("description = ?");
4046
- params.push(input.description ?? null);
4047
- }
4048
- if ("assigned_by" in input) {
4049
- setClauses.push("assigned_by = ?");
4050
- params.push(input.assigned_by ?? null);
4051
- }
4052
- if ("deadline" in input) {
4053
- setClauses.push("deadline = ?");
4054
- params.push(input.deadline ?? null);
4055
- }
4056
- if (input.status !== undefined) {
4057
- setClauses.push("status = ?");
4058
- params.push(input.status);
4059
- }
4060
- if (input.priority !== undefined) {
4061
- setClauses.push("priority = ?");
4062
- params.push(input.priority);
4063
- }
4064
- if ("entity_id" in input) {
4065
- setClauses.push("entity_id = ?");
4066
- params.push(input.entity_id ?? null);
4067
- }
4068
- if ("linked_todos_task_id" in input) {
4069
- setClauses.push("linked_todos_task_id = ?");
4070
- params.push(input.linked_todos_task_id ?? null);
4071
- }
4072
- if (input.escalation_rules !== undefined) {
4073
- setClauses.push("escalation_rules = ?");
4074
- params.push(JSON.stringify(input.escalation_rules));
4075
- }
4076
- params.push(id);
4077
- d.run(`UPDATE contact_tasks SET ${setClauses.join(", ")} WHERE id = ?`, params);
4078
- return rowToContactTask(d.query(`SELECT * FROM contact_tasks WHERE id = ?`).get(id));
5005
+ const today = new Date().toISOString().slice(0, 10);
5006
+ const rows = d.query(`SELECT * FROM vendor_communications
5007
+ WHERE follow_up_date <= ? AND follow_up_done = 0
5008
+ ORDER BY follow_up_date ASC`).all(today);
5009
+ return rows.map(rowToVendorComm);
4079
5010
  }
4080
- function listOverdueTasks(db) {
5011
+ function listMissingInvoices(db) {
4081
5012
  const d = db || getDatabase();
4082
- const now_iso = new Date().toISOString();
4083
- const rows = d.query(`SELECT * FROM contact_tasks
4084
- WHERE deadline < ? AND status NOT IN ('completed','cancelled')
4085
- ORDER BY deadline ASC`).all(now_iso);
4086
- return rows.map(rowToContactTask);
5013
+ const rows = d.query(`SELECT * FROM vendor_communications
5014
+ WHERE type = 'invoice_request' AND status IN ('awaiting_response','no_response')
5015
+ ORDER BY comm_date ASC`).all();
5016
+ return rows.map(rowToVendorComm);
4087
5017
  }
4088
5018
 
5019
+ // src/cli/index.tsx
5020
+ init_contact_tasks();
5021
+
4089
5022
  // src/db/applications.ts
4090
5023
  init_database();
4091
5024
  function rowToApplication(row) {
@@ -5532,4 +6465,433 @@ program.command("seed").description("Seed demo contacts, companies, and relation
5532
6465
  console.log(chalk.gray(" Try: contacts entities list"));
5533
6466
  console.log(chalk.gray(" Try: contacts workload " + alina.id));
5534
6467
  });
6468
+ program.command("brief <id>").description("Generate pre-meeting briefing for a contact").action(async (id) => {
6469
+ const { generateBrief: generateBrief2 } = await Promise.resolve().then(() => (init_brief(), exports_brief));
6470
+ const db = getDatabase();
6471
+ const brief = generateBrief2(id, db);
6472
+ console.log(brief);
6473
+ });
6474
+ program.command("cold").description("Show contacts you haven't reached out to recently").option("--days <n>", "Days threshold", "30").action(async (opts) => {
6475
+ const { listColdContacts: listColdContacts2 } = await Promise.resolve().then(() => (init_contacts(), exports_contacts));
6476
+ const db = getDatabase();
6477
+ const contacts = listColdContacts2(parseInt(opts.days, 10), db);
6478
+ if (!contacts.length) {
6479
+ console.log(chalk.green(`
6480
+ No cold contacts!
6481
+ `));
6482
+ return;
6483
+ }
6484
+ console.log();
6485
+ const rows = contacts.map((c) => {
6486
+ const daysCold = c.days_cold ?? null;
6487
+ const lastContact = c.last_contacted_at ? c.last_contacted_at.slice(0, 10) : "never";
6488
+ const dayStr = daysCold === null ? chalk.red("never") : daysCold > 60 ? chalk.red(String(daysCold) + "d") : chalk.yellow(String(daysCold) + "d");
6489
+ return {
6490
+ Name: c.display_name,
6491
+ Company: c.company?.name ?? "",
6492
+ "Last Contact": lastContact,
6493
+ "Days Cold": dayStr
6494
+ };
6495
+ });
6496
+ renderTable(["Name", "Company", "Last Contact", "Days Cold"], rows);
6497
+ console.log(chalk.gray(`
6498
+ ${contacts.length} cold contact(s) (${opts.days}+ days)
6499
+ `));
6500
+ });
6501
+ program.command("upcoming").option("--days <n>", "Days ahead to show", "7").description("Show upcoming follow-ups, birthdays, and deadlines").action(async (opts) => {
6502
+ const { getUpcomingItems: getUpcomingItems2 } = await Promise.resolve().then(() => (init_upcoming(), exports_upcoming));
6503
+ const db = getDatabase();
6504
+ const items = getUpcomingItems2(parseInt(opts.days, 10), db);
6505
+ if (!items.length) {
6506
+ console.log(chalk.green(`
6507
+ Nothing upcoming!
6508
+ `));
6509
+ return;
6510
+ }
6511
+ console.log();
6512
+ const iconMap = {
6513
+ follow_up: "\uD83D\uDCC5",
6514
+ birthday: "\uD83C\uDF82",
6515
+ task_deadline: "\u26A0\uFE0F",
6516
+ application_followup: "\uD83D\uDCCB",
6517
+ vendor_followup: "\uD83D\uDCBC"
6518
+ };
6519
+ for (const item of items) {
6520
+ const icon = iconMap[item.type] ?? "\u2022";
6521
+ const urgencyColor = item.urgency === "overdue" ? chalk.red : item.urgency === "today" ? chalk.yellow : chalk.white;
6522
+ console.log(` ${icon} ${urgencyColor(item.date?.slice(0, 10) ?? "")} ${chalk.bold(item.title ?? "")} ${chalk.gray(item.type ?? "")}`);
6523
+ }
6524
+ console.log(chalk.gray(`
6525
+ ${items.length} upcoming item(s) in next ${opts.days} days
6526
+ `));
6527
+ });
6528
+ program.command("stats").description("Network health dashboard").action(async () => {
6529
+ const { getNetworkStats: getNetworkStats2 } = await Promise.resolve().then(() => (init_stats(), exports_stats));
6530
+ const db = getDatabase();
6531
+ const stats = getNetworkStats2(db);
6532
+ console.log(chalk.bold.blue(`
6533
+ \u2501\u2501\u2501 Network Health Dashboard \u2501\u2501\u2501
6534
+ `));
6535
+ const size = stats["network_size"];
6536
+ if (size) {
6537
+ console.log(chalk.bold(" Network Size:"));
6538
+ console.log(` ${chalk.cyan(String(size["contacts"] ?? 0))} contacts ${chalk.cyan(String(size["companies"] ?? 0))} companies ${chalk.cyan(String(size["tags"] ?? 0))} tags`);
6539
+ }
6540
+ const cold = stats["cold_contacts"];
6541
+ if (cold) {
6542
+ console.log(chalk.bold(`
6543
+ Cold Contacts:`));
6544
+ const c30 = cold["30d"] ?? 0;
6545
+ const c60 = cold["60d"] ?? 0;
6546
+ const cnever = cold["never"] ?? 0;
6547
+ console.log(` ${c30 > 5 ? chalk.red(String(c30)) : c30 > 0 ? chalk.yellow(String(c30)) : chalk.green(String(c30))} not contacted in 30d`);
6548
+ console.log(` ${c60 > 5 ? chalk.red(String(c60)) : c60 > 0 ? chalk.yellow(String(c60)) : chalk.green(String(c60))} not contacted in 60d`);
6549
+ console.log(` ${cnever > 5 ? chalk.red(String(cnever)) : cnever > 0 ? chalk.yellow(String(cnever)) : chalk.green(String(cnever))} never contacted`);
6550
+ }
6551
+ const action = stats["action_required"];
6552
+ if (action) {
6553
+ console.log(chalk.bold(`
6554
+ Action Required:`));
6555
+ const overdue = action["overdue_tasks"] ?? 0;
6556
+ const pending = action["pending_applications"] ?? 0;
6557
+ const missing = action["missing_invoices"] ?? 0;
6558
+ const upcoming = action["upcoming_7d"] ?? 0;
6559
+ console.log(` ${overdue > 0 ? chalk.red(String(overdue)) : chalk.green("0")} overdue tasks`);
6560
+ console.log(` ${pending > 0 ? chalk.yellow(String(pending)) : chalk.green("0")} pending applications`);
6561
+ console.log(` ${missing > 0 ? chalk.yellow(String(missing)) : chalk.green("0")} missing invoices`);
6562
+ console.log(` ${upcoming > 0 ? chalk.yellow(String(upcoming)) : chalk.green("0")} upcoming in 7d`);
6563
+ }
6564
+ const pipeline = stats["pipeline_value_usd"];
6565
+ if (pipeline !== undefined) {
6566
+ console.log(chalk.bold(`
6567
+ Deal Pipeline:`));
6568
+ console.log(` ${chalk.cyan("$" + pipeline.toLocaleString())} active pipeline`);
6569
+ }
6570
+ console.log();
6571
+ });
6572
+ program.command("audit").description("Score contacts for data completeness").option("--limit <n>", "Number to show", "20").action(async (opts) => {
6573
+ const { listContactAudit: listContactAudit2 } = await Promise.resolve().then(() => (init_audit(), exports_audit));
6574
+ const db = getDatabase();
6575
+ const results = (await listContactAudit2(db)).slice(0, parseInt(opts.limit, 10));
6576
+ if (!results.length) {
6577
+ console.log(chalk.gray(`
6578
+ No contacts found.
6579
+ `));
6580
+ return;
6581
+ }
6582
+ console.log();
6583
+ for (const r of results) {
6584
+ const score = r.score;
6585
+ const name = r.display_name;
6586
+ const missing = r.missing_fields ?? [];
6587
+ const filled = Math.round(score / 10);
6588
+ const bar = chalk.green("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(10 - filled));
6589
+ const scoreColor = score < 40 ? chalk.red : score < 70 ? chalk.yellow : chalk.green;
6590
+ console.log(` ${bar} ${scoreColor(String(score).padStart(3) + "%")} ${chalk.bold(name)} ${chalk.gray(missing.join(", "))}`);
6591
+ }
6592
+ console.log(chalk.gray(`
6593
+ ${results.length} contact(s) shown (sorted by completeness ascending)
6594
+ `));
6595
+ });
6596
+ var dealsCmd = program.command("deals").description("Manage deals and opportunities");
6597
+ dealsCmd.command("list").description("List deals").option("--stage <s>", "Filter by stage").action(async (opts) => {
6598
+ const { listDeals: listDeals2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
6599
+ const db = getDatabase();
6600
+ const deals = listDeals2({ stage: opts.stage }, db);
6601
+ if (!deals.length) {
6602
+ console.log(chalk.gray(`
6603
+ No deals found.
6604
+ `));
6605
+ return;
6606
+ }
6607
+ console.log();
6608
+ renderTable(["Title", "Stage", "Value", "Close Date", "Contact"], deals.map((d) => ({
6609
+ Title: d.title,
6610
+ Stage: d.stage,
6611
+ Value: d.value_usd ? "$" + d.value_usd.toLocaleString() : "",
6612
+ "Close Date": d.close_date ? d.close_date.slice(0, 10) : "",
6613
+ Contact: d.contact_id ?? ""
6614
+ })));
6615
+ console.log(chalk.gray(`
6616
+ ${deals.length} deal(s)
6617
+ `));
6618
+ });
6619
+ dealsCmd.command("add").description("Add a new deal").option("--title <title>", "Deal title (required)").option("--stage <stage>", "Stage: prospecting|qualified|proposal|negotiation|won|lost", "prospecting").option("--value <usd>", "Value in USD").option("--contact <id>", "Contact ID").option("--company <id>", "Company ID").option("--close-date <date>", "Expected close date (YYYY-MM-DD)").option("--notes <text>", "Notes").action(async (opts) => {
6620
+ const { createDeal: createDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
6621
+ const db = getDatabase();
6622
+ let title = opts.title;
6623
+ if (!title) {
6624
+ title = await prompt("Deal title (required):");
6625
+ if (!title) {
6626
+ console.error(chalk.red("Title is required."));
6627
+ process.exit(1);
6628
+ }
6629
+ }
6630
+ const deal = createDeal2({
6631
+ title,
6632
+ stage: opts.stage,
6633
+ value_usd: opts.value ? parseFloat(opts.value) : undefined,
6634
+ contact_id: opts.contact,
6635
+ company_id: opts.company,
6636
+ close_date: opts.closeDate,
6637
+ notes: opts.notes
6638
+ }, db);
6639
+ console.log(chalk.green(`
6640
+ \u2713 Deal created: ${deal.title} (${deal.id})
6641
+ `));
6642
+ });
6643
+ dealsCmd.command("show <id>").description("Show deal details").action(async (id) => {
6644
+ const { getDeal: getDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
6645
+ const db = getDatabase();
6646
+ const deal = getDeal2(id, db);
6647
+ console.log();
6648
+ for (const [k, v] of Object.entries(deal)) {
6649
+ if (v !== null && v !== undefined)
6650
+ console.log(` ${chalk.gray(k.padEnd(15))} ${v}`);
6651
+ }
6652
+ console.log();
6653
+ });
6654
+ dealsCmd.command("won <id>").description("Mark a deal as won").action(async (id) => {
6655
+ const { updateDeal: updateDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
6656
+ const db = getDatabase();
6657
+ const deal = updateDeal2(id, { stage: "won" }, db);
6658
+ console.log(chalk.green(`
6659
+ \u2713 Deal won: ${deal.title}
6660
+ `));
6661
+ });
6662
+ dealsCmd.command("lost <id>").description("Mark a deal as lost").action(async (id) => {
6663
+ const { updateDeal: updateDeal2 } = await Promise.resolve().then(() => (init_deals(), exports_deals));
6664
+ const db = getDatabase();
6665
+ const deal = updateDeal2(id, { stage: "lost" }, db);
6666
+ console.log(chalk.yellow(`
6667
+ Deal lost: ${deal.title}
6668
+ `));
6669
+ });
6670
+ var eventsCmd = program.command("events").description("Log meetings and interactions");
6671
+ eventsCmd.command("log").description("Log an event/meeting").option("--title <title>", "Event title (required)").option("--type <type>", "Type: meeting|call|email|lunch|conference|demo|other", "meeting").option("--date <date>", "Date (YYYY-MM-DD, default today)").option("--contact <id>", "Contact ID (can repeat)", collect, []).option("--duration <min>", "Duration in minutes").option("--notes <text>", "Notes").option("--outcome <text>", "Outcome").action(async (opts) => {
6672
+ const { logEvent: logEvent2 } = await Promise.resolve().then(() => (init_events(), exports_events));
6673
+ const db = getDatabase();
6674
+ let title = opts.title;
6675
+ if (!title) {
6676
+ title = await prompt("Event title (required):");
6677
+ if (!title) {
6678
+ console.error(chalk.red("Title is required."));
6679
+ process.exit(1);
6680
+ }
6681
+ }
6682
+ const eventDate = opts.date ?? new Date().toISOString().slice(0, 10);
6683
+ const event = logEvent2({
6684
+ title,
6685
+ type: opts.type,
6686
+ event_date: eventDate,
6687
+ duration_min: opts.duration ? parseInt(opts.duration, 10) : undefined,
6688
+ contact_ids: opts.contact.length ? opts.contact : undefined,
6689
+ notes: opts.notes,
6690
+ outcome: opts.outcome
6691
+ }, db);
6692
+ console.log(chalk.green(`
6693
+ \u2713 Event logged: ${event.title} on ${eventDate}
6694
+ `));
6695
+ });
6696
+ eventsCmd.command("list [contact-id]").description("List events, optionally for a specific contact").action(async (contactId) => {
6697
+ const { listEvents: listEvents2 } = await Promise.resolve().then(() => (init_events(), exports_events));
6698
+ const db = getDatabase();
6699
+ const events = listEvents2({ contact_id: contactId }, db);
6700
+ if (!events.length) {
6701
+ console.log(chalk.gray(`
6702
+ No events found.
6703
+ `));
6704
+ return;
6705
+ }
6706
+ console.log();
6707
+ renderTable(["Title", "Type", "Date", "Duration"], events.map((e) => ({
6708
+ Title: e.title,
6709
+ Type: e.type,
6710
+ Date: e.event_date.slice(0, 10),
6711
+ Duration: e.duration_min ? `${e.duration_min}m` : ""
6712
+ })));
6713
+ console.log(chalk.gray(`
6714
+ ${events.length} event(s)
6715
+ `));
6716
+ });
6717
+ program.command("timeline <id>").description("Full chronological activity history for a contact").option("--limit <n>", "Items to show", "20").action(async (id, opts) => {
6718
+ const { getContactTimeline: getContactTimeline2 } = await Promise.resolve().then(() => (init_timeline(), exports_timeline));
6719
+ const db = getDatabase();
6720
+ const items = getContactTimeline2(id, parseInt(opts.limit, 10), db);
6721
+ if (!items.length) {
6722
+ console.log(chalk.gray(`
6723
+ No timeline items found.
6724
+ `));
6725
+ return;
6726
+ }
6727
+ const contact = getContact(id);
6728
+ console.log(chalk.bold(`
6729
+ Timeline: ${contact.display_name}
6730
+ `));
6731
+ const iconMap = {
6732
+ note: "\uD83D\uDCDD",
6733
+ event: "\uD83D\uDCC5",
6734
+ task: "\u2705",
6735
+ vendor_comm: "\uD83D\uDCE7",
6736
+ interaction: "\uD83D\uDCAC",
6737
+ deal: "\uD83D\uDCB0"
6738
+ };
6739
+ for (const item of items) {
6740
+ const icon = iconMap[item.type] ?? "\u2022";
6741
+ console.log(` ${icon} ${chalk.gray(item.date?.slice(0, 10) ?? "")} ${chalk.bold(item.title)} ${chalk.gray(item.body ? item.body.slice(0, 60) : "")}`);
6742
+ }
6743
+ console.log();
6744
+ });
6745
+ program.command("enrich <id>").description("Auto-fill missing contact data via web search (requires EXA_API_KEY)").action(async (id) => {
6746
+ const contact = getContact(id);
6747
+ const exaKey = process.env["EXA_API_KEY"];
6748
+ if (!exaKey) {
6749
+ console.error(chalk.red(`
6750
+ Set EXA_API_KEY environment variable to use enrichment.
6751
+ `));
6752
+ process.exit(1);
6753
+ }
6754
+ console.log(chalk.blue(`
6755
+ Searching for: ${contact.display_name}...
6756
+ `));
6757
+ const primaryEmail = contact.emails?.[0]?.address ?? "";
6758
+ const query = `${contact.display_name} ${primaryEmail} site:linkedin.com OR site:twitter.com OR site:github.com`;
6759
+ const res = await fetch("https://api.exa.ai/search", {
6760
+ method: "POST",
6761
+ headers: { "x-api-key": exaKey, "content-type": "application/json" },
6762
+ body: JSON.stringify({ query, num_results: 5 })
6763
+ });
6764
+ const data = await res.json();
6765
+ const results = data.results ?? [];
6766
+ if (!results.length) {
6767
+ console.log(chalk.gray(`No results found.
6768
+ `));
6769
+ return;
6770
+ }
6771
+ const socialProfiles = contact.social_profiles;
6772
+ const suggestions = [];
6773
+ for (const r of results) {
6774
+ if (r.url?.includes("linkedin.com") && !socialProfiles?.find((s) => s.platform === "linkedin"))
6775
+ suggestions.push({ field: "linkedin", value: r.url });
6776
+ if (r.url?.includes("twitter.com") && !socialProfiles?.find((s) => s.platform === "twitter"))
6777
+ suggestions.push({ field: "twitter", value: r.url });
6778
+ if (r.url?.includes("github.com") && !socialProfiles?.find((s) => s.platform === "github"))
6779
+ suggestions.push({ field: "github", value: r.url });
6780
+ }
6781
+ if (!suggestions.length) {
6782
+ console.log(chalk.green(`No new data found to enrich.
6783
+ `));
6784
+ return;
6785
+ }
6786
+ console.log(chalk.yellow(`Suggestions (review before applying):
6787
+ `));
6788
+ for (const s of suggestions) {
6789
+ console.log(` ${chalk.cyan(s.field.padEnd(10))} ${s.value}`);
6790
+ }
6791
+ console.log(chalk.gray("\nUse `contacts edit <id>` to apply these manually.\n"));
6792
+ });
6793
+ program.command("remind <id>").option("--in <duration>", "Duration (e.g. 7d, 2w, 1m)").option("--on <date>", "Specific date YYYY-MM-DD").option("--note <text>", "Reminder note").description("Schedule a follow-up reminder").action(async (id, opts) => {
6794
+ let date;
6795
+ if (opts.on) {
6796
+ date = opts.on;
6797
+ } else if (opts.in) {
6798
+ const raw = opts.in;
6799
+ const n = parseInt(raw, 10);
6800
+ const unit = raw.slice(-1);
6801
+ const ms = unit === "w" ? n * 7 * 86400000 : unit === "m" ? n * 30 * 86400000 : n * 86400000;
6802
+ date = new Date(Date.now() + ms).toISOString().slice(0, 10);
6803
+ } else {
6804
+ console.error(chalk.red("Provide --in (e.g. 7d) or --on (YYYY-MM-DD)"));
6805
+ process.exit(1);
6806
+ }
6807
+ const db = getDatabase();
6808
+ updateContact(id, { follow_up_at: date });
6809
+ if (opts.note) {
6810
+ const { addNote: addNote2 } = await Promise.resolve().then(() => (init_notes(), exports_notes));
6811
+ addNote2(id, `Reminder (${date}): ${opts.note}`, undefined, db);
6812
+ }
6813
+ const contact = getContact(id);
6814
+ console.log(chalk.green(`
6815
+ \u2713 Reminder set for ${contact.display_name} on ${date}
6816
+ `));
6817
+ });
6818
+ var bulkTag = tagsCmd.command("bulk").description("Bulk tag operations");
6819
+ bulkTag.command("add <tag>").description("Apply a tag to multiple contacts").option("--query <q>", "Apply to all contacts matching search query").option("--all", "Apply to all contacts").option("--contact-ids <ids>", "Comma-separated contact IDs").action(async (tag, opts) => {
6820
+ const { addTagToContact: addTag } = await Promise.resolve().then(() => (init_tags(), exports_tags));
6821
+ const { getTagByName: getTagByNameFn } = await Promise.resolve().then(() => (init_tags(), exports_tags));
6822
+ const db = getDatabase();
6823
+ const tagRecord = getTagByNameFn(tag, db);
6824
+ if (!tagRecord) {
6825
+ console.error(chalk.red(`Tag not found: ${tag}`));
6826
+ process.exit(1);
6827
+ }
6828
+ let contactIds = [];
6829
+ if (opts.contactIds)
6830
+ contactIds = opts.contactIds.split(",").map((s) => s.trim());
6831
+ if (opts.query) {
6832
+ const found = searchContacts(opts.query);
6833
+ contactIds = [...contactIds, ...found.map((c) => c.id)];
6834
+ }
6835
+ if (opts.all) {
6836
+ const all = listContacts({ limit: 1e4 });
6837
+ contactIds = [...contactIds, ...all.contacts.map((c) => c.id)];
6838
+ }
6839
+ contactIds = [...new Set(contactIds)];
6840
+ let count = 0;
6841
+ for (const cid of contactIds) {
6842
+ try {
6843
+ addTag(cid, tagRecord.id);
6844
+ count++;
6845
+ } catch {}
6846
+ }
6847
+ console.log(chalk.green(`
6848
+ \u2713 Tagged ${count} contact(s) with #${tagRecord.name}
6849
+ `));
6850
+ });
6851
+ bulkTag.command("remove <tag>").description("Remove a tag from matching contacts").option("--query <q>", "Remove from all contacts matching search query").option("--contact-ids <ids>", "Comma-separated contact IDs").action(async (tag, opts) => {
6852
+ const { removeTagFromContact: removeTag } = await Promise.resolve().then(() => (init_tags(), exports_tags));
6853
+ const { getTagByName: getTagByNameFn } = await Promise.resolve().then(() => (init_tags(), exports_tags));
6854
+ const db = getDatabase();
6855
+ const tagRecord = getTagByNameFn(tag, db);
6856
+ if (!tagRecord) {
6857
+ console.error(chalk.red(`Tag not found: ${tag}`));
6858
+ process.exit(1);
6859
+ }
6860
+ let contactIds = [];
6861
+ if (opts.contactIds)
6862
+ contactIds = opts.contactIds.split(",").map((s) => s.trim());
6863
+ if (opts.query) {
6864
+ const found = searchContacts(opts.query);
6865
+ contactIds = [...contactIds, ...found.map((c) => c.id)];
6866
+ }
6867
+ contactIds = [...new Set(contactIds)];
6868
+ let count = 0;
6869
+ for (const cid of contactIds) {
6870
+ try {
6871
+ removeTag(cid, tagRecord.id);
6872
+ count++;
6873
+ } catch {}
6874
+ }
6875
+ console.log(chalk.green(`
6876
+ \u2713 Removed #${tagRecord.name} from ${count} contact(s)
6877
+ `));
6878
+ });
6879
+ program.command("dnc <id>").description("Mark contact as do-not-contact").option("--remove", "Remove DNC flag").option("--reason <text>", "Reason for DNC").action(async (id, opts) => {
6880
+ const contact = getContact(id);
6881
+ const db = getDatabase();
6882
+ updateContact(id, { do_not_contact: !opts.remove });
6883
+ if (opts.reason && !opts.remove) {
6884
+ const { addNote: addNote2 } = await Promise.resolve().then(() => (init_notes(), exports_notes));
6885
+ addNote2(id, `DNC: ${opts.reason}`, undefined, db);
6886
+ }
6887
+ if (opts.remove) {
6888
+ console.log(chalk.green(`
6889
+ \u2713 DNC flag removed for ${contact.display_name}
6890
+ `));
6891
+ } else {
6892
+ console.log(chalk.yellow(`
6893
+ \u26A0 ${contact.display_name} marked as do-not-contact
6894
+ `));
6895
+ }
6896
+ });
5535
6897
  program.parse(process.argv);