@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 +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +363 -0
- package/dist/lib/connector.d.ts +44 -0
- package/dist/lib/connector.d.ts.map +1 -0
- package/dist/lib/gmail-import.d.ts +18 -5
- package/dist/lib/gmail-import.d.ts.map +1 -1
- package/dist/lib/google-contacts.d.ts +115 -0
- package/dist/lib/google-contacts.d.ts.map +1 -0
- package/dist/lib/google-contacts.test.d.ts +2 -0
- package/dist/lib/google-contacts.test.d.ts.map +1 -0
- package/dist/mcp/index.js +341 -31
- package/package.json +1 -1
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
|
3
|
-
* senders/recipients, and returns them as
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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;
|
|
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 @@
|
|
|
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/
|
|
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
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
|
|
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
|
|
1949
|
+
throw new ConnectorAuthError("gmail", "token expired \u2014 run connect-gmail auth login");
|
|
1870
1950
|
}
|
|
1871
|
-
throw new Error(`Gmail API error ${listResp.status}: ${
|
|
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.
|
|
1886
|
-
url.searchParams.
|
|
1887
|
-
url.searchParams.
|
|
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
|
|
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
|
-
|
|
3371
|
-
|
|
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() {
|