@mcpher/gas-fakes 2.3.13 → 2.3.15

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.
Files changed (54) hide show
  1. package/README.md +11 -5
  2. package/gf_agent/README.md +101 -0
  3. package/gf_agent/SKILL.md +396 -0
  4. package/gf_agent/documentation.md +105 -0
  5. package/gf_agent/gf-agent-contributor/SKILL.md +56 -0
  6. package/gf_agent/index.md +21 -0
  7. package/gf_agent/knowledge/00-execution-context.md +4 -0
  8. package/gf_agent/knowledge/01-drive.md +12 -0
  9. package/gf_agent/knowledge/02-syntax.md +13 -0
  10. package/gf_agent/knowledge/03-auth.md +15 -0
  11. package/gf_agent/knowledge/04-advanced.md +24 -0
  12. package/gf_agent/knowledge/05-sheets-forms.md +25 -0
  13. package/gf_agent/knowledge/06-jdbc-cloudsql.md +21 -0
  14. package/gf_agent/knowledge/07-jdbc-auth-details.md +30 -0
  15. package/gf_agent/knowledge/08-docs-limitations.md +4 -0
  16. package/gf_agent/knowledge/09-orchestrator-pattern.md +54 -0
  17. package/gf_agent/knowledge/10-sandbox-security.md +61 -0
  18. package/gf_agent/knowledge/11-chart-builder-limitations.md +15 -0
  19. package/gf_agent/knowledge/12-gmail-eventual-consistency.md +13 -0
  20. package/gf_agent/knowledge/README.md +16 -0
  21. package/gf_agent/scripts/SKILL.template.md +65 -0
  22. package/gf_agent/scripts/builder.js +78 -47
  23. package/gf_agent/skills/base.md +156 -0
  24. package/gf_agent/skills/cache.md +20 -0
  25. package/gf_agent/skills/calendar.md +780 -0
  26. package/gf_agent/skills/charts.md +127 -0
  27. package/gf_agent/skills/document.md +6626 -0
  28. package/gf_agent/skills/drive.md +423 -0
  29. package/gf_agent/skills/forms.md +4036 -0
  30. package/gf_agent/skills/gmail.md +576 -0
  31. package/gf_agent/skills/jdbc.md +3101 -0
  32. package/gf_agent/skills/lock.md +20 -0
  33. package/gf_agent/skills/properties.md +19 -0
  34. package/gf_agent/skills/script.md +50 -0
  35. package/gf_agent/skills/slides.md +5054 -0
  36. package/gf_agent/skills/spreadsheet.md +56075 -0
  37. package/gf_agent/skills/urlfetch.md +28 -0
  38. package/gf_agent/skills/utilities.md +33 -0
  39. package/gf_agent/skills/xml.md +270 -0
  40. package/package.json +1 -1
  41. package/src/cli/mcp.js +82 -67
  42. package/src/cli/setup.js +87 -9
  43. package/src/services/advgmail/fakeadvgmailmessages.js +85 -3
  44. package/src/services/driveapp/fakedrivemeta.js +1 -1
  45. package/src/services/gmailapp/fakegmailapp.js +217 -1
  46. package/src/services/gmailapp/fakegmailattachment.js +5 -0
  47. package/src/services/gmailapp/fakegmaildraft.js +32 -4
  48. package/src/services/gmailapp/fakegmaillabel.js +45 -0
  49. package/src/services/gmailapp/fakegmailmessage.js +212 -9
  50. package/src/services/gmailapp/fakegmailthread.js +151 -1
  51. package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +113 -28
  52. package/src/support/sxgmail.js +22 -2
  53. package/docs_discovery.json +0 -4939
  54. package/drive_tools.js +0 -20
@@ -0,0 +1,28 @@
1
+ # Service: urlfetch
2
+
3
+ ## Class: HTTPResponse
4
+
5
+ Supported Methods:
6
+ - `getAllHeaders()`
7
+ - `getAs(String)`
8
+ - `getBlob()`
9
+ - `getContent()`
10
+ - `getContentText()`
11
+ - `getContentText(String)`
12
+ - `getHeaders()`
13
+ - `getResponseCode()`
14
+ - `fetch(String,Object)`
15
+ - `fetch(String)`
16
+ - `fetchAll(Object)`
17
+ - `getRequest(String,Object)`
18
+ - `getRequest(String)`
19
+
20
+ ## Class: UrlFetchApp
21
+
22
+ Supported Methods:
23
+ - `fetch(String,Object)`
24
+ - `fetch(String)`
25
+ - `fetchAll(Object)`
26
+ - `getRequest(String,Object)`
27
+ - `getRequest(String)`
28
+
@@ -0,0 +1,33 @@
1
+ # Service: utilities
2
+
3
+ ## Class: Utilities
4
+
5
+ Supported Methods:
6
+ - `base64Decode(String,Charset)`
7
+ - `base64Decode(String)`
8
+ - `base64DecodeWebSafe(String,Charset)`
9
+ - `base64DecodeWebSafe(String)`
10
+ - `base64Encode(Byte)`
11
+ - `base64Encode(String,Charset)`
12
+ - `base64Encode(String)`
13
+ - `base64EncodeWebSafe(Byte)`
14
+ - `base64EncodeWebSafe(String,Charset)`
15
+ - `base64EncodeWebSafe(String)`
16
+ - `computeHmacSha256Signature(Byte,Byte)`
17
+ - `computeHmacSha256Signature(String,String,Charset)`
18
+ - `computeHmacSha256Signature(String,String)`
19
+ - `getUuid()`
20
+ - `gzip(BlobSource,String)`
21
+ - `gzip(BlobSource)`
22
+ - `newBlob(Byte,String,String)`
23
+ - `newBlob(Byte,String)`
24
+ - `newBlob(Byte)`
25
+ - `newBlob(String,String,String)`
26
+ - `newBlob(String,String)`
27
+ - `newBlob(String)`
28
+ - `sleep(Integer)`
29
+ - `ungzip(BlobSource)`
30
+ - `unzip(BlobSource)`
31
+ - `zip(BlobSource,String)`
32
+ - `zip(BlobSource)`
33
+
@@ -0,0 +1,270 @@
1
+ # Service: xml
2
+
3
+ ## Class: Attribute
4
+
5
+ Supported Methods:
6
+ - `getName()`
7
+ - `getNamespace()`
8
+ - `getRootElement()`
9
+ - `getAttribute(String,Namespace)`
10
+ - `getAttribute(String)`
11
+ - `getChild(String,Namespace)`
12
+ - `getChild(String)`
13
+ - `getChildren()`
14
+ - `getChildren(String,Namespace)`
15
+ - `getChildren(String)`
16
+ - `getChildText(String,Namespace)`
17
+ - `getChildText(String)`
18
+ - `getName()`
19
+ - `getNamespace()`
20
+ - `getNamespace(String)`
21
+ - `getQualifiedName()`
22
+ - `getText()`
23
+ - `getValue()`
24
+ - `getName()`
25
+ - `format(Document)`
26
+ - `format(Element)`
27
+ - `getPrefix()`
28
+ - `getURI()`
29
+ - `getText()`
30
+ - `getValue()`
31
+ - `getNamespace(String,String)`
32
+ - `getNamespace(String)`
33
+ - `getPrettyFormat()`
34
+ - `getRawFormat()`
35
+ - `parse(String)`
36
+
37
+ ## Class: Cdata
38
+
39
+ Supported Methods:
40
+ - `getRootElement()`
41
+ - `getAttribute(String,Namespace)`
42
+ - `getAttribute(String)`
43
+ - `getChild(String,Namespace)`
44
+ - `getChild(String)`
45
+ - `getChildren()`
46
+ - `getChildren(String,Namespace)`
47
+ - `getChildren(String)`
48
+ - `getChildText(String,Namespace)`
49
+ - `getChildText(String)`
50
+ - `getName()`
51
+ - `getNamespace()`
52
+ - `getNamespace(String)`
53
+ - `getQualifiedName()`
54
+ - `getText()`
55
+ - `getValue()`
56
+ - `getName()`
57
+ - `format(Document)`
58
+ - `format(Element)`
59
+ - `getPrefix()`
60
+ - `getURI()`
61
+ - `getText()`
62
+ - `getValue()`
63
+ - `getNamespace(String,String)`
64
+ - `getNamespace(String)`
65
+ - `getPrettyFormat()`
66
+ - `getRawFormat()`
67
+ - `parse(String)`
68
+
69
+ ## Class: Comment
70
+
71
+ Supported Methods:
72
+ - `getRootElement()`
73
+ - `getAttribute(String,Namespace)`
74
+ - `getAttribute(String)`
75
+ - `getChild(String,Namespace)`
76
+ - `getChild(String)`
77
+ - `getChildren()`
78
+ - `getChildren(String,Namespace)`
79
+ - `getChildren(String)`
80
+ - `getChildText(String,Namespace)`
81
+ - `getChildText(String)`
82
+ - `getName()`
83
+ - `getNamespace()`
84
+ - `getNamespace(String)`
85
+ - `getQualifiedName()`
86
+ - `getText()`
87
+ - `getValue()`
88
+ - `getName()`
89
+ - `format(Document)`
90
+ - `format(Element)`
91
+ - `getPrefix()`
92
+ - `getURI()`
93
+ - `getText()`
94
+ - `getValue()`
95
+ - `getNamespace(String,String)`
96
+ - `getNamespace(String)`
97
+ - `getPrettyFormat()`
98
+ - `getRawFormat()`
99
+ - `parse(String)`
100
+
101
+ ## Class: DocType
102
+
103
+ Supported Methods:
104
+ - `getRootElement()`
105
+ - `getAttribute(String,Namespace)`
106
+ - `getAttribute(String)`
107
+ - `getChild(String,Namespace)`
108
+ - `getChild(String)`
109
+ - `getChildren()`
110
+ - `getChildren(String,Namespace)`
111
+ - `getChildren(String)`
112
+ - `getChildText(String,Namespace)`
113
+ - `getChildText(String)`
114
+ - `getName()`
115
+ - `getNamespace()`
116
+ - `getNamespace(String)`
117
+ - `getQualifiedName()`
118
+ - `getText()`
119
+ - `getValue()`
120
+ - `getName()`
121
+ - `format(Document)`
122
+ - `format(Element)`
123
+ - `getPrefix()`
124
+ - `getURI()`
125
+ - `getText()`
126
+ - `getValue()`
127
+ - `getNamespace(String,String)`
128
+ - `getNamespace(String)`
129
+ - `getPrettyFormat()`
130
+ - `getRawFormat()`
131
+ - `parse(String)`
132
+
133
+ ## Class: Document
134
+
135
+ Supported Methods:
136
+ - `getRootElement()`
137
+ - `getAttribute(String,Namespace)`
138
+ - `getAttribute(String)`
139
+ - `getChild(String,Namespace)`
140
+ - `getChild(String)`
141
+ - `getChildren()`
142
+ - `getChildren(String,Namespace)`
143
+ - `getChildren(String)`
144
+ - `getChildText(String,Namespace)`
145
+ - `getChildText(String)`
146
+ - `getName()`
147
+ - `getNamespace()`
148
+ - `getNamespace(String)`
149
+ - `getQualifiedName()`
150
+ - `getText()`
151
+ - `getValue()`
152
+ - `getName()`
153
+ - `format(Document)`
154
+ - `format(Element)`
155
+ - `getPrefix()`
156
+ - `getURI()`
157
+ - `getText()`
158
+ - `getValue()`
159
+ - `getNamespace(String,String)`
160
+ - `getNamespace(String)`
161
+ - `getPrettyFormat()`
162
+ - `getRawFormat()`
163
+ - `parse(String)`
164
+
165
+ ## Class: Element
166
+
167
+ Supported Methods:
168
+ - `getAttribute(String,Namespace)`
169
+ - `getAttribute(String)`
170
+ - `getChild(String,Namespace)`
171
+ - `getChild(String)`
172
+ - `getChildren()`
173
+ - `getChildren(String,Namespace)`
174
+ - `getChildren(String)`
175
+ - `getChildText(String,Namespace)`
176
+ - `getChildText(String)`
177
+ - `getName()`
178
+ - `getNamespace()`
179
+ - `getNamespace(String)`
180
+ - `getQualifiedName()`
181
+ - `getText()`
182
+ - `getValue()`
183
+ - `getName()`
184
+ - `format(Document)`
185
+ - `format(Element)`
186
+ - `getPrefix()`
187
+ - `getURI()`
188
+ - `getText()`
189
+ - `getValue()`
190
+ - `getNamespace(String,String)`
191
+ - `getNamespace(String)`
192
+ - `getPrettyFormat()`
193
+ - `getRawFormat()`
194
+ - `parse(String)`
195
+
196
+ ## Class: EntityRef
197
+
198
+ Supported Methods:
199
+ - `getName()`
200
+ - `format(Document)`
201
+ - `format(Element)`
202
+ - `getPrefix()`
203
+ - `getURI()`
204
+ - `getText()`
205
+ - `getValue()`
206
+ - `getNamespace(String,String)`
207
+ - `getNamespace(String)`
208
+ - `getPrettyFormat()`
209
+ - `getRawFormat()`
210
+ - `parse(String)`
211
+
212
+ ## Class: Format
213
+
214
+ Supported Methods:
215
+ - `format(Document)`
216
+ - `format(Element)`
217
+ - `getPrefix()`
218
+ - `getURI()`
219
+ - `getText()`
220
+ - `getValue()`
221
+ - `getNamespace(String,String)`
222
+ - `getNamespace(String)`
223
+ - `getPrettyFormat()`
224
+ - `getRawFormat()`
225
+ - `parse(String)`
226
+
227
+ ## Class: Namespace
228
+
229
+ Supported Methods:
230
+ - `getPrefix()`
231
+ - `getURI()`
232
+ - `getText()`
233
+ - `getValue()`
234
+ - `getNamespace(String,String)`
235
+ - `getNamespace(String)`
236
+ - `getPrettyFormat()`
237
+ - `getRawFormat()`
238
+ - `parse(String)`
239
+
240
+ ## Class: ProcessingInstruction
241
+
242
+ Supported Methods:
243
+ - `getText()`
244
+ - `getValue()`
245
+ - `getNamespace(String,String)`
246
+ - `getNamespace(String)`
247
+ - `getPrettyFormat()`
248
+ - `getRawFormat()`
249
+ - `parse(String)`
250
+
251
+ ## Class: Text
252
+
253
+ Supported Methods:
254
+ - `getText()`
255
+ - `getValue()`
256
+ - `getNamespace(String,String)`
257
+ - `getNamespace(String)`
258
+ - `getPrettyFormat()`
259
+ - `getRawFormat()`
260
+ - `parse(String)`
261
+
262
+ ## Class: XmlService
263
+
264
+ Supported Methods:
265
+ - `getNamespace(String,String)`
266
+ - `getNamespace(String)`
267
+ - `getPrettyFormat()`
268
+ - `getRawFormat()`
269
+ - `parse(String)`
270
+
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "name": "@mcpher/gas-fakes",
42
42
  "author": "bruce mcpherson",
43
- "version": "2.3.13",
43
+ "version": "2.3.15",
44
44
  "license": "MIT",
45
45
  "main": "main.js",
46
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
@@ -2,8 +2,15 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import { z } from "zod";
4
4
  import { spawn } from "child_process";
5
+ import fs from "fs";
6
+ import os from "os";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
5
9
  import { MCP_VERSION } from "./utils.js";
6
10
 
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
7
14
  /**
8
15
  * Executes a GAS script using the gas-fakes CLI logic.
9
16
  */
@@ -37,57 +44,11 @@ async function runGasFakes(script) {
37
44
  });
38
45
  }
39
46
 
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());"
90
- }
47
+ // Available services mapping for the documentation lookup
48
+ const AVAILABLE_SERVICES = [
49
+ "base", "cache", "calendar", "charts", "document", "drive",
50
+ "forms", "gmail", "jdbc", "lock", "properties", "script",
51
+ "slides", "spreadsheet", "urlfetch", "utilities", "xml"
91
52
  ];
92
53
 
93
54
  export async function startMcpServer() {
@@ -96,27 +57,81 @@ export async function startMcpServer() {
96
57
  version: MCP_VERSION,
97
58
  });
98
59
 
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
- },
60
+ // Documentation lookup tool
61
+ server.registerTool(
62
+ "lookup_docs",
63
+ {
64
+ description: "Lookup the available Google Apps Script classes and methods supported by gas-fakes for a specific service. You MUST use this tool to read the documentation before writing a script to ensure the methods you plan to use are actually implemented.",
65
+ inputSchema: {
66
+ service: z.enum(AVAILABLE_SERVICES).describe("The Google Apps Script service to look up (e.g., 'spreadsheet', 'drive', 'document')."),
108
67
  },
109
- async ({ script }) => {
110
- return await runGasFakes(script);
68
+ },
69
+ async ({ service }) => {
70
+ try {
71
+ const fileName = `${service.toLowerCase()}.md`;
72
+
73
+ // Define potential paths for the skills documentation
74
+ const potentialPaths = [
75
+ // 1. Local project installation (e.g. from gas-fakes init standalone)
76
+ path.resolve(process.cwd(), "gf_agent", "skills", fileName),
77
+ path.resolve(process.cwd(), "gf_agent_standalone", "gf_agent", "skills", fileName),
78
+
79
+ // 2. Global Gemini CLI skill installation
80
+ path.resolve(os.homedir(), ".gemini", "skills", "gf_agent", "skills", fileName),
81
+
82
+ // 3. Bundled inside the npm package
83
+ path.resolve(__dirname, "../../gf_agent/skills", fileName)
84
+ ];
85
+
86
+ let docsPath = null;
87
+ for (const p of potentialPaths) {
88
+ if (fs.existsSync(p)) {
89
+ docsPath = p;
90
+ break;
91
+ }
92
+ }
93
+
94
+ if (!docsPath) {
95
+ // If not found locally, try fetching from the GitHub repository
96
+ const githubRawUrl = `https://raw.githubusercontent.com/brucemcpherson/gas-fakes/main/gf_agent/skills/${fileName}`;
97
+ try {
98
+ const response = await fetch(githubRawUrl);
99
+ if (response.ok) {
100
+ const githubContent = await response.text();
101
+ return {
102
+ content: [{ type: "text", text: githubContent }],
103
+ isError: false,
104
+ };
105
+ }
106
+ } catch (fetchErr) {
107
+ // Silently fail the fetch and fall through to the local error message
108
+ }
109
+
110
+ return {
111
+ content: [{ type: "text", text: `Documentation not found for service: ${service}. Checked multiple standard locations and GitHub origin.` }],
112
+ isError: true,
113
+ };
114
+ }
115
+
116
+ const content = fs.readFileSync(docsPath, "utf-8");
117
+ return {
118
+ content: [{ type: "text", text: content }],
119
+ isError: false,
120
+ };
121
+ } catch (err) {
122
+ return {
123
+ content: [{ type: "text", text: `Failed to read documentation: ${err.message}` }],
124
+ isError: true,
125
+ };
111
126
  }
112
- );
113
- }
127
+ }
128
+ );
114
129
 
115
- // Also add a generic tool for tasks that span multiple services
130
+ // Consolidated execution tool
116
131
  server.registerTool(
117
- "workspace_agent",
132
+ "run_script",
118
133
  {
119
- description: "A general-purpose agent to automate tasks across multiple Google Workspace services (Sheets, Docs, Drive, etc.)",
134
+ description: "Executes Google Apps Script code locally using the gas-fakes emulator. You can interact with multiple Workspace services in a single script.",
120
135
  inputSchema: {
121
136
  script: z.string().describe("The Google Apps Script code to execute locally."),
122
137
  },
package/src/cli/setup.js CHANGED
@@ -626,20 +626,98 @@ export async function initializeConfiguration(options = {}) {
626
626
  try {
627
627
  // 1. Install or link the agent skill
628
628
  let skillCmd;
629
- const localSkillPath = path.resolve(process.cwd(), "gf_agent", "SKILL.md");
630
- const isLocalClone = fs.existsSync(localSkillPath);
629
+ const gfAgentSubdir = path.resolve(process.cwd(), "gf_agent", "SKILL.md");
630
+ const gfAgentCurrent = path.resolve(process.cwd(), "SKILL.md");
631
631
 
632
- if (isLocalClone) {
632
+ if (fs.existsSync(gfAgentSubdir)) {
633
633
  console.log("Detected local gas-fakes repository. Linking local skill for development...");
634
- skillCmd = "gemini skills link ./gf_agent";
634
+ skillCmd = "gemini skills link ./gf_agent --consent";
635
635
  manualSkillCmd = "1. gemini skills link ./gf_agent";
636
+ execSync(skillCmd, { stdio: ["ignore", "pipe", "ignore"] });
637
+ console.log("Skill linked successfully.");
638
+ } else if (fs.existsSync(gfAgentCurrent) && fs.existsSync(path.resolve(process.cwd(), "index.md"))) {
639
+ console.log("Detected local gf_agent directory. Linking local skill for development...");
640
+ skillCmd = "gemini skills link . --consent";
641
+ manualSkillCmd = "1. gemini skills link .";
642
+ execSync(skillCmd, { stdio: ["ignore", "pipe", "ignore"] });
643
+ console.log("Skill linked successfully.");
636
644
  } else {
637
- skillCmd = "gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
638
- manualSkillCmd = "1. gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
639
- }
645
+ // Not a local clone, check if already installed to avoid overwriting
646
+ let isAlreadyInstalled = false;
647
+ try {
648
+ const existingSkills = execSync("gemini skills list", {
649
+ encoding: "utf8",
650
+ stdio: ["ignore", "pipe", "ignore"],
651
+ });
652
+ if (existingSkills.includes("gf_agent")) {
653
+ isAlreadyInstalled = true;
654
+ }
655
+ } catch (err) {
656
+ // Ignore errors checking skills list
657
+ }
658
+
659
+ if (isAlreadyInstalled) {
660
+ console.log("gf_agent skill is already installed. Skipping remote installation.");
661
+ manualSkillCmd = "1. gemini skills update gf_agent (if needed)";
662
+ } else {
663
+ const installChoice = await prompts({
664
+ type: "select",
665
+ name: "method",
666
+ message: "How would you like to install the gf_agent skill?",
667
+ choices: [
668
+ {
669
+ title: "Global (Standard)",
670
+ value: "global",
671
+ description: "Recommended for most users. Installs a read-only copy globally.",
672
+ },
673
+ {
674
+ title: "Local Standalone (Contributor)",
675
+ value: "local",
676
+ description: "Installs a local sparse-clone for skill development and linking.",
677
+ },
678
+ ],
679
+ initial: 0,
680
+ });
681
+
682
+ if (installChoice.method === "local") {
683
+ const standaloneDir = "gf_agent_standalone";
684
+ const fullStandalonePath = path.resolve(process.cwd(), standaloneDir);
685
+ console.log(`Setting up local standalone skill environment in "./${standaloneDir}"...`);
686
+
687
+ try {
688
+ if (!fs.existsSync(fullStandalonePath)) {
689
+ fs.mkdirSync(fullStandalonePath, { recursive: true });
690
+ execSync("git init", { cwd: fullStandalonePath, stdio: "ignore" });
691
+ execSync("git remote add origin https://github.com/brucemcpherson/gas-fakes.git", {
692
+ cwd: fullStandalonePath,
693
+ stdio: "ignore",
694
+ });
695
+ execSync("git config core.sparseCheckout true", { cwd: fullStandalonePath, stdio: "ignore" });
696
+
697
+ const sparsePath = path.join(fullStandalonePath, ".git", "info", "sparse-checkout");
698
+ fs.writeFileSync(sparsePath, "gf_agent/*\n");
699
+ }
700
+
701
+ execSync("git pull origin main", { cwd: fullStandalonePath, stdio: "ignore" });
640
702
 
641
- console.log(`Executing: ${skillCmd}`);
642
- execSync(skillCmd, { stdio: "inherit" });
703
+ skillCmd = "gemini skills link ./gf_agent --consent";
704
+ execSync(skillCmd, { cwd: fullStandalonePath, stdio: ["ignore", "pipe", "ignore"] });
705
+ console.log("Skill linked successfully.");
706
+
707
+ manualSkillCmd = `1. cd ${standaloneDir} && gemini skills link ./gf_agent`;
708
+ } catch (gitErr) {
709
+ console.error(`Error during local setup: ${gitErr.message}`);
710
+ throw gitErr;
711
+ }
712
+ } else {
713
+ skillCmd = "gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent --consent";
714
+ manualSkillCmd = "1. gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
715
+ console.log(`Installing global skill from remote...`);
716
+ execSync(skillCmd, { stdio: ["ignore", "pipe", "ignore"] });
717
+ console.log("Skill installed successfully.");
718
+ }
719
+ }
720
+ }
643
721
 
644
722
  // 2. Add the MCP server
645
723
  const mcpCmd = "gemini mcp add --scope project gas-fakes-mcp gas-fakes mcp";