gogcli-mcp 1.0.3 → 1.0.4
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 +91 -16
- package/{gogcli-mcp-1.0.3.skill → gogcli-mcp-1.0.4.skill} +0 -0
- package/manifest.json +25 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/runner.ts +8 -6
- package/src/tools/auth.ts +2 -6
- package/src/tools/docs.ts +83 -1
- package/src/tools/utils.ts +11 -4
- package/tests/tools/auth.test.ts +2 -2
- package/tests/tools/docs.test.ts +187 -0
- package/tests/tools/utils.test.ts +79 -0
package/dist/index.js
CHANGED
|
@@ -30138,8 +30138,8 @@ async function run(args, options = {}) {
|
|
|
30138
30138
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
30139
30139
|
return new Promise((resolve, reject) => {
|
|
30140
30140
|
const child = spawner(process.env.GOG_PATH ?? "gog", fullArgs, { env: process.env });
|
|
30141
|
-
|
|
30142
|
-
|
|
30141
|
+
const stdoutChunks = [];
|
|
30142
|
+
const stderrChunks = [];
|
|
30143
30143
|
let settled = false;
|
|
30144
30144
|
const timer = setTimeout(() => {
|
|
30145
30145
|
settled = true;
|
|
@@ -30147,23 +30147,25 @@ async function run(args, options = {}) {
|
|
|
30147
30147
|
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
30148
30148
|
}, effectiveTimeout);
|
|
30149
30149
|
child.stdout.on("data", (chunk) => {
|
|
30150
|
-
|
|
30150
|
+
stdoutChunks.push(chunk);
|
|
30151
30151
|
});
|
|
30152
30152
|
child.stderr.on("data", (chunk) => {
|
|
30153
|
-
|
|
30153
|
+
stderrChunks.push(chunk);
|
|
30154
30154
|
});
|
|
30155
30155
|
child.on("close", (code) => {
|
|
30156
30156
|
clearTimeout(timer);
|
|
30157
30157
|
if (settled) return;
|
|
30158
30158
|
settled = true;
|
|
30159
|
+
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
30160
|
+
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
30159
30161
|
if (code === 0) {
|
|
30160
|
-
if (interactive && stderr
|
|
30162
|
+
if (interactive && stderr) {
|
|
30161
30163
|
resolve(stdout + "\n" + stderr);
|
|
30162
30164
|
} else {
|
|
30163
30165
|
resolve(stdout);
|
|
30164
30166
|
}
|
|
30165
30167
|
} else {
|
|
30166
|
-
reject(new Error(stderr
|
|
30168
|
+
reject(new Error(stderr || `gog exited with code ${code}`));
|
|
30167
30169
|
}
|
|
30168
30170
|
});
|
|
30169
30171
|
child.on("error", (err) => {
|
|
@@ -30185,19 +30187,24 @@ function toText(output) {
|
|
|
30185
30187
|
function toError(err) {
|
|
30186
30188
|
return toText(err instanceof Error ? `Error: ${err.message}` : String(err));
|
|
30187
30189
|
}
|
|
30190
|
+
var AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
|
|
30191
|
+
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.";
|
|
30188
30192
|
async function runOrDiagnose(args, options) {
|
|
30189
30193
|
try {
|
|
30190
30194
|
return toText(await run(args, options));
|
|
30191
30195
|
} catch (err) {
|
|
30192
30196
|
const base = toError(err);
|
|
30197
|
+
const errText = base.content[0].text;
|
|
30198
|
+
const isAuthError = AUTH_ERROR_PATTERN.test(errText);
|
|
30199
|
+
const hint = isAuthError ? AUTH_HINT : "";
|
|
30193
30200
|
try {
|
|
30194
30201
|
const accounts = await run(["auth", "list"]);
|
|
30195
|
-
return toText(`${
|
|
30202
|
+
return toText(`${errText}
|
|
30196
30203
|
|
|
30197
30204
|
Configured accounts:
|
|
30198
|
-
${accounts}`);
|
|
30205
|
+
${accounts}${hint}`);
|
|
30199
30206
|
} catch {
|
|
30200
|
-
return
|
|
30207
|
+
return toText(`${errText}${hint}`);
|
|
30201
30208
|
}
|
|
30202
30209
|
}
|
|
30203
30210
|
}
|
|
@@ -30264,11 +30271,7 @@ function registerAuthTools(server2) {
|
|
|
30264
30271
|
args: external_exports3.array(external_exports3.string()).describe("Additional positional args and flags")
|
|
30265
30272
|
}
|
|
30266
30273
|
}, async ({ subcommand, args }) => {
|
|
30267
|
-
|
|
30268
|
-
return toText(await run(["auth", subcommand, ...args]));
|
|
30269
|
-
} catch (err) {
|
|
30270
|
-
return toError(err);
|
|
30271
|
-
}
|
|
30274
|
+
return runOrDiagnose(["auth", subcommand, ...args], {});
|
|
30272
30275
|
});
|
|
30273
30276
|
}
|
|
30274
30277
|
|
|
@@ -30481,7 +30484,6 @@ function registerDocsTools(server2) {
|
|
|
30481
30484
|
});
|
|
30482
30485
|
server2.registerTool("gog_docs_create", {
|
|
30483
30486
|
description: "Create a new Google Doc. Returns JSON with the new docId and URL.",
|
|
30484
|
-
annotations: { destructiveHint: false },
|
|
30485
30487
|
inputSchema: {
|
|
30486
30488
|
title: external_exports3.string().describe("Title for the new document"),
|
|
30487
30489
|
account: accountParam
|
|
@@ -30525,6 +30527,79 @@ function registerDocsTools(server2) {
|
|
|
30525
30527
|
}, async ({ docId, account }) => {
|
|
30526
30528
|
return runOrDiagnose(["docs", "structure", docId], { account });
|
|
30527
30529
|
});
|
|
30530
|
+
server2.registerTool("gog_docs_comments_list", {
|
|
30531
|
+
description: "List comments on a Google Doc. Returns open comments by default; set includeResolved=true to include resolved comments.",
|
|
30532
|
+
annotations: { readOnlyHint: true },
|
|
30533
|
+
inputSchema: {
|
|
30534
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30535
|
+
includeResolved: external_exports3.boolean().optional().describe("Include resolved comments (default: false, open only)"),
|
|
30536
|
+
account: accountParam
|
|
30537
|
+
}
|
|
30538
|
+
}, async ({ docId, includeResolved, account }) => {
|
|
30539
|
+
const args = ["docs", "comments", "list", docId];
|
|
30540
|
+
if (includeResolved) args.push("--include-resolved");
|
|
30541
|
+
return runOrDiagnose(args, { account });
|
|
30542
|
+
});
|
|
30543
|
+
server2.registerTool("gog_docs_comments_get", {
|
|
30544
|
+
description: "Get a single comment by ID, including its replies.",
|
|
30545
|
+
annotations: { readOnlyHint: true },
|
|
30546
|
+
inputSchema: {
|
|
30547
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30548
|
+
commentId: external_exports3.string().describe("Comment ID"),
|
|
30549
|
+
account: accountParam
|
|
30550
|
+
}
|
|
30551
|
+
}, async ({ docId, commentId, account }) => {
|
|
30552
|
+
return runOrDiagnose(["docs", "comments", "get", docId, commentId], { account });
|
|
30553
|
+
});
|
|
30554
|
+
server2.registerTool("gog_docs_comments_add", {
|
|
30555
|
+
description: "Add a comment to a Google Doc. Optionally attach quoted text that appears as the highlighted passage in the Google Docs UI.",
|
|
30556
|
+
inputSchema: {
|
|
30557
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30558
|
+
content: external_exports3.string().describe("Comment text"),
|
|
30559
|
+
quoted: external_exports3.string().optional().describe("Quoted text to attach to the comment (shown in UIs when available)"),
|
|
30560
|
+
account: accountParam
|
|
30561
|
+
}
|
|
30562
|
+
}, async ({ docId, content, quoted, account }) => {
|
|
30563
|
+
const args = ["docs", "comments", "add", docId, content];
|
|
30564
|
+
if (quoted) args.push(`--quoted=${quoted}`);
|
|
30565
|
+
return runOrDiagnose(args, { account });
|
|
30566
|
+
});
|
|
30567
|
+
server2.registerTool("gog_docs_comments_reply", {
|
|
30568
|
+
description: "Reply to an existing comment on a Google Doc.",
|
|
30569
|
+
inputSchema: {
|
|
30570
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30571
|
+
commentId: external_exports3.string().describe("Comment ID to reply to"),
|
|
30572
|
+
content: external_exports3.string().describe("Reply text"),
|
|
30573
|
+
account: accountParam
|
|
30574
|
+
}
|
|
30575
|
+
}, async ({ docId, commentId, content, account }) => {
|
|
30576
|
+
return runOrDiagnose(["docs", "comments", "reply", docId, commentId, content], { account });
|
|
30577
|
+
});
|
|
30578
|
+
server2.registerTool("gog_docs_comments_resolve", {
|
|
30579
|
+
description: "Resolve a comment (mark as done). Optionally include a closing message.",
|
|
30580
|
+
annotations: { destructiveHint: true },
|
|
30581
|
+
inputSchema: {
|
|
30582
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30583
|
+
commentId: external_exports3.string().describe("Comment ID to resolve"),
|
|
30584
|
+
message: external_exports3.string().optional().describe("Optional message to include when resolving"),
|
|
30585
|
+
account: accountParam
|
|
30586
|
+
}
|
|
30587
|
+
}, async ({ docId, commentId, message, account }) => {
|
|
30588
|
+
const args = ["docs", "comments", "resolve", docId, commentId];
|
|
30589
|
+
if (message) args.push(`--message=${message}`);
|
|
30590
|
+
return runOrDiagnose(args, { account });
|
|
30591
|
+
});
|
|
30592
|
+
server2.registerTool("gog_docs_comments_delete", {
|
|
30593
|
+
description: "Delete a comment from a Google Doc. This action is permanent.",
|
|
30594
|
+
annotations: { destructiveHint: true },
|
|
30595
|
+
inputSchema: {
|
|
30596
|
+
docId: external_exports3.string().describe("Doc ID (from the URL)"),
|
|
30597
|
+
commentId: external_exports3.string().describe("Comment ID to delete"),
|
|
30598
|
+
account: accountParam
|
|
30599
|
+
}
|
|
30600
|
+
}, async ({ docId, commentId, account }) => {
|
|
30601
|
+
return runOrDiagnose(["docs", "comments", "delete", docId, commentId], { account });
|
|
30602
|
+
});
|
|
30528
30603
|
server2.registerTool("gog_docs_run", {
|
|
30529
30604
|
description: "Run any gog docs subcommand not covered by the other tools. Run `gog docs --help` for the full list of subcommands, or `gog docs <subcommand> --help` for flags on a specific subcommand.",
|
|
30530
30605
|
annotations: { destructiveHint: true },
|
|
@@ -30890,7 +30965,7 @@ function registerTasksTools(server2) {
|
|
|
30890
30965
|
}
|
|
30891
30966
|
|
|
30892
30967
|
// src/index.ts
|
|
30893
|
-
var server = new McpServer({ name: "gogcli", version: "1.0.
|
|
30968
|
+
var server = new McpServer({ name: "gogcli", version: "1.0.4" });
|
|
30894
30969
|
registerAuthTools(server);
|
|
30895
30970
|
registerCalendarTools(server);
|
|
30896
30971
|
registerContactsTools(server);
|
|
Binary file
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp",
|
|
5
5
|
"display_name": "gogcli",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.4",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
|
@@ -236,6 +236,30 @@
|
|
|
236
236
|
"name": "gog_docs_structure",
|
|
237
237
|
"description": "Show document structure with numbered paragraphs"
|
|
238
238
|
},
|
|
239
|
+
{
|
|
240
|
+
"name": "gog_docs_comments_list",
|
|
241
|
+
"description": "List comments on a Google Doc"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "gog_docs_comments_get",
|
|
245
|
+
"description": "Get a single comment by ID with replies"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "gog_docs_comments_add",
|
|
249
|
+
"description": "Add a comment to a Google Doc"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"name": "gog_docs_comments_reply",
|
|
253
|
+
"description": "Reply to a comment on a Google Doc"
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
"name": "gog_docs_comments_resolve",
|
|
257
|
+
"description": "Resolve (mark as done) a comment on a Google Doc"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"name": "gog_docs_comments_delete",
|
|
261
|
+
"description": "Delete a comment from a Google Doc"
|
|
262
|
+
},
|
|
239
263
|
{
|
|
240
264
|
"name": "gog_docs_run",
|
|
241
265
|
"description": "Run any gog docs subcommand (escape hatch)"
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { registerGmailTools } from './tools/gmail.js';
|
|
|
10
10
|
import { registerSheetsTools } from './tools/sheets.js';
|
|
11
11
|
import { registerTasksTools } from './tools/tasks.js';
|
|
12
12
|
|
|
13
|
-
const server = new McpServer({ name: 'gogcli', version: '1.0.
|
|
13
|
+
const server = new McpServer({ name: 'gogcli', version: '1.0.4' });
|
|
14
14
|
|
|
15
15
|
registerAuthTools(server);
|
|
16
16
|
registerCalendarTools(server);
|
package/src/runner.ts
CHANGED
|
@@ -43,8 +43,8 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
|
|
|
43
43
|
|
|
44
44
|
return new Promise((resolve, reject) => {
|
|
45
45
|
const child = spawner(process.env.GOG_PATH ?? 'gog', fullArgs, { env: process.env });
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const stdoutChunks: Buffer[] = [];
|
|
47
|
+
const stderrChunks: Buffer[] = [];
|
|
48
48
|
let settled = false;
|
|
49
49
|
|
|
50
50
|
const timer = setTimeout(() => {
|
|
@@ -53,21 +53,23 @@ export async function run(args: string[], options: RunOptions = {}): Promise<str
|
|
|
53
53
|
reject(new Error(`gog timed out after ${formatTimeout(effectiveTimeout)}`));
|
|
54
54
|
}, effectiveTimeout);
|
|
55
55
|
|
|
56
|
-
child.stdout!.on('data', (chunk: Buffer) => {
|
|
57
|
-
child.stderr!.on('data', (chunk: Buffer) => {
|
|
56
|
+
child.stdout!.on('data', (chunk: Buffer) => { stdoutChunks.push(chunk); });
|
|
57
|
+
child.stderr!.on('data', (chunk: Buffer) => { stderrChunks.push(chunk); });
|
|
58
58
|
|
|
59
59
|
child.on('close', (code: number | null) => {
|
|
60
60
|
clearTimeout(timer);
|
|
61
61
|
if (settled) return;
|
|
62
62
|
settled = true;
|
|
63
|
+
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
64
|
+
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
63
65
|
if (code === 0) {
|
|
64
|
-
if (interactive && stderr
|
|
66
|
+
if (interactive && stderr) {
|
|
65
67
|
resolve(stdout + '\n' + stderr);
|
|
66
68
|
} else {
|
|
67
69
|
resolve(stdout);
|
|
68
70
|
}
|
|
69
71
|
} else {
|
|
70
|
-
reject(new Error(stderr
|
|
72
|
+
reject(new Error(stderr || `gog exited with code ${code}`));
|
|
71
73
|
}
|
|
72
74
|
});
|
|
73
75
|
|
package/src/tools/auth.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { run } from '../runner.js';
|
|
4
|
-
import { toText, toError } from './utils.js';
|
|
4
|
+
import { toText, toError, runOrDiagnose } from './utils.js';
|
|
5
5
|
|
|
6
6
|
export function registerAuthTools(server: McpServer): void {
|
|
7
7
|
server.registerTool('gog_auth_list', {
|
|
@@ -73,10 +73,6 @@ export function registerAuthTools(server: McpServer): void {
|
|
|
73
73
|
args: z.array(z.string()).describe('Additional positional args and flags'),
|
|
74
74
|
},
|
|
75
75
|
}, async ({ subcommand, args }) => {
|
|
76
|
-
|
|
77
|
-
return toText(await run(['auth', subcommand, ...args]));
|
|
78
|
-
} catch (err) {
|
|
79
|
-
return toError(err);
|
|
80
|
-
}
|
|
76
|
+
return runOrDiagnose(['auth', subcommand, ...args], {});
|
|
81
77
|
});
|
|
82
78
|
}
|
package/src/tools/docs.ts
CHANGED
|
@@ -27,7 +27,6 @@ export function registerDocsTools(server: McpServer): void {
|
|
|
27
27
|
|
|
28
28
|
server.registerTool('gog_docs_create', {
|
|
29
29
|
description: 'Create a new Google Doc. Returns JSON with the new docId and URL.',
|
|
30
|
-
annotations: { destructiveHint: false },
|
|
31
30
|
inputSchema: {
|
|
32
31
|
title: z.string().describe('Title for the new document'),
|
|
33
32
|
account: accountParam,
|
|
@@ -75,6 +74,89 @@ export function registerDocsTools(server: McpServer): void {
|
|
|
75
74
|
return runOrDiagnose(['docs', 'structure', docId], { account });
|
|
76
75
|
});
|
|
77
76
|
|
|
77
|
+
// --- Comments tools ---
|
|
78
|
+
|
|
79
|
+
server.registerTool('gog_docs_comments_list', {
|
|
80
|
+
description:
|
|
81
|
+
'List comments on a Google Doc. Returns open comments by default; set includeResolved=true to include resolved comments.',
|
|
82
|
+
annotations: { readOnlyHint: true },
|
|
83
|
+
inputSchema: {
|
|
84
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
85
|
+
includeResolved: z.boolean().optional().describe('Include resolved comments (default: false, open only)'),
|
|
86
|
+
account: accountParam,
|
|
87
|
+
},
|
|
88
|
+
}, async ({ docId, includeResolved, account }) => {
|
|
89
|
+
const args = ['docs', 'comments', 'list', docId];
|
|
90
|
+
if (includeResolved) args.push('--include-resolved');
|
|
91
|
+
return runOrDiagnose(args, { account });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
server.registerTool('gog_docs_comments_get', {
|
|
95
|
+
description: 'Get a single comment by ID, including its replies.',
|
|
96
|
+
annotations: { readOnlyHint: true },
|
|
97
|
+
inputSchema: {
|
|
98
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
99
|
+
commentId: z.string().describe('Comment ID'),
|
|
100
|
+
account: accountParam,
|
|
101
|
+
},
|
|
102
|
+
}, async ({ docId, commentId, account }) => {
|
|
103
|
+
return runOrDiagnose(['docs', 'comments', 'get', docId, commentId], { account });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
server.registerTool('gog_docs_comments_add', {
|
|
107
|
+
description:
|
|
108
|
+
'Add a comment to a Google Doc. Optionally attach quoted text that appears as the highlighted passage in the Google Docs UI.',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
111
|
+
content: z.string().describe('Comment text'),
|
|
112
|
+
quoted: z.string().optional().describe('Quoted text to attach to the comment (shown in UIs when available)'),
|
|
113
|
+
account: accountParam,
|
|
114
|
+
},
|
|
115
|
+
}, async ({ docId, content, quoted, account }) => {
|
|
116
|
+
const args = ['docs', 'comments', 'add', docId, content];
|
|
117
|
+
if (quoted) args.push(`--quoted=${quoted}`);
|
|
118
|
+
return runOrDiagnose(args, { account });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
server.registerTool('gog_docs_comments_reply', {
|
|
122
|
+
description: 'Reply to an existing comment on a Google Doc.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
125
|
+
commentId: z.string().describe('Comment ID to reply to'),
|
|
126
|
+
content: z.string().describe('Reply text'),
|
|
127
|
+
account: accountParam,
|
|
128
|
+
},
|
|
129
|
+
}, async ({ docId, commentId, content, account }) => {
|
|
130
|
+
return runOrDiagnose(['docs', 'comments', 'reply', docId, commentId, content], { account });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
server.registerTool('gog_docs_comments_resolve', {
|
|
134
|
+
description: 'Resolve a comment (mark as done). Optionally include a closing message.',
|
|
135
|
+
annotations: { destructiveHint: true },
|
|
136
|
+
inputSchema: {
|
|
137
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
138
|
+
commentId: z.string().describe('Comment ID to resolve'),
|
|
139
|
+
message: z.string().optional().describe('Optional message to include when resolving'),
|
|
140
|
+
account: accountParam,
|
|
141
|
+
},
|
|
142
|
+
}, async ({ docId, commentId, message, account }) => {
|
|
143
|
+
const args = ['docs', 'comments', 'resolve', docId, commentId];
|
|
144
|
+
if (message) args.push(`--message=${message}`);
|
|
145
|
+
return runOrDiagnose(args, { account });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.registerTool('gog_docs_comments_delete', {
|
|
149
|
+
description: 'Delete a comment from a Google Doc. This action is permanent.',
|
|
150
|
+
annotations: { destructiveHint: true },
|
|
151
|
+
inputSchema: {
|
|
152
|
+
docId: z.string().describe('Doc ID (from the URL)'),
|
|
153
|
+
commentId: z.string().describe('Comment ID to delete'),
|
|
154
|
+
account: accountParam,
|
|
155
|
+
},
|
|
156
|
+
}, async ({ docId, commentId, account }) => {
|
|
157
|
+
return runOrDiagnose(['docs', 'comments', 'delete', docId, commentId], { account });
|
|
158
|
+
});
|
|
159
|
+
|
|
78
160
|
server.registerTool('gog_docs_run', {
|
|
79
161
|
description: 'Run any gog docs subcommand not covered by the other tools. Run `gog docs --help` for the full list of subcommands, or `gog docs <subcommand> --help` for flags on a specific subcommand.',
|
|
80
162
|
annotations: { destructiveHint: true },
|
package/src/tools/utils.ts
CHANGED
|
@@ -15,8 +15,12 @@ export function toError(err: unknown): ToolResult {
|
|
|
15
15
|
return toText(err instanceof Error ? `Error: ${err.message}` : String(err));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
|
|
19
|
+
|
|
20
|
+
const AUTH_HINT =
|
|
21
|
+
'\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. ' +
|
|
22
|
+
'Ask the user if they would like to re-authenticate.';
|
|
23
|
+
|
|
20
24
|
export async function runOrDiagnose(
|
|
21
25
|
args: string[],
|
|
22
26
|
options: { account?: string },
|
|
@@ -25,11 +29,14 @@ export async function runOrDiagnose(
|
|
|
25
29
|
return toText(await run(args, options));
|
|
26
30
|
} catch (err) {
|
|
27
31
|
const base = toError(err);
|
|
32
|
+
const errText = base.content[0].text;
|
|
33
|
+
const isAuthError = AUTH_ERROR_PATTERN.test(errText);
|
|
34
|
+
const hint = isAuthError ? AUTH_HINT : '';
|
|
28
35
|
try {
|
|
29
36
|
const accounts = await run(['auth', 'list']);
|
|
30
|
-
return toText(`${
|
|
37
|
+
return toText(`${errText}\n\nConfigured accounts:\n${accounts}${hint}`);
|
|
31
38
|
} catch {
|
|
32
|
-
return
|
|
39
|
+
return toText(`${errText}${hint}`);
|
|
33
40
|
}
|
|
34
41
|
}
|
|
35
42
|
}
|
package/tests/tools/auth.test.ts
CHANGED
|
@@ -121,7 +121,7 @@ describe('gog_auth_run', () => {
|
|
|
121
121
|
vi.mocked(runner.run).mockResolvedValue('removed user@gmail.com');
|
|
122
122
|
const handlers = setupHandlers();
|
|
123
123
|
const result = await handlers.get('gog_auth_run')!({ subcommand: 'remove', args: ['user@gmail.com'] });
|
|
124
|
-
expect(runner.run).toHaveBeenCalledWith(['auth', 'remove', 'user@gmail.com']);
|
|
124
|
+
expect(runner.run).toHaveBeenCalledWith(['auth', 'remove', 'user@gmail.com'], {});
|
|
125
125
|
expect(result.content[0].text).toBe('removed user@gmail.com');
|
|
126
126
|
});
|
|
127
127
|
|
|
@@ -129,7 +129,7 @@ describe('gog_auth_run', () => {
|
|
|
129
129
|
vi.mocked(runner.run).mockResolvedValue('token info');
|
|
130
130
|
const handlers = setupHandlers();
|
|
131
131
|
await handlers.get('gog_auth_run')!({ subcommand: 'tokens', args: [] });
|
|
132
|
-
expect(runner.run).toHaveBeenCalledWith(['auth', 'tokens']);
|
|
132
|
+
expect(runner.run).toHaveBeenCalledWith(['auth', 'tokens'], {});
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
it('returns error text on failure', async () => {
|
package/tests/tools/docs.test.ts
CHANGED
|
@@ -161,6 +161,193 @@ describe('gog_docs_structure', () => {
|
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
+
// --- Comments tools ---
|
|
165
|
+
|
|
166
|
+
describe('gog_docs_comments_list', () => {
|
|
167
|
+
it('calls run with correct args for open comments', async () => {
|
|
168
|
+
vi.mocked(runner.run).mockResolvedValue('[{"id":"c1","content":"Fix this"}]');
|
|
169
|
+
const handlers = setupHandlers();
|
|
170
|
+
const result = await handlers.get('gog_docs_comments_list')!({ docId: 'abc' });
|
|
171
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'comments', 'list', 'abc'], { account: undefined });
|
|
172
|
+
expect(result.content[0].text).toContain('Fix this');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('includes --include-resolved when set', async () => {
|
|
176
|
+
vi.mocked(runner.run).mockResolvedValue('[]');
|
|
177
|
+
const handlers = setupHandlers();
|
|
178
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', includeResolved: true });
|
|
179
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
180
|
+
['docs', 'comments', 'list', 'abc', '--include-resolved'],
|
|
181
|
+
{ account: undefined },
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('omits --include-resolved when false', async () => {
|
|
186
|
+
vi.mocked(runner.run).mockResolvedValue('[]');
|
|
187
|
+
const handlers = setupHandlers();
|
|
188
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', includeResolved: false });
|
|
189
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'comments', 'list', 'abc'], { account: undefined });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('forwards account override', async () => {
|
|
193
|
+
vi.mocked(runner.run).mockResolvedValue('[]');
|
|
194
|
+
const handlers = setupHandlers();
|
|
195
|
+
await handlers.get('gog_docs_comments_list')!({ docId: 'abc', account: 'other@gmail.com' });
|
|
196
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
197
|
+
['docs', 'comments', 'list', 'abc'],
|
|
198
|
+
{ account: 'other@gmail.com' },
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns error text on failure', async () => {
|
|
203
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('List failed'));
|
|
204
|
+
const handlers = setupHandlers();
|
|
205
|
+
const result = await handlers.get('gog_docs_comments_list')!({ docId: 'bad' });
|
|
206
|
+
expect(result.content[0].text).toContain('Error: List failed');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('gog_docs_comments_get', () => {
|
|
211
|
+
it('calls run with correct args', async () => {
|
|
212
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c1","content":"Fix this","replies":[]}');
|
|
213
|
+
const handlers = setupHandlers();
|
|
214
|
+
const result = await handlers.get('gog_docs_comments_get')!({ docId: 'abc', commentId: 'c1' });
|
|
215
|
+
expect(runner.run).toHaveBeenCalledWith(['docs', 'comments', 'get', 'abc', 'c1'], { account: undefined });
|
|
216
|
+
expect(result.content[0].text).toContain('Fix this');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('returns error text on failure', async () => {
|
|
220
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Not found'));
|
|
221
|
+
const handlers = setupHandlers();
|
|
222
|
+
const result = await handlers.get('gog_docs_comments_get')!({ docId: 'abc', commentId: 'bad' });
|
|
223
|
+
expect(result.content[0].text).toContain('Error: Not found');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('gog_docs_comments_add', () => {
|
|
228
|
+
it('calls run with content', async () => {
|
|
229
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c2"}');
|
|
230
|
+
const handlers = setupHandlers();
|
|
231
|
+
await handlers.get('gog_docs_comments_add')!({ docId: 'abc', content: 'Please review' });
|
|
232
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
233
|
+
['docs', 'comments', 'add', 'abc', 'Please review'],
|
|
234
|
+
{ account: undefined },
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('includes --quoted when provided', async () => {
|
|
239
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c2"}');
|
|
240
|
+
const handlers = setupHandlers();
|
|
241
|
+
await handlers.get('gog_docs_comments_add')!({
|
|
242
|
+
docId: 'abc',
|
|
243
|
+
content: 'Typo here',
|
|
244
|
+
quoted: 'teh',
|
|
245
|
+
});
|
|
246
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
247
|
+
['docs', 'comments', 'add', 'abc', 'Typo here', '--quoted=teh'],
|
|
248
|
+
{ account: undefined },
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('omits --quoted when not provided', async () => {
|
|
253
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"c2"}');
|
|
254
|
+
const handlers = setupHandlers();
|
|
255
|
+
await handlers.get('gog_docs_comments_add')!({ docId: 'abc', content: 'Nice' });
|
|
256
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
257
|
+
['docs', 'comments', 'add', 'abc', 'Nice'],
|
|
258
|
+
{ account: undefined },
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('returns error text on failure', async () => {
|
|
263
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Add failed'));
|
|
264
|
+
const handlers = setupHandlers();
|
|
265
|
+
const result = await handlers.get('gog_docs_comments_add')!({ docId: 'bad', content: 'x' });
|
|
266
|
+
expect(result.content[0].text).toContain('Error: Add failed');
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('gog_docs_comments_reply', () => {
|
|
271
|
+
it('calls run with correct args', async () => {
|
|
272
|
+
vi.mocked(runner.run).mockResolvedValue('{"id":"r1"}');
|
|
273
|
+
const handlers = setupHandlers();
|
|
274
|
+
await handlers.get('gog_docs_comments_reply')!({ docId: 'abc', commentId: 'c1', content: 'Done' });
|
|
275
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
276
|
+
['docs', 'comments', 'reply', 'abc', 'c1', 'Done'],
|
|
277
|
+
{ account: undefined },
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns error text on failure', async () => {
|
|
282
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Reply failed'));
|
|
283
|
+
const handlers = setupHandlers();
|
|
284
|
+
const result = await handlers.get('gog_docs_comments_reply')!({
|
|
285
|
+
docId: 'abc', commentId: 'c1', content: 'x',
|
|
286
|
+
});
|
|
287
|
+
expect(result.content[0].text).toContain('Error: Reply failed');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('gog_docs_comments_resolve', () => {
|
|
292
|
+
it('calls run with correct args', async () => {
|
|
293
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
294
|
+
const handlers = setupHandlers();
|
|
295
|
+
await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
296
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
297
|
+
['docs', 'comments', 'resolve', 'abc', 'c1'],
|
|
298
|
+
{ account: undefined },
|
|
299
|
+
);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('includes --message when provided', async () => {
|
|
303
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
304
|
+
const handlers = setupHandlers();
|
|
305
|
+
await handlers.get('gog_docs_comments_resolve')!({
|
|
306
|
+
docId: 'abc', commentId: 'c1', message: 'Fixed in v2',
|
|
307
|
+
});
|
|
308
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
309
|
+
['docs', 'comments', 'resolve', 'abc', 'c1', '--message=Fixed in v2'],
|
|
310
|
+
{ account: undefined },
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('omits --message when not provided', async () => {
|
|
315
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
316
|
+
const handlers = setupHandlers();
|
|
317
|
+
await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
318
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
319
|
+
['docs', 'comments', 'resolve', 'abc', 'c1'],
|
|
320
|
+
{ account: undefined },
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('returns error text on failure', async () => {
|
|
325
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Resolve failed'));
|
|
326
|
+
const handlers = setupHandlers();
|
|
327
|
+
const result = await handlers.get('gog_docs_comments_resolve')!({ docId: 'abc', commentId: 'c1' });
|
|
328
|
+
expect(result.content[0].text).toContain('Error: Resolve failed');
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
describe('gog_docs_comments_delete', () => {
|
|
333
|
+
it('calls run with correct args', async () => {
|
|
334
|
+
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
335
|
+
const handlers = setupHandlers();
|
|
336
|
+
await handlers.get('gog_docs_comments_delete')!({ docId: 'abc', commentId: 'c1' });
|
|
337
|
+
expect(runner.run).toHaveBeenCalledWith(
|
|
338
|
+
['docs', 'comments', 'delete', 'abc', 'c1'],
|
|
339
|
+
{ account: undefined },
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('returns error text on failure', async () => {
|
|
344
|
+
vi.mocked(runner.run).mockRejectedValue(new Error('Delete failed'));
|
|
345
|
+
const handlers = setupHandlers();
|
|
346
|
+
const result = await handlers.get('gog_docs_comments_delete')!({ docId: 'abc', commentId: 'c1' });
|
|
347
|
+
expect(result.content[0].text).toContain('Error: Delete failed');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
164
351
|
describe('gog_docs_run', () => {
|
|
165
352
|
it('passes raw subcommand and args to runner', async () => {
|
|
166
353
|
vi.mocked(runner.run).mockResolvedValue('{}');
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as runner from '../../src/runner.js';
|
|
3
|
+
import { runOrDiagnose } from '../../src/tools/utils.js';
|
|
4
|
+
|
|
5
|
+
vi.mock('../../src/runner.js');
|
|
6
|
+
|
|
7
|
+
beforeEach(() => vi.clearAllMocks());
|
|
8
|
+
|
|
9
|
+
describe('runOrDiagnose', () => {
|
|
10
|
+
it('returns output text on success', async () => {
|
|
11
|
+
vi.mocked(runner.run).mockResolvedValue('{"ok":true}');
|
|
12
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
13
|
+
expect(result.content[0].text).toBe('{"ok":true}');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('appends auth list on non-auth failure', async () => {
|
|
17
|
+
vi.mocked(runner.run)
|
|
18
|
+
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
19
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
20
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
21
|
+
expect(result.content[0].text).toBe(
|
|
22
|
+
'Error: Doc not found\n\nConfigured accounts:\nuser@gmail.com',
|
|
23
|
+
);
|
|
24
|
+
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('appends re-auth hint on 401 error', async () => {
|
|
28
|
+
vi.mocked(runner.run)
|
|
29
|
+
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
30
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
31
|
+
const result = await runOrDiagnose(['docs', 'comments', 'list', 'abc'], {});
|
|
32
|
+
expect(result.content[0].text).toContain('Error: Request failed with status 401');
|
|
33
|
+
expect(result.content[0].text).toContain('Configured accounts:\nuser@gmail.com');
|
|
34
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('appends re-auth hint on "unauthorized" error', async () => {
|
|
38
|
+
vi.mocked(runner.run)
|
|
39
|
+
.mockRejectedValueOnce(new Error('unauthorized access'))
|
|
40
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
41
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
42
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('appends re-auth hint on "token expired" error', async () => {
|
|
46
|
+
vi.mocked(runner.run)
|
|
47
|
+
.mockRejectedValueOnce(new Error('token has been expired or revoked'))
|
|
48
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
49
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
50
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('appends re-auth hint on "invalid_grant" error', async () => {
|
|
54
|
+
vi.mocked(runner.run)
|
|
55
|
+
.mockRejectedValueOnce(new Error('invalid_grant'))
|
|
56
|
+
.mockResolvedValueOnce('user@gmail.com');
|
|
57
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
58
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns plain error with auth hint when auth list also fails on auth error', async () => {
|
|
62
|
+
vi.mocked(runner.run)
|
|
63
|
+
.mockRejectedValueOnce(new Error('Request failed with status 401'))
|
|
64
|
+
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
65
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
66
|
+
expect(result.content[0].text).toContain('Error: Request failed with status 401');
|
|
67
|
+
expect(result.content[0].text).toContain('gog_auth_add');
|
|
68
|
+
expect(result.content[0].text).not.toContain('Configured accounts');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('returns plain error when auth list also fails on non-auth error', async () => {
|
|
72
|
+
vi.mocked(runner.run)
|
|
73
|
+
.mockRejectedValueOnce(new Error('Doc not found'))
|
|
74
|
+
.mockRejectedValueOnce(new Error('auth list failed'));
|
|
75
|
+
const result = await runOrDiagnose(['docs', 'cat', 'abc'], {});
|
|
76
|
+
expect(result.content[0].text).toBe('Error: Doc not found');
|
|
77
|
+
expect(result.content[0].text).not.toContain('gog_auth_add');
|
|
78
|
+
});
|
|
79
|
+
});
|