@mcpher/gas-fakes 2.3.8 → 2.3.9
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 +2 -0
- package/gf_agent/scripts/builder.js +62 -0
- package/package.json +1 -1
- package/src/cli/mcp.js +89 -232
- package/src/cli/setup.js +35 -0
- package/src/services/gmailapp/fakegmailmessage.js +62 -0
- package/src/services/gmailapp/fakegmailthread.js +17 -0
- package/summarize_advanced.js +69 -0
package/README.md
CHANGED
|
@@ -183,6 +183,8 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
183
183
|
- [gas fakes intro video](https://youtu.be/oEjpIrkYpEM)
|
|
184
184
|
- [getting started](GETTING_STARTED.md) - how to handle authentication for Workspace scopes.
|
|
185
185
|
- [readme](README.md)
|
|
186
|
+
- [Natural Language Automation with Gemini Skills & MCP Server](gemini-skills-mcp.md) - new skills-based agent approach.
|
|
187
|
+
- [gf_agent documentation](../gf_agent/README.md) - instructions for the Gemini CLI automation agent and MCP server.
|
|
186
188
|
- [gas fakes cli](gas-fakes-cli.md)
|
|
187
189
|
- [github actions using adc](https://github.com/brucemcpherson/gas-fakes-actions-adc)
|
|
188
190
|
- [github actions using dwd and wif](https://github.com/brucemcpherson/gas-fakes-actions-dwd)
|
|
@@ -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
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"name": "@mcpher/gas-fakes",
|
|
40
40
|
"author": "bruce mcpherson",
|
|
41
|
-
"version": "2.3.
|
|
41
|
+
"version": "2.3.9",
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"main": "main.js",
|
|
44
44
|
"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
|
|
8
|
+
* Executes a GAS script using the gas-fakes CLI logic.
|
|
41
9
|
*/
|
|
42
|
-
async function
|
|
10
|
+
async function runGasFakes(script) {
|
|
43
11
|
return new Promise((resolve) => {
|
|
44
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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,68 @@ 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
|
+
|
|
34
96
|
toString() {
|
|
35
97
|
return this.__fakeObjectType;
|
|
36
98
|
}
|
|
@@ -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
|
+
}
|