gogcli-mcp 2.25.0 → 2.27.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +10 -5
- package/SKILL.md +11 -6
- package/dist/index.js +553 -128
- package/dist/lib.js +480 -89
- package/manifest.json +89 -1
- package/mint.yaml +106 -0
- package/package.json +5 -5
- package/server.json +2 -2
- package/src/gmail-results.ts +29 -2
- package/src/lib.ts +8 -0
- package/src/runner.ts +1 -1
- package/src/server.ts +6 -0
- package/src/tools/appscript.ts +173 -0
- package/src/tools/calendar.ts +56 -2
- package/src/tools/chat.ts +253 -0
- package/src/tools/gmail.ts +136 -5
- package/src/tools/utils.ts +20 -0
- package/src/worker.ts +1 -1
- package/tests/gmail-results.test.ts +47 -0
- package/tests/server.test.ts +61 -0
- package/tests/tools/appscript.test.ts +159 -0
- package/tests/tools/calendar.test.ts +121 -0
- package/tests/tools/chat.test.ts +284 -0
- package/tests/tools/gmail.test.ts +320 -0
|
@@ -260,6 +260,25 @@ describe('gog_gmail_send', () => {
|
|
|
260
260
|
);
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
// gog's --quote is opt-in on `gmail send` and default-on for `gmail reply`;
|
|
264
|
+
// a threaded send without it arrives with the original nowhere in the body.
|
|
265
|
+
it('appends --quote only when quote is set', async () => {
|
|
266
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
267
|
+
const harness = await setupHandlers();
|
|
268
|
+
await harness.callTool('gog_gmail_send', {
|
|
269
|
+
to: 'bob@example.com', subject: 'Re: Hi', body: 'Sure',
|
|
270
|
+
replyToMessageId: 'msg1', quote: true,
|
|
271
|
+
});
|
|
272
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
273
|
+
[
|
|
274
|
+
'gmail', 'send',
|
|
275
|
+
'--to=bob@example.com', '--subject=Re: Hi', '--body=Sure',
|
|
276
|
+
'--reply-to-message-id=msg1', '--quote',
|
|
277
|
+
],
|
|
278
|
+
{ account: undefined },
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
263
282
|
it('appends one --attach flag per file path', async () => {
|
|
264
283
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
265
284
|
const harness = await setupHandlers();
|
|
@@ -423,3 +442,304 @@ describe('gog_gmail_run', () => {
|
|
|
423
442
|
expect(result.content[0].text).toBe('Error: Run failed');
|
|
424
443
|
});
|
|
425
444
|
});
|
|
445
|
+
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// REPLY / REPLY-ALL. The base package used to expose gog_gmail_send as its only
|
|
448
|
+
// Gmail write, so replying meant send + replyToMessageId: that threads the
|
|
449
|
+
// message (In-Reply-To/References) but quotes nothing, inherits no recipients
|
|
450
|
+
// and no "Re:" subject — the reply lands looking like a brand-new message.
|
|
451
|
+
// gog's `gmail reply` quotes BY DEFAULT (opt out with --no-quote), which is why
|
|
452
|
+
// these are separate tools rather than flags on send.
|
|
453
|
+
// ============================================================================
|
|
454
|
+
describe.each([
|
|
455
|
+
['gog_gmail_reply', 'reply'],
|
|
456
|
+
['gog_gmail_reply_all', 'reply-all'],
|
|
457
|
+
])('%s', (tool, subcommand) => {
|
|
458
|
+
it('calls run with the message id and body, quoting by default', async () => {
|
|
459
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"msg9"}');
|
|
460
|
+
const harness = await setupHandlers();
|
|
461
|
+
await harness.callTool(tool, { messageId: 'msg1', body: 'Sounds good' });
|
|
462
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
463
|
+
['gmail', subcommand, 'msg1', '--body=Sounds good', '--auto-from-addressed-alias=false'],
|
|
464
|
+
{ account: undefined },
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('appends --no-quote only when noQuote is set', async () => {
|
|
469
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
470
|
+
const harness = await setupHandlers();
|
|
471
|
+
await harness.callTool(tool, { messageId: 'msg1', body: 'Ack', noQuote: true });
|
|
472
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
473
|
+
['gmail', subcommand, 'msg1', '--body=Ack', '--no-quote', '--auto-from-addressed-alias=false'],
|
|
474
|
+
{ account: undefined },
|
|
475
|
+
);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('appends optional flags when provided', async () => {
|
|
479
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
480
|
+
const harness = await setupHandlers();
|
|
481
|
+
await harness.callTool(tool, {
|
|
482
|
+
messageId: 'msg1',
|
|
483
|
+
body: 'Adding Carol',
|
|
484
|
+
to: ['bob@example.com'],
|
|
485
|
+
cc: ['carol@example.com'],
|
|
486
|
+
bcc: ['dave@example.com'],
|
|
487
|
+
remove: ['eve@example.com'],
|
|
488
|
+
subject: 'Re: Custom',
|
|
489
|
+
from: 'me@example.com',
|
|
490
|
+
account: 'me@example.com',
|
|
491
|
+
});
|
|
492
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
493
|
+
[
|
|
494
|
+
'gmail', subcommand, 'msg1', '--body=Adding Carol',
|
|
495
|
+
'--to=bob@example.com', '--cc=carol@example.com', '--bcc=dave@example.com',
|
|
496
|
+
'--remove=eve@example.com', '--subject=Re: Custom', '--from=me@example.com',
|
|
497
|
+
'--auto-from-addressed-alias=false',
|
|
498
|
+
],
|
|
499
|
+
{ account: 'me@example.com' },
|
|
500
|
+
);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('pins --auto-from-addressed-alias on when requested', async () => {
|
|
504
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
505
|
+
const harness = await setupHandlers();
|
|
506
|
+
await harness.callTool(tool, { messageId: 'msg1', body: 'Hi', autoFromAddressedAlias: true });
|
|
507
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
508
|
+
['gmail', subcommand, 'msg1', '--body=Hi', '--auto-from-addressed-alias'],
|
|
509
|
+
{ account: undefined },
|
|
510
|
+
);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it('appends one --attach flag per file path', async () => {
|
|
514
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
515
|
+
const harness = await setupHandlers();
|
|
516
|
+
await harness.callTool(tool, {
|
|
517
|
+
messageId: 'msg1',
|
|
518
|
+
body: 'See attached',
|
|
519
|
+
attach: ['/tmp/shot.png', '/tmp/notes.pdf'],
|
|
520
|
+
});
|
|
521
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
522
|
+
[
|
|
523
|
+
'gmail', subcommand, 'msg1', '--body=See attached',
|
|
524
|
+
'--attach=/tmp/shot.png', '--attach=/tmp/notes.pdf',
|
|
525
|
+
'--auto-from-addressed-alias=false',
|
|
526
|
+
],
|
|
527
|
+
{ account: undefined },
|
|
528
|
+
);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('turns attachInline bytes into a --attach file arg', async () => {
|
|
532
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
533
|
+
const harness = await setupHandlers();
|
|
534
|
+
const bytes = Buffer.from('hi').toString('base64');
|
|
535
|
+
await harness.callTool(tool, {
|
|
536
|
+
messageId: 'msg1',
|
|
537
|
+
body: 'Bytes',
|
|
538
|
+
attachInline: [{ filename: 'a.txt', contentBase64: bytes }],
|
|
539
|
+
});
|
|
540
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
541
|
+
[
|
|
542
|
+
'gmail', subcommand, 'msg1', '--body=Bytes',
|
|
543
|
+
{ kind: 'file', flag: 'attach', contents: bytes, encoding: 'base64', filename: 'a.txt' },
|
|
544
|
+
'--auto-from-addressed-alias=false',
|
|
545
|
+
],
|
|
546
|
+
{ account: undefined },
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('routes an oversize body to --body-file', async () => {
|
|
551
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
552
|
+
const harness = await setupHandlers();
|
|
553
|
+
const big = 'x'.repeat(PAYLOAD_INLINE_MAX + 1);
|
|
554
|
+
await harness.callTool(tool, { messageId: 'msg1', body: big });
|
|
555
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
556
|
+
[
|
|
557
|
+
'gmail', subcommand, 'msg1',
|
|
558
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
559
|
+
'--auto-from-addressed-alias=false',
|
|
560
|
+
],
|
|
561
|
+
{ account: undefined },
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('returns error text on failure', async () => {
|
|
566
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Reply failed'));
|
|
567
|
+
const harness = await setupHandlers();
|
|
568
|
+
const result = await harness.callTool(tool, { messageId: 'msg1', body: 'x' });
|
|
569
|
+
expect(result.content[0].text).toBe('Error: Reply failed');
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Moved here with the tools themselves: reply/reply-all used to live in the
|
|
574
|
+
// gmail sub-package, which now imports replySchema/appendReplyFlags from this
|
|
575
|
+
// package instead of declaring a second copy.
|
|
576
|
+
describe('gog_gmail_reply — full flag set', () => {
|
|
577
|
+
it('calls runOrDiagnose with messageId and --body', async () => {
|
|
578
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
579
|
+
const harness = await setupHandlers();
|
|
580
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Thanks' });
|
|
581
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
582
|
+
['gmail', 'reply', 'm1', '--body=Thanks', '--auto-from-addressed-alias=false'],
|
|
583
|
+
{ account: undefined },
|
|
584
|
+
);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('passes all reply flags including repeatable recipients', async () => {
|
|
588
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
589
|
+
const harness = await setupHandlers();
|
|
590
|
+
await harness.callTool('gog_gmail_reply', {
|
|
591
|
+
messageId: 'm1',
|
|
592
|
+
body: 'Hi',
|
|
593
|
+
bodyHtml: '<p>Hi</p>',
|
|
594
|
+
to: ['a@b.com', 'c@d.com'],
|
|
595
|
+
cc: ['cc@x.com'],
|
|
596
|
+
bcc: ['bcc@x.com'],
|
|
597
|
+
remove: ['old@x.com'],
|
|
598
|
+
subject: 'New subject',
|
|
599
|
+
noQuote: true,
|
|
600
|
+
attach: ['/tmp/a.pdf', '/tmp/b.pdf'],
|
|
601
|
+
from: 'me@x.com',
|
|
602
|
+
signature: true,
|
|
603
|
+
signatureFrom: 'alias@x.com',
|
|
604
|
+
signatureFile: '/tmp/sig.txt',
|
|
605
|
+
account: 'me@gmail.com',
|
|
606
|
+
});
|
|
607
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
608
|
+
[
|
|
609
|
+
'gmail', 'reply', 'm1',
|
|
610
|
+
'--body=Hi',
|
|
611
|
+
'--body-html=<p>Hi</p>',
|
|
612
|
+
'--to=a@b.com',
|
|
613
|
+
'--to=c@d.com',
|
|
614
|
+
'--cc=cc@x.com',
|
|
615
|
+
'--bcc=bcc@x.com',
|
|
616
|
+
'--remove=old@x.com',
|
|
617
|
+
'--subject=New subject',
|
|
618
|
+
'--no-quote',
|
|
619
|
+
'--attach=/tmp/a.pdf',
|
|
620
|
+
'--attach=/tmp/b.pdf',
|
|
621
|
+
'--from=me@x.com',
|
|
622
|
+
'--signature',
|
|
623
|
+
'--signature-from=alias@x.com',
|
|
624
|
+
'--signature-file=/tmp/sig.txt', '--auto-from-addressed-alias=false'
|
|
625
|
+
],
|
|
626
|
+
{ account: 'me@gmail.com' },
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('omits --no-quote and --signature when false', async () => {
|
|
631
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
632
|
+
const harness = await setupHandlers();
|
|
633
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', noQuote: false, signature: false });
|
|
634
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
635
|
+
['gmail', 'reply', 'm1', '--body=Hi', '--auto-from-addressed-alias=false'],
|
|
636
|
+
{ account: undefined },
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
describe('gog_gmail_reply_all — full flag set', () => {
|
|
642
|
+
it('uses the reply-all subcommand', async () => {
|
|
643
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
644
|
+
const harness = await setupHandlers();
|
|
645
|
+
await harness.callTool('gog_gmail_reply_all', { messageId: 'm1', body: 'Thanks all' });
|
|
646
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
647
|
+
['gmail', 'reply-all', 'm1', '--body=Thanks all', '--auto-from-addressed-alias=false'],
|
|
648
|
+
{ account: undefined },
|
|
649
|
+
);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('passes repeatable recipient and signature flags', async () => {
|
|
653
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
654
|
+
const harness = await setupHandlers();
|
|
655
|
+
await harness.callTool('gog_gmail_reply_all', {
|
|
656
|
+
messageId: 'm1',
|
|
657
|
+
bodyHtml: '<p>Hi</p>',
|
|
658
|
+
cc: ['x@y.com', 'z@y.com'],
|
|
659
|
+
remove: ['drop@y.com'],
|
|
660
|
+
signatureFile: '/tmp/sig.html',
|
|
661
|
+
});
|
|
662
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
663
|
+
[
|
|
664
|
+
'gmail', 'reply-all', 'm1',
|
|
665
|
+
'--body-html=<p>Hi</p>',
|
|
666
|
+
'--cc=x@y.com',
|
|
667
|
+
'--cc=z@y.com',
|
|
668
|
+
'--remove=drop@y.com',
|
|
669
|
+
'--signature-file=/tmp/sig.html', '--auto-from-addressed-alias=false'
|
|
670
|
+
],
|
|
671
|
+
{ account: undefined },
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
describe('gog_gmail_reply body-vs-file conflicts', () => {
|
|
676
|
+
it('rejects bodyHtml plus bodyHtmlFile before gog runs', async () => {
|
|
677
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
678
|
+
const harness = await setupHandlers();
|
|
679
|
+
const res = await harness.callTool('gog_gmail_reply', {
|
|
680
|
+
messageId: 'm1', bodyHtml: '<p>Hi</p>', bodyHtmlFile: '/tmp/b.html',
|
|
681
|
+
});
|
|
682
|
+
expect(res.isError).toBe(true);
|
|
683
|
+
expect(res.content[0].text).toContain('bodyHtml and bodyHtmlFile are mutually exclusive');
|
|
684
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('treats an empty-string bodyHtml as supplied, so it still conflicts', async () => {
|
|
688
|
+
// Guards the `!== undefined` check against a falsy-but-present value
|
|
689
|
+
// sliding through to gog, which rejects the pair regardless of content.
|
|
690
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
691
|
+
const harness = await setupHandlers();
|
|
692
|
+
const res = await harness.callTool('gog_gmail_reply', {
|
|
693
|
+
messageId: 'm1', body: 'B', bodyHtml: '', bodyHtmlFile: '/tmp/b.html',
|
|
694
|
+
});
|
|
695
|
+
expect(res.isError).toBe(true);
|
|
696
|
+
expect(runner.run).not.toHaveBeenCalled();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it('passes bodyHtmlFile alone through as --body-html-file', async () => {
|
|
700
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
701
|
+
const harness = await setupHandlers();
|
|
702
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: 'Hi', bodyHtmlFile: '/tmp/b.html' });
|
|
703
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
704
|
+
['gmail', 'reply', 'm1', '--body=Hi', '--body-html-file=/tmp/b.html', '--auto-from-addressed-alias=false'],
|
|
705
|
+
{ account: undefined },
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('routes a large body and bodyHtml to file args', async () => {
|
|
710
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
711
|
+
const harness = await setupHandlers();
|
|
712
|
+
const big = 'x'.repeat(PAYLOAD_INLINE_MAX + 1);
|
|
713
|
+
const bigHtml = `<p>${'y'.repeat(PAYLOAD_INLINE_MAX)}</p>`;
|
|
714
|
+
await harness.callTool('gog_gmail_reply', { messageId: 'm1', body: big, bodyHtml: bigHtml });
|
|
715
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
716
|
+
[
|
|
717
|
+
'gmail', 'reply', 'm1',
|
|
718
|
+
{ kind: 'file', flag: 'body-file', contents: big, ext: undefined },
|
|
719
|
+
{ kind: 'file', flag: 'body-html-file', contents: bigHtml, ext: 'html' },
|
|
720
|
+
'--auto-from-addressed-alias=false',
|
|
721
|
+
],
|
|
722
|
+
{ account: undefined },
|
|
723
|
+
);
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// gog reads "-" from stdin, but runner.ts spawns with default stdio and never
|
|
728
|
+
// writes to or closes the child's stdin, so a "-" here hangs until the 30s
|
|
729
|
+
// timeout. No file param may advertise it as usable.
|
|
730
|
+
describe('reply file params never advertise stdin as usable', () => {
|
|
731
|
+
it.each(['gog_gmail_reply', 'gog_gmail_reply_all'])('%s.bodyHtmlFile warns that stdin hangs', async (tool) => {
|
|
732
|
+
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
|
|
733
|
+
const server = new McpServer({ name: 'test', version: '0.0.0' });
|
|
734
|
+
const configs = new Map<string, Record<string, { description?: string }>>();
|
|
735
|
+
vi.spyOn(server, 'registerTool').mockImplementation((name, config) => {
|
|
736
|
+
configs.set(name, (config as { inputSchema: Record<string, { description?: string }> }).inputSchema);
|
|
737
|
+
return undefined as never;
|
|
738
|
+
});
|
|
739
|
+
registerGmailTools(server);
|
|
740
|
+
const desc = configs.get(tool)?.bodyHtmlFile?.description ?? '';
|
|
741
|
+
expect(desc).not.toBe('');
|
|
742
|
+
expect(desc).not.toMatch(/(?:or|use)\s+"?-"?\s+(?:for|to read)/i);
|
|
743
|
+
expect(desc).toMatch(/stdin/i);
|
|
744
|
+
});
|
|
745
|
+
});
|