gogcli-mcp 4.2.5 → 4.4.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.
Files changed (63) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/index.js +1340 -262
  4. package/dist/lib.js +1381 -260
  5. package/manifest.json +9 -2
  6. package/package.json +2 -2
  7. package/server.json +2 -2
  8. package/src/arg-guard.ts +68 -0
  9. package/src/argv.ts +27 -0
  10. package/src/attachment-root.ts +111 -0
  11. package/src/attachments.ts +1 -0
  12. package/src/blob-upload.ts +4 -2
  13. package/src/dispatch-confirmation.ts +277 -0
  14. package/src/file-roots.ts +66 -0
  15. package/src/gmail-dispatch-guard.ts +70 -30
  16. package/src/gmail-results.ts +21 -2
  17. package/src/lib.ts +15 -0
  18. package/src/run-path-guard.ts +118 -0
  19. package/src/runner.ts +86 -18
  20. package/src/send-confirm-token.ts +167 -0
  21. package/src/tools/api.ts +60 -7
  22. package/src/tools/appscript.ts +12 -8
  23. package/src/tools/auth.ts +13 -3
  24. package/src/tools/calendar.ts +178 -15
  25. package/src/tools/chat.ts +94 -18
  26. package/src/tools/classroom.ts +110 -28
  27. package/src/tools/contacts.ts +5 -3
  28. package/src/tools/docs.ts +8 -6
  29. package/src/tools/drive.ts +96 -20
  30. package/src/tools/gmail.ts +175 -24
  31. package/src/tools/sheets.ts +10 -8
  32. package/src/tools/slides.ts +14 -9
  33. package/src/tools/tasks.ts +7 -5
  34. package/src/tools/utils.ts +52 -6
  35. package/tests/arg-guard.test.ts +80 -0
  36. package/tests/attachment-root.test.ts +130 -0
  37. package/tests/attachments.test.ts +8 -0
  38. package/tests/blob-upload.test.ts +4 -3
  39. package/tests/file-roots.test.ts +101 -0
  40. package/tests/gmail-dispatch-guard.test.ts +270 -11
  41. package/tests/gmail-results.test.ts +35 -1
  42. package/tests/run-path-guard.test.ts +142 -0
  43. package/tests/runner.test.ts +136 -0
  44. package/tests/send-confirm-token.test.ts +200 -0
  45. package/tests/tools/api.test.ts +74 -8
  46. package/tests/tools/appscript.test.ts +34 -8
  47. package/tests/tools/auth.test.ts +44 -17
  48. package/tests/tools/calendar.test.ts +33 -28
  49. package/tests/tools/chat.test.ts +50 -21
  50. package/tests/tools/classroom.test.ts +43 -39
  51. package/tests/tools/contacts.test.ts +3 -2
  52. package/tests/tools/dispatch-gates.test.ts +396 -0
  53. package/tests/tools/docs.test.ts +44 -15
  54. package/tests/tools/drive.test.ts +110 -20
  55. package/tests/tools/gmail-confirm-token.test.ts +274 -0
  56. package/tests/tools/gmail.test.ts +227 -29
  57. package/tests/tools/run-tool-examples.test.ts +69 -0
  58. package/tests/tools/run-vets.test.ts +131 -0
  59. package/tests/tools/sheets.test.ts +16 -15
  60. package/tests/tools/slides.test.ts +47 -11
  61. package/tests/tools/tasks.test.ts +7 -6
  62. package/tests/tools/utils.test.ts +32 -31
  63. package/vitest.config.ts +5 -0
@@ -1,6 +1,7 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import * as runner from '../../src/runner.js';
3
3
  import { runOrDiagnose, pushPaginationFlags, formatAccountList, formatAuthHealth } from '../../src/tools/utils.js';
4
+ import { pos } from '../../src/argv.js';
4
5
 
5
6
  // PARTIAL mock: only `run` is stubbed; everything else stays real.
6
7
  vi.mock('../../src/runner.js', async (importOriginal) => ({
@@ -51,7 +52,7 @@ describe('formatAccountList', () => {
51
52
  email: 'chris.c.hall@gmail.com',
52
53
  subject: '109876543210987654321',
53
54
  client: 'default',
54
- services: ['gmail', 'drive'],
55
+ services: ['gmail', pos('drive')],
55
56
  scopes: ['https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/drive'],
56
57
  created_at: '2026-01-02T03:04:05Z',
57
58
  },
@@ -94,7 +95,7 @@ describe('formatAccountList', () => {
94
95
  describe('runOrDiagnose', () => {
95
96
  it('returns output text on success (not flagged as an error)', async () => {
96
97
  vi.mocked(runner.run).mockResolvedValue('{"ok":true}');
97
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
98
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
98
99
  expect(result.content[0].text).toBe('{"ok":true}');
99
100
  expect(result.isError).toBeUndefined();
100
101
  });
@@ -106,7 +107,7 @@ describe('runOrDiagnose', () => {
106
107
  it('leaves a lossless response byte-for-byte untouched', async () => {
107
108
  const raw = '{\n "id": "m1",\n "internalDate": "1785209760000"\n}';
108
109
  vi.mocked(runner.run).mockResolvedValue(raw);
109
- const result = await runOrDiagnose(['gmail', 'raw', 'm1'], { lossless: true });
110
+ const result = await runOrDiagnose(['gmail', 'raw', pos('m1')], { lossless: true });
110
111
  expect(result.content[0].text).toBe(raw);
111
112
  });
112
113
 
@@ -182,7 +183,7 @@ describe('runOrDiagnose', () => {
182
183
  it('does NOT minify a lossless response', async () => {
183
184
  const raw = '{\n "id": "m1"\n}';
184
185
  vi.mocked(runner.run).mockResolvedValue(raw);
185
- const result = await runOrDiagnose(['gmail', 'raw', 'm1'], { lossless: true });
186
+ const result = await runOrDiagnose(['gmail', 'raw', pos('m1')], { lossless: true });
186
187
  expect(result.content[0].text).toBe(raw);
187
188
  });
188
189
 
@@ -230,7 +231,7 @@ describe('runOrDiagnose', () => {
230
231
  vi.mocked(runner.run).mockResolvedValue(
231
232
  '{\n "id": "f1",\n "thumbnailLink": "https://lh3.googleusercontent.com/x=s220",\n "webViewLink": "https://docs.google.com/d/f1"\n}',
232
233
  );
233
- const result = await runOrDiagnose(['drive', 'get', 'f1'], { stripMedia: true });
234
+ const result = await runOrDiagnose(['drive', 'get', pos('f1')], { stripMedia: true });
234
235
  expect(result.content[0].text).toBe('{"id":"f1","webViewLink":"https://docs.google.com/d/f1"}');
235
236
  });
236
237
 
@@ -240,14 +241,14 @@ describe('runOrDiagnose', () => {
240
241
  vi.mocked(runner.run).mockResolvedValue(
241
242
  JSON.stringify({ files: [{ id: 'f1', hasThumbnail: false, thumbnailLink: 'https://x/y=s220', webViewLink: 'https://docs/f1' }] }),
242
243
  );
243
- const result = await runOrDiagnose(['drive', 'search', 'q'], { stripMedia: true });
244
+ const result = await runOrDiagnose(['drive', 'search', pos('q')], { stripMedia: true });
244
245
  const parsed = JSON.parse(result.content[0].text as string);
245
246
  expect(parsed.files[0]).toEqual({ id: 'f1', hasThumbnail: false, webViewLink: 'https://docs/f1' });
246
247
  });
247
248
 
248
249
  it('does not strip media unless asked', async () => {
249
250
  vi.mocked(runner.run).mockResolvedValue('{"thumbnailLink":"https://x/y=s220"}');
250
- const result = await runOrDiagnose(['drive', 'get', 'f1'], {});
251
+ const result = await runOrDiagnose(['drive', 'get', pos('f1')], {});
251
252
  expect(result.content[0].text).toBe('{"thumbnailLink":"https://x/y=s220"}');
252
253
  });
253
254
 
@@ -263,7 +264,7 @@ describe('runOrDiagnose', () => {
263
264
  vi.mocked(runner.run)
264
265
  .mockRejectedValueOnce(new Error('Doc not found'))
265
266
  .mockResolvedValueOnce('user@gmail.com');
266
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
267
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
267
268
  expect(result.content[0].text).toBe(
268
269
  'Error: Doc not found\n\nConfigured accounts:\nuser@gmail.com',
269
270
  );
@@ -284,7 +285,7 @@ describe('runOrDiagnose', () => {
284
285
  vi.mocked(runner.run)
285
286
  .mockRejectedValueOnce(new Error('refusing to delete gmail draft r123 without --force (non-interactive)'))
286
287
  .mockResolvedValueOnce(authListJson);
287
- const result = await runOrDiagnose(['gmail', 'drafts', 'delete', 'r123'], {});
288
+ const result = await runOrDiagnose(['gmail', 'drafts', 'delete', pos('r123')], {});
288
289
  const text = result.content[0].text;
289
290
  expect(text).toContain('Configured accounts:\nchris.c.hall@gmail.com');
290
291
  expect(text).not.toContain('scopes');
@@ -297,7 +298,7 @@ describe('runOrDiagnose', () => {
297
298
  vi.mocked(runner.run)
298
299
  .mockRejectedValueOnce(new Error('Doc not found'))
299
300
  .mockResolvedValueOnce('{"accounts":[]}');
300
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
301
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
301
302
  expect(result.content[0].text).toBe('Error: Doc not found\n\nConfigured accounts:\n(none)');
302
303
  });
303
304
 
@@ -305,7 +306,7 @@ describe('runOrDiagnose', () => {
305
306
  vi.mocked(runner.run)
306
307
  .mockRejectedValueOnce(new Error('Request failed with status 401'))
307
308
  .mockResolvedValueOnce('user@gmail.com');
308
- const result = await runOrDiagnose(['docs', 'comments', 'list', 'abc'], {});
309
+ const result = await runOrDiagnose(['docs', 'comments', 'list', pos('abc')], {});
309
310
  expect(result.content[0].text).toContain('Error: Request failed with status 401');
310
311
  expect(result.content[0].text).toContain('Configured accounts:\nuser@gmail.com');
311
312
  expect(result.content[0].text).toContain('gog_auth_add');
@@ -315,7 +316,7 @@ describe('runOrDiagnose', () => {
315
316
  vi.mocked(runner.run)
316
317
  .mockRejectedValueOnce(new Error('unauthorized access'))
317
318
  .mockResolvedValueOnce('user@gmail.com');
318
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
319
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
319
320
  expect(result.content[0].text).toContain('gog_auth_add');
320
321
  });
321
322
 
@@ -323,7 +324,7 @@ describe('runOrDiagnose', () => {
323
324
  vi.mocked(runner.run)
324
325
  .mockRejectedValueOnce(new Error('token has been expired or revoked'))
325
326
  .mockResolvedValueOnce('user@gmail.com');
326
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
327
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
327
328
  expect(result.content[0].text).toContain('gog_auth_add');
328
329
  });
329
330
 
@@ -331,7 +332,7 @@ describe('runOrDiagnose', () => {
331
332
  vi.mocked(runner.run)
332
333
  .mockRejectedValueOnce(new Error('invalid_grant'))
333
334
  .mockResolvedValueOnce('user@gmail.com');
334
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
335
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
335
336
  expect(result.content[0].text).toContain('gog_auth_add');
336
337
  });
337
338
 
@@ -346,7 +347,7 @@ describe('runOrDiagnose', () => {
346
347
  vi.mocked(runner.run)
347
348
  .mockRejectedValueOnce(new Error('429 rateLimitExceeded: the access token expired mid-request, retry'))
348
349
  .mockResolvedValueOnce('user@gmail.com');
349
- const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
350
+ const result = await runOrDiagnose(['sheets', 'get', pos('A1')], {});
350
351
  const text = result.content[0].text as string;
351
352
  expect(text).toContain('often transient');
352
353
  expect(text).not.toContain('gog_auth_add');
@@ -359,7 +360,7 @@ describe('runOrDiagnose', () => {
359
360
  vi.mocked(runner.run)
360
361
  .mockRejectedValueOnce(new Error('401 unauthorized (quota project unset)'))
361
362
  .mockResolvedValueOnce('user@gmail.com');
362
- const result = await runOrDiagnose(['sheets', 'get', 'A1'], {});
363
+ const result = await runOrDiagnose(['sheets', 'get', pos('A1')], {});
363
364
  expect(result.content[0].text).toContain('gog_auth_add');
364
365
  });
365
366
 
@@ -375,7 +376,7 @@ describe('runOrDiagnose', () => {
375
376
  new Error('page token accepted; the requested export link has expired and must be regenerated'),
376
377
  )
377
378
  .mockResolvedValueOnce('user@gmail.com');
378
- const result = await runOrDiagnose(['drive', 'export', 'abc'], {});
379
+ const result = await runOrDiagnose(['drive', pos('export'), pos('abc')], {});
379
380
  expect(result.content[0].text).not.toContain('gog_auth_add');
380
381
  });
381
382
 
@@ -383,7 +384,7 @@ describe('runOrDiagnose', () => {
383
384
  vi.mocked(runner.run)
384
385
  .mockRejectedValueOnce(new Error('oauth2: "invalid_grant" "Token has been expired or revoked."'))
385
386
  .mockResolvedValueOnce('user@gmail.com');
386
- const result = await runOrDiagnose(['gmail', 'search', 'is:unread'], {});
387
+ const result = await runOrDiagnose(['gmail', 'search', pos('is:unread')], {});
387
388
  const text = result.content[0].text as string;
388
389
  // plain-English cause
389
390
  expect(text).toContain('7-day');
@@ -398,7 +399,7 @@ describe('runOrDiagnose', () => {
398
399
  vi.mocked(runner.run)
399
400
  .mockRejectedValueOnce(new Error('Request failed with status 401'))
400
401
  .mockResolvedValueOnce('user@gmail.com');
401
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
402
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
402
403
  const text = result.content[0].text as string;
403
404
  expect(text).toContain('gog_auth_add');
404
405
  expect(text).not.toContain('In production');
@@ -408,7 +409,7 @@ describe('runOrDiagnose', () => {
408
409
  vi.mocked(runner.run)
409
410
  .mockRejectedValueOnce(new Error('Request failed with status 401'))
410
411
  .mockRejectedValueOnce(new Error('auth list failed'));
411
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
412
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
412
413
  expect(result.content[0].text).toContain('Error: Request failed with status 401');
413
414
  expect(result.content[0].text).toContain('gog_auth_add');
414
415
  expect(result.content[0].text).not.toContain('Configured accounts');
@@ -418,7 +419,7 @@ describe('runOrDiagnose', () => {
418
419
  vi.mocked(runner.run)
419
420
  .mockRejectedValueOnce(new Error('Doc not found'))
420
421
  .mockRejectedValueOnce(new Error('auth list failed'));
421
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
422
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
422
423
  expect(result.content[0].text).toBe('Error: Doc not found');
423
424
  expect(result.isError).toBe(true);
424
425
  expect(result.content[0].text).not.toContain('gog_auth_add');
@@ -428,7 +429,7 @@ describe('runOrDiagnose', () => {
428
429
  vi.mocked(runner.run)
429
430
  .mockRejectedValueOnce(new Error('Request failed with status 429'))
430
431
  .mockResolvedValueOnce('user@gmail.com');
431
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
432
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
432
433
  expect(result.content[0].text).toContain('transient');
433
434
  expect(result.content[0].text).toContain('Retry');
434
435
  });
@@ -439,7 +440,7 @@ describe('runOrDiagnose', () => {
439
440
  vi.mocked(runner.run)
440
441
  .mockRejectedValueOnce(new Error(`Request failed with status ${status}`))
441
442
  .mockResolvedValueOnce('user@gmail.com');
442
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
443
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
443
444
  expect(result.content[0].text, `status ${status}`).toContain('transient');
444
445
  }
445
446
  });
@@ -450,7 +451,7 @@ describe('runOrDiagnose', () => {
450
451
  vi.mocked(runner.run)
451
452
  .mockRejectedValueOnce(new Error(msg))
452
453
  .mockResolvedValueOnce('user@gmail.com');
453
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
454
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
454
455
  expect(result.content[0].text, msg).toContain('transient');
455
456
  }
456
457
  });
@@ -459,7 +460,7 @@ describe('runOrDiagnose', () => {
459
460
  vi.mocked(runner.run)
460
461
  .mockRejectedValueOnce(new Error('DEADLINE_EXCEEDED: context deadline exceeded'))
461
462
  .mockResolvedValueOnce('user@gmail.com');
462
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
463
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
463
464
  expect(result.content[0].text).toContain('transient');
464
465
  });
465
466
 
@@ -467,7 +468,7 @@ describe('runOrDiagnose', () => {
467
468
  vi.mocked(runner.run)
468
469
  .mockRejectedValueOnce(new Error('Request failed with status 404'))
469
470
  .mockResolvedValueOnce('user@gmail.com');
470
- const result = await runOrDiagnose(['sheets', 'get', 'abc', 'A1'], {});
471
+ const result = await runOrDiagnose(['sheets', 'get', pos('abc'), pos('A1')], {});
471
472
  expect(result.content[0].text).not.toContain('transient');
472
473
  });
473
474
 
@@ -475,7 +476,7 @@ describe('runOrDiagnose', () => {
475
476
  vi.mocked(runner.run)
476
477
  .mockRejectedValueOnce(new Error('Request failed with status 401'))
477
478
  .mockResolvedValueOnce('user@gmail.com');
478
- const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
479
+ const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
479
480
  expect(result.content[0].text).toContain('gog_auth_add');
480
481
  expect(result.content[0].text).not.toContain('transient');
481
482
  });
@@ -484,7 +485,7 @@ describe('runOrDiagnose', () => {
484
485
  vi.mocked(runner.run)
485
486
  .mockRejectedValueOnce(new Error('Request failed with status 503'))
486
487
  .mockRejectedValueOnce(new Error('auth list failed'));
487
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
488
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
488
489
  expect(result.content[0].text).toContain('transient');
489
490
  expect(result.content[0].text).not.toContain('Configured accounts');
490
491
  });
@@ -493,7 +494,7 @@ describe('runOrDiagnose', () => {
493
494
  vi.mocked(runner.run)
494
495
  .mockRejectedValueOnce(new Error('Range (Sheet1!AP1:AW1) exceeds grid limits. Max rows: 1000, max columns: 41'))
495
496
  .mockResolvedValueOnce('user@gmail.com');
496
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'AP1'], {});
497
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('AP1')], {});
497
498
  expect(result.content[0].text).toContain('exceeds grid limits');
498
499
  expect(result.content[0].text).toContain('gog_sheets_insert');
499
500
  });
@@ -502,7 +503,7 @@ describe('runOrDiagnose', () => {
502
503
  vi.mocked(runner.run)
503
504
  .mockRejectedValueOnce(new Error('Spreadsheet not found'))
504
505
  .mockResolvedValueOnce('user@gmail.com');
505
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
506
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
506
507
  expect(result.content[0].text).not.toContain('gog_sheets_insert');
507
508
  });
508
509
 
@@ -510,7 +511,7 @@ describe('runOrDiagnose', () => {
510
511
  vi.mocked(runner.run)
511
512
  .mockRejectedValueOnce(new Error('exceeds grid limits. Max rows: 1000, max columns: 41'))
512
513
  .mockRejectedValueOnce(new Error('auth list failed'));
513
- const result = await runOrDiagnose(['sheets', 'update', 'abc', 'A1'], {});
514
+ const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
514
515
  expect(result.content[0].text).toContain('gog_sheets_insert');
515
516
  expect(result.content[0].text).not.toContain('Configured accounts');
516
517
  });
package/vitest.config.ts CHANGED
@@ -20,6 +20,11 @@ export default defineConfig({
20
20
  GOG_ACCOUNT: '',
21
21
  GOG_PATH: '',
22
22
  GOG_READONLY: '',
23
+ // The arg-shape tests pass arbitrary server paths (/tmp/shot.png,
24
+ // /path/to/file); confinement is exercised explicitly by the tests that
25
+ // narrow GOG_FILE_ROOTS themselves (tests/file-roots.test.ts and the
26
+ // per-tool "refuses a path outside" cases).
27
+ GOG_FILE_ROOTS: '/',
23
28
  },
24
29
  coverage: {
25
30
  provider: 'v8',