@hasna/contacts 0.5.1 → 0.5.3

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
@@ -4,6 +4,7 @@ var __create = Object.create;
4
4
  var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
9
  var __toESM = (mod, isNodeMode, target) => {
9
10
  target = mod != null ? __create(__getProtoOf(mod)) : {};
@@ -16,6 +17,20 @@ var __toESM = (mod, isNodeMode, target) => {
16
17
  });
17
18
  return to;
18
19
  };
20
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
21
+ var __toCommonJS = (from) => {
22
+ var entry = __moduleCache.get(from), desc;
23
+ if (entry)
24
+ return entry;
25
+ entry = __defProp({}, "__esModule", { value: true });
26
+ if (from && typeof from === "object" || typeof from === "function")
27
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
+ get: () => from[key],
29
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
+ }));
31
+ __moduleCache.set(from, entry);
32
+ return entry;
33
+ };
19
34
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
35
  var __export = (target, all) => {
21
36
  for (var name in all)
@@ -3263,6 +3278,19 @@ var init_contacts = __esm(() => {
3263
3278
  });
3264
3279
 
3265
3280
  // src/db/companies.ts
3281
+ var exports_companies = {};
3282
+ __export(exports_companies, {
3283
+ updateCompany: () => updateCompany,
3284
+ unarchiveCompany: () => unarchiveCompany,
3285
+ searchCompanies: () => searchCompanies,
3286
+ listOwnedEntities: () => listOwnedEntities,
3287
+ listCompanyEmployees: () => listCompanyEmployees,
3288
+ listCompanies: () => listCompanies,
3289
+ getCompany: () => getCompany,
3290
+ deleteCompany: () => deleteCompany,
3291
+ createCompany: () => createCompany,
3292
+ archiveCompany: () => archiveCompany
3293
+ });
3266
3294
  function rowToCompany2(row) {
3267
3295
  return {
3268
3296
  ...row,
@@ -3485,6 +3513,58 @@ function deleteCompany(id, db) {
3485
3513
  logActivity(d, { company_id: id, action: "company.deleted", details: `Deleted company: ${row.name}` });
3486
3514
  d.run(`DELETE FROM companies WHERE id = ?`, [id]);
3487
3515
  }
3516
+ function searchCompanies(query, db) {
3517
+ const d = db || getDatabase();
3518
+ const rows = d.query(`
3519
+ SELECT * FROM companies
3520
+ WHERE name LIKE ? OR domain LIKE ? OR description LIKE ? OR industry LIKE ?
3521
+ LIMIT 50
3522
+ `).all(`%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`);
3523
+ return rows.map((row) => loadCompanyDetails(d, rowToCompany2(row)));
3524
+ }
3525
+ function listCompanyEmployees(companyId, db) {
3526
+ const d = db || getDatabase();
3527
+ const row = d.query(`SELECT id FROM companies WHERE id = ?`).get(companyId);
3528
+ if (!row)
3529
+ throw new CompanyNotFoundError(companyId);
3530
+ const rows = d.query(`SELECT * FROM contacts WHERE company_id = ? ORDER BY display_name ASC`).all(companyId);
3531
+ return rows.map((r) => ({
3532
+ ...r,
3533
+ source: r.source,
3534
+ custom_fields: JSON.parse(r.custom_fields || "{}"),
3535
+ preferred_contact_method: r.preferred_contact_method ?? null,
3536
+ status: r.status ?? "active",
3537
+ follow_up_at: r.follow_up_at ?? null,
3538
+ archived: !!r.archived,
3539
+ project_id: r.project_id ?? null,
3540
+ do_not_contact: !!r.do_not_contact,
3541
+ priority: r.priority ?? 3,
3542
+ timezone: r.timezone ?? null
3543
+ }));
3544
+ }
3545
+ function archiveCompany(id, db) {
3546
+ const d = db || getDatabase();
3547
+ const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3548
+ if (!row)
3549
+ throw new CompanyNotFoundError(id);
3550
+ d.run(`UPDATE companies SET archived = 1, updated_at = ? WHERE id = ?`, [now(), id]);
3551
+ logActivity(d, { company_id: id, action: "company.archived", details: `Archived company: ${row.name}` });
3552
+ const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3553
+ return loadCompanyDetails(d, rowToCompany2(updated));
3554
+ }
3555
+ function unarchiveCompany(id, db) {
3556
+ const d = db || getDatabase();
3557
+ const row = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3558
+ if (!row)
3559
+ throw new CompanyNotFoundError(id);
3560
+ d.run(`UPDATE companies SET archived = 0, updated_at = ? WHERE id = ?`, [now(), id]);
3561
+ logActivity(d, { company_id: id, action: "company.unarchived", details: `Unarchived company: ${row.name}` });
3562
+ const updated = d.query(`SELECT * FROM companies WHERE id = ?`).get(id);
3563
+ return loadCompanyDetails(d, rowToCompany2(updated));
3564
+ }
3565
+ function listOwnedEntities(db) {
3566
+ return listCompanies({ is_owned_entity: true }, db);
3567
+ }
3488
3568
  var init_companies = __esm(() => {
3489
3569
  init_types();
3490
3570
  init_database();
@@ -4384,13 +4464,104 @@ async function exportContacts(format, contacts) {
4384
4464
  }
4385
4465
  }
4386
4466
 
4467
+ // src/lib/images.ts
4468
+ var exports_images = {};
4469
+ __export(exports_images, {
4470
+ saveImage: () => saveImage,
4471
+ listImages: () => listImages,
4472
+ getImagesDir: () => getImagesDir,
4473
+ getImagePath: () => getImagePath,
4474
+ getImageAsBase64: () => getImageAsBase64,
4475
+ deleteImage: () => deleteImage
4476
+ });
4477
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, copyFileSync, unlinkSync, readdirSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4478
+ import { join as join3, extname, basename } from "path";
4479
+ function ensureImagesDir() {
4480
+ if (!existsSync3(IMAGES_DIR))
4481
+ mkdirSync3(IMAGES_DIR, { recursive: true });
4482
+ }
4483
+ function getImagesDir() {
4484
+ ensureImagesDir();
4485
+ return IMAGES_DIR;
4486
+ }
4487
+ function saveImage(entityId, source, options) {
4488
+ ensureImagesDir();
4489
+ deleteImage(entityId);
4490
+ const base64Match = source.match(/^data:image\/(\w+);base64,(.+)$/s);
4491
+ if (base64Match) {
4492
+ const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
4493
+ const data = Buffer.from(base64Match[2], "base64");
4494
+ const filename2 = `${entityId}.${ext2}`;
4495
+ writeFileSync2(join3(IMAGES_DIR, filename2), data);
4496
+ return filename2;
4497
+ }
4498
+ if (!existsSync3(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
4499
+ const ext2 = options?.format || "jpg";
4500
+ const data = Buffer.from(source.trim(), "base64");
4501
+ const filename2 = `${entityId}.${ext2}`;
4502
+ writeFileSync2(join3(IMAGES_DIR, filename2), data);
4503
+ return filename2;
4504
+ }
4505
+ if (!existsSync3(source)) {
4506
+ throw new Error(`Image file not found: ${source}`);
4507
+ }
4508
+ const ext = extname(source).slice(1).toLowerCase() || "jpg";
4509
+ const validExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "ico"];
4510
+ if (!validExts.includes(ext)) {
4511
+ throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
4512
+ }
4513
+ const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
4514
+ copyFileSync(source, join3(IMAGES_DIR, filename));
4515
+ return filename;
4516
+ }
4517
+ function getImagePath(entityId) {
4518
+ ensureImagesDir();
4519
+ const files = readdirSync(IMAGES_DIR);
4520
+ const match = files.find((f) => f.startsWith(`${entityId}.`));
4521
+ return match ? join3(IMAGES_DIR, match) : null;
4522
+ }
4523
+ function getImageAsBase64(entityId) {
4524
+ const path = getImagePath(entityId);
4525
+ if (!path)
4526
+ return null;
4527
+ const ext = extname(path).slice(1);
4528
+ const mime = ext === "jpg" ? "image/jpeg" : ext === "svg" ? "image/svg+xml" : `image/${ext}`;
4529
+ const data = readFileSync2(path);
4530
+ return `data:${mime};base64,${data.toString("base64")}`;
4531
+ }
4532
+ function deleteImage(entityId) {
4533
+ ensureImagesDir();
4534
+ const files = readdirSync(IMAGES_DIR);
4535
+ let deleted = false;
4536
+ for (const f of files) {
4537
+ if (f.startsWith(`${entityId}.`)) {
4538
+ unlinkSync(join3(IMAGES_DIR, f));
4539
+ deleted = true;
4540
+ }
4541
+ }
4542
+ return deleted;
4543
+ }
4544
+ function listImages() {
4545
+ ensureImagesDir();
4546
+ const files = readdirSync(IMAGES_DIR).filter((f) => !f.startsWith("."));
4547
+ return files.map((f) => ({
4548
+ entity_id: basename(f, extname(f)),
4549
+ filename: f,
4550
+ path: join3(IMAGES_DIR, f)
4551
+ }));
4552
+ }
4553
+ var IMAGES_DIR;
4554
+ var init_images = __esm(() => {
4555
+ IMAGES_DIR = join3(process.env["HOME"] || "~", ".contacts", "images");
4556
+ });
4557
+
4387
4558
  // src/server/serve.ts
4388
4559
  var exports_serve = {};
4389
4560
  __export(exports_serve, {
4390
4561
  startServer: () => startServer
4391
4562
  });
4392
- import { existsSync as existsSync3 } from "fs";
4393
- import { join as join3 } from "path";
4563
+ import { existsSync as existsSync4 } from "fs";
4564
+ import { join as join4 } from "path";
4394
4565
  function json(data, status = 200) {
4395
4566
  return new Response(JSON.stringify(data), {
4396
4567
  status,
@@ -4595,8 +4766,60 @@ async function handleExport(req) {
4595
4766
  }
4596
4767
  });
4597
4768
  }
4769
+ async function handleImages(req, _url, segments) {
4770
+ const entityId = segments[2];
4771
+ if (!entityId)
4772
+ return apiError("Entity ID required");
4773
+ if (req.method === "GET") {
4774
+ const imagePath = getImagePath(entityId);
4775
+ if (!imagePath || !existsSync4(imagePath)) {
4776
+ return new Response(null, { status: 404, headers: { "Content-Type": "text/plain" } });
4777
+ }
4778
+ return new Response(Bun.file(imagePath), {
4779
+ headers: { "Cache-Control": "public, max-age=3600" }
4780
+ });
4781
+ }
4782
+ if (req.method === "POST") {
4783
+ const contentType = req.headers.get("content-type") || "";
4784
+ if (contentType.includes("multipart/form-data")) {
4785
+ const formData = await req.formData();
4786
+ const file = formData.get("image");
4787
+ if (!file)
4788
+ return apiError("No image file in form data");
4789
+ const ext = file.name?.split(".").pop() || "jpg";
4790
+ const buffer = Buffer.from(await file.arrayBuffer());
4791
+ const tmpPath = join4(getImagesDir(), `_upload_${entityId}.${ext}`);
4792
+ const { writeFileSync: wfs } = await import("fs");
4793
+ wfs(tmpPath, buffer);
4794
+ try {
4795
+ const filename = saveImage(entityId, tmpPath);
4796
+ const { unlinkSync: unlinkSync2 } = await import("fs");
4797
+ try {
4798
+ unlinkSync2(tmpPath);
4799
+ } catch {}
4800
+ return json({ ok: true, entity_id: entityId, filename });
4801
+ } catch (e) {
4802
+ return apiError(e instanceof Error ? e.message : "Upload failed");
4803
+ }
4804
+ }
4805
+ const body = await parseJson(req);
4806
+ if (!body?.image)
4807
+ return apiError("Provide image as base64 string or file upload");
4808
+ try {
4809
+ const filename = saveImage(entityId, body.image, { format: body.format });
4810
+ return json({ ok: true, entity_id: entityId, filename });
4811
+ } catch (e) {
4812
+ return apiError(e instanceof Error ? e.message : "Upload failed");
4813
+ }
4814
+ }
4815
+ if (req.method === "DELETE") {
4816
+ const deleted = deleteImage(entityId);
4817
+ return json({ ok: true, deleted });
4818
+ }
4819
+ return apiError("Method not allowed", 405);
4820
+ }
4598
4821
  function serveStaticFile(filePath) {
4599
- if (!existsSync3(filePath))
4822
+ if (!existsSync4(filePath))
4600
4823
  return null;
4601
4824
  return new Response(Bun.file(filePath));
4602
4825
  }
@@ -4636,12 +4859,15 @@ function startServer(port) {
4636
4859
  case "export":
4637
4860
  response = req.method === "GET" ? await handleExport(req) : apiError("Method not allowed", 405);
4638
4861
  break;
4862
+ case "images":
4863
+ response = await handleImages(req, url, segments);
4864
+ break;
4639
4865
  default:
4640
4866
  response = apiError("Not found", 404);
4641
4867
  }
4642
4868
  } else {
4643
- const filePath = join3(DASHBOARD_DIST, url.pathname === "/" ? "index.html" : url.pathname);
4644
- response = serveStaticFile(filePath) ?? serveStaticFile(join3(DASHBOARD_DIST, "index.html")) ?? new Response("Not Found", { status: 404 });
4869
+ const filePath = join4(DASHBOARD_DIST, url.pathname === "/" ? "index.html" : url.pathname);
4870
+ response = serveStaticFile(filePath) ?? serveStaticFile(join4(DASHBOARD_DIST, "index.html")) ?? new Response("Not Found", { status: 404 });
4645
4871
  }
4646
4872
  } catch (err) {
4647
4873
  console.error("Request error:", err);
@@ -4662,7 +4888,8 @@ var init_serve = __esm(() => {
4662
4888
  init_contacts();
4663
4889
  init_companies();
4664
4890
  init_tags();
4665
- DASHBOARD_DIST = join3(import.meta.dir, "../../dashboard/dist");
4891
+ init_images();
4892
+ DASHBOARD_DIST = join4(import.meta.dir, "../../dashboard/dist");
4666
4893
  });
4667
4894
 
4668
4895
  // src/db/notes.ts
@@ -5648,16 +5875,18 @@ function getFreshnessScore(contactId, db) {
5648
5875
  }
5649
5876
  function getStaleContacts(threshold = 40, db) {
5650
5877
  const _db2 = db || getDatabase();
5651
- const rows = _db2.query(`SELECT c.id as contact_id, c.display_name,
5652
- (CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
5653
- CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
5654
- CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
5655
- CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
5656
- CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
5657
- CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
5658
- CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
5659
- ) as score
5660
- FROM contacts c WHERE c.archived=0 HAVING score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
5878
+ const rows = _db2.query(`SELECT * FROM (
5879
+ SELECT c.id as contact_id, c.display_name,
5880
+ (CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
5881
+ CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
5882
+ CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
5883
+ CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
5884
+ CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
5885
+ CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
5886
+ CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
5887
+ ) as score
5888
+ FROM contacts c WHERE c.archived=0
5889
+ ) WHERE score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
5661
5890
  return rows;
5662
5891
  }
5663
5892
  function markFieldVerified(contactId, fieldName, source, db) {
@@ -6072,8 +6301,8 @@ function readConfig() {
6072
6301
  }
6073
6302
 
6074
6303
  // src/cli/index.tsx
6075
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync4, copyFileSync, statSync, mkdirSync as mkdirSync3, readdirSync } from "fs";
6076
- import { extname, join as join4 } from "path";
6304
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync5, copyFileSync as copyFileSync2, statSync, mkdirSync as mkdirSync4, readdirSync as readdirSync2 } from "fs";
6305
+ import { extname as extname2, join as join5 } from "path";
6077
6306
  function renderTable(headers, rows) {
6078
6307
  const colWidths = headers.map((h) => h.length);
6079
6308
  for (const row of rows) {
@@ -6484,13 +6713,13 @@ Add New Tag
6484
6713
  `));
6485
6714
  });
6486
6715
  program.command("import <file>").description("Import contacts from CSV, vCard (.vcf), or JSON file").action(async (file) => {
6487
- if (!existsSync4(file)) {
6716
+ if (!existsSync5(file)) {
6488
6717
  console.error(chalk.red(`
6489
6718
  File not found: ${file}
6490
6719
  `));
6491
6720
  process.exit(1);
6492
6721
  }
6493
- const ext = extname(file).toLowerCase();
6722
+ const ext = extname2(file).toLowerCase();
6494
6723
  const formatMap = {
6495
6724
  ".csv": "csv",
6496
6725
  ".vcf": "vcf",
@@ -6504,7 +6733,7 @@ Unsupported file type: ${ext}. Use .csv, .vcf, or .json
6504
6733
  `));
6505
6734
  process.exit(1);
6506
6735
  }
6507
- const data = readFileSync2(file, "utf8");
6736
+ const data = readFileSync3(file, "utf8");
6508
6737
  console.log(chalk.blue(`
6509
6738
  Importing ${format.toUpperCase()} from ${file}...
6510
6739
  `));
@@ -6535,7 +6764,7 @@ Invalid format: ${format}. Use csv, vcf, or json
6535
6764
  const { contacts } = listContacts({ limit: 1e5 });
6536
6765
  const output = await exportContacts(format, contacts);
6537
6766
  if (opts.output) {
6538
- writeFileSync2(opts.output, output, "utf8");
6767
+ writeFileSync3(opts.output, output, "utf8");
6539
6768
  console.log(chalk.green(`
6540
6769
  \u2713 Exported ${contacts.length} contact(s) to ${opts.output}
6541
6770
  `));
@@ -6807,15 +7036,15 @@ program.command("init").description("Show setup info, stats, and configuration")
6807
7036
  console.log();
6808
7037
  });
6809
7038
  program.command("backup").description("Backup the contacts database").option("--output <path>", "Output path").option("--list", "List existing backups").action((opts) => {
6810
- const backupDir = join4(process.env["HOME"] || "~", ".contacts", "backups");
7039
+ const backupDir = join5(process.env["HOME"] || "~", ".contacts", "backups");
6811
7040
  if (opts.list) {
6812
- if (!existsSync4(backupDir)) {
7041
+ if (!existsSync5(backupDir)) {
6813
7042
  console.log(chalk.gray(`
6814
7043
  No backups found.
6815
7044
  `));
6816
7045
  return;
6817
7046
  }
6818
- const files = readdirSync(backupDir).filter((f) => f.endsWith(".db")).sort().reverse();
7047
+ const files = readdirSync2(backupDir).filter((f) => f.endsWith(".db")).sort().reverse();
6819
7048
  if (files.length === 0) {
6820
7049
  console.log(chalk.gray(`
6821
7050
  No backups found.
@@ -6826,7 +7055,7 @@ No backups found.
6826
7055
  Existing Backups:
6827
7056
  `));
6828
7057
  for (const f of files) {
6829
- const filePath = join4(backupDir, f);
7058
+ const filePath = join5(backupDir, f);
6830
7059
  const size2 = statSync(filePath).size;
6831
7060
  const mtime = statSync(filePath).mtime.toISOString().slice(0, 19).replace("T", " ");
6832
7061
  console.log(` ${chalk.cyan(f)} ${chalk.gray(`${(size2 / 1024).toFixed(1)} KB ${mtime}`)}`);
@@ -6835,16 +7064,16 @@ Existing Backups:
6835
7064
  return;
6836
7065
  }
6837
7066
  const src = getDbPath();
6838
- if (!existsSync4(src)) {
7067
+ if (!existsSync5(src)) {
6839
7068
  console.error(chalk.red(`
6840
7069
  Database not found: ${src}
6841
7070
  `));
6842
7071
  process.exit(1);
6843
7072
  }
6844
- mkdirSync3(backupDir, { recursive: true });
7073
+ mkdirSync4(backupDir, { recursive: true });
6845
7074
  const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
6846
- const dest = opts.output || join4(backupDir, `contacts-${ts}.db`);
6847
- copyFileSync(src, dest);
7075
+ const dest = opts.output || join5(backupDir, `contacts-${ts}.db`);
7076
+ copyFileSync2(src, dest);
6848
7077
  const size = statSync(dest).size;
6849
7078
  console.log(chalk.green(`
6850
7079
  \u2713 Backed up to ${dest} (${(size / 1024).toFixed(1)} KB)
@@ -8098,4 +8327,84 @@ dealsTeamCmd.command("assign <deal-id> <contact-id>").description("Assign a cont
8098
8327
  \u2713 ${contact.display_name} assigned as ${opts.role || "other"} in deal ${dealId}
8099
8328
  `));
8100
8329
  });
8330
+ var photoCmd = program.command("photo").description("Manage contact profile photos");
8331
+ photoCmd.command("set <contact-id> <image-path>").description("Set a contact's profile photo from a local file").action((contactId, imagePath) => {
8332
+ const { saveImage: saveImage2 } = (init_images(), __toCommonJS(exports_images));
8333
+ try {
8334
+ const contact = getContact(contactId);
8335
+ const filename = saveImage2(contactId, imagePath);
8336
+ updateContact(contactId, { avatar_url: `~/.contacts/images/${filename}` });
8337
+ console.log(chalk.green(`Photo set for ${contact.display_name}: ~/.contacts/images/${filename}`));
8338
+ } catch (e) {
8339
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
8340
+ }
8341
+ });
8342
+ photoCmd.command("show <contact-id>").description("Show the path to a contact's profile photo").action((contactId) => {
8343
+ const { getImagePath: getImagePath2 } = (init_images(), __toCommonJS(exports_images));
8344
+ const contact = getContact(contactId);
8345
+ const path = getImagePath2(contactId);
8346
+ if (path) {
8347
+ console.log(`${chalk.bold(contact.display_name)}: ${chalk.cyan(path)}`);
8348
+ } else {
8349
+ console.log(chalk.yellow(`No photo set for ${contact.display_name}`));
8350
+ }
8351
+ });
8352
+ photoCmd.command("remove <contact-id>").description("Remove a contact's profile photo").action((contactId) => {
8353
+ const { deleteImage: deleteImage2 } = (init_images(), __toCommonJS(exports_images));
8354
+ const contact = getContact(contactId);
8355
+ const deleted = deleteImage2(contactId);
8356
+ if (deleted) {
8357
+ updateContact(contactId, { avatar_url: null });
8358
+ console.log(chalk.green(`Photo removed for ${contact.display_name}`));
8359
+ } else {
8360
+ console.log(chalk.yellow(`No photo found for ${contact.display_name}`));
8361
+ }
8362
+ });
8363
+ photoCmd.command("list").description("List all stored photos").action(() => {
8364
+ const { listImages: listImages2 } = (init_images(), __toCommonJS(exports_images));
8365
+ const images = listImages2();
8366
+ if (!images.length) {
8367
+ console.log(chalk.yellow("No photos stored"));
8368
+ return;
8369
+ }
8370
+ for (const img of images) {
8371
+ try {
8372
+ const contact = getContact(img.entity_id);
8373
+ console.log(`${chalk.bold(contact.display_name)}: ${chalk.cyan(img.path)}`);
8374
+ } catch {
8375
+ console.log(`${chalk.gray(img.entity_id)}: ${chalk.cyan(img.path)}`);
8376
+ }
8377
+ }
8378
+ });
8379
+ var logoCmd = program.command("logo").description("Manage company logos");
8380
+ logoCmd.command("set <company-id> <image-path>").description("Set a company's logo from a local file").action((companyId, imagePath) => {
8381
+ const { saveImage: saveImage2 } = (init_images(), __toCommonJS(exports_images));
8382
+ try {
8383
+ const company = getCompany(companyId);
8384
+ if (!company) {
8385
+ console.error(chalk.red("Company not found"));
8386
+ return;
8387
+ }
8388
+ const filename = saveImage2(companyId, imagePath);
8389
+ const { updateCompany: updateCompany2 } = (init_companies(), __toCommonJS(exports_companies));
8390
+ updateCompany2(companyId, { logo_url: `~/.contacts/images/${filename}` });
8391
+ console.log(chalk.green(`Logo set for ${company.name}: ~/.contacts/images/${filename}`));
8392
+ } catch (e) {
8393
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
8394
+ }
8395
+ });
8396
+ logoCmd.command("show <company-id>").description("Show the path to a company's logo").action((companyId) => {
8397
+ const { getImagePath: getImagePath2 } = (init_images(), __toCommonJS(exports_images));
8398
+ const company = getCompany(companyId);
8399
+ if (!company) {
8400
+ console.error(chalk.red("Company not found"));
8401
+ return;
8402
+ }
8403
+ const path = getImagePath2(companyId);
8404
+ if (path) {
8405
+ console.log(`${chalk.bold(company.name)}: ${chalk.cyan(path)}`);
8406
+ } else {
8407
+ console.log(chalk.yellow(`No logo set for ${company.name}`));
8408
+ }
8409
+ });
8101
8410
  program.parse(process.argv);
@@ -1 +1 @@
1
- {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/db/freshness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;IACvE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAID,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,cAAc,CAoFlF;AAED,wBAAgB,gBAAgB,CAAC,SAAS,SAAK,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAkBlI;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAsB5G"}
1
+ {"version":3,"file":"freshness.d.ts","sourceRoot":"","sources":["../../src/db/freshness.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,UAAU,EAAE,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;IACvE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAID,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,cAAc,CAoFlF;AAED,wBAAgB,gBAAgB,CAAC,SAAS,SAAK,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBlI;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CAsB5G"}
package/dist/index.d.ts CHANGED
@@ -61,4 +61,5 @@ export { parseEmailSignature, extractContactsFromEmailThread } from "./lib/signa
61
61
  export type { ParsedSignature } from "./lib/signature-parser.js";
62
62
  export { ingestMeetingParticipants } from "./lib/meeting-capture.js";
63
63
  export { findOrCreateContact } from "./db/contacts.js";
64
+ export { saveImage, getImagePath, getImageAsBase64, deleteImage, listImages, getImagesDir } from "./lib/images.js";
64
65
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4121,16 +4121,18 @@ function getFreshnessScore(contactId, db) {
4121
4121
  }
4122
4122
  function getStaleContacts(threshold = 40, db) {
4123
4123
  const _db2 = db || getDatabase();
4124
- const rows = _db2.query(`SELECT c.id as contact_id, c.display_name,
4125
- (CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
4126
- CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
4127
- CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
4128
- CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
4129
- CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
4130
- CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
4131
- CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
4132
- ) as score
4133
- FROM contacts c WHERE c.archived=0 HAVING score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
4124
+ const rows = _db2.query(`SELECT * FROM (
4125
+ SELECT c.id as contact_id, c.display_name,
4126
+ (CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
4127
+ CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
4128
+ CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
4129
+ CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
4130
+ CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
4131
+ CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
4132
+ CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
4133
+ ) as score
4134
+ FROM contacts c WHERE c.archived=0
4135
+ ) WHERE score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
4134
4136
  return rows;
4135
4137
  }
4136
4138
  function markFieldVerified(contactId, fieldName, source, db) {
@@ -4374,6 +4376,85 @@ async function ingestMeetingParticipants(event, db) {
4374
4376
 
4375
4377
  // src/index.ts
4376
4378
  init_contacts();
4379
+
4380
+ // src/lib/images.ts
4381
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, copyFileSync, unlinkSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4382
+ import { join as join3, extname, basename } from "path";
4383
+ var IMAGES_DIR = join3(process.env["HOME"] || "~", ".contacts", "images");
4384
+ function ensureImagesDir() {
4385
+ if (!existsSync3(IMAGES_DIR))
4386
+ mkdirSync2(IMAGES_DIR, { recursive: true });
4387
+ }
4388
+ function getImagesDir() {
4389
+ ensureImagesDir();
4390
+ return IMAGES_DIR;
4391
+ }
4392
+ function saveImage(entityId, source, options) {
4393
+ ensureImagesDir();
4394
+ deleteImage(entityId);
4395
+ const base64Match = source.match(/^data:image\/(\w+);base64,(.+)$/s);
4396
+ if (base64Match) {
4397
+ const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
4398
+ const data = Buffer.from(base64Match[2], "base64");
4399
+ const filename2 = `${entityId}.${ext2}`;
4400
+ writeFileSync(join3(IMAGES_DIR, filename2), data);
4401
+ return filename2;
4402
+ }
4403
+ if (!existsSync3(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
4404
+ const ext2 = options?.format || "jpg";
4405
+ const data = Buffer.from(source.trim(), "base64");
4406
+ const filename2 = `${entityId}.${ext2}`;
4407
+ writeFileSync(join3(IMAGES_DIR, filename2), data);
4408
+ return filename2;
4409
+ }
4410
+ if (!existsSync3(source)) {
4411
+ throw new Error(`Image file not found: ${source}`);
4412
+ }
4413
+ const ext = extname(source).slice(1).toLowerCase() || "jpg";
4414
+ const validExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "ico"];
4415
+ if (!validExts.includes(ext)) {
4416
+ throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
4417
+ }
4418
+ const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
4419
+ copyFileSync(source, join3(IMAGES_DIR, filename));
4420
+ return filename;
4421
+ }
4422
+ function getImagePath(entityId) {
4423
+ ensureImagesDir();
4424
+ const files = readdirSync(IMAGES_DIR);
4425
+ const match = files.find((f) => f.startsWith(`${entityId}.`));
4426
+ return match ? join3(IMAGES_DIR, match) : null;
4427
+ }
4428
+ function getImageAsBase64(entityId) {
4429
+ const path = getImagePath(entityId);
4430
+ if (!path)
4431
+ return null;
4432
+ const ext = extname(path).slice(1);
4433
+ const mime = ext === "jpg" ? "image/jpeg" : ext === "svg" ? "image/svg+xml" : `image/${ext}`;
4434
+ const data = readFileSync2(path);
4435
+ return `data:${mime};base64,${data.toString("base64")}`;
4436
+ }
4437
+ function deleteImage(entityId) {
4438
+ ensureImagesDir();
4439
+ const files = readdirSync(IMAGES_DIR);
4440
+ let deleted = false;
4441
+ for (const f of files) {
4442
+ if (f.startsWith(`${entityId}.`)) {
4443
+ unlinkSync(join3(IMAGES_DIR, f));
4444
+ deleted = true;
4445
+ }
4446
+ }
4447
+ return deleted;
4448
+ }
4449
+ function listImages() {
4450
+ ensureImagesDir();
4451
+ const files = readdirSync(IMAGES_DIR).filter((f) => !f.startsWith("."));
4452
+ return files.map((f) => ({
4453
+ entity_id: basename(f, extname(f)),
4454
+ filename: f,
4455
+ path: join3(IMAGES_DIR, f)
4456
+ }));
4457
+ }
4377
4458
  export {
4378
4459
  updateVendorCommunication,
4379
4460
  updateTag,
@@ -4393,6 +4474,7 @@ export {
4393
4474
  searchContacts,
4394
4475
  searchCompanies,
4395
4476
  saveLearning,
4477
+ saveImage,
4396
4478
  runConnector,
4397
4479
  resolveIdentity,
4398
4480
  resolveByPartial,
@@ -4433,6 +4515,7 @@ export {
4433
4515
  listNotesForContactAtCompany,
4434
4516
  listNotes,
4435
4517
  listMissingInvoices,
4518
+ listImages,
4436
4519
  listGroupsForContact,
4437
4520
  listGroupsForCompany,
4438
4521
  listGroups,
@@ -4470,6 +4553,9 @@ export {
4470
4553
  getNetworkStats,
4471
4554
  getLearnings,
4472
4555
  getJobHistory,
4556
+ getImagesDir,
4557
+ getImagePath,
4558
+ getImageAsBase64,
4473
4559
  getIdentities,
4474
4560
  getGroup,
4475
4561
  getGhostContacts,
@@ -4512,6 +4598,7 @@ export {
4512
4598
  deleteRelationship,
4513
4599
  deleteNote,
4514
4600
  deleteLearning,
4601
+ deleteImage,
4515
4602
  deleteGroup,
4516
4603
  deleteEvent,
4517
4604
  deleteDeal,
@@ -0,0 +1,30 @@
1
+ export declare function getImagesDir(): string;
2
+ /**
3
+ * Save an image for a contact or company.
4
+ * Accepts a file path (copies it) or base64 data (writes it).
5
+ * Returns the stored filename (e.g. "abc123.jpg")
6
+ */
7
+ export declare function saveImage(entityId: string, source: string, options?: {
8
+ format?: string;
9
+ }): string;
10
+ /**
11
+ * Get the image path for an entity. Returns null if no image exists.
12
+ */
13
+ export declare function getImagePath(entityId: string): string | null;
14
+ /**
15
+ * Get image as base64 data URI for embedding in responses.
16
+ */
17
+ export declare function getImageAsBase64(entityId: string): string | null;
18
+ /**
19
+ * Delete an entity's image.
20
+ */
21
+ export declare function deleteImage(entityId: string): boolean;
22
+ /**
23
+ * List all stored images.
24
+ */
25
+ export declare function listImages(): Array<{
26
+ entity_id: string;
27
+ filename: string;
28
+ path: string;
29
+ }>;
30
+ //# sourceMappingURL=images.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../src/lib/images.ts"],"names":[],"mappings":"AAaA,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5B,MAAM,CAuCR;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOhE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAWrD;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAQzF"}
package/dist/mcp/index.js CHANGED
@@ -4140,16 +4140,18 @@ function getFreshnessScore(contactId, db) {
4140
4140
  }
4141
4141
  function getStaleContacts(threshold = 40, db) {
4142
4142
  const _db2 = db || getDatabase();
4143
- const rows = _db2.query(`SELECT c.id as contact_id, c.display_name,
4144
- (CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
4145
- CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
4146
- CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
4147
- CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
4148
- CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
4149
- CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
4150
- CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
4151
- ) as score
4152
- FROM contacts c WHERE c.archived=0 HAVING score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
4143
+ const rows = _db2.query(`SELECT * FROM (
4144
+ SELECT c.id as contact_id, c.display_name,
4145
+ (CASE WHEN c.job_title IS NOT NULL THEN 15 ELSE 0 END +
4146
+ CASE WHEN c.company_id IS NOT NULL THEN 15 ELSE 0 END +
4147
+ CASE WHEN c.last_contacted_at IS NOT NULL THEN 20 ELSE 0 END +
4148
+ CASE WHEN EXISTS(SELECT 1 FROM emails WHERE contact_id=c.id) THEN 20 ELSE 0 END +
4149
+ CASE WHEN EXISTS(SELECT 1 FROM phones WHERE contact_id=c.id) THEN 15 ELSE 0 END +
4150
+ CASE WHEN c.notes IS NOT NULL THEN 10 ELSE 0 END +
4151
+ CASE WHEN EXISTS(SELECT 1 FROM contact_tags WHERE contact_id=c.id) THEN 5 ELSE 0 END
4152
+ ) as score
4153
+ FROM contacts c WHERE c.archived=0
4154
+ ) WHERE score < ? ORDER BY score ASC LIMIT 100`).all(threshold);
4153
4155
  return rows;
4154
4156
  }
4155
4157
  function markFieldVerified(contactId, fieldName, source, db) {
@@ -4203,6 +4205,72 @@ function getCoverageGaps(companyId, db) {
4203
4205
  };
4204
4206
  }
4205
4207
 
4208
+ // src/lib/images.ts
4209
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, copyFileSync, unlinkSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
4210
+ import { join as join3, extname, basename } from "path";
4211
+ var IMAGES_DIR = join3(process.env["HOME"] || "~", ".contacts", "images");
4212
+ function ensureImagesDir() {
4213
+ if (!existsSync3(IMAGES_DIR))
4214
+ mkdirSync2(IMAGES_DIR, { recursive: true });
4215
+ }
4216
+ function saveImage(entityId, source, options) {
4217
+ ensureImagesDir();
4218
+ deleteImage(entityId);
4219
+ const base64Match = source.match(/^data:image\/(\w+);base64,(.+)$/s);
4220
+ if (base64Match) {
4221
+ const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
4222
+ const data = Buffer.from(base64Match[2], "base64");
4223
+ const filename2 = `${entityId}.${ext2}`;
4224
+ writeFileSync(join3(IMAGES_DIR, filename2), data);
4225
+ return filename2;
4226
+ }
4227
+ if (!existsSync3(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
4228
+ const ext2 = options?.format || "jpg";
4229
+ const data = Buffer.from(source.trim(), "base64");
4230
+ const filename2 = `${entityId}.${ext2}`;
4231
+ writeFileSync(join3(IMAGES_DIR, filename2), data);
4232
+ return filename2;
4233
+ }
4234
+ if (!existsSync3(source)) {
4235
+ throw new Error(`Image file not found: ${source}`);
4236
+ }
4237
+ const ext = extname(source).slice(1).toLowerCase() || "jpg";
4238
+ const validExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "ico"];
4239
+ if (!validExts.includes(ext)) {
4240
+ throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
4241
+ }
4242
+ const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
4243
+ copyFileSync(source, join3(IMAGES_DIR, filename));
4244
+ return filename;
4245
+ }
4246
+ function getImagePath(entityId) {
4247
+ ensureImagesDir();
4248
+ const files = readdirSync(IMAGES_DIR);
4249
+ const match = files.find((f) => f.startsWith(`${entityId}.`));
4250
+ return match ? join3(IMAGES_DIR, match) : null;
4251
+ }
4252
+ function getImageAsBase64(entityId) {
4253
+ const path = getImagePath(entityId);
4254
+ if (!path)
4255
+ return null;
4256
+ const ext = extname(path).slice(1);
4257
+ const mime = ext === "jpg" ? "image/jpeg" : ext === "svg" ? "image/svg+xml" : `image/${ext}`;
4258
+ const data = readFileSync2(path);
4259
+ return `data:${mime};base64,${data.toString("base64")}`;
4260
+ }
4261
+ function deleteImage(entityId) {
4262
+ ensureImagesDir();
4263
+ const files = readdirSync(IMAGES_DIR);
4264
+ let deleted = false;
4265
+ for (const f of files) {
4266
+ if (f.startsWith(`${entityId}.`)) {
4267
+ unlinkSync(join3(IMAGES_DIR, f));
4268
+ deleted = true;
4269
+ }
4270
+ }
4271
+ return deleted;
4272
+ }
4273
+
4206
4274
  // src/mcp/index.ts
4207
4275
  var server = new Server({ name: "contacts", version: "0.1.0" }, { capabilities: { tools: {} } });
4208
4276
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -5663,7 +5731,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
5663
5731
  { name: "set_deal_contact_role", description: "Assign a contact a buying committee role in a deal (economic_buyer, technical_evaluator, champion, blocker, influencer, user, sponsor, other).", inputSchema: { type: "object", properties: { deal_id: { type: "string" }, contact_id: { type: "string" }, account_role: { type: "string", enum: ["economic_buyer", "technical_evaluator", "champion", "blocker", "influencer", "user", "sponsor", "other"] } }, required: ["deal_id", "contact_id", "account_role"] } },
5664
5732
  { name: "get_deal_team", description: "Get the full buying committee for a deal with contact names and roles.", inputSchema: { type: "object", properties: { deal_id: { type: "string" } }, required: ["deal_id"] } },
5665
5733
  { name: "get_coverage_gaps", description: "Identify coverage gaps in a company account \u2014 missing economic buyer, technical evaluator, or org chart relationships.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } },
5666
- { name: "get_recent_contact_events", description: "Polling fallback for change events \u2014 returns recent activity log entries, optionally filtered by event type or date.", inputSchema: { type: "object", properties: { since: { type: "string", description: "ISO 8601 datetime \u2014 only events after this date" }, event_types: { type: "array", items: { type: "string" } } } } }
5734
+ { name: "get_recent_contact_events", description: "Polling fallback for change events \u2014 returns recent activity log entries, optionally filtered by event type or date.", inputSchema: { type: "object", properties: { since: { type: "string", description: "ISO 8601 datetime \u2014 only events after this date" }, event_types: { type: "array", items: { type: "string" } } } } },
5735
+ { name: "set_contact_photo", description: "Set a contact's profile photo. Provide either a local file path or base64-encoded image data (with or without data URI prefix). Stores image in ~/.contacts/images/ and updates avatar_url. Supported formats: jpg, png, gif, webp, svg, avif.", inputSchema: { type: "object", properties: { contact_id: { type: "string" }, image: { type: "string", description: "File path (e.g. /tmp/photo.jpg) OR base64 data (e.g. data:image/png;base64,...) OR raw base64 string" }, format: { type: "string", description: "Image format hint when using raw base64 (jpg, png, webp). Not needed for file paths or data URIs." } }, required: ["contact_id", "image"] } },
5736
+ { name: "get_contact_photo", description: "Get a contact's profile photo as base64 data URI. Returns null if no photo is set.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
5737
+ { name: "delete_contact_photo", description: "Remove a contact's profile photo.", inputSchema: { type: "object", properties: { contact_id: { type: "string" } }, required: ["contact_id"] } },
5738
+ { name: "set_company_logo", description: "Set a company's logo image. Provide either a local file path or base64-encoded image data. Stores image in ~/.contacts/images/ and updates logo_url.", inputSchema: { type: "object", properties: { company_id: { type: "string" }, image: { type: "string", description: "File path or base64 data" }, format: { type: "string", description: "Image format hint for raw base64" } }, required: ["company_id", "image"] } },
5739
+ { name: "get_company_logo", description: "Get a company's logo as base64 data URI.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } },
5740
+ { name: "delete_company_logo", description: "Remove a company's logo image.", inputSchema: { type: "object", properties: { company_id: { type: "string" } }, required: ["company_id"] } }
5667
5741
  ]
5668
5742
  }));
5669
5743
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -7154,6 +7228,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7154
7228
  const events = db.query(sql).all(...params);
7155
7229
  return { content: [{ type: "text", text: JSON.stringify({ events }, null, 2) }] };
7156
7230
  }
7231
+ case "set_contact_photo": {
7232
+ const { contact_id, image, format } = a;
7233
+ const contact = getContact(contact_id);
7234
+ const filename = saveImage(contact_id, image, { format });
7235
+ updateContact(contact_id, { avatar_url: `~/.contacts/images/${filename}` });
7236
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, contact_id, filename, avatar_url: `~/.contacts/images/${filename}` }) }] };
7237
+ }
7238
+ case "get_contact_photo": {
7239
+ const { contact_id } = a;
7240
+ const dataUri = getImageAsBase64(contact_id);
7241
+ if (!dataUri)
7242
+ return { content: [{ type: "text", text: JSON.stringify({ contact_id, has_photo: false, data: null }) }] };
7243
+ return { content: [{ type: "text", text: JSON.stringify({ contact_id, has_photo: true, data: dataUri }) }] };
7244
+ }
7245
+ case "delete_contact_photo": {
7246
+ const { contact_id } = a;
7247
+ const deleted = deleteImage(contact_id);
7248
+ if (deleted)
7249
+ updateContact(contact_id, { avatar_url: null });
7250
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted }) }] };
7251
+ }
7252
+ case "set_company_logo": {
7253
+ const { company_id, image, format } = a;
7254
+ const co = getCompany(company_id);
7255
+ const filename = saveImage(company_id, image, { format });
7256
+ updateCompany(company_id, { logo_url: `~/.contacts/images/${filename}` });
7257
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, company_id, filename, logo_url: `~/.contacts/images/${filename}` }) }] };
7258
+ }
7259
+ case "get_company_logo": {
7260
+ const { company_id } = a;
7261
+ const dataUri = getImageAsBase64(company_id);
7262
+ if (!dataUri)
7263
+ return { content: [{ type: "text", text: JSON.stringify({ company_id, has_logo: false, data: null }) }] };
7264
+ return { content: [{ type: "text", text: JSON.stringify({ company_id, has_logo: true, data: dataUri }) }] };
7265
+ }
7266
+ case "delete_company_logo": {
7267
+ const { company_id } = a;
7268
+ const deleted = deleteImage(company_id);
7269
+ if (deleted)
7270
+ updateCompany(company_id, { logo_url: null });
7271
+ return { content: [{ type: "text", text: JSON.stringify({ ok: true, deleted }) }] };
7272
+ }
7157
7273
  default:
7158
7274
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
7159
7275
  }
@@ -1,9 +1,26 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __require = import.meta.require;
3
20
 
4
21
  // src/server/serve.ts
5
- import { existsSync as existsSync2 } from "fs";
6
- import { join as join2 } from "path";
22
+ import { existsSync as existsSync3 } from "fs";
23
+ import { join as join3 } from "path";
7
24
 
8
25
  // src/db/database.ts
9
26
  import { Database } from "bun:sqlite";
@@ -1706,8 +1723,69 @@ async function exportContacts(format, contacts) {
1706
1723
  }
1707
1724
  }
1708
1725
 
1726
+ // src/lib/images.ts
1727
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, copyFileSync, unlinkSync, readdirSync, readFileSync, writeFileSync } from "fs";
1728
+ import { join as join2, extname, basename } from "path";
1729
+ var IMAGES_DIR = join2(process.env["HOME"] || "~", ".contacts", "images");
1730
+ function ensureImagesDir() {
1731
+ if (!existsSync2(IMAGES_DIR))
1732
+ mkdirSync2(IMAGES_DIR, { recursive: true });
1733
+ }
1734
+ function getImagesDir() {
1735
+ ensureImagesDir();
1736
+ return IMAGES_DIR;
1737
+ }
1738
+ function saveImage(entityId, source, options) {
1739
+ ensureImagesDir();
1740
+ deleteImage(entityId);
1741
+ const base64Match = source.match(/^data:image\/(\w+);base64,(.+)$/s);
1742
+ if (base64Match) {
1743
+ const ext2 = base64Match[1] === "jpeg" ? "jpg" : base64Match[1];
1744
+ const data = Buffer.from(base64Match[2], "base64");
1745
+ const filename2 = `${entityId}.${ext2}`;
1746
+ writeFileSync(join2(IMAGES_DIR, filename2), data);
1747
+ return filename2;
1748
+ }
1749
+ if (!existsSync2(source) && /^[A-Za-z0-9+/=\n\r]+$/.test(source.trim()) && source.length > 100) {
1750
+ const ext2 = options?.format || "jpg";
1751
+ const data = Buffer.from(source.trim(), "base64");
1752
+ const filename2 = `${entityId}.${ext2}`;
1753
+ writeFileSync(join2(IMAGES_DIR, filename2), data);
1754
+ return filename2;
1755
+ }
1756
+ if (!existsSync2(source)) {
1757
+ throw new Error(`Image file not found: ${source}`);
1758
+ }
1759
+ const ext = extname(source).slice(1).toLowerCase() || "jpg";
1760
+ const validExts = ["jpg", "jpeg", "png", "gif", "webp", "svg", "avif", "ico"];
1761
+ if (!validExts.includes(ext)) {
1762
+ throw new Error(`Unsupported image format: ${ext}. Supported: ${validExts.join(", ")}`);
1763
+ }
1764
+ const filename = `${entityId}.${ext === "jpeg" ? "jpg" : ext}`;
1765
+ copyFileSync(source, join2(IMAGES_DIR, filename));
1766
+ return filename;
1767
+ }
1768
+ function getImagePath(entityId) {
1769
+ ensureImagesDir();
1770
+ const files = readdirSync(IMAGES_DIR);
1771
+ const match = files.find((f) => f.startsWith(`${entityId}.`));
1772
+ return match ? join2(IMAGES_DIR, match) : null;
1773
+ }
1774
+ function deleteImage(entityId) {
1775
+ ensureImagesDir();
1776
+ const files = readdirSync(IMAGES_DIR);
1777
+ let deleted = false;
1778
+ for (const f of files) {
1779
+ if (f.startsWith(`${entityId}.`)) {
1780
+ unlinkSync(join2(IMAGES_DIR, f));
1781
+ deleted = true;
1782
+ }
1783
+ }
1784
+ return deleted;
1785
+ }
1786
+
1709
1787
  // src/server/serve.ts
1710
- var DASHBOARD_DIST = join2(import.meta.dir, "../../dashboard/dist");
1788
+ var DASHBOARD_DIST = join3(import.meta.dir, "../../dashboard/dist");
1711
1789
  function json(data, status = 200) {
1712
1790
  return new Response(JSON.stringify(data), {
1713
1791
  status,
@@ -1912,8 +1990,60 @@ async function handleExport(req) {
1912
1990
  }
1913
1991
  });
1914
1992
  }
1993
+ async function handleImages(req, _url, segments) {
1994
+ const entityId = segments[2];
1995
+ if (!entityId)
1996
+ return apiError("Entity ID required");
1997
+ if (req.method === "GET") {
1998
+ const imagePath = getImagePath(entityId);
1999
+ if (!imagePath || !existsSync3(imagePath)) {
2000
+ return new Response(null, { status: 404, headers: { "Content-Type": "text/plain" } });
2001
+ }
2002
+ return new Response(Bun.file(imagePath), {
2003
+ headers: { "Cache-Control": "public, max-age=3600" }
2004
+ });
2005
+ }
2006
+ if (req.method === "POST") {
2007
+ const contentType = req.headers.get("content-type") || "";
2008
+ if (contentType.includes("multipart/form-data")) {
2009
+ const formData = await req.formData();
2010
+ const file = formData.get("image");
2011
+ if (!file)
2012
+ return apiError("No image file in form data");
2013
+ const ext = file.name?.split(".").pop() || "jpg";
2014
+ const buffer = Buffer.from(await file.arrayBuffer());
2015
+ const tmpPath = join3(getImagesDir(), `_upload_${entityId}.${ext}`);
2016
+ const { writeFileSync: wfs } = await import("fs");
2017
+ wfs(tmpPath, buffer);
2018
+ try {
2019
+ const filename = saveImage(entityId, tmpPath);
2020
+ const { unlinkSync: unlinkSync2 } = await import("fs");
2021
+ try {
2022
+ unlinkSync2(tmpPath);
2023
+ } catch {}
2024
+ return json({ ok: true, entity_id: entityId, filename });
2025
+ } catch (e) {
2026
+ return apiError(e instanceof Error ? e.message : "Upload failed");
2027
+ }
2028
+ }
2029
+ const body = await parseJson(req);
2030
+ if (!body?.image)
2031
+ return apiError("Provide image as base64 string or file upload");
2032
+ try {
2033
+ const filename = saveImage(entityId, body.image, { format: body.format });
2034
+ return json({ ok: true, entity_id: entityId, filename });
2035
+ } catch (e) {
2036
+ return apiError(e instanceof Error ? e.message : "Upload failed");
2037
+ }
2038
+ }
2039
+ if (req.method === "DELETE") {
2040
+ const deleted = deleteImage(entityId);
2041
+ return json({ ok: true, deleted });
2042
+ }
2043
+ return apiError("Method not allowed", 405);
2044
+ }
1915
2045
  function serveStaticFile(filePath) {
1916
- if (!existsSync2(filePath))
2046
+ if (!existsSync3(filePath))
1917
2047
  return null;
1918
2048
  return new Response(Bun.file(filePath));
1919
2049
  }
@@ -1953,12 +2083,15 @@ function startServer(port) {
1953
2083
  case "export":
1954
2084
  response = req.method === "GET" ? await handleExport(req) : apiError("Method not allowed", 405);
1955
2085
  break;
2086
+ case "images":
2087
+ response = await handleImages(req, url, segments);
2088
+ break;
1956
2089
  default:
1957
2090
  response = apiError("Not found", 404);
1958
2091
  }
1959
2092
  } else {
1960
- const filePath = join2(DASHBOARD_DIST, url.pathname === "/" ? "index.html" : url.pathname);
1961
- response = serveStaticFile(filePath) ?? serveStaticFile(join2(DASHBOARD_DIST, "index.html")) ?? new Response("Not Found", { status: 404 });
2093
+ const filePath = join3(DASHBOARD_DIST, url.pathname === "/" ? "index.html" : url.pathname);
2094
+ response = serveStaticFile(filePath) ?? serveStaticFile(join3(DASHBOARD_DIST, "index.html")) ?? new Response("Not Found", { status: 404 });
1962
2095
  }
1963
2096
  } catch (err) {
1964
2097
  console.error("Request error:", err);
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAsRA,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAsE9C"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAoVA,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAyE9C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/contacts",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Contact management for AI coding agents — CLI + MCP + Web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",