@hasna/contacts 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,12 @@ export { createTag, getTag, getTagByName, listTags, updateTag, deleteTag, addTag
5
5
  export { createRelationship, listRelationships, getRelationship, deleteRelationship, } from "./db/relationships.js";
6
6
  export { createGroup, getGroup, listGroups, updateGroup, deleteGroup, addContactToGroup, removeContactFromGroup, listContactsInGroup, listGroupsForContact, addCompanyToGroup, removeCompanyFromGroup, listCompaniesInGroup, listGroupsForCompany, } from "./db/groups.js";
7
7
  export { logActivity, listActivity, getActivity } from "./db/activity.js";
8
+ export { runConnector, readConnectorTokens, getConnectorTokenPath, ConnectorNotInstalledError, ConnectorAuthError } from "./lib/connector.js";
9
+ export type { ConnectorRunOptions } from "./lib/connector.js";
10
+ export { extractContactsFromGmail, parseAddressHeader, domainToCompany, parseName } from "./lib/gmail-import.js";
11
+ export type { GmailImportOptions, ExtractedContact } from "./lib/gmail-import.js";
12
+ export { listGoogleContacts, searchGoogleContacts, pullGoogleContactsAsInputs, pushContactToGoogle, googlePersonToContactInput, contactToGoogleArgs, } from "./lib/google-contacts.js";
13
+ export type { GooglePerson, GoogleContactsSyncOptions, GoogleContactsPushOptions, SyncResult } from "./lib/google-contacts.js";
8
14
  export type { EmailType, PhoneType, AddressType, SocialPlatform, RelationshipType, ContactSource, PreferredContactMethod, ContactStatus, Email, Phone, Address, SocialProfile, Tag, Contact, ContactWithDetails, Company, CompanyWithDetails, ContactRelationship, ActivityLog, Webhook, Group, CreateGroupInput, CreateEmailInput, CreatePhoneInput, CreateAddressInput, CreateSocialProfileInput, CreateTagInput, UpdateTagInput, CreateContactInput, UpdateContactInput, ContactListOptions, CreateCompanyInput, UpdateCompanyInput, CompanyListOptions, CreateRelationshipInput, CreateActivityInput, CreateWebhookInput, UpdateWebhookInput, DuplicateByEmail, DuplicateByName, ContactRow, CompanyRow, EmailRow, PhoneRow, AddressRow, SocialProfileRow, TagRow, RelationshipRow, ActivityRow, WebhookRow, } from "./types/index.js";
9
15
  export { ContactNotFoundError, CompanyNotFoundError, TagNotFoundError, DuplicateTagNameError, } from "./types/index.js";
10
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG9D,OAAO,EACL,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,aAAa,EACb,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,SAAS,EACT,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,WAAW,EACX,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG1E,YAAY,EAEV,SAAS,EACT,SAAS,EACT,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,sBAAsB,EACtB,aAAa,EAEb,KAAK,EACL,KAAK,EACL,OAAO,EACP,aAAa,EAEb,GAAG,EACH,OAAO,EACP,kBAAkB,EAClB,OAAO,EACP,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,OAAO,EACP,KAAK,EACL,gBAAgB,EAEhB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EAEf,UAAU,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,eAAe,EACf,WAAW,EACX,UAAU,GACX,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAG9D,OAAO,EACL,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,aAAa,EACb,UAAU,EACV,aAAa,EACb,aAAa,EACb,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,cAAc,EACd,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EACL,SAAS,EACT,MAAM,EACN,YAAY,EACZ,QAAQ,EACR,SAAS,EACT,SAAS,EACT,eAAe,EACf,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,oBAAoB,GACrB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EACf,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,WAAW,EACX,QAAQ,EACR,UAAU,EACV,WAAW,EACX,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,sBAAsB,EACtB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG1E,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC9I,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAG9D,OAAO,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACjH,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGlF,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,0BAA0B,EAC1B,mBAAmB,EACnB,0BAA0B,EAC1B,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,YAAY,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAG/H,YAAY,EAEV,SAAS,EACT,SAAS,EACT,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,sBAAsB,EACtB,aAAa,EAEb,KAAK,EACL,KAAK,EACL,OAAO,EACP,aAAa,EAEb,GAAG,EACH,OAAO,EACP,kBAAkB,EAClB,OAAO,EACP,kBAAkB,EAClB,mBAAmB,EACnB,WAAW,EACX,OAAO,EACP,KAAK,EACL,gBAAgB,EAEhB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EAEf,UAAU,EACV,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,gBAAgB,EAChB,MAAM,EACN,eAAe,EACf,WAAW,EACX,UAAU,GACX,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -1312,6 +1312,354 @@ function listCompaniesInGroup(db, groupId) {
1312
1312
  function listGroupsForCompany(db, companyId) {
1313
1313
  return db.query(`SELECT g.* FROM groups g JOIN company_groups cog ON g.id = cog.group_id WHERE cog.company_id = ? ORDER BY g.name`).all(companyId);
1314
1314
  }
1315
+ // src/lib/connector.ts
1316
+ import { join as join2 } from "path";
1317
+ import { existsSync as existsSync2, readFileSync } from "fs";
1318
+ import { homedir } from "os";
1319
+
1320
+ class ConnectorNotInstalledError extends Error {
1321
+ connectorName;
1322
+ constructor(connectorName) {
1323
+ super(`connect-${connectorName} is not installed. ` + `Run: bun install -g @hasnaxyz/connect-${connectorName}`);
1324
+ this.connectorName = connectorName;
1325
+ this.name = "ConnectorNotInstalledError";
1326
+ }
1327
+ }
1328
+
1329
+ class ConnectorAuthError extends Error {
1330
+ connectorName;
1331
+ constructor(connectorName, detail) {
1332
+ super(`connect-${connectorName} is not authenticated. ` + `Run: connect-${connectorName} auth login` + (detail ? ` (${detail})` : ""));
1333
+ this.connectorName = connectorName;
1334
+ this.name = "ConnectorAuthError";
1335
+ }
1336
+ }
1337
+ async function runConnector(name, args, opts = {}) {
1338
+ const binary = `connect-${name}`;
1339
+ const profile = opts.profile ?? "default";
1340
+ const fullArgs = [
1341
+ "--format",
1342
+ "json",
1343
+ "--profile",
1344
+ profile,
1345
+ ...args
1346
+ ];
1347
+ let proc;
1348
+ try {
1349
+ proc = Bun.spawn([binary, ...fullArgs], {
1350
+ stdout: "pipe",
1351
+ stderr: "pipe"
1352
+ });
1353
+ } catch (err) {
1354
+ const msg = err instanceof Error ? err.message : String(err);
1355
+ if (msg.includes("not found") || msg.includes("ENOENT")) {
1356
+ throw new ConnectorNotInstalledError(name);
1357
+ }
1358
+ throw err;
1359
+ }
1360
+ const timeoutMs = opts.timeout ?? 30000;
1361
+ const timer = setTimeout(() => proc.kill(), timeoutMs);
1362
+ try {
1363
+ const [stdout, stderr, exitCode] = await Promise.all([
1364
+ proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
1365
+ proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
1366
+ proc.exited
1367
+ ]);
1368
+ if (exitCode !== 0) {
1369
+ const errText = (stderr || stdout).trim();
1370
+ const lower = errText.toLowerCase();
1371
+ if (lower.includes("auth") || lower.includes("token") || lower.includes("401") || lower.includes("unauthorized") || lower.includes("not authenticated")) {
1372
+ throw new ConnectorAuthError(name, errText.slice(0, 200));
1373
+ }
1374
+ throw new Error(`connect-${name} exited ${exitCode}: ${errText.slice(0, 400)}`);
1375
+ }
1376
+ const text = stdout.trim();
1377
+ if (!text || text === "null")
1378
+ return null;
1379
+ return JSON.parse(text);
1380
+ } finally {
1381
+ clearTimeout(timer);
1382
+ }
1383
+ }
1384
+ function getConnectorTokenPath(name, profile = "default") {
1385
+ const bases = [
1386
+ join2(homedir(), ".connectors", `connect-${name}`, "profiles", profile, "tokens.json"),
1387
+ join2(homedir(), ".connect", `connect-${name}`, "profiles", profile, "tokens.json"),
1388
+ join2(homedir(), ".connect", `connect-${name}`, "tokens.json")
1389
+ ];
1390
+ for (const p of bases) {
1391
+ if (existsSync2(p))
1392
+ return p;
1393
+ }
1394
+ return null;
1395
+ }
1396
+ function readConnectorTokens(name, profile = "default") {
1397
+ const path = getConnectorTokenPath(name, profile);
1398
+ if (!path)
1399
+ throw new ConnectorAuthError(name);
1400
+ try {
1401
+ return JSON.parse(readFileSync(path, "utf-8"));
1402
+ } catch {
1403
+ throw new ConnectorAuthError(name, "tokens file unreadable");
1404
+ }
1405
+ }
1406
+ // src/lib/gmail-import.ts
1407
+ function parseAddressHeader(header) {
1408
+ const results = [];
1409
+ const parts = header.split(/,(?![^<>]*>)(?![^"]*")/);
1410
+ for (const part of parts) {
1411
+ const trimmed = part.trim();
1412
+ if (!trimmed)
1413
+ continue;
1414
+ const angleMatch = trimmed.match(/^(.*?)\s*<([^>]+)>\s*$/);
1415
+ if (angleMatch) {
1416
+ const name = angleMatch[1].trim().replace(/^"|"$/g, "");
1417
+ const email = angleMatch[2].trim().toLowerCase();
1418
+ if (email.includes("@"))
1419
+ results.push({ name, email });
1420
+ } else if (trimmed.includes("@")) {
1421
+ results.push({ name: "", email: trimmed.toLowerCase() });
1422
+ }
1423
+ }
1424
+ return results;
1425
+ }
1426
+ function domainToCompany(email) {
1427
+ const domain = email.split("@")[1];
1428
+ if (!domain)
1429
+ return null;
1430
+ const genericDomains = new Set([
1431
+ "gmail.com",
1432
+ "googlemail.com",
1433
+ "yahoo.com",
1434
+ "yahoo.co.uk",
1435
+ "hotmail.com",
1436
+ "hotmail.co.uk",
1437
+ "outlook.com",
1438
+ "live.com",
1439
+ "icloud.com",
1440
+ "me.com",
1441
+ "mac.com",
1442
+ "protonmail.com",
1443
+ "pm.me",
1444
+ "fastmail.com",
1445
+ "hey.com",
1446
+ "aol.com",
1447
+ "msn.com"
1448
+ ]);
1449
+ if (genericDomains.has(domain.toLowerCase()))
1450
+ return null;
1451
+ const parts = domain.split(".");
1452
+ const name = parts.length > 2 ? parts[parts.length - 2] : parts[0];
1453
+ return name.charAt(0).toUpperCase() + name.slice(1);
1454
+ }
1455
+ function parseName(displayName) {
1456
+ const name = displayName.trim();
1457
+ if (!name)
1458
+ return {};
1459
+ const parts = name.split(/\s+/);
1460
+ if (parts.length === 1)
1461
+ return { first_name: parts[0] };
1462
+ const last = parts.pop();
1463
+ return { first_name: parts.join(" "), last_name: last };
1464
+ }
1465
+ async function extractContactsFromGmail(opts) {
1466
+ const maxMessages = Math.min(opts.max_messages ?? 200, 500);
1467
+ const profile = opts.gmail_profile ?? "default";
1468
+ let token;
1469
+ try {
1470
+ const tokens = readConnectorTokens("gmail", profile);
1471
+ token = tokens["accessToken"];
1472
+ if (!token)
1473
+ throw new ConnectorAuthError("gmail", "accessToken missing");
1474
+ } catch (err) {
1475
+ if (err instanceof ConnectorAuthError)
1476
+ throw err;
1477
+ throw new ConnectorAuthError("gmail", String(err));
1478
+ }
1479
+ const listUrl = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
1480
+ listUrl.searchParams.set("q", opts.query);
1481
+ listUrl.searchParams.set("maxResults", String(maxMessages));
1482
+ const listResp = await fetch(listUrl.toString(), {
1483
+ headers: { Authorization: `Bearer ${token}` }
1484
+ });
1485
+ if (!listResp.ok) {
1486
+ if (listResp.status === 401) {
1487
+ throw new ConnectorAuthError("gmail", "token expired \u2014 run connect-gmail auth login");
1488
+ }
1489
+ throw new Error(`Gmail API error ${listResp.status}: ${await listResp.text()}`);
1490
+ }
1491
+ const listData = await listResp.json();
1492
+ const messageRefs = listData.messages ?? [];
1493
+ if (messageRefs.length === 0)
1494
+ return [];
1495
+ const seen = new Map;
1496
+ const batchSize = 20;
1497
+ for (let i = 0;i < messageRefs.length; i += batchSize) {
1498
+ const batch = messageRefs.slice(i, i + batchSize);
1499
+ const fetches = batch.map((ref) => {
1500
+ const url = new URL(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${ref.id}`);
1501
+ url.searchParams.set("format", "metadata");
1502
+ url.searchParams.append("metadataHeaders", "From");
1503
+ url.searchParams.append("metadataHeaders", "To");
1504
+ url.searchParams.append("metadataHeaders", "Cc");
1505
+ return fetch(url.toString(), {
1506
+ headers: { Authorization: `Bearer ${token}` }
1507
+ }).then((r) => r.ok ? r.json() : null);
1508
+ });
1509
+ const results = await Promise.all(fetches);
1510
+ for (const msg of results) {
1511
+ if (!msg?.payload?.headers)
1512
+ continue;
1513
+ const getHeader = (name) => msg.payload.headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? "";
1514
+ const addresses = [
1515
+ ...parseAddressHeader(getHeader("From")),
1516
+ ...parseAddressHeader(getHeader("To")),
1517
+ ...parseAddressHeader(getHeader("Cc"))
1518
+ ];
1519
+ for (const { name, email } of addresses) {
1520
+ if (seen.has(email))
1521
+ continue;
1522
+ const company_hint = domainToCompany(email);
1523
+ const nameParts = parseName(name);
1524
+ const contact_input = {
1525
+ ...nameParts,
1526
+ display_name: name || email.split("@")[0],
1527
+ emails: [{ address: email, type: "work", is_primary: true }],
1528
+ ...opts.tag_ids?.length ? { tag_ids: opts.tag_ids } : {},
1529
+ source: "email"
1530
+ };
1531
+ seen.set(email, { email, name, company_hint, contact_input });
1532
+ }
1533
+ }
1534
+ }
1535
+ return Array.from(seen.values());
1536
+ }
1537
+ // src/lib/google-contacts.ts
1538
+ function googlePersonToContactInput(person) {
1539
+ const name = person.names?.[0];
1540
+ const primaryEmail = person.emailAddresses?.[0]?.value;
1541
+ const nameParts = name?.givenName || name?.familyName ? { first_name: name.givenName, last_name: name.familyName } : primaryEmail ? parseName(name?.displayName ?? primaryEmail.split("@")[0] ?? "") : {};
1542
+ const emails = (person.emailAddresses ?? []).filter((e) => e.value).map((e, i) => ({
1543
+ address: e.value.toLowerCase(),
1544
+ type: normalizeEmailType(e.type),
1545
+ is_primary: i === 0
1546
+ }));
1547
+ const phones = (person.phoneNumbers ?? []).filter((p) => p.value).map((p, i) => ({
1548
+ number: p.value,
1549
+ type: normalizePhoneType(p.type),
1550
+ is_primary: i === 0
1551
+ }));
1552
+ const org = person.organizations?.[0];
1553
+ const website = person.urls?.[0]?.value;
1554
+ const birthday = (() => {
1555
+ const b = person.birthdays?.[0]?.date;
1556
+ if (!b?.year || !b?.month || !b?.day)
1557
+ return;
1558
+ const mm = String(b.month).padStart(2, "0");
1559
+ const dd = String(b.day).padStart(2, "0");
1560
+ return `${b.year}-${mm}-${dd}`;
1561
+ })();
1562
+ const notes = person.biographies?.map((b) => b.value).filter(Boolean).join(`
1563
+
1564
+ `) || undefined;
1565
+ return {
1566
+ ...nameParts,
1567
+ display_name: name?.displayName ?? (nameParts.first_name ? `${nameParts.first_name} ${nameParts.last_name ?? ""}`.trim() : primaryEmail?.split("@")[0] ?? "Unknown"),
1568
+ job_title: org?.title,
1569
+ notes,
1570
+ birthday,
1571
+ website,
1572
+ source: "import",
1573
+ emails,
1574
+ phones,
1575
+ custom_fields: { google_resource_name: person.resourceName }
1576
+ };
1577
+ }
1578
+ function normalizeEmailType(type) {
1579
+ if (!type)
1580
+ return "work";
1581
+ const t = type.toLowerCase();
1582
+ if (t === "work" || t === "home" || t === "personal")
1583
+ return t === "home" ? "personal" : "work";
1584
+ return "other";
1585
+ }
1586
+ function normalizePhoneType(type) {
1587
+ if (!type)
1588
+ return "mobile";
1589
+ const t = type.toLowerCase();
1590
+ if (t === "mobile" || t === "cell")
1591
+ return "mobile";
1592
+ if (t === "work")
1593
+ return "work";
1594
+ if (t === "home")
1595
+ return "home";
1596
+ if (t === "fax")
1597
+ return "fax";
1598
+ return "other";
1599
+ }
1600
+ function contactToGoogleArgs(contact) {
1601
+ const args = [];
1602
+ const displayName = contact.display_name;
1603
+ if (displayName)
1604
+ args.push("--name", displayName);
1605
+ if (contact.first_name)
1606
+ args.push("--given-name", contact.first_name);
1607
+ if (contact.last_name)
1608
+ args.push("--family-name", contact.last_name);
1609
+ const primaryEmail = contact.emails.find((e) => e.is_primary) ?? contact.emails[0];
1610
+ if (primaryEmail)
1611
+ args.push("--email", primaryEmail.address);
1612
+ const primaryPhone = contact.phones.find((p) => p.is_primary) ?? contact.phones[0];
1613
+ if (primaryPhone)
1614
+ args.push("--phone", primaryPhone.number);
1615
+ if (contact.job_title)
1616
+ args.push("--title", contact.job_title);
1617
+ if (contact.company)
1618
+ args.push("--company", contact.company.name);
1619
+ if (contact.notes)
1620
+ args.push("--notes", contact.notes);
1621
+ if (contact.website)
1622
+ args.push("--url", contact.website);
1623
+ return args;
1624
+ }
1625
+ async function listGoogleContacts(opts = {}) {
1626
+ const args = ["contacts", "list"];
1627
+ if (opts.page_size)
1628
+ args.push("--page-size", String(opts.page_size));
1629
+ const raw = await runConnector("googlecontacts", args, opts);
1630
+ return normalizeGoogleContactsResponse(raw);
1631
+ }
1632
+ async function searchGoogleContacts(query, opts = {}) {
1633
+ const raw = await runConnector("googlecontacts", ["contacts", "search", query], opts);
1634
+ return normalizeGoogleContactsResponse(raw);
1635
+ }
1636
+ function normalizeGoogleContactsResponse(raw) {
1637
+ if (!raw)
1638
+ return [];
1639
+ if (Array.isArray(raw))
1640
+ return raw;
1641
+ const obj = raw;
1642
+ if (Array.isArray(obj["connections"]))
1643
+ return obj["connections"];
1644
+ if (Array.isArray(obj["contacts"]))
1645
+ return obj["contacts"];
1646
+ return [];
1647
+ }
1648
+ async function pushContactToGoogle(contact, opts = {}) {
1649
+ const googleResourceName = contact.custom_fields?.["google_resource_name"];
1650
+ if (googleResourceName && opts.update_existing) {
1651
+ const args2 = ["contacts", "update", googleResourceName, ...contactToGoogleArgs(contact)];
1652
+ const result2 = await runConnector("googlecontacts", args2, opts);
1653
+ return { resourceName: result2?.resourceName ?? googleResourceName, action: "updated" };
1654
+ }
1655
+ const args = ["contacts", "create", ...contactToGoogleArgs(contact)];
1656
+ const result = await runConnector("googlecontacts", args, opts);
1657
+ return { resourceName: result?.resourceName ?? "", action: "created" };
1658
+ }
1659
+ async function pullGoogleContactsAsInputs(opts = {}) {
1660
+ const people = opts.query ? await searchGoogleContacts(opts.query, opts) : await listGoogleContacts(opts);
1661
+ return people.filter((p) => p.emailAddresses?.some((e) => e.value)).map(googlePersonToContactInput);
1662
+ }
1315
1663
  export {
1316
1664
  updateTag,
1317
1665
  updateGroup,
@@ -1319,13 +1667,20 @@ export {
1319
1667
  updateCompany,
1320
1668
  unarchiveContact,
1321
1669
  unarchiveCompany,
1670
+ searchGoogleContacts,
1322
1671
  searchContacts,
1323
1672
  searchCompanies,
1673
+ runConnector,
1324
1674
  resetDatabase,
1325
1675
  removeTagFromContact,
1326
1676
  removeTagFromCompany,
1327
1677
  removeContactFromGroup,
1328
1678
  removeCompanyFromGroup,
1679
+ readConnectorTokens,
1680
+ pushContactToGoogle,
1681
+ pullGoogleContactsAsInputs,
1682
+ parseName,
1683
+ parseAddressHeader,
1329
1684
  mergeContacts,
1330
1685
  logActivity,
1331
1686
  listTags,
@@ -1334,6 +1689,7 @@ export {
1334
1689
  listGroupsForContact,
1335
1690
  listGroupsForCompany,
1336
1691
  listGroups,
1692
+ listGoogleContacts,
1337
1693
  listContactsInGroup,
1338
1694
  listContactsByTag,
1339
1695
  listContacts,
@@ -1341,6 +1697,7 @@ export {
1341
1697
  listCompaniesInGroup,
1342
1698
  listCompanies,
1343
1699
  listActivity,
1700
+ googlePersonToContactInput,
1344
1701
  getTagByName,
1345
1702
  getTag,
1346
1703
  getRelationship,
@@ -1348,8 +1705,11 @@ export {
1348
1705
  getDatabase,
1349
1706
  getContactByEmail,
1350
1707
  getContact,
1708
+ getConnectorTokenPath,
1351
1709
  getCompany,
1352
1710
  getActivity,
1711
+ extractContactsFromGmail,
1712
+ domainToCompany,
1353
1713
  deleteTag,
1354
1714
  deleteRelationship,
1355
1715
  deleteGroup,
@@ -1360,6 +1720,7 @@ export {
1360
1720
  createGroup,
1361
1721
  createContact,
1362
1722
  createCompany,
1723
+ contactToGoogleArgs,
1363
1724
  autoLinkContactToCompany,
1364
1725
  archiveContact,
1365
1726
  archiveCompany,
@@ -1372,5 +1733,7 @@ export {
1372
1733
  TagNotFoundError,
1373
1734
  DuplicateTagNameError,
1374
1735
  ContactNotFoundError,
1736
+ ConnectorNotInstalledError,
1737
+ ConnectorAuthError,
1375
1738
  CompanyNotFoundError
1376
1739
  };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Generic connector runner — the scalable foundation for all external service
3
+ * integrations. Any connector (Gmail, Google Contacts, HubSpot, Salesforce…)
4
+ * uses this same interface; only the `name` and `args` differ.
5
+ *
6
+ * Prerequisites: the target connector must be globally installed and authed:
7
+ * bun install -g @hasnaxyz/connect-{name}
8
+ * connect-{name} auth login
9
+ */
10
+ export interface ConnectorRunOptions {
11
+ /** Connector profile to use (default: "default") */
12
+ profile?: string;
13
+ /** Timeout in ms (default: 30_000) */
14
+ timeout?: number;
15
+ }
16
+ export declare class ConnectorNotInstalledError extends Error {
17
+ readonly connectorName: string;
18
+ constructor(connectorName: string);
19
+ }
20
+ export declare class ConnectorAuthError extends Error {
21
+ readonly connectorName: string;
22
+ constructor(connectorName: string, detail?: string);
23
+ }
24
+ /**
25
+ * Execute any connector operation and return the parsed JSON output.
26
+ *
27
+ * Usage:
28
+ * const messages = await runConnector("gmail", ["messages", "list", "-q", "from:acme.com"]);
29
+ * const contacts = await runConnector("googlecontacts", ["contacts", "list"]);
30
+ */
31
+ export declare function runConnector(name: string, args: string[], opts?: ConnectorRunOptions): Promise<unknown>;
32
+ /**
33
+ * Returns the path to a connector's token file.
34
+ * Useful for implementations that need direct API access with refreshed tokens.
35
+ * Checks canonical path first, then legacy fallback.
36
+ */
37
+ export declare function getConnectorTokenPath(name: string, profile?: string): string | null;
38
+ /**
39
+ * Read and parse connector tokens from disk.
40
+ * Useful when a library needs a raw access token (e.g. for batched REST calls
41
+ * that would be too slow to do one-at-a-time through the CLI).
42
+ */
43
+ export declare function readConnectorTokens(name: string, profile?: string): Record<string, unknown>;
44
+ //# sourceMappingURL=connector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connector.d.ts","sourceRoot":"","sources":["../../src/lib/connector.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,0BAA2B,SAAQ,KAAK;aACvB,aAAa,EAAE,MAAM;gBAArB,aAAa,EAAE,MAAM;CAOlD;AAED,qBAAa,kBAAmB,SAAQ,KAAK;aACf,aAAa,EAAE,MAAM;gBAArB,aAAa,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAQnE;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAyDlB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAY,GAAG,MAAM,GAAG,IAAI,CAUtF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAQ9F"}
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Gmail contact import — fetches messages matching a query, extracts unique
3
- * senders/recipients, and returns them as CreateContactInput objects.
2
+ * Gmail contact import — fetches messages matching a query via the connect-gmail
3
+ * connector, extracts unique senders/recipients, and returns them as
4
+ * CreateContactInput objects ready to upsert into the local DB.
4
5
  *
5
- * Uses the Gmail REST API directly with tokens stored at
6
- * ~/.connectors/connect-gmail/profiles/default/tokens.json
7
- * (requires connect-gmail auth login to be completed first).
6
+ * Auth: connect-gmail auth login (run once per profile)
7
+ * Install: bun install -g @hasnaxyz/connect-gmail
8
8
  */
9
9
  import type { CreateContactInput } from "../types/index.js";
10
10
  /**
@@ -15,6 +15,18 @@ export declare function parseAddressHeader(header: string): Array<{
15
15
  name: string;
16
16
  email: string;
17
17
  }>;
18
+ /**
19
+ * Infer a company name from an email domain.
20
+ * Returns null for personal/generic domains (gmail, yahoo, hotmail, etc.).
21
+ */
22
+ export declare function domainToCompany(email: string): string | null;
23
+ /**
24
+ * Parse display name into first/last name.
25
+ */
26
+ export declare function parseName(displayName: string): {
27
+ first_name?: string;
28
+ last_name?: string;
29
+ };
18
30
  export interface GmailImportOptions {
19
31
  query: string;
20
32
  /** Max messages to scan (default 200, max 500) */
@@ -32,6 +44,7 @@ export interface ExtractedContact {
32
44
  }
33
45
  /**
34
46
  * Fetch messages matching `query` from Gmail and extract unique contacts.
47
+ * Uses connect-gmail's stored tokens for auth — no API keys needed in code.
35
48
  * Does NOT write to the database — returns prepared CreateContactInput objects.
36
49
  */
37
50
  export declare function extractContactsFromGmail(opts: GmailImportOptions): Promise<ExtractedContact[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"gmail-import.d.ts","sourceRoot":"","sources":["../../src/lib/gmail-import.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAoD5D;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBzF;AAqCD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,kBAAkB,CAAC;CACnC;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAoF7B"}
1
+ {"version":3,"file":"gmail-import.d.ts","sourceRoot":"","sources":["../../src/lib/gmail-import.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAK5D;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAgB5D;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAO1F;AAID,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,kBAAkB,CAAC;CACnC;AAQD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAsF7B"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Google Contacts sync — bidirectional sync between local DB and Google Contacts
3
+ * via the connect-googlecontacts connector.
4
+ *
5
+ * Auth: connect-googlecontacts auth login (run once per profile)
6
+ * Install: bun install -g @hasnaxyz/connect-googlecontacts
7
+ *
8
+ * Data format: Google People API
9
+ * https://developers.google.com/people/api/rest/v1/people
10
+ */
11
+ import type { ContactWithDetails, CreateContactInput } from "../types/index.js";
12
+ import { ConnectorRunOptions } from "./connector.js";
13
+ interface PeopleName {
14
+ displayName?: string;
15
+ givenName?: string;
16
+ familyName?: string;
17
+ middleName?: string;
18
+ }
19
+ interface PeopleEmail {
20
+ value?: string;
21
+ type?: string;
22
+ formattedType?: string;
23
+ }
24
+ interface PeoplePhone {
25
+ value?: string;
26
+ type?: string;
27
+ formattedType?: string;
28
+ }
29
+ interface PeopleOrg {
30
+ name?: string;
31
+ title?: string;
32
+ department?: string;
33
+ }
34
+ interface PeopleAddress {
35
+ streetAddress?: string;
36
+ city?: string;
37
+ region?: string;
38
+ postalCode?: string;
39
+ country?: string;
40
+ type?: string;
41
+ }
42
+ interface PeopleBio {
43
+ value?: string;
44
+ }
45
+ interface PeopleUrl {
46
+ value?: string;
47
+ type?: string;
48
+ }
49
+ export interface GooglePerson {
50
+ resourceName: string;
51
+ etag?: string;
52
+ names?: PeopleName[];
53
+ emailAddresses?: PeopleEmail[];
54
+ phoneNumbers?: PeoplePhone[];
55
+ organizations?: PeopleOrg[];
56
+ addresses?: PeopleAddress[];
57
+ biographies?: PeopleBio[];
58
+ urls?: PeopleUrl[];
59
+ birthdays?: Array<{
60
+ date?: {
61
+ year?: number;
62
+ month?: number;
63
+ day?: number;
64
+ };
65
+ }>;
66
+ }
67
+ export interface GoogleContactsSyncOptions extends ConnectorRunOptions {
68
+ /** Max contacts to pull per page (connector handles pagination automatically) */
69
+ page_size?: number;
70
+ /** Only import contacts matching this query */
71
+ query?: string;
72
+ }
73
+ export interface GoogleContactsPushOptions extends ConnectorRunOptions {
74
+ /** If true, update existing Google contact if resourceName is stored in custom_fields */
75
+ update_existing?: boolean;
76
+ }
77
+ export interface SyncResult {
78
+ imported: number;
79
+ updated: number;
80
+ skipped: number;
81
+ errors: string[];
82
+ }
83
+ /**
84
+ * Convert a Google People API person to a CreateContactInput.
85
+ */
86
+ export declare function googlePersonToContactInput(person: GooglePerson): CreateContactInput;
87
+ /**
88
+ * Convert a local ContactWithDetails to Google People API create/update args.
89
+ * Returns the CLI flags for `connect-googlecontacts contacts create`.
90
+ */
91
+ export declare function contactToGoogleArgs(contact: ContactWithDetails): string[];
92
+ /**
93
+ * List all contacts from Google Contacts.
94
+ */
95
+ export declare function listGoogleContacts(opts?: GoogleContactsSyncOptions): Promise<GooglePerson[]>;
96
+ /**
97
+ * Search contacts in Google Contacts.
98
+ */
99
+ export declare function searchGoogleContacts(query: string, opts?: ConnectorRunOptions): Promise<GooglePerson[]>;
100
+ /**
101
+ * Create a new contact in Google Contacts from a local contact.
102
+ * Returns the Google resource name (e.g. "people/c12345").
103
+ */
104
+ export declare function pushContactToGoogle(contact: ContactWithDetails, opts?: GoogleContactsPushOptions): Promise<{
105
+ resourceName: string;
106
+ action: "created" | "updated";
107
+ }>;
108
+ /**
109
+ * Sync all Google Contacts into the local database.
110
+ * Caller is responsible for the actual upsert (pass a db upsert function).
111
+ * Returns structured CreateContactInput objects ready for upsert.
112
+ */
113
+ export declare function pullGoogleContactsAsInputs(opts?: GoogleContactsSyncOptions): Promise<CreateContactInput[]>;
114
+ export {};
115
+ //# sourceMappingURL=google-contacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-contacts.d.ts","sourceRoot":"","sources":["../../src/lib/google-contacts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=google-contacts.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-contacts.test.d.ts","sourceRoot":"","sources":["../../src/lib/google-contacts.test.ts"],"names":[],"mappings":""}
package/dist/mcp/index.js CHANGED
@@ -1773,28 +1773,99 @@ async function exportContacts(format, contacts) {
1773
1773
  }
1774
1774
  }
1775
1775
 
1776
- // src/lib/gmail-import.ts
1776
+ // src/lib/connector.ts
1777
+ import { join as join2 } from "path";
1777
1778
  import { existsSync as existsSync2, readFileSync } from "fs";
1778
1779
  import { homedir } from "os";
1779
- import { join as join2 } from "path";
1780
- function loadGmailToken(profile = "default") {
1781
- const bases = [
1782
- join2(homedir(), ".connectors", "connect-gmail"),
1783
- join2(homedir(), ".connect", "connect-gmail")
1780
+
1781
+ class ConnectorNotInstalledError extends Error {
1782
+ connectorName;
1783
+ constructor(connectorName) {
1784
+ super(`connect-${connectorName} is not installed. ` + `Run: bun install -g @hasnaxyz/connect-${connectorName}`);
1785
+ this.connectorName = connectorName;
1786
+ this.name = "ConnectorNotInstalledError";
1787
+ }
1788
+ }
1789
+
1790
+ class ConnectorAuthError extends Error {
1791
+ connectorName;
1792
+ constructor(connectorName, detail) {
1793
+ super(`connect-${connectorName} is not authenticated. ` + `Run: connect-${connectorName} auth login` + (detail ? ` (${detail})` : ""));
1794
+ this.connectorName = connectorName;
1795
+ this.name = "ConnectorAuthError";
1796
+ }
1797
+ }
1798
+ async function runConnector(name, args, opts = {}) {
1799
+ const binary = `connect-${name}`;
1800
+ const profile = opts.profile ?? "default";
1801
+ const fullArgs = [
1802
+ "--format",
1803
+ "json",
1804
+ "--profile",
1805
+ profile,
1806
+ ...args
1784
1807
  ];
1785
- for (const base of bases) {
1786
- const tokenPath = join2(base, "profiles", profile, "tokens.json");
1787
- if (existsSync2(tokenPath)) {
1788
- try {
1789
- const raw = readFileSync(tokenPath, "utf-8");
1790
- const tokens = JSON.parse(raw);
1791
- if (tokens.accessToken)
1792
- return tokens.accessToken;
1793
- } catch {}
1808
+ let proc;
1809
+ try {
1810
+ proc = Bun.spawn([binary, ...fullArgs], {
1811
+ stdout: "pipe",
1812
+ stderr: "pipe"
1813
+ });
1814
+ } catch (err) {
1815
+ const msg = err instanceof Error ? err.message : String(err);
1816
+ if (msg.includes("not found") || msg.includes("ENOENT")) {
1817
+ throw new ConnectorNotInstalledError(name);
1794
1818
  }
1819
+ throw err;
1820
+ }
1821
+ const timeoutMs = opts.timeout ?? 30000;
1822
+ const timer = setTimeout(() => proc.kill(), timeoutMs);
1823
+ try {
1824
+ const [stdout, stderr, exitCode] = await Promise.all([
1825
+ proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
1826
+ proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
1827
+ proc.exited
1828
+ ]);
1829
+ if (exitCode !== 0) {
1830
+ const errText = (stderr || stdout).trim();
1831
+ const lower = errText.toLowerCase();
1832
+ if (lower.includes("auth") || lower.includes("token") || lower.includes("401") || lower.includes("unauthorized") || lower.includes("not authenticated")) {
1833
+ throw new ConnectorAuthError(name, errText.slice(0, 200));
1834
+ }
1835
+ throw new Error(`connect-${name} exited ${exitCode}: ${errText.slice(0, 400)}`);
1836
+ }
1837
+ const text = stdout.trim();
1838
+ if (!text || text === "null")
1839
+ return null;
1840
+ return JSON.parse(text);
1841
+ } finally {
1842
+ clearTimeout(timer);
1795
1843
  }
1796
- throw new Error("Gmail not authenticated. Run `connect-gmail auth login` first.");
1797
1844
  }
1845
+ function getConnectorTokenPath(name, profile = "default") {
1846
+ const bases = [
1847
+ join2(homedir(), ".connectors", `connect-${name}`, "profiles", profile, "tokens.json"),
1848
+ join2(homedir(), ".connect", `connect-${name}`, "profiles", profile, "tokens.json"),
1849
+ join2(homedir(), ".connect", `connect-${name}`, "tokens.json")
1850
+ ];
1851
+ for (const p of bases) {
1852
+ if (existsSync2(p))
1853
+ return p;
1854
+ }
1855
+ return null;
1856
+ }
1857
+ function readConnectorTokens(name, profile = "default") {
1858
+ const path = getConnectorTokenPath(name, profile);
1859
+ if (!path)
1860
+ throw new ConnectorAuthError(name);
1861
+ try {
1862
+ return JSON.parse(readFileSync(path, "utf-8"));
1863
+ } catch {
1864
+ throw new ConnectorAuthError(name, "tokens file unreadable");
1865
+ }
1866
+ }
1867
+
1868
+ // src/lib/gmail-import.ts
1798
1869
  function parseAddressHeader(header) {
1799
1870
  const results = [];
1800
1871
  const parts = header.split(/,(?![^<>]*>)(?![^"]*")/);
@@ -1856,7 +1927,17 @@ function parseName(displayName) {
1856
1927
  async function extractContactsFromGmail(opts) {
1857
1928
  const maxMessages = Math.min(opts.max_messages ?? 200, 500);
1858
1929
  const profile = opts.gmail_profile ?? "default";
1859
- const token = loadGmailToken(profile);
1930
+ let token;
1931
+ try {
1932
+ const tokens = readConnectorTokens("gmail", profile);
1933
+ token = tokens["accessToken"];
1934
+ if (!token)
1935
+ throw new ConnectorAuthError("gmail", "accessToken missing");
1936
+ } catch (err) {
1937
+ if (err instanceof ConnectorAuthError)
1938
+ throw err;
1939
+ throw new ConnectorAuthError("gmail", String(err));
1940
+ }
1860
1941
  const listUrl = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
1861
1942
  listUrl.searchParams.set("q", opts.query);
1862
1943
  listUrl.searchParams.set("maxResults", String(maxMessages));
@@ -1864,17 +1945,15 @@ async function extractContactsFromGmail(opts) {
1864
1945
  headers: { Authorization: `Bearer ${token}` }
1865
1946
  });
1866
1947
  if (!listResp.ok) {
1867
- const body = await listResp.text();
1868
1948
  if (listResp.status === 401) {
1869
- throw new Error("Gmail token expired. Run `connect-gmail auth login` to re-authenticate.");
1949
+ throw new ConnectorAuthError("gmail", "token expired \u2014 run connect-gmail auth login");
1870
1950
  }
1871
- throw new Error(`Gmail API error ${listResp.status}: ${body}`);
1951
+ throw new Error(`Gmail API error ${listResp.status}: ${await listResp.text()}`);
1872
1952
  }
1873
1953
  const listData = await listResp.json();
1874
1954
  const messageRefs = listData.messages ?? [];
1875
- if (messageRefs.length === 0) {
1955
+ if (messageRefs.length === 0)
1876
1956
  return [];
1877
- }
1878
1957
  const seen = new Map;
1879
1958
  const batchSize = 20;
1880
1959
  for (let i = 0;i < messageRefs.length; i += batchSize) {
@@ -1882,9 +1961,9 @@ async function extractContactsFromGmail(opts) {
1882
1961
  const fetches = batch.map((ref) => {
1883
1962
  const url = new URL(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${ref.id}`);
1884
1963
  url.searchParams.set("format", "metadata");
1885
- url.searchParams.set("metadataHeaders", "From");
1886
- url.searchParams.set("metadataHeaders", "To");
1887
- url.searchParams.set("metadataHeaders", "Cc");
1964
+ url.searchParams.append("metadataHeaders", "From");
1965
+ url.searchParams.append("metadataHeaders", "To");
1966
+ url.searchParams.append("metadataHeaders", "Cc");
1888
1967
  return fetch(url.toString(), {
1889
1968
  headers: { Authorization: `Bearer ${token}` }
1890
1969
  }).then((r) => r.ok ? r.json() : null);
@@ -1893,8 +1972,7 @@ async function extractContactsFromGmail(opts) {
1893
1972
  for (const msg of results) {
1894
1973
  if (!msg?.payload?.headers)
1895
1974
  continue;
1896
- const headers = msg.payload.headers;
1897
- const getHeader = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? "";
1975
+ const getHeader = (name) => msg.payload.headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? "";
1898
1976
  const addresses = [
1899
1977
  ...parseAddressHeader(getHeader("From")),
1900
1978
  ...parseAddressHeader(getHeader("To")),
@@ -1919,6 +1997,133 @@ async function extractContactsFromGmail(opts) {
1919
1997
  return Array.from(seen.values());
1920
1998
  }
1921
1999
 
2000
+ // src/lib/google-contacts.ts
2001
+ function googlePersonToContactInput(person) {
2002
+ const name = person.names?.[0];
2003
+ const primaryEmail = person.emailAddresses?.[0]?.value;
2004
+ const nameParts = name?.givenName || name?.familyName ? { first_name: name.givenName, last_name: name.familyName } : primaryEmail ? parseName(name?.displayName ?? primaryEmail.split("@")[0] ?? "") : {};
2005
+ const emails = (person.emailAddresses ?? []).filter((e) => e.value).map((e, i) => ({
2006
+ address: e.value.toLowerCase(),
2007
+ type: normalizeEmailType(e.type),
2008
+ is_primary: i === 0
2009
+ }));
2010
+ const phones = (person.phoneNumbers ?? []).filter((p) => p.value).map((p, i) => ({
2011
+ number: p.value,
2012
+ type: normalizePhoneType(p.type),
2013
+ is_primary: i === 0
2014
+ }));
2015
+ const org = person.organizations?.[0];
2016
+ const website = person.urls?.[0]?.value;
2017
+ const birthday = (() => {
2018
+ const b = person.birthdays?.[0]?.date;
2019
+ if (!b?.year || !b?.month || !b?.day)
2020
+ return;
2021
+ const mm = String(b.month).padStart(2, "0");
2022
+ const dd = String(b.day).padStart(2, "0");
2023
+ return `${b.year}-${mm}-${dd}`;
2024
+ })();
2025
+ const notes = person.biographies?.map((b) => b.value).filter(Boolean).join(`
2026
+
2027
+ `) || undefined;
2028
+ return {
2029
+ ...nameParts,
2030
+ display_name: name?.displayName ?? (nameParts.first_name ? `${nameParts.first_name} ${nameParts.last_name ?? ""}`.trim() : primaryEmail?.split("@")[0] ?? "Unknown"),
2031
+ job_title: org?.title,
2032
+ notes,
2033
+ birthday,
2034
+ website,
2035
+ source: "import",
2036
+ emails,
2037
+ phones,
2038
+ custom_fields: { google_resource_name: person.resourceName }
2039
+ };
2040
+ }
2041
+ function normalizeEmailType(type) {
2042
+ if (!type)
2043
+ return "work";
2044
+ const t = type.toLowerCase();
2045
+ if (t === "work" || t === "home" || t === "personal")
2046
+ return t === "home" ? "personal" : "work";
2047
+ return "other";
2048
+ }
2049
+ function normalizePhoneType(type) {
2050
+ if (!type)
2051
+ return "mobile";
2052
+ const t = type.toLowerCase();
2053
+ if (t === "mobile" || t === "cell")
2054
+ return "mobile";
2055
+ if (t === "work")
2056
+ return "work";
2057
+ if (t === "home")
2058
+ return "home";
2059
+ if (t === "fax")
2060
+ return "fax";
2061
+ return "other";
2062
+ }
2063
+ function contactToGoogleArgs(contact) {
2064
+ const args = [];
2065
+ const displayName = contact.display_name;
2066
+ if (displayName)
2067
+ args.push("--name", displayName);
2068
+ if (contact.first_name)
2069
+ args.push("--given-name", contact.first_name);
2070
+ if (contact.last_name)
2071
+ args.push("--family-name", contact.last_name);
2072
+ const primaryEmail = contact.emails.find((e) => e.is_primary) ?? contact.emails[0];
2073
+ if (primaryEmail)
2074
+ args.push("--email", primaryEmail.address);
2075
+ const primaryPhone = contact.phones.find((p) => p.is_primary) ?? contact.phones[0];
2076
+ if (primaryPhone)
2077
+ args.push("--phone", primaryPhone.number);
2078
+ if (contact.job_title)
2079
+ args.push("--title", contact.job_title);
2080
+ if (contact.company)
2081
+ args.push("--company", contact.company.name);
2082
+ if (contact.notes)
2083
+ args.push("--notes", contact.notes);
2084
+ if (contact.website)
2085
+ args.push("--url", contact.website);
2086
+ return args;
2087
+ }
2088
+ async function listGoogleContacts(opts = {}) {
2089
+ const args = ["contacts", "list"];
2090
+ if (opts.page_size)
2091
+ args.push("--page-size", String(opts.page_size));
2092
+ const raw = await runConnector("googlecontacts", args, opts);
2093
+ return normalizeGoogleContactsResponse(raw);
2094
+ }
2095
+ async function searchGoogleContacts(query, opts = {}) {
2096
+ const raw = await runConnector("googlecontacts", ["contacts", "search", query], opts);
2097
+ return normalizeGoogleContactsResponse(raw);
2098
+ }
2099
+ function normalizeGoogleContactsResponse(raw) {
2100
+ if (!raw)
2101
+ return [];
2102
+ if (Array.isArray(raw))
2103
+ return raw;
2104
+ const obj = raw;
2105
+ if (Array.isArray(obj["connections"]))
2106
+ return obj["connections"];
2107
+ if (Array.isArray(obj["contacts"]))
2108
+ return obj["contacts"];
2109
+ return [];
2110
+ }
2111
+ async function pushContactToGoogle(contact, opts = {}) {
2112
+ const googleResourceName = contact.custom_fields?.["google_resource_name"];
2113
+ if (googleResourceName && opts.update_existing) {
2114
+ const args2 = ["contacts", "update", googleResourceName, ...contactToGoogleArgs(contact)];
2115
+ const result2 = await runConnector("googlecontacts", args2, opts);
2116
+ return { resourceName: result2?.resourceName ?? googleResourceName, action: "updated" };
2117
+ }
2118
+ const args = ["contacts", "create", ...contactToGoogleArgs(contact)];
2119
+ const result = await runConnector("googlecontacts", args, opts);
2120
+ return { resourceName: result?.resourceName ?? "", action: "created" };
2121
+ }
2122
+ async function pullGoogleContactsAsInputs(opts = {}) {
2123
+ const people = opts.query ? await searchGoogleContacts(opts.query, opts) : await listGoogleContacts(opts);
2124
+ return people.filter((p) => p.emailAddresses?.some((e) => e.value)).map(googlePersonToContactInput);
2125
+ }
2126
+
1922
2127
  // src/mcp/index.ts
1923
2128
  var server = new Server({ name: "contacts", version: "0.1.0" }, { capabilities: { tools: {} } });
1924
2129
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -2733,6 +2938,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2733
2938
  },
2734
2939
  required: ["query"]
2735
2940
  }
2941
+ },
2942
+ {
2943
+ name: "sync_from_google_contacts",
2944
+ description: "Pull contacts from Google Contacts (People API) and upsert them into the local database. Skips contacts that already exist by email. Requires connect-googlecontacts auth login. Returns { imported, skipped, errors }.",
2945
+ inputSchema: {
2946
+ type: "object",
2947
+ properties: {
2948
+ query: { type: "string", description: "Optional search query to filter which Google Contacts to import" },
2949
+ page_size: { type: "number", description: "Contacts per page (default: connector default)" },
2950
+ google_profile: { type: "string", description: "connect-googlecontacts profile (default: 'default')" },
2951
+ tag_ids: { type: "array", items: { type: "string" }, description: "Tag IDs to apply to all imported contacts" },
2952
+ project_id: { type: "string", description: "Project ID to assign to all imported contacts" },
2953
+ dry_run: { type: "boolean", description: "Preview what would be imported without saving" }
2954
+ }
2955
+ }
2956
+ },
2957
+ {
2958
+ name: "push_contact_to_google",
2959
+ description: "Push a local contact to Google Contacts \u2014 creates a new Google contact (or updates if google_resource_name is stored in custom_fields). Requires connect-googlecontacts auth login.",
2960
+ inputSchema: {
2961
+ type: "object",
2962
+ properties: {
2963
+ contact_id: { type: "string", description: "Local contact ID to push" },
2964
+ google_profile: { type: "string", description: "connect-googlecontacts profile (default: 'default')" },
2965
+ update_existing: { type: "boolean", description: "If true, update existing Google contact when google_resource_name is present in custom_fields" }
2966
+ },
2967
+ required: ["contact_id"]
2968
+ }
2969
+ },
2970
+ {
2971
+ name: "search_google_contacts",
2972
+ description: "Search Google Contacts by name or email (read-only, does not import). Returns raw Google People API objects. Useful for lookup or preview before a push/sync.",
2973
+ inputSchema: {
2974
+ type: "object",
2975
+ properties: {
2976
+ query: { type: "string" },
2977
+ google_profile: { type: "string", description: "connect-googlecontacts profile (default: 'default')" }
2978
+ },
2979
+ required: ["query"]
2980
+ }
2736
2981
  }
2737
2982
  ]
2738
2983
  }));
@@ -3363,14 +3608,79 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3363
3608
  }]
3364
3609
  };
3365
3610
  }
3611
+ case "sync_from_google_contacts": {
3612
+ const db = getDatabase();
3613
+ const googleProfile = a.google_profile;
3614
+ const inputs = await pullGoogleContactsAsInputs({
3615
+ query: a.query,
3616
+ page_size: a.page_size,
3617
+ profile: googleProfile ?? "default"
3618
+ });
3619
+ const enriched = inputs.map((inp) => ({
3620
+ ...inp,
3621
+ ...a.tag_ids ? { tag_ids: a.tag_ids } : {},
3622
+ ...a.project_id ? { project_id: a.project_id } : {}
3623
+ }));
3624
+ if (a.dry_run) {
3625
+ return {
3626
+ content: [{
3627
+ type: "text",
3628
+ text: JSON.stringify({ dry_run: true, would_import: enriched.length, contacts: enriched }, null, 2)
3629
+ }]
3630
+ };
3631
+ }
3632
+ let imported = 0;
3633
+ let skipped = 0;
3634
+ const syncErrors = [];
3635
+ for (const input of enriched) {
3636
+ const primaryEmail = input.emails?.[0]?.address;
3637
+ if (!primaryEmail) {
3638
+ skipped++;
3639
+ continue;
3640
+ }
3641
+ try {
3642
+ const existing = db.prepare(`SELECT contact_id FROM emails WHERE LOWER(address) = LOWER(?) AND contact_id IS NOT NULL LIMIT 1`).get(primaryEmail);
3643
+ if (existing) {
3644
+ skipped++;
3645
+ continue;
3646
+ }
3647
+ createContact(input);
3648
+ imported++;
3649
+ } catch (err) {
3650
+ syncErrors.push(`${primaryEmail}: ${err instanceof Error ? err.message : String(err)}`);
3651
+ }
3652
+ }
3653
+ return {
3654
+ content: [{
3655
+ type: "text",
3656
+ text: JSON.stringify({ imported, skipped, errors: syncErrors.length, error_details: syncErrors }, null, 2)
3657
+ }]
3658
+ };
3659
+ }
3660
+ case "push_contact_to_google": {
3661
+ const contact = getContact(a.contact_id);
3662
+ const result = await pushContactToGoogle(contact, {
3663
+ profile: a.google_profile ?? "default",
3664
+ update_existing: a.update_existing
3665
+ });
3666
+ return { content: [{ type: "text", text: JSON.stringify({ ...result, contact_id: contact.id }, null, 2) }] };
3667
+ }
3668
+ case "search_google_contacts": {
3669
+ const people = await searchGoogleContacts(a.query, {
3670
+ profile: a.google_profile ?? "default"
3671
+ });
3672
+ const mapped = people.map((p) => ({
3673
+ google: p,
3674
+ as_contact_input: googlePersonToContactInput(p)
3675
+ }));
3676
+ return { content: [{ type: "text", text: JSON.stringify(mapped, null, 2) }] };
3677
+ }
3366
3678
  default:
3367
3679
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
3368
3680
  }
3369
3681
  } catch (err) {
3370
- return {
3371
- content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3372
- isError: true
3373
- };
3682
+ const msg = err instanceof ConnectorNotInstalledError || err instanceof ConnectorAuthError ? err.message : `Error: ${err instanceof Error ? err.message : String(err)}`;
3683
+ return { content: [{ type: "text", text: msg }], isError: true };
3374
3684
  }
3375
3685
  });
3376
3686
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/contacts",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Contact management for AI coding agents — CLI + MCP + Web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",