google-tools-mcp 1.2.12 → 2.0.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/LICENSE +21 -21
- package/README.md +537 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +128 -0
- package/dist/markdown-transformer/index.js +17 -7
- package/dist/markdown-transformer/markdownToDocs.js +185 -27
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +12 -0
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +197 -196
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +30 -6
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +71 -8
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- package/package.json +82 -81
|
@@ -4,7 +4,7 @@ import { getDocsClient } from '../../clients.js';
|
|
|
4
4
|
import { DocumentIdParameter, TextFindParameter, TextStyleParameters, ParagraphStyleParameters } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
6
|
import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
|
|
7
|
-
import { guardMutation, trackMutation } from '../../readTracker.js';
|
|
7
|
+
import { guardMutation, getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
8
8
|
const RangeTarget = z
|
|
9
9
|
.object({
|
|
10
10
|
startIndex: z.number().int().min(1).describe('Start of range (inclusive, 1-based).'),
|
|
@@ -110,7 +110,11 @@ export function register(server) {
|
|
|
110
110
|
await guardMutation(args.documentId, {
|
|
111
111
|
contentFetcher: async () => {
|
|
112
112
|
const current = await docs.documents.get({ documentId: args.documentId });
|
|
113
|
-
|
|
113
|
+
// Return the revision this content came from alongside the
|
|
114
|
+
// content itself so guardMutation can refresh both together
|
|
115
|
+
// instead of leaving revisionId stale after a diff (see
|
|
116
|
+
// readTracker.js guardMutation for why that matters).
|
|
117
|
+
return { content: docsJsonToMarkdown(current.data), revisionId: current.data.revisionId };
|
|
114
118
|
},
|
|
115
119
|
});
|
|
116
120
|
log.info(`modifyText on doc ${args.documentId}: target=${JSON.stringify(args.target)}` +
|
|
@@ -173,8 +177,14 @@ export function register(server) {
|
|
|
173
177
|
if (requests.length === 0) {
|
|
174
178
|
return 'No operations to perform.';
|
|
175
179
|
}
|
|
176
|
-
|
|
177
|
-
|
|
180
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
181
|
+
const writeResponse = await GDocsHelpers.executeBatchUpdate(
|
|
182
|
+
docs,
|
|
183
|
+
args.documentId,
|
|
184
|
+
requests,
|
|
185
|
+
revisionId ? { requiredRevisionId: revisionId } : undefined
|
|
186
|
+
);
|
|
187
|
+
trackMutation(args.documentId, writeResponse?.writeControl?.requiredRevisionId);
|
|
178
188
|
// Build descriptive result
|
|
179
189
|
const actions = [];
|
|
180
190
|
if (endIndex !== undefined && normalizedText === '')
|
|
@@ -2,91 +2,91 @@ import { describe, it } from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { buildModifyTextRequests } from './modifyText.js';
|
|
4
4
|
import { stripMarkdownListMarkersForSearch } from '../../googleDocsApiHelpers.js';
|
|
5
|
-
|
|
6
|
-
describe('buildModifyTextRequests', () => {
|
|
7
|
-
it('generates deleteContentRange + insertText for a replacement', () => {
|
|
8
|
-
const reqs = buildModifyTextRequests({
|
|
9
|
-
startIndex: 5,
|
|
10
|
-
endIndex: 10,
|
|
11
|
-
text: 'hello',
|
|
12
|
-
});
|
|
13
|
-
assert.equal(reqs.length, 2);
|
|
14
|
-
assert.deepEqual(reqs[0], {
|
|
15
|
-
deleteContentRange: { range: { startIndex: 5, endIndex: 10 } },
|
|
16
|
-
});
|
|
17
|
-
assert.deepEqual(reqs[1], {
|
|
18
|
-
insertText: { location: { index: 5 }, text: 'hello' },
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('generates only insertText for an insertion (no endIndex)', () => {
|
|
23
|
-
const reqs = buildModifyTextRequests({
|
|
24
|
-
startIndex: 3,
|
|
25
|
-
endIndex: undefined,
|
|
26
|
-
text: 'hi',
|
|
27
|
-
});
|
|
28
|
-
assert.equal(reqs.length, 1);
|
|
29
|
-
assert.deepEqual(reqs[0], {
|
|
30
|
-
insertText: { location: { index: 3 }, text: 'hi' },
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('returns empty array when no text/style/paragraphStyle given', () => {
|
|
35
|
-
const reqs = buildModifyTextRequests({
|
|
36
|
-
startIndex: 1,
|
|
37
|
-
endIndex: 5,
|
|
38
|
-
});
|
|
39
|
-
assert.equal(reqs.length, 0);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('includes tabId in range and location when provided', () => {
|
|
43
|
-
const reqs = buildModifyTextRequests({
|
|
44
|
-
startIndex: 1,
|
|
45
|
-
endIndex: 4,
|
|
46
|
-
text: 'x',
|
|
47
|
-
tabId: 'tab1',
|
|
48
|
-
});
|
|
49
|
-
assert.deepEqual(reqs[0].deleteContentRange.range.tabId, 'tab1');
|
|
50
|
-
assert.deepEqual(reqs[1].insertText.location.tabId, 'tab1');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
5
|
+
|
|
6
|
+
describe('buildModifyTextRequests', () => {
|
|
7
|
+
it('generates deleteContentRange + insertText for a replacement', () => {
|
|
8
|
+
const reqs = buildModifyTextRequests({
|
|
9
|
+
startIndex: 5,
|
|
10
|
+
endIndex: 10,
|
|
11
|
+
text: 'hello',
|
|
12
|
+
});
|
|
13
|
+
assert.equal(reqs.length, 2);
|
|
14
|
+
assert.deepEqual(reqs[0], {
|
|
15
|
+
deleteContentRange: { range: { startIndex: 5, endIndex: 10 } },
|
|
16
|
+
});
|
|
17
|
+
assert.deepEqual(reqs[1], {
|
|
18
|
+
insertText: { location: { index: 5 }, text: 'hello' },
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('generates only insertText for an insertion (no endIndex)', () => {
|
|
23
|
+
const reqs = buildModifyTextRequests({
|
|
24
|
+
startIndex: 3,
|
|
25
|
+
endIndex: undefined,
|
|
26
|
+
text: 'hi',
|
|
27
|
+
});
|
|
28
|
+
assert.equal(reqs.length, 1);
|
|
29
|
+
assert.deepEqual(reqs[0], {
|
|
30
|
+
insertText: { location: { index: 3 }, text: 'hi' },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns empty array when no text/style/paragraphStyle given', () => {
|
|
35
|
+
const reqs = buildModifyTextRequests({
|
|
36
|
+
startIndex: 1,
|
|
37
|
+
endIndex: 5,
|
|
38
|
+
});
|
|
39
|
+
assert.equal(reqs.length, 0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('includes tabId in range and location when provided', () => {
|
|
43
|
+
const reqs = buildModifyTextRequests({
|
|
44
|
+
startIndex: 1,
|
|
45
|
+
endIndex: 4,
|
|
46
|
+
text: 'x',
|
|
47
|
+
tabId: 'tab1',
|
|
48
|
+
});
|
|
49
|
+
assert.deepEqual(reqs[0].deleteContentRange.range.tabId, 'tab1');
|
|
50
|
+
assert.deepEqual(reqs[1].insertText.location.tabId, 'tab1');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
54
|
describe('escape sequence normalization (issue #9)', () => {
|
|
55
|
-
// Simulate the normalization that happens in the execute handler
|
|
56
|
-
function normalize(text) {
|
|
57
|
-
return text?.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
it('converts literal \\n to real newline', () => {
|
|
61
|
-
const input = 'line one\\nline two\\nline three';
|
|
62
|
-
const result = normalize(input);
|
|
63
|
-
assert.equal(result, 'line one\nline two\nline three');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('converts literal \\t to real tab', () => {
|
|
67
|
-
const input = 'col1\\tcol2\\tcol3';
|
|
68
|
-
const result = normalize(input);
|
|
69
|
-
assert.equal(result, 'col1\tcol2\tcol3');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('handles mixed \\n and \\t', () => {
|
|
73
|
-
const input = 'row1\\tcol2\\nrow2\\tcol2';
|
|
74
|
-
const result = normalize(input);
|
|
75
|
-
assert.equal(result, 'row1\tcol2\nrow2\tcol2');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('leaves text without escape sequences unchanged', () => {
|
|
79
|
-
const input = 'no escapes here';
|
|
80
|
-
assert.equal(normalize(input), 'no escapes here');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('preserves real newlines that are already in the string', () => {
|
|
84
|
-
const input = 'already\nreal';
|
|
85
|
-
assert.equal(normalize(input), 'already\nreal');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('returns undefined for undefined input', () => {
|
|
89
|
-
assert.equal(normalize(undefined), undefined);
|
|
55
|
+
// Simulate the normalization that happens in the execute handler
|
|
56
|
+
function normalize(text) {
|
|
57
|
+
return text?.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
it('converts literal \\n to real newline', () => {
|
|
61
|
+
const input = 'line one\\nline two\\nline three';
|
|
62
|
+
const result = normalize(input);
|
|
63
|
+
assert.equal(result, 'line one\nline two\nline three');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('converts literal \\t to real tab', () => {
|
|
67
|
+
const input = 'col1\\tcol2\\tcol3';
|
|
68
|
+
const result = normalize(input);
|
|
69
|
+
assert.equal(result, 'col1\tcol2\tcol3');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('handles mixed \\n and \\t', () => {
|
|
73
|
+
const input = 'row1\\tcol2\\nrow2\\tcol2';
|
|
74
|
+
const result = normalize(input);
|
|
75
|
+
assert.equal(result, 'row1\tcol2\nrow2\tcol2');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('leaves text without escape sequences unchanged', () => {
|
|
79
|
+
const input = 'no escapes here';
|
|
80
|
+
assert.equal(normalize(input), 'no escapes here');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('preserves real newlines that are already in the string', () => {
|
|
84
|
+
const input = 'already\nreal';
|
|
85
|
+
assert.equal(normalize(input), 'already\nreal');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('returns undefined for undefined input', () => {
|
|
89
|
+
assert.equal(normalize(undefined), undefined);
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
92
|
|
|
@@ -4,8 +4,9 @@ import { createPatch } from 'diff';
|
|
|
4
4
|
import { getDocsClient, getDriveClient } from '../../clients.js';
|
|
5
5
|
import { DocumentIdParameter, NotImplementedError } from '../../types.js';
|
|
6
6
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
7
|
-
import { docsJsonToMarkdown } from '../../markdown-transformer/index.js';
|
|
7
|
+
import { docsJsonToMarkdown, checkMarkdownFidelity } from '../../markdown-transformer/index.js';
|
|
8
8
|
import { trackRead, getLastReadContent } from '../../readTracker.js';
|
|
9
|
+
import { writeWorkspaceFile } from '../../workspace.js';
|
|
9
10
|
|
|
10
11
|
async function fetchModifiedTime(documentId) {
|
|
11
12
|
try {
|
|
@@ -24,7 +25,10 @@ async function fetchModifiedTime(documentId) {
|
|
|
24
25
|
export function register(server) {
|
|
25
26
|
server.addTool({
|
|
26
27
|
name: 'readDocument',
|
|
27
|
-
description: "Reads the content of a Google Document. Returns markdown by default (formatted content suitable for editing and re-uploading with replaceDocumentWithMarkdown)
|
|
28
|
+
description: "Reads the content of a Google Document. Returns markdown by default (formatted content suitable for editing and re-uploading with replaceDocumentWithMarkdown) and saves it to a local working-copy file (path included in the response). " +
|
|
29
|
+
"PREFERRED EDITING WORKFLOW for large edits: (1) readDocument to get the local file path, (2) edit that file locally, (3) call replaceDocumentWithMarkdown with filePath pointing to it. This avoids inline content truncation and gives you a reviewable working copy before pushing changes. " +
|
|
30
|
+
"If the document contains content markdown cannot represent (images, footnotes), a warning is appended listing what replaceDocumentWithMarkdown would permanently remove — prefer modifyText or appendMarkdown for those documents. " +
|
|
31
|
+
"Use format='text' for plain text, or format='json' for the raw document structure. Set diffFromLastRead=true (markdown only) to get a unified diff from your previous read in this session instead of the full content.",
|
|
28
32
|
parameters: DocumentIdParameter.extend({
|
|
29
33
|
format: z
|
|
30
34
|
.enum(['text', 'json', 'markdown'])
|
|
@@ -53,7 +57,7 @@ export function register(server) {
|
|
|
53
57
|
const needsTabsContent = !!args.tabId;
|
|
54
58
|
const fields = args.format === 'json' || args.format === 'markdown'
|
|
55
59
|
? '*' // Get everything for structure analysis
|
|
56
|
-
: 'body(content(paragraph(elements(textRun(content)))))'; // Just text content
|
|
60
|
+
: 'revisionId,body(content(paragraph(elements(textRun(content)))))'; // Just text content
|
|
57
61
|
const res = await docs.documents.get({
|
|
58
62
|
documentId: args.documentId,
|
|
59
63
|
includeTabsContent: needsTabsContent,
|
|
@@ -71,7 +75,12 @@ export function register(server) {
|
|
|
71
75
|
if (!targetTab.documentTab) {
|
|
72
76
|
throw new UserError(`Tab "${args.tabId}" does not have content (may not be a document tab).`);
|
|
73
77
|
}
|
|
74
|
-
|
|
78
|
+
// List definitions are scoped to the document tab. Without them,
|
|
79
|
+
// docsJsonToMarkdown cannot distinguish ordered lists from bullets.
|
|
80
|
+
contentSource = {
|
|
81
|
+
body: targetTab.documentTab.body,
|
|
82
|
+
lists: targetTab.documentTab.lists,
|
|
83
|
+
};
|
|
75
84
|
log.info(`Using content from tab: ${targetTab.tabProperties?.title || 'Untitled'}`);
|
|
76
85
|
}
|
|
77
86
|
else {
|
|
@@ -82,7 +91,7 @@ export function register(server) {
|
|
|
82
91
|
if (args.diffFromLastRead) {
|
|
83
92
|
log.info('diffFromLastRead ignored: only supported for format=markdown');
|
|
84
93
|
}
|
|
85
|
-
trackRead(args.documentId, modifiedTime);
|
|
94
|
+
trackRead(args.documentId, modifiedTime, undefined, res.data.revisionId);
|
|
86
95
|
const jsonContent = JSON.stringify(contentSource, null, 2);
|
|
87
96
|
// Apply length limit to JSON if specified
|
|
88
97
|
if (args.maxLength && jsonContent.length > args.maxLength) {
|
|
@@ -95,6 +104,18 @@ export function register(server) {
|
|
|
95
104
|
const markdownContent = docsJsonToMarkdown(contentSource);
|
|
96
105
|
const totalLength = markdownContent.length;
|
|
97
106
|
log.info(`Generated markdown: ${totalLength} characters`);
|
|
107
|
+
// Derive fidelity warnings from EXACTLY the body that will be replaced
|
|
108
|
+
// (contentSource.body is the active tab's body in tab mode, the document
|
|
109
|
+
// body otherwise). This scopes image/footnote detection to the content
|
|
110
|
+
// the replacement mutates and never over-reports images or footnotes
|
|
111
|
+
// living in other tabs or in headers/footers a body replacement leaves
|
|
112
|
+
// untouched.
|
|
113
|
+
const fidelityWarnings = checkMarkdownFidelity(contentSource.body?.content);
|
|
114
|
+
const fidelityNotice = fidelityWarnings.length > 0
|
|
115
|
+
? '\n\n---\n⚠️ FORMATTING LOSS WARNING: This document contains content that cannot be represented in markdown. Calling replaceDocumentWithMarkdown will permanently lose:\n' +
|
|
116
|
+
fidelityWarnings.map(w => ` • ${w}`).join('\n') +
|
|
117
|
+
'\nConsider using modifyText or appendMarkdown for targeted edits instead.\n---'
|
|
118
|
+
: '';
|
|
98
119
|
if (args.diffFromLastRead) {
|
|
99
120
|
const previous = getLastReadContent(args.documentId);
|
|
100
121
|
if (previous !== null) {
|
|
@@ -106,18 +127,57 @@ export function register(server) {
|
|
|
106
127
|
'current',
|
|
107
128
|
{ context: 3 }
|
|
108
129
|
);
|
|
109
|
-
trackRead(args.documentId, modifiedTime, markdownContent);
|
|
110
|
-
|
|
130
|
+
trackRead(args.documentId, modifiedTime, markdownContent, res.data.revisionId);
|
|
131
|
+
// Keep the on-disk working copy in sync even on diff reads, so a
|
|
132
|
+
// subsequent edit-and-push starts from the current document state.
|
|
133
|
+
try {
|
|
134
|
+
await writeWorkspaceFile(args.documentId, markdownContent, args.tabId);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
log.info(`Could not update workspace on diff read: ${e.message}`);
|
|
137
|
+
}
|
|
138
|
+
// The diff can be silent about content the converter has no
|
|
139
|
+
// representation for: an image or footnote another editor added
|
|
140
|
+
// since the last read simply is not in either markdown snapshot,
|
|
141
|
+
// so it never shows up as a change. The workspace file was still
|
|
142
|
+
// refreshed above, and pushing it back would delete that content.
|
|
143
|
+
// Carry the same warning the full read gives.
|
|
144
|
+
return patch + fidelityNotice;
|
|
111
145
|
}
|
|
112
146
|
log.info('diffFromLastRead requested but no prior snapshot exists; returning full content');
|
|
113
147
|
}
|
|
114
|
-
|
|
148
|
+
// Store clean markdown (without warning) for future diffs and guardMutation
|
|
149
|
+
trackRead(args.documentId, modifiedTime, markdownContent, res.data.revisionId);
|
|
150
|
+
// Save to local workspace file so the AI can edit it and push with filePath.
|
|
151
|
+
// Scoped by tabId so two tabs of the same document keep separate copies.
|
|
152
|
+
let localPath = null;
|
|
153
|
+
try {
|
|
154
|
+
localPath = await writeWorkspaceFile(args.documentId, markdownContent, args.tabId);
|
|
155
|
+
log.info(`Saved to ${localPath}`);
|
|
156
|
+
} catch (e) {
|
|
157
|
+
log.info(`Could not save to workspace: ${e.message}`);
|
|
158
|
+
localPath = null;
|
|
159
|
+
}
|
|
115
160
|
// Apply length limit to markdown if specified
|
|
161
|
+
let output;
|
|
116
162
|
if (args.maxLength && totalLength > args.maxLength) {
|
|
117
163
|
const truncatedContent = markdownContent.substring(0, args.maxLength);
|
|
118
|
-
|
|
164
|
+
output = `${truncatedContent}\n\n... [Markdown truncated to ${args.maxLength} chars of ${totalLength} total. Use maxLength parameter to adjust limit or remove it to get full content.]`;
|
|
165
|
+
} else {
|
|
166
|
+
output = markdownContent;
|
|
167
|
+
}
|
|
168
|
+
// Append fidelity warning after the markdown so the AI knows what
|
|
169
|
+
// replaceDocumentWithMarkdown would permanently destroy.
|
|
170
|
+
output += fidelityNotice;
|
|
171
|
+
if (localPath) {
|
|
172
|
+
// Use forward slashes in the advice string so the path is valid JSON
|
|
173
|
+
// regardless of OS (backslashes in Windows paths break JSON encoding).
|
|
174
|
+
const jsonSafePath = localPath.replace(/\\/g, '/');
|
|
175
|
+
// If this was a tab read, the file holds only that tab's content, so the
|
|
176
|
+
// push must target the same tab to avoid writing it into the wrong tab.
|
|
177
|
+
const tabAdvice = args.tabId ? ` tabId="${args.tabId}"` : '';
|
|
178
|
+
output += `\n\n📄 Local file: ${localPath}\nEdit this file, then call replaceDocumentWithMarkdown with filePath="${jsonSafePath}"${tabAdvice} to push changes.`;
|
|
119
179
|
}
|
|
120
|
-
return
|
|
180
|
+
return output;
|
|
121
181
|
}
|
|
122
182
|
// Default: Text format - extract all text content
|
|
123
183
|
if (args.diffFromLastRead) {
|
|
@@ -151,7 +211,7 @@ export function register(server) {
|
|
|
151
211
|
});
|
|
152
212
|
}
|
|
153
213
|
});
|
|
154
|
-
trackRead(args.documentId, modifiedTime);
|
|
214
|
+
trackRead(args.documentId, modifiedTime, undefined, res.data.revisionId);
|
|
155
215
|
if (!textContent.trim())
|
|
156
216
|
return 'Document found, but appears empty.';
|
|
157
217
|
const totalLength = textContent.length;
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { getDocsClient } from '../../clients.js';
|
|
4
4
|
import { DocumentIdParameter } from '../../types.js';
|
|
5
5
|
import * as GDocsHelpers from '../../googleDocsApiHelpers.js';
|
|
6
|
+
import { getLastReadRevisionId, trackMutation } from '../../readTracker.js';
|
|
6
7
|
export function register(server) {
|
|
7
8
|
server.addTool({
|
|
8
9
|
name: 'renameTab',
|
|
@@ -28,22 +29,19 @@ export function register(server) {
|
|
|
28
29
|
throw new UserError(`Tab with ID "${args.tabId}" not found in document.`);
|
|
29
30
|
}
|
|
30
31
|
const oldTitle = targetTab.tabProperties?.title || '(untitled)';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
tabId: args.tabId,
|
|
39
|
-
title: args.newTitle,
|
|
40
|
-
},
|
|
41
|
-
fields: 'title',
|
|
42
|
-
},
|
|
32
|
+
const revisionId = getLastReadRevisionId(args.documentId);
|
|
33
|
+
const response = await GDocsHelpers.executeBatchUpdate(docs, args.documentId, [
|
|
34
|
+
{
|
|
35
|
+
updateDocumentTabProperties: {
|
|
36
|
+
tabProperties: {
|
|
37
|
+
tabId: args.tabId,
|
|
38
|
+
title: args.newTitle,
|
|
43
39
|
},
|
|
44
|
-
|
|
40
|
+
fields: 'title',
|
|
41
|
+
},
|
|
45
42
|
},
|
|
46
|
-
});
|
|
43
|
+
], revisionId ? { requiredRevisionId: revisionId } : undefined);
|
|
44
|
+
trackMutation(args.documentId, response?.writeControl?.requiredRevisionId);
|
|
47
45
|
const docUrl = `https://docs.google.com/document/d/${args.documentId}/edit`;
|
|
48
46
|
return `${docUrl}\nSuccessfully renamed tab from "${oldTitle}" to "${args.newTitle}".`;
|
|
49
47
|
}
|