@hasna/contacts 0.2.8 → 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/google-contacts.test.d.ts +2 -0
- package/dist/lib/google-contacts.test.d.ts.map +1 -0
- package/dist/mcp/index.js +293 -4
- 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 @@
|
|
|
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
|
@@ -1777,6 +1777,16 @@ async function exportContacts(format, contacts) {
|
|
|
1777
1777
|
import { join as join2 } from "path";
|
|
1778
1778
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1779
1779
|
import { homedir } from "os";
|
|
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
|
+
|
|
1780
1790
|
class ConnectorAuthError extends Error {
|
|
1781
1791
|
connectorName;
|
|
1782
1792
|
constructor(connectorName, detail) {
|
|
@@ -1785,6 +1795,53 @@ class ConnectorAuthError extends Error {
|
|
|
1785
1795
|
this.name = "ConnectorAuthError";
|
|
1786
1796
|
}
|
|
1787
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
|
|
1807
|
+
];
|
|
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);
|
|
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);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1788
1845
|
function getConnectorTokenPath(name, profile = "default") {
|
|
1789
1846
|
const bases = [
|
|
1790
1847
|
join2(homedir(), ".connectors", `connect-${name}`, "profiles", profile, "tokens.json"),
|
|
@@ -1940,6 +1997,133 @@ async function extractContactsFromGmail(opts) {
|
|
|
1940
1997
|
return Array.from(seen.values());
|
|
1941
1998
|
}
|
|
1942
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
|
+
|
|
1943
2127
|
// src/mcp/index.ts
|
|
1944
2128
|
var server = new Server({ name: "contacts", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
1945
2129
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -2754,6 +2938,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
2754
2938
|
},
|
|
2755
2939
|
required: ["query"]
|
|
2756
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
|
+
}
|
|
2757
2981
|
}
|
|
2758
2982
|
]
|
|
2759
2983
|
}));
|
|
@@ -3384,14 +3608,79 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3384
3608
|
}]
|
|
3385
3609
|
};
|
|
3386
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
|
+
}
|
|
3387
3678
|
default:
|
|
3388
3679
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
3389
3680
|
}
|
|
3390
3681
|
} catch (err) {
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
isError: true
|
|
3394
|
-
};
|
|
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 };
|
|
3395
3684
|
}
|
|
3396
3685
|
});
|
|
3397
3686
|
async function main() {
|