gogcli-mcp-contacts 2.1.0 → 2.3.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/dist/index.js CHANGED
@@ -31074,7 +31074,7 @@ async function run(args, options = {}) {
31074
31074
 
31075
31075
  // ../gogcli-mcp/src/tools/utils.ts
31076
31076
  var accountParam = external_exports.string().optional().describe(
31077
- "Google account email to use (overrides GOG_ACCOUNT env var)"
31077
+ "Google account email to use, e.g. you@gmail.com \u2014 must be the full address, not a bare username. Overrides the GOG_ACCOUNT env var. Omit to use the single configured account."
31078
31078
  );
31079
31079
  var ids = {
31080
31080
  course: external_exports.string().describe("Course ID"),
@@ -31133,26 +31133,41 @@ function toError(err) {
31133
31133
  }
31134
31134
  var AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
31135
31135
  var TRANSIENT_ERROR_PATTERN = /\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
31136
+ var GRID_LIMIT_ERROR_PATTERN = /exceeds grid limits/i;
31136
31137
  var AUTH_HINT = "\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. Ask the user if they would like to re-authenticate.";
31137
31138
  var TRANSIENT_HINT = "\n\nThis error is often transient. Retry the same call before trying a different approach (do not fall back to smaller writes or row-by-row operations).";
31139
+ var GRID_LIMIT_HINT = "\n\nThe target range is outside the sheet's current grid. Add the missing rows or columns first with gog_sheets_insert (dimension: rows or cols), then retry the write.";
31140
+ function formatAccountList(raw) {
31141
+ try {
31142
+ const parsed = JSON.parse(raw);
31143
+ if (Array.isArray(parsed?.accounts)) {
31144
+ return parsed.accounts.map((a) => a?.email).filter(Boolean).join("\n");
31145
+ }
31146
+ } catch {
31147
+ }
31148
+ return raw.trim();
31149
+ }
31150
+ async function diagnose(err) {
31151
+ const errText = toError(err).content[0].text;
31152
+ const isAuthError = AUTH_ERROR_PATTERN.test(errText);
31153
+ const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
31154
+ const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
31155
+ const hint = isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
31156
+ try {
31157
+ const accounts = formatAccountList(await run(["auth", "list"]));
31158
+ return toText(`${errText}
31159
+
31160
+ Configured accounts:
31161
+ ${accounts || "(none)"}${hint}`);
31162
+ } catch {
31163
+ return toText(`${errText}${hint}`);
31164
+ }
31165
+ }
31138
31166
  async function runOrDiagnose(args, options) {
31139
31167
  try {
31140
31168
  return toText(await run(args, options));
31141
31169
  } catch (err) {
31142
- const base = toError(err);
31143
- const errText = base.content[0].text;
31144
- const isAuthError = AUTH_ERROR_PATTERN.test(errText);
31145
- const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
31146
- const hint = isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : "";
31147
- try {
31148
- const accounts = await run(["auth", "list"]);
31149
- return toText(`${errText}
31150
-
31151
- Configured accounts:
31152
- ${accounts}${hint}`);
31153
- } catch {
31154
- return toText(`${errText}${hint}`);
31155
- }
31170
+ return diagnose(err);
31156
31171
  }
31157
31172
  }
31158
31173
 
@@ -31275,9 +31290,15 @@ function registerContactsTools(server2) {
31275
31290
 
31276
31291
  // ../gogcli-mcp/src/tools/sheets.ts
31277
31292
  var cellValueParam = external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]);
31293
+ var dryRunParam = external_exports.boolean().optional().describe(
31294
+ "Preview the operation without modifying the sheet (gog --dry-run): reports the intended actions and exits without writing."
31295
+ );
31296
+ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31297
+ 'Safety guard against silent overwrites: before writing, read the target range and refuse the write if any target cell already holds data. Costs one extra read. Anchor ranges (e.g. "Sheet1!A1") are expanded to the full area your values will cover; explicit and named ranges are checked as-is.'
31298
+ );
31278
31299
 
31279
31300
  // ../gogcli-mcp/src/server.ts
31280
- var VERSION = true ? "2.1.0" : "0.0.0";
31301
+ var VERSION = true ? "2.3.0" : "0.0.0";
31281
31302
  function createServer(options) {
31282
31303
  return new McpServer({
31283
31304
  name: options?.name ?? "gogcli",
@@ -31337,6 +31358,129 @@ function registerExtraContactsTools(server2) {
31337
31358
  if (type) args.push(`--type=${type}`);
31338
31359
  return runOrDiagnose(args, { account });
31339
31360
  });
31361
+ server2.registerTool("gog_contacts_update", {
31362
+ description: "Update an existing Google Contact. Empty string clears a field; repeatable fields (url/address/custom/relation) take comma/semicolon-separated lists.",
31363
+ inputSchema: {
31364
+ resourceName: external_exports.string().describe("Contact resource name (people/...)"),
31365
+ given: external_exports.string().optional().describe("Given (first) name"),
31366
+ family: external_exports.string().optional().describe("Family (last) name"),
31367
+ email: external_exports.string().optional().describe("Email address (empty string clears)"),
31368
+ phone: external_exports.string().optional().describe("Phone number (empty string clears)"),
31369
+ org: external_exports.string().optional().describe("Organization/company name (empty string clears)"),
31370
+ title: external_exports.string().optional().describe("Job title (empty string clears)"),
31371
+ url: external_exports.string().optional().describe("URL(s), comma-separated (empty string clears all)"),
31372
+ note: external_exports.string().optional().describe("Note/biography (empty string clears)"),
31373
+ address: external_exports.string().optional().describe("Postal address(es), semicolon-separated (empty string clears all)"),
31374
+ birthday: external_exports.string().optional().describe("Birthday in YYYY-MM-DD (empty string clears)"),
31375
+ ignoreEtag: external_exports.boolean().optional().describe("Allow update even if a supplied etag is stale (may overwrite concurrent changes)"),
31376
+ account: accountParam
31377
+ }
31378
+ }, async ({ resourceName, given, family, email: email3, phone, org, title, url: url2, note, address, birthday, ignoreEtag, account }) => {
31379
+ const args = ["contacts", "update", resourceName];
31380
+ if (given !== void 0) args.push(`--given=${given}`);
31381
+ if (family !== void 0) args.push(`--family=${family}`);
31382
+ if (email3 !== void 0) args.push(`--email=${email3}`);
31383
+ if (phone !== void 0) args.push(`--phone=${phone}`);
31384
+ if (org !== void 0) args.push(`--org=${org}`);
31385
+ if (title !== void 0) args.push(`--title=${title}`);
31386
+ if (url2 !== void 0) args.push(`--url=${url2}`);
31387
+ if (note !== void 0) args.push(`--note=${note}`);
31388
+ if (address !== void 0) args.push(`--address=${address}`);
31389
+ if (birthday !== void 0) args.push(`--birthday=${birthday}`);
31390
+ if (ignoreEtag) args.push("--ignore-etag");
31391
+ return runOrDiagnose(args, { account });
31392
+ });
31393
+ server2.registerTool("gog_contacts_delete", {
31394
+ description: "Delete a Google Contact by resource name.",
31395
+ annotations: { destructiveHint: true },
31396
+ inputSchema: {
31397
+ resourceName: external_exports.string().describe("Contact resource name (people/...)"),
31398
+ account: accountParam
31399
+ }
31400
+ }, async ({ resourceName, account }) => {
31401
+ return runOrDiagnose(["contacts", "delete", resourceName], { account });
31402
+ });
31403
+ server2.registerTool("gog_contacts_export", {
31404
+ description: "Export contacts as vCard (.vcf). Provide a selector (resource name, email, or name), or use query / all to export multiple.",
31405
+ annotations: { readOnlyHint: true },
31406
+ inputSchema: {
31407
+ selector: external_exports.string().optional().describe("Contact resource name (people/...), email, or name"),
31408
+ query: external_exports.string().optional().describe("Search query to export (max 30 results)"),
31409
+ all: external_exports.boolean().optional().describe("Export all personal contacts"),
31410
+ out: external_exports.string().optional().describe("Output path (.vcf), or - for stdout (default: stdout)"),
31411
+ max: external_exports.number().optional().describe("Max results for query (1-30)"),
31412
+ page: external_exports.string().optional().describe("Start page token for all"),
31413
+ account: accountParam
31414
+ }
31415
+ }, async ({ selector, query, all, out, max, page, account }) => {
31416
+ const args = ["contacts", "export"];
31417
+ if (selector) args.push(selector);
31418
+ if (query) args.push(`--query=${query}`);
31419
+ if (all) args.push("--all");
31420
+ if (out) args.push(`--out=${out}`);
31421
+ if (max !== void 0) args.push(`--max=${max}`);
31422
+ if (page) args.push(`--page=${page}`);
31423
+ return runOrDiagnose(args, { account });
31424
+ });
31425
+ server2.registerTool("gog_contacts_dedupe", {
31426
+ description: "Find likely duplicate personal contacts (preview only \u2014 does not modify anything).",
31427
+ annotations: { readOnlyHint: true },
31428
+ inputSchema: {
31429
+ match: external_exports.string().optional().describe("Match fields, comma-separated from email,phone,name (default: email,phone)"),
31430
+ max: external_exports.number().optional().describe("Max contacts to scan (0 = all)"),
31431
+ account: accountParam
31432
+ }
31433
+ }, async ({ match, max, account }) => {
31434
+ const args = ["contacts", "dedupe"];
31435
+ if (match) args.push(`--match=${match}`);
31436
+ if (max !== void 0) args.push(`--max=${max}`);
31437
+ return runOrDiagnose(args, { account });
31438
+ });
31439
+ server2.registerTool("gog_contacts_directory_list", {
31440
+ description: "List people from the Google Workspace directory (domain shared contacts).",
31441
+ annotations: { readOnlyHint: true },
31442
+ inputSchema: {
31443
+ max: external_exports.number().optional().describe("Max results (default: 50)"),
31444
+ page: external_exports.string().optional().describe("Page token"),
31445
+ all: external_exports.boolean().optional().describe("Fetch all pages"),
31446
+ account: accountParam
31447
+ }
31448
+ }, async ({ max, page, all, account }) => {
31449
+ const args = ["contacts", "directory", "list"];
31450
+ if (max !== void 0) args.push(`--max=${max}`);
31451
+ if (page) args.push(`--page=${page}`);
31452
+ if (all) args.push("--all");
31453
+ return runOrDiagnose(args, { account });
31454
+ });
31455
+ server2.registerTool("gog_contacts_other_list", {
31456
+ description: 'List "other contacts" \u2014 auto-collected addresses (e.g. people you have emailed) that are not in your saved contacts.',
31457
+ annotations: { readOnlyHint: true },
31458
+ inputSchema: {
31459
+ max: external_exports.number().optional().describe("Max results (default: 100)"),
31460
+ page: external_exports.string().optional().describe("Page token"),
31461
+ all: external_exports.boolean().optional().describe("Fetch all pages"),
31462
+ account: accountParam
31463
+ }
31464
+ }, async ({ max, page, all, account }) => {
31465
+ const args = ["contacts", "other", "list"];
31466
+ if (max !== void 0) args.push(`--max=${max}`);
31467
+ if (page) args.push(`--page=${page}`);
31468
+ if (all) args.push("--all");
31469
+ return runOrDiagnose(args, { account });
31470
+ });
31471
+ server2.registerTool("gog_contacts_other_search", {
31472
+ description: 'Search "other contacts" \u2014 auto-collected addresses not in your saved contacts.',
31473
+ annotations: { readOnlyHint: true },
31474
+ inputSchema: {
31475
+ query: external_exports.string().describe("Search query"),
31476
+ max: external_exports.number().optional().describe("Max results (default: 50)"),
31477
+ account: accountParam
31478
+ }
31479
+ }, async ({ query, max, account }) => {
31480
+ const args = ["contacts", "other", "search", query];
31481
+ if (max !== void 0) args.push(`--max=${max}`);
31482
+ return runOrDiagnose(args, { account });
31483
+ });
31340
31484
  server2.registerTool("gog_people_raw", {
31341
31485
  description: "Dump the raw People API response as JSON (lossless; for scripting and LLM consumption).",
31342
31486
  annotations: { readOnlyHint: true },
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-contacts",
5
5
  "display_name": "gogcli (Contacts)",
6
- "version": "2.1.0",
6
+ "version": "2.3.0",
7
7
  "description": "Extended Google Contacts for Claude via gogcli — auth + Contacts + Workspace directory (People API)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -113,6 +113,34 @@
113
113
  {
114
114
  "name": "gog_people_raw",
115
115
  "description": "Dump raw People API JSON for a person"
116
+ },
117
+ {
118
+ "name": "gog_contacts_update",
119
+ "description": "Update an existing contact (empty string clears a field)"
120
+ },
121
+ {
122
+ "name": "gog_contacts_delete",
123
+ "description": "Delete a contact by resource name"
124
+ },
125
+ {
126
+ "name": "gog_contacts_export",
127
+ "description": "Export contacts as vCard (.vcf)"
128
+ },
129
+ {
130
+ "name": "gog_contacts_dedupe",
131
+ "description": "Find likely duplicate contacts (preview only)"
132
+ },
133
+ {
134
+ "name": "gog_contacts_directory_list",
135
+ "description": "List people from the Workspace directory"
136
+ },
137
+ {
138
+ "name": "gog_contacts_other_list",
139
+ "description": "List auto-collected other contacts"
140
+ },
141
+ {
142
+ "name": "gog_contacts_other_search",
143
+ "description": "Search auto-collected other contacts"
116
144
  }
117
145
  ],
118
146
  "compatibility": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-contacts",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-contacts",
5
5
  "description": "Extended Google Contacts + People MCP server via gogcli — auth + Contacts + Workspace directory (People API)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -59,6 +59,136 @@ export function registerExtraContactsTools(server: McpServer): void {
59
59
  return runOrDiagnose(args, { account });
60
60
  });
61
61
 
62
+ server.registerTool('gog_contacts_update', {
63
+ description: 'Update an existing Google Contact. Empty string clears a field; repeatable fields (url/address/custom/relation) take comma/semicolon-separated lists.',
64
+ inputSchema: {
65
+ resourceName: z.string().describe('Contact resource name (people/...)'),
66
+ given: z.string().optional().describe('Given (first) name'),
67
+ family: z.string().optional().describe('Family (last) name'),
68
+ email: z.string().optional().describe('Email address (empty string clears)'),
69
+ phone: z.string().optional().describe('Phone number (empty string clears)'),
70
+ org: z.string().optional().describe('Organization/company name (empty string clears)'),
71
+ title: z.string().optional().describe('Job title (empty string clears)'),
72
+ url: z.string().optional().describe('URL(s), comma-separated (empty string clears all)'),
73
+ note: z.string().optional().describe('Note/biography (empty string clears)'),
74
+ address: z.string().optional().describe('Postal address(es), semicolon-separated (empty string clears all)'),
75
+ birthday: z.string().optional().describe('Birthday in YYYY-MM-DD (empty string clears)'),
76
+ ignoreEtag: z.boolean().optional().describe('Allow update even if a supplied etag is stale (may overwrite concurrent changes)'),
77
+ account: accountParam,
78
+ },
79
+ }, async ({ resourceName, given, family, email, phone, org, title, url, note, address, birthday, ignoreEtag, account }) => {
80
+ const args = ['contacts', 'update', resourceName];
81
+ if (given !== undefined) args.push(`--given=${given}`);
82
+ if (family !== undefined) args.push(`--family=${family}`);
83
+ if (email !== undefined) args.push(`--email=${email}`);
84
+ if (phone !== undefined) args.push(`--phone=${phone}`);
85
+ if (org !== undefined) args.push(`--org=${org}`);
86
+ if (title !== undefined) args.push(`--title=${title}`);
87
+ if (url !== undefined) args.push(`--url=${url}`);
88
+ if (note !== undefined) args.push(`--note=${note}`);
89
+ if (address !== undefined) args.push(`--address=${address}`);
90
+ if (birthday !== undefined) args.push(`--birthday=${birthday}`);
91
+ if (ignoreEtag) args.push('--ignore-etag');
92
+ return runOrDiagnose(args, { account });
93
+ });
94
+
95
+ server.registerTool('gog_contacts_delete', {
96
+ description: 'Delete a Google Contact by resource name.',
97
+ annotations: { destructiveHint: true },
98
+ inputSchema: {
99
+ resourceName: z.string().describe('Contact resource name (people/...)'),
100
+ account: accountParam,
101
+ },
102
+ }, async ({ resourceName, account }) => {
103
+ return runOrDiagnose(['contacts', 'delete', resourceName], { account });
104
+ });
105
+
106
+ server.registerTool('gog_contacts_export', {
107
+ description: 'Export contacts as vCard (.vcf). Provide a selector (resource name, email, or name), or use query / all to export multiple.',
108
+ annotations: { readOnlyHint: true },
109
+ inputSchema: {
110
+ selector: z.string().optional().describe('Contact resource name (people/...), email, or name'),
111
+ query: z.string().optional().describe('Search query to export (max 30 results)'),
112
+ all: z.boolean().optional().describe('Export all personal contacts'),
113
+ out: z.string().optional().describe('Output path (.vcf), or - for stdout (default: stdout)'),
114
+ max: z.number().optional().describe('Max results for query (1-30)'),
115
+ page: z.string().optional().describe('Start page token for all'),
116
+ account: accountParam,
117
+ },
118
+ }, async ({ selector, query, all, out, max, page, account }) => {
119
+ const args = ['contacts', 'export'];
120
+ if (selector) args.push(selector);
121
+ if (query) args.push(`--query=${query}`);
122
+ if (all) args.push('--all');
123
+ if (out) args.push(`--out=${out}`);
124
+ if (max !== undefined) args.push(`--max=${max}`);
125
+ if (page) args.push(`--page=${page}`);
126
+ return runOrDiagnose(args, { account });
127
+ });
128
+
129
+ server.registerTool('gog_contacts_dedupe', {
130
+ description: 'Find likely duplicate personal contacts (preview only — does not modify anything).',
131
+ annotations: { readOnlyHint: true },
132
+ inputSchema: {
133
+ match: z.string().optional().describe('Match fields, comma-separated from email,phone,name (default: email,phone)'),
134
+ max: z.number().optional().describe('Max contacts to scan (0 = all)'),
135
+ account: accountParam,
136
+ },
137
+ }, async ({ match, max, account }) => {
138
+ const args = ['contacts', 'dedupe'];
139
+ if (match) args.push(`--match=${match}`);
140
+ if (max !== undefined) args.push(`--max=${max}`);
141
+ return runOrDiagnose(args, { account });
142
+ });
143
+
144
+ server.registerTool('gog_contacts_directory_list', {
145
+ description: 'List people from the Google Workspace directory (domain shared contacts).',
146
+ annotations: { readOnlyHint: true },
147
+ inputSchema: {
148
+ max: z.number().optional().describe('Max results (default: 50)'),
149
+ page: z.string().optional().describe('Page token'),
150
+ all: z.boolean().optional().describe('Fetch all pages'),
151
+ account: accountParam,
152
+ },
153
+ }, async ({ max, page, all, account }) => {
154
+ const args = ['contacts', 'directory', 'list'];
155
+ if (max !== undefined) args.push(`--max=${max}`);
156
+ if (page) args.push(`--page=${page}`);
157
+ if (all) args.push('--all');
158
+ return runOrDiagnose(args, { account });
159
+ });
160
+
161
+ server.registerTool('gog_contacts_other_list', {
162
+ description: 'List "other contacts" — auto-collected addresses (e.g. people you have emailed) that are not in your saved contacts.',
163
+ annotations: { readOnlyHint: true },
164
+ inputSchema: {
165
+ max: z.number().optional().describe('Max results (default: 100)'),
166
+ page: z.string().optional().describe('Page token'),
167
+ all: z.boolean().optional().describe('Fetch all pages'),
168
+ account: accountParam,
169
+ },
170
+ }, async ({ max, page, all, account }) => {
171
+ const args = ['contacts', 'other', 'list'];
172
+ if (max !== undefined) args.push(`--max=${max}`);
173
+ if (page) args.push(`--page=${page}`);
174
+ if (all) args.push('--all');
175
+ return runOrDiagnose(args, { account });
176
+ });
177
+
178
+ server.registerTool('gog_contacts_other_search', {
179
+ description: 'Search "other contacts" — auto-collected addresses not in your saved contacts.',
180
+ annotations: { readOnlyHint: true },
181
+ inputSchema: {
182
+ query: z.string().describe('Search query'),
183
+ max: z.number().optional().describe('Max results (default: 50)'),
184
+ account: accountParam,
185
+ },
186
+ }, async ({ query, max, account }) => {
187
+ const args = ['contacts', 'other', 'search', query];
188
+ if (max !== undefined) args.push(`--max=${max}`);
189
+ return runOrDiagnose(args, { account });
190
+ });
191
+
62
192
  server.registerTool('gog_people_raw', {
63
193
  description: 'Dump the raw People API response as JSON (lossless; for scripting and LLM consumption).',
64
194
  annotations: { readOnlyHint: true },
@@ -73,6 +73,148 @@ describe('gog_people_relations', () => {
73
73
  });
74
74
  });
75
75
 
76
+ describe('gog_contacts_update', () => {
77
+ it('calls runOrDiagnose with just resourceName', async () => {
78
+ await handlers.get('gog_contacts_update')!({ resourceName: 'people/c1' });
79
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'update', 'people/c1'], { account: undefined });
80
+ });
81
+
82
+ it('passes all fields including empty-string clears', async () => {
83
+ await handlers.get('gog_contacts_update')!({
84
+ resourceName: 'people/c1',
85
+ given: 'Ada',
86
+ family: 'Lovelace',
87
+ email: '',
88
+ phone: '+1',
89
+ org: 'Analytical',
90
+ title: 'Engineer',
91
+ url: 'https://a.com',
92
+ note: 'hi',
93
+ address: '1 St;City',
94
+ birthday: '1815-12-10',
95
+ ignoreEtag: true,
96
+ });
97
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
98
+ [
99
+ 'contacts', 'update', 'people/c1',
100
+ '--given=Ada', '--family=Lovelace', '--email=', '--phone=+1',
101
+ '--org=Analytical', '--title=Engineer', '--url=https://a.com',
102
+ '--note=hi', '--address=1 St;City', '--birthday=1815-12-10', '--ignore-etag',
103
+ ],
104
+ { account: undefined },
105
+ );
106
+ });
107
+
108
+ it('omits --ignore-etag when false', async () => {
109
+ await handlers.get('gog_contacts_update')!({ resourceName: 'people/c1', ignoreEtag: false });
110
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'update', 'people/c1'], { account: undefined });
111
+ });
112
+ });
113
+
114
+ describe('gog_contacts_delete', () => {
115
+ it('calls runOrDiagnose with resourceName', async () => {
116
+ await handlers.get('gog_contacts_delete')!({ resourceName: 'people/c1' });
117
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'delete', 'people/c1'], { account: undefined });
118
+ });
119
+ });
120
+
121
+ describe('gog_contacts_export', () => {
122
+ it('calls runOrDiagnose with no options', async () => {
123
+ await handlers.get('gog_contacts_export')!({});
124
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'export'], { account: undefined });
125
+ });
126
+
127
+ it('passes selector and all flags', async () => {
128
+ await handlers.get('gog_contacts_export')!({
129
+ selector: 'people/c1',
130
+ query: 'ada',
131
+ all: true,
132
+ out: 'out.vcf',
133
+ max: 10,
134
+ page: 'tok',
135
+ });
136
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
137
+ ['contacts', 'export', 'people/c1', '--query=ada', '--all', '--out=out.vcf', '--max=10', '--page=tok'],
138
+ { account: undefined },
139
+ );
140
+ });
141
+
142
+ it('omits --all when false', async () => {
143
+ await handlers.get('gog_contacts_export')!({ all: false });
144
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'export'], { account: undefined });
145
+ });
146
+ });
147
+
148
+ describe('gog_contacts_dedupe', () => {
149
+ it('calls runOrDiagnose with no options', async () => {
150
+ await handlers.get('gog_contacts_dedupe')!({});
151
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'dedupe'], { account: undefined });
152
+ });
153
+
154
+ it('passes --match and --max', async () => {
155
+ await handlers.get('gog_contacts_dedupe')!({ match: 'name', max: 100 });
156
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
157
+ ['contacts', 'dedupe', '--match=name', '--max=100'],
158
+ { account: undefined },
159
+ );
160
+ });
161
+ });
162
+
163
+ describe('gog_contacts_directory_list', () => {
164
+ it('calls runOrDiagnose with no options', async () => {
165
+ await handlers.get('gog_contacts_directory_list')!({});
166
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'directory', 'list'], { account: undefined });
167
+ });
168
+
169
+ it('passes pagination flags', async () => {
170
+ await handlers.get('gog_contacts_directory_list')!({ max: 50, page: 'tok', all: true });
171
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
172
+ ['contacts', 'directory', 'list', '--max=50', '--page=tok', '--all'],
173
+ { account: undefined },
174
+ );
175
+ });
176
+
177
+ it('omits --all when false', async () => {
178
+ await handlers.get('gog_contacts_directory_list')!({ all: false });
179
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'directory', 'list'], { account: undefined });
180
+ });
181
+ });
182
+
183
+ describe('gog_contacts_other_list', () => {
184
+ it('calls runOrDiagnose with no options', async () => {
185
+ await handlers.get('gog_contacts_other_list')!({});
186
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'other', 'list'], { account: undefined });
187
+ });
188
+
189
+ it('passes pagination flags', async () => {
190
+ await handlers.get('gog_contacts_other_list')!({ max: 100, page: 'tok', all: true });
191
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
192
+ ['contacts', 'other', 'list', '--max=100', '--page=tok', '--all'],
193
+ { account: undefined },
194
+ );
195
+ });
196
+
197
+ it('omits --all when false', async () => {
198
+ await handlers.get('gog_contacts_other_list')!({ all: false });
199
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'other', 'list'], { account: undefined });
200
+ });
201
+ });
202
+
203
+ describe('gog_contacts_other_search', () => {
204
+ it('calls runOrDiagnose with query', async () => {
205
+ await handlers.get('gog_contacts_other_search')!({ query: 'ada' });
206
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['contacts', 'other', 'search', 'ada'], { account: undefined });
207
+ });
208
+
209
+ it('passes --max when provided', async () => {
210
+ await handlers.get('gog_contacts_other_search')!({ query: 'ada', max: 25 });
211
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
212
+ ['contacts', 'other', 'search', 'ada', '--max=25'],
213
+ { account: undefined },
214
+ );
215
+ });
216
+ });
217
+
76
218
  describe('gog_people_raw', () => {
77
219
  it('calls runOrDiagnose with userId', async () => {
78
220
  await handlers.get('gog_people_raw')!({ userId: 'people/c123' });