@piotr-agier/google-drive-mcp 1.7.2 → 1.7.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/README.md +19 -10
- package/dist/index.js +430 -48
- package/dist/index.js.map +3 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -240,13 +240,20 @@ Add this configuration to use the Docker container with Claude Desktop:
|
|
|
240
240
|
|
|
241
241
|
The server supports multiple methods for providing OAuth credentials (in order of priority):
|
|
242
242
|
|
|
243
|
-
#### 1. **Environment Variable** (
|
|
243
|
+
#### 1. **Environment Variable** (Highest Priority)
|
|
244
244
|
```bash
|
|
245
245
|
export GOOGLE_DRIVE_OAUTH_CREDENTIALS="/path/to/your/gcp-oauth.keys.json"
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
-
#### 2. **
|
|
249
|
-
Place `gcp-oauth.keys.json` in the
|
|
248
|
+
#### 2. **Config Directory** (Recommended)
|
|
249
|
+
Place `gcp-oauth.keys.json` in the XDG config directory:
|
|
250
|
+
```
|
|
251
|
+
~/.config/google-drive-mcp/gcp-oauth.keys.json
|
|
252
|
+
```
|
|
253
|
+
This is the recommended location — it works reliably with `npx`, global installs, and local setups.
|
|
254
|
+
|
|
255
|
+
#### 3. **Project Root** (Legacy Fallback)
|
|
256
|
+
Place `gcp-oauth.keys.json` in the project root directory. This still works for local development but is unreliable with `npx` or global installs.
|
|
250
257
|
|
|
251
258
|
### OAuth Scope Configuration
|
|
252
259
|
|
|
@@ -580,11 +587,12 @@ Add the server to your Claude Desktop configuration:
|
|
|
580
587
|
- `uploadToSameFolder`: Upload to same folder as document (optional, default: true)
|
|
581
588
|
|
|
582
589
|
#### Comments
|
|
583
|
-
- **listComments** - List all comments in a Google Document
|
|
590
|
+
- **listComments** - List all comments in a Google Document with position context, character offsets, and full reply chains
|
|
584
591
|
- `documentId`: Document ID
|
|
585
592
|
- `includeDeleted`: Include deleted comments (optional, default: false)
|
|
586
593
|
- `pageSize`: Max comments to return, 1-100 (optional, default: 100)
|
|
587
594
|
- `pageToken`: Token for next page of results (optional)
|
|
595
|
+
- Returns surrounding context and Docs API character offsets for each comment using a two-tiered approach (Docs API text matching, DOCX export fallback for ambiguous matches)
|
|
588
596
|
|
|
589
597
|
- **getComment** - Get a specific comment with its full thread of replies
|
|
590
598
|
- `documentId`: Document ID
|
|
@@ -942,15 +950,15 @@ After revoking access, you'll need to re-authenticate the next time you use the
|
|
|
942
950
|
#### "OAuth credentials not found"
|
|
943
951
|
```
|
|
944
952
|
OAuth credentials not found. Please provide credentials using one of these methods:
|
|
945
|
-
1.
|
|
953
|
+
1. Config directory (recommended):
|
|
954
|
+
Place your gcp-oauth.keys.json file in: ~/.config/google-drive-mcp/
|
|
955
|
+
2. Environment variable:
|
|
946
956
|
export GOOGLE_DRIVE_OAUTH_CREDENTIALS="/path/to/gcp-oauth.keys.json"
|
|
947
|
-
2. Default file path:
|
|
948
|
-
Place your gcp-oauth.keys.json file in the package root directory.
|
|
949
957
|
```
|
|
950
958
|
|
|
951
959
|
**Solution:**
|
|
952
960
|
- Download credentials from Google Cloud Console
|
|
953
|
-
-
|
|
961
|
+
- Place the file in `~/.config/google-drive-mcp/gcp-oauth.keys.json` (recommended), or set the environment variable
|
|
954
962
|
- Ensure the file has proper read permissions
|
|
955
963
|
|
|
956
964
|
#### "Authentication failed" or Browser doesn't open
|
|
@@ -1143,7 +1151,7 @@ npm run typecheck # Type checking without compilation
|
|
|
1143
1151
|
- `npm run typecheck` - Run TypeScript type checking only
|
|
1144
1152
|
- `npm run lint` - Run TypeScript type checking (alias for typecheck)
|
|
1145
1153
|
- `npm run prepare` - Auto-runs build before npm publish
|
|
1146
|
-
- `npm test` - Run
|
|
1154
|
+
- `npm test` - Run unit tests
|
|
1147
1155
|
|
|
1148
1156
|
## Advanced Configuration
|
|
1149
1157
|
|
|
@@ -1155,7 +1163,8 @@ npm run typecheck # Type checking without compilation
|
|
|
1155
1163
|
| Variable | Description | Example |
|
|
1156
1164
|
|----------|-------------|---------|
|
|
1157
1165
|
| `GOOGLE_DRIVE_OAUTH_CREDENTIALS` | Path to your OAuth credentials JSON file | `/home/user/secrets/oauth.json` |
|
|
1158
|
-
| *(or place file at)* |
|
|
1166
|
+
| *(or place file at)* | Config directory (recommended): `~/.config/google-drive-mcp/gcp-oauth.keys.json` | `~/.config/google-drive-mcp/gcp-oauth.keys.json` |
|
|
1167
|
+
| *(or place file at)* | Project root (legacy fallback): `gcp-oauth.keys.json` | `./gcp-oauth.keys.json` |
|
|
1159
1168
|
|
|
1160
1169
|
**Optional** (for customization):
|
|
1161
1170
|
| Variable | Description | Default | Example |
|
package/dist/index.js
CHANGED
|
@@ -29,14 +29,16 @@ function getProjectRoot() {
|
|
|
29
29
|
const projectRoot = path.join(__dirname2, "..", "..");
|
|
30
30
|
return path.resolve(projectRoot);
|
|
31
31
|
}
|
|
32
|
+
function getConfigDir() {
|
|
33
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
34
|
+
return path.join(configHome, "google-drive-mcp");
|
|
35
|
+
}
|
|
32
36
|
function getSecureTokenPath() {
|
|
33
37
|
const customTokenPath = process.env.GOOGLE_DRIVE_MCP_TOKEN_PATH;
|
|
34
38
|
if (customTokenPath) {
|
|
35
39
|
return path.resolve(customTokenPath);
|
|
36
40
|
}
|
|
37
|
-
|
|
38
|
-
const tokenDir = path.join(configHome, "google-drive-mcp");
|
|
39
|
-
return path.join(tokenDir, "tokens.json");
|
|
41
|
+
return path.join(getConfigDir(), "tokens.json");
|
|
40
42
|
}
|
|
41
43
|
function getLegacyTokenPath() {
|
|
42
44
|
const projectRoot = getProjectRoot();
|
|
@@ -49,26 +51,29 @@ function getAdditionalLegacyPaths() {
|
|
|
49
51
|
path.join(process.cwd(), ".gcp-saved-tokens.json")
|
|
50
52
|
].filter(Boolean);
|
|
51
53
|
}
|
|
52
|
-
function
|
|
54
|
+
function getKeysFilePaths() {
|
|
55
|
+
const paths = [];
|
|
53
56
|
const envCredentialsPath = process.env.GOOGLE_DRIVE_OAUTH_CREDENTIALS;
|
|
54
57
|
if (envCredentialsPath) {
|
|
55
|
-
|
|
58
|
+
paths.push(path.resolve(envCredentialsPath));
|
|
56
59
|
}
|
|
60
|
+
paths.push(path.join(getConfigDir(), "gcp-oauth.keys.json"));
|
|
57
61
|
const projectRoot = getProjectRoot();
|
|
58
|
-
|
|
59
|
-
return
|
|
62
|
+
paths.push(path.join(projectRoot, "gcp-oauth.keys.json"));
|
|
63
|
+
return paths;
|
|
60
64
|
}
|
|
61
65
|
function generateCredentialsErrorMessage() {
|
|
66
|
+
const configDir = getConfigDir();
|
|
62
67
|
return `
|
|
63
68
|
OAuth credentials not found. Please provide credentials using one of these methods:
|
|
64
69
|
|
|
65
|
-
1.
|
|
70
|
+
1. Config directory (recommended):
|
|
71
|
+
Place your gcp-oauth.keys.json file in: ${configDir}/
|
|
72
|
+
|
|
73
|
+
2. Environment variable:
|
|
66
74
|
Set GOOGLE_DRIVE_OAUTH_CREDENTIALS to the path of your credentials file:
|
|
67
75
|
export GOOGLE_DRIVE_OAUTH_CREDENTIALS="/path/to/gcp-oauth.keys.json"
|
|
68
76
|
|
|
69
|
-
2. Default file path:
|
|
70
|
-
Place your gcp-oauth.keys.json file in the package root directory.
|
|
71
|
-
|
|
72
77
|
Token storage:
|
|
73
78
|
- Tokens are saved to: ${getSecureTokenPath()}
|
|
74
79
|
- To use a custom token location, set GOOGLE_DRIVE_MCP_TOKEN_PATH environment variable
|
|
@@ -83,9 +88,7 @@ To get OAuth credentials:
|
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
// src/auth/client.ts
|
|
86
|
-
|
|
87
|
-
const keysContent = await fs.readFile(getKeysFilePath(), "utf-8");
|
|
88
|
-
const keys = JSON.parse(keysContent);
|
|
91
|
+
function parseCredentialsFile(keys) {
|
|
89
92
|
if (keys.installed) {
|
|
90
93
|
const { client_id, client_secret, redirect_uris } = keys.installed;
|
|
91
94
|
return { client_id, client_secret, redirect_uris };
|
|
@@ -102,6 +105,21 @@ async function loadCredentialsFromFile() {
|
|
|
102
105
|
throw new Error('Invalid credentials file format. Expected either "installed", "web" object or direct client_id field.');
|
|
103
106
|
}
|
|
104
107
|
}
|
|
108
|
+
async function loadCredentialsFromFile() {
|
|
109
|
+
const paths = getKeysFilePaths();
|
|
110
|
+
for (const keysPath of paths) {
|
|
111
|
+
try {
|
|
112
|
+
const keysContent = await fs.readFile(keysPath, "utf-8");
|
|
113
|
+
const keys = JSON.parse(keysContent);
|
|
114
|
+
return parseCredentialsFile(keys);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err instanceof SyntaxError || err instanceof Error && err.message.includes("Invalid credentials")) {
|
|
117
|
+
throw new Error(`Invalid credentials file at ${keysPath}: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Credentials file not found. Searched: ${paths.join(", ")}`);
|
|
122
|
+
}
|
|
105
123
|
async function loadCredentialsWithFallback() {
|
|
106
124
|
try {
|
|
107
125
|
return await loadCredentialsFromFile();
|
|
@@ -2252,12 +2270,17 @@ ${JSON.stringify({ message }, null, 2)}` }],
|
|
|
2252
2270
|
// src/tools/docs.ts
|
|
2253
2271
|
var docs_exports = {};
|
|
2254
2272
|
__export(docs_exports, {
|
|
2273
|
+
buildFlatTextFromDoc: () => buildFlatTextFromDoc,
|
|
2274
|
+
extractRowCells: () => extractRowCells,
|
|
2255
2275
|
handleTool: () => handleTool2,
|
|
2276
|
+
matchDocxToDriveComments: () => matchDocxToDriveComments,
|
|
2277
|
+
resolveContextFromDocx: () => resolveContextFromDocx,
|
|
2256
2278
|
toolDefinitions: () => toolDefinitions2
|
|
2257
2279
|
});
|
|
2258
2280
|
import { z as z2 } from "zod";
|
|
2259
2281
|
import { createReadStream as createReadStream2, existsSync as existsSync3 } from "fs";
|
|
2260
2282
|
import { basename as basename3, extname as extname3 } from "path";
|
|
2283
|
+
import JSZip from "jszip";
|
|
2261
2284
|
function hexToRgbColor(hex) {
|
|
2262
2285
|
if (!hex) return null;
|
|
2263
2286
|
let hexClean = hex.startsWith("#") ? hex.slice(1) : hex;
|
|
@@ -2280,6 +2303,28 @@ function rgbColorToHex(color) {
|
|
|
2280
2303
|
const b = Math.round((rgb.blue || 0) * 255);
|
|
2281
2304
|
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
2282
2305
|
}
|
|
2306
|
+
function collectAllTabsWithLevel(tabs, level = 0) {
|
|
2307
|
+
const result = [];
|
|
2308
|
+
for (const tab of tabs) {
|
|
2309
|
+
result.push({ tab, level });
|
|
2310
|
+
if (tab.childTabs && tab.childTabs.length > 0) {
|
|
2311
|
+
result.push(...collectAllTabsWithLevel(tab.childTabs, level + 1));
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return result;
|
|
2315
|
+
}
|
|
2316
|
+
function findTabById(tabs, targetId) {
|
|
2317
|
+
for (const tab of tabs) {
|
|
2318
|
+
if (tab.tabProperties?.tabId === targetId) {
|
|
2319
|
+
return tab;
|
|
2320
|
+
}
|
|
2321
|
+
if (tab.childTabs && tab.childTabs.length > 0) {
|
|
2322
|
+
const found = findTabById(tab.childTabs, targetId);
|
|
2323
|
+
if (found) return found;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2283
2328
|
async function executeBatchUpdate(ctx, documentId, requests) {
|
|
2284
2329
|
if (!requests || requests.length === 0) {
|
|
2285
2330
|
return {};
|
|
@@ -2589,6 +2634,236 @@ async function uploadImageToDriveHelper(ctx, localFilePath, parentFolderId, make
|
|
|
2589
2634
|
}
|
|
2590
2635
|
return webContentLink;
|
|
2591
2636
|
}
|
|
2637
|
+
var MAX_ROW_XML_DISTANCE = 1e5;
|
|
2638
|
+
var MAX_PARAGRAPH_XML_DISTANCE = 5e4;
|
|
2639
|
+
var MAX_PARAGRAPH_CONTEXT_LENGTH = 300;
|
|
2640
|
+
function buildFlatTextFromDoc(docData) {
|
|
2641
|
+
function extractSegments(bodyContent) {
|
|
2642
|
+
const segs = [];
|
|
2643
|
+
function fromElements(elements) {
|
|
2644
|
+
for (const el of elements) {
|
|
2645
|
+
if (el.textRun?.content && el.startIndex != null) {
|
|
2646
|
+
segs.push({ text: el.textRun.content, startIndex: el.startIndex });
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
for (const el of bodyContent) {
|
|
2651
|
+
if (el.paragraph?.elements) {
|
|
2652
|
+
fromElements(el.paragraph.elements);
|
|
2653
|
+
} else if (el.table) {
|
|
2654
|
+
for (const row of el.table.tableRows || []) {
|
|
2655
|
+
for (const cell of row.tableCells || []) {
|
|
2656
|
+
for (const cc of cell.content || []) {
|
|
2657
|
+
if (cc.paragraph?.elements) fromElements(cc.paragraph.elements);
|
|
2658
|
+
if (cc.table) {
|
|
2659
|
+
const nested = extractSegments([cc]);
|
|
2660
|
+
segs.push(...nested);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return segs;
|
|
2668
|
+
}
|
|
2669
|
+
const allSegments = [];
|
|
2670
|
+
const tabs = docData.tabs;
|
|
2671
|
+
if (tabs && tabs.length > 0) {
|
|
2672
|
+
for (const tab of tabs) {
|
|
2673
|
+
const bc = tab.documentTab?.body?.content;
|
|
2674
|
+
if (bc) allSegments.push(...extractSegments(bc));
|
|
2675
|
+
}
|
|
2676
|
+
} else if (docData.body?.content) {
|
|
2677
|
+
allSegments.push(...extractSegments(docData.body.content));
|
|
2678
|
+
}
|
|
2679
|
+
let flatText = "";
|
|
2680
|
+
const offsetMap = [];
|
|
2681
|
+
for (const seg of allSegments) {
|
|
2682
|
+
for (let i = 0; i < seg.text.length; i++) {
|
|
2683
|
+
offsetMap.push(seg.startIndex + i);
|
|
2684
|
+
flatText += seg.text[i];
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
return { flatText, offsetMap };
|
|
2688
|
+
}
|
|
2689
|
+
function extractRowCells(rowXml) {
|
|
2690
|
+
const cells = [];
|
|
2691
|
+
let searchFrom = 0;
|
|
2692
|
+
while (true) {
|
|
2693
|
+
const tcStart1 = rowXml.indexOf("<w:tc>", searchFrom);
|
|
2694
|
+
const tcStart2 = rowXml.indexOf("<w:tc ", searchFrom);
|
|
2695
|
+
const tcStart = tcStart1 === -1 && tcStart2 === -1 ? -1 : tcStart1 === -1 ? tcStart2 : tcStart2 === -1 ? tcStart1 : Math.min(tcStart1, tcStart2);
|
|
2696
|
+
if (tcStart === -1) break;
|
|
2697
|
+
const tcEnd = rowXml.indexOf("</w:tc>", tcStart);
|
|
2698
|
+
if (tcEnd === -1) break;
|
|
2699
|
+
const cellXml = rowXml.substring(tcStart, tcEnd);
|
|
2700
|
+
const tTexts = [];
|
|
2701
|
+
const tRegex = /<w:t[^>]*>([^<]*)<\/w:t>/g;
|
|
2702
|
+
let t;
|
|
2703
|
+
while ((t = tRegex.exec(cellXml)) !== null) tTexts.push(t[1]);
|
|
2704
|
+
if (tTexts.length > 0) cells.push(tTexts.join(""));
|
|
2705
|
+
searchFrom = tcEnd + 7;
|
|
2706
|
+
}
|
|
2707
|
+
return cells;
|
|
2708
|
+
}
|
|
2709
|
+
async function resolveContextFromDocx(docxData) {
|
|
2710
|
+
const zip = await JSZip.loadAsync(docxData);
|
|
2711
|
+
const commentsXml = await zip.file("word/comments.xml")?.async("string");
|
|
2712
|
+
const documentXml = await zip.file("word/document.xml")?.async("string");
|
|
2713
|
+
if (!commentsXml || !documentXml) return null;
|
|
2714
|
+
const docxComments = /* @__PURE__ */ new Map();
|
|
2715
|
+
const commentTagRegex = /<w:comment\s+[^>]*?w:id="(\d+)"[^>]*>/g;
|
|
2716
|
+
let cMatch;
|
|
2717
|
+
while ((cMatch = commentTagRegex.exec(commentsXml)) !== null) {
|
|
2718
|
+
const id = parseInt(cMatch[1]);
|
|
2719
|
+
const tagStr = cMatch[0];
|
|
2720
|
+
const authorMatch = tagStr.match(/w:author="([^"]*)"/);
|
|
2721
|
+
const dateMatch = tagStr.match(/w:date="([^"]*)"/);
|
|
2722
|
+
const author = authorMatch ? authorMatch[1] : "";
|
|
2723
|
+
const date = dateMatch ? dateMatch[1] : "";
|
|
2724
|
+
const endPos = commentsXml.indexOf("</w:comment>", cMatch.index);
|
|
2725
|
+
if (endPos !== -1) {
|
|
2726
|
+
const body = commentsXml.substring(cMatch.index, endPos);
|
|
2727
|
+
const texts = [];
|
|
2728
|
+
const tRegex = /<w:t[^>]*>([^<]*)<\/w:t>/g;
|
|
2729
|
+
let tMatch;
|
|
2730
|
+
while ((tMatch = tRegex.exec(body)) !== null) {
|
|
2731
|
+
texts.push(tMatch[1]);
|
|
2732
|
+
}
|
|
2733
|
+
docxComments.set(id, { author, date, content: texts.join("") });
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
const contextsBefore = /* @__PURE__ */ new Map();
|
|
2737
|
+
const contextsAfter = /* @__PURE__ */ new Map();
|
|
2738
|
+
const rowCells = /* @__PURE__ */ new Map();
|
|
2739
|
+
const rangeStartRegex = /<w:commentRangeStart\s+w:id="(\d+)"\/>/g;
|
|
2740
|
+
let rMatch;
|
|
2741
|
+
while ((rMatch = rangeStartRegex.exec(documentXml)) !== null) {
|
|
2742
|
+
const docxId = parseInt(rMatch[1]);
|
|
2743
|
+
const startPos = rMatch.index;
|
|
2744
|
+
const trStart = documentXml.lastIndexOf("<w:tr>", startPos);
|
|
2745
|
+
const trEnd = documentXml.indexOf("</w:tr>", startPos);
|
|
2746
|
+
if (trStart !== -1 && trEnd !== -1 && startPos - trStart < MAX_ROW_XML_DISTANCE) {
|
|
2747
|
+
const rowXml = documentXml.substring(trStart, trEnd);
|
|
2748
|
+
const cellTexts = extractRowCells(rowXml);
|
|
2749
|
+
const commentMarker = `commentRangeStart w:id="${docxId}"`;
|
|
2750
|
+
let commentCellIdx = -1;
|
|
2751
|
+
let cellSearchFrom = 0;
|
|
2752
|
+
for (let ci = 0; ci < cellTexts.length; ci++) {
|
|
2753
|
+
const tcStart1 = rowXml.indexOf("<w:tc>", cellSearchFrom);
|
|
2754
|
+
const tcStart2 = rowXml.indexOf("<w:tc ", cellSearchFrom);
|
|
2755
|
+
const tcStart = tcStart1 === -1 && tcStart2 === -1 ? -1 : tcStart1 === -1 ? tcStart2 : tcStart2 === -1 ? tcStart1 : Math.min(tcStart1, tcStart2);
|
|
2756
|
+
if (tcStart === -1) break;
|
|
2757
|
+
const tcEnd = rowXml.indexOf("</w:tc>", tcStart);
|
|
2758
|
+
if (tcEnd === -1) break;
|
|
2759
|
+
const cellXml = rowXml.substring(tcStart, tcEnd);
|
|
2760
|
+
if (cellXml.includes(commentMarker)) {
|
|
2761
|
+
commentCellIdx = ci;
|
|
2762
|
+
}
|
|
2763
|
+
cellSearchFrom = tcEnd + 7;
|
|
2764
|
+
}
|
|
2765
|
+
if (cellTexts.length > 0) {
|
|
2766
|
+
const allTexts = cellTexts;
|
|
2767
|
+
rowCells.set(docxId, allTexts);
|
|
2768
|
+
if (commentCellIdx !== -1) {
|
|
2769
|
+
const before = cellTexts.slice(0, commentCellIdx);
|
|
2770
|
+
let after = cellTexts.slice(commentCellIdx + 1);
|
|
2771
|
+
if (commentCellIdx === cellTexts.length - 1) {
|
|
2772
|
+
const nextTrStart = documentXml.indexOf("<w:tr>", trEnd);
|
|
2773
|
+
const nextTrEnd = nextTrStart !== -1 ? documentXml.indexOf("</w:tr>", nextTrStart) : -1;
|
|
2774
|
+
if (nextTrStart !== -1 && nextTrEnd !== -1) {
|
|
2775
|
+
const nextRowXml = documentXml.substring(nextTrStart, nextTrEnd);
|
|
2776
|
+
after = extractRowCells(nextRowXml);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
const commentText = cellTexts[commentCellIdx];
|
|
2780
|
+
contextsBefore.set(docxId, [...before, commentText].join(" | "));
|
|
2781
|
+
contextsAfter.set(docxId, [commentText, ...after].join(" | "));
|
|
2782
|
+
} else {
|
|
2783
|
+
contextsBefore.set(docxId, allTexts.join(" | "));
|
|
2784
|
+
contextsAfter.set(docxId, "");
|
|
2785
|
+
}
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
const pStart = documentXml.lastIndexOf("<w:p ", startPos);
|
|
2790
|
+
const pEnd = documentXml.indexOf("</w:p>", startPos);
|
|
2791
|
+
if (pStart !== -1 && pEnd !== -1 && startPos - pStart < MAX_PARAGRAPH_XML_DISTANCE) {
|
|
2792
|
+
const pXml = documentXml.substring(pStart, pEnd);
|
|
2793
|
+
const pTexts = [];
|
|
2794
|
+
const tRegex = /<w:t[^>]*>([^<]*)<\/w:t>/g;
|
|
2795
|
+
let t;
|
|
2796
|
+
while ((t = tRegex.exec(pXml)) !== null) pTexts.push(t[1]);
|
|
2797
|
+
const pText = pTexts.join("").trim();
|
|
2798
|
+
if (pText) {
|
|
2799
|
+
contextsBefore.set(docxId, pText.length > MAX_PARAGRAPH_CONTEXT_LENGTH ? pText.substring(0, MAX_PARAGRAPH_CONTEXT_LENGTH) + "..." : pText);
|
|
2800
|
+
contextsAfter.set(docxId, "");
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
return { docxComments, contextsBefore, contextsAfter, rowCells };
|
|
2805
|
+
}
|
|
2806
|
+
function matchDocxToDriveComments(driveComments, docxResult, contextMap, flatText, offsetMap) {
|
|
2807
|
+
const { docxComments, contextsBefore, contextsAfter } = docxResult;
|
|
2808
|
+
for (const comment of driveComments) {
|
|
2809
|
+
if (contextMap.has(comment.id)) continue;
|
|
2810
|
+
if (comment.resolved) continue;
|
|
2811
|
+
const apiAuthor = comment.author?.displayName || "";
|
|
2812
|
+
const apiDate = (comment.createdTime || "").replace(/\.\d+Z$/, "Z");
|
|
2813
|
+
let matchedDocxId = null;
|
|
2814
|
+
for (const [docxId, docxComment] of docxComments) {
|
|
2815
|
+
if (docxComment.author === apiAuthor && docxComment.date === apiDate) {
|
|
2816
|
+
matchedDocxId = docxId;
|
|
2817
|
+
break;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (matchedDocxId !== null) {
|
|
2821
|
+
const ctxBefore = contextsBefore.get(matchedDocxId) || "";
|
|
2822
|
+
const ctxAfter = contextsAfter.get(matchedDocxId) || "";
|
|
2823
|
+
if (ctxBefore || ctxAfter) {
|
|
2824
|
+
const entry = {
|
|
2825
|
+
contextBefore: ctxBefore,
|
|
2826
|
+
contextAfter: ctxAfter
|
|
2827
|
+
};
|
|
2828
|
+
const quoted = comment.quotedFileContent?.value;
|
|
2829
|
+
if (quoted && flatText && offsetMap.length > 0 && ctxBefore) {
|
|
2830
|
+
const beforePattern = ctxBefore.split(" | ").join("\n");
|
|
2831
|
+
const findAll = (pattern) => {
|
|
2832
|
+
const results = [];
|
|
2833
|
+
let from = 0;
|
|
2834
|
+
while (true) {
|
|
2835
|
+
const idx = flatText.indexOf(pattern, from);
|
|
2836
|
+
if (idx === -1) break;
|
|
2837
|
+
results.push(idx);
|
|
2838
|
+
from = idx + 1;
|
|
2839
|
+
}
|
|
2840
|
+
return results;
|
|
2841
|
+
};
|
|
2842
|
+
let matches = findAll(beforePattern);
|
|
2843
|
+
if (matches.length !== 1 && ctxAfter) {
|
|
2844
|
+
const afterCells = ctxAfter.split(" | ");
|
|
2845
|
+
const afterWithoutAnchor = afterCells.slice(1).join("\n");
|
|
2846
|
+
if (afterWithoutAnchor) {
|
|
2847
|
+
const fullPattern = beforePattern + "\n" + afterWithoutAnchor;
|
|
2848
|
+
matches = findAll(fullPattern);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (matches.length === 1) {
|
|
2852
|
+
const patternStart = matches[0];
|
|
2853
|
+
const qIdx = patternStart + beforePattern.length - quoted.length;
|
|
2854
|
+
const endIdx = qIdx + quoted.length - 1;
|
|
2855
|
+
if (endIdx < offsetMap.length && flatText.substring(qIdx, qIdx + quoted.length) === quoted) {
|
|
2856
|
+
entry.startIndex = offsetMap[qIdx];
|
|
2857
|
+
entry.endIndex = offsetMap[endIdx] + 1;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
contextMap.set(comment.id, entry);
|
|
2862
|
+
}
|
|
2863
|
+
docxComments.delete(matchedDocxId);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2592
2867
|
var CreateGoogleDocSchema = z2.object({
|
|
2593
2868
|
name: z2.string().min(1, "Document name is required"),
|
|
2594
2869
|
content: z2.string(),
|
|
@@ -3418,12 +3693,17 @@ Link: ${doc.webViewLink}` }],
|
|
|
3418
3693
|
const tabs = document.data.tabs;
|
|
3419
3694
|
let formattedContent = "Document content with indices:\n\n";
|
|
3420
3695
|
let totalLength = 0;
|
|
3421
|
-
if (tabs && tabs.length >
|
|
3422
|
-
|
|
3423
|
-
|
|
3696
|
+
if (tabs && tabs.length > 0) {
|
|
3697
|
+
const allTabs = collectAllTabsWithLevel(tabs);
|
|
3698
|
+
const isMultiTab = allTabs.length > 1;
|
|
3699
|
+
for (const { tab, level } of allTabs) {
|
|
3424
3700
|
const bodyContent = tab.documentTab?.body?.content;
|
|
3425
|
-
|
|
3701
|
+
if (isMultiTab) {
|
|
3702
|
+
const title = tab.tabProperties?.title || "Untitled";
|
|
3703
|
+
const indent = " ".repeat(level);
|
|
3704
|
+
formattedContent += `${indent}=== Tab: ${title} ===
|
|
3426
3705
|
`;
|
|
3706
|
+
}
|
|
3427
3707
|
if (bodyContent) {
|
|
3428
3708
|
const segments = extractSegments2(bodyContent);
|
|
3429
3709
|
trackFonts2(segments);
|
|
@@ -3432,15 +3712,9 @@ Link: ${doc.webViewLink}` }],
|
|
|
3432
3712
|
totalLength += segments[segments.length - 1].endIndex;
|
|
3433
3713
|
}
|
|
3434
3714
|
}
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
const bodyContent = tabs[0].documentTab?.body?.content;
|
|
3439
|
-
if (bodyContent) {
|
|
3440
|
-
const segments = extractSegments2(bodyContent);
|
|
3441
|
-
trackFonts2(segments);
|
|
3442
|
-
formattedContent += formatSegments2(segments);
|
|
3443
|
-
totalLength = segments.length > 0 ? segments[segments.length - 1].endIndex : 0;
|
|
3715
|
+
if (isMultiTab) {
|
|
3716
|
+
formattedContent += "\n";
|
|
3717
|
+
}
|
|
3444
3718
|
}
|
|
3445
3719
|
} else {
|
|
3446
3720
|
const bodyContent = document.data.body?.content;
|
|
@@ -3581,7 +3855,7 @@ Total length: ${totalLength} characters`
|
|
|
3581
3855
|
const tabs = doc.tabs;
|
|
3582
3856
|
if (tabs && tabs.length > 0) {
|
|
3583
3857
|
if (a.tabId) {
|
|
3584
|
-
const tab = tabs
|
|
3858
|
+
const tab = findTabById(tabs, a.tabId);
|
|
3585
3859
|
if (!tab) {
|
|
3586
3860
|
return errorResponse(`Tab with ID "${a.tabId}" not found. Use listDocumentTabs to see available tabs.`);
|
|
3587
3861
|
}
|
|
@@ -3589,21 +3863,23 @@ Total length: ${totalLength} characters`
|
|
|
3589
3863
|
if (bodyContent) {
|
|
3590
3864
|
text = extractText2(bodyContent);
|
|
3591
3865
|
}
|
|
3592
|
-
} else
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
`;
|
|
3866
|
+
} else {
|
|
3867
|
+
const allTabs = collectAllTabsWithLevel(tabs);
|
|
3868
|
+
const isMultiTab = allTabs.length > 1;
|
|
3869
|
+
for (const { tab, level } of allTabs) {
|
|
3597
3870
|
const bodyContent = tab.documentTab?.body?.content;
|
|
3871
|
+
if (isMultiTab) {
|
|
3872
|
+
const title = tab.tabProperties?.title || "Untitled";
|
|
3873
|
+
const indent = " ".repeat(level);
|
|
3874
|
+
text += `${indent}=== Tab: ${title} ===
|
|
3875
|
+
`;
|
|
3876
|
+
}
|
|
3598
3877
|
if (bodyContent) {
|
|
3599
3878
|
text += extractText2(bodyContent);
|
|
3600
3879
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
const bodyContent = tabs[0].documentTab?.body?.content;
|
|
3605
|
-
if (bodyContent) {
|
|
3606
|
-
text = extractText2(bodyContent);
|
|
3880
|
+
if (isMultiTab) {
|
|
3881
|
+
text += "\n";
|
|
3882
|
+
}
|
|
3607
3883
|
}
|
|
3608
3884
|
}
|
|
3609
3885
|
} else {
|
|
@@ -3933,7 +4209,7 @@ ${text}`;
|
|
|
3933
4209
|
const a = validation.data;
|
|
3934
4210
|
const response = await ctx.getDrive().comments.list({
|
|
3935
4211
|
fileId: a.documentId,
|
|
3936
|
-
fields: "comments(id,content,quotedFileContent,author,createdTime,resolved,replies),nextPageToken",
|
|
4212
|
+
fields: "comments(id,content,quotedFileContent,author,createdTime,resolved,replies(id,content,author,createdTime)),nextPageToken",
|
|
3937
4213
|
pageSize: a.pageSize || 100,
|
|
3938
4214
|
pageToken: a.pageToken,
|
|
3939
4215
|
includeDeleted: a.includeDeleted || false
|
|
@@ -3946,18 +4222,124 @@ ${text}`;
|
|
|
3946
4222
|
isError: false
|
|
3947
4223
|
};
|
|
3948
4224
|
}
|
|
4225
|
+
const contextMap = /* @__PURE__ */ new Map();
|
|
4226
|
+
let flatText = "";
|
|
4227
|
+
let offsetMap = [];
|
|
4228
|
+
let needsDocxFallback = false;
|
|
4229
|
+
let isGoogleDoc = false;
|
|
4230
|
+
try {
|
|
4231
|
+
const fileInfo = await ctx.getDrive().files.get({
|
|
4232
|
+
fileId: a.documentId,
|
|
4233
|
+
fields: "mimeType",
|
|
4234
|
+
supportsAllDrives: true
|
|
4235
|
+
});
|
|
4236
|
+
isGoogleDoc = fileInfo.data.mimeType === "application/vnd.google-apps.document";
|
|
4237
|
+
} catch (err) {
|
|
4238
|
+
ctx.log("Failed to check file MIME type:", err);
|
|
4239
|
+
}
|
|
4240
|
+
if (isGoogleDoc) {
|
|
4241
|
+
try {
|
|
4242
|
+
let getContext2 = function(matchStart, matchLen) {
|
|
4243
|
+
const matchText = flatText.substring(matchStart, matchStart + matchLen);
|
|
4244
|
+
const beforeStart = Math.max(0, matchStart - 120);
|
|
4245
|
+
let before = flatText.substring(beforeStart, matchStart).trim();
|
|
4246
|
+
if (beforeStart > 0) before = "..." + before;
|
|
4247
|
+
before = before + matchText;
|
|
4248
|
+
const afterEnd = Math.min(flatText.length, matchStart + matchLen + 120);
|
|
4249
|
+
let after = flatText.substring(matchStart + matchLen, afterEnd).trim();
|
|
4250
|
+
if (afterEnd < flatText.length) after = after + "...";
|
|
4251
|
+
after = matchText + after;
|
|
4252
|
+
return { before, after };
|
|
4253
|
+
};
|
|
4254
|
+
var getContext = getContext2;
|
|
4255
|
+
const docs = ctx.google.docs({ version: "v1", auth: ctx.authClient });
|
|
4256
|
+
const docResponse = await docs.documents.get({
|
|
4257
|
+
documentId: a.documentId,
|
|
4258
|
+
includeTabsContent: true
|
|
4259
|
+
});
|
|
4260
|
+
const result = buildFlatTextFromDoc(docResponse.data);
|
|
4261
|
+
flatText = result.flatText;
|
|
4262
|
+
offsetMap = result.offsetMap;
|
|
4263
|
+
const ambiguousComments = [];
|
|
4264
|
+
for (const comment of comments) {
|
|
4265
|
+
const quoted = comment.quotedFileContent?.value;
|
|
4266
|
+
if (!quoted) continue;
|
|
4267
|
+
const positions = [];
|
|
4268
|
+
let searchFrom = 0;
|
|
4269
|
+
while (true) {
|
|
4270
|
+
const idx = flatText.indexOf(quoted, searchFrom);
|
|
4271
|
+
if (idx === -1) break;
|
|
4272
|
+
positions.push(idx);
|
|
4273
|
+
searchFrom = idx + 1;
|
|
4274
|
+
}
|
|
4275
|
+
if (positions.length === 1) {
|
|
4276
|
+
const surrounding = getContext2(positions[0], quoted.length);
|
|
4277
|
+
const entry = {
|
|
4278
|
+
contextBefore: surrounding.before,
|
|
4279
|
+
contextAfter: surrounding.after
|
|
4280
|
+
};
|
|
4281
|
+
const endIdx = positions[0] + quoted.length - 1;
|
|
4282
|
+
if (endIdx < offsetMap.length) {
|
|
4283
|
+
entry.startIndex = offsetMap[positions[0]];
|
|
4284
|
+
entry.endIndex = offsetMap[endIdx] + 1;
|
|
4285
|
+
}
|
|
4286
|
+
if (comment.id) contextMap.set(comment.id, entry);
|
|
4287
|
+
} else if (positions.length > 1) {
|
|
4288
|
+
ambiguousComments.push(comment);
|
|
4289
|
+
}
|
|
4290
|
+
}
|
|
4291
|
+
needsDocxFallback = ambiguousComments.length > 0;
|
|
4292
|
+
} catch (err) {
|
|
4293
|
+
ctx.log("Tier 1 context extraction failed:", err);
|
|
4294
|
+
needsDocxFallback = true;
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
if (needsDocxFallback) {
|
|
4298
|
+
const unresolved = comments.filter((c) => !contextMap.has(c.id) && !c.resolved);
|
|
4299
|
+
if (unresolved.length > 0) {
|
|
4300
|
+
try {
|
|
4301
|
+
const docxResponse = await ctx.getDrive().files.export({
|
|
4302
|
+
fileId: a.documentId,
|
|
4303
|
+
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
4304
|
+
}, { responseType: "arraybuffer" });
|
|
4305
|
+
const docxResult = await resolveContextFromDocx(docxResponse.data);
|
|
4306
|
+
if (docxResult) {
|
|
4307
|
+
matchDocxToDriveComments(comments, docxResult, contextMap, flatText, offsetMap);
|
|
4308
|
+
}
|
|
4309
|
+
} catch (err) {
|
|
4310
|
+
ctx.log("Tier 2 DOCX context extraction failed:", err);
|
|
4311
|
+
}
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
3949
4314
|
const formattedComments = comments.map((comment, index) => {
|
|
3950
|
-
const replies = comment.replies?.length || 0;
|
|
3951
4315
|
const status = comment.resolved ? " [RESOLVED]" : "";
|
|
3952
4316
|
const author = comment.author?.displayName || "Unknown";
|
|
3953
4317
|
const date = comment.createdTime ? new Date(comment.createdTime).toLocaleDateString() : "Unknown date";
|
|
3954
|
-
const quotedText = comment.quotedFileContent?.value
|
|
3955
|
-
const
|
|
3956
|
-
let
|
|
3957
|
-
|
|
3958
|
-
if (
|
|
3959
|
-
|
|
3960
|
-
|
|
4318
|
+
const quotedText = comment.quotedFileContent?.value;
|
|
4319
|
+
const commentCtx = contextMap.get(comment.id);
|
|
4320
|
+
let positionInfo = "";
|
|
4321
|
+
const indexStr = commentCtx?.startIndex != null ? ` [chars ${commentCtx.startIndex}-${commentCtx.endIndex}]` : "";
|
|
4322
|
+
if (quotedText) {
|
|
4323
|
+
const snippet = quotedText.length > 100 ? quotedText.substring(0, 100) + "..." : quotedText;
|
|
4324
|
+
positionInfo = `
|
|
4325
|
+
Anchored to: "${snippet}"${indexStr}`;
|
|
4326
|
+
}
|
|
4327
|
+
if (commentCtx) {
|
|
4328
|
+
if (commentCtx.contextBefore) positionInfo += `
|
|
4329
|
+
Context before: "${commentCtx.contextBefore}"`;
|
|
4330
|
+
if (commentCtx.contextAfter) positionInfo += `
|
|
4331
|
+
Context after: "${commentCtx.contextAfter}"`;
|
|
4332
|
+
}
|
|
4333
|
+
let result = `${index + 1}. ${author} (${date})${status}${positionInfo}
|
|
4334
|
+
Comment: ${comment.content}`;
|
|
4335
|
+
if (comment.replies && comment.replies.length > 0) {
|
|
4336
|
+
for (const reply of comment.replies) {
|
|
4337
|
+
const replyAuthor = reply.author?.displayName || "Unknown";
|
|
4338
|
+
const replyDate = reply.createdTime ? new Date(reply.createdTime).toLocaleDateString() : "Unknown date";
|
|
4339
|
+
const replyContent = reply.content || "(empty)";
|
|
4340
|
+
result += `
|
|
4341
|
+
\u2514\u2500 ${replyAuthor} (${replyDate}): ${replyContent}`;
|
|
4342
|
+
}
|
|
3961
4343
|
}
|
|
3962
4344
|
result += `
|
|
3963
4345
|
Comment ID: ${comment.id}`;
|