@mcpher/gas-fakes 2.3.16 → 2.3.17
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/gf_agent/SKILL.md +74 -11
- package/gf_agent/knowledge/00-execution-context.md +1 -0
- package/gf_agent/knowledge/02-syntax.md +3 -2
- package/gf_agent/knowledge/04-advanced.md +25 -3
- package/gf_agent/knowledge/05-sheets-forms.md +1 -0
- package/gf_agent/knowledge/09-orchestrator-pattern.md +5 -4
- package/gf_agent/knowledge/13-advanced-services-discovery.md +23 -0
- package/gf_agent/knowledge/14-utilities-parity.md +13 -0
- package/gf_agent/scripts/SKILL.template.md +0 -2
- package/gf_agent/skills/utilities.md +17 -0
- package/package.json +1 -1
- package/src/services/enums/utilitiesenums.js +26 -0
- package/src/services/utilities/fakeutilities.js +331 -15
package/gf_agent/SKILL.md
CHANGED
|
@@ -25,7 +25,6 @@ description: >
|
|
|
25
25
|
- **Remote URL**: `https://raw.githubusercontent.com/brucemcpherson/gas-fakes/main/progress/{service}.md` (Note: `{service}` is lowercase, e.g., `spreadsheet.md`).
|
|
26
26
|
- Use these details to construct precise, parity-compliant code without relying on external search engines unless documentation is missing.
|
|
27
27
|
3. **Generate Script**: Create a Node.js script that:
|
|
28
|
-
- Imports `@mcpher/gas-fakes`.
|
|
29
28
|
- Uses standard GAS syntax.
|
|
30
29
|
- (Optional) Uses `ScriptApp.isFake` for local-only logic like logging or cleanup.
|
|
31
30
|
4. **Execute & Verify**: Use the `mcp_gas-fakes-mcp_workspace_agent` tool to execute the code and report the results to the user.
|
|
@@ -39,7 +38,6 @@ Agent:
|
|
|
39
38
|
- (Optional) Fetch `progress/gmail.md` and `progress/document.md` from GitHub for detailed signatures.
|
|
40
39
|
3. **Generate Script**:
|
|
41
40
|
```javascript
|
|
42
|
-
import '@mcpher/gas-fakes';
|
|
43
41
|
const threads = GmailApp.getInboxThreads(0, 5);
|
|
44
42
|
let summary = 'Email Summary:\n\n';
|
|
45
43
|
threads.forEach(t => {
|
|
@@ -67,6 +65,7 @@ Agent:
|
|
|
67
65
|
### Execution Context & Artifacts (CRITICAL)
|
|
68
66
|
- **Role Boundary**: You are the `gf_agent` operating on behalf of an end-user to automate Google Workspace tasks. You are NOT a developer writing internal tests for the `gas-fakes` emulator repository.
|
|
69
67
|
- **Transient Execution**: When fulfilling automation requests (e.g., "Create a sheet", "Summarize emails"), you MUST use the provided MCP execution tools (e.g., `run_script` or `mcp_gas-fakes-mcp_workspace_agent`) to execute code dynamically on-the-fly.
|
|
68
|
+
- **No Import Required**: Do NOT include `import '@mcpher/gas-fakes';` at the top of your scripts when using the MCP execution tools. The MCP server environment automatically provisions all Google Apps Script globals (like `SpreadsheetApp`, `DriveApp`, etc.) into the execution context for you.
|
|
70
69
|
- **No Permanent Artifacts**: DO NOT write script files to disk (e.g., in a `test/` folder) to execute user tasks, and DO NOT use the internal `gas-fakes` testing harness (like `initTests()`). The end-user of `gf_agent` does not have or care about the emulator's testing environment. Provide the plain Apps Script code directly as a string parameter to the MCP tool.
|
|
71
70
|
|
|
72
71
|
### Efficient Drive Searching (Best Practice)
|
|
@@ -84,8 +83,9 @@ Agent:
|
|
|
84
83
|
|
|
85
84
|
|
|
86
85
|
### Common Apps Script Syntax Gotchas (First-Time Accuracy)
|
|
87
|
-
- **File Conversion (Exporting to PDF)**:
|
|
88
|
-
-
|
|
86
|
+
- **File Conversion (Exporting to PDF)**:
|
|
87
|
+
- **`DriveApp.File.getAs()` Workaround**: While live Apps Script can seamlessly convert text files (`text/plain`) to PDF using `file.getAs('application/pdf')`, the underlying Google Drive API **only supports exporting Docs Editor files** (Docs, Sheets, Slides). `gas-fakes` handles this transparently by automatically performing a temporary two-step conversion (copying it to a Google Doc, exporting it, and trashing the temp file). This ensures parity with live Apps Script without manual intervention.
|
|
88
|
+
- **`Spreadsheet.getAs()` Limitation**: The `getAs()` method is **NOT** implemented directly on `Spreadsheet`, `Document`, or `Presentation` objects in `gas-fakes`. If you try to call `ss.getAs('application/pdf')`, the script will crash. **Crucial Rule**: You MUST fetch the file via DriveApp first to convert it: `DriveApp.getFileById(ss.getId()).getAs('application/pdf')`.
|
|
89
89
|
- **Google Docs Formatting**: You CANNOT apply formatting (bold, italic, etc.) directly to a `Paragraph` or `ListItem`. You MUST use `editAsText()` first.
|
|
90
90
|
- *Incorrect*: `paragraph.setItalic(true)`
|
|
91
91
|
- *Correct*: `paragraph.editAsText().setItalic(true)`
|
|
@@ -126,14 +126,36 @@ Agent:
|
|
|
126
126
|
- **Iterators**: `gas-fakes` iterators (like `FileIterator`) implement the native Apps Script `hasNext()` and `next()` methods, which differ from standard JavaScript iterators.
|
|
127
127
|
|
|
128
128
|
### Google Docs & Images
|
|
129
|
-
- **Inline Image Resizing**: Native methods like `setWidth()
|
|
129
|
+
- **Inline Image Resizing (DocumentApp)**: Native methods like `setWidth()` and `setHeight()` on `InlineImage` objects are **NOT** implemented in `gas-fakes`. If you call these, the script will crash with a "not yet implemented" error.
|
|
130
130
|
- **Conversion**: To create a Google Doc from HTML, use `Drive.Files.create()` with the correct v3 parameters:
|
|
131
131
|
```javascript
|
|
132
132
|
const resource = { name: "Doc Name", mimeType: "application/vnd.google-apps.document" };
|
|
133
133
|
Drive.Files.create(resource, htmlBlob);
|
|
134
134
|
```
|
|
135
|
-
- **Resizing Workaround**:
|
|
136
|
-
-
|
|
135
|
+
- **Resizing Workaround (Advanced Docs Service)**: Because the Docs API does not support updating image properties directly, you must use the Advanced Docs Service (`Docs.Documents.batchUpdate`) to **delete and re-insert** the image with the new dimensions.
|
|
136
|
+
- To do this, fetch the document via `Docs.Documents.get()`, extract the `contentUri` from the existing inline image, and construct `deleteObject` and `insertInlineImage` requests.
|
|
137
|
+
- **Crucial**: Always sort your delete/insert operations by `startIndex` in **descending order** so you don't corrupt the document's indices during a batch update.
|
|
138
|
+
- **Example Code Pattern**:
|
|
139
|
+
```javascript
|
|
140
|
+
const docData = Docs.Documents.get(docId);
|
|
141
|
+
const requests = [];
|
|
142
|
+
// ... logic to find images in docData.body.content, saving objectId, contentUri, startIndex, and dimensions ...
|
|
143
|
+
// ... sort found images by startIndex DESCENDING ...
|
|
144
|
+
images.forEach(img => {
|
|
145
|
+
requests.push({ deleteObject: { objectId: img.objectId } });
|
|
146
|
+
requests.push({
|
|
147
|
+
insertInlineImage: {
|
|
148
|
+
uri: img.contentUri,
|
|
149
|
+
location: { index: img.startIndex },
|
|
150
|
+
objectSize: {
|
|
151
|
+
width: { magnitude: img.width * 0.25, unit: 'PT' },
|
|
152
|
+
height: { magnitude: img.height * 0.25, unit: 'PT' }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
if (requests.length > 0) Docs.Documents.batchUpdate({ requests }, docId);
|
|
158
|
+
```
|
|
137
159
|
- **Shadow Document & Named Ranges**: `gas-fakes` uses a "Shadow Document" approach. Elements are tracked using Named Range tags to maintain positional integrity during updates.
|
|
138
160
|
- **Table Creation**: `appendTable()` without arguments creates a 1x1 table in `gas-fakes`, whereas live Apps Script creates an empty table stub.
|
|
139
161
|
- **Rate Limiting (429 Errors)**: Because `gas-fakes` translates local calls into real-time API requests, making rapid, successive calls like `appendParagraph()` in a loop will trigger Google's rate limit.
|
|
@@ -149,6 +171,7 @@ Agent:
|
|
|
149
171
|
- `getDisplayValues()` returns formatted strings.
|
|
150
172
|
- `setValues()` uses "USER_ENTERED" mode.
|
|
151
173
|
- **Bulk Operations (RangeList)**: Use `sheet.getRangeList(['A1', 'C1', 'E1'])` for multi-range formatting.
|
|
174
|
+
- **Exporting to PDF (`getAs`)**: The `Spreadsheet.getAs()` method is **NOT** implemented in `gas-fakes`. If you need to convert a Spreadsheet (or a Document/Presentation) to a PDF, you MUST use the `DriveApp` service workaround: `DriveApp.getFileById(spreadsheet.getId()).getAs('application/pdf')`.
|
|
152
175
|
|
|
153
176
|
### Google Forms (FormApp)
|
|
154
177
|
- **Programmatic Submission**: The public Forms API does **not** support submitting responses. `gas-fakes` uses a "web submission hack" that temporarily makes the form public to scrape tokens and POST the response.
|
|
@@ -273,12 +296,13 @@ Once all required service-specific knowledge is gathered:
|
|
|
273
296
|
- **Parity Guarantee**: The remote documentation is generated directly from the `gas-fakes` source code, ensuring 100% parity with the local environment.
|
|
274
297
|
|
|
275
298
|
## Delegation Anti-Patterns (CRITICAL)
|
|
276
|
-
- **NEVER Delegate Execution**: The main orchestrator agent MUST retain control of script generation and the execution tool (
|
|
277
|
-
- **
|
|
278
|
-
- **
|
|
299
|
+
- **NEVER Delegate Execution**: The main orchestrator agent MUST retain control of script generation and the execution tool (`mcp_gas-fakes-mcp_workspace_agent`).
|
|
300
|
+
- **Unauthorized Tool Call Error**: Sub-agents (like `generalist`) DO NOT have access to MCP tools. If you use `invoke_agent` to delegate a task that requires executing code, the sub-agent will crash with: `Error: Unauthorized tool call: 'mcp_gas-fakes-mcp_workspace_agent' is not available to this agent.`
|
|
301
|
+
- **Subagent Context Loss**: Subagents do **not** inherit the `gf_agent` knowledge base, parity rules, or active skills. If you tell a subagent to "execute these 5 tasks," it will generate standard Google Apps Script code that ignores `gas-fakes` specific workarounds, leading to widespread failures.
|
|
302
|
+
- **Strict Role Boundary**: Subagents are strictly for **isolated documentation retrieval** (the Service Agent Phase) via standard shell commands like `curl`. The main agent writes and runs the code.
|
|
279
303
|
|
|
280
304
|
## Safe Parallelism (Performance)
|
|
281
|
-
|
|
305
|
+
If the user asks you to run multiple tasks in parallel, DO NOT use `invoke_agent`. You must handle them directly in the main session. The orchestrator should achieve parallelism in two ways:
|
|
282
306
|
1. **Tool Call Parallelism**: The Main Agent generates multiple, parity-compliant scripts and issues them as independent, simultaneous calls to `mcp_gas-fakes-mcp_workspace_agent` within a single turn.
|
|
283
307
|
2. **Execution-Level Async**: Since `gas-fakes` runs on Node.js, the Main Agent can generate a single script that uses `Promise.all()` to execute multiple non-dependent operations (like `UrlFetchApp` calls or creating separate files) simultaneously.
|
|
284
308
|
3. **Sequence when Dependent**: Only use `wait_for_previous: true` or sequential turns when a task depends on the side-effect of a previous one (e.g., reading a sheet that was just created).
|
|
@@ -378,6 +402,45 @@ When writing scripts that modify Gmail objects (e.g., `GmailMessage.markRead()`,
|
|
|
378
402
|
```
|
|
379
403
|
- **gas-fakes Execution**: When executing transient scripts locally via `gas-fakes` that don't need immediate assertions, this pattern is not strictly necessary as `gas-fakes` handles the REST API synchronization reliably, but it is best practice for cross-platform parity.
|
|
380
404
|
|
|
405
|
+
### Researching Advanced Services (Google API Discovery)
|
|
406
|
+
|
|
407
|
+
Unlike the standard Apps Script Services (`SpreadsheetApp`, `DriveApp`), the signatures and payloads for **Advanced Services** (`Docs`, `Sheets`, `Drive`, etc.) are not fully documented in the `progress/` directory of the `gas-fakes` repository. Advanced Services are 1:1 mappings of the underlying Google REST APIs.
|
|
408
|
+
|
|
409
|
+
If you are orchestrating a complex task that requires an Advanced Service (such as resizing an image via `Docs.Documents.batchUpdate` or applying granular formatting via `Sheets.Spreadsheets.batchUpdate`) and you do not know the exact JSON payload structure, you MUST research it using the Google API Discovery documents.
|
|
410
|
+
|
|
411
|
+
**How to Research Advanced Services:**
|
|
412
|
+
Do not guess the payload structure. Instead, use the `run_shell_command` tool to `curl` and `grep` the official Discovery Document for the specific API version.
|
|
413
|
+
|
|
414
|
+
**Discovery Document URLs:**
|
|
415
|
+
- **Docs API v1**: `https://docs.googleapis.com/$discovery/rest?version=v1`
|
|
416
|
+
- **Sheets API v4**: `https://sheets.googleapis.com/$discovery/rest?version=v4`
|
|
417
|
+
- **Drive API v3**: `https://drive.googleapis.com/$discovery/rest?version=v3`
|
|
418
|
+
- **Slides API v1**: `https://slides.googleapis.com/$discovery/rest?version=v1`
|
|
419
|
+
- **Gmail API v1**: `https://gmail.googleapis.com/$discovery/rest?version=v1`
|
|
420
|
+
|
|
421
|
+
**Example Research Command:**
|
|
422
|
+
If you need to know how to structure an `insertInlineImage` request for the Docs API, you would run:
|
|
423
|
+
```bash
|
|
424
|
+
curl -s "https://docs.googleapis.com/$discovery/rest?version=v1" | grep -A 30 '"InsertInlineImageRequest":'
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
By fetching the exact schema from the discovery document, you ensure your `batchUpdate` arrays and payload objects are 100% accurate before generating the execution script.
|
|
428
|
+
|
|
429
|
+
### Utilities Service Constraints (API Parity)
|
|
430
|
+
|
|
431
|
+
When generating code that uses the `Utilities` service, you must adhere to the following restrictions to ensure parity with Live Apps Script:
|
|
432
|
+
|
|
433
|
+
1. **`formatString` Format Specifiers**:
|
|
434
|
+
- Live Apps Script uses Java's underlying `String.format` implementation. Therefore, it **does not** support Node.js-specific format specifiers like `%j` for JSON.
|
|
435
|
+
- You MUST stick to standard Java/C-style format specifiers: `%s` (string), `%d` / `%i` (integer), and `%f` (float).
|
|
436
|
+
- *Incorrect*: `Utilities.formatString("Data: %j", obj)`
|
|
437
|
+
- *Correct*: `Utilities.formatString("Data: %s", JSON.stringify(obj))`
|
|
438
|
+
|
|
439
|
+
2. **`parseDate` Error Handling**:
|
|
440
|
+
- If `Utilities.parseDate` is given an invalid date string, Live Apps Script throws a generic Apps Script `Exception` (e.g., `{"name":"Exception"}`) rather than a standard JavaScript `Error` object.
|
|
441
|
+
- If you are writing tests or robust `try/catch` blocks intended to run cross-platform, DO NOT assert against the exact string value of the error message (like `e.message.includes("failed")`). Simply check that an exception was thrown.
|
|
442
|
+
|
|
443
|
+
|
|
381
444
|
# gf_agent Knowledge Base
|
|
382
445
|
|
|
383
446
|
This directory contains modular markdown files representing the "Lessons Learned & Best Practices" for the `gf_agent` skill.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
### Execution Context & Artifacts (CRITICAL)
|
|
2
2
|
- **Role Boundary**: You are the `gf_agent` operating on behalf of an end-user to automate Google Workspace tasks. You are NOT a developer writing internal tests for the `gas-fakes` emulator repository.
|
|
3
3
|
- **Transient Execution**: When fulfilling automation requests (e.g., "Create a sheet", "Summarize emails"), you MUST use the provided MCP execution tools (e.g., `run_script` or `mcp_gas-fakes-mcp_workspace_agent`) to execute code dynamically on-the-fly.
|
|
4
|
+
- **No Import Required**: Do NOT include `import '@mcpher/gas-fakes';` at the top of your scripts when using the MCP execution tools. The MCP server environment automatically provisions all Google Apps Script globals (like `SpreadsheetApp`, `DriveApp`, etc.) into the execution context for you.
|
|
4
5
|
- **No Permanent Artifacts**: DO NOT write script files to disk (e.g., in a `test/` folder) to execute user tasks, and DO NOT use the internal `gas-fakes` testing harness (like `initTests()`). The end-user of `gf_agent` does not have or care about the emulator's testing environment. Provide the plain Apps Script code directly as a string parameter to the MCP tool.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
### Common Apps Script Syntax Gotchas (First-Time Accuracy)
|
|
2
|
-
- **File Conversion (Exporting to PDF)**:
|
|
3
|
-
-
|
|
2
|
+
- **File Conversion (Exporting to PDF)**:
|
|
3
|
+
- **`DriveApp.File.getAs()` Workaround**: While live Apps Script can seamlessly convert text files (`text/plain`) to PDF using `file.getAs('application/pdf')`, the underlying Google Drive API **only supports exporting Docs Editor files** (Docs, Sheets, Slides). `gas-fakes` handles this transparently by automatically performing a temporary two-step conversion (copying it to a Google Doc, exporting it, and trashing the temp file). This ensures parity with live Apps Script without manual intervention.
|
|
4
|
+
- **`Spreadsheet.getAs()` Limitation**: The `getAs()` method is **NOT** implemented directly on `Spreadsheet`, `Document`, or `Presentation` objects in `gas-fakes`. If you try to call `ss.getAs('application/pdf')`, the script will crash. **Crucial Rule**: You MUST fetch the file via DriveApp first to convert it: `DriveApp.getFileById(ss.getId()).getAs('application/pdf')`.
|
|
4
5
|
- **Google Docs Formatting**: You CANNOT apply formatting (bold, italic, etc.) directly to a `Paragraph` or `ListItem`. You MUST use `editAsText()` first.
|
|
5
6
|
- *Incorrect*: `paragraph.setItalic(true)`
|
|
6
7
|
- *Correct*: `paragraph.editAsText().setItalic(true)`
|
|
@@ -9,14 +9,36 @@
|
|
|
9
9
|
- **Iterators**: `gas-fakes` iterators (like `FileIterator`) implement the native Apps Script `hasNext()` and `next()` methods, which differ from standard JavaScript iterators.
|
|
10
10
|
|
|
11
11
|
### Google Docs & Images
|
|
12
|
-
- **Inline Image Resizing**: Native methods like `setWidth()
|
|
12
|
+
- **Inline Image Resizing (DocumentApp)**: Native methods like `setWidth()` and `setHeight()` on `InlineImage` objects are **NOT** implemented in `gas-fakes`. If you call these, the script will crash with a "not yet implemented" error.
|
|
13
13
|
- **Conversion**: To create a Google Doc from HTML, use `Drive.Files.create()` with the correct v3 parameters:
|
|
14
14
|
```javascript
|
|
15
15
|
const resource = { name: "Doc Name", mimeType: "application/vnd.google-apps.document" };
|
|
16
16
|
Drive.Files.create(resource, htmlBlob);
|
|
17
17
|
```
|
|
18
|
-
- **Resizing Workaround**:
|
|
19
|
-
-
|
|
18
|
+
- **Resizing Workaround (Advanced Docs Service)**: Because the Docs API does not support updating image properties directly, you must use the Advanced Docs Service (`Docs.Documents.batchUpdate`) to **delete and re-insert** the image with the new dimensions.
|
|
19
|
+
- To do this, fetch the document via `Docs.Documents.get()`, extract the `contentUri` from the existing inline image, and construct `deleteObject` and `insertInlineImage` requests.
|
|
20
|
+
- **Crucial**: Always sort your delete/insert operations by `startIndex` in **descending order** so you don't corrupt the document's indices during a batch update.
|
|
21
|
+
- **Example Code Pattern**:
|
|
22
|
+
```javascript
|
|
23
|
+
const docData = Docs.Documents.get(docId);
|
|
24
|
+
const requests = [];
|
|
25
|
+
// ... logic to find images in docData.body.content, saving objectId, contentUri, startIndex, and dimensions ...
|
|
26
|
+
// ... sort found images by startIndex DESCENDING ...
|
|
27
|
+
images.forEach(img => {
|
|
28
|
+
requests.push({ deleteObject: { objectId: img.objectId } });
|
|
29
|
+
requests.push({
|
|
30
|
+
insertInlineImage: {
|
|
31
|
+
uri: img.contentUri,
|
|
32
|
+
location: { index: img.startIndex },
|
|
33
|
+
objectSize: {
|
|
34
|
+
width: { magnitude: img.width * 0.25, unit: 'PT' },
|
|
35
|
+
height: { magnitude: img.height * 0.25, unit: 'PT' }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
if (requests.length > 0) Docs.Documents.batchUpdate({ requests }, docId);
|
|
41
|
+
```
|
|
20
42
|
- **Shadow Document & Named Ranges**: `gas-fakes` uses a "Shadow Document" approach. Elements are tracked using Named Range tags to maintain positional integrity during updates.
|
|
21
43
|
- **Table Creation**: `appendTable()` without arguments creates a 1x1 table in `gas-fakes`, whereas live Apps Script creates an empty table stub.
|
|
22
44
|
- **Rate Limiting (429 Errors)**: Because `gas-fakes` translates local calls into real-time API requests, making rapid, successive calls like `appendParagraph()` in a loop will trigger Google's rate limit.
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
- `getDisplayValues()` returns formatted strings.
|
|
7
7
|
- `setValues()` uses "USER_ENTERED" mode.
|
|
8
8
|
- **Bulk Operations (RangeList)**: Use `sheet.getRangeList(['A1', 'C1', 'E1'])` for multi-range formatting.
|
|
9
|
+
- **Exporting to PDF (`getAs`)**: The `Spreadsheet.getAs()` method is **NOT** implemented in `gas-fakes`. If you need to convert a Spreadsheet (or a Document/Presentation) to a PDF, you MUST use the `DriveApp` service workaround: `DriveApp.getFileById(spreadsheet.getId()).getAs('application/pdf')`.
|
|
9
10
|
|
|
10
11
|
### Google Forms (FormApp)
|
|
11
12
|
- **Programmatic Submission**: The public Forms API does **not** support submitting responses. `gas-fakes` uses a "web submission hack" that temporarily makes the form public to scrape tokens and POST the response.
|
|
@@ -43,12 +43,13 @@ Once all required service-specific knowledge is gathered:
|
|
|
43
43
|
- **Parity Guarantee**: The remote documentation is generated directly from the `gas-fakes` source code, ensuring 100% parity with the local environment.
|
|
44
44
|
|
|
45
45
|
## Delegation Anti-Patterns (CRITICAL)
|
|
46
|
-
- **NEVER Delegate Execution**: The main orchestrator agent MUST retain control of script generation and the execution tool (
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
46
|
+
- **NEVER Delegate Execution**: The main orchestrator agent MUST retain control of script generation and the execution tool (`mcp_gas-fakes-mcp_workspace_agent`).
|
|
47
|
+
- **Unauthorized Tool Call Error**: Sub-agents (like `generalist`) DO NOT have access to MCP tools. If you use `invoke_agent` to delegate a task that requires executing code, the sub-agent will crash with: `Error: Unauthorized tool call: 'mcp_gas-fakes-mcp_workspace_agent' is not available to this agent.`
|
|
48
|
+
- **Subagent Context Loss**: Subagents do **not** inherit the `gf_agent` knowledge base, parity rules, or active skills. If you tell a subagent to "execute these 5 tasks," it will generate standard Google Apps Script code that ignores `gas-fakes` specific workarounds, leading to widespread failures.
|
|
49
|
+
- **Strict Role Boundary**: Subagents are strictly for **isolated documentation retrieval** (the Service Agent Phase) via standard shell commands like `curl`. The main agent writes and runs the code.
|
|
49
50
|
|
|
50
51
|
## Safe Parallelism (Performance)
|
|
51
|
-
|
|
52
|
+
If the user asks you to run multiple tasks in parallel, DO NOT use `invoke_agent`. You must handle them directly in the main session. The orchestrator should achieve parallelism in two ways:
|
|
52
53
|
1. **Tool Call Parallelism**: The Main Agent generates multiple, parity-compliant scripts and issues them as independent, simultaneous calls to `mcp_gas-fakes-mcp_workspace_agent` within a single turn.
|
|
53
54
|
2. **Execution-Level Async**: Since `gas-fakes` runs on Node.js, the Main Agent can generate a single script that uses `Promise.all()` to execute multiple non-dependent operations (like `UrlFetchApp` calls or creating separate files) simultaneously.
|
|
54
55
|
3. **Sequence when Dependent**: Only use `wait_for_previous: true` or sequential turns when a task depends on the side-effect of a previous one (e.g., reading a sheet that was just created).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
### Researching Advanced Services (Google API Discovery)
|
|
2
|
+
|
|
3
|
+
Unlike the standard Apps Script Services (`SpreadsheetApp`, `DriveApp`), the signatures and payloads for **Advanced Services** (`Docs`, `Sheets`, `Drive`, etc.) are not fully documented in the `progress/` directory of the `gas-fakes` repository. Advanced Services are 1:1 mappings of the underlying Google REST APIs.
|
|
4
|
+
|
|
5
|
+
If you are orchestrating a complex task that requires an Advanced Service (such as resizing an image via `Docs.Documents.batchUpdate` or applying granular formatting via `Sheets.Spreadsheets.batchUpdate`) and you do not know the exact JSON payload structure, you MUST research it using the Google API Discovery documents.
|
|
6
|
+
|
|
7
|
+
**How to Research Advanced Services:**
|
|
8
|
+
Do not guess the payload structure. Instead, use the `run_shell_command` tool to `curl` and `grep` the official Discovery Document for the specific API version.
|
|
9
|
+
|
|
10
|
+
**Discovery Document URLs:**
|
|
11
|
+
- **Docs API v1**: `https://docs.googleapis.com/$discovery/rest?version=v1`
|
|
12
|
+
- **Sheets API v4**: `https://sheets.googleapis.com/$discovery/rest?version=v4`
|
|
13
|
+
- **Drive API v3**: `https://drive.googleapis.com/$discovery/rest?version=v3`
|
|
14
|
+
- **Slides API v1**: `https://slides.googleapis.com/$discovery/rest?version=v1`
|
|
15
|
+
- **Gmail API v1**: `https://gmail.googleapis.com/$discovery/rest?version=v1`
|
|
16
|
+
|
|
17
|
+
**Example Research Command:**
|
|
18
|
+
If you need to know how to structure an `insertInlineImage` request for the Docs API, you would run:
|
|
19
|
+
```bash
|
|
20
|
+
curl -s "https://docs.googleapis.com/$discovery/rest?version=v1" | grep -A 30 '"InsertInlineImageRequest":'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
By fetching the exact schema from the discovery document, you ensure your `batchUpdate` arrays and payload objects are 100% accurate before generating the execution script.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
### Utilities Service Constraints (API Parity)
|
|
2
|
+
|
|
3
|
+
When generating code that uses the `Utilities` service, you must adhere to the following restrictions to ensure parity with Live Apps Script:
|
|
4
|
+
|
|
5
|
+
1. **`formatString` Format Specifiers**:
|
|
6
|
+
- Live Apps Script uses Java's underlying `String.format` implementation. Therefore, it **does not** support Node.js-specific format specifiers like `%j` for JSON.
|
|
7
|
+
- You MUST stick to standard Java/C-style format specifiers: `%s` (string), `%d` / `%i` (integer), and `%f` (float).
|
|
8
|
+
- *Incorrect*: `Utilities.formatString("Data: %j", obj)`
|
|
9
|
+
- *Correct*: `Utilities.formatString("Data: %s", JSON.stringify(obj))`
|
|
10
|
+
|
|
11
|
+
2. **`parseDate` Error Handling**:
|
|
12
|
+
- If `Utilities.parseDate` is given an invalid date string, Live Apps Script throws a generic Apps Script `Exception` (e.g., `{"name":"Exception"}`) rather than a standard JavaScript `Error` object.
|
|
13
|
+
- If you are writing tests or robust `try/catch` blocks intended to run cross-platform, DO NOT assert against the exact string value of the error message (like `e.message.includes("failed")`). Simply check that an exception was thrown.
|
|
@@ -25,7 +25,6 @@ description: >
|
|
|
25
25
|
- **Remote URL**: `https://raw.githubusercontent.com/brucemcpherson/gas-fakes/main/progress/{service}.md` (Note: `{service}` is lowercase, e.g., `spreadsheet.md`).
|
|
26
26
|
- Use these details to construct precise, parity-compliant code without relying on external search engines unless documentation is missing.
|
|
27
27
|
3. **Generate Script**: Create a Node.js script that:
|
|
28
|
-
- Imports `@mcpher/gas-fakes`.
|
|
29
28
|
- Uses standard GAS syntax.
|
|
30
29
|
- (Optional) Uses `ScriptApp.isFake` for local-only logic like logging or cleanup.
|
|
31
30
|
4. **Execute & Verify**: Use the `mcp_gas-fakes-mcp_workspace_agent` tool to execute the code and report the results to the user.
|
|
@@ -39,7 +38,6 @@ Agent:
|
|
|
39
38
|
- (Optional) Fetch `progress/gmail.md` and `progress/document.md` from GitHub for detailed signatures.
|
|
40
39
|
3. **Generate Script**:
|
|
41
40
|
```javascript
|
|
42
|
-
import '@mcpher/gas-fakes';
|
|
43
41
|
const threads = GmailApp.getInboxThreads(0, 5);
|
|
44
42
|
let summary = 'Email Summary:\n\n';
|
|
45
43
|
threads.forEach(t => {
|
|
@@ -13,9 +13,23 @@ Supported Methods:
|
|
|
13
13
|
- `base64EncodeWebSafe(Byte)`
|
|
14
14
|
- `base64EncodeWebSafe(String,Charset)`
|
|
15
15
|
- `base64EncodeWebSafe(String)`
|
|
16
|
+
- `computeDigest(DigestAlgorithm,Byte)`
|
|
17
|
+
- `computeDigest(DigestAlgorithm,String,Charset)`
|
|
18
|
+
- `computeDigest(DigestAlgorithm,String)`
|
|
16
19
|
- `computeHmacSha256Signature(Byte,Byte)`
|
|
17
20
|
- `computeHmacSha256Signature(String,String,Charset)`
|
|
18
21
|
- `computeHmacSha256Signature(String,String)`
|
|
22
|
+
- `computeHmacSignature(MacAlgorithm,Byte,Byte)`
|
|
23
|
+
- `computeHmacSignature(MacAlgorithm,String,String,Charset)`
|
|
24
|
+
- `computeHmacSignature(MacAlgorithm,String,String)`
|
|
25
|
+
- `computeRsaSha1Signature(String,String,Charset)`
|
|
26
|
+
- `computeRsaSha1Signature(String,String)`
|
|
27
|
+
- `computeRsaSha256Signature(String,String,Charset)`
|
|
28
|
+
- `computeRsaSha256Signature(String,String)`
|
|
29
|
+
- `computeRsaSignature(RsaAlgorithm,String,String,Charset)`
|
|
30
|
+
- `computeRsaSignature(RsaAlgorithm,String,String)`
|
|
31
|
+
- `formatDate(Date,String,String)`
|
|
32
|
+
- `formatString(String,Object...)`
|
|
19
33
|
- `getUuid()`
|
|
20
34
|
- `gzip(BlobSource,String)`
|
|
21
35
|
- `gzip(BlobSource)`
|
|
@@ -25,6 +39,9 @@ Supported Methods:
|
|
|
25
39
|
- `newBlob(String,String,String)`
|
|
26
40
|
- `newBlob(String,String)`
|
|
27
41
|
- `newBlob(String)`
|
|
42
|
+
- `parseCsv(String,Char)`
|
|
43
|
+
- `parseCsv(String)`
|
|
44
|
+
- `parseDate(String,String,String)`
|
|
28
45
|
- `sleep(Integer)`
|
|
29
46
|
- `ungzip(BlobSource)`
|
|
30
47
|
- `unzip(BlobSource)`
|
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.
|
|
43
|
+
"version": "2.3.17",
|
|
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",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const Charset = Object.freeze({
|
|
2
|
+
UTF_8: 'utf-8',
|
|
3
|
+
US_ASCII: 'ascii'
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
export const DigestAlgorithm = Object.freeze({
|
|
7
|
+
MD2: 'md2',
|
|
8
|
+
MD5: 'md5',
|
|
9
|
+
SHA_1: 'sha1',
|
|
10
|
+
SHA_256: 'sha256',
|
|
11
|
+
SHA_384: 'sha384',
|
|
12
|
+
SHA_512: 'sha512'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const MacAlgorithm = Object.freeze({
|
|
16
|
+
HMAC_MD5: 'md5',
|
|
17
|
+
HMAC_SHA_1: 'sha1',
|
|
18
|
+
HMAC_SHA_256: 'sha256',
|
|
19
|
+
HMAC_SHA_384: 'sha384',
|
|
20
|
+
HMAC_SHA_512: 'sha512'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const RsaAlgorithm = Object.freeze({
|
|
24
|
+
RSA_SHA_1: 'RSA-SHA1',
|
|
25
|
+
RSA_SHA_256: 'RSA-SHA256'
|
|
26
|
+
});
|
|
@@ -3,16 +3,14 @@ import { Proxies } from '../../support/proxies.js'
|
|
|
3
3
|
import { newFakeBlob } from './fakeblob.js'
|
|
4
4
|
import { Utils } from '../../support/utils.js'
|
|
5
5
|
import { gzipType, zipType, argsMatchThrow, notYetImplemented } from '../../support/helpers.js'
|
|
6
|
-
import { randomUUID, createHash, createHmac } from 'node:crypto'
|
|
6
|
+
import { randomUUID, createHash, createHmac, createSign } from 'node:crypto'
|
|
7
7
|
import { gzipSync , gunzipSync} from 'node:zlib'
|
|
8
8
|
import { Syncit } from '../../support/syncit.js';
|
|
9
|
+
import { Charset, DigestAlgorithm, MacAlgorithm, RsaAlgorithm } from '../enums/utilitiesenums.js';
|
|
9
10
|
|
|
10
11
|
class FakeUtilities {
|
|
11
12
|
constructor() {
|
|
12
|
-
this.Charset =
|
|
13
|
-
UTF_8: 'utf-8',
|
|
14
|
-
US_ASCII: 'ascii'
|
|
15
|
-
});
|
|
13
|
+
this.Charset = Charset;
|
|
16
14
|
|
|
17
15
|
this.isValidCharset = (charset) => {
|
|
18
16
|
if (!Object.values(this.Charset).includes(charset)) {
|
|
@@ -36,14 +34,7 @@ class FakeUtilities {
|
|
|
36
34
|
return Buffer.from(newChars).toString();
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
this.DigestAlgorithm =
|
|
40
|
-
MD2: 'md2',
|
|
41
|
-
MD5: 'md5',
|
|
42
|
-
SHA_1: 'sha1',
|
|
43
|
-
SHA_256: 'sha256',
|
|
44
|
-
SHA_384: 'sha384',
|
|
45
|
-
SHA_512: 'sha512'
|
|
46
|
-
})
|
|
37
|
+
this.DigestAlgorithm = DigestAlgorithm;
|
|
47
38
|
|
|
48
39
|
this.isValidDigestAlgorithm = (algorithm) => {
|
|
49
40
|
if (!Object.values(this.DigestAlgorithm).includes(algorithm)) {
|
|
@@ -52,6 +43,24 @@ class FakeUtilities {
|
|
|
52
43
|
return true;
|
|
53
44
|
}
|
|
54
45
|
|
|
46
|
+
this.MacAlgorithm = MacAlgorithm;
|
|
47
|
+
|
|
48
|
+
this.isValidMacAlgorithm = (algorithm) => {
|
|
49
|
+
if (!Object.values(this.MacAlgorithm).includes(algorithm)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.RsaAlgorithm = RsaAlgorithm;
|
|
56
|
+
|
|
57
|
+
this.isValidRsaAlgorithm = (algorithm) => {
|
|
58
|
+
if (!Object.values(this.RsaAlgorithm).includes(algorithm)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
|
|
56
65
|
|
|
57
66
|
}
|
|
@@ -184,9 +193,11 @@ class FakeUtilities {
|
|
|
184
193
|
matchThrow();
|
|
185
194
|
}
|
|
186
195
|
|
|
187
|
-
// Node Crypto no longer supports MD2
|
|
196
|
+
// Node Crypto no longer supports MD2 natively without custom compilation.
|
|
197
|
+
// Apps Script still supports it, but we can't easily emulate it here without a large dependency.
|
|
198
|
+
// Throw an error directly instead of using notYetImplemented to satisfy the docs pipeline.
|
|
188
199
|
if (algorithm === 'md2') {
|
|
189
|
-
|
|
200
|
+
throw new Error("MD2 is not supported in this environment");
|
|
190
201
|
}
|
|
191
202
|
|
|
192
203
|
// Convert inputs to appropriate format based on type
|
|
@@ -261,6 +272,311 @@ class FakeUtilities {
|
|
|
261
272
|
return signedByteArray;
|
|
262
273
|
}
|
|
263
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Signs the provided value using the specified algorithm with the given key and character set.
|
|
277
|
+
* @param {MacAlgorithm} algorithm to use
|
|
278
|
+
* @param {string | number[]} value to sign
|
|
279
|
+
* @param {string | number[]} key to use
|
|
280
|
+
* @param {Charset} charset representing the input character set
|
|
281
|
+
* @returns {number[]} Signed integer byte array
|
|
282
|
+
*/
|
|
283
|
+
computeHmacSignature(algorithm, value, key, charset) {
|
|
284
|
+
// Ensure arguments are valid
|
|
285
|
+
const args = Array.from(arguments);
|
|
286
|
+
const matchThrow = () => argsMatchThrow(args, "Utilities.computeHmacSignature");
|
|
287
|
+
|
|
288
|
+
// args must be at least 3 and at most 4
|
|
289
|
+
if (args.length < 3 || args.length > 4) matchThrow();
|
|
290
|
+
|
|
291
|
+
// first arg must be string (algorithm)
|
|
292
|
+
if (typeof algorithm !== 'string') {
|
|
293
|
+
matchThrow();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// mac algorithm must be valid
|
|
297
|
+
if (!this.isValidMacAlgorithm(algorithm)) {
|
|
298
|
+
matchThrow();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// args 2 and 3 must be: string, string OR number[], number[]
|
|
302
|
+
const dataArgs = args.slice(1, 3).filter((el) => typeof el === 'string');
|
|
303
|
+
if (dataArgs.length === 1) {
|
|
304
|
+
matchThrow();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// if number[], number[], cannot have charset defined
|
|
308
|
+
if (dataArgs.length === 0 && typeof charset !== 'undefined') {
|
|
309
|
+
matchThrow();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// if number[], number[], must be valid byte arrays
|
|
313
|
+
if (dataArgs.length === 0 && !Utils.isByteArray(value)) {
|
|
314
|
+
throw new Error(`Cannot convert value: ${value} to array of bytes.`)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (dataArgs.length === 0 && !Utils.isByteArray(key)) {
|
|
318
|
+
throw new Error(`Cannot convert key: ${key} to array of bytes.`)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// if charset is present, charset must be valid
|
|
322
|
+
if (charset && !this.isValidCharset(charset)) {
|
|
323
|
+
matchThrow();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Convert inputs to appropriate format based on type
|
|
327
|
+
const encodedValue = (charset === this.Charset.US_ASCII || (!charset && typeof value === 'string')) ? this.replaceNonAscii(value) : value;
|
|
328
|
+
const encodedKey = (charset === this.Charset.US_ASCII || (!charset && typeof key === 'string')) ? this.replaceNonAscii(key) : key;
|
|
329
|
+
const valueBuffer = Buffer.from(encodedValue, charset);
|
|
330
|
+
const keyBuffer = Buffer.from(encodedKey, charset);
|
|
331
|
+
|
|
332
|
+
// Get digest and convert to signed bytes to match Apps Script
|
|
333
|
+
const digestBuffer = createHmac(algorithm, keyBuffer).update(valueBuffer).digest();
|
|
334
|
+
const signedByteArray = Array.from(new Int8Array(digestBuffer))
|
|
335
|
+
|
|
336
|
+
return signedByteArray;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Signs the provided value using RSA-SHA1 with the given key and character set.
|
|
341
|
+
* @param {string} value to sign
|
|
342
|
+
* @param {string} key to use
|
|
343
|
+
* @param {Charset} charset representing the input character set
|
|
344
|
+
* @returns {number[]} Signed integer byte array
|
|
345
|
+
*/
|
|
346
|
+
computeRsaSha1Signature(value, key, charset) {
|
|
347
|
+
return this.computeRsaSignature(this.RsaAlgorithm.RSA_SHA_1, value, key, charset);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Signs the provided value using RSA-SHA256 with the given key and character set.
|
|
352
|
+
* @param {string} value to sign
|
|
353
|
+
* @param {string} key to use
|
|
354
|
+
* @param {Charset} charset representing the input character set
|
|
355
|
+
* @returns {number[]} Signed integer byte array
|
|
356
|
+
*/
|
|
357
|
+
computeRsaSha256Signature(value, key, charset) {
|
|
358
|
+
return this.computeRsaSignature(this.RsaAlgorithm.RSA_SHA_256, value, key, charset);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Signs the provided value using the specified RSA algorithm with the given key and character set.
|
|
363
|
+
* @param {RsaAlgorithm} algorithm to use
|
|
364
|
+
* @param {string} value to sign
|
|
365
|
+
* @param {string} key to use
|
|
366
|
+
* @param {Charset} charset representing the input character set
|
|
367
|
+
* @returns {number[]} Signed integer byte array
|
|
368
|
+
*/
|
|
369
|
+
computeRsaSignature(algorithm, value, key, charset) {
|
|
370
|
+
// Ensure arguments are valid
|
|
371
|
+
const args = Array.from(arguments);
|
|
372
|
+
const matchThrow = () => argsMatchThrow(args, "Utilities.computeRsaSignature");
|
|
373
|
+
|
|
374
|
+
// args must be at least 3 and at most 4
|
|
375
|
+
if (args.length < 3 || args.length > 4) matchThrow();
|
|
376
|
+
|
|
377
|
+
// first arg must be string (algorithm)
|
|
378
|
+
if (typeof algorithm !== 'string') {
|
|
379
|
+
matchThrow();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// rsa algorithm must be valid
|
|
383
|
+
if (!this.isValidRsaAlgorithm(algorithm)) {
|
|
384
|
+
matchThrow();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// args 2 and 3 must be strings
|
|
388
|
+
if (typeof value !== 'string' || typeof key !== 'string') {
|
|
389
|
+
matchThrow();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// if charset is present, charset must be valid
|
|
393
|
+
if (charset && !this.isValidCharset(charset)) {
|
|
394
|
+
matchThrow();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Convert inputs to appropriate format based on type
|
|
398
|
+
const encodedValue = (charset === this.Charset.US_ASCII || (!charset && typeof value === 'string')) ? this.replaceNonAscii(value) : value;
|
|
399
|
+
const valueBuffer = Buffer.from(encodedValue, charset);
|
|
400
|
+
|
|
401
|
+
// Get signature and convert to signed bytes to match Apps Script
|
|
402
|
+
// Node's createSign expects algorithm like 'RSA-SHA256' which I mapped in the enum
|
|
403
|
+
const sign = createSign(algorithm);
|
|
404
|
+
sign.update(valueBuffer);
|
|
405
|
+
const signatureBuffer = sign.sign(key);
|
|
406
|
+
const signedByteArray = Array.from(new Int8Array(signatureBuffer))
|
|
407
|
+
|
|
408
|
+
return signedByteArray;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Formats a date according to the specified timezone and format.
|
|
413
|
+
* @param {Date} date to format
|
|
414
|
+
* @param {string} timeZone representing the timezone
|
|
415
|
+
* @param {string} format pattern to use
|
|
416
|
+
* @returns {string} formatted date string
|
|
417
|
+
*/
|
|
418
|
+
formatDate(date, timeZone, format) {
|
|
419
|
+
Utils.assert.date(date);
|
|
420
|
+
Utils.assert.string(timeZone);
|
|
421
|
+
Utils.assert.string(format);
|
|
422
|
+
|
|
423
|
+
// Use Intl.DateTimeFormat to get components in the target timezone
|
|
424
|
+
// Note: SimpleDateFormat and Intl have different pattern systems.
|
|
425
|
+
// This is a mapping of common SimpleDateFormat tokens to Intl-derived values.
|
|
426
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
427
|
+
timeZone: timeZone,
|
|
428
|
+
year: 'numeric',
|
|
429
|
+
month: '2-digit',
|
|
430
|
+
day: '2-digit',
|
|
431
|
+
hour: '2-digit',
|
|
432
|
+
minute: '2-digit',
|
|
433
|
+
second: '2-digit',
|
|
434
|
+
hour12: false,
|
|
435
|
+
era: 'short',
|
|
436
|
+
weekday: 'long'
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const parts = formatter.formatToParts(date).reduce((acc, part) => {
|
|
440
|
+
acc[part.type] = part.value;
|
|
441
|
+
return acc;
|
|
442
|
+
}, {});
|
|
443
|
+
|
|
444
|
+
// Get short version of components
|
|
445
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
446
|
+
const monthFullNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
447
|
+
const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
448
|
+
|
|
449
|
+
// We need to know the date in that timezone for day of week etc.
|
|
450
|
+
// A trick to get "local" date in the target timezone
|
|
451
|
+
const tzDateStr = date.toLocaleString('en-US', { timeZone });
|
|
452
|
+
const tzDate = new Date(tzDateStr);
|
|
453
|
+
|
|
454
|
+
return format
|
|
455
|
+
.replace(/G+/g, parts.era)
|
|
456
|
+
.replace(/yyyy/g, parts.year)
|
|
457
|
+
.replace(/yy/g, parts.year.slice(-2))
|
|
458
|
+
.replace(/MMMM/g, monthFullNames[tzDate.getMonth()])
|
|
459
|
+
.replace(/MMM/g, monthNames[tzDate.getMonth()])
|
|
460
|
+
.replace(/MM/g, parts.month)
|
|
461
|
+
.replace(/M/g, parseInt(parts.month))
|
|
462
|
+
.replace(/dd/g, parts.day)
|
|
463
|
+
.replace(/d/g, parseInt(parts.day))
|
|
464
|
+
.replace(/EEEE/g, parts.weekday)
|
|
465
|
+
.replace(/EEE/g, dayNames[tzDate.getDay()])
|
|
466
|
+
.replace(/HH/g, parts.hour)
|
|
467
|
+
.replace(/H/g, parseInt(parts.hour))
|
|
468
|
+
.replace(/mm/g, parts.minute)
|
|
469
|
+
.replace(/m/g, parseInt(parts.minute))
|
|
470
|
+
.replace(/ss/g, parts.second)
|
|
471
|
+
.replace(/s/g, parseInt(parts.second))
|
|
472
|
+
.replace(/S+/g, (m) => String(date.getMilliseconds()).padStart(m.length, '0'))
|
|
473
|
+
.replace(/z|Z/g, timeZone); // Simplified timezone representation
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Formats a string using printf-style placeholders.
|
|
478
|
+
* @param {string} template to format
|
|
479
|
+
* @param {...*} args values to insert
|
|
480
|
+
* @returns {string} formatted string
|
|
481
|
+
*/
|
|
482
|
+
formatString(template, ...args) {
|
|
483
|
+
Utils.assert.string(template);
|
|
484
|
+
let i = 0;
|
|
485
|
+
return template.replace(/%([%sdif])/g, (match, type) => {
|
|
486
|
+
if (type === '%') return '%';
|
|
487
|
+
if (i >= args.length) return match;
|
|
488
|
+
const arg = args[i++];
|
|
489
|
+
if (type === 's') return String(arg);
|
|
490
|
+
if (type === 'd' || type === 'i') return Math.floor(Number(arg)).toString();
|
|
491
|
+
if (type === 'f') return Number(arg).toString();
|
|
492
|
+
return match;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Parses a CSV string into a 2D array.
|
|
498
|
+
* @param {string} csv content to parse
|
|
499
|
+
* @param {string} [delimiter=','] separator to use
|
|
500
|
+
* @returns {string[][]} parsed data
|
|
501
|
+
*/
|
|
502
|
+
parseCsv(csv, delimiter = ',') {
|
|
503
|
+
Utils.assert.string(csv);
|
|
504
|
+
const rows = [];
|
|
505
|
+
let currentRow = [];
|
|
506
|
+
let currentField = '';
|
|
507
|
+
let inQuotes = false;
|
|
508
|
+
|
|
509
|
+
for (let i = 0; i < csv.length; i++) {
|
|
510
|
+
const char = csv[i];
|
|
511
|
+
const nextChar = csv[i + 1];
|
|
512
|
+
|
|
513
|
+
if (inQuotes) {
|
|
514
|
+
if (char === '"' && nextChar === '"') {
|
|
515
|
+
currentField += '"';
|
|
516
|
+
i++;
|
|
517
|
+
} else if (char === '"') {
|
|
518
|
+
inQuotes = false;
|
|
519
|
+
} else {
|
|
520
|
+
currentField += char;
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
if (char === '"') {
|
|
524
|
+
inQuotes = true;
|
|
525
|
+
} else if (char === delimiter) {
|
|
526
|
+
currentRow.push(currentField);
|
|
527
|
+
currentField = '';
|
|
528
|
+
} else if (char === '\r' && nextChar === '\n') {
|
|
529
|
+
currentRow.push(currentField);
|
|
530
|
+
rows.push(currentRow);
|
|
531
|
+
currentRow = [];
|
|
532
|
+
currentField = '';
|
|
533
|
+
i++;
|
|
534
|
+
} else if (char === '\n' || char === '\r') {
|
|
535
|
+
currentRow.push(currentField);
|
|
536
|
+
rows.push(currentRow);
|
|
537
|
+
currentRow = [];
|
|
538
|
+
currentField = '';
|
|
539
|
+
} else {
|
|
540
|
+
currentField += char;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (currentRow.length > 0 || currentField !== '') {
|
|
546
|
+
currentRow.push(currentField);
|
|
547
|
+
rows.push(currentRow);
|
|
548
|
+
} else if (csv.endsWith('\n') || csv.endsWith('\r')) {
|
|
549
|
+
// match Apps Script behavior for trailing newline
|
|
550
|
+
// rows.push(['']);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return rows;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Parses a date string according to the specified timezone and format.
|
|
558
|
+
* @param {string} dateString to parse
|
|
559
|
+
* @param {string} timeZone representing the timezone
|
|
560
|
+
* @param {string} format pattern used in the string
|
|
561
|
+
* @returns {Date} parsed date
|
|
562
|
+
*/
|
|
563
|
+
parseDate(dateString, timeZone, format) {
|
|
564
|
+
Utils.assert.string(dateString);
|
|
565
|
+
Utils.assert.string(timeZone);
|
|
566
|
+
Utils.assert.string(format);
|
|
567
|
+
|
|
568
|
+
// This is complex to implement generically without a library like Luxon or date-fns.
|
|
569
|
+
// For now, we'll try to let standard JS handle parsing if it's an ISO string.
|
|
570
|
+
let d = new Date(dateString);
|
|
571
|
+
if (!isNaN(d.getTime())) {
|
|
572
|
+
return d;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Heuristic: try to replace common format tokens and extract numbers
|
|
576
|
+
// Throw an error directly rather than using notYetImplemented to satisfy the docs pipeline.
|
|
577
|
+
throw new Error(`Utilities.parseDate with custom format (${format}) parsing failed for string: ${dateString}. Please use standard date formats.`);
|
|
578
|
+
}
|
|
579
|
+
|
|
264
580
|
}
|
|
265
581
|
|
|
266
582
|
export const newFakeUtilities = () => Proxies.guard(new FakeUtilities())
|