@zapier/google-contacts-connector 0.0.0 → 0.1.0
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/LICENSE +93 -0
- package/NOTICE +8 -0
- package/README.md +106 -2
- package/SKILL.md +135 -0
- package/cli.js +71 -0
- package/cli.ts +5 -0
- package/connections.ts +8 -0
- package/dist/cli.js +4 -0
- package/dist/index.js +1134 -0
- package/index.ts +64 -0
- package/package.json +59 -4
- package/preflight.sh +157 -0
- package/references/google-contacts-api-gotchas.md +181 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/copyOtherContact.ts +57 -0
- package/scripts/createContact.ts +140 -0
- package/scripts/createContactGroup.ts +51 -0
- package/scripts/deleteContact.ts +52 -0
- package/scripts/deleteContactGroup.ts +61 -0
- package/scripts/deleteContactPhoto.ts +56 -0
- package/scripts/getContact.ts +57 -0
- package/scripts/getContactGroup.ts +71 -0
- package/scripts/listContactGroups.ts +81 -0
- package/scripts/listContacts.ts +110 -0
- package/scripts/listOtherContacts.ts +80 -0
- package/scripts/modifyContactGroupMembers.ts +82 -0
- package/scripts/searchContacts.ts +72 -0
- package/scripts/searchOtherContacts.ts +71 -0
- package/scripts/updateContact.ts +166 -0
- package/scripts/updateContactGroup.ts +67 -0
- package/scripts/updateContactPhoto.ts +64 -0
- package/tsup.config.ts +63 -0
package/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineConnector, toFunctions } from "@zapier/connectors-sdk";
|
|
2
|
+
|
|
3
|
+
import { connectionResolvers } from "./connections.ts";
|
|
4
|
+
import copyOtherContactDefinition from "./scripts/copyOtherContact.ts";
|
|
5
|
+
import createContactDefinition from "./scripts/createContact.ts";
|
|
6
|
+
import createContactGroupDefinition from "./scripts/createContactGroup.ts";
|
|
7
|
+
import deleteContactDefinition from "./scripts/deleteContact.ts";
|
|
8
|
+
import deleteContactGroupDefinition from "./scripts/deleteContactGroup.ts";
|
|
9
|
+
import deleteContactPhotoDefinition from "./scripts/deleteContactPhoto.ts";
|
|
10
|
+
import getContactDefinition from "./scripts/getContact.ts";
|
|
11
|
+
import getContactGroupDefinition from "./scripts/getContactGroup.ts";
|
|
12
|
+
import listContactGroupsDefinition from "./scripts/listContactGroups.ts";
|
|
13
|
+
import listContactsDefinition from "./scripts/listContacts.ts";
|
|
14
|
+
import listOtherContactsDefinition from "./scripts/listOtherContacts.ts";
|
|
15
|
+
import modifyContactGroupMembersDefinition from "./scripts/modifyContactGroupMembers.ts";
|
|
16
|
+
import searchContactsDefinition from "./scripts/searchContacts.ts";
|
|
17
|
+
import searchOtherContactsDefinition from "./scripts/searchOtherContacts.ts";
|
|
18
|
+
import updateContactDefinition from "./scripts/updateContact.ts";
|
|
19
|
+
import updateContactGroupDefinition from "./scripts/updateContactGroup.ts";
|
|
20
|
+
import updateContactPhotoDefinition from "./scripts/updateContactPhoto.ts";
|
|
21
|
+
|
|
22
|
+
const connector = defineConnector({
|
|
23
|
+
scripts: {
|
|
24
|
+
copyOtherContact: copyOtherContactDefinition,
|
|
25
|
+
createContact: createContactDefinition,
|
|
26
|
+
createContactGroup: createContactGroupDefinition,
|
|
27
|
+
deleteContact: deleteContactDefinition,
|
|
28
|
+
deleteContactGroup: deleteContactGroupDefinition,
|
|
29
|
+
deleteContactPhoto: deleteContactPhotoDefinition,
|
|
30
|
+
getContact: getContactDefinition,
|
|
31
|
+
getContactGroup: getContactGroupDefinition,
|
|
32
|
+
listContactGroups: listContactGroupsDefinition,
|
|
33
|
+
listContacts: listContactsDefinition,
|
|
34
|
+
listOtherContacts: listOtherContactsDefinition,
|
|
35
|
+
modifyContactGroupMembers: modifyContactGroupMembersDefinition,
|
|
36
|
+
searchContacts: searchContactsDefinition,
|
|
37
|
+
searchOtherContacts: searchOtherContactsDefinition,
|
|
38
|
+
updateContact: updateContactDefinition,
|
|
39
|
+
updateContactGroup: updateContactGroupDefinition,
|
|
40
|
+
updateContactPhoto: updateContactPhotoDefinition,
|
|
41
|
+
},
|
|
42
|
+
connectionResolvers,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export default connector;
|
|
46
|
+
export const {
|
|
47
|
+
copyOtherContact,
|
|
48
|
+
createContact,
|
|
49
|
+
createContactGroup,
|
|
50
|
+
deleteContact,
|
|
51
|
+
deleteContactGroup,
|
|
52
|
+
deleteContactPhoto,
|
|
53
|
+
getContact,
|
|
54
|
+
getContactGroup,
|
|
55
|
+
listContactGroups,
|
|
56
|
+
listContacts,
|
|
57
|
+
listOtherContacts,
|
|
58
|
+
modifyContactGroupMembers,
|
|
59
|
+
searchContacts,
|
|
60
|
+
searchOtherContacts,
|
|
61
|
+
updateContact,
|
|
62
|
+
updateContactGroup,
|
|
63
|
+
updateContactPhoto,
|
|
64
|
+
} = toFunctions(connector);
|
package/package.json
CHANGED
|
@@ -1,7 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zapier/google-contacts-connector",
|
|
3
|
-
"
|
|
4
|
-
"description": "Placeholder published to enable OIDC Trusted Publisher setup. The real package is published via GitLab CI.",
|
|
3
|
+
"description": "Agent-callable Google Contacts tools — create, find, update, and delete contacts, manage contact groups (labels) and membership, and read auto-saved other contacts. Use when the user mentions Google Contacts or wants to look up, save, or organize people — including requests that don't name Google Contacts explicitly, e.g. add Jane to my contacts, find Bob's email.",
|
|
5
4
|
"license": "Elastic-2.0",
|
|
6
|
-
"
|
|
7
|
-
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"types": "./index.ts"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"@zapier/google-contacts-connector": "./cli.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist/",
|
|
17
|
+
"cli.js",
|
|
18
|
+
"*.ts",
|
|
19
|
+
"scripts/",
|
|
20
|
+
"preflight.sh",
|
|
21
|
+
"SKILL.md",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"references/",
|
|
25
|
+
"NOTICE"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@zapier/connectors-sdk": "^0.2.0",
|
|
29
|
+
"zod": "^4.0.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@zapier/zapier-sdk": ">=0.59.0 <1.0.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"google-contacts",
|
|
37
|
+
"zapier",
|
|
38
|
+
"connector",
|
|
39
|
+
"tools",
|
|
40
|
+
"skills",
|
|
41
|
+
"mcp",
|
|
42
|
+
"agent",
|
|
43
|
+
"ai",
|
|
44
|
+
"automation"
|
|
45
|
+
],
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/zapier/connectors.git",
|
|
49
|
+
"directory": "apps/google-contacts"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"tsup": "^8.0.0",
|
|
53
|
+
"typescript": "^5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"version": "0.1.0",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "restricted"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsup"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/preflight.sh
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Connector pre-flight readiness check.
|
|
3
|
+
#
|
|
4
|
+
# Managed by @zapier/connectors-dev — do not edit; synced byte-for-byte
|
|
5
|
+
# across every connector.
|
|
6
|
+
#
|
|
7
|
+
# Runs inside whatever agent harness installed the connector (Cursor, Claude
|
|
8
|
+
# Code, Codex, Gemini CLI, Goose, ...) — often a minimal container — and answers
|
|
9
|
+
# ONE question: how do I run the TypeScript scripts here? It picks a runtime —
|
|
10
|
+
# Node 22.18+ when it can already resolve the connector's deps, else an explicit
|
|
11
|
+
# install step (`npm install`, or `bun install` when only Bun is present) — and
|
|
12
|
+
# tells the agent the exact command to run (see EXIT CODES). When deps are
|
|
13
|
+
# missing it disambiguates the two sandbox failures that block an install: a
|
|
14
|
+
# read-only connector dir (must run unsandboxed / be granted write) vs. a
|
|
15
|
+
# blocked home dir (point the package cache inside this dir). Both surface as a
|
|
16
|
+
# misleading `EPERM`, so the recommendation names the actual fix.
|
|
17
|
+
#
|
|
18
|
+
# NEEDS_ACTION is a single self-verifying step (install a runtime / deps), not a
|
|
19
|
+
# loop: do it, then run a script. The action confirms its own success and the
|
|
20
|
+
# first `--help` run is the authoritative check, so re-running this pre-flight to
|
|
21
|
+
# reconfirm is optional, not required.
|
|
22
|
+
#
|
|
23
|
+
# THE AGENT CONTRACT IS THE STDOUT, NOT THIS HEADER. Agents don't read this file;
|
|
24
|
+
# they run it and parse the `PREFLIGHT_*` lines — each value starts with a stable
|
|
25
|
+
# token (parse as `KEY: (\w+)`), with an optional human gloss in parens, and
|
|
26
|
+
# `PREFLIGHT_RECOMMENDATION` is the one-line next step. SKILL.md "Step 0" is the
|
|
27
|
+
# agent-facing spec; this header is for maintainers of the canonical script.
|
|
28
|
+
#
|
|
29
|
+
# WHY POSIX sh (not bash)
|
|
30
|
+
# Minimal sandboxes often ship only BusyBox `sh` with no bash. This script runs
|
|
31
|
+
# unchanged under BusyBox sh, dash, and bash, and never hard-requires
|
|
32
|
+
# node/bun/npm — a missing runtime degrades to a NEEDS_ACTION instruction.
|
|
33
|
+
#
|
|
34
|
+
# EXIT CODES (the verdict; also emitted on PREFLIGHT_STATUS)
|
|
35
|
+
# 0 READY a runtime + deps are in place; run the scripts
|
|
36
|
+
# 1 NEEDS_ACTION perform the printed action (install runtime / deps), then
|
|
37
|
+
# run a script — re-running this check is optional
|
|
38
|
+
|
|
39
|
+
set -u
|
|
40
|
+
|
|
41
|
+
EXIT_READY=0
|
|
42
|
+
EXIT_NEEDS_ACTION=1
|
|
43
|
+
|
|
44
|
+
# Directory this script lives in — deps + scripts are resolved relative to it,
|
|
45
|
+
# not the caller's cwd, so `./preflight.sh` works from anywhere.
|
|
46
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
47
|
+
|
|
48
|
+
has() {
|
|
49
|
+
command -v "$1" >/dev/null 2>&1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Node >= 22.18 is the connector baseline (native .ts stripping). Anything older
|
|
53
|
+
# is treated as "no Node" so we fall back to Bun.
|
|
54
|
+
node_ge_2218() {
|
|
55
|
+
has node || return 1
|
|
56
|
+
v=$(node -v 2>/dev/null) || return 1
|
|
57
|
+
v=${v#v}
|
|
58
|
+
major=${v%%.*}
|
|
59
|
+
rest=${v#*.}
|
|
60
|
+
minor=${rest%%.*}
|
|
61
|
+
case "$major" in '' | *[!0-9]*) return 1 ;; esac
|
|
62
|
+
case "$minor" in '' | *[!0-9]*) minor=0 ;; esac
|
|
63
|
+
[ "$major" -gt 22 ] && return 0
|
|
64
|
+
[ "$major" -eq 22 ] && [ "$minor" -ge 18 ] && return 0
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Are the connector's declared deps installed where Node would find them? Node
|
|
69
|
+
# won't fetch — they must be on disk (this dir's node_modules or an ancestor's).
|
|
70
|
+
# Reads the connector's own package.json, so this stays connector-agnostic. A
|
|
71
|
+
# bare `[ -d node_modules ]` is the wrong test: under pnpm/monorepo layouts the
|
|
72
|
+
# deps can live in an ancestor (or be hoisted), and a local node_modules can
|
|
73
|
+
# exist without the package being present. We check each dep's package.json
|
|
74
|
+
# exists in one of Node's resolution paths rather than `require.resolve(name)`,
|
|
75
|
+
# because resolving the package ENTRY can fail for ESM-only / `exports`-map
|
|
76
|
+
# packages even when they're fully installed and importable.
|
|
77
|
+
node_resolves() {
|
|
78
|
+
( CDPATH= cd -- "$SCRIPT_DIR" && node -e 'const fs=require("fs"),path=require("path");const d=require("./package.json").dependencies||{};for(const k of Object.keys(d)){const ps=require.resolve.paths(k)||[];if(!ps.some(b=>fs.existsSync(path.join(b,k,"package.json"))))process.exit(1);}' ) >/dev/null 2>&1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Can we actually WRITE into this directory right now? Any dep install must land
|
|
82
|
+
# `node_modules/` here, so if the harness mounts the connector read-only (common
|
|
83
|
+
# when skills live under ~/.<agent>/skills, outside the agent's writable
|
|
84
|
+
# workspace) no install can succeed in place — the only fixes are to run it
|
|
85
|
+
# unsandboxed or grant write access. Two deliberate choices:
|
|
86
|
+
# - Probe with a real create+remove, not `[ -w ]`: a sandbox denies the write
|
|
87
|
+
# at the syscall while the permission bits still look writable.
|
|
88
|
+
# - Probe by creating a DIRECTORY (`mkdir`), not a file: that's the install's
|
|
89
|
+
# very first on-disk action (node_modules/ and the cache dirs), and at least
|
|
90
|
+
# one sandbox (Claude Code) permits creating a file here while denying
|
|
91
|
+
# `mkdir` — a file-based probe reports writable and the install then EPERMs.
|
|
92
|
+
dir_writable() {
|
|
93
|
+
_t="$SCRIPT_DIR/.preflight-write-test.$$"
|
|
94
|
+
mkdir "$_t" 2>/dev/null || return 1
|
|
95
|
+
rmdir "$_t" 2>/dev/null
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# ---- 1) Pick a runtime -----------------------------------------------------
|
|
100
|
+
# Node 22.18+ (native TS strip) is the baseline and the preferred runner whenever
|
|
101
|
+
# it's present — it runs the .ts scripts directly. Bun is the fallback runner
|
|
102
|
+
# only when there's no usable Node. We DON'T lean on Bun's implicit auto-install:
|
|
103
|
+
# it's silently suppressed by any ancestor node_modules (monorepo layouts) and
|
|
104
|
+
# fails the same way Node's `npm install` does under a sandbox that blocks Bun's
|
|
105
|
+
# home cache — so missing deps are always an explicit, cache-local install step.
|
|
106
|
+
nresolves=false
|
|
107
|
+
node_ge_2218 && node_resolves && nresolves=true
|
|
108
|
+
|
|
109
|
+
if [ "$nresolves" = true ]; then
|
|
110
|
+
runner=node
|
|
111
|
+
ready=true
|
|
112
|
+
elif node_ge_2218; then
|
|
113
|
+
runner=node # present but deps unresolved — install step below
|
|
114
|
+
ready=false
|
|
115
|
+
elif has bun; then
|
|
116
|
+
runner=bun # no usable Node — Bun runs the scripts; install step below
|
|
117
|
+
ready=false
|
|
118
|
+
else
|
|
119
|
+
echo "PREFLIGHT_STATUS: NEEDS_ACTION"
|
|
120
|
+
echo "PREFLIGHT_RECOMMENDATION: no Node 22.18+ or Bun found — install Node 22.18+ (ships npm) or Bun, then run \`<runner> ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
121
|
+
exit "$EXIT_NEEDS_ACTION"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# ---- 2) Install deps if the chosen runtime can't run as-is -----------------
|
|
125
|
+
# Two distinct sandbox failures to disambiguate, because their fixes differ:
|
|
126
|
+
# a) This directory is READ-ONLY (skills installed outside the agent's
|
|
127
|
+
# writable workspace). No install can place node_modules here — the agent
|
|
128
|
+
# must run it unsandboxed or be granted write access. A cache flag can't
|
|
129
|
+
# help.
|
|
130
|
+
# b) The directory is writable but the HOME dir is sandboxed, so the default
|
|
131
|
+
# ~/.npm / ~/.bun cache write is what EPERMs mid-install. Pointing the cache
|
|
132
|
+
# INSIDE this directory sidesteps that without disabling the sandbox (and is
|
|
133
|
+
# harmless otherwise — just an unused cache dir).
|
|
134
|
+
# So probe writability first and recommend accordingly.
|
|
135
|
+
if [ "$ready" != true ]; then
|
|
136
|
+
echo "PREFLIGHT_STATUS: NEEDS_ACTION"
|
|
137
|
+
[ "$runner" = bun ] && install_cmd="bun install" || install_cmd="npm install"
|
|
138
|
+
if ! dir_writable; then
|
|
139
|
+
echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed and ${SCRIPT_DIR} is read-only in the current sandbox (a test write there failed) — \`${install_cmd}\` can't place node_modules here. Run the install with the sandbox disabled, or grant the agent write access to ${SCRIPT_DIR} (a cache flag won't help — node_modules must land in this directory); then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
140
|
+
elif [ "$runner" = bun ]; then
|
|
141
|
+
echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed — run \`BUN_INSTALL_CACHE_DIR=\"${SCRIPT_DIR}/.bun-cache\" bun install\` in ${SCRIPT_DIR} (the workspace-local cache survives a sandbox that blocks ~/.bun; plain \`bun install\` works otherwise), then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
142
|
+
elif has npm; then
|
|
143
|
+
echo "PREFLIGHT_RECOMMENDATION: dependencies are not installed — run \`npm install --cache \"${SCRIPT_DIR}/.npm-cache\"\` in ${SCRIPT_DIR} (the workspace-local --cache survives a sandbox that blocks ~/.npm; plain \`npm install\` works otherwise), then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
144
|
+
else
|
|
145
|
+
# node >= 22.18 ships npm, so a missing npm means it was removed from the
|
|
146
|
+
# Node install. Restore it, then install with a workspace-local cache.
|
|
147
|
+
echo "PREFLIGHT_RECOMMENDATION: npm is missing (it ships with Node 22.18+) — reinstall/repair Node 22.18+, run \`npm install --cache \"${SCRIPT_DIR}/.npm-cache\"\` in ${SCRIPT_DIR}, then run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials."
|
|
148
|
+
fi
|
|
149
|
+
exit "$EXIT_NEEDS_ACTION"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# ---- 3) Ready --------------------------------------------------------------
|
|
153
|
+
# Runtime + deps are in place — the scripts run.
|
|
154
|
+
echo "PREFLIGHT_STATUS: READY"
|
|
155
|
+
echo "PREFLIGHT_RUNNER: ${runner}"
|
|
156
|
+
echo "PREFLIGHT_RECOMMENDATION: run \`${runner} ${SCRIPT_DIR}/scripts/<name>.ts --help\` to discover arguments and credentials, then run the script with the required env vars set."
|
|
157
|
+
exit "$EXIT_READY"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Google Contacts (People API) — API gotchas
|
|
2
|
+
|
|
3
|
+
Agent-facing reference for non-obvious Google People API behaviors.
|
|
4
|
+
Every claim below is sourced from public Google documentation.
|
|
5
|
+
|
|
6
|
+
## Error envelope
|
|
7
|
+
|
|
8
|
+
The People API returns errors as `{ error: { code, message, status } }` where
|
|
9
|
+
`status` is a [canonical Google status string](https://developers.google.com/people/api/rest/v1/people/updateContact).
|
|
10
|
+
Key mappings an agent should recognize:
|
|
11
|
+
|
|
12
|
+
| HTTP | status string | Meaning / recovery |
|
|
13
|
+
| ---- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| 401 | `UNAUTHENTICATED` | Credentials expired or revoked — reconnect. |
|
|
15
|
+
| 429 | `RESOURCE_EXHAUSTED` | Quota or rate limit hit. The People API tracks per-operation quota costs: a single create or update costs 1 critical read + 1 critical write + 1 daily contact write; batch operations cost more ([quota table](https://developers.google.com/people/v1/contacts)). Back off with exponential jitter. |
|
|
16
|
+
| 403 | `PERMISSION_DENIED` | OAuth scope too narrow — reconnect. Writes require the `contacts` scope; other-contacts reads require `contacts.other.readonly` ([otherContacts.list](https://developers.google.com/people/api/rest/v1/otherContacts/list)). |
|
|
17
|
+
| 400 | `FAILED_PRECONDITION` | Stale etag — "The server returns a 400 error with reason `failedPrecondition` if `person.metadata.sources.etag` is different than the contact's etag, which indicates the contact has changed since its data was read" ([updateContact](https://developers.google.com/people/api/rest/v1/people/updateContact)). Re-fetch with getContact and retry. |
|
|
18
|
+
| 400 | `INVALID_ARGUMENT` | Malformed request. Common cause: sending more than one value on a singleton field — "The server returns a 400 error if more than one field is specified on a field that is a singleton for contact sources: biographies, birthdays, genders, names" ([updateContact](https://developers.google.com/people/api/rest/v1/people/updateContact)). |
|
|
19
|
+
| 404 | `NOT_FOUND` | Resource does not exist. Verify the `resourceName`. |
|
|
20
|
+
| 409 | `ALREADY_EXISTS` | Duplicate contact group name — "Attempting to create a group with a duplicate name will return a HTTP 409 error" ([contactGroups.create](https://developers.google.com/people/api/rest/v1/contactGroups/create)). |
|
|
21
|
+
|
|
22
|
+
## Update replacement semantics (updateContact)
|
|
23
|
+
|
|
24
|
+
"All fields specified in the `updateMask` will be replaced"
|
|
25
|
+
([updateContact](https://developers.google.com/people/api/rest/v1/people/updateContact)).
|
|
26
|
+
Each array you name (e.g. `emailAddresses`, `phoneNumbers`) is **replaced wholesale** —
|
|
27
|
+
to add an entry without losing the others, read the contact first, append, then send
|
|
28
|
+
the complete array. Fields you omit from the mask are left untouched.
|
|
29
|
+
"Any non-contact data will not be modified. Any non-contact data in the person to
|
|
30
|
+
update will be ignored"
|
|
31
|
+
([updateContact](https://developers.google.com/people/api/rest/v1/people/updateContact)).
|
|
32
|
+
|
|
33
|
+
## Etag-based optimistic concurrency
|
|
34
|
+
|
|
35
|
+
The People API requires `person.metadata.sources.etag` for updates:
|
|
36
|
+
"you must include the `person.metadata.sources.etag` field in the person for the
|
|
37
|
+
contact to be updated"
|
|
38
|
+
([Read and Manage Contacts](https://developers.google.com/people/v1/contacts)).
|
|
39
|
+
A stale etag produces a 400 / `FAILED_PRECONDITION` — re-fetch and retry.
|
|
40
|
+
|
|
41
|
+
## Singleton fields
|
|
42
|
+
|
|
43
|
+
Some Person fields accept only a single value per contact source:
|
|
44
|
+
biographies, birthdays, genders, names. Sending an array with more than one entry
|
|
45
|
+
on these fields returns a 400
|
|
46
|
+
([updateContact](https://developers.google.com/people/api/rest/v1/people/updateContact)).
|
|
47
|
+
|
|
48
|
+
## Search: prefix matching and warmup
|
|
49
|
+
|
|
50
|
+
`searchContacts` and `searchOtherContacts` use **prefix phrase matching**:
|
|
51
|
+
"a person with name 'foo name' matches queries such as 'f', 'fo', 'foo', 'foo n',
|
|
52
|
+
'nam', etc., but not 'oo n'"
|
|
53
|
+
([searchContacts](https://developers.google.com/people/api/rest/v1/people/searchContacts)).
|
|
54
|
+
|
|
55
|
+
**Warmup required:** "Before searching, clients should send a warmup request with an
|
|
56
|
+
empty query to update the cache"
|
|
57
|
+
([searchContacts](https://developers.google.com/people/api/rest/v1/people/searchContacts)).
|
|
58
|
+
Without the warmup the cache may not reflect recent changes.
|
|
59
|
+
|
|
60
|
+
**Page size capped at 30:** "Values greater than 30 will be capped to 30"
|
|
61
|
+
([searchContacts](https://developers.google.com/people/api/rest/v1/people/searchContacts)).
|
|
62
|
+
Default is 10.
|
|
63
|
+
|
|
64
|
+
## Write propagation delay
|
|
65
|
+
|
|
66
|
+
"Writes may have a propagation delay of several minutes for sync requests.
|
|
67
|
+
Incremental syncs are not intended for read-after-write use cases"
|
|
68
|
+
([people.connections.list](https://developers.google.com/people/api/rest/v1/people.connections/list)).
|
|
69
|
+
Use `listContacts` (people.connections.list) when freshness matters — it is not
|
|
70
|
+
subject to the search-index propagation delay.
|
|
71
|
+
|
|
72
|
+
## Mutate request serialization
|
|
73
|
+
|
|
74
|
+
"Mutate requests for the same user should be sent sequentially to avoid increased
|
|
75
|
+
latency and failures"
|
|
76
|
+
([Read and Manage Contacts](https://developers.google.com/people/v1/contacts)).
|
|
77
|
+
Do not fire concurrent writes for the same account.
|
|
78
|
+
|
|
79
|
+
## Resource name formats
|
|
80
|
+
|
|
81
|
+
- Contacts: `people/{person_id}` — "An ASCII string in the form of `people/{person_id}`"
|
|
82
|
+
([people resource](https://developers.google.com/people/api/rest/v1/people)).
|
|
83
|
+
- Contact groups: `contactGroups/{contactGroupId}` — "An ASCII string, in the form
|
|
84
|
+
of `contactGroups/{contactGroupId}`"
|
|
85
|
+
([contactGroups resource](https://developers.google.com/people/api/rest/v1/contactGroups)).
|
|
86
|
+
- Other contacts: `otherContacts/{person_id}` — the endpoint is
|
|
87
|
+
`GET https://people.googleapis.com/v1/otherContacts`
|
|
88
|
+
([otherContacts.list](https://developers.google.com/people/api/rest/v1/otherContacts/list)).
|
|
89
|
+
- The resource name may change: "The resource name may change when adding or removing
|
|
90
|
+
fields that link a contact and profile"
|
|
91
|
+
([people resource](https://developers.google.com/people/api/rest/v1/people)).
|
|
92
|
+
|
|
93
|
+
## Contact groups: user vs system
|
|
94
|
+
|
|
95
|
+
`groupType` distinguishes `USER_CONTACT_GROUP` (user-defined) from
|
|
96
|
+
`SYSTEM_CONTACT_GROUP` (system-defined, e.g. `myContacts`, `starred`)
|
|
97
|
+
([contactGroups resource](https://developers.google.com/people/api/rest/v1/contactGroups)).
|
|
98
|
+
System group names are "a system provided name" that is "translated and formatted in
|
|
99
|
+
the viewer's account locale"
|
|
100
|
+
([contactGroups resource](https://developers.google.com/people/api/rest/v1/contactGroups)).
|
|
101
|
+
The `contactGroups.create`, `update`, and `delete` methods operate on groups "owned
|
|
102
|
+
by the authenticated user"
|
|
103
|
+
([contactGroups.delete](https://developers.google.com/people/api/rest/v1/contactGroups/delete)).
|
|
104
|
+
System groups have system-provided names and localized `formattedName` values; the
|
|
105
|
+
`name` field description distinguishes "the group owner" from "a system provided name
|
|
106
|
+
for system groups"
|
|
107
|
+
([contactGroups](https://developers.google.com/people/api/rest/v1/contactGroups)).
|
|
108
|
+
|
|
109
|
+
Group names must be unique: "Created contact group names must be unique to the users
|
|
110
|
+
contact groups" ([contactGroups.create](https://developers.google.com/people/api/rest/v1/contactGroups/create)).
|
|
111
|
+
|
|
112
|
+
## Contact group membership
|
|
113
|
+
|
|
114
|
+
`modifyContactGroupMembers` enforces a combined limit: "The total number of resource
|
|
115
|
+
names in `resourceNamesToAdd` and `resourceNamesToRemove` must be less than or equal
|
|
116
|
+
to 1000"
|
|
117
|
+
([contactGroups.members.modify](https://developers.google.com/people/api/rest/v1/contactGroups.members/modify)).
|
|
118
|
+
|
|
119
|
+
The response reports partial failures without erroring:
|
|
120
|
+
|
|
121
|
+
- `notFoundResourceNames` — "The contact people resource names that were not found."
|
|
122
|
+
- `canNotRemoveLastContactGroupResourceNames` — "The contact people resource names
|
|
123
|
+
that cannot be removed from their last contact group"
|
|
124
|
+
([contactGroups.members.modify](https://developers.google.com/people/api/rest/v1/contactGroups.members/modify)).
|
|
125
|
+
|
|
126
|
+
## Other contacts
|
|
127
|
+
|
|
128
|
+
"Other contacts" are "typically auto created contacts from interactions" that are
|
|
129
|
+
"not in a contact group"
|
|
130
|
+
([otherContacts.list](https://developers.google.com/people/api/rest/v1/otherContacts/list)).
|
|
131
|
+
They require the `contacts.other.readonly` OAuth scope
|
|
132
|
+
([otherContacts.list](https://developers.google.com/people/api/rest/v1/otherContacts/list))
|
|
133
|
+
and the connector exposes them only through read/copy tools: `listOtherContacts`, `searchOtherContacts`, and `copyOtherContact`.
|
|
134
|
+
|
|
135
|
+
**Limited field set:** other contacts support only a subset of person fields for
|
|
136
|
+
`READ_SOURCE_TYPE_CONTACT`: emailAddresses, metadata, names, phoneNumbers, photos
|
|
137
|
+
([otherContacts.list](https://developers.google.com/people/api/rest/v1/otherContacts/list)).
|
|
138
|
+
|
|
139
|
+
The `copyOtherContact` tool promotes an other contact to a saved contact (wrapping
|
|
140
|
+
Google's `otherContacts.copyOtherContactToMyContactsGroup` method). The `copyMask`
|
|
141
|
+
is limited to: emailAddresses, names, phoneNumbers
|
|
142
|
+
([docs](https://developers.google.com/people/api/rest/v1/otherContacts/copyOtherContactToMyContactsGroup)).
|
|
143
|
+
|
|
144
|
+
## Pagination
|
|
145
|
+
|
|
146
|
+
`listContacts` (people.connections.list): pageSize 1–1000, default 100.
|
|
147
|
+
`listContactGroups`: pageSize 1–1000, default 30.
|
|
148
|
+
`otherContacts.list`: pageSize 1–1000, default 100.
|
|
149
|
+
Search endpoints: pageSize capped at 30, default 10.
|
|
150
|
+
([people.connections.list](https://developers.google.com/people/api/rest/v1/people.connections/list),
|
|
151
|
+
[contactGroups.list](https://developers.google.com/people/api/rest/v1/contactGroups/list),
|
|
152
|
+
[otherContacts.list](https://developers.google.com/people/api/rest/v1/otherContacts/list),
|
|
153
|
+
[searchContacts](https://developers.google.com/people/api/rest/v1/people/searchContacts))
|
|
154
|
+
|
|
155
|
+
## Email address cap on list endpoints
|
|
156
|
+
|
|
157
|
+
"For `people.connections.list` and `otherContacts.list` the number of email addresses
|
|
158
|
+
is limited to 100. If a Person has more email addresses the entire set can be obtained
|
|
159
|
+
by calling `people.getBatchGet`"
|
|
160
|
+
([people resource](https://developers.google.com/people/api/rest/v1/people)).
|
|
161
|
+
|
|
162
|
+
## personFields is required on reads
|
|
163
|
+
|
|
164
|
+
"The request returns a 400 error if 'personFields' is not specified"
|
|
165
|
+
([people.get](https://developers.google.com/people/api/rest/v1/people/get)).
|
|
166
|
+
Every read operation requires an explicit field mask.
|
|
167
|
+
|
|
168
|
+
## Delete responses are empty
|
|
169
|
+
|
|
170
|
+
Both `deleteContact` and `deleteContactGroup` return an empty body on success
|
|
171
|
+
([deleteContact](https://developers.google.com/people/api/rest/v1/people/deleteContact),
|
|
172
|
+
[contactGroups.delete](https://developers.google.com/people/api/rest/v1/contactGroups/delete)).
|
|
173
|
+
|
|
174
|
+
## Sync token expiry
|
|
175
|
+
|
|
176
|
+
"Sync tokens expire 7 days after the full sync"
|
|
177
|
+
([people.connections.list](https://developers.google.com/people/api/rest/v1/people.connections/list)).
|
|
178
|
+
"A request with an expired sync token will get an error with an google.rpc.ErrorInfo
|
|
179
|
+
with reason `EXPIRED_SYNC_TOKEN`"
|
|
180
|
+
([people.connections.list](https://developers.google.com/people/api/rest/v1/people.connections/list)) —
|
|
181
|
+
perform a full sync without a `syncToken` to recover.
|
package/scripts/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineTool, handleIfScriptMain } from "@zapier/connectors-sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { connectionResolvers } from "../connections.ts";
|
|
6
|
+
import {
|
|
7
|
+
PersonSchema,
|
|
8
|
+
throwForGoogleContacts,
|
|
9
|
+
} from "../lib/google-contacts.ts";
|
|
10
|
+
|
|
11
|
+
const inputSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
resourceName: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe(
|
|
16
|
+
"Other-contact resource name, e.g. otherContacts/c12345 (from listOtherContacts or searchOtherContacts).",
|
|
17
|
+
),
|
|
18
|
+
copyMask: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe(
|
|
21
|
+
"Which fields to copy onto the new contact. Only emailAddresses, names, and phoneNumbers are supported. Defaults to all three.",
|
|
22
|
+
)
|
|
23
|
+
.default("names,emailAddresses,phoneNumbers"),
|
|
24
|
+
})
|
|
25
|
+
.strict();
|
|
26
|
+
|
|
27
|
+
const definition = defineTool({
|
|
28
|
+
name: "copyOtherContact",
|
|
29
|
+
title: "Copy Other Contact",
|
|
30
|
+
description:
|
|
31
|
+
'Promote an "other contact" into the user\'s saved contacts (myContacts), returning an editable contact with a people/c… resourceName.',
|
|
32
|
+
inputSchema,
|
|
33
|
+
outputSchema: PersonSchema,
|
|
34
|
+
annotations: {
|
|
35
|
+
readOnlyHint: false,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: false,
|
|
38
|
+
openWorldHint: true,
|
|
39
|
+
},
|
|
40
|
+
connection: "google-contacts",
|
|
41
|
+
run: async (input, ctx) => {
|
|
42
|
+
// resourceName (otherContacts/…) is a Google resource path — the slash is
|
|
43
|
+
// significant and must NOT be percent-encoded.
|
|
44
|
+
const url = `https://people.googleapis.com/v1/${input.resourceName}:copyOtherContactToMyContactsGroup`;
|
|
45
|
+
const res = await ctx.fetch(url, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ copyMask: input.copyMask }),
|
|
49
|
+
});
|
|
50
|
+
await throwForGoogleContacts(res, "copyOtherContact");
|
|
51
|
+
return res.json();
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default definition;
|
|
56
|
+
|
|
57
|
+
await handleIfScriptMain(import.meta, definition, { connectionResolvers });
|