Initial commit
This commit is contained in:
155
skills/google-workspace/docs/create_doc.js
Normal file
155
skills/google-workspace/docs/create_doc.js
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Create Google Doc with Markdown Formatting
|
||||
*
|
||||
* Usage: bun create_doc.js <account> --title <title> [options]
|
||||
*
|
||||
* Options:
|
||||
* --title Document title (required)
|
||||
* --content Markdown content
|
||||
* --folder Subfolder in Geoffrey (Research, Notes, Reports, Travel)
|
||||
* --raw Don't apply markdown formatting (plain text)
|
||||
*
|
||||
* Examples:
|
||||
* bun create_doc.js hrg --title "Meeting Notes" --folder Notes
|
||||
* bun create_doc.js psd --title "Report" --content "# Header\n\nContent" --folder Research
|
||||
*/
|
||||
|
||||
const { google } = require('googleapis');
|
||||
const path = require('path');
|
||||
const { getAuthClient } = require(path.join(__dirname, '..', 'auth', 'token_manager'));
|
||||
const { ensureGeoffreyFolders } = require(path.join(__dirname, '..', 'utils', 'folder_manager'));
|
||||
const { insertFormattedContent } = require(path.join(__dirname, '..', 'utils', 'markdown_to_docs'));
|
||||
|
||||
async function createDoc(account, options) {
|
||||
const auth = await getAuthClient(account);
|
||||
const docs = google.docs({ version: 'v1', auth });
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
// Get or create Geoffrey folder structure
|
||||
let folderId = null;
|
||||
if (options.folder) {
|
||||
const folderResult = await ensureGeoffreyFolders(account, options.folder);
|
||||
folderId = folderResult.targetFolder;
|
||||
}
|
||||
|
||||
// Create the document
|
||||
const createResponse = await docs.documents.create({
|
||||
requestBody: {
|
||||
title: options.title,
|
||||
},
|
||||
});
|
||||
|
||||
const documentId = createResponse.data.documentId;
|
||||
|
||||
// Move to Geoffrey folder if specified
|
||||
if (folderId) {
|
||||
// Get current parents
|
||||
const file = await drive.files.get({
|
||||
fileId: documentId,
|
||||
fields: 'parents',
|
||||
});
|
||||
|
||||
// Move to Geoffrey folder
|
||||
await drive.files.update({
|
||||
fileId: documentId,
|
||||
addParents: folderId,
|
||||
removeParents: file.data.parents.join(','),
|
||||
fields: 'id, parents',
|
||||
});
|
||||
}
|
||||
|
||||
// Add content if provided
|
||||
let formattingInfo = null;
|
||||
if (options.content) {
|
||||
if (options.raw) {
|
||||
// Plain text insertion
|
||||
await docs.documents.batchUpdate({
|
||||
documentId,
|
||||
requestBody: {
|
||||
requests: [{
|
||||
insertText: {
|
||||
location: { index: 1 },
|
||||
text: options.content,
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Markdown formatted insertion
|
||||
formattingInfo = await insertFormattedContent(docs, documentId, options.content);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
account,
|
||||
document: {
|
||||
id: documentId,
|
||||
title: createResponse.data.title,
|
||||
url: `https://docs.google.com/document/d/${documentId}/edit`,
|
||||
folder: options.folder || 'root',
|
||||
},
|
||||
formatting: formattingInfo,
|
||||
metadata: {
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const account = args[0];
|
||||
|
||||
if (!account) {
|
||||
console.error(JSON.stringify({
|
||||
error: 'Missing account',
|
||||
usage: 'bun create_doc.js <account> --title <title> [--content <markdown>] [--folder <subfolder>]'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse options
|
||||
const options = {};
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--title':
|
||||
options.title = args[++i];
|
||||
break;
|
||||
case '--content':
|
||||
options.content = args[++i];
|
||||
break;
|
||||
case '--folder':
|
||||
options.folder = args[++i];
|
||||
break;
|
||||
case '--raw':
|
||||
options.raw = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.title) {
|
||||
console.error(JSON.stringify({
|
||||
error: 'Missing --title option',
|
||||
usage: 'bun create_doc.js <account> --title <title> [--content <markdown>] [--folder <subfolder>]'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createDoc(account, options);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({
|
||||
error: error.message,
|
||||
account,
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
module.exports = { createDoc };
|
||||
282
skills/google-workspace/docs/edit_doc.js
Normal file
282
skills/google-workspace/docs/edit_doc.js
Normal file
@@ -0,0 +1,282 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Edit Google Doc with Markdown Formatting
|
||||
*
|
||||
* Usage: bun edit_doc.js <account> <document-id> --append <markdown>
|
||||
* bun edit_doc.js <account> <document-id> --replace <old> --with <new>
|
||||
*
|
||||
* Options:
|
||||
* --raw Don't apply markdown formatting (plain text)
|
||||
*
|
||||
* Examples:
|
||||
* bun edit_doc.js psd DOC_ID --append "## New Section\n\nContent here"
|
||||
* bun edit_doc.js psd DOC_ID --append "Plain text" --raw
|
||||
*/
|
||||
|
||||
const { google } = require('googleapis');
|
||||
const path = require('path');
|
||||
const { getAuthClient } = require(path.join(__dirname, '..', 'auth', 'token_manager'));
|
||||
const { parseMarkdown } = require(path.join(__dirname, '..', 'utils', 'markdown_to_docs'));
|
||||
|
||||
/**
|
||||
* Parse markdown with custom start index (for appending)
|
||||
*/
|
||||
function parseMarkdownAtIndex(markdown, startIndex) {
|
||||
const requests = [];
|
||||
let plainText = '';
|
||||
let currentIndex = startIndex;
|
||||
|
||||
const lines = markdown.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
|
||||
// Horizontal rule
|
||||
if (line.match(/^---+$/)) {
|
||||
const ruleLine = '─'.repeat(50) + '\n';
|
||||
plainText += ruleLine;
|
||||
currentIndex += ruleLine.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Headers
|
||||
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headerMatch) {
|
||||
const level = headerMatch[1].length;
|
||||
const headerContent = headerMatch[2];
|
||||
const { plainText: processedText, requests: inlineRequests } =
|
||||
processInlineFormattingAtIndex(headerContent, currentIndex);
|
||||
|
||||
const fullLine = processedText + '\n';
|
||||
const headingStyle = level === 1 ? 'HEADING_1' :
|
||||
level === 2 ? 'HEADING_2' :
|
||||
level === 3 ? 'HEADING_3' : 'HEADING_4';
|
||||
|
||||
requests.push({
|
||||
updateParagraphStyle: {
|
||||
range: { startIndex: currentIndex, endIndex: currentIndex + fullLine.length },
|
||||
paragraphStyle: { namedStyleType: headingStyle },
|
||||
fields: 'namedStyleType',
|
||||
},
|
||||
});
|
||||
requests.push(...inlineRequests);
|
||||
plainText += fullLine;
|
||||
currentIndex += fullLine.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bullet lists
|
||||
const bulletMatch = line.match(/^[-*]\s+(.+)$/);
|
||||
if (bulletMatch) {
|
||||
const { plainText: processedText, requests: inlineRequests } =
|
||||
processInlineFormattingAtIndex(bulletMatch[1], currentIndex + 2);
|
||||
const fullLine = '• ' + processedText + '\n';
|
||||
plainText += fullLine;
|
||||
requests.push(...inlineRequests);
|
||||
currentIndex += fullLine.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Numbered lists
|
||||
const numberedMatch = line.match(/^(\d+)\.\s+(.+)$/);
|
||||
if (numberedMatch) {
|
||||
const prefix = numberedMatch[1] + '. ';
|
||||
const { plainText: processedText, requests: inlineRequests } =
|
||||
processInlineFormattingAtIndex(numberedMatch[2], currentIndex + prefix.length);
|
||||
const fullLine = prefix + processedText + '\n';
|
||||
plainText += fullLine;
|
||||
requests.push(...inlineRequests);
|
||||
currentIndex += fullLine.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular line
|
||||
const { plainText: processedText, requests: inlineRequests } =
|
||||
processInlineFormattingAtIndex(line, currentIndex);
|
||||
const fullLine = processedText + '\n';
|
||||
plainText += fullLine;
|
||||
requests.push(...inlineRequests);
|
||||
currentIndex += fullLine.length;
|
||||
}
|
||||
|
||||
return { text: plainText, requests };
|
||||
}
|
||||
|
||||
function processInlineFormattingAtIndex(text, startIndex) {
|
||||
const requests = [];
|
||||
let plainText = '';
|
||||
let charIndex = startIndex;
|
||||
let remaining = text;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
|
||||
if (boldMatch) {
|
||||
plainText += boldMatch[1];
|
||||
requests.push({
|
||||
updateTextStyle: {
|
||||
range: { startIndex: charIndex, endIndex: charIndex + boldMatch[1].length },
|
||||
textStyle: { bold: true },
|
||||
fields: 'bold',
|
||||
},
|
||||
});
|
||||
charIndex += boldMatch[1].length;
|
||||
remaining = remaining.substring(boldMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (linkMatch) {
|
||||
plainText += linkMatch[1];
|
||||
requests.push({
|
||||
updateTextStyle: {
|
||||
range: { startIndex: charIndex, endIndex: charIndex + linkMatch[1].length },
|
||||
textStyle: { link: { url: linkMatch[2] } },
|
||||
fields: 'link',
|
||||
},
|
||||
});
|
||||
charIndex += linkMatch[1].length;
|
||||
remaining = remaining.substring(linkMatch[0].length);
|
||||
continue;
|
||||
}
|
||||
|
||||
plainText += remaining[0];
|
||||
charIndex++;
|
||||
remaining = remaining.substring(1);
|
||||
}
|
||||
|
||||
return { plainText, requests };
|
||||
}
|
||||
|
||||
async function editDoc(account, documentId, options) {
|
||||
const auth = await getAuthClient(account);
|
||||
const docs = google.docs({ version: 'v1', auth });
|
||||
|
||||
if (options.append) {
|
||||
// Get document to find end index
|
||||
const doc = await docs.documents.get({ documentId });
|
||||
const endIndex = doc.data.body.content[doc.data.body.content.length - 1].endIndex - 1;
|
||||
|
||||
if (options.raw) {
|
||||
// Plain text append
|
||||
await docs.documents.batchUpdate({
|
||||
documentId,
|
||||
requestBody: {
|
||||
requests: [{
|
||||
insertText: {
|
||||
location: { index: endIndex },
|
||||
text: '\n' + options.append,
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
account,
|
||||
documentId,
|
||||
updates: 1,
|
||||
metadata: { timestamp: new Date().toISOString() }
|
||||
};
|
||||
}
|
||||
|
||||
// Formatted append
|
||||
const { text, requests } = parseMarkdownAtIndex(options.append, endIndex + 1);
|
||||
|
||||
// Insert text
|
||||
await docs.documents.batchUpdate({
|
||||
documentId,
|
||||
requestBody: {
|
||||
requests: [{
|
||||
insertText: {
|
||||
location: { index: endIndex },
|
||||
text: '\n' + text,
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
// Apply formatting
|
||||
if (requests.length > 0) {
|
||||
await docs.documents.batchUpdate({
|
||||
documentId,
|
||||
requestBody: { requests },
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
account,
|
||||
documentId,
|
||||
updates: 1 + requests.length,
|
||||
formatting: { textLength: text.length, formattingRequests: requests.length },
|
||||
metadata: { timestamp: new Date().toISOString() }
|
||||
};
|
||||
}
|
||||
|
||||
if (options.replace && options.with) {
|
||||
await docs.documents.batchUpdate({
|
||||
documentId,
|
||||
requestBody: {
|
||||
requests: [{
|
||||
replaceAllText: {
|
||||
containsText: { text: options.replace, matchCase: true },
|
||||
replaceText: options.with,
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
account,
|
||||
documentId,
|
||||
updates: 1,
|
||||
metadata: { timestamp: new Date().toISOString() }
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, error: 'No edit operation specified' };
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const account = args[0];
|
||||
const documentId = args[1];
|
||||
|
||||
if (!account || !documentId) {
|
||||
console.error(JSON.stringify({
|
||||
error: 'Missing arguments',
|
||||
usage: 'bun edit_doc.js <account> <document-id> --append <markdown>'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const options = {};
|
||||
for (let i = 2; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--append': options.append = args[++i]; break;
|
||||
case '--replace': options.replace = args[++i]; break;
|
||||
case '--with': options.with = args[++i]; break;
|
||||
case '--raw': options.raw = true; break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await editDoc(account, documentId, options);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({
|
||||
error: error.message,
|
||||
account,
|
||||
documentId,
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { editDoc };
|
||||
87
skills/google-workspace/docs/read_doc.js
Normal file
87
skills/google-workspace/docs/read_doc.js
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Read Google Doc Content
|
||||
*
|
||||
* Usage: node read_doc.js <account> <document-id>
|
||||
*
|
||||
* Examples:
|
||||
* node read_doc.js psd 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms
|
||||
*/
|
||||
|
||||
const { google } = require('googleapis');
|
||||
const path = require('path');
|
||||
const { getAuthClient } = require(path.join(__dirname, '..', 'auth', 'token_manager'));
|
||||
|
||||
async function readDoc(account, documentId) {
|
||||
const auth = await getAuthClient(account);
|
||||
const docs = google.docs({ version: 'v1', auth });
|
||||
|
||||
const response = await docs.documents.get({
|
||||
documentId,
|
||||
});
|
||||
|
||||
const doc = response.data;
|
||||
|
||||
// Extract text content from document body
|
||||
let textContent = '';
|
||||
if (doc.body && doc.body.content) {
|
||||
for (const element of doc.body.content) {
|
||||
if (element.paragraph && element.paragraph.elements) {
|
||||
for (const textElement of element.paragraph.elements) {
|
||||
if (textElement.textRun && textElement.textRun.content) {
|
||||
textContent += textElement.textRun.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (element.table) {
|
||||
textContent += '[TABLE]\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
account,
|
||||
document: {
|
||||
id: doc.documentId,
|
||||
title: doc.title,
|
||||
textContent: textContent.trim(),
|
||||
revisionId: doc.revisionId,
|
||||
},
|
||||
metadata: {
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// CLI interface
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const account = args[0];
|
||||
const documentId = args[1];
|
||||
|
||||
if (!account || !documentId) {
|
||||
console.error(JSON.stringify({
|
||||
error: 'Missing arguments',
|
||||
usage: 'node read_doc.js <account> <document-id>'
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await readDoc(account, documentId);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
console.error(JSON.stringify({
|
||||
error: error.message,
|
||||
account,
|
||||
documentId,
|
||||
}));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
module.exports = { readDoc };
|
||||
Reference in New Issue
Block a user