@hasna/contacts 0.2.7 → 0.2.8

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.
@@ -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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"}
package/dist/mcp/index.js CHANGED
@@ -1773,28 +1773,42 @@ 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") {
1780
+ class ConnectorAuthError extends Error {
1781
+ connectorName;
1782
+ constructor(connectorName, detail) {
1783
+ super(`connect-${connectorName} is not authenticated. ` + `Run: connect-${connectorName} auth login` + (detail ? ` (${detail})` : ""));
1784
+ this.connectorName = connectorName;
1785
+ this.name = "ConnectorAuthError";
1786
+ }
1787
+ }
1788
+ function getConnectorTokenPath(name, profile = "default") {
1781
1789
  const bases = [
1782
- join2(homedir(), ".connectors", "connect-gmail"),
1783
- join2(homedir(), ".connect", "connect-gmail")
1790
+ join2(homedir(), ".connectors", `connect-${name}`, "profiles", profile, "tokens.json"),
1791
+ join2(homedir(), ".connect", `connect-${name}`, "profiles", profile, "tokens.json"),
1792
+ join2(homedir(), ".connect", `connect-${name}`, "tokens.json")
1784
1793
  ];
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 {}
1794
- }
1794
+ for (const p of bases) {
1795
+ if (existsSync2(p))
1796
+ return p;
1795
1797
  }
1796
- throw new Error("Gmail not authenticated. Run `connect-gmail auth login` first.");
1798
+ return null;
1797
1799
  }
1800
+ function readConnectorTokens(name, profile = "default") {
1801
+ const path = getConnectorTokenPath(name, profile);
1802
+ if (!path)
1803
+ throw new ConnectorAuthError(name);
1804
+ try {
1805
+ return JSON.parse(readFileSync(path, "utf-8"));
1806
+ } catch {
1807
+ throw new ConnectorAuthError(name, "tokens file unreadable");
1808
+ }
1809
+ }
1810
+
1811
+ // src/lib/gmail-import.ts
1798
1812
  function parseAddressHeader(header) {
1799
1813
  const results = [];
1800
1814
  const parts = header.split(/,(?![^<>]*>)(?![^"]*")/);
@@ -1856,7 +1870,17 @@ function parseName(displayName) {
1856
1870
  async function extractContactsFromGmail(opts) {
1857
1871
  const maxMessages = Math.min(opts.max_messages ?? 200, 500);
1858
1872
  const profile = opts.gmail_profile ?? "default";
1859
- const token = loadGmailToken(profile);
1873
+ let token;
1874
+ try {
1875
+ const tokens = readConnectorTokens("gmail", profile);
1876
+ token = tokens["accessToken"];
1877
+ if (!token)
1878
+ throw new ConnectorAuthError("gmail", "accessToken missing");
1879
+ } catch (err) {
1880
+ if (err instanceof ConnectorAuthError)
1881
+ throw err;
1882
+ throw new ConnectorAuthError("gmail", String(err));
1883
+ }
1860
1884
  const listUrl = new URL("https://gmail.googleapis.com/gmail/v1/users/me/messages");
1861
1885
  listUrl.searchParams.set("q", opts.query);
1862
1886
  listUrl.searchParams.set("maxResults", String(maxMessages));
@@ -1864,17 +1888,15 @@ async function extractContactsFromGmail(opts) {
1864
1888
  headers: { Authorization: `Bearer ${token}` }
1865
1889
  });
1866
1890
  if (!listResp.ok) {
1867
- const body = await listResp.text();
1868
1891
  if (listResp.status === 401) {
1869
- throw new Error("Gmail token expired. Run `connect-gmail auth login` to re-authenticate.");
1892
+ throw new ConnectorAuthError("gmail", "token expired \u2014 run connect-gmail auth login");
1870
1893
  }
1871
- throw new Error(`Gmail API error ${listResp.status}: ${body}`);
1894
+ throw new Error(`Gmail API error ${listResp.status}: ${await listResp.text()}`);
1872
1895
  }
1873
1896
  const listData = await listResp.json();
1874
1897
  const messageRefs = listData.messages ?? [];
1875
- if (messageRefs.length === 0) {
1898
+ if (messageRefs.length === 0)
1876
1899
  return [];
1877
- }
1878
1900
  const seen = new Map;
1879
1901
  const batchSize = 20;
1880
1902
  for (let i = 0;i < messageRefs.length; i += batchSize) {
@@ -1882,9 +1904,9 @@ async function extractContactsFromGmail(opts) {
1882
1904
  const fetches = batch.map((ref) => {
1883
1905
  const url = new URL(`https://gmail.googleapis.com/gmail/v1/users/me/messages/${ref.id}`);
1884
1906
  url.searchParams.set("format", "metadata");
1885
- url.searchParams.set("metadataHeaders", "From");
1886
- url.searchParams.set("metadataHeaders", "To");
1887
- url.searchParams.set("metadataHeaders", "Cc");
1907
+ url.searchParams.append("metadataHeaders", "From");
1908
+ url.searchParams.append("metadataHeaders", "To");
1909
+ url.searchParams.append("metadataHeaders", "Cc");
1888
1910
  return fetch(url.toString(), {
1889
1911
  headers: { Authorization: `Bearer ${token}` }
1890
1912
  }).then((r) => r.ok ? r.json() : null);
@@ -1893,8 +1915,7 @@ async function extractContactsFromGmail(opts) {
1893
1915
  for (const msg of results) {
1894
1916
  if (!msg?.payload?.headers)
1895
1917
  continue;
1896
- const headers = msg.payload.headers;
1897
- const getHeader = (name) => headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? "";
1918
+ const getHeader = (name) => msg.payload.headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value ?? "";
1898
1919
  const addresses = [
1899
1920
  ...parseAddressHeader(getHeader("From")),
1900
1921
  ...parseAddressHeader(getHeader("To")),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/contacts",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Contact management for AI coding agents — CLI + MCP + Web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",