@zapier/google-contacts-connector 0.0.0 → 0.1.1

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/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
- "version": "0.0.0",
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
- "private": false
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.1",
56
+ "publishConfig": {
57
+ "access": "public"
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.
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 });