@mcpher/gas-fakes 2.3.8 → 2.3.10

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.
@@ -0,0 +1,62 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ const PROGRESS_DIR = './progress';
5
+ const SKILLS_DIR = './gf_agent/skills';
6
+ const INDEX_FILE = './gf_agent/index.md';
7
+
8
+ async function build() {
9
+ await fs.mkdir(SKILLS_DIR, { recursive: true });
10
+
11
+ const files = await fs.readdir(PROGRESS_DIR);
12
+ const mdFiles = files.filter(f => f.endsWith('.md') || f.endsWith('.MD'));
13
+
14
+ let masterIndex = '# gf_agent Skills Index\n\nThis index lists all Google Apps Script services and classes supported by `gf_agent` via `gas-fakes`.\n\n';
15
+
16
+ for (const file of mdFiles) {
17
+ const content = await fs.readFile(path.join(PROGRESS_DIR, file), 'utf-8');
18
+ const serviceName = file.replace(/\.md$/i, '');
19
+
20
+ // Extract classes
21
+ const classMatches = content.matchAll(/## Class: \[(.*?)\]/g);
22
+ const classes = [];
23
+
24
+ for (const match of classMatches) {
25
+ const className = match[1];
26
+ // Find the table for this class
27
+ const classSection = content.slice(match.index);
28
+ const tableEnd = classSection.indexOf('## Class:') > 0 ? classSection.indexOf('## Class:', 10) : classSection.length;
29
+ const tableContent = classSection.slice(0, tableEnd);
30
+
31
+ // Extract completed methods
32
+ const methodMatches = tableContent.matchAll(/\| \[(.*?)\]\(.*?\) \| .*? \| .*? \| .*? \| (completed) \|/g);
33
+ const methods = Array.from(methodMatches).map(m => m[1]);
34
+
35
+ if (methods.length > 0) {
36
+ classes.push({ name: className, methods });
37
+ }
38
+ }
39
+
40
+ if (classes.length > 0) {
41
+ const skillFile = `${serviceName.toLowerCase()}.md`;
42
+ let skillContent = `# Service: ${serviceName}\n\n`;
43
+
44
+ classes.forEach(c => {
45
+ skillContent += `## Class: ${c.name}\n\n`;
46
+ skillContent += `Supported Methods:\n`;
47
+ c.methods.forEach(m => {
48
+ skillContent += `- \`${m}\`\n`;
49
+ });
50
+ skillContent += '\n';
51
+ });
52
+
53
+ await fs.writeFile(path.join(SKILLS_DIR, skillFile), skillContent);
54
+ masterIndex += `- [${serviceName}](skills/${skillFile})\n`;
55
+ }
56
+ }
57
+
58
+ await fs.writeFile(INDEX_FILE, masterIndex);
59
+ console.log('Build complete! Skills and Index generated.');
60
+ }
61
+
62
+ build().catch(console.error);
package/package.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "dependencies": {
6
6
  "@azure/identity": "^4.13.1",
7
7
  "@mcpher/fake-gasenum": "^1.0.6",
8
+ "@mcpher/gas-fakes": "^2.3.9",
8
9
  "@mcpher/gas-flex-cache": "^1.1.5",
9
10
  "@microsoft/microsoft-graph-client": "^3.0.7",
10
11
  "@modelcontextprotocol/sdk": "^1.28.0",
@@ -34,11 +35,12 @@
34
35
  "pub": "npm publish --access public",
35
36
  "includes": "node ./doccreation/include-docs.js",
36
37
  "progress": "cd ./doccreation && bash pipeline.sh",
37
- "docs": "npm run includes && npm run progress"
38
+ "docs": "npm run includes && npm run progress && npm run gf_agent",
39
+ "gf_agent": "node ./gf_agent/scripts/builder.js"
38
40
  },
39
41
  "name": "@mcpher/gas-fakes",
40
42
  "author": "bruce mcpherson",
41
- "version": "2.3.8",
43
+ "version": "2.3.10",
42
44
  "license": "MIT",
43
45
  "main": "main.js",
44
46
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
package/src/cli/mcp.js CHANGED
@@ -1,49 +1,15 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { spawn } from "child_process";
4
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
3
  import { z } from "zod";
4
+ import { spawn } from "child_process";
7
5
  import { MCP_VERSION } from "./utils.js";
8
- import { getLibraries } from "./lib-manager.js";
9
-
10
- /**
11
- * Constructs the CLI arguments array for gas-fakes execution.
12
- */
13
- function buildCliArguments(params) {
14
- const {
15
- filename,
16
- script,
17
- args,
18
- sandbox,
19
- whitelistRead,
20
- whitelistReadWrite,
21
- whitelistReadWriteTrash,
22
- json,
23
- } = params;
24
-
25
- const cliArgs = [];
26
-
27
- if (filename) cliArgs.push("-f", filename);
28
- if (script) cliArgs.push("-s", script);
29
- if (args) cliArgs.push("-a", JSON.stringify(args));
30
- if (sandbox) cliArgs.push("-x");
31
- if (whitelistRead) cliArgs.push("-w", whitelistRead);
32
- if (whitelistReadWrite) cliArgs.push("--ww", whitelistReadWrite);
33
- if (whitelistReadWriteTrash) cliArgs.push("--wt", whitelistReadWriteTrash);
34
- if (json) cliArgs.push("-j", JSON.stringify(json));
35
-
36
- return cliArgs;
37
- }
38
6
 
39
7
  /**
40
- * Executes the gas-fakes command via child_process.spawn.
8
+ * Executes a GAS script using the gas-fakes CLI logic.
41
9
  */
42
- async function runGasFakesProcess(cliArgs) {
10
+ async function runGasFakes(script) {
43
11
  return new Promise((resolve) => {
44
- // Invoke the current node executable with the current script (gas-fakes.js)
45
- // We assume process.argv[1] points to the entry point
46
- const child = spawn(process.execPath, [process.argv[1], ...cliArgs], {
12
+ const child = spawn(process.execPath, [process.argv[1], "-s", script], {
47
13
  env: process.env,
48
14
  stdio: ["ignore", "pipe", "pipe"],
49
15
  shell: false,
@@ -52,223 +18,114 @@ async function runGasFakesProcess(cliArgs) {
52
18
  let stdoutData = "";
53
19
  let stderrData = "";
54
20
 
55
- child.stdout.on("data", (data) => {
56
- stdoutData += data.toString();
57
- });
58
-
59
- child.stderr.on("data", (data) => {
60
- stderrData += data.toString();
61
- });
21
+ child.stdout.on("data", (data) => (stdoutData += data.toString()));
22
+ child.stderr.on("data", (data) => (stderrData += data.toString()));
62
23
 
63
24
  child.on("close", (code) => {
64
25
  if (code === 0) {
65
26
  resolve({
66
- content: [
67
- { type: "text", text: stdoutData || "Execution finished." },
68
- ],
27
+ content: [{ type: "text", text: stdoutData || "Success" }],
69
28
  isError: false,
70
29
  });
71
30
  } else {
72
- const output = stderrData || stdoutData || "Unknown error occurred";
73
31
  resolve({
74
- content: [{ type: "text", text: output }],
32
+ content: [{ type: "text", text: stderrData || stdoutData || "Error" }],
75
33
  isError: true,
76
34
  });
77
35
  }
78
36
  });
79
-
80
- child.on("error", (err) => {
81
- resolve({
82
- content: [{ type: "text", text: err.message }],
83
- isError: true,
84
- });
85
- });
86
- });
87
- }
88
-
89
- function registerDefaultTool(server) {
90
- const schema1 = {
91
- description: [
92
- `Use this to safely run Google Apps Script in a sandbox using gas-fakes.`,
93
- `# Important`,
94
- `- Use the extension of the Google Apps Script files as \`js\`. Don't use \`gs\``,
95
- `- When providing script content, ensure functions are called (e.g., add \`sample();\`).`,
96
- ].join("\n"),
97
- inputSchema: {
98
- filename: z
99
- .string()
100
- .optional()
101
- .describe(`Path to the file containing Google Apps Script.`),
102
- script: z
103
- .string()
104
- .optional()
105
- .describe(`Direct GAS script content string.`),
106
- sandbox: z
107
- .boolean()
108
- .describe("Use to run Google Apps Script in a sandbox."),
109
- whitelistRead: z
110
- .string()
111
- .optional()
112
- .describe("Whitelist of file IDs for readonly access."),
113
- whitelistReadWrite: z
114
- .string()
115
- .optional()
116
- .describe("Whitelist of file IDs for read/write access."),
117
- whitelistReadWriteTrash: z
118
- .string()
119
- .optional()
120
- .describe("Whitelist of file IDs for read/write/trash access."),
121
- json: z.any().optional().describe("Advanced sandbox configuration JSON."),
122
- },
123
- };
124
-
125
- server.registerTool("run-gas-by-gas-fakes", schema1, async (args) => {
126
- if (!args.filename && !args.script) {
127
- return {
128
- content: [
129
- {
130
- type: "text",
131
- text: "Error: Either `filename` or `script` is required.",
132
- },
133
- ],
134
- isError: true,
135
- };
136
- }
137
- const cliArgs = buildCliArguments(args);
138
- return await runGasFakesProcess(cliArgs);
139
- });
140
-
141
- const schema2 = {
142
- description: "Use this to create the tools of the MCP server...",
143
- inputSchema: {
144
- filename: z.string().describe("Filename of the tool file (.js)."),
145
- tools: z.array(
146
- z.object({
147
- name: z.string(),
148
- schema: z.string(),
149
- gas_script: z.string(),
150
- libraries: z.array(z.string()).default([]),
151
- })
152
- ),
153
- },
154
- };
155
-
156
- server.registerTool("create-new-tools", schema2, async (args) => {
157
- if (!args.filename || !args.tools) {
158
- return {
159
- content: [
160
- {
161
- type: "text",
162
- text: "Error: `filename` and `tools` are required.",
163
- },
164
- ],
165
- isError: true,
166
- };
167
- }
168
-
169
- const tool_ar = [];
170
- for (let i = 0; i < args.tools.length; i++) {
171
- const { name, schema, gas_script, libraries } = args.tools[i];
172
- tool_ar.push(
173
- `{ name: "${name}", schema: ${schema}, func: (object = {}) => { \n\n${gas_script} }, libraries: ${JSON.stringify(
174
- libraries || []
175
- )} }`
176
- );
177
- }
178
-
179
- const tool_script = [
180
- `import { z } from "zod";`,
181
- ``,
182
- `const tools = [${tool_ar.join(", ")}];`,
183
- ].join("\n");
184
- const absolutePath = path.resolve(process.cwd(), args.filename);
185
- fs.writeFileSync(absolutePath, tool_script);
186
- return {
187
- content: [
188
- {
189
- type: "text",
190
- text: `A new file including tools for gas-fakes-mcp was successfully created as "${absolutePath}".`,
191
- },
192
- ],
193
- isError: false,
194
- };
195
37
  });
196
38
  }
197
39
 
198
- async function registerCustomTools(server, toolsPath) {
199
- if (!toolsPath || !fs.existsSync(toolsPath)) {
200
- if (toolsPath) console.error(`No tool file: ${toolsPath}`);
201
- return;
40
+ const SERVICES = [
41
+ {
42
+ name: "spreadsheet_service",
43
+ description: "Automate Google Sheets: Create, read, and modify spreadsheets, ranges, and formatting.",
44
+ example: "const ss = SpreadsheetApp.create('Test'); ss.getActiveSheet().getRange('A1').setValue('Hello');"
45
+ },
46
+ {
47
+ name: "document_service",
48
+ description: "Automate Google Docs: Create and edit documents, paragraphs, tables, and styles.",
49
+ example: "const doc = DocumentApp.create('Hello'); doc.getBody().appendParagraph('World');"
50
+ },
51
+ {
52
+ name: "drive_service",
53
+ description: "Manage Google Drive: Search files, create folders, and handle permissions.",
54
+ example: "const files = DriveApp.getFilesByName('Test'); while(files.hasNext()) console.log(files.next().getName());"
55
+ },
56
+ {
57
+ name: "gmail_service",
58
+ description: "Automate Gmail: Send emails, search threads, and manage labels.",
59
+ example: "GmailApp.sendEmail('test@example.com', 'Subject', 'Body');"
60
+ },
61
+ {
62
+ name: "calendar_service",
63
+ description: "Manage Google Calendar: Create events, list calendars, and handle invitations.",
64
+ example: "CalendarApp.getDefaultCalendar().createEvent('Meeting', new Date(), new Date());"
65
+ },
66
+ {
67
+ name: "slides_service",
68
+ description: "Automate Google Slides: Create and edit presentations, slides, and shapes.",
69
+ example: "const deck = SlidesApp.create('Presentation'); deck.appendSlide().insertShape(SlidesApp.ShapeType.RECTANGLE);"
70
+ },
71
+ {
72
+ name: "forms_service",
73
+ description: "Automate Google Forms: Create forms, add items, and manage responses.",
74
+ example: "const form = FormApp.create('Survey'); form.addTextItem().setTitle('Name');"
75
+ },
76
+ {
77
+ name: "jdbc_service",
78
+ description: "Connect to databases via JDBC: Execute SQL queries and manage connections.",
79
+ example: "const conn = Jdbc.getConnection(url, user, pass); const stmt = conn.createStatement();"
80
+ },
81
+ {
82
+ name: "utilities_service",
83
+ description: "General utilities: Formatting, parsing, and base64 encoding/decoding.",
84
+ example: "const base64 = Utilities.base64Encode('hello');"
85
+ },
86
+ {
87
+ name: "urlfetch_service",
88
+ description: "HTTP Requests: Fetch external resources and APIs via GET/POST.",
89
+ example: "const res = UrlFetchApp.fetch('https://api.example.com'); console.log(res.getContentText());"
202
90
  }
91
+ ];
203
92
 
204
- const absolutePath = path.resolve(process.cwd(), toolsPath);
205
- let toolsStr = fs.readFileSync(absolutePath, "utf8");
206
- toolsStr = toolsStr.replace(/^import.*/gm, "");
207
- const getTools = new Function("z", `${toolsStr} return tools || [];`);
208
- const tools = getTools(z);
209
-
210
- if (!tools || tools.length === 0) return;
211
-
212
- for (let i = 0; i < tools.length; i++) {
213
- const tool = tools[i];
214
- const extendedSchema = { ...tool.schema };
215
- extendedSchema.inputSchema = {
216
- gas_args: z
217
- .object(tool.schema.inputSchema)
218
- .describe("Arguments for Google Apps Script."),
219
- sandbox: z.boolean().describe("Run in sandbox."),
220
- whitelistRead: z.string().optional(),
221
- whitelistReadWrite: z.string().optional(),
222
- whitelistReadWriteTrash: z.string().optional(),
223
- json: z.any().optional(),
224
- };
225
-
226
- let originalFuncStr = tool.func.toString();
227
- if (tool.libraries && tool.libraries.length > 0) {
228
- const gas_library = await getLibraries({ libraries: tool.libraries });
229
-
230
- if (gas_library && gas_library.length > 0) {
231
- const libs = gas_library.reduce((ar, { identifier, libScript }) => {
232
- if (originalFuncStr.includes(identifier)) {
233
- ar.push(libScript);
234
- }
235
- return ar;
236
- }, []);
237
- if (libs.length > 0) {
238
- originalFuncStr = `(object = {}) => {\n\n${libs.join(
239
- "\n\n"
240
- )}\n\nconst main_gas_fakes = ${originalFuncStr}\n\nreturn main_gas_fakes(object);\n}`;
241
- }
242
- }
243
- }
244
-
245
- const toolHandler = async (opts) => {
246
- const wrappedScript = `return (${originalFuncStr})(args)`;
247
- const cliArgs = buildCliArguments({
248
- script: wrappedScript,
249
- args: opts.gas_args,
250
- ...opts,
251
- });
252
- return await runGasFakesProcess(cliArgs);
253
- };
254
-
255
- server.registerTool(tool.name, extendedSchema, toolHandler);
256
- }
257
- }
258
-
259
- export async function startMcpServer(options) {
260
- const { tools } = options;
93
+ export async function startMcpServer() {
261
94
  const server = new McpServer({
262
- name: "gas-fakes-mcp",
95
+ name: "gas-fakes-skills-mcp",
263
96
  version: MCP_VERSION,
264
97
  });
265
98
 
266
- registerDefaultTool(server);
267
-
268
- if (tools) {
269
- await registerCustomTools(server, tools);
99
+ // Register a tool for each high-level service
100
+ for (const service of SERVICES) {
101
+ server.registerTool(
102
+ service.name,
103
+ {
104
+ description: `${service.description}\nExample script:\n${service.example}`,
105
+ inputSchema: {
106
+ script: z.string().describe("The Google Apps Script code to execute locally."),
107
+ },
108
+ },
109
+ async ({ script }) => {
110
+ return await runGasFakes(script);
111
+ }
112
+ );
270
113
  }
271
114
 
115
+ // Also add a generic tool for tasks that span multiple services
116
+ server.registerTool(
117
+ "workspace_agent",
118
+ {
119
+ description: "A general-purpose agent to automate tasks across multiple Google Workspace services (Sheets, Docs, Drive, etc.)",
120
+ inputSchema: {
121
+ script: z.string().describe("The Google Apps Script code to execute locally."),
122
+ },
123
+ },
124
+ async ({ script }) => {
125
+ return await runGasFakes(script);
126
+ }
127
+ );
128
+
272
129
  const transport = new StdioServerTransport();
273
130
  await server.connect(transport);
274
131
  }
package/src/cli/setup.js CHANGED
@@ -610,6 +610,41 @@ export async function initializeConfiguration(options = {}) {
610
610
 
611
611
  fs.writeFileSync(envPath, envContent + "\n", "utf8");
612
612
  console.log("Setup complete. Your .env file has been updated.");
613
+
614
+ // --- Skill Installation Hint ---
615
+ console.log("\n--- Gemini CLI Integration ---");
616
+ const skillResponse = await prompts({
617
+ type: "confirm",
618
+ name: "installSkills",
619
+ message: "Would you like to install the gas-fakes skills for Gemini CLI?",
620
+ initial: true,
621
+ });
622
+
623
+ if (skillResponse.installSkills) {
624
+ console.log("Installing Gemini CLI skills and MCP server...");
625
+ try {
626
+ // 1. Install the agent skill
627
+ const skillCmd = "gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
628
+ console.log(`Executing: ${skillCmd}`);
629
+ execSync(skillCmd, { stdio: "inherit" });
630
+
631
+ // 2. Add the MCP server
632
+ const mcpCmd = "gemini mcp add --scope project gas-fakes-mcp gas-fakes mcp";
633
+ console.log(`Executing: ${mcpCmd}`);
634
+ execSync(mcpCmd, { stdio: "inherit" });
635
+
636
+ console.log("\x1b[1;32mInstallation complete!\x1b[0m");
637
+ console.log("\nYou can now use natural language to automate tasks:");
638
+ console.log(" \x1b[1;33m\"Create a spreadsheet of my recent Drive files\"\x1b[0m");
639
+ } catch (err) {
640
+ console.error(`\x1b[1;31mError during Gemini installation: ${err.message}\x1b[0m`);
641
+ console.log("You may need to install them manually:");
642
+ console.log("1. gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent");
643
+ console.log("2. gemini mcp add --scope project gas-fakes-mcp gas-fakes mcp");
644
+ }
645
+ } else {
646
+ console.log("Skipping Gemini CLI integration.");
647
+ }
613
648
  }
614
649
 
615
650
  /**
@@ -31,6 +31,108 @@ class FakeGmailMessage {
31
31
  return newFakeGmailThread(threadResource);
32
32
  }
33
33
 
34
+ /**
35
+ * Helper to get a header value.
36
+ * @param {string} name - Header name
37
+ * @returns {string}
38
+ */
39
+ __getHeader(name) {
40
+ if (this.__messageResource.payload && this.__messageResource.payload.headers) {
41
+ const header = this.__messageResource.payload.headers.find(h => h.name.toLowerCase() === name.toLowerCase());
42
+ return header ? header.value : '';
43
+ }
44
+ return '';
45
+ }
46
+
47
+ /**
48
+ * Gets the subject of this message.
49
+ * @returns {string} The subject.
50
+ */
51
+ getSubject() {
52
+ ScriptApp.__behavior.checkMethod('GmailMessage', 'getSubject');
53
+ return this.__getHeader('Subject');
54
+ }
55
+
56
+ /**
57
+ * Gets the date and time of this message.
58
+ * @returns {Date} The date and time.
59
+ */
60
+ getDate() {
61
+ ScriptApp.__behavior.checkMethod('GmailMessage', 'getDate');
62
+ if (this.__messageResource.internalDate) {
63
+ return new Date(parseInt(this.__messageResource.internalDate, 10));
64
+ }
65
+ const dateHeader = this.__getHeader('Date');
66
+ return dateHeader ? new Date(dateHeader) : new Date();
67
+ }
68
+
69
+ /**
70
+ * Gets the snippet of the email.
71
+ * @returns {string} The snippet.
72
+ */
73
+ getSnippet() {
74
+ ScriptApp.__behavior.checkMethod('GmailMessage', 'getSnippet');
75
+ return this.__messageResource.snippet || '';
76
+ }
77
+
78
+ /**
79
+ * Gets the sender of this message.
80
+ * @returns {string} The sender's email address.
81
+ */
82
+ getFrom() {
83
+ ScriptApp.__behavior.checkMethod('GmailMessage', 'getFrom');
84
+ return this.__getHeader('From');
85
+ }
86
+
87
+ /**
88
+ * Gets the recipient of this message.
89
+ * @returns {string} The recipient's email address.
90
+ */
91
+ getTo() {
92
+ ScriptApp.__behavior.checkMethod('GmailMessage', 'getTo');
93
+ return this.__getHeader('To');
94
+ }
95
+
96
+ /**
97
+ * Helper to extract body by mime type
98
+ * @param {Object} payload The message payload
99
+ * @param {string} mimeType The mime type to search for
100
+ * @returns {string} The decoded content
101
+ */
102
+ __extractBodyData(payload, mimeType) {
103
+ if (!payload) return '';
104
+ if (payload.mimeType === mimeType && payload.body && payload.body.data) {
105
+ return Buffer.from(payload.body.data, 'base64url').toString('utf8');
106
+ }
107
+ if (payload.parts && payload.parts.length > 0) {
108
+ for (const part of payload.parts) {
109
+ const data = this.__extractBodyData(part, mimeType);
110
+ if (data) return data;
111
+ }
112
+ }
113
+ return '';
114
+ }
115
+
116
+ /**
117
+ * Gets the HTML content of the body of this message.
118
+ * @returns {string} The body content.
119
+ */
120
+ getBody() {
121
+ ScriptApp.__behavior.checkMethod('GmailMessage', 'getBody');
122
+ const htmlData = this.__extractBodyData(this.__messageResource.payload, 'text/html');
123
+ if (htmlData) return htmlData;
124
+ return this.__extractBodyData(this.__messageResource.payload, 'text/plain');
125
+ }
126
+
127
+ /**
128
+ * Gets the plain-text content of the body of this message.
129
+ * @returns {string} The plain-text body content.
130
+ */
131
+ getPlainBody() {
132
+ ScriptApp.__behavior.checkMethod('GmailMessage', 'getPlainBody');
133
+ return this.__extractBodyData(this.__messageResource.payload, 'text/plain');
134
+ }
135
+
34
136
  toString() {
35
137
  return this.__fakeObjectType;
36
138
  }
@@ -20,6 +20,23 @@ class FakeGmailThread {
20
20
  return this.__threadResource.id;
21
21
  }
22
22
 
23
+ /**
24
+ * Gets the messages in this thread.
25
+ * @returns {GmailMessage[]} An array of messages in this thread.
26
+ */
27
+ getMessages() {
28
+ ScriptApp.__behavior.checkMethod('GmailThread', 'getMessages');
29
+ if (globalThis.GmailApp) {
30
+ return globalThis.GmailApp.getMessagesForThread(this);
31
+ }
32
+ // Fallback if GmailApp isn't initialized, though it should be.
33
+ if (this.__threadResource.messages) {
34
+ const { newFakeGmailMessage } = require('./fakegmailmessage.js');
35
+ return this.__threadResource.messages.map(m => newFakeGmailMessage(m));
36
+ }
37
+ return [];
38
+ }
39
+
23
40
  /**
24
41
  * Gets the labels of this thread.
25
42
  * @returns {GmailLabel[]} An array of labels for this thread.
@@ -0,0 +1,69 @@
1
+ import './main.js';
2
+
3
+ try {
4
+ console.log('Searching for emails from Martin...');
5
+
6
+ // Search Gmail for messages from Martin (getting up to 10 recent threads)
7
+ const threads = GmailApp.search('from:Martin', 0, 10);
8
+
9
+ if (threads.length === 0) {
10
+ console.log('No emails found from Martin.');
11
+ process.exit(0);
12
+ }
13
+
14
+ console.log(`Found ${threads.length} threads. Creating summary doc...`);
15
+
16
+ // Create the Document
17
+ const doc = DocumentApp.create('Email Summary: Martin');
18
+ const body = doc.getBody();
19
+
20
+ // Add a title
21
+ body.appendParagraph('Summary of Recent Emails from Martin')
22
+ .setHeading(DocumentApp.ParagraphHeading.TITLE);
23
+
24
+ body.appendParagraph(`Generated on: ${new Date().toLocaleString()}`);
25
+ body.appendParagraph('');
26
+
27
+ // We have to use the advanced Gmail service directly because gas-fakes'
28
+ // FakeGmailMessage does not yet support getSubject(), getDate(), or getSnippet()
29
+ for (const thread of threads) {
30
+ // Get the raw thread resource from the Gmail API
31
+ const threadResource = Gmail.Users.Threads.get('me', thread.getId());
32
+ if (!threadResource.messages || threadResource.messages.length === 0) continue;
33
+
34
+ const firstMsg = threadResource.messages[0];
35
+
36
+ // Extract Subject from headers
37
+ const headers = firstMsg.payload && firstMsg.payload.headers ? firstMsg.payload.headers : [];
38
+ const subjectHeader = headers.find(h => h.name.toLowerCase() === 'subject');
39
+ const dateHeader = headers.find(h => h.name.toLowerCase() === 'date');
40
+
41
+ const subject = subjectHeader ? subjectHeader.value : 'No Subject';
42
+ const date = dateHeader ? dateHeader.value : 'Unknown Date';
43
+
44
+ body.appendParagraph(`Subject: ${subject}`)
45
+ .setHeading(DocumentApp.ParagraphHeading.HEADING1);
46
+
47
+ for (const msg of threadResource.messages) {
48
+ const msgHeaders = msg.payload && msg.payload.headers ? msg.payload.headers : [];
49
+ const msgDateHeader = msgHeaders.find(h => h.name.toLowerCase() === 'date');
50
+ const msgDate = msgDateHeader ? msgDateHeader.value : 'Unknown Date';
51
+
52
+ body.appendParagraph(`Date: ${msgDate}`)
53
+ .setHeading(DocumentApp.ParagraphHeading.HEADING3);
54
+
55
+ const snippet = msg.snippet || "No content.";
56
+ body.appendParagraph(snippet);
57
+ }
58
+
59
+ body.appendParagraph(''); // Spacing
60
+ }
61
+
62
+ doc.saveAndClose();
63
+
64
+ console.log(`Successfully created document "${doc.getName()}"`);
65
+ console.log(`Document URL: ${doc.getUrl()}`);
66
+
67
+ } catch (error) {
68
+ console.error(`Error: ${error.message}`);
69
+ }