docupuncture 0.1.0
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/LICENSE +21 -0
- package/README.md +53 -0
- package/package.json +50 -0
- package/skills/docupuncture-docs/SKILL.md +84 -0
- package/skills/docupuncture-docs/references/sample-skeleton.js +110 -0
- package/skills/docupuncture-sheets/SKILL.md +55 -0
- package/skills/docupuncture-sheets/references/sample-skeleton.js +69 -0
- package/skills/docupuncture-slides/SKILL.md +56 -0
- package/skills/docupuncture-slides/references/sample-skeleton.js +103 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Catalyst Forge LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Docupuncture
|
|
2
|
+
|
|
3
|
+
Treat the document you already have.
|
|
4
|
+
|
|
5
|
+
Insert precise edits without changing the theme, format, comments, or history.
|
|
6
|
+
|
|
7
|
+
When an AI regenerates a Google Doc, Sheet, or deck, those things usually go with it. Docupuncture writes a small Apps Script you paste and run on the live file.
|
|
8
|
+
|
|
9
|
+
## The skills
|
|
10
|
+
|
|
11
|
+
Copy a folder into Cursor, Claude Code, or anything else that reads `SKILL.md`. When someone asks for edits to an existing Workspace file, the agent writes a script instead of a new document.
|
|
12
|
+
|
|
13
|
+
| Skill | Surface | Fit |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| [`docupuncture-docs`](skills/docupuncture-docs/SKILL.md) | Google Docs | Comments, tabs, lists, partial styling. |
|
|
16
|
+
| [`docupuncture-sheets`](skills/docupuncture-sheets/SKILL.md) | Google Sheets | Grid and ranges. Notes and validation stay. |
|
|
17
|
+
| [`docupuncture-slides`](skills/docupuncture-slides/SKILL.md) | Google Slides | Text and content. Layout surgery is higher risk. |
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add docupuncture
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Copy one folder from `node_modules/docupuncture/skills/` into the agent's skills directory.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
git clone https://github.com/Catalyst-Forge-LLC/docupuncture.git
|
|
29
|
+
# then copy skills/docupuncture-docs/ (or -sheets / -slides)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Site: [docupuncture.com](https://docupuncture.com)
|
|
33
|
+
|
|
34
|
+
## How a run works
|
|
35
|
+
|
|
36
|
+
1. The agent reads the live file and confirms the edits.
|
|
37
|
+
2. It writes an Apps Script.
|
|
38
|
+
3. You open the file → Extensions → Apps Script → paste → run `applyEdits`.
|
|
39
|
+
4. First run is a dry run (`DRY_RUN = true`). Read the log, flip the flag, run again.
|
|
40
|
+
|
|
41
|
+
The script is safe to re-run. Missing anchors are logged. It never invents a location.
|
|
42
|
+
|
|
43
|
+
## Why the name
|
|
44
|
+
|
|
45
|
+
The other tools in the shop strike, press, or contain. A needle is still metal from the same fire, but you don't swing it. You place it.
|
|
46
|
+
|
|
47
|
+
Acupuncture treats a living body in place: insert at a point, leave the rest alone. Docupuncture does that to a document you already care about.
|
|
48
|
+
|
|
49
|
+
Closest cousin on the shelf is [TemperPass](https://temperpass.dev): a rule set you point an agent at.
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT. Built by [Catalyst Forge LLC](https://www.catalystforge.com).
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "docupuncture",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Insert precise edits without changing the theme, format, comments, or history.",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Catalyst Forge LLC",
|
|
9
|
+
"keywords": [
|
|
10
|
+
"ai",
|
|
11
|
+
"agents",
|
|
12
|
+
"skills",
|
|
13
|
+
"cursor",
|
|
14
|
+
"claude",
|
|
15
|
+
"google-docs",
|
|
16
|
+
"google-sheets",
|
|
17
|
+
"google-slides",
|
|
18
|
+
"apps-script"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://docupuncture.com",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Catalyst-Forge-LLC/docupuncture.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Catalyst-Forge-LLC/docupuncture/issues"
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
"./skills/*": "./skills/*",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"skills",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"site:dev": "pnpm --dir site dev",
|
|
45
|
+
"site:build": "pnpm --dir site run build",
|
|
46
|
+
"site:check": "pnpm --dir site check",
|
|
47
|
+
"site:preview": "pnpm --dir site preview",
|
|
48
|
+
"ship": "pnpm --dir site run ship"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docupuncture-docs
|
|
3
|
+
description: Precise in-place edits to an existing Google Doc via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Doc (or says "docupuncture") — deliver a self-contained script that patches the live doc while preserving all formatting, instead of regenerating a new document. Prefer this over full regeneration for formatted docs. Part of the Docupuncture family (Docs, Sheets, Slides).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docupuncture for Docs
|
|
7
|
+
|
|
8
|
+
Docupuncture = insert precise edits without changing the theme, format, comments, or history. This skill is the **Docs** implementation.
|
|
9
|
+
|
|
10
|
+
Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs, instead of regenerating a new document. In-place patching preserves all formatting (fonts, colors, images, tables, comments, theme) that API imports and full regenerations destroy. Use the name "docupuncture" when referring to this workflow with the user.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## When to offer
|
|
14
|
+
|
|
15
|
+
Whenever the user asks for edits, tweaks, or iterations to an existing Google Doc — especially repeated iteration rounds — use AskUserQuestion to offer:
|
|
16
|
+
|
|
17
|
+
1. **Docupuncture: Apps Script patch** (recommended for formatted docs) — I generate a script; you run it once. Formatting preserved. First run requires a one-time authorization prompt.
|
|
18
|
+
2. **New generated doc** — full regeneration (warn: loses theme/logo/comments).
|
|
19
|
+
3. **Manual edit checklist** — exact paste-ready text.
|
|
20
|
+
|
|
21
|
+
Skip the question and go straight to the script if the user says "docupuncture" or has already established this preference in the conversation.
|
|
22
|
+
|
|
23
|
+
### Edit volume / risk guidance
|
|
24
|
+
|
|
25
|
+
Before generating the script, quickly assess the size and nature of the requested changes:
|
|
26
|
+
|
|
27
|
+
- **Low risk** (1–5 discrete edits, mostly text swaps or simple inserts/deletes): proceed with pure docupuncture.
|
|
28
|
+
- **Medium risk** (6–10 edits or light structural work): still prefer docupuncture, but warn the user that a fresh re-read of the doc is required if they make manual changes between runs, and consider splitting into two smaller scripts if the anchors feel fragile.
|
|
29
|
+
- **High risk** (>10 discrete edits, major restructuring, multi-section moves, or many interdependent changes): strongly recommend breaking into multiple sequential docupuncture rounds, or switching to a full regeneration / hybrid approach. Do not force a single massive script.
|
|
30
|
+
|
|
31
|
+
If the set of changes is high-risk, surface this explicitly to the user before writing the script.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. **Read the doc first** via the Google Drive MCP `read_file_content` (extract the file ID from the URL). Get the EXACT current text — anchors must match character-for-character. Confirm with the user which edits to make before writing the script.
|
|
36
|
+
|
|
37
|
+
2. **Write the script** (rules below) and deliver it in a code block with run instructions:
|
|
38
|
+
- Open the doc → Extensions → Apps Script
|
|
39
|
+
- Delete any existing code, paste the script, click Run (▶) on `applyEdits`
|
|
40
|
+
- First run: authorize when prompted (it runs as you, on your doc)
|
|
41
|
+
- Check the Execution log for the ✓/✗ summary
|
|
42
|
+
|
|
43
|
+
3. **Verify** after the user says they ran it: re-read the doc via MCP and confirm every edit landed.
|
|
44
|
+
|
|
45
|
+
## Script rules
|
|
46
|
+
|
|
47
|
+
- **Container-bound**: use `DocumentApp.getActiveDocument()` (opened from the doc's own Script editor) — minimal auth friction. Do not use `openById` unless the user wants a standalone script.
|
|
48
|
+
|
|
49
|
+
- **Tabs**: if the doc has tabs, select the right one: `doc.getTabs()[i].asDocumentTab().getBody()`. Plain `doc.getBody()` only for untabbed docs. Prefer a small helper that iterates all tabs (and child tabs) and searches each body when the target tab is not known with certainty. Apps Script cannot delete tabs — flag tab deletion as a manual step.
|
|
50
|
+
|
|
51
|
+
- **Anchors (critical)**: locate insertion / edit points with `body.findText(pattern)`.
|
|
52
|
+
- Prefer a short, highly unique literal that is unlikely to change (section titles, distinctive phrases near the edit point, or rare words).
|
|
53
|
+
- `findText` treats its pattern as a regex, so always route anchor literals through the `escapeRegExp` helper (see skeleton) rather than passing raw strings — a stray `(` or `?` in a section title will silently break the match otherwise.
|
|
54
|
+
- Use a distinctive substring, never an entire long paragraph (text may span multiple runs).
|
|
55
|
+
- For higher robustness on medium/high-risk edits: make each edit function try a primary unique anchor, then a secondary nearby unique context string if the primary is missing. Log which strategy succeeded.
|
|
56
|
+
- Never invent a location if no anchor is found.
|
|
57
|
+
|
|
58
|
+
- **Inserting paragraphs**: `body.getChildIndex(anchorElement.getParent())` then `body.insertParagraph(idx + 1, text)`. Copy formatting from a sibling: `newPara.setAttributes(sibling.getAttributes())`.
|
|
59
|
+
|
|
60
|
+
- **Bullets**: use `body.insertListItem(idx, text)` and inherit list formatting: `.setListId(siblingListItem).setNestingLevel(siblingListItem.getNestingLevel()).setGlyphType(siblingListItem.getGlyphType())`, plus `setAttributes` from the sibling.
|
|
61
|
+
|
|
62
|
+
- **Deleting elements**: find the anchor, walk up to the containing `LIST_ITEM` or `PARAGRAPH`, then `removeFromParent()`.
|
|
63
|
+
|
|
64
|
+
- **Partial bolding / styling spans**: after inserting, use `para.editAsText().setBold(startOffset, endOffset, true)` (or other style methods) with carefully computed offsets.
|
|
65
|
+
|
|
66
|
+
- **Replacements**: prefer `body.replaceText(escapedPattern, replacement)` for simple text swaps; for structural changes (splitting a bullet, inserting between existing items), edit the existing element's text and insert new siblings.
|
|
67
|
+
|
|
68
|
+
- **Idempotent**: before each edit, check whether the target state already exists (`findText` for new content; absence for deletions) and skip with a log message if so. Re-running must never duplicate content.
|
|
69
|
+
|
|
70
|
+
- **Fail loudly**: if an anchor isn't found, log `✗ SKIPPED: <edit name> — anchor not found` and continue with the other edits. Never guess a location. End with a summary: `Logger.log('Done: X applied, Y skipped')`.
|
|
71
|
+
|
|
72
|
+
- **Dry run**: include a `DRY_RUN` flag (default `true`) at the top of the script. When on, each edit function locates its anchor and logs `→ WOULD APPLY: <edit name>` without modifying the document. The user reads the log, flips the flag to `false`, and runs again. This is a stronger safety story than "test on a copy" for important documents.
|
|
73
|
+
|
|
74
|
+
- **Structure**: one `applyEdits()` entry function; each edit in its own small named function (or clearly-commented block) whose name matches how you described the edit to the user. See `references/sample-skeleton.js` for a recommended starting template.
|
|
75
|
+
|
|
76
|
+
- **Never** hardcode credentials or API keys in the script.
|
|
77
|
+
|
|
78
|
+
## Caveats to tell the user (once)
|
|
79
|
+
|
|
80
|
+
- First run shows Google's authorization screen — the script runs entirely as them, on their doc.
|
|
81
|
+
- Apps Script can't delete tabs or edit comments; those stay manual.
|
|
82
|
+
- Comments are preserved in untouched regions, but an edit that deletes or replaces the exact text a comment is anchored to will orphan that comment. Flag this when an edit overlaps a commented range.
|
|
83
|
+
- Suggest running on a copy the first time if the doc is important.
|
|
84
|
+
- After several iterative rounds, anchors can drift if the surrounding text has changed. A fresh read of the current document is always recommended before generating the next script.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docupuncture script skeleton (Docs)
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* 1. Open the target Google Doc → Extensions → Apps Script
|
|
6
|
+
* 2. Replace all code with this script
|
|
7
|
+
* 3. Click Run ▶ on applyEdits — with DRY_RUN = true it only logs what it
|
|
8
|
+
* WOULD change, without touching the document
|
|
9
|
+
* 4. Read the Execution log; if it looks right, set DRY_RUN = false and run again
|
|
10
|
+
* 5. Authorize on first run
|
|
11
|
+
*
|
|
12
|
+
* Each edit lives in its own named function so the log is human-readable
|
|
13
|
+
* and individual edits can be skipped or re-run safely.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DRY_RUN = true; // ← set to false to actually apply the edits
|
|
17
|
+
|
|
18
|
+
function applyEdits() {
|
|
19
|
+
const doc = DocumentApp.getActiveDocument();
|
|
20
|
+
// Prefer the correct tab body when the document uses tabs
|
|
21
|
+
const body = getBodyForTab(doc /*, 'Preferred Tab Name' */);
|
|
22
|
+
|
|
23
|
+
let applied = 0;
|
|
24
|
+
let skipped = 0;
|
|
25
|
+
|
|
26
|
+
// ---- Individual edits ----
|
|
27
|
+
// Each named function returns true (applied / would apply) or false (skipped)
|
|
28
|
+
if (edit_AddIntroParagraph(body)) applied++; else skipped++;
|
|
29
|
+
// if (edit_UpdatePricingTable(body)) applied++; else skipped++;
|
|
30
|
+
// ... add more here
|
|
31
|
+
|
|
32
|
+
const mode = DRY_RUN ? 'DRY RUN — no changes made' : 'LIVE';
|
|
33
|
+
Logger.log(`Done (${mode}): ${applied} ${DRY_RUN ? 'would apply' : 'applied'}, ${skipped} skipped`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Example edit function.
|
|
38
|
+
* Name it after the human description of the change.
|
|
39
|
+
* Always idempotent, always fail loudly, always escape anchors.
|
|
40
|
+
*/
|
|
41
|
+
function edit_AddIntroParagraph(body) {
|
|
42
|
+
const EDIT_NAME = 'AddIntroParagraph';
|
|
43
|
+
|
|
44
|
+
// 1. Check if the desired end state already exists (idempotency)
|
|
45
|
+
if (body.findText(escapeRegExp('Welcome to the new section'))) {
|
|
46
|
+
Logger.log(`✓ SKIPPED (already present): ${EDIT_NAME}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Locate a robust anchor (primary + optional secondary).
|
|
51
|
+
// Always route anchor literals through escapeRegExp — findText treats
|
|
52
|
+
// its pattern as a regex, and a stray '(' or '?' silently breaks the match.
|
|
53
|
+
let range = body.findText(escapeRegExp('Section 2: Overview')); // primary unique literal
|
|
54
|
+
if (!range) {
|
|
55
|
+
range = body.findText(escapeRegExp('Overview of the system')); // secondary nearby context
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!range) {
|
|
59
|
+
Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor not found`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 3. Dry run: report and stop before modifying anything
|
|
64
|
+
if (DRY_RUN) {
|
|
65
|
+
Logger.log(`→ WOULD APPLY: ${EDIT_NAME} (anchor found)`);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. Perform the edit
|
|
70
|
+
const element = range.getElement();
|
|
71
|
+
const parent = element.getParent();
|
|
72
|
+
const idx = body.getChildIndex(parent);
|
|
73
|
+
|
|
74
|
+
const newPara = body.insertParagraph(idx + 1, 'Welcome to the new section. This is the injected content.');
|
|
75
|
+
|
|
76
|
+
// Optional: copy attributes from a sibling for consistent formatting
|
|
77
|
+
// newPara.setAttributes(parent.getAttributes());
|
|
78
|
+
|
|
79
|
+
Logger.log(`✓ APPLIED: ${EDIT_NAME}`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Helper: safely escape a string for use in findText / replaceText regex.
|
|
85
|
+
* Use this on EVERY anchor literal — do not pass raw strings.
|
|
86
|
+
*/
|
|
87
|
+
function escapeRegExp(str) {
|
|
88
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Optional robust tab helper (use when the document may have tabs)
|
|
93
|
+
*/
|
|
94
|
+
function getBodyForTab(doc, preferredTabName) {
|
|
95
|
+
const tabs = doc.getTabs();
|
|
96
|
+
if (!tabs || tabs.length === 0) {
|
|
97
|
+
return doc.getBody();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Prefer matching by name if provided
|
|
101
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
102
|
+
const tab = tabs[i];
|
|
103
|
+
if (preferredTabName && tab.getTitle() === preferredTabName) {
|
|
104
|
+
return tab.asDocumentTab().getBody();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fallback: first tab (or implement recursive child-tab search if needed)
|
|
109
|
+
return tabs[0].asDocumentTab().getBody();
|
|
110
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docupuncture-sheets
|
|
3
|
+
description: Precise in-place edits to an existing Google Sheet via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Sheet (or says "docupuncture" in a sheets context) — deliver a self-contained script that patches the live spreadsheet while preserving formatting, notes, data validation, and structure, instead of regenerating a new sheet.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docupuncture for Sheets
|
|
7
|
+
|
|
8
|
+
Docupuncture applied to Google Sheets: insert precise edits without changing the theme, format, comments, or history. Notes, data validation, charts, and structure stay too. Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs.
|
|
9
|
+
|
|
10
|
+
Use the name "docupuncture" (or "docupuncture on this sheet") when referring to this workflow.
|
|
11
|
+
|
|
12
|
+
## When to offer
|
|
13
|
+
|
|
14
|
+
Whenever the user asks for edits, tweaks, or iterations to an existing Google Sheet — especially repeated rounds — offer:
|
|
15
|
+
|
|
16
|
+
1. **Docupuncture: Apps Script patch** (recommended) — I generate a script; you run it once. Formatting, notes, and validation preserved.
|
|
17
|
+
2. **New generated sheet** — full regeneration (warn: loses most formatting, notes, validation, charts).
|
|
18
|
+
3. **Manual edit checklist** — exact values/ranges to change.
|
|
19
|
+
|
|
20
|
+
Skip the question if the user already said "docupuncture" or established the preference.
|
|
21
|
+
|
|
22
|
+
### Edit volume / risk guidance
|
|
23
|
+
|
|
24
|
+
- **Low risk** (1–6 discrete cell/range updates): pure docupuncture.
|
|
25
|
+
- **Medium risk** (7–12 updates or light structural work such as inserting a few rows): still preferred, but warn about anchor drift.
|
|
26
|
+
- **High risk** (large structural changes, many interdependent formulas, or wholesale table rewrites): recommend sequential smaller scripts or full regeneration.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
1. **Read the sheet first** (via available tools or user-provided ranges/values). Confirm the exact current state and the requested changes.
|
|
31
|
+
2. **Write the script** and deliver it with clear run instructions:
|
|
32
|
+
- Open the spreadsheet → Extensions → Apps Script
|
|
33
|
+
- Paste the script, run `applyEdits`
|
|
34
|
+
- Authorize on first run
|
|
35
|
+
- Check the Execution log for the summary
|
|
36
|
+
3. **Verify** after the user runs it by re-checking key cells/ranges if possible.
|
|
37
|
+
|
|
38
|
+
## Script rules
|
|
39
|
+
|
|
40
|
+
- **Container-bound**: use `SpreadsheetApp.getActiveSpreadsheet()` (and `getActiveSheet()` or named sheets). Prefer this over `openById` for minimal friction.
|
|
41
|
+
- **Anchors**: locate targets with `sheet.createTextFinder(text).findNext()` or by stable headers + relative offsets. Prefer unique header values or distinctive cell content as anchors. Escape regex when needed.
|
|
42
|
+
- **Preserve formatting**: when updating values, prefer patterns that keep existing formatting (e.g. `setValue` on already-formatted cells, or capture/re-apply RichTextValue / `copyTo` with format options when necessary). Explicitly preserve notes and data validation where the edit should not touch them.
|
|
43
|
+
- **Idempotent**: before changing a cell or range, check whether the desired end state already exists and skip if so. Re-running must never create duplicate rows or overwrite correct data unnecessarily.
|
|
44
|
+
- **Fail loudly**: if an anchor/header is missing, log `✗ SKIPPED: <edit name> — anchor not found` and continue. Never invent row/column numbers.
|
|
45
|
+
- **Dry run**: include a `DRY_RUN` flag (default `true`). When on, each edit locates its anchor and logs `→ WOULD APPLY` without writing anything. User reads the log, flips to `false`, runs again.
|
|
46
|
+
- **Structure**: one `applyEdits()` entry point. Each logical change in its own named function that returns success/failure. End with a clear `Logger.log` summary.
|
|
47
|
+
- **Never** hardcode credentials.
|
|
48
|
+
|
|
49
|
+
## Caveats to tell the user (once)
|
|
50
|
+
|
|
51
|
+
- First run shows Google’s authorization screen — the script runs as them.
|
|
52
|
+
- Complex formula dependencies can still break if the structural change is large; prefer small, targeted needles.
|
|
53
|
+
- Suggest testing on a copy first if the sheet is critical.
|
|
54
|
+
|
|
55
|
+
See `references/sample-skeleton.js` for a recommended starting template.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docupuncture for Sheets – sample skeleton
|
|
3
|
+
*
|
|
4
|
+
* Paste into the target spreadsheet's Extensions → Apps Script, then run
|
|
5
|
+
* applyEdits. With DRY_RUN = true it only logs what it WOULD change;
|
|
6
|
+
* read the log, set DRY_RUN = false, and run again to apply.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DRY_RUN = true; // ← set to false to actually apply the edits
|
|
10
|
+
|
|
11
|
+
function applyEdits() {
|
|
12
|
+
const ss = SpreadsheetApp.getActiveSpreadsheet();
|
|
13
|
+
const sheet = ss.getActiveSheet(); // or ss.getSheetByName('Specific Name')
|
|
14
|
+
|
|
15
|
+
let applied = 0;
|
|
16
|
+
let skipped = 0;
|
|
17
|
+
|
|
18
|
+
if (edit_UpdateStatusColumn(sheet)) applied++; else skipped++;
|
|
19
|
+
// if (edit_AddNewRowForItem(sheet)) applied++; else skipped++;
|
|
20
|
+
|
|
21
|
+
const mode = DRY_RUN ? 'DRY RUN — no changes made' : 'LIVE';
|
|
22
|
+
Logger.log(`Done (${mode}): ${applied} ${DRY_RUN ? 'would apply' : 'applied'}, ${skipped} skipped`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Example: find a row by unique key and update a specific column.
|
|
27
|
+
* Returns true if applied (or would apply in dry run), false if skipped.
|
|
28
|
+
*/
|
|
29
|
+
function edit_UpdateStatusColumn(sheet) {
|
|
30
|
+
const EDIT_NAME = 'UpdateStatusColumn';
|
|
31
|
+
|
|
32
|
+
// 1. Locate the row via a unique anchor value.
|
|
33
|
+
// matchEntireCell(true) keeps plain-text matching safe; if you switch
|
|
34
|
+
// to useRegularExpression(true), escape the anchor with escapeRegExp.
|
|
35
|
+
const finder = sheet.createTextFinder('PROJECT-1234').matchEntireCell(true);
|
|
36
|
+
const cell = finder.findNext();
|
|
37
|
+
if (!cell) {
|
|
38
|
+
Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor not found`);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const row = cell.getRow();
|
|
43
|
+
const statusCell = sheet.getRange(row, 5); // column E for status, adjust as needed
|
|
44
|
+
|
|
45
|
+
// 2. Idempotency: skip if already correct
|
|
46
|
+
if (statusCell.getValue() === 'Completed') {
|
|
47
|
+
Logger.log(`✓ SKIPPED (already present): ${EDIT_NAME}`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Dry run: report and stop before writing anything
|
|
52
|
+
if (DRY_RUN) {
|
|
53
|
+
Logger.log(`→ WOULD APPLY: ${EDIT_NAME} (row ${row}, col 5 → 'Completed')`);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 4. Apply the change (formatting, notes, and validation on the cell stay intact)
|
|
58
|
+
statusCell.setValue('Completed');
|
|
59
|
+
Logger.log(`✓ APPLIED: ${EDIT_NAME} (row ${row})`);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Helper – escape a string for TextFinder in regex mode / RegExp use.
|
|
65
|
+
* Route every anchor through this whenever regex matching is enabled.
|
|
66
|
+
*/
|
|
67
|
+
function escapeRegExp(str) {
|
|
68
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: docupuncture-slides
|
|
3
|
+
description: Precise in-place edits to an existing Google Slides presentation via paste-and-run Apps Script. Use when the user wants edits, tweaks, or iterations to a Google Slides deck (or says "docupuncture" in a slides context) — deliver a self-contained script that patches the live presentation while preserving layout, styles, and structure, instead of regenerating a new deck.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Docupuncture for Slides
|
|
7
|
+
|
|
8
|
+
Docupuncture applied to Google Slides: insert precise edits without changing the theme, format, comments, or history. Layout and speaker notes stay too. Deliver edits as a self-contained Apps Script the user pastes into Extensions → Apps Script and runs.
|
|
9
|
+
|
|
10
|
+
Use the name "docupuncture" (or "docupuncture on these slides") when referring to this workflow.
|
|
11
|
+
|
|
12
|
+
## When to offer
|
|
13
|
+
|
|
14
|
+
Whenever the user asks for edits, tweaks, or iterations to an existing Google Slides presentation — especially repeated rounds — offer:
|
|
15
|
+
|
|
16
|
+
1. **Docupuncture: Apps Script patch** (recommended for content/text changes) — I generate a script; you run it once. Layout and theme preserved.
|
|
17
|
+
2. **New generated deck** — full regeneration (warn: loses theme, precise positioning, speaker notes, etc.).
|
|
18
|
+
3. **Manual edit checklist** — exact text or slide-by-slide changes.
|
|
19
|
+
|
|
20
|
+
Skip the question if the user already said "docupuncture" or established the preference.
|
|
21
|
+
|
|
22
|
+
### Edit volume / risk guidance
|
|
23
|
+
|
|
24
|
+
- **Low risk** (1–6 text replacements or simple content updates on existing shapes): pure docupuncture.
|
|
25
|
+
- **Medium risk** (7–12 changes or light additions of text boxes): still preferred.
|
|
26
|
+
- **High risk** (major layout restructuring, many new slides, complex object insertion/repositioning): recommend sequential smaller scripts or full regeneration. Positioning is fragile.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
1. **Read / understand the presentation first**. Confirm the exact current text content and the requested changes (slide numbers or unique text anchors help).
|
|
31
|
+
2. **Write the script** and deliver it with clear run instructions:
|
|
32
|
+
- Open the presentation → Extensions → Apps Script
|
|
33
|
+
- Paste the script, run `applyEdits`
|
|
34
|
+
- Authorize on first run
|
|
35
|
+
- Check the Execution log
|
|
36
|
+
3. **Verify** after the user runs it if possible.
|
|
37
|
+
|
|
38
|
+
## Script rules
|
|
39
|
+
|
|
40
|
+
- **Container-bound**: use `SlidesApp.getActivePresentation()`. Prefer this over opening by ID.
|
|
41
|
+
- **Anchors**: locate shapes by their text content (`shape.getText().asString()` or `find`). Prefer unique or distinctive phrases that are unlikely to change. For multi-slide decks, iterate slides and search each page’s shapes.
|
|
42
|
+
- **Text edits**: prefer `TextRange` methods. **Warning**: `getText().setText()` on a shape with mixed character styling (e.g. one bolded word, an inline color) flattens it to a single run. For shapes with mixed styling, prefer `replaceAllText` on the shape, or targeted insert/delete on the sub-`TextRange` around only the changed span, so untouched runs keep their styling. Full `setText` is fine only when the shape's text is uniformly styled.
|
|
43
|
+
- **Idempotent**: check whether the target text or state already exists before changing it. Re-running must not duplicate shapes or text.
|
|
44
|
+
- **Fail loudly**: if a text anchor / shape is not found, log `✗ SKIPPED: <edit name> — anchor not found` and continue. Never guess slide indices or object IDs.
|
|
45
|
+
- **Dry run**: include a `DRY_RUN` flag (default `true`). When on, each edit locates its anchor/shape and logs `→ WOULD APPLY` without modifying the deck. User reads the log, flips to `false`, runs again.
|
|
46
|
+
- **Structure**: one `applyEdits()` entry point. Each logical change in its own named function. End with a clear summary log.
|
|
47
|
+
- **Scope note**: v1 focuses on text content and simple shape text updates. Complex layout surgery (moving/resizing many objects) is higher risk and should be flagged.
|
|
48
|
+
- **Never** hardcode credentials.
|
|
49
|
+
|
|
50
|
+
## Caveats to tell the user (once)
|
|
51
|
+
|
|
52
|
+
- First run shows Google’s authorization screen — the script runs as them.
|
|
53
|
+
- Heavy layout changes are more fragile than pure text edits; prefer content-focused needles.
|
|
54
|
+
- Suggest testing on a copy first if the presentation is important.
|
|
55
|
+
|
|
56
|
+
See `references/sample-skeleton.js` for a recommended starting template.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docupuncture for Slides – sample skeleton
|
|
3
|
+
*
|
|
4
|
+
* Paste into the target presentation's Extensions → Apps Script, then run
|
|
5
|
+
* applyEdits. With DRY_RUN = true it only logs what it WOULD change;
|
|
6
|
+
* read the log, set DRY_RUN = false, and run again to apply.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DRY_RUN = true; // ← set to false to actually apply the edits
|
|
10
|
+
|
|
11
|
+
function applyEdits() {
|
|
12
|
+
const presentation = SlidesApp.getActivePresentation();
|
|
13
|
+
const slides = presentation.getSlides();
|
|
14
|
+
|
|
15
|
+
let applied = 0;
|
|
16
|
+
let skipped = 0;
|
|
17
|
+
|
|
18
|
+
if (edit_UpdateTitleOnSlide1(slides)) applied++; else skipped++;
|
|
19
|
+
// if (edit_ReplacePlaceholderAcrossDeck(presentation)) applied++; else skipped++;
|
|
20
|
+
|
|
21
|
+
const mode = DRY_RUN ? 'DRY RUN — no changes made' : 'LIVE';
|
|
22
|
+
Logger.log(`Done (${mode}): ${applied} ${DRY_RUN ? 'would apply' : 'applied'}, ${skipped} skipped`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Example: find a shape containing specific text and update it.
|
|
27
|
+
*
|
|
28
|
+
* Styling note: getText().setText() flattens mixed character styling
|
|
29
|
+
* (e.g. one bolded word) to a single run. It is safe here only because
|
|
30
|
+
* the whole title is being replaced and titles are usually uniformly
|
|
31
|
+
* styled. For partial edits inside a shape with mixed styling, operate
|
|
32
|
+
* on a sub-TextRange around just the changed span instead.
|
|
33
|
+
*/
|
|
34
|
+
function edit_UpdateTitleOnSlide1(slides) {
|
|
35
|
+
const EDIT_NAME = 'UpdateTitleOnSlide1';
|
|
36
|
+
const targetSlide = slides[0]; // or find by content
|
|
37
|
+
|
|
38
|
+
const shapes = targetSlide.getShapes();
|
|
39
|
+
let targetShape = null;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < shapes.length; i++) {
|
|
42
|
+
const text = shapes[i].getText().asString().trim();
|
|
43
|
+
if (text.indexOf('Old Title Text') !== -1) {
|
|
44
|
+
targetShape = shapes[i];
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!targetShape) {
|
|
50
|
+
Logger.log(`✗ SKIPPED: ${EDIT_NAME} — anchor not found`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const current = targetShape.getText().asString().trim();
|
|
55
|
+
if (current === 'New Title Text') {
|
|
56
|
+
Logger.log(`✓ SKIPPED (already present): ${EDIT_NAME}`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (DRY_RUN) {
|
|
61
|
+
Logger.log(`→ WOULD APPLY: ${EDIT_NAME} ('${current}' → 'New Title Text')`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
targetShape.getText().setText('New Title Text');
|
|
66
|
+
Logger.log(`✓ APPLIED: ${EDIT_NAME}`);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Example: replace text across the whole presentation.
|
|
72
|
+
* replaceAllText preserves each match's run styling, so it is the
|
|
73
|
+
* preferred tool for text swaps inside shapes with mixed styling.
|
|
74
|
+
*
|
|
75
|
+
* Note: replaceAllText has no read-only mode, so in a dry run we count
|
|
76
|
+
* matches manually instead of calling it.
|
|
77
|
+
*/
|
|
78
|
+
function edit_ReplacePlaceholderAcrossDeck(presentation) {
|
|
79
|
+
const EDIT_NAME = 'ReplacePlaceholderAcrossDeck';
|
|
80
|
+
|
|
81
|
+
if (DRY_RUN) {
|
|
82
|
+
let matches = 0;
|
|
83
|
+
presentation.getSlides().forEach(function (slide) {
|
|
84
|
+
slide.getShapes().forEach(function (shape) {
|
|
85
|
+
matches += shape.getText().asString().split('{{CLIENT_NAME}}').length - 1;
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
if (matches === 0) {
|
|
89
|
+
Logger.log(`✗ SKIPPED: ${EDIT_NAME} — no matches found`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
Logger.log(`→ WOULD APPLY: ${EDIT_NAME} (${matches} replacements)`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const count = presentation.replaceAllText('{{CLIENT_NAME}}', 'Acme Corp');
|
|
97
|
+
if (count === 0) {
|
|
98
|
+
Logger.log(`✗ SKIPPED: ${EDIT_NAME} — no matches found`);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
Logger.log(`✓ APPLIED: ${EDIT_NAME} (${count} replacements)`);
|
|
102
|
+
return true;
|
|
103
|
+
}
|