gogcli-mcp 4.5.0 → 4.5.2
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +659 -162
- package/dist/lib.js +619 -142
- package/manifest.json +7 -7
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/dispatch-confirmation.ts +63 -3
- package/src/gmail-dispatch-guard.ts +19 -12
- package/src/lib.ts +6 -2
- package/src/tools/api.ts +74 -8
- package/src/tools/appscript.ts +63 -2
- package/src/tools/calendar.ts +83 -13
- package/src/tools/chat.ts +35 -4
- package/src/tools/classroom.ts +254 -25
- package/src/tools/docs.ts +7 -1
- package/src/tools/drive.ts +82 -10
- package/src/tools/gmail.ts +37 -1
- package/src/tools/sheets.ts +16 -1
- package/src/tools/utils.ts +10 -4
- package/tests/gmail-dispatch-guard.test.ts +24 -8
- package/tests/tools/api.test.ts +130 -5
- package/tests/tools/appscript.test.ts +6 -2
- package/tests/tools/dispatch-gates-more.test.ts +447 -0
- package/tests/tools/dispatch-gates.test.ts +52 -1
- package/tests/tools/docs.test.ts +5 -6
- package/tests/tools/drive.test.ts +2 -5
- package/tests/tools/run-vets-more.test.ts +269 -0
- package/tests/tools/run-vets.test.ts +158 -4
- package/tests/tools/sheets.test.ts +10 -11
- package/tests/tools/utils.test.ts +38 -41
|
@@ -217,8 +217,7 @@ describe('runOrDiagnose', () => {
|
|
|
217
217
|
// mask, and retrying it would just spend a second call to fail identically.
|
|
218
218
|
it('does NOT retry an error that is not a rejected mask', async () => {
|
|
219
219
|
vi.mocked(runner.run)
|
|
220
|
-
.mockRejectedValueOnce(new Error('File not found'))
|
|
221
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
220
|
+
.mockRejectedValueOnce(new Error('File not found'));
|
|
222
221
|
const result = await runOrDiagnose(['drive', 'ls'], { fieldsMask: 'files(id)' });
|
|
223
222
|
expect(result.isError).toBe(true);
|
|
224
223
|
expect(runner.run).not.toHaveBeenNthCalledWith(2, ['drive', 'ls'], expect.anything());
|
|
@@ -260,34 +259,35 @@ describe('runOrDiagnose', () => {
|
|
|
260
259
|
expect(result.content[0].text).toBe('user@gmail.com');
|
|
261
260
|
});
|
|
262
261
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
// PRIV-1 (fleet-audit #1012): the account list is PII and only helps when
|
|
263
|
+
// the failure is about WHICH account is signed in. A "Doc not found" told the
|
|
264
|
+
// model every identity on the host for nothing — and cost a `gog auth list`
|
|
265
|
+
// spawn on every error path.
|
|
266
|
+
it('omits the account list on a non-auth failure and does not spawn auth list', async () => {
|
|
267
|
+
vi.mocked(runner.run).mockRejectedValueOnce(new Error('Doc not found'));
|
|
267
268
|
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
268
|
-
expect(result.content[0].text).toBe(
|
|
269
|
-
'Error: Doc not found\n\nConfigured accounts:\nuser@gmail.com',
|
|
270
|
-
);
|
|
269
|
+
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
271
270
|
// mcp-utils errorResult flags diagnosed failures for the client.
|
|
272
271
|
expect(result.isError).toBe(true);
|
|
273
272
|
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
273
|
+
expect(runner.run).toHaveBeenCalledTimes(1);
|
|
274
274
|
});
|
|
275
275
|
|
|
276
276
|
it('redacts scopes/subject/timestamps from the configured-accounts block', async () => {
|
|
277
277
|
const authListJson = JSON.stringify({
|
|
278
278
|
accounts: [{
|
|
279
|
-
email: '
|
|
279
|
+
email: 'user@example.com',
|
|
280
280
|
subject: '109876543210987654321',
|
|
281
281
|
scopes: ['https://www.googleapis.com/auth/gmail.modify'],
|
|
282
282
|
created_at: '2026-01-02T03:04:05Z',
|
|
283
283
|
}],
|
|
284
284
|
});
|
|
285
285
|
vi.mocked(runner.run)
|
|
286
|
-
.mockRejectedValueOnce(new Error('
|
|
286
|
+
.mockRejectedValueOnce(new Error('Google API error (401 authError): invalid credentials'))
|
|
287
287
|
.mockResolvedValueOnce(authListJson);
|
|
288
288
|
const result = await runOrDiagnose(['gmail', 'drafts', 'delete', pos('r123')], {});
|
|
289
289
|
const text = result.content[0].text;
|
|
290
|
-
expect(text).toContain('Configured accounts:\
|
|
290
|
+
expect(text).toContain('Configured accounts:\nuser@example.com');
|
|
291
291
|
expect(text).not.toContain('scopes');
|
|
292
292
|
expect(text).not.toContain('gmail.modify');
|
|
293
293
|
expect(text).not.toContain('109876543210987654321');
|
|
@@ -296,10 +296,19 @@ describe('runOrDiagnose', () => {
|
|
|
296
296
|
|
|
297
297
|
it('shows (none) when no accounts are configured', async () => {
|
|
298
298
|
vi.mocked(runner.run)
|
|
299
|
-
.mockRejectedValueOnce(new Error('
|
|
299
|
+
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
300
300
|
.mockResolvedValueOnce('{"accounts":[]}');
|
|
301
301
|
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
302
|
-
expect(result.content[0].text).
|
|
302
|
+
expect(result.content[0].text).toContain('Error: Request failed with status 401\n\nConfigured accounts:\n(none)');
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('appends the account list on a dead refresh token even when a transient signal is present', async () => {
|
|
306
|
+
vi.mocked(runner.run)
|
|
307
|
+
.mockRejectedValueOnce(new Error('503: token has been expired or revoked'))
|
|
308
|
+
.mockResolvedValueOnce('user@example.com');
|
|
309
|
+
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
310
|
+
expect(result.content[0].text).toContain('Configured accounts:\nuser@example.com');
|
|
311
|
+
expect(result.content[0].text).toContain('invalid_grant');
|
|
303
312
|
});
|
|
304
313
|
|
|
305
314
|
it('appends re-auth hint on 401 error', async () => {
|
|
@@ -345,8 +354,7 @@ describe('runOrDiagnose', () => {
|
|
|
345
354
|
// `invalid_grant` is exempt from this and stays auth (below) — it is the
|
|
346
355
|
// one signal that definitively means the refresh token is dead.
|
|
347
356
|
vi.mocked(runner.run)
|
|
348
|
-
.mockRejectedValueOnce(new Error('429 rateLimitExceeded: the access token expired mid-request, retry'))
|
|
349
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
357
|
+
.mockRejectedValueOnce(new Error('429 rateLimitExceeded: the access token expired mid-request, retry'));
|
|
350
358
|
const result = await runOrDiagnose(['sheets', 'get', pos('A1')], {});
|
|
351
359
|
const text = result.content[0].text as string;
|
|
352
360
|
expect(text).toContain('often transient');
|
|
@@ -374,8 +382,7 @@ describe('runOrDiagnose', () => {
|
|
|
374
382
|
// exercised — and gog's real errors are frequently one long line.
|
|
375
383
|
.mockRejectedValueOnce(
|
|
376
384
|
new Error('page token accepted; the requested export link has expired and must be regenerated'),
|
|
377
|
-
)
|
|
378
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
385
|
+
);
|
|
379
386
|
const result = await runOrDiagnose(['drive', pos('export'), pos('abc')], {});
|
|
380
387
|
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
381
388
|
});
|
|
@@ -415,10 +422,9 @@ describe('runOrDiagnose', () => {
|
|
|
415
422
|
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
416
423
|
});
|
|
417
424
|
|
|
418
|
-
it('returns plain error
|
|
425
|
+
it('returns plain error on a non-auth error (auth list is never consulted)', async () => {
|
|
419
426
|
vi.mocked(runner.run)
|
|
420
|
-
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
421
|
-
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
427
|
+
.mockRejectedValueOnce(new Error('Doc not found'));
|
|
422
428
|
const result = await runOrDiagnose(['docs', 'cat', pos('abc')], {});
|
|
423
429
|
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
424
430
|
expect(result.isError).toBe(true);
|
|
@@ -427,8 +433,7 @@ describe('runOrDiagnose', () => {
|
|
|
427
433
|
|
|
428
434
|
it('appends transient-retry hint on 429 error', async () => {
|
|
429
435
|
vi.mocked(runner.run)
|
|
430
|
-
.mockRejectedValueOnce(new Error('Request failed with status 429'))
|
|
431
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
436
|
+
.mockRejectedValueOnce(new Error('Request failed with status 429'));
|
|
432
437
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
433
438
|
expect(result.content[0].text).toContain('transient');
|
|
434
439
|
expect(result.content[0].text).toContain('Retry');
|
|
@@ -438,8 +443,7 @@ describe('runOrDiagnose', () => {
|
|
|
438
443
|
for (const status of [500, 502, 503, 504]) {
|
|
439
444
|
vi.clearAllMocks();
|
|
440
445
|
vi.mocked(runner.run)
|
|
441
|
-
.mockRejectedValueOnce(new Error(`Request failed with status ${status}`))
|
|
442
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
446
|
+
.mockRejectedValueOnce(new Error(`Request failed with status ${status}`));
|
|
443
447
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
444
448
|
expect(result.content[0].text, `status ${status}`).toContain('transient');
|
|
445
449
|
}
|
|
@@ -449,8 +453,7 @@ describe('runOrDiagnose', () => {
|
|
|
449
453
|
for (const msg of ['Quota exceeded', 'rateLimitExceeded', 'userRateLimitExceeded']) {
|
|
450
454
|
vi.clearAllMocks();
|
|
451
455
|
vi.mocked(runner.run)
|
|
452
|
-
.mockRejectedValueOnce(new Error(msg))
|
|
453
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
456
|
+
.mockRejectedValueOnce(new Error(msg));
|
|
454
457
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
455
458
|
expect(result.content[0].text, msg).toContain('transient');
|
|
456
459
|
}
|
|
@@ -458,16 +461,14 @@ describe('runOrDiagnose', () => {
|
|
|
458
461
|
|
|
459
462
|
it('appends transient-retry hint on DEADLINE_EXCEEDED error', async () => {
|
|
460
463
|
vi.mocked(runner.run)
|
|
461
|
-
.mockRejectedValueOnce(new Error('DEADLINE_EXCEEDED: context deadline exceeded'))
|
|
462
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
464
|
+
.mockRejectedValueOnce(new Error('DEADLINE_EXCEEDED: context deadline exceeded'));
|
|
463
465
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
464
466
|
expect(result.content[0].text).toContain('transient');
|
|
465
467
|
});
|
|
466
468
|
|
|
467
469
|
it('does not append transient hint on 404 error', async () => {
|
|
468
470
|
vi.mocked(runner.run)
|
|
469
|
-
.mockRejectedValueOnce(new Error('Request failed with status 404'))
|
|
470
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
471
|
+
.mockRejectedValueOnce(new Error('Request failed with status 404'));
|
|
471
472
|
const result = await runOrDiagnose(['sheets', 'get', pos('abc'), pos('A1')], {});
|
|
472
473
|
expect(result.content[0].text).not.toContain('transient');
|
|
473
474
|
});
|
|
@@ -481,10 +482,9 @@ describe('runOrDiagnose', () => {
|
|
|
481
482
|
expect(result.content[0].text).not.toContain('transient');
|
|
482
483
|
});
|
|
483
484
|
|
|
484
|
-
it('keeps transient hint
|
|
485
|
+
it('keeps the transient hint and names no accounts on a transient failure', async () => {
|
|
485
486
|
vi.mocked(runner.run)
|
|
486
|
-
.mockRejectedValueOnce(new Error('Request failed with status 503'))
|
|
487
|
-
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
487
|
+
.mockRejectedValueOnce(new Error('Request failed with status 503'));
|
|
488
488
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
489
489
|
expect(result.content[0].text).toContain('transient');
|
|
490
490
|
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
@@ -492,8 +492,7 @@ describe('runOrDiagnose', () => {
|
|
|
492
492
|
|
|
493
493
|
it('appends grid-limit hint pointing at gog_sheets_insert', async () => {
|
|
494
494
|
vi.mocked(runner.run)
|
|
495
|
-
.mockRejectedValueOnce(new Error('Range (Sheet1!AP1:AW1) exceeds grid limits. Max rows: 1000, max columns: 41'))
|
|
496
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
495
|
+
.mockRejectedValueOnce(new Error('Range (Sheet1!AP1:AW1) exceeds grid limits. Max rows: 1000, max columns: 41'));
|
|
497
496
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('AP1')], {});
|
|
498
497
|
expect(result.content[0].text).toContain('exceeds grid limits');
|
|
499
498
|
expect(result.content[0].text).toContain('gog_sheets_insert');
|
|
@@ -501,16 +500,14 @@ describe('runOrDiagnose', () => {
|
|
|
501
500
|
|
|
502
501
|
it('does not append grid-limit hint on unrelated errors', async () => {
|
|
503
502
|
vi.mocked(runner.run)
|
|
504
|
-
.mockRejectedValueOnce(new Error('Spreadsheet not found'))
|
|
505
|
-
.mockResolvedValueOnce('user@gmail.com');
|
|
503
|
+
.mockRejectedValueOnce(new Error('Spreadsheet not found'));
|
|
506
504
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
507
505
|
expect(result.content[0].text).not.toContain('gog_sheets_insert');
|
|
508
506
|
});
|
|
509
507
|
|
|
510
|
-
it('keeps grid-limit hint
|
|
508
|
+
it('keeps the grid-limit hint and names no accounts on a grid failure', async () => {
|
|
511
509
|
vi.mocked(runner.run)
|
|
512
|
-
.mockRejectedValueOnce(new Error('exceeds grid limits. Max rows: 1000, max columns: 41'))
|
|
513
|
-
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
510
|
+
.mockRejectedValueOnce(new Error('exceeds grid limits. Max rows: 1000, max columns: 41'));
|
|
514
511
|
const result = await runOrDiagnose(['sheets', 'update', pos('abc'), pos('A1')], {});
|
|
515
512
|
expect(result.content[0].text).toContain('gog_sheets_insert');
|
|
516
513
|
expect(result.content[0].text).not.toContain('Configured accounts');
|